1use serde::Serialize;
50
51use crate::{AttributeValue, DocumentAttributes};
52
53#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)]
55#[serde(rename_all = "snake_case")]
56#[non_exhaustive]
57pub enum Substitution {
58 SpecialChars,
59 Attributes,
60 Replacements,
61 Macros,
62 PostReplacements,
63 Normal,
64 Verbatim,
65 Quotes,
66 Callouts,
67}
68
69impl std::fmt::Display for Substitution {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 let name = match self {
72 Self::SpecialChars => "special_chars",
73 Self::Attributes => "attributes",
74 Self::Replacements => "replacements",
75 Self::Macros => "macros",
76 Self::PostReplacements => "post_replacements",
77 Self::Normal => "normal",
78 Self::Verbatim => "verbatim",
79 Self::Quotes => "quotes",
80 Self::Callouts => "callouts",
81 };
82 write!(f, "{name}")
83 }
84}
85
86pub(crate) fn parse_substitution(value: &str) -> Option<Substitution> {
90 match value {
91 "attributes" | "a" => Some(Substitution::Attributes),
92 "replacements" | "r" => Some(Substitution::Replacements),
93 "macros" | "m" => Some(Substitution::Macros),
94 "post_replacements" | "p" => Some(Substitution::PostReplacements),
95 "normal" | "n" => Some(Substitution::Normal),
96 "verbatim" | "v" => Some(Substitution::Verbatim),
97 "quotes" | "q" => Some(Substitution::Quotes),
98 "callouts" => Some(Substitution::Callouts),
99 "specialchars" | "specialcharacters" | "c" => Some(Substitution::SpecialChars),
100 unknown => {
101 tracing::error!(
102 substitution = %unknown,
103 "unknown substitution type, ignoring - check for typos"
104 );
105 None
106 }
107 }
108}
109
110pub const HEADER: &[Substitution] = &[Substitution::SpecialChars, Substitution::Attributes];
112
113pub const NORMAL: &[Substitution] = &[
115 Substitution::SpecialChars,
116 Substitution::Attributes,
117 Substitution::Quotes,
118 Substitution::Replacements,
119 Substitution::Macros,
120 Substitution::PostReplacements,
121];
122
123pub const VERBATIM: &[Substitution] = &[Substitution::SpecialChars, Substitution::Callouts];
125
126#[derive(Clone, Debug, Hash, Eq, PartialEq)]
130pub enum SubstitutionOp {
131 Append(Substitution),
133 Prepend(Substitution),
135 Remove(Substitution),
137}
138
139impl std::fmt::Display for SubstitutionOp {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 match self {
142 Self::Append(sub) => write!(f, "+{sub}"),
143 Self::Prepend(sub) => write!(f, "{sub}+"),
144 Self::Remove(sub) => write!(f, "-{sub}"),
145 }
146 }
147}
148
149#[derive(Clone, Debug, Hash, Eq, PartialEq)]
167pub enum SubstitutionSpec {
168 Explicit(Vec<Substitution>),
170 Modifiers(Vec<SubstitutionOp>),
172}
173
174impl Serialize for SubstitutionSpec {
175 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176 where
177 S: serde::Serializer,
178 {
179 let strings: Vec<String> = match self {
180 Self::Explicit(subs) => subs.iter().map(ToString::to_string).collect(),
181 Self::Modifiers(ops) => ops.iter().map(ToString::to_string).collect(),
182 };
183 strings.serialize(serializer)
184 }
185}
186
187impl SubstitutionSpec {
188 #[must_use]
192 pub fn apply_modifiers(ops: &[SubstitutionOp], default: &[Substitution]) -> Vec<Substitution> {
193 let mut result = default.to_vec();
194 for op in ops {
195 match op {
196 SubstitutionOp::Append(sub) => append_substitution(&mut result, sub),
197 SubstitutionOp::Prepend(sub) => prepend_substitution(&mut result, sub),
198 SubstitutionOp::Remove(sub) => remove_substitution(&mut result, sub),
199 }
200 }
201 result
202 }
203
204 #[must_use]
208 pub fn macros_disabled(&self) -> bool {
209 match self {
210 Self::Explicit(subs) => !subs.contains(&Substitution::Macros),
211 Self::Modifiers(ops) => ops
212 .iter()
213 .any(|op| matches!(op, SubstitutionOp::Remove(Substitution::Macros))),
214 }
215 }
216
217 #[must_use]
221 pub fn attributes_disabled(&self) -> bool {
222 match self {
223 Self::Explicit(subs) => !subs.contains(&Substitution::Attributes),
224 Self::Modifiers(ops) => ops
225 .iter()
226 .any(|op| matches!(op, SubstitutionOp::Remove(Substitution::Attributes))),
227 }
228 }
229
230 #[must_use]
235 pub fn resolve(&self, default: &[Substitution]) -> Vec<Substitution> {
236 match self {
237 SubstitutionSpec::Explicit(subs) => subs.clone(),
238 SubstitutionSpec::Modifiers(ops) => Self::apply_modifiers(ops, default),
239 }
240 }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245enum SubsModifier {
246 Append,
248 Prepend,
250 Remove,
252}
253
254fn parse_subs_part(part: &str) -> (&str, Option<SubsModifier>) {
256 if let Some(name) = part.strip_prefix('+') {
257 (name, Some(SubsModifier::Append))
258 } else if let Some(name) = part.strip_suffix('+') {
259 (name, Some(SubsModifier::Prepend))
260 } else if let Some(name) = part.strip_prefix('-') {
261 (name, Some(SubsModifier::Remove))
262 } else {
263 (part, None)
264 }
265}
266
267#[must_use]
285pub(crate) fn parse_subs_attribute(value: &str) -> SubstitutionSpec {
286 let value = value.trim();
287
288 if value.is_empty() || value == "none" {
290 return SubstitutionSpec::Explicit(Vec::new());
291 }
292
293 let parts: Vec<_> = value
295 .split(',')
296 .map(str::trim)
297 .filter(|p| !p.is_empty())
298 .map(parse_subs_part)
299 .collect();
300
301 let has_modifiers = parts.iter().any(|(_, m)| m.is_some());
303
304 if has_modifiers {
305 let mut ops = Vec::new();
307
308 for (name, modifier) in parts {
309 let Some(sub) = parse_substitution(name) else {
311 continue;
312 };
313
314 match modifier {
315 Some(SubsModifier::Append) => {
316 ops.push(SubstitutionOp::Append(sub));
317 }
318 Some(SubsModifier::Prepend) => {
319 ops.push(SubstitutionOp::Prepend(sub));
320 }
321 Some(SubsModifier::Remove) => {
322 ops.push(SubstitutionOp::Remove(sub));
323 }
324 None => {
325 tracing::warn!(
327 substitution = %name,
328 "plain substitution in modifier context; consider +{name} for clarity"
329 );
330 ops.push(SubstitutionOp::Append(sub));
331 }
332 }
333 }
334 SubstitutionSpec::Modifiers(ops)
335 } else {
336 let mut result = Vec::new();
338 for (name, _) in parts {
339 if let Some(ref sub) = parse_substitution(name) {
340 append_substitution(&mut result, sub);
341 }
342 }
343 SubstitutionSpec::Explicit(result)
344 }
345}
346
347fn expand_substitution(sub: &Substitution) -> &[Substitution] {
351 match sub {
352 Substitution::Normal => NORMAL,
353 Substitution::Verbatim => VERBATIM,
354 Substitution::SpecialChars
355 | Substitution::Attributes
356 | Substitution::Replacements
357 | Substitution::Macros
358 | Substitution::PostReplacements
359 | Substitution::Quotes
360 | Substitution::Callouts => std::slice::from_ref(sub),
361 }
362}
363
364pub(crate) fn append_substitution(result: &mut Vec<Substitution>, sub: &Substitution) {
366 for s in expand_substitution(sub) {
367 if !result.contains(s) {
368 result.push(s.clone());
369 }
370 }
371}
372
373pub(crate) fn prepend_substitution(result: &mut Vec<Substitution>, sub: &Substitution) {
375 for s in expand_substitution(sub).iter().rev() {
377 if !result.contains(s) {
378 result.insert(0, s.clone());
379 }
380 }
381}
382
383pub(crate) fn remove_substitution(result: &mut Vec<Substitution>, sub: &Substitution) {
385 for s in expand_substitution(sub) {
386 result.retain(|x| x != s);
387 }
388}
389
390#[must_use]
411pub fn substitute(
412 text: &str,
413 substitutions: &[Substitution],
414 attributes: &DocumentAttributes,
415) -> String {
416 let mut result = text.to_string();
417 for substitution in substitutions {
418 match substitution {
419 Substitution::Attributes => {
420 let mut expanded = String::with_capacity(result.len());
422 let mut chars = result.chars().peekable();
423
424 while let Some(ch) = chars.next() {
425 if ch == '{' {
426 let mut attr_name = String::new();
427 let mut found_closing_brace = false;
428
429 while let Some(&next_ch) = chars.peek() {
430 if next_ch == '}' {
431 chars.next();
432 found_closing_brace = true;
433 break;
434 }
435 attr_name.push(next_ch);
436 chars.next();
437 }
438
439 if found_closing_brace {
440 match attributes.get(&attr_name) {
441 Some(AttributeValue::Bool(true)) => {
442 }
444 Some(AttributeValue::String(attr_value)) => {
445 expanded.push_str(attr_value);
446 }
447 _ => {
448 expanded.push('{');
450 expanded.push_str(&attr_name);
451 expanded.push('}');
452 }
453 }
454 } else {
455 expanded.push('{');
457 expanded.push_str(&attr_name);
458 }
459 } else {
460 expanded.push(ch);
461 }
462 }
463 result = expanded;
464 }
465 Substitution::SpecialChars
467 | Substitution::Quotes
468 | Substitution::Replacements
469 | Substitution::Macros
470 | Substitution::PostReplacements
471 | Substitution::Callouts => {}
472 Substitution::Normal => {
474 result = substitute(&result, NORMAL, attributes);
475 }
476 Substitution::Verbatim => {
477 result = substitute(&result, VERBATIM, attributes);
478 }
479 }
480 }
481 result
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[allow(clippy::panic)]
490 fn explicit(spec: &SubstitutionSpec) -> &Vec<Substitution> {
491 match spec {
492 SubstitutionSpec::Explicit(subs) => subs,
493 SubstitutionSpec::Modifiers(_) => panic!("Expected Explicit, got Modifiers"),
494 }
495 }
496
497 #[allow(clippy::panic)]
499 fn modifiers(spec: &SubstitutionSpec) -> &Vec<SubstitutionOp> {
500 match spec {
501 SubstitutionSpec::Modifiers(ops) => ops,
502 SubstitutionSpec::Explicit(_) => panic!("Expected Modifiers, got Explicit"),
503 }
504 }
505
506 #[test]
507 fn test_parse_subs_none() {
508 let result = parse_subs_attribute("none");
509 assert!(explicit(&result).is_empty());
510 }
511
512 #[test]
513 fn test_parse_subs_empty_string() {
514 let result = parse_subs_attribute("");
515 assert!(explicit(&result).is_empty());
516 }
517
518 #[test]
519 fn test_parse_subs_none_with_whitespace() {
520 let result = parse_subs_attribute(" none ");
521 assert!(explicit(&result).is_empty());
522 }
523
524 #[test]
525 fn test_parse_subs_specialchars() {
526 let result = parse_subs_attribute("specialchars");
527 assert_eq!(explicit(&result), &vec![Substitution::SpecialChars]);
528 }
529
530 #[test]
531 fn test_parse_subs_specialchars_shorthand() {
532 let result = parse_subs_attribute("c");
533 assert_eq!(explicit(&result), &vec![Substitution::SpecialChars]);
534 }
535
536 #[test]
537 fn test_parse_subs_specialcharacters_alias() {
538 let result = parse_subs_attribute("specialcharacters");
539 assert_eq!(explicit(&result), &vec![Substitution::SpecialChars]);
540 }
541
542 #[test]
543 fn test_parse_subs_normal_expands() {
544 let result = parse_subs_attribute("normal");
545 assert_eq!(explicit(&result), &NORMAL.to_vec());
546 }
547
548 #[test]
549 fn test_parse_subs_verbatim_expands() {
550 let result = parse_subs_attribute("verbatim");
551 assert_eq!(explicit(&result), &VERBATIM.to_vec());
552 }
553
554 #[test]
555 fn test_parse_subs_append_modifier() {
556 let result = parse_subs_attribute("+quotes");
557 let ops = modifiers(&result);
558 assert_eq!(ops, &vec![SubstitutionOp::Append(Substitution::Quotes)]);
559
560 let resolved = result.resolve(VERBATIM);
562 assert!(resolved.contains(&Substitution::SpecialChars));
563 assert!(resolved.contains(&Substitution::Callouts));
564 assert!(resolved.contains(&Substitution::Quotes));
565 assert_eq!(resolved.last(), Some(&Substitution::Quotes));
566 }
567
568 #[test]
569 fn test_parse_subs_prepend_modifier() {
570 let result = parse_subs_attribute("quotes+");
571 let ops = modifiers(&result);
572 assert_eq!(ops, &vec![SubstitutionOp::Prepend(Substitution::Quotes)]);
573
574 let resolved = result.resolve(VERBATIM);
576 assert_eq!(resolved.first(), Some(&Substitution::Quotes));
577 assert!(resolved.contains(&Substitution::SpecialChars));
578 assert!(resolved.contains(&Substitution::Callouts));
579 }
580
581 #[test]
582 fn test_parse_subs_remove_modifier() {
583 let result = parse_subs_attribute("-specialchars");
584 let ops = modifiers(&result);
585 assert_eq!(
586 ops,
587 &vec![SubstitutionOp::Remove(Substitution::SpecialChars)]
588 );
589
590 let resolved = result.resolve(VERBATIM);
592 assert!(!resolved.contains(&Substitution::SpecialChars));
593 assert!(resolved.contains(&Substitution::Callouts));
594 }
595
596 #[test]
597 fn test_parse_subs_remove_all_verbatim() {
598 let result = parse_subs_attribute("-specialchars,-callouts");
599 let ops = modifiers(&result);
600 assert_eq!(ops.len(), 2);
601
602 let resolved = result.resolve(VERBATIM);
604 assert!(resolved.is_empty());
605 }
606
607 #[test]
608 fn test_parse_subs_combined_modifiers() {
609 let result = parse_subs_attribute("+quotes,-callouts");
610 let ops = modifiers(&result);
611 assert_eq!(ops.len(), 2);
612
613 let resolved = result.resolve(VERBATIM);
615 assert!(resolved.contains(&Substitution::SpecialChars)); assert!(resolved.contains(&Substitution::Quotes)); assert!(!resolved.contains(&Substitution::Callouts)); }
619
620 #[test]
621 fn test_parse_subs_ordering_preserved() {
622 let result = parse_subs_attribute("quotes,attributes,specialchars");
623 assert_eq!(
624 explicit(&result),
625 &vec![
626 Substitution::Quotes,
627 Substitution::Attributes,
628 Substitution::SpecialChars
629 ]
630 );
631 }
632
633 #[test]
634 fn test_parse_subs_shorthand_list() {
635 let result = parse_subs_attribute("q,a,c");
636 assert_eq!(
637 explicit(&result),
638 &vec![
639 Substitution::Quotes,
640 Substitution::Attributes,
641 Substitution::SpecialChars
642 ]
643 );
644 }
645
646 #[test]
647 fn test_parse_subs_with_spaces() {
648 let result = parse_subs_attribute(" quotes , attributes ");
649 assert_eq!(
650 explicit(&result),
651 &vec![Substitution::Quotes, Substitution::Attributes]
652 );
653 }
654
655 #[test]
656 fn test_parse_subs_duplicates_ignored() {
657 let result = parse_subs_attribute("quotes,quotes,quotes");
658 assert_eq!(explicit(&result), &vec![Substitution::Quotes]);
659 }
660
661 #[test]
662 fn test_parse_subs_normal_in_list_expands() {
663 let result = parse_subs_attribute("normal");
664 let subs = explicit(&result);
665 assert_eq!(subs.len(), NORMAL.len());
667 for sub in NORMAL {
668 assert!(subs.contains(sub));
669 }
670 }
671
672 #[test]
673 fn test_parse_subs_append_normal_group() {
674 let result = parse_subs_attribute("+normal");
675 let resolved = result.resolve(&[Substitution::Callouts]);
677 assert!(resolved.contains(&Substitution::Callouts));
679 for sub in NORMAL {
680 assert!(resolved.contains(sub));
681 }
682 }
683
684 #[test]
685 fn test_parse_subs_remove_normal_group() {
686 let result = parse_subs_attribute("-normal");
687 let resolved = result.resolve(NORMAL);
689 assert!(resolved.is_empty());
691 }
692
693 #[test]
694 fn test_parse_subs_unknown_is_skipped() {
695 let result = parse_subs_attribute("unknown");
697 assert!(explicit(&result).is_empty());
698 }
699
700 #[test]
701 fn test_parse_subs_unknown_mixed_with_valid() {
702 let result = parse_subs_attribute("quotes,typo,attributes");
704 assert_eq!(
705 explicit(&result),
706 &vec![Substitution::Quotes, Substitution::Attributes]
707 );
708 }
709
710 #[test]
711 fn test_parse_subs_all_individual_types() {
712 assert_eq!(
714 explicit(&parse_subs_attribute("attributes")),
715 &vec![Substitution::Attributes]
716 );
717 assert_eq!(
718 explicit(&parse_subs_attribute("replacements")),
719 &vec![Substitution::Replacements]
720 );
721 assert_eq!(
722 explicit(&parse_subs_attribute("macros")),
723 &vec![Substitution::Macros]
724 );
725 assert_eq!(
726 explicit(&parse_subs_attribute("post_replacements")),
727 &vec![Substitution::PostReplacements]
728 );
729 assert_eq!(
730 explicit(&parse_subs_attribute("quotes")),
731 &vec![Substitution::Quotes]
732 );
733 assert_eq!(
734 explicit(&parse_subs_attribute("callouts")),
735 &vec![Substitution::Callouts]
736 );
737 }
738
739 #[test]
740 fn test_parse_subs_shorthand_types() {
741 assert_eq!(
742 explicit(&parse_subs_attribute("a")),
743 &vec![Substitution::Attributes]
744 );
745 assert_eq!(
746 explicit(&parse_subs_attribute("r")),
747 &vec![Substitution::Replacements]
748 );
749 assert_eq!(
750 explicit(&parse_subs_attribute("m")),
751 &vec![Substitution::Macros]
752 );
753 assert_eq!(
754 explicit(&parse_subs_attribute("p")),
755 &vec![Substitution::PostReplacements]
756 );
757 assert_eq!(
758 explicit(&parse_subs_attribute("q")),
759 &vec![Substitution::Quotes]
760 );
761 assert_eq!(
762 explicit(&parse_subs_attribute("c")),
763 &vec![Substitution::SpecialChars]
764 );
765 }
766
767 #[test]
768 fn test_parse_subs_mixed_modifier_list() {
769 let result = parse_subs_attribute("specialchars,+quotes");
771 let ops = modifiers(&result);
773 assert_eq!(ops.len(), 2); let resolved = result.resolve(VERBATIM);
777 assert!(resolved.contains(&Substitution::SpecialChars));
778 assert!(resolved.contains(&Substitution::Callouts)); assert!(resolved.contains(&Substitution::Quotes)); }
781
782 #[test]
783 fn test_parse_subs_modifier_in_middle() {
784 let result = parse_subs_attribute("attributes,+quotes,-callouts");
786 let ops = modifiers(&result);
787 assert_eq!(ops.len(), 3);
788
789 let resolved = result.resolve(VERBATIM);
791 assert!(resolved.contains(&Substitution::Attributes)); assert!(resolved.contains(&Substitution::Quotes)); assert!(!resolved.contains(&Substitution::Callouts)); }
795
796 #[test]
797 fn test_parse_subs_asciidoctor_example() {
798 let result = parse_subs_attribute("attributes+,+replacements,-callouts");
800 let ops = modifiers(&result);
801 assert_eq!(ops.len(), 3);
802
803 let resolved = result.resolve(VERBATIM);
805 assert_eq!(resolved.first(), Some(&Substitution::Attributes)); assert!(resolved.contains(&Substitution::Replacements)); assert!(!resolved.contains(&Substitution::Callouts)); }
809
810 #[test]
811 fn test_parse_subs_modifier_only_at_end() {
812 let result = parse_subs_attribute("quotes,-specialchars");
814 let ops = modifiers(&result);
816 assert_eq!(ops.len(), 2);
817
818 let resolved = result.resolve(VERBATIM);
820 assert!(resolved.contains(&Substitution::Quotes)); assert!(!resolved.contains(&Substitution::SpecialChars)); assert!(resolved.contains(&Substitution::Callouts)); }
824
825 #[test]
826 fn test_resolve_modifiers_with_normal_baseline() {
827 let result = parse_subs_attribute("-quotes");
830 let resolved = result.resolve(NORMAL);
831
832 assert!(resolved.contains(&Substitution::SpecialChars));
834 assert!(resolved.contains(&Substitution::Attributes));
835 assert!(!resolved.contains(&Substitution::Quotes)); assert!(resolved.contains(&Substitution::Replacements));
837 assert!(resolved.contains(&Substitution::Macros));
838 assert!(resolved.contains(&Substitution::PostReplacements));
839 }
840
841 #[test]
842 fn test_resolve_modifiers_with_verbatim_baseline() {
843 let result = parse_subs_attribute("-quotes");
845 let resolved = result.resolve(VERBATIM);
846
847 assert!(resolved.contains(&Substitution::SpecialChars));
849 assert!(resolved.contains(&Substitution::Callouts));
850 assert!(!resolved.contains(&Substitution::Quotes));
851 }
852
853 #[test]
854 fn test_resolve_explicit_ignores_baseline() {
855 let result = parse_subs_attribute("quotes,attributes");
857 let resolved_normal = result.resolve(NORMAL);
858 let resolved_verbatim = result.resolve(VERBATIM);
859
860 assert_eq!(resolved_normal, resolved_verbatim);
862 assert_eq!(
863 resolved_normal,
864 vec![Substitution::Quotes, Substitution::Attributes]
865 );
866 }
867
868 #[test]
869 fn test_resolve_attribute_references() {
870 let attribute_weight = AttributeValue::String(String::from("weight"));
872 let attribute_mass = AttributeValue::String(String::from("mass"));
873
874 let attribute_volume_repeat = String::from("value {attribute_volume}");
877
878 let mut attributes = DocumentAttributes::default();
879 attributes.insert("weight".into(), attribute_weight.clone());
880 attributes.insert("mass".into(), attribute_mass.clone());
881
882 let resolved = substitute("{weight}", HEADER, &attributes);
884 assert_eq!(resolved, "weight".to_string());
885
886 let resolved = substitute("{weight} {mass}", HEADER, &attributes);
888 assert_eq!(resolved, "weight mass".to_string());
889
890 let resolved = substitute("value {attribute_volume}", HEADER, &attributes);
892 assert_eq!(resolved, attribute_volume_repeat);
893 }
894
895 #[test]
896 fn test_substitute_single_pass_expansion() {
897 let mut attributes = DocumentAttributes::default();
905 attributes.insert("foo".into(), AttributeValue::String("{bar}".to_string()));
906 attributes.insert(
907 "bar".into(),
908 AttributeValue::String("should-not-appear".to_string()),
909 );
910
911 let resolved = substitute("{foo}", HEADER, &attributes);
912 assert_eq!(resolved, "{bar}");
913 }
914
915 #[test]
916 fn test_utf8_boundary_handling() {
917 let attributes = DocumentAttributes::default();
920
921 let values = [
922 ":J::~\x01\x00\x00Ô",
924 "{attr}Ô{missing}日本語",
926 "{attrÔ}test",
928 ];
929 for value in values {
930 let resolved = substitute(value, HEADER, &attributes);
931 assert_eq!(resolved, value);
932 }
933 }
934
935 #[test]
936 fn test_macros_disabled_explicit_without_macros() {
937 let spec = parse_subs_attribute("specialchars");
938 assert!(spec.macros_disabled());
939 }
940
941 #[test]
942 fn test_macros_disabled_explicit_with_macros() {
943 let spec = parse_subs_attribute("macros");
944 assert!(!spec.macros_disabled());
945 }
946
947 #[test]
948 fn test_macros_disabled_explicit_normal_includes_macros() {
949 let spec = parse_subs_attribute("normal");
950 assert!(!spec.macros_disabled());
951 }
952
953 #[test]
954 fn test_macros_disabled_modifier_remove() {
955 let spec = parse_subs_attribute("-macros");
956 assert!(spec.macros_disabled());
957 }
958
959 #[test]
960 fn test_macros_disabled_modifier_add() {
961 let spec = parse_subs_attribute("+macros");
962 assert!(!spec.macros_disabled());
963 }
964
965 #[test]
966 fn test_macros_disabled_explicit_none() {
967 let spec = parse_subs_attribute("none");
968 assert!(spec.macros_disabled());
969 }
970
971 #[test]
972 fn test_attributes_disabled_explicit_without_attributes() {
973 let spec = parse_subs_attribute("specialchars");
974 assert!(spec.attributes_disabled());
975 }
976
977 #[test]
978 fn test_attributes_disabled_explicit_with_attributes() {
979 let spec = parse_subs_attribute("attributes");
980 assert!(!spec.attributes_disabled());
981 }
982
983 #[test]
984 fn test_attributes_disabled_explicit_normal_includes_attributes() {
985 let spec = parse_subs_attribute("normal");
986 assert!(!spec.attributes_disabled());
987 }
988
989 #[test]
990 fn test_attributes_disabled_modifier_remove() {
991 let spec = parse_subs_attribute("-attributes");
992 assert!(spec.attributes_disabled());
993 }
994
995 #[test]
996 fn test_attributes_disabled_modifier_add() {
997 let spec = parse_subs_attribute("+attributes");
998 assert!(!spec.attributes_disabled());
999 }
1000
1001 #[test]
1002 fn test_attributes_disabled_explicit_none() {
1003 let spec = parse_subs_attribute("none");
1004 assert!(spec.attributes_disabled());
1005 }
1006}