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, Debug, Eq, PartialEq)]
18pub struct ElementAttribute<'src> {
19 name: Option<CowStr<'src>>,
20 value: CowStr<'src>,
21 shorthand_item_indices: Vec<usize>,
22}
23
24impl<'src> ElementAttribute<'src> {
25 pub(crate) fn parse(
26 source_text: &CowStr<'src>,
27 start_index: usize,
28 parser: &Parser,
29 mut parse_shorthand: ParseShorthand,
30 attrlist_context: AttrlistContext,
31 ) -> (Self, usize, Vec<WarningType>) {
32 let mut warnings: Vec<WarningType> = vec![];
33
34 let (name, value, shorthand_item_indices, offset) = {
35 let mut source = Span::new(source_text.as_ref());
36 source = source.discard(start_index);
37
38 let (name, after): (Option<Span<'_>>, Span) = match source.take_attr_name() {
39 Some(name) => {
40 let space = name.after.take_whitespace_with_newline();
41 match space.after.take_prefix("=") {
42 Some(equals) => {
43 let space = equals.after.take_whitespace_with_newline();
44 (Some(name.item), space.after)
50 }
51 None => (None, source),
52 }
53 }
54 None => (None, source),
55 };
56
57 let after = after.take_whitespace_with_newline().after;
58 let first_char = after.data().chars().next();
59
60 let value = match first_char {
61 Some('\'') | Some('"') => match after.take_quoted_string() {
62 Some(v) => {
63 parse_shorthand = ParseShorthand(false);
64 v
65 }
66 None => {
67 warnings.push(WarningType::AttributeValueMissingTerminatingQuote);
68 after.take_while(|c| c != ',').trim_item_trailing_spaces()
69 }
70 },
71 _ => after.take_while(|c| c != ',').trim_item_trailing_spaces(),
72 };
73
74 let after = value.after;
75 let mut value = cowstr_from_source_and_span(source_text, &value.item);
76
77 if let Some(first) = first_char
78 && (first == '\'' || first == '\"')
79 {
80 let escaped_quote = format!("\\{first}");
81 let mut new_value = value.replace(&escaped_quote, &first.to_string());
82
83 if first == '\'' && attrlist_context == AttrlistContext::Block {
84 let span = Span::new(&new_value);
85 let mut content = Content::from(span);
86 SubstitutionGroup::Normal.apply(&mut content, parser, None);
87
88 if content.rendered.as_ref() != new_value {
89 new_value = content.rendered.to_string();
90 }
91 }
92
93 if new_value != *value {
94 value = CowStr::from(new_value);
95 }
96 }
97
98 let shorthand_item_indices = if name.is_none() && parse_shorthand.0 {
99 parse_shorthand_items(&value, &mut warnings)
100 } else {
101 vec![]
102 };
103
104 let name = name.map(|name| cowstr_from_source_and_span(source_text, &name));
105
106 (name, value, shorthand_item_indices, after.byte_offset())
107 };
108
109 (
110 Self {
111 name,
112 value,
113 shorthand_item_indices,
114 },
115 offset,
116 warnings,
117 )
118 }
119
120 pub fn name(&'src self) -> Option<&'src str> {
122 self.name_str()
123 }
124
125 pub(crate) fn name_str(&self) -> Option<&str> {
131 self.name.as_deref()
132 }
133
134 pub fn shorthand_items(&'src self) -> Vec<&'src str> {
140 self.shorthand_items_internal()
141 }
142
143 fn shorthand_items_internal(&self) -> Vec<&str> {
146 let mut result = vec![];
147 let value = self.value.as_ref();
148
149 let mut iter = self.shorthand_item_indices.iter().peekable();
150
151 while let Some(curr) = iter.next() {
152 let mut next_item = if let Some(next) = iter.peek() {
153 &value[*curr..**next]
154 } else {
155 &value[*curr..]
156 };
157
158 if next_item == "#" || next_item == "." || next_item == "%" {
159 continue;
160 }
161
162 next_item = next_item.trim_end();
163
164 if !next_item.is_empty() {
165 result.push(next_item);
166 }
167 }
168
169 result
170 }
171
172 pub fn block_style(&'src self) -> Option<&'src str> {
174 self.block_style_internal()
175 }
176
177 fn block_style_internal(&self) -> Option<&str> {
180 self.shorthand_items_internal()
181 .first()
182 .filter(|v| !v.chars().any(is_shorthand_delimiter))
183 .cloned()
184 }
185
186 pub fn id(&'src self) -> Option<&'src str> {
220 self.id_internal()
221 }
222
223 fn id_internal(&self) -> Option<&str> {
225 self.shorthand_items_internal()
226 .into_iter()
227 .find(|v| v.starts_with('#'))
228 .map(|v| &v[1..])
229 }
230
231 pub fn roles(&'src self) -> Vec<&'src str> {
253 self.roles_internal()
254 }
255
256 fn roles_internal(&self) -> Vec<&str> {
259 self.shorthand_items_internal()
260 .into_iter()
261 .filter(|span| span.starts_with('.'))
262 .map(|span| &span[1..])
263 .collect()
264 }
265
266 pub fn options(&'src self) -> Vec<&'src str> {
331 self.options_internal()
332 }
333
334 fn options_internal(&self) -> Vec<&str> {
337 self.shorthand_items_internal()
338 .into_iter()
339 .filter(|v| v.starts_with('%'))
340 .map(|v| &v[1..])
341 .collect()
342 }
343
344 pub(crate) fn merge_block_style_shorthand(earlier: &Self, later: &Self) -> Self {
355 let block_style = later
356 .block_style_internal()
357 .or(earlier.block_style_internal());
358 let id = later.id_internal().or(earlier.id_internal());
359
360 let mut roles = earlier.roles_internal();
361 roles.extend(later.roles_internal());
362
363 let mut options = earlier.options_internal();
364 options.extend(later.options_internal());
365
366 let mut value = String::new();
367 if let Some(block_style) = block_style {
368 value.push_str(block_style);
369 }
370 if let Some(id) = id {
371 value.push('#');
372 value.push_str(id);
373 }
374 for role in &roles {
375 value.push('.');
376 value.push_str(role);
377 }
378 for option in &options {
379 value.push('%');
380 value.push_str(option);
381 }
382
383 let mut warnings: Vec<WarningType> = vec![];
391 let shorthand_item_indices = if value.is_empty() {
392 vec![]
393 } else {
394 parse_shorthand_items(&value, &mut warnings)
395 };
396
397 debug_assert!(
398 warnings.is_empty(),
399 "merging block-style shorthand should not produce warnings, got: {warnings:?}"
400 );
401
402 Self {
403 name: None,
404 value: CowStr::from(value),
405 shorthand_item_indices,
406 }
407 }
408
409 pub fn value(&'src self) -> &'src str {
414 self.value.as_ref()
415 }
416}
417
418fn parse_shorthand_items(source: &str, warnings: &mut Vec<WarningType>) -> Vec<usize> {
419 let mut shorthand_item_indices: Vec<usize> = vec![];
420 let mut span = Span::new(source);
421
422 if let Some(block_style_pr) = span.split_at_match_non_empty(is_shorthand_delimiter) {
424 shorthand_item_indices.push(block_style_pr.item.discard_whitespace().byte_offset());
425
426 span = block_style_pr.after;
427 }
428
429 while !span.is_empty() {
430 let after_delimiter = span.discard(1);
432
433 match after_delimiter.position(is_shorthand_delimiter) {
434 None => {
435 if after_delimiter.is_empty() {
436 warnings.push(WarningType::EmptyShorthandItem);
437 shorthand_item_indices.push(span.byte_offset());
438 span = after_delimiter;
439 } else {
440 shorthand_item_indices.push(span.byte_offset());
441 span = span.discard_all();
442 }
443 }
444
445 Some(0) => {
446 shorthand_item_indices.push(span.byte_offset());
447 warnings.push(WarningType::EmptyShorthandItem);
448 span = after_delimiter;
449 }
450
451 Some(index) => {
452 let mi: MatchedItem<Span> = span.into_parse_result(index + 1);
453 shorthand_item_indices.push(span.byte_offset());
454 span = mi.after;
455 }
456 }
457 }
458
459 shorthand_item_indices
460}
461
462fn is_shorthand_delimiter(c: char) -> bool {
463 c == '#' || c == '%' || c == '.'
464}
465
466#[derive(Clone, Debug)]
467pub(crate) struct ParseShorthand(pub bool);
468
469fn cowstr_from_source_and_span<'src>(source: &CowStr<'src>, span: &Span<'_>) -> CowStr<'src> {
470 if let CowStr::Borrowed(source) = source {
471 let borrowed: Span<'src> = Span::new(source)
472 .discard(span.byte_offset())
473 .slice_to(..span.len());
474
475 CowStr::Borrowed(borrowed.data())
476 } else {
477 CowStr::from(span.data().to_string())
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 #![allow(clippy::unwrap_used)]
484
485 use crate::{
486 attributes::{AttrlistContext, element_attribute::ParseShorthand},
487 strings::CowStr,
488 tests::prelude::*,
489 };
490
491 #[test]
492 fn impl_clone() {
493 let p = Parser::default();
495
496 let b1 = crate::attributes::ElementAttribute::parse(
497 &CowStr::from("abc"),
498 0,
499 &p,
500 ParseShorthand(false),
501 AttrlistContext::Inline,
502 )
503 .0;
504
505 let b2 = b1.clone();
506
507 assert_eq!(b1, b2);
508 }
509
510 #[test]
511 fn empty_source() {
512 let p = Parser::default();
513
514 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
515 &CowStr::from(""),
516 0,
517 &p,
518 ParseShorthand(false),
519 AttrlistContext::Inline,
520 );
521
522 assert!(warning_types.is_empty());
523
524 assert_eq!(
525 element_attr,
526 ElementAttribute {
527 name: None,
528 shorthand_items: &[],
529 value: "",
530 }
531 );
532
533 assert!(element_attr.name().is_none());
534 assert!(element_attr.block_style().is_none());
535 assert!(element_attr.id().is_none());
536 assert!(element_attr.roles().is_empty());
537 assert!(element_attr.options().is_empty());
538
539 assert_eq!(offset, 0);
540 }
541
542 #[test]
543 fn only_spaces() {
544 let p = Parser::default();
545
546 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
547 &CowStr::from(" "),
548 0,
549 &p,
550 ParseShorthand(false),
551 AttrlistContext::Inline,
552 );
553
554 assert!(warning_types.is_empty());
555
556 assert_eq!(
557 element_attr,
558 ElementAttribute {
559 name: None,
560 shorthand_items: &[],
561 value: "",
562 }
563 );
564
565 assert!(element_attr.name().is_none());
566 assert!(element_attr.block_style().is_none());
567 assert!(element_attr.id().is_none());
568 assert!(element_attr.roles().is_empty());
569 assert!(element_attr.options().is_empty());
570
571 assert_eq!(offset, 3);
572 }
573
574 #[test]
575 fn unquoted_and_unnamed_value() {
576 let p = Parser::default();
577
578 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
579 &CowStr::from("abc"),
580 0,
581 &p,
582 ParseShorthand(false),
583 AttrlistContext::Inline,
584 );
585
586 assert!(warning_types.is_empty());
587
588 assert_eq!(
589 element_attr,
590 ElementAttribute {
591 name: None,
592 shorthand_items: &[],
593 value: "abc",
594 }
595 );
596
597 assert!(element_attr.name().is_none());
598 assert!(element_attr.block_style().is_none());
599 assert!(element_attr.id().is_none());
600 assert!(element_attr.roles().is_empty());
601 assert!(element_attr.options().is_empty());
602
603 assert_eq!(offset, 3);
604 }
605
606 #[test]
607 fn unquoted_stops_at_comma() {
608 let p = Parser::default();
609
610 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
611 &CowStr::from("abc,def"),
612 0,
613 &p,
614 ParseShorthand(false),
615 AttrlistContext::Inline,
616 );
617
618 assert!(warning_types.is_empty());
619
620 assert_eq!(
621 element_attr,
622 ElementAttribute {
623 name: None,
624 shorthand_items: &[],
625 value: "abc",
626 }
627 );
628
629 assert!(element_attr.name().is_none());
630 assert!(element_attr.block_style().is_none());
631 assert!(element_attr.id().is_none());
632 assert!(element_attr.roles().is_empty());
633 assert!(element_attr.options().is_empty());
634
635 assert_eq!(offset, 3);
636 }
637
638 mod quoted_string {
639 use crate::{
640 attributes::{AttrlistContext, element_attribute::ParseShorthand},
641 strings::CowStr,
642 tests::prelude::*,
643 };
644
645 #[test]
646 fn err_unterminated_double_quote() {
647 let p = Parser::default();
648
649 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
650 &CowStr::from("\"xyz"),
651 0,
652 &p,
653 ParseShorthand(false),
654 AttrlistContext::Inline,
655 );
656
657 assert_eq!(
658 element_attr,
659 ElementAttribute {
660 name: None,
661 shorthand_items: &[],
662 value: "\"xyz"
663 }
664 );
665
666 assert!(element_attr.name().is_none());
667 assert!(element_attr.block_style().is_none());
668 assert!(element_attr.id().is_none());
669 assert!(element_attr.roles().is_empty());
670 assert!(element_attr.options().is_empty());
671
672 assert_eq!(offset, 4);
673
674 assert_eq!(
675 warning_types,
676 vec![WarningType::AttributeValueMissingTerminatingQuote]
677 );
678 }
679
680 #[test]
681 fn err_unterminated_double_quote_ends_at_comma() {
682 let p = Parser::default();
683
684 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
685 &CowStr::from("\"xyz,abc"),
686 0,
687 &p,
688 ParseShorthand(false),
689 AttrlistContext::Inline,
690 );
691
692 assert_eq!(
693 element_attr,
694 ElementAttribute {
695 name: None,
696 shorthand_items: &[],
697 value: "\"xyz"
698 }
699 );
700
701 assert!(element_attr.name().is_none());
702 assert!(element_attr.block_style().is_none());
703 assert!(element_attr.id().is_none());
704 assert!(element_attr.roles().is_empty());
705 assert!(element_attr.options().is_empty());
706
707 assert_eq!(offset, 4);
708 assert_eq!(
709 warning_types,
710 vec![WarningType::AttributeValueMissingTerminatingQuote]
711 );
712 }
713
714 #[test]
715 fn double_quoted_string() {
716 let p = Parser::default();
717
718 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
719 &CowStr::from("\"abc\"def"),
720 0,
721 &p,
722 ParseShorthand(false),
723 AttrlistContext::Inline,
724 );
725
726 assert!(warning_types.is_empty());
727
728 assert_eq!(
729 element_attr,
730 ElementAttribute {
731 name: None,
732 shorthand_items: &[],
733 value: "abc"
734 }
735 );
736
737 assert!(element_attr.name().is_none());
738 assert!(element_attr.block_style().is_none());
739 assert!(element_attr.id().is_none());
740 assert!(element_attr.roles().is_empty());
741 assert!(element_attr.options().is_empty());
742
743 assert_eq!(offset, 5);
744 }
745
746 #[test]
747 fn double_quoted_with_escape() {
748 let p = Parser::default();
749
750 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
751 &CowStr::from("\"a\\\"bc\"def"),
752 0,
753 &p,
754 ParseShorthand(false),
755 AttrlistContext::Inline,
756 );
757
758 assert!(warning_types.is_empty());
759
760 assert_eq!(
761 element_attr,
762 ElementAttribute {
763 name: None,
764 shorthand_items: &[],
765 value: "a\"bc"
766 }
767 );
768
769 assert!(element_attr.name().is_none());
770 assert!(element_attr.block_style().is_none());
771 assert!(element_attr.id().is_none());
772 assert!(element_attr.roles().is_empty());
773 assert!(element_attr.options().is_empty());
774
775 assert_eq!(offset, 7);
776 }
777
778 #[test]
779 fn double_quoted_with_single_quote() {
780 let p = Parser::default();
781
782 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
783 &CowStr::from("\"a'bc\"def"),
784 0,
785 &p,
786 ParseShorthand(false),
787 AttrlistContext::Inline,
788 );
789
790 assert!(warning_types.is_empty());
791
792 assert_eq!(
793 element_attr,
794 ElementAttribute {
795 name: None,
796 shorthand_items: &[],
797 value: "a'bc"
798 }
799 );
800
801 assert!(element_attr.name().is_none());
802 assert!(element_attr.block_style().is_none());
803 assert!(element_attr.id().is_none());
804 assert!(element_attr.roles().is_empty());
805 assert!(element_attr.options().is_empty());
806
807 assert_eq!(offset, 6);
808 }
809
810 #[test]
811 fn err_unterminated_single_quote() {
812 let p = Parser::default();
813
814 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
815 &CowStr::from("\'xyz"),
816 0,
817 &p,
818 ParseShorthand(false),
819 AttrlistContext::Inline,
820 );
821
822 assert_eq!(
823 element_attr,
824 ElementAttribute {
825 name: None,
826 shorthand_items: &[],
827 value: "\'xyz"
828 }
829 );
830
831 assert!(element_attr.name().is_none());
832 assert!(element_attr.block_style().is_none());
833 assert!(element_attr.id().is_none());
834 assert!(element_attr.roles().is_empty());
835 assert!(element_attr.options().is_empty());
836
837 assert_eq!(offset, 4);
838
839 assert_eq!(
840 warning_types,
841 vec![WarningType::AttributeValueMissingTerminatingQuote]
842 );
843 }
844
845 #[test]
846 fn err_unterminated_single_quote_ends_at_comma() {
847 let p = Parser::default();
848
849 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
850 &CowStr::from("\'xyz,abc"),
851 0,
852 &p,
853 ParseShorthand(false),
854 AttrlistContext::Inline,
855 );
856
857 assert_eq!(
858 element_attr,
859 ElementAttribute {
860 name: None,
861 shorthand_items: &[],
862 value: "\'xyz"
863 }
864 );
865
866 assert!(element_attr.name().is_none());
867 assert!(element_attr.block_style().is_none());
868 assert!(element_attr.id().is_none());
869 assert!(element_attr.roles().is_empty());
870 assert!(element_attr.options().is_empty());
871
872 assert_eq!(offset, 4);
873 assert_eq!(
874 warning_types,
875 vec![WarningType::AttributeValueMissingTerminatingQuote]
876 );
877 }
878
879 #[test]
880 fn single_quoted_string() {
881 let p = Parser::default();
882
883 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
884 &CowStr::from("'abc'def"),
885 0,
886 &p,
887 ParseShorthand(false),
888 AttrlistContext::Inline,
889 );
890
891 assert!(warning_types.is_empty());
892
893 assert_eq!(
894 element_attr,
895 ElementAttribute {
896 name: None,
897 shorthand_items: &[],
898 value: "abc"
899 }
900 );
901
902 assert!(element_attr.name().is_none());
903 assert!(element_attr.block_style().is_none());
904 assert!(element_attr.id().is_none());
905 assert!(element_attr.roles().is_empty());
906 assert!(element_attr.options().is_empty());
907
908 assert_eq!(offset, 5);
909 }
910
911 #[test]
912 fn single_quoted_with_escape() {
913 let p = Parser::default();
914
915 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
916 &CowStr::from("'a\\'bc'def"),
917 0,
918 &p,
919 ParseShorthand(false),
920 AttrlistContext::Inline,
921 );
922
923 assert!(warning_types.is_empty());
924
925 assert_eq!(
926 element_attr,
927 ElementAttribute {
928 name: None,
929 shorthand_items: &[],
930 value: "a'bc"
931 }
932 );
933
934 assert!(element_attr.name().is_none());
935 assert!(element_attr.block_style().is_none());
936 assert!(element_attr.id().is_none());
937 assert!(element_attr.roles().is_empty());
938 assert!(element_attr.options().is_empty());
939
940 assert_eq!(offset, 7);
941 }
942
943 #[test]
944 fn single_quoted_with_double_quote() {
945 let p = Parser::default();
946
947 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
948 &CowStr::from("'a\"bc'def"),
949 0,
950 &p,
951 ParseShorthand(false),
952 AttrlistContext::Inline,
953 );
954
955 assert!(warning_types.is_empty());
956
957 assert_eq!(
958 element_attr,
959 ElementAttribute {
960 name: None,
961 shorthand_items: &[],
962 value: "a\"bc"
963 }
964 );
965
966 assert!(element_attr.name().is_none());
967 assert!(element_attr.block_style().is_none());
968 assert!(element_attr.id().is_none());
969 assert!(element_attr.roles().is_empty());
970 assert!(element_attr.options().is_empty());
971
972 assert_eq!(offset, 6);
973 }
974
975 #[test]
976 fn single_quoted_gets_substitions() {
977 let p = Parser::default().with_intrinsic_attribute(
978 "foo",
979 "bar",
980 ModificationContext::Anywhere,
981 );
982
983 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
984 &CowStr::from("'*abc* def {foo}'"),
985 0,
986 &p,
987 ParseShorthand(false),
988 AttrlistContext::Block,
989 );
990
991 assert!(warning_types.is_empty());
992
993 assert_eq!(
994 element_attr,
995 ElementAttribute {
996 name: None,
997 shorthand_items: &[],
998 value: "<strong>abc</strong> def bar"
999 }
1000 );
1001
1002 assert!(element_attr.name().is_none());
1003 assert!(element_attr.block_style().is_none());
1004 assert!(element_attr.id().is_none());
1005 assert!(element_attr.roles().is_empty());
1006 assert!(element_attr.options().is_empty());
1007
1008 assert_eq!(offset, 17);
1009 }
1010 }
1011
1012 mod named {
1013 use crate::{
1014 attributes::{AttrlistContext, element_attribute::ParseShorthand},
1015 strings::CowStr,
1016 tests::prelude::*,
1017 };
1018
1019 #[test]
1020 fn simple_named_value() {
1021 let p = Parser::default();
1022
1023 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1024 &CowStr::from("abc=def"),
1025 0,
1026 &p,
1027 ParseShorthand(false),
1028 AttrlistContext::Inline,
1029 );
1030
1031 assert!(warning_types.is_empty());
1032
1033 assert_eq!(
1034 element_attr,
1035 ElementAttribute {
1036 name: Some("abc"),
1037 shorthand_items: &[],
1038 value: "def"
1039 }
1040 );
1041
1042 assert_eq!(element_attr.name().unwrap(), "abc");
1043 assert!(element_attr.block_style().is_none());
1044 assert!(element_attr.id().is_none());
1045 assert!(element_attr.roles().is_empty());
1046 assert!(element_attr.options().is_empty());
1047
1048 assert_eq!(offset, 7);
1049 }
1050
1051 #[test]
1052 fn ignores_spaces_around_equals() {
1053 let p = Parser::default();
1054
1055 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1056 &CowStr::from("abc = def"),
1057 0,
1058 &p,
1059 ParseShorthand(false),
1060 AttrlistContext::Inline,
1061 );
1062
1063 assert!(warning_types.is_empty());
1064
1065 assert_eq!(
1066 element_attr,
1067 ElementAttribute {
1068 name: Some("abc"),
1069 shorthand_items: &[],
1070 value: "def"
1071 }
1072 );
1073
1074 assert_eq!(element_attr.name().unwrap(), "abc");
1075
1076 assert_eq!(offset, 10);
1077 }
1078
1079 #[test]
1080 fn numeric_name() {
1081 let p = Parser::default();
1082
1083 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1084 &CowStr::from("94-x =def"),
1085 0,
1086 &p,
1087 ParseShorthand(false),
1088 AttrlistContext::Inline,
1089 );
1090
1091 assert!(warning_types.is_empty());
1092
1093 assert_eq!(
1094 element_attr,
1095 ElementAttribute {
1096 name: Some("94-x"),
1097 shorthand_items: &[],
1098 value: "def"
1099 }
1100 );
1101
1102 assert_eq!(element_attr.name().unwrap(), "94-x");
1103 assert!(element_attr.block_style().is_none());
1104 assert!(element_attr.id().is_none());
1105 assert!(element_attr.roles().is_empty());
1106 assert!(element_attr.options().is_empty());
1107
1108 assert_eq!(offset, 9);
1109 }
1110
1111 #[test]
1112 fn quoted_value() {
1113 let p = Parser::default();
1114
1115 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1116 &CowStr::from("abc='def'g"),
1117 0,
1118 &p,
1119 ParseShorthand(false),
1120 AttrlistContext::Inline,
1121 );
1122
1123 assert!(warning_types.is_empty());
1124
1125 assert_eq!(
1126 element_attr,
1127 ElementAttribute {
1128 name: Some("abc"),
1129 shorthand_items: &[],
1130 value: "def"
1131 }
1132 );
1133
1134 assert_eq!(element_attr.name().unwrap(), "abc");
1135 assert!(element_attr.block_style().is_none());
1136 assert!(element_attr.id().is_none());
1137 assert!(element_attr.roles().is_empty());
1138 assert!(element_attr.options().is_empty());
1139
1140 assert_eq!(offset, 9);
1141 }
1142
1143 #[test]
1144 fn named_with_empty_value() {
1145 let p = Parser::default();
1146
1147 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1151 &CowStr::from("abc="),
1152 0,
1153 &p,
1154 ParseShorthand(false),
1155 AttrlistContext::Inline,
1156 );
1157
1158 assert!(warning_types.is_empty());
1159
1160 assert_eq!(
1161 element_attr,
1162 ElementAttribute {
1163 name: Some("abc"),
1164 shorthand_items: &[],
1165 value: ""
1166 }
1167 );
1168
1169 assert_eq!(element_attr.name(), Some("abc"));
1170 assert!(element_attr.block_style().is_none());
1171 assert!(element_attr.id().is_none());
1172 assert!(element_attr.roles().is_empty());
1173 assert!(element_attr.options().is_empty());
1174
1175 assert_eq!(offset, 4);
1176 }
1177
1178 #[test]
1179 fn named_with_empty_value_before_comma() {
1180 let p = Parser::default();
1181
1182 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1186 &CowStr::from("abc=,def"),
1187 0,
1188 &p,
1189 ParseShorthand(false),
1190 AttrlistContext::Inline,
1191 );
1192
1193 assert!(warning_types.is_empty());
1194
1195 assert_eq!(
1196 element_attr,
1197 ElementAttribute {
1198 name: Some("abc"),
1199 shorthand_items: &[],
1200 value: ""
1201 }
1202 );
1203
1204 assert_eq!(element_attr.name(), Some("abc"));
1205 assert!(element_attr.block_style().is_none());
1206 assert!(element_attr.id().is_none());
1207 assert!(element_attr.roles().is_empty());
1208 assert!(element_attr.options().is_empty());
1209
1210 assert_eq!(offset, 4);
1211 }
1212 }
1213
1214 mod parse_with_shorthand {
1215 use crate::{
1216 attributes::{AttrlistContext, element_attribute::ParseShorthand},
1217 strings::CowStr,
1218 tests::prelude::*,
1219 };
1220
1221 #[test]
1222 fn block_style_only() {
1223 let p = Parser::default();
1224
1225 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1226 &CowStr::from("abc"),
1227 0,
1228 &p,
1229 ParseShorthand(true),
1230 AttrlistContext::Inline,
1231 );
1232
1233 assert!(warning_types.is_empty());
1234
1235 assert_eq!(
1236 element_attr,
1237 ElementAttribute {
1238 name: None,
1239 shorthand_items: &["abc"],
1240 value: "abc"
1241 }
1242 );
1243
1244 assert!(element_attr.name().is_none());
1245 assert_eq!(element_attr.shorthand_items(), vec!["abc"]);
1246 assert_eq!(element_attr.block_style().unwrap(), "abc");
1247 assert!(element_attr.id().is_none());
1248 assert!(element_attr.roles().is_empty());
1249 assert!(element_attr.options().is_empty());
1250
1251 assert_eq!(offset, 3);
1252 }
1253
1254 #[test]
1255 fn ignore_if_named_attribute() {
1256 let p = Parser::default();
1257
1258 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1259 &CowStr::from("name=block_style#id"),
1260 0,
1261 &p,
1262 ParseShorthand(true),
1263 AttrlistContext::Inline,
1264 );
1265
1266 assert!(warning_types.is_empty());
1267
1268 assert_eq!(
1269 element_attr,
1270 ElementAttribute {
1271 name: Some("name"),
1272 shorthand_items: &[],
1273 value: "block_style#id"
1274 }
1275 );
1276
1277 assert_eq!(element_attr.name().unwrap(), "name");
1278 assert!(element_attr.shorthand_items().is_empty());
1279 assert!(element_attr.block_style().is_none());
1280 assert!(element_attr.id().is_none());
1281 assert!(element_attr.roles().is_empty());
1282 assert!(element_attr.options().is_empty());
1283
1284 assert_eq!(offset, 19);
1285 }
1286
1287 #[test]
1288 fn error_empty_id() {
1289 let p = Parser::default();
1290
1291 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1292 &CowStr::from("abc#"),
1293 0,
1294 &p,
1295 ParseShorthand(true),
1296 AttrlistContext::Inline,
1297 );
1298
1299 assert_eq!(
1300 element_attr,
1301 ElementAttribute {
1302 name: None,
1303 shorthand_items: &["abc"],
1304 value: "abc#"
1305 }
1306 );
1307
1308 assert_eq!(offset, 4);
1309 assert_eq!(warning_types, vec![WarningType::EmptyShorthandItem]);
1310 }
1311
1312 #[test]
1313 fn error_duplicate_delimiter() {
1314 let p = Parser::default();
1315
1316 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1317 &CowStr::from("abc##id"),
1318 0,
1319 &p,
1320 ParseShorthand(true),
1321 AttrlistContext::Inline,
1322 );
1323
1324 assert_eq!(
1325 element_attr,
1326 ElementAttribute {
1327 name: None,
1328 shorthand_items: &["abc", "#id"],
1329 value: "abc##id"
1330 }
1331 );
1332
1333 assert_eq!(offset, 7);
1334 assert_eq!(warning_types, vec![WarningType::EmptyShorthandItem]);
1335 }
1336
1337 #[test]
1338 fn id_only() {
1339 let p = Parser::default();
1340
1341 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1342 &CowStr::from("#xyz"),
1343 0,
1344 &p,
1345 ParseShorthand(true),
1346 AttrlistContext::Inline,
1347 );
1348
1349 assert!(warning_types.is_empty());
1350
1351 assert_eq!(
1352 element_attr,
1353 ElementAttribute {
1354 name: None,
1355 shorthand_items: &["#xyz"],
1356 value: "#xyz"
1357 }
1358 );
1359
1360 assert!(element_attr.name().is_none());
1361 assert_eq!(element_attr.shorthand_items(), vec!["#xyz"]);
1362 assert!(element_attr.block_style().is_none());
1363 assert_eq!(element_attr.id().unwrap(), "xyz");
1364 assert!(element_attr.roles().is_empty());
1365 assert!(element_attr.options().is_empty());
1366
1367 assert_eq!(offset, 4);
1368 }
1369
1370 #[test]
1371 fn one_role_only() {
1372 let p = Parser::default();
1373
1374 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1375 &CowStr::from(".role1"),
1376 0,
1377 &p,
1378 ParseShorthand(true),
1379 AttrlistContext::Inline,
1380 );
1381
1382 assert!(warning_types.is_empty());
1383
1384 assert_eq!(
1385 element_attr,
1386 ElementAttribute {
1387 name: None,
1388 shorthand_items: &[".role1",],
1389 value: ".role1"
1390 }
1391 );
1392
1393 assert!(element_attr.name().is_none());
1394 assert_eq!(element_attr.shorthand_items(), vec![".role1"]);
1395 assert!(element_attr.block_style().is_none());
1396 assert!(element_attr.id().is_none());
1397 assert_eq!(element_attr.roles(), vec!("role1"));
1398 assert!(element_attr.options().is_empty());
1399
1400 assert_eq!(offset, 6);
1401 }
1402
1403 #[test]
1404 fn multiple_roles() {
1405 let p = Parser::default();
1406
1407 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1408 &CowStr::from(".role1.role2.role3"),
1409 0,
1410 &p,
1411 ParseShorthand(true),
1412 AttrlistContext::Inline,
1413 );
1414
1415 assert!(warning_types.is_empty());
1416
1417 assert_eq!(
1418 element_attr,
1419 ElementAttribute {
1420 name: None,
1421 shorthand_items: &[".role1", ".role2", ".role3"],
1422 value: ".role1.role2.role3"
1423 }
1424 );
1425
1426 assert!(element_attr.name().is_none());
1427
1428 assert_eq!(
1429 element_attr.shorthand_items(),
1430 vec![".role1", ".role2", ".role3"]
1431 );
1432
1433 assert!(element_attr.block_style().is_none());
1434 assert!(element_attr.id().is_none());
1435 assert_eq!(element_attr.roles(), vec!("role1", "role2", "role3",));
1436 assert!(element_attr.options().is_empty());
1437
1438 assert_eq!(offset, 18);
1439 }
1440
1441 #[test]
1442 fn one_option_only() {
1443 let p = Parser::default();
1444
1445 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1446 &CowStr::from("%option1"),
1447 0,
1448 &p,
1449 ParseShorthand(true),
1450 AttrlistContext::Inline,
1451 );
1452
1453 assert!(warning_types.is_empty());
1454
1455 assert_eq!(
1456 element_attr,
1457 ElementAttribute {
1458 name: None,
1459 shorthand_items: &["%option1"],
1460 value: "%option1"
1461 }
1462 );
1463
1464 assert!(element_attr.name().is_none());
1465 assert_eq!(element_attr.shorthand_items(), vec!["%option1"]);
1466 assert!(element_attr.block_style().is_none());
1467 assert!(element_attr.id().is_none());
1468 assert!(element_attr.roles().is_empty());
1469 assert_eq!(element_attr.options(), vec!("option1"));
1470
1471 assert_eq!(offset, 8);
1472 }
1473
1474 #[test]
1475 fn multiple_options() {
1476 let p = Parser::default();
1477
1478 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1479 &CowStr::from("%option1%option2%option3"),
1480 0,
1481 &p,
1482 ParseShorthand(true),
1483 AttrlistContext::Inline,
1484 );
1485
1486 assert!(warning_types.is_empty());
1487
1488 assert_eq!(
1489 element_attr,
1490 ElementAttribute {
1491 name: None,
1492 shorthand_items: &["%option1", "%option2", "%option3"],
1493 value: "%option1%option2%option3"
1494 }
1495 );
1496
1497 assert!(element_attr.name().is_none());
1498
1499 assert_eq!(
1500 element_attr.shorthand_items(),
1501 vec!["%option1", "%option2", "%option3"]
1502 );
1503
1504 assert!(element_attr.block_style().is_none());
1505 assert!(element_attr.id().is_none());
1506 assert!(element_attr.roles().is_empty());
1507 assert_eq!(
1508 element_attr.options(),
1509 vec!("option1", "option2", "option3")
1510 );
1511
1512 assert_eq!(offset, 24);
1513 }
1514
1515 #[test]
1516 fn block_style_and_id() {
1517 let p = Parser::default();
1518
1519 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1520 &CowStr::from("appendix#custom-id"),
1521 0,
1522 &p,
1523 ParseShorthand(true),
1524 AttrlistContext::Inline,
1525 );
1526
1527 assert!(warning_types.is_empty());
1528
1529 assert_eq!(
1530 element_attr,
1531 ElementAttribute {
1532 name: None,
1533 shorthand_items: &["appendix", "#custom-id"],
1534 value: "appendix#custom-id"
1535 }
1536 );
1537
1538 assert!(element_attr.name().is_none());
1539 assert_eq!(
1540 element_attr.shorthand_items(),
1541 vec!["appendix", "#custom-id"]
1542 );
1543 assert_eq!(element_attr.block_style().unwrap(), "appendix",);
1544 assert_eq!(element_attr.id().unwrap(), "custom-id",);
1545 assert!(element_attr.roles().is_empty());
1546 assert!(element_attr.options().is_empty());
1547
1548 assert_eq!(offset, 18);
1549 }
1550
1551 #[test]
1552 fn id_role_and_option() {
1553 let p = Parser::default();
1554
1555 let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1556 &CowStr::from("#rules.prominent%incremental"),
1557 0,
1558 &p,
1559 ParseShorthand(true),
1560 AttrlistContext::Inline,
1561 );
1562
1563 assert!(warning_types.is_empty());
1564
1565 assert_eq!(
1566 element_attr,
1567 ElementAttribute {
1568 name: None,
1569 shorthand_items: &["#rules", ".prominent", "%incremental"],
1570 value: "#rules.prominent%incremental"
1571 }
1572 );
1573
1574 assert!(element_attr.name().is_none());
1575
1576 assert_eq!(
1577 element_attr.shorthand_items(),
1578 vec!["#rules", ".prominent", "%incremental"]
1579 );
1580
1581 assert!(element_attr.block_style().is_none());
1582 assert_eq!(element_attr.id().unwrap(), "rules");
1583 assert_eq!(element_attr.roles(), vec!("prominent"));
1584 assert_eq!(element_attr.options(), vec!("incremental"));
1585
1586 assert_eq!(offset, 28);
1587 }
1588 }
1589
1590 mod merge_block_style_shorthand {
1591 use crate::{
1592 attributes::{AttrlistContext, element_attribute::ParseShorthand},
1593 strings::CowStr,
1594 tests::prelude::*,
1595 };
1596
1597 fn parse(value: &str) -> crate::attributes::ElementAttribute<'_> {
1598 let p = Parser::default();
1599 crate::attributes::ElementAttribute::parse(
1600 &CowStr::from(value),
1601 0,
1602 &p,
1603 ParseShorthand(true),
1604 AttrlistContext::Block,
1605 )
1606 .0
1607 }
1608
1609 #[test]
1610 fn merges_disjoint_shorthand() {
1611 let earlier = parse("#myid");
1612 let later = parse(".myrole");
1613
1614 let merged =
1615 crate::attributes::ElementAttribute::merge_block_style_shorthand(&earlier, &later);
1616
1617 assert!(merged.name().is_none());
1618 assert_eq!(merged.id().unwrap(), "myid");
1619 assert_eq!(merged.roles(), vec!["myrole"]);
1620 }
1621
1622 #[test]
1623 fn two_empty_positionals_stay_empty() {
1624 let earlier = parse("");
1627 let later = parse("");
1628
1629 let merged =
1630 crate::attributes::ElementAttribute::merge_block_style_shorthand(&earlier, &later);
1631
1632 assert_eq!(
1633 merged,
1634 ElementAttribute {
1635 name: None,
1636 shorthand_items: &[],
1637 value: "",
1638 }
1639 );
1640
1641 assert!(merged.name().is_none());
1642 assert!(merged.block_style().is_none());
1643 assert!(merged.id().is_none());
1644 assert!(merged.roles().is_empty());
1645 assert!(merged.options().is_empty());
1646 }
1647 }
1648}