fix(parity): close the 2026-06-02 re-audit config/command findings
Implements the remaining fourth-pass findings (gap doc updated):
- list_margin: rework to upstream's buffer-side meaning. Drop the
nuwiki-only HTML em-margin (renderer field/method + render_page_html
param removed) and prepend max(0, list_margin) leading spaces to every
generated bullet in build_toc_text / build_links_text /
build_tag_links_text / diary::build_index_body; headings stay at col 0.
From<RawWiki> derives 0 for markdown wikis when unset. Negatives can't
resolve 'shiftwidth' server-side, so they collapse to zero indent
(documented divergence).
- diary_months: per-wiki Vec<String> (default 12 English names), threaded
into the diary-index month labels; missing/empty slots fall back to the
English name.
- diary_caption_level: widen u8 -> i8 so vimwiki's -1 (min: -1) parses;
build_index_body clamps < 0 to base tree level 0.
- VimwikiRemoveDone: regain upstream's -range. All four defs are now
-bang -range, dispatched via remove_done(bang, range, l1, l2) in both
clients: ! -> whole buffer, explicit range -> new list_remove_done_range
({range:[l1-1,l2-1]}), else current list. Server remove_done_edit gained
an Option<(u32,u32)> range that filters whole-doc victims by start line.
markdown_header_style is deferred: the generators emit vimwiki syntax only
(caption_line never writes markdown headers), so there's no markdown header
to attach the style to. Logged as the "generated-content is vimwiki-only"
intentional divergence pending a later markdown generated-content effort.
Tests: list_margin indent, markdown list_margin-0 default, diary_months
custom + fallback, negative caption_level clamp/parse, ranged remove-done
(server + both keymap harnesses). 553 lsp/core tests pass; clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -262,7 +262,15 @@ fn nuwiki_diary_index_resolves_index_uri() {
|
||||
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
||||
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 1);
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
1,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(body.starts_with("= Diary ="));
|
||||
assert!(body.contains("2026-05-12"));
|
||||
assert!(body.contains("2026-05-10"));
|
||||
@@ -298,7 +306,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
let src = "= Top =\n== Sub ==\ntext\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &doc, &uri, true, "Contents", 1).expect("toc edit produced");
|
||||
let edit = ops::toc_edit(src, &doc, &uri, true, "Contents", 1, 0).expect("toc edit produced");
|
||||
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
||||
|
||||
let toc = ops::build_toc_text(
|
||||
@@ -308,6 +316,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
],
|
||||
"Contents",
|
||||
1,
|
||||
0,
|
||||
);
|
||||
assert!(toc.contains("= Contents ="));
|
||||
assert!(toc.contains("- [[#top|Top]]"));
|
||||
@@ -318,7 +327,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
#[test]
|
||||
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
||||
let text = ops::build_links_text(&pages, "Links", 1, Some("Home"));
|
||||
let text = ops::build_links_text(&pages, "Links", 1, Some("Home"), 0);
|
||||
assert!(text.contains("= Links ="));
|
||||
assert!(text.contains("- [[About]]"));
|
||||
assert!(text.contains("- [[Notes]]"));
|
||||
@@ -327,7 +336,18 @@ fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||
let src = "= Existing =\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
assert!(ops::links_edit(src, &doc, &uri, "Home", &pages, true, "Generated Links", 1).is_some());
|
||||
assert!(ops::links_edit(
|
||||
src,
|
||||
&doc,
|
||||
&uri,
|
||||
"Home",
|
||||
&pages,
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
// :NuwikiCheckLinks — surface broken links across the workspace.
|
||||
@@ -384,13 +404,13 @@ fn nuwiki_generate_tag_links_builds_section() {
|
||||
);
|
||||
let by_tag = ops::tag_pages_snapshot(&idx);
|
||||
|
||||
let single = ops::build_tag_links_text(&by_tag, Some("alpha"), "Generated Tags", 1).unwrap();
|
||||
let single = ops::build_tag_links_text(&by_tag, Some("alpha"), "Generated Tags", 1, 0).unwrap();
|
||||
assert!(single.starts_with("= Tag: alpha ="));
|
||||
assert!(single.contains("- [[Home]]"));
|
||||
assert!(single.contains("- [[Work]]"));
|
||||
|
||||
// No-arg variant emits a section per tag.
|
||||
let all = ops::build_tag_links_text(&by_tag, None, "Generated Tags", 1).unwrap();
|
||||
let all = ops::build_tag_links_text(&by_tag, None, "Generated Tags", 1, 0).unwrap();
|
||||
assert!(all.starts_with("= Generated Tags ="));
|
||||
assert!(all.contains("== alpha =="));
|
||||
assert!(all.contains("== beta =="));
|
||||
@@ -406,7 +426,8 @@ fn nuwiki_generate_tag_links_builds_section() {
|
||||
&by_tag,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
@@ -442,7 +463,6 @@ fn nuwiki_2html_renders_page() {
|
||||
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
||||
&cfg.html,
|
||||
Some(".wiki"),
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -464,7 +484,6 @@ fn nuwiki_2html_browse_shares_render_and_is_advertised() {
|
||||
DiaryDate::today_utc(),
|
||||
&cfg.html,
|
||||
Some(".wiki"),
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -61,7 +61,7 @@ fn render_marker_handles_each_symbol() {
|
||||
fn remove_done_strips_done_and_rejected_items() {
|
||||
let src = "- [ ] todo\n- [X] done\n- [-] rejected\n- [ ] also todo\n";
|
||||
let ast = parse(src);
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, true).expect("edit");
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, None, true).expect("edit");
|
||||
let changes = edit.changes.expect("changes");
|
||||
let edits = &changes[&uri()];
|
||||
assert_eq!(edits.len(), 2, "expected 2 deletions");
|
||||
@@ -75,14 +75,14 @@ fn remove_done_strips_done_and_rejected_items() {
|
||||
fn remove_done_leaves_open_items_alone() {
|
||||
let src = "- [ ] todo\n- [.] partial\n- [o] more\n";
|
||||
let ast = parse(src);
|
||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, true).is_none());
|
||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, None, true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_done_returns_none_when_no_lists() {
|
||||
let src = "= heading =\nparagraph\n";
|
||||
let ast = parse(src);
|
||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, true).is_none());
|
||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, None, true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,7 +96,7 @@ fn remove_done_scoped_by_position_to_current_list() {
|
||||
line: 0,
|
||||
character: 0,
|
||||
});
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, None, true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 2, "both done items in the current list match");
|
||||
}
|
||||
@@ -112,7 +112,7 @@ fn remove_done_position_leaves_other_lists_untouched() {
|
||||
line: 0,
|
||||
character: 0,
|
||||
});
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, None, true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 1, "only the first list's done item is removed");
|
||||
}
|
||||
@@ -126,11 +126,32 @@ fn remove_done_position_cascades_into_sublists() {
|
||||
line: 0,
|
||||
character: 0,
|
||||
});
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, None, true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 1, "the nested done child is removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_done_ranged_restricts_to_selected_lines() {
|
||||
// The `:'<,'>VimwikiRemoveDone` form: a 0-indexed inclusive line range
|
||||
// sweeps the whole doc but only deletes done/rejected items inside it.
|
||||
// Lines: 0 `[X]`, 1 `[ ]`, 2 `[X]`, 3 `[-]`. Range 0..=1 keeps only the
|
||||
// line-0 done item; the line-2/3 done+rejected items survive.
|
||||
let src = "- [X] a\n- [ ] b\n- [X] c\n- [-] d\n";
|
||||
let ast = parse(src);
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, Some((0, 1)), true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 1, "only the done item on line 0 is in range");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_done_ranged_returns_none_when_range_has_no_done() {
|
||||
// Range covering only open items → nothing to delete.
|
||||
let src = "- [X] a\n- [ ] b\n- [ ] c\n";
|
||||
let ast = parse(src);
|
||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, Some((1, 2)), true).is_none());
|
||||
}
|
||||
|
||||
// ===== renumber =====
|
||||
|
||||
#[test]
|
||||
@@ -280,6 +301,6 @@ fn commands_list_includes_cluster_a() {
|
||||
fn one_edit_text_round_trip() {
|
||||
let src = "- [X] done\n";
|
||||
let ast = parse(src);
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, true).unwrap();
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, None, true).unwrap();
|
||||
let _ = one_edit_text(edit);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ fn tag_pages_snapshot_sorts_pages_alphabetically() {
|
||||
fn build_tag_links_for_single_tag() {
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("release".to_string(), vec!["Alpha".into(), "Beta".into()]);
|
||||
let out = ops::build_tag_links_text(&snap, Some("release"), "Generated Tags", 1).unwrap();
|
||||
let out = ops::build_tag_links_text(&snap, Some("release"), "Generated Tags", 1, 0).unwrap();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Tag: release =");
|
||||
assert_eq!(lines[1], "- [[Alpha]]");
|
||||
@@ -213,7 +213,7 @@ fn build_tag_links_for_single_tag() {
|
||||
#[test]
|
||||
fn build_tag_links_for_missing_tag_returns_none() {
|
||||
let snap = BTreeMap::new();
|
||||
assert!(ops::build_tag_links_text(&snap, Some("ghost"), "Generated Tags", 1).is_none());
|
||||
assert!(ops::build_tag_links_text(&snap, Some("ghost"), "Generated Tags", 1, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -221,7 +221,7 @@ fn build_tag_links_full_index_groups_by_tag() {
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("a".to_string(), vec!["P1".into()]);
|
||||
snap.insert("b".to_string(), vec!["P2".into(), "P3".into()]);
|
||||
let out = ops::build_tag_links_text(&snap, None, "Generated Tags", 1).unwrap();
|
||||
let out = ops::build_tag_links_text(&snap, None, "Generated Tags", 1, 0).unwrap();
|
||||
assert!(out.starts_with("= Generated Tags =\n"));
|
||||
assert!(out.contains("== a =="));
|
||||
assert!(out.contains("== b =="));
|
||||
@@ -233,7 +233,7 @@ fn build_tag_links_full_index_groups_by_tag() {
|
||||
#[test]
|
||||
fn build_tag_links_full_index_returns_none_when_no_tags() {
|
||||
let snap = BTreeMap::new();
|
||||
assert!(ops::build_tag_links_text(&snap, None, "Generated Tags", 1).is_none());
|
||||
assert!(ops::build_tag_links_text(&snap, None, "Generated Tags", 1, 0).is_none());
|
||||
}
|
||||
|
||||
// ===== `nuwiki.tags.generateLinks` — tag_links_edit (in-buffer rewrite) =====
|
||||
@@ -254,6 +254,7 @@ fn tag_links_edit_inserts_when_section_missing() {
|
||||
true,
|
||||
"Generated Tags",
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.expect("got an edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
@@ -279,6 +280,7 @@ fn tag_links_edit_replaces_existing_section() {
|
||||
true,
|
||||
"Generated Tags",
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
@@ -294,8 +296,8 @@ fn tag_links_edit_full_index_replaces_existing_tags_section() {
|
||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("alpha".to_string(), vec!["P".into()]);
|
||||
let edit =
|
||||
ops::tag_links_edit(src, &ast, &uri, None, &snap, true, "Generated Tags", 1).expect("edit");
|
||||
let edit = ops::tag_links_edit(src, &ast, &uri, None, &snap, true, "Generated Tags", 1, 0)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("== alpha =="));
|
||||
assert!(!te.new_text.contains("[[old]]"));
|
||||
@@ -315,7 +317,8 @@ fn tag_links_rebuild_edit_only_acts_when_index_present() {
|
||||
&snap,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_none(),
|
||||
"auto_generate_tags must not insert an index into a page that lacks one"
|
||||
@@ -328,7 +331,8 @@ fn tag_links_rebuild_edit_only_acts_when_index_present() {
|
||||
&snap,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
@@ -347,7 +351,8 @@ fn tag_links_edit_returns_none_for_unknown_tag() {
|
||||
&snap,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
@@ -274,7 +274,15 @@ fn build_index_body_is_newest_first_grouped_by_year_month() {
|
||||
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
||||
);
|
||||
let entries = diary::list_entries(&idx);
|
||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 1);
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
1,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
let lines: Vec<&str> = body.lines().collect();
|
||||
assert_eq!(lines[0], "= Diary =");
|
||||
// First date in output should be the latest: 2026-05-12
|
||||
@@ -292,7 +300,7 @@ fn build_index_body_is_newest_first_grouped_by_year_month() {
|
||||
|
||||
#[test]
|
||||
fn build_index_body_empty_when_no_entries() {
|
||||
let body = diary::build_index_body(&[], "diary", "Diary", diary::DiarySort::Desc, 1);
|
||||
let body = diary::build_index_body(&[], "diary", "Diary", diary::DiarySort::Desc, 1, &[], 0);
|
||||
assert_eq!(body, "= Diary =\n");
|
||||
}
|
||||
|
||||
@@ -300,7 +308,8 @@ fn build_index_body_empty_when_no_entries() {
|
||||
fn build_index_body_ascending_sort_lists_oldest_first() {
|
||||
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Asc, 1);
|
||||
let body =
|
||||
diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Asc, 1, &[], 0);
|
||||
let older = body.find("2026-05-11").unwrap();
|
||||
let newer = body.find("2026-05-12").unwrap();
|
||||
assert!(older < newer, "ascending sort lists oldest first");
|
||||
@@ -310,7 +319,15 @@ fn build_index_body_ascending_sort_lists_oldest_first() {
|
||||
fn build_index_body_caption_level_shifts_all_headings() {
|
||||
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 2);
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
2,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(body.contains("== Diary =="), "caption at level 2");
|
||||
assert!(
|
||||
body.contains("=== 2026 ==="),
|
||||
@@ -327,11 +344,102 @@ fn build_index_body_clamps_caption_level_to_six() {
|
||||
let idx = build_index_with_entries("/tmp/diaryClamp", &["2026-05-11"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
// caption_level 6 → year/month would be 7/8 but clamp to 6.
|
||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 6);
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
6,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(body.contains("====== Diary ======"));
|
||||
assert!(!body.contains("======="), "no heading exceeds level 6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_index_body_uses_custom_diary_months() {
|
||||
let idx = build_index_with_entries("/tmp/diaryMonths", &["2026-05-11", "2026-12-01"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
// A localized month table (Portuguese) replaces the English labels.
|
||||
let months: Vec<String> = [
|
||||
"Janeiro",
|
||||
"Fevereiro",
|
||||
"Março",
|
||||
"Abril",
|
||||
"Maio",
|
||||
"Junho",
|
||||
"Julho",
|
||||
"Agosto",
|
||||
"Setembro",
|
||||
"Outubro",
|
||||
"Novembro",
|
||||
"Dezembro",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
1,
|
||||
&months,
|
||||
0,
|
||||
);
|
||||
assert!(body.contains("=== Maio ==="), "May → Maio: {body}");
|
||||
assert!(
|
||||
body.contains("=== Dezembro ==="),
|
||||
"December → Dezembro: {body}"
|
||||
);
|
||||
assert!(!body.contains("May"), "English label leaked: {body}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_index_body_falls_back_per_missing_month_slot() {
|
||||
let idx = build_index_with_entries("/tmp/diaryShort", &["2026-05-11"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
// A too-short table only covers Jan–Mar; May falls back to English.
|
||||
let months: Vec<String> = ["Jan", "Feb", "Mar"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
1,
|
||||
&months,
|
||||
0,
|
||||
);
|
||||
assert!(
|
||||
body.contains("=== May ==="),
|
||||
"missing slot → English: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_index_body_clamps_negative_caption_level_to_zero() {
|
||||
let idx = build_index_with_entries("/tmp/diaryNeg", &["2026-05-11"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
// vimwiki allows diary_caption_level = -1; nuwiki clamps it to 0 (same as
|
||||
// a caption_level of 0: caption h1, year h1, month h2).
|
||||
let body = diary::build_index_body(
|
||||
&entries,
|
||||
"diary",
|
||||
"Diary",
|
||||
diary::DiarySort::Desc,
|
||||
-1,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(body.contains("= Diary ="), "caption at level 1: {body}");
|
||||
assert!(body.contains("= 2026 ="), "year at level 1: {body}");
|
||||
assert!(body.contains("== May =="), "month at level 2: {body}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_index_body_round_trip() {
|
||||
let cfg = wiki_cfg("/tmp/diaryA");
|
||||
|
||||
@@ -245,7 +245,6 @@ fn render_page_html_substitutes_title_content_and_extras() {
|
||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
extras,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -265,7 +264,6 @@ fn render_page_html_uses_metadata_date_when_set() {
|
||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -285,7 +283,6 @@ fn render_page_html_numbers_headers_when_enabled() {
|
||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -308,7 +305,6 @@ fn render_page_html_numbering_skips_headers_above_start_level() {
|
||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -331,7 +327,6 @@ fn render_page_html_no_header_numbering_by_default() {
|
||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -350,7 +345,6 @@ fn render_page_html_root_path_for_nested_pages() {
|
||||
DiaryDate::today_utc(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -370,7 +364,6 @@ fn render_page_html_extra_var_overrides_default() {
|
||||
DiaryDate::today_utc(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
extras,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -393,7 +386,6 @@ fn render_page_html_substitutes_vimwiki_percent_template() {
|
||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -420,7 +412,6 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
||||
DiaryDate::today_utc(),
|
||||
&c.html,
|
||||
Some(".wiki"),
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -430,49 +421,6 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
||||
assert!(html.contains("href=\"posts/index.html\""), "plain: {html}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_page_html_list_margin_only_styles_top_level_list() {
|
||||
// `list_margin >= 0` forces a `margin-left:<n>em` on the outermost
|
||||
// list; nested lists and the default of `-1` stay unstyled.
|
||||
let ast = parse("- a\n - b\n");
|
||||
let c = cfg("/tmp/x");
|
||||
|
||||
let styled = export::render_page_html(
|
||||
&ast,
|
||||
Some("{{content}}".into()),
|
||||
"index",
|
||||
DiaryDate::today_utc(),
|
||||
&c.html,
|
||||
None,
|
||||
2,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
styled.contains("<ul style=\"margin-left:2em\">"),
|
||||
"top-level margin: {styled}"
|
||||
);
|
||||
// Exactly one styled list — the nested `<ul>` is plain.
|
||||
assert_eq!(
|
||||
styled.matches("margin-left").count(),
|
||||
1,
|
||||
"only top: {styled}"
|
||||
);
|
||||
|
||||
let plain = export::render_page_html(
|
||||
&ast,
|
||||
Some("{{content}}".into()),
|
||||
"index",
|
||||
DiaryDate::today_utc(),
|
||||
&c.html,
|
||||
None,
|
||||
-1,
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!plain.contains("margin-left"), "default unstyled: {plain}");
|
||||
}
|
||||
|
||||
// ===== fallback_template + DEFAULT_CSS =====
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -270,6 +270,10 @@ fn defaults_carry_vimwiki_per_wiki_keys() {
|
||||
assert_eq!(cfg.diary_caption_level, 0); // upstream default
|
||||
assert_eq!(cfg.diary_sort, "desc");
|
||||
assert_eq!(cfg.diary_header, "Diary");
|
||||
// diary_months defaults to the 12 English month names.
|
||||
assert_eq!(cfg.diary_months.len(), 12);
|
||||
assert_eq!(cfg.diary_months[0], "January");
|
||||
assert_eq!(cfg.diary_months[11], "December");
|
||||
assert_eq!(cfg.listsyms, " .oOX");
|
||||
assert!(cfg.listsyms_propagate);
|
||||
assert_eq!(cfg.list_margin, -1);
|
||||
@@ -305,6 +309,7 @@ fn raw_wiki_parses_every_new_key() {
|
||||
"diary_caption_level": 2,
|
||||
"diary_sort": "asc",
|
||||
"diary_header": "Journal",
|
||||
"diary_months": ["Jan","Feb","Mar","Apr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],
|
||||
"listsyms": "abcde",
|
||||
"listsym_rejected": "✗",
|
||||
"listsyms_propagate": false,
|
||||
@@ -334,6 +339,8 @@ fn raw_wiki_parses_every_new_key() {
|
||||
assert_eq!(w.diary_caption_level, 2);
|
||||
assert_eq!(w.diary_sort, "asc");
|
||||
assert_eq!(w.diary_header, "Journal");
|
||||
assert_eq!(w.diary_months[4], "Mai");
|
||||
assert_eq!(w.diary_months[11], "Dez");
|
||||
assert_eq!(w.listsyms, "abcde");
|
||||
assert_eq!(w.listsym_rejected, "✗");
|
||||
assert_eq!(w.list_syms().rejected_glyph(), '✗');
|
||||
@@ -369,6 +376,36 @@ fn raw_wiki_parses_every_new_key() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_wiki_derives_list_margin_zero_when_unset() {
|
||||
// vimwiki sets list_margin = 0 for markdown wikis (vs -1 elsewhere) when
|
||||
// the key is absent; an explicit value still wins.
|
||||
let cfg = config_from_json(serde_json::json!({
|
||||
"wikis": [{ "root": "/tmp/md", "syntax": "markdown" }],
|
||||
}));
|
||||
assert_eq!(cfg.wikis[0].list_margin, 0);
|
||||
|
||||
let vw = config_from_json(serde_json::json!({
|
||||
"wikis": [{ "root": "/tmp/vw", "syntax": "vimwiki" }],
|
||||
}));
|
||||
assert_eq!(vw.wikis[0].list_margin, -1);
|
||||
|
||||
let explicit = config_from_json(serde_json::json!({
|
||||
"wikis": [{ "root": "/tmp/md2", "syntax": "markdown", "list_margin": 4 }],
|
||||
}));
|
||||
assert_eq!(explicit.wikis[0].list_margin, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diary_caption_level_accepts_negative() {
|
||||
// vimwiki allows diary_caption_level = -1 (min: -1); the field is i8 so it
|
||||
// parses instead of failing deserialization.
|
||||
let cfg = config_from_json(serde_json::json!({
|
||||
"wikis": [{ "root": "/tmp/w", "diary_caption_level": -1 }],
|
||||
}));
|
||||
assert_eq!(cfg.wikis[0].diary_caption_level, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diary_calendar_defaults_to_iso_weeks() {
|
||||
let cfg = WikiConfig::empty();
|
||||
|
||||
@@ -290,7 +290,7 @@ fn build_toc_text_nests_by_level() {
|
||||
(2u8, "Sub".into(), "sub".into()),
|
||||
(1u8, "Other".into(), "other".into()),
|
||||
];
|
||||
let out = ops::build_toc_text(&items, "Contents", 1);
|
||||
let out = ops::build_toc_text(&items, "Contents", 1, 0);
|
||||
assert!(out.starts_with("= Contents =\n"));
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Contents =");
|
||||
@@ -301,7 +301,7 @@ fn build_toc_text_nests_by_level() {
|
||||
|
||||
#[test]
|
||||
fn build_toc_text_with_no_headings_is_just_the_heading() {
|
||||
let out = ops::build_toc_text(&[], "Contents", 1);
|
||||
let out = ops::build_toc_text(&[], "Contents", 1, 0);
|
||||
assert_eq!(out, "= Contents =\n");
|
||||
}
|
||||
|
||||
@@ -319,7 +319,8 @@ fn links_rebuild_edit_only_acts_when_section_present() {
|
||||
&pages,
|
||||
true,
|
||||
"Generated Links",
|
||||
1
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_none(),
|
||||
"must not insert a links section into a page that lacks one"
|
||||
@@ -333,7 +334,8 @@ fn links_rebuild_edit_only_acts_when_section_present() {
|
||||
&pages,
|
||||
true,
|
||||
"Generated Links",
|
||||
1
|
||||
1,
|
||||
0
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
@@ -358,15 +360,42 @@ fn find_section_range_stops_at_same_level_sibling() {
|
||||
#[test]
|
||||
fn captions_honour_custom_header_and_level() {
|
||||
// toc_header="Table of Contents", level 2 → `== Table of Contents ==`.
|
||||
let out = ops::build_toc_text(&[], "Table of Contents", 2);
|
||||
let out = ops::build_toc_text(&[], "Table of Contents", 2, 0);
|
||||
assert_eq!(out, "== Table of Contents ==\n");
|
||||
// links_header at level 3.
|
||||
let pages = vec!["A".to_string(), "B".to_string()];
|
||||
let links = ops::build_links_text(&pages, "All Pages", 3, None);
|
||||
let links = ops::build_links_text(&pages, "All Pages", 3, None, 0);
|
||||
assert!(links.starts_with("=== All Pages ===\n"));
|
||||
// level clamps to 1..=6.
|
||||
assert!(ops::build_toc_text(&[], "X", 9).starts_with("====== X ======"));
|
||||
assert!(ops::build_toc_text(&[], "X", 0).starts_with("= X ="));
|
||||
assert!(ops::build_toc_text(&[], "X", 9, 0).starts_with("====== X ======"));
|
||||
assert!(ops::build_toc_text(&[], "X", 0, 0).starts_with("= X ="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_margin_indents_generated_bullets_not_headings() {
|
||||
// vimwiki `list_margin` = leading spaces before each generated bullet.
|
||||
// The heading stays at column 0; bullets get `margin` spaces (TOC keeps
|
||||
// its per-depth nesting *after* the base margin).
|
||||
let pages = vec!["A".to_string(), "B".to_string()];
|
||||
let links = ops::build_links_text(&pages, "Generated Links", 1, None, 2);
|
||||
assert!(links.contains("\n - [[A]]\n"), "2-space margin: {links:?}");
|
||||
assert!(
|
||||
links.starts_with("= Generated Links =\n"),
|
||||
"heading unindented: {links:?}"
|
||||
);
|
||||
|
||||
let toc = ops::build_toc_text(
|
||||
&[
|
||||
(1, "Top".into(), "top".into()),
|
||||
(2, "Sub".into(), "sub".into()),
|
||||
],
|
||||
"Contents",
|
||||
1,
|
||||
2,
|
||||
);
|
||||
// Top bullet: 2-space margin; Sub bullet: margin + one nesting level.
|
||||
assert!(toc.contains("\n - [[#top|Top]]\n"), "top margin: {toc:?}");
|
||||
assert!(toc.contains("\n - [[#sub|Sub]]\n"), "nested: {toc:?}");
|
||||
}
|
||||
|
||||
// ===== Links text generation =====
|
||||
@@ -374,7 +403,7 @@ fn captions_honour_custom_header_and_level() {
|
||||
#[test]
|
||||
fn build_links_text_excludes_current_page() {
|
||||
let pages = vec!["A".to_string(), "Home".to_string(), "B".to_string()];
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, Some("Home"));
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, Some("Home"), 0);
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Generated Links =");
|
||||
assert!(lines.contains(&"- [[A]]"));
|
||||
@@ -385,7 +414,7 @@ fn build_links_text_excludes_current_page() {
|
||||
#[test]
|
||||
fn build_links_text_no_excludes_includes_all() {
|
||||
let pages = vec!["A".to_string(), "B".to_string()];
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, None);
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, None, 0);
|
||||
assert!(out.contains("[[A]]"));
|
||||
assert!(out.contains("[[B]]"));
|
||||
}
|
||||
@@ -423,7 +452,7 @@ fn toc_edit_inserts_at_top_when_no_existing_toc() {
|
||||
let src = "= One =\n== Two ==\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1).expect("got an edit");
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0).expect("got an edit");
|
||||
let changes = edit.changes.expect("changes map");
|
||||
let edits = &changes[&uri];
|
||||
assert_eq!(edits.len(), 1);
|
||||
@@ -439,7 +468,7 @@ fn toc_edit_replaces_existing_toc() {
|
||||
let src = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1).expect("got an edit");
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0).expect("got an edit");
|
||||
let changes = edit.changes.expect("changes map");
|
||||
let edits = &changes[&uri];
|
||||
let te = &edits[0];
|
||||
@@ -454,7 +483,7 @@ fn toc_edit_returns_none_for_empty_doc() {
|
||||
let src = "";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/empty.wiki").unwrap();
|
||||
assert!(ops::toc_edit(src, &ast, &uri, true, "Contents", 1).is_none());
|
||||
assert!(ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -465,14 +494,15 @@ fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
||||
let without = "= One =\n== Two ==\n";
|
||||
let ast = parse(without);
|
||||
assert!(
|
||||
ops::toc_rebuild_edit(without, &ast, &uri, true, "Contents", 1).is_none(),
|
||||
ops::toc_rebuild_edit(without, &ast, &uri, true, "Contents", 1, 0).is_none(),
|
||||
"auto_toc should not insert a TOC where none existed"
|
||||
);
|
||||
|
||||
// Existing TOC → rebuild refreshes it.
|
||||
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||
let ast = parse(with);
|
||||
let edit = ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1).expect("rebuild edit");
|
||||
let edit =
|
||||
ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1, 0).expect("rebuild edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
||||
assert!(!te.new_text.contains("Stale"));
|
||||
@@ -486,8 +516,18 @@ fn links_edit_inserts_when_section_absent() {
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let pages = vec!["A".into(), "B".into(), "Home".into()];
|
||||
let edit =
|
||||
ops::links_edit(src, &ast, &uri, "Home", &pages, true, "Generated Links", 1).expect("edit");
|
||||
let edit = ops::links_edit(
|
||||
src,
|
||||
&ast,
|
||||
&uri,
|
||||
"Home",
|
||||
&pages,
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[A]]"));
|
||||
assert!(te.new_text.contains("[[B]]"));
|
||||
@@ -500,8 +540,18 @@ fn links_edit_replaces_when_section_present() {
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let pages = vec!["Fresh".into()];
|
||||
let edit =
|
||||
ops::links_edit(src, &ast, &uri, "Home", &pages, true, "Generated Links", 1).expect("edit");
|
||||
let edit = ops::links_edit(
|
||||
src,
|
||||
&ast,
|
||||
&uri,
|
||||
"Home",
|
||||
&pages,
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[Fresh]]"));
|
||||
assert!(!te.new_text.contains("Stale"));
|
||||
@@ -513,7 +563,7 @@ fn links_edit_returns_none_for_empty_page_list() {
|
||||
let src = "Hi\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
assert!(ops::links_edit(src, &ast, &uri, "Home", &[], true, "Generated Links", 1).is_none());
|
||||
assert!(ops::links_edit(src, &ast, &uri, "Home", &[], true, "Generated Links", 1, 0).is_none());
|
||||
}
|
||||
|
||||
// ===== find_orphans =====
|
||||
|
||||
Reference in New Issue
Block a user