Skip to main content

asciidoc_parser/parser/
parser.rs

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