fix(toc): honour toc_header/level in single-wiki config + div.toc on export
CI / cargo fmt --check (push) Successful in 48s
CI / cargo clippy (push) Successful in 54s
CI / cargo test (push) Successful in 1m4s
CI / editor keymaps (push) Successful in 1m38s

Two TOC bugs:

1. :NuwikiTOC ignored toc_header / toc_header_level (always "= Contents ="
   at level 1). In the single-wiki shorthand the server only carried
   file_extension/syntax from the top-level options into the synthesized
   wiki; every other per-wiki key (toc_header, toc_header_level,
   links_header, html_path, auto_export, …) was dropped. Add
   single_wiki_from_value(), which re-parses the top-level object as a full
   RawWiki (injecting root from wiki_root), so all per-wiki keys set at the
   top level flow through. Wired into from_init_params + apply_change.

2. HTML export put class="toc" on the <hN> element, but upstream vimwiki
   (and its stylesheet) wraps the TOC heading in <div class="toc">…</div>.
   render_heading now emits the div wrapper, matching upstream so .toc CSS
   applies. With (1) fixed, detection also works for a custom toc_header.

Tests: single_wiki_shorthand_honours_top_level_per_wiki_keys (config),
toc_header_heading_wrapped_in_div_toc + non_toc_heading_not_wrapped
(renderer). Full suite 572 passed; clippy clean; config-parity goldens
match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 22:56:17 +00:00
parent f3d8af5f23
commit fc0d74bfbe
4 changed files with 98 additions and 35 deletions
+32 -16
View File
@@ -639,14 +639,11 @@ impl Config {
let wikis = if let Some(list) = raw.wikis {
list.into_iter().map(WikiConfig::from).collect()
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
let mut wc = WikiConfig::from_root(root);
if let Some(ext) = raw.file_extension {
wc.file_extension = ext;
}
if let Some(s) = raw.syntax {
wc.syntax = s;
}
} else if let Some(wc) = params
.initialization_options
.as_ref()
.and_then(single_wiki_from_value)
{
vec![wc]
} else if let Some(folder) = first_workspace_folder(params) {
vec![WikiConfig::from_root(folder)]
@@ -687,14 +684,7 @@ impl Config {
};
if let Some(list) = raw.wikis {
self.wikis = list.into_iter().map(WikiConfig::from).collect();
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
let mut wc = WikiConfig::from_root(root);
if let Some(ext) = raw.file_extension {
wc.file_extension = ext;
}
if let Some(s) = raw.syntax {
wc.syntax = s;
}
} else if let Some(wc) = single_wiki_from_value(inner) {
self.wikis = vec![wc];
}
// Only touch diagnostic settings when the payload carries them, so a
@@ -1042,6 +1032,32 @@ impl From<RawWiki> for WikiConfig {
}
}
/// Build the single-wiki [`WikiConfig`] from the flat top-level options object
/// (single-wiki shorthand: a `wiki_root` plus any per-wiki keys set alongside
/// it). Returns `None` when there's no expandable `wiki_root` (so callers fall
/// through to the workspace-folder default) or a `wikis` list is present.
///
/// Re-uses the full [`RawWiki`] schema so **every** per-wiki key set at the top
/// level — `toc_header`/`toc_header_level`, `links_header`, `html_path`,
/// `auto_export`, … — is honoured, not just `file_extension`/`syntax`. The
/// shorthand keys on `wiki_root`; `RawWiki` keys on `root`, so we inject it.
fn single_wiki_from_value(value: &serde_json::Value) -> Option<WikiConfig> {
let obj = value.as_object()?;
if obj.contains_key("wikis") {
return None;
}
let wiki_root = obj.get("wiki_root").and_then(|v| v.as_str())?;
// Bail (→ workspace-folder fallback) when the root can't be expanded.
expand_path(wiki_root)?;
let mut wiki_obj = obj.clone();
wiki_obj.insert(
"root".to_string(),
serde_json::Value::String(wiki_root.to_string()),
);
let raw: RawWiki = serde_json::from_value(serde_json::Value::Object(wiki_obj)).ok()?;
Some(WikiConfig::from(raw))
}
fn expand_path(s: &str) -> Option<PathBuf> {
let trimmed = s.trim();
if trimmed.is_empty() {
@@ -152,6 +152,28 @@ fn v1_0_single_wiki_desugars_to_one_entry() {
assert_eq!(cfg.wikis[0].syntax, "vimwiki");
}
#[test]
fn single_wiki_shorthand_honours_top_level_per_wiki_keys() {
// Regression: a single-wiki user sets per-wiki keys at the top level
// (alongside wiki_root); they must reach the synthesized wiki, not just
// file_extension/syntax. Previously toc_header_level etc. were dropped.
let cfg = config_from_json(json!({
"wiki_root": "/tmp/vimwiki",
"toc_header": "Table of Contents",
"toc_header_level": 2,
"links_header_level": 3,
"auto_export": true,
"html_path": "/tmp/out",
}));
assert_eq!(cfg.wikis.len(), 1);
let w = &cfg.wikis[0];
assert_eq!(w.toc_header, "Table of Contents");
assert_eq!(w.toc_header_level, 2);
assert_eq!(w.links_header_level, 3);
assert!(w.html.auto_export);
assert_eq!(w.html.html_path, PathBuf::from("/tmp/out"));
}
#[test]
fn explicit_wikis_list_overrides_legacy_shape() {
let cfg = config_from_json(json!({