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>
This commit is contained in:
2026-05-12 21:45:43 +00:00
parent 2562a046db
commit 214d54e3b9
9 changed files with 147 additions and 13 deletions
@@ -210,7 +210,7 @@ impl<'a> ParseState<'a> {
K::HorizontalRule => self.parse_horizontal_rule(),
K::ListMarker { .. } => self.parse_list(),
K::BlockquoteMarker | K::BlockquoteIndent => self.parse_blockquote(),
K::TableSep | K::TableHeaderRow => self.parse_table(),
K::TableSep | K::TableHeaderRow(_) => self.parse_table(),
K::PreformattedOpen { .. } => self.parse_preformatted(),
K::MathBlockOpen { .. } => self.parse_math_block(),
K::CommentLine(_) => self.parse_single_comment(),
@@ -620,11 +620,15 @@ impl<'a> ParseState<'a> {
let mut rows: Vec<TableRowNode> = Vec::new();
let mut next_is_header = false;
let mut has_header = false;
let mut alignments: Vec<crate::ast::TableAlign> = Vec::new();
while let Some(t) = self.peek() {
match &t.kind {
K::TableHeaderRow => {
K::TableHeaderRow(aligns) => {
has_header = true;
if alignments.is_empty() {
alignments = aligns.clone();
}
if let Some(last) = rows.last_mut() {
last.is_header = true;
}
@@ -638,7 +642,6 @@ impl<'a> ParseState<'a> {
span_end = row.span.end;
rows.push(row);
if next_is_header {
// Header sep applies to the *previous* row, not this one.
next_is_header = false;
}
}
@@ -649,6 +652,7 @@ impl<'a> ParseState<'a> {
span: Span::new(span_start, span_end),
rows,
has_header,
alignments,
})
}
@@ -870,7 +874,7 @@ fn starts_new_block(kind: &K) -> bool {
| K::BlockquoteMarker
| K::BlockquoteIndent
| K::TableSep
| K::TableHeaderRow
| K::TableHeaderRow(_)
| K::PreformattedOpen { .. }
| K::MathBlockOpen { .. }
| K::CommentLine(_)