feat(core): make checkbox lexing honor a configurable listsyms palette

Add a ListSyms type (vimwiki's g:vimwiki_listsyms) and thread it through
the lexer so checkbox glyphs are recognized from the configured palette
rather than the hardcoded ' .oOX'. Glyphs map to the existing five-bucket
CheckboxState, so the HTML renderer and AST consumers are unchanged.
VimwikiSyntax::parse_with_listsyms opts into a custom palette; the trait
parse() keeps the default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 23:32:07 -03:00
parent d22c81ea4f
commit 85094d6e3b
5 changed files with 249 additions and 18 deletions
+29 -18
View File
@@ -21,6 +21,7 @@
use std::collections::HashMap;
use crate::ast::{CheckboxState, Keyword, ListSymbol, Position, Span};
use crate::listsyms::ListSyms;
use crate::syntax::{Lexer, TokenStream};
/// A single vimwiki token. The `kind` carries the variant, `span` carries
@@ -134,11 +135,20 @@ pub enum VimwikiTokenKind {
}
#[derive(Debug, Default, Clone)]
pub struct VimwikiLexer;
pub struct VimwikiLexer {
listsyms: ListSyms,
}
impl VimwikiLexer {
pub fn new() -> Self {
Self
Self::default()
}
/// Recognise checkbox glyphs from a custom palette
/// (vimwiki's `g:vimwiki_listsyms`) instead of the default `" .oOX"`.
pub fn with_listsyms(mut self, listsyms: ListSyms) -> Self {
self.listsyms = listsyms;
self
}
}
@@ -146,7 +156,7 @@ impl Lexer for VimwikiLexer {
type Token = VimwikiToken;
fn lex(&self, text: &str) -> TokenStream<VimwikiToken> {
let mut state = LexState::new(text);
let mut state = LexState::new(text, &self.listsyms);
state.run();
TokenStream::from_vec(state.tokens)
}
@@ -170,16 +180,18 @@ struct LexState<'src> {
line: u32,
tokens: Vec<VimwikiToken>,
mode: BlockMode,
listsyms: &'src ListSyms,
}
impl<'src> LexState<'src> {
fn new(src: &'src str) -> Self {
fn new(src: &'src str, listsyms: &'src ListSyms) -> Self {
Self {
src,
line_start_offset: 0,
line: 0,
tokens: Vec::new(),
mode: BlockMode::Normal,
listsyms,
}
}
@@ -704,7 +716,7 @@ impl<'src> LexState<'src> {
// Optional checkbox `[ ]` / `[.]` / `[o]` / `[O]` / `[X]` / `[-]`,
// followed by a space.
if let Some((cb, cb_len)) = checkbox_at(after) {
if let Some((cb, cb_len)) = checkbox_at(after, self.listsyms) {
let cb_start = indent + cursor as u32;
let cb_end = cb_start + cb_len as u32;
self.emit(VimwikiTokenKind::Checkbox(cb), cb_start, cb_end);
@@ -1230,19 +1242,18 @@ fn parse_sep_alignment(cell: &str) -> crate::ast::TableAlign {
}
}
fn checkbox_at(s: &str) -> Option<(CheckboxState, usize)> {
let b = s.as_bytes();
if b.len() < 3 || b[0] != b'[' || b[2] != b']' {
/// Recognise a `[g]` checkbox at the start of `s`, where `g` is a single
/// glyph drawn from `listsyms` (or the rejected marker). Returns the bucketed
/// state and the byte length consumed (including the brackets).
fn checkbox_at(s: &str, listsyms: &ListSyms) -> Option<(CheckboxState, usize)> {
let rest = s.strip_prefix('[')?;
let mut chars = rest.char_indices();
let (_, glyph) = chars.next()?;
let (close_off, ']') = chars.next()? else {
return None;
}
let cb = match b[1] {
b' ' => CheckboxState::Empty,
b'.' => CheckboxState::Quarter,
b'o' => CheckboxState::Half,
b'O' => CheckboxState::ThreeQuarters,
b'X' => CheckboxState::Done,
b'-' => CheckboxState::Rejected,
_ => return None,
};
Some((cb, 3))
let state = listsyms.state_of(glyph)?;
// `[` + glyph + `]`: 1 + glyph.len_utf8() + 1, i.e. the close bracket
// offset within `rest` plus one for the opening bracket and one for `]`.
Some((state, 1 + close_off + 1))
}
@@ -7,6 +7,7 @@ pub use lexer::{VimwikiLexer, VimwikiToken, VimwikiTokenKind};
pub use parser::VimwikiParser;
use crate::ast::DocumentNode;
use crate::listsyms::ListSyms;
use crate::syntax::{Lexer, Parser, SyntaxPlugin};
/// SyntaxPlugin facade: chains `VimwikiLexer` + `VimwikiParser` so the
@@ -18,6 +19,15 @@ impl VimwikiSyntax {
pub fn new() -> Self {
Self
}
/// Parse with a custom checkbox palette (vimwiki's `g:vimwiki_listsyms`).
/// The trait-level [`SyntaxPlugin::parse`] uses [`ListSyms::default`].
pub fn parse_with_listsyms(&self, text: &str, listsyms: &ListSyms) -> DocumentNode {
let tokens = VimwikiLexer::new()
.with_listsyms(listsyms.clone())
.lex(text);
VimwikiParser::new().parse(tokens)
}
}
impl SyntaxPlugin for VimwikiSyntax {