fix(export): render vimwiki-compatible HTML for templates, links, and tasks

Stock vimwiki templates and stylesheets rendered broken pages because the
HTML output diverged from vimwiki conventions on three fronts:

- Templates: the renderer only substituted nuwiki's `{{key}}` placeholders,
  so vimwiki's `%title%`/`%content%`/`%root_path%`/`%date%`/`%wiki_css%`
  passed through literally — dropping the body and breaking asset links.
  Both delimiter styles are now recognised; `%wiki_css%` aliases the css var.
- Links: `[[todo.wiki]]` exported to `todo.wiki.html`. render_page_html now
  takes the wiki file extension and strips it from wiki/interwiki targets
  (via index::strip_wiki_extension) before the `.html` URL is built, matching
  in-editor navigation. file:/local: links keep their literal extension.
- Tasks: checkbox items used bespoke `task-*` classes plus an `<input>`,
  which double-rendered against vimwiki stylesheets. Emit `done0..done4`
  (`[ ] [.] [o] [O] [X]`) and `rejected` (`[-]`) classes with no input — the
  stylesheet draws the box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 21:19:55 -03:00
parent 7f6d811a47
commit e90bbab39e
5 changed files with 142 additions and 20 deletions
+26 -16
View File
@@ -11,6 +11,11 @@
//! `{{date}}`, `{{root_path}}`, `{{toc}}`). Order: `{{content}}` is
//! substituted first so a body that happens to contain a literal
//! `{{title}}` isn't itself rewritten in the next pass.
//!
//! Both delimiter styles are recognised for every placeholder: nuwiki's
//! native `{{key}}` and vimwiki's `%key%` (`%title%`, `%content%`,
//! `%root_path%`, `%date%`, `%wiki_css%`, …). This lets stock vimwiki
//! templates render unchanged.
use std::collections::HashMap;
use std::io::{self, Write};
@@ -107,14 +112,17 @@ impl Renderer for HtmlRenderer {
let body_str = std::str::from_utf8(&body)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let title = doc.metadata.title.as_deref().unwrap_or("");
// Replace `{{content}}` first so its substituted body, which may
// legitimately contain the literal `{{title}}`, isn't itself
// rewritten in the next pass.
// Substitute `content` before `title` so a body that legitimately
// contains a literal title placeholder isn't rewritten. Both the
// native `{{key}}` and vimwiki's `%key%` delimiters are accepted so
// stock vimwiki templates render unchanged.
let mut out = template.replace("{{content}}", body_str);
out = out.replace("%content%", body_str);
out = out.replace("{{title}}", title);
out = out.replace("%title%", title);
for (k, v) in &self.vars {
let needle = format!("{{{{{k}}}}}");
out = out.replace(&needle, v);
out = out.replace(&format!("{{{{{k}}}}}"), v);
out = out.replace(&format!("%{k}%"), v);
}
w.write_all(out.as_bytes())?;
} else {
@@ -239,18 +247,20 @@ impl HtmlRenderer {
fn render_list_item(&self, n: &ListItemNode, w: &mut dyn Write) -> io::Result<()> {
match n.checkbox {
Some(state) => {
let (checked, class) = match state {
CheckboxState::Empty => ("", "task-empty"),
CheckboxState::Quarter => ("", "task-quarter"),
CheckboxState::Half => ("", "task-half"),
CheckboxState::ThreeQuarters => ("", "task-three-quarters"),
CheckboxState::Done => (" checked", "task-done"),
CheckboxState::Rejected => ("", "task-rejected"),
// vimwiki-compatible checkbox classes: `[ ]`→done0, `[.]`→done1,
// `[o]`→done2, `[O]`→done3, `[X]`→done4, `[-]`→rejected. The
// checkbox glyph/progress fill is drawn entirely by the
// stylesheet (`li.doneN::before`); vimwiki emits no `<input>`,
// so neither do we — an input would double-render against it.
let class = match state {
CheckboxState::Empty => "done0",
CheckboxState::Quarter => "done1",
CheckboxState::Half => "done2",
CheckboxState::ThreeQuarters => "done3",
CheckboxState::Done => "done4",
CheckboxState::Rejected => "rejected",
};
write!(
w,
"<li class=\"{class}\"><input type=\"checkbox\" disabled{checked}> "
)?;
write!(w, "<li class=\"{class}\">")?;
}
None => w.write_all(b"<li>")?,
}