145 lines
4.1 KiB
Rust
145 lines
4.1 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 (SPEC.md §6.3).
|
||
|
|
#[test]
|
||
|
|
fn registry_is_send_and_sync() {
|
||
|
|
fn assert_send_sync<T: Send + Sync>() {}
|
||
|
|
assert_send_sync::<SyntaxRegistry>();
|
||
|
|
}
|