use crate::engine::ast::{
Block, InlineContent, Table, TableCaption, TableCell, TableRow, TextRange,
};
use super::block::{is_blank, range, trim_range};
pub(super) enum TableOutcome {
Consumed,
Continuation,
Closed {
rest: Option<(usize, usize)>,
},
}
pub(super) struct TableCtx {
attributes: Option<TextRange>,
caption: Option<TableCaption>,
rows: Vec<TableRow>,
row_open: bool,
pending: Vec<Block>,
pending_at: usize,
saw_cell: bool,
}
impl TableCtx {
pub(super) fn new(attributes: Option<TextRange>) -> Self {
Self {
attributes,
caption: None,
rows: Vec::new(),
row_open: false,
pending: Vec::new(),
pending_at: 0,
saw_cell: false,
}
}
pub(super) fn handle_line(&mut self, flat: &str, ls: usize, le: usize) -> TableOutcome {
let bytes = flat.as_bytes();
let line = &bytes[ls..le];
if line.starts_with(b"|}") {
let rest = if is_blank(flat, ls + 2, le) {
None
} else {
Some((ls + 2, le))
};
return TableOutcome::Closed { rest };
}
if line.starts_with(b"|+") {
let (attributes, content) = split_attr_segment(flat, ls + 2, le);
self.caption = Some(TableCaption {
attributes,
content,
});
return TableOutcome::Consumed;
}
if line.starts_with(b"|-") {
let mut p = ls + 1;
while p < le && bytes[p] == b'-' {
p += 1;
}
self.rows.push(TableRow {
attributes: trim_range(flat, p, le),
cells: Vec::new(),
});
self.row_open = true;
return TableOutcome::Consumed;
}
if line.first() == Some(&b'!') {
self.cell_line(flat, ls + 1, le, true);
return TableOutcome::Consumed;
}
if line.first() == Some(&b'|') {
self.cell_line(flat, ls + 1, le, false);
return TableOutcome::Consumed;
}
TableOutcome::Continuation
}
fn cell_line(&mut self, flat: &str, start: usize, end: usize, header: bool) {
self.saw_cell = true;
if !self.row_open {
self.rows.push(TableRow::default());
self.row_open = true;
}
for (s, e) in split_segments(flat, start, end, header) {
let (attributes, content) = split_attr_segment(flat, s, e);
let cell = TableCell {
header,
attributes,
content,
children: Vec::new(),
};
if let Some(row) = self.rows.last_mut() {
row.cells.push(cell);
}
}
}
pub(super) fn attach_continuation(&mut self, at: usize, blocks: Vec<Block>) {
if blocks.is_empty() {
return;
}
match self.current_cell() {
Some(cell) => cell.children.extend(blocks),
None => {
if self.pending.is_empty() {
self.pending_at = at;
}
self.pending.extend(blocks);
}
}
}
fn current_cell(&mut self) -> Option<&mut TableCell> {
if !self.row_open {
return None;
}
self.rows.last_mut()?.cells.last_mut()
}
pub(super) fn finish(self, closed: bool) -> Table {
let mut rows = self.rows;
if self.saw_cell && !self.pending.is_empty() {
rows.insert(
0,
TableRow {
attributes: None,
cells: vec![TableCell {
header: false,
attributes: None,
content: InlineContent::Raw(range(self.pending_at, self.pending_at)),
children: self.pending,
}],
},
);
}
Table {
attributes: self.attributes,
caption: self.caption,
rows,
closed,
}
}
}
fn split_segments(flat: &str, start: usize, end: usize, header: bool) -> Vec<(usize, usize)> {
let bytes = flat.as_bytes();
let mut out = Vec::new();
let mut seg = start;
let mut i = start;
while i + 1 < end {
let pair = &bytes[i..i + 2];
if pair == b"||" || (header && pair == b"!!") {
out.push((seg, i));
i += 2;
seg = i;
} else {
i += 1;
}
}
out.push((seg, end));
out
}
fn split_attr_segment(flat: &str, start: usize, end: usize) -> (Option<TextRange>, InlineContent) {
match find_attr_pipe(flat, start, end) {
Some(pipe) => (
trim_range(flat, start, pipe),
InlineContent::Raw(range(pipe + 1, end)),
),
None => (None, InlineContent::Raw(range(start, end))),
}
}
fn find_attr_pipe(flat: &str, start: usize, end: usize) -> Option<usize> {
let bytes = flat.as_bytes();
let mut i = start;
while i < end {
match bytes[i] {
0x7f => return None,
b'[' if i + 1 < end && bytes[i + 1] == b'[' => return None,
b'|' => {
let mut run = i + 1;
while run < end && bytes[run] == b'|' {
run += 1;
}
if run - i == 1 {
return Some(i);
}
i = run;
}
_ => i += 1,
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::ast::{DocTree, ListItem, ListItemKind, ListKind};
use crate::engine::parse::parse_blocks;
use crate::engine::strip;
use crate::warn::{Warning, WarningCollector};
fn parse(flat: &str) -> (DocTree, Vec<Warning>) {
let mut warnings = WarningCollector::new();
let doc = parse_blocks(flat, &mut warnings);
(doc, warnings.into_vec())
}
fn blocks(flat: &str) -> Vec<Block> {
let (doc, warnings) = parse(flat);
assert_eq!(warnings, vec![]);
doc.blocks
}
fn as_table(block: &Block) -> Option<&Table> {
match block {
Block::Table(table) => Some(table),
_ => None,
}
}
fn raw_text<'a>(flat: &'a str, content: &InlineContent) -> &'a str {
match content {
InlineContent::Raw(range) => range.slice(flat),
InlineContent::Parsed(_) => "",
}
}
fn r(flat: &str, sub: &str) -> TextRange {
let start = flat.find(sub).unwrap();
range(start, start + sub.len())
}
fn raw(flat: &str, sub: &str) -> InlineContent {
InlineContent::Raw(r(flat, sub))
}
fn heading(flat: &str, level: u8, name: &str) -> Block {
let inner = r(flat, name);
let bytes = flat.as_bytes();
let mut ls = inner.start as usize;
while ls > 0 && bytes[ls - 1] != b'\n' {
ls -= 1;
}
let mut le = inner.end as usize;
while le < flat.len() && bytes[le] != b'\n' {
le += 1;
}
Block::Heading {
level,
line: range(ls, le),
inner,
content: InlineContent::Raw(inner),
}
}
fn cell(header: bool, attributes: Option<TextRange>, content: InlineContent) -> TableCell {
TableCell {
header,
attributes,
content,
children: Vec::new(),
}
}
#[test]
fn test_helpers_reject_other_variants() {
assert!(as_table(&Block::HorizontalRule).is_none());
assert_eq!(raw_text("", &InlineContent::Parsed(Vec::new())), "");
}
#[test]
fn rows_cells_attributes_and_close() {
let flat = "{| class=\"wikitable\"\n|x\n|-\n|y||z\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: Some(r(flat, "class=\"wikitable\"")),
caption: None,
rows: vec![
TableRow {
attributes: None,
cells: vec![cell(false, None, raw(flat, "x"))],
},
TableRow {
attributes: None,
cells: vec![
cell(false, None, raw(flat, "y")),
cell(false, None, raw(flat, "z")),
],
},
],
closed: true,
})]
);
}
#[test]
fn header_line_splits_on_double_bang_and_double_pipe() {
let flat = "{|\n!h1!!h2||h3\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![TableRow {
attributes: None,
cells: vec![
cell(true, None, raw(flat, "h1")),
cell(true, None, raw(flat, "h2")),
cell(true, None, raw(flat, "h3")),
],
}],
closed: true,
})]
);
}
#[test]
fn header_cell_with_attributes() {
let flat = "{|\n! scope=col | H1 !!H2\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
let cells = &table.rows[0].cells;
assert_eq!(cells.len(), 2);
assert!(cells[0].header && cells[1].header);
assert_eq!(cells[0].attributes, Some(r(flat, "scope=col")));
assert_eq!(raw_text(flat, &cells[0].content), " H1 ");
assert_eq!(cells[1].attributes, None);
assert_eq!(raw_text(flat, &cells[1].content), "H2");
}
#[test]
fn caption_with_and_without_attributes_last_wins() {
let flat = "{|\n|+first\n|+ align=x | second\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
assert_eq!(
table.caption,
Some(TableCaption {
attributes: Some(r(flat, "align=x")),
content: raw(flat, " second"),
})
);
let plain = "{|\n|+plain\n|}";
let got = blocks(plain);
let table = as_table(&got[0]).unwrap();
assert_eq!(
table.caption,
Some(TableCaption {
attributes: None,
content: raw(plain, "plain"),
})
);
}
#[test]
fn caption_double_pipe_is_not_an_attribute_separator() {
let flat = "{|\n|+a||b\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
assert_eq!(
table.caption,
Some(TableCaption {
attributes: None,
content: raw(flat, "a||b"),
})
);
}
#[test]
fn cell_attribute_segment_splits_on_the_lone_pipe() {
let flat = "{|\n| style=\"q\" | y\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
assert_eq!(
table.rows[0].cells,
vec![cell(false, Some(r(flat, "style=\"q\"")), raw(flat, " y"))]
);
}
#[test]
fn link_before_pipe_suppresses_the_attribute_split() {
let flat = "{|\n| [[a|b]] | c\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
assert_eq!(
table.rows[0].cells,
vec![cell(false, None, raw(flat, " [[a|b]] | c"))]
);
}
#[test]
fn strip_markers_never_split_cells_or_attributes() {
let mut flat = String::from("{|\n|");
strip::write_marker(&mut flat, strip::MarkerKind::ExtTag, strip::MarkerId(2));
flat.push_str("|x||y\n|}");
let got = blocks(&flat);
let table = as_table(&got[0]).unwrap();
let cells = &table.rows[0].cells;
assert_eq!(cells.len(), 2);
assert_eq!(cells[0].attributes, None);
assert!(raw_text(&flat, &cells[0].content).starts_with('\u{7f}'));
assert!(raw_text(&flat, &cells[0].content).ends_with("|x"));
assert_eq!(raw_text(&flat, &cells[1].content), "y");
}
#[test]
fn row_attributes_and_extra_dashes() {
let flat = "{|\n|- style=\"r\"\n|u\n|----\n|w\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![
TableRow {
attributes: Some(r(flat, "style=\"r\"")),
cells: vec![cell(false, None, raw(flat, "u"))],
},
TableRow {
attributes: None,
cells: vec![cell(false, None, raw(flat, "w"))],
},
],
closed: true,
})]
);
}
#[test]
fn explicit_row_markers_keep_empty_rows() {
let flat = "{|\n|-\n|-\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![TableRow::default(), TableRow::default()],
closed: true,
})]
);
}
#[test]
fn nested_table_lands_in_the_cell_children() {
let flat = "{|\n|outer\n{|\n|inner\n|}\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
let outer_cell = &table.rows[0].cells[0];
assert_eq!(raw_text(flat, &outer_cell.content), "outer");
assert_eq!(
outer_cell.children,
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![TableRow {
attributes: None,
cells: vec![cell(false, None, raw(flat, "inner"))],
}],
closed: true,
})]
);
assert!(table.closed);
}
#[test]
fn continuation_lines_parse_as_blocks_inside_the_cell() {
let flat = "{|\n|x\n==h==\n*a\n*b\n p\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
let target = &table.rows[0].cells[0];
assert_eq!(raw_text(flat, &target.content), "x");
assert_eq!(
target.children,
vec![
heading(flat, 2, "h"),
Block::List {
kind: ListKind::Bullet,
items: vec![
ListItem {
kind: ListItemKind::Item,
content: Some(raw(flat, "a")),
children: Vec::new(),
},
ListItem {
kind: ListItemKind::Item,
content: Some(raw(flat, "b")),
children: Vec::new(),
},
],
},
Block::IndentPre {
lines: vec![raw(flat, "p")],
},
]
);
}
#[test]
fn pre_first_cell_lines_adopt_into_an_implicit_first_row() {
let flat = "{|\ngarbage\n|x\n|}";
let got = blocks(flat);
let table = as_table(&got[0]).unwrap();
assert_eq!(table.rows.len(), 2);
assert_eq!(
table.rows[0],
TableRow {
attributes: None,
cells: vec![TableCell {
header: false,
attributes: None,
content: InlineContent::Raw(TextRange::new(11, 11)),
children: vec![Block::Paragraph {
lines: vec![raw(flat, "garbage")],
}],
}],
}
);
assert_eq!(table.rows[1].cells, vec![cell(false, None, raw(flat, "x"))]);
}
#[test]
fn pre_first_cell_lines_drop_when_no_cell_ever_appears() {
let flat = "{|\ngarbage\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![],
closed: true,
})]
);
}
#[test]
fn blank_lines_inside_a_table_are_inert() {
let flat = "{|\n\n|x\n\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![TableRow {
attributes: None,
cells: vec![cell(false, None, raw(flat, "x"))],
}],
closed: true,
})]
);
}
#[test]
fn unclosed_tables_auto_close_with_a_warning() {
let flat = "{|\n|x\n{|\n|y";
let (doc, warnings) = parse(flat);
assert_eq!(warnings, vec![Warning::UnclosedTable]);
let outer = as_table(&doc.blocks[0]).unwrap();
assert!(!outer.closed);
let inner = as_table(&outer.rows[0].cells[0].children[0]).unwrap();
assert!(!inner.closed);
assert_eq!(inner.rows[0].cells, vec![cell(false, None, raw(flat, "y"))]);
}
#[test]
fn colon_indented_table_nests_in_a_definition_list() {
let flat = "::{|\n|x\n|}";
let expected_table = Block::Table(Table {
attributes: None,
caption: None,
rows: vec![TableRow {
attributes: None,
cells: vec![cell(false, None, raw(flat, "x"))],
}],
closed: true,
});
assert_eq!(
blocks(flat),
vec![Block::List {
kind: ListKind::Definition,
items: vec![ListItem {
kind: ListItemKind::Definition,
content: None,
children: vec![Block::List {
kind: ListKind::Definition,
items: vec![ListItem {
kind: ListItemKind::Definition,
content: None,
children: vec![expected_table],
}],
}],
}],
}]
);
}
#[test]
fn text_after_the_closing_brace_becomes_a_following_line() {
let flat = "{|\n|x\n|}tail";
let got = blocks(flat);
assert_eq!(got.len(), 2);
assert!(as_table(&got[0]).unwrap().closed);
assert_eq!(
got[1],
Block::Paragraph {
lines: vec![raw(flat, "tail")],
}
);
let blank_rest = "{|\n|x\n|} \t";
assert_eq!(blocks(blank_rest).len(), 1);
}
#[test]
fn degenerate_cell_lines() {
let flat = "{|\n|\n||z\n|}";
assert_eq!(
blocks(flat),
vec![Block::Table(Table {
attributes: None,
caption: None,
rows: vec![TableRow {
attributes: None,
cells: vec![
cell(false, None, InlineContent::Raw(range(4, 4))),
cell(false, None, raw(flat, "z")),
],
}],
closed: true,
})]
);
}
}