phase 1: core AST, spans, and Visitor
Define every node type in SPEC.md §6.4 — Document, 11 block variants, 15 inline variants (including the 4 link-related ones), plus ListItem / DefinitionItem / TableRow / TableCell, ListSymbol, CheckboxState, Keyword, LinkTarget, LinkKind. Every node carries a `Span` (line + column + byte offset, 0-indexed) per §6.5. Visitor: open-recursion trait with default `visit_*` bodies that delegate to `walk_*` free functions. Override at any granularity; call `walk_*` from your override to keep descending. P4 resolved by defining the trait now so Phase 5 (renderer) and Phase 7 (semantic tokens) can plug in without refactoring traversal. P3 resolved: AST string fields are owned `String`. Cow / Arc<str> can be revisited if the parser grows a zero-copy mode. Tests: visitor smoke test builds a heading + paragraph + list + table by hand and asserts exact visit counts (4 blocks, 12 inlines, 9 text leaves, 1 bold, 1 external link, 1 transclusion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# nuwiki — Project Specification
|
# nuwiki — Project Specification
|
||||||
|
|
||||||
> Last updated: 2026-05-08
|
> Last updated: 2026-05-10
|
||||||
> Status: In design — Phase 0 not yet started
|
> Status: Phase 1 (Core AST) complete — moving to Phase 2 (Syntax Plugin Interface)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -586,8 +586,8 @@ These decisions are required before or during the phase indicated.
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| ~~P1~~ | ~~**License**~~ | ~~Phase 0~~ | ✅ **Dual MIT/Apache-2.0** | |
|
| ~~P1~~ | ~~**License**~~ | ~~Phase 0~~ | ✅ **Dual MIT/Apache-2.0** | |
|
||||||
| ~~P2~~ | ~~**MSRV**~~ | ~~Phase 0~~ | ✅ **stable-2 (Rust 1.83)** | |
|
| ~~P2~~ | ~~**MSRV**~~ | ~~Phase 0~~ | ✅ **stable-2 (Rust 1.83)** | |
|
||||||
| P3 | **String representation in AST** | Phase 1 | `String` (owned) · `Cow<'a, str>` · `Arc<str>` | `String` simplest; `Cow` enables zero-copy parsing later |
|
| ~~P3~~ | ~~**String representation in AST**~~ | ~~Phase 1~~ | ✅ **`String` (owned)** | |
|
||||||
| P4 | **Visitor pattern** | Phase 1 | Define trait now · Defer to Phase 5 | Recommend defining interface now; avoids refactor when renderer arrives |
|
| ~~P4~~ | ~~**Visitor pattern**~~ | ~~Phase 1~~ | ✅ **Defined in Phase 1** | Open-recursion `Visitor` trait + `walk_*` helpers |
|
||||||
| P5 | **Minimum Neovim version** | Phase 9 | 0.8 (`vim.lsp.start`) · 0.11 (`vim.lsp.config`) | 0.8 = broader compat; 0.11 = cleaner Lua API |
|
| P5 | **Minimum Neovim version** | Phase 9 | 0.8 (`vim.lsp.start`) · 0.11 (`vim.lsp.config`) | 0.8 = broader compat; 0.11 = cleaner Lua API |
|
||||||
| P6 | **Minimum Vim version** | Phase 9 | Vim 8.2 · Vim 9.0 · Vim 9.1 | LSP client plugins work best on 9.0+; recommend documenting 9.1 as minimum |
|
| P6 | **Minimum Vim version** | Phase 9 | Vim 8.2 · Vim 9.0 · Vim 9.1 | LSP client plugins work best on 9.0+; recommend documenting 9.1 as minimum |
|
||||||
| P7 | **Semantic token type mapping** | Phase 7 | Standard LSP types only · Custom token types | Custom types need client-side highlight group definitions; more flexible but more setup for users |
|
| P7 | **Semantic token type mapping** | Phase 7 | Standard LSP types only · Custom token types | Custom types need client-side highlight group definitions; more flexible but more setup for users |
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
//! Block-level AST nodes and their supporting types.
|
||||||
|
//!
|
||||||
|
//! See SPEC.md §6.4.
|
||||||
|
|
||||||
|
use super::inline::InlineNode;
|
||||||
|
use super::span::Span;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BlockNode {
|
||||||
|
Heading(HeadingNode),
|
||||||
|
Paragraph(ParagraphNode),
|
||||||
|
HorizontalRule(HorizontalRuleNode),
|
||||||
|
Blockquote(BlockquoteNode),
|
||||||
|
Preformatted(PreformattedNode),
|
||||||
|
MathBlock(MathBlockNode),
|
||||||
|
List(ListNode),
|
||||||
|
DefinitionList(DefinitionListNode),
|
||||||
|
Table(TableNode),
|
||||||
|
Comment(CommentNode),
|
||||||
|
Error(ErrorNode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct HeadingNode {
|
||||||
|
pub span: Span,
|
||||||
|
/// 1..=6 — invariant enforced by the parser, not the type.
|
||||||
|
pub level: u8,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
pub centered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ParagraphNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct HorizontalRuleNode {
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct BlockquoteNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<BlockNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PreformattedNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub content: String,
|
||||||
|
pub language: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MathBlockNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub content: String,
|
||||||
|
pub environment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ListNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub ordered: bool,
|
||||||
|
pub symbol: ListSymbol,
|
||||||
|
pub items: Vec<ListItemNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ListItemNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub symbol: ListSymbol,
|
||||||
|
pub level: usize,
|
||||||
|
pub checkbox: Option<CheckboxState>,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
pub sublist: Option<ListNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ListSymbol {
|
||||||
|
Dash,
|
||||||
|
Star,
|
||||||
|
Hash,
|
||||||
|
Numeric,
|
||||||
|
NumericParen,
|
||||||
|
AlphaParen,
|
||||||
|
AlphaUpperParen,
|
||||||
|
RomanParen,
|
||||||
|
RomanUpperParen,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum CheckboxState {
|
||||||
|
Empty,
|
||||||
|
Quarter,
|
||||||
|
Half,
|
||||||
|
ThreeQuarters,
|
||||||
|
Done,
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DefinitionListNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub items: Vec<DefinitionItemNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DefinitionItemNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub term: Option<Vec<InlineNode>>,
|
||||||
|
pub definitions: Vec<Vec<InlineNode>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TableNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub rows: Vec<TableRowNode>,
|
||||||
|
pub has_header: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TableRowNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub cells: Vec<TableCellNode>,
|
||||||
|
pub is_header: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TableCellNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
pub col_span: bool,
|
||||||
|
pub row_span: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CommentNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resilient parse-failure node — emitted instead of aborting the document.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ErrorNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub raw: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//! Inline AST nodes.
|
||||||
|
//!
|
||||||
|
//! See SPEC.md §6.4. The link-related variants (`WikiLink`, `ExternalLink`,
|
||||||
|
//! `Transclusion`, `RawUrl`) wrap structs defined in `super::link`.
|
||||||
|
|
||||||
|
use super::link::{ExternalLinkNode, RawUrlNode, TransclusionNode, WikiLinkNode};
|
||||||
|
use super::span::Span;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Keyword {
|
||||||
|
Todo,
|
||||||
|
Done,
|
||||||
|
Started,
|
||||||
|
Fixme,
|
||||||
|
Fixed,
|
||||||
|
Xxx,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum InlineNode {
|
||||||
|
Text(TextNode),
|
||||||
|
Bold(BoldNode),
|
||||||
|
Italic(ItalicNode),
|
||||||
|
BoldItalic(BoldItalicNode),
|
||||||
|
Strikethrough(StrikethroughNode),
|
||||||
|
Code(CodeNode),
|
||||||
|
Superscript(SuperscriptNode),
|
||||||
|
Subscript(SubscriptNode),
|
||||||
|
MathInline(MathInlineNode),
|
||||||
|
Keyword(KeywordNode),
|
||||||
|
Color(ColorNode),
|
||||||
|
WikiLink(WikiLinkNode),
|
||||||
|
ExternalLink(ExternalLinkNode),
|
||||||
|
Transclusion(TransclusionNode),
|
||||||
|
RawUrl(RawUrlNode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TextNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct BoldNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ItalicNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct BoldItalicNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct StrikethroughNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CodeNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SuperscriptNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SubscriptNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MathInlineNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct KeywordNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub keyword: Keyword,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ColorNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub color: String,
|
||||||
|
pub children: Vec<InlineNode>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//! Link-related AST nodes and supporting types.
|
||||||
|
//!
|
||||||
|
//! See SPEC.md §6.4. All nodes here are inline (`InlineNode` variants);
|
||||||
|
//! they live in their own module because the link model has enough surface
|
||||||
|
//! area (kinds, anchors, interwiki indirection) to warrant separation.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::inline::InlineNode;
|
||||||
|
use super::span::Span;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum LinkKind {
|
||||||
|
Wiki,
|
||||||
|
Interwiki,
|
||||||
|
Diary,
|
||||||
|
File,
|
||||||
|
Local,
|
||||||
|
Raw,
|
||||||
|
AnchorOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub struct LinkTarget {
|
||||||
|
pub kind: LinkKind,
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub wiki_index: Option<usize>,
|
||||||
|
pub wiki_name: Option<String>,
|
||||||
|
pub anchor: Option<String>,
|
||||||
|
pub is_absolute: bool,
|
||||||
|
pub is_directory: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LinkKind {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Wiki
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct WikiLinkNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub target: LinkTarget,
|
||||||
|
pub description: Option<Vec<InlineNode>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExternalLinkNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub url: String,
|
||||||
|
pub description: Option<Vec<InlineNode>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TransclusionNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub url: String,
|
||||||
|
pub alt: Option<String>,
|
||||||
|
pub attrs: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RawUrlNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//! AST types for nuwiki documents.
|
||||||
|
//!
|
||||||
|
//! Types here are syntax-agnostic — both vimwiki and (eventually) markdown
|
||||||
|
//! produce the same node shapes. See SPEC.md §6.4.
|
||||||
|
|
||||||
|
pub mod block;
|
||||||
|
pub mod inline;
|
||||||
|
pub mod link;
|
||||||
|
pub mod span;
|
||||||
|
pub mod visit;
|
||||||
|
|
||||||
|
pub use block::{
|
||||||
|
BlockNode, BlockquoteNode, CheckboxState, CommentNode, DefinitionItemNode, DefinitionListNode,
|
||||||
|
ErrorNode, HeadingNode, HorizontalRuleNode, ListItemNode, ListNode, ListSymbol, MathBlockNode,
|
||||||
|
ParagraphNode, PreformattedNode, TableCellNode, TableNode, TableRowNode,
|
||||||
|
};
|
||||||
|
pub use inline::{
|
||||||
|
BoldItalicNode, BoldNode, CodeNode, ColorNode, InlineNode, ItalicNode, Keyword, KeywordNode,
|
||||||
|
MathInlineNode, StrikethroughNode, SubscriptNode, SuperscriptNode, TextNode,
|
||||||
|
};
|
||||||
|
pub use link::{
|
||||||
|
ExternalLinkNode, LinkKind, LinkTarget, RawUrlNode, TransclusionNode, WikiLinkNode,
|
||||||
|
};
|
||||||
|
pub use span::{Position, Span};
|
||||||
|
pub use visit::{
|
||||||
|
walk_block, walk_definition_item, walk_document, walk_inline, walk_list, walk_list_item,
|
||||||
|
walk_table, walk_table_row, Visitor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Root of an AST. See SPEC.md §6.4.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct DocumentNode {
|
||||||
|
pub span: Span,
|
||||||
|
pub children: Vec<BlockNode>,
|
||||||
|
pub metadata: PageMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Document-level placeholders parsed from `%title` / `%nohtml` / `%template`
|
||||||
|
/// / `%date` directives. See SPEC.md §9.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct PageMetadata {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub nohtml: bool,
|
||||||
|
pub template: Option<String>,
|
||||||
|
pub date: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! Source positions and spans carried on every AST node.
|
||||||
|
//!
|
||||||
|
//! See SPEC.md §6.5. Positions are 0-indexed; `column` is a byte offset
|
||||||
|
//! within a line; `offset` is a byte offset from the start of the document.
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Position {
|
||||||
|
pub line: u32,
|
||||||
|
pub column: u32,
|
||||||
|
pub offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Position {
|
||||||
|
pub const fn new(line: u32, column: u32, offset: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Span {
|
||||||
|
pub start: Position,
|
||||||
|
pub end: Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Span {
|
||||||
|
pub const fn new(start: Position, end: Position) -> Self {
|
||||||
|
Self { start, end }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
//! Open-recursion AST visitor.
|
||||||
|
//!
|
||||||
|
//! Each `visit_*` method has a default body that calls the corresponding
|
||||||
|
//! `walk_*` free function, which in turn dispatches back into the trait.
|
||||||
|
//! Override any method to short-circuit or augment traversal — call the
|
||||||
|
//! matching `walk_*` from your override to keep descending.
|
||||||
|
//!
|
||||||
|
//! Pattern follows `rustc`'s and `syn`'s visitors. See SPEC.md §6.4 + P4.
|
||||||
|
//!
|
||||||
|
//! Read-only for now. A `VisitorMut` flavor can be added when transformations
|
||||||
|
//! become relevant (post-Phase 5).
|
||||||
|
|
||||||
|
use super::block::{
|
||||||
|
BlockNode, BlockquoteNode, CommentNode, DefinitionItemNode, DefinitionListNode, ErrorNode,
|
||||||
|
HeadingNode, HorizontalRuleNode, ListItemNode, ListNode, MathBlockNode, ParagraphNode,
|
||||||
|
PreformattedNode, TableCellNode, TableNode, TableRowNode,
|
||||||
|
};
|
||||||
|
use super::inline::{
|
||||||
|
BoldItalicNode, BoldNode, CodeNode, ColorNode, InlineNode, ItalicNode, KeywordNode,
|
||||||
|
MathInlineNode, StrikethroughNode, SubscriptNode, SuperscriptNode, TextNode,
|
||||||
|
};
|
||||||
|
use super::link::{ExternalLinkNode, RawUrlNode, TransclusionNode, WikiLinkNode};
|
||||||
|
use super::DocumentNode;
|
||||||
|
|
||||||
|
pub trait Visitor {
|
||||||
|
// Top level
|
||||||
|
fn visit_document(&mut self, node: &DocumentNode) {
|
||||||
|
walk_document(self, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block dispatch
|
||||||
|
fn visit_block(&mut self, node: &BlockNode) {
|
||||||
|
walk_block(self, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline dispatch
|
||||||
|
fn visit_inline(&mut self, node: &InlineNode) {
|
||||||
|
walk_inline(self, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block leaves and recursive block nodes
|
||||||
|
fn visit_heading(&mut self, node: &HeadingNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_paragraph(&mut self, node: &ParagraphNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_horizontal_rule(&mut self, _node: &HorizontalRuleNode) {}
|
||||||
|
fn visit_blockquote(&mut self, node: &BlockquoteNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_block(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_preformatted(&mut self, _node: &PreformattedNode) {}
|
||||||
|
fn visit_math_block(&mut self, _node: &MathBlockNode) {}
|
||||||
|
fn visit_list(&mut self, node: &ListNode) {
|
||||||
|
walk_list(self, node);
|
||||||
|
}
|
||||||
|
fn visit_list_item(&mut self, node: &ListItemNode) {
|
||||||
|
walk_list_item(self, node);
|
||||||
|
}
|
||||||
|
fn visit_definition_list(&mut self, node: &DefinitionListNode) {
|
||||||
|
for item in &node.items {
|
||||||
|
self.visit_definition_item(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_definition_item(&mut self, node: &DefinitionItemNode) {
|
||||||
|
walk_definition_item(self, node);
|
||||||
|
}
|
||||||
|
fn visit_table(&mut self, node: &TableNode) {
|
||||||
|
walk_table(self, node);
|
||||||
|
}
|
||||||
|
fn visit_table_row(&mut self, node: &TableRowNode) {
|
||||||
|
walk_table_row(self, node);
|
||||||
|
}
|
||||||
|
fn visit_table_cell(&mut self, node: &TableCellNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_comment(&mut self, _node: &CommentNode) {}
|
||||||
|
fn visit_error(&mut self, _node: &ErrorNode) {}
|
||||||
|
|
||||||
|
// Inline leaves and recursive inline nodes
|
||||||
|
fn visit_text(&mut self, _node: &TextNode) {}
|
||||||
|
fn visit_bold(&mut self, node: &BoldNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_italic(&mut self, node: &ItalicNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_bold_italic(&mut self, node: &BoldItalicNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_strikethrough(&mut self, node: &StrikethroughNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_code(&mut self, _node: &CodeNode) {}
|
||||||
|
fn visit_superscript(&mut self, node: &SuperscriptNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_subscript(&mut self, node: &SubscriptNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_math_inline(&mut self, _node: &MathInlineNode) {}
|
||||||
|
fn visit_keyword(&mut self, _node: &KeywordNode) {}
|
||||||
|
fn visit_color(&mut self, node: &ColorNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_wiki_link(&mut self, node: &WikiLinkNode) {
|
||||||
|
if let Some(desc) = &node.description {
|
||||||
|
for child in desc {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_external_link(&mut self, node: &ExternalLinkNode) {
|
||||||
|
if let Some(desc) = &node.description {
|
||||||
|
for child in desc {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_transclusion(&mut self, _node: &TransclusionNode) {}
|
||||||
|
fn visit_raw_url(&mut self, _node: &RawUrlNode) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_document<V: Visitor + ?Sized>(v: &mut V, node: &DocumentNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
v.visit_block(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_block<V: Visitor + ?Sized>(v: &mut V, node: &BlockNode) {
|
||||||
|
match node {
|
||||||
|
BlockNode::Heading(n) => v.visit_heading(n),
|
||||||
|
BlockNode::Paragraph(n) => v.visit_paragraph(n),
|
||||||
|
BlockNode::HorizontalRule(n) => v.visit_horizontal_rule(n),
|
||||||
|
BlockNode::Blockquote(n) => v.visit_blockquote(n),
|
||||||
|
BlockNode::Preformatted(n) => v.visit_preformatted(n),
|
||||||
|
BlockNode::MathBlock(n) => v.visit_math_block(n),
|
||||||
|
BlockNode::List(n) => v.visit_list(n),
|
||||||
|
BlockNode::DefinitionList(n) => v.visit_definition_list(n),
|
||||||
|
BlockNode::Table(n) => v.visit_table(n),
|
||||||
|
BlockNode::Comment(n) => v.visit_comment(n),
|
||||||
|
BlockNode::Error(n) => v.visit_error(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_inline<V: Visitor + ?Sized>(v: &mut V, node: &InlineNode) {
|
||||||
|
match node {
|
||||||
|
InlineNode::Text(n) => v.visit_text(n),
|
||||||
|
InlineNode::Bold(n) => v.visit_bold(n),
|
||||||
|
InlineNode::Italic(n) => v.visit_italic(n),
|
||||||
|
InlineNode::BoldItalic(n) => v.visit_bold_italic(n),
|
||||||
|
InlineNode::Strikethrough(n) => v.visit_strikethrough(n),
|
||||||
|
InlineNode::Code(n) => v.visit_code(n),
|
||||||
|
InlineNode::Superscript(n) => v.visit_superscript(n),
|
||||||
|
InlineNode::Subscript(n) => v.visit_subscript(n),
|
||||||
|
InlineNode::MathInline(n) => v.visit_math_inline(n),
|
||||||
|
InlineNode::Keyword(n) => v.visit_keyword(n),
|
||||||
|
InlineNode::Color(n) => v.visit_color(n),
|
||||||
|
InlineNode::WikiLink(n) => v.visit_wiki_link(n),
|
||||||
|
InlineNode::ExternalLink(n) => v.visit_external_link(n),
|
||||||
|
InlineNode::Transclusion(n) => v.visit_transclusion(n),
|
||||||
|
InlineNode::RawUrl(n) => v.visit_raw_url(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_list<V: Visitor + ?Sized>(v: &mut V, node: &ListNode) {
|
||||||
|
for item in &node.items {
|
||||||
|
v.visit_list_item(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_list_item<V: Visitor + ?Sized>(v: &mut V, node: &ListItemNode) {
|
||||||
|
for child in &node.children {
|
||||||
|
v.visit_inline(child);
|
||||||
|
}
|
||||||
|
if let Some(sublist) = &node.sublist {
|
||||||
|
v.visit_list(sublist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_definition_item<V: Visitor + ?Sized>(v: &mut V, node: &DefinitionItemNode) {
|
||||||
|
if let Some(term) = &node.term {
|
||||||
|
for child in term {
|
||||||
|
v.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for definition in &node.definitions {
|
||||||
|
for child in definition {
|
||||||
|
v.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_table<V: Visitor + ?Sized>(v: &mut V, node: &TableNode) {
|
||||||
|
for row in &node.rows {
|
||||||
|
v.visit_table_row(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn walk_table_row<V: Visitor + ?Sized>(v: &mut V, node: &TableRowNode) {
|
||||||
|
for cell in &node.cells {
|
||||||
|
v.visit_table_cell(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@
|
|||||||
//!
|
//!
|
||||||
//! This crate is editor-independent and must never depend on `nuwiki-lsp` or
|
//! This crate is editor-independent and must never depend on `nuwiki-lsp` or
|
||||||
//! `nuwiki-ls`. See SPEC.md §6.2 for the dependency rules.
|
//! `nuwiki-ls`. See SPEC.md §6.2 for the dependency rules.
|
||||||
|
|
||||||
|
pub mod ast;
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
//! Smoke test for the AST visitor: build a small document by hand and
|
||||||
|
//! confirm a Visitor descends into every nested node exactly once.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use nuwiki_core::ast::{
|
||||||
|
BlockNode, BoldNode, DocumentNode, ExternalLinkNode, HeadingNode, InlineNode, ListItemNode,
|
||||||
|
ListNode, ListSymbol, ParagraphNode, Span, TableCellNode, TableNode, TableRowNode, TextNode,
|
||||||
|
TransclusionNode, Visitor,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Counter {
|
||||||
|
headings: usize,
|
||||||
|
paragraphs: usize,
|
||||||
|
text: usize,
|
||||||
|
bold: usize,
|
||||||
|
list_items: usize,
|
||||||
|
table_cells: usize,
|
||||||
|
external_links: usize,
|
||||||
|
transclusions: usize,
|
||||||
|
blocks: usize,
|
||||||
|
inlines: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visitor for Counter {
|
||||||
|
fn visit_block(&mut self, node: &BlockNode) {
|
||||||
|
self.blocks += 1;
|
||||||
|
nuwiki_core::ast::walk_block(self, node);
|
||||||
|
}
|
||||||
|
fn visit_inline(&mut self, node: &InlineNode) {
|
||||||
|
self.inlines += 1;
|
||||||
|
nuwiki_core::ast::walk_inline(self, node);
|
||||||
|
}
|
||||||
|
fn visit_heading(&mut self, node: &HeadingNode) {
|
||||||
|
self.headings += 1;
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_paragraph(&mut self, node: &ParagraphNode) {
|
||||||
|
self.paragraphs += 1;
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_text(&mut self, _: &TextNode) {
|
||||||
|
self.text += 1;
|
||||||
|
}
|
||||||
|
fn visit_bold(&mut self, node: &BoldNode) {
|
||||||
|
self.bold += 1;
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_list_item(&mut self, node: &ListItemNode) {
|
||||||
|
self.list_items += 1;
|
||||||
|
nuwiki_core::ast::walk_list_item(self, node);
|
||||||
|
}
|
||||||
|
fn visit_table_cell(&mut self, node: &TableCellNode) {
|
||||||
|
self.table_cells += 1;
|
||||||
|
for child in &node.children {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_external_link(&mut self, node: &ExternalLinkNode) {
|
||||||
|
self.external_links += 1;
|
||||||
|
if let Some(desc) = &node.description {
|
||||||
|
for child in desc {
|
||||||
|
self.visit_inline(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn visit_transclusion(&mut self, _: &TransclusionNode) {
|
||||||
|
self.transclusions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(s: &str) -> InlineNode {
|
||||||
|
InlineNode::Text(TextNode {
|
||||||
|
span: Span::default(),
|
||||||
|
content: s.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn visitor_descends_into_every_node() {
|
||||||
|
// = Title with *bold* word =
|
||||||
|
let heading = BlockNode::Heading(HeadingNode {
|
||||||
|
span: Span::default(),
|
||||||
|
level: 1,
|
||||||
|
centered: false,
|
||||||
|
children: vec![
|
||||||
|
text("Title with "),
|
||||||
|
InlineNode::Bold(BoldNode {
|
||||||
|
span: Span::default(),
|
||||||
|
children: vec![text("bold")],
|
||||||
|
}),
|
||||||
|
text(" word"),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// A paragraph containing an external link with two text children in its
|
||||||
|
// description, and a transclusion sibling.
|
||||||
|
let paragraph = BlockNode::Paragraph(ParagraphNode {
|
||||||
|
span: Span::default(),
|
||||||
|
children: vec![
|
||||||
|
InlineNode::ExternalLink(ExternalLinkNode {
|
||||||
|
span: Span::default(),
|
||||||
|
url: "https://example.com".into(),
|
||||||
|
description: Some(vec![text("see "), text("docs")]),
|
||||||
|
}),
|
||||||
|
InlineNode::Transclusion(TransclusionNode {
|
||||||
|
span: Span::default(),
|
||||||
|
url: "image.png".into(),
|
||||||
|
alt: None,
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// - one
|
||||||
|
// - two
|
||||||
|
let list = BlockNode::List(ListNode {
|
||||||
|
span: Span::default(),
|
||||||
|
ordered: false,
|
||||||
|
symbol: ListSymbol::Dash,
|
||||||
|
items: vec![
|
||||||
|
ListItemNode {
|
||||||
|
span: Span::default(),
|
||||||
|
symbol: ListSymbol::Dash,
|
||||||
|
level: 0,
|
||||||
|
checkbox: None,
|
||||||
|
children: vec![text("one")],
|
||||||
|
sublist: None,
|
||||||
|
},
|
||||||
|
ListItemNode {
|
||||||
|
span: Span::default(),
|
||||||
|
symbol: ListSymbol::Dash,
|
||||||
|
level: 0,
|
||||||
|
checkbox: None,
|
||||||
|
children: vec![text("two")],
|
||||||
|
sublist: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// | a | b |
|
||||||
|
let table = BlockNode::Table(TableNode {
|
||||||
|
span: Span::default(),
|
||||||
|
has_header: false,
|
||||||
|
rows: vec![TableRowNode {
|
||||||
|
span: Span::default(),
|
||||||
|
is_header: false,
|
||||||
|
cells: vec![
|
||||||
|
TableCellNode {
|
||||||
|
span: Span::default(),
|
||||||
|
children: vec![text("a")],
|
||||||
|
col_span: false,
|
||||||
|
row_span: false,
|
||||||
|
},
|
||||||
|
TableCellNode {
|
||||||
|
span: Span::default(),
|
||||||
|
children: vec![text("b")],
|
||||||
|
col_span: false,
|
||||||
|
row_span: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let doc = DocumentNode {
|
||||||
|
span: Span::default(),
|
||||||
|
metadata: Default::default(),
|
||||||
|
children: vec![heading, paragraph, list, table],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut counter = Counter::default();
|
||||||
|
counter.visit_document(&doc);
|
||||||
|
|
||||||
|
// Block-level: 4 top-level + 2 list items aren't blocks
|
||||||
|
assert_eq!(counter.blocks, 4, "top-level blocks");
|
||||||
|
assert_eq!(counter.headings, 1);
|
||||||
|
assert_eq!(counter.paragraphs, 1);
|
||||||
|
assert_eq!(counter.list_items, 2);
|
||||||
|
// 1 cell row with 2 cells
|
||||||
|
assert_eq!(counter.table_cells, 2);
|
||||||
|
|
||||||
|
// Inlines:
|
||||||
|
// heading: 3 ("Title with ", Bold, " word") + 1 (bold child) = 4
|
||||||
|
// paragraph: 2 (ExternalLink, Transclusion) + 2 (link desc) = 4
|
||||||
|
// list items: 2 (one each)
|
||||||
|
// table cells: 2 (one each)
|
||||||
|
// total: 12
|
||||||
|
assert_eq!(counter.inlines, 12);
|
||||||
|
// Text leaves: heading 3, link description 2, list items 2, table cells 2 = 9
|
||||||
|
assert_eq!(counter.text, 9, "Text leaves");
|
||||||
|
assert_eq!(counter.bold, 1);
|
||||||
|
assert_eq!(counter.external_links, 1);
|
||||||
|
assert_eq!(counter.transclusions, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_default_is_empty() {
|
||||||
|
let doc = DocumentNode::default();
|
||||||
|
assert!(doc.children.is_empty());
|
||||||
|
assert_eq!(doc.metadata.title, None);
|
||||||
|
assert!(!doc.metadata.nohtml);
|
||||||
|
|
||||||
|
let mut counter = Counter::default();
|
||||||
|
counter.visit_document(&doc);
|
||||||
|
assert_eq!(counter.blocks, 0);
|
||||||
|
assert_eq!(counter.inlines, 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user