1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::{ElementAttribute, element_attribute::ParseShorthand},
6 content::{Content, SubstitutionStep},
7 internal::debug::DebugSliceReference,
8 span::MatchedItem,
9 strings::CowStr,
10 warnings::{MatchAndWarnings, Warning, WarningType},
11};
12
13#[derive(Clone, Eq, PartialEq, Default)]
21pub struct Attrlist<'src> {
22 attributes: Vec<ElementAttribute<'src>>,
23 anchor: Option<CowStr<'src>>,
24 source: Span<'src>,
25}
26
27impl<'src> Attrlist<'src> {
28 pub(crate) fn parse(
33 source: Span<'src>,
34 parser: &Parser,
35 attrlist_context: AttrlistContext,
36 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
37 let mut attributes: Vec<ElementAttribute> = vec![];
38 let mut parse_shorthand_items = true;
39 let mut warnings: Vec<Warning<'src>> = vec![];
40
41 let source_cow = if source.contains('{') && source.contains('}') {
43 let mut content = Content::from(source);
44 SubstitutionStep::AttributeReferences.apply(&mut content, parser, None);
45 CowStr::from(content.rendered.to_string())
46 } else {
47 CowStr::from(source.data())
48 };
49
50 if source_cow.starts_with('[') && source_cow.ends_with(']') {
51 let anchor = source_cow[1..source_cow.len() - 1].to_owned();
52
53 return MatchAndWarnings {
54 item: MatchedItem {
55 item: Self {
56 attributes,
57 anchor: Some(CowStr::from(anchor)),
58 source,
59 },
60 after: source.discard_all(),
61 },
62 warnings,
63 };
64 }
65
66 let mut index = 0;
67
68 let after_index = loop {
69 let (attr, new_index, warning_types) = ElementAttribute::parse(
70 &source_cow,
71 index,
72 parser,
73 ParseShorthand(parse_shorthand_items),
74 attrlist_context,
75 );
76
77 for warning_type in warning_types {
82 warnings.push(Warning {
83 source,
84 warning: warning_type,
85 });
86 }
87
88 if attr.name().is_none() {
89 parse_shorthand_items = false;
90 }
91
92 let mut after = Span::new(source_cow.as_ref()).discard(new_index);
93
94 if attr.name().is_none()
95 && attr.value().is_empty()
96 && after.is_empty()
97 && attributes.is_empty()
98 {
99 break index;
100 }
101
102 if attr.name().is_none() || attr.value() != "None" {
103 attributes.push(attr);
104 }
105
106 after = after.take_whitespace().after;
107
108 match after.take_prefix(",") {
109 Some(comma) => {
110 after = comma.after.take_whitespace().after;
111
112 if after.starts_with(',') {
113 warnings.push(Warning {
114 source,
115 warning: WarningType::EmptyAttributeValue,
116 });
117 after = after.discard(1);
118 index = after.byte_offset();
119 continue;
120 }
121
122 index = after.byte_offset();
123 }
124 None => {
125 break after.byte_offset();
126 }
127 }
128 };
129
130 if after_index < source_cow.len() {
131 warnings.push(Warning {
132 source,
133 warning: WarningType::MissingCommaAfterQuotedAttributeValue,
134 });
135 }
136
137 MatchAndWarnings {
138 item: MatchedItem {
139 item: Self {
140 attributes,
141 anchor: None,
142 source,
143 },
144 after: source.discard_all(),
145 },
146 warnings,
147 }
148 }
149
150 pub fn attributes(&'src self) -> Iter<'src, ElementAttribute<'src>> {
153 self.attributes.iter()
154 }
155
156 pub fn anchor(&'src self) -> Option<&'src str> {
158 self.anchor.as_deref()
159 }
160
161 pub fn named_attribute(&'src self, name: &str) -> Option<&'src ElementAttribute<'src>> {
163 self.attributes.iter().find(|attr| {
164 if let Some(attr_name) = attr.name() {
165 attr_name == name
166 } else {
167 false
168 }
169 })
170 }
171
172 pub fn nth_attribute(&'src self, n: usize) -> Option<&'src ElementAttribute<'src>> {
177 if n == 0 {
178 None
179 } else {
180 self.attributes
181 .iter()
182 .filter(|attr| attr.name().is_none())
183 .nth(n - 1)
184 }
185 }
186
187 pub fn named_or_positional_attribute(
195 &'src self,
196 name: &str,
197 index: usize,
198 ) -> Option<&'src ElementAttribute<'src>> {
199 self.named_attribute(name)
200 .or_else(|| self.nth_attribute(index))
201 }
202
203 pub fn id(&'src self) -> Option<&'src str> {
234 self.anchor().or_else(|| {
235 self.nth_attribute(1)
236 .and_then(|attr1| attr1.id())
237 .or_else(|| self.named_attribute("id").map(|attr| attr.value()))
238 })
239 }
240
241 pub fn roles(&'src self) -> Vec<&'src str> {
263 let mut roles = self
264 .nth_attribute(1)
265 .map(|attr1| attr1.roles())
266 .unwrap_or_default();
267
268 if let Some(role_attr) = self.named_attribute("role") {
269 let mut role_span = Span::new(role_attr.value());
270 let mut formal_roles: Vec<&'src str> = vec![];
271 role_span = role_span.take_while(|c| c == ' ').after;
272
273 while !role_span.is_empty() {
274 let mi = role_span.take_while(|c| c != ' ');
275 if !mi.item.is_empty() {
276 formal_roles.push(mi.item.data());
277 }
278 role_span = mi.after.take_while(|c| c == ' ').after;
279 }
280
281 roles.append(&mut formal_roles);
282 }
283
284 roles
285 }
286
287 pub fn options(&'src self) -> Vec<&'src str> {
351 let mut options = self
352 .nth_attribute(1)
353 .map(|attr1| attr1.options())
354 .unwrap_or_default();
355
356 if let Some(option_attr) = self.named_attribute("opts") {
357 let mut option_span = Span::new(option_attr.value());
358 let mut formal_options: Vec<&'src str> = vec![];
359 option_span = option_span.take_while(|c| c == ',').after;
360
361 while !option_span.is_empty() {
362 let mi = option_span.take_while(|c| c != ',');
363 if !mi.item.is_empty() {
364 formal_options.push(mi.item.data());
365 }
366 option_span = mi.after.take_while(|c| c == ',').after;
367 }
368
369 options.append(&mut formal_options);
370 }
371
372 if let Some(option_attr) = self.named_attribute("options") {
373 let mut option_span = Span::new(option_attr.value());
374 let mut formal_options: Vec<&'_ str> = vec![];
375 option_span = option_span.take_while(|c| c == ',').after;
376
377 while !option_span.is_empty() {
378 let mi = option_span.take_while(|c| c != ',');
379 if !mi.item.is_empty() {
380 formal_options.push(mi.item.data());
381 }
382 option_span = mi.after.take_while(|c| c == ',').after;
383 }
384
385 options.append(&mut formal_options);
386 }
387
388 options
389 }
390
391 pub fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
397 let options = self.options();
399 let name = name.as_ref();
400 options.contains(&name)
401 }
402
403 pub fn block_style(&'src self) -> Option<&'src str> {
405 self.nth_attribute(1).and_then(|a| a.block_style())
406 }
407}
408
409impl<'src> HasSpan<'src> for Attrlist<'src> {
410 fn span(&self) -> Span<'src> {
411 self.source
412 }
413}
414
415impl std::fmt::Debug for Attrlist<'_> {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 f.debug_struct("Attrlist")
418 .field("attributes", &DebugSliceReference(&self.attributes))
419 .field("anchor", &self.anchor)
420 .field("source", &self.source)
421 .finish()
422 }
423}
424
425#[derive(Clone, Copy, Debug, Eq, PartialEq)]
427pub(crate) enum AttrlistContext {
428 Block,
429 Inline,
430}
431
432#[cfg(test)]
433mod tests {
434 #![allow(clippy::unwrap_used)]
435 use pretty_assertions_sorted::assert_eq;
436
437 use crate::{
438 HasSpan, Parser, attributes::AttrlistContext, parser::ModificationContext,
439 tests::prelude::*, warnings::WarningType,
440 };
441
442 #[test]
443 fn impl_clone() {
444 let p = Parser::default();
446 let b1 = crate::attributes::Attrlist::parse(
447 crate::Span::new("abc"),
448 &p,
449 AttrlistContext::Inline,
450 )
451 .unwrap_if_no_warnings();
452
453 let b2 = b1.item.clone();
454 assert_eq!(b1.item, b2);
455 }
456
457 #[test]
458 fn impl_default() {
459 let attrlist = crate::attributes::Attrlist::default();
460
461 assert_eq!(
462 attrlist,
463 Attrlist {
464 attributes: &[],
465 anchor: None,
466 source: Span {
467 data: "",
468 line: 1,
469 col: 1,
470 offset: 0
471 }
472 }
473 );
474
475 assert!(attrlist.named_attribute("foo").is_none());
476
477 assert!(attrlist.nth_attribute(0).is_none());
478 assert!(attrlist.nth_attribute(1).is_none());
479 assert!(attrlist.nth_attribute(42).is_none());
480
481 assert!(attrlist.named_or_positional_attribute("foo", 0).is_none());
482 assert!(attrlist.named_or_positional_attribute("foo", 1).is_none());
483 assert!(attrlist.named_or_positional_attribute("foo", 42).is_none());
484
485 assert!(attrlist.id().is_none());
486 assert!(attrlist.roles().is_empty());
487 assert!(attrlist.block_style().is_none());
488
489 assert_eq!(
490 attrlist.span(),
491 Span {
492 data: "",
493 line: 1,
494 col: 1,
495 offset: 0,
496 }
497 );
498 }
499
500 #[test]
501 fn empty_source() {
502 let p = Parser::default();
503
504 let mi =
505 crate::attributes::Attrlist::parse(crate::Span::default(), &p, AttrlistContext::Inline)
506 .unwrap_if_no_warnings();
507
508 assert_eq!(
509 mi.item,
510 Attrlist {
511 attributes: &[],
512 anchor: None,
513 source: Span {
514 data: "",
515 line: 1,
516 col: 1,
517 offset: 0
518 }
519 }
520 );
521
522 assert!(mi.item.named_attribute("foo").is_none());
523
524 assert!(mi.item.nth_attribute(0).is_none());
525 assert!(mi.item.nth_attribute(1).is_none());
526 assert!(mi.item.nth_attribute(42).is_none());
527
528 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
529 assert!(mi.item.named_or_positional_attribute("foo", 1).is_none());
530 assert!(mi.item.named_or_positional_attribute("foo", 42).is_none());
531
532 assert!(mi.item.id().is_none());
533 assert!(mi.item.roles().is_empty());
534 assert!(mi.item.block_style().is_none());
535
536 assert_eq!(
537 mi.item.span(),
538 Span {
539 data: "",
540 line: 1,
541 col: 1,
542 offset: 0,
543 }
544 );
545
546 assert_eq!(
547 mi.after,
548 Span {
549 data: "",
550 line: 1,
551 col: 1,
552 offset: 0
553 }
554 );
555 }
556
557 #[test]
558 fn empty_positional_attributes() {
559 let p = Parser::default();
560
561 let mi = crate::attributes::Attrlist::parse(
562 crate::Span::new(",300,400"),
563 &p,
564 AttrlistContext::Inline,
565 )
566 .unwrap_if_no_warnings();
567
568 assert_eq!(
569 mi.item,
570 Attrlist {
571 attributes: &[
572 ElementAttribute {
573 name: None,
574 shorthand_items: &[],
575 value: ""
576 },
577 ElementAttribute {
578 name: None,
579 shorthand_items: &[],
580 value: "300"
581 },
582 ElementAttribute {
583 name: None,
584 shorthand_items: &[],
585 value: "400"
586 }
587 ],
588 anchor: None,
589 source: Span {
590 data: ",300,400",
591 line: 1,
592 col: 1,
593 offset: 0
594 }
595 }
596 );
597
598 assert!(mi.item.named_attribute("foo").is_none());
599 assert!(mi.item.nth_attribute(0).is_none());
600 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
601
602 assert!(mi.item.id().is_none());
603 assert!(mi.item.roles().is_empty());
604 assert!(mi.item.block_style().is_none());
605
606 assert_eq!(
607 mi.item.nth_attribute(1).unwrap(),
608 ElementAttribute {
609 name: None,
610 shorthand_items: &[],
611 value: ""
612 }
613 );
614
615 assert_eq!(
616 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
617 ElementAttribute {
618 name: None,
619 shorthand_items: &[],
620 value: ""
621 }
622 );
623
624 assert_eq!(
625 mi.item.nth_attribute(2).unwrap(),
626 ElementAttribute {
627 name: None,
628 shorthand_items: &[],
629 value: "300"
630 }
631 );
632
633 assert_eq!(
634 mi.item.named_or_positional_attribute("width", 2).unwrap(),
635 ElementAttribute {
636 name: None,
637 shorthand_items: &[],
638 value: "300"
639 }
640 );
641
642 assert_eq!(
643 mi.item.nth_attribute(3).unwrap(),
644 ElementAttribute {
645 name: None,
646 shorthand_items: &[],
647 value: "400"
648 }
649 );
650
651 assert_eq!(
652 mi.item.named_or_positional_attribute("height", 3).unwrap(),
653 ElementAttribute {
654 name: None,
655 shorthand_items: &[],
656 value: "400"
657 }
658 );
659
660 assert!(mi.item.nth_attribute(4).is_none());
661 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
662 assert!(mi.item.nth_attribute(42).is_none());
663
664 assert_eq!(
665 mi.item.span(),
666 Span {
667 data: ",300,400",
668 line: 1,
669 col: 1,
670 offset: 0,
671 }
672 );
673
674 assert_eq!(
675 mi.after,
676 Span {
677 data: "",
678 line: 1,
679 col: 9,
680 offset: 8
681 }
682 );
683 }
684
685 #[test]
686 fn only_positional_attributes() {
687 let p = Parser::default();
688
689 let mi = crate::attributes::Attrlist::parse(
690 crate::Span::new("Sunset,300,400"),
691 &p,
692 AttrlistContext::Inline,
693 )
694 .unwrap_if_no_warnings();
695
696 assert_eq!(
697 mi.item,
698 Attrlist {
699 attributes: &[
700 ElementAttribute {
701 name: None,
702 shorthand_items: &["Sunset"],
703 value: "Sunset"
704 },
705 ElementAttribute {
706 name: None,
707 shorthand_items: &[],
708 value: "300"
709 },
710 ElementAttribute {
711 name: None,
712 shorthand_items: &[],
713 value: "400"
714 }
715 ],
716 anchor: None,
717 source: Span {
718 data: "Sunset,300,400",
719 line: 1,
720 col: 1,
721 offset: 0
722 }
723 }
724 );
725
726 assert!(mi.item.named_attribute("foo").is_none());
727 assert!(mi.item.nth_attribute(0).is_none());
728 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
729
730 assert!(mi.item.id().is_none());
731 assert!(mi.item.roles().is_empty());
732 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
733
734 assert_eq!(
735 mi.item.nth_attribute(1).unwrap(),
736 ElementAttribute {
737 name: None,
738 shorthand_items: &["Sunset"],
739 value: "Sunset"
740 }
741 );
742
743 assert_eq!(
744 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
745 ElementAttribute {
746 name: None,
747 shorthand_items: &["Sunset"],
748 value: "Sunset"
749 }
750 );
751
752 assert_eq!(
753 mi.item.nth_attribute(2).unwrap(),
754 ElementAttribute {
755 name: None,
756 shorthand_items: &[],
757 value: "300"
758 }
759 );
760
761 assert_eq!(
762 mi.item.named_or_positional_attribute("width", 2).unwrap(),
763 ElementAttribute {
764 name: None,
765 shorthand_items: &[],
766 value: "300"
767 }
768 );
769
770 assert_eq!(
771 mi.item.nth_attribute(3).unwrap(),
772 ElementAttribute {
773 name: None,
774 shorthand_items: &[],
775 value: "400"
776 }
777 );
778
779 assert_eq!(
780 mi.item.named_or_positional_attribute("height", 3).unwrap(),
781 ElementAttribute {
782 name: None,
783 shorthand_items: &[],
784 value: "400"
785 }
786 );
787
788 assert!(mi.item.nth_attribute(4).is_none());
789 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
790 assert!(mi.item.nth_attribute(42).is_none());
791
792 assert_eq!(
793 mi.item.span(),
794 Span {
795 data: "Sunset,300,400",
796 line: 1,
797 col: 1,
798 offset: 0,
799 }
800 );
801
802 assert_eq!(
803 mi.after,
804 Span {
805 data: "",
806 line: 1,
807 col: 15,
808 offset: 14
809 }
810 );
811 }
812
813 #[test]
814 fn trim_trailing_space() {
815 let p = Parser::default();
816
817 let mi = crate::attributes::Attrlist::parse(
818 crate::Span::new("Sunset ,300 , 400"),
819 &p,
820 AttrlistContext::Inline,
821 )
822 .unwrap_if_no_warnings();
823
824 assert_eq!(
825 mi.item,
826 Attrlist {
827 attributes: &[
828 ElementAttribute {
829 name: None,
830 shorthand_items: &["Sunset"],
831 value: "Sunset"
832 },
833 ElementAttribute {
834 name: None,
835 shorthand_items: &[],
836 value: "300"
837 },
838 ElementAttribute {
839 name: None,
840 shorthand_items: &[],
841 value: "400"
842 }
843 ],
844 anchor: None,
845 source: Span {
846 data: "Sunset ,300 , 400",
847 line: 1,
848 col: 1,
849 offset: 0
850 }
851 }
852 );
853
854 assert!(mi.item.named_attribute("foo").is_none());
855 assert!(mi.item.nth_attribute(0).is_none());
856 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
857
858 assert!(mi.item.id().is_none());
859 assert!(mi.item.roles().is_empty());
860 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
861
862 assert_eq!(
863 mi.item.nth_attribute(1).unwrap(),
864 ElementAttribute {
865 name: None,
866 shorthand_items: &["Sunset"],
867 value: "Sunset"
868 }
869 );
870
871 assert_eq!(
872 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
873 ElementAttribute {
874 name: None,
875 shorthand_items: &["Sunset"],
876 value: "Sunset"
877 }
878 );
879
880 assert_eq!(
881 mi.item.nth_attribute(2).unwrap(),
882 ElementAttribute {
883 name: None,
884 shorthand_items: &[],
885 value: "300"
886 }
887 );
888
889 assert_eq!(
890 mi.item.named_or_positional_attribute("width", 2).unwrap(),
891 ElementAttribute {
892 name: None,
893 shorthand_items: &[],
894 value: "300"
895 }
896 );
897
898 assert_eq!(
899 mi.item.nth_attribute(3).unwrap(),
900 ElementAttribute {
901 name: None,
902 shorthand_items: &[],
903 value: "400"
904 }
905 );
906
907 assert_eq!(
908 mi.item.named_or_positional_attribute("height", 3).unwrap(),
909 ElementAttribute {
910 name: None,
911 shorthand_items: &[],
912 value: "400"
913 }
914 );
915
916 assert!(mi.item.nth_attribute(4).is_none());
917 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
918 assert!(mi.item.nth_attribute(42).is_none());
919
920 assert_eq!(
921 mi.item.span(),
922 Span {
923 data: "Sunset ,300 , 400",
924 line: 1,
925 col: 1,
926 offset: 0,
927 }
928 );
929
930 assert_eq!(
931 mi.after,
932 Span {
933 data: "",
934 line: 1,
935 col: 18,
936 offset: 17
937 }
938 );
939 }
940
941 #[test]
942 fn only_named_attributes() {
943 let p = Parser::default();
944
945 let mi = crate::attributes::Attrlist::parse(
946 crate::Span::new("alt=Sunset,width=300,height=400"),
947 &p,
948 AttrlistContext::Inline,
949 )
950 .unwrap_if_no_warnings();
951
952 assert_eq!(
953 mi.item,
954 Attrlist {
955 attributes: &[
956 ElementAttribute {
957 name: Some("alt"),
958 shorthand_items: &[],
959 value: "Sunset"
960 },
961 ElementAttribute {
962 name: Some("width"),
963 shorthand_items: &[],
964 value: "300"
965 },
966 ElementAttribute {
967 name: Some("height"),
968 shorthand_items: &[],
969 value: "400"
970 }
971 ],
972 anchor: None,
973 source: Span {
974 data: "alt=Sunset,width=300,height=400",
975 line: 1,
976 col: 1,
977 offset: 0
978 }
979 }
980 );
981
982 assert!(mi.item.named_attribute("foo").is_none());
983 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
984
985 assert_eq!(
986 mi.item.named_attribute("alt").unwrap(),
987 ElementAttribute {
988 name: Some("alt"),
989 shorthand_items: &[],
990 value: "Sunset"
991 }
992 );
993
994 assert_eq!(
995 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
996 ElementAttribute {
997 name: Some("alt"),
998 shorthand_items: &[],
999 value: "Sunset"
1000 }
1001 );
1002
1003 assert_eq!(
1004 mi.item.named_attribute("width").unwrap(),
1005 ElementAttribute {
1006 name: Some("width"),
1007 shorthand_items: &[],
1008 value: "300"
1009 }
1010 );
1011
1012 assert_eq!(
1013 mi.item.named_or_positional_attribute("width", 2).unwrap(),
1014 ElementAttribute {
1015 name: Some("width"),
1016 shorthand_items: &[],
1017 value: "300"
1018 }
1019 );
1020
1021 assert_eq!(
1022 mi.item.named_attribute("height").unwrap(),
1023 ElementAttribute {
1024 name: Some("height"),
1025 shorthand_items: &[],
1026 value: "400"
1027 }
1028 );
1029
1030 assert_eq!(
1031 mi.item.named_or_positional_attribute("height", 3).unwrap(),
1032 ElementAttribute {
1033 name: Some("height"),
1034 shorthand_items: &[],
1035 value: "400"
1036 }
1037 );
1038
1039 assert!(mi.item.nth_attribute(0).is_none());
1040 assert!(mi.item.nth_attribute(1).is_none());
1041 assert!(mi.item.nth_attribute(2).is_none());
1042 assert!(mi.item.nth_attribute(3).is_none());
1043 assert!(mi.item.nth_attribute(4).is_none());
1044 assert!(mi.item.nth_attribute(42).is_none());
1045
1046 assert!(mi.item.id().is_none());
1047 assert!(mi.item.roles().is_empty());
1048 assert!(mi.item.block_style().is_none());
1049
1050 assert_eq!(
1051 mi.item.span(),
1052 Span {
1053 data: "alt=Sunset,width=300,height=400",
1054 line: 1,
1055 col: 1,
1056 offset: 0
1057 }
1058 );
1059
1060 assert_eq!(
1061 mi.after,
1062 Span {
1063 data: "",
1064 line: 1,
1065 col: 32,
1066 offset: 31
1067 }
1068 );
1069 }
1070
1071 #[test]
1072 fn ignore_named_attribute_with_none_value() {
1073 let p = Parser::default();
1074 let mi = crate::attributes::Attrlist::parse(
1075 crate::Span::new("alt=Sunset,width=None,height=400"),
1076 &p,
1077 AttrlistContext::Inline,
1078 )
1079 .unwrap_if_no_warnings();
1080
1081 assert_eq!(
1082 mi.item,
1083 Attrlist {
1084 attributes: &[
1085 ElementAttribute {
1086 name: Some("alt"),
1087 shorthand_items: &[],
1088 value: "Sunset"
1089 },
1090 ElementAttribute {
1091 name: Some("height"),
1092 shorthand_items: &[],
1093 value: "400"
1094 }
1095 ],
1096 anchor: None,
1097 source: Span {
1098 data: "alt=Sunset,width=None,height=400",
1099 line: 1,
1100 col: 1,
1101 offset: 0
1102 }
1103 }
1104 );
1105
1106 assert!(mi.item.named_attribute("foo").is_none());
1107 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1108
1109 assert_eq!(
1110 mi.item.named_attribute("alt").unwrap(),
1111 ElementAttribute {
1112 name: Some("alt"),
1113 shorthand_items: &[],
1114 value: "Sunset"
1115 }
1116 );
1117
1118 assert_eq!(
1119 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1120 ElementAttribute {
1121 name: Some("alt"),
1122 shorthand_items: &[],
1123 value: "Sunset"
1124 }
1125 );
1126
1127 assert!(mi.item.named_attribute("width").is_none());
1128 assert!(mi.item.named_or_positional_attribute("width", 2).is_none());
1129
1130 assert_eq!(
1131 mi.item.named_attribute("height").unwrap(),
1132 ElementAttribute {
1133 name: Some("height"),
1134 shorthand_items: &[],
1135 value: "400"
1136 }
1137 );
1138
1139 assert_eq!(
1140 mi.item.named_or_positional_attribute("height", 2).unwrap(),
1141 ElementAttribute {
1142 name: Some("height"),
1143 shorthand_items: &[],
1144 value: "400"
1145 }
1146 );
1147
1148 assert!(mi.item.nth_attribute(0).is_none());
1149 assert!(mi.item.nth_attribute(1).is_none());
1150 assert!(mi.item.nth_attribute(2).is_none());
1151 assert!(mi.item.nth_attribute(3).is_none());
1152 assert!(mi.item.nth_attribute(4).is_none());
1153 assert!(mi.item.nth_attribute(42).is_none());
1154
1155 assert!(mi.item.id().is_none());
1156 assert!(mi.item.roles().is_empty());
1157 assert!(mi.item.block_style().is_none());
1158
1159 assert_eq!(
1160 mi.item.span(),
1161 Span {
1162 data: "alt=Sunset,width=None,height=400",
1163 line: 1,
1164 col: 1,
1165 offset: 0
1166 }
1167 );
1168
1169 assert_eq!(
1170 mi.after,
1171 Span {
1172 data: "",
1173 line: 1,
1174 col: 33,
1175 offset: 32
1176 }
1177 );
1178 }
1179
1180 #[test]
1181 fn err_unparsed_remainder_after_value() {
1182 let p = Parser::default();
1183
1184 let maw = crate::attributes::Attrlist::parse(
1185 crate::Span::new("alt=\"Sunset\"width=300"),
1186 &p,
1187 AttrlistContext::Inline,
1188 );
1189
1190 let mi = maw.item.clone();
1191
1192 assert_eq!(
1193 mi.item,
1194 Attrlist {
1195 attributes: &[ElementAttribute {
1196 name: Some("alt"),
1197 shorthand_items: &[],
1198 value: "Sunset"
1199 }],
1200 anchor: None,
1201 source: Span {
1202 data: "alt=\"Sunset\"width=300",
1203 line: 1,
1204 col: 1,
1205 offset: 0
1206 }
1207 }
1208 );
1209
1210 assert_eq!(
1211 mi.after,
1212 Span {
1213 data: "",
1214 line: 1,
1215 col: 22,
1216 offset: 21
1217 }
1218 );
1219
1220 assert_eq!(
1221 maw.warnings,
1222 vec![Warning {
1223 source: Span {
1224 data: "alt=\"Sunset\"width=300",
1225 line: 1,
1226 col: 1,
1227 offset: 0,
1228 },
1229 warning: WarningType::MissingCommaAfterQuotedAttributeValue,
1230 }]
1231 );
1232 }
1233
1234 #[test]
1235 fn propagates_error_from_element_attribute() {
1236 let p = Parser::default();
1237
1238 let maw = crate::attributes::Attrlist::parse(
1239 crate::Span::new("foo%#id"),
1240 &p,
1241 AttrlistContext::Inline,
1242 );
1243
1244 let mi = maw.item.clone();
1245
1246 assert_eq!(
1247 mi.item,
1248 Attrlist {
1249 attributes: &[ElementAttribute {
1250 name: None,
1251 shorthand_items: &["foo", "#id"],
1252 value: "foo%#id"
1253 }],
1254 anchor: None,
1255 source: Span {
1256 data: "foo%#id",
1257 line: 1,
1258 col: 1,
1259 offset: 0
1260 }
1261 }
1262 );
1263
1264 assert_eq!(
1265 mi.after,
1266 Span {
1267 data: "",
1268 line: 1,
1269 col: 8,
1270 offset: 7
1271 }
1272 );
1273
1274 assert_eq!(
1275 maw.warnings,
1276 vec![Warning {
1277 source: Span {
1278 data: "foo%#id",
1279 line: 1,
1280 col: 1,
1281 offset: 0,
1282 },
1283 warning: WarningType::EmptyShorthandItem,
1284 }]
1285 );
1286 }
1287
1288 #[test]
1289 fn anchor_syntax() {
1290 let p = Parser::default();
1291
1292 let maw = crate::attributes::Attrlist::parse(
1293 crate::Span::new("[notice]"),
1294 &p,
1295 AttrlistContext::Inline,
1296 );
1297
1298 let mi = maw.item.clone();
1299
1300 assert_eq!(
1301 mi.item,
1302 Attrlist {
1303 attributes: &[],
1304 anchor: Some("notice"),
1305 source: Span {
1306 data: "[notice]",
1307 line: 1,
1308 col: 1,
1309 offset: 0
1310 }
1311 }
1312 );
1313
1314 assert_eq!(
1315 mi.after,
1316 Span {
1317 data: "",
1318 line: 1,
1319 col: 9,
1320 offset: 8
1321 }
1322 );
1323
1324 assert!(maw.warnings.is_empty());
1325 }
1326
1327 mod id {
1328 use pretty_assertions_sorted::assert_eq;
1329
1330 use crate::{HasSpan, Parser, attributes::AttrlistContext, tests::prelude::*};
1331
1332 #[test]
1333 fn via_shorthand_syntax() {
1334 let p = Parser::default();
1335
1336 let mi = crate::attributes::Attrlist::parse(
1337 crate::Span::new("#goals"),
1338 &p,
1339 AttrlistContext::Inline,
1340 )
1341 .unwrap_if_no_warnings();
1342
1343 assert_eq!(
1344 mi.item,
1345 Attrlist {
1346 attributes: &[ElementAttribute {
1347 name: None,
1348 shorthand_items: &["#goals"],
1349 value: "#goals"
1350 }],
1351 anchor: None,
1352 source: Span {
1353 data: "#goals",
1354 line: 1,
1355 col: 1,
1356 offset: 0
1357 }
1358 }
1359 );
1360
1361 assert!(mi.item.named_attribute("foo").is_none());
1362 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1363 assert_eq!(mi.item.id().unwrap(), "goals");
1364 assert!(mi.item.roles().is_empty());
1365 assert!(mi.item.block_style().is_none());
1366
1367 assert_eq!(
1368 mi.item.span(),
1369 Span {
1370 data: "#goals",
1371 line: 1,
1372 col: 1,
1373 offset: 0
1374 }
1375 );
1376
1377 assert_eq!(
1378 mi.after,
1379 Span {
1380 data: "",
1381 line: 1,
1382 col: 7,
1383 offset: 6
1384 }
1385 );
1386 }
1387
1388 #[test]
1389 fn via_named_attribute() {
1390 let p = Parser::default();
1391
1392 let mi = crate::attributes::Attrlist::parse(
1393 crate::Span::new("foo=bar,id=goals"),
1394 &p,
1395 AttrlistContext::Inline,
1396 )
1397 .unwrap_if_no_warnings();
1398
1399 assert_eq!(
1400 mi.item,
1401 Attrlist {
1402 attributes: &[
1403 ElementAttribute {
1404 name: Some("foo"),
1405 shorthand_items: &[],
1406 value: "bar"
1407 },
1408 ElementAttribute {
1409 name: Some("id"),
1410 shorthand_items: &[],
1411 value: "goals"
1412 },
1413 ],
1414 anchor: None,
1415 source: Span {
1416 data: "foo=bar,id=goals",
1417 line: 1,
1418 col: 1,
1419 offset: 0
1420 }
1421 }
1422 );
1423
1424 assert_eq!(
1425 mi.item.named_attribute("foo").unwrap(),
1426 ElementAttribute {
1427 name: Some("foo"),
1428 shorthand_items: &[],
1429 value: "bar"
1430 }
1431 );
1432
1433 assert_eq!(
1434 mi.item.named_attribute("id").unwrap(),
1435 ElementAttribute {
1436 name: Some("id"),
1437 shorthand_items: &[],
1438 value: "goals"
1439 }
1440 );
1441
1442 assert_eq!(mi.item.id().unwrap(), "goals");
1443 assert!(mi.item.roles().is_empty());
1444 assert!(mi.item.block_style().is_none());
1445
1446 assert_eq!(
1447 mi.after,
1448 Span {
1449 data: "",
1450 line: 1,
1451 col: 17,
1452 offset: 16
1453 }
1454 );
1455 }
1456
1457 #[test]
1458 fn via_block_anchor_syntax() {
1459 let p = Parser::default();
1460
1461 let mi = crate::attributes::Attrlist::parse(
1462 crate::Span::new("[goals]"),
1463 &p,
1464 AttrlistContext::Inline,
1465 )
1466 .unwrap_if_no_warnings();
1467
1468 assert_eq!(
1469 mi.item,
1470 Attrlist {
1471 attributes: &[],
1472 anchor: Some("goals"),
1473 source: Span {
1474 data: "[goals]",
1475 line: 1,
1476 col: 1,
1477 offset: 0
1478 }
1479 }
1480 );
1481
1482 assert_eq!(mi.item.id().unwrap(), "goals");
1483
1484 assert_eq!(
1485 mi.after,
1486 Span {
1487 data: "",
1488 line: 1,
1489 col: 8,
1490 offset: 7
1491 }
1492 );
1493 }
1494
1495 #[test]
1496 fn shorthand_only_first_attribute() {
1497 let p = Parser::default();
1498
1499 let mi = crate::attributes::Attrlist::parse(
1500 crate::Span::new("foo,blah#goals"),
1501 &p,
1502 AttrlistContext::Inline,
1503 )
1504 .unwrap_if_no_warnings();
1505
1506 assert_eq!(
1507 mi.item,
1508 Attrlist {
1509 attributes: &[
1510 ElementAttribute {
1511 name: None,
1512 shorthand_items: &["foo"],
1513 value: "foo"
1514 },
1515 ElementAttribute {
1516 name: None,
1517 shorthand_items: &[],
1518 value: "blah#goals"
1519 },
1520 ],
1521 anchor: None,
1522 source: Span {
1523 data: "foo,blah#goals",
1524 line: 1,
1525 col: 1,
1526 offset: 0
1527 }
1528 }
1529 );
1530
1531 assert!(mi.item.id().is_none());
1532 assert!(mi.item.roles().is_empty());
1533 assert_eq!(mi.item.block_style().unwrap(), "foo");
1534
1535 assert_eq!(
1536 mi.after,
1537 Span {
1538 data: "",
1539 line: 1,
1540 col: 15,
1541 offset: 14
1542 }
1543 );
1544 }
1545 }
1546
1547 mod roles {
1548 use pretty_assertions_sorted::assert_eq;
1549
1550 use crate::{HasSpan, Parser, attributes::AttrlistContext, tests::prelude::*};
1551
1552 #[test]
1553 fn via_shorthand_syntax() {
1554 let p = Parser::default();
1555
1556 let mi = crate::attributes::Attrlist::parse(
1557 crate::Span::new(".rolename"),
1558 &p,
1559 AttrlistContext::Inline,
1560 )
1561 .unwrap_if_no_warnings();
1562
1563 assert_eq!(
1564 mi.item,
1565 Attrlist {
1566 attributes: &[ElementAttribute {
1567 name: None,
1568 shorthand_items: &[".rolename"],
1569 value: ".rolename"
1570 }],
1571 anchor: None,
1572 source: Span {
1573 data: ".rolename",
1574 line: 1,
1575 col: 1,
1576 offset: 0
1577 }
1578 }
1579 );
1580
1581 assert!(mi.item.named_attribute("foo").is_none());
1582 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1583
1584 let roles = mi.item.roles();
1585 let mut roles = roles.iter();
1586 assert_eq!(roles.next().unwrap(), &"rolename");
1587 assert!(roles.next().is_none());
1588
1589 assert!(mi.item.block_style().is_none());
1590
1591 assert_eq!(
1592 mi.item.span(),
1593 Span {
1594 data: ".rolename",
1595 line: 1,
1596 col: 1,
1597 offset: 0
1598 }
1599 );
1600
1601 assert_eq!(
1602 mi.after,
1603 Span {
1604 data: "",
1605 line: 1,
1606 col: 10,
1607 offset: 9
1608 }
1609 );
1610 }
1611
1612 #[test]
1613 fn via_shorthand_syntax_trim_trailing_whitespace() {
1614 let p = Parser::default();
1615
1616 let mi = crate::attributes::Attrlist::parse(
1617 crate::Span::new(".rolename "),
1618 &p,
1619 AttrlistContext::Inline,
1620 )
1621 .unwrap_if_no_warnings();
1622
1623 assert_eq!(
1624 mi.item,
1625 Attrlist {
1626 attributes: &[ElementAttribute {
1627 name: None,
1628 shorthand_items: &[".rolename"],
1629 value: ".rolename"
1630 }],
1631 anchor: None,
1632 source: Span {
1633 data: ".rolename ",
1634 line: 1,
1635 col: 1,
1636 offset: 0
1637 }
1638 }
1639 );
1640
1641 assert!(mi.item.named_attribute("foo").is_none());
1642 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1643
1644 let roles = mi.item.roles();
1645 let mut roles = roles.iter();
1646
1647 assert_eq!(roles.next().unwrap(), &"rolename");
1648 assert!(roles.next().is_none());
1649
1650 assert!(mi.item.block_style().is_none());
1651
1652 assert_eq!(
1653 mi.item.span(),
1654 Span {
1655 data: ".rolename ",
1656 line: 1,
1657 col: 1,
1658 offset: 0
1659 }
1660 );
1661
1662 assert_eq!(
1663 mi.after,
1664 Span {
1665 data: "",
1666 line: 1,
1667 col: 11,
1668 offset: 10
1669 }
1670 );
1671 }
1672
1673 #[test]
1674 fn multiple_roles_via_shorthand_syntax() {
1675 let p = Parser::default();
1676
1677 let mi = crate::attributes::Attrlist::parse(
1678 crate::Span::new(".role1.role2.role3"),
1679 &p,
1680 AttrlistContext::Inline,
1681 )
1682 .unwrap_if_no_warnings();
1683
1684 assert_eq!(
1685 mi.item,
1686 Attrlist {
1687 attributes: &[ElementAttribute {
1688 name: None,
1689 shorthand_items: &[".role1", ".role2", ".role3"],
1690 value: ".role1.role2.role3"
1691 }],
1692 anchor: None,
1693 source: Span {
1694 data: ".role1.role2.role3",
1695 line: 1,
1696 col: 1,
1697 offset: 0
1698 }
1699 }
1700 );
1701
1702 assert!(mi.item.named_attribute("foo").is_none());
1703 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1704
1705 let roles = mi.item.roles();
1706 let mut roles = roles.iter();
1707 assert_eq!(roles.next().unwrap(), &"role1");
1708 assert_eq!(roles.next().unwrap(), &"role2");
1709 assert_eq!(roles.next().unwrap(), &"role3");
1710 assert!(roles.next().is_none());
1711
1712 assert!(mi.item.block_style().is_none());
1713
1714 assert_eq!(
1715 mi.item.span(),
1716 Span {
1717 data: ".role1.role2.role3",
1718 line: 1,
1719 col: 1,
1720 offset: 0
1721 }
1722 );
1723
1724 assert_eq!(
1725 mi.after,
1726 Span {
1727 data: "",
1728 line: 1,
1729 col: 19,
1730 offset: 18
1731 }
1732 );
1733 }
1734
1735 #[test]
1736 fn multiple_roles_via_shorthand_syntax_trim_whitespace() {
1737 let p = Parser::default();
1738
1739 let mi = crate::attributes::Attrlist::parse(
1740 crate::Span::new(".role1 .role2 .role3 "),
1741 &p,
1742 AttrlistContext::Inline,
1743 )
1744 .unwrap_if_no_warnings();
1745
1746 assert_eq!(
1747 mi.item,
1748 Attrlist {
1749 attributes: &[ElementAttribute {
1750 name: None,
1751 shorthand_items: &[".role1", ".role2", ".role3"],
1752 value: ".role1 .role2 .role3"
1753 }],
1754 anchor: None,
1755 source: Span {
1756 data: ".role1 .role2 .role3 ",
1757 line: 1,
1758 col: 1,
1759 offset: 0
1760 }
1761 }
1762 );
1763
1764 assert!(mi.item.named_attribute("foo").is_none());
1765 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1766
1767 let roles = mi.item.roles();
1768 let mut roles = roles.iter();
1769 assert_eq!(roles.next().unwrap(), &"role1");
1770 assert_eq!(roles.next().unwrap(), &"role2");
1771 assert_eq!(roles.next().unwrap(), &"role3");
1772 assert!(roles.next().is_none());
1773
1774 assert!(mi.item.block_style().is_none());
1775
1776 assert_eq!(
1777 mi.item.span(),
1778 Span {
1779 data: ".role1 .role2 .role3 ",
1780 line: 1,
1781 col: 1,
1782 offset: 0
1783 }
1784 );
1785
1786 assert_eq!(
1787 mi.after,
1788 Span {
1789 data: "",
1790 line: 1,
1791 col: 22,
1792 offset: 21
1793 }
1794 );
1795 }
1796
1797 #[test]
1798 fn via_named_attribute() {
1799 let p = Parser::default();
1800
1801 let mi = crate::attributes::Attrlist::parse(
1802 crate::Span::new("foo=bar,role=role1"),
1803 &p,
1804 AttrlistContext::Inline,
1805 )
1806 .unwrap_if_no_warnings();
1807
1808 assert_eq!(
1809 mi.item,
1810 Attrlist {
1811 attributes: &[
1812 ElementAttribute {
1813 name: Some("foo"),
1814 shorthand_items: &[],
1815 value: "bar"
1816 },
1817 ElementAttribute {
1818 name: Some("role"),
1819 shorthand_items: &[],
1820 value: "role1"
1821 },
1822 ],
1823 anchor: None,
1824 source: Span {
1825 data: "foo=bar,role=role1",
1826 line: 1,
1827 col: 1,
1828 offset: 0
1829 }
1830 }
1831 );
1832
1833 assert_eq!(
1834 mi.item.named_attribute("foo").unwrap(),
1835 ElementAttribute {
1836 name: Some("foo"),
1837 shorthand_items: &[],
1838 value: "bar"
1839 }
1840 );
1841
1842 assert_eq!(
1843 mi.item.named_attribute("role").unwrap(),
1844 ElementAttribute {
1845 name: Some("role"),
1846 shorthand_items: &[],
1847 value: "role1"
1848 }
1849 );
1850
1851 let roles = mi.item.roles();
1852 let mut roles = roles.iter();
1853 assert_eq!(roles.next().unwrap(), &"role1");
1854 assert!(roles.next().is_none());
1855
1856 assert!(mi.item.block_style().is_none());
1857
1858 assert_eq!(
1859 mi.after,
1860 Span {
1861 data: "",
1862 line: 1,
1863 col: 19,
1864 offset: 18
1865 }
1866 );
1867 }
1868
1869 #[test]
1870 fn multiple_roles_via_named_attribute() {
1871 let p = Parser::default();
1872
1873 let mi = crate::attributes::Attrlist::parse(
1874 crate::Span::new("foo=bar,role=role1 role2 role3 "),
1875 &p,
1876 AttrlistContext::Inline,
1877 )
1878 .unwrap_if_no_warnings();
1879
1880 assert_eq!(
1881 mi.item,
1882 Attrlist {
1883 attributes: &[
1884 ElementAttribute {
1885 name: Some("foo"),
1886 shorthand_items: &[],
1887 value: "bar"
1888 },
1889 ElementAttribute {
1890 name: Some("role"),
1891 shorthand_items: &[],
1892 value: "role1 role2 role3"
1893 },
1894 ],
1895 anchor: None,
1896 source: Span {
1897 data: "foo=bar,role=role1 role2 role3 ",
1898 line: 1,
1899 col: 1,
1900 offset: 0
1901 }
1902 }
1903 );
1904
1905 assert_eq!(
1906 mi.item.named_attribute("foo").unwrap(),
1907 ElementAttribute {
1908 name: Some("foo"),
1909 shorthand_items: &[],
1910 value: "bar"
1911 }
1912 );
1913
1914 assert_eq!(
1915 mi.item.named_attribute("role").unwrap(),
1916 ElementAttribute {
1917 name: Some("role"),
1918 shorthand_items: &[],
1919 value: "role1 role2 role3"
1920 }
1921 );
1922
1923 let roles = mi.item.roles();
1924 let mut roles = roles.iter();
1925 assert_eq!(roles.next().unwrap(), &"role1");
1926 assert_eq!(roles.next().unwrap(), &"role2");
1927 assert_eq!(roles.next().unwrap(), &"role3");
1928 assert!(roles.next().is_none());
1929
1930 assert!(mi.item.block_style().is_none());
1931
1932 assert_eq!(
1933 mi.after,
1934 Span {
1935 data: "",
1936 line: 1,
1937 col: 34,
1938 offset: 33
1939 }
1940 );
1941 }
1942
1943 #[test]
1944 fn shorthand_role_and_named_attribute_role() {
1945 let p = Parser::default();
1946
1947 let mi = crate::attributes::Attrlist::parse(
1948 crate::Span::new("#foo.sh1.sh2,role=na1 na2 na3 "),
1949 &p,
1950 AttrlistContext::Inline,
1951 )
1952 .unwrap_if_no_warnings();
1953
1954 assert_eq!(
1955 mi.item,
1956 Attrlist {
1957 attributes: &[
1958 ElementAttribute {
1959 name: None,
1960 shorthand_items: &["#foo", ".sh1", ".sh2"],
1961 value: "#foo.sh1.sh2"
1962 },
1963 ElementAttribute {
1964 name: Some("role"),
1965 shorthand_items: &[],
1966 value: "na1 na2 na3"
1967 },
1968 ],
1969 anchor: None,
1970 source: Span {
1971 data: "#foo.sh1.sh2,role=na1 na2 na3 ",
1972 line: 1,
1973 col: 1,
1974 offset: 0
1975 }
1976 }
1977 );
1978
1979 assert!(mi.item.named_attribute("foo").is_none());
1980
1981 assert_eq!(
1982 mi.item.named_attribute("role").unwrap(),
1983 ElementAttribute {
1984 name: Some("role"),
1985 shorthand_items: &[],
1986 value: "na1 na2 na3"
1987 }
1988 );
1989
1990 let roles = mi.item.roles();
1991 let mut roles = roles.iter();
1992 assert_eq!(roles.next().unwrap(), &"sh1");
1993 assert_eq!(roles.next().unwrap(), &"sh2");
1994 assert_eq!(roles.next().unwrap(), &"na1");
1995 assert_eq!(roles.next().unwrap(), &"na2");
1996 assert_eq!(roles.next().unwrap(), &"na3");
1997 assert!(roles.next().is_none());
1998
1999 assert!(mi.item.block_style().is_none());
2000
2001 assert_eq!(
2002 mi.after,
2003 Span {
2004 data: "",
2005 line: 1,
2006 col: 33,
2007 offset: 32
2008 }
2009 );
2010 }
2011
2012 #[test]
2013 fn shorthand_only_first_attribute() {
2014 let p = Parser::default();
2015
2016 let mi = crate::attributes::Attrlist::parse(
2017 crate::Span::new("foo,blah.rolename"),
2018 &p,
2019 AttrlistContext::Inline,
2020 )
2021 .unwrap_if_no_warnings();
2022
2023 assert_eq!(
2024 mi.item,
2025 Attrlist {
2026 attributes: &[
2027 ElementAttribute {
2028 name: None,
2029 shorthand_items: &["foo"],
2030 value: "foo"
2031 },
2032 ElementAttribute {
2033 name: None,
2034 shorthand_items: &[],
2035 value: "blah.rolename"
2036 },
2037 ],
2038 anchor: None,
2039 source: Span {
2040 data: "foo,blah.rolename",
2041 line: 1,
2042 col: 1,
2043 offset: 0
2044 }
2045 }
2046 );
2047
2048 let roles = mi.item.roles();
2049 assert_eq!(roles.iter().len(), 0);
2050
2051 assert_eq!(mi.item.block_style().unwrap(), "foo");
2052
2053 assert_eq!(
2054 mi.after,
2055 Span {
2056 data: "",
2057 line: 1,
2058 col: 18,
2059 offset: 17
2060 }
2061 );
2062 }
2063 }
2064
2065 mod options {
2066 use pretty_assertions_sorted::assert_eq;
2067
2068 use crate::{HasSpan, Parser, attributes::AttrlistContext, tests::prelude::*};
2069
2070 #[test]
2071 fn via_shorthand_syntax() {
2072 let p = Parser::default();
2073
2074 let mi = crate::attributes::Attrlist::parse(
2075 crate::Span::new("%option"),
2076 &p,
2077 AttrlistContext::Inline,
2078 )
2079 .unwrap_if_no_warnings();
2080
2081 assert_eq!(
2082 mi.item,
2083 Attrlist {
2084 attributes: &[ElementAttribute {
2085 name: None,
2086 shorthand_items: &["%option"],
2087 value: "%option"
2088 }],
2089 anchor: None,
2090 source: Span {
2091 data: "%option",
2092 line: 1,
2093 col: 1,
2094 offset: 0
2095 }
2096 }
2097 );
2098
2099 assert!(mi.item.named_attribute("foo").is_none());
2100 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2101
2102 let options = mi.item.options();
2103 let mut options = options.iter();
2104 assert_eq!(options.next().unwrap(), &"option",);
2105 assert!(options.next().is_none());
2106
2107 assert!(mi.item.has_option("option"));
2108 assert!(!mi.item.has_option("option1"));
2109
2110 assert_eq!(
2111 mi.item.span(),
2112 Span {
2113 data: "%option",
2114 line: 1,
2115 col: 1,
2116 offset: 0
2117 }
2118 );
2119
2120 assert_eq!(
2121 mi.after,
2122 Span {
2123 data: "",
2124 line: 1,
2125 col: 8,
2126 offset: 7
2127 }
2128 );
2129 }
2130
2131 #[test]
2132 fn multiple_options_via_shorthand_syntax() {
2133 let p = Parser::default();
2134
2135 let mi = crate::attributes::Attrlist::parse(
2136 crate::Span::new("%option1%option2%option3"),
2137 &p,
2138 AttrlistContext::Inline,
2139 )
2140 .unwrap_if_no_warnings();
2141
2142 assert_eq!(
2143 mi.item,
2144 Attrlist {
2145 attributes: &[ElementAttribute {
2146 name: None,
2147 shorthand_items: &["%option1", "%option2", "%option3",],
2148 value: "%option1%option2%option3"
2149 }],
2150 anchor: None,
2151 source: Span {
2152 data: "%option1%option2%option3",
2153 line: 1,
2154 col: 1,
2155 offset: 0
2156 }
2157 }
2158 );
2159
2160 assert!(mi.item.named_attribute("foo").is_none());
2161 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2162
2163 let options = mi.item.options();
2164 let mut options = options.iter();
2165 assert_eq!(options.next().unwrap(), &"option1");
2166 assert_eq!(options.next().unwrap(), &"option2");
2167 assert_eq!(options.next().unwrap(), &"option3");
2168 assert!(options.next().is_none());
2169
2170 assert!(mi.item.has_option("option1"));
2171 assert!(mi.item.has_option("option2"));
2172 assert!(mi.item.has_option("option3"));
2173 assert!(!mi.item.has_option("option4"));
2174
2175 assert_eq!(
2176 mi.item.span(),
2177 Span {
2178 data: "%option1%option2%option3",
2179 line: 1,
2180 col: 1,
2181 offset: 0
2182 }
2183 );
2184
2185 assert_eq!(
2186 mi.after,
2187 Span {
2188 data: "",
2189 line: 1,
2190 col: 25,
2191 offset: 24
2192 }
2193 );
2194 }
2195
2196 #[test]
2197 fn via_options_attribute() {
2198 let p = Parser::default();
2199
2200 let mi = crate::attributes::Attrlist::parse(
2201 crate::Span::new("foo=bar,options=option1"),
2202 &p,
2203 AttrlistContext::Inline,
2204 )
2205 .unwrap_if_no_warnings();
2206
2207 assert_eq!(
2208 mi.item,
2209 Attrlist {
2210 attributes: &[
2211 ElementAttribute {
2212 name: Some("foo"),
2213 shorthand_items: &[],
2214 value: "bar"
2215 },
2216 ElementAttribute {
2217 name: Some("options"),
2218 shorthand_items: &[],
2219 value: "option1"
2220 },
2221 ],
2222 anchor: None,
2223 source: Span {
2224 data: "foo=bar,options=option1",
2225 line: 1,
2226 col: 1,
2227 offset: 0
2228 }
2229 }
2230 );
2231
2232 assert_eq!(
2233 mi.item.named_attribute("foo").unwrap(),
2234 ElementAttribute {
2235 name: Some("foo"),
2236 shorthand_items: &[],
2237 value: "bar"
2238 }
2239 );
2240
2241 assert_eq!(
2242 mi.item.named_attribute("options").unwrap(),
2243 ElementAttribute {
2244 name: Some("options"),
2245 shorthand_items: &[],
2246 value: "option1"
2247 }
2248 );
2249
2250 let options = mi.item.options();
2251 let mut options = options.iter();
2252 assert_eq!(options.next().unwrap(), &"option1");
2253 assert!(options.next().is_none());
2254
2255 assert!(mi.item.has_option("option1"));
2256 assert!(!mi.item.has_option("option2"));
2257
2258 assert_eq!(
2259 mi.after,
2260 Span {
2261 data: "",
2262 line: 1,
2263 col: 24,
2264 offset: 23
2265 }
2266 );
2267 }
2268
2269 #[test]
2270 fn via_opts_attribute() {
2271 let p = Parser::default();
2272
2273 let mi = crate::attributes::Attrlist::parse(
2274 crate::Span::new("foo=bar,opts=option1"),
2275 &p,
2276 AttrlistContext::Inline,
2277 )
2278 .unwrap_if_no_warnings();
2279
2280 assert_eq!(
2281 mi.item,
2282 Attrlist {
2283 attributes: &[
2284 ElementAttribute {
2285 name: Some("foo"),
2286 shorthand_items: &[],
2287 value: "bar"
2288 },
2289 ElementAttribute {
2290 name: Some("opts"),
2291 shorthand_items: &[],
2292 value: "option1"
2293 },
2294 ],
2295 anchor: None,
2296 source: Span {
2297 data: "foo=bar,opts=option1",
2298 line: 1,
2299 col: 1,
2300 offset: 0
2301 }
2302 }
2303 );
2304
2305 assert_eq!(
2306 mi.item.named_attribute("foo").unwrap(),
2307 ElementAttribute {
2308 name: Some("foo"),
2309 shorthand_items: &[],
2310 value: "bar"
2311 }
2312 );
2313
2314 assert_eq!(
2315 mi.item.named_attribute("opts").unwrap(),
2316 ElementAttribute {
2317 name: Some("opts"),
2318 shorthand_items: &[],
2319 value: "option1"
2320 }
2321 );
2322
2323 let options = mi.item.options();
2324 let mut options = options.iter();
2325 assert_eq!(options.next().unwrap(), &"option1");
2326 assert!(options.next().is_none());
2327
2328 assert!(!mi.item.has_option("option"));
2329 assert!(mi.item.has_option("option1"));
2330 assert!(!mi.item.has_option("option2"));
2331
2332 assert_eq!(
2333 mi.after,
2334 Span {
2335 data: "",
2336 line: 1,
2337 col: 21,
2338 offset: 20
2339 }
2340 );
2341 }
2342
2343 #[test]
2344 fn multiple_options_via_named_attribute() {
2345 let p = Parser::default();
2346
2347 let mi = crate::attributes::Attrlist::parse(
2348 crate::Span::new("foo=bar,options=\"option1,option2,option3\""),
2349 &p,
2350 AttrlistContext::Inline,
2351 )
2352 .unwrap_if_no_warnings();
2353
2354 assert_eq!(
2355 mi.item,
2356 Attrlist {
2357 attributes: &[
2358 ElementAttribute {
2359 name: Some("foo"),
2360 shorthand_items: &[],
2361 value: "bar"
2362 },
2363 ElementAttribute {
2364 name: Some("options"),
2365 shorthand_items: &[],
2366 value: "option1,option2,option3"
2367 },
2368 ],
2369 anchor: None,
2370 source: Span {
2371 data: "foo=bar,options=\"option1,option2,option3\"",
2372 line: 1,
2373 col: 1,
2374 offset: 0
2375 }
2376 }
2377 );
2378
2379 assert_eq!(
2380 mi.item.named_attribute("foo").unwrap(),
2381 ElementAttribute {
2382 name: Some("foo"),
2383 shorthand_items: &[],
2384 value: "bar"
2385 }
2386 );
2387
2388 assert_eq!(
2389 mi.item.named_attribute("options").unwrap(),
2390 ElementAttribute {
2391 name: Some("options"),
2392 shorthand_items: &[],
2393 value: "option1,option2,option3"
2394 }
2395 );
2396
2397 let options = mi.item.options();
2398 let mut options = options.iter();
2399 assert_eq!(options.next().unwrap(), &"option1");
2400 assert_eq!(options.next().unwrap(), &"option2");
2401 assert_eq!(options.next().unwrap(), &"option3");
2402 assert!(options.next().is_none());
2403
2404 assert!(mi.item.has_option("option1"));
2405 assert!(mi.item.has_option("option2"));
2406 assert!(mi.item.has_option("option3"));
2407 assert!(!mi.item.has_option("option4"));
2408
2409 assert_eq!(
2410 mi.after,
2411 Span {
2412 data: "",
2413 line: 1,
2414 col: 42,
2415 offset: 41
2416 }
2417 );
2418 }
2419
2420 #[test]
2421 fn shorthand_option_and_named_attribute_option() {
2422 let p = Parser::default();
2423
2424 let mi = crate::attributes::Attrlist::parse(
2425 crate::Span::new("#foo%sh1%sh2,options=\"na1,na2,na3\""),
2426 &p,
2427 AttrlistContext::Inline,
2428 )
2429 .unwrap_if_no_warnings();
2430
2431 assert_eq!(
2432 mi.item,
2433 Attrlist {
2434 attributes: &[
2435 ElementAttribute {
2436 name: None,
2437 shorthand_items: &["#foo", "%sh1", "%sh2"],
2438 value: "#foo%sh1%sh2"
2439 },
2440 ElementAttribute {
2441 name: Some("options"),
2442 shorthand_items: &[],
2443 value: "na1,na2,na3"
2444 },
2445 ],
2446 anchor: None,
2447 source: Span {
2448 data: "#foo%sh1%sh2,options=\"na1,na2,na3\"",
2449 line: 1,
2450 col: 1,
2451 offset: 0
2452 }
2453 }
2454 );
2455
2456 assert!(mi.item.named_attribute("foo").is_none(),);
2457
2458 assert_eq!(
2459 mi.item.named_attribute("options").unwrap(),
2460 ElementAttribute {
2461 name: Some("options"),
2462 shorthand_items: &[],
2463 value: "na1,na2,na3"
2464 }
2465 );
2466
2467 let options = mi.item.options();
2468 let mut options = options.iter();
2469 assert_eq!(options.next().unwrap(), &"sh1");
2470 assert_eq!(options.next().unwrap(), &"sh2");
2471 assert_eq!(options.next().unwrap(), &"na1");
2472 assert_eq!(options.next().unwrap(), &"na2");
2473 assert_eq!(options.next().unwrap(), &"na3");
2474 assert!(options.next().is_none(),);
2475
2476 assert!(mi.item.has_option("sh1"));
2477 assert!(mi.item.has_option("sh2"));
2478 assert!(!mi.item.has_option("sh3"));
2479 assert!(mi.item.has_option("na1"));
2480 assert!(mi.item.has_option("na2"));
2481 assert!(mi.item.has_option("na3"));
2482 assert!(!mi.item.has_option("na4"));
2483
2484 assert_eq!(
2485 mi.after,
2486 Span {
2487 data: "",
2488 line: 1,
2489 col: 35,
2490 offset: 34
2491 }
2492 );
2493 }
2494
2495 #[test]
2496 fn shorthand_only_first_attribute() {
2497 let p = Parser::default();
2498
2499 let mi = crate::attributes::Attrlist::parse(
2500 crate::Span::new("foo,blah%option"),
2501 &p,
2502 AttrlistContext::Inline,
2503 )
2504 .unwrap_if_no_warnings();
2505
2506 assert_eq!(
2507 mi.item,
2508 Attrlist {
2509 attributes: &[
2510 ElementAttribute {
2511 name: None,
2512 shorthand_items: &["foo"],
2513 value: "foo"
2514 },
2515 ElementAttribute {
2516 name: None,
2517 shorthand_items: &[],
2518 value: "blah%option"
2519 },
2520 ],
2521 anchor: None,
2522 source: Span {
2523 data: "foo,blah%option",
2524 line: 1,
2525 col: 1,
2526 offset: 0
2527 }
2528 }
2529 );
2530
2531 let options = mi.item.options();
2532 assert_eq!(options.iter().len(), 0);
2533
2534 assert!(!mi.item.has_option("option"));
2535
2536 assert_eq!(
2537 mi.after,
2538 Span {
2539 data: "",
2540 line: 1,
2541 col: 16,
2542 offset: 15
2543 }
2544 );
2545 }
2546 }
2547
2548 #[test]
2549 fn block_style() {
2550 let p = Parser::default();
2551
2552 let mi = crate::attributes::Attrlist::parse(
2553 crate::Span::new("blah#goals"),
2554 &p,
2555 AttrlistContext::Inline,
2556 )
2557 .unwrap_if_no_warnings();
2558
2559 let attrlist = mi.item;
2560 assert_eq!(attrlist.block_style().unwrap(), "blah");
2561 }
2562
2563 #[test]
2564 fn err_double_comma() {
2565 let p = Parser::default();
2566
2567 let maw = crate::attributes::Attrlist::parse(
2568 crate::Span::new("alt=Sunset,width=300,,height=400"),
2569 &p,
2570 AttrlistContext::Inline,
2571 );
2572
2573 let mi = maw.item.clone();
2574
2575 assert_eq!(
2576 mi.item,
2577 Attrlist {
2578 attributes: &[
2579 ElementAttribute {
2580 name: Some("alt"),
2581 shorthand_items: &[],
2582 value: "Sunset"
2583 },
2584 ElementAttribute {
2585 name: Some("width"),
2586 shorthand_items: &[],
2587 value: "300"
2588 },
2589 ElementAttribute {
2590 name: Some("height"),
2591 shorthand_items: &[],
2592 value: "400"
2593 },
2594 ],
2595 anchor: None,
2596 source: Span {
2597 data: "alt=Sunset,width=300,,height=400",
2598 line: 1,
2599 col: 1,
2600 offset: 0,
2601 }
2602 }
2603 );
2604
2605 assert_eq!(
2606 mi.after,
2607 Span {
2608 data: "",
2609 line: 1,
2610 col: 33,
2611 offset: 32,
2612 }
2613 );
2614
2615 assert_eq!(
2616 maw.warnings,
2617 vec![Warning {
2618 source: Span {
2619 data: "alt=Sunset,width=300,,height=400",
2620 line: 1,
2621 col: 1,
2622 offset: 0,
2623 },
2624 warning: WarningType::EmptyAttributeValue,
2625 }]
2626 );
2627 }
2628
2629 #[test]
2630 fn applies_attribute_substitution_before_parsing() {
2631 let p = Parser::default().with_intrinsic_attribute(
2632 "sunset_dimensions",
2633 "300,400",
2634 ModificationContext::Anywhere,
2635 );
2636
2637 let mi = crate::attributes::Attrlist::parse(
2638 crate::Span::new("Sunset,{sunset_dimensions}"),
2639 &p,
2640 AttrlistContext::Inline,
2641 )
2642 .unwrap_if_no_warnings();
2643
2644 assert_eq!(
2645 mi.item,
2646 Attrlist {
2647 attributes: &[
2648 ElementAttribute {
2649 name: None,
2650 shorthand_items: &["Sunset"],
2651 value: "Sunset"
2652 },
2653 ElementAttribute {
2654 name: None,
2655 shorthand_items: &[],
2656 value: "300"
2657 },
2658 ElementAttribute {
2659 name: None,
2660 shorthand_items: &[],
2661 value: "400"
2662 }
2663 ],
2664 anchor: None,
2665 source: Span {
2666 data: "Sunset,{sunset_dimensions}",
2667 line: 1,
2668 col: 1,
2669 offset: 0
2670 }
2671 }
2672 );
2673
2674 assert!(mi.item.named_attribute("foo").is_none());
2675 assert!(mi.item.nth_attribute(0).is_none());
2676 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2677
2678 assert!(mi.item.id().is_none());
2679 assert!(mi.item.roles().is_empty());
2680 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
2681
2682 assert_eq!(
2683 mi.item.nth_attribute(1).unwrap(),
2684 ElementAttribute {
2685 name: None,
2686 shorthand_items: &["Sunset"],
2687 value: "Sunset"
2688 }
2689 );
2690
2691 assert_eq!(
2692 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
2693 ElementAttribute {
2694 name: None,
2695 shorthand_items: &["Sunset"],
2696 value: "Sunset"
2697 }
2698 );
2699
2700 assert_eq!(
2701 mi.item.nth_attribute(2).unwrap(),
2702 ElementAttribute {
2703 name: None,
2704 shorthand_items: &[],
2705 value: "300"
2706 }
2707 );
2708
2709 assert_eq!(
2710 mi.item.named_or_positional_attribute("width", 2).unwrap(),
2711 ElementAttribute {
2712 name: None,
2713 shorthand_items: &[],
2714 value: "300"
2715 }
2716 );
2717
2718 assert_eq!(
2719 mi.item.nth_attribute(3).unwrap(),
2720 ElementAttribute {
2721 name: None,
2722 shorthand_items: &[],
2723 value: "400"
2724 }
2725 );
2726
2727 assert_eq!(
2728 mi.item.named_or_positional_attribute("height", 3).unwrap(),
2729 ElementAttribute {
2730 name: None,
2731 shorthand_items: &[],
2732 value: "400"
2733 }
2734 );
2735
2736 assert!(mi.item.nth_attribute(4).is_none());
2737 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
2738 assert!(mi.item.nth_attribute(42).is_none());
2739
2740 assert_eq!(
2741 mi.item.span(),
2742 Span {
2743 data: "Sunset,{sunset_dimensions}",
2744 line: 1,
2745 col: 1,
2746 offset: 0,
2747 }
2748 );
2749
2750 assert_eq!(
2751 mi.after,
2752 Span {
2753 data: "",
2754 line: 1,
2755 col: 27,
2756 offset: 26,
2757 }
2758 );
2759 }
2760
2761 #[test]
2762 fn ignores_unknown_attribute_when_applying_attribution_substitution() {
2763 let p = Parser::default().with_intrinsic_attribute(
2764 "sunset_dimensions",
2765 "300,400",
2766 ModificationContext::Anywhere,
2767 );
2768
2769 let mi = crate::attributes::Attrlist::parse(
2770 crate::Span::new("Sunset,{not_sunset_dimensions}"),
2771 &p,
2772 AttrlistContext::Inline,
2773 )
2774 .unwrap_if_no_warnings();
2775
2776 assert_eq!(
2777 mi.item,
2778 Attrlist {
2779 attributes: &[
2780 ElementAttribute {
2781 name: None,
2782 shorthand_items: &["Sunset"],
2783 value: "Sunset"
2784 },
2785 ElementAttribute {
2786 name: None,
2787 shorthand_items: &[],
2788 value: "{not_sunset_dimensions}"
2789 },
2790 ],
2791 anchor: None,
2792 source: Span {
2793 data: "Sunset,{not_sunset_dimensions}",
2794 line: 1,
2795 col: 1,
2796 offset: 0
2797 }
2798 }
2799 );
2800
2801 assert!(mi.item.named_attribute("foo").is_none());
2802 assert!(mi.item.nth_attribute(0).is_none());
2803 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2804
2805 assert!(mi.item.id().is_none());
2806 assert!(mi.item.roles().is_empty());
2807 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
2808
2809 assert_eq!(
2810 mi.item.nth_attribute(1).unwrap(),
2811 ElementAttribute {
2812 name: None,
2813 shorthand_items: &["Sunset"],
2814 value: "Sunset"
2815 }
2816 );
2817
2818 assert_eq!(
2819 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
2820 ElementAttribute {
2821 name: None,
2822 shorthand_items: &["Sunset"],
2823 value: "Sunset"
2824 }
2825 );
2826
2827 assert_eq!(
2828 mi.item.nth_attribute(2).unwrap(),
2829 ElementAttribute {
2830 name: None,
2831 shorthand_items: &[],
2832 value: "{not_sunset_dimensions}"
2833 }
2834 );
2835
2836 assert_eq!(
2837 mi.item.named_or_positional_attribute("width", 2).unwrap(),
2838 ElementAttribute {
2839 name: None,
2840 shorthand_items: &[],
2841 value: "{not_sunset_dimensions}"
2842 }
2843 );
2844
2845 assert!(mi.item.nth_attribute(3).is_none());
2846 assert!(mi.item.named_or_positional_attribute("height", 3).is_none());
2847 assert!(mi.item.nth_attribute(42).is_none());
2848
2849 assert_eq!(
2850 mi.item.span(),
2851 Span {
2852 data: "Sunset,{not_sunset_dimensions}",
2853 line: 1,
2854 col: 1,
2855 offset: 0,
2856 }
2857 );
2858
2859 assert_eq!(
2860 mi.after,
2861 Span {
2862 data: "",
2863 line: 1,
2864 col: 31,
2865 offset: 30,
2866 }
2867 );
2868 }
2869
2870 #[test]
2871 fn impl_debug() {
2872 let p = Parser::default();
2873
2874 let mi = crate::attributes::Attrlist::parse(
2875 crate::Span::new("Sunset,300,400"),
2876 &p,
2877 AttrlistContext::Inline,
2878 )
2879 .unwrap_if_no_warnings();
2880
2881 let attrlist = mi.item;
2882
2883 assert_eq!(
2884 format!("{attrlist:#?}"),
2885 r#"Attrlist {
2886 attributes: &[
2887 ElementAttribute {
2888 name: None,
2889 value: "Sunset",
2890 shorthand_item_indices: [
2891 0,
2892 ],
2893 },
2894 ElementAttribute {
2895 name: None,
2896 value: "300",
2897 shorthand_item_indices: [],
2898 },
2899 ElementAttribute {
2900 name: None,
2901 value: "400",
2902 shorthand_item_indices: [],
2903 },
2904 ],
2905 anchor: None,
2906 source: Span {
2907 data: "Sunset,300,400",
2908 line: 1,
2909 col: 1,
2910 offset: 0,
2911 },
2912}"#
2913 );
2914 }
2915}