1use crate::{
2 Parser,
3 attributes::Attrlist,
4 content::{Content, Passthroughs, SubstitutionStep},
5};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum SubstitutionGroup {
14 Normal,
18
19 Title,
22
23 Header,
33
34 Verbatim,
38
39 Pass,
47
48 None,
51
52 AttributeEntryValue,
56
57 Stem,
62
63 Custom(Vec<SubstitutionStep>),
73}
74
75impl SubstitutionGroup {
76 pub(crate) fn from_custom_string(start_from: Option<&Self>, mut custom: &str) -> Option<Self> {
81 custom = custom.trim();
82
83 if custom == "none" {
84 return Some(Self::None);
85 }
86
87 if custom == "n" || custom == "normal" {
88 return Some(Self::Normal);
89 }
90
91 if custom == "v" || custom == "verbatim" {
92 return Some(Self::Verbatim);
93 }
94
95 let mut steps: Vec<SubstitutionStep> = vec![];
96
97 for (count, mut step) in custom.split(",").enumerate() {
98 step = step.trim();
99
100 if step == "n" || step == "normal" {
105 steps.extend([
106 SubstitutionStep::SpecialCharacters,
107 SubstitutionStep::Quotes,
108 SubstitutionStep::AttributeReferences,
109 SubstitutionStep::CharacterReplacements,
110 SubstitutionStep::Macros,
111 SubstitutionStep::PostReplacement,
112 ]);
113 continue;
114 }
115
116 if step == "v" || step == "verbatim" {
117 steps.extend([
118 SubstitutionStep::SpecialCharacters,
119 SubstitutionStep::Callouts,
120 ]);
121 continue;
122 }
123
124 let append = if step.starts_with('+') {
125 step = &step[1..];
126 true
127 } else {
128 false
129 };
130
131 let prepend = if !append && step.ends_with('+') {
132 step = &step[0..step.len() - 1];
133 true
134 } else {
135 false
136 };
137
138 let subtract = if !append && !prepend && step.starts_with('-') {
139 step = &step[1..];
140 true
141 } else {
142 false
143 };
144
145 if count == 0
146 && let Some(start_from) = start_from
147 && (append || prepend || subtract)
148 {
149 steps = start_from.steps().to_owned();
150 }
151
152 let step = match step {
153 "c" | "specialcharacters" | "specialchars" => SubstitutionStep::SpecialCharacters,
154 "q" | "quotes" => SubstitutionStep::Quotes,
155 "a" | "attributes" => SubstitutionStep::AttributeReferences,
156 "r" | "replacements" => SubstitutionStep::CharacterReplacements,
157 "m" | "macros" => SubstitutionStep::Macros,
158 "p" | "post_replacements" => SubstitutionStep::PostReplacement,
159 "callouts" => SubstitutionStep::Callouts,
160 _ => {
161 return None;
162 }
163 };
164
165 if prepend {
166 steps.insert(0, step);
167 } else if append {
168 steps.push(step);
169 } else if subtract {
170 steps.retain(|s| s != &step);
171 } else {
172 steps.push(step);
173 }
174 }
175
176 let mut deduped: Vec<SubstitutionStep> = Vec::with_capacity(steps.len());
181 for step in steps {
182 if !deduped.contains(&step) {
183 deduped.push(step);
184 }
185 }
186
187 Some(Self::Custom(deduped))
188 }
189
190 pub(crate) fn apply(
191 &self,
192 content: &mut Content<'_>,
193 parser: &Parser,
194 attrlist: Option<&Attrlist>,
195 ) {
196 let steps = self.steps();
197
198 let passthroughs: Option<Passthroughs> =
199 if steps.contains(&SubstitutionStep::Macros) || self == &Self::Header {
200 Some(Passthroughs::extract_from(content, parser))
201 } else {
202 None
203 };
204
205 for step in steps {
206 step.apply(content, parser, attrlist);
207 }
208
209 if let Some(passthroughs) = passthroughs {
210 passthroughs.restore_to(content, parser);
211 }
212
213 content.finalize_deferred(&*parser.renderer);
218 }
219
220 pub(crate) fn override_via_attrlist(&self, attrlist: Option<&Attrlist>) -> Self {
221 let mut result = self.clone();
222
223 if let Some(attrlist) = attrlist {
224 if result == SubstitutionGroup::Normal
232 && let Some(block_style) = attrlist.nth_attribute(1).and_then(|a| a.block_style())
233 {
234 result = match block_style {
235 "literal" | "listing" | "source" => SubstitutionGroup::Verbatim,
238
239 "pass" => SubstitutionGroup::None,
242
243 _ => result,
247 };
248 }
249
250 if let Some(sub_group) = attrlist
251 .named_attribute("subs")
252 .map(|attr| attr.value())
253 .and_then(|s| Self::from_custom_string(Some(self), s))
254 {
255 result = sub_group;
256 }
257 }
258
259 result
260 }
261
262 fn steps(&self) -> &[SubstitutionStep] {
263 match self {
264 Self::Normal | Self::Title => &[
265 SubstitutionStep::SpecialCharacters,
266 SubstitutionStep::Quotes,
267 SubstitutionStep::AttributeReferences,
268 SubstitutionStep::CharacterReplacements,
269 SubstitutionStep::Macros,
270 SubstitutionStep::PostReplacement,
271 ],
272
273 Self::Header | Self::AttributeEntryValue => &[
274 SubstitutionStep::SpecialCharacters,
275 SubstitutionStep::AttributeReferences,
276 ],
277
278 Self::Verbatim => &[
279 SubstitutionStep::SpecialCharacters,
280 SubstitutionStep::Callouts,
281 ],
282
283 Self::Stem => &[SubstitutionStep::SpecialCharacters],
284
285 Self::Pass | Self::None => &[],
286
287 Self::Custom(steps) => steps,
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 #![allow(clippy::unwrap_used)]
295
296 mod stem {
297 use crate::{content::Content, strings::CowStr, tests::prelude::*};
298
299 #[test]
300 fn applies_special_characters_only() {
301 let mut content = Content::from(crate::Span::new("*a* < {color}"));
305 let p = Parser::default();
306 SubstitutionGroup::Stem.apply(&mut content, &p, None);
307 assert_eq!(
308 content.rendered,
309 CowStr::Boxed("*a* < {color}".to_string().into_boxed_str())
310 );
311 }
312 }
313
314 mod from_custom_string {
315 use crate::{
316 content::{Content, SubstitutionStep},
317 strings::CowStr,
318 tests::prelude::*,
319 };
320
321 #[test]
322 fn empty() {
323 assert_eq!(SubstitutionGroup::from_custom_string(None, ""), None);
324 }
325
326 #[test]
327 fn none() {
328 assert_eq!(
329 SubstitutionGroup::from_custom_string(None, "none"),
330 Some(SubstitutionGroup::None)
331 );
332
333 assert_eq!(SubstitutionGroup::from_custom_string(None, "nermal"), None);
334 }
335
336 #[test]
337 fn normal() {
338 assert_eq!(
339 SubstitutionGroup::from_custom_string(None, "n"),
340 Some(SubstitutionGroup::Normal)
341 );
342
343 assert_eq!(
344 SubstitutionGroup::from_custom_string(None, "normal"),
345 Some(SubstitutionGroup::Normal)
346 );
347
348 assert_eq!(SubstitutionGroup::from_custom_string(None, "nermal"), None);
349 }
350
351 #[test]
352 fn verbatim() {
353 assert_eq!(
354 SubstitutionGroup::from_custom_string(None, "v"),
355 Some(SubstitutionGroup::Verbatim)
356 );
357
358 assert_eq!(
359 SubstitutionGroup::from_custom_string(None, "verbatim"),
360 Some(SubstitutionGroup::Verbatim)
361 );
362
363 assert_eq!(
364 SubstitutionGroup::from_custom_string(None, "verboten"),
365 None
366 );
367 }
368
369 #[test]
370 fn special_chars() {
371 assert_eq!(
372 SubstitutionGroup::from_custom_string(None, "c"),
373 Some(SubstitutionGroup::Custom(vec![
374 SubstitutionStep::SpecialCharacters
375 ]))
376 );
377
378 assert_eq!(
379 SubstitutionGroup::from_custom_string(None, "specialchars"),
380 Some(SubstitutionGroup::Custom(vec![
381 SubstitutionStep::SpecialCharacters
382 ]))
383 );
384 }
385
386 #[test]
387 fn quotes() {
388 assert_eq!(
389 SubstitutionGroup::from_custom_string(None, "q"),
390 Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]))
391 );
392
393 assert_eq!(
394 SubstitutionGroup::from_custom_string(None, "quotes"),
395 Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]))
396 );
397 }
398
399 #[test]
400 fn attributes() {
401 assert_eq!(
402 SubstitutionGroup::from_custom_string(None, "a"),
403 Some(SubstitutionGroup::Custom(vec![
404 SubstitutionStep::AttributeReferences
405 ]))
406 );
407
408 assert_eq!(
409 SubstitutionGroup::from_custom_string(None, "attributes"),
410 Some(SubstitutionGroup::Custom(vec![
411 SubstitutionStep::AttributeReferences
412 ]))
413 );
414 }
415
416 #[test]
417 fn replacements() {
418 assert_eq!(
419 SubstitutionGroup::from_custom_string(None, "r"),
420 Some(SubstitutionGroup::Custom(vec![
421 SubstitutionStep::CharacterReplacements
422 ]))
423 );
424
425 assert_eq!(
426 SubstitutionGroup::from_custom_string(None, "replacements"),
427 Some(SubstitutionGroup::Custom(vec![
428 SubstitutionStep::CharacterReplacements
429 ]))
430 );
431 }
432
433 #[test]
434 fn macros() {
435 assert_eq!(
436 SubstitutionGroup::from_custom_string(None, "m"),
437 Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]))
438 );
439
440 assert_eq!(
441 SubstitutionGroup::from_custom_string(None, "macros"),
442 Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]))
443 );
444 }
445
446 #[test]
447 fn post_replacements() {
448 assert_eq!(
449 SubstitutionGroup::from_custom_string(None, "p"),
450 Some(SubstitutionGroup::Custom(vec![
451 SubstitutionStep::PostReplacement
452 ]))
453 );
454
455 assert_eq!(
456 SubstitutionGroup::from_custom_string(None, "post_replacements"),
457 Some(SubstitutionGroup::Custom(vec![
458 SubstitutionStep::PostReplacement
459 ]))
460 );
461 }
462
463 #[test]
464 fn multiple() {
465 assert_eq!(
466 SubstitutionGroup::from_custom_string(None, "q,a"),
467 Some(SubstitutionGroup::Custom(vec![
468 SubstitutionStep::Quotes,
469 SubstitutionStep::AttributeReferences
470 ]))
471 );
472
473 assert_eq!(
474 SubstitutionGroup::from_custom_string(None, "q, a"),
475 Some(SubstitutionGroup::Custom(vec![
476 SubstitutionStep::Quotes,
477 SubstitutionStep::AttributeReferences
478 ]))
479 );
480
481 assert_eq!(
482 SubstitutionGroup::from_custom_string(None, "quotes,attributes"),
483 Some(SubstitutionGroup::Custom(vec![
484 SubstitutionStep::Quotes,
485 SubstitutionStep::AttributeReferences
486 ]))
487 );
488
489 assert_eq!(
490 SubstitutionGroup::from_custom_string(None, "x,bogus,no such step"),
491 None
492 );
493 }
494
495 #[test]
496 fn subtraction() {
497 assert_eq!(
498 SubstitutionGroup::from_custom_string(None, "n,-r"),
499 Some(SubstitutionGroup::Custom(vec![
500 SubstitutionStep::SpecialCharacters,
501 SubstitutionStep::Quotes,
502 SubstitutionStep::AttributeReferences,
503 SubstitutionStep::Macros,
504 SubstitutionStep::PostReplacement,
505 ]))
506 );
507
508 assert_eq!(
509 SubstitutionGroup::from_custom_string(None, "n,-r,-r,-m"),
510 Some(SubstitutionGroup::Custom(vec![
511 SubstitutionStep::SpecialCharacters,
512 SubstitutionStep::Quotes,
513 SubstitutionStep::AttributeReferences,
514 SubstitutionStep::PostReplacement,
515 ]))
516 );
517
518 assert_eq!(
519 SubstitutionGroup::from_custom_string(None, "v,-r"),
520 Some(SubstitutionGroup::Custom(vec![
521 SubstitutionStep::SpecialCharacters,
522 SubstitutionStep::Callouts,
523 ]))
524 );
525
526 assert_eq!(
527 SubstitutionGroup::from_custom_string(None, "v,-c"),
528 Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Callouts]))
529 );
530
531 assert_eq!(
532 SubstitutionGroup::from_custom_string(None, "v,-callouts"),
533 Some(SubstitutionGroup::Custom(vec![
534 SubstitutionStep::SpecialCharacters,
535 ]))
536 );
537 }
538
539 #[test]
540 fn addition() {
541 assert_eq!(
545 SubstitutionGroup::from_custom_string(None, "n,r"),
546 Some(SubstitutionGroup::Custom(vec![
547 SubstitutionStep::SpecialCharacters,
548 SubstitutionStep::Quotes,
549 SubstitutionStep::AttributeReferences,
550 SubstitutionStep::CharacterReplacements,
551 SubstitutionStep::Macros,
552 SubstitutionStep::PostReplacement,
553 ]))
554 );
555
556 assert_eq!(
557 SubstitutionGroup::from_custom_string(None, "v,m"),
558 Some(SubstitutionGroup::Custom(vec![
559 SubstitutionStep::SpecialCharacters,
560 SubstitutionStep::Callouts,
561 SubstitutionStep::Macros,
562 ]))
563 );
564 }
565
566 #[test]
567 fn incremental() {
568 assert_eq!(
572 SubstitutionGroup::from_custom_string(None, "n,r"),
573 Some(SubstitutionGroup::Custom(vec![
574 SubstitutionStep::SpecialCharacters,
575 SubstitutionStep::Quotes,
576 SubstitutionStep::AttributeReferences,
577 SubstitutionStep::CharacterReplacements,
578 SubstitutionStep::Macros,
579 SubstitutionStep::PostReplacement,
580 ]))
581 );
582
583 assert_eq!(
584 SubstitutionGroup::from_custom_string(None, "v,m"),
585 Some(SubstitutionGroup::Custom(vec![
586 SubstitutionStep::SpecialCharacters,
587 SubstitutionStep::Callouts,
588 SubstitutionStep::Macros,
589 ]))
590 );
591 }
592
593 #[test]
594 fn group_name_mid_list_expands_in_place_and_dedups() {
595 assert_eq!(
601 SubstitutionGroup::from_custom_string(None, "quotes,normal"),
602 Some(SubstitutionGroup::Custom(vec![
603 SubstitutionStep::Quotes,
604 SubstitutionStep::SpecialCharacters,
605 SubstitutionStep::AttributeReferences,
606 SubstitutionStep::CharacterReplacements,
607 SubstitutionStep::Macros,
608 SubstitutionStep::PostReplacement,
609 ]))
610 );
611
612 assert_eq!(
614 SubstitutionGroup::from_custom_string(None, "m,v"),
615 Some(SubstitutionGroup::Custom(vec![
616 SubstitutionStep::Macros,
617 SubstitutionStep::SpecialCharacters,
618 SubstitutionStep::Callouts,
619 ]))
620 );
621 }
622
623 #[test]
624 fn prepend() {
625 assert_eq!(
626 SubstitutionGroup::from_custom_string(
627 Some(&SubstitutionGroup::Verbatim),
628 "attributes+"
629 ),
630 Some(SubstitutionGroup::Custom(vec![
631 SubstitutionStep::AttributeReferences,
632 SubstitutionStep::SpecialCharacters,
633 SubstitutionStep::Callouts,
634 ]))
635 );
636
637 assert_eq!(
638 SubstitutionGroup::from_custom_string(None, "attributes+"),
639 Some(SubstitutionGroup::Custom(vec![
640 SubstitutionStep::AttributeReferences,
641 ]))
642 );
643 }
644
645 #[test]
646 fn append() {
647 assert_eq!(
648 SubstitutionGroup::from_custom_string(
649 Some(&SubstitutionGroup::Verbatim),
650 "+attributes"
651 ),
652 Some(SubstitutionGroup::Custom(vec![
653 SubstitutionStep::SpecialCharacters,
654 SubstitutionStep::Callouts,
655 SubstitutionStep::AttributeReferences,
656 ]))
657 );
658
659 assert_eq!(
660 SubstitutionGroup::from_custom_string(None, "attributes+"),
661 Some(SubstitutionGroup::Custom(vec![
662 SubstitutionStep::AttributeReferences,
663 ]))
664 );
665 }
666
667 #[test]
668 fn subtract() {
669 assert_eq!(
670 SubstitutionGroup::from_custom_string(
671 Some(&SubstitutionGroup::Normal),
672 "-attributes"
673 ),
674 Some(SubstitutionGroup::Custom(vec![
675 SubstitutionStep::SpecialCharacters,
676 SubstitutionStep::Quotes,
677 SubstitutionStep::CharacterReplacements,
678 SubstitutionStep::Macros,
679 SubstitutionStep::PostReplacement,
680 ]))
681 );
682
683 assert_eq!(
684 SubstitutionGroup::from_custom_string(None, "-attributes"),
685 Some(SubstitutionGroup::Custom(vec![]))
686 );
687 }
688
689 #[test]
690 fn custom_group_with_macros_preserves_passthroughs() {
691 let custom_group = SubstitutionGroup::from_custom_string(None, "q,m").unwrap();
692
693 let mut content = Content::from(crate::Span::new(
694 "Text with +++pass<through>+++ icon:github[] content.",
695 ));
696 let p = Parser::default();
697 custom_group.apply(&mut content, &p, None);
698
699 assert!(!content.is_empty());
700 assert_eq!(
701 content.rendered,
702 CowStr::Boxed(
703 "Text with pass<through> <span class=\"icon\">[github]</span> content."
704 .to_string()
705 .into_boxed_str()
706 )
707 );
708 }
709 }
710
711 mod override_via_attrlist {
712 use crate::{
713 attributes::{Attrlist, AttrlistContext},
714 tests::prelude::*,
715 };
716
717 fn resolve(base: SubstitutionGroup, attrlist: &str) -> SubstitutionGroup {
720 let p = Parser::default();
721 let attrlist = Attrlist::parse(crate::Span::new(attrlist), &p, AttrlistContext::Block)
722 .item
723 .item;
724
725 base.override_via_attrlist(Some(&attrlist))
726 }
727
728 #[test]
729 fn verbatim_masquerade_styles_promote_normal_to_verbatim() {
730 for style in ["literal", "listing", "source"] {
733 assert_eq!(
734 resolve(SubstitutionGroup::Normal, style),
735 SubstitutionGroup::Verbatim,
736 "style `{style}` should map Normal to Verbatim"
737 );
738 }
739 }
740
741 #[test]
742 fn pass_style_suppresses_substitutions_on_normal() {
743 assert_eq!(
744 resolve(SubstitutionGroup::Normal, "pass"),
745 SubstitutionGroup::None
746 );
747 }
748
749 #[test]
750 fn non_masquerade_styles_keep_normal() {
751 for style in ["normal", "verse", "quote", "sidebar", "example"] {
755 assert_eq!(
756 resolve(SubstitutionGroup::Normal, style),
757 SubstitutionGroup::Normal,
758 "style `{style}` should keep Normal"
759 );
760 }
761 }
762
763 #[test]
764 fn style_does_not_override_a_delimited_block_group() {
765 assert_eq!(
770 resolve(SubstitutionGroup::Verbatim, "pass"),
771 SubstitutionGroup::Verbatim
772 );
773
774 assert_eq!(
775 resolve(SubstitutionGroup::Pass, "source"),
776 SubstitutionGroup::Pass
777 );
778
779 assert_eq!(
780 resolve(SubstitutionGroup::Stem, "source"),
781 SubstitutionGroup::Stem
782 );
783 }
784
785 #[test]
786 fn subs_attribute_still_overrides() {
787 assert_eq!(
790 resolve(SubstitutionGroup::Normal, "listing,subs=normal"),
791 SubstitutionGroup::Normal
792 );
793
794 assert_eq!(
795 resolve(SubstitutionGroup::Verbatim, "subs=none"),
796 SubstitutionGroup::None
797 );
798 }
799 }
800
801 mod normal {
802 use crate::{content::Content, strings::CowStr, tests::prelude::*};
803
804 #[test]
805 fn empty() {
806 let mut content = Content::from(crate::Span::default());
807 let p = Parser::default();
808 SubstitutionGroup::Normal.apply(&mut content, &p, None);
809 assert!(content.is_empty());
810 assert_eq!(content.rendered, CowStr::Borrowed(""));
811 }
812
813 #[test]
814 fn basic_non_empty_span() {
815 let mut content = Content::from(crate::Span::new("blah"));
816 let p = Parser::default();
817 SubstitutionGroup::Normal.apply(&mut content, &p, None);
818 assert!(!content.is_empty());
819 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
820 }
821
822 #[test]
823 fn match_lt_and_gt() {
824 let mut content = Content::from(crate::Span::new("bl<ah>"));
825 let p = Parser::default();
826 SubstitutionGroup::Normal.apply(&mut content, &p, None);
827 assert!(!content.is_empty());
828 assert_eq!(
829 content.rendered,
830 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
831 );
832 }
833
834 #[test]
835 fn match_amp() {
836 let mut content = Content::from(crate::Span::new("bl<a&h>"));
837 let p = Parser::default();
838 SubstitutionGroup::Normal.apply(&mut content, &p, None);
839 assert!(!content.is_empty());
840 assert_eq!(
841 content.rendered,
842 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
843 );
844 }
845
846 #[test]
847 fn strong_word() {
848 let mut content = Content::from(crate::Span::new("One *word* is strong."));
849 let p = Parser::default();
850 SubstitutionGroup::Normal.apply(&mut content, &p, None);
851 assert!(!content.is_empty());
852 assert_eq!(
853 content.rendered,
854 CowStr::Boxed(
855 "One <strong>word</strong> is strong."
856 .to_string()
857 .into_boxed_str()
858 )
859 );
860 }
861
862 #[test]
863 fn strong_word_with_special_chars() {
864 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
865 let p = Parser::default();
866 SubstitutionGroup::Normal.apply(&mut content, &p, None);
867 assert!(!content.is_empty());
868 assert_eq!(
869 content.rendered,
870 CowStr::Boxed(
871 "One <strong>wo<r>d</strong> is strong."
872 .to_string()
873 .into_boxed_str()
874 )
875 );
876 }
877
878 #[test]
879 fn marked_string_with_id() {
880 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
881 let p = Parser::default();
882 SubstitutionGroup::Normal.apply(&mut content, &p, None);
883 assert!(!content.is_empty());
884 assert_eq!(
885 content.rendered,
886 CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
887 );
888 }
889 }
890
891 mod attribute_entry_value {
892 use crate::{
893 content::Content, parser::ModificationContext, strings::CowStr, tests::prelude::*,
894 };
895
896 #[test]
897 fn empty() {
898 let mut content = Content::from(crate::Span::default());
899 let p = Parser::default();
900 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
901 assert!(content.is_empty());
902 assert_eq!(content.rendered, CowStr::Borrowed(""));
903 }
904
905 #[test]
906 fn basic_non_empty_span() {
907 let mut content = Content::from(crate::Span::new("blah"));
908 let p = Parser::default();
909 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
910 assert!(!content.is_empty());
911 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
912 }
913
914 #[test]
915 fn match_lt_and_gt() {
916 let mut content = Content::from(crate::Span::new("bl<ah>"));
917 let p = Parser::default();
918 SubstitutionGroup::Normal.apply(&mut content, &p, None);
919 assert!(!content.is_empty());
920 assert_eq!(
921 content.rendered,
922 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
923 );
924 }
925
926 #[test]
927 fn match_amp() {
928 let mut content = Content::from(crate::Span::new("bl<a&h>"));
929 let p = Parser::default();
930 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
931 assert!(!content.is_empty());
932 assert_eq!(
933 content.rendered,
934 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
935 );
936 }
937
938 #[test]
939 fn ignores_strong_word() {
940 let mut content = Content::from(crate::Span::new("One *word* is strong."));
941 let p = Parser::default();
942 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
943 assert!(!content.is_empty());
944 assert_eq!(
945 content.rendered,
946 CowStr::Boxed("One *word* is strong.".to_string().into_boxed_str())
947 );
948 }
949
950 #[test]
951 fn special_chars_and_attributes() {
952 let mut content = Content::from(crate::Span::new("bl<ah> {color}"));
953
954 let p = Parser::default().with_intrinsic_attribute(
955 "color",
956 "red",
957 ModificationContext::Anywhere,
958 );
959
960 SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
961 assert!(!content.is_empty());
962 assert_eq!(
963 content.rendered,
964 CowStr::Boxed("bl<ah> red".to_string().into_boxed_str())
965 );
966 }
967 }
968
969 mod header {
970 use crate::{content::Content, strings::CowStr, tests::prelude::*};
971
972 #[test]
973 fn empty() {
974 let mut content = Content::from(crate::Span::default());
975 let p = Parser::default();
976 SubstitutionGroup::Header.apply(&mut content, &p, None);
977 assert!(content.is_empty());
978 assert_eq!(content.rendered, CowStr::Borrowed(""));
979 }
980
981 #[test]
982 fn basic_non_empty_span() {
983 let mut content = Content::from(crate::Span::new("blah"));
984 let p = Parser::default();
985 SubstitutionGroup::Header.apply(&mut content, &p, None);
986 assert!(!content.is_empty());
987 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
988 }
989
990 #[test]
991 fn match_lt_and_gt() {
992 let mut content = Content::from(crate::Span::new("bl<ah>"));
993 let p = Parser::default();
994 SubstitutionGroup::Header.apply(&mut content, &p, None);
995 assert!(!content.is_empty());
996 assert_eq!(
997 content.rendered,
998 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
999 );
1000 }
1001
1002 #[test]
1003 fn match_amp() {
1004 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1005 let p = Parser::default();
1006 SubstitutionGroup::Header.apply(&mut content, &p, None);
1007 assert!(!content.is_empty());
1008 assert_eq!(
1009 content.rendered,
1010 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1011 );
1012 }
1013
1014 #[test]
1015 fn ignores_strong_word() {
1016 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1017 let p = Parser::default();
1018 SubstitutionGroup::Header.apply(&mut content, &p, None);
1019 assert!(!content.is_empty());
1020 assert_eq!(content.rendered, CowStr::Borrowed("One *word* is strong."));
1021 }
1022
1023 #[test]
1024 fn ignores_strong_word_with_special_chars() {
1025 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1026 let p = Parser::default();
1027 SubstitutionGroup::Header.apply(&mut content, &p, None);
1028 assert!(!content.is_empty());
1029 assert_eq!(
1030 content.rendered,
1031 CowStr::Boxed("One *wo<r>d* is strong.".to_string().into_boxed_str())
1032 );
1033 }
1034
1035 #[test]
1036 fn ignores_marked_string_with_id() {
1037 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1038 let p = Parser::default();
1039 SubstitutionGroup::Header.apply(&mut content, &p, None);
1040 assert!(!content.is_empty());
1041 assert_eq!(content.rendered, CowStr::Borrowed("[#id]#a few words#"));
1042 }
1043 }
1044
1045 mod title {
1046 use crate::{content::Content, strings::CowStr, tests::prelude::*};
1047
1048 #[test]
1049 fn empty() {
1050 let mut content = Content::from(crate::Span::default());
1051 let p = Parser::default();
1052 SubstitutionGroup::Title.apply(&mut content, &p, None);
1053 assert!(content.is_empty());
1054 assert_eq!(content.rendered, CowStr::Borrowed(""));
1055 }
1056
1057 #[test]
1058 fn basic_non_empty_span() {
1059 let mut content = Content::from(crate::Span::new("blah"));
1060 let p = Parser::default();
1061 SubstitutionGroup::Title.apply(&mut content, &p, None);
1062 assert!(!content.is_empty());
1063 assert_eq!(content.rendered, CowStr::Borrowed("blah"));
1064 }
1065
1066 #[test]
1067 fn match_lt_and_gt() {
1068 let mut content = Content::from(crate::Span::new("bl<ah>"));
1069 let p = Parser::default();
1070 SubstitutionGroup::Title.apply(&mut content, &p, None);
1071 assert!(!content.is_empty());
1072 assert_eq!(
1073 content.rendered,
1074 CowStr::Boxed("bl<ah>".to_string().into_boxed_str())
1075 );
1076 }
1077
1078 #[test]
1079 fn match_amp() {
1080 let mut content = Content::from(crate::Span::new("bl<a&h>"));
1081 let p = Parser::default();
1082 SubstitutionGroup::Title.apply(&mut content, &p, None);
1083 assert!(!content.is_empty());
1084 assert_eq!(
1085 content.rendered,
1086 CowStr::Boxed("bl<a&h>".to_string().into_boxed_str())
1087 );
1088 }
1089
1090 #[test]
1091 fn strong_word() {
1092 let mut content = Content::from(crate::Span::new("One *word* is strong."));
1093 let p = Parser::default();
1094 SubstitutionGroup::Title.apply(&mut content, &p, None);
1095 assert!(!content.is_empty());
1096 assert_eq!(
1097 content.rendered,
1098 CowStr::Boxed(
1099 "One <strong>word</strong> is strong."
1100 .to_string()
1101 .into_boxed_str()
1102 )
1103 );
1104 }
1105
1106 #[test]
1107 fn strong_word_with_special_chars() {
1108 let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
1109 let p = Parser::default();
1110 SubstitutionGroup::Title.apply(&mut content, &p, None);
1111 assert!(!content.is_empty());
1112 assert_eq!(
1113 content.rendered,
1114 CowStr::Boxed(
1115 "One <strong>wo<r>d</strong> is strong."
1116 .to_string()
1117 .into_boxed_str()
1118 )
1119 );
1120 }
1121
1122 #[test]
1123 fn marked_string_with_id() {
1124 let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
1125 let p = Parser::default();
1126 SubstitutionGroup::Title.apply(&mut content, &p, None);
1127 assert!(!content.is_empty());
1128 assert_eq!(
1129 content.rendered,
1130 CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
1131 );
1132 }
1133
1134 #[test]
1135 fn title_behaves_same_as_normal() {
1136 let test_input = "One *wo<r>d* is strong with [#id]#marked text#.";
1137
1138 let mut title_content = Content::from(crate::Span::new(test_input));
1139 let mut normal_content = Content::from(crate::Span::new(test_input));
1140 let p = Parser::default();
1141
1142 SubstitutionGroup::Title.apply(&mut title_content, &p, None);
1143 SubstitutionGroup::Normal.apply(&mut normal_content, &p, None);
1144
1145 assert_eq!(title_content.rendered, normal_content.rendered);
1147 }
1148 }
1149}