parity(1): vimwiki per-wiki config keys, table colspan/rowspan,
named link commands, mouse maps
Parity audit Cluster 1 — small wins.
WikiConfig (server) extended with 14 vimwiki globals:
- `index` (`g:vimwiki_index`), wired into `:VimwikiIndex` so users
with a custom index page stem (e.g. `home`) get the right file.
- `diary_frequency`, `diary_start_week_day`, `diary_caption_level`,
`diary_sort`, `diary_header` — diary keys (weekly/monthly/yearly
semantics land in Cluster 4; the keys parse now so config
migration doesn't need to wait).
- `maxhi`, `listsyms`, `listsyms_propagate`, `list_margin`,
`links_space_char`, `nested_syntaxes`, `auto_toc` — list +
highlight knobs the renderer/handlers can consult.
All keys threaded through `RawWiki` → `WikiConfig::from(RawWiki)`,
defaults centralised in `wiki_defaults()` so `empty()`, `from_root()`,
the legacy single-wiki path, and the `RawWiki` impl stay in sync.
Lua front-door (`lua/nuwiki/config.lua`) now documents the multi-wiki
`wikis = {…}` shape with every accepted per-wiki key — users who
prefer Lua tables no longer have to round-trip through
`init_options` raw JSON.
HTML renderer (`crates/nuwiki-core/src/render/html.rs`):
- New `table_spans()` pre-pass resolves vimwiki's `>` (col_span) and
`\\/` (row_span) continuation markers into proper HTML
`colspan="N"` / `rowspan="N"` attributes on the lead cell. Continuation
cells emit nothing, matching what browsers expect.
- The old behaviour (emitting `class="col-span"` / `class="row-span"`
as visual markers) is gone — replaced with real merging so the
rendered HTML matches the source semantics.
Named ex-commands:
- `:VimwikiNextLink` / `:VimwikiPrevLink` — were keymapped only
(`<Tab>`/`<S-Tab>`); now have explicit `:` commands in both editor
paths, dispatching to new `nuwiki.commands.link_next` / `link_prev`
pure-Vim/Lua helpers.
- `:VimwikiBaddLink` — adds the link target's file to the buffer
list without switching focus. Goes through
`textDocument/definition` and runs `:badd <fname>` on the resolved
URI.
Mouse maps (opt-in via `mappings.mouse = true` in Lua or
`g:nuwiki_mouse_mappings = 1` in Vim):
- `<2-LeftMouse>` follows, `<S-2-LeftMouse>` / `<C-2-LeftMouse>`
follow in split/vsplit, `<MiddleMouse>` adds to buflist,
`<RightMouse>` goes back. Mirrors upstream vimwiki's defaults.
Tests: 7 new in `parity_cluster_1.rs` covering the per-wiki config
defaults + raw JSON parse, the legacy-single-wiki path, colspan
folding into a lead cell with no leftover class markers, rowspan
across rows, no-attrs when the table has no spans, and a sanity
paragraph render. Total 421 Rust tests pass; clippy clean; both
keymap harnesses still green (20 nvim + 12 vim).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
//! 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,
|
||||
})],
|
||||
}
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
Reference in New Issue
Block a user