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