Files
nuwiki/crates/nuwiki-core/tests/syntax.rs
T
gffranco 21b485c91b Remove stale phase/spec references from code comments
Strip "Phase N", "SPEC §X.Y", and "v1.x" planning labels from comments
across the Rust crates and editor layers now that those tracking
artifacts are gone. Comments only — no logic, strings, or test names
touched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:25:51 -03:00

145 lines
4.0 KiB
Rust

//! Integration tests for the syntax plugin interface.
use nuwiki_core::ast::{
inline::InlineNode, BlockNode, DocumentNode, ParagraphNode, Span, TextNode,
};
use nuwiki_core::syntax::{Lexer, Parser, SyntaxPlugin, SyntaxRegistry, TokenStream};
// --- A minimal mock plugin: tokenises the input on whitespace, then wraps
// the joined text in a single Paragraph. Just enough surface to exercise
// every trait + the registry.
#[derive(Debug, Clone, PartialEq, Eq)]
struct MockToken(String);
struct MockLexer;
impl Lexer for MockLexer {
type Token = MockToken;
fn lex(&self, text: &str) -> TokenStream<MockToken> {
TokenStream::from_vec(
text.split_whitespace()
.map(|w| MockToken(w.to_owned()))
.collect(),
)
}
}
struct MockParser;
impl Parser for MockParser {
type Token = MockToken;
fn parse(&self, tokens: TokenStream<MockToken>) -> DocumentNode {
let joined = tokens
.iter()
.map(|t| t.0.as_str())
.collect::<Vec<_>>()
.join(" ");
let paragraph = BlockNode::Paragraph(ParagraphNode {
span: Span::default(),
children: vec![InlineNode::Text(TextNode {
span: Span::default(),
content: joined,
})],
});
DocumentNode {
span: Span::default(),
children: vec![paragraph],
metadata: Default::default(),
}
}
}
struct MockPlugin;
impl SyntaxPlugin for MockPlugin {
fn id(&self) -> &str {
"mock"
}
fn display_name(&self) -> &str {
"Mock Syntax"
}
fn file_extensions(&self) -> &[&str] {
&[".mock", ".mk"]
}
fn parse(&self, text: &str) -> DocumentNode {
MockParser.parse(MockLexer.lex(text))
}
}
#[test]
fn token_stream_roundtrip() {
let mut s = TokenStream::<u32>::new();
assert!(s.is_empty());
s.push(1);
s.push(2);
s.push(3);
assert_eq!(s.len(), 3);
assert_eq!(s.as_slice(), &[1, 2, 3]);
assert_eq!(s.iter().sum::<u32>(), 6);
assert_eq!((&s).into_iter().sum::<u32>(), 6);
assert_eq!(s.into_vec(), vec![1, 2, 3]);
let from_vec: TokenStream<&str> = vec!["a", "b"].into();
assert_eq!(from_vec.len(), 2);
let collected: Vec<&str> = from_vec.into_iter().collect();
assert_eq!(collected, vec!["a", "b"]);
}
#[test]
fn lexer_and_parser_chain_through_plugin() {
let plugin = MockPlugin;
let doc = plugin.parse(" hello world ");
let BlockNode::Paragraph(p) = &doc.children[0] else {
panic!("expected a paragraph, got {:?}", doc.children[0]);
};
let InlineNode::Text(t) = &p.children[0] else {
panic!("expected a text node");
};
assert_eq!(t.content, "hello world");
}
#[test]
fn registry_lookup_by_id_and_extension() {
let mut registry = SyntaxRegistry::new();
assert!(registry.is_empty());
registry.register(MockPlugin);
assert_eq!(registry.len(), 1);
let by_id = registry
.get("mock")
.expect("plugin should be findable by id");
assert_eq!(by_id.display_name(), "Mock Syntax");
let by_ext = registry
.detect_from_extension(".mk")
.expect("plugin should be findable by secondary extension");
assert_eq!(by_ext.id(), "mock");
assert!(registry.get("nonexistent").is_none());
assert!(registry.detect_from_extension(".unknown").is_none());
// Iteration order matches registration order.
let ids: Vec<&str> = registry.iter().map(|p| p.id()).collect();
assert_eq!(ids, vec!["mock"]);
}
#[test]
fn registry_can_parse_through_trait_object() {
let mut registry = SyntaxRegistry::new();
registry.register(MockPlugin);
let plugin = registry.detect_from_extension(".mock").unwrap();
let doc = plugin.parse("a b c");
assert_eq!(doc.children.len(), 1);
}
/// Compile-time check: SyntaxRegistry must be Send + Sync so the LSP server
/// can share it across handler tasks.
#[test]
fn registry_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SyntaxRegistry>();
}