1use crate::{
2 HasSpan, Parser,
3 attributes::Attrlist,
4 content::{Content, Passthroughs, SubstitutionStep},
5 warnings::WarningType,
6};
7
8#[derive(Clone, Debug, Eq, Hash, PartialEq)]
14pub enum SubstitutionGroup {
15 Normal,
19
20 Title,
23
24 Header,
34
35 Verbatim,
39
40 Pass,
48
49 None,
52
53 AttributeEntryValue,
57
58 Stem,
63
64 Custom(Vec<SubstitutionStep>),
74}
75
76const NORMAL_STEPS: &[SubstitutionStep] = &[
78 SubstitutionStep::SpecialCharacters,
79 SubstitutionStep::Quotes,
80 SubstitutionStep::AttributeReferences,
81 SubstitutionStep::CharacterReplacements,
82 SubstitutionStep::Macros,
83 SubstitutionStep::PostReplacement,
84];
85
86const VERBATIM_STEPS: &[SubstitutionStep] = &[
88 SubstitutionStep::SpecialCharacters,
89 SubstitutionStep::Callouts,
90];
91
92impl SubstitutionGroup {
93 pub(crate) fn from_custom_string(
104 start_from: Option<&Self>,
105 mut custom: &str,
106 ) -> (Self, Vec<String>) {
107 custom = custom.trim();
108
109 if custom == "none" {
110 return (Self::None, vec![]);
111 }
112
113 if custom == "n" || custom == "normal" {
114 return (Self::Normal, vec![]);
115 }
116
117 if custom == "v" || custom == "verbatim" {
118 return (Self::Verbatim, vec![]);
119 }
120
121 let mut tokens: Vec<&str> = custom.split(',').map(str::trim).collect();
122
123 while tokens.last() == Some(&"") {
129 tokens.pop();
130 }
131
132 let mut steps: Vec<SubstitutionStep> = vec![];
133 let mut invalid: Vec<String> = vec![];
134 let mut first = true;
135
136 for mut step in tokens {
137 let is_first = first;
138 first = false;
139
140 let append = if step.starts_with('+') {
141 step = &step[1..];
142 true
143 } else {
144 false
145 };
146
147 let prepend = if !append && step.ends_with('+') {
148 step = &step[0..step.len() - 1];
149 true
150 } else {
151 false
152 };
153
154 let subtract = if !append && !prepend && step.starts_with('-') {
155 step = &step[1..];
156 true
157 } else {
158 false
159 };
160
161 if is_first
162 && let Some(start_from) = start_from
163 && (append || prepend || subtract)
164 {
165 steps = start_from.steps().to_owned();
166 }
167
168 let resolved: &[SubstitutionStep] = match step {
173 "none" => &[],
174 "n" | "normal" => NORMAL_STEPS,
175 "v" | "verbatim" => VERBATIM_STEPS,
176 "c" | "specialcharacters" | "specialchars" => {
177 &[SubstitutionStep::SpecialCharacters]
178 }
179 "q" | "quotes" => &[SubstitutionStep::Quotes],
180 "a" | "attributes" => &[SubstitutionStep::AttributeReferences],
181 "r" | "replacements" => &[SubstitutionStep::CharacterReplacements],
182 "m" | "macros" => &[SubstitutionStep::Macros],
183 "p" | "post_replacements" => &[SubstitutionStep::PostReplacement],
184 "callouts" => &[SubstitutionStep::Callouts],
185 _ => {
186 if !subtract {
191 invalid.push(step.to_owned());
192 }
193
194 continue;
195 }
196 };
197
198 if prepend {
199 for (index, step) in resolved.iter().enumerate() {
200 steps.insert(index, *step);
201 }
202 } else if subtract {
203 steps.retain(|s| !resolved.contains(s));
204 } else {
205 steps.extend_from_slice(resolved);
206 }
207 }
208
209 let mut deduped: Vec<SubstitutionStep> = Vec::with_capacity(steps.len());
214 for step in steps {
215 if !deduped.contains(&step) {
216 deduped.push(step);
217 }
218 }
219
220 (Self::Custom(deduped), invalid)
221 }
222
223 pub(crate) fn apply(
224 &self,
225 content: &mut Content<'_>,
226 parser: &Parser,
227 attrlist: Option<&Attrlist>,
228 ) {
229 let steps = self.steps();
230
231 let passthroughs: Option<Passthroughs> =
232 if steps.contains(&SubstitutionStep::Macros) || self == &Self::Header {
233 Some(Passthroughs::extract_from(content, parser))
234 } else {
235 None
236 };
237
238 for step in steps {
239 step.apply(content, parser, attrlist);
240 }
241
242 if let Some(passthroughs) = passthroughs {
243 passthroughs.restore_to(content, parser);
244
245 content.set_passthroughs(passthroughs.0);
248 }
249
250 content.finalize_deferred(&*parser.renderer);
255 }
256
257 pub(crate) fn override_via_attrlist(
265 &self,
266 attrlist: Option<&Attrlist>,
267 parser: Option<&Parser>,
268 ) -> Self {
269 let mut result = self.clone();
270
271 if let Some(attrlist) = attrlist {
272 if result == SubstitutionGroup::Normal
280 && let Some(block_style) = attrlist.nth_attribute(1).and_then(|a| a.block_style())
281 {
282 result = match block_style {
283 "literal" | "listing" | "source" => SubstitutionGroup::Verbatim,
286
287 "pass" => SubstitutionGroup::None,
290
291 _ => result,
295 };
296 }
297
298 if let Some(subs) = attrlist.named_attribute("subs").map(|attr| attr.value()) {
299 let (sub_group, invalid) = Self::from_custom_string(Some(self), subs);
300
301 if !invalid.is_empty()
302 && let Some(parser) = parser
303 {
304 parser.record_substitution_warning(
305 attrlist.span(),
306 WarningType::InvalidSubstitutionTypeForBlock(invalid.join(", ")),
307 );
308 }
309
310 result = sub_group;
311 }
312 }
313
314 result
315 }
316
317 pub fn steps(&self) -> &[SubstitutionStep] {
326 match self {
327 Self::Normal | Self::Title => NORMAL_STEPS,
328
329 Self::Header | Self::AttributeEntryValue => &[
330 SubstitutionStep::SpecialCharacters,
331 SubstitutionStep::AttributeReferences,
332 ],
333
334 Self::Verbatim => VERBATIM_STEPS,
335
336 Self::Stem => &[SubstitutionStep::SpecialCharacters],
337
338 Self::Pass | Self::None => &[],
339
340 Self::Custom(steps) => steps,
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 #![allow(clippy::unwrap_used)]
348
349 mod stem {
350 use crate::{content::Content, strings::CowStr, tests::prelude::*};
351
352 #[test]
353 fn applies_special_characters_only() {
354 let mut content = Content::from(crate::Span::new("*a* < {color}"));
358 let p = Parser::default();
359 SubstitutionGroup::Stem.apply(&mut content, &p, None);
360 assert_eq!(
361 content.rendered,
362 CowStr::Boxed("*a* < {color}".to_string().into_boxed_str())
363 );
364 }
365 }
366
367 mod from_custom_string {
368 use crate::{
369 content::{Content, SubstitutionStep},
370 strings::CowStr,
371 tests::prelude::*,
372 };
373
374 #[test]
375 fn empty() {
376 assert_eq!(
380 SubstitutionGroup::from_custom_string(None, ""),
381 (SubstitutionGroup::Custom(vec![]), vec![])
382 );
383 }
384
385 #[test]
386 fn empty_entries() {
387 assert_eq!(
392 SubstitutionGroup::from_custom_string(Some(&SubstitutionGroup::Verbatim), ","),
393 (SubstitutionGroup::Custom(vec![]), vec![])
394 );
395
396 assert_eq!(
397 SubstitutionGroup::from_custom_string(None, " , ,"),
398 (SubstitutionGroup::Custom(vec![]), vec![])
399 );
400
401 assert_eq!(
402 SubstitutionGroup::from_custom_string(None, "quotes,"),
403 (
404 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
405 vec![]
406 )
407 );
408
409 assert_eq!(
414 SubstitutionGroup::from_custom_string(None, "quotes,,macros"),
415 (
416 SubstitutionGroup::Custom(vec![
417 SubstitutionStep::Quotes,
418 SubstitutionStep::Macros
419 ]),
420 vec!["".to_owned()]
421 )
422 );
423
424 assert_eq!(
429 SubstitutionGroup::from_custom_string(
430 Some(&SubstitutionGroup::Verbatim),
431 ",+quotes"
432 ),
433 (
434 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
435 vec!["".to_owned()]
436 )
437 );
438 }
439
440 #[test]
441 fn invalid_names() {
442 assert_eq!(
445 SubstitutionGroup::from_custom_string(None, "bogus"),
446 (SubstitutionGroup::Custom(vec![]), vec!["bogus".to_owned()])
447 );
448
449 assert_eq!(
450 SubstitutionGroup::from_custom_string(None, "bogus,quotes"),
451 (
452 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
453 vec!["bogus".to_owned()]
454 )
455 );
456
457 assert_eq!(
460 SubstitutionGroup::from_custom_string(
461 Some(&SubstitutionGroup::Verbatim),
462 "+bogus,quotes"
463 ),
464 (
465 SubstitutionGroup::Custom(vec![
466 SubstitutionStep::SpecialCharacters,
467 SubstitutionStep::Callouts,
468 SubstitutionStep::Quotes,
469 ]),
470 vec!["bogus".to_owned()]
471 )
472 );
473
474 assert_eq!(
478 SubstitutionGroup::from_custom_string(Some(&SubstitutionGroup::Verbatim), "-bogus"),
479 (
480 SubstitutionGroup::Custom(vec![
481 SubstitutionStep::SpecialCharacters,
482 SubstitutionStep::Callouts,
483 ]),
484 vec![]
485 )
486 );
487 }
488
489 #[test]
490 fn none() {
491 assert_eq!(
492 SubstitutionGroup::from_custom_string(None, "none"),
493 (SubstitutionGroup::None, vec![])
494 );
495
496 assert_eq!(
499 SubstitutionGroup::from_custom_string(None, "quotes,none"),
500 (
501 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
502 vec![]
503 )
504 );
505
506 assert_eq!(
507 SubstitutionGroup::from_custom_string(None, "nermal"),
508 (SubstitutionGroup::Custom(vec![]), vec!["nermal".to_owned()])
509 );
510 }
511
512 #[test]
513 fn normal() {
514 assert_eq!(
515 SubstitutionGroup::from_custom_string(None, "n"),
516 (SubstitutionGroup::Normal, vec![])
517 );
518
519 assert_eq!(
520 SubstitutionGroup::from_custom_string(None, "normal"),
521 (SubstitutionGroup::Normal, vec![])
522 );
523 }
524
525 #[test]
526 fn verbatim() {
527 assert_eq!(
528 SubstitutionGroup::from_custom_string(None, "v"),
529 (SubstitutionGroup::Verbatim, vec![])
530 );
531
532 assert_eq!(
533 SubstitutionGroup::from_custom_string(None, "verbatim"),
534 (SubstitutionGroup::Verbatim, vec![])
535 );
536
537 assert_eq!(
538 SubstitutionGroup::from_custom_string(None, "verboten"),
539 (
540 SubstitutionGroup::Custom(vec![]),
541 vec!["verboten".to_owned()]
542 )
543 );
544 }
545
546 #[test]
547 fn special_chars() {
548 assert_eq!(
549 SubstitutionGroup::from_custom_string(None, "c"),
550 (
551 SubstitutionGroup::Custom(vec![SubstitutionStep::SpecialCharacters]),
552 vec![]
553 )
554 );
555
556 assert_eq!(
557 SubstitutionGroup::from_custom_string(None, "specialchars"),
558 (
559 SubstitutionGroup::Custom(vec![SubstitutionStep::SpecialCharacters]),
560 vec![]
561 )
562 );
563 }
564
565 #[test]
566 fn quotes() {
567 assert_eq!(
568 SubstitutionGroup::from_custom_string(None, "q"),
569 (
570 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
571 vec![]
572 )
573 );
574
575 assert_eq!(
576 SubstitutionGroup::from_custom_string(None, "quotes"),
577 (
578 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
579 vec![]
580 )
581 );
582 }
583
584 #[test]
585 fn attributes() {
586 assert_eq!(
587 SubstitutionGroup::from_custom_string(None, "a"),
588 (
589 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences]),
590 vec![]
591 )
592 );
593
594 assert_eq!(
595 SubstitutionGroup::from_custom_string(None, "attributes"),
596 (
597 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences]),
598 vec![]
599 )
600 );
601 }
602
603 #[test]
604 fn replacements() {
605 assert_eq!(
606 SubstitutionGroup::from_custom_string(None, "r"),
607 (
608 SubstitutionGroup::Custom(vec![SubstitutionStep::CharacterReplacements]),
609 vec![]
610 )
611 );
612
613 assert_eq!(
614 SubstitutionGroup::from_custom_string(None, "replacements"),
615 (
616 SubstitutionGroup::Custom(vec![SubstitutionStep::CharacterReplacements]),
617 vec![]
618 )
619 );
620 }
621
622 #[test]
623 fn macros() {
624 assert_eq!(
625 SubstitutionGroup::from_custom_string(None, "m"),
626 (
627 SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]),
628 vec![]
629 )
630 );
631
632 assert_eq!(
633 SubstitutionGroup::from_custom_string(None, "macros"),
634 (
635 SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]),
636 vec![]
637 )
638 );
639 }
640
641 #[test]
642 fn post_replacements() {
643 assert_eq!(
644 SubstitutionGroup::from_custom_string(None, "p"),
645 (
646 SubstitutionGroup::Custom(vec![SubstitutionStep::PostReplacement]),
647 vec![]
648 )
649 );
650
651 assert_eq!(
652 SubstitutionGroup::from_custom_string(None, "post_replacements"),
653 (
654 SubstitutionGroup::Custom(vec![SubstitutionStep::PostReplacement]),
655 vec![]
656 )
657 );
658 }
659
660 #[test]
661 fn multiple() {
662 assert_eq!(
663 SubstitutionGroup::from_custom_string(None, "q,a"),
664 (
665 SubstitutionGroup::Custom(vec![
666 SubstitutionStep::Quotes,
667 SubstitutionStep::AttributeReferences
668 ]),
669 vec![]
670 )
671 );
672
673 assert_eq!(
674 SubstitutionGroup::from_custom_string(None, "q, a"),
675 (
676 SubstitutionGroup::Custom(vec![
677 SubstitutionStep::Quotes,
678 SubstitutionStep::AttributeReferences
679 ]),
680 vec![]
681 )
682 );
683
684 assert_eq!(
685 SubstitutionGroup::from_custom_string(None, "quotes,attributes"),
686 (
687 SubstitutionGroup::Custom(vec![
688 SubstitutionStep::Quotes,
689 SubstitutionStep::AttributeReferences
690 ]),
691 vec![]
692 )
693 );
694
695 assert_eq!(
696 SubstitutionGroup::from_custom_string(None, "x,bogus,no such step"),
697 (
698 SubstitutionGroup::Custom(vec![]),
699 vec![
700 "x".to_owned(),
701 "bogus".to_owned(),
702 "no such step".to_owned()
703 ]
704 )
705 );
706 }
707
708 #[test]
709 fn subtraction() {
710 assert_eq!(
711 SubstitutionGroup::from_custom_string(None, "n,-r"),
712 (
713 SubstitutionGroup::Custom(vec![
714 SubstitutionStep::SpecialCharacters,
715 SubstitutionStep::Quotes,
716 SubstitutionStep::AttributeReferences,
717 SubstitutionStep::Macros,
718 SubstitutionStep::PostReplacement,
719 ]),
720 vec![]
721 )
722 );
723
724 assert_eq!(
725 SubstitutionGroup::from_custom_string(None, "n,-r,-r,-m"),
726 (
727 SubstitutionGroup::Custom(vec![
728 SubstitutionStep::SpecialCharacters,
729 SubstitutionStep::Quotes,
730 SubstitutionStep::AttributeReferences,
731 SubstitutionStep::PostReplacement,
732 ]),
733 vec![]
734 )
735 );
736
737 assert_eq!(
738 SubstitutionGroup::from_custom_string(None, "v,-r"),
739 (
740 SubstitutionGroup::Custom(vec![
741 SubstitutionStep::SpecialCharacters,
742 SubstitutionStep::Callouts,
743 ]),
744 vec![]
745 )
746 );
747
748 assert_eq!(
749 SubstitutionGroup::from_custom_string(None, "v,-c"),
750 (
751 SubstitutionGroup::Custom(vec![SubstitutionStep::Callouts]),
752 vec![]
753 )
754 );
755
756 assert_eq!(
757 SubstitutionGroup::from_custom_string(None, "v,-callouts"),
758 (
759 SubstitutionGroup::Custom(vec![SubstitutionStep::SpecialCharacters,]),
760 vec![]
761 )
762 );
763 }
764
765 #[test]
766 fn addition() {
767 assert_eq!(
771 SubstitutionGroup::from_custom_string(None, "n,r"),
772 (
773 SubstitutionGroup::Custom(vec![
774 SubstitutionStep::SpecialCharacters,
775 SubstitutionStep::Quotes,
776 SubstitutionStep::AttributeReferences,
777 SubstitutionStep::CharacterReplacements,
778 SubstitutionStep::Macros,
779 SubstitutionStep::PostReplacement,
780 ]),
781 vec![]
782 )
783 );
784
785 assert_eq!(
786 SubstitutionGroup::from_custom_string(None, "v,m"),
787 (
788 SubstitutionGroup::Custom(vec![
789 SubstitutionStep::SpecialCharacters,
790 SubstitutionStep::Callouts,
791 SubstitutionStep::Macros,
792 ]),
793 vec![]
794 )
795 );
796 }
797
798 #[test]
799 fn incremental() {
800 assert_eq!(
804 SubstitutionGroup::from_custom_string(None, "n,r"),
805 (
806 SubstitutionGroup::Custom(vec![
807 SubstitutionStep::SpecialCharacters,
808 SubstitutionStep::Quotes,
809 SubstitutionStep::AttributeReferences,
810 SubstitutionStep::CharacterReplacements,
811 SubstitutionStep::Macros,
812 SubstitutionStep::PostReplacement,
813 ]),
814 vec![]
815 )
816 );
817
818 assert_eq!(
819 SubstitutionGroup::from_custom_string(None, "v,m"),
820 (
821 SubstitutionGroup::Custom(vec![
822 SubstitutionStep::SpecialCharacters,
823 SubstitutionStep::Callouts,
824 SubstitutionStep::Macros,
825 ]),
826 vec![]
827 )
828 );
829 }
830
831 #[test]
832 fn group_name_mid_list_expands_in_place_and_dedups() {
833 assert_eq!(
839 SubstitutionGroup::from_custom_string(None, "quotes,normal"),
840 (
841 SubstitutionGroup::Custom(vec![
842 SubstitutionStep::Quotes,
843 SubstitutionStep::SpecialCharacters,
844 SubstitutionStep::AttributeReferences,
845 SubstitutionStep::CharacterReplacements,
846 SubstitutionStep::Macros,
847 SubstitutionStep::PostReplacement,
848 ]),
849 vec![]
850 )
851 );
852
853 assert_eq!(
855 SubstitutionGroup::from_custom_string(None, "m,v"),
856 (
857 SubstitutionGroup::Custom(vec![
858 SubstitutionStep::Macros,
859 SubstitutionStep::SpecialCharacters,
860 SubstitutionStep::Callouts,
861 ]),
862 vec![]
863 )
864 );
865 }
866
867 #[test]
868 fn prepend() {
869 assert_eq!(
870 SubstitutionGroup::from_custom_string(
871 Some(&SubstitutionGroup::Verbatim),
872 "attributes+"
873 ),
874 (
875 SubstitutionGroup::Custom(vec![
876 SubstitutionStep::AttributeReferences,
877 SubstitutionStep::SpecialCharacters,
878 SubstitutionStep::Callouts,
879 ]),
880 vec![]
881 )
882 );
883
884 assert_eq!(
885 SubstitutionGroup::from_custom_string(None, "attributes+"),
886 (
887 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences,]),
888 vec![]
889 )
890 );
891 }
892
893 #[test]
894 fn append() {
895 assert_eq!(
896 SubstitutionGroup::from_custom_string(
897 Some(&SubstitutionGroup::Verbatim),
898 "+attributes"
899 ),
900 (
901 SubstitutionGroup::Custom(vec![
902 SubstitutionStep::SpecialCharacters,
903 SubstitutionStep::Callouts,
904 SubstitutionStep::AttributeReferences,
905 ]),
906 vec![]
907 )
908 );
909
910 assert_eq!(
911 SubstitutionGroup::from_custom_string(None, "attributes+"),
912 (
913 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences,]),
914 vec![]
915 )
916 );
917 }
918
919 #[test]
920 fn subtract() {
921 assert_eq!(
922 SubstitutionGroup::from_custom_string(
923 Some(&SubstitutionGroup::Normal),
924 "-attributes"
925 ),
926 (
927 SubstitutionGroup::Custom(vec![
928 SubstitutionStep::SpecialCharacters,
929 SubstitutionStep::Quotes,
930 SubstitutionStep::CharacterReplacements,
931 SubstitutionStep::Macros,
932 SubstitutionStep::PostReplacement,
933 ]),
934 vec![]
935 )
936 );
937
938 assert_eq!(
939 SubstitutionGroup::from_custom_string(None, "-attributes"),
940 (SubstitutionGroup::Custom(vec![]), vec![])
941 );
942 }
943
944 #[test]
945 fn custom_group_with_macros_preserves_passthroughs() {
946 let custom_group = SubstitutionGroup::from_custom_string(None, "q,m").0;
947
948 let mut content = Content::from(crate::Span::new(
949 "Text with +++pass<through>+++ icon:github[] content.",
950 ));
951 let p = Parser::default();
952 custom_group.apply(&mut content, &p, None);
953
954 assert!(!content.is_empty());
955 assert_eq!(
956 content.rendered,
957 CowStr::Boxed(
958 "Text with pass<through> <span class=\"icon\">[github]</span> content."
959 .to_string()
960 .into_boxed_str()
961 )
962 );
963 }
964 }
965
966 mod override_via_attrlist {
967 use crate::{
968 attributes::{Attrlist, AttrlistContext},
969 tests::prelude::*,
970 };
971
972 fn resolve(base: SubstitutionGroup, attrlist: &str) -> SubstitutionGroup {
975 let p = Parser::default();
976 let attrlist = Attrlist::parse(crate::Span::new(attrlist), &p, AttrlistContext::Block)
977 .item
978 .item;
979
980 base.override_via_attrlist(Some(&attrlist), None)
981 }
982
983 #[test]
984 fn verbatim_masquerade_styles_promote_normal_to_verbatim() {
985 for style in ["literal", "listing", "source"] {
988 assert_eq!(
989 resolve(SubstitutionGroup::Normal, style),
990 SubstitutionGroup::Verbatim,
991 "style `{style}` should map Normal to Verbatim"
992 );
993 }
994 }
995
996 #[test]
997 fn pass_style_suppresses_substitutions_on_normal() {
998 assert_eq!(
999 resolve(SubstitutionGroup::Normal, "pass"),
1000 SubstitutionGroup::None
1001 );
1002 }
1003
1004 #[test]
1005 fn non_masquerade_styles_keep_normal() {
1006 for style in ["normal", "verse", "quote", "sidebar", "example"] {
1010 assert_eq!(
1011 resolve(SubstitutionGroup::Normal, style),
1012 SubstitutionGroup::Normal,
1013 "style `{style}` should keep Normal"
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn style_does_not_override_a_delimited_block_group() {
1020 assert_eq!(
1025 resolve(SubstitutionGroup::Verbatim, "pass"),
1026 SubstitutionGroup::Verbatim
1027 );
1028
1029 assert_eq!(
1030 resolve(SubstitutionGroup::Pass, "source"),
1031 SubstitutionGroup::Pass
1032 );
1033
1034 assert_eq!(
1035 resolve(SubstitutionGroup::Stem, "source"),
1036 SubstitutionGroup::Stem
1037 );
1038 }
1039
1040 #[test]
1041 fn subs_attribute_still_overrides() {
1042 assert_eq!(
1045 resolve(SubstitutionGroup::Normal, "listing,subs=normal"),
1046 SubstitutionGroup::Normal
1047 );
1048
1049 assert_eq!(
1050 resolve(SubstitutionGroup::Verbatim, "subs=none"),
1051 SubstitutionGroup::None
1052 );
1053 }
1054
1055 #[test]
1056 fn warns_on_invalid_subs_name_and_honors_valid_names() {
1057 let mut p = Parser::default();
1062 let doc = p.parse("[subs=\"bogus,quotes\"]\nabc *bold* &\ndef");
1063
1064 let block = doc.child_blocks().next().unwrap();
1065 assert_eq!(
1066 block.rendered_content(),
1067 Some("abc <strong>bold</strong> &\ndef")
1068 );
1069
1070 let warnings: Vec<_> = doc.warnings().collect();
1071 assert_eq!(warnings.len(), 1);
1072 assert_eq!(
1073 warnings.first().unwrap().warning,
1074 crate::warnings::WarningType::InvalidSubstitutionTypeForBlock("bogus".to_owned())
1075 );
1076 }
1077
1078 #[test]
1079 fn no_warning_for_empty_subs_list() {
1080 let mut p = Parser::default();
1083 let doc = p.parse("[subs=\",\"]\n....\ncontent <here>\n....");
1084
1085 let block = doc.child_blocks().next().unwrap();
1086 assert_eq!(block.rendered_content(), Some("content <here>"));
1087
1088 assert_eq!(doc.warnings().count(), 0);
1089 }
1090 }
1091
1092 mod normal {
1093 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1094
1095 #[test]
1096 fn empty() {
1097 let mut content = Content::from(crate::Span::default());
1098 let p = Parser::default();
1099 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1100 assert!(content.is_empty());
1101 assert_eq!(content.rendered, CowStr::Borrowed(""));
1102 }
1103
1104 #[test]
1105 fn basic_non_empty_span() {
1106 let mut content = Content::from(crate::Span::new("blah"));
1107 let p = Parser::default();
1108 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1109 assert!(!content.is_empty());
1110 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1111 }
1112
1113 #[test]
1114 fn match_lt_and_gt() {
1115 let mut content = Content::from(crate::Span::new("bl<ah>"));
1116 let p = Parser::default();
1117 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1118 assert!(!content.is_empty());
1119 assert_eq!(
1120 content.rendered,
1121 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1122 );
1123 }
1124
1125 #[test]
1126 fn match_amp() {
1127 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1128 let p = Parser::default();
1129 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1130 assert!(!content.is_empty());
1131 assert_eq!(
1132 content.rendered,
1133 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1134 );
1135 }
1136
1137 #[test]
1138 fn strong_word() {
1139 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1140 let p = Parser::default();
1141 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1142 assert!(!content.is_empty());
1143 assert_eq!(
1144 content.rendered,
1145 CowStr::Boxed(
1146 "One <strong>word</strong> is strong."
1147 .to_string()
1148 .into_boxed_str()
1149 )
1150 );
1151 }
1152
1153 #[test]
1154 fn strong_word_with_special_chars() {
1155 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1156 let p = Parser::default();
1157 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1158 assert!(!content.is_empty());
1159 assert_eq!(
1160 content.rendered,
1161 CowStr::Boxed(
1162 "One <strong>wo<r>d</strong> is strong."
1163 .to_string()
1164 .into_boxed_str()
1165 )
1166 );
1167 }
1168
1169 #[test]
1170 fn marked_string_with_id() {
1171 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1172 let p = Parser::default();
1173 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1174 assert!(!content.is_empty());
1175 assert_eq!(
1176 content.rendered,
1177 CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
1178 );
1179 }
1180 }
1181
1182 mod attribute_entry_value {
1183 use crate::{
1184 content::Content, parser::ModificationContext, strings::CowStr, tests::prelude::*,
1185 };
1186
1187 #[test]
1188 fn empty() {
1189 let mut content = Content::from(crate::Span::default());
1190 let p = Parser::default();
1191 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1192 assert!(content.is_empty());
1193 assert_eq!(content.rendered, CowStr::Borrowed(""));
1194 }
1195
1196 #[test]
1197 fn basic_non_empty_span() {
1198 let mut content = Content::from(crate::Span::new("blah"));
1199 let p = Parser::default();
1200 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1201 assert!(!content.is_empty());
1202 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1203 }
1204
1205 #[test]
1206 fn match_lt_and_gt() {
1207 let mut content = Content::from(crate::Span::new("bl<ah>"));
1208 let p = Parser::default();
1209 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1210 assert!(!content.is_empty());
1211 assert_eq!(
1212 content.rendered,
1213 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1214 );
1215 }
1216
1217 #[test]
1218 fn match_amp() {
1219 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1220 let p = Parser::default();
1221 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1222 assert!(!content.is_empty());
1223 assert_eq!(
1224 content.rendered,
1225 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1226 );
1227 }
1228
1229 #[test]
1230 fn ignores_strong_word() {
1231 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1232 let p = Parser::default();
1233 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1234 assert!(!content.is_empty());
1235 assert_eq!(
1236 content.rendered,
1237 CowStr::Boxed("One *word* is strong.".to_string().into_boxed_str())
1238 );
1239 }
1240
1241 #[test]
1242 fn special_chars_and_attributes() {
1243 let mut content = Content::from(crate::Span::new("bl<ah> {color}"));
1244
1245 let p = Parser::default().with_intrinsic_attribute(
1246 "color",
1247 "red",
1248 ModificationContext::Anywhere,
1249 );
1250
1251 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1252 assert!(!content.is_empty());
1253 assert_eq!(
1254 content.rendered,
1255 CowStr::Boxed("bl<ah> red".to_string().into_boxed_str())
1256 );
1257 }
1258 }
1259
1260 mod header {
1261 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1262
1263 #[test]
1264 fn empty() {
1265 let mut content = Content::from(crate::Span::default());
1266 let p = Parser::default();
1267 SubstitutionGroup::Header.apply(&mut content, &p, None);
1268 assert!(content.is_empty());
1269 assert_eq!(content.rendered, CowStr::Borrowed(""));
1270 }
1271
1272 #[test]
1273 fn basic_non_empty_span() {
1274 let mut content = Content::from(crate::Span::new("blah"));
1275 let p = Parser::default();
1276 SubstitutionGroup::Header.apply(&mut content, &p, None);
1277 assert!(!content.is_empty());
1278 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1279 }
1280
1281 #[test]
1282 fn match_lt_and_gt() {
1283 let mut content = Content::from(crate::Span::new("bl<ah>"));
1284 let p = Parser::default();
1285 SubstitutionGroup::Header.apply(&mut content, &p, None);
1286 assert!(!content.is_empty());
1287 assert_eq!(
1288 content.rendered,
1289 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1290 );
1291 }
1292
1293 #[test]
1294 fn match_amp() {
1295 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1296 let p = Parser::default();
1297 SubstitutionGroup::Header.apply(&mut content, &p, None);
1298 assert!(!content.is_empty());
1299 assert_eq!(
1300 content.rendered,
1301 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1302 );
1303 }
1304
1305 #[test]
1306 fn ignores_strong_word() {
1307 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1308 let p = Parser::default();
1309 SubstitutionGroup::Header.apply(&mut content, &p, None);
1310 assert!(!content.is_empty());
1311 assert_eq!(content.rendered, CowStr::Borrowed("One *word* is strong."));
1312 }
1313
1314 #[test]
1315 fn ignores_strong_word_with_special_chars() {
1316 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1317 let p = Parser::default();
1318 SubstitutionGroup::Header.apply(&mut content, &p, None);
1319 assert!(!content.is_empty());
1320 assert_eq!(
1321 content.rendered,
1322 CowStr::Boxed("One *wo<r>d* is strong.".to_string().into_boxed_str())
1323 );
1324 }
1325
1326 #[test]
1327 fn ignores_marked_string_with_id() {
1328 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1329 let p = Parser::default();
1330 SubstitutionGroup::Header.apply(&mut content, &p, None);
1331 assert!(!content.is_empty());
1332 assert_eq!(content.rendered, CowStr::Borrowed("[#id]#a few words#"));
1333 }
1334 }
1335
1336 mod title {
1337 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1338
1339 #[test]
1340 fn empty() {
1341 let mut content = Content::from(crate::Span::default());
1342 let p = Parser::default();
1343 SubstitutionGroup::Title.apply(&mut content, &p, None);
1344 assert!(content.is_empty());
1345 assert_eq!(content.rendered, CowStr::Borrowed(""));
1346 }
1347
1348 #[test]
1349 fn basic_non_empty_span() {
1350 let mut content = Content::from(crate::Span::new("blah"));
1351 let p = Parser::default();
1352 SubstitutionGroup::Title.apply(&mut content, &p, None);
1353 assert!(!content.is_empty());
1354 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1355 }
1356
1357 #[test]
1358 fn match_lt_and_gt() {
1359 let mut content = Content::from(crate::Span::new("bl<ah>"));
1360 let p = Parser::default();
1361 SubstitutionGroup::Title.apply(&mut content, &p, None);
1362 assert!(!content.is_empty());
1363 assert_eq!(
1364 content.rendered,
1365 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1366 );
1367 }
1368
1369 #[test]
1370 fn match_amp() {
1371 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1372 let p = Parser::default();
1373 SubstitutionGroup::Title.apply(&mut content, &p, None);
1374 assert!(!content.is_empty());
1375 assert_eq!(
1376 content.rendered,
1377 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1378 );
1379 }
1380
1381 #[test]
1382 fn strong_word() {
1383 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1384 let p = Parser::default();
1385 SubstitutionGroup::Title.apply(&mut content, &p, None);
1386 assert!(!content.is_empty());
1387 assert_eq!(
1388 content.rendered,
1389 CowStr::Boxed(
1390 "One <strong>word</strong> is strong."
1391 .to_string()
1392 .into_boxed_str()
1393 )
1394 );
1395 }
1396
1397 #[test]
1398 fn strong_word_with_special_chars() {
1399 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1400 let p = Parser::default();
1401 SubstitutionGroup::Title.apply(&mut content, &p, None);
1402 assert!(!content.is_empty());
1403 assert_eq!(
1404 content.rendered,
1405 CowStr::Boxed(
1406 "One <strong>wo<r>d</strong> is strong."
1407 .to_string()
1408 .into_boxed_str()
1409 )
1410 );
1411 }
1412
1413 #[test]
1414 fn marked_string_with_id() {
1415 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1416 let p = Parser::default();
1417 SubstitutionGroup::Title.apply(&mut content, &p, None);
1418 assert!(!content.is_empty());
1419 assert_eq!(
1420 content.rendered,
1421 CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
1422 );
1423 }
1424
1425 #[test]
1426 fn title_behaves_same_as_normal() {
1427 let test_input = "One *wo<r>d* is strong with [#id]#marked text#.";
1428
1429 let mut title_content = Content::from(crate::Span::new(test_input));
1430 let mut normal_content = Content::from(crate::Span::new(test_input));
1431 let p = Parser::default();
1432
1433 SubstitutionGroup::Title.apply(&mut title_content, &p, None);
1434 SubstitutionGroup::Normal.apply(&mut normal_content, &p, None);
1435
1436 assert_eq!(title_content.rendered, normal_content.rendered);
1438 }
1439 }
1440}