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