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 `<`, 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}