Files
gffranco 214d54e3b9 parity(7): table cell alignment markers (|:--|--:|:--:|)
The Markdown-style alignment anchors on a table's header-separator
row now propagate from the lexer through the parser into the AST,
and the HTML renderer emits a `style="text-align: …;"` attribute on
each affected `<th>` / `<td>`.

  |:--|   → left
  |--:|   → right
  |:--:|  → center
  |---|   → default (no style attr)

Encoding choice: rather than threading the source string into the
parser, the lexer now extracts per-cell alignment when it recognises
the separator row and carries the `Vec<TableAlign>` inline on the
`TableHeaderRow` token. The parser unpacks it into `TableNode`'s
new `alignments: Vec<TableAlign>` field. Cells past the end of the
vector render with the default alignment.

`TableAlign` lives in `nuwiki_core::ast::block` alongside the table
nodes and is re-exported via `nuwiki_core::ast`.

Tests: 4 new in crates/nuwiki-core/tests/vimwiki_table_alignment.rs
covering plain dashes, all three anchor flavours, the HTML output
shape, and the no-style-attr case. The existing
`vimwiki_lexer::table_header_separator_row` was updated to assert
the new payload shape.

Gates: 425 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:45:43 +00:00

216 lines
6.3 KiB
Rust

//! Smoke test for the AST visitor: build a small document by hand and
//! confirm a Visitor descends into every nested node exactly once.
use std::collections::HashMap;
use nuwiki_core::ast::{
BlockNode, BoldNode, DocumentNode, ExternalLinkNode, HeadingNode, InlineNode, ListItemNode,
ListNode, ListSymbol, ParagraphNode, Span, TableCellNode, TableNode, TableRowNode, TextNode,
TransclusionNode, Visitor,
};
#[derive(Default)]
struct Counter {
headings: usize,
paragraphs: usize,
text: usize,
bold: usize,
list_items: usize,
table_cells: usize,
external_links: usize,
transclusions: usize,
blocks: usize,
inlines: usize,
}
impl Visitor for Counter {
fn visit_block(&mut self, node: &BlockNode) {
self.blocks += 1;
nuwiki_core::ast::walk_block(self, node);
}
fn visit_inline(&mut self, node: &InlineNode) {
self.inlines += 1;
nuwiki_core::ast::walk_inline(self, node);
}
fn visit_heading(&mut self, node: &HeadingNode) {
self.headings += 1;
for child in &node.children {
self.visit_inline(child);
}
}
fn visit_paragraph(&mut self, node: &ParagraphNode) {
self.paragraphs += 1;
for child in &node.children {
self.visit_inline(child);
}
}
fn visit_text(&mut self, _: &TextNode) {
self.text += 1;
}
fn visit_bold(&mut self, node: &BoldNode) {
self.bold += 1;
for child in &node.children {
self.visit_inline(child);
}
}
fn visit_list_item(&mut self, node: &ListItemNode) {
self.list_items += 1;
nuwiki_core::ast::walk_list_item(self, node);
}
fn visit_table_cell(&mut self, node: &TableCellNode) {
self.table_cells += 1;
for child in &node.children {
self.visit_inline(child);
}
}
fn visit_external_link(&mut self, node: &ExternalLinkNode) {
self.external_links += 1;
if let Some(desc) = &node.description {
for child in desc {
self.visit_inline(child);
}
}
}
fn visit_transclusion(&mut self, _: &TransclusionNode) {
self.transclusions += 1;
}
}
fn text(s: &str) -> InlineNode {
InlineNode::Text(TextNode {
span: Span::default(),
content: s.into(),
})
}
#[test]
fn visitor_descends_into_every_node() {
// = Title with *bold* word =
let heading = BlockNode::Heading(HeadingNode {
span: Span::default(),
level: 1,
centered: false,
children: vec![
text("Title with "),
InlineNode::Bold(BoldNode {
span: Span::default(),
children: vec![text("bold")],
}),
text(" word"),
],
});
// A paragraph containing an external link with two text children in its
// description, and a transclusion sibling.
let paragraph = BlockNode::Paragraph(ParagraphNode {
span: Span::default(),
children: vec![
InlineNode::ExternalLink(ExternalLinkNode {
span: Span::default(),
url: "https://example.com".into(),
description: Some(vec![text("see "), text("docs")]),
}),
InlineNode::Transclusion(TransclusionNode {
span: Span::default(),
url: "image.png".into(),
alt: None,
attrs: HashMap::new(),
}),
],
});
// - one
// - two
let list = BlockNode::List(ListNode {
span: Span::default(),
ordered: false,
symbol: ListSymbol::Dash,
items: vec![
ListItemNode {
span: Span::default(),
symbol: ListSymbol::Dash,
level: 0,
checkbox: None,
children: vec![text("one")],
sublist: None,
},
ListItemNode {
span: Span::default(),
symbol: ListSymbol::Dash,
level: 0,
checkbox: None,
children: vec![text("two")],
sublist: None,
},
],
});
// | a | b |
let table = BlockNode::Table(TableNode {
span: Span::default(),
has_header: false,
alignments: Vec::new(),
rows: vec![TableRowNode {
span: Span::default(),
is_header: false,
cells: vec![
TableCellNode {
span: Span::default(),
children: vec![text("a")],
col_span: false,
row_span: false,
},
TableCellNode {
span: Span::default(),
children: vec![text("b")],
col_span: false,
row_span: false,
},
],
}],
});
let doc = DocumentNode {
span: Span::default(),
metadata: Default::default(),
children: vec![heading, paragraph, list, table],
};
let mut counter = Counter::default();
counter.visit_document(&doc);
// Block-level: 4 top-level + 2 list items aren't blocks
assert_eq!(counter.blocks, 4, "top-level blocks");
assert_eq!(counter.headings, 1);
assert_eq!(counter.paragraphs, 1);
assert_eq!(counter.list_items, 2);
// 1 cell row with 2 cells
assert_eq!(counter.table_cells, 2);
// Inlines:
// heading: 3 ("Title with ", Bold, " word") + 1 (bold child) = 4
// paragraph: 2 (ExternalLink, Transclusion) + 2 (link desc) = 4
// list items: 2 (one each)
// table cells: 2 (one each)
// total: 12
assert_eq!(counter.inlines, 12);
// Text leaves: heading 3, link description 2, list items 2, table cells 2 = 9
assert_eq!(counter.text, 9, "Text leaves");
assert_eq!(counter.bold, 1);
assert_eq!(counter.external_links, 1);
assert_eq!(counter.transclusions, 1);
}
#[test]
fn document_default_is_empty() {
let doc = DocumentNode::default();
assert!(doc.children.is_empty());
assert_eq!(doc.metadata.title, None);
assert!(!doc.metadata.nohtml);
let mut counter = Counter::default();
counter.visit_document(&doc);
assert_eq!(counter.blocks, 0);
assert_eq!(counter.inlines, 0);
}