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