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
+17 -19
View File
@@ -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 `<div class="toc">…</div>` on export, matching upstream so vimwiki's
/// stylesheet applies.
toc_header: String,
/// vimwiki `valid_html_tags`: inline HTML tag names passed through to
/// export verbatim (instead of escaping `<`/`>`). Empty = escape all.
@@ -327,25 +328,18 @@ impl HtmlRenderer {
fn render_heading(&self, n: &HeadingNode, number: &str, w: &mut dyn Write) -> io::Result<()> {
let level = n.level.clamp(1, 6);
let anchor = crate::ast::inline_text(&n.children);
let is_toc = self
.toc_header
.as_bytes()
.iter()
.zip(anchor.as_bytes())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
&& !self.toc_header.is_empty()
&& self.toc_header.len() == anchor.len();
let mut classes = Vec::new();
// The TOC section heading (vimwiki `toc_header`) is wrapped in a
// `<div class="toc">`, 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"<div class=\"toc\">")?;
}
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, "<h{level}{class} id=\"")?;
write_escaped(&anchor, w)?;
@@ -354,7 +348,11 @@ impl HtmlRenderer {
write_escaped(number, w)?;
}
self.render_inlines(&n.children, w)?;
writeln!(w, "</h{level}>")
write!(w, "</h{level}>")?;
if is_toc {
w.write_all(b"</div>")?;
}
writeln!(w)
}
fn render_paragraph(&self, n: &ParagraphNode, w: &mut dyn Write) -> io::Result<()> {
+27
View File
@@ -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 `<div class="toc">` 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("<div class=\"toc\"><h2 id=\"Contents\">Contents</h2></div>"),
"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("<h2 id=\"Intro\">Intro</h2>"), "got: {out}");
assert!(!out.contains("class=\"toc\""), "got: {out}");
}
#[test]
fn interwiki_numbered_link() {
let out = render("[[wiki1:Page]]\n");