Skip to main content

asciidoc_parser/blocks/
table.rs

1use std::{collections::VecDeque, sync::Arc};
2
3use self_cell::self_cell;
4
5use crate::{
6    HasSpan, Parser, Span,
7    attributes::Attrlist,
8    blocks::{
9        Block, ContentModel, IsBlock, metadata::BlockMetadata, parse_utils::parse_blocks_until,
10    },
11    content::{Content, SubstitutionGroup},
12    document::InterpretedValue,
13    parser::{
14        InlineSubstitutionRenderer, ModificationContext, ReferenceResolver, ReferenceWarning,
15        preprocessor::preprocess,
16    },
17    span::MatchedItem,
18    strings::CowStr,
19    warnings::{MatchAndWarnings, Warning, WarningType},
20};
21
22/// Attributes that an AsciiDoc table cell may modify even when they are set in
23/// the parent document.
24///
25/// An AsciiDoc cell inherits the parent's attributes and cannot modify them,
26/// but the AsciiDoc specification carves out a handful of exceptions:
27/// `doctype`, `toc`, `notitle` (and its complement, `showtitle`), and
28/// `compat-mode`.
29const ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES: &[&str] =
30    &["doctype", "toc", "notitle", "showtitle", "compat-mode"];
31
32/// A table is a delimited block that arranges content into a grid of rows and
33/// columns.
34///
35/// A table is introduced by a table delimiter (`|===`, or `!===` for a nested
36/// table) and closed by a matching delimiter. By default cells are separated
37/// using prefix-separated value (PSV) syntax: the table's cell separator — a
38/// vertical bar (`|`) by default — at the start of a line or preceded by
39/// whitespace begins a new cell. Cells flow, in document order, into rows whose
40/// length is fixed by the number of columns. (The separator defaults to `!`
41/// inside a nested table and can be overridden with the `separator` attribute;
42/// see below.)
43///
44/// The number of columns is determined either by the `cols` attribute or,
45/// implicitly, by the number of cells found in the first non-empty line after
46/// the opening delimiter.
47///
48/// # Data formats
49///
50/// In addition to the default PSV format, a table can be populated from
51/// delimiter-separated data with the [`format`](Self::data_format) attribute:
52/// `csv` (comma-separated values), `tsv` (tab-separated values), or `dsv`
53/// (delimited values, colon-separated by default). The `,===` and `:===`
54/// shorthand delimiters select the CSV and DSV formats respectively without an
55/// explicit `format` attribute. In a data format the separator is placed
56/// *between* values (not in front of each cell) and a cell carries no
57/// formatting spec; cell formatting is instead applied per column with the
58/// `cols` attribute. See [`DataFormat`] for the parsing rules.
59///
60/// Column specifier style operators (the `a`, `d`, `e`, `h`, `l`, `m`, and `s`
61/// operators) are supported, along with proportional width and the horizontal
62/// and vertical alignment operators. Per-cell horizontal and vertical alignment
63/// operators are supported and override the column's alignment, and a per-cell
64/// style operator (in the last position of the cell specifier) is supported and
65/// overrides the column's style. The per-cell span (`+`) operator is supported:
66/// a cell can span multiple columns (`<n>+`), multiple rows (`.<n>+`), or a
67/// block of both (`<n>.<n>+`). The per-cell duplication (`*`) operator is
68/// supported: a cell with a duplication factor (`<n>*`) clones its content and
69/// properties into `<n>` consecutive cells.
70///
71/// Table sizing is supported: the [`width`](Self::width) attribute sets a fixed
72/// table width, the `autowidth` option ([`is_autowidth`](Self::is_autowidth))
73/// sizes the table and its columns to their content, and an individual column
74/// can be made [autowidth](TableColumn::is_autowidth) with the `~` width value.
75///
76/// Table borders are supported: the [`frame`](Self::frame) attribute controls
77/// the border around the table and the [`grid`](Self::grid) attribute controls
78/// the borders between cells. Each falls back to a document-level default
79/// (`table-frame` / `table-grid`) and then to `all`.
80///
81/// Zebra striping is supported via the [`stripes`](Self::stripes) attribute,
82/// which falls back to the `table-stripes` document attribute and then to
83/// `none`.
84///
85/// Nested tables are supported: an [`AsciiDoc`](ColumnStyle::AsciiDoc) cell may
86/// contain its own table. The cell separator defaults to the vertical bar (`|`)
87/// but switches to the exclamation mark (`!`) inside an AsciiDoc cell, so a
88/// nested table is opened with `!===` and separates its cells with `!`. The
89/// `separator` attribute overrides the default separator with an explicit
90/// character at any level.
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct TableBlock<'src> {
93    columns: Vec<TableColumn>,
94    data_format: DataFormat,
95    header_row: Option<TableRow<'src>>,
96    body_rows: Vec<TableRow<'src>>,
97    footer_row: Option<TableRow<'src>>,
98    source: Span<'src>,
99    title_source: Option<Span<'src>>,
100    title: Option<String>,
101    caption: Option<String>,
102    frame: Frame,
103    grid: Grid,
104    stripes: Stripes,
105    anchor: Option<Span<'src>>,
106    anchor_reftext: Option<Span<'src>>,
107    attrlist: Option<Attrlist<'src>>,
108}
109
110impl<'src> TableBlock<'src> {
111    /// Returns `true` if `line` is a table delimiter.
112    ///
113    /// A table delimiter is one of the lead characters `|`, `!`, `,`, or `:`
114    /// followed by three or more equals signs (`===`). The lead character also
115    /// selects the table's data format and default cell separator:
116    ///
117    /// * `|===` is the ordinary (PSV) table delimiter.
118    /// * `!===` opens a table whose default cell separator is the exclamation
119    ///   mark, which lets a nested table be distinguished from the
120    ///   `|`-separated table that encloses it.
121    /// * `,===` is the shorthand for a CSV table.
122    /// * `:===` is the shorthand for a DSV table.
123    pub(crate) fn is_table_delimiter(line: &Span<'src>) -> bool {
124        let data = line.data();
125        // `len() >= 4` plus the leading delimiter character guarantees `rest`
126        // holds at least three bytes, so the closure only needs to confirm they
127        // are all `=`.
128        data.len() >= 4
129            && matches!(data.as_bytes().first(), Some(b'|' | b'!' | b',' | b':'))
130            && data
131                .get(1..)
132                .is_some_and(|rest| rest.bytes().all(|b| b == b'='))
133    }
134
135    pub(crate) fn parse(
136        metadata: &BlockMetadata<'src>,
137        parser: &mut Parser,
138    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
139        let delimiter = metadata.block_start.take_normalized_line();
140
141        if !Self::is_table_delimiter(&delimiter.item) {
142            return None;
143        }
144
145        let delimiter_text = delimiter.item.data();
146
147        // Find the matching closing delimiter.
148        let mut next = delimiter.after;
149        let (closing_delimiter, after) = loop {
150            if next.is_empty() {
151                break (next, next);
152            }
153
154            let line = next.take_normalized_line();
155            if line.item.data() == delimiter_text {
156                break (line.item, line.after);
157            }
158            next = line.after;
159        };
160
161        let inside = delimiter.after.trim_remainder(closing_delimiter);
162
163        // The data format governs how the table body is split into cells. It
164        // defaults to PSV, but the `format` attribute selects CSV, TSV, or DSV,
165        // and the `,===` / `:===` shorthand delimiters select CSV / DSV. The
166        // lead character of the delimiter (`delimiter_text`) is passed so the
167        // shorthand can be honored.
168        let data_format = resolve_data_format(metadata, delimiter_text);
169
170        // The cell separator partitions each row into cells. In PSV it defaults
171        // to the vertical bar (`|`), except inside an AsciiDoc table cell — a
172        // nested, standalone document — where it defaults to the exclamation
173        // mark (`!`) so a nested table is distinguished from the `|`-separated
174        // table that encloses it. Each data format has its own default (CSV =
175        // comma, TSV = tab, DSV = colon). The `separator` attribute overrides
176        // the default; an empty `separator` falls back to the default, and the
177        // two-character sequence `\t` is interpreted as a tab.
178        let separator = resolve_separator(metadata, parser, data_format);
179
180        // The `cols` attribute, when present, fixes the number of columns and
181        // carries the per-column formatting. When it is absent the column count
182        // is implicit (resolved per format below).
183        let cols_attr: Vec<TableColumn> = metadata
184            .attrlist
185            .as_ref()
186            .and_then(|a| a.named_attribute("cols"))
187            .map(|attr| parse_cols(attr.value()))
188            .unwrap_or_default();
189
190        // The `autowidth` option sizes the table to its content; the columns
191        // inherit the setting, so every column becomes autowidth regardless of
192        // any proportional width set on its specifier.
193        let autowidth = metadata
194            .attrlist
195            .as_ref()
196            .is_some_and(|a| a.has_option("autowidth"));
197
198        // The first row is an (implicit) header row when the line directly after
199        // the opening delimiter is non-empty and is itself followed by an empty
200        // line. The `header` option forces the same interpretation; the
201        // `noheader` option suppresses only the implicit detection, so an
202        // explicit `header` still wins when both are present.
203        let opts_header = metadata
204            .attrlist
205            .as_ref()
206            .is_some_and(|a| a.has_option("header"));
207        let opts_noheader = metadata
208            .attrlist
209            .as_ref()
210            .is_some_and(|a| a.has_option("noheader"));
211
212        // The last row is promoted to a footer row when the `footer` option is
213        // set. Unlike the header row, a footer cell is processed with its
214        // column's style (it is simply the last body row, relabeled).
215        let opts_footer = metadata
216            .attrlist
217            .as_ref()
218            .is_some_and(|a| a.has_option("footer"));
219
220        // The blank line must genuinely exist after the first row; the end of the
221        // table (an empty remainder) does not count, so a single-row table is not
222        // mistaken for an all-header table.
223        let line1 = inside.take_line();
224        let line1_blank = line1.item.data().trim().is_empty();
225        let line2_blank =
226            !line1.after.is_empty() && line1.after.take_line().item.data().trim().is_empty();
227
228        // An implicit header additionally requires that the first row be complete
229        // on the first line. If the first cell spans multiple lines — for PSV,
230        // the first non-blank line after the blank gap continues the cell instead
231        // of starting a new one; for CSV/TSV, the first line opens a quoted value
232        // that is not closed on that line — there is no implicit header (matching
233        // Asciidoctor, which cancels the implicit header in these cases).
234        let first_row_complete = match data_format {
235            DataFormat::Psv => first_nonblank_line(line1.after)
236                .is_none_or(|line| psv_line_starts_cell(line.data(), separator.as_str())),
237            DataFormat::Csv | DataFormat::Tsv => !line_has_unclosed_quote(line1.item.data()),
238            DataFormat::Dsv => true,
239        };
240
241        let has_header =
242            opts_header || (!opts_noheader && !line1_blank && line2_blank && first_row_complete);
243
244        // A titled table is given a caption (e.g. "Table 1. ") that a processor
245        // prepends to the title.
246        //
247        // An explicit `caption` attribute on the table sets the label verbatim
248        // (including any trailing whitespace) and is used as-is, with no
249        // automatically incremented number; it applies even when
250        // `table-caption` has been unset. An explicitly empty `caption` (e.g.
251        // `[caption=]`) removes the label entirely, so the title renders with no
252        // prefix. Otherwise the label comes from the `table-caption` attribute
253        // (which defaults to "Table"), and each such captioned table consumes
254        // the next value of a document-wide table counter. When `table-caption`
255        // is unset and no explicit `caption` is given, no caption (and no
256        // number) is assigned.
257        //
258        // Computed before the cell iterator below borrows `parser` immutably, so
259        // that the mutable counter update does not conflict with that borrow.
260        let caption = if metadata.title.is_some() {
261            match metadata
262                .attrlist
263                .as_ref()
264                .and_then(|a| a.named_attribute("caption"))
265            {
266                Some(attr) if attr.value().is_empty() => None,
267                Some(attr) => Some(attr.value().to_string()),
268                None => match parser.attribute_value("table-caption") {
269                    InterpretedValue::Value(label) if !label.is_empty() => {
270                        let number = parser.assign_table_number();
271                        Some(format!("{label} {number}. "))
272                    }
273                    _ => None,
274                },
275            }
276        } else {
277            None
278        };
279
280        // The `frame` and `grid` attributes control the table's borders, and the
281        // `stripes` attribute controls zebra striping. The borders each default
282        // to `all` and stripes defaults to `none`; the default can be changed for
283        // the whole document with the `table-frame` / `table-grid` /
284        // `table-stripes` attribute, and an explicit attribute on the table
285        // overrides both. Each value is resolved here (while `parser` is borrowed
286        // only immutably) and stored on the block so the accessors need no further
287        // document lookup.
288        let frame = resolve_table_attribute::<Frame>(metadata, parser, "frame", "table-frame");
289        let grid = resolve_table_attribute::<Grid>(metadata, parser, "grid", "table-grid");
290        let stripes =
291            resolve_table_attribute::<Stripes>(metadata, parser, "stripes", "table-stripes");
292
293        // Split the body into columns and rows according to the data format.
294        // PSV walks a grid that honors cell spans and duplication; the data
295        // formats (CSV/TSV/DSV) split on a separator with no per-cell spec and
296        // flow the values into fixed-width rows.
297        let mut warnings: Vec<Warning<'src>> = vec![];
298        let body = TableBody {
299            inside,
300            separator,
301            cols_attr,
302            autowidth,
303            has_header,
304        };
305        let (columns, rows) = match data_format {
306            DataFormat::Psv => build_psv_table(body, parser, &mut warnings),
307            DataFormat::Csv | DataFormat::Tsv | DataFormat::Dsv => {
308                build_data_table(body, data_format, parser, &mut warnings)
309            }
310        };
311
312        let mut rows = rows.into_iter();
313        let header_row = if has_header { rows.next() } else { None };
314        let mut body_rows: Vec<TableRow<'src>> = rows.collect();
315
316        // The footer row, when requested, is the last row of the table. It is
317        // moved out of the body so the caller sees it as a distinct footer. When
318        // the table has no rows to spare, no footer is produced.
319        let footer_row = if opts_footer { body_rows.pop() } else { None };
320
321        let source = metadata
322            .source
323            .trim_remainder(closing_delimiter.discard_all())
324            .trim_trailing_whitespace();
325
326        if closing_delimiter.is_empty() {
327            warnings.push(Warning {
328                source: delimiter.item,
329                warning: WarningType::UnterminatedDelimitedBlock,
330            });
331        }
332
333        Some(MatchAndWarnings {
334            item: Some(MatchedItem {
335                item: Self {
336                    columns,
337                    data_format,
338                    header_row,
339                    body_rows,
340                    footer_row,
341                    source,
342                    title_source: metadata.title_source,
343                    title: metadata.title.clone(),
344                    caption,
345                    frame,
346                    grid,
347                    stripes,
348                    anchor: metadata.anchor,
349                    anchor_reftext: metadata.anchor_reftext,
350                    attrlist: metadata.attrlist.clone(),
351                },
352                after,
353            }),
354            warnings,
355        })
356    }
357
358    /// Returns the caption assigned to this table, if any.
359    ///
360    /// A titled table is captioned with a label that a processor prepends to
361    /// the [`title`](IsBlock::title). By default the label combines the
362    /// `table-caption` attribute and an automatically incremented number (e.g.
363    /// `"Table 1. "`). An explicit `caption` attribute on the table overrides
364    /// this with a verbatim label and no number; an explicitly empty `caption`
365    /// (e.g. `[caption=]`) removes the label entirely. The caption is absent
366    /// when the table has no title, when `table-caption` has been unset and no
367    /// explicit `caption` is given, or when an empty `caption` was supplied.
368    pub fn caption(&self) -> Option<&str> {
369        self.caption.as_deref()
370    }
371
372    /// Returns the columns of this table.
373    pub fn columns(&self) -> &[TableColumn] {
374        &self.columns
375    }
376
377    /// Returns the [`DataFormat`] used to populate this table.
378    ///
379    /// The format comes from the `format` attribute on the table (`psv`, `csv`,
380    /// `tsv`, or `dsv`) or from a shorthand delimiter (`,===` selects CSV,
381    /// `:===` selects DSV). When neither is present the format defaults to
382    /// [`DataFormat::Psv`].
383    pub fn data_format(&self) -> DataFormat {
384        self.data_format
385    }
386
387    /// Returns the fixed width of this table, as a percentage of the content
388    /// area, when the `width` attribute is set.
389    ///
390    /// The `width` attribute is an integer percentage from 1 to 100; the
391    /// trailing `%` sign is optional (`[width=75%]` and `[width=75]` are
392    /// equivalent). A value outside that range, or one that is not an integer,
393    /// is ignored and reported as `None`. When the attribute is absent the
394    /// table spans the width of the content area and this returns `None`.
395    pub fn width(&self) -> Option<usize> {
396        let raw = self
397            .attrlist
398            .as_ref()
399            .and_then(|a| a.named_attribute("width"))?
400            .value();
401
402        let raw = raw.strip_suffix('%').unwrap_or(raw);
403        match raw.parse::<usize>() {
404            Ok(width) if (1..=100).contains(&width) => Some(width),
405            _ => None,
406        }
407    }
408
409    /// Returns `true` if this table carries the `autowidth` option.
410    ///
411    /// An autowidth table is sized to fit its content rather than spanning the
412    /// width of the content area, and each of its [columns](TableColumn) is
413    /// likewise [autowidth](TableColumn::is_autowidth).
414    pub fn is_autowidth(&self) -> bool {
415        self.attrlist
416            .as_ref()
417            .is_some_and(|a| a.has_option("autowidth"))
418    }
419
420    /// Returns the [`Frame`] that controls the border drawn around this table.
421    ///
422    /// The frame comes from the `frame` attribute on the table, which accepts
423    /// `all`, `ends`, `sides`, or `none`. When the attribute is absent the
424    /// value is taken from the `table-frame` document attribute, and when
425    /// that too is absent it defaults to [`Frame::All`].
426    pub fn frame(&self) -> Frame {
427        self.frame
428    }
429
430    /// Returns the [`Grid`] that controls the borders drawn between this
431    /// table's cells.
432    ///
433    /// The grid comes from the `grid` attribute on the table, which accepts
434    /// `all`, `rows`, `cols`, or `none`. When the attribute is absent the value
435    /// is taken from the `table-grid` document attribute, and when that too is
436    /// absent it defaults to [`Grid::All`].
437    pub fn grid(&self) -> Grid {
438        self.grid
439    }
440
441    /// Returns the [`Stripes`] that control which rows of this table are shaded
442    /// to create a zebra-striping effect.
443    ///
444    /// The stripes come from the `stripes` attribute on the table, which
445    /// accepts `none`, `even`, `odd`, `all`, or `hover`. When the attribute
446    /// is absent the value is taken from the `table-stripes` document
447    /// attribute, and when that too is absent it defaults to
448    /// [`Stripes::None`].
449    ///
450    /// As a shorthand, a `stripes-<value>` role on the table (e.g.
451    /// `[.stripes-even]`) applies the same CSS class directly without setting
452    /// the `stripes` attribute. That shorthand does not affect this value
453    /// (which remains [`Stripes::None`]); the role is instead reported
454    /// among the table's [roles](crate::attributes::Attrlist::roles).
455    pub fn stripes(&self) -> Stripes {
456        self.stripes
457    }
458
459    /// Returns the header row of this table, if one was declared.
460    pub fn header_row(&self) -> Option<&TableRow<'src>> {
461        self.header_row.as_ref()
462    }
463
464    /// Returns the body rows of this table.
465    pub fn body_rows(&self) -> &[TableRow<'src>] {
466        &self.body_rows
467    }
468
469    /// Returns the footer row of this table, if one was declared.
470    pub fn footer_row(&self) -> Option<&TableRow<'src>> {
471        self.footer_row.as_ref()
472    }
473
474    /// Resolves any deferred cross-references in this table's cells.
475    pub(crate) fn resolve_references(
476        &mut self,
477        resolver: &dyn ReferenceResolver,
478        renderer: &dyn InlineSubstitutionRenderer,
479        warnings: &mut Vec<ReferenceWarning>,
480    ) {
481        let rows = self
482            .header_row
483            .iter_mut()
484            .chain(self.body_rows.iter_mut())
485            .chain(self.footer_row.iter_mut());
486
487        for row in rows {
488            for cell in row.cells.iter_mut() {
489                cell.resolve_references(resolver, renderer, warnings);
490            }
491        }
492    }
493}
494
495impl<'src> IsBlock<'src> for TableBlock<'src> {
496    fn content_model(&self) -> ContentModel {
497        ContentModel::Table
498    }
499
500    fn raw_context(&self) -> CowStr<'src> {
501        "table".into()
502    }
503
504    fn title_source(&'src self) -> Option<Span<'src>> {
505        self.title_source
506    }
507
508    fn title(&self) -> Option<&str> {
509        self.title.as_deref()
510    }
511
512    fn anchor(&'src self) -> Option<Span<'src>> {
513        self.anchor
514    }
515
516    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
517        self.anchor_reftext
518    }
519
520    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
521        self.attrlist.as_ref()
522    }
523}
524
525impl<'src> HasSpan<'src> for TableBlock<'src> {
526    fn span(&self) -> Span<'src> {
527        self.source
528    }
529}
530
531/// A column in a [`TableBlock`].
532///
533/// A column carries its proportional width, the horizontal and vertical
534/// alignment applied to its cells' content, and the [style](ColumnStyle) used
535/// to process and render that content.
536#[derive(Clone, Debug, Eq, PartialEq)]
537pub struct TableColumn {
538    width: usize,
539    autowidth: bool,
540    h_align: HorizontalAlignment,
541    v_align: VerticalAlignment,
542    style: ColumnStyle,
543}
544
545impl TableColumn {
546    /// Returns the width of this column relative to the other columns in the
547    /// table. The default width is `1`.
548    ///
549    /// This value carries two different meanings depending on the table, and a
550    /// caller that resolves columns to final sizes must check which applies:
551    ///
552    /// * In an ordinary table (no column is [autowidth](Self::is_autowidth)),
553    ///   the width is a *proportional* ratio. Each column's share of the table
554    ///   is its width divided by the sum of all the column widths, so
555    ///   `[cols="1,2,3"]` yields shares of 1/6, 2/6, and 3/6.
556    /// * When at least one column in the table is autowidth (its specifier uses
557    ///   the special width value `~`), the AsciiDoc specification instead reads
558    ///   these widths as literal *percentages* (100-based): in
559    ///   `[cols="25,~,~"]` the first column is 25% wide and the `~` columns are
560    ///   sized to their content.
561    ///
562    /// The two cases are distinguished by whether any column in the table is
563    /// autowidth, which the caller can test with
564    /// `table.columns().iter().any(TableColumn::is_autowidth)`.
565    ///
566    /// When this column itself is autowidth, this width is not used to size the
567    /// column (the column is sized to its content instead). A column made
568    /// autowidth by the `~` specifier reports the default width of `1`, but one
569    /// that inherits autowidth from the table's `autowidth` option retains
570    /// whatever width its specifier set (e.g. `2` for the first column of
571    /// `[%autowidth,cols="2,1"]`).
572    pub fn width(&self) -> usize {
573        self.width
574    }
575
576    /// Returns `true` if this column is sized to fit its content rather than to
577    /// a proportional width.
578    ///
579    /// A column is autowidth when its column specifier uses the special width
580    /// value `~`, or when the table as a whole carries the `autowidth` option
581    /// (in which case every column inherits the setting).
582    pub fn is_autowidth(&self) -> bool {
583        self.autowidth
584    }
585
586    /// Returns the horizontal alignment applied to this column's content.
587    ///
588    /// The alignment comes from a horizontal alignment operator (`<`, `>`, or
589    /// `^`) on the column's specifier and defaults to
590    /// [`HorizontalAlignment::Left`].
591    pub fn h_align(&self) -> HorizontalAlignment {
592        self.h_align
593    }
594
595    /// Returns the vertical alignment applied to this column's content.
596    ///
597    /// The alignment comes from a vertical alignment operator (`.<`, `.>`, or
598    /// `.^`) on the column's specifier and defaults to
599    /// [`VerticalAlignment::Top`].
600    pub fn v_align(&self) -> VerticalAlignment {
601        self.v_align
602    }
603
604    /// Returns the [style](ColumnStyle) applied to this column's content.
605    ///
606    /// The style comes from a style operator in the last position of the
607    /// column's specifier (`a`, `d`, `e`, `h`, `l`, `m`, or `s`) and defaults
608    /// to [`ColumnStyle::Default`].
609    pub fn style(&self) -> ColumnStyle {
610        self.style
611    }
612}
613
614impl Default for TableColumn {
615    fn default() -> Self {
616        Self {
617            width: 1,
618            autowidth: false,
619            h_align: HorizontalAlignment::Left,
620            v_align: VerticalAlignment::Top,
621            style: ColumnStyle::Default,
622        }
623    }
624}
625
626/// The data format that governs how a [`TableBlock`]'s body is split into
627/// cells.
628///
629/// The format is selected by the `format` attribute (`psv`, `csv`, `tsv`, or
630/// `dsv`) or by a shorthand delimiter (`,===` for CSV, `:===` for DSV). The
631/// default is [`Psv`](Self::Psv).
632///
633/// In the PSV format the separator is placed in front of each cell and a cell
634/// may carry a formatting spec. In the delimiter-separated formats (CSV, TSV,
635/// and DSV) the separator is placed *between* values and a cell carries no
636/// spec; cell formatting is applied per column with the `cols` attribute
637/// instead. In every delimiter-separated format empty lines are skipped,
638/// whitespace surrounding each value is stripped, and a "ragged" table (whose
639/// rows do not all have the same number of cells) has its cells flowed into
640/// fixed-width rows, dropping any cells left over at the end.
641#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
642pub enum DataFormat {
643    /// Prefix-separated values: the default format. The separator (a vertical
644    /// bar, `|`, by default) is placed in front of each cell.
645    #[default]
646    Psv,
647
648    /// Comma-separated values (the `csv` format). The default separator is a
649    /// comma (`,`). Values may be enclosed in double quotes (`"`), within which
650    /// the separator and newlines are literal and a double quote is written by
651    /// doubling it (`""`); a newline that is not inside a quoted value begins a
652    /// new row. Loosely based on RFC 4180.
653    Csv,
654
655    /// Tab-separated values (the `tsv` format). Parsed by the same rules as
656    /// [`Csv`](Self::Csv), but the default separator is a tab.
657    Tsv,
658
659    /// Delimited values (the `dsv` format). The default separator is a colon
660    /// (`:`). Unlike CSV and TSV, an enclosing character is not recognized;
661    /// instead the separator can be included in a value by escaping it with a
662    /// single backslash (`\:`).
663    Dsv,
664}
665
666/// The style applied to the content of a [column](TableColumn) (and, by
667/// extension, to each body cell in that column).
668///
669/// A style is specified by a style operator in the last position of a column
670/// specifier. When no style operator is present, [`Default`](Self::Default) is
671/// assigned and the column is processed as paragraph text.
672///
673/// The style governs both how a cell's content is parsed and how it is
674/// rendered: most styles leave the content as inline markup (changing only the
675/// surrounding formatting), [`Literal`](Self::Literal) processes the content
676/// verbatim, and [`AsciiDoc`](Self::AsciiDoc) parses the content as a nested,
677/// standalone AsciiDoc document.
678///
679/// The verse operator (`v`) recognized by older versions of AsciiDoc has been
680/// deprecated and is not modeled here.
681#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
682pub enum ColumnStyle {
683    /// Block elements (lists, delimited blocks, and block macros) are
684    /// supported; the content is parsed as a nested, standalone AsciiDoc
685    /// document (the `a` operator).
686    AsciiDoc,
687
688    /// All of the markup permitted in a paragraph (inline formatting and inline
689    /// macros) is supported (the `d` operator). This is the default style,
690    /// assigned automatically when no style operator is present.
691    #[default]
692    Default,
693
694    /// Text is italicized (the `e` operator).
695    Emphasis,
696
697    /// The header semantics and styles are applied to the text and cell borders
698    /// (the `h` operator).
699    Header,
700
701    /// Content is treated as if it were inside a literal block (the `l`
702    /// operator).
703    Literal,
704
705    /// Text is rendered using a monospace font (the `m` operator).
706    Monospace,
707
708    /// Text is bold (the `s` operator).
709    Strong,
710}
711
712/// The horizontal alignment of a column's content.
713///
714/// Specified by a horizontal alignment operator at the start of a
715/// [column specifier](TableColumn): the less-than sign (`<`) for
716/// [`Left`](Self::Left), the greater-than sign (`>`) for
717/// [`Right`](Self::Right), and the caret (`^`) for [`Center`](Self::Center).
718/// The default is [`Left`](Self::Left).
719#[derive(Clone, Copy, Debug, Eq, PartialEq)]
720pub enum HorizontalAlignment {
721    /// Content is aligned to the left side of the column (the `<` operator).
722    /// This is the default horizontal alignment.
723    Left,
724
725    /// Content is centered horizontally in the column (the `^` operator).
726    Center,
727
728    /// Content is aligned to the right side of the column (the `>` operator).
729    Right,
730}
731
732/// The vertical alignment of a column's content.
733///
734/// Specified by a vertical alignment operator on a
735/// [column specifier](TableColumn), always introduced by a dot (`.`): `.<` for
736/// [`Top`](Self::Top), `.>` for [`Bottom`](Self::Bottom), and `.^` for
737/// [`Middle`](Self::Middle). The default is [`Top`](Self::Top).
738#[derive(Clone, Copy, Debug, Eq, PartialEq)]
739pub enum VerticalAlignment {
740    /// Content is aligned to the top of the column's cells (the `.<` operator).
741    /// This is the default vertical alignment.
742    Top,
743
744    /// Content is centered vertically in the column's cells (the `.^`
745    /// operator).
746    Middle,
747
748    /// Content is aligned to the bottom of the column's cells (the `.>`
749    /// operator).
750    Bottom,
751}
752
753/// The border drawn around a [`TableBlock`].
754///
755/// The frame is set with the `frame` attribute on the table (or, document-wide,
756/// the `table-frame` attribute). The default is [`All`](Self::All).
757///
758/// An unrecognized value falls back to [`All`](Self::All). (Asciidoctor instead
759/// passes an unrecognized value straight through to a CSS class, which the
760/// stylesheet ignores; this parser models only the four documented values.)
761#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
762pub enum Frame {
763    /// A border is drawn on every side of the table (the `all` value). This is
764    /// the default frame.
765    #[default]
766    All,
767
768    /// A border is drawn on the top and bottom of the table (the `ends` value).
769    ///
770    /// The `topbot` value recognized by older versions of AsciiDoc is accepted
771    /// as a synonym.
772    Ends,
773
774    /// A border is drawn on the left and right sides of the table (the `sides`
775    /// value).
776    Sides,
777
778    /// No border is drawn around the table (the `none` value).
779    None,
780}
781
782/// The borders drawn between the cells of a [`TableBlock`].
783///
784/// The grid is set with the `grid` attribute on the table (or, document-wide,
785/// the `table-grid` attribute). The default is [`All`](Self::All).
786///
787/// An unrecognized value falls back to [`All`](Self::All). (Asciidoctor instead
788/// passes an unrecognized value straight through to a CSS class, which the
789/// stylesheet ignores; this parser models only the four documented values.)
790#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
791pub enum Grid {
792    /// A border is drawn between all cells (the `all` value). This is the
793    /// default grid.
794    #[default]
795    All,
796
797    /// A border is drawn between the rows of the table (the `rows` value).
798    Rows,
799
800    /// A border is drawn between the columns of the table (the `cols` value).
801    Cols,
802
803    /// No border is drawn between the cells (the `none` value).
804    None,
805}
806
807/// The zebra striping applied to the rows of a [`TableBlock`].
808///
809/// Striping shades the specified rows with a background color to create a zebra
810/// effect. It is set with the `stripes` attribute on the table (or,
811/// document-wide, the `table-stripes` attribute). The default is
812/// [`None`](Self::None).
813///
814/// Under the covers, a converter applies the CSS class `stripes-<value>` to the
815/// table; the actual shading depends on the stylesheet. As a shorthand, the
816/// same class can be applied directly with a role (e.g. `[.stripes-even]`)
817/// rather than the `stripes` attribute. A role does not set this value (see
818/// [`TableBlock::stripes`]).
819///
820/// An unrecognized value falls back to [`None`](Self::None). (Asciidoctor
821/// instead passes an unrecognized value straight through to a CSS class, which
822/// the stylesheet ignores; this parser models only the five documented values.)
823#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
824pub enum Stripes {
825    /// No rows are shaded (the `none` value). This is the default.
826    #[default]
827    None,
828
829    /// Even rows are shaded (the `even` value).
830    Even,
831
832    /// Odd rows are shaded (the `odd` value).
833    Odd,
834
835    /// All rows are shaded (the `all` value).
836    All,
837
838    /// The row under the mouse cursor is shaded (the `hover` value). This has
839    /// an effect only in HTML output.
840    Hover,
841}
842
843/// A table-level attribute value ([`Frame`], [`Grid`], or [`Stripes`]) that can
844/// be parsed from an attribute value and has a default.
845trait TableAttributeValue: Copy + Default {
846    /// Parse the value of the table attribute (or its document-level
847    /// `table-<name>` counterpart). An unrecognized value yields the default.
848    fn from_attr_value(value: &str) -> Self;
849}
850
851impl TableAttributeValue for Frame {
852    fn from_attr_value(value: &str) -> Self {
853        match value.trim() {
854            // `topbot` is the older synonym for `ends`.
855            "ends" | "topbot" => Frame::Ends,
856            "sides" => Frame::Sides,
857            "none" => Frame::None,
858            // `all` and any unrecognized value.
859            _ => Frame::All,
860        }
861    }
862}
863
864impl TableAttributeValue for Grid {
865    fn from_attr_value(value: &str) -> Self {
866        match value.trim() {
867            "rows" => Grid::Rows,
868            "cols" => Grid::Cols,
869            "none" => Grid::None,
870            // `all` and any unrecognized value.
871            _ => Grid::All,
872        }
873    }
874}
875
876impl TableAttributeValue for Stripes {
877    fn from_attr_value(value: &str) -> Self {
878        match value.trim() {
879            "even" => Stripes::Even,
880            "odd" => Stripes::Odd,
881            "all" => Stripes::All,
882            "hover" => Stripes::Hover,
883            // `none` and any unrecognized value.
884            _ => Stripes::None,
885        }
886    }
887}
888
889/// Resolve a table-level attribute ([`Frame`], [`Grid`], or [`Stripes`]).
890///
891/// An explicit attribute on the table (`attr_name`) wins; otherwise the
892/// document-level default (`doc_attr_name`) is consulted; otherwise the value
893/// falls back to the type's default.
894fn resolve_table_attribute<B: TableAttributeValue>(
895    metadata: &BlockMetadata<'_>,
896    parser: &Parser,
897    attr_name: &str,
898    doc_attr_name: &str,
899) -> B {
900    if let Some(attr) = metadata
901        .attrlist
902        .as_ref()
903        .and_then(|a| a.named_attribute(attr_name))
904    {
905        B::from_attr_value(attr.value())
906    } else if let InterpretedValue::Value(value) = parser.attribute_value(doc_attr_name) {
907        B::from_attr_value(&value)
908    } else {
909        B::default()
910    }
911}
912
913/// Resolve the [`DataFormat`] of a table.
914///
915/// An explicit, recognized `format` attribute (`psv`, `csv`, `tsv`, or `dsv`)
916/// always wins. Otherwise the lead character of the delimiter selects the
917/// format via its shorthand — `,===` is CSV and `:===` is DSV — and any other
918/// delimiter (`|===`, `!===`) is PSV.
919fn resolve_data_format(metadata: &BlockMetadata<'_>, delimiter_text: &str) -> DataFormat {
920    if let Some(attr) = metadata
921        .attrlist
922        .as_ref()
923        .and_then(|a| a.named_attribute("format"))
924    {
925        match attr.value().trim() {
926            "psv" => return DataFormat::Psv,
927            "csv" => return DataFormat::Csv,
928            "tsv" => return DataFormat::Tsv,
929            "dsv" => return DataFormat::Dsv,
930            // An unrecognized format value falls through to the shorthand (or
931            // the PSV default).
932            _ => {}
933        }
934    }
935
936    match delimiter_text.as_bytes().first() {
937        Some(b',') => DataFormat::Csv,
938        Some(b':') => DataFormat::Dsv,
939        _ => DataFormat::Psv,
940    }
941}
942
943/// Resolve the cell separator for a table.
944///
945/// Each [`DataFormat`] supplies a default separator: PSV uses the vertical bar
946/// (`|`), except inside an AsciiDoc table cell — a nested, standalone document
947/// — where it defaults to the exclamation mark (`!`) so a nested table is
948/// distinguished from the `|`-separated table that encloses it; CSV defaults to
949/// a comma (`,`), TSV to a tab, and DSV to a colon (`:`). An explicit
950/// `separator` attribute on the table overrides the default; an empty
951/// `separator` value (e.g. `[separator=]`) falls back to the default. The
952/// two-character sequence `\t` in the attribute value is interpreted as a tab,
953/// so a tab-separated table can be written `[format=csv,separator=\t]`.
954fn resolve_separator(
955    metadata: &BlockMetadata<'_>,
956    parser: &Parser,
957    data_format: DataFormat,
958) -> String {
959    let default = match data_format {
960        DataFormat::Psv => {
961            if parser.nested_document_depth > 0 {
962                "!"
963            } else {
964                "|"
965            }
966        }
967        DataFormat::Csv => ",",
968        DataFormat::Tsv => "\t",
969        DataFormat::Dsv => ":",
970    };
971
972    metadata
973        .attrlist
974        .as_ref()
975        .and_then(|a| a.named_attribute("separator"))
976        .map(|attr| attr.value())
977        .filter(|value| !value.is_empty())
978        // The author writes a literal tab as the escape sequence `\t`.
979        .map(|value| value.replace("\\t", "\t"))
980        .unwrap_or_else(|| default.to_string())
981}
982
983/// Finalize a table's columns once the column count is known.
984///
985/// When the `cols` attribute supplied columns (`cols_attr` is non-empty) they
986/// are used as-is; otherwise `ncols` default columns are created. When the
987/// table carries the `autowidth` option, every column is made autowidth
988/// regardless of the proportional width set on its specifier.
989fn finalize_columns(
990    cols_attr: Vec<TableColumn>,
991    ncols: usize,
992    autowidth: bool,
993) -> Vec<TableColumn> {
994    let mut columns = if cols_attr.is_empty() {
995        (0..ncols).map(|_| TableColumn::default()).collect()
996    } else {
997        cols_attr
998    };
999
1000    if autowidth {
1001        for column in columns.iter_mut() {
1002            column.autowidth = true;
1003        }
1004    }
1005
1006    columns
1007}
1008
1009/// The inputs shared by the PSV and data-format table-body builders.
1010struct TableBody<'src> {
1011    /// The region between the opening and closing delimiters.
1012    inside: Span<'src>,
1013
1014    /// The resolved cell separator.
1015    separator: String,
1016
1017    /// Columns parsed from the `cols` attribute (empty when the attribute is
1018    /// absent, in which case the column count is implicit).
1019    cols_attr: Vec<TableColumn>,
1020
1021    /// Whether the table carries the `autowidth` option.
1022    autowidth: bool,
1023
1024    /// Whether the first row is a header row.
1025    has_header: bool,
1026}
1027
1028/// Build the columns and rows of a PSV (prefix-separated values) table.
1029///
1030/// The column count comes from the `cols` attribute (`cols_attr`) or, when that
1031/// is absent, from the number of column slots in the first non-empty line.
1032/// Cells are then scanned in document order and partitioned into rows by
1033/// walking the grid: a cell's span (colspan/rowspan) governs how many column
1034/// slots it occupies, so a column-spanning cell fills its row with fewer cells
1035/// and a row-spanning cell carries its columns down into the rows below.
1036///
1037/// This mirrors Asciidoctor's grid walk. `active_rowspans[k]` records the
1038/// number of column slots that cells from earlier rows occupy in the row `k`
1039/// steps ahead of the one being filled; a row closes once its own cells'
1040/// colspans plus the slots carried into it (`active_rowspans[0]`) reach
1041/// `ncols`. A cell whose span pushes the row *past* `ncols` overruns the grid:
1042/// the whole overrunning row is dropped (with a warning), again matching
1043/// Asciidoctor. A row whose columns are entirely pre-filled by carried slots
1044/// has no cells of its own to close it, so the next cell overruns and is
1045/// dropped together with that pre-filled row. A duplicated cell (`<n>*`) is
1046/// expanded into `<n>` independent cells — each carrying the original's
1047/// content, alignment, and style — before the grid walk, so each clone occupies
1048/// its own column slot exactly like an ordinary cell. A duplication factor of
1049/// zero drops the cell entirely.
1050fn build_psv_table<'src>(
1051    body: TableBody<'src>,
1052    parser: &mut Parser,
1053    warnings: &mut Vec<Warning<'src>>,
1054) -> (Vec<TableColumn>, Vec<TableRow<'src>>) {
1055    let TableBody {
1056        inside,
1057        separator,
1058        cols_attr,
1059        autowidth,
1060        has_header,
1061    } = body;
1062
1063    let separator = separator.as_str();
1064
1065    // When the column count is implicit, it is the number of column slots in the
1066    // first non-empty line: a cell that spans columns (`<n>+`) counts as `<n>`
1067    // slots, not one, and a cell duplicated `<n>` times (`<n>*`) counts as `<n>`
1068    // single-column slots (one per clone).
1069    let first_line_cells: usize =
1070        scan_cells(inside.discard_empty_lines().take_line().item, separator)
1071            .0
1072            .iter()
1073            .map(|c| c.spec.colspan.max(1) * c.spec.repeat.min(MAX_DUPLICATION_FACTOR))
1074            .sum();
1075
1076    let ncols = if cols_attr.is_empty() {
1077        first_line_cells
1078    } else {
1079        cols_attr.len()
1080    };
1081
1082    let columns = finalize_columns(cols_attr, ncols, autowidth);
1083
1084    let (raw_cells, recovered_first_cell) = scan_cells(inside, separator);
1085    if let Some(source) = recovered_first_cell {
1086        warnings.push(Warning {
1087            source,
1088            warning: WarningType::TableMissingLeadingSeparator,
1089        });
1090    }
1091
1092    let raw_cells = expand_duplicates(raw_cells);
1093
1094    // A table can never have more rows than it has cells, so a row span is
1095    // clamped to the cell count for the `active_rowspans` bookkeeping below: a
1096    // larger span carries into rows that can't exist and so has no additional
1097    // layout effect. The clamp also bounds the `active_rowspans` allocation, so a
1098    // hostile specifier such as `.1000000000+` can't trigger a multi-gigabyte
1099    // allocation. (The cell's reported [`rowspan`] keeps the literal parsed
1100    // value, matching Asciidoctor.)
1101    //
1102    // [`rowspan`]: TableCell::rowspan
1103    let max_rowspan = raw_cells.len().saturating_add(1);
1104
1105    let mut raw_rows: Vec<Vec<RawCell<'src>>> = vec![];
1106
1107    if ncols > 0 {
1108        // A queue: each completed row consumes the slots carried into it from the
1109        // front (`pop_front`), while a multi-row cell reserves slots in the rows it
1110        // extends into via the back. `VecDeque` keeps both ends O(1); a `Vec` would
1111        // pay an O(n) shift on every `remove(0)`.
1112        let mut active_rowspans: VecDeque<usize> = VecDeque::from([0]);
1113        let mut column_visits = 0usize;
1114        let mut current_row: Vec<RawCell<'src>> = vec![];
1115
1116        for raw in raw_cells {
1117            let colspan = raw.spec.colspan.max(1);
1118            let rowspan = raw.spec.rowspan.max(1).min(max_rowspan);
1119
1120            // A cell that spans more than one row reserves `colspan` slots in
1121            // each of the rows it extends into (but not its own row).
1122            if rowspan > 1 {
1123                if active_rowspans.len() < rowspan {
1124                    active_rowspans.resize(rowspan, 0);
1125                }
1126                for slot in active_rowspans.iter_mut().take(rowspan).skip(1) {
1127                    *slot += colspan;
1128                }
1129            }
1130
1131            column_visits += colspan;
1132            let cell_source = raw.content;
1133            current_row.push(raw);
1134
1135            // The slots carried into the current row are `active_rowspans[0]`; the
1136            // deque is never empty here, so the fallback is unreachable.
1137            let carried = active_rowspans.front().copied().unwrap_or(0);
1138            let effective = column_visits + carried;
1139            if effective >= ncols {
1140                if effective == ncols {
1141                    raw_rows.push(std::mem::take(&mut current_row));
1142                } else {
1143                    // Overrun: this cell's span pushes the row past `ncols`.
1144                    // Discard the whole row so the remaining cells stay aligned to
1145                    // the grid.
1146                    current_row.clear();
1147                    warnings.push(Warning {
1148                        source: cell_source,
1149                        warning: WarningType::TableCellExceedsColumnCount,
1150                    });
1151                }
1152                column_visits = 0;
1153                active_rowspans.pop_front();
1154                if active_rowspans.is_empty() {
1155                    active_rowspans.push_back(0);
1156                }
1157            }
1158        }
1159
1160        // If the table ends mid-row, the cells accumulated since the last
1161        // complete row never filled `ncols`. Matching Asciidoctor's
1162        // `close_table`, that incomplete row is dropped and an error is logged
1163        // against its last cell.
1164        if let Some(last) = current_row.last() {
1165            warnings.push(Warning {
1166                source: last.content,
1167                warning: WarningType::TableDroppingIncompleteRowAtEndOfTable,
1168            });
1169        }
1170    }
1171
1172    // Each cell is processed according to the style of the column it falls in. A
1173    // cell's column is its ordinal position within its row (matching Asciidoctor,
1174    // which assigns the column by cell count, not grid slot). The header row
1175    // (when present) is the first row and is always processed as plain header
1176    // content, regardless of the column styles, so that a style operator doesn't
1177    // affect the header row.
1178    let mut rows: Vec<TableRow<'src>> = Vec::with_capacity(raw_rows.len());
1179    for (row_idx, raw_row) in raw_rows.into_iter().enumerate() {
1180        let is_header = has_header && row_idx == 0;
1181        let mut cells = Vec::with_capacity(raw_row.len());
1182        for (col_idx, raw) in raw_row.into_iter().enumerate() {
1183            let column = columns.get(col_idx).cloned().unwrap_or_default();
1184            cells.push(TableCell::parse(
1185                raw, &column, is_header, separator, parser, warnings,
1186            ));
1187        }
1188        rows.push(TableRow { cells });
1189    }
1190
1191    (columns, rows)
1192}
1193
1194/// Build the columns and rows of a delimiter-separated table (CSV, TSV, or
1195/// DSV).
1196///
1197/// The body is split into a flat list of [fields](DataField) by the format's
1198/// parser, then flowed into fixed-width rows. The column count comes from the
1199/// `cols` attribute (`cols_attr`) or, when that is absent, from the number of
1200/// fields in the first row. Because a data cell carries no span, the fields are
1201/// simply chunked `ncols` at a time; any fields left over after the last
1202/// complete row are dropped ("extra cells at the end of the last row get
1203/// dropped"). The first row is the header when `has_header` is set.
1204fn build_data_table<'src>(
1205    body: TableBody<'src>,
1206    data_format: DataFormat,
1207    parser: &mut Parser,
1208    warnings: &mut Vec<Warning<'src>>,
1209) -> (Vec<TableColumn>, Vec<TableRow<'src>>) {
1210    let TableBody {
1211        inside,
1212        separator,
1213        cols_attr,
1214        autowidth,
1215        has_header,
1216    } = body;
1217
1218    let separator = separator.as_str();
1219
1220    // DSV is parsed by its own, simpler rules; CSV and TSV share their rules and
1221    // differ only in the default separator (resolved by the caller). PSV never
1222    // reaches this builder.
1223    let (fields, first_row_len) = if data_format == DataFormat::Dsv {
1224        parse_dsv_fields(inside, separator)
1225    } else {
1226        parse_csv_fields(inside, separator, warnings)
1227    };
1228
1229    let ncols = if cols_attr.is_empty() {
1230        first_row_len
1231    } else {
1232        cols_attr.len()
1233    };
1234
1235    let columns = finalize_columns(cols_attr, ncols, autowidth);
1236
1237    // Integer division drops any partial trailing row; `checked_div` yields zero
1238    // rows when there are no columns.
1239    let nrows = fields.len().checked_div(ncols).unwrap_or(0);
1240    let mut rows: Vec<TableRow<'src>> = Vec::with_capacity(nrows);
1241    let mut fields = fields.into_iter();
1242    for row_idx in 0..nrows {
1243        let is_header = has_header && row_idx == 0;
1244        let mut cells = Vec::with_capacity(ncols);
1245        for col_idx in 0..ncols {
1246            // `nrows * ncols <= fields.len()`, so the iterator always yields.
1247            let Some(field) = fields.next() else { break };
1248            let column = columns.get(col_idx).cloned().unwrap_or_default();
1249            cells.push(TableCell::parse_data(
1250                field, &column, is_header, parser, warnings,
1251            ));
1252        }
1253        rows.push(TableRow { cells });
1254    }
1255
1256    (columns, rows)
1257}
1258
1259/// A single field of a delimiter-separated (CSV, TSV, or DSV) table, as located
1260/// by [`parse_csv_fields`] or [`parse_dsv_fields`].
1261///
1262/// `content` is the field's value span with surrounding whitespace already
1263/// stripped. `replacement` holds the value after quote or escape processing
1264/// when it differs from `content` (a CSV value with a doubled-quote escape, or
1265/// a DSV value with a backslash-escaped separator); it is `None` when the span
1266/// is the verbatim value.
1267struct DataField<'src> {
1268    content: Span<'src>,
1269    replacement: Option<String>,
1270}
1271
1272/// Parse a CSV/TSV region into its [fields](DataField), returning them in
1273/// document order together with the number of fields in the first row.
1274///
1275/// The rules, loosely based on RFC 4180: empty lines are skipped; whitespace
1276/// surrounding each value is stripped; a value may be enclosed in double
1277/// quotes, within which the separator and newlines are literal and a double
1278/// quote is written by doubling it (`""`). A newline that is not inside a
1279/// quoted value ends the row. The fields are returned flat; the caller flows
1280/// them into rows.
1281///
1282/// This mirrors Asciidoctor's `Table::ParserContext`: a separator or newline is
1283/// a cell boundary only when the text accumulated since the previous boundary
1284/// has no [unclosed quote](has_unclosed_quotes); otherwise it is part of the
1285/// value. As a result a value whose opening quote is never properly closed (or
1286/// that has trailing characters after its closing quote) keeps its quotes and
1287/// absorbs the following separators, rather than being treated as enclosed.
1288fn parse_csv_fields<'src>(
1289    region: Span<'src>,
1290    separator: &str,
1291    warnings: &mut Vec<Warning<'src>>,
1292) -> (Vec<DataField<'src>>, usize) {
1293    let data = region.data();
1294    let n = data.len();
1295    let sep_len = separator.len().max(1);
1296    let at = |k: usize| data.as_bytes().get(k).copied();
1297    let starts_with_sep = |pos: usize| data.get(pos..).is_some_and(|s| s.starts_with(separator));
1298
1299    let mut fields: Vec<DataField<'src>> = vec![];
1300    let mut first_row_len = 0usize;
1301    let mut first_row_done = false;
1302    let mut fields_in_row = 0usize;
1303
1304    // The raw text of the cell currently being accumulated runs from `cell_start`
1305    // to the next boundary.
1306    let mut cell_start = 0usize;
1307    let mut i = 0usize;
1308
1309    while i <= n {
1310        let at_eof = i == n;
1311        let at_sep = !at_eof && starts_with_sep(i);
1312        let at_nl = !at_eof && at(i) == Some(b'\n');
1313
1314        if !(at_eof || at_sep || at_nl) {
1315            i += 1;
1316            continue;
1317        }
1318
1319        let raw = data.get(cell_start..i).unwrap_or_default();
1320
1321        // A separator or newline that falls inside an unclosed quoted value is
1322        // part of the value, not a boundary; absorb it and keep scanning.
1323        if !at_eof && has_unclosed_quotes(raw) {
1324            i += if at_sep { sep_len } else { 1 };
1325            continue;
1326        }
1327
1328        // A wholly blank physical line (or trailing blank text at the end of the
1329        // region) between rows is skipped rather than emitted as an empty cell. A
1330        // blank cell that follows a separator on a populated line is kept.
1331        let blank_skip = (at_nl || at_eof) && fields_in_row == 0 && raw.trim().is_empty();
1332        if !blank_skip {
1333            fields.push(make_csv_field(region, cell_start, i, warnings));
1334            fields_in_row += 1;
1335            if !first_row_done {
1336                first_row_len = fields_in_row;
1337            }
1338        }
1339
1340        if at_eof {
1341            break;
1342        }
1343
1344        if at_nl {
1345            // The newline ends the row. The first populated row fixes the implicit
1346            // column count.
1347            if fields_in_row > 0 {
1348                first_row_done = true;
1349            }
1350            fields_in_row = 0;
1351            cell_start = i + 1;
1352            i += 1;
1353        } else {
1354            cell_start = i + sep_len;
1355            i += sep_len;
1356        }
1357    }
1358
1359    (fields, first_row_len)
1360}
1361
1362/// Build a CSV/TSV [field](DataField) from the byte range `start..end`,
1363/// applying Asciidoctor's `close_cell` value processing.
1364///
1365/// The value is stripped of surrounding whitespace; then, if it is enclosed in
1366/// double quotes, the quotes are removed and the inner value is stripped again,
1367/// so the field's [content](DataField::content) span points at the actual value
1368/// (this matters for an AsciiDoc cell, which parses that span). Finally any run
1369/// of consecutive double quotes is collapsed to one (so an escaped `""` becomes
1370/// a single `"`). A value that is not enclosed (no leading quote, or trailing
1371/// characters after the closing quote) keeps its quotes and is only collapsed.
1372///
1373/// A lone double quote is an unclosed quoted value: it logs an error and the
1374/// cell is set to empty (matching Asciidoctor).
1375fn make_csv_field<'src>(
1376    region: Span<'src>,
1377    start: usize,
1378    end: usize,
1379    warnings: &mut Vec<Warning<'src>>,
1380) -> DataField<'src> {
1381    let trimmed = trim_surrounding_whitespace(region.slice(start..end));
1382    let data = trimmed.data();
1383
1384    let content = if data == "\"" {
1385        warnings.push(Warning {
1386            source: trimmed,
1387            warning: WarningType::TableCsvDataHasUnclosedQuote,
1388        });
1389        trimmed.slice(0..0)
1390    } else if data.len() >= 2 && data.starts_with('"') && data.ends_with('"') {
1391        trim_surrounding_whitespace(trimmed.slice(1..data.len() - 1))
1392    } else {
1393        trimmed
1394    };
1395
1396    let value = squeeze_quotes(content.data());
1397    let replacement = (value != content.data()).then_some(value);
1398
1399    DataField {
1400        content,
1401        replacement,
1402    }
1403}
1404
1405/// Collapse every run of consecutive double quotes to a single double quote,
1406/// matching Ruby's `String#squeeze('"')`.
1407///
1408/// Note: the `continue` intentionally leaves `prev_quote` set, so a run of
1409/// *N ≥ 2* consecutive `"` collapses to a single `"` (e.g. `""""` -> `"`), not
1410/// to pairs. This deliberately matches Asciidoctor rather than strict RFC 4180,
1411/// under which only `""` is a double-quote escape — don't "fix" it to a
1412/// two-character collapse without also changing Asciidoctor.
1413fn squeeze_quotes(text: &str) -> String {
1414    let mut out = String::with_capacity(text.len());
1415    let mut prev_quote = false;
1416    for c in text.chars() {
1417        if c == '"' {
1418            if prev_quote {
1419                continue;
1420            }
1421            prev_quote = true;
1422        } else {
1423            prev_quote = false;
1424        }
1425        out.push(c);
1426    }
1427    out
1428}
1429
1430/// Determine whether `buffer` (the cell text accumulated so far) holds an
1431/// unclosed double quote, a direct port of Asciidoctor's
1432/// `Table::ParserContext#buffer_has_unclosed_quotes?`.
1433///
1434/// Only a value that begins with a double quote can be "quoted"; for any other
1435/// value embedded quotes are literal and this returns `false`. A leading quote
1436/// is unclosed until a matching trailing quote appears (accounting for escaped
1437/// `""` pairs).
1438///
1439/// Note: the escaped-pair collapse (`replace("\"\"", "")`) runs before the
1440/// start/end check, so `"""` collapses to a single `"` and is reported
1441/// *closed*. Strict RFC 4180 would read `"""` as an unclosed field (open quote
1442/// plus escaped `""` + missing close); this matches Asciidoctor's
1443/// `buffer_has_unclosed_quotes?` instead, so the divergence is intentional.
1444fn has_unclosed_quotes(buffer: &str) -> bool {
1445    let record = buffer.trim();
1446
1447    if record == "\"" {
1448        return true;
1449    }
1450
1451    if !record.starts_with('"') {
1452        return false;
1453    }
1454
1455    let trailing_quote = record.ends_with('"');
1456    if (trailing_quote && record.ends_with("\"\"")) || record.starts_with("\"\"") {
1457        let collapsed = record.replace("\"\"", "");
1458        collapsed.starts_with('"') && !collapsed.ends_with('"')
1459    } else {
1460        !trailing_quote
1461    }
1462}
1463
1464/// Parse a DSV region into its [fields](DataField), returning them in document
1465/// order together with the number of fields in the first row.
1466///
1467/// Each non-empty line is a row. Whitespace surrounding each value is stripped,
1468/// and the separator can be included in a value by escaping it with a single
1469/// backslash (`\:`). An enclosing character is not recognized.
1470fn parse_dsv_fields<'src>(region: Span<'src>, separator: &str) -> (Vec<DataField<'src>>, usize) {
1471    let data = region.data();
1472    let n = data.len();
1473    let sep_len = separator.len().max(1);
1474    let escaped = format!("\\{separator}");
1475    let at = |k: usize| data.as_bytes().get(k).copied();
1476
1477    let mut fields: Vec<DataField<'src>> = vec![];
1478    let mut first_row_len = 0usize;
1479    let mut row_count = 0usize;
1480    let mut i = 0usize;
1481
1482    while i < n {
1483        let mut line_end = i;
1484        while line_end < n && at(line_end) != Some(b'\n') {
1485            line_end += 1;
1486        }
1487
1488        if data.get(i..line_end).unwrap_or("").trim().is_empty() {
1489            i = if line_end < n { line_end + 1 } else { line_end };
1490            continue;
1491        }
1492
1493        let in_line = |pos: usize| {
1494            data.get(pos..line_end)
1495                .is_some_and(|s| s.starts_with(separator))
1496        };
1497
1498        let mut fields_in_row = 0usize;
1499        let mut field_start = i;
1500        let mut p = i;
1501
1502        while p < line_end {
1503            // A backslash that escapes the separator (`\:`) is not a boundary;
1504            // skip past both so the separator stays in the value.
1505            if at(p) == Some(b'\\')
1506                && data
1507                    .get(p + 1..line_end)
1508                    .is_some_and(|s| s.starts_with(separator))
1509            {
1510                p += 1 + sep_len;
1511                continue;
1512            }
1513
1514            if in_line(p) {
1515                fields.push(make_dsv_field(region, field_start, p, &escaped, separator));
1516                fields_in_row += 1;
1517                p += sep_len;
1518                field_start = p;
1519                continue;
1520            }
1521
1522            p += 1;
1523        }
1524
1525        // The final field of the line runs to the line end.
1526        fields.push(make_dsv_field(
1527            region,
1528            field_start,
1529            line_end,
1530            &escaped,
1531            separator,
1532        ));
1533        fields_in_row += 1;
1534
1535        if row_count == 0 {
1536            first_row_len = fields_in_row;
1537        }
1538        row_count += 1;
1539
1540        i = if line_end < n { line_end + 1 } else { line_end };
1541    }
1542
1543    (fields, first_row_len)
1544}
1545
1546/// Build a DSV [field](DataField) from the byte range `start..end`, unescaping
1547/// any backslash-escaped separators (`escaped`, e.g. `\:`) into the bare
1548/// separator.
1549fn make_dsv_field<'src>(
1550    region: Span<'src>,
1551    start: usize,
1552    end: usize,
1553    escaped: &str,
1554    separator: &str,
1555) -> DataField<'src> {
1556    let trimmed = trim_surrounding_whitespace(region.slice(start..end));
1557    let replacement = if trimmed.data().contains(escaped) {
1558        Some(trimmed.data().replace(escaped, separator))
1559    } else {
1560        None
1561    };
1562
1563    DataField {
1564        content: trimmed,
1565        replacement,
1566    }
1567}
1568
1569/// Process a cell's content according to its [style](ColumnStyle), shared by
1570/// the PSV and data-format cell builders.
1571///
1572/// `trimmed` is the cell's content span with surrounding whitespace already
1573/// removed. `replacement` is the pre-filtered value (an escaped separator
1574/// unescaped, or a CSV/DSV value after quote/escape processing) when it differs
1575/// from `trimmed`; it is ignored for the [`AsciiDoc`](ColumnStyle::AsciiDoc)
1576/// style, which parses `trimmed` verbatim as a nested document. Every other
1577/// style produces inline [`Simple`](TableCellContent::Simple) content with the
1578/// verbatim substitution group for [`Literal`](ColumnStyle::Literal) and the
1579/// normal group otherwise.
1580fn process_content<'src>(
1581    trimmed: Span<'src>,
1582    replacement: Option<String>,
1583    style: ColumnStyle,
1584    parser: &mut Parser,
1585    warnings: &mut Vec<Warning<'src>>,
1586) -> TableCellContent<'src> {
1587    if style == ColumnStyle::AsciiDoc {
1588        // The AsciiDoc style effectively creates a nested, standalone AsciiDoc
1589        // document in the cell. It inherits the parent document's attributes, but
1590        // any attribute it defines is scoped to the cell and must not leak back
1591        // into the parent. Snapshot the attribute set before parsing and restore
1592        // it afterward to enforce that boundary (matching Asciidoctor, where a
1593        // `:foo:` set inside a cell is not visible after the table).
1594        let saved_attributes = parser.attribute_values.clone();
1595
1596        // An attribute that is set in the parent document cannot be modified
1597        // inside the cell. Lock every inherited attribute that currently holds a
1598        // value for the duration of the cell (other than the handful of
1599        // exceptions the spec carves out), so a body assignment to one of them is
1600        // ignored. An attribute that is unset in the parent is not locked: the
1601        // cell may assign it (matching Asciidoctor, which here diverges from the
1602        // spec's "set or explicitly unset" wording). The lock set is saved and
1603        // restored so it applies only within the cell and nests correctly.
1604        // An attribute set in the parent is locked, as is one hard set or unset
1605        // through the API (its modification context is `ApiOnly`) even though it
1606        // is unset — matching Asciidoctor, where an API-controlled attribute can
1607        // never be overridden in a cell. An attribute merely unset in the parent
1608        // document is not locked, so the cell may assign it.
1609        let saved_locks = parser.locked_attribute_names.clone();
1610        for (name, value) in saved_attributes.iter() {
1611            let api_locked = value.modification_context == ModificationContext::ApiOnly;
1612            if (!matches!(value.value, InterpretedValue::Unset) || api_locked)
1613                && !ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES.contains(&name.as_str())
1614            {
1615                parser.locked_attribute_names.insert(name.clone());
1616            }
1617        }
1618
1619        // The modifiable attributes may always be changed inside a cell, even
1620        // when the parent or the API set them with a restrictive modification
1621        // context. Relax their context for the duration of the cell so a body
1622        // assignment is honored; the snapshot restore reverts it afterward.
1623        for name in ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES {
1624            if let Some(attr) = parser.attribute_values.get_mut(*name) {
1625                attr.modification_context = ModificationContext::Anywhere;
1626            }
1627        }
1628
1629        // A cell does not inherit the parent's doctype; it resets to the default
1630        // (`article`). The cell body may still set its own doctype, and the
1631        // derived `backend-html5-doctype-*` attribute is refreshed to match.
1632        parser.force_doctype("article");
1633
1634        // A cell whose content holds a preprocessor directive (an `include::`)
1635        // is parsed from an owned, expanded source the cell carries; every other
1636        // cell is parsed in place from the parent document's source, which keeps
1637        // its spans (and line numbers) and avoids a copy.
1638        let cell = if content_has_directive(trimmed.data()) {
1639            let (expanded, _source_map) = preprocess(trimmed.data(), parser);
1640            let owned = OwnedCell::new(expanded, |source| {
1641                // Warnings from the owned parse borrow the owned source and so
1642                // cannot escape it; the include path is rare and currently
1643                // warning-free, so they are dropped here. The `debug_assert`
1644                // turns any future warning added to this path into a loud test
1645                // failure rather than a silent loss.
1646                let mut owned_warnings: Vec<Warning<'_>> = vec![];
1647                let (title, inline, blocks) =
1648                    parse_asciidoc_cell_body(Span::new(source), parser, &mut owned_warnings);
1649
1650                debug_assert!(
1651                    owned_warnings.is_empty(),
1652                    "warnings from an include-expanded AsciiDoc cell are dropped; \
1653                     propagate them before adding any to this path"
1654                );
1655
1656                OwnedCellInner {
1657                    title,
1658                    inline,
1659                    blocks,
1660                }
1661            });
1662            AsciiDocCell::Owned(Arc::new(owned))
1663        } else {
1664            let (title, inline, blocks) = parse_asciidoc_cell_body(trimmed, parser, warnings);
1665            AsciiDocCell::Borrowed(BorrowedCell {
1666                title,
1667                inline,
1668                blocks,
1669            })
1670        };
1671
1672        parser.locked_attribute_names = saved_locks;
1673        parser.attribute_values = saved_attributes;
1674        TableCellContent::AsciiDoc(cell)
1675    } else {
1676        let mut content = match replacement {
1677            Some(replacement) => Content::from_filtered(trimmed, replacement),
1678            None => Content::from(trimmed),
1679        };
1680
1681        let substitutions = if style == ColumnStyle::Literal {
1682            SubstitutionGroup::Verbatim
1683        } else {
1684            SubstitutionGroup::Normal
1685        };
1686        substitutions.apply(&mut content, parser, None);
1687
1688        TableCellContent::Simple(content)
1689    }
1690}
1691
1692/// Parses the body of an AsciiDoc table cell — a nested, standalone AsciiDoc
1693/// document — returning its (shown) title, whether its doctype is `inline`, and
1694/// its blocks.
1695///
1696/// A leading level-0 title line (`= Title`) is the nested document's title
1697/// rather than a section, so it is split off and rendered here (a level-0
1698/// heading is otherwise rejected in block parsing). The render-time decisions
1699/// (`inline`, and whether the title is shown) depend on the cell's now-mutated
1700/// attribute state, so they are resolved before the caller restores the
1701/// parent's attribute snapshot.
1702fn parse_asciidoc_cell_body<'src>(
1703    content: Span<'src>,
1704    parser: &mut Parser,
1705    warnings: &mut Vec<Warning<'src>>,
1706) -> (Option<String>, bool, Vec<Block<'src>>) {
1707    let first_line = content.take_line();
1708    let (title_source, body) = if first_line.item.data().starts_with("= ") {
1709        (
1710            Some(first_line.item.discard(2).discard_whitespace()),
1711            first_line.after,
1712        )
1713    } else {
1714        (None, content)
1715    };
1716
1717    // Mark that we are inside an AsciiDoc cell (a nested document) for the
1718    // duration of the parse, so a table found within defaults its cell separator
1719    // to `!` rather than `|` (matching Asciidoctor's `Document#nested?`).
1720    parser.nested_document_depth += 1;
1721    let mut maw = parse_blocks_until(body, |_| false, parser);
1722    parser.nested_document_depth -= 1;
1723    warnings.append(&mut maw.warnings);
1724
1725    let inline = matches!(
1726        parser.attribute_value("doctype"),
1727        InterpretedValue::Value(ref v) if v == "inline"
1728    );
1729
1730    let title = if parser.resolve_show_title(true) {
1731        title_source.map(|span| {
1732            let mut content = Content::from(span);
1733            SubstitutionGroup::Header.apply(&mut content, parser, None);
1734            content.rendered().to_string()
1735        })
1736    } else {
1737        None
1738    };
1739
1740    (title, inline, maw.item.item)
1741}
1742
1743/// Returns `true` when the cell content holds an `include::` preprocessor
1744/// directive at the start of a line, which must be expanded before the cell is
1745/// parsed.
1746fn content_has_directive(content: &str) -> bool {
1747    content.starts_with("include::") || content.contains("\ninclude::")
1748}
1749
1750/// A row of cells in a [`TableBlock`].
1751#[derive(Clone, Debug, Eq, PartialEq)]
1752pub struct TableRow<'src> {
1753    cells: Vec<TableCell<'src>>,
1754}
1755
1756impl<'src> TableRow<'src> {
1757    /// Returns the cells in this row.
1758    pub fn cells(&self) -> &[TableCell<'src>] {
1759        &self.cells
1760    }
1761}
1762
1763/// A single cell in a [`TableBlock`].
1764#[derive(Clone, Debug, Eq, PartialEq)]
1765pub struct TableCell<'src> {
1766    h_align: HorizontalAlignment,
1767    v_align: VerticalAlignment,
1768    style: ColumnStyle,
1769    colspan: usize,
1770    rowspan: usize,
1771    content: TableCellContent<'src>,
1772    source: Span<'src>,
1773}
1774
1775impl<'src> TableCell<'src> {
1776    /// Build a cell from the raw (untrimmed) span of its content, processing it
1777    /// according to the [style](ColumnStyle) of the `column` the cell belongs
1778    /// to.
1779    ///
1780    /// The cell's horizontal and vertical alignment come from the alignment
1781    /// operators on its [specifier](RawCell::spec) when present; otherwise they
1782    /// are inherited from the column. Likewise, a style operator on the cell's
1783    /// specifier overrides the column's [style](ColumnStyle); with no cell
1784    /// style operator, the cell is processed with the column's style. A
1785    /// header cell (`is_header`) is always processed as plain header
1786    /// content, regardless of any style operator on the column or the cell.
1787    ///
1788    /// Leading and trailing whitespace is always stripped. For every style but
1789    /// [`AsciiDoc`](ColumnStyle::AsciiDoc) the cell holds inline
1790    /// [`Content`](TableCellContent::Simple): escaped cell separators (the
1791    /// table's `separator` character preceded by a backslash, e.g. `\|`) are
1792    /// unescaped and substitutions are applied — the verbatim group for
1793    /// [`Literal`](ColumnStyle::Literal), the normal group otherwise. An
1794    /// [`AsciiDoc`](ColumnStyle::AsciiDoc) cell instead parses its content as a
1795    /// nested sequence of [blocks](TableCellContent::AsciiDoc).
1796    fn parse(
1797        raw: RawCell<'src>,
1798        column: &TableColumn,
1799        is_header: bool,
1800        separator: &str,
1801        parser: &mut Parser,
1802        warnings: &mut Vec<Warning<'src>>,
1803    ) -> Self {
1804        // A cell's own alignment operator overrides the column's alignment; with
1805        // no operator, the cell inherits the column's alignment.
1806        let h_align = raw.spec.h_align.unwrap_or(column.h_align);
1807        let v_align = raw.spec.v_align.unwrap_or(column.v_align);
1808
1809        // A cell's own style operator overrides the column's style; with no
1810        // operator, the cell is processed with the column's style. The header
1811        // row is always processed as plain header content, so neither a column
1812        // nor a cell style operator ever affects a header cell.
1813        let style = if is_header {
1814            ColumnStyle::Default
1815        } else {
1816            raw.spec.style.unwrap_or(column.style)
1817        };
1818
1819        let trimmed = trim_cell_content(raw.content, style);
1820
1821        // An escaped cell separator (a backslash in front of the table's
1822        // separator, e.g. `\|` or `\!`) is unescaped to the bare separator. Only
1823        // the active separator is unescaped, so a `\|` in a `!`-separated table
1824        // is left untouched. The replacement is computed only for the inline
1825        // styles; an AsciiDoc cell parses its content verbatim (see
1826        // [`process_content`]).
1827        let escaped = format!("\\{separator}");
1828        let replacement = if style != ColumnStyle::AsciiDoc && trimmed.data().contains(&escaped) {
1829            Some(trimmed.data().replace(&escaped, separator))
1830        } else {
1831            None
1832        };
1833
1834        let content = process_content(trimmed, replacement, style, parser, warnings);
1835
1836        Self {
1837            h_align,
1838            v_align,
1839            style,
1840            colspan: raw.spec.colspan.max(1),
1841            rowspan: raw.spec.rowspan.max(1),
1842            content,
1843            // The cell's source begins at its content, immediately after the
1844            // separator (before any trimming), so the cell's reported line is
1845            // the separator's line.
1846            source: raw.content,
1847        }
1848    }
1849
1850    /// Build a cell from a [data field](DataField) of a delimiter-separated
1851    /// table (CSV, TSV, or DSV).
1852    ///
1853    /// Unlike a PSV cell, a data cell carries no per-cell specifier: its
1854    /// alignment and [style](ColumnStyle) come entirely from the `column`, and
1855    /// it always spans a single row and column. The separator escaping is
1856    /// handled by the format parser before this point, so the field already
1857    /// holds the extracted value (its [`replacement`](DataField::replacement),
1858    /// when present, is the value after quote/escape processing). A header cell
1859    /// (`is_header`) is processed as plain header content.
1860    fn parse_data(
1861        field: DataField<'src>,
1862        column: &TableColumn,
1863        is_header: bool,
1864        parser: &mut Parser,
1865        warnings: &mut Vec<Warning<'src>>,
1866    ) -> Self {
1867        let style = if is_header {
1868            ColumnStyle::Default
1869        } else {
1870            column.style
1871        };
1872
1873        let source = field.content;
1874        let content = process_content(field.content, field.replacement, style, parser, warnings);
1875
1876        Self {
1877            h_align: column.h_align,
1878            v_align: column.v_align,
1879            style,
1880            colspan: 1,
1881            rowspan: 1,
1882            content,
1883            source,
1884        }
1885    }
1886
1887    /// Returns the horizontal alignment of this cell's content.
1888    ///
1889    /// The alignment comes from a horizontal alignment operator (`<`, `>`, or
1890    /// `^`) on the cell's specifier, which overrides the column's alignment. A
1891    /// cell with no horizontal alignment operator inherits its column's
1892    /// [`h_align`](TableColumn::h_align).
1893    pub fn h_align(&self) -> HorizontalAlignment {
1894        self.h_align
1895    }
1896
1897    /// Returns the vertical alignment of this cell's content.
1898    ///
1899    /// The alignment comes from a vertical alignment operator (`.<`, `.>`, or
1900    /// `.^`) on the cell's specifier, which overrides the column's alignment. A
1901    /// cell with no vertical alignment operator inherits its column's
1902    /// [`v_align`](TableColumn::v_align).
1903    pub fn v_align(&self) -> VerticalAlignment {
1904        self.v_align
1905    }
1906
1907    /// Returns the [style](ColumnStyle) applied to this cell's content.
1908    ///
1909    /// The style comes from a style operator in the last position of the cell's
1910    /// specifier (`a`, `d`, `e`, `h`, `l`, `m`, or `s`), which overrides the
1911    /// column's style. A cell with no style operator inherits its column's
1912    /// [`style`](TableColumn::style). A header cell is always
1913    /// [`Default`](ColumnStyle::Default), because the header row ignores style
1914    /// operators on both column and cell specifiers.
1915    pub fn style(&self) -> ColumnStyle {
1916        self.style
1917    }
1918
1919    /// Returns the number of columns this cell spans.
1920    ///
1921    /// The span comes from a column span factor (`<n>`) or block span factor
1922    /// (`<n>.<n>`) in front of the span operator (`+`) on the cell's specifier.
1923    /// A cell with no column span factor spans a single column, so the default
1924    /// is `1`.
1925    pub fn colspan(&self) -> usize {
1926        self.colspan
1927    }
1928
1929    /// Returns the number of rows this cell spans.
1930    ///
1931    /// The span comes from a row span factor (`.<n>`) or block span factor
1932    /// (`<n>.<n>`) in front of the span operator (`+`) on the cell's specifier.
1933    /// A cell with no row span factor spans a single row, so the default is
1934    /// `1`.
1935    pub fn rowspan(&self) -> usize {
1936        self.rowspan
1937    }
1938
1939    /// Returns the interpreted content of this cell.
1940    pub fn content(&self) -> &TableCellContent<'src> {
1941        &self.content
1942    }
1943
1944    /// Resolves any deferred cross-references in this cell's content.
1945    fn resolve_references(
1946        &mut self,
1947        resolver: &dyn ReferenceResolver,
1948        renderer: &dyn InlineSubstitutionRenderer,
1949        warnings: &mut Vec<ReferenceWarning>,
1950    ) {
1951        match &mut self.content {
1952            TableCellContent::Simple(content) => {
1953                content.resolve_references(resolver, renderer, warnings);
1954            }
1955            TableCellContent::AsciiDoc(cell) => {
1956                cell.resolve_references(resolver, renderer, warnings);
1957            }
1958        }
1959    }
1960}
1961
1962impl<'src> HasSpan<'src> for TableCell<'src> {
1963    /// Returns the cell's source span, which begins at the cell's content
1964    /// immediately after its separator. Its [line](Span::line) is therefore the
1965    /// line on which the cell starts.
1966    fn span(&self) -> Span<'src> {
1967        self.source
1968    }
1969}
1970
1971/// The interpreted content of a [`TableCell`].
1972///
1973/// The variant is determined by the [style](ColumnStyle) of the cell's column:
1974/// an [`AsciiDoc`](ColumnStyle::AsciiDoc) column produces
1975/// [`AsciiDoc`](Self::AsciiDoc) content, and every other style produces
1976/// [`Simple`](Self::Simple) inline content.
1977#[derive(Clone, Debug, Eq, PartialEq)]
1978pub enum TableCellContent<'src> {
1979    /// Inline content: the cell's text after its substitutions (normal for most
1980    /// styles, verbatim for [`Literal`](ColumnStyle::Literal)) have been
1981    /// applied.
1982    Simple(Content<'src>),
1983
1984    /// Block content: the cell's text parsed as a nested, standalone AsciiDoc
1985    /// document. Produced by the [`AsciiDoc`](ColumnStyle::AsciiDoc) style.
1986    AsciiDoc(AsciiDocCell<'src>),
1987}
1988
1989/// The content of an [`AsciiDoc`](TableCellContent::AsciiDoc) table cell: a
1990/// nested, standalone AsciiDoc document.
1991///
1992/// Because the cell behaves like its own document, a few render-time decisions
1993/// depend on attribute state that is scoped to the cell and gone by the time
1994/// the document is rendered. They are therefore resolved while the cell is
1995/// parsed and captured here: whether the cell's nested document title is shown
1996/// (and its rendered text), and whether the cell's `doctype` is `inline` (in
1997/// which case a lone paragraph renders without the usual block wrapper).
1998///
1999/// A cell whose content has no preprocessor directives is parsed in place from
2000/// the parent document's source ([`Borrowed`](Self::Borrowed)). A cell that
2001/// expands an `include::` directive owns its preprocessed source
2002/// ([`Owned`](Self::Owned)); the owned store is shared behind an [`Arc`] so the
2003/// cell stays cheaply cloneable.
2004#[derive(Clone, Debug, Eq, PartialEq)]
2005pub enum AsciiDocCell<'src> {
2006    /// Parsed in place from the parent document's source.
2007    Borrowed(BorrowedCell<'src>),
2008
2009    /// Parsed from an owned, include-expanded source the cell carries.
2010    Owned(Arc<OwnedCell>),
2011}
2012
2013impl<'src> AsciiDocCell<'src> {
2014    /// Returns the cell's nested-document title, rendered to its display text.
2015    ///
2016    /// This is `Some` only when the cell began with a level-0 title line
2017    /// (`= Title`) *and* the cell's effective `showtitle`/`notitle` state means
2018    /// that title is shown; otherwise it is `None`.
2019    pub fn title(&self) -> Option<&str> {
2020        match self {
2021            Self::Borrowed(cell) => cell.title.as_deref(),
2022            Self::Owned(cell) => cell.borrow_dependent().title.as_deref(),
2023        }
2024    }
2025
2026    /// Returns `true` when the cell's `doctype` resolves to `inline`.
2027    ///
2028    /// An `inline` document renders a lone paragraph as bare inline content,
2029    /// without the enclosing block wrapper.
2030    pub fn is_inline(&self) -> bool {
2031        match self {
2032            Self::Borrowed(cell) => cell.inline,
2033            Self::Owned(cell) => cell.borrow_dependent().inline,
2034        }
2035    }
2036
2037    /// Returns the blocks parsed from the cell's content.
2038    pub fn blocks(&self) -> &[Block<'_>] {
2039        match self {
2040            Self::Borrowed(cell) => &cell.blocks,
2041            Self::Owned(cell) => &cell.borrow_dependent().blocks,
2042        }
2043    }
2044
2045    /// Resolves any deferred cross-references in the cell's blocks.
2046    fn resolve_references(
2047        &mut self,
2048        resolver: &dyn ReferenceResolver,
2049        renderer: &dyn InlineSubstitutionRenderer,
2050        warnings: &mut Vec<ReferenceWarning>,
2051    ) {
2052        match self {
2053            Self::Borrowed(cell) => {
2054                for block in &mut cell.blocks {
2055                    block.resolve_references(resolver, renderer, warnings);
2056                }
2057            }
2058
2059            // The owned store is shared behind an `Arc`, but references are
2060            // resolved immediately after parsing while the cell is still its sole
2061            // owner, so `get_mut` succeeds.
2062            Self::Owned(cell) => {
2063                if let Some(cell) = Arc::get_mut(cell) {
2064                    cell.with_dependent_mut(|_, dependent| {
2065                        for block in &mut dependent.blocks {
2066                            block.resolve_references(resolver, renderer, warnings);
2067                        }
2068                    });
2069                }
2070            }
2071        }
2072    }
2073}
2074
2075/// An [`AsciiDoc`](TableCellContent::AsciiDoc) cell parsed in place from the
2076/// parent document's source.
2077#[derive(Clone, Debug, Eq, PartialEq)]
2078pub struct BorrowedCell<'src> {
2079    title: Option<String>,
2080    inline: bool,
2081    blocks: Vec<Block<'src>>,
2082}
2083
2084self_cell! {
2085    /// An [`AsciiDoc`](TableCellContent::AsciiDoc) cell that owns its
2086    /// (include-expanded) source, with the parsed blocks borrowing from it.
2087    pub struct OwnedCell {
2088        owner: String,
2089
2090        #[covariant]
2091        dependent: OwnedCellInner,
2092    }
2093
2094    impl {Debug, Eq, PartialEq}
2095}
2096
2097/// The parsed contents of an [`OwnedCell`], borrowing its owned source.
2098#[derive(Debug, Eq, PartialEq)]
2099struct OwnedCellInner<'src> {
2100    title: Option<String>,
2101    inline: bool,
2102    blocks: Vec<Block<'src>>,
2103}
2104
2105/// Parse the value of the `cols` attribute into a list of columns, mirroring
2106/// Asciidoctor's `parse_colspecs`.
2107///
2108/// All spaces are first removed from the value. A wholly blank value yields no
2109/// columns (the caller then takes the column count from the first row), and a
2110/// lone integer (the deprecated `cols="3"` form) yields that many default
2111/// columns. Otherwise the value is a list of column specifiers separated by
2112/// commas, or by semicolons when no comma is present. An empty record (e.g. the
2113/// trailing field of `cols="1,,1"`) contributes a default column, and a
2114/// specifier may be preceded by a multiplier (`<n>*`) that repeats the column
2115/// `n` times. Each specifier's alignment operators, proportional width, and
2116/// [style operator](parse_col_spec) are interpreted.
2117fn parse_cols(value: &str) -> Vec<TableColumn> {
2118    // Asciidoctor strips every space from the cols value before parsing, so
2119    // `cols=" 1, 1 "` is equivalent to `cols="1,1"`.
2120    let records: String = value.chars().filter(|c| !c.is_whitespace()).collect();
2121
2122    // A wholly blank cols value is ignored: the caller falls back to the column
2123    // count of the first row.
2124    if records.is_empty() {
2125        return vec![];
2126    }
2127
2128    // Deprecated single-integer form: `cols=3` is equivalent to `cols="3*"` and
2129    // produces that many equally sized columns.
2130    if let Ok(count) = records.parse::<usize>() {
2131        return vec![TableColumn::default(); count];
2132    }
2133
2134    // Split on commas when present, otherwise on semicolons (Asciidoctor accepts
2135    // either as the column-spec separator, but not a mix). Empty records are
2136    // kept: each one contributes a default column.
2137    let parts: Vec<&str> = if records.contains(',') {
2138        records.split(',').collect()
2139    } else {
2140        records.split(';').collect()
2141    };
2142
2143    let mut columns: Vec<TableColumn> = vec![];
2144    for part in parts {
2145        if part.is_empty() {
2146            columns.push(TableColumn::default());
2147        } else if let Some((count, spec)) = part.split_once('*') {
2148            let repeat = count.parse::<usize>().unwrap_or(1).max(1);
2149            let column = parse_col_spec(spec);
2150            for _ in 0..repeat {
2151                columns.push(column.clone());
2152            }
2153        } else {
2154            columns.push(parse_col_spec(part));
2155        }
2156    }
2157
2158    columns
2159}
2160
2161/// Parse a single column specifier, extracting its alignment, proportional
2162/// width, and style.
2163///
2164/// A column specifier is positional: an optional horizontal alignment operator
2165/// (`<`, `>`, or `^`) comes first, followed by an optional vertical alignment
2166/// operator (`.<`, `.>`, or `.^`), followed by the width, and finally an
2167/// optional style operator in the last position. When a multiplier (`<n>*`) is
2168/// present, the operators follow the multiplier, so the `spec` passed here is
2169/// the portion after the `*`.
2170///
2171/// The width is either the special autowidth value `~` (sizing the column to
2172/// its content) or the first contiguous run of digits after any alignment
2173/// operators; a spec with neither falls back to the default width. The style
2174/// operator is the trailing letter (`a`, `d`, `e`, `h`, `l`, `m`, or `s`); an
2175/// unrecognized trailing letter leaves the style at its default.
2176fn parse_col_spec(spec: &str) -> TableColumn {
2177    let mut rest = spec.trim();
2178
2179    // Horizontal alignment operator (if present) always comes first.
2180    let mut h_align = HorizontalAlignment::Left;
2181    match rest.as_bytes().first() {
2182        Some(b'<') => {
2183            h_align = HorizontalAlignment::Left;
2184            rest = &rest[1..];
2185        }
2186
2187        Some(b'>') => {
2188            h_align = HorizontalAlignment::Right;
2189            rest = &rest[1..];
2190        }
2191
2192        Some(b'^') => {
2193            h_align = HorizontalAlignment::Center;
2194            rest = &rest[1..];
2195        }
2196
2197        _ => {}
2198    }
2199
2200    // Vertical alignment operator (if present) follows, introduced by a dot.
2201    let mut v_align = VerticalAlignment::Top;
2202    if let Some(after_dot) = rest.strip_prefix('.') {
2203        match after_dot.as_bytes().first() {
2204            Some(b'<') => {
2205                v_align = VerticalAlignment::Top;
2206                rest = &after_dot[1..];
2207            }
2208
2209            Some(b'>') => {
2210                v_align = VerticalAlignment::Bottom;
2211                rest = &after_dot[1..];
2212            }
2213
2214            Some(b'^') => {
2215                v_align = VerticalAlignment::Middle;
2216                rest = &after_dot[1..];
2217            }
2218
2219            _ => {}
2220        }
2221    }
2222
2223    // Width comes after the alignment operators. The special value `~` marks
2224    // the column as autowidth (sized to its content); otherwise the width is
2225    // the first run of digits. A spec with neither falls back to the default
2226    // proportional width.
2227    let mut autowidth = false;
2228    let mut width = TableColumn::default().width;
2229    if let Some(after_tilde) = rest.strip_prefix('~') {
2230        autowidth = true;
2231        rest = after_tilde;
2232    } else {
2233        let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
2234        if let Ok(parsed) = digits.parse::<usize>()
2235            && parsed > 0
2236        {
2237            width = parsed;
2238        }
2239        rest = &rest[digits.len()..];
2240    }
2241
2242    // The style operator, if present, occupies the last position on the
2243    // specifier, so it is the entire remainder after the width. Matching the
2244    // whole remainder (rather than just its first byte) means a malformed spec
2245    // with trailing junk — e.g. `1em` — falls back to the default style instead
2246    // of silently honoring the first letter and discarding the rest.
2247    let style = match rest.trim() {
2248        "a" => ColumnStyle::AsciiDoc,
2249        "d" => ColumnStyle::Default,
2250        "e" => ColumnStyle::Emphasis,
2251        "h" => ColumnStyle::Header,
2252        "l" => ColumnStyle::Literal,
2253        "m" => ColumnStyle::Monospace,
2254        "s" => ColumnStyle::Strong,
2255        _ => ColumnStyle::Default,
2256    };
2257
2258    TableColumn {
2259        width,
2260        autowidth,
2261        h_align,
2262        v_align,
2263        style,
2264    }
2265}
2266
2267/// The span, alignment, and style overrides parsed from a
2268/// [cell specifier](RawCell::spec).
2269///
2270/// Each alignment and style field is `None` when the corresponding operator is
2271/// absent from the specifier, in which case the cell inherits that alignment
2272/// (or style) from its column. `colspan` and `rowspan` are the number of
2273/// columns and rows the cell spans; they default to `1` (no span). `repeat` is
2274/// the duplication factor — the number of consecutive cells the content is
2275/// cloned into — and defaults to `1` (no duplication).
2276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2277struct CellSpec {
2278    h_align: Option<HorizontalAlignment>,
2279    v_align: Option<VerticalAlignment>,
2280    style: Option<ColumnStyle>,
2281    colspan: usize,
2282    rowspan: usize,
2283    repeat: usize,
2284}
2285
2286impl Default for CellSpec {
2287    fn default() -> Self {
2288        Self {
2289            h_align: None,
2290            v_align: None,
2291            style: None,
2292            colspan: 1,
2293            rowspan: 1,
2294            repeat: 1,
2295        }
2296    }
2297}
2298
2299/// A single PSV cell as located by [`scan_cells`]: the alignment operators from
2300/// its specifier together with the raw (untrimmed) span of its content.
2301#[derive(Clone, Copy)]
2302struct RawCell<'src> {
2303    spec: CellSpec,
2304    content: Span<'src>,
2305}
2306
2307/// The largest number of cells a single duplication factor (`<n>*`) is allowed
2308/// to expand into.
2309///
2310/// A duplicated cell is materialized as `<n>` independent cells, so the factor
2311/// is an amplification: a dozen source bytes such as `1000000000*` would
2312/// otherwise request a billion `RawCell`s (a multi-gigabyte allocation).
2313/// Capping the per-specifier factor bounds that amplification while leaving any
2314/// realistic table — which never duplicates a cell more than a handful of times
2315/// — untouched. (This is the one point where the implementation diverges from
2316/// Asciidoctor, which expands the literal factor however large.)
2317const MAX_DUPLICATION_FACTOR: usize = 1_000;
2318
2319/// Expand each duplicated cell into the `<n>` independent cells it represents.
2320///
2321/// A cell specifier with a duplication factor (`<n>*`) clones the cell's
2322/// content and properties into `<n>` consecutive cells. Each clone is an
2323/// ordinary single-slot cell (colspan and rowspan of 1), so expanding here —
2324/// before the grid is walked — lets the clones flow into rows exactly like
2325/// cells the author typed out by hand. A duplication factor of zero produces no
2326/// cells, dropping the original (matching Asciidoctor). A cell with no
2327/// duplication factor has a `repeat` of 1 and so passes through unchanged. The
2328/// factor is clamped to [`MAX_DUPLICATION_FACTOR`] so a hostile specifier can't
2329/// trigger a runaway allocation.
2330fn expand_duplicates(cells: Vec<RawCell<'_>>) -> Vec<RawCell<'_>> {
2331    // The common case is no duplication at all, so only the clones beyond the
2332    // first add to the count.
2333    let extra: usize = cells
2334        .iter()
2335        .map(|c| c.spec.repeat.min(MAX_DUPLICATION_FACTOR).saturating_sub(1))
2336        .sum();
2337
2338    let mut expanded = Vec::with_capacity(cells.len() + extra);
2339    for cell in cells {
2340        for _ in 0..cell.spec.repeat.min(MAX_DUPLICATION_FACTOR) {
2341            expanded.push(cell);
2342        }
2343    }
2344
2345    expanded
2346}
2347
2348/// Scan a region for PSV cell boundaries, returning the [specifier](CellSpec)
2349/// and raw (untrimmed) content span of each cell.
2350///
2351/// Every unescaped occurrence of the table's `separator` (the vertical bar
2352/// (`|`) by default, the exclamation mark (`!`) for a nested table, or any
2353/// string set with the `separator` attribute, e.g. the broken bar `¦`) is a
2354/// cell boundary, matching Asciidoctor. The token immediately preceding a
2355/// separator is treated as that cell's [specifier](CellSpec) (e.g. `^`, `2+`,
2356/// `.>`) only when it parses as one (see [`parse_cell_spec`]) *and* is anchored
2357/// at the line start or preceded by whitespace; otherwise the token is ordinary
2358/// content of the preceding cell and the separator is a plain boundary (so the
2359/// `a` in `|a|b` is content, not a style operator). Content before the first
2360/// boundary is ignored.
2361///
2362/// A separator immediately preceded by a backslash (e.g. `\|`) is escaped: it
2363/// is literal content rather than a boundary, and the backslash is stripped
2364/// later in [`TableCell::parse`]. Only the single byte before the separator is
2365/// inspected, so `\\|` is also read as an escaped separator — matching
2366/// Asciidoctor, whose check is likewise the single-character
2367/// `pre_match.end_with? '\'`.
2368fn scan_cells<'src>(
2369    region: Span<'src>,
2370    separator: &str,
2371) -> (Vec<RawCell<'src>>, Option<Span<'src>>) {
2372    let data = region.data();
2373    let bytes = data.as_bytes();
2374    let len = bytes.len();
2375
2376    // A zero-length separator would never advance; treat it as a single byte to
2377    // stay safe. (The resolver never produces an empty separator.)
2378    let sep_len = separator.len().max(1);
2379
2380    let mut cells: Vec<RawCell<'src>> = vec![];
2381
2382    // The content start and specifier of the cell currently being accumulated.
2383    let mut content_start: Option<usize> = None;
2384
2385    let mut cur_spec = CellSpec::default();
2386
2387    // The span of a cell recovered from content that precedes the first
2388    // separator (see below); `Some` drives a missing-leading-separator warning.
2389    let mut recovered: Option<Span<'src>> = None;
2390
2391    let mut i = 0;
2392    while i < len {
2393        if data
2394            .get(i..)
2395            .is_some_and(|rest| rest.starts_with(separator))
2396        {
2397            // A separator immediately preceded by a backslash is escaped: it is
2398            // literal content, not a cell boundary. The backslash is stripped
2399            // from the rendered cell later (see `TableCell::parse`).
2400            if i > 0 && bytes.get(i - 1).copied() == Some(b'\\') {
2401                i += sep_len;
2402                continue;
2403            }
2404
2405            // Walk back to the start of the token directly preceding this
2406            // separator. The token (a possible cell specifier) runs back to the
2407            // previous whitespace, tab, or newline, or to the start of the
2408            // region.
2409            let mut tok_start = i;
2410            while tok_start > 0
2411                && !matches!(
2412                    bytes.get(tok_start - 1).copied(),
2413                    Some(b' ' | b'\t' | b'\n')
2414                )
2415            {
2416                tok_start -= 1;
2417            }
2418
2419            let token = data.get(tok_start..i).unwrap_or_default();
2420            let spec = if token.is_empty() {
2421                Some(CellSpec::default())
2422            } else {
2423                parse_cell_spec(token)
2424            };
2425
2426            // Every unescaped separator is a cell boundary (matching
2427            // Asciidoctor). When the token is empty or a valid specifier it
2428            // belongs to the *next* cell, so the previous cell's content ends
2429            // before the token. Otherwise the token is ordinary content of the
2430            // previous cell (e.g. the `a` in `|a|b`, where `a` is not preceded
2431            // by whitespace and so is not a specifier), the separator is plain,
2432            // and the next cell takes the default specifier.
2433            let (content_end, next_spec) = match spec {
2434                Some(spec) => (tok_start, spec),
2435                None => (i, CellSpec::default()),
2436            };
2437
2438            match content_start {
2439                Some(start) => {
2440                    // The separating whitespace, included in the slice, is
2441                    // trimmed later in `TableCell::parse`.
2442                    cells.push(RawCell {
2443                        spec: cur_spec,
2444                        content: region.slice(start..content_end),
2445                    });
2446                }
2447
2448                None => {
2449                    // No cell has been opened yet, so this is the table's first
2450                    // separator. Non-blank content in front of it means the first
2451                    // cell is missing its leading separator; recover that content
2452                    // as the first cell (with the default specifier) and record
2453                    // its span so the caller can warn, matching Asciidoctor.
2454                    let leading = region.slice(0..content_end);
2455                    if !leading.data().trim().is_empty() {
2456                        cells.push(RawCell {
2457                            spec: CellSpec::default(),
2458                            content: leading,
2459                        });
2460                        recovered = Some(leading);
2461                    }
2462                }
2463            }
2464
2465            cur_spec = next_spec;
2466            content_start = Some(i + sep_len);
2467            i += sep_len;
2468            continue;
2469        }
2470
2471        i += 1;
2472    }
2473
2474    if let Some(start) = content_start {
2475        cells.push(RawCell {
2476            spec: cur_spec,
2477            content: region.slice(start..len),
2478        });
2479    }
2480
2481    (cells, recovered)
2482}
2483
2484/// Parse a cell specifier, returning its [span and overrides](CellSpec), or
2485/// `None` if `token` is not a valid cell specifier.
2486///
2487/// A cell specifier is positional and every part is optional, but the whole
2488/// token must be consumed for it to be valid:
2489///
2490/// ```text
2491/// <factor><span or duplication operator><horizontal><vertical><style>
2492/// ```
2493///
2494/// * The factor and span/duplication operator are an optional count (e.g. `2`,
2495///   `2.3`, `.3`) that, when present, must be followed by `+` (span) or `*`
2496///   (duplication). For a span the factor is interpreted as the cell's colspan
2497///   and rowspan (a missing column or row count defaults to 1). For a
2498///   duplication the column part of the factor is the duplication count — the
2499///   number of consecutive cells the content is cloned into — and any row part
2500///   is ignored; a duplicated cell keeps a colspan and rowspan of 1.
2501/// * The horizontal alignment operator is `<`, `>`, or `^`.
2502/// * The vertical alignment operator is a dot followed by `<`, `>`, or `^`.
2503/// * The style operator is a single lowercase letter in the last position. A
2504///   recognized operator (`a`, `d`, `e`, `h`, `l`, `m`, or `s`) overrides the
2505///   column's style on this cell. Any other single lowercase letter still
2506///   locates the separator but leaves the style at `None`, so the cell inherits
2507///   its column's style (matching Asciidoctor, which ignores an unrecognized
2508///   style operator).
2509fn parse_cell_spec(token: &str) -> Option<CellSpec> {
2510    let b = token.as_bytes();
2511    let mut i = 0;
2512
2513    // Optional span/duplication: an optional span factor followed by `+` (span)
2514    // or `*` (duplication). The factor is a column count, an optional dot, and an
2515    // optional row count (`<n>`, `.<n>`, or `<n>.<n>`). The factor is committed
2516    // only when the operator that must follow it is present; otherwise the
2517    // leading digits remain and the token fails the full-consumption check below.
2518    let mut colspan = 1;
2519    let mut rowspan = 1;
2520    let mut repeat = 1;
2521    let col_start = i;
2522
2523    let mut j = i;
2524    while matches!(b.get(j).copied(), Some(c) if c.is_ascii_digit()) {
2525        j += 1;
2526    }
2527
2528    let col_end = j;
2529    let mut has_dot = false;
2530
2531    let mut row_start = j;
2532    if b.get(j).copied() == Some(b'.') {
2533        has_dot = true;
2534        j += 1;
2535        row_start = j;
2536        while matches!(b.get(j).copied(), Some(c) if c.is_ascii_digit()) {
2537            j += 1;
2538        }
2539    }
2540
2541    let row_end = j;
2542    match b.get(j).copied() {
2543        // Span: the factor is interpreted as a colspan and rowspan. A missing
2544        // column or row count defaults to 1, so `2+` spans two columns, `.3+`
2545        // spans three rows, and `2.3+` spans a 2x3 block.
2546        Some(b'+') => {
2547            // The factor consists only of ASCII digits and dots, so these ranges
2548            // are always valid `str` slices.
2549            let col_digits = token.get(col_start..col_end).unwrap_or_default();
2550            if !col_digits.is_empty() {
2551                colspan = col_digits.parse().unwrap_or(1);
2552            }
2553            if has_dot {
2554                let row_digits = token.get(row_start..row_end).unwrap_or_default();
2555                if !row_digits.is_empty() {
2556                    rowspan = row_digits.parse().unwrap_or(1);
2557                }
2558            }
2559            i = j + 1;
2560        }
2561
2562        // Duplication: the factor is interpreted as a duplication count, so the
2563        // cell's content and properties are cloned into `<n>` consecutive cells.
2564        // Only the column part of the factor is the count; any row part (`<n>.`)
2565        // is ignored, matching Asciidoctor. A missing column count defaults to 1.
2566        // Unlike a span, a duplication leaves `colspan` and `rowspan` at 1: each
2567        // clone is an ordinary single-slot cell.
2568        Some(b'*') => {
2569            let col_digits = token.get(col_start..col_end).unwrap_or_default();
2570            if !col_digits.is_empty() {
2571                repeat = col_digits.parse().unwrap_or(1);
2572            }
2573            i = j + 1;
2574        }
2575
2576        _ => {}
2577    }
2578
2579    // Optional horizontal alignment operator.
2580    let mut h_align = None;
2581    match b.get(i).copied() {
2582        Some(b'<') => {
2583            h_align = Some(HorizontalAlignment::Left);
2584            i += 1;
2585        }
2586
2587        Some(b'>') => {
2588            h_align = Some(HorizontalAlignment::Right);
2589            i += 1;
2590        }
2591
2592        Some(b'^') => {
2593            h_align = Some(HorizontalAlignment::Center);
2594            i += 1;
2595        }
2596
2597        _ => {}
2598    }
2599
2600    // Optional vertical alignment operator, introduced by a dot.
2601    let mut v_align = None;
2602    if b.get(i).copied() == Some(b'.') {
2603        match b.get(i + 1).copied() {
2604            Some(b'<') => {
2605                v_align = Some(VerticalAlignment::Top);
2606                i += 2;
2607            }
2608
2609            Some(b'>') => {
2610                v_align = Some(VerticalAlignment::Bottom);
2611                i += 2;
2612            }
2613
2614            Some(b'^') => {
2615                v_align = Some(VerticalAlignment::Middle);
2616                i += 2;
2617            }
2618
2619            _ => {}
2620        }
2621    }
2622
2623    // Optional style operator: a single lowercase letter in the last position.
2624    // A recognized letter overrides the column's style; any other lowercase
2625    // letter is consumed (so the separator is still located) but leaves the
2626    // style at `None`, so the cell inherits its column's style.
2627    let mut style = None;
2628    if let Some(c) = b.get(i).copied()
2629        && c.is_ascii_lowercase()
2630    {
2631        style = match c {
2632            b'a' => Some(ColumnStyle::AsciiDoc),
2633            b'd' => Some(ColumnStyle::Default),
2634            b'e' => Some(ColumnStyle::Emphasis),
2635            b'h' => Some(ColumnStyle::Header),
2636            b'l' => Some(ColumnStyle::Literal),
2637            b'm' => Some(ColumnStyle::Monospace),
2638            b's' => Some(ColumnStyle::Strong),
2639            _ => None,
2640        };
2641        i += 1;
2642    }
2643
2644    // The token is a cell specifier only if it was consumed in its entirety.
2645    if i == b.len() {
2646        Some(CellSpec {
2647            h_align,
2648            v_align,
2649            style,
2650            colspan,
2651            rowspan,
2652            repeat,
2653        })
2654    } else {
2655        None
2656    }
2657}
2658
2659/// Return the subspan of `s` with surrounding whitespace (including newlines)
2660/// removed.
2661fn trim_surrounding_whitespace(s: Span<'_>) -> Span<'_> {
2662    let data = s.data();
2663    let start = data.len() - data.trim_start().len();
2664    let len = data.trim().len();
2665    s.slice(start..start + len)
2666}
2667
2668/// Trim a PSV cell's content according to its [style](ColumnStyle), matching
2669/// Asciidoctor's `Table::Cell` initializer:
2670///
2671/// * A [`Literal`](ColumnStyle::Literal) cell has its trailing whitespace
2672///   removed and any leading blank lines stripped, but the leading indentation
2673///   of its first content line is preserved (so an indented literal cell keeps
2674///   its indentation).
2675/// * An [`AsciiDoc`](ColumnStyle::AsciiDoc) cell likewise removes trailing
2676///   whitespace; if the remaining content begins with a newline it strips the
2677///   leading blank lines (preserving the first content line's indentation, so a
2678///   leading-indented line is interpreted as a literal block), otherwise it
2679///   strips the leading whitespace.
2680/// * Every other style has all surrounding whitespace removed.
2681fn trim_cell_content(s: Span<'_>, style: ColumnStyle) -> Span<'_> {
2682    let data = s.data();
2683    match style {
2684        ColumnStyle::Literal => {
2685            let end = data.trim_end().len();
2686            let mut start = 0;
2687            while data[start..end].starts_with('\n') {
2688                start += 1;
2689            }
2690            s.slice(start..end)
2691        }
2692
2693        ColumnStyle::AsciiDoc => {
2694            let end = data.trim_end().len();
2695            if data[..end].starts_with('\n') {
2696                let mut start = 0;
2697                while data[start..end].starts_with('\n') {
2698                    start += 1;
2699                }
2700                s.slice(start..end)
2701            } else {
2702                let start = end - data[..end].trim_start().len();
2703                s.slice(start..end)
2704            }
2705        }
2706
2707        _ => trim_surrounding_whitespace(s),
2708    }
2709}
2710
2711/// Returns the first non-blank line in `rest`, or `None` when every remaining
2712/// line is blank (or `rest` is empty).
2713fn first_nonblank_line(mut rest: Span<'_>) -> Option<Span<'_>> {
2714    while !rest.is_empty() {
2715        let line = rest.take_line();
2716        if !line.item.data().trim().is_empty() {
2717            return Some(line.item);
2718        }
2719        rest = line.after;
2720    }
2721    None
2722}
2723
2724/// Returns `true` when `line` begins a new PSV cell, i.e. it contains the
2725/// separator and the text before the first separator (after any leading
2726/// whitespace) is either empty or a valid cell specifier. A line that continues
2727/// the previous cell returns `false`.
2728fn psv_line_starts_cell(line: &str, separator: &str) -> bool {
2729    match line.find(separator) {
2730        Some(pos) => {
2731            let prefix = line[..pos].trim_start();
2732            prefix.is_empty() || parse_cell_spec(prefix).is_some()
2733        }
2734        None => false,
2735    }
2736}
2737
2738/// Returns `true` when `line` contains an odd number of double quotes, i.e. it
2739/// opens a quoted CSV/TSV value that is not closed on the same line.
2740fn line_has_unclosed_quote(line: &str) -> bool {
2741    line.bytes().filter(|&b| b == b'"').count() % 2 == 1
2742}
2743
2744#[cfg(test)]
2745mod tests {
2746    use std::sync::Arc;
2747
2748    use super::{AsciiDocCell, OwnedCell, OwnedCellInner};
2749    use crate::parser::{
2750        HtmlSubstitutionRenderer, ReferenceResolver, ResolutionContext, ResolvedReference,
2751    };
2752
2753    /// A resolver that resolves nothing; the owned-cell resolution path under
2754    /// test carries no references, so it is never actually consulted.
2755    struct NoopResolver;
2756
2757    impl ReferenceResolver for NoopResolver {
2758        fn resolve(&self, _context: &ResolutionContext<'_>) -> Option<ResolvedReference> {
2759            None
2760        }
2761    }
2762
2763    /// When an owned (include-expanded) AsciiDoc cell is shared behind more
2764    /// than one `Arc` reference, `resolve_references` cannot obtain a
2765    /// mutable borrow of the store and leaves it untouched rather than
2766    /// panicking. Production code resolves while the cell is its sole
2767    /// owner, so this defensive branch is exercised here by deliberately
2768    /// holding a second reference.
2769    #[test]
2770    fn resolve_references_skips_shared_owned_cell() {
2771        let mut cell = AsciiDocCell::Owned(Arc::new(OwnedCell::new(String::new(), |_source| {
2772            OwnedCellInner {
2773                title: None,
2774                inline: false,
2775                blocks: vec![],
2776            }
2777        })));
2778
2779        // Hold a second reference to the same store so `Arc::get_mut` fails.
2780        let shared = cell.clone();
2781
2782        let mut warnings = vec![];
2783        cell.resolve_references(&NoopResolver, &HtmlSubstitutionRenderer {}, &mut warnings);
2784
2785        // Resolution was skipped silently: no warnings, and the two references
2786        // still describe the same (unmodified) cell.
2787        assert!(warnings.is_empty());
2788        assert_eq!(cell, shared);
2789    }
2790}