css_variable_lsp/parsers/
html.rs

1use 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
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: &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        // Check that inline variables are marked as inline
150        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        // Missing closing tags
196        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()); // Should still parse what it can
203    }
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        // Should only find the real CSS variable, not the one in JavaScript
222        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}