Skip to main content

asciidoc_parser/blocks/
table.rs

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