use std::{collections::VecDeque, sync::Arc};
use self_cell::self_cell;
use crate::{
HasSpan, Parser, Span,
attributes::Attrlist,
blocks::{
Block, ContentModel, IsBlock, caption::assign_block_caption, metadata::BlockMetadata,
parse_utils::parse_blocks_until,
},
content::{Content, SubstitutionGroup},
document::{InterpretedValue, TocConfig, TocMode},
parser::{
InlineSubstitutionRenderer, ModificationContext, ReferenceResolver, ReferenceWarning,
preprocessor::preprocess,
},
span::MatchedItem,
strings::CowStr,
warnings::{MatchAndWarnings, Warning, WarningType},
};
const ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES: &[&str] =
&["doctype", "toc", "notitle", "showtitle", "compat-mode"];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TableBlock<'src> {
columns: Vec<TableColumn>,
data_format: DataFormat,
header_row: Option<TableRow<'src>>,
body_rows: Vec<TableRow<'src>>,
footer_row: Option<TableRow<'src>>,
source: Span<'src>,
title_source: Option<Span<'src>>,
title: Option<String>,
caption: Option<String>,
number: Option<usize>,
frame: Frame,
grid: Grid,
stripes: Stripes,
anchor: Option<Span<'src>>,
anchor_reftext: Option<Span<'src>>,
attrlist: Option<Attrlist<'src>>,
}
impl<'src> TableBlock<'src> {
pub(crate) fn is_table_delimiter(line: &Span<'src>) -> bool {
let data = line.data();
data.len() >= 4
&& matches!(data.as_bytes().first(), Some(b'|' | b'!' | b',' | b':'))
&& data
.get(1..)
.is_some_and(|rest| rest.bytes().all(|b| b == b'='))
}
pub(crate) fn parse(
metadata: &BlockMetadata<'src>,
parser: &mut Parser,
) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
let delimiter = metadata.block_start.take_normalized_line();
if !Self::is_table_delimiter(&delimiter.item) {
return None;
}
let delimiter_text = delimiter.item.data();
let mut next = delimiter.after;
let (closing_delimiter, after) = loop {
if next.is_empty() {
break (next, next);
}
let line = next.take_normalized_line();
if line.item.data() == delimiter_text {
break (line.item, line.after);
}
next = line.after;
};
let inside = delimiter.after.trim_remainder(closing_delimiter);
let data_format = resolve_data_format(metadata, delimiter_text);
let separator = resolve_separator(metadata, parser, data_format);
let cols_attr: Vec<TableColumn> = metadata
.attrlist
.as_ref()
.and_then(|a| a.named_attribute("cols"))
.map(|attr| parse_cols(attr.value()))
.unwrap_or_default();
let autowidth = metadata
.attrlist
.as_ref()
.is_some_and(|a| a.has_option("autowidth"));
let opts_header = metadata
.attrlist
.as_ref()
.is_some_and(|a| a.has_option("header"));
let opts_noheader = metadata
.attrlist
.as_ref()
.is_some_and(|a| a.has_option("noheader"));
let opts_footer = metadata
.attrlist
.as_ref()
.is_some_and(|a| a.has_option("footer"));
let line1 = inside.take_line();
let line1_blank = line1.item.data().trim().is_empty();
let line2_blank =
!line1.after.is_empty() && line1.after.take_line().item.data().trim().is_empty();
let first_row_complete = match data_format {
DataFormat::Psv => first_nonblank_line(line1.after)
.is_none_or(|line| psv_line_starts_cell(line.data(), separator.as_str())),
DataFormat::Csv | DataFormat::Tsv => !line_has_unclosed_quote(line1.item.data()),
DataFormat::Dsv => true,
};
let has_header =
opts_header || (!opts_noheader && !line1_blank && line2_blank && first_row_complete);
let caption = assign_block_caption(
parser,
"table",
metadata.attrlist.as_ref(),
metadata.title.is_some(),
);
let number = caption.as_ref().and_then(|caption| caption.number);
let caption = caption.map(|caption| caption.prefix);
let frame = resolve_table_attribute::<Frame>(metadata, parser, "frame", "table-frame");
let grid = resolve_table_attribute::<Grid>(metadata, parser, "grid", "table-grid");
let stripes =
resolve_table_attribute::<Stripes>(metadata, parser, "stripes", "table-stripes");
let mut warnings: Vec<Warning<'src>> = vec![];
let body = TableBody {
inside,
separator,
cols_attr,
autowidth,
has_header,
};
let (columns, rows) = match data_format {
DataFormat::Psv => build_psv_table(body, parser, &mut warnings),
DataFormat::Csv | DataFormat::Tsv | DataFormat::Dsv => {
build_data_table(body, data_format, parser, &mut warnings)
}
};
let mut rows = rows.into_iter();
let header_row = if has_header { rows.next() } else { None };
let mut body_rows: Vec<TableRow<'src>> = rows.collect();
let footer_row = if opts_footer { body_rows.pop() } else { None };
let source = metadata
.source
.trim_remainder(closing_delimiter.discard_all())
.trim_trailing_whitespace();
if closing_delimiter.is_empty() {
warnings.push(Warning {
source: delimiter.item,
warning: WarningType::UnterminatedDelimitedBlock,
});
}
Some(MatchAndWarnings {
item: Some(MatchedItem {
item: Self {
columns,
data_format,
header_row,
body_rows,
footer_row,
source,
title_source: metadata.title_source,
title: metadata.title.clone(),
caption,
number,
frame,
grid,
stripes,
anchor: metadata.anchor,
anchor_reftext: metadata.anchor_reftext,
attrlist: metadata.attrlist.clone(),
},
after,
}),
warnings,
})
}
pub fn caption(&self) -> Option<&str> {
self.caption.as_deref()
}
pub fn number(&self) -> Option<usize> {
self.number
}
pub fn columns(&self) -> &[TableColumn] {
&self.columns
}
pub fn data_format(&self) -> DataFormat {
self.data_format
}
pub fn width(&self) -> Option<usize> {
let raw = self
.attrlist
.as_ref()
.and_then(|a| a.named_attribute("width"))?
.value();
let raw = raw.strip_suffix('%').unwrap_or(raw);
match raw.parse::<usize>() {
Ok(width) if (1..=100).contains(&width) => Some(width),
_ => None,
}
}
pub fn is_autowidth(&self) -> bool {
self.attrlist
.as_ref()
.is_some_and(|a| a.has_option("autowidth"))
}
pub fn frame(&self) -> Frame {
self.frame
}
pub fn grid(&self) -> Grid {
self.grid
}
pub fn stripes(&self) -> Stripes {
self.stripes
}
pub fn header_row(&self) -> Option<&TableRow<'src>> {
self.header_row.as_ref()
}
pub fn body_rows(&self) -> &[TableRow<'src>] {
&self.body_rows
}
pub fn footer_row(&self) -> Option<&TableRow<'src>> {
self.footer_row.as_ref()
}
pub(crate) fn resolve_references(
&mut self,
resolver: &dyn ReferenceResolver,
renderer: &dyn InlineSubstitutionRenderer,
warnings: &mut Vec<ReferenceWarning>,
) {
let rows = self
.header_row
.iter_mut()
.chain(self.body_rows.iter_mut())
.chain(self.footer_row.iter_mut());
for row in rows {
for cell in row.cells.iter_mut() {
cell.resolve_references(resolver, renderer, warnings);
}
}
}
}
impl<'src> IsBlock<'src> for TableBlock<'src> {
fn content_model(&self) -> ContentModel {
ContentModel::Table
}
fn raw_context(&self) -> CowStr<'src> {
"table".into()
}
fn title_source(&'src self) -> Option<Span<'src>> {
self.title_source
}
fn title(&self) -> Option<&str> {
self.title.as_deref()
}
fn caption(&self) -> Option<&str> {
self.caption.as_deref()
}
fn number(&self) -> Option<usize> {
self.number
}
fn anchor(&'src self) -> Option<Span<'src>> {
self.anchor
}
fn anchor_reftext(&'src self) -> Option<Span<'src>> {
self.anchor_reftext
}
fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
self.attrlist.as_ref()
}
}
impl<'src> HasSpan<'src> for TableBlock<'src> {
fn span(&self) -> Span<'src> {
self.source
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TableColumn {
width: usize,
autowidth: bool,
h_align: HorizontalAlignment,
v_align: VerticalAlignment,
style: ColumnStyle,
}
impl TableColumn {
pub fn width(&self) -> usize {
self.width
}
pub fn is_autowidth(&self) -> bool {
self.autowidth
}
pub fn h_align(&self) -> HorizontalAlignment {
self.h_align
}
pub fn v_align(&self) -> VerticalAlignment {
self.v_align
}
pub fn style(&self) -> ColumnStyle {
self.style
}
}
impl Default for TableColumn {
fn default() -> Self {
Self {
width: 1,
autowidth: false,
h_align: HorizontalAlignment::Left,
v_align: VerticalAlignment::Top,
style: ColumnStyle::Default,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum DataFormat {
#[default]
Psv,
Csv,
Tsv,
Dsv,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ColumnStyle {
AsciiDoc,
#[default]
Default,
Emphasis,
Header,
Literal,
Monospace,
Strong,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HorizontalAlignment {
Left,
Center,
Right,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VerticalAlignment {
Top,
Middle,
Bottom,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Frame {
#[default]
All,
Ends,
Sides,
None,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Grid {
#[default]
All,
Rows,
Cols,
None,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Stripes {
#[default]
None,
Even,
Odd,
All,
Hover,
}
trait TableAttributeValue: Copy + Default {
fn from_attr_value(value: &str) -> Self;
}
impl TableAttributeValue for Frame {
fn from_attr_value(value: &str) -> Self {
match value.trim() {
"ends" | "topbot" => Frame::Ends,
"sides" => Frame::Sides,
"none" => Frame::None,
_ => Frame::All,
}
}
}
impl TableAttributeValue for Grid {
fn from_attr_value(value: &str) -> Self {
match value.trim() {
"rows" => Grid::Rows,
"cols" => Grid::Cols,
"none" => Grid::None,
_ => Grid::All,
}
}
}
impl TableAttributeValue for Stripes {
fn from_attr_value(value: &str) -> Self {
match value.trim() {
"even" => Stripes::Even,
"odd" => Stripes::Odd,
"all" => Stripes::All,
"hover" => Stripes::Hover,
_ => Stripes::None,
}
}
}
fn resolve_table_attribute<B: TableAttributeValue>(
metadata: &BlockMetadata<'_>,
parser: &Parser,
attr_name: &str,
doc_attr_name: &str,
) -> B {
if let Some(attr) = metadata
.attrlist
.as_ref()
.and_then(|a| a.named_attribute(attr_name))
{
B::from_attr_value(attr.value())
} else if let InterpretedValue::Value(value) = parser.attribute_value(doc_attr_name) {
B::from_attr_value(&value)
} else {
B::default()
}
}
fn resolve_data_format(metadata: &BlockMetadata<'_>, delimiter_text: &str) -> DataFormat {
if let Some(attr) = metadata
.attrlist
.as_ref()
.and_then(|a| a.named_attribute("format"))
{
match attr.value().trim() {
"psv" => return DataFormat::Psv,
"csv" => return DataFormat::Csv,
"tsv" => return DataFormat::Tsv,
"dsv" => return DataFormat::Dsv,
_ => {}
}
}
match delimiter_text.as_bytes().first() {
Some(b',') => DataFormat::Csv,
Some(b':') => DataFormat::Dsv,
_ => DataFormat::Psv,
}
}
fn resolve_separator(
metadata: &BlockMetadata<'_>,
parser: &Parser,
data_format: DataFormat,
) -> String {
let default = match data_format {
DataFormat::Psv => {
if parser.nested_document_depth > 0 {
"!"
} else {
"|"
}
}
DataFormat::Csv => ",",
DataFormat::Tsv => "\t",
DataFormat::Dsv => ":",
};
metadata
.attrlist
.as_ref()
.and_then(|a| a.named_attribute("separator"))
.map(|attr| attr.value())
.filter(|value| !value.is_empty())
.map(|value| value.replace("\\t", "\t"))
.unwrap_or_else(|| default.to_string())
}
fn finalize_columns(
cols_attr: Vec<TableColumn>,
ncols: usize,
autowidth: bool,
) -> Vec<TableColumn> {
let mut columns = if cols_attr.is_empty() {
(0..ncols).map(|_| TableColumn::default()).collect()
} else {
cols_attr
};
if autowidth {
for column in columns.iter_mut() {
column.autowidth = true;
}
}
columns
}
struct TableBody<'src> {
inside: Span<'src>,
separator: String,
cols_attr: Vec<TableColumn>,
autowidth: bool,
has_header: bool,
}
fn build_psv_table<'src>(
body: TableBody<'src>,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) -> (Vec<TableColumn>, Vec<TableRow<'src>>) {
let TableBody {
inside,
separator,
cols_attr,
autowidth,
has_header,
} = body;
let separator = separator.as_str();
let first_line_cells: usize =
scan_cells(inside.discard_empty_lines().take_line().item, separator)
.0
.iter()
.map(|c| c.spec.colspan.max(1) * c.spec.repeat.min(MAX_DUPLICATION_FACTOR))
.sum();
let ncols = if cols_attr.is_empty() {
first_line_cells
} else {
cols_attr.len()
};
let columns = finalize_columns(cols_attr, ncols, autowidth);
let (raw_cells, recovered_first_cell) = scan_cells(inside, separator);
if let Some(source) = recovered_first_cell {
warnings.push(Warning {
source,
warning: WarningType::TableMissingLeadingSeparator,
});
}
let raw_cells = expand_duplicates(raw_cells);
let max_rowspan = raw_cells.len().saturating_add(1);
let mut raw_rows: Vec<Vec<RawCell<'src>>> = vec![];
if ncols > 0 {
let mut active_rowspans: VecDeque<usize> = VecDeque::from([0]);
let mut column_visits = 0usize;
let mut current_row: Vec<RawCell<'src>> = vec![];
for raw in raw_cells {
let colspan = raw.spec.colspan.max(1);
let rowspan = raw.spec.rowspan.max(1).min(max_rowspan);
if rowspan > 1 {
if active_rowspans.len() < rowspan {
active_rowspans.resize(rowspan, 0);
}
for slot in active_rowspans.iter_mut().take(rowspan).skip(1) {
*slot += colspan;
}
}
column_visits += colspan;
let cell_source = raw.content;
current_row.push(raw);
let carried = active_rowspans.front().copied().unwrap_or(0);
let effective = column_visits + carried;
if effective >= ncols {
if effective == ncols {
raw_rows.push(std::mem::take(&mut current_row));
} else {
current_row.clear();
warnings.push(Warning {
source: cell_source,
warning: WarningType::TableCellExceedsColumnCount,
});
}
column_visits = 0;
active_rowspans.pop_front();
if active_rowspans.is_empty() {
active_rowspans.push_back(0);
}
}
}
if let Some(last) = current_row.last() {
warnings.push(Warning {
source: last.content,
warning: WarningType::TableDroppingIncompleteRowAtEndOfTable,
});
}
}
let mut rows: Vec<TableRow<'src>> = Vec::with_capacity(raw_rows.len());
for (row_idx, raw_row) in raw_rows.into_iter().enumerate() {
let is_header = has_header && row_idx == 0;
let mut cells = Vec::with_capacity(raw_row.len());
for (col_idx, raw) in raw_row.into_iter().enumerate() {
let column = columns.get(col_idx).cloned().unwrap_or_default();
cells.push(TableCell::parse(
raw, &column, is_header, separator, parser, warnings,
));
}
rows.push(TableRow { cells });
}
(columns, rows)
}
fn build_data_table<'src>(
body: TableBody<'src>,
data_format: DataFormat,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) -> (Vec<TableColumn>, Vec<TableRow<'src>>) {
let TableBody {
inside,
separator,
cols_attr,
autowidth,
has_header,
} = body;
let separator = separator.as_str();
let (fields, first_row_len) = if data_format == DataFormat::Dsv {
parse_dsv_fields(inside, separator)
} else {
parse_csv_fields(inside, separator, warnings)
};
let ncols = if cols_attr.is_empty() {
first_row_len
} else {
cols_attr.len()
};
let columns = finalize_columns(cols_attr, ncols, autowidth);
let nrows = fields.len().checked_div(ncols).unwrap_or(0);
let mut rows: Vec<TableRow<'src>> = Vec::with_capacity(nrows);
let mut fields = fields.into_iter();
for row_idx in 0..nrows {
let is_header = has_header && row_idx == 0;
let mut cells = Vec::with_capacity(ncols);
for col_idx in 0..ncols {
let Some(field) = fields.next() else { break };
let column = columns.get(col_idx).cloned().unwrap_or_default();
cells.push(TableCell::parse_data(
field, &column, is_header, parser, warnings,
));
}
rows.push(TableRow { cells });
}
(columns, rows)
}
struct DataField<'src> {
content: Span<'src>,
replacement: Option<String>,
}
fn parse_csv_fields<'src>(
region: Span<'src>,
separator: &str,
warnings: &mut Vec<Warning<'src>>,
) -> (Vec<DataField<'src>>, usize) {
let data = region.data();
let n = data.len();
let sep_len = separator.len().max(1);
let at = |k: usize| data.as_bytes().get(k).copied();
let starts_with_sep = |pos: usize| data.get(pos..).is_some_and(|s| s.starts_with(separator));
let mut fields: Vec<DataField<'src>> = vec![];
let mut first_row_len = 0usize;
let mut first_row_done = false;
let mut fields_in_row = 0usize;
let mut cell_start = 0usize;
let mut i = 0usize;
while i <= n {
let at_eof = i == n;
let at_sep = !at_eof && starts_with_sep(i);
let at_nl = !at_eof && at(i) == Some(b'\n');
if !(at_eof || at_sep || at_nl) {
i += 1;
continue;
}
let raw = data.get(cell_start..i).unwrap_or_default();
if !at_eof && has_unclosed_quotes(raw) {
i += if at_sep { sep_len } else { 1 };
continue;
}
let blank_skip = (at_nl || at_eof) && fields_in_row == 0 && raw.trim().is_empty();
if !blank_skip {
fields.push(make_csv_field(region, cell_start, i, warnings));
fields_in_row += 1;
if !first_row_done {
first_row_len = fields_in_row;
}
}
if at_eof {
break;
}
if at_nl {
if fields_in_row > 0 {
first_row_done = true;
}
fields_in_row = 0;
cell_start = i + 1;
i += 1;
} else {
cell_start = i + sep_len;
i += sep_len;
}
}
(fields, first_row_len)
}
fn make_csv_field<'src>(
region: Span<'src>,
start: usize,
end: usize,
warnings: &mut Vec<Warning<'src>>,
) -> DataField<'src> {
let trimmed = trim_surrounding_whitespace(region.slice(start..end));
let data = trimmed.data();
let content = if data == "\"" {
warnings.push(Warning {
source: trimmed,
warning: WarningType::TableCsvDataHasUnclosedQuote,
});
trimmed.slice(0..0)
} else if data.len() >= 2 && data.starts_with('"') && data.ends_with('"') {
trim_surrounding_whitespace(trimmed.slice(1..data.len() - 1))
} else {
trimmed
};
let value = squeeze_quotes(content.data());
let replacement = (value != content.data()).then_some(value);
DataField {
content,
replacement,
}
}
fn squeeze_quotes(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut prev_quote = false;
for c in text.chars() {
if c == '"' {
if prev_quote {
continue;
}
prev_quote = true;
} else {
prev_quote = false;
}
out.push(c);
}
out
}
fn has_unclosed_quotes(buffer: &str) -> bool {
let record = buffer.trim();
if record == "\"" {
return true;
}
if !record.starts_with('"') {
return false;
}
let trailing_quote = record.ends_with('"');
if (trailing_quote && record.ends_with("\"\"")) || record.starts_with("\"\"") {
let collapsed = record.replace("\"\"", "");
collapsed.starts_with('"') && !collapsed.ends_with('"')
} else {
!trailing_quote
}
}
fn parse_dsv_fields<'src>(region: Span<'src>, separator: &str) -> (Vec<DataField<'src>>, usize) {
let data = region.data();
let n = data.len();
let sep_len = separator.len().max(1);
let escaped = format!("\\{separator}");
let at = |k: usize| data.as_bytes().get(k).copied();
let mut fields: Vec<DataField<'src>> = vec![];
let mut first_row_len = 0usize;
let mut row_count = 0usize;
let mut i = 0usize;
while i < n {
let mut line_end = i;
while line_end < n && at(line_end) != Some(b'\n') {
line_end += 1;
}
if data.get(i..line_end).unwrap_or("").trim().is_empty() {
i = if line_end < n { line_end + 1 } else { line_end };
continue;
}
let in_line = |pos: usize| {
data.get(pos..line_end)
.is_some_and(|s| s.starts_with(separator))
};
let mut fields_in_row = 0usize;
let mut field_start = i;
let mut p = i;
while p < line_end {
if at(p) == Some(b'\\')
&& data
.get(p + 1..line_end)
.is_some_and(|s| s.starts_with(separator))
{
p += 1 + sep_len;
continue;
}
if in_line(p) {
fields.push(make_dsv_field(region, field_start, p, &escaped, separator));
fields_in_row += 1;
p += sep_len;
field_start = p;
continue;
}
p += 1;
}
fields.push(make_dsv_field(
region,
field_start,
line_end,
&escaped,
separator,
));
fields_in_row += 1;
if row_count == 0 {
first_row_len = fields_in_row;
}
row_count += 1;
i = if line_end < n { line_end + 1 } else { line_end };
}
(fields, first_row_len)
}
fn make_dsv_field<'src>(
region: Span<'src>,
start: usize,
end: usize,
escaped: &str,
separator: &str,
) -> DataField<'src> {
let trimmed = trim_surrounding_whitespace(region.slice(start..end));
let replacement = if trimmed.data().contains(escaped) {
Some(trimmed.data().replace(escaped, separator))
} else {
None
};
DataField {
content: trimmed,
replacement,
}
}
fn process_content<'src>(
trimmed: Span<'src>,
replacement: Option<String>,
style: ColumnStyle,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) -> TableCellContent<'src> {
if style == ColumnStyle::AsciiDoc {
let saved_attributes = parser.attribute_values.clone();
let saved_locks = parser.locked_attribute_names.clone();
for (name, value) in saved_attributes.iter() {
let api_locked = value.modification_context == ModificationContext::ApiOnly;
if (!matches!(value.value, InterpretedValue::Unset) || api_locked)
&& !ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES.contains(&name.as_str())
{
parser.locked_attribute_names.insert(name.clone());
}
}
for name in ASCIIDOC_CELL_MODIFIABLE_ATTRIBUTES {
if let Some(attr) = Arc::make_mut(&mut parser.attribute_values).get_mut(*name) {
attr.modification_context = ModificationContext::Anywhere;
}
}
parser.force_doctype("article");
if let Some(toc) = Arc::make_mut(&mut parser.attribute_values).get_mut("toc") {
toc.value = InterpretedValue::Unset;
}
let cell = if content_has_directive(trimmed.data()) {
let (expanded, _source_map, _preprocessor_warnings) =
preprocess(trimmed.data(), parser);
let owned = OwnedCell::new(expanded, |source| {
let mut owned_warnings: Vec<Warning<'_>> = vec![];
let substitution_warnings_mark = parser.substitution_warnings_len();
let (title, inline, toc, blocks) =
parse_asciidoc_cell_body(Span::new(source), parser, &mut owned_warnings);
parser.truncate_substitution_warnings(substitution_warnings_mark);
debug_assert!(
owned_warnings.is_empty(),
"warnings from an include-expanded AsciiDoc cell are dropped; \
propagate them before adding any to this path"
);
OwnedCellInner {
title,
inline,
toc,
blocks,
}
});
AsciiDocCell::Owned(Arc::new(owned))
} else {
let (title, inline, toc, blocks) = parse_asciidoc_cell_body(trimmed, parser, warnings);
AsciiDocCell::Borrowed(BorrowedCell {
title,
inline,
toc,
blocks,
})
};
parser.locked_attribute_names = saved_locks;
parser.attribute_values = saved_attributes;
TableCellContent::AsciiDoc(cell)
} else {
let mut content = match replacement {
Some(replacement) => Content::from_filtered(trimmed, replacement),
None => Content::from(trimmed),
};
let substitutions = if style == ColumnStyle::Literal {
SubstitutionGroup::Verbatim
} else {
SubstitutionGroup::Normal
};
substitutions.apply(&mut content, parser, None);
TableCellContent::Simple(content)
}
}
fn parse_asciidoc_cell_body<'src>(
content: Span<'src>,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) -> (Option<String>, bool, TocConfig, Vec<Block<'src>>) {
let first_line = content.take_line();
let (title_source, body) = if first_line.item.data().starts_with("= ") {
(
Some(first_line.item.discard(2).discard_whitespace()),
first_line.after,
)
} else {
(None, content)
};
let saved_footnotes = parser.take_footnotes();
parser.nested_document_depth += 1;
let mut maw = parse_blocks_until(body, |_| false, parser);
parser.nested_document_depth -= 1;
warnings.append(&mut maw.warnings);
parser.restore_footnotes(saved_footnotes);
let inline = matches!(
parser.attribute_value("doctype"),
InterpretedValue::Value(ref v) if v == "inline"
);
let title = if parser.resolve_show_title(true) {
title_source.map(|span| {
let mut content = Content::from(span);
SubstitutionGroup::Header.apply(&mut content, parser, None);
content.rendered().to_string()
})
} else {
None
};
let toc = TocConfig::from_parser(parser);
(title, inline, toc, maw.item.item)
}
fn content_has_directive(content: &str) -> bool {
content.starts_with("include::") || content.contains("\ninclude::")
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TableRow<'src> {
cells: Vec<TableCell<'src>>,
}
impl<'src> TableRow<'src> {
pub fn cells(&self) -> &[TableCell<'src>] {
&self.cells
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TableCell<'src> {
h_align: HorizontalAlignment,
v_align: VerticalAlignment,
style: ColumnStyle,
colspan: usize,
rowspan: usize,
content: TableCellContent<'src>,
source: Span<'src>,
}
impl<'src> TableCell<'src> {
fn parse(
raw: RawCell<'src>,
column: &TableColumn,
is_header: bool,
separator: &str,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) -> Self {
let h_align = raw.spec.h_align.unwrap_or(column.h_align);
let v_align = raw.spec.v_align.unwrap_or(column.v_align);
let style = if is_header {
ColumnStyle::Default
} else {
raw.spec.style.unwrap_or(column.style)
};
let trimmed = trim_cell_content(raw.content, style);
let escaped = format!("\\{separator}");
let replacement = if style != ColumnStyle::AsciiDoc && trimmed.data().contains(&escaped) {
Some(trimmed.data().replace(&escaped, separator))
} else {
None
};
let content = process_content(trimmed, replacement, style, parser, warnings);
Self {
h_align,
v_align,
style,
colspan: raw.spec.colspan.max(1),
rowspan: raw.spec.rowspan.max(1),
content,
source: raw.content,
}
}
fn parse_data(
field: DataField<'src>,
column: &TableColumn,
is_header: bool,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) -> Self {
let style = if is_header {
ColumnStyle::Default
} else {
column.style
};
let source = field.content;
let content = process_content(field.content, field.replacement, style, parser, warnings);
Self {
h_align: column.h_align,
v_align: column.v_align,
style,
colspan: 1,
rowspan: 1,
content,
source,
}
}
pub fn h_align(&self) -> HorizontalAlignment {
self.h_align
}
pub fn v_align(&self) -> VerticalAlignment {
self.v_align
}
pub fn style(&self) -> ColumnStyle {
self.style
}
pub fn colspan(&self) -> usize {
self.colspan
}
pub fn rowspan(&self) -> usize {
self.rowspan
}
pub fn content(&self) -> &TableCellContent<'src> {
&self.content
}
fn resolve_references(
&mut self,
resolver: &dyn ReferenceResolver,
renderer: &dyn InlineSubstitutionRenderer,
warnings: &mut Vec<ReferenceWarning>,
) {
match &mut self.content {
TableCellContent::Simple(content) => {
content.resolve_references(resolver, renderer, warnings);
}
TableCellContent::AsciiDoc(cell) => {
cell.resolve_references(resolver, renderer, warnings);
}
}
}
}
impl<'src> HasSpan<'src> for TableCell<'src> {
fn span(&self) -> Span<'src> {
self.source
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TableCellContent<'src> {
Simple(Content<'src>),
AsciiDoc(AsciiDocCell<'src>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AsciiDocCell<'src> {
Borrowed(BorrowedCell<'src>),
Owned(Arc<OwnedCell>),
}
impl<'src> AsciiDocCell<'src> {
pub fn title(&self) -> Option<&str> {
match self {
Self::Borrowed(cell) => cell.title.as_deref(),
Self::Owned(cell) => cell.borrow_dependent().title.as_deref(),
}
}
pub fn is_inline(&self) -> bool {
match self {
Self::Borrowed(cell) => cell.inline,
Self::Owned(cell) => cell.borrow_dependent().inline,
}
}
pub fn toc_mode(&self) -> TocMode {
self.toc().mode
}
pub fn toc_levels(&self) -> usize {
self.toc().levels
}
pub fn toc_title(&self) -> &str {
&self.toc().title
}
pub fn toc_class(&self) -> &str {
&self.toc().class
}
pub(crate) fn toc(&self) -> &TocConfig {
match self {
Self::Borrowed(cell) => &cell.toc,
Self::Owned(cell) => &cell.borrow_dependent().toc,
}
}
pub fn blocks(&self) -> &[Block<'_>] {
match self {
Self::Borrowed(cell) => &cell.blocks,
Self::Owned(cell) => &cell.borrow_dependent().blocks,
}
}
fn resolve_references(
&mut self,
resolver: &dyn ReferenceResolver,
renderer: &dyn InlineSubstitutionRenderer,
warnings: &mut Vec<ReferenceWarning>,
) {
match self {
Self::Borrowed(cell) => {
for block in &mut cell.blocks {
block.resolve_references(resolver, renderer, warnings);
}
}
Self::Owned(cell) => {
if let Some(cell) = Arc::get_mut(cell) {
cell.with_dependent_mut(|_, dependent| {
for block in &mut dependent.blocks {
block.resolve_references(resolver, renderer, warnings);
}
});
}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BorrowedCell<'src> {
title: Option<String>,
inline: bool,
toc: TocConfig,
blocks: Vec<Block<'src>>,
}
self_cell! {
pub struct OwnedCell {
owner: String,
#[covariant]
dependent: OwnedCellInner,
}
impl {Debug, Eq, PartialEq}
}
#[derive(Debug, Eq, PartialEq)]
struct OwnedCellInner<'src> {
title: Option<String>,
inline: bool,
toc: TocConfig,
blocks: Vec<Block<'src>>,
}
fn parse_cols(value: &str) -> Vec<TableColumn> {
let records: String = value.chars().filter(|c| !c.is_whitespace()).collect();
if records.is_empty() {
return vec![];
}
if let Ok(count) = records.parse::<usize>() {
return vec![TableColumn::default(); count];
}
let parts: Vec<&str> = if records.contains(',') {
records.split(',').collect()
} else {
records.split(';').collect()
};
let mut columns: Vec<TableColumn> = vec![];
for part in parts {
if part.is_empty() {
columns.push(TableColumn::default());
} else if let Some((count, spec)) = part.split_once('*') {
let repeat = count.parse::<usize>().unwrap_or(1).max(1);
let column = parse_col_spec(spec);
for _ in 0..repeat {
columns.push(column.clone());
}
} else {
columns.push(parse_col_spec(part));
}
}
columns
}
fn parse_col_spec(spec: &str) -> TableColumn {
let mut rest = spec.trim();
let mut h_align = HorizontalAlignment::Left;
match rest.as_bytes().first() {
Some(b'<') => {
h_align = HorizontalAlignment::Left;
rest = &rest[1..];
}
Some(b'>') => {
h_align = HorizontalAlignment::Right;
rest = &rest[1..];
}
Some(b'^') => {
h_align = HorizontalAlignment::Center;
rest = &rest[1..];
}
_ => {}
}
let mut v_align = VerticalAlignment::Top;
if let Some(after_dot) = rest.strip_prefix('.') {
match after_dot.as_bytes().first() {
Some(b'<') => {
v_align = VerticalAlignment::Top;
rest = &after_dot[1..];
}
Some(b'>') => {
v_align = VerticalAlignment::Bottom;
rest = &after_dot[1..];
}
Some(b'^') => {
v_align = VerticalAlignment::Middle;
rest = &after_dot[1..];
}
_ => {}
}
}
let mut autowidth = false;
let mut width = TableColumn::default().width;
if let Some(after_tilde) = rest.strip_prefix('~') {
autowidth = true;
rest = after_tilde;
} else {
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(parsed) = digits.parse::<usize>()
&& parsed > 0
{
width = parsed;
}
rest = &rest[digits.len()..];
}
let style = match rest.trim() {
"a" => ColumnStyle::AsciiDoc,
"d" => ColumnStyle::Default,
"e" => ColumnStyle::Emphasis,
"h" => ColumnStyle::Header,
"l" => ColumnStyle::Literal,
"m" => ColumnStyle::Monospace,
"s" => ColumnStyle::Strong,
_ => ColumnStyle::Default,
};
TableColumn {
width,
autowidth,
h_align,
v_align,
style,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct CellSpec {
h_align: Option<HorizontalAlignment>,
v_align: Option<VerticalAlignment>,
style: Option<ColumnStyle>,
colspan: usize,
rowspan: usize,
repeat: usize,
}
impl Default for CellSpec {
fn default() -> Self {
Self {
h_align: None,
v_align: None,
style: None,
colspan: 1,
rowspan: 1,
repeat: 1,
}
}
}
#[derive(Clone, Copy)]
struct RawCell<'src> {
spec: CellSpec,
content: Span<'src>,
}
const MAX_DUPLICATION_FACTOR: usize = 1_000;
fn expand_duplicates(cells: Vec<RawCell<'_>>) -> Vec<RawCell<'_>> {
let extra: usize = cells
.iter()
.map(|c| c.spec.repeat.min(MAX_DUPLICATION_FACTOR).saturating_sub(1))
.sum();
let mut expanded = Vec::with_capacity(cells.len() + extra);
for cell in cells {
for _ in 0..cell.spec.repeat.min(MAX_DUPLICATION_FACTOR) {
expanded.push(cell);
}
}
expanded
}
fn scan_cells<'src>(
region: Span<'src>,
separator: &str,
) -> (Vec<RawCell<'src>>, Option<Span<'src>>) {
let data = region.data();
let bytes = data.as_bytes();
let len = bytes.len();
let sep_len = separator.len().max(1);
let mut cells: Vec<RawCell<'src>> = vec![];
let mut content_start: Option<usize> = None;
let mut cur_spec = CellSpec::default();
let mut recovered: Option<Span<'src>> = None;
let mut i = 0;
while i < len {
if data
.get(i..)
.is_some_and(|rest| rest.starts_with(separator))
{
if i > 0 && bytes.get(i - 1).copied() == Some(b'\\') {
i += sep_len;
continue;
}
let mut tok_start = i;
while tok_start > 0
&& !matches!(
bytes.get(tok_start - 1).copied(),
Some(b' ' | b'\t' | b'\n')
)
{
tok_start -= 1;
}
let token = data.get(tok_start..i).unwrap_or_default();
let spec = if token.is_empty() {
Some(CellSpec::default())
} else {
parse_cell_spec(token)
};
let (content_end, next_spec) = match spec {
Some(spec) => (tok_start, spec),
None => (i, CellSpec::default()),
};
match content_start {
Some(start) => {
cells.push(RawCell {
spec: cur_spec,
content: region.slice(start..content_end),
});
}
None => {
let leading = region.slice(0..content_end);
if !leading.data().trim().is_empty() {
cells.push(RawCell {
spec: CellSpec::default(),
content: leading,
});
recovered = Some(leading);
}
}
}
cur_spec = next_spec;
content_start = Some(i + sep_len);
i += sep_len;
continue;
}
i += 1;
}
if let Some(start) = content_start {
cells.push(RawCell {
spec: cur_spec,
content: region.slice(start..len),
});
}
(cells, recovered)
}
fn parse_cell_spec(token: &str) -> Option<CellSpec> {
let b = token.as_bytes();
let mut i = 0;
let mut colspan = 1;
let mut rowspan = 1;
let mut repeat = 1;
let col_start = i;
let mut j = i;
while matches!(b.get(j).copied(), Some(c) if c.is_ascii_digit()) {
j += 1;
}
let col_end = j;
let mut has_dot = false;
let mut row_start = j;
if b.get(j).copied() == Some(b'.') {
has_dot = true;
j += 1;
row_start = j;
while matches!(b.get(j).copied(), Some(c) if c.is_ascii_digit()) {
j += 1;
}
}
let row_end = j;
match b.get(j).copied() {
Some(b'+') => {
let col_digits = token.get(col_start..col_end).unwrap_or_default();
if !col_digits.is_empty() {
colspan = col_digits.parse().unwrap_or(1);
}
if has_dot {
let row_digits = token.get(row_start..row_end).unwrap_or_default();
if !row_digits.is_empty() {
rowspan = row_digits.parse().unwrap_or(1);
}
}
i = j + 1;
}
Some(b'*') => {
let col_digits = token.get(col_start..col_end).unwrap_or_default();
if !col_digits.is_empty() {
repeat = col_digits.parse().unwrap_or(1);
}
i = j + 1;
}
_ => {}
}
let mut h_align = None;
match b.get(i).copied() {
Some(b'<') => {
h_align = Some(HorizontalAlignment::Left);
i += 1;
}
Some(b'>') => {
h_align = Some(HorizontalAlignment::Right);
i += 1;
}
Some(b'^') => {
h_align = Some(HorizontalAlignment::Center);
i += 1;
}
_ => {}
}
let mut v_align = None;
if b.get(i).copied() == Some(b'.') {
match b.get(i + 1).copied() {
Some(b'<') => {
v_align = Some(VerticalAlignment::Top);
i += 2;
}
Some(b'>') => {
v_align = Some(VerticalAlignment::Bottom);
i += 2;
}
Some(b'^') => {
v_align = Some(VerticalAlignment::Middle);
i += 2;
}
_ => {}
}
}
let mut style = None;
if let Some(c) = b.get(i).copied()
&& c.is_ascii_lowercase()
{
style = match c {
b'a' => Some(ColumnStyle::AsciiDoc),
b'd' => Some(ColumnStyle::Default),
b'e' => Some(ColumnStyle::Emphasis),
b'h' => Some(ColumnStyle::Header),
b'l' => Some(ColumnStyle::Literal),
b'm' => Some(ColumnStyle::Monospace),
b's' => Some(ColumnStyle::Strong),
_ => None,
};
i += 1;
}
if i == b.len() {
Some(CellSpec {
h_align,
v_align,
style,
colspan,
rowspan,
repeat,
})
} else {
None
}
}
fn trim_surrounding_whitespace(s: Span<'_>) -> Span<'_> {
let data = s.data();
let start = data.len() - data.trim_start().len();
let len = data.trim().len();
s.slice(start..start + len)
}
fn trim_cell_content(s: Span<'_>, style: ColumnStyle) -> Span<'_> {
let data = s.data();
match style {
ColumnStyle::Literal => {
let end = data.trim_end().len();
let mut start = 0;
while data[start..end].starts_with('\n') {
start += 1;
}
s.slice(start..end)
}
ColumnStyle::AsciiDoc => {
let end = data.trim_end().len();
if data[..end].starts_with('\n') {
let mut start = 0;
while data[start..end].starts_with('\n') {
start += 1;
}
s.slice(start..end)
} else {
let start = end - data[..end].trim_start().len();
s.slice(start..end)
}
}
_ => trim_surrounding_whitespace(s),
}
}
fn first_nonblank_line(mut rest: Span<'_>) -> Option<Span<'_>> {
while !rest.is_empty() {
let line = rest.take_line();
if !line.item.data().trim().is_empty() {
return Some(line.item);
}
rest = line.after;
}
None
}
fn psv_line_starts_cell(line: &str, separator: &str) -> bool {
match line.find(separator) {
Some(pos) => {
let prefix = line[..pos].trim_start();
prefix.is_empty() || parse_cell_spec(prefix).is_some()
}
None => false,
}
}
fn line_has_unclosed_quote(line: &str) -> bool {
line.bytes().filter(|&b| b == b'"').count() % 2 == 1
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::{AsciiDocCell, OwnedCell, OwnedCellInner, TocConfig};
use crate::parser::{
HtmlSubstitutionRenderer, ReferenceResolver, ResolutionContext, ResolvedReference,
};
struct NoopResolver;
impl ReferenceResolver for NoopResolver {
fn resolve(&self, _context: &ResolutionContext<'_>) -> Option<ResolvedReference> {
None
}
}
#[test]
fn resolve_references_skips_shared_owned_cell() {
let mut cell = AsciiDocCell::Owned(Arc::new(OwnedCell::new(String::new(), |_source| {
OwnedCellInner {
title: None,
inline: false,
toc: TocConfig::disabled(),
blocks: vec![],
}
})));
let shared = cell.clone();
let mut warnings = vec![];
cell.resolve_references(&NoopResolver, &HtmlSubstitutionRenderer {}, &mut warnings);
assert!(warnings.is_empty());
assert_eq!(cell, shared);
}
}