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