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