asciidoc_parser/parser/parser.rs
1use std::{
2 cell::{Cell, RefCell},
3 collections::{HashMap, HashSet},
4 rc::Rc,
5 sync::Arc,
6};
7
8use crate::{
9 Document, HasSpan,
10 blocks::{SectionNumber, SectionType},
11 document::{Attribute, Catalog, InterpretedValue, RefType},
12 parser::{
13 AllowableValue, AttributeValue, DocinfoFileHandler, HtmlSubstitutionRenderer,
14 IncludeFileHandler, InlineSubstitutionRenderer, ModificationContext, PathResolver,
15 ResolvedAttributes, SafeMode, SvgFileHandler,
16 built_in_attrs::{built_in_attrs, built_in_default_values},
17 preprocessor::preprocess,
18 },
19 warnings::{Warning, WarningType},
20};
21
22/// The [`Parser`] struct and its related structs allow a caller to configure
23/// how AsciiDoc parsing occurs and then to initiate the parsing process.
24#[derive(Clone, Debug)]
25pub struct Parser {
26 /// Attribute values at current state of parsing.
27 ///
28 /// Shared (copy-on-write via [`Arc`]) with the immutable built-in attribute
29 /// table, so creating or cloning a parser does not deep-copy the table; the
30 /// map is only copied the first time this parser modifies an attribute.
31 pub(crate) attribute_values: Arc<HashMap<String, AttributeValue>>,
32
33 /// Default values for attributes if "set." Immutable after construction and
34 /// shared via [`Arc`] (never copied per parser).
35 default_attribute_values: Arc<HashMap<String, String>>,
36
37 /// Specifies how the basic raw text of a simple block will be converted to
38 /// the format which will ultimately be presented in the final output.
39 ///
40 /// Typically this is an [`HtmlSubstitutionRenderer`] but clients may
41 /// provide alternative implementations.
42 pub(crate) renderer: Rc<dyn InlineSubstitutionRenderer>,
43
44 /// Specifies the name of the primary file to be parsed.
45 pub(crate) primary_file_name: Option<String>,
46
47 /// Specifies how to generate clean and secure paths relative to the parsing
48 /// context.
49 pub path_resolver: PathResolver,
50
51 /// Handler for resolving include:: directives.
52 pub(crate) include_file_handler: Option<Rc<dyn IncludeFileHandler>>,
53
54 /// Handler for resolving docinfo files. If absent, no docinfo content is
55 /// resolved.
56 pub(crate) docinfo_file_handler: Option<Rc<dyn DocinfoFileHandler>>,
57
58 /// Handler for reading the contents of an SVG file requested by an inline
59 /// image with the `inline` option. If absent, inline SVG images fall back
60 /// to rendering their alt text.
61 pub(crate) svg_file_handler: Option<Rc<dyn SvgFileHandler>>,
62
63 /// The safe mode under which the document is parsed and rendered. Controls
64 /// security-sensitive rendering behavior (such as whether an interactive
65 /// SVG image is rendered as an `<object>` element). Defaults to
66 /// [`SafeMode::Secure`].
67 pub(crate) safe: SafeMode,
68
69 /// Document catalog for tracking referenceable elements during parsing.
70 /// This is created during parsing and transferred to the Document when
71 /// complete.
72 ///
73 /// Wrapped in a [`RefCell`] so that anchors and references discovered deep
74 /// inside inline substitution (where only a shared `&Parser` is available,
75 /// e.g. within a regex [`Replacer`](regex::Replacer)) can still be
76 /// registered.
77 catalog: RefCell<Catalog>,
78
79 /// Most recently-assigned section number.
80 pub(crate) last_section_number: SectionNumber,
81
82 /// Most recently-assigned appendix section number.
83 pub(crate) last_appendix_section_number: SectionNumber,
84
85 /// Saved copy of sectnumlevels at end of document header.
86 pub(crate) sectnumlevels: usize,
87
88 /// Section type of outermost section. (Used to determine whether to number
89 /// child sections as a normal section or appendix.)
90 pub(crate) topmost_section_type: SectionType,
91
92 /// True while parsing the direct block children of a section that carries
93 /// the `bibliography` style.
94 ///
95 /// A top-level unordered list parsed in this scope implicitly inherits the
96 /// `bibliography` style (matching Asciidoctor), even without its own
97 /// `[bibliography]` attribute. The flag is saved and restored around each
98 /// section body, so a non-bibliography subsection clears it for its own
99 /// children (the style does not propagate into subsections).
100 pub(crate) parsing_bibliography_section_body: bool,
101
102 /// True while the principal text of a bibliography list item is being
103 /// substituted.
104 ///
105 /// Read through a shared `&Parser` by the macros substitution step so it
106 /// recognizes a leading bibliography anchor (`[[[id]]]`). It is wrapped in
107 /// a [`Cell`] because the substitution code paths (e.g. a regex
108 /// [`Replacer`](regex::Replacer)) only hold a shared reference to the
109 /// parser.
110 pub(crate) in_bibliography_list_item: Cell<bool>,
111
112 /// Live values of [counter] attributes, keyed by counter name (e.g.
113 /// `index`, `example-number`, `table-number`).
114 ///
115 /// A counter is a specialized document attribute: its value is *also* the
116 /// value of the document attribute of the same name. Counters are resolved
117 /// (and advanced) deep inside the attribute-reference substitution step,
118 /// where only a shared `&Parser` is available, so the new value is recorded
119 /// here through a [`RefCell`] and read back as an attribute by
120 /// [`attribute_value()`]. An explicit attribute assignment to a counter's
121 /// name supersedes this overlay (and is what allows `:!name:` to reset a
122 /// counter), so every attribute setter clears the matching entry.
123 ///
124 /// Captioned blocks (example, table, …) are numbered with this same
125 /// mechanism: each context's caption number is the counter named
126 /// `<context>-number`, mirroring Asciidoctor's `Document#counter`.
127 ///
128 /// [counter]: https://docs.asciidoctor.org/asciidoc/latest/attributes/counters/
129 /// [`attribute_value()`]: Self::attribute_value
130 pub(crate) counter_values: RefCell<HashMap<String, String>>,
131
132 /// Canonical names of attributes that are locked against modification from
133 /// the document body for the current scope.
134 ///
135 /// An AsciiDoc table cell creates a nested document that inherits the
136 /// parent document's attributes. An attribute that is *set* in the
137 /// parent _cannot_ be modified inside the cell (matching Asciidoctor,
138 /// which here diverges from the spec's "set or explicitly unset" wording),
139 /// so while a cell is being parsed every inherited attribute name
140 /// (other than a handful of exceptions) is recorded here and a body
141 /// attribute assignment to such a name is silently ignored. The set is
142 /// saved and restored around each cell, so the lock applies only within
143 /// the cell (and nests correctly).
144 pub(crate) locked_attribute_names: HashSet<String>,
145
146 /// Number of AsciiDoc table cells currently being parsed in the call stack.
147 ///
148 /// An AsciiDoc table cell creates a nested, standalone AsciiDoc document.
149 /// While that document is being parsed this counter is greater than zero,
150 /// which (matching Asciidoctor's `Document#nested?`) changes the default
151 /// cell separator of any table found inside from the vertical bar (`|`) to
152 /// the exclamation mark (`!`), so a nested table needs no explicit
153 /// `separator` attribute. The counter is incremented and decremented around
154 /// each AsciiDoc cell, so it nests correctly.
155 pub(crate) nested_document_depth: usize,
156
157 /// Catalog of callout numbers registered by verbatim blocks, used to
158 /// validate the callout lists that annotate them.
159 ///
160 /// Wrapped in a [`RefCell`] because callouts are registered deep inside the
161 /// callouts substitution step, where only a shared `&Parser` is available.
162 callouts: RefCell<CalloutCatalog>,
163
164 /// Warnings produced while replacing attribute references (e.g. a reference
165 /// to a missing attribute when `attribute-missing` is `warn`).
166 ///
167 /// Wrapped in a [`RefCell`] because attribute references are replaced deep
168 /// inside the attributes substitution step, where only a shared `&Parser`
169 /// is available. Each entry stores the byte offset and length of the source
170 /// span the warning refers to (rather than a borrowed
171 /// [`Span`](crate::Span), which the lifetime-free `Parser` cannot
172 /// hold), so the warnings can be turned into
173 /// spanned [`Warning`]s once the document's owned source is available.
174 substitution_warnings: RefCell<Vec<DeferredWarning>>,
175}
176
177/// A warning recorded in a form that does not borrow the source so it can live
178/// on the [`Parser`] (or be returned from preprocessing), to be reconstituted
179/// into a spanned [`Warning`] once the document's owned source is available.
180///
181/// This is used both for warnings raised while replacing attribute references
182/// and for warnings raised during preprocessing (e.g. an unresolved include
183/// directive). The `offset`/`len` pair locates the relevant text within the
184/// (preprocessed) document source.
185#[derive(Clone, Debug)]
186pub(crate) struct DeferredWarning {
187 /// Byte offset into the document source of the span this warning refers to.
188 pub(crate) offset: usize,
189
190 /// Byte length of the span this warning refers to.
191 pub(crate) len: usize,
192
193 /// The type of warning, already carrying any owned data it needs (such as
194 /// the missing attribute's name).
195 pub(crate) warning: WarningType,
196}
197
198/// Tracks the callout numbers defined by verbatim blocks so that a callout list
199/// can be validated against the callouts it annotates.
200///
201/// This mirrors the relevant behavior of Asciidoctor's `Callouts` catalog: each
202/// verbatim block registers the callout numbers it defines into the current
203/// list, and each callout list checks its items against that list (warning
204/// about any item with no matching callout) before the list is closed.
205#[derive(Clone, Debug, Default)]
206struct CalloutCatalog {
207 /// Callout numbers registered (in document order) since the last callout
208 /// list was closed.
209 current: Vec<u32>,
210}
211
212impl Default for Parser {
213 fn default() -> Self {
214 Self {
215 attribute_values: built_in_attrs(),
216 default_attribute_values: built_in_default_values(),
217 renderer: Rc::new(HtmlSubstitutionRenderer {}),
218 primary_file_name: None,
219 path_resolver: PathResolver::default(),
220 include_file_handler: None,
221 docinfo_file_handler: None,
222 svg_file_handler: None,
223 safe: SafeMode::default(),
224 catalog: RefCell::new(Catalog::new()),
225 last_section_number: SectionNumber::default(),
226 last_appendix_section_number: SectionNumber {
227 section_type: SectionType::Appendix,
228 components: vec![],
229 },
230 sectnumlevels: 3,
231 topmost_section_type: SectionType::Normal,
232 parsing_bibliography_section_body: false,
233 in_bibliography_list_item: Cell::new(false),
234 counter_values: RefCell::new(HashMap::new()),
235 locked_attribute_names: HashSet::new(),
236 nested_document_depth: 0,
237 callouts: RefCell::new(CalloutCatalog::default()),
238 substitution_warnings: RefCell::new(vec![]),
239 }
240 }
241}
242
243impl Parser {
244 /// Parse a UTF-8 string as an AsciiDoc document.
245 ///
246 /// The [`Document`] data structure returned by this call has a '`static`
247 /// lifetime; this is an implementation detail. It retains a copy of the
248 /// `source` string that was passed in, but it is not tied to the lifetime
249 /// of that string.
250 ///
251 /// Nearly all of the data structures contained within the [`Document`]
252 /// structure are tied to the lifetime of the document and have a `'src`
253 /// lifetime to signal their dependency on the source document.
254 ///
255 /// **IMPORTANT:** The AsciiDoc language documentation states that UTF-16
256 /// encoding is allowed if a byte-order-mark (BOM) is present at the
257 /// start of a file. This format is not directly supported by the
258 /// `asciidoc-parser` crate. Any UTF-16 content must be re-encoded as
259 /// UTF-8 prior to parsing.
260 ///
261 /// The `Parser` struct will be updated with document attribute values
262 /// discovered during parsing. These values may be inspected using
263 /// [`attribute_value()`].
264 ///
265 /// # Warnings, not errors
266 ///
267 /// Any UTF-8 string is a valid AsciiDoc document, so this function does not
268 /// return an [`Option`] or [`Result`] data type. There may be any number of
269 /// character sequences that have ambiguous or potentially unintended
270 /// meanings. For that reason, a caller is advised to review the warnings
271 /// provided via the [`warnings()`] iterator.
272 ///
273 /// [`warnings()`]: Document::warnings
274 /// [`attribute_value()`]: Self::attribute_value
275 pub fn parse(&mut self, source: &str) -> Document<'static> {
276 let mut document = self.parse_deferred(source);
277
278 // Resolve cross-references against this document's own catalog. For
279 // multi-document workflows, use `parse_deferred` and resolve later with
280 // a caller-supplied resolver via `Document::resolve_references`.
281 document.resolve_against_own_catalog(&*self.renderer);
282
283 document
284 }
285
286 /// Parse a UTF-8 string as an AsciiDoc document, leaving cross-references
287 /// unresolved.
288 ///
289 /// This behaves like [`parse()`], except it does not resolve
290 /// cross-references (`<<id>>`, `xref:id[…]`). The returned [`Document`]
291 /// carries its references in a deferred state; resolve them later with
292 /// [`Document::resolve_references`].
293 ///
294 /// This is the entry point for multi-document workflows (e.g. Antora-style
295 /// site generation): parse every document with this method, build a
296 /// combined index from each document's [`catalog()`], then resolve each
297 /// document against that index. This crate does not merge catalogs
298 /// itself.
299 ///
300 /// [`parse()`]: Self::parse
301 /// [`catalog()`]: Document::catalog
302 pub fn parse_deferred(&mut self, source: &str) -> Document<'static> {
303 let (preprocessed_source, source_map, preprocessor_warnings) = preprocess(source, self);
304
305 // NOTE: `Document::parse` will transfer the catalog to itself at the end of the
306 // parsing operation. Start each parse with a fresh catalog.
307 *self.catalog.borrow_mut() = Catalog::new();
308
309 // Start each parse with an empty callout catalog.
310 *self.callouts.borrow_mut() = CalloutCatalog::default();
311
312 // Start each parse with no pending substitution warnings.
313 self.substitution_warnings.borrow_mut().clear();
314
315 // Reset section numbering for each new document.
316 self.last_section_number = SectionNumber::default();
317
318 // Reset counter (and captioned-block) numbering for each new document.
319 self.counter_values.borrow_mut().clear();
320
321 Document::parse(
322 &preprocessed_source,
323 source_map,
324 preprocessor_warnings,
325 self,
326 )
327 }
328
329 /// Retrieves the current interpreted value of a [document attribute].
330 ///
331 /// Each document holds a set of name-value pairs called document
332 /// attributes. These attributes provide a means of configuring the AsciiDoc
333 /// processor, declaring document metadata, and defining reusable content.
334 /// This page introduces document attributes and answers some questions
335 /// about the terminology used when referring to them.
336 ///
337 /// ## What are document attributes?
338 ///
339 /// Document attributes are effectively document-scoped variables for the
340 /// AsciiDoc language. The AsciiDoc language defines a set of built-in
341 /// attributes, and also allows the author (or extensions) to define
342 /// additional document attributes, which may replace built-in attributes
343 /// when permitted.
344 ///
345 /// Built-in attributes either provide access to read-only information about
346 /// the document and its environment or allow the author to configure
347 /// behavior of the AsciiDoc processor for a whole document or select
348 /// regions. Built-in attributes are effectively unordered. User-defined
349 /// attribute serve as a powerful text replacement tool. User-defined
350 /// attributes are stored in the order in which they are defined.
351 ///
352 /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
353 pub fn attribute_value<N: AsRef<str>>(&self, name: N) -> InterpretedValue {
354 // A counter's current value lives in the overlay and supersedes any
355 // earlier value of the attribute of the same name (see
356 // [`counter_values`](Self::counter_values)).
357 if let Some(value) = self.counter_values.borrow().get(name.as_ref()) {
358 return InterpretedValue::Value(value.clone());
359 }
360
361 self.attribute_values
362 .get(name.as_ref())
363 .map(|av| av.value.clone())
364 .map(|av| {
365 if let InterpretedValue::Set = av
366 && let Some(default) = self.default_attribute_values.get(name.as_ref())
367 {
368 InterpretedValue::Value(default.clone())
369 } else {
370 av
371 }
372 })
373 .unwrap_or(InterpretedValue::Unset)
374 }
375
376 /// Returns `true` if the parser has a [document attribute] by this name.
377 ///
378 /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
379 pub fn has_attribute<N: AsRef<str>>(&self, name: N) -> bool {
380 self.counter_values.borrow().contains_key(name.as_ref())
381 || self.attribute_values.contains_key(name.as_ref())
382 }
383
384 /// Returns `true` if the parser has a [document attribute] by this name
385 /// which has been set (i.e. is present and not [unset]).
386 ///
387 /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
388 /// [unset]: https://docs.asciidoctor.org/asciidoc/latest/attributes/unset-attributes/
389 pub fn is_attribute_set<N: AsRef<str>>(&self, name: N) -> bool {
390 // A counter always holds a concrete (set) value.
391 if self.counter_values.borrow().contains_key(name.as_ref()) {
392 return true;
393 }
394
395 self.attribute_values
396 .get(name.as_ref())
397 .map(|a| a.value != InterpretedValue::Unset)
398 .unwrap_or(false)
399 }
400
401 /// Captures the parser's fully-resolved document-attribute state so it can
402 /// outlive the parser — for example, retained on a [`Document`] to answer
403 /// [`attribute_value`]/[`has_attribute`]/[`is_attribute_set`] without a
404 /// parser in hand (the embed path a renderer uses for `convert_document`).
405 ///
406 /// This shares the parser's attribute tables by [`Arc`] rather than copying
407 /// them, so it is cheap to take on every parse (the large built-in table is
408 /// never deep-cloned). See [`ResolvedAttributes`].
409 ///
410 /// [`Document`]: crate::Document
411 /// [`attribute_value`]: Self::attribute_value
412 /// [`has_attribute`]: Self::has_attribute
413 /// [`is_attribute_set`]: Self::is_attribute_set
414 pub(crate) fn snapshot_attributes(&self) -> ResolvedAttributes {
415 ResolvedAttributes::new(
416 Arc::clone(&self.attribute_values),
417 Arc::clone(&self.default_attribute_values),
418 self.counter_values.borrow().clone(),
419 )
420 }
421
422 /// Resolves whether a document title should be displayed, from the
423 /// `showtitle`/`notitle` attribute pair (which are complements).
424 ///
425 /// `showtitle` takes precedence: if present, the title shows precisely when
426 /// it is set. Otherwise `notitle`, if present, hides the title when set.
427 /// When neither attribute is present, `default_shown` decides — a
428 /// standalone document (such as a nested AsciiDoc table cell) shows its
429 /// title, while an embedded document does not.
430 pub(crate) fn resolve_show_title(&self, default_shown: bool) -> bool {
431 if self.has_attribute("showtitle") {
432 self.is_attribute_set("showtitle")
433 } else if self.has_attribute("notitle") {
434 !self.is_attribute_set("notitle")
435 } else {
436 default_shown
437 }
438 }
439
440 /// Forces the `doctype` attribute to `value`, refreshing the derived
441 /// `backend-html5-doctype-*` attribute.
442 ///
443 /// Used when a nested AsciiDoc table cell resets its doctype to the default
444 /// (a cell does not inherit the parent's doctype). The value stays
445 /// modifiable from the document body so the cell may still set its own
446 /// doctype.
447 pub(crate) fn force_doctype(&mut self, value: &str) {
448 Arc::make_mut(&mut self.attribute_values).insert(
449 "doctype".to_string(),
450 AttributeValue {
451 allowable_value: AllowableValue::Any,
452 modification_context: ModificationContext::ApiOrDocumentBody,
453 value: InterpretedValue::Value(value.to_string()),
454 },
455 );
456 self.refresh_doctype_derived_attr();
457 }
458
459 /// Recomputes the `backend-html5-doctype-{doctype}` intrinsic attribute so
460 /// exactly one exists — for the active doctype — resolving to an empty
461 /// (defined) value. References to any other doctype stay undefined and so
462 /// render literally.
463 pub(crate) fn refresh_doctype_derived_attr(&mut self) {
464 Arc::make_mut(&mut self.attribute_values)
465 .retain(|name, _| !name.starts_with("backend-html5-doctype-"));
466
467 if let InterpretedValue::Value(doctype) = self.attribute_value("doctype") {
468 Arc::make_mut(&mut self.attribute_values).insert(
469 format!("backend-html5-doctype-{doctype}"),
470 AttributeValue {
471 allowable_value: AllowableValue::Any,
472 modification_context: ModificationContext::Anywhere,
473 value: InterpretedValue::Value(String::new()),
474 },
475 );
476 }
477 }
478
479 /// Sets the value of an [intrinsic attribute].
480 ///
481 /// Intrinsic attributes are set automatically by the processor. These
482 /// attributes provide information about the document being processed (e.g.,
483 /// `docfile`), the security mode under which the processor is running
484 /// (e.g., `safe-mode-name`), and information about the user’s environment
485 /// (e.g., `user-home`).
486 ///
487 /// The [`modification_context`](ModificationContext) establishes whether
488 /// the value can be subsequently modified by the document header and/or in
489 /// the document body.
490 ///
491 /// Subsequent calls to this function or [`with_intrinsic_attribute_bool()`]
492 /// are always permitted. The last such call for any given attribute name
493 /// takes precendence.
494 ///
495 /// [intrinsic attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
496 ///
497 /// [`with_intrinsic_attribute_bool()`]: Self::with_intrinsic_attribute_bool
498 pub fn with_intrinsic_attribute<N: AsRef<str>, V: AsRef<str>>(
499 mut self,
500 name: N,
501 value: V,
502 modification_context: ModificationContext,
503 ) -> Self {
504 let attribute_value = AttributeValue {
505 allowable_value: AllowableValue::Any,
506 modification_context,
507 value: InterpretedValue::Value(value.as_ref().to_string()),
508 };
509
510 Arc::make_mut(&mut self.attribute_values)
511 .insert(name.as_ref().to_lowercase(), attribute_value);
512
513 self
514 }
515
516 /// Register a referenceable element (anchor, section, bibliography entry)
517 /// in the document catalog.
518 ///
519 /// This takes `&self` (rather than `&mut self`) so that it can be called
520 /// from inline-substitution code paths that only hold a shared reference to
521 /// the parser, such as a regex [`Replacer`](regex::Replacer).
522 pub(crate) fn register_ref(
523 &self,
524 id: &str,
525 reftext: Option<&str>,
526 ref_type: RefType,
527 ) -> Result<(), crate::document::DuplicateIdError> {
528 self.catalog
529 .borrow_mut()
530 .register_ref(id, reftext, ref_type)
531 }
532
533 /// Registers a callout number defined by a verbatim block.
534 ///
535 /// Takes `&self` so it can be called from the callouts substitution step,
536 /// which only holds a shared reference to the parser.
537 pub(crate) fn register_callout(&self, number: u32) {
538 self.callouts.borrow_mut().current.push(number);
539 }
540
541 /// Returns `true` if a callout numbered `number` was registered for the
542 /// current (not-yet-closed) callout list.
543 pub(crate) fn callout_defined(&self, number: u32) -> bool {
544 self.callouts.borrow().current.contains(&number)
545 }
546
547 /// Closes the current callout list, so callouts registered afterward belong
548 /// to the next list.
549 pub(crate) fn close_callout_list(&self) {
550 self.callouts.borrow_mut().current.clear();
551 }
552
553 /// Returns the number of an already-defined footnote with the given ID, if
554 /// one exists in the current document's footnote registry.
555 ///
556 /// Takes `&self` so it can be called from the macros substitution step,
557 /// which only holds a shared reference to the parser.
558 pub(crate) fn footnote_index_for_id(&self, id: &str) -> Option<String> {
559 self.catalog
560 .borrow()
561 .footnote_with_id(id)
562 .map(|f| f.index.clone())
563 }
564
565 /// Defines a new footnote, advancing the `footnote-number` counter and
566 /// registering the footnote in the current document's registry. Returns the
567 /// number assigned to the footnote.
568 ///
569 /// Takes `&self` so it can be called from the macros substitution step.
570 pub(crate) fn define_footnote(
571 &self,
572 id: Option<&str>,
573 text: String,
574 xrefs: Vec<crate::content::XrefSegment>,
575 ) -> String {
576 // A footnote's text is extracted out of the block during macro
577 // substitution, so any cross-reference inside it never reaches the
578 // document-level resolution pass over block content. Those
579 // cross-references are captured (as placeholders in `text` plus the
580 // `xrefs` segments) so they can be resolved alongside the block
581 // references. The stored `text` is the unresolved fallback rendering
582 // until then, so it is always clean.
583 let (text, deferred) = if xrefs.is_empty() {
584 (text, None)
585 } else {
586 let deferred = crate::content::FootnoteDeferred::new(text, xrefs);
587 let rendered = deferred.render(&*self.renderer);
588 (rendered, Some(Box::new(deferred)))
589 };
590
591 // Footnotes are numbered consecutively throughout the document via the
592 // `footnote-number` counter, which is seeded to `0` so the first
593 // footnote is numbered `1`. The counter is a document-wide attribute, so
594 // numbering continues across nested documents (AsciiDoc table cells)
595 // even though the footnote *list* does not. The counter honors any seed
596 // the document sets, so a non-integer seed yields a non-integer number
597 // (matching Asciidoctor); the value is therefore kept as a string.
598 let index = self.counter("footnote-number", None);
599
600 self.catalog
601 .borrow_mut()
602 .register_footnote(crate::document::Footnote {
603 index: index.clone(),
604 id: id.map(|s| s.to_owned()),
605 text,
606 deferred,
607 });
608
609 index
610 }
611
612 /// Removes and returns the current document's footnote list, leaving an
613 /// empty list behind. Used to give a nested document (an AsciiDoc table
614 /// cell) its own footnote registry; see [`restore_footnotes`].
615 ///
616 /// [`restore_footnotes`]: Self::restore_footnotes
617 pub(crate) fn take_footnotes(&self) -> Vec<crate::document::Footnote> {
618 self.catalog.borrow_mut().take_footnotes()
619 }
620
621 /// Restores a previously-[taken](Self::take_footnotes) footnote list,
622 /// discarding any footnotes registered in the meantime (i.e. those defined
623 /// inside the nested document).
624 pub(crate) fn restore_footnotes(&self, footnotes: Vec<crate::document::Footnote>) {
625 self.catalog.borrow_mut().restore_footnotes(footnotes);
626 }
627
628 /// Records a warning produced while replacing attribute references.
629 ///
630 /// Takes `&self` so it can be called from the attributes substitution step,
631 /// which only holds a shared reference to the parser. `source` locates the
632 /// text the warning refers to; its byte offset and length are stored so a
633 /// spanned [`Warning`] can be reconstructed later (see
634 /// [`take_substitution_warnings`](Self::take_substitution_warnings)).
635 pub(crate) fn record_substitution_warning(
636 &self,
637 source: crate::Span<'_>,
638 warning: WarningType,
639 ) {
640 self.substitution_warnings
641 .borrow_mut()
642 .push(DeferredWarning {
643 offset: source.byte_offset(),
644 len: source.len(),
645 warning,
646 });
647 }
648
649 /// Returns the number of substitution warnings recorded so far.
650 ///
651 /// Used together with [`truncate_substitution_warnings`] to discard
652 /// warnings recorded while parsing an owned (e.g. include-expanded) source,
653 /// whose offsets do not refer to the primary document source.
654 ///
655 /// [`truncate_substitution_warnings`]: Self::truncate_substitution_warnings
656 pub(crate) fn substitution_warnings_len(&self) -> usize {
657 self.substitution_warnings.borrow().len()
658 }
659
660 /// Discards any substitution warnings recorded since the buffer held `len`
661 /// entries.
662 pub(crate) fn truncate_substitution_warnings(&self, len: usize) {
663 self.substitution_warnings.borrow_mut().truncate(len);
664 }
665
666 /// Takes the substitution warnings recorded during parsing, leaving the
667 /// buffer empty.
668 pub(crate) fn take_substitution_warnings(&self) -> Vec<DeferredWarning> {
669 std::mem::take(&mut *self.substitution_warnings.borrow_mut())
670 }
671
672 /// Generate a unique ID derived from `base_id` and register it in the
673 /// document catalog, returning the ID that was assigned.
674 pub(crate) fn generate_and_register_unique_id(
675 &self,
676 base_id: &str,
677 reftext: Option<&str>,
678 ref_type: RefType,
679 ) -> String {
680 self.catalog
681 .borrow_mut()
682 .generate_and_register_unique_id(base_id, reftext, ref_type)
683 }
684
685 /// Takes the catalog from the parser, transferring ownership and leaving an
686 /// empty catalog in its place.
687 ///
688 /// This is used by `Document::parse` to transfer the catalog from the
689 /// parser to the document at the end of parsing.
690 pub(crate) fn take_catalog(&mut self) -> Catalog {
691 std::mem::take(&mut *self.catalog.borrow_mut())
692 }
693
694 /* Comment out until we're prepared to use and test this.
695 /// Sets the default value for an [intrinsic attribute].
696 ///
697 /// Default values for attributes are provided automatically by the
698 /// processor. These values provide a falllback textual value for an
699 /// attribute when it is merely "set" by the document via API, header, or
700 /// document body.
701 ///
702 /// Calling this does not imply that the value is set automatically by
703 /// default, nor does it establish any policy for where the value may be
704 /// modified. For that, please use [`with_intrinsic_attribute`].
705 ///
706 /// [intrinsic attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
707 /// [`with_intrinsic_attribute`]: Self::with_intrinsic_attribute
708 pub fn with_default_attribute_value<N: AsRef<str>, V: AsRef<str>>(
709 mut self,
710 name: N,
711 value: V,
712 ) -> Self {
713 self.default_attribute_values
714 .insert(name.as_ref().to_string(), value.as_ref().to_string());
715
716 self
717 }
718 */
719
720 /// Sets the value of an [intrinsic attribute] from a boolean flag.
721 ///
722 /// A boolean `true` is interpreted as "set." A boolean `false` is
723 /// interpreted as "unset."
724 ///
725 /// Intrinsic attributes are set automatically by the processor. These
726 /// attributes provide information about the document being processed (e.g.,
727 /// `docfile`), the security mode under which the processor is running
728 /// (e.g., `safe-mode-name`), and information about the user’s environment
729 /// (e.g., `user-home`).
730 ///
731 /// The [`modification_context`](ModificationContext) establishes whether
732 /// the value can be subsequently modified by the document header and/or in
733 /// the document body.
734 ///
735 /// Subsequent calls to this function or [`with_intrinsic_attribute()`] are
736 /// always permitted. The last such call for any given attribute name takes
737 /// precendence.
738 ///
739 /// [intrinsic attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
740 ///
741 /// [`with_intrinsic_attribute()`]: Self::with_intrinsic_attribute
742 pub fn with_intrinsic_attribute_bool<N: AsRef<str>>(
743 mut self,
744 name: N,
745 value: bool,
746 modification_context: ModificationContext,
747 ) -> Self {
748 let attribute_value = AttributeValue {
749 allowable_value: AllowableValue::Any,
750 modification_context,
751 value: if value {
752 InterpretedValue::Set
753 } else {
754 InterpretedValue::Unset
755 },
756 };
757
758 Arc::make_mut(&mut self.attribute_values)
759 .insert(name.as_ref().to_lowercase(), attribute_value);
760
761 self
762 }
763
764 /// Replace the default [`InlineSubstitutionRenderer`] for this parser.
765 ///
766 /// The default implementation of [`InlineSubstitutionRenderer`] that is
767 /// provided is suitable for HTML5 rendering. If you are targeting a
768 /// different back-end rendering, you will need to provide your own
769 /// implementation and set it using this call before parsing.
770 pub fn with_inline_substitution_renderer<ISR: InlineSubstitutionRenderer + 'static>(
771 mut self,
772 renderer: ISR,
773 ) -> Self {
774 self.renderer = Rc::new(renderer);
775 self
776 }
777
778 /// Sets the name of the primary file to be parsed when [`parse()`] is
779 /// called.
780 ///
781 /// This name will be used for any error messages detected in this file and
782 /// also will be passed to [`IncludeFileHandler::resolve_target()`] as the
783 /// `source` argument for any `include::` file resolution requests from this
784 /// file.
785 ///
786 /// [`parse()`]: Self::parse
787 /// [`IncludeFileHandler::resolve_target()`]: crate::parser::IncludeFileHandler::resolve_target
788 pub fn with_primary_file_name<S: AsRef<str>>(mut self, name: S) -> Self {
789 self.primary_file_name = Some(name.as_ref().to_owned());
790 self
791 }
792
793 /// Sets the [`IncludeFileHandler`] for this parser.
794 ///
795 /// The include file handler is responsible for resolving `include::`
796 /// directives encountered during preprocessing. If no handler is provided,
797 /// include directives will be ignored.
798 ///
799 /// [`IncludeFileHandler`]: crate::parser::IncludeFileHandler
800 pub fn with_include_file_handler<IFH: IncludeFileHandler + 'static>(
801 mut self,
802 handler: IFH,
803 ) -> Self {
804 self.include_file_handler = Some(Rc::new(handler));
805 self
806 }
807
808 /// Sets the [`DocinfoFileHandler`] for this parser.
809 ///
810 /// The docinfo file handler is responsible for providing the content of
811 /// [docinfo files] requested while resolving a document's docinfo (see the
812 /// `docinfo` attribute). If no handler is provided, no docinfo content is
813 /// resolved and [`Document::docinfo`] returns an empty string for every
814 /// location.
815 ///
816 /// [`DocinfoFileHandler`]: crate::parser::DocinfoFileHandler
817 /// [docinfo files]: https://docs.asciidoctor.org/asciidoc/latest/docinfo/
818 /// [`Document::docinfo`]: crate::Document::docinfo
819 pub fn with_docinfo_file_handler<DFH: DocinfoFileHandler + 'static>(
820 mut self,
821 handler: DFH,
822 ) -> Self {
823 self.docinfo_file_handler = Some(Rc::new(handler));
824 self
825 }
826
827 /// Sets the [`SvgFileHandler`] for this parser.
828 ///
829 /// The SVG file handler is responsible for providing the raw contents of an
830 /// SVG file requested by an inline image with the `inline` option (e.g.
831 /// `image:diagram.svg[opts=inline]`). If no handler is provided, inline SVG
832 /// images fall back to rendering their alt text.
833 ///
834 /// [`SvgFileHandler`]: crate::parser::SvgFileHandler
835 pub fn with_svg_file_handler<SFH: SvgFileHandler + 'static>(mut self, handler: SFH) -> Self {
836 self.svg_file_handler = Some(Rc::new(handler));
837 self
838 }
839
840 /// Sets the [`SafeMode`] under which the document is parsed and rendered.
841 ///
842 /// The default is [`SafeMode::Secure`], the most conservative setting.
843 /// Relaxing the safe mode enables security-sensitive rendering behavior,
844 /// such as rendering an interactive SVG image as an `<object>` element.
845 ///
846 /// [`SafeMode`]: crate::SafeMode
847 pub fn with_safe_mode(mut self, safe: SafeMode) -> Self {
848 self.safe = safe;
849 self.apply_safe_mode_attributes();
850 self
851 }
852
853 /// Refreshes the `safe-mode-*` family of [intrinsic attributes] from the
854 /// current safe mode.
855 ///
856 /// These attributes let a document (or a downstream converter) inspect the
857 /// security mode under which it is being processed:
858 ///
859 /// * `safe-mode-level` — the numeric level (`0`, `1`, `10`, or `20`).
860 /// * `safe-mode-name` — the lowercase mode name (`unsafe`, `safe`,
861 /// `server`, or `secure`).
862 /// * `safe-mode-<name>` — a single flag attribute (set to an empty value)
863 /// naming the active mode; the flags for the other modes are left unset
864 /// so that a reference to them resolves literally.
865 ///
866 /// All of these are read-only from the document's perspective (they can
867 /// only be established via the API), matching Ruby Asciidoctor.
868 ///
869 /// [intrinsic attributes]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes
870 fn apply_safe_mode_attributes(&mut self) {
871 let attrs = Arc::make_mut(&mut self.attribute_values);
872
873 let intrinsic = |value: InterpretedValue| AttributeValue {
874 allowable_value: AllowableValue::Any,
875 modification_context: ModificationContext::ApiOnly,
876 value,
877 };
878
879 attrs.insert(
880 "safe-mode-level".to_string(),
881 intrinsic(InterpretedValue::Value(self.safe.level().to_string())),
882 );
883 attrs.insert(
884 "safe-mode-name".to_string(),
885 intrinsic(InterpretedValue::Value(self.safe.name().to_string())),
886 );
887
888 // Exactly one `safe-mode-<name>` flag is set (to an empty value); the
889 // rest are removed so that referencing them resolves literally.
890 for mode in [
891 SafeMode::Unsafe,
892 SafeMode::Safe,
893 SafeMode::Server,
894 SafeMode::Secure,
895 ] {
896 let name = format!("safe-mode-{}", mode.name());
897 if mode == self.safe {
898 attrs.insert(name, intrinsic(InterpretedValue::Set));
899 } else {
900 attrs.remove(&name);
901 }
902 }
903 }
904
905 /// Returns the [`SafeMode`] under which this parser operates.
906 ///
907 /// [`SafeMode`]: crate::SafeMode
908 pub fn safe_mode(&self) -> SafeMode {
909 self.safe
910 }
911
912 /// Returns the document name (`docname`): the base name of the primary
913 /// file, stripped of its directory and final extension.
914 ///
915 /// This is the `<docname>` used to build private docinfo file names (e.g.
916 /// `mydoc-docinfo.html` for `mydoc.adoc`). Returns `None` when no primary
917 /// file name has been set, in which case private docinfo files cannot be
918 /// resolved.
919 pub(crate) fn docname(&self) -> Option<String> {
920 let primary = self.primary_file_name.as_deref()?;
921
922 // Strip the directory portion (handling both separators, since the
923 // primary file name may have been supplied on either platform).
924 let base = primary.rsplit(['/', '\\']).next().unwrap_or(primary);
925
926 // Strip a single trailing extension, if present. A leading-dot name
927 // (e.g. `.adoc`) is treated as having no extension and is kept whole as
928 // the stem, matching Ruby's `File.basename(".adoc", ".*")`.
929 let stem = match base.rfind('.') {
930 Some(0) | None => base,
931 Some(idx) => &base[..idx],
932 };
933
934 if stem.is_empty() {
935 None
936 } else {
937 Some(stem.to_string())
938 }
939 }
940
941 /// Called from [`Header::parse()`] to accept or reject an attribute value.
942 ///
943 /// [`Header::parse()`]: crate::document::Header::parse
944 pub(crate) fn set_attribute_from_header<'src>(
945 &mut self,
946 attr: &Attribute<'src>,
947 warnings: &mut Vec<Warning<'src>>,
948 ) {
949 let attr_name = remap_attr_name(attr.name().data());
950
951 let existing_attr = self.attribute_values.get(&attr_name);
952
953 // Verify that we have permission to overwrite any existing attribute value.
954 if let Some(existing_attr) = existing_attr
955 && (existing_attr.modification_context == ModificationContext::ApiOnly
956 || existing_attr.modification_context == ModificationContext::ApiOrDocumentBody)
957 {
958 warnings.push(Warning {
959 source: attr.span(),
960 warning: WarningType::AttributeValueIsLocked(attr_name),
961 });
962 return;
963 }
964
965 let mut value = attr.value().clone();
966
967 if let InterpretedValue::Set = value
968 && let Some(default_value) = self.default_attribute_values.get(&attr_name)
969 {
970 value = InterpretedValue::Value(default_value.clone());
971 }
972
973 let attribute_value = AttributeValue {
974 allowable_value: AllowableValue::Any,
975 modification_context: ModificationContext::Anywhere,
976 value,
977 };
978
979 // An explicit assignment supersedes (and resets) any counter of the same
980 // name.
981 self.counter_values.borrow_mut().remove(&attr_name);
982
983 let is_doctype = attr_name == "doctype";
984 Arc::make_mut(&mut self.attribute_values).insert(attr_name, attribute_value);
985 if is_doctype {
986 self.refresh_doctype_derived_attr();
987 }
988 }
989
990 /// Called from [`Header::parse()`] for a value that is derived from parsing
991 /// the header (except for attribute lines).
992 ///
993 /// [`Header::parse()`]: crate::document::Header::parse
994 pub(crate) fn set_attribute_by_value_from_header<N: AsRef<str>, V: AsRef<str>>(
995 &mut self,
996 name: N,
997 value: V,
998 ) {
999 let attr_name = remap_attr_name(name);
1000
1001 let attribute_value = AttributeValue {
1002 allowable_value: AllowableValue::Any,
1003 modification_context: ModificationContext::Anywhere,
1004 value: InterpretedValue::Value(value.as_ref().to_owned()),
1005 };
1006
1007 self.counter_values.borrow_mut().remove(&attr_name);
1008 Arc::make_mut(&mut self.attribute_values).insert(attr_name, attribute_value);
1009 }
1010
1011 /// Applies the `imagesdir`-relative default for the `iconsdir` attribute.
1012 ///
1013 /// The `iconsdir` attribute defaults to `{imagesdir}/icons`; when
1014 /// `imagesdir` is left empty this resolves to the built-in
1015 /// [`DEFAULT_ICONSDIR`] (`./images/icons`). When `imagesdir` is set to a
1016 /// non-empty value and `iconsdir` was left at its built-in default, the
1017 /// icons directory is derived as `{imagesdir}/icons`.
1018 ///
1019 /// The derivation is skipped — so an explicit `iconsdir` wins — when either
1020 /// the attribute was set in the header (`iconsdir_set_in_header`) or its
1021 /// resolved value differs from [`DEFAULT_ICONSDIR`] (which is how an
1022 /// override applied any other way, e.g. via the API, is detected). The one
1023 /// case this cannot detect is a non-header override whose value happens to
1024 /// equal the built-in default (e.g. an API caller setting `iconsdir` to
1025 /// exactly `./images/icons`): it is indistinguishable from the default and
1026 /// so is re-derived. That combination is contradictory in practice (it
1027 /// pins `iconsdir` to the value it would take were `imagesdir` unset) and
1028 /// is not worth a dedicated provenance flag.
1029 ///
1030 /// This is called once, after the document header is parsed, mirroring
1031 /// Asciidoctor's document-initialization timing (a later `imagesdir` change
1032 /// in the document body does not retroactively re-derive `iconsdir`). See
1033 /// icons-image.adoc.
1034 ///
1035 /// [`DEFAULT_ICONSDIR`]: super::built_in_attrs::DEFAULT_ICONSDIR
1036 pub(crate) fn apply_iconsdir_default(&mut self, iconsdir_set_in_header: bool) {
1037 if iconsdir_set_in_header {
1038 return;
1039 }
1040
1041 // Preserve any override whose value differs from the built-in default
1042 // (e.g. one applied via the API); only the built-in default itself is
1043 // eligible for `imagesdir`-relative derivation. See the doc comment for
1044 // the one indistinguishable corner case.
1045 if self.attribute_value("iconsdir").as_maybe_str()
1046 != Some(super::built_in_attrs::DEFAULT_ICONSDIR)
1047 {
1048 return;
1049 }
1050
1051 let imagesdir = self.attribute_value("imagesdir");
1052 let derived = match imagesdir.as_maybe_str().filter(|d| !d.is_empty()) {
1053 Some(dir) => format!("{}/icons", dir.trim_end_matches('/')),
1054 None => return,
1055 };
1056
1057 self.set_attribute_by_value_from_header("iconsdir", derived);
1058 }
1059
1060 /// Called while parsing a block (see [`Block::parse_with_outcome()`]) to
1061 /// accept or reject an attribute value from a document (body) attribute.
1062 ///
1063 /// [`Block::parse_with_outcome()`]: crate::blocks::Block::parse_with_outcome
1064 pub(crate) fn set_attribute_from_body<'src>(
1065 &mut self,
1066 attr: &Attribute<'src>,
1067 warnings: &mut Vec<Warning<'src>>,
1068 ) {
1069 let attr_name = remap_attr_name(attr.name().data());
1070
1071 // An attribute inherited from the parent document of an AsciiDoc table
1072 // cell is locked for the duration of that cell: a body assignment to it
1073 // is silently ignored (no warning), matching Asciidoctor.
1074 if self.locked_attribute_names.contains(&attr_name) {
1075 return;
1076 }
1077
1078 // Verify that we have permission to overwrite any existing attribute value.
1079 if let Some(existing_attr) = self.attribute_values.get(&attr_name)
1080 && (existing_attr.modification_context != ModificationContext::Anywhere
1081 && existing_attr.modification_context != ModificationContext::ApiOrDocumentBody)
1082 {
1083 warnings.push(Warning {
1084 source: attr.span(),
1085 warning: WarningType::AttributeValueIsLocked(attr_name),
1086 });
1087 return;
1088 }
1089
1090 let attribute_value = AttributeValue {
1091 allowable_value: AllowableValue::Any,
1092 modification_context: ModificationContext::Anywhere,
1093 value: attr.value().clone(),
1094 };
1095
1096 // An explicit assignment supersedes (and resets) any counter of the same
1097 // name. This is what lets `:!name:` reset a counter.
1098 self.counter_values.borrow_mut().remove(&attr_name);
1099
1100 let is_doctype = attr_name == "doctype";
1101 Arc::make_mut(&mut self.attribute_values).insert(attr_name, attribute_value);
1102 if is_doctype {
1103 self.refresh_doctype_derived_attr();
1104 }
1105 }
1106
1107 /// Assign the next section number for a given level.
1108 pub(crate) fn assign_section_number(&mut self, level: usize) -> SectionNumber {
1109 match self.topmost_section_type {
1110 SectionType::Normal => {
1111 self.last_section_number.assign_next_number(level);
1112 self.last_section_number.clone()
1113 }
1114 SectionType::Appendix => {
1115 self.last_appendix_section_number.assign_next_number(level);
1116 self.last_appendix_section_number.clone()
1117 }
1118 SectionType::Discrete => {
1119 // Shouldn't happen, but ignore if it does.
1120 self.last_section_number.clone()
1121 }
1122 }
1123 }
1124
1125 /// Resolves a [counter] of the given `name`, advancing it to the next value
1126 /// in its sequence and returning that value.
1127 ///
1128 /// A counter is a specialized document attribute: its value is stored as
1129 /// (and read back from) the attribute of the same name, so a later
1130 /// `{name}` reference shows the current value and an attribute assignment
1131 /// such as `:!name:` resets it. Each resolution advances the counter:
1132 ///
1133 /// * an integer value is incremented (`1` -> `2`);
1134 /// * any other value is advanced like Ruby's `String#succ` (`a` -> `b`, `z`
1135 /// -> `aa`, `Az` -> `Ba`), matching Asciidoctor.
1136 ///
1137 /// `seed` (from the `{counter:name:seed}` form) supplies the first value,
1138 /// but only when the counter is currently unset; otherwise it is ignored.
1139 /// With no seed the sequence starts at `1`.
1140 ///
1141 /// This mirrors Asciidoctor's `Document#counter`.
1142 ///
1143 /// [counter]: https://docs.asciidoctor.org/asciidoc/latest/attributes/counters/
1144 pub(crate) fn counter(&self, name: &str, seed: Option<&str>) -> String {
1145 let next = match self.attribute_value(name) {
1146 InterpretedValue::Value(current) if !current.is_empty() => next_counter_value(¤t),
1147 _ => match seed {
1148 Some(seed) if !seed.is_empty() => seed.to_string(),
1149 _ => "1".to_string(),
1150 },
1151 };
1152
1153 self.counter_values
1154 .borrow_mut()
1155 .insert(name.to_string(), next.clone());
1156
1157 next
1158 }
1159}
1160
1161/// Advances a counter value to the next value in its sequence, mirroring
1162/// Asciidoctor's `Helpers.nextval`.
1163///
1164/// A canonical integer string (one that round-trips through integer parsing,
1165/// e.g. `7` but not `07` or `+7`) is incremented numerically. Anything else is
1166/// advanced with [`string_succ`].
1167fn next_counter_value(current: &str) -> String {
1168 if let Ok(n) = current.parse::<i64>()
1169 && n.to_string() == current
1170 {
1171 // `saturating_add` keeps a counter that has somehow reached `i64::MAX`
1172 // pinned there rather than panicking (debug) or wrapping (release).
1173 return n.saturating_add(1).to_string();
1174 }
1175
1176 string_succ(current)
1177}
1178
1179/// Returns the successor of a string, mirroring Ruby's `String#succ` for the
1180/// ASCII cases that AsciiDoc counters can produce.
1181///
1182/// The right-most alphanumeric character is incremented within its own class
1183/// (digits, lowercase letters, uppercase letters), carrying leftward on
1184/// wrap-around (`9` -> `0`, `z` -> `a`, `Z` -> `A`) and prepending a fresh
1185/// leading character (`1`, `a`, or `A`) when the carry runs off the front
1186/// (`z` -> `aa`, `Zz` -> `AAa`). A string with no alphanumeric characters has
1187/// the code point of its last character incremented.
1188fn string_succ(current: &str) -> String {
1189 let chars: Vec<char> = current.chars().collect();
1190
1191 // Without an alphanumeric to carry through, Ruby increments the code point
1192 // of the final character.
1193 if !chars.iter().any(char::is_ascii_alphanumeric) {
1194 let mut chars = chars;
1195 if let Some(last) = chars.last_mut() {
1196 *last = char::from_u32(*last as u32 + 1).unwrap_or(*last);
1197 }
1198 return chars.into_iter().collect();
1199 }
1200
1201 // Walk right to left. `carrying` stays true while we are still looking for
1202 // (or carrying through) the alphanumeric run: trailing non-alphanumeric
1203 // characters are passed over unchanged, then the right-most alphanumeric is
1204 // incremented within its class and any wrap-around carries leftward to the
1205 // next alphanumeric. When the carry runs off the front, a fresh leading
1206 // character of the same class is prepended (`z` -> `aa`, `9` -> `10`).
1207 let mut out_rev: Vec<char> = Vec::with_capacity(chars.len() + 1);
1208 let mut carrying = true;
1209 let mut lead = '1';
1210
1211 for &c in chars.iter().rev() {
1212 if carrying && c.is_ascii_alphanumeric() {
1213 // Increment within the character's class, carrying on wrap-around.
1214 // The arms are exhaustive over ASCII alphanumerics, so the catch-all
1215 // can only be `Z` (the one value not matched above).
1216 let (next, carry) = match c {
1217 '0'..='8' | 'a'..='y' | 'A'..='Y' => ((c as u8 + 1) as char, false),
1218 '9' => ('0', true),
1219 'z' => ('a', true),
1220 _ => ('A', true),
1221 };
1222 out_rev.push(next);
1223 carrying = carry;
1224 // On a carry, remember the class of leading character to prepend if
1225 // the carry runs off the front; `next` is `0`, `a`, or `A` here.
1226 lead = match next {
1227 '0' => '1',
1228 'a' => 'a',
1229 _ => 'A',
1230 };
1231 } else {
1232 // Either the carry is spent, or this is a trailing non-alphanumeric
1233 // we pass over while still searching for the run to increment.
1234 out_rev.push(c);
1235 }
1236 }
1237
1238 if carrying {
1239 out_rev.push(lead);
1240 }
1241
1242 out_rev.into_iter().rev().collect()
1243}
1244
1245fn remap_attr_name<N: AsRef<str>>(raw_attr_name: N) -> String {
1246 let attr_name = raw_attr_name.as_ref().to_lowercase();
1247
1248 // Some attribute names have aliases. Remap to the primary name.
1249 match attr_name.as_str() {
1250 "hardbreaks" => "hardbreaks-option".to_string(),
1251 _ => attr_name,
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 #![allow(clippy::panic)]
1258 #![allow(clippy::unwrap_used)]
1259
1260 use crate::{
1261 attributes::Attrlist,
1262 blocks::Block,
1263 parser::{
1264 CharacterReplacementType, IconRenderParams, ImageRenderParams,
1265 InlineSubstitutionRenderer, LinkRenderParams, QuoteScope, QuoteType, SpecialCharacter,
1266 },
1267 tests::prelude::*,
1268 };
1269
1270 #[test]
1271 fn default_is_unset() {
1272 let p = Parser::default();
1273 assert_eq!(p.attribute_value("foo"), InterpretedValue::Unset);
1274 }
1275
1276 #[test]
1277 fn creates_catalog_if_needed() {
1278 let mut p = Parser::default();
1279 let doc = p.parse("= Hello, World!\n\n== First Section Title");
1280 let cat = doc.catalog();
1281 assert!(cat.refs.contains_key("_first_section_title"));
1282
1283 let doc = p.parse("= Hello, World!\n\n== Second Section Title");
1284 let cat = doc.catalog();
1285 assert!(!cat.refs.contains_key("_first_section_title"));
1286 assert!(cat.refs.contains_key("_second_section_title"));
1287 }
1288
1289 #[test]
1290 fn with_intrinsic_attribute() {
1291 let p =
1292 Parser::default().with_intrinsic_attribute("foo", "bar", ModificationContext::Anywhere);
1293
1294 assert_eq!(p.attribute_value("foo"), InterpretedValue::Value("bar"));
1295 assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
1296
1297 assert!(p.is_attribute_set("foo"));
1298 assert!(!p.is_attribute_set("foo2"));
1299 assert!(!p.is_attribute_set("xyz"));
1300 }
1301
1302 #[test]
1303 fn with_intrinsic_attribute_set() {
1304 let p = Parser::default().with_intrinsic_attribute_bool(
1305 "foo",
1306 true,
1307 ModificationContext::Anywhere,
1308 );
1309
1310 assert_eq!(p.attribute_value("foo"), InterpretedValue::Set);
1311 assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
1312
1313 assert!(p.is_attribute_set("foo"));
1314 assert!(!p.is_attribute_set("foo2"));
1315 assert!(!p.is_attribute_set("xyz"));
1316 }
1317
1318 #[test]
1319 fn with_intrinsic_attribute_unset() {
1320 let p = Parser::default().with_intrinsic_attribute_bool(
1321 "foo",
1322 false,
1323 ModificationContext::Anywhere,
1324 );
1325
1326 assert_eq!(p.attribute_value("foo"), InterpretedValue::Unset);
1327 assert_eq!(p.attribute_value("foo2"), InterpretedValue::Unset);
1328
1329 assert!(!p.is_attribute_set("foo"));
1330 assert!(!p.is_attribute_set("foo2"));
1331 assert!(!p.is_attribute_set("xyz"));
1332 }
1333
1334 #[test]
1335 fn can_not_override_locked_default_value() {
1336 let mut parser = Parser::default();
1337
1338 let doc = parser.parse(":sp: not a space!");
1339
1340 assert_eq!(
1341 doc.warnings().next().unwrap().warning,
1342 WarningType::AttributeValueIsLocked("sp".to_owned())
1343 );
1344
1345 assert_eq!(parser.attribute_value("sp"), InterpretedValue::Value(" "));
1346 }
1347
1348 #[test]
1349 fn catalog_transferred_to_document() {
1350 let mut parser = Parser::default();
1351 let doc = parser.parse("= Test Document\n\nSome content");
1352
1353 let catalog = doc.catalog();
1354 assert!(catalog.is_empty());
1355
1356 // The catalog was transferred to the document, leaving the parser with
1357 // an empty catalog.
1358 assert!(parser.catalog.borrow().is_empty());
1359 }
1360
1361 #[test]
1362 fn block_ids_registered_in_catalog() {
1363 let mut parser = Parser::default();
1364 let doc = parser.parse("= Test Document\n\n[#my-block]\nSome content with an ID");
1365
1366 let catalog = doc.catalog();
1367 assert!(!catalog.is_empty());
1368 assert!(catalog.contains_id("my-block"));
1369
1370 let entry = catalog.get_ref("my-block").unwrap();
1371 assert_eq!(entry.id, "my-block");
1372 assert_eq!(entry.ref_type, crate::document::RefType::Anchor);
1373 }
1374
1375 /// A simple test renderer that modifies special characters differently
1376 /// from the default HTML renderer.
1377 #[derive(Debug)]
1378 struct TestRenderer;
1379
1380 impl InlineSubstitutionRenderer for TestRenderer {
1381 fn render_special_character(&self, type_: SpecialCharacter, dest: &mut String) {
1382 // Custom rendering: wrap special characters in brackets.
1383 match type_ {
1384 SpecialCharacter::Lt => dest.push_str("[LT]"),
1385 SpecialCharacter::Gt => dest.push_str("[GT]"),
1386 SpecialCharacter::Ampersand => dest.push_str("[AMP]"),
1387 }
1388 }
1389
1390 fn render_quoted_substitition(
1391 &self,
1392 _type_: QuoteType,
1393 _scope: QuoteScope,
1394 _attrlist: Option<Attrlist<'_>>,
1395 _id: Option<String>,
1396 body: &str,
1397 dest: &mut String,
1398 ) {
1399 dest.push_str(body);
1400 }
1401
1402 fn render_character_replacement(
1403 &self,
1404 _type_: CharacterReplacementType,
1405 dest: &mut String,
1406 ) {
1407 dest.push_str("[CHAR]");
1408 }
1409
1410 fn render_line_break(&self, dest: &mut String) {
1411 dest.push_str("[BR]");
1412 }
1413
1414 fn render_image(&self, _params: &ImageRenderParams, dest: &mut String) {
1415 dest.push_str("[IMAGE]");
1416 }
1417
1418 fn image_uri(
1419 &self,
1420 target_image_path: &str,
1421 _parser: &Parser,
1422 _asset_dir_key: Option<&str>,
1423 ) -> String {
1424 target_image_path.to_string()
1425 }
1426
1427 fn render_icon(&self, _params: &IconRenderParams, dest: &mut String) {
1428 dest.push_str("[ICON]");
1429 }
1430
1431 fn render_link(&self, _params: &LinkRenderParams, dest: &mut String) {
1432 dest.push_str("[LINK]");
1433 }
1434
1435 fn render_anchor(&self, id: &str, _reftext: Option<String>, dest: &mut String) {
1436 dest.push_str(&format!("[ANCHOR:{}]", id));
1437 }
1438
1439 fn render_xref(&self, params: &crate::parser::XrefRenderParams, dest: &mut String) {
1440 dest.push_str(&format!("[XREF:{}]", params.target));
1441 }
1442
1443 fn render_callout(&self, params: &crate::parser::CalloutRenderParams, dest: &mut String) {
1444 dest.push_str(&format!("[CALLOUT:{}]", params.number));
1445 }
1446
1447 fn render_index_term(
1448 &self,
1449 params: &crate::parser::IndexTermRenderParams,
1450 dest: &mut String,
1451 ) {
1452 match params.visible_term {
1453 Some(term) => dest.push_str(&format!("[INDEXTERM:{term}]")),
1454 None => dest.push_str("[INDEXTERM]"),
1455 }
1456 }
1457
1458 fn render_button(&self, text: &str, dest: &mut String) {
1459 dest.push_str(&format!("[BUTTON:{text}]"));
1460 }
1461
1462 fn render_keyboard(&self, keys: &[String], dest: &mut String) {
1463 dest.push_str(&format!("[KBD:{}]", keys.join("+")));
1464 }
1465
1466 fn render_menu(&self, params: &crate::parser::MenuRenderParams, dest: &mut String) {
1467 dest.push_str(&format!("[MENU:{}]", params.menu));
1468 }
1469
1470 fn render_footnote(&self, params: &crate::parser::FootnoteRenderParams, dest: &mut String) {
1471 match params.index {
1472 Some(index) => dest.push_str(&format!("[FOOTNOTE:{index}]")),
1473 None => dest.push_str(&format!("[FOOTNOTE:{}]", params.text)),
1474 }
1475 }
1476 }
1477
1478 #[test]
1479 fn with_inline_substitution_renderer() {
1480 let mut parser = Parser::default().with_inline_substitution_renderer(TestRenderer);
1481
1482 // Parse a simple document with special characters and a footnote.
1483 let doc = parser.parse("Hello & goodbye < world > test footnote:[a note]");
1484
1485 // The document should parse successfully.
1486 assert_eq!(doc.warnings().count(), 0);
1487
1488 // Get the first block from the document.
1489 let block = doc.nested_blocks().next().unwrap();
1490
1491 let Block::Simple(simple_block) = block else {
1492 panic!("Expected simple block, got: {block:?}");
1493 };
1494
1495 // Our custom renderer should show [AMP], [LT], and [GT] instead of HTML
1496 // entities, and a resolved footnote as [FOOTNOTE:<index>].
1497 assert_eq!(
1498 simple_block.content().rendered(),
1499 "Hello [AMP] goodbye [LT] world [GT] test [FOOTNOTE:1]"
1500 );
1501 }
1502
1503 #[test]
1504 fn custom_renderer_renders_unresolved_footnote() {
1505 let mut parser = Parser::default().with_inline_substitution_renderer(TestRenderer);
1506
1507 // An unresolved footnote reference exercises the renderer's `None`
1508 // (no index) branch, which our custom renderer shows as
1509 // [FOOTNOTE:<text>].
1510 let doc = parser.parse("test.footnote:missing[]");
1511
1512 let block = doc.nested_blocks().next().unwrap();
1513 let Block::Simple(simple_block) = block else {
1514 panic!("Expected simple block, got: {block:?}");
1515 };
1516
1517 assert_eq!(simple_block.content().rendered(), "test.[FOOTNOTE:missing]");
1518 }
1519
1520 mod resolve_show_title {
1521 use crate::parser::{ModificationContext, Parser};
1522
1523 fn with(name: &str, set: bool) -> Parser {
1524 Parser::default().with_intrinsic_attribute_bool(
1525 name,
1526 set,
1527 ModificationContext::Anywhere,
1528 )
1529 }
1530
1531 #[test]
1532 fn neither_present_uses_default() {
1533 assert!(Parser::default().resolve_show_title(true));
1534 assert!(!Parser::default().resolve_show_title(false));
1535 }
1536
1537 #[test]
1538 fn showtitle_takes_precedence_and_decides() {
1539 // Present and set -> shown; present and unset -> hidden, regardless
1540 // of the default.
1541 assert!(with("showtitle", true).resolve_show_title(false));
1542 assert!(!with("showtitle", false).resolve_show_title(true));
1543 }
1544
1545 #[test]
1546 fn notitle_is_the_complement_when_showtitle_absent() {
1547 // notitle set -> hidden; notitle unset -> shown.
1548 assert!(!with("notitle", true).resolve_show_title(true));
1549 assert!(with("notitle", false).resolve_show_title(false));
1550 }
1551 }
1552
1553 mod refresh_doctype_derived_attr {
1554 use crate::{document::InterpretedValue, parser::Parser};
1555
1556 #[test]
1557 fn tracks_the_active_doctype() {
1558 let mut parser = Parser::default();
1559
1560 // The default doctype is `article`, so only its derived attribute is
1561 // defined (to an empty value).
1562 assert_eq!(
1563 parser.attribute_value("backend-html5-doctype-article"),
1564 InterpretedValue::Value(String::new())
1565 );
1566 assert_eq!(
1567 parser.attribute_value("backend-html5-doctype-book"),
1568 InterpretedValue::Unset
1569 );
1570
1571 // Forcing a new doctype moves the derived attribute with it.
1572 parser.force_doctype("book");
1573 assert_eq!(
1574 parser.attribute_value("backend-html5-doctype-book"),
1575 InterpretedValue::Value(String::new())
1576 );
1577 assert_eq!(
1578 parser.attribute_value("backend-html5-doctype-article"),
1579 InterpretedValue::Unset
1580 );
1581 }
1582
1583 #[test]
1584 fn defines_no_derived_attr_when_doctype_is_not_a_value() {
1585 let mut parser = Parser::default();
1586
1587 // The default article derived attribute starts out defined.
1588 assert_eq!(
1589 parser.attribute_value("backend-html5-doctype-article"),
1590 InterpretedValue::Value(String::new())
1591 );
1592
1593 // With `doctype` unset (no `Value`), a refresh clears any existing
1594 // derived attribute and defines none.
1595 std::sync::Arc::make_mut(&mut parser.attribute_values).remove("doctype");
1596 parser.refresh_doctype_derived_attr();
1597
1598 assert_eq!(parser.attribute_value("doctype"), InterpretedValue::Unset);
1599 assert_eq!(
1600 parser.attribute_value("backend-html5-doctype-article"),
1601 InterpretedValue::Unset
1602 );
1603 }
1604 }
1605
1606 mod docname {
1607 use crate::Parser;
1608
1609 #[test]
1610 fn none_without_primary_file_name() {
1611 assert_eq!(Parser::default().docname(), None);
1612 }
1613
1614 #[test]
1615 fn strips_directory_and_extension() {
1616 assert_eq!(
1617 Parser::default()
1618 .with_primary_file_name("mydoc.adoc")
1619 .docname()
1620 .as_deref(),
1621 Some("mydoc")
1622 );
1623 assert_eq!(
1624 Parser::default()
1625 .with_primary_file_name("docs/guide/mydoc.adoc")
1626 .docname()
1627 .as_deref(),
1628 Some("mydoc")
1629 );
1630 // A Windows-style separator is handled too, since the primary file
1631 // name may be supplied on either platform.
1632 assert_eq!(
1633 Parser::default()
1634 .with_primary_file_name(r"docs\guide\mydoc.adoc")
1635 .docname()
1636 .as_deref(),
1637 Some("mydoc")
1638 );
1639 }
1640
1641 #[test]
1642 fn keeps_name_with_no_extension() {
1643 assert_eq!(
1644 Parser::default()
1645 .with_primary_file_name("README")
1646 .docname()
1647 .as_deref(),
1648 Some("README")
1649 );
1650 }
1651
1652 #[test]
1653 fn none_when_path_has_no_file_component() {
1654 // A primary file name that ends in a separator has an empty base
1655 // name, which yields no document name.
1656 assert_eq!(
1657 Parser::default()
1658 .with_primary_file_name("docs/guide/")
1659 .docname(),
1660 None
1661 );
1662 }
1663
1664 #[test]
1665 fn leading_dot_name_is_kept_whole() {
1666 // A leading-dot name (e.g. `.adoc`) is treated as a dotfile with no
1667 // extension and kept whole, matching Ruby's
1668 // `File.basename(".adoc", ".*")`.
1669 assert_eq!(
1670 Parser::default()
1671 .with_primary_file_name(".adoc")
1672 .docname()
1673 .as_deref(),
1674 Some(".adoc")
1675 );
1676 }
1677 }
1678
1679 mod counter {
1680 use super::super::next_counter_value;
1681 use crate::{document::InterpretedValue, tests::prelude::*};
1682
1683 #[test]
1684 fn next_counter_value_integer() {
1685 assert_eq!(next_counter_value("1"), "2");
1686 assert_eq!(next_counter_value("9"), "10");
1687 assert_eq!(next_counter_value("0"), "1");
1688 assert_eq!(next_counter_value("-1"), "0");
1689 }
1690
1691 #[test]
1692 fn next_counter_value_non_canonical_integer_is_advanced_as_a_string() {
1693 // A leading zero (or sign) does not round-trip through integer
1694 // parsing, so it is advanced like a string instead.
1695 assert_eq!(next_counter_value("07"), "08");
1696 assert_eq!(next_counter_value("+5"), "+6");
1697 // A leading-zero value still carries digit-to-digit like a string.
1698 assert_eq!(next_counter_value("09"), "10");
1699 assert_eq!(next_counter_value("099"), "100");
1700 }
1701
1702 #[test]
1703 fn next_counter_value_saturates_at_i64_max() {
1704 // A counter pinned at `i64::MAX` stays there rather than panicking
1705 // (debug) or wrapping (release).
1706 let max = i64::MAX.to_string();
1707 assert_eq!(next_counter_value(&max), max);
1708 }
1709
1710 #[test]
1711 fn next_counter_value_characters() {
1712 assert_eq!(next_counter_value("a"), "b");
1713 assert_eq!(next_counter_value("A"), "B");
1714 assert_eq!(next_counter_value("z"), "aa");
1715 assert_eq!(next_counter_value("Z"), "AA");
1716 assert_eq!(next_counter_value("az"), "ba");
1717 assert_eq!(next_counter_value("zz"), "aaa");
1718 assert_eq!(next_counter_value("Zz"), "AAa");
1719 }
1720
1721 #[test]
1722 fn next_counter_value_trailing_non_alphanumeric() {
1723 // The right-most alphanumeric is incremented; trailing punctuation is
1724 // left in place.
1725 assert_eq!(next_counter_value("a)"), "b)");
1726 }
1727
1728 #[test]
1729 fn next_counter_value_no_alphanumeric() {
1730 // With nothing alphanumeric to carry, the final code point advances.
1731 assert_eq!(next_counter_value("{"), "|");
1732 }
1733
1734 #[test]
1735 fn counter_defaults_to_one() {
1736 let p = Parser::default();
1737 assert_eq!(p.counter("x", None), "1");
1738 assert_eq!(p.counter("x", None), "2");
1739 assert_eq!(
1740 p.attribute_value("x"),
1741 InterpretedValue::Value("2".to_string())
1742 );
1743 assert!(p.has_attribute("x"));
1744 assert!(p.is_attribute_set("x"));
1745 }
1746
1747 #[test]
1748 fn counter_seed_used_only_while_unset() {
1749 let p = Parser::default();
1750 assert_eq!(p.counter("c", Some("A")), "A");
1751 // Once set, a later seed is ignored.
1752 assert_eq!(p.counter("c", Some("Q")), "B");
1753 }
1754
1755 #[test]
1756 fn counter_empty_seed_falls_back_to_one() {
1757 let p = Parser::default();
1758 assert_eq!(p.counter("c", Some("")), "1");
1759 }
1760 }
1761}