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:
@@ -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"
|
||||
|
||||
@@ -631,6 +631,32 @@ fn write_rss_uses_base_url_for_public_links() {
|
||||
assert!(!xml.contains("file:///tmp/diary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_orphan_html_deletes_sourceless_keeps_protected() {
|
||||
let root = temp_root("prune");
|
||||
let mut c = WikiConfig::from_root(root.clone());
|
||||
c.html = HtmlConfig::from_root(&root);
|
||||
c.html.user_htmls = vec!["keep.html".into()];
|
||||
let html = &c.html.html_path;
|
||||
std::fs::create_dir_all(html).unwrap();
|
||||
// A sourced page (Home.wiki exists) → kept.
|
||||
std::fs::write(root.join("Home.wiki"), "= Home =\n").unwrap();
|
||||
std::fs::write(html.join("Home.html"), "<h1>Home</h1>").unwrap();
|
||||
// An orphan (no source) → pruned.
|
||||
std::fs::write(html.join("Ghost.html"), "<h1>Ghost</h1>").unwrap();
|
||||
// A user_htmls-protected orphan → kept.
|
||||
std::fs::write(html.join("keep.html"), "<p>manual</p>").unwrap();
|
||||
// The CSS file → never pruned.
|
||||
std::fs::write(html.join(&c.html.css_name), "body{}").unwrap();
|
||||
|
||||
let pruned = nuwiki_lsp::commands::export_ops::prune_orphan_html(&c);
|
||||
assert_eq!(pruned.len(), 1, "only the orphan: {pruned:?}");
|
||||
assert!(!html.join("Ghost.html").exists(), "orphan deleted");
|
||||
assert!(html.join("Home.html").exists(), "sourced kept");
|
||||
assert!(html.join("keep.html").exists(), "user_htmls kept");
|
||||
assert!(html.join(&c.html.css_name).exists(), "css kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rss_honours_rss_name_and_max_items() {
|
||||
use nuwiki_lsp::diary::DiaryEntry;
|
||||
|
||||
Reference in New Issue
Block a user