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 SectionType::Discrete => {
470 // Shouldn't happen, but ignore if it does.
471 self.last_section_number.clone()
472 }
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 #![allow(clippy::panic)]
480 #![allow(clippy::unwrap_used)]
481
482 use pretty_assertions_sorted::assert_eq;
483
484 use crate::{
485 Parser,
486 attributes::Attrlist,
487 blocks::{Block, IsBlock},
488 parser::{
489 CharacterReplacementType, IconRenderParams, ImageRenderParams,
490 InlineSubstitutionRenderer, LinkRenderParams, ModificationContext, QuoteScope,
491 QuoteType, SpecialCharacter,
492 },
493 tests::prelude::*,
494 warnings::WarningType,
495 };
496
497 #[test]
498 fn default_is_unset() {
499 let p = Parser::default();
500 assert_eq!(p.attribute_value("foo"), InterpretedValue::Unset);
501 }
502
503 #[test]
504 fn creates_catalog_if_needed() {
505 let mut p = Parser::default();
506 let doc = p.parse("= Hello, World!\n\n== First Section Title");
507 let cat = doc.catalog();
508 assert!(cat.refs.contains_key("_first_section_title"));
509
510 let doc = p.parse("= Hello, World!\n\n== Second Section Title");
511 let cat = doc.catalog();
512 assert!(!cat.refs.contains_key("_first_section_title"));
513 assert!(cat.refs.contains_key("_second_section_title"));
514 }
515
516 #[test]
517 fn with_intrinsic_attribute() {
518 let p =
519 Parser::default().with_intrinsic_attribute("foo", "bar", ModificationContext::Anywhere);
520
521 assert_eq!(p.attribute_value("foo"), InterpretedValue::Value("bar"));
522 assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
523
524 assert!(p.is_attribute_set("foo"));
525 assert!(!p.is_attribute_set("foo2"));
526 assert!(!p.is_attribute_set("xyz"));
527 }
528
529 #[test]
530 fn with_intrinsic_attribute_set() {
531 let p = Parser::default().with_intrinsic_attribute_bool(
532 "foo",
533 true,
534 ModificationContext::Anywhere,
535 );
536
537 assert_eq!(p.attribute_value("foo"), InterpretedValue::Set);
538 assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
539
540 assert!(p.is_attribute_set("foo"));
541 assert!(!p.is_attribute_set("foo2"));
542 assert!(!p.is_attribute_set("xyz"));
543 }
544
545 #[test]
546 fn with_intrinsic_attribute_unset() {
547 let p = Parser::default().with_intrinsic_attribute_bool(
548 "foo",
549 false,
550 ModificationContext::Anywhere,
551 );
552
553 assert_eq!(p.attribute_value("foo"), InterpretedValue::Unset);
554 assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
555
556 assert!(!p.is_attribute_set("foo"));
557 assert!(!p.is_attribute_set("foo2"));
558 assert!(!p.is_attribute_set("xyz"));
559 }
560
561 #[test]
562 fn can_not_override_locked_default_value() {
563 let mut parser = Parser::default();
564
565 let doc = parser.parse(":sp: not a space!");
566
567 assert_eq!(
568 doc.warnings().next().unwrap().warning,
569 WarningType::AttributeValueIsLocked("sp".to_owned())
570 );
571
572 assert_eq!(parser.attribute_value("sp"), InterpretedValue::Value(" "));
573 }
574
575 #[test]
576 fn catalog_transferred_to_document() {
577 let mut parser = Parser::default();
578 let doc = parser.parse("= Test Document\n\nSome content");
579
580 let catalog = doc.catalog();
581 assert!(catalog.is_empty());
582
583 assert!(parser.catalog.is_none());
584 }
585
586 #[test]
587 fn block_ids_registered_in_catalog() {
588 let mut parser = Parser::default();
589 let doc = parser.parse("= Test Document\n\n[#my-block]\nSome content with an ID");
590
591 let catalog = doc.catalog();
592 assert!(!catalog.is_empty());
593 assert!(catalog.contains_id("my-block"));
594
595 let entry = catalog.get_ref("my-block").unwrap();
596 assert_eq!(entry.id, "my-block");
597 assert_eq!(entry.ref_type, crate::document::RefType::Anchor);
598 }
599
600 /// A simple test renderer that modifies special characters differently
601 /// from the default HTML renderer.
602 #[derive(Debug)]
603 struct TestRenderer;
604
605 impl InlineSubstitutionRenderer for TestRenderer {
606 fn render_special_character(&self, type_: SpecialCharacter, dest: &mut String) {
607 // Custom rendering: wrap special characters in brackets.
608 match type_ {
609 SpecialCharacter::Lt => dest.push_str("[LT]"),
610 SpecialCharacter::Gt => dest.push_str("[GT]"),
611 SpecialCharacter::Ampersand => dest.push_str("[AMP]"),
612 }
613 }
614
615 fn render_quoted_substitition(
616 &self,
617 _type_: QuoteType,
618 _scope: QuoteScope,
619 _attrlist: Option<Attrlist<'_>>,
620 _id: Option<String>,
621 body: &str,
622 dest: &mut String,
623 ) {
624 dest.push_str(body);
625 }
626
627 fn render_character_replacement(
628 &self,
629 _type_: CharacterReplacementType,
630 dest: &mut String,
631 ) {
632 dest.push_str("[CHAR]");
633 }
634
635 fn render_line_break(&self, dest: &mut String) {
636 dest.push_str("[BR]");
637 }
638
639 fn render_image(&self, _params: &ImageRenderParams, dest: &mut String) {
640 dest.push_str("[IMAGE]");
641 }
642
643 fn image_uri(
644 &self,
645 target_image_path: &str,
646 _parser: &Parser,
647 _asset_dir_key: Option<&str>,
648 ) -> String {
649 target_image_path.to_string()
650 }
651
652 fn render_icon(&self, _params: &IconRenderParams, dest: &mut String) {
653 dest.push_str("[ICON]");
654 }
655
656 fn render_link(&self, _params: &LinkRenderParams, dest: &mut String) {
657 dest.push_str("[LINK]");
658 }
659
660 fn render_anchor(&self, id: &str, _reftext: Option<String>, dest: &mut String) {
661 dest.push_str(&format!("[ANCHOR:{}]", id));
662 }
663 }
664
665 #[test]
666 fn with_inline_substitution_renderer() {
667 let mut parser = Parser::default().with_inline_substitution_renderer(TestRenderer);
668
669 // Parse a simple document with special characters.
670 let doc = parser.parse("Hello & goodbye < world > test");
671
672 // The document should parse successfully.
673 assert_eq!(doc.warnings().count(), 0);
674
675 // Get the first block from the document.
676 let block = doc.nested_blocks().next().unwrap();
677
678 let Block::Simple(simple_block) = block else {
679 panic!("Expected simple block, got: {block:?}");
680 };
681
682 // Our custom renderer should show [AMP], [LT], and [GT] instead of HTML
683 // entities.
684 assert_eq!(
685 simple_block.content().rendered(),
686 "Hello [AMP] goodbye [LT] world [GT] test"
687 );
688 }
689}