Skip to main content

asciidoc_parser/content/
substitution_group.rs

1use crate::{
2    Parser,
3    attributes::Attrlist,
4    content::{Content, Passthroughs, SubstitutionStep},
5};
6
7/// Each block and inline element has a default substitution group that is
8/// applied unless you customize the substitutions for a particular element.
9///
10/// `SubstitutionGroup` specifies the default or overridden substitution group
11/// to be applied.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum SubstitutionGroup {
14    /// The normal substitution group is applied to the majority of the AsciiDoc
15    /// block and inline elements except for specific elements described in the
16    /// next sections.
17    Normal,
18
19    /// The title substitution group is applied to section and block titles.
20    /// It uses the same substitution steps as Normal.
21    Title,
22
23    /// The header substitution group is applied to metadata lines (author and
24    /// revision information) in the document header. It’s also applied to the
25    /// values of attribute entries, regardless of whether those entries are
26    /// defined in the document header or body. Only special characters,
27    /// attribute references, and the inline pass macro are replaced in elements
28    /// that fall under the header group.
29    ///
30    /// You can use the inline pass macro in attribute entries to customize the
31    /// substitution types applied to the attribute’s value.
32    Header,
33
34    /// Literal, listing, and source blocks are processed using the verbatim
35    /// substitution group. Only special characters are replaced in these
36    /// blocks.
37    Verbatim,
38
39    /// No substitutions are applied to three of the elements in the pass
40    /// substitution group. These elements include the passthrough block, inline
41    /// pass macro, and triple plus macro.
42    ///
43    /// The inline single plus and double plus macros also belong to the pass
44    /// group. Only the special characters substitution is applied to these
45    /// elements.
46    Pass,
47
48    /// The none substitution group is applied to comment blocks. No
49    /// substitutions are applied to comments.
50    None,
51
52    /// The attribute entry value substitution group is applied to attribute
53    /// values. Only special characters and attribute references are applied to
54    /// these values.
55    AttributeEntryValue,
56
57    /// You can customize the substitutions applied to the content of an inline
58    /// pass macro by specifying one or more substitution values. Multiple
59    /// values must be separated by commas and may not contain any spaces. The
60    /// substitution value is either the formal name of a substitution type or
61    /// group, or its shorthand.
62    ///
63    /// See [Custom substitutions].
64    ///
65    /// [Custom substitutions]: https://docs.asciidoctor.org/asciidoc/latest/pass/pass-macro/#custom-substitutions
66    Custom(Vec<SubstitutionStep>),
67}
68
69impl SubstitutionGroup {
70    /// Parse the custom substitution group syntax defined in [Custom
71    /// substitutions].
72    ///
73    /// [Custom substitutions]: https://docs.asciidoctor.org/asciidoc/latest/pass/pass-macro/#custom-substitutions
74    pub(crate) fn from_custom_string(start_from: Option<&Self>, mut custom: &str) -> Option<Self> {
75        custom = custom.trim();
76
77        if custom == "none" {
78            return Some(Self::None);
79        }
80
81        if custom == "n" || custom == "normal" {
82            return Some(Self::Normal);
83        }
84
85        if custom == "v" || custom == "verbatim" {
86            return Some(Self::Verbatim);
87        }
88
89        let mut steps: Vec<SubstitutionStep> = vec![];
90
91        for (count, mut step) in custom.split(",").enumerate() {
92            step = step.trim();
93
94            if step == "n" || step == "normal" {
95                steps = vec![
96                    SubstitutionStep::SpecialCharacters,
97                    SubstitutionStep::Quotes,
98                    SubstitutionStep::AttributeReferences,
99                    SubstitutionStep::CharacterReplacements,
100                    SubstitutionStep::Macros,
101                    SubstitutionStep::PostReplacement,
102                ];
103                continue;
104            }
105
106            if step == "v" || step == "verbatim" {
107                steps = vec![SubstitutionStep::SpecialCharacters];
108                continue;
109            }
110
111            let append = if step.starts_with('+') {
112                step = &step[1..];
113                true
114            } else {
115                false
116            };
117
118            let prepend = if !append && step.ends_with('+') {
119                step = &step[0..step.len() - 1];
120                true
121            } else {
122                false
123            };
124
125            let subtract = if !append && !prepend && step.starts_with('-') {
126                step = &step[1..];
127                true
128            } else {
129                false
130            };
131
132            if count == 0
133                && let Some(start_from) = start_from
134                && (append || prepend || subtract)
135            {
136                steps = start_from.steps().to_owned();
137            }
138
139            let step = match step {
140                "c" | "specialcharacters" | "specialchars" => SubstitutionStep::SpecialCharacters,
141                "q" | "quotes" => SubstitutionStep::Quotes,
142                "a" | "attributes" => SubstitutionStep::AttributeReferences,
143                "r" | "replacements" => SubstitutionStep::CharacterReplacements,
144                "m" | "macros" => SubstitutionStep::Macros,
145                "p" | "post_replacements" => SubstitutionStep::PostReplacement,
146                _ => {
147                    return None;
148                }
149            };
150
151            if prepend {
152                steps.insert(0, step);
153            } else if append {
154                steps.push(step);
155            } else if subtract {
156                steps.retain(|s| s != &step);
157            } else {
158                steps.push(step);
159            }
160        }
161
162        Some(Self::Custom(steps))
163    }
164
165    pub(crate) fn apply(
166        &self,
167        content: &mut Content<'_>,
168        parser: &Parser,
169        attrlist: Option<&Attrlist>,
170    ) {
171        let steps = self.steps();
172
173        let passthroughs: Option<Passthroughs> =
174            if steps.contains(&SubstitutionStep::Macros) || self == &Self::Header {
175                Some(Passthroughs::extract_from(content))
176            } else {
177                None
178            };
179
180        for step in steps {
181            step.apply(content, parser, attrlist);
182        }
183
184        if let Some(passthroughs) = passthroughs {
185            passthroughs.restore_to(content, parser);
186        }
187
188        // Capture any deferred cross-references as a placeholder template and
189        // render the unresolved fallback, so `rendered()` is clean even before
190        // references are resolved. This is a no-op when no cross-references were
191        // found.
192        content.finalize_deferred(&*parser.renderer);
193    }
194
195    pub(crate) fn override_via_attrlist(&self, attrlist: Option<&Attrlist>) -> Self {
196        let mut result = self.clone();
197
198        if let Some(attrlist) = attrlist {
199            if let Some(block_style) = attrlist.nth_attribute(1).and_then(|a| a.block_style()) {
200                result = match block_style {
201                    // TO DO: Many other style-specific substitution groups.
202                    "pass" => SubstitutionGroup::None,
203                    _ => result,
204                };
205            }
206
207            if let Some(sub_group) = attrlist
208                .named_attribute("subs")
209                .map(|attr| attr.value())
210                .and_then(|s| Self::from_custom_string(Some(self), s))
211            {
212                result = sub_group;
213            }
214        }
215
216        result
217    }
218
219    fn steps(&self) -> &[SubstitutionStep] {
220        match self {
221            Self::Normal | Self::Title => &[
222                SubstitutionStep::SpecialCharacters,
223                SubstitutionStep::Quotes,
224                SubstitutionStep::AttributeReferences,
225                SubstitutionStep::CharacterReplacements,
226                SubstitutionStep::Macros,
227                SubstitutionStep::PostReplacement,
228            ],
229
230            Self::Header | Self::AttributeEntryValue => &[
231                SubstitutionStep::SpecialCharacters,
232                SubstitutionStep::AttributeReferences,
233            ],
234
235            Self::Verbatim => &[SubstitutionStep::SpecialCharacters],
236
237            Self::Pass | Self::None => &[],
238
239            Self::Custom(steps) => steps,
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    #![allow(clippy::unwrap_used)]
247
248    mod from_custom_string {
249        use crate::{
250            content::{Content, SubstitutionStep},
251            strings::CowStr,
252            tests::prelude::*,
253        };
254
255        #[test]
256        fn empty() {
257            assert_eq!(SubstitutionGroup::from_custom_string(None, ""), None);
258        }
259
260        #[test]
261        fn none() {
262            assert_eq!(
263                SubstitutionGroup::from_custom_string(None, "none"),
264                Some(SubstitutionGroup::None)
265            );
266
267            assert_eq!(SubstitutionGroup::from_custom_string(None, "nermal"), None);
268        }
269
270        #[test]
271        fn normal() {
272            assert_eq!(
273                SubstitutionGroup::from_custom_string(None, "n"),
274                Some(SubstitutionGroup::Normal)
275            );
276
277            assert_eq!(
278                SubstitutionGroup::from_custom_string(None, "normal"),
279                Some(SubstitutionGroup::Normal)
280            );
281
282            assert_eq!(SubstitutionGroup::from_custom_string(None, "nermal"), None);
283        }
284
285        #[test]
286        fn verbatim() {
287            assert_eq!(
288                SubstitutionGroup::from_custom_string(None, "v"),
289                Some(SubstitutionGroup::Verbatim)
290            );
291
292            assert_eq!(
293                SubstitutionGroup::from_custom_string(None, "verbatim"),
294                Some(SubstitutionGroup::Verbatim)
295            );
296
297            assert_eq!(
298                SubstitutionGroup::from_custom_string(None, "verboten"),
299                None
300            );
301        }
302
303        #[test]
304        fn special_chars() {
305            assert_eq!(
306                SubstitutionGroup::from_custom_string(None, "c"),
307                Some(SubstitutionGroup::Custom(vec![
308                    SubstitutionStep::SpecialCharacters
309                ]))
310            );
311
312            assert_eq!(
313                SubstitutionGroup::from_custom_string(None, "specialchars"),
314                Some(SubstitutionGroup::Custom(vec![
315                    SubstitutionStep::SpecialCharacters
316                ]))
317            );
318        }
319
320        #[test]
321        fn quotes() {
322            assert_eq!(
323                SubstitutionGroup::from_custom_string(None, "q"),
324                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]))
325            );
326
327            assert_eq!(
328                SubstitutionGroup::from_custom_string(None, "quotes"),
329                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]))
330            );
331        }
332
333        #[test]
334        fn attributes() {
335            assert_eq!(
336                SubstitutionGroup::from_custom_string(None, "a"),
337                Some(SubstitutionGroup::Custom(vec![
338                    SubstitutionStep::AttributeReferences
339                ]))
340            );
341
342            assert_eq!(
343                SubstitutionGroup::from_custom_string(None, "attributes"),
344                Some(SubstitutionGroup::Custom(vec![
345                    SubstitutionStep::AttributeReferences
346                ]))
347            );
348        }
349
350        #[test]
351        fn replacements() {
352            assert_eq!(
353                SubstitutionGroup::from_custom_string(None, "r"),
354                Some(SubstitutionGroup::Custom(vec![
355                    SubstitutionStep::CharacterReplacements
356                ]))
357            );
358
359            assert_eq!(
360                SubstitutionGroup::from_custom_string(None, "replacements"),
361                Some(SubstitutionGroup::Custom(vec![
362                    SubstitutionStep::CharacterReplacements
363                ]))
364            );
365        }
366
367        #[test]
368        fn macros() {
369            assert_eq!(
370                SubstitutionGroup::from_custom_string(None, "m"),
371                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]))
372            );
373
374            assert_eq!(
375                SubstitutionGroup::from_custom_string(None, "macros"),
376                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]))
377            );
378        }
379
380        #[test]
381        fn post_replacements() {
382            assert_eq!(
383                SubstitutionGroup::from_custom_string(None, "p"),
384                Some(SubstitutionGroup::Custom(vec![
385                    SubstitutionStep::PostReplacement
386                ]))
387            );
388
389            assert_eq!(
390                SubstitutionGroup::from_custom_string(None, "post_replacements"),
391                Some(SubstitutionGroup::Custom(vec![
392                    SubstitutionStep::PostReplacement
393                ]))
394            );
395        }
396
397        #[test]
398        fn multiple() {
399            assert_eq!(
400                SubstitutionGroup::from_custom_string(None, "q,a"),
401                Some(SubstitutionGroup::Custom(vec![
402                    SubstitutionStep::Quotes,
403                    SubstitutionStep::AttributeReferences
404                ]))
405            );
406
407            assert_eq!(
408                SubstitutionGroup::from_custom_string(None, "q, a"),
409                Some(SubstitutionGroup::Custom(vec![
410                    SubstitutionStep::Quotes,
411                    SubstitutionStep::AttributeReferences
412                ]))
413            );
414
415            assert_eq!(
416                SubstitutionGroup::from_custom_string(None, "quotes,attributes"),
417                Some(SubstitutionGroup::Custom(vec![
418                    SubstitutionStep::Quotes,
419                    SubstitutionStep::AttributeReferences
420                ]))
421            );
422
423            assert_eq!(
424                SubstitutionGroup::from_custom_string(None, "x,bogus,no such step"),
425                None
426            );
427        }
428
429        #[test]
430        fn subtraction() {
431            assert_eq!(
432                SubstitutionGroup::from_custom_string(None, "n,-r"),
433                Some(SubstitutionGroup::Custom(vec![
434                    SubstitutionStep::SpecialCharacters,
435                    SubstitutionStep::Quotes,
436                    SubstitutionStep::AttributeReferences,
437                    SubstitutionStep::Macros,
438                    SubstitutionStep::PostReplacement,
439                ]))
440            );
441
442            assert_eq!(
443                SubstitutionGroup::from_custom_string(None, "n,-r,-r,-m"),
444                Some(SubstitutionGroup::Custom(vec![
445                    SubstitutionStep::SpecialCharacters,
446                    SubstitutionStep::Quotes,
447                    SubstitutionStep::AttributeReferences,
448                    SubstitutionStep::PostReplacement,
449                ]))
450            );
451
452            assert_eq!(
453                SubstitutionGroup::from_custom_string(None, "v,-r"),
454                Some(SubstitutionGroup::Custom(vec![
455                    SubstitutionStep::SpecialCharacters,
456                ]))
457            );
458
459            assert_eq!(
460                SubstitutionGroup::from_custom_string(None, "v,-c"),
461                Some(SubstitutionGroup::Custom(vec![]))
462            );
463        }
464
465        #[test]
466        fn addition() {
467            assert_eq!(
468                SubstitutionGroup::from_custom_string(None, "n,r"),
469                Some(SubstitutionGroup::Custom(vec![
470                    SubstitutionStep::SpecialCharacters,
471                    SubstitutionStep::Quotes,
472                    SubstitutionStep::AttributeReferences,
473                    SubstitutionStep::CharacterReplacements,
474                    SubstitutionStep::Macros,
475                    SubstitutionStep::PostReplacement,
476                    SubstitutionStep::CharacterReplacements,
477                ]))
478            );
479
480            assert_eq!(
481                SubstitutionGroup::from_custom_string(None, "v,m"),
482                Some(SubstitutionGroup::Custom(vec![
483                    SubstitutionStep::SpecialCharacters,
484                    SubstitutionStep::Macros,
485                ]))
486            );
487        }
488
489        #[test]
490        fn incremental() {
491            assert_eq!(
492                SubstitutionGroup::from_custom_string(None, "n,r"),
493                Some(SubstitutionGroup::Custom(vec![
494                    SubstitutionStep::SpecialCharacters,
495                    SubstitutionStep::Quotes,
496                    SubstitutionStep::AttributeReferences,
497                    SubstitutionStep::CharacterReplacements,
498                    SubstitutionStep::Macros,
499                    SubstitutionStep::PostReplacement,
500                    SubstitutionStep::CharacterReplacements,
501                ]))
502            );
503
504            assert_eq!(
505                SubstitutionGroup::from_custom_string(None, "v,m"),
506                Some(SubstitutionGroup::Custom(vec![
507                    SubstitutionStep::SpecialCharacters,
508                    SubstitutionStep::Macros,
509                ]))
510            );
511        }
512
513        #[test]
514        fn prepend() {
515            assert_eq!(
516                SubstitutionGroup::from_custom_string(
517                    Some(&SubstitutionGroup::Verbatim),
518                    "attributes+"
519                ),
520                Some(SubstitutionGroup::Custom(vec![
521                    SubstitutionStep::AttributeReferences,
522                    SubstitutionStep::SpecialCharacters,
523                ]))
524            );
525
526            assert_eq!(
527                SubstitutionGroup::from_custom_string(None, "attributes+"),
528                Some(SubstitutionGroup::Custom(vec![
529                    SubstitutionStep::AttributeReferences,
530                ]))
531            );
532        }
533
534        #[test]
535        fn append() {
536            assert_eq!(
537                SubstitutionGroup::from_custom_string(
538                    Some(&SubstitutionGroup::Verbatim),
539                    "+attributes"
540                ),
541                Some(SubstitutionGroup::Custom(vec![
542                    SubstitutionStep::SpecialCharacters,
543                    SubstitutionStep::AttributeReferences,
544                ]))
545            );
546
547            assert_eq!(
548                SubstitutionGroup::from_custom_string(None, "attributes+"),
549                Some(SubstitutionGroup::Custom(vec![
550                    SubstitutionStep::AttributeReferences,
551                ]))
552            );
553        }
554
555        #[test]
556        fn subtract() {
557            assert_eq!(
558                SubstitutionGroup::from_custom_string(
559                    Some(&SubstitutionGroup::Normal),
560                    "-attributes"
561                ),
562                Some(SubstitutionGroup::Custom(vec![
563                    SubstitutionStep::SpecialCharacters,
564                    SubstitutionStep::Quotes,
565                    SubstitutionStep::CharacterReplacements,
566                    SubstitutionStep::Macros,
567                    SubstitutionStep::PostReplacement,
568                ]))
569            );
570
571            assert_eq!(
572                SubstitutionGroup::from_custom_string(None, "-attributes"),
573                Some(SubstitutionGroup::Custom(vec![]))
574            );
575        }
576
577        #[test]
578        fn custom_group_with_macros_preserves_passthroughs() {
579            let custom_group = SubstitutionGroup::from_custom_string(None, "q,m").unwrap();
580
581            let mut content = Content::from(crate::Span::new(
582                "Text with +++pass<through>+++ icon:github[] content.",
583            ));
584            let p = Parser::default();
585            custom_group.apply(&mut content, &p, None);
586
587            assert!(!content.is_empty());
588            assert_eq!(
589                content.rendered,
590                CowStr::Boxed(
591                    "Text with pass<through> <span class=\"icon\">[github&#93;</span> content."
592                        .to_string()
593                        .into_boxed_str()
594                )
595            );
596        }
597    }
598
599    mod normal {
600        use crate::{content::Content, strings::CowStr, tests::prelude::*};
601
602        #[test]
603        fn empty() {
604            let mut content = Content::from(crate::Span::default());
605            let p = Parser::default();
606            SubstitutionGroup::Normal.apply(&mut content, &p, None);
607            assert!(content.is_empty());
608            assert_eq!(content.rendered, CowStr::Borrowed(""));
609        }
610
611        #[test]
612        fn basic_non_empty_span() {
613            let mut content = Content::from(crate::Span::new("blah"));
614            let p = Parser::default();
615            SubstitutionGroup::Normal.apply(&mut content, &p, None);
616            assert!(!content.is_empty());
617            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
618        }
619
620        #[test]
621        fn match_lt_and_gt() {
622            let mut content = Content::from(crate::Span::new("bl<ah>"));
623            let p = Parser::default();
624            SubstitutionGroup::Normal.apply(&mut content, &p, None);
625            assert!(!content.is_empty());
626            assert_eq!(
627                content.rendered,
628                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
629            );
630        }
631
632        #[test]
633        fn match_amp() {
634            let mut content = Content::from(crate::Span::new("bl<a&h>"));
635            let p = Parser::default();
636            SubstitutionGroup::Normal.apply(&mut content, &p, None);
637            assert!(!content.is_empty());
638            assert_eq!(
639                content.rendered,
640                CowStr::Boxed("bl&lt;a&amp;h&gt;".to_string().into_boxed_str())
641            );
642        }
643
644        #[test]
645        fn strong_word() {
646            let mut content = Content::from(crate::Span::new("One *word* is strong."));
647            let p = Parser::default();
648            SubstitutionGroup::Normal.apply(&mut content, &p, None);
649            assert!(!content.is_empty());
650            assert_eq!(
651                content.rendered,
652                CowStr::Boxed(
653                    "One <strong>word</strong> is strong."
654                        .to_string()
655                        .into_boxed_str()
656                )
657            );
658        }
659
660        #[test]
661        fn strong_word_with_special_chars() {
662            let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
663            let p = Parser::default();
664            SubstitutionGroup::Normal.apply(&mut content, &p, None);
665            assert!(!content.is_empty());
666            assert_eq!(
667                content.rendered,
668                CowStr::Boxed(
669                    "One <strong>wo&lt;r&gt;d</strong> is strong."
670                        .to_string()
671                        .into_boxed_str()
672                )
673            );
674        }
675
676        #[test]
677        fn marked_string_with_id() {
678            let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
679            let p = Parser::default();
680            SubstitutionGroup::Normal.apply(&mut content, &p, None);
681            assert!(!content.is_empty());
682            assert_eq!(
683                content.rendered,
684                CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
685            );
686        }
687    }
688
689    mod attribute_entry_value {
690        use crate::{
691            content::Content, parser::ModificationContext, strings::CowStr, tests::prelude::*,
692        };
693
694        #[test]
695        fn empty() {
696            let mut content = Content::from(crate::Span::default());
697            let p = Parser::default();
698            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
699            assert!(content.is_empty());
700            assert_eq!(content.rendered, CowStr::Borrowed(""));
701        }
702
703        #[test]
704        fn basic_non_empty_span() {
705            let mut content = Content::from(crate::Span::new("blah"));
706            let p = Parser::default();
707            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
708            assert!(!content.is_empty());
709            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
710        }
711
712        #[test]
713        fn match_lt_and_gt() {
714            let mut content = Content::from(crate::Span::new("bl<ah>"));
715            let p = Parser::default();
716            SubstitutionGroup::Normal.apply(&mut content, &p, None);
717            assert!(!content.is_empty());
718            assert_eq!(
719                content.rendered,
720                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
721            );
722        }
723
724        #[test]
725        fn match_amp() {
726            let mut content = Content::from(crate::Span::new("bl<a&h>"));
727            let p = Parser::default();
728            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
729            assert!(!content.is_empty());
730            assert_eq!(
731                content.rendered,
732                CowStr::Boxed("bl&lt;a&amp;h&gt;".to_string().into_boxed_str())
733            );
734        }
735
736        #[test]
737        fn ignores_strong_word() {
738            let mut content = Content::from(crate::Span::new("One *word* is strong."));
739            let p = Parser::default();
740            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
741            assert!(!content.is_empty());
742            assert_eq!(
743                content.rendered,
744                CowStr::Boxed("One *word* is strong.".to_string().into_boxed_str())
745            );
746        }
747
748        #[test]
749        fn special_chars_and_attributes() {
750            let mut content = Content::from(crate::Span::new("bl<ah> {color}"));
751
752            let p = Parser::default().with_intrinsic_attribute(
753                "color",
754                "red",
755                ModificationContext::Anywhere,
756            );
757
758            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
759            assert!(!content.is_empty());
760            assert_eq!(
761                content.rendered,
762                CowStr::Boxed("bl&lt;ah&gt; red".to_string().into_boxed_str())
763            );
764        }
765    }
766
767    mod header {
768        use crate::{content::Content, strings::CowStr, tests::prelude::*};
769
770        #[test]
771        fn empty() {
772            let mut content = Content::from(crate::Span::default());
773            let p = Parser::default();
774            SubstitutionGroup::Header.apply(&mut content, &p, None);
775            assert!(content.is_empty());
776            assert_eq!(content.rendered, CowStr::Borrowed(""));
777        }
778
779        #[test]
780        fn basic_non_empty_span() {
781            let mut content = Content::from(crate::Span::new("blah"));
782            let p = Parser::default();
783            SubstitutionGroup::Header.apply(&mut content, &p, None);
784            assert!(!content.is_empty());
785            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
786        }
787
788        #[test]
789        fn match_lt_and_gt() {
790            let mut content = Content::from(crate::Span::new("bl<ah>"));
791            let p = Parser::default();
792            SubstitutionGroup::Header.apply(&mut content, &p, None);
793            assert!(!content.is_empty());
794            assert_eq!(
795                content.rendered,
796                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
797            );
798        }
799
800        #[test]
801        fn match_amp() {
802            let mut content = Content::from(crate::Span::new("bl<a&h>"));
803            let p = Parser::default();
804            SubstitutionGroup::Header.apply(&mut content, &p, None);
805            assert!(!content.is_empty());
806            assert_eq!(
807                content.rendered,
808                CowStr::Boxed("bl&lt;a&amp;h&gt;".to_string().into_boxed_str())
809            );
810        }
811
812        #[test]
813        fn ignores_strong_word() {
814            let mut content = Content::from(crate::Span::new("One *word* is strong."));
815            let p = Parser::default();
816            SubstitutionGroup::Header.apply(&mut content, &p, None);
817            assert!(!content.is_empty());
818            assert_eq!(content.rendered, CowStr::Borrowed("One *word* is strong."));
819        }
820
821        #[test]
822        fn ignores_strong_word_with_special_chars() {
823            let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
824            let p = Parser::default();
825            SubstitutionGroup::Header.apply(&mut content, &p, None);
826            assert!(!content.is_empty());
827            assert_eq!(
828                content.rendered,
829                CowStr::Boxed("One *wo&lt;r&gt;d* is strong.".to_string().into_boxed_str())
830            );
831        }
832
833        #[test]
834        fn ignores_marked_string_with_id() {
835            let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
836            let p = Parser::default();
837            SubstitutionGroup::Header.apply(&mut content, &p, None);
838            assert!(!content.is_empty());
839            assert_eq!(content.rendered, CowStr::Borrowed("[#id]#a few words#"));
840        }
841    }
842
843    mod title {
844        use crate::{content::Content, strings::CowStr, tests::prelude::*};
845
846        #[test]
847        fn empty() {
848            let mut content = Content::from(crate::Span::default());
849            let p = Parser::default();
850            SubstitutionGroup::Title.apply(&mut content, &p, None);
851            assert!(content.is_empty());
852            assert_eq!(content.rendered, CowStr::Borrowed(""));
853        }
854
855        #[test]
856        fn basic_non_empty_span() {
857            let mut content = Content::from(crate::Span::new("blah"));
858            let p = Parser::default();
859            SubstitutionGroup::Title.apply(&mut content, &p, None);
860            assert!(!content.is_empty());
861            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
862        }
863
864        #[test]
865        fn match_lt_and_gt() {
866            let mut content = Content::from(crate::Span::new("bl<ah>"));
867            let p = Parser::default();
868            SubstitutionGroup::Title.apply(&mut content, &p, None);
869            assert!(!content.is_empty());
870            assert_eq!(
871                content.rendered,
872                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
873            );
874        }
875
876        #[test]
877        fn match_amp() {
878            let mut content = Content::from(crate::Span::new("bl<a&h>"));
879            let p = Parser::default();
880            SubstitutionGroup::Title.apply(&mut content, &p, None);
881            assert!(!content.is_empty());
882            assert_eq!(
883                content.rendered,
884                CowStr::Boxed("bl&lt;a&amp;h&gt;".to_string().into_boxed_str())
885            );
886        }
887
888        #[test]
889        fn strong_word() {
890            let mut content = Content::from(crate::Span::new("One *word* is strong."));
891            let p = Parser::default();
892            SubstitutionGroup::Title.apply(&mut content, &p, None);
893            assert!(!content.is_empty());
894            assert_eq!(
895                content.rendered,
896                CowStr::Boxed(
897                    "One <strong>word</strong> is strong."
898                        .to_string()
899                        .into_boxed_str()
900                )
901            );
902        }
903
904        #[test]
905        fn strong_word_with_special_chars() {
906            let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
907            let p = Parser::default();
908            SubstitutionGroup::Title.apply(&mut content, &p, None);
909            assert!(!content.is_empty());
910            assert_eq!(
911                content.rendered,
912                CowStr::Boxed(
913                    "One <strong>wo&lt;r&gt;d</strong> is strong."
914                        .to_string()
915                        .into_boxed_str()
916                )
917            );
918        }
919
920        #[test]
921        fn marked_string_with_id() {
922            let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
923            let p = Parser::default();
924            SubstitutionGroup::Title.apply(&mut content, &p, None);
925            assert!(!content.is_empty());
926            assert_eq!(
927                content.rendered,
928                CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
929            );
930        }
931
932        #[test]
933        fn title_behaves_same_as_normal() {
934            let test_input = "One *wo<r>d* is strong with [#id]#marked text#.";
935
936            let mut title_content = Content::from(crate::Span::new(test_input));
937            let mut normal_content = Content::from(crate::Span::new(test_input));
938            let p = Parser::default();
939
940            SubstitutionGroup::Title.apply(&mut title_content, &p, None);
941            SubstitutionGroup::Normal.apply(&mut normal_content, &p, None);
942
943            // Title should produce exactly the same result as Normal
944            assert_eq!(title_content.rendered, normal_content.rendered);
945        }
946    }
947}