214d54e3b9
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>
187 lines
5.5 KiB
Rust
187 lines
5.5 KiB
Rust
//! Cluster 1 of the parity-audit punch list: per-wiki config keys
|
|
//! (vimwiki globals shipped through `initializationOptions`) and the
|
|
//! HTML renderer's table-span attributes.
|
|
|
|
use nuwiki_core::ast::{
|
|
BlockNode, DocumentNode, InlineNode, ParagraphNode, Span, TableCellNode, TableNode,
|
|
TableRowNode, TextNode,
|
|
};
|
|
use nuwiki_core::render::{HtmlRenderer, Renderer};
|
|
use nuwiki_lsp::config::{config_from_json, WikiConfig};
|
|
|
|
// ===== Per-wiki config keys =====
|
|
|
|
#[test]
|
|
fn defaults_carry_vimwiki_per_wiki_keys() {
|
|
let cfg = WikiConfig::empty();
|
|
assert_eq!(cfg.index, "index");
|
|
assert_eq!(cfg.diary_frequency, "daily");
|
|
assert_eq!(cfg.diary_start_week_day, "monday");
|
|
assert_eq!(cfg.diary_caption_level, 1);
|
|
assert_eq!(cfg.diary_sort, "desc");
|
|
assert_eq!(cfg.diary_header, "Diary");
|
|
assert_eq!(cfg.maxhi, 1);
|
|
assert_eq!(cfg.listsyms, " .oOX");
|
|
assert!(cfg.listsyms_propagate);
|
|
assert_eq!(cfg.list_margin, -1);
|
|
assert_eq!(cfg.links_space_char, " ");
|
|
assert!(cfg.nested_syntaxes.is_empty());
|
|
assert!(!cfg.auto_toc);
|
|
}
|
|
|
|
#[test]
|
|
fn raw_wiki_parses_every_new_key() {
|
|
let cfg = config_from_json(serde_json::json!({
|
|
"wikis": [{
|
|
"root": "/tmp/w",
|
|
"index": "home",
|
|
"diary_frequency": "weekly",
|
|
"diary_start_week_day": "sunday",
|
|
"diary_caption_level": 2,
|
|
"diary_sort": "asc",
|
|
"diary_header": "Journal",
|
|
"maxhi": 6,
|
|
"listsyms": "abcde",
|
|
"listsyms_propagate": false,
|
|
"list_margin": 2,
|
|
"links_space_char": "_",
|
|
"nested_syntaxes": { "python": "python", "ruby": "ruby" },
|
|
"auto_toc": true,
|
|
}],
|
|
}));
|
|
let w = &cfg.wikis[0];
|
|
assert_eq!(w.index, "home");
|
|
assert_eq!(w.diary_frequency, "weekly");
|
|
assert_eq!(w.diary_start_week_day, "sunday");
|
|
assert_eq!(w.diary_caption_level, 2);
|
|
assert_eq!(w.diary_sort, "asc");
|
|
assert_eq!(w.diary_header, "Journal");
|
|
assert_eq!(w.maxhi, 6);
|
|
assert_eq!(w.listsyms, "abcde");
|
|
assert!(!w.listsyms_propagate);
|
|
assert_eq!(w.list_margin, 2);
|
|
assert_eq!(w.links_space_char, "_");
|
|
assert_eq!(
|
|
w.nested_syntaxes.get("python").map(String::as_str),
|
|
Some("python")
|
|
);
|
|
assert!(w.auto_toc);
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_single_wiki_shape_picks_up_defaults() {
|
|
let cfg = config_from_json(serde_json::json!({
|
|
"wiki_root": "/tmp/legacy",
|
|
}));
|
|
assert_eq!(cfg.wikis.len(), 1);
|
|
assert_eq!(cfg.wikis[0].index, "index");
|
|
assert_eq!(cfg.wikis[0].diary_frequency, "daily");
|
|
}
|
|
|
|
// ===== Table colspan/rowspan =====
|
|
|
|
fn cell(text: &str, col_span: bool, row_span: bool) -> TableCellNode {
|
|
let s = Span::default();
|
|
TableCellNode {
|
|
span: s,
|
|
children: vec![InlineNode::Text(TextNode {
|
|
span: s,
|
|
content: text.into(),
|
|
})],
|
|
col_span,
|
|
row_span,
|
|
}
|
|
}
|
|
|
|
fn table(rows: Vec<Vec<TableCellNode>>, has_header: bool) -> DocumentNode {
|
|
let span = Span::default();
|
|
let rows: Vec<TableRowNode> = rows
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, cells)| TableRowNode {
|
|
span,
|
|
cells,
|
|
is_header: has_header && i == 0,
|
|
})
|
|
.collect();
|
|
DocumentNode {
|
|
span,
|
|
metadata: Default::default(),
|
|
children: vec![BlockNode::Table(TableNode {
|
|
span,
|
|
rows,
|
|
has_header,
|
|
alignments: Vec::new(),
|
|
})],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn colspan_marker_folds_into_lead_cell() {
|
|
// | a | b | c |
|
|
// | d | > | e | → second row has b spanning 2 cols (cell index 1 is >)
|
|
let doc = table(
|
|
vec![
|
|
vec![
|
|
cell("a", false, false),
|
|
cell("b", false, false),
|
|
cell("c", false, false),
|
|
],
|
|
vec![
|
|
cell("d", false, false),
|
|
cell(">", true, false),
|
|
cell("e", false, false),
|
|
],
|
|
],
|
|
false,
|
|
);
|
|
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
|
assert!(out.contains(r#"<td colspan="2">d</td>"#), "got: {out}");
|
|
assert!(!out.contains(r#"class="col-span""#));
|
|
}
|
|
|
|
#[test]
|
|
fn rowspan_marker_folds_into_lead_cell_above() {
|
|
let doc = table(
|
|
vec![
|
|
vec![cell("a", false, false), cell("b", false, false)],
|
|
vec![cell("c", false, false), cell("\\/", false, true)],
|
|
],
|
|
false,
|
|
);
|
|
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
|
assert!(out.contains(r#"<td rowspan="2">b</td>"#), "got: {out}");
|
|
}
|
|
|
|
#[test]
|
|
fn no_spans_emits_no_attrs() {
|
|
let doc = table(
|
|
vec![
|
|
vec![cell("a", false, false), cell("b", false, false)],
|
|
vec![cell("c", false, false), cell("d", false, false)],
|
|
],
|
|
false,
|
|
);
|
|
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
|
assert!(!out.contains("colspan="));
|
|
assert!(!out.contains("rowspan="));
|
|
}
|
|
|
|
// Make sure ParagraphNode is exercised so the import stays live.
|
|
#[test]
|
|
fn paragraph_render_is_unchanged() {
|
|
let p = DocumentNode {
|
|
span: Span::default(),
|
|
metadata: Default::default(),
|
|
children: vec![BlockNode::Paragraph(ParagraphNode {
|
|
span: Span::default(),
|
|
children: vec![InlineNode::Text(TextNode {
|
|
span: Span::default(),
|
|
content: "hi".into(),
|
|
})],
|
|
})],
|
|
};
|
|
let out = HtmlRenderer::new().render_to_string(&p).unwrap();
|
|
assert!(out.contains("hi"));
|
|
}
|