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
189    pub(crate) fn override_via_attrlist(&self, attrlist: Option<&Attrlist>) -> Self {
190        let mut result = self.clone();
191
192        if let Some(attrlist) = attrlist {
193            if let Some(block_style) = attrlist.nth_attribute(1).and_then(|a| a.block_style()) {
194                result = match block_style {
195                    // TO DO: Many other style-specific substitution groups.
196                    "pass" => SubstitutionGroup::None,
197                    _ => result,
198                };
199            }
200
201            if let Some(sub_group) = attrlist
202                .named_attribute("subs")
203                .map(|attr| attr.value())
204                .and_then(|s| Self::from_custom_string(Some(self), s))
205            {
206                result = sub_group;
207            }
208        }
209
210        result
211    }
212
213    fn steps(&self) -> &[SubstitutionStep] {
214        match self {
215            Self::Normal | Self::Title => &[
216                SubstitutionStep::SpecialCharacters,
217                SubstitutionStep::Quotes,
218                SubstitutionStep::AttributeReferences,
219                SubstitutionStep::CharacterReplacements,
220                SubstitutionStep::Macros,
221                SubstitutionStep::PostReplacement,
222            ],
223
224            Self::Header | Self::AttributeEntryValue => &[
225                SubstitutionStep::SpecialCharacters,
226                SubstitutionStep::AttributeReferences,
227            ],
228
229            Self::Verbatim => &[SubstitutionStep::SpecialCharacters],
230
231            Self::Pass | Self::None => &[],
232
233            Self::Custom(steps) => steps,
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    #![allow(clippy::unwrap_used)]
241
242    mod from_custom_string {
243        use pretty_assertions_sorted::assert_eq;
244
245        use crate::{
246            Parser,
247            content::{Content, SubstitutionGroup, SubstitutionStep},
248            strings::CowStr,
249        };
250
251        #[test]
252        fn empty() {
253            assert_eq!(SubstitutionGroup::from_custom_string(None, ""), None);
254        }
255
256        #[test]
257        fn none() {
258            assert_eq!(
259                SubstitutionGroup::from_custom_string(None, "none"),
260                Some(SubstitutionGroup::None)
261            );
262
263            assert_eq!(SubstitutionGroup::from_custom_string(None, "nermal"), None);
264        }
265
266        #[test]
267        fn normal() {
268            assert_eq!(
269                SubstitutionGroup::from_custom_string(None, "n"),
270                Some(SubstitutionGroup::Normal)
271            );
272
273            assert_eq!(
274                SubstitutionGroup::from_custom_string(None, "normal"),
275                Some(SubstitutionGroup::Normal)
276            );
277
278            assert_eq!(SubstitutionGroup::from_custom_string(None, "nermal"), None);
279        }
280
281        #[test]
282        fn verbatim() {
283            assert_eq!(
284                SubstitutionGroup::from_custom_string(None, "v"),
285                Some(SubstitutionGroup::Verbatim)
286            );
287
288            assert_eq!(
289                SubstitutionGroup::from_custom_string(None, "verbatim"),
290                Some(SubstitutionGroup::Verbatim)
291            );
292
293            assert_eq!(
294                SubstitutionGroup::from_custom_string(None, "verboten"),
295                None
296            );
297        }
298
299        #[test]
300        fn special_chars() {
301            assert_eq!(
302                SubstitutionGroup::from_custom_string(None, "c"),
303                Some(SubstitutionGroup::Custom(vec![
304                    SubstitutionStep::SpecialCharacters
305                ]))
306            );
307
308            assert_eq!(
309                SubstitutionGroup::from_custom_string(None, "specialchars"),
310                Some(SubstitutionGroup::Custom(vec![
311                    SubstitutionStep::SpecialCharacters
312                ]))
313            );
314        }
315
316        #[test]
317        fn quotes() {
318            assert_eq!(
319                SubstitutionGroup::from_custom_string(None, "q"),
320                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]))
321            );
322
323            assert_eq!(
324                SubstitutionGroup::from_custom_string(None, "quotes"),
325                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]))
326            );
327        }
328
329        #[test]
330        fn attributes() {
331            assert_eq!(
332                SubstitutionGroup::from_custom_string(None, "a"),
333                Some(SubstitutionGroup::Custom(vec![
334                    SubstitutionStep::AttributeReferences
335                ]))
336            );
337
338            assert_eq!(
339                SubstitutionGroup::from_custom_string(None, "attributes"),
340                Some(SubstitutionGroup::Custom(vec![
341                    SubstitutionStep::AttributeReferences
342                ]))
343            );
344        }
345
346        #[test]
347        fn replacements() {
348            assert_eq!(
349                SubstitutionGroup::from_custom_string(None, "r"),
350                Some(SubstitutionGroup::Custom(vec![
351                    SubstitutionStep::CharacterReplacements
352                ]))
353            );
354
355            assert_eq!(
356                SubstitutionGroup::from_custom_string(None, "replacements"),
357                Some(SubstitutionGroup::Custom(vec![
358                    SubstitutionStep::CharacterReplacements
359                ]))
360            );
361        }
362
363        #[test]
364        fn macros() {
365            assert_eq!(
366                SubstitutionGroup::from_custom_string(None, "m"),
367                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]))
368            );
369
370            assert_eq!(
371                SubstitutionGroup::from_custom_string(None, "macros"),
372                Some(SubstitutionGroup::Custom(vec![SubstitutionStep::Macros]))
373            );
374        }
375
376        #[test]
377        fn post_replacements() {
378            assert_eq!(
379                SubstitutionGroup::from_custom_string(None, "p"),
380                Some(SubstitutionGroup::Custom(vec![
381                    SubstitutionStep::PostReplacement
382                ]))
383            );
384
385            assert_eq!(
386                SubstitutionGroup::from_custom_string(None, "post_replacements"),
387                Some(SubstitutionGroup::Custom(vec![
388                    SubstitutionStep::PostReplacement
389                ]))
390            );
391        }
392
393        #[test]
394        fn multiple() {
395            assert_eq!(
396                SubstitutionGroup::from_custom_string(None, "q,a"),
397                Some(SubstitutionGroup::Custom(vec![
398                    SubstitutionStep::Quotes,
399                    SubstitutionStep::AttributeReferences
400                ]))
401            );
402
403            assert_eq!(
404                SubstitutionGroup::from_custom_string(None, "q, a"),
405                Some(SubstitutionGroup::Custom(vec![
406                    SubstitutionStep::Quotes,
407                    SubstitutionStep::AttributeReferences
408                ]))
409            );
410
411            assert_eq!(
412                SubstitutionGroup::from_custom_string(None, "quotes,attributes"),
413                Some(SubstitutionGroup::Custom(vec![
414                    SubstitutionStep::Quotes,
415                    SubstitutionStep::AttributeReferences
416                ]))
417            );
418
419            assert_eq!(
420                SubstitutionGroup::from_custom_string(None, "x,bogus,no such step"),
421                None
422            );
423        }
424
425        #[test]
426        fn subtraction() {
427            assert_eq!(
428                SubstitutionGroup::from_custom_string(None, "n,-r"),
429                Some(SubstitutionGroup::Custom(vec![
430                    SubstitutionStep::SpecialCharacters,
431                    SubstitutionStep::Quotes,
432                    SubstitutionStep::AttributeReferences,
433                    SubstitutionStep::Macros,
434                    SubstitutionStep::PostReplacement,
435                ]))
436            );
437
438            assert_eq!(
439                SubstitutionGroup::from_custom_string(None, "n,-r,-r,-m"),
440                Some(SubstitutionGroup::Custom(vec![
441                    SubstitutionStep::SpecialCharacters,
442                    SubstitutionStep::Quotes,
443                    SubstitutionStep::AttributeReferences,
444                    SubstitutionStep::PostReplacement,
445                ]))
446            );
447
448            assert_eq!(
449                SubstitutionGroup::from_custom_string(None, "v,-r"),
450                Some(SubstitutionGroup::Custom(vec![
451                    SubstitutionStep::SpecialCharacters,
452                ]))
453            );
454
455            assert_eq!(
456                SubstitutionGroup::from_custom_string(None, "v,-c"),
457                Some(SubstitutionGroup::Custom(vec![]))
458            );
459        }
460
461        #[test]
462        fn addition() {
463            assert_eq!(
464                SubstitutionGroup::from_custom_string(None, "n,r"),
465                Some(SubstitutionGroup::Custom(vec![
466                    SubstitutionStep::SpecialCharacters,
467                    SubstitutionStep::Quotes,
468                    SubstitutionStep::AttributeReferences,
469                    SubstitutionStep::CharacterReplacements,
470                    SubstitutionStep::Macros,
471                    SubstitutionStep::PostReplacement,
472                    SubstitutionStep::CharacterReplacements,
473                ]))
474            );
475
476            assert_eq!(
477                SubstitutionGroup::from_custom_string(None, "v,m"),
478                Some(SubstitutionGroup::Custom(vec![
479                    SubstitutionStep::SpecialCharacters,
480                    SubstitutionStep::Macros,
481                ]))
482            );
483        }
484
485        #[test]
486        fn incremental() {
487            assert_eq!(
488                SubstitutionGroup::from_custom_string(None, "n,r"),
489                Some(SubstitutionGroup::Custom(vec![
490                    SubstitutionStep::SpecialCharacters,
491                    SubstitutionStep::Quotes,
492                    SubstitutionStep::AttributeReferences,
493                    SubstitutionStep::CharacterReplacements,
494                    SubstitutionStep::Macros,
495                    SubstitutionStep::PostReplacement,
496                    SubstitutionStep::CharacterReplacements,
497                ]))
498            );
499
500            assert_eq!(
501                SubstitutionGroup::from_custom_string(None, "v,m"),
502                Some(SubstitutionGroup::Custom(vec![
503                    SubstitutionStep::SpecialCharacters,
504                    SubstitutionStep::Macros,
505                ]))
506            );
507        }
508
509        #[test]
510        fn prepend() {
511            assert_eq!(
512                SubstitutionGroup::from_custom_string(
513                    Some(&SubstitutionGroup::Verbatim),
514                    "attributes+"
515                ),
516                Some(SubstitutionGroup::Custom(vec![
517                    SubstitutionStep::AttributeReferences,
518                    SubstitutionStep::SpecialCharacters,
519                ]))
520            );
521
522            assert_eq!(
523                SubstitutionGroup::from_custom_string(None, "attributes+"),
524                Some(SubstitutionGroup::Custom(vec![
525                    SubstitutionStep::AttributeReferences,
526                ]))
527            );
528        }
529
530        #[test]
531        fn append() {
532            assert_eq!(
533                SubstitutionGroup::from_custom_string(
534                    Some(&SubstitutionGroup::Verbatim),
535                    "+attributes"
536                ),
537                Some(SubstitutionGroup::Custom(vec![
538                    SubstitutionStep::SpecialCharacters,
539                    SubstitutionStep::AttributeReferences,
540                ]))
541            );
542
543            assert_eq!(
544                SubstitutionGroup::from_custom_string(None, "attributes+"),
545                Some(SubstitutionGroup::Custom(vec![
546                    SubstitutionStep::AttributeReferences,
547                ]))
548            );
549        }
550
551        #[test]
552        fn subtract() {
553            assert_eq!(
554                SubstitutionGroup::from_custom_string(
555                    Some(&SubstitutionGroup::Normal),
556                    "-attributes"
557                ),
558                Some(SubstitutionGroup::Custom(vec![
559                    SubstitutionStep::SpecialCharacters,
560                    SubstitutionStep::Quotes,
561                    SubstitutionStep::CharacterReplacements,
562                    SubstitutionStep::Macros,
563                    SubstitutionStep::PostReplacement,
564                ]))
565            );
566
567            assert_eq!(
568                SubstitutionGroup::from_custom_string(None, "-attributes"),
569                Some(SubstitutionGroup::Custom(vec![]))
570            );
571        }
572
573        #[test]
574        fn custom_group_with_macros_preserves_passthroughs() {
575            let custom_group = SubstitutionGroup::from_custom_string(None, "q,m").unwrap();
576
577            let mut content = Content::from(crate::Span::new(
578                "Text with +++pass<through>+++ icon:github[] content.",
579            ));
580            let p = Parser::default();
581            custom_group.apply(&mut content, &p, None);
582
583            assert!(!content.is_empty());
584            assert_eq!(
585                content.rendered,
586                CowStr::Boxed(
587                    "Text with pass<through> <span class=\"icon\">[github&#93;</span> content."
588                        .to_string()
589                        .into_boxed_str()
590                )
591            );
592        }
593    }
594
595    mod normal {
596        use crate::{
597            Parser,
598            content::{Content, SubstitutionGroup},
599            strings::CowStr,
600        };
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            Parser,
692            content::{Content, SubstitutionGroup},
693            parser::ModificationContext,
694            strings::CowStr,
695        };
696
697        #[test]
698        fn empty() {
699            let mut content = Content::from(crate::Span::default());
700            let p = Parser::default();
701            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
702            assert!(content.is_empty());
703            assert_eq!(content.rendered, CowStr::Borrowed(""));
704        }
705
706        #[test]
707        fn basic_non_empty_span() {
708            let mut content = Content::from(crate::Span::new("blah"));
709            let p = Parser::default();
710            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
711            assert!(!content.is_empty());
712            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
713        }
714
715        #[test]
716        fn match_lt_and_gt() {
717            let mut content = Content::from(crate::Span::new("bl<ah>"));
718            let p = Parser::default();
719            SubstitutionGroup::Normal.apply(&mut content, &p, None);
720            assert!(!content.is_empty());
721            assert_eq!(
722                content.rendered,
723                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
724            );
725        }
726
727        #[test]
728        fn match_amp() {
729            let mut content = Content::from(crate::Span::new("bl<a&h>"));
730            let p = Parser::default();
731            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
732            assert!(!content.is_empty());
733            assert_eq!(
734                content.rendered,
735                CowStr::Boxed("bl&lt;a&amp;h&gt;".to_string().into_boxed_str())
736            );
737        }
738
739        #[test]
740        fn ignores_strong_word() {
741            let mut content = Content::from(crate::Span::new("One *word* is strong."));
742            let p = Parser::default();
743            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
744            assert!(!content.is_empty());
745            assert_eq!(
746                content.rendered,
747                CowStr::Boxed("One *word* is strong.".to_string().into_boxed_str())
748            );
749        }
750
751        #[test]
752        fn special_chars_and_attributes() {
753            let mut content = Content::from(crate::Span::new("bl<ah> {color}"));
754
755            let p = Parser::default().with_intrinsic_attribute(
756                "color",
757                "red",
758                ModificationContext::Anywhere,
759            );
760
761            SubstitutionGroup::AttributeEntryValue.apply(&mut content, &p, None);
762            assert!(!content.is_empty());
763            assert_eq!(
764                content.rendered,
765                CowStr::Boxed("bl&lt;ah&gt; red".to_string().into_boxed_str())
766            );
767        }
768    }
769
770    mod header {
771        use crate::{
772            Parser,
773            content::{Content, SubstitutionGroup},
774            strings::CowStr,
775        };
776
777        #[test]
778        fn empty() {
779            let mut content = Content::from(crate::Span::default());
780            let p = Parser::default();
781            SubstitutionGroup::Header.apply(&mut content, &p, None);
782            assert!(content.is_empty());
783            assert_eq!(content.rendered, CowStr::Borrowed(""));
784        }
785
786        #[test]
787        fn basic_non_empty_span() {
788            let mut content = Content::from(crate::Span::new("blah"));
789            let p = Parser::default();
790            SubstitutionGroup::Header.apply(&mut content, &p, None);
791            assert!(!content.is_empty());
792            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
793        }
794
795        #[test]
796        fn match_lt_and_gt() {
797            let mut content = Content::from(crate::Span::new("bl<ah>"));
798            let p = Parser::default();
799            SubstitutionGroup::Header.apply(&mut content, &p, None);
800            assert!(!content.is_empty());
801            assert_eq!(
802                content.rendered,
803                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
804            );
805        }
806
807        #[test]
808        fn match_amp() {
809            let mut content = Content::from(crate::Span::new("bl<a&h>"));
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;a&amp;h&gt;".to_string().into_boxed_str())
816            );
817        }
818
819        #[test]
820        fn ignores_strong_word() {
821            let mut content = Content::from(crate::Span::new("One *word* is strong."));
822            let p = Parser::default();
823            SubstitutionGroup::Header.apply(&mut content, &p, None);
824            assert!(!content.is_empty());
825            assert_eq!(content.rendered, CowStr::Borrowed("One *word* is strong."));
826        }
827
828        #[test]
829        fn ignores_strong_word_with_special_chars() {
830            let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
831            let p = Parser::default();
832            SubstitutionGroup::Header.apply(&mut content, &p, None);
833            assert!(!content.is_empty());
834            assert_eq!(
835                content.rendered,
836                CowStr::Boxed("One *wo&lt;r&gt;d* is strong.".to_string().into_boxed_str())
837            );
838        }
839
840        #[test]
841        fn ignores_marked_string_with_id() {
842            let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
843            let p = Parser::default();
844            SubstitutionGroup::Header.apply(&mut content, &p, None);
845            assert!(!content.is_empty());
846            assert_eq!(content.rendered, CowStr::Borrowed("[#id]#a few words#"));
847        }
848    }
849
850    mod title {
851        use crate::{
852            Parser,
853            content::{Content, SubstitutionGroup},
854            strings::CowStr,
855        };
856
857        #[test]
858        fn empty() {
859            let mut content = Content::from(crate::Span::default());
860            let p = Parser::default();
861            SubstitutionGroup::Title.apply(&mut content, &p, None);
862            assert!(content.is_empty());
863            assert_eq!(content.rendered, CowStr::Borrowed(""));
864        }
865
866        #[test]
867        fn basic_non_empty_span() {
868            let mut content = Content::from(crate::Span::new("blah"));
869            let p = Parser::default();
870            SubstitutionGroup::Title.apply(&mut content, &p, None);
871            assert!(!content.is_empty());
872            assert_eq!(content.rendered, CowStr::Borrowed("blah"));
873        }
874
875        #[test]
876        fn match_lt_and_gt() {
877            let mut content = Content::from(crate::Span::new("bl<ah>"));
878            let p = Parser::default();
879            SubstitutionGroup::Title.apply(&mut content, &p, None);
880            assert!(!content.is_empty());
881            assert_eq!(
882                content.rendered,
883                CowStr::Boxed("bl&lt;ah&gt;".to_string().into_boxed_str())
884            );
885        }
886
887        #[test]
888        fn match_amp() {
889            let mut content = Content::from(crate::Span::new("bl<a&h>"));
890            let p = Parser::default();
891            SubstitutionGroup::Title.apply(&mut content, &p, None);
892            assert!(!content.is_empty());
893            assert_eq!(
894                content.rendered,
895                CowStr::Boxed("bl&lt;a&amp;h&gt;".to_string().into_boxed_str())
896            );
897        }
898
899        #[test]
900        fn strong_word() {
901            let mut content = Content::from(crate::Span::new("One *word* is strong."));
902            let p = Parser::default();
903            SubstitutionGroup::Title.apply(&mut content, &p, None);
904            assert!(!content.is_empty());
905            assert_eq!(
906                content.rendered,
907                CowStr::Boxed(
908                    "One <strong>word</strong> is strong."
909                        .to_string()
910                        .into_boxed_str()
911                )
912            );
913        }
914
915        #[test]
916        fn strong_word_with_special_chars() {
917            let mut content = Content::from(crate::Span::new("One *wo<r>d* is strong."));
918            let p = Parser::default();
919            SubstitutionGroup::Title.apply(&mut content, &p, None);
920            assert!(!content.is_empty());
921            assert_eq!(
922                content.rendered,
923                CowStr::Boxed(
924                    "One <strong>wo&lt;r&gt;d</strong> is strong."
925                        .to_string()
926                        .into_boxed_str()
927                )
928            );
929        }
930
931        #[test]
932        fn marked_string_with_id() {
933            let mut content = Content::from(crate::Span::new(r#"[#id]#a few words#"#));
934            let p = Parser::default();
935            SubstitutionGroup::Title.apply(&mut content, &p, None);
936            assert!(!content.is_empty());
937            assert_eq!(
938                content.rendered,
939                CowStr::Boxed(r#"<span id="id">a few words</span>"#.to_string().into_boxed_str())
940            );
941        }
942
943        #[test]
944        fn title_behaves_same_as_normal() {
945            let test_input = "One *wo<r>d* is strong with [#id]#marked text#.";
946
947            let mut title_content = Content::from(crate::Span::new(test_input));
948            let mut normal_content = Content::from(crate::Span::new(test_input));
949            let p = Parser::default();
950
951            SubstitutionGroup::Title.apply(&mut title_content, &p, None);
952            SubstitutionGroup::Normal.apply(&mut normal_content, &p, None);
953
954            // Title should produce exactly the same result as Normal
955            assert_eq!(title_content.rendered, normal_content.rendered);
956        }
957    }
958}