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