942dbe2aa8
Two-pass, hand-rolled lexer per SPEC.md §6.6. Block pass walks the
source line by line; recognised constructs emit structural tokens, and
text-bearing line content is fed to the inline pass. Multi-line fences
(`{{{ }}}`, `{{$ }}$`, `%%+ +%%`) flip a `BlockMode` so subsequent
lines are captured raw until the matching closer.
VimwikiToken covers the full §9 feature checklist:
- headings (1–6, centered), horizontal rule, blockquotes (`>` and
4-space), all list-marker variants, checkboxes, definition `::`,
tables (separators, header row, col/row span)
- preformatted fences (with language + key=val attrs), math blocks
(with `%env%`), single- and multi-line comments, all four
page placeholders (%title / %nohtml / %template / %date)
- inline: bold/italic/strike/super/sub delimiters, inline code, inline
math, all six keywords (word-bounded), wikilinks (with description
separator), transclusions (with attr separator), raw URLs across
http(s)/ftp/mailto/file schemes — trailing sentence punctuation
stripped from URL spans
Spans are byte-accurate per SPEC §6.5 (0-indexed line + byte column +
absolute byte offset). The lexer is permissive: every delimiter is
emitted, the parser will pair them and fall back to literal text on
mismatches.
Tests (41 cases) cover one example per construct in §9, plus
word-boundary keyword detection, list-vs-bold disambiguation,
indent-vs-list disambiguation, and a span-correctness check.
README now carries a per-phase status table; SPEC top-line status
advanced to Phase 3 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
3.1 KiB
Rust
118 lines
3.1 KiB
Rust
//! Syntax plugin interface.
|
|
//!
|
|
//! Each syntax (vimwiki today, markdown later) implements the same shape:
|
|
//! a `Lexer` produces a `TokenStream`, a `Parser` consumes it and produces a
|
|
//! `DocumentNode`. A `SyntaxPlugin` is the type-erased facade that the LSP
|
|
//! layer holds in a `SyntaxRegistry`.
|
|
//!
|
|
//! See SPEC.md §6.3 (interface) and §6.6 (lexer strategy).
|
|
|
|
pub mod registry;
|
|
pub mod vimwiki;
|
|
|
|
pub use registry::SyntaxRegistry;
|
|
|
|
use crate::ast::DocumentNode;
|
|
|
|
/// Eager token buffer produced by a `Lexer`. The newtype keeps the door
|
|
/// open to a lazy/streaming variant later (SPEC.md §6.6) without breaking
|
|
/// callers.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct TokenStream<T> {
|
|
tokens: Vec<T>,
|
|
}
|
|
|
|
impl<T> TokenStream<T> {
|
|
pub fn new() -> Self {
|
|
Self { tokens: Vec::new() }
|
|
}
|
|
|
|
pub fn from_vec(tokens: Vec<T>) -> Self {
|
|
Self { tokens }
|
|
}
|
|
|
|
pub fn into_vec(self) -> Vec<T> {
|
|
self.tokens
|
|
}
|
|
|
|
pub fn as_slice(&self) -> &[T] {
|
|
&self.tokens
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.tokens.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.tokens.is_empty()
|
|
}
|
|
|
|
pub fn iter(&self) -> std::slice::Iter<'_, T> {
|
|
self.tokens.iter()
|
|
}
|
|
|
|
pub fn push(&mut self, token: T) {
|
|
self.tokens.push(token);
|
|
}
|
|
}
|
|
|
|
impl<T> From<Vec<T>> for TokenStream<T> {
|
|
fn from(tokens: Vec<T>) -> Self {
|
|
Self::from_vec(tokens)
|
|
}
|
|
}
|
|
|
|
impl<T> IntoIterator for TokenStream<T> {
|
|
type Item = T;
|
|
type IntoIter = std::vec::IntoIter<T>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.tokens.into_iter()
|
|
}
|
|
}
|
|
|
|
impl<'a, T> IntoIterator for &'a TokenStream<T> {
|
|
type Item = &'a T;
|
|
type IntoIter = std::slice::Iter<'a, T>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.tokens.iter()
|
|
}
|
|
}
|
|
|
|
/// Lexer half of a syntax plugin. Operates on raw source text and emits a
|
|
/// `TokenStream` whose token type is syntax-specific (e.g. `VimwikiToken`).
|
|
pub trait Lexer {
|
|
type Token;
|
|
|
|
fn lex(&self, text: &str) -> TokenStream<Self::Token>;
|
|
}
|
|
|
|
/// Parser half of a syntax plugin. Consumes a `TokenStream` and produces a
|
|
/// `DocumentNode`. Per SPEC.md §6.7 parsing is resilient — malformed input
|
|
/// becomes `BlockNode::Error(ErrorNode)`, never a hard failure.
|
|
pub trait Parser {
|
|
type Token;
|
|
|
|
fn parse(&self, tokens: TokenStream<Self::Token>) -> DocumentNode;
|
|
}
|
|
|
|
/// Type-erased syntax plugin held by the `SyntaxRegistry`.
|
|
///
|
|
/// Implementations typically own a `Lexer` and a `Parser` and chain them
|
|
/// inside `parse`. The token type is deliberately hidden so the registry can
|
|
/// hold heterogeneous plugins behind a single trait object.
|
|
///
|
|
/// `Send + Sync` is required — the LSP server stores plugins behind an `Arc`
|
|
/// and shares them across request handler tasks (SPEC.md §6.3).
|
|
pub trait SyntaxPlugin: Send + Sync {
|
|
fn id(&self) -> &str;
|
|
fn display_name(&self) -> &str;
|
|
|
|
/// File extensions associated with this syntax. Each entry includes the
|
|
/// leading dot, e.g. `[".wiki"]`. See SPEC.md §6.3.
|
|
fn file_extensions(&self) -> &[&str];
|
|
|
|
fn parse(&self, text: &str) -> DocumentNode;
|
|
}
|