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}