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