asciidoc_parser/parser/
parser.rs

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