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