Skip to main content

tree_sitter_htmlx/
lib.rs

1//! Tree-sitter grammar for HTMLX (expression-enhanced HTML)
2//!
3//! HTMLX extends HTML with expression syntax commonly used in modern
4//! frontend frameworks like Svelte, Vue, and others.
5//!
6//! ## Features
7//!
8//! - Expression interpolation: `{expression}`
9//! - Shorthand attributes: `{name}` (equivalent to `name={name}`)
10//! - Spread attributes: `{...props}`
11//! - Directive attributes: `bind:value`, `on:click`, `class:active`, etc.
12//!
13//! ## Example
14//!
15//! ```rust
16//! use tree_sitter_htmlx::LANGUAGE;
17//!
18//! let mut parser = tree_sitter::Parser::new();
19//! parser.set_language(&LANGUAGE.into()).expect("Failed to load HTMLX grammar");
20//!
21//! let source = r#"<div class="container" {hidden}>
22//!   <p>{greeting}, {name}!</p>
23//!   <button onclick={handleClick}>Click me</button>
24//! </div>"#;
25//!
26//! let tree = parser.parse(source, None).unwrap();
27//! assert!(!tree.root_node().has_error());
28//! ```
29
30use tree_sitter_language::LanguageFn;
31
32extern "C" {
33    fn tree_sitter_htmlx() -> *const ();
34}
35
36/// The tree-sitter [`LanguageFn`] for HTMLX.
37pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_htmlx) };
38
39/// The tree-sitter language for HTMLX.
40pub fn language() -> tree_sitter::Language {
41    LANGUAGE.into()
42}
43
44/// The syntax highlighting query for HTMLX.
45pub const HIGHLIGHTS_QUERY: &str = include_str!("../queries/highlights.scm");
46
47/// The injection query for HTMLX.
48pub const INJECTIONS_QUERY: &str = include_str!("../queries/injections.scm");
49
50/// The content of the [`node-types.json`] file for HTMLX.
51pub const NODE_TYPES: &str = include_str!("../src/node-types.json");
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_can_load_grammar() {
59        let mut parser = tree_sitter::Parser::new();
60        parser
61            .set_language(&LANGUAGE.into())
62            .expect("Failed to load HTMLX grammar");
63    }
64
65    #[test]
66    fn test_parse_simple_html() {
67        let mut parser = tree_sitter::Parser::new();
68        parser.set_language(&LANGUAGE.into()).unwrap();
69
70        let source = "<div>Hello</div>";
71        let tree = parser.parse(source, None).unwrap();
72
73        assert!(!tree.root_node().has_error());
74        assert_eq!(tree.root_node().kind(), "document");
75    }
76
77    #[test]
78    fn test_parse_expression() {
79        let mut parser = tree_sitter::Parser::new();
80        parser.set_language(&LANGUAGE.into()).unwrap();
81
82        let source = "<div>{name}</div>";
83        let tree = parser.parse(source, None).unwrap();
84
85        assert!(!tree.root_node().has_error());
86    }
87
88    #[test]
89    fn test_parse_expression_attribute() {
90        let mut parser = tree_sitter::Parser::new();
91        parser.set_language(&LANGUAGE.into()).unwrap();
92
93        let source = r#"<input value={text} />"#;
94        let tree = parser.parse(source, None).unwrap();
95
96        assert!(!tree.root_node().has_error());
97    }
98
99    #[test]
100    fn test_parse_shorthand_attribute() {
101        let mut parser = tree_sitter::Parser::new();
102        parser.set_language(&LANGUAGE.into()).unwrap();
103
104        let source = r#"<div {hidden} {id}></div>"#;
105        let tree = parser.parse(source, None).unwrap();
106
107        assert!(!tree.root_node().has_error());
108    }
109
110    #[test]
111    fn test_parse_spread_attribute() {
112        let mut parser = tree_sitter::Parser::new();
113        parser.set_language(&LANGUAGE.into()).unwrap();
114
115        let source = r#"<Component {...props} />"#;
116        let tree = parser.parse(source, None).unwrap();
117
118        assert!(!tree.root_node().has_error());
119    }
120
121    #[test]
122    fn test_parse_directive_attribute() {
123        let mut parser = tree_sitter::Parser::new();
124        parser.set_language(&LANGUAGE.into()).unwrap();
125
126        let source = r#"<input bind:value={name} on:input={handleInput} />"#;
127        let tree = parser.parse(source, None).unwrap();
128
129        assert!(!tree.root_node().has_error());
130    }
131
132    #[test]
133    fn test_parse_nested_expressions() {
134        let mut parser = tree_sitter::Parser::new();
135        parser.set_language(&LANGUAGE.into()).unwrap();
136
137        let source = r#"<div>{items.map(item => item.name)}</div>"#;
138        let tree = parser.parse(source, None).unwrap();
139
140        assert!(!tree.root_node().has_error());
141    }
142
143    #[test]
144    fn test_highlights_query_includes_html_captures() {
145        let language = language();
146        let query = tree_sitter::Query::new(&language, HIGHLIGHTS_QUERY)
147            .expect("HTMLX highlights query should compile");
148        let captures = query.capture_names();
149
150        for capture in ["tag", "attribute", "string", "punctuation.bracket"] {
151            assert!(
152                captures.iter().any(|name| *name == capture),
153                "missing @{capture} from HTMLX highlights query"
154            );
155        }
156    }
157}