diff --git a/crates/nuwiki-core/src/render/html.rs b/crates/nuwiki-core/src/render/html.rs
index 2e4f935..3d2558b 100644
--- a/crates/nuwiki-core/src/render/html.rs
+++ b/crates/nuwiki-core/src/render/html.rs
@@ -60,8 +60,9 @@ pub struct HtmlRenderer {
/// section number (e.g. `.` → `1.2. Heading`). Empty by default.
header_numbering_sym: String,
/// vimwiki `toc_header`: the heading text that identifies the TOC
- /// section. When a heading matches this value (case-insensitive), it
- /// gets `class="toc"` in HTML export.
+ /// section. A heading matching this value (case-insensitive) is wrapped
+ /// in `
`, matching upstream's HTML so vimwiki's stylesheet
+ // (`.toc { … }`) applies. The class goes on the wrapping div, not the
+ // heading element.
+ let is_toc = !self.toc_header.is_empty() && anchor.eq_ignore_ascii_case(&self.toc_header);
if is_toc {
- classes.push("toc");
+ w.write_all(b"
")?;
}
- if n.centered {
- classes.push("centered");
- }
- let class = if classes.is_empty() {
- String::new()
+ let class = if n.centered {
+ " class=\"centered\""
} else {
- format!(" class=\"{}\"", classes.join(" "))
+ ""
};
write!(w, "")
+ write!(w, "")?;
+ if is_toc {
+ w.write_all(b"
")?;
+ }
+ writeln!(w)
}
fn render_paragraph(&self, n: &ParagraphNode, w: &mut dyn Write) -> io::Result<()> {
diff --git a/crates/nuwiki-core/tests/html_renderer.rs b/crates/nuwiki-core/tests/html_renderer.rs
index d77de5f..1ead749 100644
--- a/crates/nuwiki-core/tests/html_renderer.rs
+++ b/crates/nuwiki-core/tests/html_renderer.rs
@@ -253,6 +253,33 @@ fn heading_id_matches_anchor_link_href() {
);
}
+#[test]
+fn toc_header_heading_wrapped_in_div_toc() {
+ // The TOC section heading gets a `
` wrapper (vimwiki
+ // scheme) so `.toc` stylesheet rules apply. The class is on the div, not
+ // the heading.
+ let doc = VimwikiSyntax::new().parse("== Contents ==\n");
+ let out = HtmlRenderer::new()
+ .with_toc_header("Contents")
+ .render_to_string(&doc)
+ .unwrap();
+ assert!(
+ out.contains("
Contents
"),
+ "got: {out}"
+ );
+}
+
+#[test]
+fn non_toc_heading_not_wrapped() {
+ let doc = VimwikiSyntax::new().parse("== Intro ==\n");
+ let out = HtmlRenderer::new()
+ .with_toc_header("Contents")
+ .render_to_string(&doc)
+ .unwrap();
+ assert!(out.contains("
Intro
"), "got: {out}");
+ assert!(!out.contains("class=\"toc\""), "got: {out}");
+}
+
#[test]
fn interwiki_numbered_link() {
let out = render("[[wiki1:Page]]\n");
diff --git a/crates/nuwiki-lsp/src/config.rs b/crates/nuwiki-lsp/src/config.rs
index d4e3025..b88c92c 100644
--- a/crates/nuwiki-lsp/src/config.rs
+++ b/crates/nuwiki-lsp/src/config.rs
@@ -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
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 {
+ 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 {
let trimmed = s.trim();
if trimmed.is_empty() {
diff --git a/crates/nuwiki-lsp/tests/index_and_config.rs b/crates/nuwiki-lsp/tests/index_and_config.rs
index 996dc27..96e0dbb 100644
--- a/crates/nuwiki-lsp/tests/index_and_config.rs
+++ b/crates/nuwiki-lsp/tests/index_and_config.rs
@@ -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!({