feat(config): P3 config batch — group 1 (clean config + plumbing)
Config plumbing for the whole config batch (HtmlConfig + WikiConfig fields, RawWiki deserialization, From/defaults) plus these behaviors: - rss_name / rss_max_items: HtmlConfig fields (wired into write_rss in the RSS group). - toc_link_format: build_toc_text emits [[#anchor]] (no description) for 1, [[#anchor|title]] for 0. - generated_links_caption: build_links_text emits [[page|FirstHeading]] from a page->heading map (ops::page_captions) when enabled. - table_reduce_last_col: column_widths clamps the last column to width 1 when set; threaded through table_align_edit/table_move_column_edit + commands. - color_dic now ships a populated default palette (red/green/blue/...). - commentstring aligned to upstream's no-space `%%%s`. - auto_chdir (default off): :lcd into the owning wiki root on buffer enter; both clients (ftplugin.lua setup_auto_chdir + Vim NuwikiAutoChdir augroup), with config.wiki_root_for / nuwiki#commands#wiki_root_for resolvers. Also seeded HtmlConfig fields for later groups (valid_html_tags, list/text_ignore_newline, emoji_enable, user_htmls, color_tag_template) and WikiConfig (create_link, dir_link, bullet_types, cycle_bullets). Tests: toc_link_format, generated_links_caption, table_reduce_last_col, config defaults + JSON round-trip for every new key. Full rust suite + both keymap harnesses green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -306,7 +306,8 @@ 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, 0).expect("toc edit produced");
|
||||
let edit =
|
||||
ops::toc_edit(src, &doc, &uri, true, "Contents", 1, 0, 0).expect("toc edit produced");
|
||||
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
||||
|
||||
let toc = ops::build_toc_text(
|
||||
@@ -317,6 +318,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
"Contents",
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
assert!(toc.contains("= Contents ="));
|
||||
assert!(toc.contains("- [[#top|Top]]"));
|
||||
@@ -327,7 +329,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"), 0);
|
||||
let text = ops::build_links_text(&pages, "Links", 1, Some("Home"), 0, None);
|
||||
assert!(text.contains("= Links ="));
|
||||
assert!(text.contains("- [[About]]"));
|
||||
assert!(text.contains("- [[Notes]]"));
|
||||
@@ -345,7 +347,8 @@ fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0
|
||||
0,
|
||||
None
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ fn blank_table_handles_single_cell_grid() {
|
||||
fn align_pads_short_cells_to_widest() {
|
||||
let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n";
|
||||
let ast = parse(src);
|
||||
let edit = ops::table_align_edit(src, &ast, &uri(), 0, true).expect("edit");
|
||||
let edit = ops::table_align_edit(src, &ast, &uri(), 0, true, false).expect("edit");
|
||||
let body = one_text(edit);
|
||||
let lines: Vec<&str> = body.lines().collect();
|
||||
eprintln!("rendered table:");
|
||||
@@ -64,7 +64,23 @@ fn align_pads_short_cells_to_widest() {
|
||||
fn align_returns_none_when_cursor_off_table() {
|
||||
let src = "paragraph\n";
|
||||
let ast = parse(src);
|
||||
assert!(ops::table_align_edit(src, &ast, &uri(), 0, true).is_none());
|
||||
assert!(ops::table_align_edit(src, &ast, &uri(), 0, true, false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_reduce_last_col_keeps_last_column_minimal() {
|
||||
// With reduce_last_col=true, the last column isn't padded to its content
|
||||
// width — it stays at the minimum (1), so `three` is not surrounded by
|
||||
// alignment padding on the right edge.
|
||||
let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n";
|
||||
let ast = parse(src);
|
||||
let edit = ops::table_align_edit(src, &ast, &uri(), 0, true, true).expect("edit");
|
||||
let body = one_text(edit);
|
||||
let lines: Vec<&str> = body.lines().collect();
|
||||
// First two columns still align; the last column stays at width 1 (not 5),
|
||||
// so the header's last cell renders `| c |` not `| c |`.
|
||||
assert_eq!(lines[0], "| a | bbb | c |", "got: {:?}", lines[0]);
|
||||
assert_eq!(lines[2], "| 1 | 2 | three |", "got: {:?}", lines[2]);
|
||||
}
|
||||
|
||||
// ===== moveColumn =====
|
||||
@@ -74,7 +90,7 @@ fn move_column_right_swaps_with_neighbour() {
|
||||
let src = "|a|b|c|\n|--|--|--|\n|1|2|3|\n";
|
||||
let ast = parse(src);
|
||||
// Cursor on first column → swap right.
|
||||
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, 1, true).expect("edit");
|
||||
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, 1, true, false).expect("edit");
|
||||
let body = one_text(edit);
|
||||
let lines: Vec<&str> = body.lines().collect();
|
||||
assert!(lines[0].contains("b") && lines[0].contains("a"));
|
||||
@@ -90,7 +106,7 @@ fn move_column_left_at_zero_is_noop() {
|
||||
let src = "|a|b|\n|--|--|\n|1|2|\n";
|
||||
let ast = parse(src);
|
||||
// Cursor in column 0; left would be -1 → returns None.
|
||||
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, -1, true);
|
||||
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, -1, true, false);
|
||||
assert!(edit.is_none());
|
||||
}
|
||||
|
||||
@@ -98,7 +114,7 @@ fn move_column_left_at_zero_is_noop() {
|
||||
fn move_column_returns_none_off_table() {
|
||||
let src = "paragraph\n";
|
||||
let ast = parse(src);
|
||||
assert!(ops::table_move_column_edit(src, &ast, &uri(), 0, 0, 1, true).is_none());
|
||||
assert!(ops::table_move_column_edit(src, &ast, &uri(), 0, 0, 1, true, false).is_none());
|
||||
}
|
||||
|
||||
// ===== COMMANDS list =====
|
||||
|
||||
@@ -295,6 +295,63 @@ fn defaults_carry_vimwiki_per_wiki_keys() {
|
||||
// HTML section numbering is off by default (vimwiki's `0`).
|
||||
assert_eq!(cfg.html.html_header_numbering, 0);
|
||||
assert_eq!(cfg.html.html_header_numbering_sym, "");
|
||||
// New config defaults match upstream.
|
||||
assert!(cfg.create_link);
|
||||
assert_eq!(cfg.dir_link, "");
|
||||
assert_eq!(cfg.bullet_types, vec!["-", "*", "#"]);
|
||||
assert!(!cfg.cycle_bullets);
|
||||
assert!(!cfg.generated_links_caption);
|
||||
assert_eq!(cfg.toc_link_format, 0);
|
||||
assert!(!cfg.table_reduce_last_col);
|
||||
assert_eq!(cfg.html.rss_name, "rss.xml");
|
||||
assert_eq!(cfg.html.rss_max_items, 10);
|
||||
assert!(cfg.html.list_ignore_newline);
|
||||
assert!(cfg.html.text_ignore_newline);
|
||||
assert!(cfg.html.emoji_enable);
|
||||
assert!(cfg.html.user_htmls.is_empty());
|
||||
assert!(cfg.html.valid_html_tags.contains(&"sub".to_string()));
|
||||
// color_dic ships a populated palette (vimwiki seeds one).
|
||||
assert!(cfg.html.color_dic.contains_key("red"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_wiki_parses_config_batch_keys() {
|
||||
let cfg = config_from_json(serde_json::json!({
|
||||
"wikis": [{
|
||||
"root": "/tmp/w",
|
||||
"create_link": false,
|
||||
"dir_link": "index",
|
||||
"bullet_types": ["+", "-"],
|
||||
"cycle_bullets": true,
|
||||
"generated_links_caption": 1,
|
||||
"toc_link_format": 1,
|
||||
"table_reduce_last_col": true,
|
||||
"rss_name": "feed.xml",
|
||||
"rss_max_items": 5,
|
||||
"valid_html_tags": "b,mark",
|
||||
"list_ignore_newline": 0,
|
||||
"text_ignore_newline": false,
|
||||
"emoji_enable": 0,
|
||||
"user_htmls": ["404.html"],
|
||||
"color_tag_template": "<u>__CONTENT__</u>",
|
||||
}],
|
||||
}));
|
||||
let w = &cfg.wikis[0];
|
||||
assert!(!w.create_link);
|
||||
assert_eq!(w.dir_link, "index");
|
||||
assert_eq!(w.bullet_types, vec!["+", "-"]);
|
||||
assert!(w.cycle_bullets);
|
||||
assert!(w.generated_links_caption);
|
||||
assert_eq!(w.toc_link_format, 1);
|
||||
assert!(w.table_reduce_last_col);
|
||||
assert_eq!(w.html.rss_name, "feed.xml");
|
||||
assert_eq!(w.html.rss_max_items, 5);
|
||||
assert_eq!(w.html.valid_html_tags, vec!["b", "mark"]);
|
||||
assert!(!w.html.list_ignore_newline);
|
||||
assert!(!w.html.text_ignore_newline);
|
||||
assert!(!w.html.emoji_enable);
|
||||
assert_eq!(w.html.user_htmls, vec!["404.html"]);
|
||||
assert_eq!(w.html.color_tag_template, "<u>__CONTENT__</u>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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, 0);
|
||||
let out = ops::build_toc_text(&items, "Contents", 1, 0, 0);
|
||||
assert!(out.starts_with("= Contents =\n"));
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Contents =");
|
||||
@@ -301,10 +301,36 @@ 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, 0);
|
||||
let out = ops::build_toc_text(&[], "Contents", 1, 0, 0);
|
||||
assert_eq!(out, "= Contents =\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toc_link_format_1_drops_the_description() {
|
||||
// vimwiki toc_link_format: 1 = `[[#anchor]]` (no `|title`); 0 = with title.
|
||||
let items = vec![(1u8, "Top".into(), "top".into())];
|
||||
let f0 = ops::build_toc_text(&items, "Contents", 1, 0, 0);
|
||||
assert!(f0.contains("- [[#top|Top]]"), "format 0: {f0}");
|
||||
let f1 = ops::build_toc_text(&items, "Contents", 1, 0, 1);
|
||||
assert!(f1.contains("- [[#top]]"), "format 1: {f1}");
|
||||
assert!(!f1.contains("|Top"), "format 1 has no description: {f1}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_links_caption_emits_first_heading() {
|
||||
use std::collections::BTreeMap;
|
||||
let pages = vec!["About".to_string(), "Notes".to_string()];
|
||||
let mut caps = BTreeMap::new();
|
||||
caps.insert("About".to_string(), "About Us".to_string());
|
||||
// Notes has no caption → falls back to bare [[Notes]].
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, None, 0, Some(&caps));
|
||||
assert!(out.contains("- [[About|About Us]]"), "captioned: {out}");
|
||||
assert!(out.contains("- [[Notes]]"), "fallback: {out}");
|
||||
// Without the caption map, both are bare.
|
||||
let bare = ops::build_links_text(&pages, "Generated Links", 1, None, 0, None);
|
||||
assert!(bare.contains("- [[About]]") && !bare.contains("About Us"), "bare: {bare}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn links_rebuild_edit_only_acts_when_section_present() {
|
||||
let pages = vec!["Home".to_string(), "About".to_string()];
|
||||
@@ -320,7 +346,8 @@ fn links_rebuild_edit_only_acts_when_section_present() {
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0
|
||||
0,
|
||||
None
|
||||
)
|
||||
.is_none(),
|
||||
"must not insert a links section into a page that lacks one"
|
||||
@@ -335,7 +362,8 @@ fn links_rebuild_edit_only_acts_when_section_present() {
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0
|
||||
0,
|
||||
None
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
@@ -360,15 +388,15 @@ 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, 0);
|
||||
let out = ops::build_toc_text(&[], "Table of Contents", 2, 0, 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, 0);
|
||||
let links = ops::build_links_text(&pages, "All Pages", 3, None, 0, None);
|
||||
assert!(links.starts_with("=== All Pages ===\n"));
|
||||
// level clamps to 1..=6.
|
||||
assert!(ops::build_toc_text(&[], "X", 9, 0).starts_with("====== X ======"));
|
||||
assert!(ops::build_toc_text(&[], "X", 0, 0).starts_with("= X ="));
|
||||
assert!(ops::build_toc_text(&[], "X", 9, 0, 0).starts_with("====== X ======"));
|
||||
assert!(ops::build_toc_text(&[], "X", 0, 0, 0).starts_with("= X ="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -377,7 +405,7 @@ fn list_margin_indents_generated_bullets_not_headings() {
|
||||
// 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);
|
||||
let links = ops::build_links_text(&pages, "Generated Links", 1, None, 2, None);
|
||||
assert!(links.contains("\n - [[A]]\n"), "2-space margin: {links:?}");
|
||||
assert!(
|
||||
links.starts_with("= Generated Links =\n"),
|
||||
@@ -392,6 +420,7 @@ fn list_margin_indents_generated_bullets_not_headings() {
|
||||
"Contents",
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
);
|
||||
// Top bullet: 2-space margin; Sub bullet: margin + one nesting level.
|
||||
assert!(toc.contains("\n - [[#top|Top]]\n"), "top margin: {toc:?}");
|
||||
@@ -403,7 +432,7 @@ fn list_margin_indents_generated_bullets_not_headings() {
|
||||
#[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"), 0);
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, Some("Home"), 0, None);
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Generated Links =");
|
||||
assert!(lines.contains(&"- [[A]]"));
|
||||
@@ -414,7 +443,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, 0);
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, None, 0, None);
|
||||
assert!(out.contains("[[A]]"));
|
||||
assert!(out.contains("[[B]]"));
|
||||
}
|
||||
@@ -452,7 +481,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, 0).expect("got an edit");
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0).expect("got an edit");
|
||||
let changes = edit.changes.expect("changes map");
|
||||
let edits = &changes[&uri];
|
||||
assert_eq!(edits.len(), 1);
|
||||
@@ -468,7 +497,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, 0).expect("got an edit");
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0).expect("got an edit");
|
||||
let changes = edit.changes.expect("changes map");
|
||||
let edits = &changes[&uri];
|
||||
let te = &edits[0];
|
||||
@@ -483,7 +512,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, 0).is_none());
|
||||
assert!(ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -494,7 +523,7 @@ 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, 0).is_none(),
|
||||
ops::toc_rebuild_edit(without, &ast, &uri, true, "Contents", 1, 0, 0).is_none(),
|
||||
"auto_toc should not insert a TOC where none existed"
|
||||
);
|
||||
|
||||
@@ -502,7 +531,7 @@ fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
||||
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, 0).expect("rebuild edit");
|
||||
ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1, 0, 0).expect("rebuild edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
||||
assert!(!te.new_text.contains("Stale"));
|
||||
@@ -526,6 +555,7 @@ fn links_edit_inserts_when_section_absent() {
|
||||
"Generated Links",
|
||||
1,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
@@ -550,6 +580,7 @@ fn links_edit_replaces_when_section_present() {
|
||||
"Generated Links",
|
||||
1,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
@@ -563,7 +594,19 @@ 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, 0).is_none());
|
||||
assert!(ops::links_edit(
|
||||
src,
|
||||
&ast,
|
||||
&uri,
|
||||
"Home",
|
||||
&[],
|
||||
true,
|
||||
"Generated Links",
|
||||
1,
|
||||
0,
|
||||
None
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// ===== find_orphans =====
|
||||
|
||||
Reference in New Issue
Block a user