fix(html): match vimwiki export output for headings, code fences, tags, anchors (#6)
Exported HTML diverged from upstream vimwiki's :VimwikiAll2HTML in ways that break custom templates' CSS/JS and deep-links. Bring the renderer to parity on the structural differences reported in #6: 1. Headings — restore vimwiki's structure: a wrapping `<div id="{hierarchical}">` (parent anchors joined by `-`), `class="header"`, and an in-heading `<a href="#{hierarchical}">` self-link. The flat `id` stays on the heading element so intra-page TOC / `#anchor` links keep resolving. Ancestor path is tracked while walking top-level headings. 2. Code fences — emit vimwiki's `<pre {raw-attrs}>` (the verbatim text after `{{{`, e.g. `<pre python>`) instead of `<pre><code class="language-X">`, so highlighters wired for vimwiki output apply. The fence text is now preserved verbatim through the lexer/AST (PreformattedNode.attrs). 3. Inline tag ids — drop the `tag-` prefix (`id="wiki"`, not `id="tag-wiki"`). Also fixes a real inconsistency: the LSP validated `[[Page#wiki]]` as resolvable while the HTML emitted `id="tag-wiki"`, so the exported link was broken. 4. Same-page anchors — `[[#Section]]` resolves to `index.html#Section` (current page filename + fragment), matching vimwiki, rather than a bare `#Section`. Also ship vimwiki's stock style.css verbatim as the default stylesheet (was a ~7-line minimal one) so exports look identical out of the box and the `.header`/`.tag`/`.toc`/`done*` classes are styled. Centered headings and the `<div class="toc">` Contents wrapper keep their existing form. Tests updated across core + lsp to the new output; added coverage for hierarchical ids, raw fence attrs, bare tag ids, and the same-page anchor filename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -63,7 +63,11 @@ fn heading_levels() {
|
||||
for level in 1..=6 {
|
||||
let bars = "=".repeat(level);
|
||||
let out = render(&format!("{bars} Title {bars}\n"));
|
||||
assert!(out.starts_with(&format!("<h{level} id=\"Title\">Title</h{level}>")));
|
||||
// vimwiki structure: <div id="hier"><h{l} id="flat" class="header">
|
||||
// <a href="#hier">…</a></h{l}></div>. A lone heading has hier == flat.
|
||||
assert!(out.starts_with(&format!(
|
||||
"<div id=\"Title\"><h{level} id=\"Title\" class=\"header\"><a href=\"#Title\">Title</a></h{level}></div>"
|
||||
)), "got: {out}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,11 +98,26 @@ fn blockquote_lines_become_paragraphs() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preformatted_block_emits_pre_code_with_language_class() {
|
||||
fn preformatted_block_emits_pre_with_raw_fence_attrs() {
|
||||
// vimwiki drops the verbatim fence text into the <pre> tag (`{{{rust` →
|
||||
// `<pre rust>`), with no inner <code> wrapper.
|
||||
let out = render("{{{rust\nfn main() {}\n}}}\n");
|
||||
assert!(out.contains("<pre><code class=\"language-rust\">"));
|
||||
assert!(out.contains("<pre rust>"), "got: {out}");
|
||||
assert!(out.contains("fn main() {}"));
|
||||
assert!(out.contains("</code></pre>"));
|
||||
assert!(out.contains("</pre>"));
|
||||
assert!(!out.contains("<code"), "no inner code element: {out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preformatted_block_without_attrs_is_bare_pre() {
|
||||
let out = render("{{{\nplain\n}}}\n");
|
||||
assert!(out.contains("<pre>plain"), "got: {out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preformatted_block_keeps_full_attr_string() {
|
||||
let out = render("{{{class=\"brush: python\"\nx\n}}}\n");
|
||||
assert!(out.contains("<pre class=\"brush: python\">"), "got: {out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -244,7 +263,10 @@ fn heading_id_matches_anchor_link_href() {
|
||||
// for "exported HTML anchor links don't work".
|
||||
let out = render("= My Section =\n\n[[#My Section]]\n");
|
||||
assert!(
|
||||
out.contains("<h1 id=\"My Section\">My Section</h1>"),
|
||||
out.contains(
|
||||
"<div id=\"My Section\"><h1 id=\"My Section\" class=\"header\">\
|
||||
<a href=\"#My Section\">My Section</a></h1></div>"
|
||||
),
|
||||
"heading id: {out}"
|
||||
);
|
||||
assert!(
|
||||
@@ -276,7 +298,13 @@ fn non_toc_heading_not_wrapped() {
|
||||
.with_toc_header("Contents")
|
||||
.render_to_string(&doc)
|
||||
.unwrap();
|
||||
assert!(out.contains("<h2 id=\"Intro\">Intro</h2>"), "got: {out}");
|
||||
assert!(
|
||||
out.contains(
|
||||
"<div id=\"Intro\"><h2 id=\"Intro\" class=\"header\">\
|
||||
<a href=\"#Intro\">Intro</a></h2></div>"
|
||||
),
|
||||
"got: {out}"
|
||||
);
|
||||
assert!(!out.contains("class=\"toc\""), "got: {out}");
|
||||
}
|
||||
|
||||
@@ -342,7 +370,7 @@ fn template_substitutes_content_and_title() {
|
||||
);
|
||||
let out = renderer.render_to_string(&doc).unwrap();
|
||||
assert!(out.contains("<title>My Page</title>"));
|
||||
assert!(out.contains("<body><h1 id=\"Hello\">Hello</h1>"));
|
||||
assert!(out.contains("<body><div id=\"Hello\"><h1 id=\"Hello\" class=\"header\"><a href=\"#Hello\">Hello</a></h1></div>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -351,7 +379,7 @@ fn template_with_missing_title_substitutes_empty_string() {
|
||||
let renderer = HtmlRenderer::new().with_template("<title>{{title}}</title>{{content}}");
|
||||
let out = renderer.render_to_string(&doc).unwrap();
|
||||
assert!(out.contains("<title></title>"));
|
||||
assert!(out.contains("<h1 id=\"Heading\">Heading</h1>"));
|
||||
assert!(out.contains("<div id=\"Heading\"><h1 id=\"Heading\" class=\"header\"><a href=\"#Heading\">Heading</a></h1></div>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -367,7 +395,7 @@ fn template_substitutes_vimwiki_percent_placeholders() {
|
||||
let out = renderer.render_to_string(&doc).unwrap();
|
||||
assert!(out.contains("<title>My Page</title>"), "title: {out}");
|
||||
assert!(
|
||||
out.contains("<body><h1 id=\"Hello\">Hello</h1>"),
|
||||
out.contains("<body><div id=\"Hello\"><h1 id=\"Hello\" class=\"header\"><a href=\"#Hello\">Hello</a></h1></div>"),
|
||||
"content: {out}"
|
||||
);
|
||||
assert!(out.contains("href=\"../style.css\""), "root_path: {out}");
|
||||
@@ -461,7 +489,7 @@ fn x() {}
|
||||
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
||||
// A few sanity checks; the full output is exercised piece-by-piece above.
|
||||
for needle in [
|
||||
"<h1 id=\"Heading\">Heading</h1>",
|
||||
"<div id=\"Heading\"><h1 id=\"Heading\" class=\"header\"><a href=\"#Heading\">Heading</a></h1></div>",
|
||||
"<strong>bold</strong>",
|
||||
"<em>italic</em>",
|
||||
"<code>code</code>",
|
||||
@@ -469,7 +497,7 @@ fn x() {}
|
||||
"<blockquote>",
|
||||
"<hr>",
|
||||
"<table>",
|
||||
"<pre><code class=\"language-rust\">",
|
||||
"<pre rust>",
|
||||
] {
|
||||
assert!(
|
||||
out.contains(needle),
|
||||
|
||||
Reference in New Issue
Block a user