1use crate::{
2 HasSpan, Parser,
3 attributes::Attrlist,
4 content::{Content, Passthroughs, SubstitutionStep},
5 warnings::WarningType,
6};
7
8#[derive(Clone, Debug, Eq, 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
246 content.finalize_deferred(&*parser.renderer);
251 }
252
253 pub(crate) fn override_via_attrlist(
261 &self,
262 attrlist: Option<&Attrlist>,
263 parser: Option<&Parser>,
264 ) -> Self {
265 let mut result = self.clone();
266
267 if let Some(attrlist) = attrlist {
268 if result == SubstitutionGroup::Normal
276 && let Some(block_style) = attrlist.nth_attribute(1).and_then(|a| a.block_style())
277 {
278 result = match block_style {
279 "literal" | "listing" | "source" => SubstitutionGroup::Verbatim,
282
283 "pass" => SubstitutionGroup::None,
286
287 _ => result,
291 };
292 }
293
294 if let Some(subs) = attrlist.named_attribute("subs").map(|attr| attr.value()) {
295 let (sub_group, invalid) = Self::from_custom_string(Some(self), subs);
296
297 if !invalid.is_empty()
298 && let Some(parser) = parser
299 {
300 parser.record_substitution_warning(
301 attrlist.span(),
302 WarningType::InvalidSubstitutionTypeForBlock(invalid.join(", ")),
303 );
304 }
305
306 result = sub_group;
307 }
308 }
309
310 result
311 }
312
313 fn steps(&self) -> &[SubstitutionStep] {
314 match self {
315 Self::Normal | Self::Title => NORMAL_STEPS,
316
317 Self::Header | Self::AttributeEntryValue => &[
318 SubstitutionStep::SpecialCharacters,
319 SubstitutionStep::AttributeReferences,
320 ],
321
322 Self::Verbatim => VERBATIM_STEPS,
323
324 Self::Stem => &[SubstitutionStep::SpecialCharacters],
325
326 Self::Pass | Self::None => &[],
327
328 Self::Custom(steps) => steps,
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 #![allow(clippy::unwrap_used)]
336
337 mod stem {
338 use crate::{content::Content, strings::CowStr, tests::prelude::*};
339
340 #[test]
341 fn applies_special_characters_only() {
342 let mut content = Content::from(crate::Span::new("*a* < {color}"));
346 let p = Parser::default();
347 SubstitutionGroup::Stem.apply(&mut content, &p, None);
348 assert_eq!(
349 content.rendered,
350 CowStr::Boxed("*a* < {color}".to_string().into_boxed_str())
351 );
352 }
353 }
354
355 mod from_custom_string {
356 use crate::{
357 content::{Content, SubstitutionStep},
358 strings::CowStr,
359 tests::prelude::*,
360 };
361
362 #[test]
363 fn empty() {
364 assert_eq!(
368 SubstitutionGroup::from_custom_string(None, ""),
369 (SubstitutionGroup::Custom(vec![]), vec![])
370 );
371 }
372
373 #[test]
374 fn empty_entries() {
375 assert_eq!(
380 SubstitutionGroup::from_custom_string(Some(&SubstitutionGroup::Verbatim), ","),
381 (SubstitutionGroup::Custom(vec![]), vec![])
382 );
383
384 assert_eq!(
385 SubstitutionGroup::from_custom_string(None, " , ,"),
386 (SubstitutionGroup::Custom(vec![]), vec![])
387 );
388
389 assert_eq!(
390 SubstitutionGroup::from_custom_string(None, "quotes,"),
391 (
392 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
393 vec![]
394 )
395 );
396
397 assert_eq!(
402 SubstitutionGroup::from_custom_string(None, "quotes,,macros"),
403 (
404 SubstitutionGroup::Custom(vec![
405 SubstitutionStep::Quotes,
406 SubstitutionStep::Macros
407 ]),
408 vec!["".to_owned()]
409 )
410 );
411
412 assert_eq!(
417 SubstitutionGroup::from_custom_string(
418 Some(&SubstitutionGroup::Verbatim),
419 ",+quotes"
420 ),
421 (
422 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
423 vec!["".to_owned()]
424 )
425 );
426 }
427
428 #[test]
429 fn invalid_names() {
430 assert_eq!(
433 SubstitutionGroup::from_custom_string(None, "bogus"),
434 (SubstitutionGroup::Custom(vec![]), vec!["bogus".to_owned()])
435 );
436
437 assert_eq!(
438 SubstitutionGroup::from_custom_string(None, "bogus,quotes"),
439 (
440 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
441 vec!["bogus".to_owned()]
442 )
443 );
444
445 assert_eq!(
448 SubstitutionGroup::from_custom_string(
449 Some(&SubstitutionGroup::Verbatim),
450 "+bogus,quotes"
451 ),
452 (
453 SubstitutionGroup::Custom(vec![
454 SubstitutionStep::SpecialCharacters,
455 SubstitutionStep::Callouts,
456 SubstitutionStep::Quotes,
457 ]),
458 vec!["bogus".to_owned()]
459 )
460 );
461
462 assert_eq!(
466 SubstitutionGroup::from_custom_string(Some(&SubstitutionGroup::Verbatim), "-bogus"),
467 (
468 SubstitutionGroup::Custom(vec![
469 SubstitutionStep::SpecialCharacters,
470 SubstitutionStep::Callouts,
471 ]),
472 vec![]
473 )
474 );
475 }
476
477 #[test]
478 fn none() {
479 assert_eq!(
480 SubstitutionGroup::from_custom_string(None, "none"),
481 (SubstitutionGroup::None, vec![])
482 );
483
484 assert_eq!(
487 SubstitutionGroup::from_custom_string(None, "quotes,none"),
488 (
489 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
490 vec![]
491 )
492 );
493
494 assert_eq!(
495 SubstitutionGroup::from_custom_string(None, "nermal"),
496 (SubstitutionGroup::Custom(vec![]), vec!["nermal".to_owned()])
497 );
498 }
499
500 #[test]
501 fn normal() {
502 assert_eq!(
503 SubstitutionGroup::from_custom_string(None, "n"),
504 (SubstitutionGroup::Normal, vec![])
505 );
506
507 assert_eq!(
508 SubstitutionGroup::from_custom_string(None, "normal"),
509 (SubstitutionGroup::Normal, vec![])
510 );
511 }
512
513 #[test]
514 fn verbatim() {
515 assert_eq!(
516 SubstitutionGroup::from_custom_string(None, "v"),
517 (SubstitutionGroup::Verbatim, vec![])
518 );
519
520 assert_eq!(
521 SubstitutionGroup::from_custom_string(None, "verbatim"),
522 (SubstitutionGroup::Verbatim, vec![])
523 );
524
525 assert_eq!(
526 SubstitutionGroup::from_custom_string(None, "verboten"),
527 (
528 SubstitutionGroup::Custom(vec![]),
529 vec!["verboten".to_owned()]
530 )
531 );
532 }
533
534 #[test]
535 fn special_chars() {
536 assert_eq!(
537 SubstitutionGroup::from_custom_string(None, "c"),
538 (
539 SubstitutionGroup::Custom(vec![SubstitutionStep::SpecialCharacters]),
540 vec![]
541 )
542 );
543
544 assert_eq!(
545 SubstitutionGroup::from_custom_string(None, "specialchars"),
546 (
547 SubstitutionGroup::Custom(vec![SubstitutionStep::SpecialCharacters]),
548 vec![]
549 )
550 );
551 }
552
553 #[test]
554 fn quotes() {
555 assert_eq!(
556 SubstitutionGroup::from_custom_string(None, "q"),
557 (
558 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
559 vec![]
560 )
561 );
562
563 assert_eq!(
564 SubstitutionGroup::from_custom_string(None, "quotes"),
565 (
566 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
567 vec![]
568 )
569 );
570 }
571
572 #[test]
573 fn attributes() {
574 assert_eq!(
575 SubstitutionGroup::from_custom_string(None, "a"),
576 (
577 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences]),
578 vec![]
579 )
580 );
581
582 assert_eq!(
583 SubstitutionGroup::from_custom_string(None, "attributes"),
584 (
585 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences]),
586 vec![]
587 )
588 );
589 }
590
591 #[test]
592 fn replacements() {
593 assert_eq!(
594 SubstitutionGroup::from_custom_string(None, "r"),
595 (
596 SubstitutionGroup::Custom(vec![SubstitutionStep::CharacterReplacements]),
597 vec![]
598 )
599 );
600
601 assert_eq!(
602 SubstitutionGroup::from_custom_string(None, "replacements"),
603 (
604 SubstitutionGroup::Custom(vec![SubstitutionStep::CharacterReplacements]),
605 vec![]
606 )
607 );
608 }
609
610 #[test]
611 fn macros() {
612 assert_eq!(
613 SubstitutionGroup::from_custom_string(None, "m"),
614 (
615 SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]),
616 vec![]
617 )
618 );
619
620 assert_eq!(
621 SubstitutionGroup::from_custom_string(None, "macros"),
622 (
623 SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]),
624 vec![]
625 )
626 );
627 }
628
629 #[test]
630 fn post_replacements() {
631 assert_eq!(
632 SubstitutionGroup::from_custom_string(None, "p"),
633 (
634 SubstitutionGroup::Custom(vec![SubstitutionStep::PostReplacement]),
635 vec![]
636 )
637 );
638
639 assert_eq!(
640 SubstitutionGroup::from_custom_string(None, "post_replacements"),
641 (
642 SubstitutionGroup::Custom(vec![SubstitutionStep::PostReplacement]),
643 vec![]
644 )
645 );
646 }
647
648 #[test]
649 fn multiple() {
650 assert_eq!(
651 SubstitutionGroup::from_custom_string(None, "q,a"),
652 (
653 SubstitutionGroup::Custom(vec![
654 SubstitutionStep::Quotes,
655 SubstitutionStep::AttributeReferences
656 ]),
657 vec![]
658 )
659 );
660
661 assert_eq!(
662 SubstitutionGroup::from_custom_string(None, "q, a"),
663 (
664 SubstitutionGroup::Custom(vec![
665 SubstitutionStep::Quotes,
666 SubstitutionStep::AttributeReferences
667 ]),
668 vec![]
669 )
670 );
671
672 assert_eq!(
673 SubstitutionGroup::from_custom_string(None, "quotes,attributes"),
674 (
675 SubstitutionGroup::Custom(vec![
676 SubstitutionStep::Quotes,
677 SubstitutionStep::AttributeReferences
678 ]),
679 vec![]
680 )
681 );
682
683 assert_eq!(
684 SubstitutionGroup::from_custom_string(None, "x,bogus,no such step"),
685 (
686 SubstitutionGroup::Custom(vec![]),
687 vec![
688 "x".to_owned(),
689 "bogus".to_owned(),
690 "no such step".to_owned()
691 ]
692 )
693 );
694 }
695
696 #[test]
697 fn subtraction() {
698 assert_eq!(
699 SubstitutionGroup::from_custom_string(None, "n,-r"),
700 (
701 SubstitutionGroup::Custom(vec![
702 SubstitutionStep::SpecialCharacters,
703 SubstitutionStep::Quotes,
704 SubstitutionStep::AttributeReferences,
705 SubstitutionStep::Macros,
706 SubstitutionStep::PostReplacement,
707 ]),
708 vec![]
709 )
710 );
711
712 assert_eq!(
713 SubstitutionGroup::from_custom_string(None, "n,-r,-r,-m"),
714 (
715 SubstitutionGroup::Custom(vec![
716 SubstitutionStep::SpecialCharacters,
717 SubstitutionStep::Quotes,
718 SubstitutionStep::AttributeReferences,
719 SubstitutionStep::PostReplacement,
720 ]),
721 vec![]
722 )
723 );
724
725 assert_eq!(
726 SubstitutionGroup::from_custom_string(None, "v,-r"),
727 (
728 SubstitutionGroup::Custom(vec![
729 SubstitutionStep::SpecialCharacters,
730 SubstitutionStep::Callouts,
731 ]),
732 vec![]
733 )
734 );
735
736 assert_eq!(
737 SubstitutionGroup::from_custom_string(None, "v,-c"),
738 (
739 SubstitutionGroup::Custom(vec![SubstitutionStep::Callouts]),
740 vec![]
741 )
742 );
743
744 assert_eq!(
745 SubstitutionGroup::from_custom_string(None, "v,-callouts"),
746 (
747 SubstitutionGroup::Custom(vec![SubstitutionStep::SpecialCharacters,]),
748 vec![]
749 )
750 );
751 }
752
753 #[test]
754 fn addition() {
755 assert_eq!(
759 SubstitutionGroup::from_custom_string(None, "n,r"),
760 (
761 SubstitutionGroup::Custom(vec![
762 SubstitutionStep::SpecialCharacters,
763 SubstitutionStep::Quotes,
764 SubstitutionStep::AttributeReferences,
765 SubstitutionStep::CharacterReplacements,
766 SubstitutionStep::Macros,
767 SubstitutionStep::PostReplacement,
768 ]),
769 vec![]
770 )
771 );
772
773 assert_eq!(
774 SubstitutionGroup::from_custom_string(None, "v,m"),
775 (
776 SubstitutionGroup::Custom(vec![
777 SubstitutionStep::SpecialCharacters,
778 SubstitutionStep::Callouts,
779 SubstitutionStep::Macros,
780 ]),
781 vec![]
782 )
783 );
784 }
785
786 #[test]
787 fn incremental() {
788 assert_eq!(
792 SubstitutionGroup::from_custom_string(None, "n,r"),
793 (
794 SubstitutionGroup::Custom(vec![
795 SubstitutionStep::SpecialCharacters,
796 SubstitutionStep::Quotes,
797 SubstitutionStep::AttributeReferences,
798 SubstitutionStep::CharacterReplacements,
799 SubstitutionStep::Macros,
800 SubstitutionStep::PostReplacement,
801 ]),
802 vec![]
803 )
804 );
805
806 assert_eq!(
807 SubstitutionGroup::from_custom_string(None, "v,m"),
808 (
809 SubstitutionGroup::Custom(vec![
810 SubstitutionStep::SpecialCharacters,
811 SubstitutionStep::Callouts,
812 SubstitutionStep::Macros,
813 ]),
814 vec![]
815 )
816 );
817 }
818
819 #[test]
820 fn group_name_mid_list_expands_in_place_and_dedups() {
821 assert_eq!(
827 SubstitutionGroup::from_custom_string(None, "quotes,normal"),
828 (
829 SubstitutionGroup::Custom(vec![
830 SubstitutionStep::Quotes,
831 SubstitutionStep::SpecialCharacters,
832 SubstitutionStep::AttributeReferences,
833 SubstitutionStep::CharacterReplacements,
834 SubstitutionStep::Macros,
835 SubstitutionStep::PostReplacement,
836 ]),
837 vec![]
838 )
839 );
840
841 assert_eq!(
843 SubstitutionGroup::from_custom_string(None, "m,v"),
844 (
845 SubstitutionGroup::Custom(vec![
846 SubstitutionStep::Macros,
847 SubstitutionStep::SpecialCharacters,
848 SubstitutionStep::Callouts,
849 ]),
850 vec![]
851 )
852 );
853 }
854
855 #[test]
856 fn prepend() {
857 assert_eq!(
858 SubstitutionGroup::from_custom_string(
859 Some(&SubstitutionGroup::Verbatim),
860 "attributes+"
861 ),
862 (
863 SubstitutionGroup::Custom(vec![
864 SubstitutionStep::AttributeReferences,
865 SubstitutionStep::SpecialCharacters,
866 SubstitutionStep::Callouts,
867 ]),
868 vec![]
869 )
870 );
871
872 assert_eq!(
873 SubstitutionGroup::from_custom_string(None, "attributes+"),
874 (
875 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences,]),
876 vec![]
877 )
878 );
879 }
880
881 #[test]
882 fn append() {
883 assert_eq!(
884 SubstitutionGroup::from_custom_string(
885 Some(&SubstitutionGroup::Verbatim),
886 "+attributes"
887 ),
888 (
889 SubstitutionGroup::Custom(vec![
890 SubstitutionStep::SpecialCharacters,
891 SubstitutionStep::Callouts,
892 SubstitutionStep::AttributeReferences,
893 ]),
894 vec![]
895 )
896 );
897
898 assert_eq!(
899 SubstitutionGroup::from_custom_string(None, "attributes+"),
900 (
901 SubstitutionGroup::Custom(vec![SubstitutionStep::AttributeReferences,]),
902 vec![]
903 )
904 );
905 }
906
907 #[test]
908 fn subtract() {
909 assert_eq!(
910 SubstitutionGroup::from_custom_string(
911 Some(&SubstitutionGroup::Normal),
912 "-attributes"
913 ),
914 (
915 SubstitutionGroup::Custom(vec![
916 SubstitutionStep::SpecialCharacters,
917 SubstitutionStep::Quotes,
918 SubstitutionStep::CharacterReplacements,
919 SubstitutionStep::Macros,
920 SubstitutionStep::PostReplacement,
921 ]),
922 vec![]
923 )
924 );
925
926 assert_eq!(
927 SubstitutionGroup::from_custom_string(None, "-attributes"),
928 (SubstitutionGroup::Custom(vec![]), vec![])
929 );
930 }
931
932 #[test]
933 fn custom_group_with_macros_preserves_passthroughs() {
934 let custom_group = SubstitutionGroup::from_custom_string(None, "q,m").0;
935
936 let mut content = Content::from(crate::Span::new(
937 "Text with +++pass<through>+++ icon:github[] content.",
938 ));
939 let p = Parser::default();
940 custom_group.apply(&mut content, &p, None);
941
942 assert!(!content.is_empty());
943 assert_eq!(
944 content.rendered,
945 CowStr::Boxed(
946 "Text with pass<through> <span class=\"icon\">[github]</span> content."
947 .to_string()
948 .into_boxed_str()
949 )
950 );
951 }
952 }
953
954 mod override_via_attrlist {
955 use crate::{
956 attributes::{Attrlist, AttrlistContext},
957 tests::prelude::*,
958 };
959
960 fn resolve(base: SubstitutionGroup, attrlist: &str) -> SubstitutionGroup {
963 let p = Parser::default();
964 let attrlist = Attrlist::parse(crate::Span::new(attrlist), &p, AttrlistContext::Block)
965 .item
966 .item;
967
968 base.override_via_attrlist(Some(&attrlist), None)
969 }
970
971 #[test]
972 fn verbatim_masquerade_styles_promote_normal_to_verbatim() {
973 for style in ["literal", "listing", "source"] {
976 assert_eq!(
977 resolve(SubstitutionGroup::Normal, style),
978 SubstitutionGroup::Verbatim,
979 "style `{style}` should map Normal to Verbatim"
980 );
981 }
982 }
983
984 #[test]
985 fn pass_style_suppresses_substitutions_on_normal() {
986 assert_eq!(
987 resolve(SubstitutionGroup::Normal, "pass"),
988 SubstitutionGroup::None
989 );
990 }
991
992 #[test]
993 fn non_masquerade_styles_keep_normal() {
994 for style in ["normal", "verse", "quote", "sidebar", "example"] {
998 assert_eq!(
999 resolve(SubstitutionGroup::Normal, style),
1000 SubstitutionGroup::Normal,
1001 "style `{style}` should keep Normal"
1002 );
1003 }
1004 }
1005
1006 #[test]
1007 fn style_does_not_override_a_delimited_block_group() {
1008 assert_eq!(
1013 resolve(SubstitutionGroup::Verbatim, "pass"),
1014 SubstitutionGroup::Verbatim
1015 );
1016
1017 assert_eq!(
1018 resolve(SubstitutionGroup::Pass, "source"),
1019 SubstitutionGroup::Pass
1020 );
1021
1022 assert_eq!(
1023 resolve(SubstitutionGroup::Stem, "source"),
1024 SubstitutionGroup::Stem
1025 );
1026 }
1027
1028 #[test]
1029 fn subs_attribute_still_overrides() {
1030 assert_eq!(
1033 resolve(SubstitutionGroup::Normal, "listing,subs=normal"),
1034 SubstitutionGroup::Normal
1035 );
1036
1037 assert_eq!(
1038 resolve(SubstitutionGroup::Verbatim, "subs=none"),
1039 SubstitutionGroup::None
1040 );
1041 }
1042
1043 #[test]
1044 fn warns_on_invalid_subs_name_and_honors_valid_names() {
1045 let mut p = Parser::default();
1050 let doc = p.parse("[subs=\"bogus,quotes\"]\nabc *bold* &\ndef");
1051
1052 let block = doc.child_blocks().next().unwrap();
1053 assert_eq!(
1054 block.rendered_content(),
1055 Some("abc <strong>bold</strong> &\ndef")
1056 );
1057
1058 let warnings: Vec<_> = doc.warnings().collect();
1059 assert_eq!(warnings.len(), 1);
1060 assert_eq!(
1061 warnings.first().unwrap().warning,
1062 crate::warnings::WarningType::InvalidSubstitutionTypeForBlock("bogus".to_owned())
1063 );
1064 }
1065
1066 #[test]
1067 fn no_warning_for_empty_subs_list() {
1068 let mut p = Parser::default();
1071 let doc = p.parse("[subs=\",\"]\n....\ncontent <here>\n....");
1072
1073 let block = doc.child_blocks().next().unwrap();
1074 assert_eq!(block.rendered_content(), Some("content <here>"));
1075
1076 assert_eq!(doc.warnings().count(), 0);
1077 }
1078 }
1079
1080 mod normal {
1081 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1082
1083 #[test]
1084 fn empty() {
1085 let mut content = Content::from(crate::Span::default());
1086 let p = Parser::default();
1087 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1088 assert!(content.is_empty());
1089 assert_eq!(content.rendered, CowStr::Borrowed(""));
1090 }
1091
1092 #[test]
1093 fn basic_non_empty_span() {
1094 let mut content = Content::from(crate::Span::new("blah"));
1095 let p = Parser::default();
1096 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1097 assert!(!content.is_empty());
1098 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1099 }
1100
1101 #[test]
1102 fn match_lt_and_gt() {
1103 let mut content = Content::from(crate::Span::new("bl<ah>"));
1104 let p = Parser::default();
1105 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1106 assert!(!content.is_empty());
1107 assert_eq!(
1108 content.rendered,
1109 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1110 );
1111 }
1112
1113 #[test]
1114 fn match_amp() {
1115 let mut content = Content::from(crate::Span::new("bl<a&h>"));
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<a&h>".to_string().into_boxed_str())
1122 );
1123 }
1124
1125 #[test]
1126 fn strong_word() {
1127 let mut content = Content::from(crate::Span::new("One *word* is strong."));
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(
1134 "One <strong>word</strong> is strong."
1135 .to_string()
1136 .into_boxed_str()
1137 )
1138 );
1139 }
1140
1141 #[test]
1142 fn strong_word_with_special_chars() {
1143 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1144 let p = Parser::default();
1145 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1146 assert!(!content.is_empty());
1147 assert_eq!(
1148 content.rendered,
1149 CowStr::Boxed(
1150 "One <strong>wo<r>d</strong> is strong."
1151 .to_string()
1152 .into_boxed_str()
1153 )
1154 );
1155 }
1156
1157 #[test]
1158 fn marked_string_with_id() {
1159 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1160 let p = Parser::default();
1161 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1162 assert!(!content.is_empty());
1163 assert_eq!(
1164 content.rendered,
1165 CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
1166 );
1167 }
1168 }
1169
1170 mod attribute_entry_value {
1171 use crate::{
1172 content::Content, parser::ModificationContext, strings::CowStr, tests::prelude::*,
1173 };
1174
1175 #[test]
1176 fn empty() {
1177 let mut content = Content::from(crate::Span::default());
1178 let p = Parser::default();
1179 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1180 assert!(content.is_empty());
1181 assert_eq!(content.rendered, CowStr::Borrowed(""));
1182 }
1183
1184 #[test]
1185 fn basic_non_empty_span() {
1186 let mut content = Content::from(crate::Span::new("blah"));
1187 let p = Parser::default();
1188 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1189 assert!(!content.is_empty());
1190 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1191 }
1192
1193 #[test]
1194 fn match_lt_and_gt() {
1195 let mut content = Content::from(crate::Span::new("bl<ah>"));
1196 let p = Parser::default();
1197 SubstitutionGroup::Normal.apply(&mut content, &p, None);
1198 assert!(!content.is_empty());
1199 assert_eq!(
1200 content.rendered,
1201 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1202 );
1203 }
1204
1205 #[test]
1206 fn match_amp() {
1207 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1208 let p = Parser::default();
1209 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1210 assert!(!content.is_empty());
1211 assert_eq!(
1212 content.rendered,
1213 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1214 );
1215 }
1216
1217 #[test]
1218 fn ignores_strong_word() {
1219 let mut content = Content::from(crate::Span::new("One *word* is strong."));
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("One *word* is strong.".to_string().into_boxed_str())
1226 );
1227 }
1228
1229 #[test]
1230 fn special_chars_and_attributes() {
1231 let mut content = Content::from(crate::Span::new("bl<ah> {color}"));
1232
1233 let p = Parser::default().with_intrinsic_attribute(
1234 "color",
1235 "red",
1236 ModificationContext::Anywhere,
1237 );
1238
1239 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
1240 assert!(!content.is_empty());
1241 assert_eq!(
1242 content.rendered,
1243 CowStr::Boxed("bl<ah> red".to_string().into_boxed_str())
1244 );
1245 }
1246 }
1247
1248 mod header {
1249 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1250
1251 #[test]
1252 fn empty() {
1253 let mut content = Content::from(crate::Span::default());
1254 let p = Parser::default();
1255 SubstitutionGroup::Header.apply(&mut content, &p, None);
1256 assert!(content.is_empty());
1257 assert_eq!(content.rendered, CowStr::Borrowed(""));
1258 }
1259
1260 #[test]
1261 fn basic_non_empty_span() {
1262 let mut content = Content::from(crate::Span::new("blah"));
1263 let p = Parser::default();
1264 SubstitutionGroup::Header.apply(&mut content, &p, None);
1265 assert!(!content.is_empty());
1266 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1267 }
1268
1269 #[test]
1270 fn match_lt_and_gt() {
1271 let mut content = Content::from(crate::Span::new("bl<ah>"));
1272 let p = Parser::default();
1273 SubstitutionGroup::Header.apply(&mut content, &p, None);
1274 assert!(!content.is_empty());
1275 assert_eq!(
1276 content.rendered,
1277 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1278 );
1279 }
1280
1281 #[test]
1282 fn match_amp() {
1283 let mut content = Content::from(crate::Span::new("bl<a&h>"));
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<a&h>".to_string().into_boxed_str())
1290 );
1291 }
1292
1293 #[test]
1294 fn ignores_strong_word() {
1295 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1296 let p = Parser::default();
1297 SubstitutionGroup::Header.apply(&mut content, &p, None);
1298 assert!(!content.is_empty());
1299 assert_eq!(content.rendered, CowStr::Borrowed("One *word* is strong."));
1300 }
1301
1302 #[test]
1303 fn ignores_strong_word_with_special_chars() {
1304 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1305 let p = Parser::default();
1306 SubstitutionGroup::Header.apply(&mut content, &p, None);
1307 assert!(!content.is_empty());
1308 assert_eq!(
1309 content.rendered,
1310 CowStr::Boxed("One *wo<r>d* is strong.".to_string().into_boxed_str())
1311 );
1312 }
1313
1314 #[test]
1315 fn ignores_marked_string_with_id() {
1316 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1317 let p = Parser::default();
1318 SubstitutionGroup::Header.apply(&mut content, &p, None);
1319 assert!(!content.is_empty());
1320 assert_eq!(content.rendered, CowStr::Borrowed("[#id]#a few words#"));
1321 }
1322 }
1323
1324 mod title {
1325 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1326
1327 #[test]
1328 fn empty() {
1329 let mut content = Content::from(crate::Span::default());
1330 let p = Parser::default();
1331 SubstitutionGroup::Title.apply(&mut content, &p, None);
1332 assert!(content.is_empty());
1333 assert_eq!(content.rendered, CowStr::Borrowed(""));
1334 }
1335
1336 #[test]
1337 fn basic_non_empty_span() {
1338 let mut content = Content::from(crate::Span::new("blah"));
1339 let p = Parser::default();
1340 SubstitutionGroup::Title.apply(&mut content, &p, None);
1341 assert!(!content.is_empty());
1342 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1343 }
1344
1345 #[test]
1346 fn match_lt_and_gt() {
1347 let mut content = Content::from(crate::Span::new("bl<ah>"));
1348 let p = Parser::default();
1349 SubstitutionGroup::Title.apply(&mut content, &p, None);
1350 assert!(!content.is_empty());
1351 assert_eq!(
1352 content.rendered,
1353 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1354 );
1355 }
1356
1357 #[test]
1358 fn match_amp() {
1359 let mut content = Content::from(crate::Span::new("bl<a&h>"));
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<a&h>".to_string().into_boxed_str())
1366 );
1367 }
1368
1369 #[test]
1370 fn strong_word() {
1371 let mut content = Content::from(crate::Span::new("One *word* is strong."));
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(
1378 "One <strong>word</strong> is strong."
1379 .to_string()
1380 .into_boxed_str()
1381 )
1382 );
1383 }
1384
1385 #[test]
1386 fn strong_word_with_special_chars() {
1387 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1388 let p = Parser::default();
1389 SubstitutionGroup::Title.apply(&mut content, &p, None);
1390 assert!(!content.is_empty());
1391 assert_eq!(
1392 content.rendered,
1393 CowStr::Boxed(
1394 "One <strong>wo<r>d</strong> is strong."
1395 .to_string()
1396 .into_boxed_str()
1397 )
1398 );
1399 }
1400
1401 #[test]
1402 fn marked_string_with_id() {
1403 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1404 let p = Parser::default();
1405 SubstitutionGroup::Title.apply(&mut content, &p, None);
1406 assert!(!content.is_empty());
1407 assert_eq!(
1408 content.rendered,
1409 CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
1410 );
1411 }
1412
1413 #[test]
1414 fn title_behaves_same_as_normal() {
1415 let test_input = "One *wo<r>d* is strong with [#id]#marked text#.";
1416
1417 let mut title_content = Content::from(crate::Span::new(test_input));
1418 let mut normal_content = Content::from(crate::Span::new(test_input));
1419 let p = Parser::default();
1420
1421 SubstitutionGroup::Title.apply(&mut title_content, &p, None);
1422 SubstitutionGroup::Normal.apply(&mut normal_content, &p, None);
1423
1424 assert_eq!(title_content.rendered, normal_content.rendered);
1426 }
1427 }
1428}