Skip to main content

omena_parser/facts/
icss.rs

1//! Parser facts for ICSS import/export blocks.
2//!
3//! ICSS facts preserve the raw local names and specifier spans needed by
4//! resolver and CSS Modules consumers.
5
6use cstree::text::TextRange;
7use omena_syntax::SyntaxKind;
8use std::collections::BTreeSet;
9
10use crate::{
11    ParseResult, Token, collect_css_module_value_definition_edge_names,
12    css_module_value_reference_token_can_be_name, css_module_value_source_name,
13    css_module_value_statement_end, find_block_after_header, matches_ignore_ascii_case,
14    next_non_trivia_token_index_until,
15};
16
17use super::{StyleFactNodeEvent, StyleFactSink};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ParsedIcssFact {
21    pub kind: ParsedIcssFactKind,
22    pub name: String,
23    pub range: TextRange,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum ParsedIcssFactKind {
28    ExportName,
29    ImportLocalName,
30    ImportRemoteName,
31    ImportSource,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ParsedIcssImportEdgeFact {
36    pub local_name: String,
37    pub remote_name: String,
38    pub import_source: String,
39    pub range: TextRange,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ParsedIcssExportEdgeFact {
44    pub export_name: String,
45    pub reference_names: Vec<String>,
46    pub range: TextRange,
47}
48
49pub fn collect_icss_export_values_from_cst(
50    text: &str,
51    parsed: &ParseResult,
52) -> Vec<(String, String)> {
53    let sink = StyleFactSink::from_cst(text, parsed);
54    let mut values = Vec::new();
55    for node in icss_block_nodes(&sink) {
56        collect_icss_export_values_from_block_tokens(sink.node_tokens(node), &mut values);
57    }
58    values
59}
60
61pub(crate) fn collect_icss_facts_from_sink(sink: &StyleFactSink<'_>) -> Vec<ParsedIcssFact> {
62    let mut icss = Vec::new();
63    let mut seen = BTreeSet::new();
64    for node in icss_block_nodes(sink) {
65        collect_icss_facts_from_block_tokens(sink.node_tokens(node), &mut icss, &mut seen);
66    }
67    icss
68}
69
70fn collect_icss_facts_from_block_tokens(
71    tokens: &[Token<'_>],
72    icss: &mut Vec<ParsedIcssFact>,
73    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
74) {
75    for (index, token) in tokens.iter().enumerate() {
76        if token.kind != SyntaxKind::Colon {
77            continue;
78        }
79        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
80        else {
81            continue;
82        };
83        let name = tokens[name_index].text;
84        if !matches!(tokens[name_index].kind, SyntaxKind::Ident) {
85            continue;
86        }
87        if matches_ignore_ascii_case(name, &["export"]) {
88            if let Some((open, close)) =
89                find_block_after_header(tokens, name_index + 1, tokens.len())
90            {
91                collect_icss_export_names(tokens, open + 1, close, icss, seen);
92            }
93            continue;
94        }
95        if matches_ignore_ascii_case(name, &["import"]) {
96            collect_icss_import_source(tokens, name_index + 1, icss, seen);
97            if let Some((open, close)) =
98                find_block_after_header(tokens, name_index + 1, tokens.len())
99            {
100                collect_icss_import_names(tokens, open + 1, close, icss, seen);
101            }
102        }
103    }
104}
105
106pub(crate) fn collect_icss_import_edge_facts_from_sink(
107    sink: &StyleFactSink<'_>,
108) -> Vec<ParsedIcssImportEdgeFact> {
109    let mut edges = Vec::new();
110    for node in icss_block_nodes(sink) {
111        collect_icss_import_edge_facts_from_block_tokens(sink.node_tokens(node), &mut edges);
112    }
113    edges
114}
115
116fn collect_icss_import_edge_facts_from_block_tokens(
117    tokens: &[Token<'_>],
118    edges: &mut Vec<ParsedIcssImportEdgeFact>,
119) {
120    for (index, token) in tokens.iter().enumerate() {
121        if token.kind != SyntaxKind::Colon {
122            continue;
123        }
124        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
125        else {
126            continue;
127        };
128        if tokens[name_index].kind != SyntaxKind::Ident
129            || !matches_ignore_ascii_case(tokens[name_index].text, &["import"])
130        {
131            continue;
132        }
133        let Some(import_source) = icss_import_edge_source(tokens, name_index + 1) else {
134            continue;
135        };
136        if let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len()) {
137            collect_icss_import_edges(tokens, open + 1, close, import_source, edges);
138        }
139    }
140}
141
142pub(crate) fn collect_icss_export_edge_facts_from_sink(
143    sink: &StyleFactSink<'_>,
144) -> Vec<ParsedIcssExportEdgeFact> {
145    let mut edges = Vec::new();
146    for node in icss_block_nodes(sink) {
147        collect_icss_export_edge_facts_from_block_tokens(sink.node_tokens(node), &mut edges);
148    }
149    edges
150}
151
152fn collect_icss_export_edge_facts_from_block_tokens(
153    tokens: &[Token<'_>],
154    edges: &mut Vec<ParsedIcssExportEdgeFact>,
155) {
156    for (index, token) in tokens.iter().enumerate() {
157        if token.kind != SyntaxKind::Colon {
158            continue;
159        }
160        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
161        else {
162            continue;
163        };
164        if tokens[name_index].kind != SyntaxKind::Ident
165            || !matches_ignore_ascii_case(tokens[name_index].text, &["export"])
166        {
167            continue;
168        }
169        if let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len()) {
170            collect_icss_export_edges(tokens, open + 1, close, edges);
171        }
172    }
173}
174
175fn icss_block_nodes<'sink>(
176    sink: &'sink StyleFactSink<'_>,
177) -> impl Iterator<Item = &'sink StyleFactNodeEvent> {
178    sink.nodes().filter(|node| {
179        matches!(
180            node.kind,
181            SyntaxKind::CssModuleExportBlock | SyntaxKind::CssModuleImportBlock
182        )
183    })
184}
185
186fn collect_icss_export_edges(
187    tokens: &[Token<'_>],
188    start: usize,
189    end: usize,
190    edges: &mut Vec<ParsedIcssExportEdgeFact>,
191) {
192    let mut index = start;
193    while index < end {
194        let token = tokens[index];
195        if matches!(
196            token.kind,
197            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
198        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
199            && tokens[colon_index].kind == SyntaxKind::Colon
200        {
201            let value_end = css_module_value_statement_end(tokens, colon_index + 1).min(end);
202            let reference_names = collect_css_module_value_definition_edge_names(
203                tokens,
204                colon_index + 1,
205                value_end,
206                css_module_value_reference_token_can_be_name,
207            );
208            if !reference_names.is_empty() {
209                let range_end = value_end
210                    .checked_sub(1)
211                    .and_then(|end| tokens.get(end))
212                    .map(|token| token.range.end())
213                    .unwrap_or_else(|| token.range.end());
214                edges.push(ParsedIcssExportEdgeFact {
215                    export_name: token.text.to_string(),
216                    reference_names,
217                    range: TextRange::new(token.range.start(), range_end),
218                });
219            }
220            index = value_end;
221            continue;
222        }
223        index += 1;
224    }
225}
226
227fn collect_icss_export_values_from_block_tokens(
228    tokens: &[Token<'_>],
229    values: &mut Vec<(String, String)>,
230) {
231    for (index, token) in tokens.iter().enumerate() {
232        if token.kind != SyntaxKind::Colon {
233            continue;
234        }
235        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
236        else {
237            continue;
238        };
239        if tokens[name_index].kind != SyntaxKind::Ident
240            || !matches_ignore_ascii_case(tokens[name_index].text, &["export"])
241        {
242            continue;
243        }
244        let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len())
245        else {
246            continue;
247        };
248        let mut declaration_index = open + 1;
249        while declaration_index < close {
250            let declaration = tokens[declaration_index];
251            if matches!(
252                declaration.kind,
253                SyntaxKind::Ident | SyntaxKind::CustomPropertyName
254            ) && let Some(colon_index) =
255                next_non_trivia_token_index_until(tokens, declaration_index + 1, close)
256                && tokens[colon_index].kind == SyntaxKind::Colon
257            {
258                let value_end = css_module_value_statement_end(tokens, colon_index + 1).min(close);
259                let value = tokens[colon_index + 1..value_end]
260                    .iter()
261                    .map(|token| token.text)
262                    .collect::<String>()
263                    .trim()
264                    .to_string();
265                values.push((declaration.text.to_string(), value));
266                declaration_index = value_end;
267                continue;
268            }
269            declaration_index += 1;
270        }
271    }
272}
273
274fn icss_import_edge_source(tokens: &[Token<'_>], start: usize) -> Option<String> {
275    let open_index = next_non_trivia_token_index_until(tokens, start, tokens.len())?;
276    if tokens[open_index].kind != SyntaxKind::LeftParen {
277        return None;
278    }
279    let source_index = next_non_trivia_token_index_until(tokens, open_index + 1, tokens.len())?;
280    let token = tokens[source_index];
281    matches!(
282        token.kind,
283        SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
284    )
285    .then(|| css_module_value_source_name(token))
286}
287
288fn collect_icss_import_edges(
289    tokens: &[Token<'_>],
290    start: usize,
291    end: usize,
292    import_source: String,
293    edges: &mut Vec<ParsedIcssImportEdgeFact>,
294) {
295    let mut index = start;
296    while index < end {
297        let token = tokens[index];
298        if matches!(
299            token.kind,
300            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
301        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
302            && tokens[colon_index].kind == SyntaxKind::Colon
303            && let Some(remote_index) =
304                next_non_trivia_token_index_until(tokens, colon_index + 1, end)
305            && matches!(
306                tokens[remote_index].kind,
307                SyntaxKind::Ident | SyntaxKind::CustomPropertyName
308            )
309        {
310            edges.push(ParsedIcssImportEdgeFact {
311                local_name: token.text.to_string(),
312                remote_name: tokens[remote_index].text.to_string(),
313                import_source: import_source.clone(),
314                range: token.range,
315            });
316            index = css_module_value_statement_end(tokens, colon_index + 1);
317            continue;
318        }
319        index += 1;
320    }
321}
322
323fn collect_icss_export_names(
324    tokens: &[Token<'_>],
325    start: usize,
326    end: usize,
327    icss: &mut Vec<ParsedIcssFact>,
328    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
329) {
330    let mut index = start;
331    while index < end {
332        let token = tokens[index];
333        if matches!(
334            token.kind,
335            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
336        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
337            && tokens[colon_index].kind == SyntaxKind::Colon
338        {
339            push_icss_fact(
340                icss,
341                seen,
342                ParsedIcssFactKind::ExportName,
343                token.text.to_string(),
344                token.range,
345            );
346            index = css_module_value_statement_end(tokens, colon_index + 1);
347            continue;
348        }
349        index += 1;
350    }
351}
352
353fn collect_icss_import_source(
354    tokens: &[Token<'_>],
355    start: usize,
356    icss: &mut Vec<ParsedIcssFact>,
357    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
358) {
359    let Some(open_index) = next_non_trivia_token_index_until(tokens, start, tokens.len()) else {
360        return;
361    };
362    if tokens[open_index].kind != SyntaxKind::LeftParen {
363        return;
364    }
365    let Some(source_index) =
366        next_non_trivia_token_index_until(tokens, open_index + 1, tokens.len())
367    else {
368        return;
369    };
370    let token = tokens[source_index];
371    if matches!(
372        token.kind,
373        SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
374    ) {
375        push_icss_fact(
376            icss,
377            seen,
378            ParsedIcssFactKind::ImportSource,
379            css_module_value_source_name(token),
380            token.range,
381        );
382    }
383}
384
385fn collect_icss_import_names(
386    tokens: &[Token<'_>],
387    start: usize,
388    end: usize,
389    icss: &mut Vec<ParsedIcssFact>,
390    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
391) {
392    let mut index = start;
393    while index < end {
394        let token = tokens[index];
395        if matches!(
396            token.kind,
397            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
398        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
399            && tokens[colon_index].kind == SyntaxKind::Colon
400        {
401            push_icss_fact(
402                icss,
403                seen,
404                ParsedIcssFactKind::ImportLocalName,
405                token.text.to_string(),
406                token.range,
407            );
408            if let Some(remote_index) =
409                next_non_trivia_token_index_until(tokens, colon_index + 1, end)
410                && matches!(
411                    tokens[remote_index].kind,
412                    SyntaxKind::Ident | SyntaxKind::CustomPropertyName
413                )
414            {
415                push_icss_fact(
416                    icss,
417                    seen,
418                    ParsedIcssFactKind::ImportRemoteName,
419                    tokens[remote_index].text.to_string(),
420                    tokens[remote_index].range,
421                );
422            }
423            index = css_module_value_statement_end(tokens, colon_index + 1);
424            continue;
425        }
426        index += 1;
427    }
428}
429
430fn push_icss_fact(
431    icss: &mut Vec<ParsedIcssFact>,
432    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
433    kind: ParsedIcssFactKind,
434    name: String,
435    range: TextRange,
436) {
437    if seen.insert((
438        kind,
439        name.clone(),
440        u32::from(range.start()),
441        u32::from(range.end()),
442    )) {
443        icss.push(ParsedIcssFact { kind, name, range });
444    }
445}