Skip to main content

adoctra_core/
ast.rs

1//! The document model.
2//!
3//! Every node — block *and* inline — carries a [`Span`]: its byte range in the
4//! original source. That is what makes the model usable by an editor
5//! (highlighting, fold ranges, "click in preview -> jump to source",
6//! diagnostics) directly from the AST, without a duplicated grammar.
7//!
8//! Inline text is *resolved* (semantic): attribute references are substituted
9//! and typographic replacements (`(C)` -> `©`, `--` -> `—`, …) are applied
10//! here. The crucial invariant is that an inline's textual content and its
11//! [`Span`] are **not** required to be the same length: the content is what the
12//! source *means*, the span is where it *came from*. Output-encoding
13//! (HTML-escaping `<` to `&lt;`, etc.) is deliberately left to each backend, so
14//! the shared model stays format-neutral.
15
16use crate::attributes::Attributes;
17use crate::source::SourceId;
18use std::collections::BTreeMap;
19use std::sync::Arc;
20
21/// A byte range `[start, end)` in the original source.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Span {
24    pub source_id: SourceId,
25    pub start: usize,
26    pub end: usize,
27    /// Index into ParseResult.provenance for a range crossing source occurrences.
28    /// The ordinary coordinates identify its first physical fragment.
29    pub provenance: Option<usize>,
30}
31
32impl Span {
33    pub fn new(start: usize, end: usize) -> Self {
34        Self::in_source(SourceId::default(), start, end)
35    }
36
37    pub fn in_source(source_id: SourceId, start: usize, end: usize) -> Self {
38        Self {
39            source_id,
40            start,
41            end,
42            provenance: None,
43        }
44    }
45
46    /// Length of the source range in bytes.
47    pub fn len(&self) -> usize {
48        self.end - self.start
49    }
50
51    /// Whether the range is empty.
52    pub fn is_empty(&self) -> bool {
53        self.end == self.start
54    }
55}
56
57/// A whole parsed document: header attributes plus a sequence of blocks.
58#[derive(Debug, Clone, Default, PartialEq)]
59pub struct Document {
60    pub attributes: Attributes,
61    /// Effective attributes after body conversion, for document-level output
62    /// such as math typesetting configuration. The header snapshot is unchanged.
63    pub final_attributes: Attributes,
64    pub header: Option<DocumentHeader>,
65    pub blocks: Vec<Block>,
66    /// Whether `blocks[0]` is the document title: a source `= Title` or a
67    /// `doctitle` attribute without one. A leading level-0 heading that is not
68    /// the title (for example, under a block title) is a part.
69    pub has_title: bool,
70    /// The document's footnotes in registration order (Ruby's
71    /// `Document#footnotes`), rendered after the body.
72    pub footnotes: Vec<FootnoteDefinition>,
73    /// A manual page's names (doctype `manpage`), read by the header.
74    pub manpage: Option<Manpage>,
75    /// The syntax of the HTML converter Ruby creates for the document's
76    /// backend at the end of the header: `xml` for the `xhtml`/`xhtml5`
77    /// backends or an `htmlsyntax=xml` assigned before the last `backend`
78    /// entry, `html` for other html5 documents, `None` for other backends.
79    /// The `htmlsyntax` attribute can differ (assigned after `backend`).
80    pub htmlsyntax: Option<&'static str>,
81    /// The output file and directory resolved for
82    /// [`ParseOptions::output`](crate::ParseOptions::output), if any.
83    pub output: Option<crate::OutputPaths>,
84    /// The automatic table of contents (`toc-placement` other than `macro`
85    /// or `preamble`; `auto` when standalone), converted before the body.
86    pub toc: Option<Toc>,
87    /// Ruby's `Document#base_dir` (absolute) when the document was read
88    /// through a resource provider: [`ParseOptions::base_dir`](crate::ParseOptions::base_dir),
89    /// else `docdir`. A converter resolves its assets (docinfo, stylesheet)
90    /// from it with [`asset_path`](crate::asset_path).
91    pub base_dir: Option<String>,
92    /// Ruby's `catalog[:refs]`: every registered id in registration order,
93    /// with whether it names an inline anchor (an `Inline`, not a block or
94    /// section). Ids Ruby rejects as duplicates are not listed.
95    pub references: Vec<Reference>,
96}
97
98/// One entry of [`Document::references`].
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Reference {
101    pub id: String,
102    pub inline: bool,
103}
104
105/// A table of contents as Ruby's converter renders it (`convert_outline`),
106/// resolved once while the document converts: which sections it lists and
107/// the number caption of each, from the attributes when the TOC converts
108/// (`sectnumlevels`, `chapter-signifier`, `part-signifier`), which can differ
109/// from those at the section's own heading.
110#[derive(Debug, Clone, PartialEq)]
111pub struct Toc {
112    /// The depth listed: the macro's `levels` or `toclevels`, as `to_i`.
113    pub levels: i128,
114    /// One entry per section among the converting document's top-level
115    /// blocks, in order.
116    pub entries: Vec<TocEntry>,
117}
118
119/// A listed section: its entries follow the section's own child sections in
120/// order, and are empty where the outline does not descend (the section's
121/// level is not below [`Toc::levels`], or it has no child sections). Its
122/// title is the section heading's converted title.
123#[derive(Debug, Clone, PartialEq)]
124pub struct TocEntry {
125    /// The caption or number prefix before the title, with its trailing
126    /// space (`"1.2. "`, `"Chapter 3. "`, `"Appendix A: "`), if any.
127    pub caption: Option<Vec<crate::AttributePart>>,
128    pub sections: Vec<TocEntry>,
129}
130
131/// A document dismantles its block tree without recursion: the derived drop
132/// of nested `Vec<Block>`s would recurse once per nesting level.
133impl Drop for Document {
134    fn drop(&mut self) {
135        crate::visit::dismantle(std::mem::take(&mut self.blocks));
136    }
137}
138
139/// The names of a manual page, like Ruby's `parse_manpage_header`. The header
140/// attributes keep the values (`mantitle`, `manvolnum`, `manname`,
141/// `mannames`, `manpurpose`, `manname-title`, `manname-id`); this keeps the
142/// name list and the NAME section the header consumed.
143#[derive(Debug, Clone, Default, PartialEq)]
144pub struct Manpage {
145    /// Ruby's `mannames`, in lexical spelling (the attribute holds Ruby's
146    /// array inspection).
147    pub names: Vec<String>,
148    /// The NAME section, when its name and purpose come from the source.
149    pub name_section: Option<NameSection>,
150    /// Whether `manname-title` is that section's title (the header did not
151    /// set it). The attribute holds its flat label parts; the HTML backend
152    /// renders the heading's inlines, links included.
153    pub section_title: bool,
154}
155
156/// The NAME section read by a manual page's header: its title (a level-1
157/// heading with the section's id, registered for references) and the range
158/// of the name and purpose lines.
159#[derive(Debug, Clone, PartialEq)]
160pub struct NameSection {
161    pub heading: Block,
162    /// `None` when the lines do not conform to `name - purpose`: the body
163    /// then reads the section again, while its title stays registered under
164    /// its id (Ruby's registration before the check), so a duplicate
165    /// automatic id gets a suffix.
166    pub body: Option<Span>,
167}
168
169/// A registered footnote: its number, optional id, converted content and the
170/// source range of its definition macro.
171#[derive(Debug, Clone, PartialEq)]
172pub struct FootnoteDefinition {
173    pub index: String,
174    pub id: Option<String>,
175    pub content: Vec<crate::AttributePart>,
176    pub span: Span,
177}
178
179impl Document {
180    /// The authors of the header attributes, like Ruby's `Document#authors`
181    /// as the HTML header converts them: `author`/`email`, then
182    /// `author_N`/`email_N` up to `authorcount`, names with replacements and
183    /// emails with macros (an address becomes a `mailto:` link).
184    pub fn authors(&self) -> Vec<AuthorDetails> {
185        crate::author_values::document_authors(&self.attributes)
186    }
187
188    /// The `authors` header attribute with replacements, like Ruby's
189    /// `sub_replacements` for the `author` meta element.
190    pub fn authors_line(&self) -> Option<Vec<crate::AttributePart>> {
191        crate::author_values::replaced(&self.attributes, "authors")
192    }
193
194    /// The blocks in document order with the section structure flattened: a
195    /// [`BlockKind::Section`] contributes its title heading followed by its
196    /// content, and a [`BlockKind::Preamble`] its blocks. Other containers are
197    /// returned as they are. Useful for consumers that ignore the hierarchy.
198    pub fn flat_blocks(&self) -> Vec<&Block> {
199        fn push<'a>(blocks: &'a [Block], out: &mut Vec<&'a Block>) {
200            for block in blocks {
201                match &block.kind {
202                    BlockKind::Section { blocks, .. } | BlockKind::Preamble { blocks } => {
203                        push(blocks, out)
204                    }
205                    _ => out.push(block),
206                }
207            }
208        }
209        let mut out = Vec::new();
210        push(&self.blocks, &mut out);
211        out
212    }
213
214    /// The owned counterpart of [`Document::flat_blocks`].
215    /// The top-level blocks, taken out of the document.
216    pub fn into_blocks(mut self) -> Vec<Block> {
217        std::mem::take(&mut self.blocks)
218    }
219
220    pub fn into_flat_blocks(mut self) -> Vec<Block> {
221        fn push(blocks: Vec<Block>, out: &mut Vec<Block>) {
222            for block in blocks {
223                match block.kind {
224                    BlockKind::Section { blocks, .. } | BlockKind::Preamble { blocks } => {
225                        push(blocks, out)
226                    }
227                    _ => out.push(block),
228                }
229            }
230        }
231        let mut out = Vec::new();
232        push(std::mem::take(&mut self.blocks), &mut out);
233        out
234    }
235}
236
237/// Source declarations in a document header. Effective (possibly overridden)
238/// metadata lives in `Document.attributes`. The title remains the first level-0
239/// heading in `blocks`, avoiding a second mutable copy of its inline tree.
240#[derive(Debug, Clone, PartialEq)]
241pub struct DocumentHeader {
242    pub span: Span,
243    pub title_span: Span,
244    /// Declared title text before substitutions or doctitle overrides.
245    /// The effective inline tree remains in the first level-0 heading.
246    pub title: String,
247    pub authors: Vec<Author>,
248    pub revision: Option<Revision>,
249}
250
251#[derive(Debug, Clone, PartialEq)]
252pub struct Author {
253    pub span: Span,
254    pub name: String,
255    pub firstname: String,
256    pub middlename: Option<String>,
257    pub lastname: Option<String>,
258    pub initials: String,
259    pub email: Option<String>,
260}
261
262/// An author as the HTML header shows it ([`Document::authors`]): the
263/// effective name after replacements and the email after macros.
264#[derive(Debug, Clone, PartialEq)]
265pub struct AuthorDetails {
266    pub name: Vec<crate::AttributePart>,
267    pub email: Option<Vec<crate::AttributePart>>,
268}
269
270#[derive(Debug, Clone, PartialEq)]
271pub struct Revision {
272    pub span: Span,
273    pub number: Option<String>,
274    pub date: Option<String>,
275    pub remark: Option<String>,
276}
277
278/// A block node: its source span, an optional id, and its kind.
279///
280/// `id` is the block's anchor — the target a `<<id>>` resolves to. It comes from
281/// an explicit anchor (`[[id]]`, set by the parser) or, for a section heading
282/// without one, an auto-generated [`sectids`] id assigned during the resolve
283/// pass. It is `None` for any block with no anchor, including an unanchored
284/// document title, and before resolution runs for an auto-id section.
285///
286/// `reftext` is the inline display text from an explicit `[[id,reftext]]` anchor;
287/// the resolve pass uses it as a textless reference's display text (preferring it
288/// over a section's own title when nonempty). `None` when no reftext was given.
289/// Block-anchor reftext executes an early attribute stage followed by c/q/r;
290/// even an empty result can retain source reference nodes for editor consumers.
291///
292/// `roles` are the CSS roles attached by a `[.role]` / `[#id.role1.role2]`
293/// shorthand block-attribute line — pure rendering metadata (the HTML backend
294/// emits them as a `class`), not part of cross-reference resolution. Empty when
295/// none were given. Order is source order.
296///
297/// `attrs` is the rest of a block-attribute list (`[style, name=value, %option]`):
298/// the block `style`, named attributes and `%options`. `id` and `role` are *not*
299/// here — they fold into `id` / `roles` above (the canonical anchor/role
300/// targets), regardless of whether they came from shorthand (`#id` / `.role`) or
301/// the longhand (`id=` / `role=`). See [`BlockAttrs`].
302///
303/// [`sectids`]: crate::resolve
304#[derive(Debug, Clone, PartialEq)]
305pub struct Block {
306    pub span: Span,
307    /// Immutable snapshot of attributes when this block finished parsing.
308    /// A container sees assignments made while parsing its children; an already
309    /// completed block keeps its own snapshot. Inline values retain the context
310    /// in which they were scanned.
311    /// Shared until an attribute assignment changes the parse context.
312    pub attribute_context: Arc<Attributes>,
313    /// The document attributes when this block converts, which Ruby's
314    /// converter reads (`sectanchors`, `prewrap`, `toc-placement`, …): the
315    /// header snapshot with the declarations replayed up to the block and the
316    /// effects of earlier conversions, taken on entry before its title and
317    /// content. Unlike [`attribute_context`](Block::attribute_context), it has
318    /// no reading-only effects (orphan declarations, section-title
319    /// expressions). `None` until conversion visits the block; see
320    /// [`Block::conversion_attributes`].
321    pub conversion_context: Option<Arc<Attributes>>,
322    pub id: Option<String>,
323    pub reftext: Option<Vec<Inline>>,
324    pub roles: Vec<String>,
325    /// The block's *title* — a `.Title` line preceding the block (asciidoctor's
326    /// [`BlockTitleRx`]). `None` when the block has no title. The text is parsed
327    /// inlines (inline substitutions apply, e.g. `.A *bold* caption`), so it
328    /// carries formatting. The title lives on the [`Block`] (not inside
329    /// [`BlockKind`]) so it survives the type-changing style fixups — a `.Title`
330    /// before a `[NOTE]`/`[example]` block stays on the resulting container, not
331    /// on the wrapped child.
332    /// An unrendered passthrough title keeps its source text unless it was
333    /// evaluated during reading for an explicit block ID.
334    ///
335    /// [`BlockTitleRx`]: crate::parser
336    pub title: Option<Vec<Inline>>,
337    /// The block's resolved **caption** — the counter prefix a titled block
338    /// carries before its [`title`], e.g. `"Example 1. "` (note the trailing
339    /// space). Filled at parse completion and *only* for a titled
340    /// [`BlockKind::Example`] (asciidoctor numbers example blocks via the
341    /// `example-caption` attribute; an untitled example gets no caption and does
342    /// not advance the counter). An explicit block/document `caption` replaces
343    /// the prefix without advancing the counter, including an empty caption.
344    /// A numbered section's [`BlockKind::Heading`] carries its number prefix
345    /// here instead (`"1.2. "`, `"Chapter 3. "`, `"II: "`), computed during
346    /// conversion like asciidoctor's `convert_section` (`sectnumlevels` and
347    /// the signifiers at the section); backends render it before the inlines.
348    /// `None` for every other block — a titled listing
349    /// or paragraph renders its bare title, matching asciidoctor's unset
350    /// `listing-caption`. Stored pre-rendered, like asciidoctor's `@caption`
351    /// (`#{caption}#{title}`): backends prepend it to the title verbatim.
352    ///
353    /// [`title`]: Block::title
354    pub caption: Option<String>,
355    /// The caption's semantic parts when it is not plain text, like the
356    /// HTML Ruby stores in `@caption`: a caption attribute's formatting or
357    /// raw markup (`pass:q[*Ex*]`), a raw unquoted or substituted
358    /// single-quoted `caption=`, a signifier. Empty when [`caption`] is plain
359    /// text; [`caption`] is always the flat text of these parts.
360    ///
361    /// [`caption`]: Block::caption
362    pub caption_parts: Vec<crate::AttributePart>,
363    /// The table of contents this block converts with: a `toc::[]` block's
364    /// (Ruby's `convert_toc`) or a preamble's with `toc-placement=preamble`
365    /// (`convert_preamble`). `None` for other blocks and where Ruby renders
366    /// no TOC (`<!-- toc disabled -->` for the macro).
367    pub toc: Option<Box<Toc>>,
368    pub attrs: BlockAttrs,
369    /// Ruby's `:simple` content model: a container built from a styled
370    /// paragraph (`[NOTE]`/`NOTE:`, `[example]`, `[sidebar]`, `[open]`,
371    /// `[abstract]`, `[partintro]`, `[quote]`) holding that paragraph as its
372    /// only child. Converters write the paragraph's inlines directly, without
373    /// a paragraph element, like Ruby's `content` of a simple block. `false`
374    /// for every delimited or other block.
375    pub simple: bool,
376    pub kind: BlockKind,
377}
378
379impl Block {
380    /// A block with no anchor: `id`/`reftext` default to `None` and `roles` /
381    /// `attrs` are empty. The parser fills them in from an explicit `[[id]]` /
382    /// `[[id,reftext]]` anchor or a `[style, #id.role, name=value]` attribute
383    /// line preceding the block; the resolve pass fills `id` for an auto-id
384    /// section.
385    pub fn new(span: Span, kind: BlockKind) -> Self {
386        Self {
387            span,
388            attribute_context: Arc::new(Attributes::new()),
389            conversion_context: None,
390            id: None,
391            reftext: None,
392            roles: Vec::new(),
393            title: None,
394            caption: None,
395            caption_parts: Vec::new(),
396            toc: None,
397            attrs: BlockAttrs::default(),
398            simple: false,
399            kind,
400        }
401    }
402
403    /// Set the caption from its parts (see [`caption_parts`]).
404    ///
405    /// [`caption_parts`]: Block::caption_parts
406    pub fn set_caption(&mut self, parts: Option<Vec<crate::AttributePart>>) {
407        self.caption = parts.as_deref().map(crate::attributes::parts_text);
408        self.caption_parts = match parts {
409            Some(parts) if !matches!(parts.as_slice(), [] | [crate::AttributePart::Text(_)]) => {
410                parts
411            }
412            _ => Vec::new(),
413        };
414    }
415
416    /// The attributes a converter reads for this block: its
417    /// [`conversion_context`](Block::conversion_context), else (for a block
418    /// conversion did not visit) its reading snapshot.
419    pub fn conversion_attributes(&self) -> &Attributes {
420        self.conversion_context
421            .as_deref()
422            .unwrap_or(&self.attribute_context)
423    }
424}
425
426/// The non-id, non-role part of a block-attribute list
427/// (`[style, positional, name=value, %option]`).
428///
429/// adoctra keeps the anchor id (`#id` / `id=`) on [`Block::id`] and CSS roles
430/// (`.role` / `role=`) on [`Block::roles`], because those drive cross-reference
431/// resolution and class output respectively. Everything else from the list lands
432/// here:
433///
434/// - `style` — the first positional attribute (`source`, `literal`, `NOTE`, …).
435///   `source` selects a listing's highlight form (its language); `literal` turns
436///   a paragraph into a [`BlockKind::Literal`]. Other styles are parsed and
437///   carried but do not yet change a block's type.
438/// - `named` — `name=value` pairs, deterministic order (a [`BTreeMap`] so JSON /
439///   snapshot output stays stable). For a `source`-style block the language
440///   positional is folded in here under the `language` key, in core, so every
441///   backend reads it the same way.
442/// - `options` — `%option` flags, in source order.
443/// - `positional` — asciidoctor's positional attributes after the style (and
444///   after a `source` language), by field index: named and skipped fields leave
445///   an empty slot, and a later attribute line replaces the same index.
446#[derive(Debug, Clone, Default, PartialEq)]
447pub struct BlockAttrs {
448    pub style: Option<String>,
449    pub named: BTreeMap<String, String>,
450    pub options: Vec<String>,
451    pub positional: Vec<String>,
452    /// The parts of a single-quoted `caption=` after its substitutions (Ruby
453    /// stores the converted markup); another `caption` value is raw.
454    pub(crate) caption: Option<Vec<crate::AttributePart>>,
455}
456
457/// The five admonition types. An admonition is a callout produced by an
458/// uppercase block style (`[NOTE]`, `[TIP]`, `[IMPORTANT]`, `[WARNING]`,
459/// `[CAUTION]`) on a paragraph or a `====` block, or by an uppercase label
460/// prefix on a paragraph's first line (`NOTE: …`). All forms are
461/// case-sensitive, matching asciidoctor.
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub enum AdmonitionKind {
464    Note,
465    Tip,
466    Important,
467    Warning,
468    Caution,
469}
470
471impl AdmonitionKind {
472    /// The lowercase identifier (`"note"`, …): the HTML `data-type` and the JSON
473    /// `admonition-type`.
474    pub fn as_str(self) -> &'static str {
475        match self {
476            Self::Note => "note",
477            Self::Tip => "tip",
478            Self::Important => "important",
479            Self::Warning => "warning",
480            Self::Caution => "caution",
481        }
482    }
483
484    /// The default label (`"Note"`, …): the built-in `<kind>-caption` value.
485    pub fn label(self) -> &'static str {
486        match self {
487            Self::Note => "Note",
488            Self::Tip => "Tip",
489            Self::Important => "Important",
490            Self::Warning => "Warning",
491            Self::Caution => "Caution",
492        }
493    }
494
495    /// Match an uppercase block style word (`"NOTE"`, …). Case-sensitive: `[note]`
496    /// is *not* an admonition (asciidoctor treats it as a custom paragraph style).
497    pub fn from_style(style: &str) -> Option<Self> {
498        match style {
499            "NOTE" => Some(Self::Note),
500            "TIP" => Some(Self::Tip),
501            "IMPORTANT" => Some(Self::Important),
502            "WARNING" => Some(Self::Warning),
503            "CAUTION" => Some(Self::Caution),
504            _ => None,
505        }
506    }
507
508    /// Recognise an admonition label at the start of `line`: an uppercase type
509    /// word followed by a colon and a single space (`"NOTE: "`, …). Returns the
510    /// kind and the byte length of the prefix to strip (incl. the space). `note:`
511    /// and `NOTE:` without the trailing space are *not* admonitions, matching
512    /// asciidoctor.
513    pub fn from_label_prefix(line: &str) -> Option<(Self, usize)> {
514        const LABELS: [(&str, AdmonitionKind); 5] = [
515            ("NOTE", AdmonitionKind::Note),
516            ("TIP", AdmonitionKind::Tip),
517            ("IMPORTANT", AdmonitionKind::Important),
518            ("WARNING", AdmonitionKind::Warning),
519            ("CAUTION", AdmonitionKind::Caution),
520        ];
521        for (word, kind) in LABELS {
522            if let Some(rest) = line.strip_prefix(word) {
523                if rest.strip_prefix(": ").is_some() {
524                    return Some((kind, word.len() + 2));
525                }
526            }
527        }
528        None
529    }
530}
531
532/// The attribution of a [`BlockKind::Quote`] / [`BlockKind::Verse`] block:
533/// asciidoctor's second and third positional attributes
534/// (`[quote, author, citetitle]`). Field indexes count named fields, so
535/// `[quote, role=x, Source]` carries only a `citetitle`. A nil field is absent
536/// (`[quote, , Source]`); a quoted `""` is an empty part. A block with neither
537/// part carries `None` rather than an empty `Attribution`.
538#[derive(Debug, Clone, PartialEq, Default)]
539pub struct Attribution {
540    /// The cited author (asciidoctor's `attribution`), rendered before the
541    /// `citetitle`. `None` when omitted.
542    pub author: Option<Vec<Inline>>,
543    /// The cited work (asciidoctor's `citetitle`), rendered inside a `<cite>`.
544    /// `None` when omitted.
545    pub citetitle: Option<Vec<Inline>>,
546    /// Credit text rendered after the `<cite>`. Ruby splits a blockquote
547    /// credit line in its converted markup; when the `, ` lies inside an
548    /// element, the browser closes the `cite` with that element, so the rest
549    /// of the line is not part of the citetitle. Empty otherwise.
550    pub trailing: Vec<Inline>,
551}
552
553/// Block-level element kinds.
554#[derive(Debug, Clone, PartialEq)]
555pub enum BlockKind {
556    /// A section title. `level` is the number of leading `=` minus one
557    /// (so a document title `= Title` is level 0, `== Section` is level 1).
558    /// A body section title is the first block of its [`BlockKind::Section`];
559    /// the document title and discrete headings stand alone.
560    /// The section's anchor lives on the enclosing [`Block::id`] — auto-generated
561    /// from the title during the resolve pass (asciidoctor's `sectids`, e.g.
562    /// `== Hello World` → `_hello_world`) unless an explicit `[[id]]` anchor
563    /// overrides it. Cross-references resolve against that id.
564    Heading { level: u8, inlines: Vec<Inline> },
565    /// A run of text forming a paragraph.
566    Paragraph { inlines: Vec<Inline> },
567    /// A listing block (verbatim, no inline substitutions), fenced by `----`.
568    /// Rendered as a `listingblock`; a `[source,LANG]` style further selects the
569    /// highlight form (see [`BlockAttrs::style`]). `callouts` are the callout
570    /// marks at its line ends (Ruby's verbatim `callouts` substitution).
571    Listing {
572        text: String,
573        callouts: Vec<Callout>,
574        /// The content after a `subs` attribute selected other substitutions
575        /// than the verbatim defaults (one [`Inline::Substituted`] node);
576        /// backends then render it instead of `text`.
577        inlines: Option<Vec<Inline>>,
578        /// The blank edge lines trimmed from `text`, which a highlighter of
579        /// the default substitutions still sees (see [`BlankEdges`]).
580        blank_edges: BlankEdges,
581    },
582    /// A literal block: verbatim, no inline substitutions, either fenced by
583    /// `....` or produced by a `[literal]` style on a paragraph. Same verbatim
584    /// `text` as [`BlockKind::Listing`] but rendered as a `literalblock` — no
585    /// language / `<code>` handling.
586    Literal {
587        text: String,
588        callouts: Vec<Callout>,
589        /// Content after custom substitutions, as for [`BlockKind::Listing`].
590        inlines: Option<Vec<Inline>>,
591    },
592    /// A passthrough block: verbatim, fenced by `++++` (or produced by a `[pass]`
593    /// style on a paragraph or `--` block). Like [`BlockKind::Literal`] the body
594    /// is the raw source `text` with no inline substitutions (`*bold*`, `{attr}`
595    /// stay literal) — but unlike it, the backend emits the text **completely
596    /// raw**: no HTML escaping and no wrapper element at all (asciidoctor's
597    /// `:pass` context), so even a block id/role is dropped on output.
598    /// A `subs` attribute selecting substitutions (Ruby applies none by
599    /// default) gives the converted `inlines`, still emitted raw.
600    Passthrough {
601        text: String,
602        inlines: Option<Vec<Inline>>,
603    },
604
605    /// A `++++` block styled `stem`, `asciimath` or `latexmath`. Content uses
606    /// special-character protection by default; explicit substitutions retain
607    /// neutral inline parts and their source ranges.
608    Stem {
609        style: crate::StemStyle,
610        inlines: Vec<Inline>,
611    },
612    /// An unordered (`*`, `-`, `•`) or ordered (`.`, `1.`, `a.`, `A.`, `i)`,
613    /// `I)`) list, parsed like Ruby's `parse_list`: each item's lines are
614    /// collected (`read_lines_for_list_item`) and parsed as its blocks, a
615    /// paragraph adjacent to the marker line folds into the item text, and a
616    /// different marker nests. An ordered list's implicit style (`arabic`,
617    /// `loweralpha`, …) is its [`BlockAttrs::style`]; a checklist has the
618    /// `checklist` option.
619    List {
620        ordered: bool,
621        items: Vec<ListItem>,
622        /// For a checklist, the document `icons` mode when the list
623        /// converts, like Ruby's `convert_ulist` reading the document
624        /// attribute then (a body entry before the list applies); `None`
625        /// for other lists.
626        icons: Option<String>,
627    },
628    /// A callout list (`<1> text`, `<.> text`), like Ruby's
629    /// `parse_callout_list`: each item names the callout marks of the
630    /// preceding verbatim blocks with its number (`coids`).
631    CalloutList { items: Vec<CalloutListItem> },
632    /// A description list (`term:: text`, also `:::`, `::::` and `;;`), like
633    /// Ruby's `parse_description_list`: consecutive terms without a
634    /// description share the next one. The `horizontal` and `qanda` styles
635    /// are in [`BlockAttrs::style`].
636    DescriptionList { items: Vec<DescriptionListItem> },
637    /// An admonition callout (`NOTE`/`TIP`/`IMPORTANT`/`WARNING`/`CAUTION`),
638    /// produced by an uppercase block style (`[NOTE]`), an uppercase label prefix
639    /// (`NOTE: …`) or an uppercase style on a `====` block. Like asciidoctor, the
640    /// body is a parsed *container* of child blocks: the paragraph and label forms
641    /// wrap their inlines in a single child [`BlockKind::Paragraph`] (the label
642    /// prefix, if any, stripped first), and the delimited form carries the `====`
643    /// block's children. `kind` selects the type.
644    Admonition {
645        kind: AdmonitionKind,
646        /// asciidoctor's `textlabel`: the block's `caption` attribute (raw, as
647        /// asciidoctor emits it, or the substituted parts of a single-quoted
648        /// value), else the document's `<kind>-caption` while
649        /// reading (default `Note`, …) with its own Text/Raw parts. Empty when
650        /// that attribute is unset.
651        label: Vec<crate::AttributePart>,
652        blocks: Vec<Block>,
653        /// The document `icons` mode when the block converts, like Ruby's
654        /// `convert_admonition`: `None` shows the label, `font` an icon font
655        /// glyph unless the block names an `icon`, any other value an image.
656        icons: Option<String>,
657        /// The image icon (Ruby's `icon_uri` of the block's `icon` attribute
658        /// or the admonition name under `iconsdir`); empty unless an image.
659        src: String,
660    },
661    /// An example block: a parsed container of child blocks, delimited by `====`
662    /// (or produced by an `[example]` style on a paragraph). The first container
663    /// kind besides a [`BlockKind::List`] — unlike the verbatim
664    /// [`BlockKind::Listing`]/[`BlockKind::Literal`], `blocks` are parsed *as
665    /// blocks* (not raw text), so inline substitutions, nested blocks and anchors
666    /// all apply inside.
667    Example { blocks: Vec<Block> },
668    /// A quote block: a parsed container of child blocks, delimited by `____`
669    /// (or produced by a `[quote]` style on a paragraph), with an optional
670    /// [`Attribution`] from the positional attributes (`[quote, author, work]`).
671    /// Like [`BlockKind::Example`] the body is parsed *as blocks* — inline
672    /// substitutions, nested blocks and anchors apply inside `blocks`. The default
673    /// style of a `____` block is `quote` (asciidoctor), so a bare `____` lands
674    /// here; a `[verse]` style on `____` is the verbatim [`BlockKind::Verse`].
675    Quote {
676        attribution: Option<Attribution>,
677        blocks: Vec<Block>,
678    },
679    /// A verse block: a `[verse]` style on a `____` block or a paragraph. Unlike
680    /// the container [`BlockKind::Quote`], a verse is **verbatim** — its source
681    /// whitespace and line breaks are preserved (rendered in a `<pre>`) — yet the
682    /// normal *inline* substitutions still apply (so `*bold*` formats), which is
683    /// why the body is `inlines` (scanned with newlines kept as text) rather than
684    /// parsed child blocks or the raw `text` of a [`BlockKind::Literal`]. Carries
685    /// the same optional [`Attribution`] as a quote.
686    Verse {
687        attribution: Option<Attribution>,
688        inlines: Vec<Inline>,
689    },
690    /// A sidebar block: a parsed container of child blocks, delimited by `****`
691    /// (or produced by a `[sidebar]` style on a paragraph). Like
692    /// [`BlockKind::Example`] the body is parsed *as blocks* — inline
693    /// substitutions, nested blocks and anchors all apply inside `blocks`. Unlike
694    /// a `====` example, an uppercase admonition style on a `****` block is
695    /// *inert* (asciidoctor leaves `[NOTE]` on `****` a plain sidebar), so the
696    /// style fixup never folds a sidebar into an [`BlockKind::Admonition`].
697    Sidebar { blocks: Vec<Block> },
698    /// An open block: a parsed container of child blocks, delimited by `--`
699    /// (or produced by an `[open]` style on a paragraph). The *generic*
700    /// container — with no style it renders as an `openblock`. A **container**
701    /// style "masquerades" the open block into that kind, handled in
702    /// `apply_style_fixup`: `[quote]` → [`BlockKind::Quote`], `[example]` →
703    /// [`BlockKind::Example`], `[sidebar]` → [`BlockKind::Sidebar`] and an
704    /// uppercase admonition style (`[NOTE]`, …) → [`BlockKind::Admonition`],
705    /// each moving the parsed `blocks` over unchanged. Like the other
706    /// containers the body is parsed *as blocks* — inline substitutions, nested
707    /// blocks and anchors apply inside, and the resolve pass recurses in. The
708    /// **verbatim** masquerades (`[source]`/`[literal]`/`[verse]` on `--`) switch
709    /// the content model at the `--` fence in `parse_block`, producing a
710    /// [`BlockKind::Listing`] / [`BlockKind::Literal`] / [`BlockKind::Verse`]
711    /// directly rather than an `Open`.
712    Open { blocks: Vec<Block> },
713    /// A section (asciidoctor's `Section`): the document-level structure built
714    /// from section titles, like Ruby's `next_section`. `blocks[0]` is always the
715    /// section's title, a [`BlockKind::Heading`] of the same `level` that keeps
716    /// the id, reftext, roles and attributes; the remaining blocks are the
717    /// section's content, including nested subsections. Discrete headings and
718    /// the document title stay plain headings. The span covers the title through
719    /// the end of the last nested block.
720    Section { level: u8, blocks: Vec<Block> },
721    /// asciidoctor's preamble: the blocks between a document header and the first
722    /// section. Present when the document has a title and at least one section
723    /// (a book keeps it without sections, and opens one without a header unless
724    /// it starts with an abstract); otherwise those blocks stay at the top level.
725    Preamble { blocks: Vec<Block> },
726    /// The `toc::[]` block macro (Ruby's `:toc` block): where the table of
727    /// contents goes when `toc-placement` is `macro`. Its named attributes
728    /// (`levels`, …) are in [`Block::attrs`]; `id`, `role` and `title` from the
729    /// macro's brackets replace the block's metadata. Backends build the outline
730    /// from the document's sections.
731    Toc,
732    /// A thematic break (Ruby's `:thematic_break`): `'''` (three or more
733    /// apostrophes) or Markdown's `---`, `***`, `___` with optional uniform
734    /// spaces between the characters (`- - -`), indented up to three spaces.
735    /// It has no content; backends ignore its metadata, like Ruby.
736    ThematicBreak,
737    /// A page break (Ruby's `:page_break`): three or more `<` on a line.
738    PageBreak,
739    /// The `image::target[alt, width, height]` block macro (Ruby's `:image`
740    /// block). `target` is the macro target after its attribute references;
741    /// `src` is the resolved web path (`imagesdir`, `.`/`..` segments and
742    /// spaces, as Ruby's `image_uri` in secure mode) at the block during
743    /// conversion. `alt` is Ruby's `alt`: the explicit `alt`, else the style
744    /// of a preceding attribute line, else the target's basename with `_`/`-`
745    /// as spaces; a non-default value gets replacements (generated entities
746    /// stay [`crate::AttributePart::Raw`]). The macro's named attributes (`width`,
747    /// `height`, `link`, `window`, `float`, `align`, `scaledwidth`, …) are in
748    /// [`Block::attrs`]; a `.Title` makes it a numbered figure
749    /// (`figure-caption`, [`Block::caption`]).
750    Image {
751        target: String,
752        src: String,
753        alt: Vec<crate::AttributePart>,
754        /// An SVG embedded below secure mode (`%inline`, `%interactive`),
755        /// decided where the block converts.
756        svg: Option<crate::Svg>,
757    },
758    /// The `video::target[poster, width, height]` block macro (Ruby's `:video`
759    /// block). `target` is the macro target after its attribute references.
760    /// `src` is resolved at the block during conversion, as Ruby's
761    /// `convert_video` builds it: a `poster` of `youtube`/`vimeo` makes it an
762    /// `embedded` player URL (`asset-uri-scheme`, the target's list/playlist or
763    /// hash and the `start`/`end`/`theme`/`lang` attributes and options, with
764    /// literal `&` separators); otherwise it is the target's web path under
765    /// `imagesdir` with a `#t=start,end` anchor, and a non-empty `poster` is
766    /// resolved the same way. Named attributes and options stay in
767    /// [`Block::attrs`]; a title is not captioned.
768    Video {
769        target: String,
770        src: String,
771        poster: Option<String>,
772        embedded: bool,
773    },
774    /// The `audio::target[]` block macro (Ruby's `:audio` block): `src` is the
775    /// target's web path under `imagesdir` with a `#t=start,end` anchor,
776    /// resolved during conversion.
777    Audio { target: String, src: String },
778    /// A table (Ruby's `:table` block), delimited by `|===`, `,===`, `:===` or
779    /// `!===` (a longer `=` run must close with the same line). See [`Table`].
780    Table(Box<Table>),
781}
782
783/// A table's columns and rows, parsed like Ruby's `parse_table`. The `cols`
784/// attribute, `format`/`separator`, `header`/`noheader`/`footer` and
785/// `autowidth` options stay in [`Block::attrs`]; a `.Title` makes it a numbered
786/// table (`table-caption`, [`Block::caption`]).
787#[derive(Debug, Clone, PartialEq)]
788pub struct Table {
789    pub columns: Vec<TableColumn>,
790    /// The header row (explicit `%header`, or Ruby's implicit header: a first
791    /// line holding the whole first row followed by a blank line).
792    pub head: Vec<Vec<TableCell>>,
793    pub body: Vec<Vec<TableCell>>,
794    /// The last row with `%footer`.
795    pub foot: Vec<Vec<TableCell>>,
796    /// Ruby's `tablepcwidth`: the `width` attribute as an integer percentage
797    /// (1–100, or 0 for an explicit `0`/`0%`), else 100.
798    pub width: u32,
799    /// Ruby's `frame`, `grid` and `stripes` with the `table-frame`,
800    /// `table-grid` and `table-stripes` document fallbacks at the table during
801    /// conversion (`topbot` becomes `ends`; defaults `all`).
802    pub frame: String,
803    pub grid: String,
804    pub stripes: Option<String>,
805}
806
807impl Table {
808    /// Every cell in Ruby's conversion order: head, body, foot.
809    pub fn cells(&self) -> impl Iterator<Item = &TableCell> {
810        self.head
811            .iter()
812            .chain(&self.body)
813            .chain(&self.foot)
814            .flatten()
815    }
816
817    pub fn cells_mut(&mut self) -> impl Iterator<Item = &mut TableCell> {
818        self.head
819            .iter_mut()
820            .chain(&mut self.body)
821            .chain(&mut self.foot)
822            .flatten()
823    }
824}
825
826/// A table column (Ruby's `Table::Column`) from the `cols` specs, or an equal
827/// share of the width for columns implied by the first row.
828#[derive(Debug, Clone, PartialEq)]
829pub struct TableColumn {
830    pub halign: HAlign,
831    pub valign: VAlign,
832    pub style: Option<CellStyle>,
833    /// Ruby's `colpcwidth`, spelled as Ruby prints the number (`25`,
834    /// `33.3333`, `50.0001`).
835    pub width: String,
836    /// A `~` width (Ruby's `autowidth-option`).
837    pub autowidth: bool,
838}
839
840/// A table cell (Ruby's `Table::Cell`). The alignments and style combine the
841/// column's with the cell spec's; header-row cells have no style.
842#[derive(Debug, Clone, PartialEq)]
843pub struct TableCell {
844    pub content: CellContent,
845    pub style: Option<CellStyle>,
846    pub halign: HAlign,
847    pub valign: VAlign,
848    pub colspan: Option<u32>,
849    pub rowspan: Option<u32>,
850    /// The `cellbgcolor` document attribute after converting the cell's
851    /// content (a `{set:cellbgcolor:…}` in the cell applies to it).
852    pub background: Option<String>,
853    /// From the cell spec (or the separator) through the end of the cell text.
854    pub span: Span,
855}
856
857/// What a cell holds.
858#[derive(Debug, Clone, PartialEq)]
859pub enum CellContent {
860    /// Normal substitutions. Body and footer cells split into paragraphs at
861    /// blank lines (Ruby's `Cell#content`); a header-row cell is one run.
862    Paragraphs(Vec<Vec<Inline>>),
863    /// An AsciiDoc (`a`) cell: the blocks of its nested document (Ruby's
864    /// `inner_document`), read when the cell is created with the parent's
865    /// attributes locked and converted with its own attribute entries, and
866    /// that document's own footnotes.
867    Blocks {
868        blocks: Vec<Block>,
869        footnotes: Vec<FootnoteDefinition>,
870        /// The nested document has a title: `blocks[0]` is its level-0
871        /// heading (like [`Document::has_title`]), shown unless `notitle`
872        /// (the nested default, which its header entries may unset).
873        has_title: bool,
874        /// The nested document's header attributes, which Ruby's
875        /// `convert_embedded` reads (its automatic TOC).
876        attributes: Arc<Attributes>,
877        /// The nested document's automatic TOC (see [`Document::toc`]).
878        toc: Option<Toc>,
879    },
880    /// A literal (`l`) cell: verbatim text (Ruby's basic substitutions are
881    /// output escaping).
882    Literal(String),
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq)]
886pub enum HAlign {
887    Left,
888    Center,
889    Right,
890}
891
892impl HAlign {
893    pub fn as_str(self) -> &'static str {
894        match self {
895            HAlign::Left => "left",
896            HAlign::Center => "center",
897            HAlign::Right => "right",
898        }
899    }
900}
901
902#[derive(Debug, Clone, Copy, PartialEq, Eq)]
903pub enum VAlign {
904    Top,
905    Middle,
906    Bottom,
907}
908
909impl VAlign {
910    pub fn as_str(self) -> &'static str {
911        match self {
912            VAlign::Top => "top",
913            VAlign::Middle => "middle",
914            VAlign::Bottom => "bottom",
915        }
916    }
917}
918
919/// Ruby's `TableCellStyles`: `d`, `s`, `e`, `m`, `h`, `l` and `a`.
920#[derive(Debug, Clone, Copy, PartialEq, Eq)]
921pub enum CellStyle {
922    Default,
923    Strong,
924    Emphasis,
925    Monospaced,
926    Header,
927    Literal,
928    Asciidoc,
929}
930
931impl CellStyle {
932    pub fn from_letter(letter: char) -> Option<Self> {
933        Some(match letter {
934            'd' => CellStyle::Default,
935            's' => CellStyle::Strong,
936            'e' => CellStyle::Emphasis,
937            'm' => CellStyle::Monospaced,
938            'h' => CellStyle::Header,
939            'l' => CellStyle::Literal,
940            'a' => CellStyle::Asciidoc,
941            _ => return None,
942        })
943    }
944
945    /// Ruby's style symbol (`none` for `d`).
946    pub fn as_str(self) -> &'static str {
947        match self {
948            CellStyle::Default => "none",
949            CellStyle::Strong => "strong",
950            CellStyle::Emphasis => "emphasis",
951            CellStyle::Monospaced => "monospaced",
952            CellStyle::Header => "header",
953            CellStyle::Literal => "literal",
954            CellStyle::Asciidoc => "asciidoc",
955        }
956    }
957}
958
959/// One item of a [`BlockKind::List`]: its resolved inline content (the
960/// *principal* text), any nested blocks (e.g. a sublist), and the source
961/// [`Span`] of the whole item — from its marker through the end of any nested
962/// content it contains.
963#[derive(Debug, Clone, PartialEq)]
964pub struct ListItem {
965    pub inlines: Vec<Inline>,
966    pub blocks: Vec<Block>,
967    pub span: Span,
968    /// A checklist item's state (`[x]`/`[*]` checked, `[ ]` not).
969    pub checkbox: Option<bool>,
970    /// Ruby's `ListItem#marker`: the unordered marker (`*`, `**`, `-`) or the
971    /// first marker of the ordered series (`.`, `..`, `1.`, `a.`, `i)`, …);
972    /// none for a description.
973    pub marker: Option<String>,
974}
975
976/// The blank leading and trailing lines of a listing's body. Ruby's
977/// `Block#content` trims them only *after* substitution, so a source
978/// highlighter (Rouge's line number table, `highlight` lines) numbers them
979/// while [`BlockKind::Listing`]'s `text` is already trimmed. Ruby's reader
980/// strips trailing whitespace, so each such line is empty. `empty` marks a
981/// body without any line (Ruby's `[]`), which is not highlighted at all,
982/// unlike one blank line.
983#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
984pub struct BlankEdges {
985    pub leading: usize,
986    pub trailing: usize,
987    pub empty: bool,
988}
989
990impl BlankEdges {
991    /// The untrimmed body Ruby passes to the highlighter, `None` for a body
992    /// without lines.
993    pub fn source(&self, text: &str) -> Option<String> {
994        if self.empty {
995            return None;
996        }
997        if self.leading == 0 && self.trailing == 0 {
998            return Some(text.to_owned());
999        }
1000        let mut lines = vec![""; self.leading];
1001        // An all-blank body trims every line into `leading`.
1002        if !(text.is_empty() && self.leading > 0) {
1003            lines.push(text);
1004        }
1005        lines.resize(lines.len() + self.trailing, "");
1006        Some(lines.join("\n"))
1007    }
1008}
1009
1010/// A callout mark at the end of a verbatim line (`<1>`, `<.>`, `<!--1-->`),
1011/// like Ruby's `sub_callouts`: `range` is the replaced bytes of the block
1012/// text, from any comment `guard` (`// `, `# `, `-- `, `;; ` or the
1013/// `line-comment` attribute) through the mark; `xml` marks the `<!--N-->`
1014/// form. `id` is Ruby's callout id (`CO1-2`). `span` is the mark's source.
1015/// `icons` is the document `icons` mode when the block converts and `src` the
1016/// image icon (`icon_uri "callouts/N"`) when icons are images.
1017#[derive(Debug, Clone, PartialEq)]
1018pub struct Callout {
1019    pub start: usize,
1020    pub end: usize,
1021    pub number: String,
1022    pub guard: Option<String>,
1023    pub xml: bool,
1024    pub id: String,
1025    pub span: Span,
1026    pub icons: Option<String>,
1027    pub src: String,
1028}
1029
1030/// One item of a [`BlockKind::CalloutList`] and the ids of the callout marks
1031/// it explains (Ruby's `coids`). `icons` and `src` are the item number's
1032/// icon when the list converts, as for [`Callout`].
1033#[derive(Debug, Clone, PartialEq)]
1034pub struct CalloutListItem {
1035    pub item: ListItem,
1036    pub coids: Vec<String>,
1037    pub icons: Option<String>,
1038    pub src: String,
1039}
1040
1041/// One entry of a [`BlockKind::DescriptionList`]: its terms and the shared
1042/// description (text and blocks, like a [`ListItem`]), if any.
1043#[derive(Debug, Clone, PartialEq)]
1044pub struct DescriptionListItem {
1045    pub terms: Vec<ListTerm>,
1046    pub description: Option<ListItem>,
1047    pub span: Span,
1048}
1049
1050/// A description list term.
1051#[derive(Debug, Clone, PartialEq)]
1052pub struct ListTerm {
1053    pub inlines: Vec<Inline>,
1054    pub span: Span,
1055}
1056
1057/// Inline (phrase-level) elements. Every variant carries its source [`Span`].
1058#[derive(Debug, Clone, PartialEq)]
1059pub enum Inline {
1060    /// Literal text. `value` is the resolved content; `span` is where it came
1061    /// from (the two may differ in length once substitutions apply).
1062    Text { value: String, span: Span },
1063    /// Entity syntax restored by substitutions, emitted without escaping.
1064    /// Unknown entity names are retained; `span` covers their source construct.
1065    Raw { value: String, span: Span },
1066    /// Semantic text generated by substitutions starting in ordinary source
1067    /// text. Parts carry neutral formatting/link boundaries, never generated
1068    /// backend markup. The span covers the consumed source constructs.
1069    Substituted {
1070        value: String,
1071        parts: Vec<crate::AttributePart>,
1072        span: Span,
1073    },
1074    /// `*strong*` — bold. `span` covers the whole construct incl. markers.
1075    Strong { children: Vec<Inline>, span: Span },
1076    /// `_emphasis_` — italic. `span` covers the whole construct incl. markers.
1077    Emphasis { children: Vec<Inline>, span: Span },
1078    /// `` `monospace` `` — code/literal phrase. `span` covers the whole
1079    /// construct incl. markers.
1080    Monospace { children: Vec<Inline>, span: Span },
1081    /// `#mark#` — highlighted text. `span` covers the whole construct incl.
1082    /// markers.
1083    Mark { children: Vec<Inline>, span: Span },
1084    /// `^superscript^` — an *unconstrained* quote (matches mid-word, e.g.
1085    /// `E=mc^2^`). `span` covers the whole construct incl. markers.
1086    Superscript { children: Vec<Inline>, span: Span },
1087    /// `~subscript~` — an *unconstrained* quote (matches mid-word, e.g.
1088    /// `H~2~O`). `span` covers the whole construct incl. markers.
1089    Subscript { children: Vec<Inline>, span: Span },
1090    /// An attribute reference `{name}`, resolved once in core against the
1091    /// document attributes. `value` is `Some` when the attribute was found, and
1092    /// `None` for a missing attribute (consumers then fall back to the literal
1093    /// `{name}`, matching asciidoctor's default `attribute-missing=skip`).
1094    /// `span` covers the `{name}` token in the source.
1095    AttributeRef {
1096        name: String,
1097        value: Option<String>,
1098        /// Empty for ordinary text; otherwise preserves literal/raw parts and
1099        /// semantic formatting boundaries. `value` omits formatting boundaries.
1100        parts: Vec<crate::AttributePart>,
1101        span: Span,
1102    },
1103    /// A programmatically constructed link. Parsed links use `Substituted`
1104    /// with semantic link parts after ordered substitutions. `target`
1105    /// is the resolved href; `children` is the display text (parsed inlines),
1106    /// which for a bare link is the target's own text. `bare` is `true` when no
1107    /// explicit link text was given — asciidoctor renders those with a
1108    /// `class="bare"` on the `<a>`. `span` covers the whole construct (markers,
1109    /// scheme and brackets) in the source.
1110    Link {
1111        target: String,
1112        children: Vec<Inline>,
1113        bare: bool,
1114        span: Span,
1115    },
1116    /// A cross-reference: `<<id>>`, `<<id,text>>`, or the `xref:id[text]` macro.
1117    /// Like Ruby, it is recognized by the macros stage (after links, emails and
1118    /// inline anchors) over the result of quotes, attribute references and
1119    /// replacements, so `<<{id}>>` takes the expanded id and a consumed label
1120    /// keeps its formatting as [`Inline::Substituted`] parts.
1121    /// `xref` holds the resolved target (decoded; character references keep
1122    /// their spelling, as Ruby inserts them): `#id` within this document
1123    /// (a leading `#` removed outside compat mode; an unknown bare id may name
1124    /// a title or reftext), or another document's path (`doc.adoc#id`,
1125    /// `xref:doc.adoc[]`) with `relfileprefix`/`relfilesuffix` applied, unless
1126    /// it names this document or an included one. `role` comes from the
1127    /// macro's attribute list (`xref:id[text,role=r]`). `children` is the
1128    /// display text. The parser leaves it *empty* for a textless reference;
1129    /// conversion then fills it in — with the target's reftext or title
1130    /// (formatting preserved, like asciidoctor's `xrefstyle=basic`), the
1131    /// interdocument path, or the literal `[id]` text when the id is unknown
1132    /// (asciidoctor's *unresolved*-reference rendering, which also emits a
1133    /// diagnostic). An explicit reftext is kept verbatim.
1134    /// `span` covers the whole construct (the `<<…>>` or `xref:…[…]`). A
1135    /// reference generated entirely by an attribute value stays a pair of
1136    /// [`AttributePart::OpenXref`](crate::AttributePart::OpenXref) /
1137    /// `CloseXref` parts of its reference.
1138    CrossReference {
1139        xref: crate::Xref,
1140        children: Vec<Inline>,
1141        span: Span,
1142    },
1143    /// An inline anchor: `[[id]]` / `[[id,reftext]]` appearing *mid-text*, or the
1144    /// `anchor:id[reftext]` macro. It renders as an empty `<a id="id">` right at
1145    /// this point. Like Ruby, it is recognized by the macros stage, after quotes,
1146    /// attribute references and replacements, so the node consumes everything in
1147    /// its source range (expanded references inside it remain as empty
1148    /// [`Inline::AttributeRef`] nodes after it). The reftext is not content: the
1149    /// anchor catalog built while reading supplies it to a textless `<<id>>`.
1150    /// `span` covers the whole construct (`[[…]]` or `anchor:…[…]`). An anchor
1151    /// generated entirely by an attribute value stays an
1152    /// [`AttributePart::Anchor`](crate::AttributePart::Anchor) of its reference.
1153    ///
1154    /// A *whole-line* `[[id]]` is the distinct [`block anchor`] construct, handled
1155    /// by the parser before inline scanning; only an anchor with other text on the
1156    /// line reaches here.
1157    ///
1158    /// [`block anchor`]: crate::ast::Block::id
1159    Anchor { id: String, span: Span },
1160    /// An inline `image:target[alt, width, height]` or `icon:name[size]` macro,
1161    /// recognized like Ruby's `InlineImageMacroRx`: in the macros stage before
1162    /// links, over the lexical result of quotes, attribute references and
1163    /// replacements. Its fields keep their lexical spelling (see
1164    /// [`crate::InlineImage`]); `span` covers the whole macro, and consumed
1165    /// references remain empty [`Inline::AttributeRef`] nodes after it. An image
1166    /// generated entirely by an attribute value stays an
1167    /// [`AttributePart::Image`](crate::AttributePart::Image) of its reference.
1168    Image {
1169        image: crate::InlineImage,
1170        span: Span,
1171    },
1172    /// A footnote macro (`footnote:[text]`, `footnote:id[text]`,
1173    /// `footnote:id[]`, legacy `footnoteref:[id,text]`) recognized like Ruby's
1174    /// `InlineFootnoteMacroRx`, the last macro of the macros stage, and
1175    /// numbered when converted (`footnote-number`). `span` covers the macro.
1176    /// A footnote generated entirely by an attribute value stays an
1177    /// [`AttributePart::Footnote`](crate::AttributePart::Footnote).
1178    Footnote {
1179        footnote: crate::Footnote,
1180        span: Span,
1181    },
1182    /// A keyboard (`kbd:[Ctrl+T]`), button (`btn:[OK]`) or menu
1183    /// (`menu:File[Save]`, `"File > Save"`) macro, recognized only when the
1184    /// `experimental` attribute is set, like Ruby: first in the macros stage
1185    /// (before images), over the lexical result of the earlier stages. `span`
1186    /// covers the macro. One generated entirely by an attribute value stays an
1187    /// [`AttributePart::Ui`](crate::AttributePart::Ui).
1188    Ui { ui: crate::UiMacro, span: Span },
1189    /// An index term (`((Tigers))`, `(((Big cats,Tigers)))`,
1190    /// `indexterm2:[Tigers]`, `indexterm:[Big cats,Tigers]`), recognized like
1191    /// Ruby's `InlineIndextermMacroRx` in the macros stage after images. A
1192    /// flow term shows its text, a concealed term nothing. `span` covers the
1193    /// construct; one generated entirely by an attribute value stays an
1194    /// [`AttributePart::IndexTerm`](crate::AttributePart::IndexTerm).
1195    IndexTerm { term: crate::IndexTerm, span: Span },
1196    /// A protected inline formula (`stem:`, `asciimath:` or `latexmath:`),
1197    /// extracted before quotes/attributes, restored after the outer subs.
1198    Stem { stem: crate::Stem, span: Span },
1199}
1200
1201impl Inline {
1202    pub fn span_mut(&mut self) -> &mut Span {
1203        match self {
1204            Inline::Text { span, .. }
1205            | Inline::Raw { span, .. }
1206            | Inline::Substituted { span, .. }
1207            | Inline::Strong { span, .. }
1208            | Inline::Emphasis { span, .. }
1209            | Inline::Monospace { span, .. }
1210            | Inline::Mark { span, .. }
1211            | Inline::Superscript { span, .. }
1212            | Inline::Subscript { span, .. }
1213            | Inline::AttributeRef { span, .. }
1214            | Inline::Link { span, .. }
1215            | Inline::CrossReference { span, .. }
1216            | Inline::Anchor { span, .. }
1217            | Inline::Image { span, .. }
1218            | Inline::Footnote { span, .. }
1219            | Inline::Ui { span, .. }
1220            | Inline::IndexTerm { span, .. }
1221            | Inline::Stem { span, .. } => span,
1222        }
1223    }
1224
1225    /// The source span of this inline node.
1226    pub fn span(&self) -> Span {
1227        match self {
1228            Inline::Text { span, .. }
1229            | Inline::Raw { span, .. }
1230            | Inline::Substituted { span, .. }
1231            | Inline::Strong { span, .. }
1232            | Inline::Emphasis { span, .. }
1233            | Inline::Monospace { span, .. }
1234            | Inline::Mark { span, .. }
1235            | Inline::Superscript { span, .. }
1236            | Inline::Subscript { span, .. }
1237            | Inline::AttributeRef { span, .. }
1238            | Inline::Link { span, .. }
1239            | Inline::CrossReference { span, .. }
1240            | Inline::Anchor { span, .. }
1241            | Inline::Image { span, .. }
1242            | Inline::Footnote { span, .. }
1243            | Inline::Ui { span, .. }
1244            | Inline::IndexTerm { span, .. }
1245            | Inline::Stem { span, .. } => *span,
1246        }
1247    }
1248}