feat(config): custom_wiki2html external converter + base_url for RSS (P2)
CI / cargo fmt --check (push) Failing after 16s
CI / cargo clippy (push) Successful in 32s
CI / cargo test (push) Successful in 48s
CI / editor keymaps (push) Successful in 1m39s

Add three per-wiki HtmlConfig keys mirroring vimwiki globals:

- custom_wiki2html / custom_wiki2html_args: when set, export shells out
  to the external converter via `sh -c` with vimwiki's exact arg order
  (<cmd> <force> <syntax> <ext> <out_dir> <in_file> <css> <tpl_path>
  <tpl_default> <tpl_ext> <root_path> <args>, `-` for empty optionals)
  instead of the built-in renderer.
- base_url: when set, diary RSS item + channel links become
  <base_url><diary_rel_path>/<date>.html instead of file:// URIs.

write_page() gains a `force` flag threaded through export_current (false),
export_all (its own flag) and the did_save auto-export path (false).

Tests: write_page_invokes_custom_wiki2html_converter,
write_rss_uses_base_url_for_public_links (html_export.rs), config
round-trip + defaults (index_and_config.rs). Docs: README, doc/nuwiki.txt,
lua config comment, vimwiki-gap.md item ticked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 19:06:42 +00:00
parent 09b5a2bd46
commit 23aec3e6c7
9 changed files with 236 additions and 11 deletions
+57 -3
View File
@@ -428,7 +428,7 @@ fn write_page_writes_html_to_disk() {
let mut c = WikiConfig::from_root(root.clone());
c.html = HtmlConfig::from_root(&root);
let ast = parse("= Hello =\nworld\n");
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "Hello", &c).unwrap();
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "Hello", &c, false).unwrap();
assert_eq!(outcome.page, "Hello");
assert!(outcome.output_path.exists());
let body = std::fs::read_to_string(&outcome.output_path).unwrap();
@@ -442,7 +442,7 @@ fn write_page_creates_subdirs() {
c.html = HtmlConfig::from_root(&root);
let ast = parse("= Daily =\n");
let outcome =
nuwiki_lsp::commands::export_ops::write_page(&ast, "diary/2026-05-11", &c).unwrap();
nuwiki_lsp::commands::export_ops::write_page(&ast, "diary/2026-05-11", &c, false).unwrap();
assert!(outcome.output_path.exists());
assert!(outcome
.output_path
@@ -463,7 +463,7 @@ fn write_page_uses_template_file_when_present() {
)
.unwrap();
let ast = parse("%title T\n= H =\nbody\n");
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "P", &c).unwrap();
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "P", &c, false).unwrap();
let body = std::fs::read_to_string(outcome.output_path).unwrap();
assert!(body.starts_with("<x>T|"));
assert!(body.ends_with("</x>"));
@@ -518,6 +518,60 @@ fn write_rss_emits_valid_xml_with_entries() {
let i11 = xml.find("2026-05-11").unwrap();
let i10 = xml.find("2026-05-10").unwrap();
assert!(i11 < i10);
// No base_url → item links keep the file:// URI.
assert!(xml.contains("<link>file:///tmp/diary/2026-05-11.wiki</link>"));
}
#[test]
fn write_rss_uses_base_url_for_public_links() {
use nuwiki_lsp::diary::DiaryEntry;
use tower_lsp::lsp_types::Url;
let root = temp_root("rss_base");
let mut c = WikiConfig::from_root(root.clone());
c.html = HtmlConfig::from_root(&root);
c.html.base_url = "https://example.com/wiki/".into();
c.diary_rel_path = "diary".into();
let entries = vec![DiaryEntry {
date: DiaryDate::from_ymd(2026, 5, 11).unwrap(),
uri: Url::parse("file:///tmp/diary/2026-05-11.wiki").unwrap(),
}];
let path = nuwiki_lsp::commands::export_ops::write_rss(&c, &entries).unwrap();
let xml = std::fs::read_to_string(&path).unwrap();
// Channel + item links use the public base URL, not file://.
assert!(xml.contains("<link>https://example.com/wiki/</link>"));
assert!(xml.contains("<link>https://example.com/wiki/diary/2026-05-11.html</link>"));
assert!(xml.contains("<guid>https://example.com/wiki/diary/2026-05-11.html</guid>"));
assert!(!xml.contains("file:///tmp/diary"));
}
#[test]
fn write_page_invokes_custom_wiki2html_converter() {
let root = temp_root("custom_w2h");
let mut c = WikiConfig::from_root(root.clone());
c.html = HtmlConfig::from_root(&root);
// Source file must exist on disk — the converter receives its path.
std::fs::write(root.join("Hello.wiki"), "= Hello =\nworld\n").unwrap();
// A tiny shell "converter": writes a marker into the output path it is told
// to produce. nuwiki appends args positionally after `_` ($0), so
// $1=force, $2=syntax, $3=ext, $4=output_dir, $5=input_file, …
let script = "mkdir -p \"$4\" && printf 'CUSTOM:%s' \"$5\" > \"$4\"/Hello.html";
c.html.custom_wiki2html = format!("sh -c {} _", shell_words_quote(script));
let outcome =
nuwiki_lsp::commands::export_ops::write_page(&parse("= Hello =\n"), "Hello", &c, true)
.unwrap();
assert_eq!(outcome.page, "Hello");
assert!(outcome.output_path.exists(), "converter must produce output");
let body = std::fs::read_to_string(&outcome.output_path).unwrap();
assert!(
body.starts_with("CUSTOM:"),
"marker from converter, got {body}"
);
}
/// Minimal single-quote shell escaping for the test command above.
fn shell_words_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
// ===== COMMANDS list completeness =====
@@ -284,6 +284,10 @@ fn defaults_carry_vimwiki_per_wiki_keys() {
assert_eq!(cfg.links_header, "Generated Links");
assert_eq!(cfg.tags_header, "Generated Tags");
assert_eq!(cfg.listsym_rejected, "-");
// No external converter / public base URL by default.
assert_eq!(cfg.html.custom_wiki2html, "");
assert_eq!(cfg.html.custom_wiki2html_args, "");
assert_eq!(cfg.html.base_url, "");
}
#[test]
@@ -312,6 +316,9 @@ fn raw_wiki_parses_every_new_key() {
"links_header": "All Pages",
"tags_header": "Tag Index",
"tags_header_level": 3,
"custom_wiki2html": "~/bin/my_wiki2html.sh",
"custom_wiki2html_args": "--flag",
"base_url": "https://example.com/wiki/",
}],
}));
let w = &cfg.wikis[0];
@@ -338,6 +345,11 @@ fn raw_wiki_parses_every_new_key() {
assert_eq!(w.links_header_level, 1); // unspecified → default
assert_eq!(w.tags_header, "Tag Index");
assert_eq!(w.tags_header_level, 3);
// custom_wiki2html keeps the literal command string (tilde-expansion is
// the converter's job, not ours); args + base_url round-trip verbatim.
assert_eq!(w.html.custom_wiki2html, "~/bin/my_wiki2html.sh");
assert_eq!(w.html.custom_wiki2html_args, "--flag");
assert_eq!(w.html.base_url, "https://example.com/wiki/");
// The parsed keys drive a date-mode, Sunday-start diary calendar:
// a weekly note is the week-start date (YYYY-MM-DD), stepping ±7 days.