1use crate::{
2 HasSpan, Parser, Span,
3 attributes::{ElementAttribute, element_attribute::ParseShorthand},
4 content::{Content, SubstitutionStep},
5 internal::{debug::DebugSliceReference, opaque_iter::opaque_slice_iter},
6 span::MatchedItem,
7 strings::CowStr,
8 warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11opaque_slice_iter! {
12 pub struct ElementAttributes<'a> yielding ElementAttribute<'a>;
15}
16
17#[derive(Clone, Eq, Hash, PartialEq, Default)]
25pub struct Attrlist<'src> {
26 attributes: Vec<ElementAttribute<'src>>,
27 anchor: Option<CowStr<'src>>,
28 source: Span<'src>,
29}
30
31impl<'src> Attrlist<'src> {
32 pub(crate) fn parse(
37 source: Span<'src>,
38 parser: &Parser,
39 attrlist_context: AttrlistContext,
40 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
41 let mut attributes: Vec<ElementAttribute> = vec![];
42 let mut parse_shorthand_items = true;
43 let mut warnings: Vec<Warning<'src>> = vec![];
44
45 let source_cow = if source.contains('{') && source.contains('}') {
47 let mut content = Content::from(source);
48 SubstitutionStep::AttributeReferences.apply(&mut content, parser, None);
49 CowStr::from(content.rendered.to_string())
50 } else {
51 CowStr::from(source.data())
52 };
53
54 if source_cow.starts_with('[') && source_cow.ends_with(']') {
55 let anchor = source_cow[1..source_cow.len() - 1].to_owned();
56
57 return MatchAndWarnings {
58 item: MatchedItem {
59 item: Self {
60 attributes,
61 anchor: Some(CowStr::from(anchor)),
62 source,
63 },
64 after: source.discard_all(),
65 },
66 warnings,
67 };
68 }
69
70 let mut index = 0;
71
72 let mut entry_number = 0usize;
77
78 let after_index = loop {
79 entry_number += 1;
80
81 let (mut attr, new_index, warning_types) = ElementAttribute::parse(
82 &source_cow,
83 index,
84 parser,
85 ParseShorthand(parse_shorthand_items),
86 attrlist_context,
87 );
88
89 for warning_type in warning_types {
94 warnings.push(Warning {
95 source,
96 warning: warning_type,
97 origin: None,
98 });
99 }
100
101 parse_shorthand_items = false;
108
109 let mut after = Span::new(source_cow.as_ref()).discard(new_index);
110
111 if attr.name().is_none()
117 && attr.value().is_empty()
118 && !attr.value_is_quoted()
119 && after.is_empty()
120 && attributes.is_empty()
121 {
122 break index;
123 }
124
125 if attr.name().is_some() {
126 if attr.value() != "None" {
130 attributes.push(attr);
131 }
132 } else if !attr.value().is_empty() || attr.value_is_quoted() {
133 attr.set_positional_index(entry_number);
137 attributes.push(attr);
138 }
139
140 after = after.take_whitespace().after;
145
146 match after.take_prefix(",") {
147 Some(comma) => {
148 after = comma.after.take_whitespace().after;
149
150 if after.starts_with(',') {
151 warnings.push(Warning {
152 source,
153 warning: WarningType::EmptyAttributeValue,
154 origin: None,
155 });
156
157 entry_number += 1;
160 after = after.discard(1);
161 index = after.byte_offset();
162 continue;
163 }
164
165 index = after.byte_offset();
166 }
167 None => {
168 break after.byte_offset();
169 }
170 }
171 };
172
173 if after_index < source_cow.len() {
174 warnings.push(Warning {
175 source,
176 warning: WarningType::MissingCommaAfterQuotedAttributeValue,
177 origin: None,
178 });
179 }
180
181 MatchAndWarnings {
182 item: MatchedItem {
183 item: Self {
184 attributes,
185 anchor: None,
186 source,
187 },
188 after: source.discard_all(),
189 },
190 warnings,
191 }
192 }
193
194 pub(crate) fn source_with_language(language: Span<'src>) -> Self {
209 Self {
210 attributes: vec![
211 ElementAttribute::synthesized_source_style(),
212 ElementAttribute::positional_from_span(language),
213 ],
214 anchor: None,
215 source: language,
216 }
217 }
218
219 pub(crate) fn merge_block_attribute_line(&mut self, later: Attrlist<'src>) {
248 let fold_roles =
253 self.named_attribute("role").is_some() || later.named_attribute("role").is_some();
254
255 let resolved_roles: Option<Vec<String>> = if fold_roles {
256 let current: Vec<String> = self.roles().iter().map(|r| r.to_string()).collect();
260
261 let later_formal: Option<Vec<String>> = later
266 .named_attribute("role")
267 .map(|attr| split_role_value(attr.value()).map(str::to_string).collect());
268
269 let later_shorthand: Vec<String> = later
270 .nth_attribute(1)
271 .map(|attr| attr.roles().iter().map(|r| r.to_string()).collect())
272 .unwrap_or_default();
273
274 let mut resolved = later_formal.unwrap_or(current);
275 resolved.extend(later_shorthand);
276
277 if let Some(existing) = self.nth_positional_mut(1) {
280 *existing = existing.without_shorthand_roles();
281 }
282
283 Some(resolved)
284 } else {
285 None
286 };
287
288 let Attrlist {
289 attributes: later_attributes,
290 anchor: later_anchor,
291 source: _,
292 } = later;
293
294 if later_anchor.is_some() {
295 self.anchor = later_anchor;
296 }
297
298 for attr in later_attributes {
299 if fold_roles && attr.name_str() == Some("role") {
303 continue;
304 }
305
306 match attr.positional_index() {
314 None => {
317 if let Some(existing) = self
318 .attributes
319 .iter_mut()
320 .find(|a| a.name_str() == attr.name_str())
321 {
322 *existing = attr;
323 } else {
324 self.attributes.push(attr);
325 }
326 }
327
328 Some(1) => {
333 let attr = if fold_roles {
334 attr.without_shorthand_roles()
335 } else {
336 attr
337 };
338
339 if let Some(existing) = self.nth_positional_mut(1) {
340 *existing = ElementAttribute::merge_block_style_shorthand(existing, &attr);
341 } else {
342 self.attributes.push(attr);
343 }
344 }
345
346 Some(position) => {
349 if let Some(existing) = self.nth_positional_mut(position) {
350 *existing = attr;
351 } else {
352 self.attributes.push(attr);
353 }
354 }
355 }
356 }
357
358 if let Some(resolved) = resolved_roles {
360 self.set_role_attribute(resolved);
361 }
362 }
363
364 fn set_role_attribute(&mut self, roles: Vec<String>) {
370 if roles.is_empty() {
371 self.attributes.retain(|a| a.name_str() != Some("role"));
372 return;
373 }
374
375 let attr = ElementAttribute::synthesized_role(roles.join(" "));
376 if let Some(existing) = self
377 .attributes
378 .iter_mut()
379 .find(|a| a.name_str() == Some("role"))
380 {
381 *existing = attr;
382 } else {
383 self.attributes.push(attr);
384 }
385 }
386
387 fn nth_positional_mut(&mut self, n: usize) -> Option<&mut ElementAttribute<'src>> {
397 debug_assert!(n >= 1, "nth_positional_mut requires a 1-based position");
398
399 self.attributes
400 .iter_mut()
401 .find(|attr| attr.positional_index() == Some(n))
402 }
403
404 pub fn attributes(&'src self) -> ElementAttributes<'src> {
407 ElementAttributes::new(&self.attributes)
408 }
409
410 pub fn anchor(&'src self) -> Option<&'src str> {
412 self.anchor.as_deref()
413 }
414
415 pub(crate) fn title_attribute(&self) -> Option<(&str, bool)> {
427 self.attributes
428 .iter()
429 .find(|attr| attr.name_str() == Some("title"))
430 .map(|attr| (attr.value_str(), attr.value_is_substituted()))
431 }
432
433 pub fn named_attribute(&'src self, name: &str) -> Option<&'src ElementAttribute<'src>> {
435 self.attributes.iter().find(|attr| {
436 if let Some(attr_name) = attr.name() {
437 attr_name == name
438 } else {
439 false
440 }
441 })
442 }
443
444 pub fn nth_attribute(&'src self, n: usize) -> Option<&'src ElementAttribute<'src>> {
454 if n == 0 {
455 None
456 } else {
457 self.attributes
458 .iter()
459 .find(|attr| attr.positional_index() == Some(n))
460 }
461 }
462
463 pub fn named_or_positional_attribute(
471 &'src self,
472 name: &str,
473 index: usize,
474 ) -> Option<&'src ElementAttribute<'src>> {
475 self.named_attribute(name)
476 .or_else(|| self.nth_attribute(index))
477 }
478
479 pub fn id(&'src self) -> Option<&'src str> {
510 self.anchor().or_else(|| {
511 self.nth_attribute(1)
512 .and_then(|attr1| attr1.id())
513 .or_else(|| self.named_attribute("id").map(|attr| attr.value()))
514 })
515 }
516
517 pub fn roles(&'src self) -> Vec<&'src str> {
539 let mut roles = self
540 .nth_attribute(1)
541 .map(|attr1| attr1.roles())
542 .unwrap_or_default();
543
544 if let Some(role_attr) = self.named_attribute("role") {
545 roles.extend(split_role_value(role_attr.value()));
546 }
547
548 roles
549 }
550
551 pub(crate) fn quoted_text_fallback_role(&'src self) -> Option<&'src str> {
567 if !self.nth_attribute(1)?.value_is_quoted() {
568 return None;
569 }
570
571 let raw = self.source.data();
580 Some(raw.split_once(',').map_or(raw, |(first, _)| first).trim())
581 }
582
583 pub fn options(&'src self) -> Vec<&'src str> {
647 let mut options = self
648 .nth_attribute(1)
649 .map(|attr1| attr1.options())
650 .unwrap_or_default();
651
652 if let Some(option_attr) = self.named_attribute("opts") {
653 options.append(&mut split_options(option_attr.value()));
654 }
655
656 if let Some(option_attr) = self.named_attribute("options") {
657 options.append(&mut split_options(option_attr.value()));
658 }
659
660 options
661 }
662
663 pub fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
669 let options = self.options();
671 let name = name.as_ref();
672 options.contains(&name)
673 }
674
675 pub fn block_style(&'src self) -> Option<&'src str> {
677 self.nth_attribute(1).and_then(|a| a.block_style())
678 }
679}
680
681impl<'src> HasSpan<'src> for Attrlist<'src> {
682 fn span(&self) -> Span<'src> {
683 self.source
684 }
685}
686
687impl std::fmt::Debug for Attrlist<'_> {
688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689 f.debug_struct("Attrlist")
690 .field("attributes", &DebugSliceReference(&self.attributes))
691 .field("anchor", &self.anchor)
692 .field("source", &self.source)
693 .finish()
694 }
695}
696
697fn split_options(value: &str) -> Vec<&str> {
702 value
703 .split(',')
704 .map(str::trim)
705 .filter(|opt| !opt.is_empty())
706 .collect()
707}
708
709fn split_role_value(value: &str) -> impl Iterator<Item = &str> {
717 value.split(' ').filter(|role| !role.is_empty())
718}
719
720#[derive(Clone, Copy, Debug, Eq, PartialEq)]
722pub(crate) enum AttrlistContext {
723 Block,
724 Inline,
725}
726
727#[cfg(test)]
728mod tests {
729 #![allow(clippy::unwrap_used)]
730
731 use crate::{attributes::AttrlistContext, tests::prelude::*};
732
733 #[test]
734 fn impl_clone() {
735 let p = Parser::default();
737 let b1 = crate::attributes::Attrlist::parse(
738 crate::Span::new("abc"),
739 &p,
740 AttrlistContext::Inline,
741 )
742 .unwrap_if_no_warnings();
743
744 let b2 = b1.item.clone();
745 assert_eq!(b1.item, b2);
746 }
747
748 #[test]
749 fn impl_default() {
750 let attrlist = crate::attributes::Attrlist::default();
751
752 assert_eq!(
753 attrlist,
754 Attrlist {
755 attributes: &[],
756 anchor: None,
757 source: Span {
758 data: "",
759 line: 1,
760 col: 1,
761 offset: 0
762 }
763 }
764 );
765
766 assert!(attrlist.named_attribute("foo").is_none());
767
768 assert!(attrlist.nth_attribute(0).is_none());
769 assert!(attrlist.nth_attribute(1).is_none());
770 assert!(attrlist.nth_attribute(42).is_none());
771
772 assert!(attrlist.named_or_positional_attribute("foo", 0).is_none());
773 assert!(attrlist.named_or_positional_attribute("foo", 1).is_none());
774 assert!(attrlist.named_or_positional_attribute("foo", 42).is_none());
775
776 assert!(attrlist.id().is_none());
777 assert!(attrlist.roles().is_empty());
778 assert!(attrlist.block_style().is_none());
779
780 assert_eq!(
781 attrlist.span(),
782 Span {
783 data: "",
784 line: 1,
785 col: 1,
786 offset: 0,
787 }
788 );
789 }
790
791 #[test]
792 fn empty_source() {
793 let p = Parser::default();
794
795 let mi =
796 crate::attributes::Attrlist::parse(crate::Span::default(), &p, AttrlistContext::Inline)
797 .unwrap_if_no_warnings();
798
799 assert_eq!(
800 mi.item,
801 Attrlist {
802 attributes: &[],
803 anchor: None,
804 source: Span {
805 data: "",
806 line: 1,
807 col: 1,
808 offset: 0
809 }
810 }
811 );
812
813 assert!(mi.item.named_attribute("foo").is_none());
814
815 assert!(mi.item.nth_attribute(0).is_none());
816 assert!(mi.item.nth_attribute(1).is_none());
817 assert!(mi.item.nth_attribute(42).is_none());
818
819 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
820 assert!(mi.item.named_or_positional_attribute("foo", 1).is_none());
821 assert!(mi.item.named_or_positional_attribute("foo", 42).is_none());
822
823 assert!(mi.item.id().is_none());
824 assert!(mi.item.roles().is_empty());
825 assert!(mi.item.block_style().is_none());
826
827 assert_eq!(
828 mi.item.span(),
829 Span {
830 data: "",
831 line: 1,
832 col: 1,
833 offset: 0,
834 }
835 );
836
837 assert_eq!(
838 mi.after,
839 Span {
840 data: "",
841 line: 1,
842 col: 1,
843 offset: 0
844 }
845 );
846 }
847
848 #[test]
849 fn empty_positional_attributes() {
850 let p = Parser::default();
851
852 let mi = crate::attributes::Attrlist::parse(
853 crate::Span::new(",300,400"),
854 &p,
855 AttrlistContext::Inline,
856 )
857 .unwrap_if_no_warnings();
858
859 assert_eq!(
863 mi.item,
864 Attrlist {
865 attributes: &[
866 ElementAttribute {
867 name: None,
868 shorthand_items: &[],
869 value: "300"
870 },
871 ElementAttribute {
872 name: None,
873 shorthand_items: &[],
874 value: "400"
875 }
876 ],
877 anchor: None,
878 source: Span {
879 data: ",300,400",
880 line: 1,
881 col: 1,
882 offset: 0
883 }
884 }
885 );
886
887 assert!(mi.item.named_attribute("foo").is_none());
888 assert!(mi.item.nth_attribute(0).is_none());
889 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
890
891 assert!(mi.item.id().is_none());
892 assert!(mi.item.roles().is_empty());
893 assert!(mi.item.block_style().is_none());
894
895 assert!(mi.item.nth_attribute(1).is_none());
897 assert!(mi.item.named_or_positional_attribute("alt", 1).is_none());
898
899 assert_eq!(
900 mi.item.nth_attribute(2).unwrap(),
901 ElementAttribute {
902 name: None,
903 shorthand_items: &[],
904 value: "300"
905 }
906 );
907
908 assert_eq!(
909 mi.item.named_or_positional_attribute("width", 2).unwrap(),
910 ElementAttribute {
911 name: None,
912 shorthand_items: &[],
913 value: "300"
914 }
915 );
916
917 assert_eq!(
918 mi.item.nth_attribute(3).unwrap(),
919 ElementAttribute {
920 name: None,
921 shorthand_items: &[],
922 value: "400"
923 }
924 );
925
926 assert_eq!(
927 mi.item.named_or_positional_attribute("height", 3).unwrap(),
928 ElementAttribute {
929 name: None,
930 shorthand_items: &[],
931 value: "400"
932 }
933 );
934
935 assert!(mi.item.nth_attribute(4).is_none());
936 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
937 assert!(mi.item.nth_attribute(42).is_none());
938
939 assert_eq!(
940 mi.item.span(),
941 Span {
942 data: ",300,400",
943 line: 1,
944 col: 1,
945 offset: 0,
946 }
947 );
948
949 assert_eq!(
950 mi.after,
951 Span {
952 data: "",
953 line: 1,
954 col: 9,
955 offset: 8
956 }
957 );
958 }
959
960 #[test]
961 fn only_positional_attributes() {
962 let p = Parser::default();
963
964 let mi = crate::attributes::Attrlist::parse(
965 crate::Span::new("Sunset,300,400"),
966 &p,
967 AttrlistContext::Inline,
968 )
969 .unwrap_if_no_warnings();
970
971 assert_eq!(
972 mi.item,
973 Attrlist {
974 attributes: &[
975 ElementAttribute {
976 name: None,
977 shorthand_items: &["Sunset"],
978 value: "Sunset"
979 },
980 ElementAttribute {
981 name: None,
982 shorthand_items: &[],
983 value: "300"
984 },
985 ElementAttribute {
986 name: None,
987 shorthand_items: &[],
988 value: "400"
989 }
990 ],
991 anchor: None,
992 source: Span {
993 data: "Sunset,300,400",
994 line: 1,
995 col: 1,
996 offset: 0
997 }
998 }
999 );
1000
1001 assert!(mi.item.named_attribute("foo").is_none());
1002 assert!(mi.item.nth_attribute(0).is_none());
1003 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1004
1005 assert!(mi.item.id().is_none());
1006 assert!(mi.item.roles().is_empty());
1007 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
1008
1009 assert_eq!(
1010 mi.item.nth_attribute(1).unwrap(),
1011 ElementAttribute {
1012 name: None,
1013 shorthand_items: &["Sunset"],
1014 value: "Sunset"
1015 }
1016 );
1017
1018 assert_eq!(
1019 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1020 ElementAttribute {
1021 name: None,
1022 shorthand_items: &["Sunset"],
1023 value: "Sunset"
1024 }
1025 );
1026
1027 assert_eq!(
1028 mi.item.nth_attribute(2).unwrap(),
1029 ElementAttribute {
1030 name: None,
1031 shorthand_items: &[],
1032 value: "300"
1033 }
1034 );
1035
1036 assert_eq!(
1037 mi.item.named_or_positional_attribute("width", 2).unwrap(),
1038 ElementAttribute {
1039 name: None,
1040 shorthand_items: &[],
1041 value: "300"
1042 }
1043 );
1044
1045 assert_eq!(
1046 mi.item.nth_attribute(3).unwrap(),
1047 ElementAttribute {
1048 name: None,
1049 shorthand_items: &[],
1050 value: "400"
1051 }
1052 );
1053
1054 assert_eq!(
1055 mi.item.named_or_positional_attribute("height", 3).unwrap(),
1056 ElementAttribute {
1057 name: None,
1058 shorthand_items: &[],
1059 value: "400"
1060 }
1061 );
1062
1063 assert!(mi.item.nth_attribute(4).is_none());
1064 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
1065 assert!(mi.item.nth_attribute(42).is_none());
1066
1067 assert_eq!(
1068 mi.item.span(),
1069 Span {
1070 data: "Sunset,300,400",
1071 line: 1,
1072 col: 1,
1073 offset: 0,
1074 }
1075 );
1076
1077 assert_eq!(
1078 mi.after,
1079 Span {
1080 data: "",
1081 line: 1,
1082 col: 15,
1083 offset: 14
1084 }
1085 );
1086 }
1087
1088 #[test]
1089 fn trim_trailing_space() {
1090 let p = Parser::default();
1091
1092 let mi = crate::attributes::Attrlist::parse(
1093 crate::Span::new("Sunset ,300 , 400"),
1094 &p,
1095 AttrlistContext::Inline,
1096 )
1097 .unwrap_if_no_warnings();
1098
1099 assert_eq!(
1100 mi.item,
1101 Attrlist {
1102 attributes: &[
1103 ElementAttribute {
1104 name: None,
1105 shorthand_items: &["Sunset"],
1106 value: "Sunset"
1107 },
1108 ElementAttribute {
1109 name: None,
1110 shorthand_items: &[],
1111 value: "300"
1112 },
1113 ElementAttribute {
1114 name: None,
1115 shorthand_items: &[],
1116 value: "400"
1117 }
1118 ],
1119 anchor: None,
1120 source: Span {
1121 data: "Sunset ,300 , 400",
1122 line: 1,
1123 col: 1,
1124 offset: 0
1125 }
1126 }
1127 );
1128
1129 assert!(mi.item.named_attribute("foo").is_none());
1130 assert!(mi.item.nth_attribute(0).is_none());
1131 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1132
1133 assert!(mi.item.id().is_none());
1134 assert!(mi.item.roles().is_empty());
1135 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
1136
1137 assert_eq!(
1138 mi.item.nth_attribute(1).unwrap(),
1139 ElementAttribute {
1140 name: None,
1141 shorthand_items: &["Sunset"],
1142 value: "Sunset"
1143 }
1144 );
1145
1146 assert_eq!(
1147 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1148 ElementAttribute {
1149 name: None,
1150 shorthand_items: &["Sunset"],
1151 value: "Sunset"
1152 }
1153 );
1154
1155 assert_eq!(
1156 mi.item.nth_attribute(2).unwrap(),
1157 ElementAttribute {
1158 name: None,
1159 shorthand_items: &[],
1160 value: "300"
1161 }
1162 );
1163
1164 assert_eq!(
1165 mi.item.named_or_positional_attribute("width", 2).unwrap(),
1166 ElementAttribute {
1167 name: None,
1168 shorthand_items: &[],
1169 value: "300"
1170 }
1171 );
1172
1173 assert_eq!(
1174 mi.item.nth_attribute(3).unwrap(),
1175 ElementAttribute {
1176 name: None,
1177 shorthand_items: &[],
1178 value: "400"
1179 }
1180 );
1181
1182 assert_eq!(
1183 mi.item.named_or_positional_attribute("height", 3).unwrap(),
1184 ElementAttribute {
1185 name: None,
1186 shorthand_items: &[],
1187 value: "400"
1188 }
1189 );
1190
1191 assert!(mi.item.nth_attribute(4).is_none());
1192 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
1193 assert!(mi.item.nth_attribute(42).is_none());
1194
1195 assert_eq!(
1196 mi.item.span(),
1197 Span {
1198 data: "Sunset ,300 , 400",
1199 line: 1,
1200 col: 1,
1201 offset: 0,
1202 }
1203 );
1204
1205 assert_eq!(
1206 mi.after,
1207 Span {
1208 data: "",
1209 line: 1,
1210 col: 18,
1211 offset: 17
1212 }
1213 );
1214 }
1215
1216 #[test]
1217 fn only_named_attributes() {
1218 let p = Parser::default();
1219
1220 let mi = crate::attributes::Attrlist::parse(
1221 crate::Span::new("alt=Sunset,width=300,height=400"),
1222 &p,
1223 AttrlistContext::Inline,
1224 )
1225 .unwrap_if_no_warnings();
1226
1227 assert_eq!(
1228 mi.item,
1229 Attrlist {
1230 attributes: &[
1231 ElementAttribute {
1232 name: Some("alt"),
1233 shorthand_items: &[],
1234 value: "Sunset"
1235 },
1236 ElementAttribute {
1237 name: Some("width"),
1238 shorthand_items: &[],
1239 value: "300"
1240 },
1241 ElementAttribute {
1242 name: Some("height"),
1243 shorthand_items: &[],
1244 value: "400"
1245 }
1246 ],
1247 anchor: None,
1248 source: Span {
1249 data: "alt=Sunset,width=300,height=400",
1250 line: 1,
1251 col: 1,
1252 offset: 0
1253 }
1254 }
1255 );
1256
1257 assert!(mi.item.named_attribute("foo").is_none());
1258 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1259
1260 assert_eq!(
1261 mi.item.named_attribute("alt").unwrap(),
1262 ElementAttribute {
1263 name: Some("alt"),
1264 shorthand_items: &[],
1265 value: "Sunset"
1266 }
1267 );
1268
1269 assert_eq!(
1270 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1271 ElementAttribute {
1272 name: Some("alt"),
1273 shorthand_items: &[],
1274 value: "Sunset"
1275 }
1276 );
1277
1278 assert_eq!(
1279 mi.item.named_attribute("width").unwrap(),
1280 ElementAttribute {
1281 name: Some("width"),
1282 shorthand_items: &[],
1283 value: "300"
1284 }
1285 );
1286
1287 assert_eq!(
1288 mi.item.named_or_positional_attribute("width", 2).unwrap(),
1289 ElementAttribute {
1290 name: Some("width"),
1291 shorthand_items: &[],
1292 value: "300"
1293 }
1294 );
1295
1296 assert_eq!(
1297 mi.item.named_attribute("height").unwrap(),
1298 ElementAttribute {
1299 name: Some("height"),
1300 shorthand_items: &[],
1301 value: "400"
1302 }
1303 );
1304
1305 assert_eq!(
1306 mi.item.named_or_positional_attribute("height", 3).unwrap(),
1307 ElementAttribute {
1308 name: Some("height"),
1309 shorthand_items: &[],
1310 value: "400"
1311 }
1312 );
1313
1314 assert!(mi.item.nth_attribute(0).is_none());
1315 assert!(mi.item.nth_attribute(1).is_none());
1316 assert!(mi.item.nth_attribute(2).is_none());
1317 assert!(mi.item.nth_attribute(3).is_none());
1318 assert!(mi.item.nth_attribute(4).is_none());
1319 assert!(mi.item.nth_attribute(42).is_none());
1320
1321 assert!(mi.item.id().is_none());
1322 assert!(mi.item.roles().is_empty());
1323 assert!(mi.item.block_style().is_none());
1324
1325 assert_eq!(
1326 mi.item.span(),
1327 Span {
1328 data: "alt=Sunset,width=300,height=400",
1329 line: 1,
1330 col: 1,
1331 offset: 0
1332 }
1333 );
1334
1335 assert_eq!(
1336 mi.after,
1337 Span {
1338 data: "",
1339 line: 1,
1340 col: 32,
1341 offset: 31
1342 }
1343 );
1344 }
1345
1346 #[test]
1347 fn ignore_named_attribute_with_none_value() {
1348 let p = Parser::default();
1349 let mi = crate::attributes::Attrlist::parse(
1350 crate::Span::new("alt=Sunset,width=None,height=400"),
1351 &p,
1352 AttrlistContext::Inline,
1353 )
1354 .unwrap_if_no_warnings();
1355
1356 assert_eq!(
1357 mi.item,
1358 Attrlist {
1359 attributes: &[
1360 ElementAttribute {
1361 name: Some("alt"),
1362 shorthand_items: &[],
1363 value: "Sunset"
1364 },
1365 ElementAttribute {
1366 name: Some("height"),
1367 shorthand_items: &[],
1368 value: "400"
1369 }
1370 ],
1371 anchor: None,
1372 source: Span {
1373 data: "alt=Sunset,width=None,height=400",
1374 line: 1,
1375 col: 1,
1376 offset: 0
1377 }
1378 }
1379 );
1380
1381 assert!(mi.item.named_attribute("foo").is_none());
1382 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1383
1384 assert_eq!(
1385 mi.item.named_attribute("alt").unwrap(),
1386 ElementAttribute {
1387 name: Some("alt"),
1388 shorthand_items: &[],
1389 value: "Sunset"
1390 }
1391 );
1392
1393 assert_eq!(
1394 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1395 ElementAttribute {
1396 name: Some("alt"),
1397 shorthand_items: &[],
1398 value: "Sunset"
1399 }
1400 );
1401
1402 assert!(mi.item.named_attribute("width").is_none());
1403 assert!(mi.item.named_or_positional_attribute("width", 2).is_none());
1404
1405 assert_eq!(
1406 mi.item.named_attribute("height").unwrap(),
1407 ElementAttribute {
1408 name: Some("height"),
1409 shorthand_items: &[],
1410 value: "400"
1411 }
1412 );
1413
1414 assert_eq!(
1415 mi.item.named_or_positional_attribute("height", 2).unwrap(),
1416 ElementAttribute {
1417 name: Some("height"),
1418 shorthand_items: &[],
1419 value: "400"
1420 }
1421 );
1422
1423 assert!(mi.item.nth_attribute(0).is_none());
1424 assert!(mi.item.nth_attribute(1).is_none());
1425 assert!(mi.item.nth_attribute(2).is_none());
1426 assert!(mi.item.nth_attribute(3).is_none());
1427 assert!(mi.item.nth_attribute(4).is_none());
1428 assert!(mi.item.nth_attribute(42).is_none());
1429
1430 assert!(mi.item.id().is_none());
1431 assert!(mi.item.roles().is_empty());
1432 assert!(mi.item.block_style().is_none());
1433
1434 assert_eq!(
1435 mi.item.span(),
1436 Span {
1437 data: "alt=Sunset,width=None,height=400",
1438 line: 1,
1439 col: 1,
1440 offset: 0
1441 }
1442 );
1443
1444 assert_eq!(
1445 mi.after,
1446 Span {
1447 data: "",
1448 line: 1,
1449 col: 33,
1450 offset: 32
1451 }
1452 );
1453 }
1454
1455 #[test]
1456 fn err_unparsed_remainder_after_value() {
1457 let p = Parser::default();
1458
1459 let maw = crate::attributes::Attrlist::parse(
1460 crate::Span::new("alt=\"Sunset\"width=300"),
1461 &p,
1462 AttrlistContext::Inline,
1463 );
1464
1465 let mi = maw.item.clone();
1466
1467 assert_eq!(
1468 mi.item,
1469 Attrlist {
1470 attributes: &[ElementAttribute {
1471 name: Some("alt"),
1472 shorthand_items: &[],
1473 value: "Sunset"
1474 }],
1475 anchor: None,
1476 source: Span {
1477 data: "alt=\"Sunset\"width=300",
1478 line: 1,
1479 col: 1,
1480 offset: 0
1481 }
1482 }
1483 );
1484
1485 assert_eq!(
1486 mi.after,
1487 Span {
1488 data: "",
1489 line: 1,
1490 col: 22,
1491 offset: 21
1492 }
1493 );
1494
1495 assert_eq!(
1496 maw.warnings,
1497 vec![Warning {
1498 source: Span {
1499 data: "alt=\"Sunset\"width=300",
1500 line: 1,
1501 col: 1,
1502 offset: 0,
1503 },
1504 warning: WarningType::MissingCommaAfterQuotedAttributeValue,
1505 }]
1506 );
1507 }
1508
1509 #[test]
1510 fn propagates_error_from_element_attribute() {
1511 let p = Parser::default();
1512
1513 let maw = crate::attributes::Attrlist::parse(
1514 crate::Span::new("foo%#id"),
1515 &p,
1516 AttrlistContext::Inline,
1517 );
1518
1519 let mi = maw.item.clone();
1520
1521 assert_eq!(
1522 mi.item,
1523 Attrlist {
1524 attributes: &[ElementAttribute {
1525 name: None,
1526 shorthand_items: &["foo", "#id"],
1527 value: "foo%#id"
1528 }],
1529 anchor: None,
1530 source: Span {
1531 data: "foo%#id",
1532 line: 1,
1533 col: 1,
1534 offset: 0
1535 }
1536 }
1537 );
1538
1539 assert_eq!(
1540 mi.after,
1541 Span {
1542 data: "",
1543 line: 1,
1544 col: 8,
1545 offset: 7
1546 }
1547 );
1548
1549 assert_eq!(
1550 maw.warnings,
1551 vec![Warning {
1552 source: Span {
1553 data: "foo%#id",
1554 line: 1,
1555 col: 1,
1556 offset: 0,
1557 },
1558 warning: WarningType::EmptyShorthandName,
1559 }]
1560 );
1561 }
1562
1563 #[test]
1564 fn merge_block_attribute_line_anchor_later_wins() {
1565 let p = Parser::default();
1566
1567 let mut first = crate::attributes::Attrlist::parse(
1568 crate::Span::new("[id1]"),
1569 &p,
1570 AttrlistContext::Block,
1571 )
1572 .unwrap_if_no_warnings()
1573 .item;
1574
1575 let later = crate::attributes::Attrlist::parse(
1576 crate::Span::new("[id2]"),
1577 &p,
1578 AttrlistContext::Block,
1579 )
1580 .unwrap_if_no_warnings()
1581 .item;
1582
1583 assert_eq!(first.anchor(), Some("id1"));
1584 first.merge_block_attribute_line(later);
1585 assert_eq!(first.anchor(), Some("id2"));
1586 }
1587
1588 #[test]
1589 fn merge_block_attribute_line_anchor_retained_when_later_has_none() {
1590 let p = Parser::default();
1591
1592 let mut first = crate::attributes::Attrlist::parse(
1593 crate::Span::new("[id1]"),
1594 &p,
1595 AttrlistContext::Block,
1596 )
1597 .unwrap_if_no_warnings()
1598 .item;
1599
1600 let later = crate::attributes::Attrlist::parse(
1601 crate::Span::new("foo=bar"),
1602 &p,
1603 AttrlistContext::Block,
1604 )
1605 .unwrap_if_no_warnings()
1606 .item;
1607
1608 first.merge_block_attribute_line(later);
1609 assert_eq!(first.anchor(), Some("id1"));
1610 assert_eq!(first.named_attribute("foo").unwrap().value(), "bar");
1611 }
1612
1613 #[test]
1614 fn merge_block_attribute_line_positions_account_for_named_entries() {
1615 let p = Parser::default();
1621
1622 let mut first = crate::attributes::Attrlist::parse(
1623 crate::Span::new("quote,Author1"),
1624 &p,
1625 AttrlistContext::Block,
1626 )
1627 .unwrap_if_no_warnings()
1628 .item;
1629
1630 let later = crate::attributes::Attrlist::parse(
1631 crate::Span::new("width=300,Author2,Extra"),
1632 &p,
1633 AttrlistContext::Block,
1634 )
1635 .unwrap_if_no_warnings()
1636 .item;
1637
1638 first.merge_block_attribute_line(later);
1639
1640 assert_eq!(first.nth_attribute(1).unwrap().value(), "quote");
1641 assert_eq!(first.nth_attribute(2).unwrap().value(), "Author2");
1642 assert_eq!(first.nth_attribute(3).unwrap().value(), "Extra");
1643 assert_eq!(first.named_attribute("width").unwrap().value(), "300");
1644 }
1645
1646 #[test]
1647 fn anchor_syntax() {
1648 let p = Parser::default();
1649
1650 let maw = crate::attributes::Attrlist::parse(
1651 crate::Span::new("[notice]"),
1652 &p,
1653 AttrlistContext::Inline,
1654 );
1655
1656 let mi = maw.item.clone();
1657
1658 assert_eq!(
1659 mi.item,
1660 Attrlist {
1661 attributes: &[],
1662 anchor: Some("notice"),
1663 source: Span {
1664 data: "[notice]",
1665 line: 1,
1666 col: 1,
1667 offset: 0
1668 }
1669 }
1670 );
1671
1672 assert_eq!(
1673 mi.after,
1674 Span {
1675 data: "",
1676 line: 1,
1677 col: 9,
1678 offset: 8
1679 }
1680 );
1681
1682 assert!(maw.warnings.is_empty());
1683 }
1684
1685 mod id {
1686 use crate::{attributes::AttrlistContext, tests::prelude::*};
1687
1688 #[test]
1689 fn via_shorthand_syntax() {
1690 let p = Parser::default();
1691
1692 let mi = crate::attributes::Attrlist::parse(
1693 crate::Span::new("#goals"),
1694 &p,
1695 AttrlistContext::Inline,
1696 )
1697 .unwrap_if_no_warnings();
1698
1699 assert_eq!(
1700 mi.item,
1701 Attrlist {
1702 attributes: &[ElementAttribute {
1703 name: None,
1704 shorthand_items: &["#goals"],
1705 value: "#goals"
1706 }],
1707 anchor: None,
1708 source: Span {
1709 data: "#goals",
1710 line: 1,
1711 col: 1,
1712 offset: 0
1713 }
1714 }
1715 );
1716
1717 assert!(mi.item.named_attribute("foo").is_none());
1718 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1719 assert_eq!(mi.item.id().unwrap(), "goals");
1720 assert!(mi.item.roles().is_empty());
1721 assert!(mi.item.block_style().is_none());
1722
1723 assert_eq!(
1724 mi.item.span(),
1725 Span {
1726 data: "#goals",
1727 line: 1,
1728 col: 1,
1729 offset: 0
1730 }
1731 );
1732
1733 assert_eq!(
1734 mi.after,
1735 Span {
1736 data: "",
1737 line: 1,
1738 col: 7,
1739 offset: 6
1740 }
1741 );
1742 }
1743
1744 #[test]
1745 fn via_named_attribute() {
1746 let p = Parser::default();
1747
1748 let mi = crate::attributes::Attrlist::parse(
1749 crate::Span::new("foo=bar,id=goals"),
1750 &p,
1751 AttrlistContext::Inline,
1752 )
1753 .unwrap_if_no_warnings();
1754
1755 assert_eq!(
1756 mi.item,
1757 Attrlist {
1758 attributes: &[
1759 ElementAttribute {
1760 name: Some("foo"),
1761 shorthand_items: &[],
1762 value: "bar"
1763 },
1764 ElementAttribute {
1765 name: Some("id"),
1766 shorthand_items: &[],
1767 value: "goals"
1768 },
1769 ],
1770 anchor: None,
1771 source: Span {
1772 data: "foo=bar,id=goals",
1773 line: 1,
1774 col: 1,
1775 offset: 0
1776 }
1777 }
1778 );
1779
1780 assert_eq!(
1781 mi.item.named_attribute("foo").unwrap(),
1782 ElementAttribute {
1783 name: Some("foo"),
1784 shorthand_items: &[],
1785 value: "bar"
1786 }
1787 );
1788
1789 assert_eq!(
1790 mi.item.named_attribute("id").unwrap(),
1791 ElementAttribute {
1792 name: Some("id"),
1793 shorthand_items: &[],
1794 value: "goals"
1795 }
1796 );
1797
1798 assert_eq!(mi.item.id().unwrap(), "goals");
1799 assert!(mi.item.roles().is_empty());
1800 assert!(mi.item.block_style().is_none());
1801
1802 assert_eq!(
1803 mi.after,
1804 Span {
1805 data: "",
1806 line: 1,
1807 col: 17,
1808 offset: 16
1809 }
1810 );
1811 }
1812
1813 #[test]
1814 fn via_block_anchor_syntax() {
1815 let p = Parser::default();
1816
1817 let mi = crate::attributes::Attrlist::parse(
1818 crate::Span::new("[goals]"),
1819 &p,
1820 AttrlistContext::Inline,
1821 )
1822 .unwrap_if_no_warnings();
1823
1824 assert_eq!(
1825 mi.item,
1826 Attrlist {
1827 attributes: &[],
1828 anchor: Some("goals"),
1829 source: Span {
1830 data: "[goals]",
1831 line: 1,
1832 col: 1,
1833 offset: 0
1834 }
1835 }
1836 );
1837
1838 assert_eq!(mi.item.id().unwrap(), "goals");
1839
1840 assert_eq!(
1841 mi.after,
1842 Span {
1843 data: "",
1844 line: 1,
1845 col: 8,
1846 offset: 7
1847 }
1848 );
1849 }
1850
1851 #[test]
1852 fn shorthand_only_first_attribute() {
1853 let p = Parser::default();
1854
1855 let mi = crate::attributes::Attrlist::parse(
1856 crate::Span::new("foo,blah#goals"),
1857 &p,
1858 AttrlistContext::Inline,
1859 )
1860 .unwrap_if_no_warnings();
1861
1862 assert_eq!(
1863 mi.item,
1864 Attrlist {
1865 attributes: &[
1866 ElementAttribute {
1867 name: None,
1868 shorthand_items: &["foo"],
1869 value: "foo"
1870 },
1871 ElementAttribute {
1872 name: None,
1873 shorthand_items: &[],
1874 value: "blah#goals"
1875 },
1876 ],
1877 anchor: None,
1878 source: Span {
1879 data: "foo,blah#goals",
1880 line: 1,
1881 col: 1,
1882 offset: 0
1883 }
1884 }
1885 );
1886
1887 assert!(mi.item.id().is_none());
1888 assert!(mi.item.roles().is_empty());
1889 assert_eq!(mi.item.block_style().unwrap(), "foo");
1890
1891 assert_eq!(
1892 mi.after,
1893 Span {
1894 data: "",
1895 line: 1,
1896 col: 15,
1897 offset: 14
1898 }
1899 );
1900 }
1901 }
1902
1903 mod roles {
1904 use crate::{attributes::AttrlistContext, tests::prelude::*};
1905
1906 #[test]
1907 fn via_shorthand_syntax() {
1908 let p = Parser::default();
1909
1910 let mi = crate::attributes::Attrlist::parse(
1911 crate::Span::new(".rolename"),
1912 &p,
1913 AttrlistContext::Inline,
1914 )
1915 .unwrap_if_no_warnings();
1916
1917 assert_eq!(
1918 mi.item,
1919 Attrlist {
1920 attributes: &[ElementAttribute {
1921 name: None,
1922 shorthand_items: &[".rolename"],
1923 value: ".rolename"
1924 }],
1925 anchor: None,
1926 source: Span {
1927 data: ".rolename",
1928 line: 1,
1929 col: 1,
1930 offset: 0
1931 }
1932 }
1933 );
1934
1935 assert!(mi.item.named_attribute("foo").is_none());
1936 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1937
1938 let roles = mi.item.roles();
1939 let mut roles = roles.iter();
1940 assert_eq!(roles.next().unwrap(), &"rolename");
1941 assert!(roles.next().is_none());
1942
1943 assert!(mi.item.block_style().is_none());
1944
1945 assert_eq!(
1946 mi.item.span(),
1947 Span {
1948 data: ".rolename",
1949 line: 1,
1950 col: 1,
1951 offset: 0
1952 }
1953 );
1954
1955 assert_eq!(
1956 mi.after,
1957 Span {
1958 data: "",
1959 line: 1,
1960 col: 10,
1961 offset: 9
1962 }
1963 );
1964 }
1965
1966 #[test]
1967 fn via_shorthand_syntax_trim_trailing_whitespace() {
1968 let p = Parser::default();
1969
1970 let mi = crate::attributes::Attrlist::parse(
1971 crate::Span::new(".rolename "),
1972 &p,
1973 AttrlistContext::Inline,
1974 )
1975 .unwrap_if_no_warnings();
1976
1977 assert_eq!(
1978 mi.item,
1979 Attrlist {
1980 attributes: &[ElementAttribute {
1981 name: None,
1982 shorthand_items: &[".rolename"],
1983 value: ".rolename"
1984 }],
1985 anchor: None,
1986 source: Span {
1987 data: ".rolename ",
1988 line: 1,
1989 col: 1,
1990 offset: 0
1991 }
1992 }
1993 );
1994
1995 assert!(mi.item.named_attribute("foo").is_none());
1996 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1997
1998 let roles = mi.item.roles();
1999 let mut roles = roles.iter();
2000
2001 assert_eq!(roles.next().unwrap(), &"rolename");
2002 assert!(roles.next().is_none());
2003
2004 assert!(mi.item.block_style().is_none());
2005
2006 assert_eq!(
2007 mi.item.span(),
2008 Span {
2009 data: ".rolename ",
2010 line: 1,
2011 col: 1,
2012 offset: 0
2013 }
2014 );
2015
2016 assert_eq!(
2017 mi.after,
2018 Span {
2019 data: "",
2020 line: 1,
2021 col: 11,
2022 offset: 10
2023 }
2024 );
2025 }
2026
2027 #[test]
2028 fn multiple_roles_via_shorthand_syntax() {
2029 let p = Parser::default();
2030
2031 let mi = crate::attributes::Attrlist::parse(
2032 crate::Span::new(".role1.role2.role3"),
2033 &p,
2034 AttrlistContext::Inline,
2035 )
2036 .unwrap_if_no_warnings();
2037
2038 assert_eq!(
2039 mi.item,
2040 Attrlist {
2041 attributes: &[ElementAttribute {
2042 name: None,
2043 shorthand_items: &[".role1", ".role2", ".role3"],
2044 value: ".role1.role2.role3"
2045 }],
2046 anchor: None,
2047 source: Span {
2048 data: ".role1.role2.role3",
2049 line: 1,
2050 col: 1,
2051 offset: 0
2052 }
2053 }
2054 );
2055
2056 assert!(mi.item.named_attribute("foo").is_none());
2057 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2058
2059 let roles = mi.item.roles();
2060 let mut roles = roles.iter();
2061 assert_eq!(roles.next().unwrap(), &"role1");
2062 assert_eq!(roles.next().unwrap(), &"role2");
2063 assert_eq!(roles.next().unwrap(), &"role3");
2064 assert!(roles.next().is_none());
2065
2066 assert!(mi.item.block_style().is_none());
2067
2068 assert_eq!(
2069 mi.item.span(),
2070 Span {
2071 data: ".role1.role2.role3",
2072 line: 1,
2073 col: 1,
2074 offset: 0
2075 }
2076 );
2077
2078 assert_eq!(
2079 mi.after,
2080 Span {
2081 data: "",
2082 line: 1,
2083 col: 19,
2084 offset: 18
2085 }
2086 );
2087 }
2088
2089 #[test]
2090 fn multiple_roles_via_shorthand_syntax_trim_whitespace() {
2091 let p = Parser::default();
2092
2093 let mi = crate::attributes::Attrlist::parse(
2094 crate::Span::new(".role1 .role2 .role3 "),
2095 &p,
2096 AttrlistContext::Inline,
2097 )
2098 .unwrap_if_no_warnings();
2099
2100 assert_eq!(
2101 mi.item,
2102 Attrlist {
2103 attributes: &[ElementAttribute {
2104 name: None,
2105 shorthand_items: &[".role1", ".role2", ".role3"],
2106 value: ".role1 .role2 .role3"
2107 }],
2108 anchor: None,
2109 source: Span {
2110 data: ".role1 .role2 .role3 ",
2111 line: 1,
2112 col: 1,
2113 offset: 0
2114 }
2115 }
2116 );
2117
2118 assert!(mi.item.named_attribute("foo").is_none());
2119 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2120
2121 let roles = mi.item.roles();
2122 let mut roles = roles.iter();
2123 assert_eq!(roles.next().unwrap(), &"role1");
2124 assert_eq!(roles.next().unwrap(), &"role2");
2125 assert_eq!(roles.next().unwrap(), &"role3");
2126 assert!(roles.next().is_none());
2127
2128 assert!(mi.item.block_style().is_none());
2129
2130 assert_eq!(
2131 mi.item.span(),
2132 Span {
2133 data: ".role1 .role2 .role3 ",
2134 line: 1,
2135 col: 1,
2136 offset: 0
2137 }
2138 );
2139
2140 assert_eq!(
2141 mi.after,
2142 Span {
2143 data: "",
2144 line: 1,
2145 col: 22,
2146 offset: 21
2147 }
2148 );
2149 }
2150
2151 #[test]
2152 fn via_named_attribute() {
2153 let p = Parser::default();
2154
2155 let mi = crate::attributes::Attrlist::parse(
2156 crate::Span::new("foo=bar,role=role1"),
2157 &p,
2158 AttrlistContext::Inline,
2159 )
2160 .unwrap_if_no_warnings();
2161
2162 assert_eq!(
2163 mi.item,
2164 Attrlist {
2165 attributes: &[
2166 ElementAttribute {
2167 name: Some("foo"),
2168 shorthand_items: &[],
2169 value: "bar"
2170 },
2171 ElementAttribute {
2172 name: Some("role"),
2173 shorthand_items: &[],
2174 value: "role1"
2175 },
2176 ],
2177 anchor: None,
2178 source: Span {
2179 data: "foo=bar,role=role1",
2180 line: 1,
2181 col: 1,
2182 offset: 0
2183 }
2184 }
2185 );
2186
2187 assert_eq!(
2188 mi.item.named_attribute("foo").unwrap(),
2189 ElementAttribute {
2190 name: Some("foo"),
2191 shorthand_items: &[],
2192 value: "bar"
2193 }
2194 );
2195
2196 assert_eq!(
2197 mi.item.named_attribute("role").unwrap(),
2198 ElementAttribute {
2199 name: Some("role"),
2200 shorthand_items: &[],
2201 value: "role1"
2202 }
2203 );
2204
2205 let roles = mi.item.roles();
2206 let mut roles = roles.iter();
2207 assert_eq!(roles.next().unwrap(), &"role1");
2208 assert!(roles.next().is_none());
2209
2210 assert!(mi.item.block_style().is_none());
2211
2212 assert_eq!(
2213 mi.after,
2214 Span {
2215 data: "",
2216 line: 1,
2217 col: 19,
2218 offset: 18
2219 }
2220 );
2221 }
2222
2223 #[test]
2224 fn multiple_roles_via_named_attribute() {
2225 let p = Parser::default();
2226
2227 let mi = crate::attributes::Attrlist::parse(
2228 crate::Span::new("foo=bar,role=role1 role2 role3 "),
2229 &p,
2230 AttrlistContext::Inline,
2231 )
2232 .unwrap_if_no_warnings();
2233
2234 assert_eq!(
2235 mi.item,
2236 Attrlist {
2237 attributes: &[
2238 ElementAttribute {
2239 name: Some("foo"),
2240 shorthand_items: &[],
2241 value: "bar"
2242 },
2243 ElementAttribute {
2244 name: Some("role"),
2245 shorthand_items: &[],
2246 value: "role1 role2 role3"
2247 },
2248 ],
2249 anchor: None,
2250 source: Span {
2251 data: "foo=bar,role=role1 role2 role3 ",
2252 line: 1,
2253 col: 1,
2254 offset: 0
2255 }
2256 }
2257 );
2258
2259 assert_eq!(
2260 mi.item.named_attribute("foo").unwrap(),
2261 ElementAttribute {
2262 name: Some("foo"),
2263 shorthand_items: &[],
2264 value: "bar"
2265 }
2266 );
2267
2268 assert_eq!(
2269 mi.item.named_attribute("role").unwrap(),
2270 ElementAttribute {
2271 name: Some("role"),
2272 shorthand_items: &[],
2273 value: "role1 role2 role3"
2274 }
2275 );
2276
2277 let roles = mi.item.roles();
2278 let mut roles = roles.iter();
2279 assert_eq!(roles.next().unwrap(), &"role1");
2280 assert_eq!(roles.next().unwrap(), &"role2");
2281 assert_eq!(roles.next().unwrap(), &"role3");
2282 assert!(roles.next().is_none());
2283
2284 assert!(mi.item.block_style().is_none());
2285
2286 assert_eq!(
2287 mi.after,
2288 Span {
2289 data: "",
2290 line: 1,
2291 col: 34,
2292 offset: 33
2293 }
2294 );
2295 }
2296
2297 #[test]
2298 fn shorthand_role_and_named_attribute_role() {
2299 let p = Parser::default();
2300
2301 let mi = crate::attributes::Attrlist::parse(
2302 crate::Span::new("#foo.sh1.sh2,role=na1 na2 na3 "),
2303 &p,
2304 AttrlistContext::Inline,
2305 )
2306 .unwrap_if_no_warnings();
2307
2308 assert_eq!(
2309 mi.item,
2310 Attrlist {
2311 attributes: &[
2312 ElementAttribute {
2313 name: None,
2314 shorthand_items: &["#foo", ".sh1", ".sh2"],
2315 value: "#foo.sh1.sh2"
2316 },
2317 ElementAttribute {
2318 name: Some("role"),
2319 shorthand_items: &[],
2320 value: "na1 na2 na3"
2321 },
2322 ],
2323 anchor: None,
2324 source: Span {
2325 data: "#foo.sh1.sh2,role=na1 na2 na3 ",
2326 line: 1,
2327 col: 1,
2328 offset: 0
2329 }
2330 }
2331 );
2332
2333 assert!(mi.item.named_attribute("foo").is_none());
2334
2335 assert_eq!(
2336 mi.item.named_attribute("role").unwrap(),
2337 ElementAttribute {
2338 name: Some("role"),
2339 shorthand_items: &[],
2340 value: "na1 na2 na3"
2341 }
2342 );
2343
2344 let roles = mi.item.roles();
2345 let mut roles = roles.iter();
2346 assert_eq!(roles.next().unwrap(), &"sh1");
2347 assert_eq!(roles.next().unwrap(), &"sh2");
2348 assert_eq!(roles.next().unwrap(), &"na1");
2349 assert_eq!(roles.next().unwrap(), &"na2");
2350 assert_eq!(roles.next().unwrap(), &"na3");
2351 assert!(roles.next().is_none());
2352
2353 assert!(mi.item.block_style().is_none());
2354
2355 assert_eq!(
2356 mi.after,
2357 Span {
2358 data: "",
2359 line: 1,
2360 col: 33,
2361 offset: 32
2362 }
2363 );
2364 }
2365
2366 #[test]
2367 fn shorthand_only_first_attribute() {
2368 let p = Parser::default();
2369
2370 let mi = crate::attributes::Attrlist::parse(
2371 crate::Span::new("foo,blah.rolename"),
2372 &p,
2373 AttrlistContext::Inline,
2374 )
2375 .unwrap_if_no_warnings();
2376
2377 assert_eq!(
2378 mi.item,
2379 Attrlist {
2380 attributes: &[
2381 ElementAttribute {
2382 name: None,
2383 shorthand_items: &["foo"],
2384 value: "foo"
2385 },
2386 ElementAttribute {
2387 name: None,
2388 shorthand_items: &[],
2389 value: "blah.rolename"
2390 },
2391 ],
2392 anchor: None,
2393 source: Span {
2394 data: "foo,blah.rolename",
2395 line: 1,
2396 col: 1,
2397 offset: 0
2398 }
2399 }
2400 );
2401
2402 let roles = mi.item.roles();
2403 assert_eq!(roles.iter().len(), 0);
2404
2405 assert_eq!(mi.item.block_style().unwrap(), "foo");
2406
2407 assert_eq!(
2408 mi.after,
2409 Span {
2410 data: "",
2411 line: 1,
2412 col: 18,
2413 offset: 17
2414 }
2415 );
2416 }
2417
2418 #[test]
2419 fn quoted_first_positional_becomes_verbatim_role() {
2420 let p = Parser::default();
2421
2422 let mi = crate::attributes::Attrlist::parse(
2427 crate::Span::new("'role'"),
2428 &p,
2429 AttrlistContext::Inline,
2430 )
2431 .unwrap_if_no_warnings();
2432
2433 assert!(mi.item.roles().is_empty());
2434 assert!(mi.item.block_style().is_none());
2435 assert_eq!(mi.item.quoted_text_fallback_role().unwrap(), "'role'");
2436
2437 let mi = crate::attributes::Attrlist::parse(
2440 crate::Span::new("'role',keep=dropped"),
2441 &p,
2442 AttrlistContext::Inline,
2443 )
2444 .unwrap_if_no_warnings();
2445
2446 assert_eq!(mi.item.quoted_text_fallback_role().unwrap(), "'role'");
2447
2448 let mi = crate::attributes::Attrlist::parse(
2453 crate::Span::new("'a,b'"),
2454 &p,
2455 AttrlistContext::Inline,
2456 )
2457 .unwrap_if_no_warnings();
2458
2459 assert_eq!(mi.item.quoted_text_fallback_role().unwrap(), "'a");
2460
2461 let mi = crate::attributes::Attrlist::parse(
2464 crate::Span::new("role"),
2465 &p,
2466 AttrlistContext::Inline,
2467 )
2468 .unwrap_if_no_warnings();
2469
2470 assert!(mi.item.quoted_text_fallback_role().is_none());
2471
2472 let mi = crate::attributes::Attrlist::parse(
2475 crate::Span::new("id=x"),
2476 &p,
2477 AttrlistContext::Inline,
2478 )
2479 .unwrap_if_no_warnings();
2480
2481 assert!(mi.item.quoted_text_fallback_role().is_none());
2482 }
2483 }
2484
2485 mod options {
2486 use crate::{attributes::AttrlistContext, tests::prelude::*};
2487
2488 #[test]
2489 fn via_shorthand_syntax() {
2490 let p = Parser::default();
2491
2492 let mi = crate::attributes::Attrlist::parse(
2493 crate::Span::new("%option"),
2494 &p,
2495 AttrlistContext::Inline,
2496 )
2497 .unwrap_if_no_warnings();
2498
2499 assert_eq!(
2500 mi.item,
2501 Attrlist {
2502 attributes: &[ElementAttribute {
2503 name: None,
2504 shorthand_items: &["%option"],
2505 value: "%option"
2506 }],
2507 anchor: None,
2508 source: Span {
2509 data: "%option",
2510 line: 1,
2511 col: 1,
2512 offset: 0
2513 }
2514 }
2515 );
2516
2517 assert!(mi.item.named_attribute("foo").is_none());
2518 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2519
2520 let options = mi.item.options();
2521 let mut options = options.iter();
2522 assert_eq!(options.next().unwrap(), &"option",);
2523 assert!(options.next().is_none());
2524
2525 assert!(mi.item.has_option("option"));
2526 assert!(!mi.item.has_option("option1"));
2527
2528 assert_eq!(
2529 mi.item.span(),
2530 Span {
2531 data: "%option",
2532 line: 1,
2533 col: 1,
2534 offset: 0
2535 }
2536 );
2537
2538 assert_eq!(
2539 mi.after,
2540 Span {
2541 data: "",
2542 line: 1,
2543 col: 8,
2544 offset: 7
2545 }
2546 );
2547 }
2548
2549 #[test]
2550 fn multiple_options_via_shorthand_syntax() {
2551 let p = Parser::default();
2552
2553 let mi = crate::attributes::Attrlist::parse(
2554 crate::Span::new("%option1%option2%option3"),
2555 &p,
2556 AttrlistContext::Inline,
2557 )
2558 .unwrap_if_no_warnings();
2559
2560 assert_eq!(
2561 mi.item,
2562 Attrlist {
2563 attributes: &[ElementAttribute {
2564 name: None,
2565 shorthand_items: &["%option1", "%option2", "%option3",],
2566 value: "%option1%option2%option3"
2567 }],
2568 anchor: None,
2569 source: Span {
2570 data: "%option1%option2%option3",
2571 line: 1,
2572 col: 1,
2573 offset: 0
2574 }
2575 }
2576 );
2577
2578 assert!(mi.item.named_attribute("foo").is_none());
2579 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2580
2581 let options = mi.item.options();
2582 let mut options = options.iter();
2583 assert_eq!(options.next().unwrap(), &"option1");
2584 assert_eq!(options.next().unwrap(), &"option2");
2585 assert_eq!(options.next().unwrap(), &"option3");
2586 assert!(options.next().is_none());
2587
2588 assert!(mi.item.has_option("option1"));
2589 assert!(mi.item.has_option("option2"));
2590 assert!(mi.item.has_option("option3"));
2591 assert!(!mi.item.has_option("option4"));
2592
2593 assert_eq!(
2594 mi.item.span(),
2595 Span {
2596 data: "%option1%option2%option3",
2597 line: 1,
2598 col: 1,
2599 offset: 0
2600 }
2601 );
2602
2603 assert_eq!(
2604 mi.after,
2605 Span {
2606 data: "",
2607 line: 1,
2608 col: 25,
2609 offset: 24
2610 }
2611 );
2612 }
2613
2614 #[test]
2615 fn via_options_attribute() {
2616 let p = Parser::default();
2617
2618 let mi = crate::attributes::Attrlist::parse(
2619 crate::Span::new("foo=bar,options=option1"),
2620 &p,
2621 AttrlistContext::Inline,
2622 )
2623 .unwrap_if_no_warnings();
2624
2625 assert_eq!(
2626 mi.item,
2627 Attrlist {
2628 attributes: &[
2629 ElementAttribute {
2630 name: Some("foo"),
2631 shorthand_items: &[],
2632 value: "bar"
2633 },
2634 ElementAttribute {
2635 name: Some("options"),
2636 shorthand_items: &[],
2637 value: "option1"
2638 },
2639 ],
2640 anchor: None,
2641 source: Span {
2642 data: "foo=bar,options=option1",
2643 line: 1,
2644 col: 1,
2645 offset: 0
2646 }
2647 }
2648 );
2649
2650 assert_eq!(
2651 mi.item.named_attribute("foo").unwrap(),
2652 ElementAttribute {
2653 name: Some("foo"),
2654 shorthand_items: &[],
2655 value: "bar"
2656 }
2657 );
2658
2659 assert_eq!(
2660 mi.item.named_attribute("options").unwrap(),
2661 ElementAttribute {
2662 name: Some("options"),
2663 shorthand_items: &[],
2664 value: "option1"
2665 }
2666 );
2667
2668 let options = mi.item.options();
2669 let mut options = options.iter();
2670 assert_eq!(options.next().unwrap(), &"option1");
2671 assert!(options.next().is_none());
2672
2673 assert!(mi.item.has_option("option1"));
2674 assert!(!mi.item.has_option("option2"));
2675
2676 assert_eq!(
2677 mi.after,
2678 Span {
2679 data: "",
2680 line: 1,
2681 col: 24,
2682 offset: 23
2683 }
2684 );
2685 }
2686
2687 #[test]
2688 fn via_opts_attribute() {
2689 let p = Parser::default();
2690
2691 let mi = crate::attributes::Attrlist::parse(
2692 crate::Span::new("foo=bar,opts=option1"),
2693 &p,
2694 AttrlistContext::Inline,
2695 )
2696 .unwrap_if_no_warnings();
2697
2698 assert_eq!(
2699 mi.item,
2700 Attrlist {
2701 attributes: &[
2702 ElementAttribute {
2703 name: Some("foo"),
2704 shorthand_items: &[],
2705 value: "bar"
2706 },
2707 ElementAttribute {
2708 name: Some("opts"),
2709 shorthand_items: &[],
2710 value: "option1"
2711 },
2712 ],
2713 anchor: None,
2714 source: Span {
2715 data: "foo=bar,opts=option1",
2716 line: 1,
2717 col: 1,
2718 offset: 0
2719 }
2720 }
2721 );
2722
2723 assert_eq!(
2724 mi.item.named_attribute("foo").unwrap(),
2725 ElementAttribute {
2726 name: Some("foo"),
2727 shorthand_items: &[],
2728 value: "bar"
2729 }
2730 );
2731
2732 assert_eq!(
2733 mi.item.named_attribute("opts").unwrap(),
2734 ElementAttribute {
2735 name: Some("opts"),
2736 shorthand_items: &[],
2737 value: "option1"
2738 }
2739 );
2740
2741 let options = mi.item.options();
2742 let mut options = options.iter();
2743 assert_eq!(options.next().unwrap(), &"option1");
2744 assert!(options.next().is_none());
2745
2746 assert!(!mi.item.has_option("option"));
2747 assert!(mi.item.has_option("option1"));
2748 assert!(!mi.item.has_option("option2"));
2749
2750 assert_eq!(
2751 mi.after,
2752 Span {
2753 data: "",
2754 line: 1,
2755 col: 21,
2756 offset: 20
2757 }
2758 );
2759 }
2760
2761 #[test]
2762 fn multiple_options_via_named_attribute() {
2763 let p = Parser::default();
2764
2765 let mi = crate::attributes::Attrlist::parse(
2766 crate::Span::new("foo=bar,options=\"option1,option2,option3\""),
2767 &p,
2768 AttrlistContext::Inline,
2769 )
2770 .unwrap_if_no_warnings();
2771
2772 assert_eq!(
2773 mi.item,
2774 Attrlist {
2775 attributes: &[
2776 ElementAttribute {
2777 name: Some("foo"),
2778 shorthand_items: &[],
2779 value: "bar"
2780 },
2781 ElementAttribute {
2782 name: Some("options"),
2783 shorthand_items: &[],
2784 value: "option1,option2,option3"
2785 },
2786 ],
2787 anchor: None,
2788 source: Span {
2789 data: "foo=bar,options=\"option1,option2,option3\"",
2790 line: 1,
2791 col: 1,
2792 offset: 0
2793 }
2794 }
2795 );
2796
2797 assert_eq!(
2798 mi.item.named_attribute("foo").unwrap(),
2799 ElementAttribute {
2800 name: Some("foo"),
2801 shorthand_items: &[],
2802 value: "bar"
2803 }
2804 );
2805
2806 assert_eq!(
2807 mi.item.named_attribute("options").unwrap(),
2808 ElementAttribute {
2809 name: Some("options"),
2810 shorthand_items: &[],
2811 value: "option1,option2,option3"
2812 }
2813 );
2814
2815 let options = mi.item.options();
2816 let mut options = options.iter();
2817 assert_eq!(options.next().unwrap(), &"option1");
2818 assert_eq!(options.next().unwrap(), &"option2");
2819 assert_eq!(options.next().unwrap(), &"option3");
2820 assert!(options.next().is_none());
2821
2822 assert!(mi.item.has_option("option1"));
2823 assert!(mi.item.has_option("option2"));
2824 assert!(mi.item.has_option("option3"));
2825 assert!(!mi.item.has_option("option4"));
2826
2827 assert_eq!(
2828 mi.after,
2829 Span {
2830 data: "",
2831 line: 1,
2832 col: 42,
2833 offset: 41
2834 }
2835 );
2836 }
2837
2838 #[test]
2839 fn shorthand_option_and_named_attribute_option() {
2840 let p = Parser::default();
2841
2842 let mi = crate::attributes::Attrlist::parse(
2843 crate::Span::new("#foo%sh1%sh2,options=\"na1,na2,na3\""),
2844 &p,
2845 AttrlistContext::Inline,
2846 )
2847 .unwrap_if_no_warnings();
2848
2849 assert_eq!(
2850 mi.item,
2851 Attrlist {
2852 attributes: &[
2853 ElementAttribute {
2854 name: None,
2855 shorthand_items: &["#foo", "%sh1", "%sh2"],
2856 value: "#foo%sh1%sh2"
2857 },
2858 ElementAttribute {
2859 name: Some("options"),
2860 shorthand_items: &[],
2861 value: "na1,na2,na3"
2862 },
2863 ],
2864 anchor: None,
2865 source: Span {
2866 data: "#foo%sh1%sh2,options=\"na1,na2,na3\"",
2867 line: 1,
2868 col: 1,
2869 offset: 0
2870 }
2871 }
2872 );
2873
2874 assert!(mi.item.named_attribute("foo").is_none(),);
2875
2876 assert_eq!(
2877 mi.item.named_attribute("options").unwrap(),
2878 ElementAttribute {
2879 name: Some("options"),
2880 shorthand_items: &[],
2881 value: "na1,na2,na3"
2882 }
2883 );
2884
2885 let options = mi.item.options();
2886 let mut options = options.iter();
2887 assert_eq!(options.next().unwrap(), &"sh1");
2888 assert_eq!(options.next().unwrap(), &"sh2");
2889 assert_eq!(options.next().unwrap(), &"na1");
2890 assert_eq!(options.next().unwrap(), &"na2");
2891 assert_eq!(options.next().unwrap(), &"na3");
2892 assert!(options.next().is_none(),);
2893
2894 assert!(mi.item.has_option("sh1"));
2895 assert!(mi.item.has_option("sh2"));
2896 assert!(!mi.item.has_option("sh3"));
2897 assert!(mi.item.has_option("na1"));
2898 assert!(mi.item.has_option("na2"));
2899 assert!(mi.item.has_option("na3"));
2900 assert!(!mi.item.has_option("na4"));
2901
2902 assert_eq!(
2903 mi.after,
2904 Span {
2905 data: "",
2906 line: 1,
2907 col: 35,
2908 offset: 34
2909 }
2910 );
2911 }
2912
2913 #[test]
2914 fn shorthand_only_first_attribute() {
2915 let p = Parser::default();
2916
2917 let mi = crate::attributes::Attrlist::parse(
2918 crate::Span::new("foo,blah%option"),
2919 &p,
2920 AttrlistContext::Inline,
2921 )
2922 .unwrap_if_no_warnings();
2923
2924 assert_eq!(
2925 mi.item,
2926 Attrlist {
2927 attributes: &[
2928 ElementAttribute {
2929 name: None,
2930 shorthand_items: &["foo"],
2931 value: "foo"
2932 },
2933 ElementAttribute {
2934 name: None,
2935 shorthand_items: &[],
2936 value: "blah%option"
2937 },
2938 ],
2939 anchor: None,
2940 source: Span {
2941 data: "foo,blah%option",
2942 line: 1,
2943 col: 1,
2944 offset: 0
2945 }
2946 }
2947 );
2948
2949 let options = mi.item.options();
2950 assert_eq!(options.iter().len(), 0);
2951
2952 assert!(!mi.item.has_option("option"));
2953
2954 assert_eq!(
2955 mi.after,
2956 Span {
2957 data: "",
2958 line: 1,
2959 col: 16,
2960 offset: 15
2961 }
2962 );
2963 }
2964 }
2965
2966 #[test]
2967 fn block_style() {
2968 let p = Parser::default();
2969
2970 let mi = crate::attributes::Attrlist::parse(
2971 crate::Span::new("blah#goals"),
2972 &p,
2973 AttrlistContext::Inline,
2974 )
2975 .unwrap_if_no_warnings();
2976
2977 let attrlist = mi.item;
2978 assert_eq!(attrlist.block_style().unwrap(), "blah");
2979 }
2980
2981 #[test]
2982 fn err_double_comma() {
2983 let p = Parser::default();
2984
2985 let maw = crate::attributes::Attrlist::parse(
2986 crate::Span::new("alt=Sunset,width=300,,height=400"),
2987 &p,
2988 AttrlistContext::Inline,
2989 );
2990
2991 let mi = maw.item.clone();
2992
2993 assert_eq!(
2994 mi.item,
2995 Attrlist {
2996 attributes: &[
2997 ElementAttribute {
2998 name: Some("alt"),
2999 shorthand_items: &[],
3000 value: "Sunset"
3001 },
3002 ElementAttribute {
3003 name: Some("width"),
3004 shorthand_items: &[],
3005 value: "300"
3006 },
3007 ElementAttribute {
3008 name: Some("height"),
3009 shorthand_items: &[],
3010 value: "400"
3011 },
3012 ],
3013 anchor: None,
3014 source: Span {
3015 data: "alt=Sunset,width=300,,height=400",
3016 line: 1,
3017 col: 1,
3018 offset: 0,
3019 }
3020 }
3021 );
3022
3023 assert_eq!(
3024 mi.after,
3025 Span {
3026 data: "",
3027 line: 1,
3028 col: 33,
3029 offset: 32,
3030 }
3031 );
3032
3033 assert_eq!(
3034 maw.warnings,
3035 vec![Warning {
3036 source: Span {
3037 data: "alt=Sunset,width=300,,height=400",
3038 line: 1,
3039 col: 1,
3040 offset: 0,
3041 },
3042 warning: WarningType::EmptyAttributeValue,
3043 }]
3044 );
3045 }
3046
3047 #[test]
3048 fn applies_attribute_substitution_before_parsing() {
3049 let p = Parser::default().with_intrinsic_attribute(
3050 "sunset_dimensions",
3051 "300,400",
3052 ModificationContext::Anywhere,
3053 );
3054
3055 let mi = crate::attributes::Attrlist::parse(
3056 crate::Span::new("Sunset,{sunset_dimensions}"),
3057 &p,
3058 AttrlistContext::Inline,
3059 )
3060 .unwrap_if_no_warnings();
3061
3062 assert_eq!(
3063 mi.item,
3064 Attrlist {
3065 attributes: &[
3066 ElementAttribute {
3067 name: None,
3068 shorthand_items: &["Sunset"],
3069 value: "Sunset"
3070 },
3071 ElementAttribute {
3072 name: None,
3073 shorthand_items: &[],
3074 value: "300"
3075 },
3076 ElementAttribute {
3077 name: None,
3078 shorthand_items: &[],
3079 value: "400"
3080 }
3081 ],
3082 anchor: None,
3083 source: Span {
3084 data: "Sunset,{sunset_dimensions}",
3085 line: 1,
3086 col: 1,
3087 offset: 0
3088 }
3089 }
3090 );
3091
3092 assert!(mi.item.named_attribute("foo").is_none());
3093 assert!(mi.item.nth_attribute(0).is_none());
3094 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
3095
3096 assert!(mi.item.id().is_none());
3097 assert!(mi.item.roles().is_empty());
3098 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
3099
3100 assert_eq!(
3101 mi.item.nth_attribute(1).unwrap(),
3102 ElementAttribute {
3103 name: None,
3104 shorthand_items: &["Sunset"],
3105 value: "Sunset"
3106 }
3107 );
3108
3109 assert_eq!(
3110 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
3111 ElementAttribute {
3112 name: None,
3113 shorthand_items: &["Sunset"],
3114 value: "Sunset"
3115 }
3116 );
3117
3118 assert_eq!(
3119 mi.item.nth_attribute(2).unwrap(),
3120 ElementAttribute {
3121 name: None,
3122 shorthand_items: &[],
3123 value: "300"
3124 }
3125 );
3126
3127 assert_eq!(
3128 mi.item.named_or_positional_attribute("width", 2).unwrap(),
3129 ElementAttribute {
3130 name: None,
3131 shorthand_items: &[],
3132 value: "300"
3133 }
3134 );
3135
3136 assert_eq!(
3137 mi.item.nth_attribute(3).unwrap(),
3138 ElementAttribute {
3139 name: None,
3140 shorthand_items: &[],
3141 value: "400"
3142 }
3143 );
3144
3145 assert_eq!(
3146 mi.item.named_or_positional_attribute("height", 3).unwrap(),
3147 ElementAttribute {
3148 name: None,
3149 shorthand_items: &[],
3150 value: "400"
3151 }
3152 );
3153
3154 assert!(mi.item.nth_attribute(4).is_none());
3155 assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
3156 assert!(mi.item.nth_attribute(42).is_none());
3157
3158 assert_eq!(
3159 mi.item.span(),
3160 Span {
3161 data: "Sunset,{sunset_dimensions}",
3162 line: 1,
3163 col: 1,
3164 offset: 0,
3165 }
3166 );
3167
3168 assert_eq!(
3169 mi.after,
3170 Span {
3171 data: "",
3172 line: 1,
3173 col: 27,
3174 offset: 26,
3175 }
3176 );
3177 }
3178
3179 #[test]
3180 fn ignores_unknown_attribute_when_applying_attribution_substitution() {
3181 let p = Parser::default().with_intrinsic_attribute(
3182 "sunset_dimensions",
3183 "300,400",
3184 ModificationContext::Anywhere,
3185 );
3186
3187 let mi = crate::attributes::Attrlist::parse(
3188 crate::Span::new("Sunset,{not_sunset_dimensions}"),
3189 &p,
3190 AttrlistContext::Inline,
3191 )
3192 .unwrap_if_no_warnings();
3193
3194 assert_eq!(
3195 mi.item,
3196 Attrlist {
3197 attributes: &[
3198 ElementAttribute {
3199 name: None,
3200 shorthand_items: &["Sunset"],
3201 value: "Sunset"
3202 },
3203 ElementAttribute {
3204 name: None,
3205 shorthand_items: &[],
3206 value: "{not_sunset_dimensions}"
3207 },
3208 ],
3209 anchor: None,
3210 source: Span {
3211 data: "Sunset,{not_sunset_dimensions}",
3212 line: 1,
3213 col: 1,
3214 offset: 0
3215 }
3216 }
3217 );
3218
3219 assert!(mi.item.named_attribute("foo").is_none());
3220 assert!(mi.item.nth_attribute(0).is_none());
3221 assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
3222
3223 assert!(mi.item.id().is_none());
3224 assert!(mi.item.roles().is_empty());
3225 assert_eq!(mi.item.block_style().unwrap(), "Sunset");
3226
3227 assert_eq!(
3228 mi.item.nth_attribute(1).unwrap(),
3229 ElementAttribute {
3230 name: None,
3231 shorthand_items: &["Sunset"],
3232 value: "Sunset"
3233 }
3234 );
3235
3236 assert_eq!(
3237 mi.item.named_or_positional_attribute("alt", 1).unwrap(),
3238 ElementAttribute {
3239 name: None,
3240 shorthand_items: &["Sunset"],
3241 value: "Sunset"
3242 }
3243 );
3244
3245 assert_eq!(
3246 mi.item.nth_attribute(2).unwrap(),
3247 ElementAttribute {
3248 name: None,
3249 shorthand_items: &[],
3250 value: "{not_sunset_dimensions}"
3251 }
3252 );
3253
3254 assert_eq!(
3255 mi.item.named_or_positional_attribute("width", 2).unwrap(),
3256 ElementAttribute {
3257 name: None,
3258 shorthand_items: &[],
3259 value: "{not_sunset_dimensions}"
3260 }
3261 );
3262
3263 assert!(mi.item.nth_attribute(3).is_none());
3264 assert!(mi.item.named_or_positional_attribute("height", 3).is_none());
3265 assert!(mi.item.nth_attribute(42).is_none());
3266
3267 assert_eq!(
3268 mi.item.span(),
3269 Span {
3270 data: "Sunset,{not_sunset_dimensions}",
3271 line: 1,
3272 col: 1,
3273 offset: 0,
3274 }
3275 );
3276
3277 assert_eq!(
3278 mi.after,
3279 Span {
3280 data: "",
3281 line: 1,
3282 col: 31,
3283 offset: 30,
3284 }
3285 );
3286 }
3287
3288 #[test]
3289 fn impl_debug() {
3290 let p = Parser::default();
3291
3292 let mi = crate::attributes::Attrlist::parse(
3293 crate::Span::new("Sunset,300,400"),
3294 &p,
3295 AttrlistContext::Inline,
3296 )
3297 .unwrap_if_no_warnings();
3298
3299 let attrlist = mi.item;
3300
3301 assert_eq!(
3302 format!("{attrlist:#?}"),
3303 r#"Attrlist {
3304 attributes: &[
3305 ElementAttribute {
3306 name: None,
3307 value: "Sunset",
3308 shorthand_item_indices: [
3309 0,
3310 ],
3311 },
3312 ElementAttribute {
3313 name: None,
3314 value: "300",
3315 shorthand_item_indices: [],
3316 },
3317 ElementAttribute {
3318 name: None,
3319 value: "400",
3320 shorthand_item_indices: [],
3321 },
3322 ],
3323 anchor: None,
3324 source: Span {
3325 data: "Sunset,300,400",
3326 line: 1,
3327 col: 1,
3328 offset: 0,
3329 },
3330}"#
3331 );
3332 }
3333}