feat(config): custom_wiki2html external converter + base_url for RSS (P2)
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:
@@ -1143,7 +1143,7 @@ fn export_current(
|
||||
})));
|
||||
}
|
||||
let name = crate::index::page_name_from_uri(&p.uri, Some(&cfg.root));
|
||||
let outcome = match crate::commands::export_ops::write_page(&doc.ast, &name, cfg) {
|
||||
let outcome = match crate::commands::export_ops::write_page(&doc.ast, &name, cfg, false) {
|
||||
Ok(o) => o,
|
||||
Err(e) => return Err(format!("nuwiki.export.currentToHtml: {e}")),
|
||||
};
|
||||
@@ -1222,7 +1222,7 @@ fn export_all(backend: &Backend, args: Vec<Value>, force: bool) -> Result<Option
|
||||
}
|
||||
}
|
||||
}
|
||||
match crate::commands::export_ops::write_page(&ast, &name, &cfg) {
|
||||
match crate::commands::export_ops::write_page(&ast, &name, &cfg, force) {
|
||||
Ok(outcome) => exported.push(outcome.to_json()),
|
||||
Err(e) => skipped.push(serde_json::json!({"uri": uri, "reason": e.to_string()})),
|
||||
}
|
||||
@@ -3342,6 +3342,7 @@ pub mod export_ops {
|
||||
doc: &DocumentNode,
|
||||
name: &str,
|
||||
cfg: &WikiConfig,
|
||||
force: bool,
|
||||
) -> std::io::Result<PageExport> {
|
||||
if is_excluded(&cfg.html.exclude_files, name) {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -3349,6 +3350,11 @@ pub mod export_ops {
|
||||
format!("excluded by exclude_files: {name}"),
|
||||
));
|
||||
}
|
||||
// When a `custom_wiki2html` converter is configured, hand the page off
|
||||
// to it instead of the built-in renderer (vimwiki parity).
|
||||
if !cfg.html.custom_wiki2html.trim().is_empty() {
|
||||
return run_custom_wiki2html(name, cfg, force);
|
||||
}
|
||||
let template_source = template_for(&cfg.html, doc);
|
||||
let template = load_template(&template_source)
|
||||
.unwrap_or_else(|| fallback_template(&cfg.html.css_name));
|
||||
@@ -3378,6 +3384,86 @@ pub mod export_ops {
|
||||
})
|
||||
}
|
||||
|
||||
/// `sh`-quote a path/string for the `custom_wiki2html` command line.
|
||||
fn sh_quote(s: &str) -> String {
|
||||
format!("'{}'", s.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
/// Run an external `custom_wiki2html` converter for `name`, mirroring
|
||||
/// vimwiki's argument order:
|
||||
/// script force syntax ext output_dir input_file css_file
|
||||
/// template_path template_default template_ext root_path args
|
||||
/// Empty optional args are passed as `-` (vimwiki convention). The script
|
||||
/// is responsible for writing the output file; we return the path it is
|
||||
/// expected to produce (`output_path_for`).
|
||||
fn run_custom_wiki2html(
|
||||
name: &str,
|
||||
cfg: &WikiConfig,
|
||||
force: bool,
|
||||
) -> std::io::Result<PageExport> {
|
||||
let h = &cfg.html;
|
||||
let input = cfg.root.join(format!("{name}{}", cfg.file_extension));
|
||||
std::fs::create_dir_all(&h.html_path)?;
|
||||
let css = crate::export::css_path(h);
|
||||
let ext = cfg.file_extension.trim_start_matches('.');
|
||||
let dash = |s: &str| {
|
||||
if s.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
};
|
||||
let tpl_path = if h.template_path.as_os_str().is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
sh_quote(&h.template_path.display().to_string())
|
||||
};
|
||||
let root_path = {
|
||||
let rp = crate::export::compute_root_path(name);
|
||||
if rp.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
sh_quote(&rp)
|
||||
}
|
||||
};
|
||||
let cmd = format!(
|
||||
"{} {} {} {} {} {} {} {} {} {} {} {}",
|
||||
h.custom_wiki2html,
|
||||
if force { 1 } else { 0 },
|
||||
cfg.syntax,
|
||||
ext,
|
||||
sh_quote(&h.html_path.display().to_string()),
|
||||
sh_quote(&input.display().to_string()),
|
||||
sh_quote(&css.display().to_string()),
|
||||
tpl_path,
|
||||
dash(&h.template_default),
|
||||
dash(&h.template_ext),
|
||||
root_path,
|
||||
dash(h.custom_wiki2html_args.trim()),
|
||||
);
|
||||
let status = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.status()
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("custom_wiki2html spawn failed: {e}"),
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("custom_wiki2html exited with {status}"),
|
||||
));
|
||||
}
|
||||
Ok(PageExport {
|
||||
page: name.to_string(),
|
||||
output_path: output_path_for(&cfg.html, name),
|
||||
source_uri: tower_lsp::lsp_types::Url::from_file_path(&input).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Two-tier template load. Returns the resolved template body, or
|
||||
/// `None` if neither file exists — caller falls back to the built-in
|
||||
/// template.
|
||||
@@ -3423,12 +3509,29 @@ pub mod export_ops {
|
||||
"#,
|
||||
);
|
||||
xml.push_str(&format!("<title>{}</title>\n", xml_escape(&cfg.name)));
|
||||
// `base_url` (per-wiki) prefixes the feed + item links so a hosted
|
||||
// feed points at public URLs rather than local `file://` paths.
|
||||
let base_url = cfg.html.base_url.trim();
|
||||
if !base_url.is_empty() {
|
||||
xml.push_str(&format!("<link>{}</link>\n", xml_escape(base_url)));
|
||||
}
|
||||
xml.push_str("<description>nuwiki diary</description>\n");
|
||||
for e in sorted {
|
||||
// With base_url set, build a public URL from the diary path + date;
|
||||
// otherwise fall back to the entry's `file://` URI.
|
||||
let link = if base_url.is_empty() {
|
||||
e.uri.as_str().to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{base_url}{}/{}.html",
|
||||
cfg.diary_rel_path.trim_matches('/'),
|
||||
e.date.format()
|
||||
)
|
||||
};
|
||||
xml.push_str("<item>");
|
||||
xml.push_str(&format!("<title>{}</title>", e.date.format()));
|
||||
xml.push_str(&format!("<link>{}</link>", e.uri.as_str()));
|
||||
xml.push_str(&format!("<guid>{}</guid>", e.uri.as_str()));
|
||||
xml.push_str(&format!("<link>{}</link>", xml_escape(&link)));
|
||||
xml.push_str(&format!("<guid>{}</guid>", xml_escape(&link)));
|
||||
xml.push_str("</item>\n");
|
||||
}
|
||||
xml.push_str("</channel></rss>\n");
|
||||
|
||||
@@ -130,6 +130,13 @@ pub struct HtmlConfig {
|
||||
/// renderer falls back to `class="color-<name>"` when a name
|
||||
/// isn't in the dict.
|
||||
pub color_dic: std::collections::HashMap<String, String>,
|
||||
/// External wiki→HTML converter (vimwiki's `custom_wiki2html`). When
|
||||
/// non-empty, export shells out to it instead of the built-in renderer.
|
||||
pub custom_wiki2html: String,
|
||||
/// Extra args appended to the `custom_wiki2html` invocation.
|
||||
pub custom_wiki2html_args: String,
|
||||
/// URL prefix prepended to RSS feed / diary item links (`base_url`).
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
@@ -145,6 +152,9 @@ impl Default for HtmlConfig {
|
||||
html_filename_parameterization: false,
|
||||
exclude_files: Vec::new(),
|
||||
color_dic: std::collections::HashMap::new(),
|
||||
custom_wiki2html: String::new(),
|
||||
custom_wiki2html_args: String::new(),
|
||||
base_url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -634,6 +644,12 @@ struct RawWiki {
|
||||
exclude_files: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
color_dic: Option<std::collections::HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
custom_wiki2html: Option<String>,
|
||||
#[serde(default)]
|
||||
custom_wiki2html_args: Option<String>,
|
||||
#[serde(default)]
|
||||
base_url: Option<String>,
|
||||
// per-wiki keys mirroring vimwiki globals. All optional so
|
||||
// existing single-wiki configs migrate without touching anything.
|
||||
#[serde(default)]
|
||||
@@ -720,6 +736,15 @@ impl From<RawWiki> for WikiConfig {
|
||||
if let Some(v) = r.color_dic {
|
||||
html.color_dic = v;
|
||||
}
|
||||
if let Some(s) = r.custom_wiki2html {
|
||||
html.custom_wiki2html = s;
|
||||
}
|
||||
if let Some(s) = r.custom_wiki2html_args {
|
||||
html.custom_wiki2html_args = s;
|
||||
}
|
||||
if let Some(s) = r.base_url {
|
||||
html.base_url = s;
|
||||
}
|
||||
let d = wiki_defaults();
|
||||
WikiConfig {
|
||||
name: r.name.unwrap_or(derived_name),
|
||||
|
||||
@@ -728,7 +728,7 @@ impl LanguageServer for Backend {
|
||||
return;
|
||||
}
|
||||
let name = index::page_name_from_uri(&uri, Some(&wiki.config.root));
|
||||
let outcome = commands::export_ops::write_page(&doc.ast, &name, &wiki.config);
|
||||
let outcome = commands::export_ops::write_page(&doc.ast, &name, &wiki.config, false);
|
||||
drop(doc);
|
||||
match outcome {
|
||||
Ok(out) => {
|
||||
|
||||
Reference in New Issue
Block a user