Skip to main content

asciidoc_parser/blocks/
table.rs

1use std::{collections::VecDeque, rc::Rc, sync::Arc};
2
3use self_cell::self_cell;
4
5use crate::{
6    HasSpan, Parser, Span,
7    attributes::Attrlist,
8    blocks::{
9        Block, ContentModel, IsBlock, caption::assign_block_caption, metadata::BlockMetadata,
10        parse_utils::parse_blocks_until,
11    },
12    content::{Content, SubstitutionGroup},
13    document::{InterpretedValue, TocConfig, TocMode},
14    parser::{
15        AttributeValue, InlineSubstitutionRenderer, ModificationContext, ReferenceResolver,
16        ReferenceWarnings, ResolvedAttributes, SourceLine, built_in_attr, built_in_attrs_iter,
17        preprocessor::preprocess_with_initial_file_name,
18    },
19    span::MatchedItem,
20    strings::CowStr,
21    warnings::{MatchAndWarnings, Warning, WarningType},
22};
23
24/// Resolve the absolute originating `(file, line)` for a no-output
25/// preprocessor-directive warning (a malformed/unterminated conditional or a
26/// tag-filter diagnostic) raised while expanding a table cell.
27///
28/// The warning's deferred `origin` carries a line relative to the cell's inner
29/// preprocessing pass — `1` is the cell's first content line. `cell_origin` is
30/// that first line's already-resolved location.
31///
32/// * A `None` deferred origin (e.g. an unresolved include target, located by
33///   span instead) stays `None`.
34/// * A deferred origin naming a *different* file than the cell came from
35///   originated in a file the cell *included*; its line is already absolute
36///   within that file, so it is kept unchanged.
37/// * A deferred origin naming the cell's own origin file has a pass-relative
38///   line, translated to the cell's absolute location by offsetting from
39///   `cell_origin` (line `N` is `N - 1` lines past the cell's first line). This
40///   preserves the real line of a directive on a later cell line rather than
41///   collapsing it onto the cell's opening line.
42fn absolute_cell_directive_origin(
43    deferred: Option<SourceLine>,
44    cell_origin: Option<&SourceLine>,
45) -> Option<SourceLine> {
46    let deferred = deferred?;
47    match cell_origin {
48        Some(cell_origin) if deferred.0.as_deref() == cell_origin.0.as_deref() => Some(SourceLine(
49            cell_origin.0.clone(),
50            cell_origin.1 + deferred.1.saturating_sub(1),
51        )),
52        _ => Some(deferred),
53    }
54}
55
56/// Attributes that an AsciiDoc table cell may modify even when they are set in
57/// the parent document.
58///
59/// An AsciiDoc cell inherits the parent's attributes and cannot modify them,
60/// but the AsciiDoc specification carves out a handful of exceptions:
61/// `doctype`, `toc`, `notitle` (and its complement, `showtitle`), and
62/// `compat-mode`.
63const ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES: &[&str] =
64    &["doctype", "toc", "notitle", "showtitle", "compat-mode"];
65
66/// A table is a delimited block that arranges content into a grid of rows and
67/// columns.
68///
69/// A table is introduced by a table delimiter (`|===`, or `!===` for a nested
70/// table) and closed by a matching delimiter. By default cells are separated
71/// using prefix-separated value (PSV) syntax: the table's cell separator — a
72/// vertical bar (`|`) by default — at the start of a line or preceded by
73/// whitespace begins a new cell. Cells flow, in document order, into rows whose
74/// length is fixed by the number of columns. (The separator defaults to `!`
75/// inside a nested table and can be overridden with the `separator` attribute;
76/// see below.)
77///
78/// The number of columns is determined either by the `cols` attribute or,
79/// implicitly, by the number of cells found in the first non-empty line after
80/// the opening delimiter.
81///
82/// # Data formats
83///
84/// In addition to the default PSV format, a table can be populated from
85/// delimiter-separated data with the [`format`](Self::data_format) attribute:
86/// `csv` (comma-separated values), `tsv` (tab-separated values), or `dsv`
87/// (delimited values, colon-separated by default). The `,===` and `:===`
88/// shorthand delimiters select the CSV and DSV formats respectively without an
89/// explicit `format` attribute. In a data format the separator is placed
90/// *between* values (not in front of each cell) and a cell carries no
91/// formatting spec; cell formatting is instead applied per column with the
92/// `cols` attribute. See [`DataFormat`] for the parsing rules.
93///
94/// Column specifier style operators (the `a`, `d`, `e`, `h`, `l`, `m`, and `s`
95/// operators) are supported, along with proportional width and the horizontal
96/// and vertical alignment operators. Per-cell horizontal and vertical alignment
97/// operators are supported and override the column's alignment, and a per-cell
98/// style operator (in the last position of the cell specifier) is supported and
99/// overrides the column's style. The per-cell span (`+`) operator is supported:
100/// a cell can span multiple columns (`<n>+`), multiple rows (`.<n>+`), or a
101/// block of both (`<n>.<n>+`). The per-cell duplication (`*`) operator is
102/// supported: a cell with a duplication factor (`<n>*`) clones its content and
103/// properties into `<n>` consecutive cells.
104///
105/// Table sizing is supported: the [`width`](Self::width) attribute sets a fixed
106/// table width, the `autowidth` option ([`is_autowidth`](Self::is_autowidth))
107/// sizes the table and its columns to their content, and an individual column
108/// can be made [autowidth](TableColumn::is_autowidth) with the `~` width value.
109///
110/// Table borders are supported: the [`frame`](Self::frame) attribute controls
111/// the border around the table and the [`grid`](Self::grid) attribute controls
112/// the borders between cells. Each falls back to a document-level default
113/// (`table-frame` / `table-grid`) and then to `all`.
114///
115/// Zebra striping is supported via the [`stripes`](Self::stripes) attribute,
116/// which falls back to the `table-stripes` document attribute and then to
117/// `none`.
118///
119/// Nested tables are supported: an [`AsciiDoc`](ColumnStyle::AsciiDoc) cell may
120/// contain its own table. The cell separator defaults to the vertical bar (`|`)
121/// but switches to the exclamation mark (`!`) inside an AsciiDoc cell, so a
122/// nested table is opened with `!===` and separates its cells with `!`. The
123/// `separator` attribute overrides the default separator with an explicit
124/// character at any level.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct TableBlock<'src> {
127    columns: Vec<TableColumn>,
128    data_format: DataFormat,
129    header_row: Option<TableRow<'src>>,
130    body_rows: Vec<TableRow<'src>>,
131    footer_row: Option<TableRow<'src>>,
132    source: Span<'src>,
133    title_source: Option<Span<'src>>,
134    title: Option<Content<'src>>,
135    caption: Option<String>,
136    number: Option<usize>,
137    frame: Frame,
138    grid: Grid,
139    stripes: Stripes,
140    anchor: Option<Span<'src>>,
141    anchor_reftext: Option<Span<'src>>,
142    attrlist: Option<Attrlist<'src>>,
143}
144
145impl<'src> TableBlock<'src> {
146    /// Returns the block's title as a mutable [`Content`], if the block has
147    /// one.
148    ///
149    /// This narrow seam exists for the document-order title resolution pass
150    /// (see `document::title_refs`), which installs the re-rendered title
151    /// after resolving any cross-references embedded in it. All other access
152    /// goes through the read-only [`IsBlock::title`] accessor.
153    pub(crate) fn title_content_mut(&mut self) -> Option<&mut Content<'src>> {
154        self.title.as_mut()
155    }
156
157    /// Returns `true` if `line` is a table delimiter.
158    ///
159    /// A table delimiter is one of the lead characters `|`, `!`, `,`, or `:`
160    /// followed by three or more equals signs (`===`). The lead character also
161    /// selects the table's data format and default cell separator:
162    ///
163    /// * `|===` is the ordinary (PSV) table delimiter.
164    /// * `!===` opens a table whose default cell separator is the exclamation
165    ///   mark, which lets a nested table be distinguished from the
166    ///   `|`-separated table that encloses it.
167    /// * `,===` is the shorthand for a CSV table.
168    /// * `:===` is the shorthand for a DSV table.
169    pub(crate) fn is_table_delimiter(line: &Span<'src>) -> bool {
170        let data = line.data();
171        // `len() >= 4` plus the leading delimiter character guarantees `rest`
172        // holds at least three bytes, so the closure only needs to confirm they
173        // are all `=`.
174        data.len() >= 4
175            && matches!(data.as_bytes().first(), Some(b'|' | b'!' | b',' | b':'))
176            && data
177                .get(1..)
178                .is_some_and(|rest| rest.bytes().all(|b| b == b'='))
179    }
180
181    pub(crate) fn parse(
182        metadata: &BlockMetadata<'src>,
183        parser: &mut Parser,
184    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
185        let delimiter = metadata.block_start.take_normalized_line();
186
187        if !Self::is_table_delimiter(&delimiter.item) {
188            return None;
189        }
190
191        let delimiter_text = delimiter.item.data();
192
193        // Find the matching closing delimiter.
194        let mut next = delimiter.after;
195        let (closing_delimiter, after) = loop {
196            if next.is_empty() {
197                break (next, next);
198            }
199
200            let line = next.take_normalized_line();
201            if line.item.data() == delimiter_text {
202                break (line.item, line.after);
203            }
204            next = line.after;
205        };
206
207        let inside = delimiter.after.trim_remainder(closing_delimiter);
208
209        // The data format governs how the table body is split into cells. It
210        // defaults to PSV, but the `format` attribute selects CSV, TSV, or DSV,
211        // and the `,===` / `:===` shorthand delimiters select CSV / DSV. The
212        // lead character of the delimiter (`delimiter_text`) is passed so the
213        // shorthand can be honored.
214        let data_format = resolve_data_format(metadata, delimiter_text);
215
216        // The cell separator partitions each row into cells. In PSV it defaults
217        // to the vertical bar (`|`), except inside an AsciiDoc table cell — a
218        // nested, standalone document — where it defaults to the exclamation
219        // mark (`!`) so a nested table is distinguished from the `|`-separated
220        // table that encloses it. Each data format has its own default (CSV =
221        // comma, TSV = tab, DSV = colon). The `separator` attribute overrides
222        // the default; an empty `separator` falls back to the default, and the
223        // two-character sequence `\t` is interpreted as a tab.
224        let separator = resolve_separator(metadata, parser, data_format);
225
226        // The `cols` attribute, when present, fixes the number of columns and
227        // carries the per-column formatting. When it is absent the column count
228        // is implicit (resolved per format below).
229        let cols_attr: Vec<TableColumn> = metadata
230            .attrlist
231            .as_ref()
232            .and_then(|a| a.named_attribute("cols"))
233            .map(|attr| parse_cols(attr.value()))
234            .unwrap_or_default();
235
236        // The `autowidth` option sizes the table to its content; the columns
237        // inherit the setting, so every column becomes autowidth regardless of
238        // any proportional width set on its specifier.
239        let autowidth = metadata
240            .attrlist
241            .as_ref()
242            .is_some_and(|a| a.has_option("autowidth"));
243
244        // The first row is an (implicit) header row when the line directly after
245        // the opening delimiter is non-empty and is itself followed by an empty
246        // line. The `header` option forces the same interpretation; the
247        // `noheader` option suppresses only the implicit detection, so an
248        // explicit `header` still wins when both are present.
249        let opts_header = metadata
250            .attrlist
251            .as_ref()
252            .is_some_and(|a| a.has_option("header"));
253        let opts_noheader = metadata
254            .attrlist
255            .as_ref()
256            .is_some_and(|a| a.has_option("noheader"));
257
258        // The last row is promoted to a footer row when the `footer` option is
259        // set. Unlike the header row, a footer cell is processed with its
260        // column's style (it is simply the last body row, relabeled).
261        let opts_footer = metadata
262            .attrlist
263            .as_ref()
264            .is_some_and(|a| a.has_option("footer"));
265
266        // The blank line must genuinely exist after the first row; the end of the
267        // table (an empty remainder) does not count, so a single-row table is not
268        // mistaken for an all-header table.
269        let line1 = inside.take_line();
270        let line1_blank = line1.item.data().trim().is_empty();
271        let line2_blank =
272            !line1.after.is_empty() && line1.after.take_line().item.data().trim().is_empty();
273
274        // An implicit header additionally requires that the first row be complete
275        // on the first line. If the first cell spans multiple lines — for PSV,
276        // the first non-blank line after the blank gap continues the cell instead
277        // of starting a new one; for CSV/TSV, the first line opens a quoted value
278        // that is not closed on that line — there is no implicit header (matching
279        // Asciidoctor, which cancels the implicit header in these cases).
280        let first_row_complete = match data_format {
281            DataFormat::Psv => first_nonblank_line(line1.after)
282                .is_none_or(|line| psv_line_starts_cell(line.data(), separator.as_str())),
283            DataFormat::Csv | DataFormat::Tsv => !line_has_unclosed_quote(line1.item.data()),
284            DataFormat::Dsv => true,
285        };
286
287        let has_header =
288            opts_header || (!opts_noheader && !line1_blank && line2_blank && first_row_complete);
289
290        // A titled table is given a caption (e.g. "Table 1. ") that a processor
291        // prepends to the title, drawn from the `table-caption` attribute (which
292        // defaults to "Table"); each such captioned table consumes the next
293        // value of a document-wide table counter. An explicit `caption`
294        // attribute sets the label verbatim with no number; an explicitly empty
295        // `caption` (e.g. `[caption=]`) removes the label entirely. When
296        // `table-caption` is unset and no explicit `caption` is given, no caption
297        // (and no number) is assigned. See [`assign_block_caption`] for the full,
298        // shared rules.
299        //
300        // Computed before the cell iterator below borrows `parser` immutably, so
301        // that the mutable counter update does not conflict with that borrow.
302        let caption = assign_block_caption(
303            parser,
304            "table",
305            metadata.attrlist.as_ref(),
306            metadata.title.is_some(),
307        );
308        let number = caption.as_ref().and_then(|caption| caption.number);
309        let caption = caption.map(|caption| caption.prefix);
310
311        // The `frame` and `grid` attributes control the table's borders, and the
312        // `stripes` attribute controls zebra striping. The borders each default
313        // to `all` and stripes defaults to `none`; the default can be changed for
314        // the whole document with the `table-frame` / `table-grid` /
315        // `table-stripes` attribute, and an explicit attribute on the table
316        // overrides both. Each value is resolved here (while `parser` is borrowed
317        // only immutably) and stored on the block so the accessors need no further
318        // document lookup.
319        let frame = resolve_table_attribute::<Frame>(metadata, parser, "frame", "table-frame");
320        let grid = resolve_table_attribute::<Grid>(metadata, parser, "grid", "table-grid");
321        let stripes =
322            resolve_table_attribute::<Stripes>(metadata, parser, "stripes", "table-stripes");
323
324        // Split the body into columns and rows according to the data format.
325        // PSV walks a grid that honors cell spans and duplication; the data
326        // formats (CSV/TSV/DSV) split on a separator with no per-cell spec and
327        // flow the values into fixed-width rows.
328        let mut warnings: Vec<Warning<'src>> = vec![];
329        let body = TableBody {
330            inside,
331            separator,
332            cols_attr,
333            autowidth,
334            has_header,
335        };
336        let (columns, rows) = match data_format {
337            DataFormat::Psv => build_psv_table(body, parser, &mut warnings),
338            DataFormat::Csv | DataFormat::Tsv | DataFormat::Dsv => {
339                build_data_table(body, data_format, parser, &mut warnings)
340            }
341        };
342
343        let mut rows = rows.into_iter();
344        let header_row = if has_header { rows.next() } else { None };
345        let mut body_rows: Vec<TableRow<'src>> = rows.collect();
346
347        // The footer row, when requested, is the last row of the table. It is
348        // moved out of the body so the caller sees it as a distinct footer. When
349        // the table has no rows to spare, no footer is produced.
350        let footer_row = if opts_footer { body_rows.pop() } else { None };
351
352        let source = metadata
353            .source
354            .trim_remainder(closing_delimiter.discard_all())
355            .trim_trailing_whitespace();
356
357        if closing_delimiter.is_empty() {
358            warnings.push(Warning {
359                source: delimiter.item,
360                warning: WarningType::UnterminatedDelimitedBlock,
361                origin: None,
362            });
363        }
364
365        Some(MatchAndWarnings {
366            item: Some(MatchedItem {
367                item: Self {
368                    columns,
369                    data_format,
370                    header_row,
371                    body_rows,
372                    footer_row,
373                    source,
374                    title_source: metadata.title_source,
375                    title: metadata.title.clone(),
376                    caption,
377                    number,
378                    frame,
379                    grid,
380                    stripes,
381                    anchor: metadata.anchor,
382                    anchor_reftext: metadata.anchor_reftext,
383                    attrlist: metadata.attrlist.clone(),
384                },
385                after,
386            }),
387            warnings,
388        })
389    }
390
391    /// Returns the caption assigned to this table, if any.
392    ///
393    /// A titled table is captioned with a label that a processor prepends to
394    /// the [`title`](IsBlock::title). By default the label combines the
395    /// `table-caption` attribute and an automatically incremented number (e.g.
396    /// `"Table 1. "`). An explicit `caption` attribute on the table overrides
397    /// this with a verbatim label and no number; an explicitly empty `caption`
398    /// (e.g. `[caption=]`) removes the label entirely. The caption is absent
399    /// when the table has no title, when `table-caption` has been unset and no
400    /// explicit `caption` is given, or when an empty `caption` was supplied.
401    pub fn caption(&self) -> Option<&str> {
402        self.caption.as_deref()
403    }
404
405    /// Returns the number assigned to this table, if any.
406    ///
407    /// A titled table for which the `table-caption` attribute is set is
408    /// numbered with an automatically incremented, document-wide table counter
409    /// (the same number that appears in its [`caption`](Self::caption), e.g.
410    /// the `1` in `"Table 1. "`). The number is absent when the table is
411    /// not captioned, or when its caption comes from an explicit
412    /// (unnumbered) `caption` attribute.
413    pub fn number(&self) -> Option<usize> {
414        self.number
415    }
416
417    /// Returns the columns of this table.
418    pub fn columns(&self) -> &[TableColumn] {
419        &self.columns
420    }
421
422    /// Returns the [`DataFormat`] used to populate this table.
423    ///
424    /// The format comes from the `format` attribute on the table (`psv`, `csv`,
425    /// `tsv`, or `dsv`) or from a shorthand delimiter (`,===` selects CSV,
426    /// `:===` selects DSV). When neither is present the format defaults to
427    /// [`DataFormat::Psv`].
428    pub fn data_format(&self) -> DataFormat {
429        self.data_format
430    }
431
432    /// Returns the fixed width of this table, as a percentage of the content
433    /// area, when the `width` attribute is set.
434    ///
435    /// The `width` attribute is an integer percentage from 1 to 100; the
436    /// trailing `%` sign is optional (`[width=75%]` and `[width=75]` are
437    /// equivalent). A value outside that range, or one that is not an integer,
438    /// is ignored and reported as `None`. When the attribute is absent the
439    /// table spans the width of the content area and this returns `None`.
440    pub fn width(&self) -> Option<usize> {
441        let raw = self
442            .attrlist
443            .as_ref()
444            .and_then(|a| a.named_attribute("width"))?
445            .value();
446
447        let raw = raw.strip_suffix('%').unwrap_or(raw);
448        match raw.parse::<usize>() {
449            Ok(width) if (1..=100).contains(&width) => Some(width),
450            _ => None,
451        }
452    }
453
454    /// Returns `true` if this table carries the `autowidth` option.
455    ///
456    /// An autowidth table is sized to fit its content rather than spanning the
457    /// width of the content area, and each of its [columns](TableColumn) is
458    /// likewise [autowidth](TableColumn::is_autowidth).
459    pub fn is_autowidth(&self) -> bool {
460        self.attrlist
461            .as_ref()
462            .is_some_and(|a| a.has_option("autowidth"))
463    }
464
465    /// Returns the [`Frame`] that controls the border drawn around this table.
466    ///
467    /// The frame comes from the `frame` attribute on the table, which accepts
468    /// `all`, `ends`, `sides`, or `none`. When the attribute is absent the
469    /// value is taken from the `table-frame` document attribute, and when
470    /// that too is absent it defaults to [`Frame::All`].
471    pub fn frame(&self) -> Frame {
472        self.frame
473    }
474
475    /// Returns the [`Grid`] that controls the borders drawn between this
476    /// table's cells.
477    ///
478    /// The grid comes from the `grid` attribute on the table, which accepts
479    /// `all`, `rows`, `cols`, or `none`. When the attribute is absent the value
480    /// is taken from the `table-grid` document attribute, and when that too is
481    /// absent it defaults to [`Grid::All`].
482    pub fn grid(&self) -> Grid {
483        self.grid
484    }
485
486    /// Returns the [`Stripes`] that control which rows of this table are shaded
487    /// to create a zebra-striping effect.
488    ///
489    /// The stripes come from the `stripes` attribute on the table, which
490    /// accepts `none`, `even`, `odd`, `all`, or `hover`. When the attribute
491    /// is absent the value is taken from the `table-stripes` document
492    /// attribute, and when that too is absent it defaults to
493    /// [`Stripes::None`].
494    ///
495    /// As a shorthand, a `stripes-<value>` role on the table (e.g.
496    /// `[.stripes-even]`) applies the same CSS class directly without setting
497    /// the `stripes` attribute. That shorthand does not affect this value
498    /// (which remains [`Stripes::None`]); the role is instead reported
499    /// among the table's [roles](crate::attributes::Attrlist::roles).
500    pub fn stripes(&self) -> Stripes {
501        self.stripes
502    }
503
504    /// Returns the header row of this table, if one was declared.
505    pub fn header_row(&self) -> Option<&TableRow<'src>> {
506        self.header_row.as_ref()
507    }
508
509    /// Returns the body rows of this table.
510    pub fn body_rows(&self) -> &[TableRow<'src>] {
511        &self.body_rows
512    }
513
514    /// Returns the footer row of this table, if one was declared.
515    pub fn footer_row(&self) -> Option<&TableRow<'src>> {
516        self.footer_row.as_ref()
517    }
518
519    /// Resolves any deferred cross-references in this table's cells.
520    pub(crate) fn resolve_references(
521        &mut self,
522        resolver: &dyn ReferenceResolver,
523        renderer: &dyn InlineSubstitutionRenderer,
524        warnings: &mut ReferenceWarnings<'src>,
525    ) {
526        let rows = self
527            .header_row
528            .iter_mut()
529            .chain(self.body_rows.iter_mut())
530            .chain(self.footer_row.iter_mut());
531
532        for row in rows {
533            for cell in row.cells.iter_mut() {
534                cell.resolve_references(resolver, renderer, warnings);
535            }
536        }
537    }
538}
539
540impl<'src> IsBlock<'src> for TableBlock<'src> {
541    fn content_model(&self) -> ContentModel {
542        ContentModel::Table
543    }
544
545    fn raw_context(&self) -> CowStr<'src> {
546        "table".into()
547    }
548
549    fn title_source(&'src self) -> Option<Span<'src>> {
550        self.title_source
551    }
552
553    fn title(&self) -> Option<&str> {
554        self.title.as_ref().map(Content::rendered_str)
555    }
556
557    // These forward to the inherent `caption()`/`number()` (the documented
558    // public accessors) so that the captioned table is reported correctly
559    // through the trait interface too — `dyn IsBlock` / generic `T: IsBlock`
560    // consumers resolve to these rather than the inherent methods.
561    fn caption(&self) -> Option<&str> {
562        self.caption.as_deref()
563    }
564
565    fn number(&self) -> Option<usize> {
566        self.number
567    }
568
569    fn anchor(&'src self) -> Option<Span<'src>> {
570        self.anchor
571    }
572
573    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
574        self.anchor_reftext
575    }
576
577    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
578        self.attrlist.as_ref()
579    }
580}
581
582impl<'src> HasSpan<'src> for TableBlock<'src> {
583    fn span(&self) -> Span<'src> {
584        self.source
585    }
586}
587
588/// A column in a [`TableBlock`].
589///
590/// A column carries its proportional width, the horizontal and vertical
591/// alignment applied to its cells' content, and the [style](ColumnStyle) used
592/// to process and render that content.
593#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct TableColumn {
595    width: usize,
596    autowidth: bool,
597    h_align: HorizontalAlignment,
598    v_align: VerticalAlignment,
599    style: ColumnStyle,
600}
601
602impl TableColumn {
603    /// Returns the width of this column relative to the other columns in the
604    /// table. The default width is `1`.
605    ///
606    /// This value carries two different meanings depending on the table, and a
607    /// caller that resolves columns to final sizes must check which applies:
608    ///
609    /// * In an ordinary table (no column is [autowidth](Self::is_autowidth)),
610    ///   the width is a *proportional* ratio. Each column's share of the table
611    ///   is its width divided by the sum of all the column widths, so
612    ///   `[cols="1,2,3"]` yields shares of 1/6, 2/6, and 3/6.
613    /// * When at least one column in the table is autowidth (its specifier uses
614    ///   the special width value `~`), the AsciiDoc specification instead reads
615    ///   these widths as literal *percentages* (100-based): in
616    ///   `[cols="25,~,~"]` the first column is 25% wide and the `~` columns are
617    ///   sized to their content.
618    ///
619    /// The two cases are distinguished by whether any column in the table is
620    /// autowidth, which the caller can test with
621    /// `table.columns().iter().any(TableColumn::is_autowidth)`.
622    ///
623    /// When this column itself is autowidth, this width is not used to size the
624    /// column (the column is sized to its content instead). A column made
625    /// autowidth by the `~` specifier reports the default width of `1`, but one
626    /// that inherits autowidth from the table's `autowidth` option retains
627    /// whatever width its specifier set (e.g. `2` for the first column of
628    /// `[%autowidth,cols="2,1"]`).
629    pub fn width(&self) -> usize {
630        self.width
631    }
632
633    /// Returns `true` if this column is sized to fit its content rather than to
634    /// a proportional width.
635    ///
636    /// A column is autowidth when its column specifier uses the special width
637    /// value `~`, or when the table as a whole carries the `autowidth` option
638    /// (in which case every column inherits the setting).
639    pub fn is_autowidth(&self) -> bool {
640        self.autowidth
641    }
642
643    /// Returns the horizontal alignment applied to this column's content.
644    ///
645    /// The alignment comes from a horizontal alignment operator (`<`, `>`, or
646    /// `^`) on the column's specifier and defaults to
647    /// [`HorizontalAlignment::Left`].
648    pub fn h_align(&self) -> HorizontalAlignment {
649        self.h_align
650    }
651
652    /// Returns the vertical alignment applied to this column's content.
653    ///
654    /// The alignment comes from a vertical alignment operator (`.<`, `.>`, or
655    /// `.^`) on the column's specifier and defaults to
656    /// [`VerticalAlignment::Top`].
657    pub fn v_align(&self) -> VerticalAlignment {
658        self.v_align
659    }
660
661    /// Returns the [style](ColumnStyle) applied to this column's content.
662    ///
663    /// The style comes from a style operator in the last position of the
664    /// column's specifier (`a`, `d`, `e`, `h`, `l`, `m`, or `s`) and defaults
665    /// to [`ColumnStyle::Default`].
666    pub fn style(&self) -> ColumnStyle {
667        self.style
668    }
669}
670
671impl Default for TableColumn {
672    fn default() -> Self {
673        Self {
674            width: 1,
675            autowidth: false,
676            h_align: HorizontalAlignment::Left,
677            v_align: VerticalAlignment::Top,
678            style: ColumnStyle::Default,
679        }
680    }
681}
682
683/// The data format that governs how a [`TableBlock`]'s body is split into
684/// cells.
685///
686/// The format is selected by the `format` attribute (`psv`, `csv`, `tsv`, or
687/// `dsv`) or by a shorthand delimiter (`,===` for CSV, `:===` for DSV). The
688/// default is [`Psv`](Self::Psv).
689///
690/// In the PSV format the separator is placed in front of each cell and a cell
691/// may carry a formatting spec. In the delimiter-separated formats (CSV, TSV,
692/// and DSV) the separator is placed *between* values and a cell carries no
693/// spec; cell formatting is applied per column with the `cols` attribute
694/// instead. In every delimiter-separated format empty lines are skipped,
695/// whitespace surrounding each value is stripped, and a "ragged" table (whose
696/// rows do not all have the same number of cells) has its cells flowed into
697/// fixed-width rows, dropping any cells left over at the end.
698#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
699pub enum DataFormat {
700    /// Prefix-separated values: the default format. The separator (a vertical
701    /// bar, `|`, by default) is placed in front of each cell.
702    #[default]
703    Psv,
704
705    /// Comma-separated values (the `csv` format). The default separator is a
706    /// comma (`,`). Values may be enclosed in double quotes (`"`), within which
707    /// the separator and newlines are literal and a double quote is written by
708    /// doubling it (`""`); a newline that is not inside a quoted value begins a
709    /// new row. Loosely based on RFC 4180.
710    Csv,
711
712    /// Tab-separated values (the `tsv` format). Parsed by the same rules as
713    /// [`Csv`](Self::Csv), but the default separator is a tab.
714    Tsv,
715
716    /// Delimited values (the `dsv` format). The default separator is a colon
717    /// (`:`). Unlike CSV and TSV, an enclosing character is not recognized;
718    /// instead the separator can be included in a value by escaping it with a
719    /// single backslash (`\:`).
720    Dsv,
721}
722
723/// The style applied to the content of a [column](TableColumn) (and, by
724/// extension, to each body cell in that column).
725///
726/// A style is specified by a style operator in the last position of a column
727/// specifier. When no style operator is present, [`Default`](Self::Default) is
728/// assigned and the column is processed as paragraph text.
729///
730/// The style governs both how a cell's content is parsed and how it is
731/// rendered: most styles leave the content as inline markup (changing only the
732/// surrounding formatting), [`Literal`](Self::Literal) processes the content
733/// verbatim, and [`AsciiDoc`](Self::AsciiDoc) parses the content as a nested,
734/// standalone AsciiDoc document.
735///
736/// The verse operator (`v`) recognized by older versions of AsciiDoc has been
737/// deprecated and is not modeled here.
738#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
739pub enum ColumnStyle {
740    /// Block elements (lists, delimited blocks, and block macros) are
741    /// supported; the content is parsed as a nested, standalone AsciiDoc
742    /// document (the `a` operator).
743    AsciiDoc,
744
745    /// All of the markup permitted in a paragraph (inline formatting and inline
746    /// macros) is supported (the `d` operator). This is the default style,
747    /// assigned automatically when no style operator is present.
748    #[default]
749    Default,
750
751    /// Text is italicized (the `e` operator).
752    Emphasis,
753
754    /// The header semantics and styles are applied to the text and cell borders
755    /// (the `h` operator).
756    Header,
757
758    /// Content is treated as if it were inside a literal block (the `l`
759    /// operator).
760    Literal,
761
762    /// Text is rendered using a monospace font (the `m` operator).
763    Monospace,
764
765    /// Text is bold (the `s` operator).
766    Strong,
767}
768
769/// The horizontal alignment of a column's content.
770///
771/// Specified by a horizontal alignment operator at the start of a
772/// [column specifier](TableColumn): the less-than sign (`<`) for
773/// [`Left`](Self::Left), the greater-than sign (`>`) for
774/// [`Right`](Self::Right), and the caret (`^`) for [`Center`](Self::Center).
775/// The default is [`Left`](Self::Left).
776#[derive(Clone, Copy, Debug, Eq, PartialEq)]
777pub enum HorizontalAlignment {
778    /// Content is aligned to the left side of the column (the `<` operator).
779    /// This is the default horizontal alignment.
780    Left,
781
782    /// Content is centered horizontally in the column (the `^` operator).
783    Center,
784
785    /// Content is aligned to the right side of the column (the `>` operator).
786    Right,
787}
788
789/// The vertical alignment of a column's content.
790///
791/// Specified by a vertical alignment operator on a
792/// [column specifier](TableColumn), always introduced by a dot (`.`): `.<` for
793/// [`Top`](Self::Top), `.>` for [`Bottom`](Self::Bottom), and `.^` for
794/// [`Middle`](Self::Middle). The default is [`Top`](Self::Top).
795#[derive(Clone, Copy, Debug, Eq, PartialEq)]
796pub enum VerticalAlignment {
797    /// Content is aligned to the top of the column's cells (the `.<` operator).
798    /// This is the default vertical alignment.
799    Top,
800
801    /// Content is centered vertically in the column's cells (the `.^`
802    /// operator).
803    Middle,
804
805    /// Content is aligned to the bottom of the column's cells (the `.>`
806    /// operator).
807    Bottom,
808}
809
810/// The border drawn around a [`TableBlock`].
811///
812/// The frame is set with the `frame` attribute on the table (or, document-wide,
813/// the `table-frame` attribute). The default is [`All`](Self::All).
814///
815/// An unrecognized value falls back to [`All`](Self::All). (Asciidoctor instead
816/// passes an unrecognized value straight through to a CSS class, which the
817/// stylesheet ignores; this parser models only the four documented values.)
818#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
819pub enum Frame {
820    /// A border is drawn on every side of the table (the `all` value). This is
821    /// the default frame.
822    #[default]
823    All,
824
825    /// A border is drawn on the top and bottom of the table (the `ends` value).
826    ///
827    /// The `topbot` value recognized by older versions of AsciiDoc is accepted
828    /// as a synonym.
829    Ends,
830
831    /// A border is drawn on the left and right sides of the table (the `sides`
832    /// value).
833    Sides,
834
835    /// No border is drawn around the table (the `none` value).
836    None,
837}
838
839/// The borders drawn between the cells of a [`TableBlock`].
840///
841/// The grid is set with the `grid` attribute on the table (or, document-wide,
842/// the `table-grid` attribute). The default is [`All`](Self::All).
843///
844/// An unrecognized value falls back to [`All`](Self::All). (Asciidoctor instead
845/// passes an unrecognized value straight through to a CSS class, which the
846/// stylesheet ignores; this parser models only the four documented values.)
847#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
848pub enum Grid {
849    /// A border is drawn between all cells (the `all` value). This is the
850    /// default grid.
851    #[default]
852    All,
853
854    /// A border is drawn between the rows of the table (the `rows` value).
855    Rows,
856
857    /// A border is drawn between the columns of the table (the `cols` value).
858    Cols,
859
860    /// No border is drawn between the cells (the `none` value).
861    None,
862}
863
864/// The zebra striping applied to the rows of a [`TableBlock`].
865///
866/// Striping shades the specified rows with a background color to create a zebra
867/// effect. It is set with the `stripes` attribute on the table (or,
868/// document-wide, the `table-stripes` attribute). The default is
869/// [`None`](Self::None).
870///
871/// Under the covers, a converter applies the CSS class `stripes-<value>` to the
872/// table; the actual shading depends on the stylesheet. As a shorthand, the
873/// same class can be applied directly with a role (e.g. `[.stripes-even]`)
874/// rather than the `stripes` attribute. A role does not set this value (see
875/// [`TableBlock::stripes`]).
876///
877/// An unrecognized value falls back to [`None`](Self::None). (Asciidoctor
878/// instead passes an unrecognized value straight through to a CSS class, which
879/// the stylesheet ignores; this parser models only the five documented values.)
880#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
881pub enum Stripes {
882    /// No rows are shaded (the `none` value). This is the default.
883    #[default]
884    None,
885
886    /// Even rows are shaded (the `even` value).
887    Even,
888
889    /// Odd rows are shaded (the `odd` value).
890    Odd,
891
892    /// All rows are shaded (the `all` value).
893    All,
894
895    /// The row under the mouse cursor is shaded (the `hover` value). This has
896    /// an effect only in HTML output.
897    Hover,
898}
899
900/// A table-level attribute value ([`Frame`], [`Grid`], or [`Stripes`]) that can
901/// be parsed from an attribute value and has a default.
902trait TableAttributeValue: Copy + Default {
903    /// Parse the value of the table attribute (or its document-level
904    /// `table-<name>` counterpart). An unrecognized value yields the default.
905    fn from_attr_value(value: &str) -> Self;
906}
907
908impl TableAttributeValue for Frame {
909    fn from_attr_value(value: &str) -> Self {
910        match value.trim() {
911            // `topbot` is the older synonym for `ends`.
912            "ends" | "topbot" => Frame::Ends,
913            "sides" => Frame::Sides,
914            "none" => Frame::None,
915            // `all` and any unrecognized value.
916            _ => Frame::All,
917        }
918    }
919}
920
921impl TableAttributeValue for Grid {
922    fn from_attr_value(value: &str) -> Self {
923        match value.trim() {
924            "rows" => Grid::Rows,
925            "cols" => Grid::Cols,
926            "none" => Grid::None,
927            // `all` and any unrecognized value.
928            _ => Grid::All,
929        }
930    }
931}
932
933impl TableAttributeValue for Stripes {
934    fn from_attr_value(value: &str) -> Self {
935        match value.trim() {
936            "even" => Stripes::Even,
937            "odd" => Stripes::Odd,
938            "all" => Stripes::All,
939            "hover" => Stripes::Hover,
940            // `none` and any unrecognized value.
941            _ => Stripes::None,
942        }
943    }
944}
945
946/// Resolve a table-level attribute ([`Frame`], [`Grid`], or [`Stripes`]).
947///
948/// An explicit attribute on the table (`attr_name`) wins; otherwise the
949/// document-level default (`doc_attr_name`) is consulted; otherwise the value
950/// falls back to the type's default.
951fn resolve_table_attribute<B: TableAttributeValue>(
952    metadata: &BlockMetadata<'_>,
953    parser: &Parser,
954    attr_name: &str,
955    doc_attr_name: &str,
956) -> B {
957    if let Some(attr) = metadata
958        .attrlist
959        .as_ref()
960        .and_then(|a| a.named_attribute(attr_name))
961    {
962        B::from_attr_value(attr.value())
963    } else if let InterpretedValue::Value(value) = parser.attribute_value(doc_attr_name) {
964        B::from_attr_value(&value)
965    } else {
966        B::default()
967    }
968}
969
970/// Resolve the [`DataFormat`] of a table.
971///
972/// An explicit, recognized `format` attribute (`psv`, `csv`, `tsv`, or `dsv`)
973/// always wins. Otherwise the lead character of the delimiter selects the
974/// format via its shorthand — `,===` is CSV and `:===` is DSV — and any other
975/// delimiter (`|===`, `!===`) is PSV.
976fn resolve_data_format(metadata: &BlockMetadata<'_>, delimiter_text: &str) -> DataFormat {
977    if let Some(attr) = metadata
978        .attrlist
979        .as_ref()
980        .and_then(|a| a.named_attribute("format"))
981    {
982        match attr.value().trim() {
983            "psv" => return DataFormat::Psv,
984            "csv" => return DataFormat::Csv,
985            "tsv" => return DataFormat::Tsv,
986            "dsv" => return DataFormat::Dsv,
987            // An unrecognized format value falls through to the shorthand (or
988            // the PSV default).
989            _ => {}
990        }
991    }
992
993    match delimiter_text.as_bytes().first() {
994        Some(b',') => DataFormat::Csv,
995        Some(b':') => DataFormat::Dsv,
996        _ => DataFormat::Psv,
997    }
998}
999
1000/// Resolve the cell separator for a table.
1001///
1002/// Each [`DataFormat`] supplies a default separator: PSV uses the vertical bar
1003/// (`|`), except inside an AsciiDoc table cell — a nested, standalone document
1004/// — where it defaults to the exclamation mark (`!`) so a nested table is
1005/// distinguished from the `|`-separated table that encloses it; CSV defaults to
1006/// a comma (`,`), TSV to a tab, and DSV to a colon (`:`). An explicit
1007/// `separator` attribute on the table overrides the default; an empty
1008/// `separator` value (e.g. `[separator=]`) falls back to the default. The
1009/// two-character sequence `\t` in the attribute value is interpreted as a tab,
1010/// so a tab-separated table can be written `[format=csv,separator=\t]`.
1011fn resolve_separator(
1012    metadata: &BlockMetadata<'_>,
1013    parser: &Parser,
1014    data_format: DataFormat,
1015) -> String {
1016    let default = match data_format {
1017        DataFormat::Psv => {
1018            if parser.nested_document_depth > 0 {
1019                "!"
1020            } else {
1021                "|"
1022            }
1023        }
1024        DataFormat::Csv => ",",
1025        DataFormat::Tsv => "\t",
1026        DataFormat::Dsv => ":",
1027    };
1028
1029    metadata
1030        .attrlist
1031        .as_ref()
1032        .and_then(|a| a.named_attribute("separator"))
1033        .map(|attr| attr.value())
1034        .filter(|value| !value.is_empty())
1035        // The author writes a literal tab as the escape sequence `\t`.
1036        .map(|value| value.replace("\\t", "\t"))
1037        .unwrap_or_else(|| default.to_string())
1038}
1039
1040/// Finalize a table's columns once the column count is known.
1041///
1042/// When the `cols` attribute supplied columns (`cols_attr` is non-empty) they
1043/// are used as-is; otherwise `ncols` default columns are created. When the
1044/// table carries the `autowidth` option, every column is made autowidth
1045/// regardless of the proportional width set on its specifier.
1046fn finalize_columns(
1047    cols_attr: Vec<TableColumn>,
1048    ncols: usize,
1049    autowidth: bool,
1050) -> Vec<TableColumn> {
1051    let mut columns = if cols_attr.is_empty() {
1052        (0..ncols).map(|_| TableColumn::default()).collect()
1053    } else {
1054        cols_attr
1055    };
1056
1057    if autowidth {
1058        for column in columns.iter_mut() {
1059            column.autowidth = true;
1060        }
1061    }
1062
1063    columns
1064}
1065
1066/// The inputs shared by the PSV and data-format table-body builders.
1067struct TableBody<'src> {
1068    /// The region between the opening and closing delimiters.
1069    inside: Span<'src>,
1070
1071    /// The resolved cell separator.
1072    separator: String,
1073
1074    /// Columns parsed from the `cols` attribute (empty when the attribute is
1075    /// absent, in which case the column count is implicit).
1076    cols_attr: Vec<TableColumn>,
1077
1078    /// Whether the table carries the `autowidth` option.
1079    autowidth: bool,
1080
1081    /// Whether the first row is a header row.
1082    has_header: bool,
1083}
1084
1085/// Build the columns and rows of a PSV (prefix-separated values) table.
1086///
1087/// The column count comes from the `cols` attribute (`cols_attr`) or, when that
1088/// is absent, from the number of column slots in the first non-empty line.
1089/// Cells are then scanned in document order and partitioned into rows by
1090/// walking the grid: a cell's span (colspan/rowspan) governs how many column
1091/// slots it occupies, so a column-spanning cell fills its row with fewer cells
1092/// and a row-spanning cell carries its columns down into the rows below.
1093///
1094/// This mirrors Asciidoctor's grid walk. `active_rowspans[k]` records the
1095/// number of column slots that cells from earlier rows occupy in the row `k`
1096/// steps ahead of the one being filled; a row closes once its own cells'
1097/// colspans plus the slots carried into it (`active_rowspans[0]`) reach
1098/// `ncols`. A cell whose span pushes the row *past* `ncols` overruns the grid:
1099/// the whole overrunning row is dropped (with a warning), again matching
1100/// Asciidoctor. A row whose columns are entirely pre-filled by carried slots
1101/// has no cells of its own to close it, so the next cell overruns and is
1102/// dropped together with that pre-filled row. A duplicated cell (`<n>*`) is
1103/// expanded into `<n>` independent cells — each carrying the original's
1104/// content, alignment, and style — before the grid walk, so each clone occupies
1105/// its own column slot exactly like an ordinary cell. A duplication factor of
1106/// zero drops the cell entirely.
1107fn build_psv_table<'src>(
1108    body: TableBody<'src>,
1109    parser: &mut Parser,
1110    warnings: &mut Vec<Warning<'src>>,
1111) -> (Vec<TableColumn>, Vec<TableRow<'src>>) {
1112    let TableBody {
1113        inside,
1114        separator,
1115        cols_attr,
1116        autowidth,
1117        has_header,
1118    } = body;
1119
1120    let separator = separator.as_str();
1121
1122    // When the column count is implicit, it is the number of column slots in the
1123    // first non-empty line: a cell that spans columns (`<n>+`) counts as `<n>`
1124    // slots, not one, and a cell duplicated `<n>` times (`<n>*`) counts as `<n>`
1125    // single-column slots (one per clone).
1126    let first_line_cells: usize =
1127        scan_cells(inside.discard_empty_lines().take_line().item, separator)
1128            .0
1129            .iter()
1130            .map(|c| c.spec.colspan.max(1) * c.spec.repeat.min(MAX_DUPLICATION_FACTOR))
1131            .sum();
1132
1133    let ncols = if cols_attr.is_empty() {
1134        first_line_cells
1135    } else {
1136        cols_attr.len()
1137    };
1138
1139    let columns = finalize_columns(cols_attr, ncols, autowidth);
1140
1141    let (raw_cells, recovered_first_cell) = scan_cells(inside, separator);
1142    if let Some(source) = recovered_first_cell {
1143        warnings.push(Warning {
1144            source,
1145            warning: WarningType::TableMissingLeadingSeparator,
1146            origin: None,
1147        });
1148    }
1149
1150    let raw_cells = expand_duplicates(raw_cells);
1151
1152    // A table can never have more rows than it has cells, so a row span is
1153    // clamped to the cell count for the `active_rowspans` bookkeeping below: a
1154    // larger span carries into rows that can't exist and so has no additional
1155    // layout effect. The clamp also bounds the `active_rowspans` allocation, so a
1156    // hostile specifier such as `.1000000000+` can't trigger a multi-gigabyte
1157    // allocation. (The cell's reported [`rowspan`] keeps the literal parsed
1158    // value, matching Asciidoctor.)
1159    //
1160    // [`rowspan`]: TableCell::rowspan
1161    let max_rowspan = raw_cells.len().saturating_add(1);
1162
1163    let mut raw_rows: Vec<Vec<RawCell<'src>>> = vec![];
1164
1165    if ncols > 0 {
1166        // A queue: each completed row consumes the slots carried into it from the
1167        // front (`pop_front`), while a multi-row cell reserves slots in the rows it
1168        // extends into via the back. `VecDeque` keeps both ends O(1); a `Vec` would
1169        // pay an O(n) shift on every `remove(0)`.
1170        let mut active_rowspans: VecDeque<usize> = VecDeque::from([0]);
1171        let mut column_visits = 0usize;
1172        let mut current_row: Vec<RawCell<'src>> = vec![];
1173
1174        for raw in raw_cells {
1175            let colspan = raw.spec.colspan.max(1);
1176            let rowspan = raw.spec.rowspan.max(1).min(max_rowspan);
1177
1178            // A cell that spans more than one row reserves `colspan` slots in
1179            // each of the rows it extends into (but not its own row).
1180            if rowspan > 1 {
1181                if active_rowspans.len() < rowspan {
1182                    active_rowspans.resize(rowspan, 0);
1183                }
1184                for slot in active_rowspans.iter_mut().take(rowspan).skip(1) {
1185                    *slot += colspan;
1186                }
1187            }
1188
1189            column_visits += colspan;
1190            let cell_source = raw.content;
1191            current_row.push(raw);
1192
1193            // The slots carried into the current row are `active_rowspans[0]`; the
1194            // deque is never empty here, so the fallback is unreachable.
1195            let carried = active_rowspans.front().copied().unwrap_or(0);
1196            let effective = column_visits + carried;
1197            if effective >= ncols {
1198                if effective == ncols {
1199                    raw_rows.push(std::mem::take(&mut current_row));
1200                } else {
1201                    // Overrun: this cell's span pushes the row past `ncols`.
1202                    // Discard the whole row so the remaining cells stay aligned to
1203                    // the grid.
1204                    current_row.clear();
1205                    warnings.push(Warning {
1206                        source: cell_source,
1207                        warning: WarningType::TableCellExceedsColumnCount,
1208                        origin: None,
1209                    });
1210                }
1211                column_visits = 0;
1212                active_rowspans.pop_front();
1213                if active_rowspans.is_empty() {
1214                    active_rowspans.push_back(0);
1215                }
1216            }
1217        }
1218
1219        // If the table ends mid-row, the cells accumulated since the last
1220        // complete row never filled `ncols`. Matching Asciidoctor's
1221        // `close_table`, that incomplete row is dropped and an error is logged
1222        // against its last cell.
1223        if let Some(last) = current_row.last() {
1224            warnings.push(Warning {
1225                source: last.content,
1226                warning: WarningType::TableIncompleteRowAtEndOfTable,
1227                origin: None,
1228            });
1229        }
1230    }
1231
1232    // Each cell is processed according to the style of the column it falls in. A
1233    // cell's column is its ordinal position within its row (matching Asciidoctor,
1234    // which assigns the column by cell count, not grid slot). The header row
1235    // (when present) is the first row and is always processed as plain header
1236    // content, regardless of the column styles, so that a style operator doesn't
1237    // affect the header row.
1238    let mut rows: Vec<TableRow<'src>> = Vec::with_capacity(raw_rows.len());
1239    for (row_idx, raw_row) in raw_rows.into_iter().enumerate() {
1240        let is_header = has_header && row_idx == 0;
1241        let mut cells = Vec::with_capacity(raw_row.len());
1242        for (col_idx, raw) in raw_row.into_iter().enumerate() {
1243            let column = columns.get(col_idx).cloned().unwrap_or_default();
1244            cells.push(TableCell::parse(
1245                raw, &column, is_header, separator, parser, warnings,
1246            ));
1247        }
1248        rows.push(TableRow { cells });
1249    }
1250
1251    (columns, rows)
1252}
1253
1254/// Build the columns and rows of a delimiter-separated table (CSV, TSV, or
1255/// DSV).
1256///
1257/// The body is split into a flat list of [fields](DataField) by the format's
1258/// parser, then flowed into fixed-width rows. The column count comes from the
1259/// `cols` attribute (`cols_attr`) or, when that is absent, from the number of
1260/// fields in the first row. Because a data cell carries no span, the fields are
1261/// simply chunked `ncols` at a time; any fields left over after the last
1262/// complete row are dropped ("extra cells at the end of the last row get
1263/// dropped"). The first row is the header when `has_header` is set.
1264fn build_data_table<'src>(
1265    body: TableBody<'src>,
1266    data_format: DataFormat,
1267    parser: &mut Parser,
1268    warnings: &mut Vec<Warning<'src>>,
1269) -> (Vec<TableColumn>, Vec<TableRow<'src>>) {
1270    let TableBody {
1271        inside,
1272        separator,
1273        cols_attr,
1274        autowidth,
1275        has_header,
1276    } = body;
1277
1278    let separator = separator.as_str();
1279
1280    // DSV is parsed by its own, simpler rules; CSV and TSV share their rules and
1281    // differ only in the default separator (resolved by the caller). PSV never
1282    // reaches this builder.
1283    let (fields, first_row_len) = if data_format == DataFormat::Dsv {
1284        parse_dsv_fields(inside, separator)
1285    } else {
1286        parse_csv_fields(inside, separator, warnings)
1287    };
1288
1289    let ncols = if cols_attr.is_empty() {
1290        first_row_len
1291    } else {
1292        cols_attr.len()
1293    };
1294
1295    let columns = finalize_columns(cols_attr, ncols, autowidth);
1296
1297    // Integer division drops any partial trailing row; `checked_div` yields zero
1298    // rows when there are no columns.
1299    let nrows = fields.len().checked_div(ncols).unwrap_or(0);
1300    let mut rows: Vec<TableRow<'src>> = Vec::with_capacity(nrows);
1301    let mut fields = fields.into_iter();
1302    for row_idx in 0..nrows {
1303        let is_header = has_header && row_idx == 0;
1304        let mut cells = Vec::with_capacity(ncols);
1305        for col_idx in 0..ncols {
1306            // `nrows * ncols <= fields.len()`, so the iterator always yields.
1307            let Some(field) = fields.next() else { break };
1308            let column = columns.get(col_idx).cloned().unwrap_or_default();
1309            cells.push(TableCell::parse_data(
1310                field, &column, is_header, parser, warnings,
1311            ));
1312        }
1313        rows.push(TableRow { cells });
1314    }
1315
1316    (columns, rows)
1317}
1318
1319/// A single field of a delimiter-separated (CSV, TSV, or DSV) table, as located
1320/// by [`parse_csv_fields`] or [`parse_dsv_fields`].
1321///
1322/// `content` is the field's value span with surrounding whitespace already
1323/// stripped. `replacement` holds the value after quote or escape processing
1324/// when it differs from `content` (a CSV value with a doubled-quote escape, or
1325/// a DSV value with a backslash-escaped separator); it is `None` when the span
1326/// is the verbatim value.
1327struct DataField<'src> {
1328    content: Span<'src>,
1329    replacement: Option<String>,
1330}
1331
1332/// Parse a CSV/TSV region into its [fields](DataField), returning them in
1333/// document order together with the number of fields in the first row.
1334///
1335/// The rules, loosely based on RFC 4180: empty lines are skipped; whitespace
1336/// surrounding each value is stripped; a value may be enclosed in double
1337/// quotes, within which the separator and newlines are literal and a double
1338/// quote is written by doubling it (`""`). A newline that is not inside a
1339/// quoted value ends the row. The fields are returned flat; the caller flows
1340/// them into rows.
1341///
1342/// This mirrors Asciidoctor's `Table::ParserContext`: a separator or newline is
1343/// a cell boundary only when the text accumulated since the previous boundary
1344/// has no [unclosed quote](has_unclosed_quotes); otherwise it is part of the
1345/// value. As a result a value whose opening quote is never properly closed (or
1346/// that has trailing characters after its closing quote) keeps its quotes and
1347/// absorbs the following separators, rather than being treated as enclosed.
1348fn parse_csv_fields<'src>(
1349    region: Span<'src>,
1350    separator: &str,
1351    warnings: &mut Vec<Warning<'src>>,
1352) -> (Vec<DataField<'src>>, usize) {
1353    let data = region.data();
1354    let n = data.len();
1355    let sep_len = separator.len().max(1);
1356    let at = |k: usize| data.as_bytes().get(k).copied();
1357    let starts_with_sep = |pos: usize| data.get(pos..).is_some_and(|s| s.starts_with(separator));
1358
1359    let mut fields: Vec<DataField<'src>> = vec![];
1360    let mut first_row_len = 0usize;
1361    let mut first_row_done = false;
1362    let mut fields_in_row = 0usize;
1363
1364    // The raw text of the cell currently being accumulated runs from `cell_start`
1365    // to the next boundary.
1366    let mut cell_start = 0usize;
1367    let mut i = 0usize;
1368
1369    while i <= n {
1370        let at_eof = i == n;
1371        let at_sep = !at_eof && starts_with_sep(i);
1372        let at_nl = !at_eof && at(i) == Some(b'\n');
1373
1374        if !(at_eof || at_sep || at_nl) {
1375            i += 1;
1376            continue;
1377        }
1378
1379        let raw = data.get(cell_start..i).unwrap_or_default();
1380
1381        // A separator or newline that falls inside an unclosed quoted value is
1382        // part of the value, not a boundary; absorb it and keep scanning.
1383        if !at_eof && has_unclosed_quotes(raw) {
1384            i += if at_sep { sep_len } else { 1 };
1385            continue;
1386        }
1387
1388        // A wholly blank physical line (or trailing blank text at the end of the
1389        // region) between rows is skipped rather than emitted as an empty cell. A
1390        // blank cell that follows a separator on a populated line is kept.
1391        let blank_skip = (at_nl || at_eof) && fields_in_row == 0 && raw.trim().is_empty();
1392        if !blank_skip {
1393            fields.push(make_csv_field(region, cell_start, i, warnings));
1394            fields_in_row += 1;
1395            if !first_row_done {
1396                first_row_len = fields_in_row;
1397            }
1398        }
1399
1400        if at_eof {
1401            break;
1402        }
1403
1404        if at_nl {
1405            // The newline ends the row. The first populated row fixes the implicit
1406            // column count.
1407            if fields_in_row > 0 {
1408                first_row_done = true;
1409            }
1410            fields_in_row = 0;
1411            cell_start = i + 1;
1412            i += 1;
1413        } else {
1414            cell_start = i + sep_len;
1415            i += sep_len;
1416        }
1417    }
1418
1419    (fields, first_row_len)
1420}
1421
1422/// Build a CSV/TSV [field](DataField) from the byte range `start..end`,
1423/// applying Asciidoctor's `close_cell` value processing.
1424///
1425/// The value is stripped of surrounding whitespace; then, if it is enclosed in
1426/// double quotes, the quotes are removed and the inner value is stripped again,
1427/// so the field's [content](DataField::content) span points at the actual value
1428/// (this matters for an AsciiDoc cell, which parses that span). Finally any run
1429/// of consecutive double quotes is collapsed to one (so an escaped `""` becomes
1430/// a single `"`). A value that is not enclosed (no leading quote, or trailing
1431/// characters after the closing quote) keeps its quotes and is only collapsed.
1432///
1433/// A lone double quote is an unclosed quoted value: it logs an error and the
1434/// cell is set to empty (matching Asciidoctor).
1435fn make_csv_field<'src>(
1436    region: Span<'src>,
1437    start: usize,
1438    end: usize,
1439    warnings: &mut Vec<Warning<'src>>,
1440) -> DataField<'src> {
1441    let trimmed = trim_surrounding_whitespace(region.slice(start..end));
1442    let data = trimmed.data();
1443
1444    let content = if data == "\"" {
1445        warnings.push(Warning {
1446            source: trimmed,
1447            warning: WarningType::TableCsvDataHasUnclosedQuote,
1448            origin: None,
1449        });
1450        trimmed.slice(0..0)
1451    } else if data.len() >= 2 && data.starts_with('"') && data.ends_with('"') {
1452        trim_surrounding_whitespace(trimmed.slice(1..data.len() - 1))
1453    } else {
1454        trimmed
1455    };
1456
1457    let value = squeeze_quotes(content.data());
1458    let replacement = (value != content.data()).then_some(value);
1459
1460    DataField {
1461        content,
1462        replacement,
1463    }
1464}
1465
1466/// Collapse every run of consecutive double quotes to a single double quote,
1467/// matching Ruby's `String#squeeze('"')`.
1468///
1469/// Note: the `continue` intentionally leaves `prev_quote` set, so a run of
1470/// *N ≥ 2* consecutive `"` collapses to a single `"` (e.g. `""""` -> `"`), not
1471/// to pairs. This deliberately matches Asciidoctor rather than strict RFC 4180,
1472/// under which only `""` is a double-quote escape — don't "fix" it to a
1473/// two-character collapse without also changing Asciidoctor.
1474fn squeeze_quotes(text: &str) -> String {
1475    let mut out = String::with_capacity(text.len());
1476    let mut prev_quote = false;
1477    for c in text.chars() {
1478        if c == '"' {
1479            if prev_quote {
1480                continue;
1481            }
1482            prev_quote = true;
1483        } else {
1484            prev_quote = false;
1485        }
1486        out.push(c);
1487    }
1488    out
1489}
1490
1491/// Determine whether `buffer` (the cell text accumulated so far) holds an
1492/// unclosed double quote, a direct port of Asciidoctor's
1493/// `Table::ParserContext#buffer_has_unclosed_quotes?`.
1494///
1495/// Only a value that begins with a double quote can be "quoted"; for any other
1496/// value embedded quotes are literal and this returns `false`. A leading quote
1497/// is unclosed until a matching trailing quote appears (accounting for escaped
1498/// `""` pairs).
1499///
1500/// Note: the escaped-pair collapse (`replace("\"\"", "")`) runs before the
1501/// start/end check, so `"""` collapses to a single `"` and is reported
1502/// *closed*. Strict RFC 4180 would read `"""` as an unclosed field (open quote
1503/// plus escaped `""` + missing close); this matches Asciidoctor's
1504/// `buffer_has_unclosed_quotes?` instead, so the divergence is intentional.
1505fn has_unclosed_quotes(buffer: &str) -> bool {
1506    let record = buffer.trim();
1507
1508    if record == "\"" {
1509        return true;
1510    }
1511
1512    if !record.starts_with('"') {
1513        return false;
1514    }
1515
1516    let trailing_quote = record.ends_with('"');
1517    if (trailing_quote && record.ends_with("\"\"")) || record.starts_with("\"\"") {
1518        let collapsed = record.replace("\"\"", "");
1519        collapsed.starts_with('"') && !collapsed.ends_with('"')
1520    } else {
1521        !trailing_quote
1522    }
1523}
1524
1525/// Parse a DSV region into its [fields](DataField), returning them in document
1526/// order together with the number of fields in the first row.
1527///
1528/// Each non-empty line is a row. Whitespace surrounding each value is stripped,
1529/// and the separator can be included in a value by escaping it with a single
1530/// backslash (`\:`). An enclosing character is not recognized.
1531fn parse_dsv_fields<'src>(region: Span<'src>, separator: &str) -> (Vec<DataField<'src>>, usize) {
1532    let data = region.data();
1533    let n = data.len();
1534    let sep_len = separator.len().max(1);
1535    let escaped = format!("\\{separator}");
1536    let at = |k: usize| data.as_bytes().get(k).copied();
1537
1538    let mut fields: Vec<DataField<'src>> = vec![];
1539    let mut first_row_len = 0usize;
1540    let mut row_count = 0usize;
1541    let mut i = 0usize;
1542
1543    while i < n {
1544        let mut line_end = i;
1545        while line_end < n && at(line_end) != Some(b'\n') {
1546            line_end += 1;
1547        }
1548
1549        if data.get(i..line_end).unwrap_or("").trim().is_empty() {
1550            i = if line_end < n { line_end + 1 } else { line_end };
1551            continue;
1552        }
1553
1554        let in_line = |pos: usize| {
1555            data.get(pos..line_end)
1556                .is_some_and(|s| s.starts_with(separator))
1557        };
1558
1559        let mut fields_in_row = 0usize;
1560        let mut field_start = i;
1561        let mut p = i;
1562
1563        while p < line_end {
1564            // A backslash that escapes the separator (`\:`) is not a boundary;
1565            // skip past both so the separator stays in the value.
1566            if at(p) == Some(b'\\')
1567                && data
1568                    .get(p + 1..line_end)
1569                    .is_some_and(|s| s.starts_with(separator))
1570            {
1571                p += 1 + sep_len;
1572                continue;
1573            }
1574
1575            if in_line(p) {
1576                fields.push(make_dsv_field(region, field_start, p, &escaped, separator));
1577                fields_in_row += 1;
1578                p += sep_len;
1579                field_start = p;
1580                continue;
1581            }
1582
1583            p += 1;
1584        }
1585
1586        // The final field of the line runs to the line end.
1587        fields.push(make_dsv_field(
1588            region,
1589            field_start,
1590            line_end,
1591            &escaped,
1592            separator,
1593        ));
1594        fields_in_row += 1;
1595
1596        if row_count == 0 {
1597            first_row_len = fields_in_row;
1598        }
1599        row_count += 1;
1600
1601        i = if line_end < n { line_end + 1 } else { line_end };
1602    }
1603
1604    (fields, first_row_len)
1605}
1606
1607/// Build a DSV [field](DataField) from the byte range `start..end`, unescaping
1608/// any backslash-escaped separators (`escaped`, e.g. `\:`) into the bare
1609/// separator.
1610fn make_dsv_field<'src>(
1611    region: Span<'src>,
1612    start: usize,
1613    end: usize,
1614    escaped: &str,
1615    separator: &str,
1616) -> DataField<'src> {
1617    let trimmed = trim_surrounding_whitespace(region.slice(start..end));
1618    let replacement = if trimmed.data().contains(escaped) {
1619        Some(trimmed.data().replace(escaped, separator))
1620    } else {
1621        None
1622    };
1623
1624    DataField {
1625        content: trimmed,
1626        replacement,
1627    }
1628}
1629
1630/// Process a cell's content according to its [style](ColumnStyle), shared by
1631/// the PSV and data-format cell builders.
1632///
1633/// `trimmed` is the cell's content span with surrounding whitespace already
1634/// removed. `replacement` is the pre-filtered value (an escaped separator
1635/// unescaped, or a CSV/DSV value after quote/escape processing) when it differs
1636/// from `trimmed`; it is ignored for the [`AsciiDoc`](ColumnStyle::AsciiDoc)
1637/// style, which parses `trimmed` verbatim as a nested document. Every other
1638/// style produces inline [`Simple`](TableCellContent::Simple) content with the
1639/// verbatim substitution group for [`Literal`](ColumnStyle::Literal) and the
1640/// normal group otherwise.
1641fn process_content<'src>(
1642    trimmed: Span<'src>,
1643    replacement: Option<String>,
1644    style: ColumnStyle,
1645    parser: &mut Parser,
1646    warnings: &mut Vec<Warning<'src>>,
1647) -> TableCellContent<'src> {
1648    if style == ColumnStyle::AsciiDoc {
1649        // The AsciiDoc style effectively creates a nested, standalone AsciiDoc
1650        // document in the cell. It inherits the parent document's attributes, but
1651        // any attribute it defines is scoped to the cell and must not leak back
1652        // into the parent. Snapshot the attribute set before parsing and restore
1653        // it afterward to enforce that boundary (matching Asciidoctor, where a
1654        // `:foo:` set inside a cell is not visible after the table).
1655        let saved_attributes = parser.attribute_values.clone();
1656
1657        // An attribute that is set in the parent document cannot be modified
1658        // inside the cell. Lock every inherited attribute that currently holds a
1659        // value for the duration of the cell (other than the handful of
1660        // exceptions the spec carves out), so a body assignment to one of them is
1661        // ignored. An attribute that is unset in the parent is not locked: the
1662        // cell may assign it (matching Asciidoctor, which here diverges from the
1663        // spec's "set or explicitly unset" wording). The lock set is saved and
1664        // restored so it applies only within the cell and nests correctly.
1665        // An attribute set in the parent is locked, as is one hard set or unset
1666        // through the API (its modification context is `ApiOnly`) even though it
1667        // is unset — matching Asciidoctor, where an API-controlled attribute can
1668        // never be overridden in a cell. An attribute merely unset in the parent
1669        // document is not locked, so the cell may assign it.
1670        //
1671        // The inherited attribute set is the shared built-in defaults with the
1672        // parent's per-parser entries (`saved_attributes`) layered on top, so
1673        // walk both, letting a per-parser entry shadow a like-named built-in.
1674        // The synthesized backend-family and `safe-mode-*` flags need no lock
1675        // here: they are read-only intrinsics that reject a cell-body assignment
1676        // on their own (see `DERIVED_FAMILY_FLAG` / `SAFE_MODE_ACTIVE_FLAG`),
1677        // which a static lock could not do anyway once the cell changes its own
1678        // doctype.
1679        let saved_locks = parser.locked_attribute_names.clone();
1680        {
1681            let locks = &mut parser.locked_attribute_names;
1682            let mut maybe_lock = |name: &str, value: &AttributeValue| {
1683                let api_locked = value.modification_context == ModificationContext::ApiOnly;
1684                if (!matches!(value.value, InterpretedValue::Unset) || api_locked)
1685                    && !ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES.contains(&name)
1686                {
1687                    locks.insert(name.to_owned());
1688                }
1689            };
1690            for (name, value) in built_in_attrs_iter() {
1691                if !saved_attributes.contains_key(name) {
1692                    maybe_lock(name, value);
1693                }
1694            }
1695            for (name, value) in saved_attributes.iter() {
1696                maybe_lock(name, value);
1697            }
1698        }
1699
1700        // The modifiable attributes may always be changed inside a cell, even
1701        // when the parent or the API set them with a restrictive modification
1702        // context. Materialize each into the per-parser map (a built-in such as
1703        // `toc` otherwise lives only in the shared table) with a relaxed context
1704        // for the duration of the cell so a body assignment is honored; the
1705        // snapshot restore reverts it afterward.
1706        for name in ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES {
1707            let attrs = Arc::make_mut(&mut parser.attribute_values);
1708            if let Some(mut attr) = attrs.get(*name).or_else(|| built_in_attr(name)).cloned() {
1709                attr.modification_context = ModificationContext::Anywhere;
1710                attrs.insert((*name).to_owned(), attr);
1711            }
1712        }
1713
1714        // A cell does not inherit the parent's doctype; it resets to the default
1715        // (`article`). The cell body may still set its own doctype, and the
1716        // derived `backend-html5-doctype-*` attribute is refreshed to match.
1717        parser.force_doctype("article");
1718
1719        // Likewise, a cell does not inherit the parent's `toc` setting: a nested
1720        // document starts without a table of contents and may enable its own.
1721        // Reset the value to unset; the relax loop above already made `toc`
1722        // modifiable inside the cell, so a cell-body `:toc:` is still honored.
1723        if let Some(toc) = Arc::make_mut(&mut parser.attribute_values).get_mut("toc") {
1724            toc.value = InterpretedValue::Unset;
1725        }
1726
1727        // A cell whose content holds a preprocessor directive (an `include::`)
1728        // is parsed from an owned, expanded source the cell carries; every other
1729        // cell is parsed in place from the parent document's source, which keeps
1730        // its spans (and line numbers) and avoids a copy.
1731        let cell = if content_has_directive(trimmed.data()) {
1732            // `trimmed` indexes the document source unless this cell is itself
1733            // being parsed from some *other* cell's owned (include-expanded)
1734            // source: an owned source is a private copy whose spans index that
1735            // copy's own source map rather than the document's. A cell nested
1736            // inside a borrowed cell keeps document spans and so is still at
1737            // "document level" here.
1738            let at_document_level = !parser.is_in_owned_cell_source();
1739
1740            // The cell content is a contiguous slice of the source it came from
1741            // (the document source at document level, or an enclosing owned
1742            // cell's expanded source otherwise), so it may itself have
1743            // originated from an `include::`d file. Look up the file and line
1744            // the cell's first line came from — through the document source map
1745            // at document level, or through the enclosing owned cell's source
1746            // map otherwise — so a directive that fails to resolve reports the
1747            // correct originating file (rather than "(root file)") and so its
1748            // warning carries the right cursor.
1749            let cell_origin = if at_document_level {
1750                parser
1751                    .source_map
1752                    .clone()
1753                    .and_then(|sm| sm.original_file_and_line(trimmed.line()))
1754            } else {
1755                parser.owned_cell_original_file_and_line(trimmed.line())
1756            };
1757            let cell_origin_file = cell_origin.as_ref().and_then(|sl| sl.0.clone());
1758
1759            // Re-run the preprocessor over the cell content, naming the file it
1760            // came from so an unresolved directive is attributed to it. Keep the
1761            // resulting source map: while this cell's owned source is parsed it
1762            // lets a directive buried deeper (e.g. in a nested table cell) map
1763            // its position back to the file and line it originally came from.
1764            let (expanded, cell_source_map, preprocessor_warnings, cell_includes) =
1765                preprocess_with_initial_file_name(
1766                    trimmed.data(),
1767                    parser,
1768                    cell_origin_file.as_deref(),
1769                );
1770
1771            // An AsciiDoc table cell shares the enclosing document's catalog
1772            // (only its footnote list is cell-local), so a file included by the
1773            // cell registers on the document's include registry — as it does in
1774            // Asciidoctor, where the cell's nested document shares the parent's
1775            // `catalog[:includes]`. Registration is skipped when the cell was
1776            // itself brought in from an included file: the cell's include
1777            // targets are then relative to that file, not the outermost
1778            // document, and a mis-keyed entry could falsely collapse a
1779            // root-relative xref naming a different file (the same rule the
1780            // preprocessor applies to nested includes). Note that xrefs are
1781            // interpreted in document order as blocks parse, so only xrefs in
1782            // this cell and beyond observe these entries; Asciidoctor, which
1783            // resolves xrefs after the whole document is read, has no such
1784            // ordering.
1785            if cell_origin_file.as_deref() == parser.primary_file_name.as_deref() {
1786                for (key, full) in &cell_includes {
1787                    parser.register_include(key, *full);
1788                }
1789            }
1790            let cell_source_map = Rc::new(cell_source_map);
1791
1792            // The cell's first line as it appears in the source it was sliced
1793            // from. Only a directive on that first line reaches this inner
1794            // preprocessor — a directive at the start of any later line sits at
1795            // column 0 and was already expanded by the enclosing preprocessor —
1796            // so every warning the preprocessor just produced belongs to it.
1797            let directive_line = trimmed.take_line().item;
1798
1799            // The preprocessor locates each warning (e.g. an unresolved include
1800            // target) by byte offset into the expanded cell source, which is
1801            // owned by the cell and cannot escape it.
1802            if at_document_level {
1803                // `directive_line` indexes the document source, so re-anchor
1804                // each warning to it: its cursor then maps back to the
1805                // directive's true (file, line) through the document source map.
1806                for pw in preprocessor_warnings {
1807                    // A no-output directive (a malformed/unterminated conditional
1808                    // or a tag-filter diagnostic) carries a pre-resolved `origin`;
1809                    // resolve it to an absolute location (see
1810                    // `absolute_cell_directive_origin`). Warnings without an
1811                    // `origin` (e.g. an unresolved include target) keep
1812                    // `directive_line` as their only anchor.
1813                    let origin = absolute_cell_directive_origin(pw.origin, cell_origin.as_ref());
1814                    warnings.push(Warning {
1815                        source: directive_line,
1816                        warning: pw.warning,
1817                        origin,
1818                    });
1819                }
1820            } else {
1821                // `directive_line` indexes an enclosing owned cell's private
1822                // source, which no document span maps to. Record each warning
1823                // against the directive's line in that owned source instead;
1824                // `record_owned_cell_warning` resolves it to the originating
1825                // (file, line), and a document-level cell up the stack surfaces
1826                // it with that pre-resolved origin (see below). A no-output
1827                // directive already carries an `origin`, so resolve it to an
1828                // absolute location and pass it through as the override.
1829                for pw in preprocessor_warnings {
1830                    let origin = absolute_cell_directive_origin(pw.origin, cell_origin.as_ref());
1831                    parser.record_owned_cell_warning(directive_line.line(), pw.warning, origin);
1832                }
1833            }
1834
1835            let owned = OwnedCell::new(expanded, |source| {
1836                // Warnings from the owned parse borrow the owned source and so
1837                // cannot escape it; the include path is rare and currently
1838                // warning-free, so they are dropped here. The `debug_assert`
1839                // turns any future warning added to this path into a loud test
1840                // failure rather than a silent loss.
1841                let mut owned_warnings: Vec<Warning<'_>> = vec![];
1842
1843                // Substitution warnings (e.g. `attribute-missing=warn`) recorded
1844                // while parsing this owned source carry offsets into it, not the
1845                // primary document source, so they too must be discarded.
1846                let substitution_warnings_mark = parser.substitution_warnings_len();
1847
1848                // Publish this cell's source map for the duration of its parse,
1849                // so a directive buried in the owned source (e.g. in a nested
1850                // table cell) can map its position back to the originating
1851                // (file, line), and so a table nested within cannot mis-map its
1852                // spans against the document source map.
1853                parser.push_owned_cell_source_map(cell_source_map);
1854                let (title, inline, toc, blocks, attributes) =
1855                    parse_asciidoc_cell_body(Span::new(source), parser, &mut owned_warnings);
1856
1857                let owned_root = Span::new(source);
1858                for sw in parser.drain_substitution_warnings_since(substitution_warnings_mark) {
1859                    let warning_source = owned_root.slice(sw.offset..sw.offset + sw.len);
1860                    // A substitution warning locates itself by offset into the
1861                    // owned source, so it has no pre-resolved origin; resolve it
1862                    // through this cell's source map (the directive-warning
1863                    // override path does not apply).
1864                    parser.record_owned_cell_warning(warning_source.line(), sw.warning, None);
1865                }
1866                parser.pop_owned_cell_source_map();
1867
1868                debug_assert!(
1869                    owned_warnings.is_empty(),
1870                    "warnings from an include-expanded AsciiDoc cell are dropped; \
1871                     propagate them before adding any to this path"
1872                );
1873
1874                OwnedCellInner {
1875                    title,
1876                    inline,
1877                    toc,
1878                    blocks,
1879                    attributes,
1880                }
1881            });
1882
1883            // A directive buried in this cell's owned source (e.g. an
1884            // unresolvable include in a nested table cell) recorded its warning
1885            // with a pre-resolved origin while the owned source was parsed
1886            // above. At document level, surface those now: anchor each to this
1887            // cell's directive line (a real document span) so it still has a
1888            // cursor, and carry its true origin so consumers can report the file
1889            // and line the failing directive actually lives at. Deeper owned
1890            // cells leave them queued for the document-level cell enclosing them.
1891            if at_document_level {
1892                for rw in parser.take_owned_cell_warnings() {
1893                    warnings.push(Warning {
1894                        source: directive_line,
1895                        warning: rw.warning,
1896                        origin: Some(rw.origin),
1897                    });
1898                }
1899            }
1900
1901            AsciiDocCell::Owned(Arc::new(owned))
1902        } else {
1903            let (title, inline, toc, blocks, attributes) =
1904                parse_asciidoc_cell_body(trimmed, parser, warnings);
1905            AsciiDocCell::Borrowed(BorrowedCell {
1906                title,
1907                inline,
1908                toc,
1909                blocks,
1910                attributes,
1911            })
1912        };
1913
1914        parser.locked_attribute_names = saved_locks;
1915        parser.attribute_values = saved_attributes;
1916        TableCellContent::AsciiDoc(cell)
1917    } else {
1918        let mut content = match replacement {
1919            Some(replacement) => Content::from_filtered(trimmed, replacement),
1920            None => Content::from(trimmed),
1921        };
1922
1923        let substitutions = if style == ColumnStyle::Literal {
1924            SubstitutionGroup::Verbatim
1925        } else {
1926            SubstitutionGroup::Normal
1927        };
1928        substitutions.apply(&mut content, parser, None);
1929
1930        TableCellContent::Simple(content)
1931    }
1932}
1933
1934/// Parses the body of an AsciiDoc table cell — a nested, standalone AsciiDoc
1935/// document — returning its (shown) title, whether its doctype is `inline`, its
1936/// table-of-contents configuration, its blocks, and a snapshot of the cell's
1937/// resolved attribute state.
1938///
1939/// A leading level-0 title line (`= Title`) is the nested document's title
1940/// rather than a section, so it is split off and rendered here (a level-0
1941/// heading is otherwise rejected in block parsing). The render-time decisions
1942/// (`inline`, and whether the title is shown) depend on the cell's now-mutated
1943/// attribute state, so they are resolved before the caller restores the
1944/// parent's attribute snapshot.
1945///
1946/// The attribute snapshot is likewise taken here, before that restore, so the
1947/// cell can be introspected as the nested document it is: it captures the
1948/// attributes the cell inherited from the parent (plus any the cell body set),
1949/// mirroring how a top-level [`Document`](crate::Document) retains its own
1950/// resolved attribute state.
1951fn parse_asciidoc_cell_body<'src>(
1952    content: Span<'src>,
1953    parser: &mut Parser,
1954    warnings: &mut Vec<Warning<'src>>,
1955) -> (
1956    Option<String>,
1957    bool,
1958    TocConfig,
1959    Vec<Block<'src>>,
1960    ResolvedAttributes,
1961) {
1962    let first_line = content.take_line();
1963    let (title_source, body) = if first_line.item.data().starts_with("= ") {
1964        (
1965            Some(first_line.item.discard(2).discard_whitespace()),
1966            first_line.after,
1967        )
1968    } else {
1969        (None, content)
1970    };
1971
1972    // A nested document keeps its own footnote registry: footnotes defined
1973    // inside this cell must not be shared with (or numbered into the list of)
1974    // the enclosing document. We swap in a fresh, empty footnote list for the
1975    // duration of the cell parse and restore the parent's afterward, discarding
1976    // the cell's footnotes (see issue #544). The `footnote-number` counter is a
1977    // document-wide attribute and is deliberately *not* reset, so footnote
1978    // numbering continues across the cell as Asciidoctor does.
1979    let saved_footnotes = parser.take_footnotes();
1980
1981    // A block title carried over from a section heading (see
1982    // `SectionBlock::parse`) must not cross this nested-document boundary in
1983    // either direction: a title left pending by the cell — e.g. a trailing
1984    // titled empty section — must not leak out and be claimed by the enclosing
1985    // document's next block, and (defensively) any parent-pending title must
1986    // not be claimed by the cell's first block. Reset it to `None` for the cell
1987    // and restore the parent's value afterward, like the footnote registry.
1988    let saved_pending_block_title = parser.pending_block_title.take();
1989
1990    // Mark that we are inside an AsciiDoc cell (a nested document) for the
1991    // duration of the parse, so a table found within defaults its cell separator
1992    // to `!` rather than `|` (matching Asciidoctor's `Document#nested?`).
1993    parser.nested_document_depth += 1;
1994    // The cell body parses from its own owned source (whether include-expanded or
1995    // a borrowed `a|` cell), whose offsets do not map to the document. Mark that
1996    // so a footnote defined inside records no (misleading) document location; see
1997    // `Parser::owned_subsource_depth`.
1998    parser.owned_subsource_depth += 1;
1999    let mut maw = parse_blocks_until(body, |_, _| false, parser);
2000    parser.owned_subsource_depth -= 1;
2001    parser.nested_document_depth -= 1;
2002    warnings.append(&mut maw.warnings);
2003
2004    parser.pending_block_title = saved_pending_block_title;
2005    parser.restore_footnotes(saved_footnotes);
2006
2007    let inline = matches!(
2008        parser.attribute_value("doctype"),
2009        InterpretedValue::Value(ref v) if v == "inline"
2010    );
2011
2012    let title = if parser.resolve_show_title(true) {
2013        title_source.map(|span| {
2014            let mut content = Content::from(span);
2015            SubstitutionGroup::Header.apply(&mut content, parser, None);
2016            content.rendered().to_string()
2017        })
2018    } else {
2019        None
2020    };
2021
2022    // The cell is its own standalone document, so its table-of-contents
2023    // configuration comes from the cell's own `toc` family of attributes (which
2024    // it does not inherit from the parent). Resolve it here, before the caller
2025    // restores the parent's attribute snapshot.
2026    let toc = TocConfig::from_parser(parser);
2027
2028    // Snapshot the cell's resolved attribute state while the parser still holds
2029    // it (the caller restores the parent's snapshot immediately after this
2030    // returns). The snapshot shares the parser's attribute tables by `Arc`, so
2031    // it is cheap. It lets a caller introspect the nested cell document —
2032    // including the attributes it inherited from the parent — the same way the
2033    // top-level `Document` exposes its own.
2034    let mut attributes = parser.snapshot_attributes();
2035
2036    // Materialize the cell's derived `toc-position` / `toc-placement` /
2037    // `toc-class` attributes into its snapshot, so it exposes them the same way
2038    // the top-level `Document` does — without mutating the parser (whose
2039    // attribute state the caller restores to the parent's on return anyway).
2040    attributes.materialize_toc_attributes(toc.mode);
2041
2042    (title, inline, toc, maw.item.item, attributes)
2043}
2044
2045/// Returns `true` when the cell content holds an `include::` preprocessor
2046/// directive at the start of a line, which must be expanded before the cell is
2047/// parsed.
2048fn content_has_directive(content: &str) -> bool {
2049    content.starts_with("include::") || content.contains("\ninclude::")
2050}
2051
2052/// A row of cells in a [`TableBlock`].
2053#[derive(Clone, Debug, Eq, PartialEq)]
2054pub struct TableRow<'src> {
2055    cells: Vec<TableCell<'src>>,
2056}
2057
2058impl<'src> TableRow<'src> {
2059    /// Returns the cells in this row.
2060    pub fn cells(&self) -> &[TableCell<'src>] {
2061        &self.cells
2062    }
2063}
2064
2065/// A single cell in a [`TableBlock`].
2066#[derive(Clone, Debug, Eq, PartialEq)]
2067pub struct TableCell<'src> {
2068    h_align: HorizontalAlignment,
2069    v_align: VerticalAlignment,
2070    style: ColumnStyle,
2071    colspan: usize,
2072    rowspan: usize,
2073    content: TableCellContent<'src>,
2074    source: Span<'src>,
2075}
2076
2077impl<'src> TableCell<'src> {
2078    /// Build a cell from the raw (untrimmed) span of its content, processing it
2079    /// according to the [style](ColumnStyle) of the `column` the cell belongs
2080    /// to.
2081    ///
2082    /// The cell's horizontal and vertical alignment come from the alignment
2083    /// operators on its [specifier](RawCell::spec) when present; otherwise they
2084    /// are inherited from the column. Likewise, a style operator on the cell's
2085    /// specifier overrides the column's [style](ColumnStyle); with no cell
2086    /// style operator, the cell is processed with the column's style. A
2087    /// header cell (`is_header`) is always processed as plain header
2088    /// content, regardless of any style operator on the column or the cell, and
2089    /// it ignores the column's alignment operators: with no operator on its own
2090    /// specifier, a header cell falls back to the default alignment rather than
2091    /// inheriting the column's.
2092    ///
2093    /// Leading and trailing whitespace is always stripped. For every style but
2094    /// [`AsciiDoc`](ColumnStyle::AsciiDoc) the cell holds inline
2095    /// [`Content`](TableCellContent::Simple): escaped cell separators (the
2096    /// table's `separator` character preceded by a backslash, e.g. `\|`) are
2097    /// unescaped and substitutions are applied — the verbatim group for
2098    /// [`Literal`](ColumnStyle::Literal), the normal group otherwise. An
2099    /// [`AsciiDoc`](ColumnStyle::AsciiDoc) cell instead parses its content as a
2100    /// nested sequence of [blocks](TableCellContent::AsciiDoc).
2101    fn parse(
2102        raw: RawCell<'src>,
2103        column: &TableColumn,
2104        is_header: bool,
2105        separator: &str,
2106        parser: &mut Parser,
2107        warnings: &mut Vec<Warning<'src>>,
2108    ) -> Self {
2109        // A cell's own alignment operator overrides the column's alignment; with
2110        // no operator, the cell inherits the column's alignment. The header row
2111        // ignores alignment operators on the column specifier, so a header cell
2112        // with no operator of its own falls back to the default alignment rather
2113        // than the column's; a cell specifier's own operator is still applied.
2114        let (h_align, v_align) = if is_header {
2115            (
2116                raw.spec.h_align.unwrap_or(HorizontalAlignment::Left),
2117                raw.spec.v_align.unwrap_or(VerticalAlignment::Top),
2118            )
2119        } else {
2120            (
2121                raw.spec.h_align.unwrap_or(column.h_align),
2122                raw.spec.v_align.unwrap_or(column.v_align),
2123            )
2124        };
2125
2126        // A cell's own style operator overrides the column's style; with no
2127        // operator, the cell is processed with the column's style. The header
2128        // row is always processed as plain header content, so neither a column
2129        // nor a cell style operator ever affects a header cell.
2130        let style = if is_header {
2131            ColumnStyle::Default
2132        } else {
2133            raw.spec.style.unwrap_or(column.style)
2134        };
2135
2136        let trimmed = trim_cell_content(raw.content, style);
2137
2138        // An escaped cell separator (a backslash in front of the table's
2139        // separator, e.g. `\|` or `\!`) is unescaped to the bare separator. Only
2140        // the active separator is unescaped, so a `\|` in a `!`-separated table
2141        // is left untouched. The replacement is computed only for the inline
2142        // styles; an AsciiDoc cell parses its content verbatim (see
2143        // [`process_content`]).
2144        let escaped = format!("\\{separator}");
2145        let replacement = if style != ColumnStyle::AsciiDoc && trimmed.data().contains(&escaped) {
2146            Some(trimmed.data().replace(&escaped, separator))
2147        } else {
2148            None
2149        };
2150
2151        let content = process_content(trimmed, replacement, style, parser, warnings);
2152
2153        Self {
2154            h_align,
2155            v_align,
2156            style,
2157            colspan: raw.spec.colspan.max(1),
2158            rowspan: raw.spec.rowspan.max(1),
2159            content,
2160            // The cell's source begins at its content, immediately after the
2161            // separator (before any trimming), so the cell's reported line is
2162            // the separator's line.
2163            source: raw.content,
2164        }
2165    }
2166
2167    /// Build a cell from a [data field](DataField) of a delimiter-separated
2168    /// table (CSV, TSV, or DSV).
2169    ///
2170    /// Unlike a PSV cell, a data cell carries no per-cell specifier: its
2171    /// alignment and [style](ColumnStyle) come entirely from the `column`, and
2172    /// it always spans a single row and column. The separator escaping is
2173    /// handled by the format parser before this point, so the field already
2174    /// holds the extracted value (its [`replacement`](DataField::replacement),
2175    /// when present, is the value after quote/escape processing). A header cell
2176    /// (`is_header`) is processed as plain header content.
2177    fn parse_data(
2178        field: DataField<'src>,
2179        column: &TableColumn,
2180        is_header: bool,
2181        parser: &mut Parser,
2182        warnings: &mut Vec<Warning<'src>>,
2183    ) -> Self {
2184        let style = if is_header {
2185            ColumnStyle::Default
2186        } else {
2187            column.style
2188        };
2189
2190        // A data field carries no cell specifier, so its alignment comes from the
2191        // column — except in the header row, which ignores the column's alignment
2192        // operators and falls back to the default alignment.
2193        let (h_align, v_align) = if is_header {
2194            (HorizontalAlignment::Left, VerticalAlignment::Top)
2195        } else {
2196            (column.h_align, column.v_align)
2197        };
2198
2199        let source = field.content;
2200        let content = process_content(field.content, field.replacement, style, parser, warnings);
2201
2202        Self {
2203            h_align,
2204            v_align,
2205            style,
2206            colspan: 1,
2207            rowspan: 1,
2208            content,
2209            source,
2210        }
2211    }
2212
2213    /// Returns the horizontal alignment of this cell's content.
2214    ///
2215    /// The alignment comes from a horizontal alignment operator (`<`, `>`, or
2216    /// `^`) on the cell's specifier, which overrides the column's alignment. A
2217    /// cell with no horizontal alignment operator inherits its column's
2218    /// [`h_align`](TableColumn::h_align).
2219    pub fn h_align(&self) -> HorizontalAlignment {
2220        self.h_align
2221    }
2222
2223    /// Returns the vertical alignment of this cell's content.
2224    ///
2225    /// The alignment comes from a vertical alignment operator (`.<`, `.>`, or
2226    /// `.^`) on the cell's specifier, which overrides the column's alignment. A
2227    /// cell with no vertical alignment operator inherits its column's
2228    /// [`v_align`](TableColumn::v_align).
2229    pub fn v_align(&self) -> VerticalAlignment {
2230        self.v_align
2231    }
2232
2233    /// Returns the [style](ColumnStyle) applied to this cell's content.
2234    ///
2235    /// The style comes from a style operator in the last position of the cell's
2236    /// specifier (`a`, `d`, `e`, `h`, `l`, `m`, or `s`), which overrides the
2237    /// column's style. A cell with no style operator inherits its column's
2238    /// [`style`](TableColumn::style). A header cell is always
2239    /// [`Default`](ColumnStyle::Default), because the header row ignores style
2240    /// operators on both column and cell specifiers.
2241    pub fn style(&self) -> ColumnStyle {
2242        self.style
2243    }
2244
2245    /// Returns the number of columns this cell spans.
2246    ///
2247    /// The span comes from a column span factor (`<n>`) or block span factor
2248    /// (`<n>.<n>`) in front of the span operator (`+`) on the cell's specifier.
2249    /// A cell with no column span factor spans a single column, so the default
2250    /// is `1`.
2251    pub fn colspan(&self) -> usize {
2252        self.colspan
2253    }
2254
2255    /// Returns the number of rows this cell spans.
2256    ///
2257    /// The span comes from a row span factor (`.<n>`) or block span factor
2258    /// (`<n>.<n>`) in front of the span operator (`+`) on the cell's specifier.
2259    /// A cell with no row span factor spans a single row, so the default is
2260    /// `1`.
2261    pub fn rowspan(&self) -> usize {
2262        self.rowspan
2263    }
2264
2265    /// Returns the interpreted content of this cell.
2266    pub fn content(&self) -> &TableCellContent<'src> {
2267        &self.content
2268    }
2269
2270    /// Resolves any deferred cross-references in this cell's content.
2271    fn resolve_references(
2272        &mut self,
2273        resolver: &dyn ReferenceResolver,
2274        renderer: &dyn InlineSubstitutionRenderer,
2275        warnings: &mut ReferenceWarnings<'src>,
2276    ) {
2277        let source = self.source;
2278
2279        match &mut self.content {
2280            TableCellContent::Simple(content) => {
2281                content.resolve_references(resolver, renderer, warnings);
2282            }
2283            TableCellContent::AsciiDoc(cell) => {
2284                cell.resolve_references(resolver, renderer, warnings, source);
2285            }
2286        }
2287    }
2288}
2289
2290impl<'src> HasSpan<'src> for TableCell<'src> {
2291    /// Returns the cell's source span, which begins at the cell's content
2292    /// immediately after its separator. Its [line](Span::line) is therefore the
2293    /// line on which the cell starts.
2294    fn span(&self) -> Span<'src> {
2295        self.source
2296    }
2297}
2298
2299/// The interpreted content of a [`TableCell`].
2300///
2301/// The variant is determined by the [style](ColumnStyle) of the cell's column:
2302/// an [`AsciiDoc`](ColumnStyle::AsciiDoc) column produces
2303/// [`AsciiDoc`](Self::AsciiDoc) content, and every other style produces
2304/// [`Simple`](Self::Simple) inline content.
2305#[derive(Clone, Debug, Eq, PartialEq)]
2306pub enum TableCellContent<'src> {
2307    /// Inline content: the cell's text after its substitutions (normal for most
2308    /// styles, verbatim for [`Literal`](ColumnStyle::Literal)) have been
2309    /// applied.
2310    Simple(Content<'src>),
2311
2312    /// Block content: the cell's text parsed as a nested, standalone AsciiDoc
2313    /// document. Produced by the [`AsciiDoc`](ColumnStyle::AsciiDoc) style.
2314    AsciiDoc(AsciiDocCell<'src>),
2315}
2316
2317/// The content of an [`AsciiDoc`](TableCellContent::AsciiDoc) table cell: a
2318/// nested, standalone AsciiDoc document.
2319///
2320/// Because the cell behaves like its own document, a few render-time decisions
2321/// depend on attribute state that is scoped to the cell and gone by the time
2322/// the document is rendered. They are therefore resolved while the cell is
2323/// parsed and captured here: whether the cell's nested document title is shown
2324/// (and its rendered text), and whether the cell's `doctype` is `inline` (in
2325/// which case a lone paragraph renders without the usual block wrapper).
2326///
2327/// A cell whose content has no preprocessor directives is parsed in place from
2328/// the parent document's source ([`Borrowed`](Self::Borrowed)). A cell that
2329/// expands an `include::` directive owns its preprocessed source
2330/// ([`Owned`](Self::Owned)); the owned store is shared behind an [`Arc`] so the
2331/// cell stays cheaply cloneable.
2332#[derive(Clone, Debug, Eq, PartialEq)]
2333pub enum AsciiDocCell<'src> {
2334    /// Parsed in place from the parent document's source.
2335    Borrowed(BorrowedCell<'src>),
2336
2337    /// Parsed from an owned, include-expanded source the cell carries.
2338    Owned(Arc<OwnedCell>),
2339}
2340
2341impl<'src> AsciiDocCell<'src> {
2342    /// Returns the cell's nested-document title, rendered to its display text.
2343    ///
2344    /// This is `Some` only when the cell began with a level-0 title line
2345    /// (`= Title`) *and* the cell's effective `showtitle`/`notitle` state means
2346    /// that title is shown; otherwise it is `None`.
2347    pub fn title(&self) -> Option<&str> {
2348        match self {
2349            Self::Borrowed(cell) => cell.title.as_deref(),
2350            Self::Owned(cell) => cell.borrow_dependent().title.as_deref(),
2351        }
2352    }
2353
2354    /// Returns `true` when the cell's `doctype` resolves to `inline`.
2355    ///
2356    /// An `inline` document renders a lone paragraph as bare inline content,
2357    /// without the enclosing block wrapper.
2358    pub fn is_inline(&self) -> bool {
2359        match self {
2360            Self::Borrowed(cell) => cell.inline,
2361            Self::Owned(cell) => cell.borrow_dependent().inline,
2362        }
2363    }
2364
2365    /// Returns where (and whether) the cell's table of contents is generated.
2366    ///
2367    /// The cell is a standalone nested document, so this is resolved from the
2368    /// cell's own `toc` attribute and is independent of the parent document's
2369    /// setting.
2370    pub fn toc_mode(&self) -> TocMode {
2371        self.toc().mode
2372    }
2373
2374    /// Returns the depth of section levels included in the cell's table of
2375    /// contents, resolved from the cell's own `toclevels` attribute (default
2376    /// `2`).
2377    pub fn toc_levels(&self) -> usize {
2378        self.toc().levels
2379    }
2380
2381    /// Returns the title of the cell's table of contents, resolved from the
2382    /// cell's own `toc-title` attribute (default _Table of Contents_).
2383    pub fn toc_title(&self) -> &str {
2384        &self.toc().title
2385    }
2386
2387    /// Returns the CSS class applied to the cell's table-of-contents container,
2388    /// resolved from the cell's own `toc-class` attribute (default `toc`).
2389    pub fn toc_class(&self) -> &str {
2390        &self.toc().class
2391    }
2392
2393    /// Returns the resolved table-of-contents configuration for the cell.
2394    pub(crate) fn toc(&self) -> &TocConfig {
2395        match self {
2396            Self::Borrowed(cell) => &cell.toc,
2397            Self::Owned(cell) => &cell.borrow_dependent().toc,
2398        }
2399    }
2400
2401    /// Returns the blocks parsed from the cell's content.
2402    pub fn blocks(&self) -> &[Block<'_>] {
2403        match self {
2404            Self::Borrowed(cell) => &cell.blocks,
2405            Self::Owned(cell) => &cell.borrow_dependent().blocks,
2406        }
2407    }
2408
2409    /// Returns `true` because an AsciiDoc table cell is always a nested,
2410    /// standalone document.
2411    ///
2412    /// This mirrors Asciidoctor's `Document#nested?`, which is `true` for the
2413    /// document parsed from an AsciiDoc (`a`) cell and `false` for a top-level
2414    /// document. It is provided so a caller that has navigated to the cell can
2415    /// confirm it is introspecting a nested document (see also
2416    /// [`attribute_value`](Self::attribute_value) and its siblings, which
2417    /// expose the attributes the cell inherited from its parent).
2418    pub fn is_nested(&self) -> bool {
2419        true
2420    }
2421
2422    /// Returns the resolved interpreted value of the named document attribute
2423    /// as the cell's nested document saw it.
2424    ///
2425    /// The cell inherits the parent document's attributes, so this reports an
2426    /// inherited value (such as a directory option the parent was configured
2427    /// with) as well as any attribute the cell body set for itself. It mirrors
2428    /// [`Document::attribute_value`](crate::Document::attribute_value) exactly,
2429    /// resolving the cell's introspectable attribute state the same way the
2430    /// top-level document resolves its own.
2431    pub fn attribute_value<N: AsRef<str>>(&self, name: N) -> InterpretedValue {
2432        self.attributes().attribute_value(name)
2433    }
2434
2435    /// Returns `true` if the cell's nested document has a document attribute by
2436    /// this name (whether or not it is set).
2437    ///
2438    /// Mirrors [`Document::has_attribute`](crate::Document::has_attribute).
2439    pub fn has_attribute<N: AsRef<str>>(&self, name: N) -> bool {
2440        self.attributes().has_attribute(name)
2441    }
2442
2443    /// Returns `true` if the cell's nested document has a document attribute by
2444    /// this name and it is set (i.e. not unset).
2445    ///
2446    /// Mirrors [`Document::is_attribute_set`](crate::Document::is_attribute_set).
2447    pub fn is_attribute_set<N: AsRef<str>>(&self, name: N) -> bool {
2448        self.attributes().is_attribute_set(name)
2449    }
2450
2451    /// Returns the snapshot of the cell's resolved attribute state.
2452    fn attributes(&self) -> &ResolvedAttributes {
2453        match self {
2454            Self::Borrowed(cell) => &cell.attributes,
2455            Self::Owned(cell) => &cell.borrow_dependent().attributes,
2456        }
2457    }
2458
2459    /// Resolves any deferred cross-references in the cell's blocks. `source` is
2460    /// the enclosing cell's span, used to anchor warnings raised from an
2461    /// [owned](Self::Owned) cell's private source.
2462    fn resolve_references(
2463        &mut self,
2464        resolver: &dyn ReferenceResolver,
2465        renderer: &dyn InlineSubstitutionRenderer,
2466        warnings: &mut ReferenceWarnings<'src>,
2467        source: Span<'src>,
2468    ) {
2469        match self {
2470            Self::Borrowed(cell) => {
2471                for block in &mut cell.blocks {
2472                    block.resolve_references(resolver, renderer, warnings);
2473                }
2474            }
2475
2476            // The owned store is shared behind an `Arc`, but references are
2477            // resolved immediately after parsing while the cell is still its sole
2478            // owner, so `get_mut` succeeds.
2479            Self::Owned(cell) => {
2480                if let Some(cell) = Arc::get_mut(cell) {
2481                    cell.with_dependent_mut(|_, dependent| {
2482                        // These blocks borrow the cell's own owned source, so
2483                        // their warnings are collected separately and then
2484                        // re-anchored to the cell's span in the document.
2485                        let mut owned_warnings = ReferenceWarnings::default();
2486
2487                        for block in &mut dependent.blocks {
2488                            block.resolve_references(resolver, renderer, &mut owned_warnings);
2489                        }
2490
2491                        owned_warnings.rehome_into(warnings, source);
2492                    });
2493                }
2494            }
2495        }
2496    }
2497}
2498
2499/// An [`AsciiDoc`](TableCellContent::AsciiDoc) cell parsed in place from the
2500/// parent document's source.
2501#[derive(Clone, Debug, Eq, PartialEq)]
2502pub struct BorrowedCell<'src> {
2503    title: Option<String>,
2504    inline: bool,
2505    toc: TocConfig,
2506    blocks: Vec<Block<'src>>,
2507    attributes: ResolvedAttributes,
2508}
2509
2510self_cell! {
2511    /// An [`AsciiDoc`](TableCellContent::AsciiDoc) cell that owns its
2512    /// (include-expanded) source, with the parsed blocks borrowing from it.
2513    pub struct OwnedCell {
2514        owner: String,
2515
2516        #[covariant]
2517        dependent: OwnedCellInner,
2518    }
2519
2520    impl {Debug, Eq, PartialEq}
2521}
2522
2523/// The parsed contents of an [`OwnedCell`], borrowing its owned source.
2524#[derive(Debug, Eq, PartialEq)]
2525struct OwnedCellInner<'src> {
2526    title: Option<String>,
2527    inline: bool,
2528    toc: TocConfig,
2529    blocks: Vec<Block<'src>>,
2530    attributes: ResolvedAttributes,
2531}
2532
2533/// Parse the value of the `cols` attribute into a list of columns, mirroring
2534/// Asciidoctor's `parse_colspecs`.
2535///
2536/// All spaces are first removed from the value. A wholly blank value yields no
2537/// columns (the caller then takes the column count from the first row), and a
2538/// lone integer (the deprecated `cols="3"` form) yields that many default
2539/// columns. Otherwise the value is a list of column specifiers separated by
2540/// commas, or by semicolons when no comma is present. An empty record (e.g. the
2541/// trailing field of `cols="1,,1"`) contributes a default column, and a
2542/// specifier may be preceded by a multiplier (`<n>*`) that repeats the column
2543/// `n` times. Each specifier's alignment operators, proportional width, and
2544/// [style operator](parse_col_spec) are interpreted.
2545fn parse_cols(value: &str) -> Vec<TableColumn> {
2546    // Asciidoctor strips every space from the cols value before parsing, so
2547    // `cols=" 1, 1 "` is equivalent to `cols="1,1"`.
2548    let records: String = value.chars().filter(|c| !c.is_whitespace()).collect();
2549
2550    // A wholly blank cols value is ignored: the caller falls back to the column
2551    // count of the first row.
2552    if records.is_empty() {
2553        return vec![];
2554    }
2555
2556    // Deprecated single-integer form: `cols=3` is equivalent to `cols="3*"` and
2557    // produces that many equally sized columns.
2558    if let Ok(count) = records.parse::<usize>() {
2559        return vec![TableColumn::default(); count];
2560    }
2561
2562    // Split on commas when present, otherwise on semicolons (Asciidoctor accepts
2563    // either as the column-spec separator, but not a mix). Empty records are
2564    // kept: each one contributes a default column.
2565    let parts: Vec<&str> = if records.contains(',') {
2566        records.split(',').collect()
2567    } else {
2568        records.split(';').collect()
2569    };
2570
2571    let mut columns: Vec<TableColumn> = vec![];
2572    for part in parts {
2573        if part.is_empty() {
2574            columns.push(TableColumn::default());
2575        } else if let Some((count, spec)) = part.split_once('*') {
2576            let repeat = count.parse::<usize>().unwrap_or(1).max(1);
2577            let column = parse_col_spec(spec);
2578            for _ in 0..repeat {
2579                columns.push(column.clone());
2580            }
2581        } else {
2582            columns.push(parse_col_spec(part));
2583        }
2584    }
2585
2586    columns
2587}
2588
2589/// Parse a single column specifier, extracting its alignment, proportional
2590/// width, and style.
2591///
2592/// A column specifier is positional: an optional horizontal alignment operator
2593/// (`<`, `>`, or `^`) comes first, followed by an optional vertical alignment
2594/// operator (`.<`, `.>`, or `.^`), followed by the width, and finally an
2595/// optional style operator in the last position. When a multiplier (`<n>*`) is
2596/// present, the operators follow the multiplier, so the `spec` passed here is
2597/// the portion after the `*`.
2598///
2599/// The width is either the special autowidth value `~` (sizing the column to
2600/// its content) or the first contiguous run of digits after any alignment
2601/// operators; a spec with neither falls back to the default width. The style
2602/// operator is the trailing letter (`a`, `d`, `e`, `h`, `l`, `m`, or `s`); an
2603/// unrecognized trailing letter leaves the style at its default.
2604fn parse_col_spec(spec: &str) -> TableColumn {
2605    let mut rest = spec.trim();
2606
2607    // Horizontal alignment operator (if present) always comes first.
2608    let mut h_align = HorizontalAlignment::Left;
2609    match rest.as_bytes().first() {
2610        Some(b'<') => {
2611            h_align = HorizontalAlignment::Left;
2612            rest = &rest[1..];
2613        }
2614
2615        Some(b'>') => {
2616            h_align = HorizontalAlignment::Right;
2617            rest = &rest[1..];
2618        }
2619
2620        Some(b'^') => {
2621            h_align = HorizontalAlignment::Center;
2622            rest = &rest[1..];
2623        }
2624
2625        _ => {}
2626    }
2627
2628    // Vertical alignment operator (if present) follows, introduced by a dot.
2629    let mut v_align = VerticalAlignment::Top;
2630    if let Some(after_dot) = rest.strip_prefix('.') {
2631        match after_dot.as_bytes().first() {
2632            Some(b'<') => {
2633                v_align = VerticalAlignment::Top;
2634                rest = &after_dot[1..];
2635            }
2636
2637            Some(b'>') => {
2638                v_align = VerticalAlignment::Bottom;
2639                rest = &after_dot[1..];
2640            }
2641
2642            Some(b'^') => {
2643                v_align = VerticalAlignment::Middle;
2644                rest = &after_dot[1..];
2645            }
2646
2647            _ => {}
2648        }
2649    }
2650
2651    // Width comes after the alignment operators. The special value `~` marks
2652    // the column as autowidth (sized to its content); otherwise the width is
2653    // the first run of digits. A spec with neither falls back to the default
2654    // proportional width.
2655    let mut autowidth = false;
2656    let mut width = TableColumn::default().width;
2657    if let Some(after_tilde) = rest.strip_prefix('~') {
2658        autowidth = true;
2659        rest = after_tilde;
2660    } else {
2661        let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
2662        if let Ok(parsed) = digits.parse::<usize>()
2663            && parsed > 0
2664        {
2665            width = parsed;
2666        }
2667        rest = &rest[digits.len()..];
2668    }
2669
2670    // The style operator, if present, occupies the last position on the
2671    // specifier, so it is the entire remainder after the width. Matching the
2672    // whole remainder (rather than just its first byte) means a malformed spec
2673    // with trailing junk — e.g. `1em` — falls back to the default style instead
2674    // of silently honoring the first letter and discarding the rest.
2675    let style = match rest.trim() {
2676        "a" => ColumnStyle::AsciiDoc,
2677        "d" => ColumnStyle::Default,
2678        "e" => ColumnStyle::Emphasis,
2679        "h" => ColumnStyle::Header,
2680        "l" => ColumnStyle::Literal,
2681        "m" => ColumnStyle::Monospace,
2682        "s" => ColumnStyle::Strong,
2683        _ => ColumnStyle::Default,
2684    };
2685
2686    TableColumn {
2687        width,
2688        autowidth,
2689        h_align,
2690        v_align,
2691        style,
2692    }
2693}
2694
2695/// The span, alignment, and style overrides parsed from a
2696/// [cell specifier](RawCell::spec).
2697///
2698/// Each alignment and style field is `None` when the corresponding operator is
2699/// absent from the specifier, in which case the cell inherits that alignment
2700/// (or style) from its column. `colspan` and `rowspan` are the number of
2701/// columns and rows the cell spans; they default to `1` (no span). `repeat` is
2702/// the duplication factor — the number of consecutive cells the content is
2703/// cloned into — and defaults to `1` (no duplication).
2704#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2705struct CellSpec {
2706    h_align: Option<HorizontalAlignment>,
2707    v_align: Option<VerticalAlignment>,
2708    style: Option<ColumnStyle>,
2709    colspan: usize,
2710    rowspan: usize,
2711    repeat: usize,
2712}
2713
2714impl Default for CellSpec {
2715    fn default() -> Self {
2716        Self {
2717            h_align: None,
2718            v_align: None,
2719            style: None,
2720            colspan: 1,
2721            rowspan: 1,
2722            repeat: 1,
2723        }
2724    }
2725}
2726
2727/// A single PSV cell as located by [`scan_cells`]: the alignment operators from
2728/// its specifier together with the raw (untrimmed) span of its content.
2729#[derive(Clone, Copy)]
2730struct RawCell<'src> {
2731    spec: CellSpec,
2732    content: Span<'src>,
2733}
2734
2735/// The largest number of cells a single duplication factor (`<n>*`) is allowed
2736/// to expand into.
2737///
2738/// A duplicated cell is materialized as `<n>` independent cells, so the factor
2739/// is an amplification: a dozen source bytes such as `1000000000*` would
2740/// otherwise request a billion `RawCell`s (a multi-gigabyte allocation).
2741/// Capping the per-specifier factor bounds that amplification while leaving any
2742/// realistic table — which never duplicates a cell more than a handful of times
2743/// — untouched. (This is the one point where the implementation diverges from
2744/// Asciidoctor, which expands the literal factor however large.)
2745const MAX_DUPLICATION_FACTOR: usize = 1_000;
2746
2747/// Expand each duplicated cell into the `<n>` independent cells it represents.
2748///
2749/// A cell specifier with a duplication factor (`<n>*`) clones the cell's
2750/// content and properties into `<n>` consecutive cells. Each clone is an
2751/// ordinary single-slot cell (colspan and rowspan of 1), so expanding here —
2752/// before the grid is walked — lets the clones flow into rows exactly like
2753/// cells the author typed out by hand. A duplication factor of zero produces no
2754/// cells, dropping the original (matching Asciidoctor). A cell with no
2755/// duplication factor has a `repeat` of 1 and so passes through unchanged. The
2756/// factor is clamped to [`MAX_DUPLICATION_FACTOR`] so a hostile specifier can't
2757/// trigger a runaway allocation.
2758fn expand_duplicates(cells: Vec<RawCell<'_>>) -> Vec<RawCell<'_>> {
2759    // The common case is no duplication at all, so only the clones beyond the
2760    // first add to the count.
2761    let extra: usize = cells
2762        .iter()
2763        .map(|c| c.spec.repeat.min(MAX_DUPLICATION_FACTOR).saturating_sub(1))
2764        .sum();
2765
2766    let mut expanded = Vec::with_capacity(cells.len() + extra);
2767    for cell in cells {
2768        for _ in 0..cell.spec.repeat.min(MAX_DUPLICATION_FACTOR) {
2769            expanded.push(cell);
2770        }
2771    }
2772
2773    expanded
2774}
2775
2776/// Scan a region for PSV cell boundaries, returning the [specifier](CellSpec)
2777/// and raw (untrimmed) content span of each cell.
2778///
2779/// Every unescaped occurrence of the table's `separator` (the vertical bar
2780/// (`|`) by default, the exclamation mark (`!`) for a nested table, or any
2781/// string set with the `separator` attribute, e.g. the broken bar `¦`) is a
2782/// cell boundary, matching Asciidoctor. The token immediately preceding a
2783/// separator is treated as that cell's [specifier](CellSpec) (e.g. `^`, `2+`,
2784/// `.>`) only when it parses as one (see [`parse_cell_spec`]) *and* is anchored
2785/// at the line start or preceded by whitespace; otherwise the token is ordinary
2786/// content of the preceding cell and the separator is a plain boundary (so the
2787/// `a` in `|a|b` is content, not a style operator). Content before the first
2788/// boundary is ignored.
2789///
2790/// A separator immediately preceded by a backslash (e.g. `\|`) is escaped: it
2791/// is literal content rather than a boundary, and the backslash is stripped
2792/// later in [`TableCell::parse`]. Only the single byte before the separator is
2793/// inspected, so `\\|` is also read as an escaped separator — matching
2794/// Asciidoctor, whose check is likewise the single-character
2795/// `pre_match.end_with? '\'`.
2796fn scan_cells<'src>(
2797    region: Span<'src>,
2798    separator: &str,
2799) -> (Vec<RawCell<'src>>, Option<Span<'src>>) {
2800    let data = region.data();
2801    let bytes = data.as_bytes();
2802    let len = bytes.len();
2803
2804    // A zero-length separator would never advance; treat it as a single byte to
2805    // stay safe. (The resolver never produces an empty separator.)
2806    let sep_len = separator.len().max(1);
2807
2808    let mut cells: Vec<RawCell<'src>> = vec![];
2809
2810    // The content start and specifier of the cell currently being accumulated.
2811    let mut content_start: Option<usize> = None;
2812
2813    let mut cur_spec = CellSpec::default();
2814
2815    // The span of a cell recovered from content that precedes the first
2816    // separator (see below); `Some` drives a missing-leading-separator warning.
2817    let mut recovered: Option<Span<'src>> = None;
2818
2819    let mut i = 0;
2820    while i < len {
2821        if data
2822            .get(i..)
2823            .is_some_and(|rest| rest.starts_with(separator))
2824        {
2825            // A separator immediately preceded by a backslash is escaped: it is
2826            // literal content, not a cell boundary. The backslash is stripped
2827            // from the rendered cell later (see `TableCell::parse`).
2828            if i > 0 && bytes.get(i - 1).copied() == Some(b'\\') {
2829                i += sep_len;
2830                continue;
2831            }
2832
2833            // Walk back to the start of the token directly preceding this
2834            // separator. The token (a possible cell specifier) runs back to the
2835            // previous whitespace, tab, or newline, or to the start of the
2836            // region.
2837            let mut tok_start = i;
2838            while tok_start > 0
2839                && !matches!(
2840                    bytes.get(tok_start - 1).copied(),
2841                    Some(b' ' | b'\t' | b'\n')
2842                )
2843            {
2844                tok_start -= 1;
2845            }
2846
2847            let token = data.get(tok_start..i).unwrap_or_default();
2848            let spec = if token.is_empty() {
2849                Some(CellSpec::default())
2850            } else {
2851                parse_cell_spec(token)
2852            };
2853
2854            // Every unescaped separator is a cell boundary (matching
2855            // Asciidoctor). When the token is empty or a valid specifier it
2856            // belongs to the *next* cell, so the previous cell's content ends
2857            // before the token. Otherwise the token is ordinary content of the
2858            // previous cell (e.g. the `a` in `|a|b`, where `a` is not preceded
2859            // by whitespace and so is not a specifier), the separator is plain,
2860            // and the next cell takes the default specifier.
2861            let (content_end, next_spec) = match spec {
2862                Some(spec) => (tok_start, spec),
2863                None => (i, CellSpec::default()),
2864            };
2865
2866            match content_start {
2867                Some(start) => {
2868                    // The separating whitespace, included in the slice, is
2869                    // trimmed later in `TableCell::parse`.
2870                    cells.push(RawCell {
2871                        spec: cur_spec,
2872                        content: region.slice(start..content_end),
2873                    });
2874                }
2875
2876                None => {
2877                    // No cell has been opened yet, so this is the table's first
2878                    // separator. Non-blank content in front of it means the first
2879                    // cell is missing its leading separator; recover that content
2880                    // as the first cell (with the default specifier) and record
2881                    // its span so the caller can warn, matching Asciidoctor.
2882                    let leading = region.slice(0..content_end);
2883                    if !leading.data().trim().is_empty() {
2884                        cells.push(RawCell {
2885                            spec: CellSpec::default(),
2886                            content: leading,
2887                        });
2888                        recovered = Some(leading);
2889                    }
2890                }
2891            }
2892
2893            cur_spec = next_spec;
2894            content_start = Some(i + sep_len);
2895            i += sep_len;
2896            continue;
2897        }
2898
2899        i += 1;
2900    }
2901
2902    if let Some(start) = content_start {
2903        cells.push(RawCell {
2904            spec: cur_spec,
2905            content: region.slice(start..len),
2906        });
2907    }
2908
2909    (cells, recovered)
2910}
2911
2912/// Parse a cell specifier, returning its [span and overrides](CellSpec), or
2913/// `None` if `token` is not a valid cell specifier.
2914///
2915/// A cell specifier is positional and every part is optional, but the whole
2916/// token must be consumed for it to be valid:
2917///
2918/// ```text
2919/// <factor><span or duplication operator><horizontal><vertical><style>
2920/// ```
2921///
2922/// * The factor and span/duplication operator are an optional count (e.g. `2`,
2923///   `2.3`, `.3`) that, when present, must be followed by `+` (span) or `*`
2924///   (duplication). For a span the factor is interpreted as the cell's colspan
2925///   and rowspan (a missing column or row count defaults to 1). For a
2926///   duplication the column part of the factor is the duplication count — the
2927///   number of consecutive cells the content is cloned into — and any row part
2928///   is ignored; a duplicated cell keeps a colspan and rowspan of 1.
2929/// * The horizontal alignment operator is `<`, `>`, or `^`.
2930/// * The vertical alignment operator is a dot followed by `<`, `>`, or `^`.
2931/// * The style operator is a single lowercase letter in the last position. A
2932///   recognized operator (`a`, `d`, `e`, `h`, `l`, `m`, or `s`) overrides the
2933///   column's style on this cell. Any other single lowercase letter still
2934///   locates the separator but leaves the style at `None`, so the cell inherits
2935///   its column's style (matching Asciidoctor, which ignores an unrecognized
2936///   style operator).
2937fn parse_cell_spec(token: &str) -> Option<CellSpec> {
2938    let b = token.as_bytes();
2939    let mut i = 0;
2940
2941    // Optional span/duplication: an optional span factor followed by `+` (span)
2942    // or `*` (duplication). The factor is a column count, an optional dot, and an
2943    // optional row count (`<n>`, `.<n>`, or `<n>.<n>`). The factor is committed
2944    // only when the operator that must follow it is present; otherwise the
2945    // leading digits remain and the token fails the full-consumption check below.
2946    let mut colspan = 1;
2947    let mut rowspan = 1;
2948    let mut repeat = 1;
2949    let col_start = i;
2950
2951    let mut j = i;
2952    while matches!(b.get(j).copied(), Some(c) if c.is_ascii_digit()) {
2953        j += 1;
2954    }
2955
2956    let col_end = j;
2957    let mut has_dot = false;
2958
2959    let mut row_start = j;
2960    if b.get(j).copied() == Some(b'.') {
2961        has_dot = true;
2962        j += 1;
2963        row_start = j;
2964        while matches!(b.get(j).copied(), Some(c) if c.is_ascii_digit()) {
2965            j += 1;
2966        }
2967    }
2968
2969    let row_end = j;
2970    match b.get(j).copied() {
2971        // Span: the factor is interpreted as a colspan and rowspan. A missing
2972        // column or row count defaults to 1, so `2+` spans two columns, `.3+`
2973        // spans three rows, and `2.3+` spans a 2x3 block.
2974        Some(b'+') => {
2975            // The factor consists only of ASCII digits and dots, so these ranges
2976            // are always valid `str` slices.
2977            let col_digits = token.get(col_start..col_end).unwrap_or_default();
2978            if !col_digits.is_empty() {
2979                colspan = col_digits.parse().unwrap_or(1);
2980            }
2981            if has_dot {
2982                let row_digits = token.get(row_start..row_end).unwrap_or_default();
2983                if !row_digits.is_empty() {
2984                    rowspan = row_digits.parse().unwrap_or(1);
2985                }
2986            }
2987            i = j + 1;
2988        }
2989
2990        // Duplication: the factor is interpreted as a duplication count, so the
2991        // cell's content and properties are cloned into `<n>` consecutive cells.
2992        // Only the column part of the factor is the count; any row part (`<n>.`)
2993        // is ignored, matching Asciidoctor. A missing column count defaults to 1.
2994        // Unlike a span, a duplication leaves `colspan` and `rowspan` at 1: each
2995        // clone is an ordinary single-slot cell.
2996        Some(b'*') => {
2997            let col_digits = token.get(col_start..col_end).unwrap_or_default();
2998            if !col_digits.is_empty() {
2999                repeat = col_digits.parse().unwrap_or(1);
3000            }
3001            i = j + 1;
3002        }
3003
3004        _ => {}
3005    }
3006
3007    // Optional horizontal alignment operator.
3008    let mut h_align = None;
3009    match b.get(i).copied() {
3010        Some(b'<') => {
3011            h_align = Some(HorizontalAlignment::Left);
3012            i += 1;
3013        }
3014
3015        Some(b'>') => {
3016            h_align = Some(HorizontalAlignment::Right);
3017            i += 1;
3018        }
3019
3020        Some(b'^') => {
3021            h_align = Some(HorizontalAlignment::Center);
3022            i += 1;
3023        }
3024
3025        _ => {}
3026    }
3027
3028    // Optional vertical alignment operator, introduced by a dot.
3029    let mut v_align = None;
3030    if b.get(i).copied() == Some(b'.') {
3031        match b.get(i + 1).copied() {
3032            Some(b'<') => {
3033                v_align = Some(VerticalAlignment::Top);
3034                i += 2;
3035            }
3036
3037            Some(b'>') => {
3038                v_align = Some(VerticalAlignment::Bottom);
3039                i += 2;
3040            }
3041
3042            Some(b'^') => {
3043                v_align = Some(VerticalAlignment::Middle);
3044                i += 2;
3045            }
3046
3047            _ => {}
3048        }
3049    }
3050
3051    // Optional style operator: a single lowercase letter in the last position.
3052    // A recognized letter overrides the column's style; any other lowercase
3053    // letter is consumed (so the separator is still located) but leaves the
3054    // style at `None`, so the cell inherits its column's style.
3055    let mut style = None;
3056    if let Some(c) = b.get(i).copied()
3057        && c.is_ascii_lowercase()
3058    {
3059        style = match c {
3060            b'a' => Some(ColumnStyle::AsciiDoc),
3061            b'd' => Some(ColumnStyle::Default),
3062            b'e' => Some(ColumnStyle::Emphasis),
3063            b'h' => Some(ColumnStyle::Header),
3064            b'l' => Some(ColumnStyle::Literal),
3065            b'm' => Some(ColumnStyle::Monospace),
3066            b's' => Some(ColumnStyle::Strong),
3067            _ => None,
3068        };
3069        i += 1;
3070    }
3071
3072    // The token is a cell specifier only if it was consumed in its entirety.
3073    if i == b.len() {
3074        Some(CellSpec {
3075            h_align,
3076            v_align,
3077            style,
3078            colspan,
3079            rowspan,
3080            repeat,
3081        })
3082    } else {
3083        None
3084    }
3085}
3086
3087/// Return the subspan of `s` with surrounding whitespace (including newlines)
3088/// removed.
3089fn trim_surrounding_whitespace(s: Span<'_>) -> Span<'_> {
3090    let data = s.data();
3091    let start = data.len() - data.trim_start().len();
3092    let len = data.trim().len();
3093    s.slice(start..start + len)
3094}
3095
3096/// Trim a PSV cell's content according to its [style](ColumnStyle), matching
3097/// Asciidoctor's `Table::Cell` initializer:
3098///
3099/// * A [`Literal`](ColumnStyle::Literal) cell has its trailing whitespace
3100///   removed and any leading blank lines stripped, but the leading indentation
3101///   of its first content line is preserved (so an indented literal cell keeps
3102///   its indentation).
3103/// * An [`AsciiDoc`](ColumnStyle::AsciiDoc) cell likewise removes trailing
3104///   whitespace; if the remaining content begins with a newline it strips the
3105///   leading blank lines (preserving the first content line's indentation, so a
3106///   leading-indented line is interpreted as a literal block), otherwise it
3107///   strips the leading whitespace.
3108/// * Every other style has all surrounding whitespace removed.
3109fn trim_cell_content(s: Span<'_>, style: ColumnStyle) -> Span<'_> {
3110    let data = s.data();
3111    match style {
3112        ColumnStyle::Literal => {
3113            let end = data.trim_end().len();
3114            let mut start = 0;
3115            while data[start..end].starts_with('\n') {
3116                start += 1;
3117            }
3118            s.slice(start..end)
3119        }
3120
3121        ColumnStyle::AsciiDoc => {
3122            let end = data.trim_end().len();
3123            if data[..end].starts_with('\n') {
3124                let mut start = 0;
3125                while data[start..end].starts_with('\n') {
3126                    start += 1;
3127                }
3128                s.slice(start..end)
3129            } else {
3130                let start = end - data[..end].trim_start().len();
3131                s.slice(start..end)
3132            }
3133        }
3134
3135        _ => trim_surrounding_whitespace(s),
3136    }
3137}
3138
3139/// Returns the first non-blank line in `rest`, or `None` when every remaining
3140/// line is blank (or `rest` is empty).
3141fn first_nonblank_line(mut rest: Span<'_>) -> Option<Span<'_>> {
3142    while !rest.is_empty() {
3143        let line = rest.take_line();
3144        if !line.item.data().trim().is_empty() {
3145            return Some(line.item);
3146        }
3147        rest = line.after;
3148    }
3149    None
3150}
3151
3152/// Returns `true` when `line` begins a new PSV cell, i.e. it contains the
3153/// separator and the text before the first separator (after any leading
3154/// whitespace) is either empty or a valid cell specifier. A line that continues
3155/// the previous cell returns `false`.
3156fn psv_line_starts_cell(line: &str, separator: &str) -> bool {
3157    match line.find(separator) {
3158        Some(pos) => {
3159            let prefix = line[..pos].trim_start();
3160            prefix.is_empty() || parse_cell_spec(prefix).is_some()
3161        }
3162        None => false,
3163    }
3164}
3165
3166/// Returns `true` when `line` contains an odd number of double quotes, i.e. it
3167/// opens a quoted CSV/TSV value that is not closed on the same line.
3168fn line_has_unclosed_quote(line: &str) -> bool {
3169    line.bytes().filter(|&b| b == b'"').count() % 2 == 1
3170}
3171
3172#[cfg(test)]
3173mod tests {
3174    use std::sync::Arc;
3175
3176    use super::{
3177        AsciiDocCell, OwnedCell, OwnedCellInner, ResolvedAttributes, TocConfig,
3178        absolute_cell_directive_origin,
3179    };
3180    use crate::{
3181        Span,
3182        parser::{
3183            HtmlSubstitutionRenderer, ReferenceResolver, ReferenceWarnings, ResolutionContext,
3184            ResolvedReference, SourceLine,
3185        },
3186    };
3187
3188    #[test]
3189    fn absolute_cell_directive_origin_resolves_locations() {
3190        let cell_origin = SourceLine(Some("outer.adoc".to_owned()), 5);
3191
3192        // No deferred origin (e.g. an unresolved include target): stays None.
3193        assert_eq!(
3194            absolute_cell_directive_origin(None, Some(&cell_origin)),
3195            None
3196        );
3197
3198        // A different file (included content): kept as-is — its line is already
3199        // absolute within that file.
3200        assert_eq!(
3201            absolute_cell_directive_origin(
3202                Some(SourceLine(Some("inc.adoc".to_owned()), 3)),
3203                Some(&cell_origin)
3204            ),
3205            Some(SourceLine(Some("inc.adoc".to_owned()), 3))
3206        );
3207
3208        // The cell's own file, first line (pass-relative line 1): resolves to the
3209        // cell's own location.
3210        assert_eq!(
3211            absolute_cell_directive_origin(
3212                Some(SourceLine(Some("outer.adoc".to_owned()), 1)),
3213                Some(&cell_origin)
3214            ),
3215            Some(SourceLine(Some("outer.adoc".to_owned()), 5))
3216        );
3217
3218        // The cell's own file, a later line (pass-relative line 3): translated to
3219        // two lines past the cell's first line — not collapsed onto line 5.
3220        assert_eq!(
3221            absolute_cell_directive_origin(
3222                Some(SourceLine(Some("outer.adoc".to_owned()), 3)),
3223                Some(&cell_origin)
3224            ),
3225            Some(SourceLine(Some("outer.adoc".to_owned()), 7))
3226        );
3227
3228        // With no resolved cell origin, the deferred origin is kept unchanged.
3229        assert_eq!(
3230            absolute_cell_directive_origin(Some(SourceLine(None, 2)), None),
3231            Some(SourceLine(None, 2))
3232        );
3233    }
3234
3235    /// A resolver that resolves nothing; the owned-cell resolution path under
3236    /// test carries no references, so it is never actually consulted.
3237    struct NoopResolver;
3238
3239    impl ReferenceResolver for NoopResolver {
3240        fn resolve(&self, _context: &ResolutionContext<'_>) -> Option<ResolvedReference> {
3241            None
3242        }
3243    }
3244
3245    /// When an owned (include-expanded) AsciiDoc cell is shared behind more
3246    /// than one `Arc` reference, `resolve_references` cannot obtain a
3247    /// mutable borrow of the store and leaves it untouched rather than
3248    /// panicking. Production code resolves while the cell is its sole
3249    /// owner, so this defensive branch is exercised here by deliberately
3250    /// holding a second reference.
3251    #[test]
3252    fn resolve_references_skips_shared_owned_cell() {
3253        let mut cell = AsciiDocCell::Owned(Arc::new(OwnedCell::new(String::new(), |_source| {
3254            OwnedCellInner {
3255                title: None,
3256                inline: false,
3257                toc: TocConfig::disabled(),
3258                blocks: vec![],
3259                attributes: ResolvedAttributes::default(),
3260            }
3261        })));
3262
3263        // Hold a second reference to the same store so `Arc::get_mut` fails.
3264        let shared = cell.clone();
3265
3266        let mut warnings = ReferenceWarnings::default();
3267
3268        cell.resolve_references(
3269            &NoopResolver,
3270            &HtmlSubstitutionRenderer {},
3271            &mut warnings,
3272            Span::new(""),
3273        );
3274
3275        // Resolution was skipped silently: no warnings, and the two references
3276        // still describe the same (unmodified) cell.
3277        assert!(warnings.host.is_empty());
3278        assert!(warnings.doc.is_empty());
3279        assert_eq!(cell, shared);
3280    }
3281
3282    mod unresolved_directive_in_asciidoc_cell {
3283        #![allow(clippy::indexing_slicing)]
3284
3285        use crate::{
3286            parser::SourceLine,
3287            tests::prelude::{inline_file_handler::InlineFileHandler, *},
3288        };
3289
3290        // The faithful port of Ruby Asciidoctor `tables_test.rb` 1728 (an
3291        // unresolved directive in a cell reached via an outer `include::`) lives
3292        // in `tests::asciidoctor_rb::tables_test`. These are additional
3293        // regression tests for the same fix, kept next to the code under test.
3294
3295        // The table is in the primary document itself, so the unresolved
3296        // directive is attributed to the root file (not an included one).
3297        #[test]
3298        fn root_document_cell_reports_root_cursor() {
3299            // No include handler: `does-not-exist.adoc` cannot be resolved.
3300            let doc = Parser::default()
3301                .with_safe_mode(SafeMode::Server)
3302                .parse("|===\na|include::does-not-exist.adoc[]\n|===");
3303
3304            assert_rendered_contains(&doc, "Unresolved directive in (root file)");
3305
3306            let warnings: Vec<_> = doc.warnings().collect();
3307            assert_eq!(warnings.len(), 1);
3308            assert_eq!(
3309                warnings[0].warning,
3310                WarningType::IncludeFileNotFound("does-not-exist.adoc".to_string())
3311            );
3312
3313            // The directive is on line 2 of the primary document.
3314            assert_eq!(
3315                doc.source_map()
3316                    .original_file_and_line(warnings[0].source.line()),
3317                Some(SourceLine(None, 2))
3318            );
3319        }
3320
3321        // A table nested inside a *borrowed* AsciiDoc cell (one whose own content
3322        // is not include-expanded) is still parsed in place from the document
3323        // source, so an unresolved directive in the inner cell maps through the
3324        // document source map like any other. Here the whole document is the root
3325        // file, so the cursor is the root file at the inner directive's line.
3326        #[test]
3327        fn nested_table_cell_maps_through_document_source() {
3328            let doc = Parser::default()
3329                .with_safe_mode(SafeMode::Server)
3330                .parse("|===\na|\n!===\na!include::does-not-exist.adoc[]\n!===\n|===");
3331
3332            assert_rendered_contains(&doc, "Unresolved directive in (root file)");
3333
3334            let warnings: Vec<_> = doc.warnings().collect();
3335            assert_eq!(warnings.len(), 1);
3336            assert_eq!(
3337                warnings[0].warning,
3338                WarningType::IncludeFileNotFound("does-not-exist.adoc".to_string())
3339            );
3340
3341            // The inner directive is on line 4 of the primary document.
3342            assert_eq!(
3343                doc.source_map()
3344                    .original_file_and_line(warnings[0].source.line()),
3345                Some(SourceLine(None, 4))
3346            );
3347        }
3348
3349        // Greptile #639: a table nested inside a (borrowed) cell of an *included*
3350        // file must attribute an inner unresolved directive to that included
3351        // file, not the root file.
3352        #[test]
3353        fn nested_table_cell_in_included_file_reports_include_cursor() {
3354            let handler = InlineFileHandler::from_pairs([(
3355                "outer.adoc",
3356                "|===\na|\n!===\na!include::does-not-exist.adoc[]\n!===\n|===",
3357            )]);
3358            let doc = Parser::default()
3359                .with_safe_mode(SafeMode::Server)
3360                .with_include_file_handler(handler)
3361                .parse("include::outer.adoc[]");
3362
3363            assert_rendered_contains(&doc, "Unresolved directive in outer.adoc");
3364
3365            let warnings: Vec<_> = doc.warnings().collect();
3366            assert_eq!(warnings.len(), 1);
3367            assert_eq!(
3368                warnings[0].warning,
3369                WarningType::IncludeFileNotFound("does-not-exist.adoc".to_string())
3370            );
3371
3372            // The inner directive is on line 4 of `outer.adoc`.
3373            assert_eq!(
3374                doc.source_map()
3375                    .original_file_and_line(warnings[0].source.line()),
3376                Some(SourceLine(Some("outer.adoc".to_string()), 4))
3377            );
3378        }
3379
3380        // A table nested inside an *owned* (include-expanded) cell is parsed from
3381        // that cell's private source, whose spans index the cell's own source map
3382        // rather than the document's. An unresolved directive in the inner cell
3383        // is resolved against that owned source map: it is rendered naming the
3384        // file it came from, and its warning carries a pre-resolved origin
3385        // (`Warning::origin`) pointing at that file and line, anchored to the
3386        // enclosing document-level cell's directive line. Fixes
3387        // https://github.com/asciidoc-rs/asciidoc-parser/issues/641.
3388        #[test]
3389        fn unresolved_directive_inside_owned_cell_source_reports_origin() {
3390            // `cell.adoc` is pulled in as the top cell's owned source; it holds a
3391            // nested table (so its cells use the `!` separator) whose own cell has
3392            // an unresolvable include on its line 2.
3393            let handler = InlineFileHandler::from_pairs([(
3394                "cell.adoc",
3395                "!===\na!include::does-not-exist.adoc[]\n!===",
3396            )]);
3397            let doc = Parser::default()
3398                .with_safe_mode(SafeMode::Server)
3399                .with_include_file_handler(handler)
3400                .parse("|===\na|include::cell.adoc[]\n|===");
3401
3402            // The inner directive is expanded into an "Unresolved directive"
3403            // message that now names the file the directive actually came from
3404            // (`cell.adoc`), not the root file.
3405            assert_rendered_contains(
3406                &doc,
3407                "Unresolved directive in cell.adoc - include::does-not-exist.adoc[]",
3408            );
3409
3410            // A single warning is reported (rather than dropped).
3411            let warnings: Vec<_> = doc.warnings().collect();
3412            assert_eq!(warnings.len(), 1);
3413            assert_eq!(
3414                warnings[0].warning,
3415                WarningType::IncludeFileNotFound("does-not-exist.adoc".to_string())
3416            );
3417
3418            // The directive lives in privately-expanded cell content that no
3419            // document span maps to, so its true cursor is carried directly on
3420            // the warning: `cell.adoc` line 2.
3421            assert_eq!(
3422                warnings[0].origin,
3423                Some(SourceLine(Some("cell.adoc".to_string()), 2))
3424            );
3425
3426            // Its `source` span is a best-effort anchor into the document — the
3427            // enclosing cell's `include::cell.adoc[]` directive line (line 2 of
3428            // the root document) — so it still resolves to a real cursor.
3429            assert_eq!(
3430                doc.source_map()
3431                    .original_file_and_line(warnings[0].source.line()),
3432                Some(SourceLine(None, 2))
3433            );
3434        }
3435
3436        #[test]
3437        fn duplicate_inline_anchor_in_borrowed_cell_reports_warning() {
3438            let doc = Parser::default().parse(
3439                "[#in-use]\n\
3440                 registered\n\
3441                 \n\
3442                 [cols=1a]\n\
3443                 |===\n\
3444                 |[[in-use]]duplicate\n\
3445                 |===",
3446            );
3447
3448            let warnings: Vec<_> = doc.warnings().collect();
3449            assert_eq!(warnings.len(), 1);
3450            assert_eq!(
3451                warnings[0].warning,
3452                WarningType::DuplicateId("in-use".to_string())
3453            );
3454            assert_eq!(
3455                doc.source_map()
3456                    .original_file_and_line(warnings[0].source.line()),
3457                Some(SourceLine(None, 6))
3458            );
3459            assert!(warnings[0].origin.is_none());
3460        }
3461
3462        #[test]
3463        fn duplicate_inline_anchor_in_owned_cell_reports_origin() {
3464            let handler = InlineFileHandler::from_pairs([("cell.adoc", "[[in-use]]duplicate")]);
3465            let doc = Parser::default()
3466                .with_safe_mode(SafeMode::Server)
3467                .with_include_file_handler(handler)
3468                .parse(
3469                    "[#in-use]\n\
3470                     registered\n\
3471                     \n\
3472                     [cols=1a]\n\
3473                     |===\n\
3474                     |include::cell.adoc[]\n\
3475                     |===",
3476                );
3477
3478            let warnings: Vec<_> = doc.warnings().collect();
3479            assert_eq!(warnings.len(), 1);
3480            assert_eq!(
3481                warnings[0].warning,
3482                WarningType::DuplicateId("in-use".to_string())
3483            );
3484            assert_eq!(
3485                warnings[0].origin,
3486                Some(SourceLine(Some("cell.adoc".to_string()), 1))
3487            );
3488            assert_eq!(
3489                doc.source_map()
3490                    .original_file_and_line(warnings[0].source.line()),
3491                Some(SourceLine(None, 6))
3492            );
3493        }
3494    }
3495
3496    // Cataloging a leading anchor found in a table cell (issue #543) is covered
3497    // for header and default-style cells by the ported tests in
3498    // `tests::asciidoctor_rb::tables_test`. Those styled-column fixtures place
3499    // the anchor in the first (header) row, and a header cell is always parsed
3500    // with the default column style — so `cols=1a` never actually parses the
3501    // anchored value as an AsciiDoc-style cell there. This exercises that
3502    // missing case directly: a leading anchor in an AsciiDoc-style *body* cell
3503    // must still be cataloged in the main document.
3504    mod anchor_in_asciidoc_body_cell {
3505        use crate::tests::prelude::*;
3506
3507        #[test]
3508        fn leading_anchor_in_asciidoc_body_cell_is_cataloged() {
3509            // Two `|` rows with no blank line between them defeat the implicit-
3510            // header heuristic (which requires a blank line after the first row),
3511            // so both cells are AsciiDoc-style *body* cells rather than a header.
3512            let doc = Parser::default()
3513                .parse("[cols=1a]\n|===\n|[[foo,Foo]]body anchor\n|second cell\n|===");
3514
3515            // Guard the premise: the anchored cell is a genuine AsciiDoc-style
3516            // body cell (each `a` cell renders its content as a nested document in
3517            // `div.content`), not a header cell — no `th` is produced, and the
3518            // anchor renders as a target inside the cell.
3519            assert_css(&doc, "th", 0);
3520            assert_css(&doc, "table.tableblock td.tableblock > div.content", 2);
3521            assert_xpath(&doc, "//td//div[@class=\"content\"]//a[@id=\"foo\"]", 1);
3522
3523            // The leading anchor is cataloged in the main document's catalog.
3524            assert!(doc.catalog().contains_id("foo"));
3525        }
3526    }
3527}