css_variable_lsp/parsers/
html.rs1use ls_types::Uri;
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: &Uri,
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.raw_value,
44 full_text: text,
45 uri,
46 manager,
47 base_offset: inline.value_start,
48 inline: true,
49 usage_context_override: Some("inline-style"),
50 dom_node,
51 };
52 let _ = parse_css_snippet(context).await;
53 }
54
55 Ok(())
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::manager::CssVariableManager;
62 use crate::types::{offset_to_position, Config};
63 use std::collections::HashSet;
64 use std::str::FromStr;
65
66 #[tokio::test]
67 async fn parse_html_document_extracts_definitions_and_usages() {
68 let manager = CssVariableManager::new(Config::default());
69 let uri = Uri::from_str("file:///test.html").unwrap();
70 let text = r#"
71 <style>
72 :root { --primary: #fff; }
73 .card { color: var(--primary); }
74 </style>
75 <div style="--accent: blue; color: var(--primary, #000)"></div>
76 "#;
77
78 parse_html_document(text, &uri, &manager).await.unwrap();
79
80 let primary_defs = manager.get_variables("--primary").await;
81 assert_eq!(primary_defs.len(), 1);
82 assert_eq!(primary_defs[0].value, "#fff");
83
84 let accent_defs = manager.get_variables("--accent").await;
85 assert_eq!(accent_defs.len(), 1);
86 assert_eq!(accent_defs[0].value, "blue");
87
88 let usages = manager.get_usages("--primary").await;
89 assert_eq!(usages.len(), 2);
90
91 let contexts: HashSet<String> = usages.into_iter().map(|u| u.usage_context).collect();
92 assert!(contexts.contains(".card"));
93 assert!(contexts.contains("inline-style"));
94 }
95
96 #[tokio::test]
97 async fn parse_html_document_extracts_literal_colors_from_style_and_inline() {
98 let manager = CssVariableManager::new(Config::default());
99 let uri = Uri::from_str("file:///test.html").unwrap();
100 let text = r#"
101 <style>
102 .card { color: #fff; }
103 </style>
104 <div style="background: rgb(255, 0, 0); color: var(--primary, blue)"></div>
105 "#;
106
107 parse_html_document(text, &uri, &manager).await.unwrap();
108
109 let occurrences = manager.get_document_literal_colors(&uri).await;
110 let literals: HashSet<String> = occurrences.into_iter().map(|occ| occ.text).collect();
111 assert!(literals.contains("#fff"));
112 assert!(literals.contains("rgb(255, 0, 0)"));
113 assert!(!literals.contains("blue"));
114 }
115
116 #[tokio::test]
117 async fn parse_html_document_preserves_raw_offsets_after_inline_entities() {
118 let manager = CssVariableManager::new(Config::default());
119 let uri = Uri::from_str("file:///test.html").unwrap();
120 let text =
121 r#"<div style="content: "x"; color: var(--tone); --after: red"></div>"#;
122
123 parse_html_document(text, &uri, &manager).await.unwrap();
124
125 let usage = manager.get_usages("--tone").await.pop().unwrap();
126 let usage_start = text.find("var(--tone)").unwrap();
127 let name_start = text.find("--tone").unwrap();
128 assert_eq!(usage.range.start, offset_to_position(text, usage_start));
129 assert_eq!(
130 usage.name_range.unwrap().start,
131 offset_to_position(text, name_start),
132 );
133
134 let definition = manager.get_variables("--after").await.pop().unwrap();
135 let definition_start = text.find("--after").unwrap();
136 assert_eq!(definition.source_position, definition_start);
137 assert_eq!(
138 definition.name_range.unwrap().start,
139 offset_to_position(text, definition_start),
140 );
141 }
142}
143
144#[cfg(test)]
145mod edge_case_tests {
146 use super::*;
147 use crate::types::Config;
148 use ls_types::Uri;
149 use std::str::FromStr;
150
151 #[tokio::test]
152 async fn test_parse_empty_html() {
153 let manager = CssVariableManager::new(Config::default());
154 let uri = Uri::from_str("file:///empty.html").unwrap();
155
156 let result = parse_html_document("", &uri, &manager).await;
157 assert!(result.is_ok());
158 }
159
160 #[tokio::test]
161 async fn test_parse_html_with_multiple_style_blocks() {
162 let manager = CssVariableManager::new(Config::default());
163 let uri = Uri::from_str("file:///test.html").unwrap();
164
165 let html = r#"
166 <html>
167 <head>
168 <style>:root { --head: blue; }</style>
169 </head>
170 <body>
171 <style>.body { --body: red; }</style>
172 <div>
173 <style>.nested { --nested: green; }</style>
174 </div>
175 </body>
176 </html>
177 "#;
178
179 parse_html_document(html, &uri, &manager).await.unwrap();
180
181 let vars = manager.get_all_variables().await;
182 assert!(vars.len() >= 3);
183 }
184
185 #[tokio::test]
186 async fn test_parse_html_with_inline_styles() {
187 let manager = CssVariableManager::new(Config::default());
188 let uri = Uri::from_str("file:///test.html").unwrap();
189
190 let html = r#"
191 <div style="--inline1: red; color: var(--inline1);">
192 <span style="--inline2: blue;">Test</span>
193 </div>
194 "#;
195
196 parse_html_document(html, &uri, &manager).await.unwrap();
197
198 let vars = manager.get_all_variables().await;
199 assert!(vars.len() >= 2);
200
201 let inline_vars: Vec<_> = vars.iter().filter(|v| v.inline).collect();
203 assert!(inline_vars.len() >= 2);
204 }
205
206 #[tokio::test]
207 async fn test_parse_html_mixed_quotes() {
208 let manager = CssVariableManager::new(Config::default());
209 let uri = Uri::from_str("file:///test.html").unwrap();
210
211 let html = r#"
212 <div style="--var1: blue;">Single</div>
213 <div style='--var2: red;'>Double</div>
214 "#;
215
216 parse_html_document(html, &uri, &manager).await.unwrap();
217
218 let vars = manager.get_all_variables().await;
219 assert!(vars.len() >= 2);
220 }
221
222 #[tokio::test]
223 async fn test_parse_html_with_comments() {
224 let manager = CssVariableManager::new(Config::default());
225 let uri = Uri::from_str("file:///test.html").unwrap();
226
227 let html = r#"
228 <!-- HTML Comment -->
229 <style>
230 /* CSS Comment */
231 :root { --color: blue; }
232 </style>
233 <div style="--inline: red;"><!-- Inline comment --></div>
234 "#;
235
236 parse_html_document(html, &uri, &manager).await.unwrap();
237
238 let vars = manager.get_all_variables().await;
239 assert!(vars.len() >= 2);
240 }
241
242 #[tokio::test]
243 async fn test_parse_html_malformed() {
244 let manager = CssVariableManager::new(Config::default());
245 let uri = Uri::from_str("file:///test.html").unwrap();
246
247 let html = r#"
249 <div style="--var: blue;">
250 <style>:root { --color: red; }
251 "#;
252
253 let result = parse_html_document(html, &uri, &manager).await;
254 assert!(result.is_ok()); }
256
257 #[tokio::test]
258 async fn test_parse_html_script_tags_ignored() {
259 let manager = CssVariableManager::new(Config::default());
260 let uri = Uri::from_str("file:///test.html").unwrap();
261
262 let html = r#"
263 <html>
264 <script>
265 const style = "--color: red;";
266 </script>
267 <style>:root { --real-color: blue; }</style>
268 </html>
269 "#;
270
271 parse_html_document(html, &uri, &manager).await.unwrap();
272
273 let vars = manager.get_all_variables().await;
275 assert_eq!(vars.len(), 1);
276 assert_eq!(vars[0].name, "--real-color");
277 }
278
279 #[tokio::test]
280 async fn test_parse_html_empty_style_attribute() {
281 let manager = CssVariableManager::new(Config::default());
282 let uri = Uri::from_str("file:///test.html").unwrap();
283
284 let html = r#"
285 <div style="">Empty</div>
286 <div style=" ">Whitespace</div>
287 "#;
288
289 let result = parse_html_document(html, &uri, &manager).await;
290 assert!(result.is_ok());
291 }
292
293 #[tokio::test]
294 async fn test_parse_html_style_with_media_queries() {
295 let manager = CssVariableManager::new(Config::default());
296 let uri = Uri::from_str("file:///test.html").unwrap();
297
298 let html = r#"
299 <style>
300 :root { --base: blue; }
301 @media (min-width: 768px) {
302 :root { --responsive: red; }
303 }
304 </style>
305 "#;
306
307 parse_html_document(html, &uri, &manager).await.unwrap();
308
309 let vars = manager.get_all_variables().await;
310 assert!(vars.len() >= 2);
311 }
312
313 #[tokio::test]
314 async fn test_parse_html_nested_elements_with_styles() {
315 let manager = CssVariableManager::new(Config::default());
316 let uri = Uri::from_str("file:///test.html").unwrap();
317
318 let html = r#"
319 <div style="--outer: blue;">
320 <div style="--middle: red;">
321 <div style="--inner: green;">
322 Content
323 </div>
324 </div>
325 </div>
326 "#;
327
328 parse_html_document(html, &uri, &manager).await.unwrap();
329
330 let vars = manager.get_all_variables().await;
331 assert_eq!(vars.len(), 3);
332 }
333
334 #[tokio::test]
335 async fn test_parse_html_special_characters_in_attributes() {
336 let manager = CssVariableManager::new(Config::default());
337 let uri = Uri::from_str("file:///test.html").unwrap();
338
339 let html = r#"
340 <div
341 class="test-class"
342 data-value="some&value"
343 style="--color: rgb(255, 0, 0);">
344 Test
345 </div>
346 "#;
347
348 parse_html_document(html, &uri, &manager).await.unwrap();
349
350 let vars = manager.get_all_variables().await;
351 assert_eq!(vars.len(), 1);
352 }
353
354 #[tokio::test]
355 async fn test_parse_html_vue_component() {
356 let manager = CssVariableManager::new(Config::default());
357 let uri = Uri::from_str("file:///test.vue").unwrap();
358
359 let html = r#"
360 <template>
361 <div style="--vue-var: blue;">Vue Component</div>
362 </template>
363 <style scoped>
364 :root { --vue-style: red; }
365 </style>
366 "#;
367
368 parse_html_document(html, &uri, &manager).await.unwrap();
369
370 let vars = manager.get_all_variables().await;
371 assert!(vars.len() >= 2);
372 }
373
374 #[tokio::test]
375 async fn test_parse_html_svelte_component() {
376 let manager = CssVariableManager::new(Config::default());
377 let uri = Uri::from_str("file:///test.svelte").unwrap();
378
379 let html = r#"
380 <div style="--svelte-var: green;">
381 Svelte Component
382 </div>
383 <style>
384 :global(:root) { --svelte-global: purple; }
385 </style>
386 "#;
387
388 parse_html_document(html, &uri, &manager).await.unwrap();
389
390 let vars = manager.get_all_variables().await;
391 assert!(vars.len() >= 2);
392 }
393}