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}