//! 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 { 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) -> DocumentNode { let joined = tokens .iter() .map(|t| t.0.as_str()) .collect::>() .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::::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::(), 6); assert_eq!((&s).into_iter().sum::(), 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() {} assert_send_sync::(); }