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
94#[cfg(test)]
95mod edge_case_tests {
96    use super::*;
97    use crate::types::Config;
98    use ls_types::Uri;
99    use std::str::FromStr;
100
101    #[tokio::test]
102    async fn test_parse_empty_html() {
103        let manager = CssVariableManager::new(Config::default());
104        let uri = Uri::from_str("file:///empty.html").unwrap();
105
106        let result = parse_html_document("", &uri, &manager).await;
107        assert!(result.is_ok());
108    }
109
110    #[tokio::test]
111    async fn test_parse_html_with_multiple_style_blocks() {
112        let manager = CssVariableManager::new(Config::default());
113        let uri = Uri::from_str("file:///test.html").unwrap();
114
115        let html = r#"
116            <html>
117                <head>
118                    <style>:root { --head: blue; }</style>
119                </head>
120                <body>
121                    <style>.body { --body: red; }</style>
122                    <div>
123                        <style>.nested { --nested: green; }</style>
124                    </div>
125                </body>
126            </html>
127        "#;
128
129        parse_html_document(html, &uri, &manager).await.unwrap();
130
131        let vars = manager.get_all_variables().await;
132        assert!(vars.len() >= 3);
133    }
134
135    #[tokio::test]
136    async fn test_parse_html_with_inline_styles() {
137        let manager = CssVariableManager::new(Config::default());
138        let uri = Uri::from_str("file:///test.html").unwrap();
139
140        let html = r#"
141            <div style="--inline1: red; color: var(--inline1);">
142                <span style="--inline2: blue;">Test</span>
143            </div>
144        "#;
145
146        parse_html_document(html, &uri, &manager).await.unwrap();
147
148        let vars = manager.get_all_variables().await;
149        assert!(vars.len() >= 2);
150
151        // Check that inline variables are marked as inline
152        let inline_vars: Vec<_> = vars.iter().filter(|v| v.inline).collect();
153        assert!(inline_vars.len() >= 2);
154    }
155
156    #[tokio::test]
157    async fn test_parse_html_mixed_quotes() {
158        let manager = CssVariableManager::new(Config::default());
159        let uri = Uri::from_str("file:///test.html").unwrap();
160
161        let html = r#"
162            <div style="--var1: blue;">Single</div>
163            <div style='--var2: red;'>Double</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
172    #[tokio::test]
173    async fn test_parse_html_with_comments() {
174        let manager = CssVariableManager::new(Config::default());
175        let uri = Uri::from_str("file:///test.html").unwrap();
176
177        let html = r#"
178            <!-- HTML Comment -->
179            <style>
180                /* CSS Comment */
181                :root { --color: blue; }
182            </style>
183            <div style="--inline: red;"><!-- Inline comment --></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_malformed() {
194        let manager = CssVariableManager::new(Config::default());
195        let uri = Uri::from_str("file:///test.html").unwrap();
196
197        // Missing closing tags
198        let html = r#"
199            <div style="--var: blue;">
200                <style>:root { --color: red; }
201        "#;
202
203        let result = parse_html_document(html, &uri, &manager).await;
204        assert!(result.is_ok()); // Should still parse what it can
205    }
206
207    #[tokio::test]
208    async fn test_parse_html_script_tags_ignored() {
209        let manager = CssVariableManager::new(Config::default());
210        let uri = Uri::from_str("file:///test.html").unwrap();
211
212        let html = r#"
213            <html>
214                <script>
215                    const style = "--color: red;";
216                </script>
217                <style>:root { --real-color: blue; }</style>
218            </html>
219        "#;
220
221        parse_html_document(html, &uri, &manager).await.unwrap();
222
223        // Should only find the real CSS variable, not the one in JavaScript
224        let vars = manager.get_all_variables().await;
225        assert_eq!(vars.len(), 1);
226        assert_eq!(vars[0].name, "--real-color");
227    }
228
229    #[tokio::test]
230    async fn test_parse_html_empty_style_attribute() {
231        let manager = CssVariableManager::new(Config::default());
232        let uri = Uri::from_str("file:///test.html").unwrap();
233
234        let html = r#"
235            <div style="">Empty</div>
236            <div style="  ">Whitespace</div>
237        "#;
238
239        let result = parse_html_document(html, &uri, &manager).await;
240        assert!(result.is_ok());
241    }
242
243    #[tokio::test]
244    async fn test_parse_html_style_with_media_queries() {
245        let manager = CssVariableManager::new(Config::default());
246        let uri = Uri::from_str("file:///test.html").unwrap();
247
248        let html = r#"
249            <style>
250                :root { --base: blue; }
251                @media (min-width: 768px) {
252                    :root { --responsive: red; }
253                }
254            </style>
255        "#;
256
257        parse_html_document(html, &uri, &manager).await.unwrap();
258
259        let vars = manager.get_all_variables().await;
260        assert!(vars.len() >= 2);
261    }
262
263    #[tokio::test]
264    async fn test_parse_html_nested_elements_with_styles() {
265        let manager = CssVariableManager::new(Config::default());
266        let uri = Uri::from_str("file:///test.html").unwrap();
267
268        let html = r#"
269            <div style="--outer: blue;">
270                <div style="--middle: red;">
271                    <div style="--inner: green;">
272                        Content
273                    </div>
274                </div>
275            </div>
276        "#;
277
278        parse_html_document(html, &uri, &manager).await.unwrap();
279
280        let vars = manager.get_all_variables().await;
281        assert_eq!(vars.len(), 3);
282    }
283
284    #[tokio::test]
285    async fn test_parse_html_special_characters_in_attributes() {
286        let manager = CssVariableManager::new(Config::default());
287        let uri = Uri::from_str("file:///test.html").unwrap();
288
289        let html = r#"
290            <div 
291                class="test-class" 
292                data-value="some&value" 
293                style="--color: rgb(255, 0, 0);">
294                Test
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(), 1);
302    }
303
304    #[tokio::test]
305    async fn test_parse_html_vue_component() {
306        let manager = CssVariableManager::new(Config::default());
307        let uri = Uri::from_str("file:///test.vue").unwrap();
308
309        let html = r#"
310            <template>
311                <div style="--vue-var: blue;">Vue Component</div>
312            </template>
313            <style scoped>
314                :root { --vue-style: red; }
315            </style>
316        "#;
317
318        parse_html_document(html, &uri, &manager).await.unwrap();
319
320        let vars = manager.get_all_variables().await;
321        assert!(vars.len() >= 2);
322    }
323
324    #[tokio::test]
325    async fn test_parse_html_svelte_component() {
326        let manager = CssVariableManager::new(Config::default());
327        let uri = Uri::from_str("file:///test.svelte").unwrap();
328
329        let html = r#"
330            <div style="--svelte-var: green;">
331                Svelte Component
332            </div>
333            <style>
334                :global(:root) { --svelte-global: purple; }
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}