Skip to main content

css_variable_lsp/parsers/
html.rs

1use 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
8/// Parse an HTML document and extract CSS from style blocks and inline styles
9pub 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        // Check that inline variables are marked as inline
172        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        // Missing closing tags
218        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()); // Should still parse what it can
225    }
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        // Should only find the real CSS variable, not the one in JavaScript
244        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}