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