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:
@@ -761,6 +761,26 @@ function! nuwiki#commands#list_remove_done_all() abort
|
|||||||
call s:exec('nuwiki.list.removeDone', l:args)
|
call s:exec('nuwiki.list.removeDone', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" `:'<,'>VimwikiRemoveDone` — remove done items only within the given line
|
||||||
|
" range (1-indexed, inclusive; converted to the server's 0-indexed range).
|
||||||
|
function! nuwiki#commands#list_remove_done_range(l1, l2) abort
|
||||||
|
let l:args = [{ 'uri': s:buf_uri(), 'range': [a:l1 - 1, a:l2 - 1] }]
|
||||||
|
call s:exec('nuwiki.list.removeDone', l:args)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" `:[range]VimwikiRemoveDone[!]` dispatcher. `!` → whole buffer; an explicit
|
||||||
|
" range (`a:range > 0`, e.g. `:'<,'>`) → that line span; otherwise the
|
||||||
|
" current list under the cursor. Mirrors upstream's `-bang -range` form.
|
||||||
|
function! nuwiki#commands#remove_done(bang, range, l1, l2) abort
|
||||||
|
if a:bang
|
||||||
|
call nuwiki#commands#list_remove_done_all()
|
||||||
|
elseif a:range > 0
|
||||||
|
call nuwiki#commands#list_remove_done_range(a:l1, a:l2)
|
||||||
|
else
|
||||||
|
call nuwiki#commands#list_remove_done()
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
" `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
" `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
||||||
function! nuwiki#commands#list_remove_checkbox() abort
|
function! nuwiki#commands#list_remove_checkbox() abort
|
||||||
let l:p = s:cursor_position()
|
let l:p = s:cursor_position()
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ pub struct HtmlRenderer {
|
|||||||
/// names fall through to the default `class="color-<name>"`
|
/// names fall through to the default `class="color-<name>"`
|
||||||
/// rendering. Matches vimwiki's `color_dic`.
|
/// rendering. Matches vimwiki's `color_dic`.
|
||||||
colors: HashMap<String, String>,
|
colors: HashMap<String, String>,
|
||||||
/// vimwiki `list_margin`: left margin (in `em`) applied to the
|
|
||||||
/// outermost list. `-1` (the default) emits no inline style and
|
|
||||||
/// defers to the stylesheet; `>= 0` forces `margin-left:<n>em`.
|
|
||||||
list_margin: i32,
|
|
||||||
/// vimwiki `html_header_numbering`: the heading level at which automatic
|
/// vimwiki `html_header_numbering`: the heading level at which automatic
|
||||||
/// section numbering begins (`0` = off, the default). When `>= 1`, every
|
/// section numbering begins (`0` = off, the default). When `>= 1`, every
|
||||||
/// heading at that level or deeper is prefixed with a dotted section
|
/// heading at that level or deeper is prefixed with a dotted section
|
||||||
@@ -73,7 +69,6 @@ impl HtmlRenderer {
|
|||||||
template: None,
|
template: None,
|
||||||
vars: HashMap::new(),
|
vars: HashMap::new(),
|
||||||
colors: HashMap::new(),
|
colors: HashMap::new(),
|
||||||
list_margin: -1,
|
|
||||||
header_numbering: 0,
|
header_numbering: 0,
|
||||||
header_numbering_sym: String::new(),
|
header_numbering_sym: String::new(),
|
||||||
}
|
}
|
||||||
@@ -110,13 +105,6 @@ impl HtmlRenderer {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the vimwiki `list_margin`. `-1` defers to the stylesheet;
|
|
||||||
/// `>= 0` forces a `margin-left:<n>em` on the outermost list.
|
|
||||||
pub fn with_list_margin(mut self, margin: i32) -> Self {
|
|
||||||
self.list_margin = margin;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supply a `color_dic`: vimwiki colour-tag names mapped to their
|
/// Supply a `color_dic`: vimwiki colour-tag names mapped to their
|
||||||
/// CSS values. A name listed here is rendered as `style="color:V"`;
|
/// CSS values. A name listed here is rendered as `style="color:V"`;
|
||||||
/// missing names fall through to `class="color-<name>"`.
|
/// missing names fall through to `class="color-<name>"`.
|
||||||
@@ -329,16 +317,8 @@ impl HtmlRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_list(&self, n: &ListNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_list(&self, n: &ListNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
self.render_list_nested(n, w, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_nested(&self, n: &ListNode, w: &mut dyn Write, top: bool) -> io::Result<()> {
|
|
||||||
let tag = if n.ordered { "ol" } else { "ul" };
|
let tag = if n.ordered { "ol" } else { "ul" };
|
||||||
if top && self.list_margin >= 0 {
|
|
||||||
writeln!(w, "<{tag} style=\"margin-left:{}em\">", self.list_margin)?;
|
|
||||||
} else {
|
|
||||||
writeln!(w, "<{tag}>")?;
|
writeln!(w, "<{tag}>")?;
|
||||||
}
|
|
||||||
for item in &n.items {
|
for item in &n.items {
|
||||||
self.render_list_item(item, w)?;
|
self.render_list_item(item, w)?;
|
||||||
}
|
}
|
||||||
@@ -368,7 +348,7 @@ impl HtmlRenderer {
|
|||||||
self.render_inlines(&n.children, w)?;
|
self.render_inlines(&n.children, w)?;
|
||||||
if let Some(sublist) = &n.sublist {
|
if let Some(sublist) = &n.sublist {
|
||||||
w.write_all(b"\n")?;
|
w.write_all(b"\n")?;
|
||||||
self.render_list_nested(sublist, w, false)?;
|
self.render_list(sublist, w)?;
|
||||||
}
|
}
|
||||||
w.write_all(b"</li>\n")
|
w.write_all(b"</li>\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,8 +334,12 @@ fn toc_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceE
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|w| (w.config.toc_header.clone(), w.config.toc_header_level))
|
.map(|w| (w.config.toc_header.clone(), w.config.toc_header_level))
|
||||||
.unwrap_or_else(|| (ops::TOC_HEADING.to_string(), 1));
|
.unwrap_or_else(|| (ops::TOC_HEADING.to_string(), 1));
|
||||||
|
let margin = wiki
|
||||||
|
.as_ref()
|
||||||
|
.map(|w| w.config.list_margin.max(0) as usize)
|
||||||
|
.unwrap_or(0);
|
||||||
Ok(ops::toc_edit(
|
Ok(ops::toc_edit(
|
||||||
&doc.text, &doc.ast, &p.uri, utf8, &header, level,
|
&doc.text, &doc.ast, &p.uri, utf8, &header, level, margin,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +371,7 @@ fn links_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<Workspac
|
|||||||
utf8,
|
utf8,
|
||||||
&wiki.config.links_header,
|
&wiki.config.links_header,
|
||||||
wiki.config.links_header_level,
|
wiki.config.links_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,6 +644,7 @@ fn tags_generate_links(
|
|||||||
utf8,
|
utf8,
|
||||||
&wiki.config.tags_header,
|
&wiki.config.tags_header,
|
||||||
wiki.config.tags_header_level,
|
wiki.config.tags_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,6 +894,9 @@ fn list_remove_done(backend: &Backend, args: Vec<Value>) -> Result<Option<Worksp
|
|||||||
uri: Option<Url>,
|
uri: Option<Url>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
position: Option<LspPosition>,
|
position: Option<LspPosition>,
|
||||||
|
/// `[line1, line2]` (0-indexed, inclusive) for the ranged form.
|
||||||
|
#[serde(default)]
|
||||||
|
range: Option<[u32; 2]>,
|
||||||
}
|
}
|
||||||
let parsed: Args = serde_json::from_value(raw).map_err(|e| format!("invalid args: {e}"))?;
|
let parsed: Args = serde_json::from_value(raw).map_err(|e| format!("invalid args: {e}"))?;
|
||||||
let uri = parsed
|
let uri = parsed
|
||||||
@@ -899,12 +908,16 @@ fn list_remove_done(backend: &Backend, args: Vec<Value>) -> Result<Option<Worksp
|
|||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||||
|
// A range takes precedence over a cursor position; the client sends at
|
||||||
|
// most one (range for `:'<,'>RemoveDone`, position for the current list).
|
||||||
|
let range = parsed.range.map(|[a, b]| (a.min(b), a.max(b)));
|
||||||
|
let pos = if range.is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
parsed.position
|
||||||
|
};
|
||||||
Ok(ops::remove_done_edit(
|
Ok(ops::remove_done_edit(
|
||||||
&doc.text,
|
&doc.text, &doc.ast, &uri, pos, range, utf8,
|
||||||
&doc.ast,
|
|
||||||
&uri,
|
|
||||||
parsed.position,
|
|
||||||
utf8,
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1814,14 +1827,21 @@ pub mod ops {
|
|||||||
/// Build a heading + nested list TOC for the current document. The TOC
|
/// Build a heading + nested list TOC for the current document. The TOC
|
||||||
/// itself (any existing `= Contents =` heading on the page) is skipped
|
/// itself (any existing `= Contents =` heading on the page) is skipped
|
||||||
/// so re-generation is idempotent.
|
/// so re-generation is idempotent.
|
||||||
pub fn build_toc_text(items: &[(u8, String, String)], heading_name: &str, level: u8) -> String {
|
pub fn build_toc_text(
|
||||||
|
items: &[(u8, String, String)],
|
||||||
|
heading_name: &str,
|
||||||
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
|
) -> String {
|
||||||
let mut out = caption_line(heading_name, level);
|
let mut out = caption_line(heading_name, level);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
let pad = " ".repeat(margin);
|
||||||
let min_level = items.iter().map(|(l, _, _)| *l).min().unwrap_or(1);
|
let min_level = items.iter().map(|(l, _, _)| *l).min().unwrap_or(1);
|
||||||
for (level, title, anchor) in items {
|
for (level, title, anchor) in items {
|
||||||
let depth = level.saturating_sub(min_level) as usize;
|
let depth = level.saturating_sub(min_level) as usize;
|
||||||
|
out.push_str(&pad);
|
||||||
for _ in 0..depth {
|
for _ in 0..depth {
|
||||||
out.push_str(" ");
|
out.push_str(" ");
|
||||||
}
|
}
|
||||||
@@ -1841,12 +1861,15 @@ pub mod ops {
|
|||||||
heading_name: &str,
|
heading_name: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
exclude: Option<&str>,
|
exclude: Option<&str>,
|
||||||
|
margin: usize,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut out = caption_line(heading_name, level);
|
let mut out = caption_line(heading_name, level);
|
||||||
|
let pad = " ".repeat(margin);
|
||||||
for name in pages {
|
for name in pages {
|
||||||
if Some(name.as_str()) == exclude {
|
if Some(name.as_str()) == exclude {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
out.push_str(&pad);
|
||||||
out.push_str("- [[");
|
out.push_str("- [[");
|
||||||
out.push_str(name);
|
out.push_str(name);
|
||||||
out.push_str("]]\n");
|
out.push_str("]]\n");
|
||||||
@@ -1966,6 +1989,7 @@ pub mod ops {
|
|||||||
/// Produce a `WorkspaceEdit` that replaces or inserts the TOC for the
|
/// Produce a `WorkspaceEdit` that replaces or inserts the TOC for the
|
||||||
/// given document. Returns `None` if the document has no headings at
|
/// given document. Returns `None` if the document has no headings at
|
||||||
/// all (other than possibly a pre-existing TOC heading).
|
/// all (other than possibly a pre-existing TOC heading).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn toc_edit(
|
pub fn toc_edit(
|
||||||
text: &str,
|
text: &str,
|
||||||
ast: &DocumentNode,
|
ast: &DocumentNode,
|
||||||
@@ -1973,6 +1997,7 @@ pub mod ops {
|
|||||||
utf8: bool,
|
utf8: bool,
|
||||||
heading: &str,
|
heading: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
let items = collect_toc_items(ast, heading);
|
let items = collect_toc_items(ast, heading);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
@@ -1982,7 +2007,7 @@ pub mod ops {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|it| (it.level, it.title, it.anchor))
|
.map(|it| (it.level, it.title, it.anchor))
|
||||||
.collect();
|
.collect();
|
||||||
let new_text = build_toc_text(&triples, heading, level);
|
let new_text = build_toc_text(&triples, heading, level, margin);
|
||||||
let mut b = WorkspaceEditBuilder::new();
|
let mut b = WorkspaceEditBuilder::new();
|
||||||
match find_section_range(ast, heading) {
|
match find_section_range(ast, heading) {
|
||||||
Some((start, end)) => {
|
Some((start, end)) => {
|
||||||
@@ -2010,6 +2035,7 @@ pub mod ops {
|
|||||||
/// `= Contents =` section — used by the `auto_toc`-on-save hook so we
|
/// `= Contents =` section — used by the `auto_toc`-on-save hook so we
|
||||||
/// rebuild an existing TOC without inserting one into pages that never
|
/// rebuild an existing TOC without inserting one into pages that never
|
||||||
/// had one.
|
/// had one.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn toc_rebuild_edit(
|
pub fn toc_rebuild_edit(
|
||||||
text: &str,
|
text: &str,
|
||||||
ast: &DocumentNode,
|
ast: &DocumentNode,
|
||||||
@@ -2017,9 +2043,10 @@ pub mod ops {
|
|||||||
utf8: bool,
|
utf8: bool,
|
||||||
heading: &str,
|
heading: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
find_section_range(ast, heading)?;
|
find_section_range(ast, heading)?;
|
||||||
toc_edit(text, ast, uri, utf8, heading, level)
|
toc_edit(text, ast, uri, utf8, heading, level, margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produce a `WorkspaceEdit` that replaces or inserts the
|
/// Produce a `WorkspaceEdit` that replaces or inserts the
|
||||||
@@ -2034,11 +2061,12 @@ pub mod ops {
|
|||||||
utf8: bool,
|
utf8: bool,
|
||||||
heading: &str,
|
heading: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
if all_pages.is_empty() {
|
if all_pages.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let new_text = build_links_text(all_pages, heading, level, Some(current_page));
|
let new_text = build_links_text(all_pages, heading, level, Some(current_page), margin);
|
||||||
let mut b = WorkspaceEditBuilder::new();
|
let mut b = WorkspaceEditBuilder::new();
|
||||||
match find_section_range(ast, heading) {
|
match find_section_range(ast, heading) {
|
||||||
Some((start, end)) => {
|
Some((start, end)) => {
|
||||||
@@ -2067,6 +2095,7 @@ pub mod ops {
|
|||||||
utf8: bool,
|
utf8: bool,
|
||||||
heading: &str,
|
heading: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
find_section_range(ast, heading)?;
|
find_section_range(ast, heading)?;
|
||||||
links_edit(
|
links_edit(
|
||||||
@@ -2078,6 +2107,7 @@ pub mod ops {
|
|||||||
utf8,
|
utf8,
|
||||||
heading,
|
heading,
|
||||||
level,
|
level,
|
||||||
|
margin,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2291,13 +2321,17 @@ pub mod ops {
|
|||||||
/// Delete every checkbox item whose state is `Done` or `Rejected`,
|
/// Delete every checkbox item whose state is `Done` or `Rejected`,
|
||||||
/// cascading into their sublists. When `pos` is `Some`, restrict to
|
/// cascading into their sublists. When `pos` is `Some`, restrict to
|
||||||
/// the contiguous list block containing `pos`'s line (the "current
|
/// the contiguous list block containing `pos`'s line (the "current
|
||||||
/// list" — including its nested sublists); otherwise sweep the whole
|
/// list" — including its nested sublists). When `range` is `Some`
|
||||||
/// doc.
|
/// (`(line1, line2)`, 0-indexed inclusive), sweep the whole doc but keep
|
||||||
|
/// only items whose start line falls in that range — the vimwiki
|
||||||
|
/// `:'<,'>VimwikiRemoveDone` ranged form. With neither, sweep the whole
|
||||||
|
/// doc. `pos` and `range` are mutually exclusive (the client sets one).
|
||||||
pub fn remove_done_edit(
|
pub fn remove_done_edit(
|
||||||
text: &str,
|
text: &str,
|
||||||
ast: &DocumentNode,
|
ast: &DocumentNode,
|
||||||
uri: &Url,
|
uri: &Url,
|
||||||
pos: Option<LspPosition>,
|
pos: Option<LspPosition>,
|
||||||
|
range: Option<(u32, u32)>,
|
||||||
utf8: bool,
|
utf8: bool,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
let mut victims: Vec<Span> = Vec::new();
|
let mut victims: Vec<Span> = Vec::new();
|
||||||
@@ -2307,6 +2341,12 @@ pub mod ops {
|
|||||||
Some(nuwiki_core::ast::CheckboxState::Done)
|
Some(nuwiki_core::ast::CheckboxState::Done)
|
||||||
| Some(nuwiki_core::ast::CheckboxState::Rejected),
|
| Some(nuwiki_core::ast::CheckboxState::Rejected),
|
||||||
) {
|
) {
|
||||||
|
if let Some((l1, l2)) = range {
|
||||||
|
let line = item.span.start.line;
|
||||||
|
if line < l1 || line > l2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
victims.push(extend_to_line_end(text, item.span));
|
victims.push(extend_to_line_end(text, item.span));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3187,7 +3227,9 @@ pub mod ops {
|
|||||||
tag: Option<&str>,
|
tag: Option<&str>,
|
||||||
header: &str,
|
header: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
|
let pad = " ".repeat(margin);
|
||||||
match tag {
|
match tag {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
let pages = pages_by_tag.get(t)?;
|
let pages = pages_by_tag.get(t)?;
|
||||||
@@ -3196,7 +3238,7 @@ pub mod ops {
|
|||||||
}
|
}
|
||||||
let mut out = caption_line(&format!("Tag: {t}"), level);
|
let mut out = caption_line(&format!("Tag: {t}"), level);
|
||||||
for p in pages {
|
for p in pages {
|
||||||
out.push_str(&format!("- [[{p}]]\n"));
|
out.push_str(&format!("{pad}- [[{p}]]\n"));
|
||||||
}
|
}
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
@@ -3210,7 +3252,7 @@ pub mod ops {
|
|||||||
out.push('\n');
|
out.push('\n');
|
||||||
out.push_str(&caption_line(name, sub));
|
out.push_str(&caption_line(name, sub));
|
||||||
for p in pages {
|
for p in pages {
|
||||||
out.push_str(&format!("- [[{p}]]\n"));
|
out.push_str(&format!("{pad}- [[{p}]]\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(out)
|
Some(out)
|
||||||
@@ -3242,8 +3284,9 @@ pub mod ops {
|
|||||||
utf8: bool,
|
utf8: bool,
|
||||||
header: &str,
|
header: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
let body = build_tag_links_text(pages_by_tag, tag, header, level)?;
|
let body = build_tag_links_text(pages_by_tag, tag, header, level, margin)?;
|
||||||
let heading = tag_section_heading(tag, header);
|
let heading = tag_section_heading(tag, header);
|
||||||
let mut b = WorkspaceEditBuilder::new();
|
let mut b = WorkspaceEditBuilder::new();
|
||||||
match find_section_range(ast, &heading) {
|
match find_section_range(ast, &heading) {
|
||||||
@@ -3263,6 +3306,7 @@ pub mod ops {
|
|||||||
/// Like [`tag_links_edit`] for the no-arg index, but only when a
|
/// Like [`tag_links_edit`] for the no-arg index, but only when a
|
||||||
/// `Generated Tags` section already exists — the `auto_generate_tags`
|
/// `Generated Tags` section already exists — the `auto_generate_tags`
|
||||||
/// on-save hook. Never inserts one into a page that lacks it.
|
/// on-save hook. Never inserts one into a page that lacks it.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn tag_links_rebuild_edit(
|
pub fn tag_links_rebuild_edit(
|
||||||
text: &str,
|
text: &str,
|
||||||
ast: &DocumentNode,
|
ast: &DocumentNode,
|
||||||
@@ -3271,9 +3315,20 @@ pub mod ops {
|
|||||||
utf8: bool,
|
utf8: bool,
|
||||||
header: &str,
|
header: &str,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
margin: usize,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
find_section_range(ast, header)?;
|
find_section_range(ast, header)?;
|
||||||
tag_links_edit(text, ast, uri, None, pages_by_tag, utf8, header, level)
|
tag_links_edit(
|
||||||
|
text,
|
||||||
|
ast,
|
||||||
|
uri,
|
||||||
|
None,
|
||||||
|
pages_by_tag,
|
||||||
|
utf8,
|
||||||
|
header,
|
||||||
|
level,
|
||||||
|
margin,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the import live for downstream use even when the local module
|
// Keep the import live for downstream use even when the local module
|
||||||
@@ -3378,7 +3433,6 @@ pub mod export_ops {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(cfg.file_extension.as_str()),
|
Some(cfg.file_extension.as_str()),
|
||||||
cfg.list_margin,
|
|
||||||
std::collections::HashMap::new(),
|
std::collections::HashMap::new(),
|
||||||
)?;
|
)?;
|
||||||
let out_path = output_path_for(&cfg.html, name);
|
let out_path = output_path_for(&cfg.html, name);
|
||||||
|
|||||||
@@ -49,13 +49,21 @@ pub struct WikiConfig {
|
|||||||
/// Weekday the diary week begins on (`monday`..`sunday`, vimwiki's
|
/// Weekday the diary week begins on (`monday`..`sunday`, vimwiki's
|
||||||
/// `diary_start_week_day`). Used only when `diary_weekly_style = date`.
|
/// `diary_start_week_day`). Used only when `diary_weekly_style = date`.
|
||||||
pub diary_start_week_day: String,
|
pub diary_start_week_day: String,
|
||||||
/// Caption level for the diary index page headings (1 = h1).
|
/// Caption level for the diary index page headings (1 = h1). vimwiki's
|
||||||
pub diary_caption_level: u8,
|
/// `diary_caption_level` (`min: -1`). nuwiki uses it as the base level of
|
||||||
|
/// the year/month index tree; values `< 0` clamp to `0` (upstream's `-1`
|
||||||
|
/// "read no per-page captions" mode isn't modelled — nuwiki builds the
|
||||||
|
/// tree from dates, not page headers).
|
||||||
|
pub diary_caption_level: i8,
|
||||||
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
||||||
/// index.
|
/// index.
|
||||||
pub diary_sort: String,
|
pub diary_sort: String,
|
||||||
/// H1 text for the diary index page.
|
/// H1 text for the diary index page.
|
||||||
pub diary_header: String,
|
pub diary_header: String,
|
||||||
|
/// vimwiki `diary_months`: month-number → display name for the diary
|
||||||
|
/// index tree. Twelve entries (January..December) by default; a custom
|
||||||
|
/// list falls back to the English name for any missing/out-of-range slot.
|
||||||
|
pub diary_months: Vec<String>,
|
||||||
/// Vimwiki's `g:vimwiki_listsyms`. A progression of checkbox glyphs
|
/// Vimwiki's `g:vimwiki_listsyms`. A progression of checkbox glyphs
|
||||||
/// from "empty" (first) to "done" (last); the lexer recognises these
|
/// from "empty" (first) to "done" (last); the lexer recognises these
|
||||||
/// glyphs and the toggle/cycle commands walk them. Any length ≥ 2 is
|
/// glyphs and the toggle/cycle commands walk them. Any length ≥ 2 is
|
||||||
@@ -66,8 +74,12 @@ pub struct WikiConfig {
|
|||||||
pub listsym_rejected: String,
|
pub listsym_rejected: String,
|
||||||
/// When toggling a parent list item, also cascade to descendants.
|
/// When toggling a parent list item, also cascade to descendants.
|
||||||
pub listsyms_propagate: bool,
|
pub listsyms_propagate: bool,
|
||||||
/// `-1` keeps tight lists; positive integers indent list items by
|
/// vimwiki `list_margin`: number of leading spaces before bullets in
|
||||||
/// that many extra columns. Layout hint for the renderer.
|
/// **buffer-side generated lists** — `:VimwikiGenerateLinks`, the TOC, the
|
||||||
|
/// tags index, and the diary index. `>= 0` indents by that many spaces;
|
||||||
|
/// `< 0` means "no margin" (default `-1`; `0` for markdown wikis).
|
||||||
|
/// Divergence: upstream resolves `< 0` to the buffer's `'shiftwidth'`,
|
||||||
|
/// which the server can't observe, so negatives collapse to zero indent.
|
||||||
pub list_margin: i32,
|
pub list_margin: i32,
|
||||||
/// Character used in place of literal spaces inside wikilink
|
/// Character used in place of literal spaces inside wikilink
|
||||||
/// targets when writing to disk (a single space `" "` by default,
|
/// targets when writing to disk (a single space `" "` by default,
|
||||||
@@ -229,6 +241,7 @@ impl WikiConfig {
|
|||||||
diary_caption_level: d.diary_caption_level,
|
diary_caption_level: d.diary_caption_level,
|
||||||
diary_sort: d.diary_sort,
|
diary_sort: d.diary_sort,
|
||||||
diary_header: d.diary_header,
|
diary_header: d.diary_header,
|
||||||
|
diary_months: d.diary_months,
|
||||||
listsyms: d.listsyms,
|
listsyms: d.listsyms,
|
||||||
listsym_rejected: d.listsym_rejected,
|
listsym_rejected: d.listsym_rejected,
|
||||||
listsyms_propagate: d.listsyms_propagate,
|
listsyms_propagate: d.listsyms_propagate,
|
||||||
@@ -268,6 +281,7 @@ impl WikiConfig {
|
|||||||
diary_caption_level: d.diary_caption_level,
|
diary_caption_level: d.diary_caption_level,
|
||||||
diary_sort: d.diary_sort,
|
diary_sort: d.diary_sort,
|
||||||
diary_header: d.diary_header,
|
diary_header: d.diary_header,
|
||||||
|
diary_months: d.diary_months,
|
||||||
listsyms: d.listsyms,
|
listsyms: d.listsyms,
|
||||||
listsym_rejected: d.listsym_rejected,
|
listsym_rejected: d.listsym_rejected,
|
||||||
listsyms_propagate: d.listsyms_propagate,
|
listsyms_propagate: d.listsyms_propagate,
|
||||||
@@ -371,6 +385,27 @@ fn default_diary_header() -> String {
|
|||||||
"Diary".to_string()
|
"Diary".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// vimwiki's default `diary_months` — the English month names, January..December.
|
||||||
|
fn default_diary_months() -> Vec<String> {
|
||||||
|
[
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_listsyms() -> String {
|
fn default_listsyms() -> String {
|
||||||
" .oOX".to_string()
|
" .oOX".to_string()
|
||||||
}
|
}
|
||||||
@@ -411,6 +446,7 @@ fn wiki_defaults() -> WikiDefaults {
|
|||||||
diary_caption_level: 0,
|
diary_caption_level: 0,
|
||||||
diary_sort: default_diary_sort(),
|
diary_sort: default_diary_sort(),
|
||||||
diary_header: default_diary_header(),
|
diary_header: default_diary_header(),
|
||||||
|
diary_months: default_diary_months(),
|
||||||
listsyms: default_listsyms(),
|
listsyms: default_listsyms(),
|
||||||
listsym_rejected: default_listsym_rejected(),
|
listsym_rejected: default_listsym_rejected(),
|
||||||
listsyms_propagate: true,
|
listsyms_propagate: true,
|
||||||
@@ -436,9 +472,10 @@ struct WikiDefaults {
|
|||||||
diary_frequency: String,
|
diary_frequency: String,
|
||||||
diary_weekly_style: String,
|
diary_weekly_style: String,
|
||||||
diary_start_week_day: String,
|
diary_start_week_day: String,
|
||||||
diary_caption_level: u8,
|
diary_caption_level: i8,
|
||||||
diary_sort: String,
|
diary_sort: String,
|
||||||
diary_header: String,
|
diary_header: String,
|
||||||
|
diary_months: Vec<String>,
|
||||||
listsyms: String,
|
listsyms: String,
|
||||||
listsym_rejected: String,
|
listsym_rejected: String,
|
||||||
listsyms_propagate: bool,
|
listsyms_propagate: bool,
|
||||||
@@ -673,12 +710,14 @@ struct RawWiki {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_start_week_day: Option<String>,
|
diary_start_week_day: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_caption_level: Option<u8>,
|
diary_caption_level: Option<i8>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_sort: Option<String>,
|
diary_sort: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_header: Option<String>,
|
diary_header: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
diary_months: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
listsyms: Option<String>,
|
listsyms: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
listsym_rejected: Option<String>,
|
listsym_rejected: Option<String>,
|
||||||
@@ -764,11 +803,19 @@ impl From<RawWiki> for WikiConfig {
|
|||||||
html.html_header_numbering_sym = s;
|
html.html_header_numbering_sym = s;
|
||||||
}
|
}
|
||||||
let d = wiki_defaults();
|
let d = wiki_defaults();
|
||||||
|
let syntax = r.syntax.unwrap_or_else(|| "vimwiki".into());
|
||||||
|
// vimwiki derives `list_margin = 0` for markdown wikis when the key is
|
||||||
|
// unset (vars.vim:651); other syntaxes keep the `-1` default.
|
||||||
|
let list_margin = r.list_margin.unwrap_or(if syntax == "markdown" {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
d.list_margin
|
||||||
|
});
|
||||||
WikiConfig {
|
WikiConfig {
|
||||||
name: r.name.unwrap_or(derived_name),
|
name: r.name.unwrap_or(derived_name),
|
||||||
root,
|
root,
|
||||||
file_extension: r.file_extension.unwrap_or_else(|| ".wiki".into()),
|
file_extension: r.file_extension.unwrap_or_else(|| ".wiki".into()),
|
||||||
syntax: r.syntax.unwrap_or_else(|| "vimwiki".into()),
|
syntax,
|
||||||
index: r.index.unwrap_or(d.index),
|
index: r.index.unwrap_or(d.index),
|
||||||
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
||||||
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
||||||
@@ -778,10 +825,11 @@ impl From<RawWiki> for WikiConfig {
|
|||||||
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
||||||
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
||||||
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
||||||
|
diary_months: r.diary_months.unwrap_or(d.diary_months),
|
||||||
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
||||||
listsym_rejected: r.listsym_rejected.unwrap_or(d.listsym_rejected),
|
listsym_rejected: r.listsym_rejected.unwrap_or(d.listsym_rejected),
|
||||||
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
||||||
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
list_margin,
|
||||||
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
||||||
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
||||||
auto_generate_links: r.auto_generate_links.unwrap_or(d.auto_generate_links),
|
auto_generate_links: r.auto_generate_links.unwrap_or(d.auto_generate_links),
|
||||||
|
|||||||
@@ -202,17 +202,27 @@ fn heading(level: u8, text: &str) -> String {
|
|||||||
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
||||||
///
|
///
|
||||||
/// `caption_level` sets the level of the top caption; the year and month
|
/// `caption_level` sets the level of the top caption; the year and month
|
||||||
/// subheadings nest one and two levels below it. `sort` controls whether
|
/// subheadings nest one and two levels below it. A `caption_level < 0`
|
||||||
/// the flat list runs newest- or oldest-first.
|
/// clamps to `0` (nuwiki builds the tree from dates, so upstream's `-1`
|
||||||
|
/// "no per-page captions" mode doesn't apply). `months` supplies the
|
||||||
|
/// month-number → display name table; an empty slice (or a short list)
|
||||||
|
/// falls back to the English `month_name` for any missing slot. `sort`
|
||||||
|
/// controls whether the flat list runs newest- or oldest-first. `margin`
|
||||||
|
/// is the number of leading spaces before each entry bullet (vimwiki's
|
||||||
|
/// `list_margin`).
|
||||||
pub fn build_index_body(
|
pub fn build_index_body(
|
||||||
entries: &[DiaryEntry],
|
entries: &[DiaryEntry],
|
||||||
diary_rel_path: &str,
|
diary_rel_path: &str,
|
||||||
index_heading: &str,
|
index_heading: &str,
|
||||||
sort: DiarySort,
|
sort: DiarySort,
|
||||||
caption_level: u8,
|
caption_level: i8,
|
||||||
|
months: &[String],
|
||||||
|
margin: usize,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
let base_level = caption_level.max(0) as u8;
|
||||||
|
let pad = " ".repeat(margin);
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(&heading(caption_level, index_heading));
|
out.push_str(&heading(base_level, index_heading));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
@@ -225,8 +235,8 @@ pub fn build_index_body(
|
|||||||
DiarySort::Asc => a.date.cmp(&b.date),
|
DiarySort::Asc => a.date.cmp(&b.date),
|
||||||
});
|
});
|
||||||
|
|
||||||
let year_level = caption_level.saturating_add(1);
|
let year_level = base_level.saturating_add(1);
|
||||||
let month_level = caption_level.saturating_add(2);
|
let month_level = base_level.saturating_add(2);
|
||||||
let mut current_year: Option<i32> = None;
|
let mut current_year: Option<i32> = None;
|
||||||
let mut current_month: Option<u8> = None;
|
let mut current_month: Option<u8> = None;
|
||||||
for e in sorted {
|
for e in sorted {
|
||||||
@@ -238,12 +248,17 @@ pub fn build_index_body(
|
|||||||
current_month = None;
|
current_month = None;
|
||||||
}
|
}
|
||||||
if current_month != Some(e.date.month) {
|
if current_month != Some(e.date.month) {
|
||||||
out.push_str(&heading(month_level, month_name(e.date.month)));
|
let label = months
|
||||||
|
.get((e.date.month.saturating_sub(1)) as usize)
|
||||||
|
.map(String::as_str)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| month_name(e.date.month));
|
||||||
|
out.push_str(&heading(month_level, label));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
current_month = Some(e.date.month);
|
current_month = Some(e.date.month);
|
||||||
}
|
}
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"- [[{}/{}]]\n",
|
"{pad}- [[{}/{}]]\n",
|
||||||
diary_rel_path.trim_end_matches('/'),
|
diary_rel_path.trim_end_matches('/'),
|
||||||
e.date.format()
|
e.date.format()
|
||||||
));
|
));
|
||||||
@@ -253,7 +268,8 @@ pub fn build_index_body(
|
|||||||
|
|
||||||
/// Render the diary-index body for the given wiki's currently-indexed
|
/// Render the diary-index body for the given wiki's currently-indexed
|
||||||
/// entries. Convenience wrapper around [`build_index_body`] that honours
|
/// entries. Convenience wrapper around [`build_index_body`] that honours
|
||||||
/// the wiki's `diary_header`, `diary_sort`, and `diary_caption_level`.
|
/// the wiki's `diary_header`, `diary_sort`, `diary_caption_level`, and
|
||||||
|
/// `diary_months`.
|
||||||
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
||||||
build_index_body(
|
build_index_body(
|
||||||
&list_entries(index),
|
&list_entries(index),
|
||||||
@@ -261,6 +277,8 @@ pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
|||||||
&cfg.diary_header,
|
&cfg.diary_header,
|
||||||
DiarySort::parse(&cfg.diary_sort),
|
DiarySort::parse(&cfg.diary_sort),
|
||||||
cfg.diary_caption_level,
|
cfg.diary_caption_level,
|
||||||
|
&cfg.diary_months,
|
||||||
|
cfg.list_margin.max(0) as usize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ pub fn render_page_html(
|
|||||||
today: DiaryDate,
|
today: DiaryDate,
|
||||||
cfg: &HtmlConfig,
|
cfg: &HtmlConfig,
|
||||||
wiki_extension: Option<&str>,
|
wiki_extension: Option<&str>,
|
||||||
list_margin: i32,
|
|
||||||
extra_vars: HashMap<String, String>,
|
extra_vars: HashMap<String, String>,
|
||||||
) -> std::io::Result<String> {
|
) -> std::io::Result<String> {
|
||||||
let date_str = match doc.metadata.date.as_deref() {
|
let date_str = match doc.metadata.date.as_deref() {
|
||||||
@@ -228,7 +227,7 @@ pub fn render_page_html(
|
|||||||
vars.insert(k, v);
|
vars.insert(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut r = HtmlRenderer::new().with_list_margin(list_margin);
|
let mut r = HtmlRenderer::new();
|
||||||
if let Some(ext) = wiki_extension.filter(|e| !e.trim_start_matches('.').is_empty()) {
|
if let Some(ext) = wiki_extension.filter(|e| !e.trim_start_matches('.').is_empty()) {
|
||||||
// Strip the wiki extension from page links before the default resolver
|
// Strip the wiki extension from page links before the default resolver
|
||||||
// turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather
|
// turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather
|
||||||
|
|||||||
@@ -627,6 +627,7 @@ impl LanguageServer for Backend {
|
|||||||
self.use_utf8.load(Ordering::Relaxed),
|
self.use_utf8.load(Ordering::Relaxed),
|
||||||
&wiki.config.toc_header,
|
&wiki.config.toc_header,
|
||||||
wiki.config.toc_header_level,
|
wiki.config.toc_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -656,6 +657,7 @@ impl LanguageServer for Backend {
|
|||||||
utf8,
|
utf8,
|
||||||
&wiki.config.links_header,
|
&wiki.config.links_header,
|
||||||
wiki.config.links_header_level,
|
wiki.config.links_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if let Some(edit) = edit {
|
if let Some(edit) = edit {
|
||||||
@@ -680,6 +682,7 @@ impl LanguageServer for Backend {
|
|||||||
utf8,
|
utf8,
|
||||||
&wiki.config.tags_header,
|
&wiki.config.tags_header,
|
||||||
wiki.config.tags_header_level,
|
wiki.config.tags_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if let Some(edit) = edit {
|
if let Some(edit) = edit {
|
||||||
|
|||||||
@@ -262,7 +262,15 @@ fn nuwiki_diary_index_resolves_index_uri() {
|
|||||||
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
||||||
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
||||||
let entries = diary::list_entries(&idx);
|
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.starts_with("= Diary ="));
|
||||||
assert!(body.contains("2026-05-12"));
|
assert!(body.contains("2026-05-12"));
|
||||||
assert!(body.contains("2026-05-10"));
|
assert!(body.contains("2026-05-10"));
|
||||||
@@ -298,7 +306,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
|||||||
let src = "= Top =\n== Sub ==\ntext\n";
|
let src = "= Top =\n== Sub ==\ntext\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
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());
|
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
||||||
|
|
||||||
let toc = ops::build_toc_text(
|
let toc = ops::build_toc_text(
|
||||||
@@ -308,6 +316,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
|||||||
],
|
],
|
||||||
"Contents",
|
"Contents",
|
||||||
1,
|
1,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
assert!(toc.contains("= Contents ="));
|
assert!(toc.contains("= Contents ="));
|
||||||
assert!(toc.contains("- [[#top|Top]]"));
|
assert!(toc.contains("- [[#top|Top]]"));
|
||||||
@@ -318,7 +327,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||||
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
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("= Links ="));
|
||||||
assert!(text.contains("- [[About]]"));
|
assert!(text.contains("- [[About]]"));
|
||||||
assert!(text.contains("- [[Notes]]"));
|
assert!(text.contains("- [[Notes]]"));
|
||||||
@@ -327,7 +336,18 @@ fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
|||||||
let src = "= Existing =\n";
|
let src = "= Existing =\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
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.
|
// :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 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.starts_with("= Tag: alpha ="));
|
||||||
assert!(single.contains("- [[Home]]"));
|
assert!(single.contains("- [[Home]]"));
|
||||||
assert!(single.contains("- [[Work]]"));
|
assert!(single.contains("- [[Work]]"));
|
||||||
|
|
||||||
// No-arg variant emits a section per tag.
|
// 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.starts_with("= Generated Tags ="));
|
||||||
assert!(all.contains("== alpha =="));
|
assert!(all.contains("== alpha =="));
|
||||||
assert!(all.contains("== beta =="));
|
assert!(all.contains("== beta =="));
|
||||||
@@ -406,7 +426,8 @@ fn nuwiki_generate_tag_links_builds_section() {
|
|||||||
&by_tag,
|
&by_tag,
|
||||||
true,
|
true,
|
||||||
"Generated Tags",
|
"Generated Tags",
|
||||||
1
|
1,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
.is_some());
|
.is_some());
|
||||||
}
|
}
|
||||||
@@ -442,7 +463,6 @@ fn nuwiki_2html_renders_page() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -464,7 +484,6 @@ fn nuwiki_2html_browse_shares_render_and_is_advertised() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ fn render_marker_handles_each_symbol() {
|
|||||||
fn remove_done_strips_done_and_rejected_items() {
|
fn remove_done_strips_done_and_rejected_items() {
|
||||||
let src = "- [ ] todo\n- [X] done\n- [-] rejected\n- [ ] also todo\n";
|
let src = "- [ ] todo\n- [X] done\n- [-] rejected\n- [ ] also todo\n";
|
||||||
let ast = parse(src);
|
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 changes = edit.changes.expect("changes");
|
||||||
let edits = &changes[&uri()];
|
let edits = &changes[&uri()];
|
||||||
assert_eq!(edits.len(), 2, "expected 2 deletions");
|
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() {
|
fn remove_done_leaves_open_items_alone() {
|
||||||
let src = "- [ ] todo\n- [.] partial\n- [o] more\n";
|
let src = "- [ ] todo\n- [.] partial\n- [o] more\n";
|
||||||
let ast = parse(src);
|
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]
|
#[test]
|
||||||
fn remove_done_returns_none_when_no_lists() {
|
fn remove_done_returns_none_when_no_lists() {
|
||||||
let src = "= heading =\nparagraph\n";
|
let src = "= heading =\nparagraph\n";
|
||||||
let ast = parse(src);
|
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]
|
#[test]
|
||||||
@@ -96,7 +96,7 @@ fn remove_done_scoped_by_position_to_current_list() {
|
|||||||
line: 0,
|
line: 0,
|
||||||
character: 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()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 2, "both done items in the current list match");
|
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,
|
line: 0,
|
||||||
character: 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()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 1, "only the first list's done item is removed");
|
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,
|
line: 0,
|
||||||
character: 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()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 1, "the nested done child is removed");
|
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 =====
|
// ===== renumber =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -280,6 +301,6 @@ fn commands_list_includes_cluster_a() {
|
|||||||
fn one_edit_text_round_trip() {
|
fn one_edit_text_round_trip() {
|
||||||
let src = "- [X] done\n";
|
let src = "- [X] done\n";
|
||||||
let ast = parse(src);
|
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);
|
let _ = one_edit_text(edit);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ fn tag_pages_snapshot_sorts_pages_alphabetically() {
|
|||||||
fn build_tag_links_for_single_tag() {
|
fn build_tag_links_for_single_tag() {
|
||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("release".to_string(), vec!["Alpha".into(), "Beta".into()]);
|
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();
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
assert_eq!(lines[0], "= Tag: release =");
|
assert_eq!(lines[0], "= Tag: release =");
|
||||||
assert_eq!(lines[1], "- [[Alpha]]");
|
assert_eq!(lines[1], "- [[Alpha]]");
|
||||||
@@ -213,7 +213,7 @@ fn build_tag_links_for_single_tag() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_tag_links_for_missing_tag_returns_none() {
|
fn build_tag_links_for_missing_tag_returns_none() {
|
||||||
let snap = BTreeMap::new();
|
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]
|
#[test]
|
||||||
@@ -221,7 +221,7 @@ fn build_tag_links_full_index_groups_by_tag() {
|
|||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("a".to_string(), vec!["P1".into()]);
|
snap.insert("a".to_string(), vec!["P1".into()]);
|
||||||
snap.insert("b".to_string(), vec!["P2".into(), "P3".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.starts_with("= Generated Tags =\n"));
|
||||||
assert!(out.contains("== a =="));
|
assert!(out.contains("== a =="));
|
||||||
assert!(out.contains("== b =="));
|
assert!(out.contains("== b =="));
|
||||||
@@ -233,7 +233,7 @@ fn build_tag_links_full_index_groups_by_tag() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_tag_links_full_index_returns_none_when_no_tags() {
|
fn build_tag_links_full_index_returns_none_when_no_tags() {
|
||||||
let snap = BTreeMap::new();
|
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) =====
|
// ===== `nuwiki.tags.generateLinks` — tag_links_edit (in-buffer rewrite) =====
|
||||||
@@ -254,6 +254,7 @@ fn tag_links_edit_inserts_when_section_missing() {
|
|||||||
true,
|
true,
|
||||||
"Generated Tags",
|
"Generated Tags",
|
||||||
1,
|
1,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.expect("got an edit");
|
.expect("got an edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
@@ -279,6 +280,7 @@ fn tag_links_edit_replaces_existing_section() {
|
|||||||
true,
|
true,
|
||||||
"Generated Tags",
|
"Generated Tags",
|
||||||
1,
|
1,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.expect("edit");
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
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 uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("alpha".to_string(), vec!["P".into()]);
|
snap.insert("alpha".to_string(), vec!["P".into()]);
|
||||||
let edit =
|
let edit = ops::tag_links_edit(src, &ast, &uri, None, &snap, true, "Generated Tags", 1, 0)
|
||||||
ops::tag_links_edit(src, &ast, &uri, None, &snap, true, "Generated Tags", 1).expect("edit");
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("== alpha =="));
|
assert!(te.new_text.contains("== alpha =="));
|
||||||
assert!(!te.new_text.contains("[[old]]"));
|
assert!(!te.new_text.contains("[[old]]"));
|
||||||
@@ -315,7 +317,8 @@ fn tag_links_rebuild_edit_only_acts_when_index_present() {
|
|||||||
&snap,
|
&snap,
|
||||||
true,
|
true,
|
||||||
"Generated Tags",
|
"Generated Tags",
|
||||||
1
|
1,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
.is_none(),
|
.is_none(),
|
||||||
"auto_generate_tags must not insert an index into a page that lacks one"
|
"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,
|
&snap,
|
||||||
true,
|
true,
|
||||||
"Generated Tags",
|
"Generated Tags",
|
||||||
1
|
1,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
.is_some());
|
.is_some());
|
||||||
}
|
}
|
||||||
@@ -347,7 +351,8 @@ fn tag_links_edit_returns_none_for_unknown_tag() {
|
|||||||
&snap,
|
&snap,
|
||||||
true,
|
true,
|
||||||
"Generated Tags",
|
"Generated Tags",
|
||||||
1
|
1,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
.is_none());
|
.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"],
|
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
||||||
);
|
);
|
||||||
let entries = diary::list_entries(&idx);
|
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();
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
assert_eq!(lines[0], "= Diary =");
|
assert_eq!(lines[0], "= Diary =");
|
||||||
// First date in output should be the latest: 2026-05-12
|
// 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]
|
#[test]
|
||||||
fn build_index_body_empty_when_no_entries() {
|
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");
|
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() {
|
fn build_index_body_ascending_sort_lists_oldest_first() {
|
||||||
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
||||||
let entries = diary::list_entries(&idx);
|
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 older = body.find("2026-05-11").unwrap();
|
||||||
let newer = body.find("2026-05-12").unwrap();
|
let newer = body.find("2026-05-12").unwrap();
|
||||||
assert!(older < newer, "ascending sort lists oldest first");
|
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() {
|
fn build_index_body_caption_level_shifts_all_headings() {
|
||||||
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
||||||
let entries = diary::list_entries(&idx);
|
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("== Diary =="), "caption at level 2");
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("=== 2026 ==="),
|
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 idx = build_index_with_entries("/tmp/diaryClamp", &["2026-05-11"]);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
// caption_level 6 → year/month would be 7/8 but clamp to 6.
|
// 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("====== Diary ======"));
|
||||||
assert!(!body.contains("======="), "no heading exceeds level 6");
|
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]
|
#[test]
|
||||||
fn render_index_body_round_trip() {
|
fn render_index_body_round_trip() {
|
||||||
let cfg = wiki_cfg("/tmp/diaryA");
|
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(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
extras,
|
extras,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -265,7 +264,6 @@ fn render_page_html_uses_metadata_date_when_set() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -285,7 +283,6 @@ fn render_page_html_numbers_headers_when_enabled() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -308,7 +305,6 @@ fn render_page_html_numbering_skips_headers_above_start_level() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -331,7 +327,6 @@ fn render_page_html_no_header_numbering_by_default() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -350,7 +345,6 @@ fn render_page_html_root_path_for_nested_pages() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -370,7 +364,6 @@ fn render_page_html_extra_var_overrides_default() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
extras,
|
extras,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -393,7 +386,6 @@ fn render_page_html_substitutes_vimwiki_percent_template() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -420,7 +412,6 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -430,49 +421,6 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
|||||||
assert!(html.contains("href=\"posts/index.html\""), "plain: {html}");
|
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 =====
|
// ===== fallback_template + DEFAULT_CSS =====
|
||||||
|
|
||||||
#[test]
|
#[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_caption_level, 0); // upstream default
|
||||||
assert_eq!(cfg.diary_sort, "desc");
|
assert_eq!(cfg.diary_sort, "desc");
|
||||||
assert_eq!(cfg.diary_header, "Diary");
|
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_eq!(cfg.listsyms, " .oOX");
|
||||||
assert!(cfg.listsyms_propagate);
|
assert!(cfg.listsyms_propagate);
|
||||||
assert_eq!(cfg.list_margin, -1);
|
assert_eq!(cfg.list_margin, -1);
|
||||||
@@ -305,6 +309,7 @@ fn raw_wiki_parses_every_new_key() {
|
|||||||
"diary_caption_level": 2,
|
"diary_caption_level": 2,
|
||||||
"diary_sort": "asc",
|
"diary_sort": "asc",
|
||||||
"diary_header": "Journal",
|
"diary_header": "Journal",
|
||||||
|
"diary_months": ["Jan","Feb","Mar","Apr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],
|
||||||
"listsyms": "abcde",
|
"listsyms": "abcde",
|
||||||
"listsym_rejected": "✗",
|
"listsym_rejected": "✗",
|
||||||
"listsyms_propagate": false,
|
"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_caption_level, 2);
|
||||||
assert_eq!(w.diary_sort, "asc");
|
assert_eq!(w.diary_sort, "asc");
|
||||||
assert_eq!(w.diary_header, "Journal");
|
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.listsyms, "abcde");
|
||||||
assert_eq!(w.listsym_rejected, "✗");
|
assert_eq!(w.listsym_rejected, "✗");
|
||||||
assert_eq!(w.list_syms().rejected_glyph(), '✗');
|
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]
|
#[test]
|
||||||
fn diary_calendar_defaults_to_iso_weeks() {
|
fn diary_calendar_defaults_to_iso_weeks() {
|
||||||
let cfg = WikiConfig::empty();
|
let cfg = WikiConfig::empty();
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ fn build_toc_text_nests_by_level() {
|
|||||||
(2u8, "Sub".into(), "sub".into()),
|
(2u8, "Sub".into(), "sub".into()),
|
||||||
(1u8, "Other".into(), "other".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"));
|
assert!(out.starts_with("= Contents =\n"));
|
||||||
let lines: Vec<&str> = out.lines().collect();
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
assert_eq!(lines[0], "= Contents =");
|
assert_eq!(lines[0], "= Contents =");
|
||||||
@@ -301,7 +301,7 @@ fn build_toc_text_nests_by_level() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_toc_text_with_no_headings_is_just_the_heading() {
|
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");
|
assert_eq!(out, "= Contents =\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +319,8 @@ fn links_rebuild_edit_only_acts_when_section_present() {
|
|||||||
&pages,
|
&pages,
|
||||||
true,
|
true,
|
||||||
"Generated Links",
|
"Generated Links",
|
||||||
1
|
1,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
.is_none(),
|
.is_none(),
|
||||||
"must not insert a links section into a page that lacks one"
|
"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,
|
&pages,
|
||||||
true,
|
true,
|
||||||
"Generated Links",
|
"Generated Links",
|
||||||
1
|
1,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
.is_some());
|
.is_some());
|
||||||
}
|
}
|
||||||
@@ -358,15 +360,42 @@ fn find_section_range_stops_at_same_level_sibling() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn captions_honour_custom_header_and_level() {
|
fn captions_honour_custom_header_and_level() {
|
||||||
// toc_header="Table of Contents", level 2 → `== Table of Contents ==`.
|
// 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");
|
assert_eq!(out, "== Table of Contents ==\n");
|
||||||
// links_header at level 3.
|
// links_header at level 3.
|
||||||
let pages = vec!["A".to_string(), "B".to_string()];
|
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"));
|
assert!(links.starts_with("=== All Pages ===\n"));
|
||||||
// level clamps to 1..=6.
|
// level clamps to 1..=6.
|
||||||
assert!(ops::build_toc_text(&[], "X", 9).starts_with("====== X ======"));
|
assert!(ops::build_toc_text(&[], "X", 9, 0).starts_with("====== X ======"));
|
||||||
assert!(ops::build_toc_text(&[], "X", 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 =====
|
// ===== Links text generation =====
|
||||||
@@ -374,7 +403,7 @@ fn captions_honour_custom_header_and_level() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_links_text_excludes_current_page() {
|
fn build_links_text_excludes_current_page() {
|
||||||
let pages = vec!["A".to_string(), "Home".to_string(), "B".to_string()];
|
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();
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
assert_eq!(lines[0], "= Generated Links =");
|
assert_eq!(lines[0], "= Generated Links =");
|
||||||
assert!(lines.contains(&"- [[A]]"));
|
assert!(lines.contains(&"- [[A]]"));
|
||||||
@@ -385,7 +414,7 @@ fn build_links_text_excludes_current_page() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_links_text_no_excludes_includes_all() {
|
fn build_links_text_no_excludes_includes_all() {
|
||||||
let pages = vec!["A".to_string(), "B".to_string()];
|
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("[[A]]"));
|
||||||
assert!(out.contains("[[B]]"));
|
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 src = "= One =\n== Two ==\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
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 changes = edit.changes.expect("changes map");
|
||||||
let edits = &changes[&uri];
|
let edits = &changes[&uri];
|
||||||
assert_eq!(edits.len(), 1);
|
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 src = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
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 changes = edit.changes.expect("changes map");
|
||||||
let edits = &changes[&uri];
|
let edits = &changes[&uri];
|
||||||
let te = &edits[0];
|
let te = &edits[0];
|
||||||
@@ -454,7 +483,7 @@ fn toc_edit_returns_none_for_empty_doc() {
|
|||||||
let src = "";
|
let src = "";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/empty.wiki").unwrap();
|
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]
|
#[test]
|
||||||
@@ -465,14 +494,15 @@ fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
|||||||
let without = "= One =\n== Two ==\n";
|
let without = "= One =\n== Two ==\n";
|
||||||
let ast = parse(without);
|
let ast = parse(without);
|
||||||
assert!(
|
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"
|
"auto_toc should not insert a TOC where none existed"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Existing TOC → rebuild refreshes it.
|
// Existing TOC → rebuild refreshes it.
|
||||||
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||||
let ast = parse(with);
|
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];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
assert!(te.new_text.contains("[[#real|Real]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
@@ -486,8 +516,18 @@ fn links_edit_inserts_when_section_absent() {
|
|||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
let pages = vec!["A".into(), "B".into(), "Home".into()];
|
let pages = vec!["A".into(), "B".into(), "Home".into()];
|
||||||
let edit =
|
let edit = ops::links_edit(
|
||||||
ops::links_edit(src, &ast, &uri, "Home", &pages, true, "Generated Links", 1).expect("edit");
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[A]]"));
|
assert!(te.new_text.contains("[[A]]"));
|
||||||
assert!(te.new_text.contains("[[B]]"));
|
assert!(te.new_text.contains("[[B]]"));
|
||||||
@@ -500,8 +540,18 @@ fn links_edit_replaces_when_section_present() {
|
|||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
let pages = vec!["Fresh".into()];
|
let pages = vec!["Fresh".into()];
|
||||||
let edit =
|
let edit = ops::links_edit(
|
||||||
ops::links_edit(src, &ast, &uri, "Home", &pages, true, "Generated Links", 1).expect("edit");
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[Fresh]]"));
|
assert!(te.new_text.contains("[[Fresh]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
@@ -513,7 +563,7 @@ fn links_edit_returns_none_for_empty_page_list() {
|
|||||||
let src = "Hi\n";
|
let src = "Hi\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
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 =====
|
// ===== find_orphans =====
|
||||||
|
|||||||
@@ -669,6 +669,26 @@ call s:record(
|
|||||||
\ 'cmd.VimwikiListChangeLvl_accepts_range_and_args',
|
\ 'cmd.VimwikiListChangeLvl_accepts_range_and_args',
|
||||||
\ 'err=' . s:lcl_err)
|
\ 'err=' . s:lcl_err)
|
||||||
|
|
||||||
|
" ===== :VimwikiRemoveDone -bang -range =====
|
||||||
|
" Upstream is `-range`; nuwiki had `-bang` only, so `:'<,'>VimwikiRemoveDone`
|
||||||
|
" raised E481. Now `-bang -range`: a ranged invocation must parse (no E481),
|
||||||
|
" and the `!` whole-buffer form must still work. The removal is an LSP
|
||||||
|
" roundtrip (no server here), so assert only the attribute parse.
|
||||||
|
call s:set_buf(['- [X] a', '- [ ] b', '- [X] c'])
|
||||||
|
let s:rd_err = ''
|
||||||
|
try
|
||||||
|
silent 1,2VimwikiRemoveDone
|
||||||
|
silent VimwikiRemoveDone!
|
||||||
|
catch /E481/
|
||||||
|
let s:rd_err = v:exception
|
||||||
|
catch
|
||||||
|
" non-E481 (e.g. no LSP attached) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:rd_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiRemoveDone_accepts_range_and_bang',
|
||||||
|
\ 'err=' . s:rd_err)
|
||||||
|
|
||||||
" ===== :VimwikiToggleRejectedListItem -range =====
|
" ===== :VimwikiToggleRejectedListItem -range =====
|
||||||
" Was bare (range-less): `:1,3VimwikiToggleRejectedListItem` raised E481. The
|
" Was bare (range-less): `:1,3VimwikiToggleRejectedListItem` raised E481. The
|
||||||
" reject is an LSP roundtrip (no server here), so assert only the attribute
|
" reject is an LSP roundtrip (no server here), so assert only the attribute
|
||||||
|
|||||||
@@ -824,6 +824,21 @@ vim.defer_fn(function()
|
|||||||
error('range reject incomplete: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
error('range reject incomplete: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
-- `:1,2VimwikiRemoveDone` — now -range (was -bang only → E481 on a ranged
|
||||||
|
-- invocation). Only done items within the range are removed; line-2 `[X]`
|
||||||
|
-- (0-indexed) outside [0,1] survives.
|
||||||
|
tobj_case('cmd.remove_done_range', function()
|
||||||
|
set_buf({ '- [X] a', '- [ ] b', '- [X] c' })
|
||||||
|
vim.cmd('1,2VimwikiRemoveDone')
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
local ls = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||||
|
-- line-0 done item gone (2 lines left), line-2 done item still present.
|
||||||
|
return #ls == 2 and ls[#ls]:match('%[X%] c')
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('ranged remove-done wrong: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
end)
|
||||||
-- `:VimwikiTable {cols} [rows]` — now -nargs=* (was -nargs=1, which dropped
|
-- `:VimwikiTable {cols} [rows]` — now -nargs=* (was -nargs=1, which dropped
|
||||||
-- the args entirely). A 4-col table has 5 pipes per row; 3 data rows give
|
-- the args entirely). A 4-col table has 5 pipes per row; 3 data rows give
|
||||||
-- header + separator + 3 = 5 table lines.
|
-- header + separator + 3 = 5 table lines.
|
||||||
|
|||||||
+75
-38
@@ -330,14 +330,20 @@ fix site.
|
|||||||
`gln`/`glp` siblings (the keymap-range question is a separate, family-wide
|
`gln`/`glp` siblings (the keymap-range question is a separate, family-wide
|
||||||
item). Covered by `cmd.reject_list_item_range` (`test-keymaps.lua`) +
|
item). Covered by `cmd.reject_list_item_range` (`test-keymaps.lua`) +
|
||||||
`cmd.VimwikiToggleRejectedListItem_accepts_range` (`test-keymaps-vim.vim`).
|
`cmd.VimwikiToggleRejectedListItem_accepts_range` (`test-keymaps-vim.vim`).
|
||||||
- [ ] **`VimwikiRemoveDone` dropped upstream's `-range`** _(new, 2026-06-02
|
- [x] **`VimwikiRemoveDone` regained upstream's `-range`** _(new, 2026-06-02
|
||||||
re-audit)_ — upstream is `-range` (`ftplugin/vimwiki.vim:362`, "remove done
|
re-audit; fixed 2026-06-02)_ — upstream is `-range` (`ftplugin/vimwiki.vim:362`,
|
||||||
items in the selection"); nuwiki repurposed it to `-bang` only (the documented
|
"remove done items in the selection"); nuwiki had repurposed it to `-bang` only,
|
||||||
redesign at the top of this doc), so `:'<,'>VimwikiRemoveDone` raises E481. The
|
so `:'<,'>VimwikiRemoveDone` raised E481. _Fix:_ all four defs are now
|
||||||
`-bang` whole-buffer sweep is intentional; the lost `-range`/E481 is an
|
`-bang -range`, dispatched by `nuwiki#commands#remove_done` /
|
||||||
undocumented side effect. Low priority — decide whether to also accept a range
|
`commands.remove_done(bang, range, l1, l2)` (both clients): `!` → whole-buffer
|
||||||
(filter the sweep to selected lines) or just note it as part of the documented
|
sweep (unchanged); an explicit range (`<range> > 0`, e.g. `:'<,'>`) → a new
|
||||||
divergence.
|
`list_remove_done_range` sending `{range:[l1-1,l2-1]}`; otherwise the
|
||||||
|
cursor's current list. Server `remove_done_edit` gained an `Option<(u32,u32)>`
|
||||||
|
range that filters whole-doc victims by start line (`commands.rs`). Tests:
|
||||||
|
`remove_done_ranged_restricts_to_selected_lines` +
|
||||||
|
`remove_done_ranged_returns_none_when_range_has_no_done` (`commands_lists.rs`),
|
||||||
|
`cmd.remove_done_range` (`test-keymaps.lua`),
|
||||||
|
`cmd.VimwikiRemoveDone_accepts_range_and_bang` (`test-keymaps-vim.vim`).
|
||||||
- [x] **`-complete=` specs** _(new; done 2026-05-31 command-attribute pass)_ —
|
- [x] **`-complete=` specs** _(new; done 2026-05-31 command-attribute pass)_ —
|
||||||
upstream attaches command-line completion to several commands; nuwiki had
|
upstream attaches command-line completion to several commands; nuwiki had
|
||||||
none, so `<Tab>` at the `:` line never completed page / tag / colour names.
|
none, so `<Tab>` at the `:` line never completed page / tag / colour names.
|
||||||
@@ -382,36 +388,50 @@ fix site.
|
|||||||
`cmd.VimwikiSplitLink_accepts_args` (`test-keymaps-vim.vim`, no E488).
|
`cmd.VimwikiSplitLink_accepts_args` (`test-keymaps-vim.vim`, no E488).
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
- [ ] **`list_margin` — wrong semantics + wrong markdown default** _(new,
|
- [x] **`list_margin` — buffer-side parity + markdown default** _(new,
|
||||||
2026-06-02 re-audit)_ — upstream (`vars.vim:516`, doc `vimwiki.txt:2667`):
|
2026-06-02 re-audit; fixed 2026-06-02)_ — upstream (`vars.vim:516`, doc
|
||||||
left-margin **width in spaces for buffer-side generated lists** — generated
|
`vimwiki.txt:2667`): left-margin **width in spaces for buffer-side generated
|
||||||
links (`:VimwikiGenerateLinks`), the TOC, and the list-manipulation commands —
|
lists** — generated links (`:VimwikiGenerateLinks`), the TOC, the tags index,
|
||||||
default `-1` (⇒ use `'shiftwidth'`) but **`0` for markdown** wikis
|
and the diary index — default `-1` but **`0` for markdown** wikis
|
||||||
(`vars.vim:651`). nuwiki instead applies it **only** as an HTML CSS
|
(`vars.vim:651`). nuwiki had instead applied it **only** as an HTML CSS
|
||||||
`margin-left:<n>em` on the outermost `<ul>/<ol>` (`html.rs:337`) and never
|
`margin-left:<n>em`. _Fix (true-parity, chosen over keeping the HTML meaning):_
|
||||||
indents the generated buffer content; it also defaults `-1` for all syntaxes
|
dropped the HTML `em`-margin entirely (renderer `list_margin` field/method +
|
||||||
(`config.rs:417`), so markdown wikis get the wrong default. Two parts:
|
the `render_page_html` param removed) and now prepend `max(0, list_margin)`
|
||||||
(a) the documented primary effect (buffer indentation in `build_links_text` /
|
leading spaces to every generated bullet in `build_toc_text` /
|
||||||
TOC / `diary::build_index_body`) is absent; (b) derive `0` when
|
`build_links_text` / `build_tag_links_text` / `diary::build_index_body` (the
|
||||||
`syntax == "markdown"` and the key is unset. _(The HTML `em` reinterpretation
|
TOC keeps its per-depth nesting after the base margin); headings stay at
|
||||||
may have been deliberate — decide: implement buffer-side indent + markdown-0,
|
column 0. `From<RawWiki>` derives `0` for markdown wikis when the key is unset.
|
||||||
or keep the HTML meaning and document the divergence.)_
|
**Divergence (documented in the field doc):** upstream resolves a negative
|
||||||
- [ ] **`diary_months` — absent (month names hardcoded)** _(new, 2026-06-02
|
value to the buffer's `'shiftwidth'`, which the server can't observe, so
|
||||||
re-audit)_ — upstream global (`vars.vim:141`) maps month numbers → names for
|
negatives collapse to zero indent (matches nuwiki's prior default output).
|
||||||
the generated diary-index tree; nuwiki hardcodes English in `diary.rs`
|
Tests: `list_margin_indents_generated_bullets_not_headings` (`link_health.rs`),
|
||||||
`month_name()`. i18n only — low. _Fix:_ optional `diary_months` map on
|
`markdown_wiki_derives_list_margin_zero_when_unset` (`index_and_config.rs`).
|
||||||
`RawWiki`/`WikiConfig`, threaded into `build_index_body`'s month labels.
|
- [x] **`diary_months`** _(new, 2026-06-02 re-audit; fixed 2026-06-02)_ —
|
||||||
- [ ] **`markdown_header_style` — absent** _(new, 2026-06-02 re-audit)_ —
|
upstream global (`vars.vim:141`) maps month numbers → names for the generated
|
||||||
upstream global (`vars.vim:174`, default `1`): number of blank lines inserted
|
diary-index tree; nuwiki hardcoded English. _Fix:_ per-wiki `diary_months:
|
||||||
after a **generated** header in markdown syntax (generated links, TOC, tags,
|
Vec<String>` (default the 12 English names) on `WikiConfig`/`RawWiki`, threaded
|
||||||
diary index). nuwiki's `caption_line` (`commands.rs`) emits no configurable
|
into `build_index_body`; a missing/empty slot falls back to the English
|
||||||
trailing blank line. Cosmetic, markdown-only.
|
`month_name`. Tests: `build_index_body_uses_custom_diary_months` +
|
||||||
- [ ] **`diary_caption_level = -1` not representable** _(new, 2026-06-02
|
`…_falls_back_per_missing_month_slot` (`diary.rs`), config round-trip
|
||||||
re-audit)_ — upstream allows `-1` ("read no headers from diary pages",
|
(`index_and_config.rs`).
|
||||||
`vars.vim:507` `min: -1`); nuwiki types the field as `u8` (`config.rs`,
|
- [ ] **`markdown_header_style`** _(new, 2026-06-02 re-audit; deferred)_ —
|
||||||
`RawWiki`), so a `-1` config fails deserialization and silently falls back to
|
upstream global (`vars.vim:174`, default `1`): blank lines after a **generated**
|
||||||
`0`. The *default* (`0`) is correct — only the `-1` value is lost. _Fix:_ widen
|
header in markdown syntax. **Intentionally deferred:** nuwiki's generators
|
||||||
to `i8` (and honour `-1` if/when caption-from-page-headers lands). Edge case.
|
emit vimwiki syntax only (`caption_line` always writes `= H =`, never `# H`),
|
||||||
|
so there's no markdown header to attach the style to yet. This is part of a
|
||||||
|
broader "markdown generated-content" gap — markdown-style heading generation
|
||||||
|
comes first, then this blank-line control on top. See the divergence note
|
||||||
|
below.
|
||||||
|
- [x] **`diary_caption_level = -1` now representable** _(new, 2026-06-02
|
||||||
|
re-audit; fixed 2026-06-02)_ — upstream allows `-1` (`vars.vim:507` `min: -1`);
|
||||||
|
nuwiki had typed the field as `u8`, so a `-1` config failed deserialization
|
||||||
|
and silently fell back to `0`. _Fix:_ widened to `i8` across
|
||||||
|
`WikiConfig`/`RawWiki`/`WikiDefaults`; `build_index_body` clamps `< 0` to a
|
||||||
|
base tree level of `0`. (nuwiki builds the index tree from dates, so upstream's
|
||||||
|
`-1` = "no per-page captions" semantic doesn't apply — it just clamps; noted in
|
||||||
|
the field doc.) Tests: `diary_caption_level_accepts_negative`
|
||||||
|
(`index_and_config.rs`), `build_index_body_clamps_negative_caption_level_to_zero`
|
||||||
|
(`diary.rs`).
|
||||||
- [ ] `auto_header` — auto H1-from-filename on new page (server-side).
|
- [ ] `auto_header` — auto H1-from-filename on new page (server-side).
|
||||||
- [ ] `create_link` — toggle to suppress link-target creation.
|
- [ ] `create_link` — toggle to suppress link-target creation.
|
||||||
- [ ] `dir_link` — index file to open when following a directory link.
|
- [ ] `dir_link` — index file to open when following a directory link.
|
||||||
@@ -521,6 +541,16 @@ audits don't re-flag them.
|
|||||||
→ `diary` vs upstream `diary/` (trailing slash only — paths are joined
|
→ `diary` vs upstream `diary/` (trailing slash only — paths are joined
|
||||||
identically, so behaviour matches). Listed so future audits stop re-flagging
|
identically, so behaviour matches). Listed so future audits stop re-flagging
|
||||||
them; users can still set any of these explicitly.
|
them; users can still set any of these explicitly.
|
||||||
|
- **Generated-content syntax is vimwiki-only (for now)** _(noted 2026-06-02
|
||||||
|
re-audit)_ — the buffer-side generators (TOC, generated links, tags index,
|
||||||
|
diary index) always emit vimwiki markup (`= H =` headings via `caption_line`),
|
||||||
|
regardless of the wiki's `syntax`. Consequently the markdown-specific
|
||||||
|
`markdown_header_style` (blank lines after a generated *markdown* header) has
|
||||||
|
nothing to attach to and is deferred. This is a deliberate scoping choice, not
|
||||||
|
an oversight: markdown generated-content (markdown-style headings, then
|
||||||
|
`markdown_header_style`, `markdown_link_ext` in generated links) is planned as
|
||||||
|
a single later effort. Until then, markdown wikis get vimwiki-syntax generated
|
||||||
|
sections.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -558,3 +588,10 @@ audits don't re-flag them.
|
|||||||
default output-location derivations (`path_html`/`template_path`/
|
default output-location derivations (`path_html`/`template_path`/
|
||||||
`diary_rel_path`) recorded as intentional divergences. No previously-closed
|
`diary_rel_path`) recorded as intentional divergences. No previously-closed
|
||||||
item regressed.
|
item regressed.
|
||||||
|
- Fixed the fourth-pass findings (2026-06-02), in order: `VimwikiRemoveDone`
|
||||||
|
`-range`; `list_margin` reworked to upstream's buffer-side meaning (HTML
|
||||||
|
`em`-margin dropped) + markdown-`0` default; `diary_months`;
|
||||||
|
`diary_caption_level` widened to `i8`. `markdown_header_style` deferred as part
|
||||||
|
of the documented "generated-content is vimwiki-only" divergence. All five
|
||||||
|
P1-blocking sections remain clear; only niche P3 items (and the deferred
|
||||||
|
markdown work) are left.
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ if !has('nvim')
|
|||||||
command! -buffer -range -nargs=1 VimwikiChangeSymbolTo call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
command! -buffer -range -nargs=1 VimwikiChangeSymbolTo call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -range -nargs=1 VimwikiListChangeSymbolI call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
command! -buffer -range -nargs=1 VimwikiListChangeSymbolI call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo call nuwiki#commands#list_change_symbol(<q-args>, 1)
|
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo call nuwiki#commands#list_change_symbol(<q-args>, 1)
|
||||||
command! -buffer -bang VimwikiRemoveDone if <bang>0 | call nuwiki#commands#list_remove_done_all() | else | call nuwiki#commands#list_remove_done() | endif
|
command! -buffer -bang -range VimwikiRemoveDone call nuwiki#commands#remove_done(<bang>0, <range>, <line1>, <line2>)
|
||||||
command! -buffer -range VimwikiRemoveSingleCB call nuwiki#commands#list_remove_checkbox()
|
command! -buffer -range VimwikiRemoveSingleCB call nuwiki#commands#list_remove_checkbox()
|
||||||
command! -buffer VimwikiRemoveCBInList call nuwiki#commands#list_remove_checkbox_in_list()
|
command! -buffer VimwikiRemoveCBInList call nuwiki#commands#list_remove_checkbox_in_list()
|
||||||
command! -buffer -range -nargs=+ VimwikiListChangeLvl call nuwiki#commands#list_change_lvl(<line1>, <line2>, <f-args>)
|
command! -buffer -range -nargs=+ VimwikiListChangeLvl call nuwiki#commands#list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
@@ -170,7 +170,7 @@ if !has('nvim')
|
|||||||
command! -buffer Nuwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
command! -buffer Nuwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
||||||
command! -buffer -bang NuwikiAll2HTML if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
command! -buffer -bang NuwikiAll2HTML if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
||||||
|
|
||||||
command! -buffer -bang NuwikiRemoveDone if <bang>0 | call nuwiki#commands#list_remove_done_all() | else | call nuwiki#commands#list_remove_done() | endif
|
command! -buffer -bang -range NuwikiRemoveDone call nuwiki#commands#remove_done(<bang>0, <range>, <line1>, <line2>)
|
||||||
command! -buffer -range NuwikiRemoveCheckbox call nuwiki#commands#list_remove_checkbox()
|
command! -buffer -range NuwikiRemoveCheckbox call nuwiki#commands#list_remove_checkbox()
|
||||||
command! -buffer NuwikiRemoveCheckboxInList call nuwiki#commands#list_remove_checkbox_in_list()
|
command! -buffer NuwikiRemoveCheckboxInList call nuwiki#commands#list_remove_checkbox_in_list()
|
||||||
command! -buffer -range -nargs=+ NuwikiListChangeLvl call nuwiki#commands#list_change_lvl(<line1>, <line2>, <f-args>)
|
command! -buffer -range -nargs=+ NuwikiListChangeLvl call nuwiki#commands#list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
@@ -433,7 +433,7 @@ command! -buffer -range VimwikiDecrementListItem lua require('nuwiki.c
|
|||||||
command! -buffer -range -nargs=1 VimwikiChangeSymbolTo lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
command! -buffer -range -nargs=1 VimwikiChangeSymbolTo lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -range -nargs=1 VimwikiListChangeSymbolI lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
command! -buffer -range -nargs=1 VimwikiListChangeSymbolI lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo lua require('nuwiki.commands').list_change_symbol(<q-args>, true)
|
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo lua require('nuwiki.commands').list_change_symbol(<q-args>, true)
|
||||||
command! -buffer -bang VimwikiRemoveDone lua require('nuwiki.commands')[('<bang>' == '!') and 'list_remove_done_all' or 'list_remove_done']()
|
command! -buffer -bang -range VimwikiRemoveDone lua require('nuwiki.commands').remove_done('<bang>' == '!', <range>, <line1>, <line2>)
|
||||||
command! -buffer -range VimwikiRemoveSingleCB lua require('nuwiki.commands').list_remove_checkbox()
|
command! -buffer -range VimwikiRemoveSingleCB lua require('nuwiki.commands').list_remove_checkbox()
|
||||||
command! -buffer VimwikiRemoveCBInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
command! -buffer VimwikiRemoveCBInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
||||||
command! -buffer -range -nargs=+ VimwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl(<line1>, <line2>, <f-args>)
|
command! -buffer -range -nargs=+ VimwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
@@ -519,7 +519,7 @@ command! -buffer -nargs=* NuwikiSplitLink split | lua require('nuwiki.c
|
|||||||
command! -buffer -nargs=* NuwikiVSplitLink vsplit | lua require('nuwiki.commands').follow_link_or_create()
|
command! -buffer -nargs=* NuwikiVSplitLink vsplit | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer NuwikiTabnewLink tabnew | lua require('nuwiki.commands').follow_link_or_create()
|
command! -buffer NuwikiTabnewLink tabnew | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer NuwikiTabDropLink lua require('nuwiki.commands').follow_link_drop()
|
command! -buffer NuwikiTabDropLink lua require('nuwiki.commands').follow_link_drop()
|
||||||
command! -buffer -bang NuwikiRemoveDone lua require('nuwiki.commands')[('<bang>' == '!') and 'list_remove_done_all' or 'list_remove_done']()
|
command! -buffer -bang -range NuwikiRemoveDone lua require('nuwiki.commands').remove_done('<bang>' == '!', <range>, <line1>, <line2>)
|
||||||
command! -buffer -range NuwikiRemoveCheckbox lua require('nuwiki.commands').list_remove_checkbox()
|
command! -buffer -range NuwikiRemoveCheckbox lua require('nuwiki.commands').list_remove_checkbox()
|
||||||
command! -buffer NuwikiRemoveCheckboxInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
command! -buffer NuwikiRemoveCheckboxInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
||||||
command! -buffer -range -nargs=+ NuwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl(<line1>, <line2>, <f-args>)
|
command! -buffer -range -nargs=+ NuwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
|
|||||||
@@ -566,6 +566,25 @@ function M.list_remove_done_all()
|
|||||||
exec('nuwiki.list.removeDone', uri_args())
|
exec('nuwiki.list.removeDone', uri_args())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- `:'<,'>VimwikiRemoveDone` — remove done items only within the given line
|
||||||
|
-- range (1-indexed, inclusive; converted to the server's 0-indexed range).
|
||||||
|
function M.list_remove_done_range(l1, l2)
|
||||||
|
exec('nuwiki.list.removeDone', { { uri = buf_uri(), range = { l1 - 1, l2 - 1 } } })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `:[range]VimwikiRemoveDone[!]` dispatcher. `!` → whole buffer; an explicit
|
||||||
|
-- range (`range > 0`, e.g. `:'<,'>`) → that line span; otherwise the current
|
||||||
|
-- list under the cursor. Mirrors upstream's `-bang -range` form.
|
||||||
|
function M.remove_done(bang, range, l1, l2)
|
||||||
|
if bang then
|
||||||
|
M.list_remove_done_all()
|
||||||
|
elseif range and range > 0 then
|
||||||
|
M.list_remove_done_range(l1, l2)
|
||||||
|
else
|
||||||
|
M.list_remove_done()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
-- `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
||||||
function M.list_remove_checkbox()
|
function M.list_remove_checkbox()
|
||||||
exec('nuwiki.list.removeCheckbox', pos_args())
|
exec('nuwiki.list.removeCheckbox', pos_args())
|
||||||
|
|||||||
Reference in New Issue
Block a user