feat(config): P3 config batch — group 7 (user_htmls pruning)

allToHtml now prunes orphan HTML files (no corresponding wiki source) under
html_path — export_ops::prune_orphan_html walks the output tree, maps each
<rel>.html back to <root>/<rel>.<ext>, and deletes those with no source,
skipping the CSS file and any basename in user_htmls. Gated on
!html_filename_parameterization (slugified names can't be reverse-mapped),
mirroring upstream; the pruned paths are reported in the command result.

hl_headers / hl_cb_checked are NOT implemented as toggles: nuwiki already
highlights headers per-level (@vimwikiHeading.level1..6) and checkboxes
always-on via LSP semantic tokens, which supersedes the upstream opt-in
toggles (default off) — same class as the maxhi divergence. Documented in the
gap doc rather than degrading the existing highlighting.

Test: prune_orphan_html_deletes_sourceless_keeps_protected. Full rust suite +
clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 12:37:38 +00:00
parent 5cc47b0a42
commit a63d3dd7cf
2 changed files with 83 additions and 0 deletions
+57
View File
@@ -1282,9 +1282,21 @@ fn export_all(backend: &Backend, args: Vec<Value>, force: bool) -> Result<Option
}
}
let css = crate::commands::export_ops::ensure_css(&cfg).map_err(|e| e.to_string())?;
// Prune orphan HTML (no wiki source), honouring user_htmls. Skipped when
// filename parameterization is on (slugified names can't reverse-map),
// mirroring upstream.
let pruned: Vec<serde_json::Value> = if cfg.html.html_filename_parameterization {
Vec::new()
} else {
crate::commands::export_ops::prune_orphan_html(&cfg)
.into_iter()
.map(|p| serde_json::json!(p))
.collect()
};
Ok(Some(serde_json::json!({
"exported": exported,
"skipped": skipped,
"pruned": pruned,
"files_seen": files.len(),
"css_path": css.path,
"css_created": css.created,
@@ -3788,6 +3800,51 @@ pub mod export_ops {
)
}
/// Delete HTML files under `html_path` that have no corresponding wiki
/// source (vimwiki's `allToHtml` cleanup), skipping the CSS file and any
/// basename listed in `user_htmls`. Returns the pruned paths. Gated by the
/// caller on `!html_filename_parameterization` (slugified names can't be
/// reverse-mapped to a source).
pub fn prune_orphan_html(cfg: &WikiConfig) -> Vec<PathBuf> {
let html_root = &cfg.html.html_path;
let css = css_path(&cfg.html);
let ext = cfg.file_extension.trim_start_matches('.');
let mut pruned = Vec::new();
let mut stack = vec![html_root.clone()];
while let Some(dir) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&dir) else {
continue;
};
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("html") {
continue;
}
if path == css {
continue;
}
let base = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
if cfg.html.user_htmls.iter().any(|u| u == base) {
continue;
}
// Map <html_path>/<rel>.html back to <root>/<rel>.<ext>.
let Ok(rel) = path.strip_prefix(html_root) else {
continue;
};
let mut src = cfg.root.join(rel);
src.set_extension(ext);
if !src.exists() && std::fs::remove_file(&path).is_ok() {
pruned.push(path);
}
}
}
pruned
}
fn rss_path(cfg: &WikiConfig) -> PathBuf {
let name = if cfg.html.rss_name.trim().is_empty() {
"rss.xml"