css_variable_lsp/parsers/
html.rs1use tower_lsp::lsp_types::Url;
2
3use super::css::{parse_css_snippet, CssParseContext};
4use crate::dom_tree::DomTree;
5use crate::manager::CssVariableManager;
6use crate::types::DOMNodeInfo;
7
8pub async fn parse_html_document(
10 text: &str,
11 uri: &Url,
12 manager: &CssVariableManager,
13) -> Result<(), String> {
14 let parsed = DomTree::parse(text);
15
16 manager
17 .set_dom_tree(uri.clone(), parsed.dom_tree.clone())
18 .await;
19
20 for block in parsed.style_blocks {
21 let context = CssParseContext {
22 css_text: &block.content,
23 full_text: text,
24 uri,
25 manager,
26 base_offset: block.content_start,
27 inline: false,
28 usage_context_override: None,
29 dom_node: None,
30 };
31 let _ = parse_css_snippet(context).await;
32 }
33
34 for inline in parsed.inline_styles {
35 let dom_node: Option<DOMNodeInfo> = parsed
36 .dom_tree
37 .find_node_at_position(inline.attribute_start);
38
39 let context = CssParseContext {
40 css_text: &inline.value,
41 full_text: text,
42 uri,
43 manager,
44 base_offset: inline.value_start,
45 inline: true,
46 usage_context_override: Some("inline-style"),
47 dom_node,
48 };
49 let _ = parse_css_snippet(context).await;
50 }
51
52 Ok(())
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::manager::CssVariableManager;
59 use crate::types::Config;
60 use std::collections::HashSet;
61
62 #[tokio::test]
63 async fn parse_html_document_extracts_definitions_and_usages() {
64 let manager = CssVariableManager::new(Config::default());
65 let uri = Url::parse("file:///test.html").unwrap();
66 let text = r#"
67 <style>
68 :root { --primary: #fff; }
69 .card { color: var(--primary); }
70 </style>
71 <div style="--accent: blue; color: var(--primary, #000)"></div>
72 "#;
73
74 parse_html_document(text, &uri, &manager).await.unwrap();
75
76 let primary_defs = manager.get_variables("--primary").await;
77 assert_eq!(primary_defs.len(), 1);
78 assert_eq!(primary_defs[0].value, "#fff");
79
80 let accent_defs = manager.get_variables("--accent").await;
81 assert_eq!(accent_defs.len(), 1);
82 assert_eq!(accent_defs[0].value, "blue");
83
84 let usages = manager.get_usages("--primary").await;
85 assert_eq!(usages.len(), 2);
86
87 let contexts: HashSet<String> = usages.into_iter().map(|u| u.usage_context).collect();
88 assert!(contexts.contains(".card"));
89 assert!(contexts.contains("inline-style"));
90 }
91}
92
93#[cfg(test)]
94mod edge_case_tests {
95 use super::*;
96 use crate::types::Config;
97 use tower_lsp::lsp_types::Url;
98
99 #[tokio::test]
100 async fn test_parse_empty_html() {
101 let manager = CssVariableManager::new(Config::default());
102 let uri = Url::parse("file:///empty.html").unwrap();
103
104 let result = parse_html_document("", &uri, &manager).await;
105 assert!(result.is_ok());
106 }
107
108 #[tokio::test]
109 async fn test_parse_html_with_multiple_style_blocks() {
110 let manager = CssVariableManager::new(Config::default());
111 let uri = Url::parse("file:///test.html").unwrap();
112
113 let html = r#"
114 <html>
115 <head>
116 <style>:root { --head: blue; }</style>
117 </head>
118 <body>
119 <style>.body { --body: red; }</style>
120 <div>
121 <style>.nested { --nested: green; }</style>
122 </div>
123 </body>
124 </html>
125 "#;
126
127 parse_html_document(html, &uri, &manager).await.unwrap();
128
129 let vars = manager.get_all_variables().await;
130 assert!(vars.len() >= 3);
131 }
132
133 #[tokio::test]
134 async fn test_parse_html_with_inline_styles() {
135 let manager = CssVariableManager::new(Config::default());
136 let uri = Url::parse("file:///test.html").unwrap();
137
138 let html = r#"
139 <div style="--inline1: red; color: var(--inline1);">
140 <span style="--inline2: blue;">Test</span>
141 </div>
142 "#;
143
144 parse_html_document(html, &uri, &manager).await.unwrap();
145
146 let vars = manager.get_all_variables().await;
147 assert!(vars.len() >= 2);
148
149 let inline_vars: Vec<_> = vars.iter().filter(|v| v.inline).collect();
151 assert!(inline_vars.len() >= 2);
152 }
153
154 #[tokio::test]
155 async fn test_parse_html_mixed_quotes() {
156 let manager = CssVariableManager::new(Config::default());
157 let uri = Url::parse("file:///test.html").unwrap();
158
159 let html = r#"
160 <div style="--var1: blue;">Single</div>
161 <div style='--var2: red;'>Double</div>
162 "#;
163
164 parse_html_document(html, &uri, &manager).await.unwrap();
165
166 let vars = manager.get_all_variables().await;
167 assert!(vars.len() >= 2);
168 }
169
170 #[tokio::test]
171 async fn test_parse_html_with_comments() {
172 let manager = CssVariableManager::new(Config::default());
173 let uri = Url::parse("file:///test.html").unwrap();
174
175 let html = r#"
176 <!-- HTML Comment -->
177 <style>
178 /* CSS Comment */
179 :root { --color: blue; }
180 </style>
181 <div style="--inline: red;"><!-- Inline comment --></div>
182 "#;
183
184 parse_html_document(html, &uri, &manager).await.unwrap();
185
186 let vars = manager.get_all_variables().await;
187 assert!(vars.len() >= 2);
188 }
189
190 #[tokio::test]
191 async fn test_parse_html_malformed() {
192 let manager = CssVariableManager::new(Config::default());
193 let uri = Url::parse("file:///test.html").unwrap();
194
195 let html = r#"
197 <div style="--var: blue;">
198 <style>:root { --color: red; }
199 "#;
200
201 let result = parse_html_document(html, &uri, &manager).await;
202 assert!(result.is_ok()); }
204
205 #[tokio::test]
206 async fn test_parse_html_script_tags_ignored() {
207 let manager = CssVariableManager::new(Config::default());
208 let uri = Url::parse("file:///test.html").unwrap();
209
210 let html = r#"
211 <html>
212 <script>
213 const style = "--color: red;";
214 </script>
215 <style>:root { --real-color: blue; }</style>
216 </html>
217 "#;
218
219 parse_html_document(html, &uri, &manager).await.unwrap();
220
221 let vars = manager.get_all_variables().await;
223 assert_eq!(vars.len(), 1);
224 assert_eq!(vars[0].name, "--real-color");
225 }
226
227 #[tokio::test]
228 async fn test_parse_html_empty_style_attribute() {
229 let manager = CssVariableManager::new(Config::default());
230 let uri = Url::parse("file:///test.html").unwrap();
231
232 let html = r#"
233 <div style="">Empty</div>
234 <div style=" ">Whitespace</div>
235 "#;
236
237 let result = parse_html_document(html, &uri, &manager).await;
238 assert!(result.is_ok());
239 }
240
241 #[tokio::test]
242 async fn test_parse_html_style_with_media_queries() {
243 let manager = CssVariableManager::new(Config::default());
244 let uri = Url::parse("file:///test.html").unwrap();
245
246 let html = r#"
247 <style>
248 :root { --base: blue; }
249 @media (min-width: 768px) {
250 :root { --responsive: red; }
251 }
252 </style>
253 "#;
254
255 parse_html_document(html, &uri, &manager).await.unwrap();
256
257 let vars = manager.get_all_variables().await;
258 assert!(vars.len() >= 2);
259 }
260
261 #[tokio::test]
262 async fn test_parse_html_nested_elements_with_styles() {
263 let manager = CssVariableManager::new(Config::default());
264 let uri = Url::parse("file:///test.html").unwrap();
265
266 let html = r#"
267 <div style="--outer: blue;">
268 <div style="--middle: red;">
269 <div style="--inner: green;">
270 Content
271 </div>
272 </div>
273 </div>
274 "#;
275
276 parse_html_document(html, &uri, &manager).await.unwrap();
277
278 let vars = manager.get_all_variables().await;
279 assert_eq!(vars.len(), 3);
280 }
281
282 #[tokio::test]
283 async fn test_parse_html_special_characters_in_attributes() {
284 let manager = CssVariableManager::new(Config::default());
285 let uri = Url::parse("file:///test.html").unwrap();
286
287 let html = r#"
288 <div
289 class="test-class"
290 data-value="some&value"
291 style="--color: rgb(255, 0, 0);">
292 Test
293 </div>
294 "#;
295
296 parse_html_document(html, &uri, &manager).await.unwrap();
297
298 let vars = manager.get_all_variables().await;
299 assert_eq!(vars.len(), 1);
300 }
301
302 #[tokio::test]
303 async fn test_parse_html_vue_component() {
304 let manager = CssVariableManager::new(Config::default());
305 let uri = Url::parse("file:///test.vue").unwrap();
306
307 let html = r#"
308 <template>
309 <div style="--vue-var: blue;">Vue Component</div>
310 </template>
311 <style scoped>
312 :root { --vue-style: red; }
313 </style>
314 "#;
315
316 parse_html_document(html, &uri, &manager).await.unwrap();
317
318 let vars = manager.get_all_variables().await;
319 assert!(vars.len() >= 2);
320 }
321
322 #[tokio::test]
323 async fn test_parse_html_svelte_component() {
324 let manager = CssVariableManager::new(Config::default());
325 let uri = Url::parse("file:///test.svelte").unwrap();
326
327 let html = r#"
328 <div style="--svelte-var: green;">
329 Svelte Component
330 </div>
331 <style>
332 :global(:root) { --svelte-global: purple; }
333 </style>
334 "#;
335
336 parse_html_document(html, &uri, &manager).await.unwrap();
337
338 let vars = manager.get_all_variables().await;
339 assert!(vars.len() >= 2);
340 }
341}