Skip to main content

asciidoc_parser/parser/
parser.rs

1use std::{cell::RefCell, collections::HashMap, rc::Rc};
2
3use crate::{
4    Document, HasSpan,
5    blocks::{SectionNumber, SectionType},
6    document::{Attribute, Catalog, InterpretedValue, RefType},
7    parser::{
8        AllowableValue, AttributeValue, HtmlSubstitutionRenderer, IncludeFileHandler,
9        InlineSubstitutionRenderer, ModificationContext, PathResolver,
10        built_in_attrs::{built_in_attrs, built_in_default_values},
11        preprocessor::preprocess,
12    },
13    warnings::{Warning, WarningType},
14};
15
16/// The [`Parser`] struct and its related structs allow a caller to configure
17/// how AsciiDoc parsing occurs and then to initiate the parsing process.
18#[derive(Clone, Debug)]
19pub struct Parser {
20    /// Attribute values at current state of parsing.
21    pub(crate) attribute_values: HashMap<String, AttributeValue>,
22
23    /// Default values for attributes if "set."
24    default_attribute_values: HashMap<String, String>,
25
26    /// Specifies how the basic raw text of a simple block will be converted to
27    /// the format which will ultimately be presented in the final output.
28    ///
29    /// Typically this is an [`HtmlSubstitutionRenderer`] but clients may
30    /// provide alternative implementations.
31    pub(crate) renderer: Rc<dyn InlineSubstitutionRenderer>,
32
33    /// Specifies the name of the primary file to be parsed.
34    pub(crate) primary_file_name: Option<String>,
35
36    /// Specifies how to generate clean and secure paths relative to the parsing
37    /// context.
38    pub path_resolver: PathResolver,
39
40    /// Handler for resolving include:: directives.
41    pub(crate) include_file_handler: Option<Rc<dyn IncludeFileHandler>>,
42
43    /// Document catalog for tracking referenceable elements during parsing.
44    /// This is created during parsing and transferred to the Document when
45    /// complete.
46    ///
47    /// Wrapped in a [`RefCell`] so that anchors and references discovered deep
48    /// inside inline substitution (where only a shared `&Parser` is available,
49    /// e.g. within a regex [`Replacer`](regex::Replacer)) can still be
50    /// registered.
51    catalog: RefCell<Catalog>,
52
53    /// Most recently-assigned section number.
54    pub(crate) last_section_number: SectionNumber,
55
56    /// Most recently-assigned appendix section number.
57    pub(crate) last_appendix_section_number: SectionNumber,
58
59    /// Saved copy of sectnumlevels at end of document header.
60    pub(crate) sectnumlevels: usize,
61
62    /// Section type of outermost section. (Used to determine whether to number
63    /// child sections as a normal section or appendix.)
64    pub(crate) topmost_section_type: SectionType,
65}
66
67impl Default for Parser {
68    fn default() -> Self {
69        Self {
70            attribute_values: built_in_attrs(),
71            default_attribute_values: built_in_default_values(),
72            renderer: Rc::new(HtmlSubstitutionRenderer {}),
73            primary_file_name: None,
74            path_resolver: PathResolver::default(),
75            include_file_handler: None,
76            catalog: RefCell::new(Catalog::new()),
77            last_section_number: SectionNumber::default(),
78            last_appendix_section_number: SectionNumber {
79                section_type: SectionType::Appendix,
80                components: vec![],
81            },
82            sectnumlevels: 3,
83            topmost_section_type: SectionType::Normal,
84        }
85    }
86}
87
88impl Parser {
89    /// Parse a UTF-8 string as an AsciiDoc document.
90    ///
91    /// The [`Document`] data structure returned by this call has a '`static`
92    /// lifetime; this is an implementation detail. It retains a copy of the
93    /// `source` string that was passed in, but it is not tied to the lifetime
94    /// of that string.
95    ///
96    /// Nearly all of the data structures contained within the [`Document`]
97    /// structure are tied to the lifetime of the document and have a `'src`
98    /// lifetime to signal their dependency on the source document.
99    ///
100    /// **IMPORTANT:** The AsciiDoc language documentation states that UTF-16
101    /// encoding is allowed if a byte-order-mark (BOM) is present at the
102    /// start of a file. This format is not directly supported by the
103    /// `asciidoc-parser` crate. Any UTF-16 content must be re-encoded as
104    /// UTF-8 prior to parsing.
105    ///
106    /// The `Parser` struct will be updated with document attribute values
107    /// discovered during parsing. These values may be inspected using
108    /// [`attribute_value()`].
109    ///
110    /// # Warnings, not errors
111    ///
112    /// Any UTF-8 string is a valid AsciiDoc document, so this function does not
113    /// return an [`Option`] or [`Result`] data type. There may be any number of
114    /// character sequences that have ambiguous or potentially unintended
115    /// meanings. For that reason, a caller is advised to review the warnings
116    /// provided via the [`warnings()`] iterator.
117    ///
118    /// [`warnings()`]: Document::warnings
119    /// [`attribute_value()`]: Self::attribute_value
120    pub fn parse(&mut self, source: &str) -> Document<'static> {
121        let mut document = self.parse_deferred(source);
122
123        // Resolve cross-references against this document's own catalog. For
124        // multi-document workflows, use `parse_deferred` and resolve later with
125        // a caller-supplied resolver via `Document::resolve_references`.
126        document.resolve_against_own_catalog(&*self.renderer);
127
128        document
129    }
130
131    /// Parse a UTF-8 string as an AsciiDoc document, leaving cross-references
132    /// unresolved.
133    ///
134    /// This behaves like [`parse()`], except it does not resolve
135    /// cross-references (`<<id>>`, `xref:id[…]`). The returned [`Document`]
136    /// carries its references in a deferred state; resolve them later with
137    /// [`Document::resolve_references`].
138    ///
139    /// This is the entry point for multi-document workflows (e.g. Antora-style
140    /// site generation): parse every document with this method, build a
141    /// combined index from each document's [`catalog()`], then resolve each
142    /// document against that index. This crate does not merge catalogs
143    /// itself.
144    ///
145    /// [`parse()`]: Self::parse
146    /// [`catalog()`]: Document::catalog
147    pub fn parse_deferred(&mut self, source: &str) -> Document<'static> {
148        let (preprocessed_source, source_map) = preprocess(source, self);
149
150        // NOTE: `Document::parse` will transfer the catalog to itself at the end of the
151        // parsing operation. Start each parse with a fresh catalog.
152        *self.catalog.borrow_mut() = Catalog::new();
153
154        // Reset section numbering for each new document.
155        self.last_section_number = SectionNumber::default();
156
157        Document::parse(&preprocessed_source, source_map, self)
158    }
159
160    /// Retrieves the current interpreted value of a [document attribute].
161    ///
162    /// Each document holds a set of name-value pairs called document
163    /// attributes. These attributes provide a means of configuring the AsciiDoc
164    /// processor, declaring document metadata, and defining reusable content.
165    /// This page introduces document attributes and answers some questions
166    /// about the terminology used when referring to them.
167    ///
168    /// ## What are document attributes?
169    ///
170    /// Document attributes are effectively document-scoped variables for the
171    /// AsciiDoc language. The AsciiDoc language defines a set of built-in
172    /// attributes, and also allows the author (or extensions) to define
173    /// additional document attributes, which may replace built-in attributes
174    /// when permitted.
175    ///
176    /// Built-in attributes either provide access to read-only information about
177    /// the document and its environment or allow the author to configure
178    /// behavior of the AsciiDoc processor for a whole document or select
179    /// regions. Built-in attributes are effectively unordered. User-defined
180    /// attribute serve as a powerful text replacement tool. User-defined
181    /// attributes are stored in the order in which they are defined.
182    ///
183    /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
184    pub fn attribute_value<N: AsRef<str>>(&self, name: N) -> InterpretedValue {
185        self.attribute_values
186            .get(name.as_ref())
187            .map(|av| av.value.clone())
188            .map(|av| {
189                if let InterpretedValue::Set = av
190                    && let Some(default) = self.default_attribute_values.get(name.as_ref())
191                {
192                    InterpretedValue::Value(default.clone())
193                } else {
194                    av
195                }
196            })
197            .unwrap_or(InterpretedValue::Unset)
198    }
199
200    /// Returns `true` if the parser has a [document attribute] by this name.
201    ///
202    /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
203    pub fn has_attribute<N: AsRef<str>>(&self, name: N) -> bool {
204        self.attribute_values.contains_key(name.as_ref())
205    }
206
207    /// Returns `true` if the parser has a [document attribute] by this name
208    /// which has been set (i.e. is present and not [unset]).
209    ///
210    /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
211    /// [unset]: https://docs.asciidoctor.org/asciidoc/latest/attributes/unset-attributes/
212    pub fn is_attribute_set<N: AsRef<str>>(&self, name: N) -> bool {
213        self.attribute_values
214            .get(name.as_ref())
215            .map(|a| a.value != InterpretedValue::Unset)
216            .unwrap_or(false)
217    }
218
219    /// Sets the value of an [intrinsic attribute].
220    ///
221    /// Intrinsic attributes are set automatically by the processor. These
222    /// attributes provide information about the document being processed (e.g.,
223    /// `docfile`), the security mode under which the processor is running
224    /// (e.g., `safe-mode-name`), and information about the user’s environment
225    /// (e.g., `user-home`).
226    ///
227    /// The [`modification_context`](ModificationContext) establishes whether
228    /// the value can be subsequently modified by the document header and/or in
229    /// the document body.
230    ///
231    /// Subsequent calls to this function or [`with_intrinsic_attribute_bool()`]
232    /// are always permitted. The last such call for any given attribute name
233    /// takes precendence.
234    ///
235    /// [intrinsic attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
236    ///
237    /// [`with_intrinsic_attribute_bool()`]: Self::with_intrinsic_attribute_bool
238    pub fn with_intrinsic_attribute<N: AsRef<str>, V: AsRef<str>>(
239        mut self,
240        name: N,
241        value: V,
242        modification_context: ModificationContext,
243    ) -> Self {
244        let attribute_value = AttributeValue {
245            allowable_value: AllowableValue::Any,
246            modification_context,
247            value: InterpretedValue::Value(value.as_ref().to_string()),
248        };
249
250        self.attribute_values
251            .insert(name.as_ref().to_lowercase(), attribute_value);
252
253        self
254    }
255
256    /// Register a referenceable element (anchor, section, bibliography entry)
257    /// in the document catalog.
258    ///
259    /// This takes `&self` (rather than `&mut self`) so that it can be called
260    /// from inline-substitution code paths that only hold a shared reference to
261    /// the parser, such as a regex [`Replacer`](regex::Replacer).
262    pub(crate) fn register_ref(
263        &self,
264        id: &str,
265        reftext: Option<&str>,
266        ref_type: RefType,
267    ) -> Result<(), crate::document::DuplicateIdError> {
268        self.catalog
269            .borrow_mut()
270            .register_ref(id, reftext, ref_type)
271    }
272
273    /// Generate a unique ID derived from `base_id` and register it in the
274    /// document catalog, returning the ID that was assigned.
275    pub(crate) fn generate_and_register_unique_id(
276        &self,
277        base_id: &str,
278        reftext: Option<&str>,
279        ref_type: RefType,
280    ) -> String {
281        self.catalog
282            .borrow_mut()
283            .generate_and_register_unique_id(base_id, reftext, ref_type)
284    }
285
286    /// Takes the catalog from the parser, transferring ownership and leaving an
287    /// empty catalog in its place.
288    ///
289    /// This is used by `Document::parse` to transfer the catalog from the
290    /// parser to the document at the end of parsing.
291    pub(crate) fn take_catalog(&mut self) -> Catalog {
292        std::mem::take(&mut *self.catalog.borrow_mut())
293    }
294
295    /* Comment out until we're prepared to use and test this.
296        /// Sets the default value for an [intrinsic attribute].
297        ///
298        /// Default values for attributes are provided automatically by the
299        /// processor. These values provide a falllback textual value for an
300        /// attribute when it is merely "set" by the document via API, header, or
301        /// document body.
302        ///
303        /// Calling this does not imply that the value is set automatically by
304        /// default, nor does it establish any policy for where the value may be
305        /// modified. For that, please use [`with_intrinsic_attribute`].
306        ///
307        /// [intrinsic attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
308        /// [`with_intrinsic_attribute`]: Self::with_intrinsic_attribute
309        pub fn with_default_attribute_value<N: AsRef<str>, V: AsRef<str>>(
310            mut self,
311            name: N,
312            value: V,
313        ) -> Self {
314            self.default_attribute_values
315                .insert(name.as_ref().to_string(), value.as_ref().to_string());
316
317            self
318        }
319    */
320
321    /// Sets the value of an [intrinsic attribute] from a boolean flag.
322    ///
323    /// A boolean `true` is interpreted as "set." A boolean `false` is
324    /// interpreted as "unset."
325    ///
326    /// Intrinsic attributes are set automatically by the processor. These
327    /// attributes provide information about the document being processed (e.g.,
328    /// `docfile`), the security mode under which the processor is running
329    /// (e.g., `safe-mode-name`), and information about the user’s environment
330    /// (e.g., `user-home`).
331    ///
332    /// The [`modification_context`](ModificationContext) establishes whether
333    /// the value can be subsequently modified by the document header and/or in
334    /// the document body.
335    ///
336    /// Subsequent calls to this function or [`with_intrinsic_attribute()`] are
337    /// always permitted. The last such call for any given attribute name takes
338    /// precendence.
339    ///
340    /// [intrinsic attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
341    ///
342    /// [`with_intrinsic_attribute()`]: Self::with_intrinsic_attribute
343    pub fn with_intrinsic_attribute_bool<N: AsRef<str>>(
344        mut self,
345        name: N,
346        value: bool,
347        modification_context: ModificationContext,
348    ) -> Self {
349        let attribute_value = AttributeValue {
350            allowable_value: AllowableValue::Any,
351            modification_context,
352            value: if value {
353                InterpretedValue::Set
354            } else {
355                InterpretedValue::Unset
356            },
357        };
358
359        self.attribute_values
360            .insert(name.as_ref().to_lowercase(), attribute_value);
361
362        self
363    }
364
365    /// Replace the default [`InlineSubstitutionRenderer`] for this parser.
366    ///
367    /// The default implementation of [`InlineSubstitutionRenderer`] that is
368    /// provided is suitable for HTML5 rendering. If you are targeting a
369    /// different back-end rendering, you will need to provide your own
370    /// implementation and set it using this call before parsing.
371    pub fn with_inline_substitution_renderer<ISR: InlineSubstitutionRenderer + 'static>(
372        mut self,
373        renderer: ISR,
374    ) -> Self {
375        self.renderer = Rc::new(renderer);
376        self
377    }
378
379    /// Sets the name of the primary file to be parsed when [`parse()`] is
380    /// called.
381    ///
382    /// This name will be used for any error messages detected in this file and
383    /// also will be passed to [`IncludeFileHandler::resolve_target()`] as the
384    /// `source` argument for any `include::` file resolution requests from this
385    /// file.
386    ///
387    /// [`parse()`]: Self::parse
388    /// [`IncludeFileHandler::resolve_target()`]: crate::parser::IncludeFileHandler::resolve_target
389    pub fn with_primary_file_name<S: AsRef<str>>(mut self, name: S) -> Self {
390        self.primary_file_name = Some(name.as_ref().to_owned());
391        self
392    }
393
394    /// Sets the [`IncludeFileHandler`] for this parser.
395    ///
396    /// The include file handler is responsible for resolving `include::`
397    /// directives encountered during preprocessing. If no handler is provided,
398    /// include directives will be ignored.
399    ///
400    /// [`IncludeFileHandler`]: crate::parser::IncludeFileHandler
401    pub fn with_include_file_handler<IFH: IncludeFileHandler + 'static>(
402        mut self,
403        handler: IFH,
404    ) -> Self {
405        self.include_file_handler = Some(Rc::new(handler));
406        self
407    }
408
409    /// Called from [`Header::parse()`] to accept or reject an attribute value.
410    ///
411    /// [`Header::parse()`]: crate::document::Header::parse
412    pub(crate) fn set_attribute_from_header<'src>(
413        &mut self,
414        attr: &Attribute<'src>,
415        warnings: &mut Vec<Warning<'src>>,
416    ) {
417        let attr_name = remap_attr_name(attr.name().data());
418
419        let existing_attr = self.attribute_values.get(&attr_name);
420
421        // Verify that we have permission to overwrite any existing attribute value.
422        if let Some(existing_attr) = existing_attr
423            && (existing_attr.modification_context == ModificationContext::ApiOnly
424                || existing_attr.modification_context == ModificationContext::ApiOrDocumentBody)
425        {
426            warnings.push(Warning {
427                source: attr.span(),
428                warning: WarningType::AttributeValueIsLocked(attr_name),
429            });
430            return;
431        }
432
433        let mut value = attr.value().clone();
434
435        if let InterpretedValue::Set = value
436            && let Some(default_value) = self.default_attribute_values.get(&attr_name)
437        {
438            value = InterpretedValue::Value(default_value.clone());
439        }
440
441        let attribute_value = AttributeValue {
442            allowable_value: AllowableValue::Any,
443            modification_context: ModificationContext::Anywhere,
444            value,
445        };
446
447        self.attribute_values.insert(attr_name, attribute_value);
448    }
449
450    /// Called from [`Header::parse()`] for a value that is derived from parsing
451    /// the header (except for attribute lines).
452    ///
453    /// [`Header::parse()`]: crate::document::Header::parse
454    pub(crate) fn set_attribute_by_value_from_header<N: AsRef<str>, V: AsRef<str>>(
455        &mut self,
456        name: N,
457        value: V,
458    ) {
459        let attr_name = remap_attr_name(name);
460
461        let attribute_value = AttributeValue {
462            allowable_value: AllowableValue::Any,
463            modification_context: ModificationContext::Anywhere,
464            value: InterpretedValue::Value(value.as_ref().to_owned()),
465        };
466
467        self.attribute_values.insert(attr_name, attribute_value);
468    }
469
470    /// Called from [`Block::parse()`] to accept or reject an attribute value
471    /// from a document (body) attribute.
472    ///
473    /// [`Block::parse()`]: crate::blocks::Block::parse
474    pub(crate) fn set_attribute_from_body<'src>(
475        &mut self,
476        attr: &Attribute<'src>,
477        warnings: &mut Vec<Warning<'src>>,
478    ) {
479        let attr_name = remap_attr_name(attr.name().data());
480
481        // Verify that we have permission to overwrite any existing attribute value.
482        if let Some(existing_attr) = self.attribute_values.get(&attr_name)
483            && (existing_attr.modification_context != ModificationContext::Anywhere
484                && existing_attr.modification_context != ModificationContext::ApiOrDocumentBody)
485        {
486            warnings.push(Warning {
487                source: attr.span(),
488                warning: WarningType::AttributeValueIsLocked(attr_name),
489            });
490            return;
491        }
492
493        let attribute_value = AttributeValue {
494            allowable_value: AllowableValue::Any,
495            modification_context: ModificationContext::Anywhere,
496            value: attr.value().clone(),
497        };
498
499        self.attribute_values.insert(attr_name, attribute_value);
500    }
501
502    /// Assign the next section number for a given level.
503    pub(crate) fn assign_section_number(&mut self, level: usize) -> SectionNumber {
504        match self.topmost_section_type {
505            SectionType::Normal => {
506                self.last_section_number.assign_next_number(level);
507                self.last_section_number.clone()
508            }
509            SectionType::Appendix => {
510                self.last_appendix_section_number.assign_next_number(level);
511                self.last_appendix_section_number.clone()
512            }
513            SectionType::Discrete => {
514                // Shouldn't happen, but ignore if it does.
515                self.last_section_number.clone()
516            }
517        }
518    }
519}
520
521fn remap_attr_name<N: AsRef<str>>(raw_attr_name: N) -> String {
522    let attr_name = raw_attr_name.as_ref().to_lowercase();
523
524    // Some attribute names have aliases. Remap to the primary name.
525    match attr_name.as_str() {
526        "hardbreaks" => "hardbreaks-option".to_string(),
527        _ => attr_name,
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    #![allow(clippy::panic)]
534    #![allow(clippy::unwrap_used)]
535
536    use crate::{
537        attributes::Attrlist,
538        blocks::Block,
539        parser::{
540            CharacterReplacementType, IconRenderParams, ImageRenderParams,
541            InlineSubstitutionRenderer, LinkRenderParams, QuoteScope, QuoteType, SpecialCharacter,
542        },
543        tests::prelude::*,
544    };
545
546    #[test]
547    fn default_is_unset() {
548        let p = Parser::default();
549        assert_eq!(p.attribute_value("foo"), InterpretedValue::Unset);
550    }
551
552    #[test]
553    fn creates_catalog_if_needed() {
554        let mut p = Parser::default();
555        let doc = p.parse("= Hello, World!\n\n== First Section Title");
556        let cat = doc.catalog();
557        assert!(cat.refs.contains_key("_first_section_title"));
558
559        let doc = p.parse("= Hello, World!\n\n== Second Section Title");
560        let cat = doc.catalog();
561        assert!(!cat.refs.contains_key("_first_section_title"));
562        assert!(cat.refs.contains_key("_second_section_title"));
563    }
564
565    #[test]
566    fn with_intrinsic_attribute() {
567        let p =
568            Parser::default().with_intrinsic_attribute("foo", "bar", ModificationContext::Anywhere);
569
570        assert_eq!(p.attribute_value("foo"), InterpretedValue::Value("bar"));
571        assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
572
573        assert!(p.is_attribute_set("foo"));
574        assert!(!p.is_attribute_set("foo2"));
575        assert!(!p.is_attribute_set("xyz"));
576    }
577
578    #[test]
579    fn with_intrinsic_attribute_set() {
580        let p = Parser::default().with_intrinsic_attribute_bool(
581            "foo",
582            true,
583            ModificationContext::Anywhere,
584        );
585
586        assert_eq!(p.attribute_value("foo"), InterpretedValue::Set);
587        assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
588
589        assert!(p.is_attribute_set("foo"));
590        assert!(!p.is_attribute_set("foo2"));
591        assert!(!p.is_attribute_set("xyz"));
592    }
593
594    #[test]
595    fn with_intrinsic_attribute_unset() {
596        let p = Parser::default().with_intrinsic_attribute_bool(
597            "foo",
598            false,
599            ModificationContext::Anywhere,
600        );
601
602        assert_eq!(p.attribute_value("foo"), InterpretedValue::Unset);
603        assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
604
605        assert!(!p.is_attribute_set("foo"));
606        assert!(!p.is_attribute_set("foo2"));
607        assert!(!p.is_attribute_set("xyz"));
608    }
609
610    #[test]
611    fn can_not_override_locked_default_value() {
612        let mut parser = Parser::default();
613
614        let doc = parser.parse(":sp: not a space!");
615
616        assert_eq!(
617            doc.warnings().next().unwrap().warning,
618            WarningType::AttributeValueIsLocked("sp".to_owned())
619        );
620
621        assert_eq!(parser.attribute_value("sp"), InterpretedValue::Value(" "));
622    }
623
624    #[test]
625    fn catalog_transferred_to_document() {
626        let mut parser = Parser::default();
627        let doc = parser.parse("= Test Document\n\nSome content");
628
629        let catalog = doc.catalog();
630        assert!(catalog.is_empty());
631
632        // The catalog was transferred to the document, leaving the parser with
633        // an empty catalog.
634        assert!(parser.catalog.borrow().is_empty());
635    }
636
637    #[test]
638    fn block_ids_registered_in_catalog() {
639        let mut parser = Parser::default();
640        let doc = parser.parse("= Test Document\n\n[#my-block]\nSome content with an ID");
641
642        let catalog = doc.catalog();
643        assert!(!catalog.is_empty());
644        assert!(catalog.contains_id("my-block"));
645
646        let entry = catalog.get_ref("my-block").unwrap();
647        assert_eq!(entry.id, "my-block");
648        assert_eq!(entry.ref_type, crate::document::RefType::Anchor);
649    }
650
651    /// A simple test renderer that modifies special characters differently
652    /// from the default HTML renderer.
653    #[derive(Debug)]
654    struct TestRenderer;
655
656    impl InlineSubstitutionRenderer for TestRenderer {
657        fn render_special_character(&self, type_: SpecialCharacter, dest: &mut String) {
658            // Custom rendering: wrap special characters in brackets.
659            match type_ {
660                SpecialCharacter::Lt => dest.push_str("[LT]"),
661                SpecialCharacter::Gt => dest.push_str("[GT]"),
662                SpecialCharacter::Ampersand => dest.push_str("[AMP]"),
663            }
664        }
665
666        fn render_quoted_substitition(
667            &self,
668            _type_: QuoteType,
669            _scope: QuoteScope,
670            _attrlist: Option<Attrlist<'_>>,
671            _id: Option<String>,
672            body: &str,
673            dest: &mut String,
674        ) {
675            dest.push_str(body);
676        }
677
678        fn render_character_replacement(
679            &self,
680            _type_: CharacterReplacementType,
681            dest: &mut String,
682        ) {
683            dest.push_str("[CHAR]");
684        }
685
686        fn render_line_break(&self, dest: &mut String) {
687            dest.push_str("[BR]");
688        }
689
690        fn render_image(&self, _params: &ImageRenderParams, dest: &mut String) {
691            dest.push_str("[IMAGE]");
692        }
693
694        fn image_uri(
695            &self,
696            target_image_path: &str,
697            _parser: &Parser,
698            _asset_dir_key: Option<&str>,
699        ) -> String {
700            target_image_path.to_string()
701        }
702
703        fn render_icon(&self, _params: &IconRenderParams, dest: &mut String) {
704            dest.push_str("[ICON]");
705        }
706
707        fn render_link(&self, _params: &LinkRenderParams, dest: &mut String) {
708            dest.push_str("[LINK]");
709        }
710
711        fn render_anchor(&self, id: &str, _reftext: Option<String>, dest: &mut String) {
712            dest.push_str(&format!("[ANCHOR:{}]", id));
713        }
714
715        fn render_xref(&self, params: &crate::parser::XrefRenderParams, dest: &mut String) {
716            dest.push_str(&format!("[XREF:{}]", params.target));
717        }
718    }
719
720    #[test]
721    fn with_inline_substitution_renderer() {
722        let mut parser = Parser::default().with_inline_substitution_renderer(TestRenderer);
723
724        // Parse a simple document with special characters.
725        let doc = parser.parse("Hello & goodbye < world > test");
726
727        // The document should parse successfully.
728        assert_eq!(doc.warnings().count(), 0);
729
730        // Get the first block from the document.
731        let block = doc.nested_blocks().next().unwrap();
732
733        let Block::Simple(simple_block) = block else {
734            panic!("Expected simple block, got: {block:?}");
735        };
736
737        // Our custom renderer should show [AMP], [LT], and [GT] instead of HTML
738        // entities.
739        assert_eq!(
740            simple_block.content().rendered(),
741            "Hello [AMP] goodbye [LT] world [GT] test"
742        );
743    }
744}