1use 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}