Skip to main content

asciidoc_parser/blocks/
table.rs

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