feat(config): make listsym_rejected configurable (P2)
The rejected/cancelled checkbox glyph was a hardcoded `const REJECTED = '-'`. ListSyms now carries a `rejected` field (new_with_rejected); a per-wiki `listsym_rejected` key (default `-`) is added to WikiConfig, and WikiConfig::list_syms() builds the palette with it. Routed the three config-driven ListSyms construction sites (parse path in lib.rs + the toggle/cycle commands) through list_syms(), so a custom rejected glyph both lexes and round-trips. Tests: listsyms::custom_rejected_glyph_is_honoured + config round-trip. README/doc/lua config comment updated. fmt clean; 0 Rust test failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,32 +10,47 @@
|
||||
|
||||
use crate::ast::CheckboxState;
|
||||
|
||||
/// Default rejected/cancelled marker (vimwiki's `listsym_rejected`).
|
||||
const REJECTED: char = '-';
|
||||
|
||||
/// An ordered checkbox progress palette. Always holds at least two glyphs.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListSyms {
|
||||
progression: Vec<char>,
|
||||
rejected: char,
|
||||
}
|
||||
|
||||
impl Default for ListSyms {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
progression: " .oOX".chars().collect(),
|
||||
rejected: REJECTED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListSyms {
|
||||
/// Build a palette from a glyph string. Falls back to the default when
|
||||
/// fewer than two glyphs are supplied (a one-symbol palette can't express
|
||||
/// both "empty" and "done").
|
||||
/// Build a palette from a glyph string, with the default rejected marker.
|
||||
/// Falls back to the default when fewer than two glyphs are supplied (a
|
||||
/// one-symbol palette can't express both "empty" and "done").
|
||||
pub fn new(s: &str) -> Self {
|
||||
Self::new_with_rejected(s, REJECTED)
|
||||
}
|
||||
|
||||
/// Like [`new`](Self::new) but with a custom rejected/cancelled glyph
|
||||
/// (vimwiki's `listsym_rejected`).
|
||||
pub fn new_with_rejected(s: &str, rejected: char) -> Self {
|
||||
let progression: Vec<char> = s.chars().collect();
|
||||
if progression.len() < 2 {
|
||||
Self::default()
|
||||
Self {
|
||||
rejected,
|
||||
..Self::default()
|
||||
}
|
||||
} else {
|
||||
Self { progression }
|
||||
Self {
|
||||
progression,
|
||||
rejected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +71,7 @@ impl ListSyms {
|
||||
}
|
||||
|
||||
pub fn rejected_glyph(&self) -> char {
|
||||
REJECTED
|
||||
self.rejected
|
||||
}
|
||||
|
||||
pub fn glyph_at(&self, index: usize) -> char {
|
||||
@@ -76,7 +91,7 @@ impl ListSyms {
|
||||
/// Normalised five-bucket [`CheckboxState`] for a glyph, or `None` when the
|
||||
/// glyph is not part of this palette (and is not the rejected marker).
|
||||
pub fn state_of(&self, c: char) -> Option<CheckboxState> {
|
||||
if c == REJECTED {
|
||||
if c == self.rejected {
|
||||
return Some(CheckboxState::Rejected);
|
||||
}
|
||||
let index = self.index_of(c)?;
|
||||
@@ -96,7 +111,7 @@ impl ListSyms {
|
||||
/// `ceil` rule, matching the stock five-symbol behaviour.
|
||||
pub fn glyph_for_rate(&self, rate: i32) -> char {
|
||||
if rate < 0 {
|
||||
return REJECTED;
|
||||
return self.rejected;
|
||||
}
|
||||
if rate <= 0 {
|
||||
return self.empty_glyph();
|
||||
@@ -157,6 +172,19 @@ mod tests {
|
||||
assert_eq!(ListSyms::new("x"), ListSyms::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_rejected_glyph_is_honoured() {
|
||||
// `listsym_rejected = '✗'`: that glyph lexes/snaps as Rejected and the
|
||||
// default `-` no longer does.
|
||||
let s = ListSyms::new_with_rejected(" .oOX", '✗');
|
||||
assert_eq!(s.rejected_glyph(), '✗');
|
||||
assert_eq!(s.state_of('✗'), Some(CheckboxState::Rejected));
|
||||
assert_eq!(s.glyph_for_rate(-1), '✗');
|
||||
assert_eq!(s.state_of('-'), None); // no longer the rejected marker
|
||||
// A short palette still keeps the custom rejected glyph.
|
||||
assert_eq!(ListSyms::new_with_rejected("x", '✗').rejected_glyph(), '✗');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_palette_buckets_to_nearest_state() {
|
||||
// 3 glyphs → rates 0 / 50 / 100.
|
||||
|
||||
@@ -250,7 +250,7 @@ fn list_checkbox(
|
||||
let wiki = backend.wiki_for_uri(&p.uri);
|
||||
let syms = wiki
|
||||
.as_ref()
|
||||
.map(|w| nuwiki_core::listsyms::ListSyms::new(&w.config.listsyms))
|
||||
.map(|w| w.config.list_syms())
|
||||
.unwrap_or_default();
|
||||
let propagate = wiki.map(|w| w.config.listsyms_propagate).unwrap_or(true);
|
||||
Ok(ops::checkbox_edit(
|
||||
@@ -1184,10 +1184,8 @@ fn export_all(backend: &Backend, args: Vec<Value>, force: bool) -> Result<Option
|
||||
let Ok(text) = std::fs::read_to_string(path) else {
|
||||
continue;
|
||||
};
|
||||
let ast = nuwiki_core::syntax::vimwiki::VimwikiSyntax::new().parse_with_listsyms(
|
||||
&text,
|
||||
&nuwiki_core::listsyms::ListSyms::new(&cfg.listsyms),
|
||||
);
|
||||
let ast = nuwiki_core::syntax::vimwiki::VimwikiSyntax::new()
|
||||
.parse_with_listsyms(&text, &cfg.list_syms());
|
||||
(text, ast)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,6 +61,9 @@ pub struct WikiConfig {
|
||||
/// glyphs and the toggle/cycle commands walk them. Any length ≥ 2 is
|
||||
/// honoured. Default: `" .oOX"`.
|
||||
pub listsyms: String,
|
||||
/// Vimwiki's `g:vimwiki_listsym_rejected` — the glyph for a cancelled
|
||||
/// checkbox (`[-]` by default). First character is used.
|
||||
pub listsym_rejected: String,
|
||||
/// When toggling a parent list item, also cascade to descendants.
|
||||
pub listsyms_propagate: bool,
|
||||
/// `-1` keeps tight lists; positive integers indent list items by
|
||||
@@ -188,6 +191,7 @@ impl WikiConfig {
|
||||
diary_sort: d.diary_sort,
|
||||
diary_header: d.diary_header,
|
||||
listsyms: d.listsyms,
|
||||
listsym_rejected: d.listsym_rejected,
|
||||
listsyms_propagate: d.listsyms_propagate,
|
||||
list_margin: d.list_margin,
|
||||
links_space_char: d.links_space_char,
|
||||
@@ -217,6 +221,7 @@ impl WikiConfig {
|
||||
diary_sort: d.diary_sort,
|
||||
diary_header: d.diary_header,
|
||||
listsyms: d.listsyms,
|
||||
listsym_rejected: d.listsym_rejected,
|
||||
listsyms_propagate: d.listsyms_propagate,
|
||||
list_margin: d.list_margin,
|
||||
links_space_char: d.links_space_char,
|
||||
@@ -258,6 +263,13 @@ impl WikiConfig {
|
||||
nuwiki_core::date::DiaryFrequency::parse(&self.diary_frequency)
|
||||
}
|
||||
|
||||
/// The checkbox palette for this wiki — the `listsyms` progression plus
|
||||
/// the configured `listsym_rejected` glyph (default `-`).
|
||||
pub fn list_syms(&self) -> nuwiki_core::listsyms::ListSyms {
|
||||
let rejected = self.listsym_rejected.chars().next().unwrap_or('-');
|
||||
nuwiki_core::listsyms::ListSyms::new_with_rejected(&self.listsyms, rejected)
|
||||
}
|
||||
|
||||
/// The diary navigation policy (frequency + weekly naming/week-start),
|
||||
/// used by the diary commands to compute today / next / prev.
|
||||
pub fn diary_calendar(&self) -> nuwiki_core::date::DiaryCalendar {
|
||||
@@ -306,6 +318,10 @@ fn default_listsyms() -> String {
|
||||
" .oOX".to_string()
|
||||
}
|
||||
|
||||
fn default_listsym_rejected() -> String {
|
||||
"-".to_string()
|
||||
}
|
||||
|
||||
fn default_links_space_char() -> String {
|
||||
" ".to_string()
|
||||
}
|
||||
@@ -327,6 +343,7 @@ fn wiki_defaults() -> WikiDefaults {
|
||||
diary_sort: default_diary_sort(),
|
||||
diary_header: default_diary_header(),
|
||||
listsyms: default_listsyms(),
|
||||
listsym_rejected: default_listsym_rejected(),
|
||||
listsyms_propagate: true,
|
||||
list_margin: -1,
|
||||
links_space_char: default_links_space_char(),
|
||||
@@ -345,6 +362,7 @@ struct WikiDefaults {
|
||||
diary_sort: String,
|
||||
diary_header: String,
|
||||
listsyms: String,
|
||||
listsym_rejected: String,
|
||||
listsyms_propagate: bool,
|
||||
list_margin: i32,
|
||||
links_space_char: String,
|
||||
@@ -565,6 +583,8 @@ struct RawWiki {
|
||||
diary_header: Option<String>,
|
||||
#[serde(default)]
|
||||
listsyms: Option<String>,
|
||||
#[serde(default)]
|
||||
listsym_rejected: Option<String>,
|
||||
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||
listsyms_propagate: Option<bool>,
|
||||
#[serde(default)]
|
||||
@@ -629,6 +649,7 @@ impl From<RawWiki> for WikiConfig {
|
||||
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
||||
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
||||
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
||||
listsym_rejected: r.listsym_rejected.unwrap_or(d.listsym_rejected),
|
||||
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
||||
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
||||
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
||||
|
||||
@@ -193,7 +193,7 @@ impl Backend {
|
||||
fn parse_for_uri(&self, uri: &Url, text: &str) -> nuwiki_core::ast::DocumentNode {
|
||||
let syms = self
|
||||
.wiki_for_uri(uri)
|
||||
.map(|w| nuwiki_core::listsyms::ListSyms::new(&w.config.listsyms))
|
||||
.map(|w| w.config.list_syms())
|
||||
.unwrap_or_default();
|
||||
nuwiki_core::syntax::vimwiki::VimwikiSyntax::new().parse_with_listsyms(text, &syms)
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ fn raw_wiki_parses_every_new_key() {
|
||||
"diary_sort": "asc",
|
||||
"diary_header": "Journal",
|
||||
"listsyms": "abcde",
|
||||
"listsym_rejected": "✗",
|
||||
"listsyms_propagate": false,
|
||||
"list_margin": 2,
|
||||
"links_space_char": "_",
|
||||
@@ -305,6 +306,8 @@ fn raw_wiki_parses_every_new_key() {
|
||||
assert_eq!(w.diary_sort, "asc");
|
||||
assert_eq!(w.diary_header, "Journal");
|
||||
assert_eq!(w.listsyms, "abcde");
|
||||
assert_eq!(w.listsym_rejected, "✗");
|
||||
assert_eq!(w.list_syms().rejected_glyph(), '✗');
|
||||
assert!(!w.listsyms_propagate);
|
||||
assert_eq!(w.list_margin, 2);
|
||||
assert_eq!(w.links_space_char, "_");
|
||||
|
||||
Reference in New Issue
Block a user