Files
nuwiki-rs/crates/nuwiki-core/tests/syntax.rs
T

145 lines
4.0 KiB
Rust
Raw Normal View History

//! 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>();
}