1use cstree::text::TextRange;
7use omena_syntax::SyntaxKind;
8use std::collections::BTreeSet;
9
10use crate::{
11 Token, find_selector_block_after_header, is_selector_combinator_kind,
12 matching_right_paren_from_range, next_non_trivia_token_after_range,
13 next_non_trivia_token_until, previous_non_trivia_token, selector_component_can_end,
14 selector_component_can_start, skip_statement_or_unmatched_boundary, skip_trivia_tokens,
15 style_wrapper_at_rule, token_index_by_range,
16};
17
18use super::StyleFactSink;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ParsedSelectorFact {
22 pub kind: ParsedSelectorFactKind,
23 pub name: String,
24 pub range: TextRange,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28pub enum ParsedSelectorFactKind {
29 Class,
30 Id,
31 Placeholder,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub(crate) struct SelectorBranch {
36 pub(crate) name: String,
37 pub(crate) range: TextRange,
38 pub(crate) bare_suffix_base: bool,
39}
40
41pub(crate) fn collect_selector_facts_from_sink(
42 sink: &StyleFactSink<'_>,
43) -> Vec<ParsedSelectorFact> {
44 let mut selectors = Vec::new();
45 let mut seen = BTreeSet::new();
46 let tokens = sink.tokens();
47 collect_selector_facts_in_range(
48 tokens,
49 0,
50 tokens.len(),
51 &[],
52 None,
53 &mut seen,
54 &mut selectors,
55 );
56 selectors
57}
58
59fn collect_selector_facts_in_range(
60 tokens: &[Token<'_>],
61 start: usize,
62 end: usize,
63 parent_branches: &[SelectorBranch],
64 css_module_scope: Option<&'static str>,
65 seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
66 selectors: &mut Vec<ParsedSelectorFact>,
67) {
68 let mut index = start;
69 while index < end {
70 index = skip_trivia_tokens(tokens, index, end);
71 if index >= end {
72 break;
73 }
74
75 if tokens[index].kind == SyntaxKind::AtKeyword {
76 let block = find_selector_block_after_header(tokens, index, end);
77 if let Some((open, close)) = block {
78 if tokens[index].text == "@nest" {
79 if css_module_scope == Some("global") {
80 collect_selector_facts_in_range(
81 tokens,
82 open + 1,
83 close,
84 &[],
85 css_module_scope,
86 seen,
87 selectors,
88 );
89 } else {
90 let branches =
91 resolve_selector_header(tokens, index + 1, open, parent_branches);
92 push_class_selector_facts_from_header(
93 selectors,
94 seen,
95 tokens,
96 index + 1,
97 open,
98 );
99 for branch in &branches {
100 push_selector_fact(
101 selectors,
102 seen,
103 ParsedSelectorFactKind::Class,
104 branch.name.clone(),
105 branch.range,
106 );
107 }
108 collect_selector_facts_in_range(
109 tokens,
110 open + 1,
111 close,
112 &branches,
113 css_module_scope,
114 seen,
115 selectors,
116 );
117 }
118 } else if style_wrapper_at_rule(tokens[index].text) {
119 collect_selector_facts_in_range(
120 tokens,
121 open + 1,
122 close,
123 parent_branches,
124 css_module_scope,
125 seen,
126 selectors,
127 );
128 }
129 index = close + 1;
130 } else {
131 index = skip_statement_or_unmatched_boundary(tokens, index, end);
132 }
133 continue;
134 }
135
136 let Some((open, close)) = find_selector_block_after_header(tokens, index, end) else {
137 index = skip_statement_or_unmatched_boundary(tokens, index, end);
138 continue;
139 };
140
141 let effective_scope = css_module_scope
142 .or_else(|| css_module_block_scope_marker_in_header(tokens, index, open));
143 if effective_scope == Some("global") {
144 collect_selector_facts_in_range(
145 tokens,
146 open + 1,
147 close,
148 &[],
149 effective_scope,
150 seen,
151 selectors,
152 );
153 } else {
154 let branches = resolve_selector_header(tokens, index, open, parent_branches);
155 push_class_selector_facts_from_header(selectors, seen, tokens, index, open);
156 for branch in &branches {
157 push_selector_fact(
158 selectors,
159 seen,
160 ParsedSelectorFactKind::Class,
161 branch.name.clone(),
162 branch.range,
163 );
164 }
165 for id in collect_id_selector_facts_from_header(tokens, index, open)
166 .into_iter()
167 .chain(collect_local_function_id_selector_facts_from_header(
168 tokens, index, open,
169 ))
170 {
171 push_selector_fact(selectors, seen, ParsedSelectorFactKind::Id, id.0, id.1);
172 }
173 for placeholder in collect_placeholder_selector_facts_from_header(tokens, index, open) {
174 push_selector_fact(
175 selectors,
176 seen,
177 ParsedSelectorFactKind::Placeholder,
178 placeholder.0,
179 placeholder.1,
180 );
181 }
182
183 collect_selector_facts_in_range(
184 tokens,
185 open + 1,
186 close,
187 &branches,
188 effective_scope,
189 seen,
190 selectors,
191 );
192 }
193 index = close + 1;
194 }
195}
196
197fn push_class_selector_facts_from_header(
198 selectors: &mut Vec<ParsedSelectorFact>,
199 seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
200 tokens: &[Token<'_>],
201 start: usize,
202 end: usize,
203) {
204 for (name, range) in collect_class_selector_names_from_header(tokens, start, end) {
205 push_selector_fact(selectors, seen, ParsedSelectorFactKind::Class, name, range);
206 }
207}
208
209fn push_selector_fact(
210 selectors: &mut Vec<ParsedSelectorFact>,
211 seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
212 kind: ParsedSelectorFactKind,
213 name: String,
214 range: TextRange,
215) {
216 if seen.insert((
217 kind,
218 name.clone(),
219 u32::from(range.start()),
220 u32::from(range.end()),
221 )) {
222 selectors.push(ParsedSelectorFact { kind, name, range });
223 }
224}
225
226pub(crate) fn resolve_selector_header(
227 tokens: &[Token<'_>],
228 start: usize,
229 end: usize,
230 parent_branches: &[SelectorBranch],
231) -> Vec<SelectorBranch> {
232 split_selector_groups(tokens, start, end)
233 .into_iter()
234 .flat_map(|(group_start, group_end)| {
235 resolve_selector_group(tokens, group_start, group_end, parent_branches)
236 })
237 .collect()
238}
239
240fn resolve_selector_group(
241 tokens: &[Token<'_>],
242 start: usize,
243 end: usize,
244 parent_branches: &[SelectorBranch],
245) -> Vec<SelectorBranch> {
246 if let Some(mut local_names) = collect_local_function_selector_names(tokens, start, end) {
247 local_names.extend(collect_class_selector_names_from_header(tokens, start, end));
248 let bare_suffix_base = parent_branches.is_empty() && local_names.len() == 1;
249 return local_names
250 .into_iter()
251 .map(|(name, range)| SelectorBranch {
252 name,
253 range,
254 bare_suffix_base,
255 })
256 .collect();
257 }
258
259 let (tail_start, tail_end) = selector_group_tail_range(tokens, start, end);
260 let tail_start = skip_trivia_tokens(tokens, tail_start, tail_end);
261
262 if let Some((suffix, range)) = ampersand_suffix_selector(tokens, tail_start, tail_end) {
263 let bases: Vec<&SelectorBranch> = if parent_branches.is_empty() {
264 Vec::new()
265 } else {
266 parent_branches
267 .iter()
268 .filter(|parent| parent.bare_suffix_base)
269 .collect()
270 };
271 return bases
272 .into_iter()
273 .map(|parent| SelectorBranch {
274 name: format!("{}{}", parent.name, suffix),
275 range,
276 bare_suffix_base: parent.bare_suffix_base,
277 })
278 .collect();
279 }
280
281 let class_names = collect_class_selector_names_from_header(tokens, tail_start, tail_end);
282 if class_names.is_empty() {
283 return Vec::new();
284 }
285
286 let bare_suffix_base = parent_branches.is_empty()
287 && class_names.len() == 1
288 && is_bare_class_selector_group(tokens, tail_start, tail_end);
289 class_names
290 .into_iter()
291 .map(|(name, range)| SelectorBranch {
292 name,
293 range,
294 bare_suffix_base,
295 })
296 .collect()
297}
298
299fn is_bare_class_selector_group(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
300 let dot_index = skip_trivia_tokens(tokens, start, end);
301 if tokens.get(dot_index).map(|token| token.kind) != Some(SyntaxKind::Dot) {
302 return false;
303 }
304 let name_index = skip_trivia_tokens(tokens, dot_index + 1, end);
305 if !tokens.get(name_index).is_some_and(|token| {
306 matches!(
307 token.kind,
308 SyntaxKind::Ident | SyntaxKind::CustomPropertyName
309 )
310 }) {
311 return false;
312 }
313 skip_trivia_tokens(tokens, name_index + 1, end) >= end
314}
315
316pub(crate) fn split_selector_groups(
317 tokens: &[Token<'_>],
318 start: usize,
319 end: usize,
320) -> Vec<(usize, usize)> {
321 let mut groups = Vec::new();
322 let mut group_start = start;
323 let mut paren_depth = 0usize;
324 let mut bracket_depth = 0usize;
325 let mut index = start;
326 while index < end {
327 match tokens[index].kind {
328 SyntaxKind::LeftParen => paren_depth += 1,
329 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
330 SyntaxKind::LeftBracket => bracket_depth += 1,
331 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
332 SyntaxKind::Comma if paren_depth == 0 && bracket_depth == 0 => {
333 groups.push((group_start, index));
334 group_start = index + 1;
335 }
336 _ => {}
337 }
338 index += 1;
339 }
340 groups.push((group_start, end));
341 groups
342}
343
344fn selector_group_tail_range(tokens: &[Token<'_>], start: usize, end: usize) -> (usize, usize) {
345 let mut paren_depth = 0usize;
346 let mut bracket_depth = 0usize;
347 let mut tail_start = start;
348 let mut index = start;
349 while index < end {
350 match tokens[index].kind {
351 SyntaxKind::LeftParen => paren_depth += 1,
352 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
353 SyntaxKind::LeftBracket => bracket_depth += 1,
354 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
355 kind if paren_depth == 0 && bracket_depth == 0 && is_selector_combinator_kind(kind) => {
356 tail_start = index + 1;
357 }
358 SyntaxKind::Whitespace if paren_depth == 0 && bracket_depth == 0 => {
359 let previous = previous_non_trivia_token(tokens, start, index);
360 let next = next_non_trivia_token_until(tokens, index + 1, end);
361 if previous.is_some_and(|token| selector_component_can_end(token.kind))
362 && next.is_some_and(|token| selector_component_can_start(token.kind))
363 {
364 tail_start = index + 1;
365 }
366 }
367 _ => {}
368 }
369 index += 1;
370 }
371 (tail_start, end)
372}
373
374fn ampersand_suffix_selector(
375 tokens: &[Token<'_>],
376 start: usize,
377 end: usize,
378) -> Option<(String, TextRange)> {
379 let ampersand_index = skip_trivia_tokens(tokens, start, end);
380 if tokens.get(ampersand_index)?.kind != SyntaxKind::Ampersand {
381 return None;
382 }
383 let suffix = next_non_trivia_token_until(tokens, ampersand_index + 1, end)?;
384 if matches!(
385 suffix.kind,
386 SyntaxKind::Ident | SyntaxKind::CustomPropertyName
387 ) {
388 return Some((suffix.text.to_string(), suffix.range));
389 }
390 None
391}
392
393pub(crate) fn collect_class_selector_names_from_header(
394 tokens: &[Token<'_>],
395 start: usize,
396 end: usize,
397) -> Vec<(String, TextRange)> {
398 let mut names = Vec::new();
399 let mut index = start;
400 let mut paren_depth = 0usize;
401 let mut bracket_depth = 0usize;
402 while index < end {
403 match tokens[index].kind {
404 SyntaxKind::LeftParen => paren_depth += 1,
405 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
406 SyntaxKind::LeftBracket => bracket_depth += 1,
407 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
408 _ => {}
409 }
410 if paren_depth == 0
411 && bracket_depth == 0
412 && tokens[index].kind == SyntaxKind::Dot
413 && let Some(name) = next_non_trivia_token_until(tokens, index + 1, end)
414 && matches!(
415 name.kind,
416 SyntaxKind::Ident | SyntaxKind::CustomPropertyName
417 )
418 {
419 names.push((name.text.to_string(), name.range));
420 }
421 index += 1;
422 }
423 names
424}
425
426fn collect_local_function_selector_names(
427 tokens: &[Token<'_>],
428 start: usize,
429 end: usize,
430) -> Option<Vec<(String, TextRange)>> {
431 let colon_index = skip_trivia_tokens(tokens, start, end);
432 if tokens.get(colon_index)?.kind != SyntaxKind::Colon {
433 return None;
434 }
435 let ident = next_non_trivia_token_until(tokens, colon_index + 1, end)?;
436 if ident.kind != SyntaxKind::Ident || ident.text != "local" {
437 return None;
438 }
439 let open_index = skip_trivia_tokens(tokens, colon_index + 2, end);
440 if tokens.get(open_index)?.kind != SyntaxKind::LeftParen {
441 return None;
442 }
443 Some(collect_class_selector_names_from_header(
444 tokens,
445 open_index + 1,
446 end.saturating_sub(1),
447 ))
448}
449
450fn collect_local_function_id_selector_facts_from_header(
451 tokens: &[Token<'_>],
452 start: usize,
453 end: usize,
454) -> Vec<(String, TextRange)> {
455 let mut ids = Vec::new();
456 let mut index = start;
457 while index < end {
458 if tokens[index].kind == SyntaxKind::Colon
459 && let Some(scope) = next_non_trivia_token_until(tokens, index + 1, end)
460 && scope.kind == SyntaxKind::Ident
461 && scope.text == "local"
462 && let Some(open) = next_non_trivia_token_after_range(tokens, scope.range, end)
463 && open.kind == SyntaxKind::LeftParen
464 && let Some(close) = matching_right_paren_from_range(tokens, open.range, end)
465 {
466 ids.extend(collect_id_selector_facts_from_header(
467 tokens,
468 token_index_by_range(tokens, open.range).map_or(index + 1, |value| value + 1),
469 close,
470 ));
471 index = close.saturating_add(1);
472 continue;
473 }
474 index += 1;
475 }
476 ids
477}
478
479pub(crate) fn css_module_block_scope_marker_in_header(
480 tokens: &[Token<'_>],
481 start: usize,
482 end: usize,
483) -> Option<&'static str> {
484 if next_non_trivia_token_until(tokens, start, end)
485 .is_some_and(|token| token.kind == SyntaxKind::AtKeyword)
486 {
487 return None;
488 }
489
490 css_module_scope_marker_after_colon(tokens, start, end)
491 .filter(|_| !css_module_scope_marker_is_function(tokens, start, end))
492}
493
494pub(crate) fn css_module_header_is_global_only(
495 tokens: &[Token<'_>],
496 start: usize,
497 end: usize,
498) -> bool {
499 if next_non_trivia_token_until(tokens, start, end)
500 .is_some_and(|token| token.kind == SyntaxKind::AtKeyword)
501 {
502 return false;
503 }
504 css_module_header_contains_scope(tokens, start, end, "global")
505 && collect_class_selector_names_from_header(tokens, start, end).is_empty()
506 && collect_local_function_selector_names(tokens, start, end)
507 .map(|names| names.is_empty())
508 .unwrap_or(true)
509}
510
511fn css_module_header_contains_scope(
512 tokens: &[Token<'_>],
513 start: usize,
514 end: usize,
515 expected_scope: &str,
516) -> bool {
517 let mut index = start;
518 while index < end {
519 if tokens[index].kind == SyntaxKind::Colon
520 && let Some(scope) = next_non_trivia_token_until(tokens, index + 1, end)
521 && scope.kind == SyntaxKind::Ident
522 && scope.text == expected_scope
523 {
524 return true;
525 }
526 index += 1;
527 }
528 false
529}
530
531fn css_module_scope_marker_after_colon(
532 tokens: &[Token<'_>],
533 start: usize,
534 end: usize,
535) -> Option<&'static str> {
536 let colon = skip_trivia_tokens(tokens, start, end);
537 if tokens.get(colon)?.kind != SyntaxKind::Colon {
538 return None;
539 }
540 let scope = next_non_trivia_token_until(tokens, colon + 1, end)?;
541 if scope.kind != SyntaxKind::Ident {
542 return None;
543 }
544 match scope.text {
545 "global" => Some("global"),
546 "local" => Some("local"),
547 _ => None,
548 }
549}
550
551fn css_module_scope_marker_is_function(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
552 let colon = skip_trivia_tokens(tokens, start, end);
553 let mut index = colon + 1;
554 let Some(scope) = next_non_trivia_token_until(tokens, index, end) else {
555 return false;
556 };
557 while index < end {
558 if tokens[index].range == scope.range {
559 break;
560 }
561 index += 1;
562 }
563 let Some(next) = next_non_trivia_token_until(tokens, index + 1, end) else {
564 return false;
565 };
566 scope.kind == SyntaxKind::Ident && next.kind == SyntaxKind::LeftParen
567}
568
569fn collect_id_selector_facts_from_header(
570 tokens: &[Token<'_>],
571 start: usize,
572 end: usize,
573) -> Vec<(String, TextRange)> {
574 let mut names = Vec::new();
575 let mut index = start;
576 let mut paren_depth = 0usize;
577 let mut bracket_depth = 0usize;
578 while index < end {
579 match tokens[index].kind {
580 SyntaxKind::LeftParen => paren_depth += 1,
581 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
582 SyntaxKind::LeftBracket => bracket_depth += 1,
583 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
584 _ => {}
585 }
586 let token = tokens[index];
587 if paren_depth == 0 && bracket_depth == 0 && token.kind == SyntaxKind::Hash {
588 names.push((token.text.trim_start_matches('#').to_string(), token.range));
589 }
590 index += 1;
591 }
592 names
593}
594
595fn collect_placeholder_selector_facts_from_header(
596 tokens: &[Token<'_>],
597 start: usize,
598 end: usize,
599) -> Vec<(String, TextRange)> {
600 let mut names = Vec::new();
601 let mut index = start;
602 let mut paren_depth = 0usize;
603 let mut bracket_depth = 0usize;
604 while index < end {
605 match tokens[index].kind {
606 SyntaxKind::LeftParen => paren_depth += 1,
607 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
608 SyntaxKind::LeftBracket => bracket_depth += 1,
609 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
610 _ => {}
611 }
612 let token = tokens[index];
613 if paren_depth == 0 && bracket_depth == 0 && token.kind == SyntaxKind::ScssPlaceholder {
614 names.push((token.text.trim_start_matches('%').to_string(), token.range));
615 }
616 index += 1;
617 }
618 names
619}