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