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