1use crate::{
2 Parser, Span,
3 attributes::AttrlistContext,
4 content::{Content, SubstitutionGroup},
5 span::MatchedItem,
6 strings::CowStr,
7 warnings::WarningType,
8};
9
10#[derive(Clone, Eq, PartialEq)]
18pub struct ElementAttribute<'src> {
19 name: Option<CowStr<'src>>,
20 value: CowStr<'src>,
21 shorthand_item_indices: Vec<usize>,
22
23 positional_index: Option<usize>,
33
34 value_is_quoted: bool,
42
43 value_is_substituted: bool,
50}
51
52impl std::fmt::Debug for ElementAttribute<'_> {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("ElementAttribute")
58 .field("name", &self.name)
59 .field("value", &self.value)
60 .field("shorthand_item_indices", &self.shorthand_item_indices)
61 .finish()
62 }
63}
64
65impl<'src> ElementAttribute<'src> {
66 pub(crate) fn parse(
67 source_text: &CowStr<'src>,
68 start_index: usize,
69 parser: &Parser,
70 mut parse_shorthand: ParseShorthand,
71 attrlist_context: AttrlistContext,
72 ) -> (Self, usize, Vec<WarningType>) {
73 let mut warnings: Vec<WarningType> = vec![];
74
75 let (name, value, shorthand_item_indices, value_is_quoted, value_is_substituted, offset) = {
76 let mut source = Span::new(source_text.as_ref());
77 source = source.discard(start_index);
78
79 source = source.take_whitespace_with_newline().after;
85
86 let (name, after): (Option<Span<'_>>, Span) = match source.take_attr_name() {
87 Some(name) => {
88 let space = name.after.take_whitespace_with_newline();
89 match space.after.take_prefix("=") {
90 Some(equals) => {
91 let space = equals.after.take_whitespace_with_newline();
92 (Some(name.item), space.after)
98 }
99 None => (None, source),
100 }
101 }
102 None => (None, source),
103 };
104
105 let after = after.take_whitespace_with_newline().after;
106 let first_char = after.data().chars().next();
107
108 let mut value_is_quoted = false;
115 let value = match first_char {
116 Some('\'') | Some('"') => match after.take_quoted_string() {
117 Some(v) => {
118 parse_shorthand = ParseShorthand(false);
119 value_is_quoted = true;
120 v
121 }
122 None => {
123 warnings.push(WarningType::AttributeValueMissingTerminatingQuote);
124 after.take_while(|c| c != ',').trim_item_trailing_spaces()
125 }
126 },
127 _ => after.take_while(|c| c != ',').trim_item_trailing_spaces(),
128 };
129
130 let after = value.after;
131 let mut value = cowstr_from_source_and_span(source_text, &value.item);
132 let mut value_is_substituted = false;
133
134 if value_is_quoted && let Some(first) = first_char {
135 let escaped_quote = format!("\\{first}");
136 let mut new_value = value.replace(&escaped_quote, &first.to_string());
137
138 if first == '\'' && attrlist_context == AttrlistContext::Block {
139 value_is_substituted = true;
142
143 let span = Span::new(&new_value);
144 let mut content = Content::from(span);
145 SubstitutionGroup::Normal.apply(&mut content, parser, None);
146
147 if content.rendered.as_ref() != new_value {
148 new_value = content.rendered.to_string();
149 }
150 }
151
152 if new_value != *value {
153 value = CowStr::from(new_value);
154 }
155 }
156
157 let shorthand_item_indices = if name.is_none() && parse_shorthand.0 {
158 parse_shorthand_items(&value, &mut warnings)
159 } else {
160 vec![]
161 };
162
163 let name = name.map(|name| cowstr_from_source_and_span(source_text, &name));
164
165 (
166 name,
167 value,
168 shorthand_item_indices,
169 value_is_quoted,
170 value_is_substituted,
171 after.byte_offset(),
172 )
173 };
174
175 (
176 Self {
177 name,
178 value,
179 shorthand_item_indices,
180 positional_index: None,
181 value_is_quoted,
182 value_is_substituted,
183 },
184 offset,
185 warnings,
186 )
187 }
188
189 pub(crate) fn synthesized_source_style() -> Self {
194 const SOURCE: &str = "source";
195 let mut warnings: Vec<WarningType> = vec![];
196 let shorthand_item_indices = parse_shorthand_items(SOURCE, &mut warnings);
197
198 debug_assert!(
202 warnings.is_empty(),
203 "synthesizing the `source` block style should not produce warnings, got: {warnings:?}"
204 );
205
206 Self {
207 name: None,
208 value: CowStr::from(SOURCE),
209 shorthand_item_indices,
210 positional_index: Some(1),
211 value_is_quoted: false,
212 value_is_substituted: false,
213 }
214 }
215
216 pub(crate) fn positional_from_span(span: Span<'src>) -> Self {
221 Self {
222 name: None,
223 value: CowStr::from(span.data()),
224 shorthand_item_indices: vec![],
225 positional_index: Some(2),
226 value_is_quoted: false,
227 value_is_substituted: false,
228 }
229 }
230
231 pub fn name(&'src self) -> Option<&'src str> {
233 self.name_str()
234 }
235
236 pub(crate) fn name_str(&self) -> Option<&str> {
242 self.name.as_deref()
243 }
244
245 pub fn shorthand_items(&'src self) -> Vec<&'src str> {
251 self.shorthand_items_internal()
252 }
253
254 fn shorthand_items_internal(&self) -> Vec<&str> {
257 let mut result = vec![];
258 let value = self.value.as_ref();
259
260 let mut iter = self.shorthand_item_indices.iter().peekable();
261
262 while let Some(curr) = iter.next() {
263 let mut next_item = if let Some(next) = iter.peek() {
264 &value[*curr..**next]
265 } else {
266 &value[*curr..]
267 };
268
269 if next_item == "#" || next_item == "." || next_item == "%" {
270 continue;
271 }
272
273 next_item = next_item.trim_end();
274
275 if !next_item.is_empty() {
276 result.push(next_item);
277 }
278 }
279
280 result
281 }
282
283 pub fn block_style(&'src self) -> Option<&'src str> {
285 self.block_style_internal()
286 }
287
288 fn block_style_internal(&self) -> Option<&str> {
291 self.shorthand_items_internal()
292 .first()
293 .filter(|v| !v.chars().any(is_shorthand_delimiter))
294 .cloned()
295 }
296
297 pub fn id(&'src self) -> Option<&'src str> {
331 self.id_internal()
332 }
333
334 fn id_internal(&self) -> Option<&str> {
336 self.shorthand_items_internal()
337 .into_iter()
338 .find(|v| v.starts_with('#'))
339 .map(|v| &v[1..])
340 }
341
342 pub fn roles(&'src self) -> Vec<&'src str> {
364 self.roles_internal()
365 }
366
367 fn roles_internal(&self) -> Vec<&str> {
370 self.shorthand_items_internal()
371 .into_iter()
372 .filter(|span| span.starts_with('.'))
373 .map(|span| &span[1..])
374 .collect()
375 }
376
377 pub fn options(&'src self) -> Vec<&'src str> {
442 self.options_internal()
443 }
444
445 fn options_internal(&self) -> Vec<&str> {
448 self.shorthand_items_internal()
449 .into_iter()
450 .filter(|v| v.starts_with('%'))
451 .map(|v| &v[1..])
452 .collect()
453 }
454
455 pub(crate) fn merge_block_style_shorthand(earlier: &Self, later: &Self) -> Self {
466 let block_style = later
467 .block_style_internal()
468 .or(earlier.block_style_internal());
469 let id = later.id_internal().or(earlier.id_internal());
470
471 let mut roles = earlier.roles_internal();
472 roles.extend(later.roles_internal());
473
474 let mut options = earlier.options_internal();
475 options.extend(later.options_internal());
476
477 let mut value = String::new();
478 if let Some(block_style) = block_style {
479 value.push_str(block_style);
480 }
481 if let Some(id) = id {
482 value.push('#');
483 value.push_str(id);
484 }
485 for role in &roles {
486 value.push('.');
487 value.push_str(role);
488 }
489 for option in &options {
490 value.push('%');
491 value.push_str(option);
492 }
493
494 let mut warnings: Vec<WarningType> = vec![];
502 let shorthand_item_indices = if value.is_empty() {
503 vec![]
504 } else {
505 parse_shorthand_items(&value, &mut warnings)
506 };
507
508 debug_assert!(
509 warnings.is_empty(),
510 "merging block-style shorthand should not produce warnings, got: {warnings:?}"
511 );
512
513 Self {
514 name: None,
515 value: CowStr::from(value),
516 shorthand_item_indices,
517 positional_index: earlier.positional_index,
520 value_is_quoted: false,
521 value_is_substituted: false,
522 }
523 }
524
525 pub(crate) fn positional_index(&self) -> Option<usize> {
530 self.positional_index
531 }
532
533 pub(crate) fn set_positional_index(&mut self, index: usize) {
537 self.positional_index = Some(index);
538 }
539
540 pub(crate) fn value_is_quoted(&self) -> bool {
544 self.value_is_quoted
545 }
546
547 pub fn value(&'src self) -> &'src str {
552 self.value.as_ref()
553 }
554
555 pub(crate) fn value_str(&self) -> &str {
561 self.value.as_ref()
562 }
563
564 pub(crate) fn value_is_substituted(&self) -> bool {
570 self.value_is_substituted
571 }
572}
573
574fn parse_shorthand_items(source: &str, warnings: &mut Vec<WarningType>) -> Vec<usize> {
575 let mut shorthand_item_indices: Vec<usize> = vec![];
576 let mut span = Span::new(source);
577
578 if let Some(block_style_pr) = span.split_at_match_non_empty(is_shorthand_delimiter) {
580 shorthand_item_indices.push(block_style_pr.item.discard_whitespace().byte_offset());
581
582 span = block_style_pr.after;
583 }
584
585 while !span.is_empty() {
586 let after_delimiter = span.discard(1);
588
589 match after_delimiter.position(is_shorthand_delimiter) {
590 None => {
591 if after_delimiter.is_empty() {
592 warnings.push(WarningType::EmptyShorthandItem);
593 shorthand_item_indices.push(span.byte_offset());
594 span = after_delimiter;
595 } else {
596 shorthand_item_indices.push(span.byte_offset());
597 span = span.discard_all();
598 }
599 }
600
601 Some(0) => {
602 shorthand_item_indices.push(span.byte_offset());
603 warnings.push(WarningType::EmptyShorthandItem);
604 span = after_delimiter;
605 }
606
607 Some(index) => {
608 let mi: MatchedItem<Span> = span.into_parse_result(index + 1);
609 shorthand_item_indices.push(span.byte_offset());
610 span = mi.after;
611 }
612 }
613 }
614
615 shorthand_item_indices
616}
617
618fn is_shorthand_delimiter(c: char) -> bool {
619 c == '#' || c == '%' || c == '.'
620}
621
622#[derive(Clone, Debug)]
623pub(crate) struct ParseShorthand(pub bool);
624
625fn cowstr_from_source_and_span<'src>(source: &CowStr<'src>, span: &Span<'_>) -> CowStr<'src> {
626 if let CowStr::Borrowed(source) = source {
627 let borrowed: Span<'src> = Span::new(source)
628 .discard(span.byte_offset())
629 .slice_to(..span.len());
630
631 CowStr::Borrowed(borrowed.data())
632 } else {
633 CowStr::from(span.data().to_string())
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 #![allow(clippy::unwrap_used)]
640
641 use crate::{
642 attributes::{AttrlistContext, element_attribute::ParseShorthand},
643 strings::CowStr,
644 tests::prelude::*,
645 };
646
647 #[test]
648 fn impl_clone() {
649 let p = Parser::default();
651
652 let b1 = crate::attributes::ElementAttribute::parse(
653 &CowStr::from("abc"),
654 0,
655 &p,
656 ParseShorthand(false),
657 AttrlistContext::Inline,
658 )
659 .0;
660
661 let b2 = b1.clone();
662
663 assert_eq!(b1, b2);
664 }
665
666 #[test]
667 fn empty_source() {
668 let p = Parser::default();
669
670 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
671 &CowStr::from(""),
672 0,
673 &p,
674 ParseShorthand(false),
675 AttrlistContext::Inline,
676 );
677
678 assert!(warning_types.is_empty());
679
680 assert_eq!(
681 element_attr,
682 ElementAttribute {
683 name: None,
684 shorthand_items: &[],
685 value: "",
686 }
687 );
688
689 assert!(element_attr.name().is_none());
690 assert!(element_attr.block_style().is_none());
691 assert!(element_attr.id().is_none());
692 assert!(element_attr.roles().is_empty());
693 assert!(element_attr.options().is_empty());
694
695 assert_eq!(offset, 0);
696 }
697
698 #[test]
699 fn only_spaces() {
700 let p = Parser::default();
701
702 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
703 &CowStr::from(" "),
704 0,
705 &p,
706 ParseShorthand(false),
707 AttrlistContext::Inline,
708 );
709
710 assert!(warning_types.is_empty());
711
712 assert_eq!(
713 element_attr,
714 ElementAttribute {
715 name: None,
716 shorthand_items: &[],
717 value: "",
718 }
719 );
720
721 assert!(element_attr.name().is_none());
722 assert!(element_attr.block_style().is_none());
723 assert!(element_attr.id().is_none());
724 assert!(element_attr.roles().is_empty());
725 assert!(element_attr.options().is_empty());
726
727 assert_eq!(offset, 3);
728 }
729
730 #[test]
731 fn unquoted_and_unnamed_value() {
732 let p = Parser::default();
733
734 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
735 &CowStr::from("abc"),
736 0,
737 &p,
738 ParseShorthand(false),
739 AttrlistContext::Inline,
740 );
741
742 assert!(warning_types.is_empty());
743
744 assert_eq!(
745 element_attr,
746 ElementAttribute {
747 name: None,
748 shorthand_items: &[],
749 value: "abc",
750 }
751 );
752
753 assert!(element_attr.name().is_none());
754 assert!(element_attr.block_style().is_none());
755 assert!(element_attr.id().is_none());
756 assert!(element_attr.roles().is_empty());
757 assert!(element_attr.options().is_empty());
758
759 assert_eq!(offset, 3);
760 }
761
762 #[test]
763 fn unquoted_stops_at_comma() {
764 let p = Parser::default();
765
766 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
767 &CowStr::from("abc,def"),
768 0,
769 &p,
770 ParseShorthand(false),
771 AttrlistContext::Inline,
772 );
773
774 assert!(warning_types.is_empty());
775
776 assert_eq!(
777 element_attr,
778 ElementAttribute {
779 name: None,
780 shorthand_items: &[],
781 value: "abc",
782 }
783 );
784
785 assert!(element_attr.name().is_none());
786 assert!(element_attr.block_style().is_none());
787 assert!(element_attr.id().is_none());
788 assert!(element_attr.roles().is_empty());
789 assert!(element_attr.options().is_empty());
790
791 assert_eq!(offset, 3);
792 }
793
794 mod quoted_string {
795 use crate::{
796 attributes::{AttrlistContext, element_attribute::ParseShorthand},
797 strings::CowStr,
798 tests::prelude::*,
799 };
800
801 #[test]
802 fn err_unterminated_double_quote() {
803 let p = Parser::default();
804
805 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
806 &CowStr::from("\"xyz"),
807 0,
808 &p,
809 ParseShorthand(false),
810 AttrlistContext::Inline,
811 );
812
813 assert_eq!(
814 element_attr,
815 ElementAttribute {
816 name: None,
817 shorthand_items: &[],
818 value: "\"xyz"
819 }
820 );
821
822 assert!(element_attr.name().is_none());
823 assert!(element_attr.block_style().is_none());
824 assert!(element_attr.id().is_none());
825 assert!(element_attr.roles().is_empty());
826 assert!(element_attr.options().is_empty());
827
828 assert_eq!(offset, 4);
829
830 assert_eq!(
831 warning_types,
832 vec![WarningType::AttributeValueMissingTerminatingQuote]
833 );
834 }
835
836 #[test]
837 fn err_unterminated_double_quote_ends_at_comma() {
838 let p = Parser::default();
839
840 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
841 &CowStr::from("\"xyz,abc"),
842 0,
843 &p,
844 ParseShorthand(false),
845 AttrlistContext::Inline,
846 );
847
848 assert_eq!(
849 element_attr,
850 ElementAttribute {
851 name: None,
852 shorthand_items: &[],
853 value: "\"xyz"
854 }
855 );
856
857 assert!(element_attr.name().is_none());
858 assert!(element_attr.block_style().is_none());
859 assert!(element_attr.id().is_none());
860 assert!(element_attr.roles().is_empty());
861 assert!(element_attr.options().is_empty());
862
863 assert_eq!(offset, 4);
864 assert_eq!(
865 warning_types,
866 vec![WarningType::AttributeValueMissingTerminatingQuote]
867 );
868 }
869
870 #[test]
871 fn double_quoted_string() {
872 let p = Parser::default();
873
874 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
875 &CowStr::from("\"abc\"def"),
876 0,
877 &p,
878 ParseShorthand(false),
879 AttrlistContext::Inline,
880 );
881
882 assert!(warning_types.is_empty());
883
884 assert_eq!(
885 element_attr,
886 ElementAttribute {
887 name: None,
888 shorthand_items: &[],
889 value: "abc"
890 }
891 );
892
893 assert!(element_attr.name().is_none());
894 assert!(element_attr.block_style().is_none());
895 assert!(element_attr.id().is_none());
896 assert!(element_attr.roles().is_empty());
897 assert!(element_attr.options().is_empty());
898
899 assert_eq!(offset, 5);
900 }
901
902 #[test]
903 fn double_quoted_with_escape() {
904 let p = Parser::default();
905
906 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
907 &CowStr::from("\"a\\\"bc\"def"),
908 0,
909 &p,
910 ParseShorthand(false),
911 AttrlistContext::Inline,
912 );
913
914 assert!(warning_types.is_empty());
915
916 assert_eq!(
917 element_attr,
918 ElementAttribute {
919 name: None,
920 shorthand_items: &[],
921 value: "a\"bc"
922 }
923 );
924
925 assert!(element_attr.name().is_none());
926 assert!(element_attr.block_style().is_none());
927 assert!(element_attr.id().is_none());
928 assert!(element_attr.roles().is_empty());
929 assert!(element_attr.options().is_empty());
930
931 assert_eq!(offset, 7);
932 }
933
934 #[test]
935 fn double_quoted_with_single_quote() {
936 let p = Parser::default();
937
938 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
939 &CowStr::from("\"a'bc\"def"),
940 0,
941 &p,
942 ParseShorthand(false),
943 AttrlistContext::Inline,
944 );
945
946 assert!(warning_types.is_empty());
947
948 assert_eq!(
949 element_attr,
950 ElementAttribute {
951 name: None,
952 shorthand_items: &[],
953 value: "a'bc"
954 }
955 );
956
957 assert!(element_attr.name().is_none());
958 assert!(element_attr.block_style().is_none());
959 assert!(element_attr.id().is_none());
960 assert!(element_attr.roles().is_empty());
961 assert!(element_attr.options().is_empty());
962
963 assert_eq!(offset, 6);
964 }
965
966 #[test]
967 fn err_unterminated_single_quote() {
968 let p = Parser::default();
969
970 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
971 &CowStr::from("\'xyz"),
972 0,
973 &p,
974 ParseShorthand(false),
975 AttrlistContext::Inline,
976 );
977
978 assert_eq!(
979 element_attr,
980 ElementAttribute {
981 name: None,
982 shorthand_items: &[],
983 value: "\'xyz"
984 }
985 );
986
987 assert!(element_attr.name().is_none());
988 assert!(element_attr.block_style().is_none());
989 assert!(element_attr.id().is_none());
990 assert!(element_attr.roles().is_empty());
991 assert!(element_attr.options().is_empty());
992
993 assert_eq!(offset, 4);
994
995 assert_eq!(
996 warning_types,
997 vec![WarningType::AttributeValueMissingTerminatingQuote]
998 );
999 }
1000
1001 #[test]
1002 fn err_unterminated_single_quote_ends_at_comma() {
1003 let p = Parser::default();
1004
1005 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1006 &CowStr::from("\'xyz,abc"),
1007 0,
1008 &p,
1009 ParseShorthand(false),
1010 AttrlistContext::Inline,
1011 );
1012
1013 assert_eq!(
1014 element_attr,
1015 ElementAttribute {
1016 name: None,
1017 shorthand_items: &[],
1018 value: "\'xyz"
1019 }
1020 );
1021
1022 assert!(element_attr.name().is_none());
1023 assert!(element_attr.block_style().is_none());
1024 assert!(element_attr.id().is_none());
1025 assert!(element_attr.roles().is_empty());
1026 assert!(element_attr.options().is_empty());
1027
1028 assert_eq!(offset, 4);
1029 assert_eq!(
1030 warning_types,
1031 vec![WarningType::AttributeValueMissingTerminatingQuote]
1032 );
1033 }
1034
1035 #[test]
1036 fn single_quoted_string() {
1037 let p = Parser::default();
1038
1039 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1040 &CowStr::from("'abc'def"),
1041 0,
1042 &p,
1043 ParseShorthand(false),
1044 AttrlistContext::Inline,
1045 );
1046
1047 assert!(warning_types.is_empty());
1048
1049 assert_eq!(
1050 element_attr,
1051 ElementAttribute {
1052 name: None,
1053 shorthand_items: &[],
1054 value: "abc"
1055 }
1056 );
1057
1058 assert!(element_attr.name().is_none());
1059 assert!(element_attr.block_style().is_none());
1060 assert!(element_attr.id().is_none());
1061 assert!(element_attr.roles().is_empty());
1062 assert!(element_attr.options().is_empty());
1063
1064 assert_eq!(offset, 5);
1065 }
1066
1067 #[test]
1068 fn single_quoted_with_escape() {
1069 let p = Parser::default();
1070
1071 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1072 &CowStr::from("'a\\'bc'def"),
1073 0,
1074 &p,
1075 ParseShorthand(false),
1076 AttrlistContext::Inline,
1077 );
1078
1079 assert!(warning_types.is_empty());
1080
1081 assert_eq!(
1082 element_attr,
1083 ElementAttribute {
1084 name: None,
1085 shorthand_items: &[],
1086 value: "a'bc"
1087 }
1088 );
1089
1090 assert!(element_attr.name().is_none());
1091 assert!(element_attr.block_style().is_none());
1092 assert!(element_attr.id().is_none());
1093 assert!(element_attr.roles().is_empty());
1094 assert!(element_attr.options().is_empty());
1095
1096 assert_eq!(offset, 7);
1097 }
1098
1099 #[test]
1100 fn single_quoted_with_double_quote() {
1101 let p = Parser::default();
1102
1103 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1104 &CowStr::from("'a\"bc'def"),
1105 0,
1106 &p,
1107 ParseShorthand(false),
1108 AttrlistContext::Inline,
1109 );
1110
1111 assert!(warning_types.is_empty());
1112
1113 assert_eq!(
1114 element_attr,
1115 ElementAttribute {
1116 name: None,
1117 shorthand_items: &[],
1118 value: "a\"bc"
1119 }
1120 );
1121
1122 assert!(element_attr.name().is_none());
1123 assert!(element_attr.block_style().is_none());
1124 assert!(element_attr.id().is_none());
1125 assert!(element_attr.roles().is_empty());
1126 assert!(element_attr.options().is_empty());
1127
1128 assert_eq!(offset, 6);
1129 }
1130
1131 #[test]
1132 fn single_quoted_gets_substitions() {
1133 let p = Parser::default().with_intrinsic_attribute(
1134 "foo",
1135 "bar",
1136 ModificationContext::Anywhere,
1137 );
1138
1139 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1140 &CowStr::from("'*abc* def {foo}'"),
1141 0,
1142 &p,
1143 ParseShorthand(false),
1144 AttrlistContext::Block,
1145 );
1146
1147 assert!(warning_types.is_empty());
1148
1149 assert_eq!(
1150 element_attr,
1151 ElementAttribute {
1152 name: None,
1153 shorthand_items: &[],
1154 value: "<strong>abc</strong> def bar"
1155 }
1156 );
1157
1158 assert!(element_attr.name().is_none());
1159 assert!(element_attr.block_style().is_none());
1160 assert!(element_attr.id().is_none());
1161 assert!(element_attr.roles().is_empty());
1162 assert!(element_attr.options().is_empty());
1163
1164 assert_eq!(offset, 17);
1165 }
1166 }
1167
1168 mod named {
1169 use crate::{
1170 attributes::{AttrlistContext, element_attribute::ParseShorthand},
1171 strings::CowStr,
1172 tests::prelude::*,
1173 };
1174
1175 #[test]
1176 fn simple_named_value() {
1177 let p = Parser::default();
1178
1179 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1180 &CowStr::from("abc=def"),
1181 0,
1182 &p,
1183 ParseShorthand(false),
1184 AttrlistContext::Inline,
1185 );
1186
1187 assert!(warning_types.is_empty());
1188
1189 assert_eq!(
1190 element_attr,
1191 ElementAttribute {
1192 name: Some("abc"),
1193 shorthand_items: &[],
1194 value: "def"
1195 }
1196 );
1197
1198 assert_eq!(element_attr.name().unwrap(), "abc");
1199 assert!(element_attr.block_style().is_none());
1200 assert!(element_attr.id().is_none());
1201 assert!(element_attr.roles().is_empty());
1202 assert!(element_attr.options().is_empty());
1203
1204 assert_eq!(offset, 7);
1205 }
1206
1207 #[test]
1208 fn ignores_spaces_around_equals() {
1209 let p = Parser::default();
1210
1211 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1212 &CowStr::from("abc = def"),
1213 0,
1214 &p,
1215 ParseShorthand(false),
1216 AttrlistContext::Inline,
1217 );
1218
1219 assert!(warning_types.is_empty());
1220
1221 assert_eq!(
1222 element_attr,
1223 ElementAttribute {
1224 name: Some("abc"),
1225 shorthand_items: &[],
1226 value: "def"
1227 }
1228 );
1229
1230 assert_eq!(element_attr.name().unwrap(), "abc");
1231
1232 assert_eq!(offset, 10);
1233 }
1234
1235 #[test]
1236 fn numeric_name() {
1237 let p = Parser::default();
1238
1239 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1240 &CowStr::from("94-x =def"),
1241 0,
1242 &p,
1243 ParseShorthand(false),
1244 AttrlistContext::Inline,
1245 );
1246
1247 assert!(warning_types.is_empty());
1248
1249 assert_eq!(
1250 element_attr,
1251 ElementAttribute {
1252 name: Some("94-x"),
1253 shorthand_items: &[],
1254 value: "def"
1255 }
1256 );
1257
1258 assert_eq!(element_attr.name().unwrap(), "94-x");
1259 assert!(element_attr.block_style().is_none());
1260 assert!(element_attr.id().is_none());
1261 assert!(element_attr.roles().is_empty());
1262 assert!(element_attr.options().is_empty());
1263
1264 assert_eq!(offset, 9);
1265 }
1266
1267 #[test]
1268 fn quoted_value() {
1269 let p = Parser::default();
1270
1271 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1272 &CowStr::from("abc='def'g"),
1273 0,
1274 &p,
1275 ParseShorthand(false),
1276 AttrlistContext::Inline,
1277 );
1278
1279 assert!(warning_types.is_empty());
1280
1281 assert_eq!(
1282 element_attr,
1283 ElementAttribute {
1284 name: Some("abc"),
1285 shorthand_items: &[],
1286 value: "def"
1287 }
1288 );
1289
1290 assert_eq!(element_attr.name().unwrap(), "abc");
1291 assert!(element_attr.block_style().is_none());
1292 assert!(element_attr.id().is_none());
1293 assert!(element_attr.roles().is_empty());
1294 assert!(element_attr.options().is_empty());
1295
1296 assert_eq!(offset, 9);
1297 }
1298
1299 #[test]
1300 fn named_with_empty_value() {
1301 let p = Parser::default();
1302
1303 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1307 &CowStr::from("abc="),
1308 0,
1309 &p,
1310 ParseShorthand(false),
1311 AttrlistContext::Inline,
1312 );
1313
1314 assert!(warning_types.is_empty());
1315
1316 assert_eq!(
1317 element_attr,
1318 ElementAttribute {
1319 name: Some("abc"),
1320 shorthand_items: &[],
1321 value: ""
1322 }
1323 );
1324
1325 assert_eq!(element_attr.name(), Some("abc"));
1326 assert!(element_attr.block_style().is_none());
1327 assert!(element_attr.id().is_none());
1328 assert!(element_attr.roles().is_empty());
1329 assert!(element_attr.options().is_empty());
1330
1331 assert_eq!(offset, 4);
1332 }
1333
1334 #[test]
1335 fn named_with_empty_value_before_comma() {
1336 let p = Parser::default();
1337
1338 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1342 &CowStr::from("abc=,def"),
1343 0,
1344 &p,
1345 ParseShorthand(false),
1346 AttrlistContext::Inline,
1347 );
1348
1349 assert!(warning_types.is_empty());
1350
1351 assert_eq!(
1352 element_attr,
1353 ElementAttribute {
1354 name: Some("abc"),
1355 shorthand_items: &[],
1356 value: ""
1357 }
1358 );
1359
1360 assert_eq!(element_attr.name(), Some("abc"));
1361 assert!(element_attr.block_style().is_none());
1362 assert!(element_attr.id().is_none());
1363 assert!(element_attr.roles().is_empty());
1364 assert!(element_attr.options().is_empty());
1365
1366 assert_eq!(offset, 4);
1367 }
1368 }
1369
1370 mod parse_with_shorthand {
1371 use crate::{
1372 attributes::{AttrlistContext, element_attribute::ParseShorthand},
1373 strings::CowStr,
1374 tests::prelude::*,
1375 };
1376
1377 #[test]
1378 fn block_style_only() {
1379 let p = Parser::default();
1380
1381 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1382 &CowStr::from("abc"),
1383 0,
1384 &p,
1385 ParseShorthand(true),
1386 AttrlistContext::Inline,
1387 );
1388
1389 assert!(warning_types.is_empty());
1390
1391 assert_eq!(
1392 element_attr,
1393 ElementAttribute {
1394 name: None,
1395 shorthand_items: &["abc"],
1396 value: "abc"
1397 }
1398 );
1399
1400 assert!(element_attr.name().is_none());
1401 assert_eq!(element_attr.shorthand_items(), vec!["abc"]);
1402 assert_eq!(element_attr.block_style().unwrap(), "abc");
1403 assert!(element_attr.id().is_none());
1404 assert!(element_attr.roles().is_empty());
1405 assert!(element_attr.options().is_empty());
1406
1407 assert_eq!(offset, 3);
1408 }
1409
1410 #[test]
1411 fn ignore_if_named_attribute() {
1412 let p = Parser::default();
1413
1414 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1415 &CowStr::from("name=block_style#id"),
1416 0,
1417 &p,
1418 ParseShorthand(true),
1419 AttrlistContext::Inline,
1420 );
1421
1422 assert!(warning_types.is_empty());
1423
1424 assert_eq!(
1425 element_attr,
1426 ElementAttribute {
1427 name: Some("name"),
1428 shorthand_items: &[],
1429 value: "block_style#id"
1430 }
1431 );
1432
1433 assert_eq!(element_attr.name().unwrap(), "name");
1434 assert!(element_attr.shorthand_items().is_empty());
1435 assert!(element_attr.block_style().is_none());
1436 assert!(element_attr.id().is_none());
1437 assert!(element_attr.roles().is_empty());
1438 assert!(element_attr.options().is_empty());
1439
1440 assert_eq!(offset, 19);
1441 }
1442
1443 #[test]
1444 fn error_empty_id() {
1445 let p = Parser::default();
1446
1447 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1448 &CowStr::from("abc#"),
1449 0,
1450 &p,
1451 ParseShorthand(true),
1452 AttrlistContext::Inline,
1453 );
1454
1455 assert_eq!(
1456 element_attr,
1457 ElementAttribute {
1458 name: None,
1459 shorthand_items: &["abc"],
1460 value: "abc#"
1461 }
1462 );
1463
1464 assert_eq!(offset, 4);
1465 assert_eq!(warning_types, vec![WarningType::EmptyShorthandItem]);
1466 }
1467
1468 #[test]
1469 fn error_duplicate_delimiter() {
1470 let p = Parser::default();
1471
1472 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1473 &CowStr::from("abc##id"),
1474 0,
1475 &p,
1476 ParseShorthand(true),
1477 AttrlistContext::Inline,
1478 );
1479
1480 assert_eq!(
1481 element_attr,
1482 ElementAttribute {
1483 name: None,
1484 shorthand_items: &["abc", "#id"],
1485 value: "abc##id"
1486 }
1487 );
1488
1489 assert_eq!(offset, 7);
1490 assert_eq!(warning_types, vec![WarningType::EmptyShorthandItem]);
1491 }
1492
1493 #[test]
1494 fn id_only() {
1495 let p = Parser::default();
1496
1497 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1498 &CowStr::from("#xyz"),
1499 0,
1500 &p,
1501 ParseShorthand(true),
1502 AttrlistContext::Inline,
1503 );
1504
1505 assert!(warning_types.is_empty());
1506
1507 assert_eq!(
1508 element_attr,
1509 ElementAttribute {
1510 name: None,
1511 shorthand_items: &["#xyz"],
1512 value: "#xyz"
1513 }
1514 );
1515
1516 assert!(element_attr.name().is_none());
1517 assert_eq!(element_attr.shorthand_items(), vec!["#xyz"]);
1518 assert!(element_attr.block_style().is_none());
1519 assert_eq!(element_attr.id().unwrap(), "xyz");
1520 assert!(element_attr.roles().is_empty());
1521 assert!(element_attr.options().is_empty());
1522
1523 assert_eq!(offset, 4);
1524 }
1525
1526 #[test]
1527 fn one_role_only() {
1528 let p = Parser::default();
1529
1530 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1531 &CowStr::from(".role1"),
1532 0,
1533 &p,
1534 ParseShorthand(true),
1535 AttrlistContext::Inline,
1536 );
1537
1538 assert!(warning_types.is_empty());
1539
1540 assert_eq!(
1541 element_attr,
1542 ElementAttribute {
1543 name: None,
1544 shorthand_items: &[".role1",],
1545 value: ".role1"
1546 }
1547 );
1548
1549 assert!(element_attr.name().is_none());
1550 assert_eq!(element_attr.shorthand_items(), vec![".role1"]);
1551 assert!(element_attr.block_style().is_none());
1552 assert!(element_attr.id().is_none());
1553 assert_eq!(element_attr.roles(), vec!("role1"));
1554 assert!(element_attr.options().is_empty());
1555
1556 assert_eq!(offset, 6);
1557 }
1558
1559 #[test]
1560 fn multiple_roles() {
1561 let p = Parser::default();
1562
1563 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1564 &CowStr::from(".role1.role2.role3"),
1565 0,
1566 &p,
1567 ParseShorthand(true),
1568 AttrlistContext::Inline,
1569 );
1570
1571 assert!(warning_types.is_empty());
1572
1573 assert_eq!(
1574 element_attr,
1575 ElementAttribute {
1576 name: None,
1577 shorthand_items: &[".role1", ".role2", ".role3"],
1578 value: ".role1.role2.role3"
1579 }
1580 );
1581
1582 assert!(element_attr.name().is_none());
1583
1584 assert_eq!(
1585 element_attr.shorthand_items(),
1586 vec![".role1", ".role2", ".role3"]
1587 );
1588
1589 assert!(element_attr.block_style().is_none());
1590 assert!(element_attr.id().is_none());
1591 assert_eq!(element_attr.roles(), vec!("role1", "role2", "role3",));
1592 assert!(element_attr.options().is_empty());
1593
1594 assert_eq!(offset, 18);
1595 }
1596
1597 #[test]
1598 fn one_option_only() {
1599 let p = Parser::default();
1600
1601 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1602 &CowStr::from("%option1"),
1603 0,
1604 &p,
1605 ParseShorthand(true),
1606 AttrlistContext::Inline,
1607 );
1608
1609 assert!(warning_types.is_empty());
1610
1611 assert_eq!(
1612 element_attr,
1613 ElementAttribute {
1614 name: None,
1615 shorthand_items: &["%option1"],
1616 value: "%option1"
1617 }
1618 );
1619
1620 assert!(element_attr.name().is_none());
1621 assert_eq!(element_attr.shorthand_items(), vec!["%option1"]);
1622 assert!(element_attr.block_style().is_none());
1623 assert!(element_attr.id().is_none());
1624 assert!(element_attr.roles().is_empty());
1625 assert_eq!(element_attr.options(), vec!("option1"));
1626
1627 assert_eq!(offset, 8);
1628 }
1629
1630 #[test]
1631 fn multiple_options() {
1632 let p = Parser::default();
1633
1634 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1635 &CowStr::from("%option1%option2%option3"),
1636 0,
1637 &p,
1638 ParseShorthand(true),
1639 AttrlistContext::Inline,
1640 );
1641
1642 assert!(warning_types.is_empty());
1643
1644 assert_eq!(
1645 element_attr,
1646 ElementAttribute {
1647 name: None,
1648 shorthand_items: &["%option1", "%option2", "%option3"],
1649 value: "%option1%option2%option3"
1650 }
1651 );
1652
1653 assert!(element_attr.name().is_none());
1654
1655 assert_eq!(
1656 element_attr.shorthand_items(),
1657 vec!["%option1", "%option2", "%option3"]
1658 );
1659
1660 assert!(element_attr.block_style().is_none());
1661 assert!(element_attr.id().is_none());
1662 assert!(element_attr.roles().is_empty());
1663 assert_eq!(
1664 element_attr.options(),
1665 vec!("option1", "option2", "option3")
1666 );
1667
1668 assert_eq!(offset, 24);
1669 }
1670
1671 #[test]
1672 fn block_style_and_id() {
1673 let p = Parser::default();
1674
1675 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1676 &CowStr::from("appendix#custom-id"),
1677 0,
1678 &p,
1679 ParseShorthand(true),
1680 AttrlistContext::Inline,
1681 );
1682
1683 assert!(warning_types.is_empty());
1684
1685 assert_eq!(
1686 element_attr,
1687 ElementAttribute {
1688 name: None,
1689 shorthand_items: &["appendix", "#custom-id"],
1690 value: "appendix#custom-id"
1691 }
1692 );
1693
1694 assert!(element_attr.name().is_none());
1695 assert_eq!(
1696 element_attr.shorthand_items(),
1697 vec!["appendix", "#custom-id"]
1698 );
1699 assert_eq!(element_attr.block_style().unwrap(), "appendix",);
1700 assert_eq!(element_attr.id().unwrap(), "custom-id",);
1701 assert!(element_attr.roles().is_empty());
1702 assert!(element_attr.options().is_empty());
1703
1704 assert_eq!(offset, 18);
1705 }
1706
1707 #[test]
1708 fn id_role_and_option() {
1709 let p = Parser::default();
1710
1711 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1712 &CowStr::from("#rules.prominent%incremental"),
1713 0,
1714 &p,
1715 ParseShorthand(true),
1716 AttrlistContext::Inline,
1717 );
1718
1719 assert!(warning_types.is_empty());
1720
1721 assert_eq!(
1722 element_attr,
1723 ElementAttribute {
1724 name: None,
1725 shorthand_items: &["#rules", ".prominent", "%incremental"],
1726 value: "#rules.prominent%incremental"
1727 }
1728 );
1729
1730 assert!(element_attr.name().is_none());
1731
1732 assert_eq!(
1733 element_attr.shorthand_items(),
1734 vec!["#rules", ".prominent", "%incremental"]
1735 );
1736
1737 assert!(element_attr.block_style().is_none());
1738 assert_eq!(element_attr.id().unwrap(), "rules");
1739 assert_eq!(element_attr.roles(), vec!("prominent"));
1740 assert_eq!(element_attr.options(), vec!("incremental"));
1741
1742 assert_eq!(offset, 28);
1743 }
1744 }
1745
1746 mod merge_block_style_shorthand {
1747 use crate::{
1748 attributes::{AttrlistContext, element_attribute::ParseShorthand},
1749 strings::CowStr,
1750 tests::prelude::*,
1751 };
1752
1753 fn parse(value: &str) -> crate::attributes::ElementAttribute<'_> {
1754 let p = Parser::default();
1755 crate::attributes::ElementAttribute::parse(
1756 &CowStr::from(value),
1757 0,
1758 &p,
1759 ParseShorthand(true),
1760 AttrlistContext::Block,
1761 )
1762 .0
1763 }
1764
1765 #[test]
1766 fn merges_disjoint_shorthand() {
1767 let earlier = parse("#myid");
1768 let later = parse(".myrole");
1769
1770 let merged =
1771 crate::attributes::ElementAttribute::merge_block_style_shorthand(&earlier, &later);
1772
1773 assert!(merged.name().is_none());
1774 assert_eq!(merged.id().unwrap(), "myid");
1775 assert_eq!(merged.roles(), vec!["myrole"]);
1776 }
1777
1778 #[test]
1779 fn two_empty_positionals_stay_empty() {
1780 let earlier = parse("");
1783 let later = parse("");
1784
1785 let merged =
1786 crate::attributes::ElementAttribute::merge_block_style_shorthand(&earlier, &later);
1787
1788 assert_eq!(
1789 merged,
1790 ElementAttribute {
1791 name: None,
1792 shorthand_items: &[],
1793 value: "",
1794 }
1795 );
1796
1797 assert!(merged.name().is_none());
1798 assert!(merged.block_style().is_none());
1799 assert!(merged.id().is_none());
1800 assert!(merged.roles().is_empty());
1801 assert!(merged.options().is_empty());
1802 }
1803 }
1804}