Skip to main content

css_module_lexer/
dependencies.rs

1//! Dependency parser and lexer visitor implementation.
2
3use rustc_hash::FxHashSet;
4use smallvec::SmallVec;
5
6use crate::{
7  HandleWarning, Lexer, Pos,
8  css_syntax::{
9    MAX_CSS_KEYWORD_LEN, dashed_ident_name, dashed_ident_name_start, decode_css_keyword,
10    is_css_modules_magic_comment, is_css_modules_pure_magic_comment, is_css_space_byte,
11    is_css_white_space_char, lowercase_ascii_keyword, strip_vendor_prefix, trim_css_whitespace,
12  },
13  dependency_types::{
14    Dependency, DependencyContext, Mode, Range, UrlRangeKind, ValueAtRuleImportItem, Warning,
15    WarningKind,
16  },
17  lexer::{LexerVisitor, Token, TokenFlags, TokenKind, TokenStream},
18};
19
20/// Collects dashed identifiers while the dependency parser is in local mode.
21#[derive(Debug, Default)]
22pub struct DashedIdentCollector {
23  occurrences: Vec<Range>,
24  enabled: bool,
25}
26
27impl DashedIdentCollector {
28  #[inline(always)]
29  fn set_enabled(&mut self, enabled: bool) {
30    self.enabled = enabled;
31  }
32
33  fn reserve(&mut self, additional: usize) {
34    self.occurrences.reserve(additional);
35  }
36
37  fn take(&mut self) -> Vec<Range> {
38    std::mem::take(&mut self.occurrences)
39  }
40
41  fn discard_last(&mut self, range: Range) {
42    if self.occurrences.last() == Some(&range) {
43      self.occurrences.pop();
44    }
45  }
46}
47
48impl LexerVisitor for DashedIdentCollector {
49  #[inline(always)]
50  fn visit_ident(&mut self, name: &str, range: Range) {
51    if self.enabled
52      && let Some(name_start) = dashed_ident_name_start(name)
53    {
54      self
55        .occurrences
56        .push(Range::new(range.start + name_start as Pos, range.end));
57    }
58  }
59}
60
61type DependencyLexer<'s> = Lexer<'s, DashedIdentCollector>;
62type DependencyTokenStream<'a, 's> = TokenStream<'a, 's, DashedIdentCollector>;
63
64#[derive(Debug)]
65enum Scope<'s> {
66  TopLevel,
67  InBlock,
68  InAtImport(ImportData<'s>),
69  AtImportInvalid,
70  AtNamespaceInvalid,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74enum ScanContext {
75  TopLevel,
76  BlockItem,
77  Selector,
78  DeclarationName,
79  GenericValue,
80  SpecialValue(PropertyKind),
81  AtRule,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub(crate) enum PropertyKind {
86  Generic,
87  Animation,
88  ListStyle,
89  FontPalette,
90  Container,
91  Grid,
92  Composes,
93  CustomProperty,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97enum AtRuleKind {
98  Namespace,
99  Import,
100  Charset,
101  Value,
102  Keyframes,
103  Container,
104  Function,
105  Property,
106  CounterStyle,
107  FontPaletteValues,
108  Scope,
109  Other,
110}
111
112impl ScanContext {
113  fn for_property(property: PropertyKind) -> Self {
114    if property == PropertyKind::Generic {
115      Self::GenericValue
116    } else {
117      Self::SpecialValue(property)
118    }
119  }
120}
121
122#[derive(Debug)]
123struct ImportData<'s> {
124  start: Pos,
125  magic_comments: Option<&'s str>,
126  prelude: ImportPrelude<'s>,
127  url: Option<&'s str>,
128  url_flags: TokenFlags,
129  url_range: Option<Range>,
130  supports: ImportDataSupports<'s>,
131  layer: ImportDataLayer<'s>,
132}
133
134impl ImportData<'_> {
135  pub fn new(start: Pos) -> Self {
136    Self {
137      start,
138      magic_comments: None,
139      prelude: ImportPrelude::default(),
140      url: None,
141      url_flags: TokenFlags::ascii(),
142      url_range: None,
143      supports: ImportDataSupports::None,
144      layer: ImportDataLayer::None,
145    }
146  }
147
148  pub fn in_supports(&self) -> bool {
149    matches!(self.supports, ImportDataSupports::InSupports { .. })
150  }
151
152  pub fn layer_range(&self) -> Option<&Range> {
153    let ImportDataLayer::EndLayer { range, .. } = &self.layer else {
154      return None;
155    };
156    Some(range)
157  }
158
159  pub fn supports_range(&self) -> Option<&Range> {
160    let ImportDataSupports::EndSupports { range, .. } = &self.supports else {
161      return None;
162    };
163    Some(range)
164  }
165}
166
167#[derive(Debug, Default)]
168struct ImportPrelude<'s>(SmallVec<[ImportPreludeNode<'s>; 2]>);
169
170impl<'s> ImportPrelude<'s> {
171  pub fn push(&mut self, node: ImportPreludeNode<'s>) {
172    self.0.push(node);
173  }
174
175  pub fn is_empty(&self) -> bool {
176    self.0.is_empty()
177  }
178
179  pub fn icss_import_url(&self) -> Option<(&'s str, &Range)> {
180    let [ImportPreludeNode::IcssUrlCandidate { name, range }] = self.0.as_slice() else {
181      return None;
182    };
183    Some((name, range))
184  }
185
186  pub fn first_non_url_before(&self, url_range: &Range) -> Option<&Range> {
187    self.0.iter().find_map(|node| {
188      let range = node.range();
189      if range.start >= url_range.start || matches!(node, ImportPreludeNode::Url { .. }) {
190        None
191      } else {
192        Some(range)
193      }
194    })
195  }
196}
197
198#[derive(Debug)]
199enum ImportPreludeNode<'s> {
200  IcssUrlCandidate { name: &'s str, range: Range },
201  Url { range: Range },
202  Layer { range: Range },
203  Supports { range: Range },
204  Other { range: Range },
205}
206
207impl ImportPreludeNode<'_> {
208  fn range(&self) -> &Range {
209    match self {
210      Self::IcssUrlCandidate { range, .. }
211      | Self::Url { range }
212      | Self::Layer { range }
213      | Self::Supports { range }
214      | Self::Other { range } => range,
215    }
216  }
217}
218
219#[derive(Debug)]
220enum ImportDataSupports<'s> {
221  None,
222  InSupports,
223  EndSupports { value: &'s str, range: Range },
224}
225
226#[derive(Debug)]
227enum ImportDataLayer<'s> {
228  None,
229  EndLayer { value: &'s str, range: Range },
230}
231
232#[derive(Debug, Default)]
233struct BalancedStack(SmallVec<[BalancedItem; 3]>);
234
235impl BalancedStack {
236  pub fn len(&self) -> usize {
237    self.0.len()
238  }
239
240  pub fn last(&self) -> Option<&BalancedItem> {
241    self.0.last()
242  }
243
244  pub fn is_empty(&self) -> bool {
245    self.0.is_empty()
246  }
247
248  pub fn push(&mut self, item: BalancedItem, mode_data: Option<&mut ModeData>) {
249    if let Some(mode_data) = mode_data {
250      if item.kind.is_mode_local() {
251        mode_data.set_current_mode(Mode::Local);
252      } else if item.kind.is_mode_global() {
253        mode_data.set_current_mode(Mode::Global);
254      }
255
256      if item.kind.is_mode_function() {
257        mode_data.inside_mode_function += 1;
258      } else if item.kind.is_mode_class() {
259        mode_data.inside_mode_class += 1;
260      }
261    }
262    self.0.push(item);
263  }
264
265  pub fn pop(&mut self, mode_data: Option<&mut ModeData>) -> Option<BalancedItem> {
266    let item = self.0.pop()?;
267    if let Some(mode_data) = mode_data {
268      if item.kind.is_mode_function() {
269        mode_data.inside_mode_function -= 1;
270      } else if item.kind.is_mode_class() {
271        mode_data.inside_mode_class -= 1;
272      }
273      self.update_current_mode(mode_data);
274    }
275    Some(item)
276  }
277
278  pub fn pop_without_moda_data(&mut self) -> Option<BalancedItem> {
279    self.0.pop()
280  }
281
282  pub fn pop_mode_pseudo_class(&mut self, mode_data: &mut ModeData) {
283    loop {
284      if let Some(last) = self.0.last()
285        && matches!(
286          last.kind,
287          BalancedItemKind::LocalClass | BalancedItemKind::GlobalClass
288        )
289      {
290        mode_data.inside_mode_class -= 1;
291        self.0.pop();
292        continue;
293      }
294      break;
295    }
296    self.update_current_mode(mode_data);
297  }
298
299  pub fn update_current_mode(&self, mode_data: &mut ModeData) {
300    mode_data.set_current_mode(self.topmost_mode(mode_data));
301  }
302
303  pub fn update_property_mode(&self, mode_data: &mut ModeData) {
304    mode_data.set_property_mode(self.topmost_mode(mode_data));
305  }
306
307  fn topmost_mode(&self, mode_data: &ModeData) -> Mode {
308    let mut iter = self.0.iter();
309    loop {
310      if let Some(last) = iter.next_back() {
311        if matches!(
312          last.kind,
313          BalancedItemKind::LocalFn | BalancedItemKind::LocalClass
314        ) {
315          return Mode::Local;
316        } else if matches!(
317          last.kind,
318          BalancedItemKind::GlobalFn | BalancedItemKind::GlobalClass
319        ) {
320          return Mode::Global;
321        }
322      } else {
323        return mode_data.default_mode();
324      }
325    }
326  }
327}
328
329#[derive(Debug)]
330struct BalancedItem {
331  kind: BalancedItemKind,
332  range: Range,
333  magic_comments: Option<Range>,
334}
335
336impl BalancedItem {
337  pub fn new(name: &str, flags: TokenFlags, start: Pos, end: Pos) -> Self {
338    let mut normalized = [0; MAX_CSS_KEYWORD_LEN];
339    let kind = if flags.has_escape() {
340      decode_css_keyword(name, &mut normalized)
341        .map_or(BalancedItemKind::Other, BalancedItemKind::new)
342    } else {
343      lowercase_ascii_keyword(name, &mut normalized)
344        .map_or(BalancedItemKind::Other, BalancedItemKind::new)
345    };
346    Self {
347      kind,
348      range: Range::new(start, end),
349      magic_comments: None,
350    }
351  }
352
353  pub fn new_normalized(name: &str, start: Pos, end: Pos) -> Self {
354    Self {
355      kind: BalancedItemKind::new(name),
356      range: Range::new(start, end),
357      magic_comments: None,
358    }
359  }
360
361  pub fn new_other(start: Pos, end: Pos) -> Self {
362    Self {
363      kind: BalancedItemKind::Other,
364      range: Range::new(start, end),
365      magic_comments: None,
366    }
367  }
368
369  pub fn new_curly(start: Pos, end: Pos) -> Self {
370    Self {
371      kind: BalancedItemKind::Curly,
372      range: Range::new(start, end),
373      magic_comments: None,
374    }
375  }
376}
377
378#[derive(Debug)]
379enum BalancedItemKind {
380  Url,
381  ImageSet,
382  Layer,
383  Supports,
384  PaletteMix,
385  LocalFn,
386  GlobalFn,
387  LocalClass,
388  GlobalClass,
389  Curly,
390  Other,
391}
392
393impl BalancedItemKind {
394  pub fn new(name: &str) -> Self {
395    match name {
396      "url(" => Self::Url,
397      "image-set(" => Self::ImageSet,
398      _ if strip_vendor_prefix(name) == Some("image-set(") => Self::ImageSet,
399      "layer(" => Self::Layer,
400      "supports(" => Self::Supports,
401      "palette-mix(" => Self::PaletteMix,
402      ":local(" => Self::LocalFn,
403      ":global(" => Self::GlobalFn,
404      ":local" => Self::LocalClass,
405      ":global" => Self::GlobalClass,
406      _ => Self::Other,
407    }
408  }
409
410  pub fn is_mode_local(&self) -> bool {
411    matches!(self, Self::LocalFn | Self::LocalClass)
412  }
413
414  pub fn is_mode_global(&self) -> bool {
415    matches!(self, Self::GlobalFn | Self::GlobalClass)
416  }
417
418  pub fn is_mode_function(&self) -> bool {
419    matches!(self, Self::LocalFn | Self::GlobalFn)
420  }
421
422  pub fn is_mode_class(&self) -> bool {
423    matches!(self, Self::LocalClass | Self::GlobalClass)
424  }
425}
426
427fn preceding_comment_range(input: &str) -> Option<Range> {
428  let bytes = input.as_bytes();
429  let mut cursor = bytes.len();
430  let end = cursor as Pos;
431  let mut start = None;
432
433  loop {
434    while cursor > 0 && is_css_space_byte(bytes[cursor - 1]) {
435      cursor -= 1;
436    }
437    if cursor < 2 || &bytes[cursor - 2..cursor] != b"*/" {
438      break;
439    }
440    let Some(comment_start) = input[..cursor - 2].rfind("/*") else {
441      break;
442    };
443    start = Some(comment_start as Pos);
444    cursor = comment_start;
445  }
446
447  start.map(|start| Range::new(start, end))
448}
449
450fn trivia_only(input: &str) -> bool {
451  if input.is_empty() {
452    return false;
453  }
454  let bytes = input.as_bytes();
455  let mut position = 0;
456  while position < bytes.len() {
457    if is_css_space_byte(bytes[position]) {
458      position += 1;
459      continue;
460    }
461    if position + 1 < bytes.len() && bytes[position] == b'/' && bytes[position + 1] == b'*' {
462      position += 2;
463      while position + 1 < bytes.len() && !(bytes[position] == b'*' && bytes[position + 1] == b'/')
464      {
465        position += 1;
466      }
467      if position + 1 >= bytes.len() {
468        return false;
469      }
470      position += 2;
471      continue;
472    }
473    return false;
474  }
475  true
476}
477
478fn token_text(input: &str, token: Token) -> &str {
479  Lexer::slice_range(input, &token.range).unwrap_or("")
480}
481
482fn is_ascii_keyword(name: &str, expected: &str) -> bool {
483  name.eq_ignore_ascii_case(expected)
484}
485
486fn is_open_token(kind: TokenKind) -> bool {
487  matches!(
488    kind,
489    TokenKind::Function
490      | TokenKind::LeftParenthesis
491      | TokenKind::LeftSquareBracket
492      | TokenKind::LeftCurlyBracket
493  )
494}
495
496fn is_close_token(kind: TokenKind) -> bool {
497  matches!(
498    kind,
499    TokenKind::RightParenthesis | TokenKind::RightSquareBracket | TokenKind::RightCurlyBracket
500  )
501}
502
503fn ident_like_range(token: Token) -> Option<Range> {
504  match token.kind {
505    TokenKind::Ident => Some(token.range),
506    TokenKind::Function => Some(token.value_range),
507    _ => None,
508  }
509}
510
511/// Token-aligned split of an import item by a top-level colon or `as` ident.
512/// Names are delimited by the surrounding significant tokens, so comments and
513/// whitespace at the split points stay out of the names.
514#[derive(Debug, Clone, Copy)]
515struct ValueAtRuleSplit {
516  split: Pos,
517  end: Pos,
518  prev_end: Pos,
519  next_start: Option<Pos>,
520}
521
522/// Streaming state for a single `@value` at-rule. Tokens are consumed one at a
523/// time; completed import items are written into the [`DependencyContext`] side
524/// table immediately instead of collecting a token buffer first.
525struct ValueAtRuleStream<'s> {
526  input: &'s str,
527  depth: u32,
528  params_end: Pos,
529  first_significant: Option<(Pos, Pos)>,
530  significant_count: u32,
531  first_colon: Option<ValueAtRuleSplit>,
532  first_colon_tokens_after: u32,
533  item_start: Option<Pos>,
534  item_end: Pos,
535  item_colon: Option<ValueAtRuleSplit>,
536  item_as: Option<ValueAtRuleSplit>,
537  last_significant: Option<Token>,
538  penultimate_significant: Option<Token>,
539  from_pos: Option<Pos>,
540  from_prev_end: Option<Pos>,
541}
542
543impl<'s> ValueAtRuleStream<'s> {
544  fn new(input: &'s str) -> Self {
545    Self {
546      input,
547      depth: 0,
548      params_end: 0,
549      first_significant: None,
550      significant_count: 0,
551      first_colon: None,
552      first_colon_tokens_after: 0,
553      item_start: None,
554      item_end: 0,
555      item_colon: None,
556      item_as: None,
557      last_significant: None,
558      penultimate_significant: None,
559      from_pos: None,
560      from_prev_end: None,
561    }
562  }
563
564  fn push(&mut self, context: &mut DependencyContext<'s>, token: Token) {
565    if matches!(token.kind, TokenKind::Comment | TokenKind::BadComment) {
566      self.params_end = token.range.end;
567      return;
568    }
569    self.params_end = token.range.end;
570    if self.first_significant.is_none() {
571      self.first_significant = Some((token.range.start, token.range.end));
572    }
573    self.significant_count += 1;
574    let had_first_colon = self.first_colon.is_some();
575
576    self.depth = (self.depth + u32::from(is_open_token(token.kind)))
577      .saturating_sub(u32::from(is_close_token(token.kind)));
578    let at_top = self.depth == 0;
579    let is_ident = token.kind == TokenKind::Ident;
580    let text = if is_ident {
581      self
582        .input
583        .get(token.range.start as usize..token.range.end as usize)
584        .unwrap_or("")
585    } else {
586      ""
587    };
588    if at_top {
589      if token.kind == TokenKind::Colon {
590        let split = ValueAtRuleSplit {
591          split: token.range.start,
592          end: token.range.end,
593          prev_end: self
594            .last_significant
595            .map_or(token.range.start, |previous| previous.range.end),
596          next_start: None,
597        };
598        if self.first_colon.is_none() {
599          self.first_colon = Some(split);
600        }
601        if self.item_colon.is_none() {
602          self.item_colon = Some(split);
603        }
604      }
605      if is_ident && text.eq_ignore_ascii_case("as") {
606        self.item_as = Some(ValueAtRuleSplit {
607          split: token.range.start,
608          end: token.range.end,
609          prev_end: self
610            .last_significant
611            .map_or(token.range.start, |previous| previous.range.end),
612          next_start: None,
613        });
614      }
615      if is_ident
616        && text.eq_ignore_ascii_case("from")
617        && let Some(previous) = self.last_significant
618      {
619        let gap = self
620          .input
621          .get(previous.range.end as usize..token.range.start as usize)
622          .unwrap_or("");
623        if trivia_only(gap) {
624          self.from_pos = Some(token.range.start);
625          self.from_prev_end = Some(previous.range.end);
626        }
627      }
628      self.penultimate_significant = self.last_significant;
629      self.last_significant = Some(token);
630    }
631    if had_first_colon {
632      self.first_colon_tokens_after += 1;
633    }
634
635    if token.kind == TokenKind::Comma && at_top {
636      self.finish_item(context, self.item_end);
637      self.item_start = None;
638      self.item_colon = None;
639      self.item_as = None;
640    } else {
641      if self.item_start.is_none() {
642        self.item_start = Some(token.range.start);
643      }
644      self.item_end = token.range.end;
645      if let Some(split) = self.item_colon.as_mut()
646        && split.next_start.is_none()
647        && split.split != token.range.start
648      {
649        split.next_start = Some(token.range.start);
650      }
651      if let Some(split) = self.item_as.as_mut()
652        && split.next_start.is_none()
653        && split.split != token.range.start
654      {
655        split.next_start = Some(token.range.start);
656      }
657    }
658  }
659
660  fn finish_item(&mut self, context: &mut DependencyContext<'s>, end: Pos) {
661    let Some(item_start) = self.item_start else {
662      return;
663    };
664    if end.saturating_sub(item_start) >= 2
665      && self.input.as_bytes()[item_start as usize] == b'('
666      && self.input.as_bytes()[end as usize - 1] == b')'
667    {
668      self.parse_paren_items(context, item_start + 1, end - 1);
669    } else {
670      let item = self.build_item(item_start, end, self.item_colon, self.item_as);
671      if !item.local_name().is_empty() || !item.import_name().is_empty() {
672        context.push_value_at_rule_import_item(item);
673      }
674    }
675  }
676
677  fn build_item(
678    &self,
679    start: Pos,
680    end: Pos,
681    colon: Option<ValueAtRuleSplit>,
682    as_split: Option<ValueAtRuleSplit>,
683  ) -> ValueAtRuleImportItem<'s> {
684    let slice = |a: Pos, b: Pos| -> &'s str {
685      if a >= b {
686        ""
687      } else {
688        &self.input[a as usize..b as usize]
689      }
690    };
691    if let Some(split) = colon {
692      return ValueAtRuleImportItem::new(
693        slice(start, split.prev_end),
694        split
695          .next_start
696          .map_or("", |next_start| slice(next_start, end)),
697      );
698    }
699    if let Some(split) = as_split {
700      let import_name = slice(start, split.prev_end);
701      let local_name = split
702        .next_start
703        .map_or("", |next_start| slice(next_start, end));
704      if !import_name.is_empty() && !local_name.is_empty() {
705        return ValueAtRuleImportItem::new(local_name, import_name);
706      }
707    }
708    let value = slice(start, end);
709    ValueAtRuleImportItem::new(value, value)
710  }
711
712  /// Re-parses a parenthesized item: the inner content was not streamed
713  /// token-by-token, so it is tokenized once more and split at depth-zero
714  /// commas, mirroring the legacy aligned-token behavior.
715  fn parse_paren_items(&mut self, context: &mut DependencyContext<'s>, start: Pos, end: Pos) {
716    let slice = &self.input[start as usize..end as usize];
717    let mut lexer = Lexer::new(slice, ());
718    let mut tokens: SmallVec<[Token; 8]> = SmallVec::new();
719    loop {
720      let token = lexer.next_token();
721      if token.kind == TokenKind::Eof {
722        break;
723      }
724      if matches!(
725        token.kind,
726        TokenKind::Comment | TokenKind::BadComment | TokenKind::WhiteSpace
727      ) {
728        continue;
729      }
730      tokens.push(token);
731    }
732    let mut item_start = 0;
733    let mut item_depth = 0u32;
734    for (index, token) in tokens.iter().copied().enumerate() {
735      if is_close_token(token.kind) {
736        item_depth = item_depth.saturating_sub(1);
737      }
738      if token.kind == TokenKind::Comma && item_depth == 0 {
739        self.push_parsed_item(context, &tokens, item_start, index, slice);
740        item_start = index + 1;
741      }
742      if is_open_token(token.kind) {
743        item_depth += 1;
744      }
745    }
746    self.push_parsed_item(context, &tokens, item_start, tokens.len(), slice);
747  }
748
749  fn push_parsed_item(
750    &mut self,
751    context: &mut DependencyContext<'s>,
752    tokens: &[Token],
753    start: usize,
754    end: usize,
755    slice: &'s str,
756  ) {
757    let mut depth = 0u32;
758    let mut colon_index = None;
759    let mut as_index = None;
760    for (index, token) in tokens[start..end].iter().copied().enumerate() {
761      let index = start + index;
762      if is_close_token(token.kind) {
763        depth = depth.saturating_sub(1);
764      }
765      if depth == 0 {
766        if token.kind == TokenKind::Colon && colon_index.is_none() {
767          colon_index = Some(index);
768        }
769        if token.kind == TokenKind::Ident && token_text(slice, token).eq_ignore_ascii_case("as") {
770          as_index = Some(index);
771        }
772      }
773      if is_open_token(token.kind) {
774        depth += 1;
775      }
776    }
777    let span = |a: usize, b: usize| -> &'s str {
778      if a >= b {
779        ""
780      } else {
781        &slice[tokens[a].range.start as usize..tokens[b - 1].range.end as usize]
782      }
783    };
784    let item = if let Some(index) = colon_index {
785      ValueAtRuleImportItem::new(span(start, index), span(index + 1, end))
786    } else if let Some(index) = as_index {
787      let import_name = span(start, index);
788      let local_name = span(index + 1, end);
789      if !import_name.is_empty() && !local_name.is_empty() {
790        ValueAtRuleImportItem::new(local_name, import_name)
791      } else {
792        ValueAtRuleImportItem::new("", "")
793      }
794    } else {
795      let value = span(start, end);
796      ValueAtRuleImportItem::new(value, value)
797    };
798    if !item.local_name().is_empty() || !item.import_name().is_empty() {
799      context.push_value_at_rule_import_item(item);
800    }
801  }
802
803  /// Returns the last two significant tokens, or `None` if there are fewer
804  /// than two.
805  fn last_two(&self) -> Option<(Token, Token)> {
806    Some((self.penultimate_significant?, self.last_significant?))
807  }
808}
809
810#[derive(Debug)]
811pub struct ModeData<'s> {
812  default: Mode,
813  current: Mode,
814  property: Mode,
815  resulting_global: Option<Pos>,
816  pure_global: Option<Pos>,
817  pure_no_check: bool,
818  pure_ignore_pending: bool,
819  pure_ignored_block_nesting_level: Option<u32>,
820  composes_local_classes: ComposesLocalClasses<'s>,
821  inside_mode_function: u32,
822  inside_mode_class: u32,
823}
824
825impl ModeData<'_> {
826  pub fn new(default: Mode) -> Self {
827    Self {
828      default,
829      current: default,
830      property: default,
831      resulting_global: None,
832      pure_global: Some(0),
833      pure_no_check: false,
834      pure_ignore_pending: false,
835      pure_ignored_block_nesting_level: None,
836      composes_local_classes: ComposesLocalClasses::default(),
837      inside_mode_function: 0,
838      inside_mode_class: 0,
839    }
840  }
841
842  pub fn is_pure_mode(&self) -> bool {
843    matches!(self.default, Mode::Pure)
844  }
845
846  pub fn mark_pure_ignore(&mut self) {
847    if self.is_pure_mode() {
848      self.pure_ignore_pending = true;
849    }
850  }
851
852  pub fn mark_pure_no_check(&mut self) {
853    if self.is_pure_mode() {
854      self.pure_no_check = true;
855    }
856  }
857
858  pub fn is_pure_check_disabled(&self) -> bool {
859    self.pure_no_check
860      || self.pure_ignore_pending
861      || self.pure_ignored_block_nesting_level.is_some()
862  }
863
864  pub fn enter_block(&mut self, block_nesting_level: u32) {
865    if self.pure_ignore_pending {
866      self.pure_ignore_pending = false;
867      if self.pure_ignored_block_nesting_level.is_none() {
868        self.pure_ignored_block_nesting_level = Some(block_nesting_level);
869      }
870    }
871  }
872
873  pub fn clear_pure_ignore_pending(&mut self) {
874    self.pure_ignore_pending = false;
875  }
876
877  pub fn exit_block(&mut self, block_nesting_level: u32) {
878    if self
879      .pure_ignored_block_nesting_level
880      .is_some_and(|level| block_nesting_level < level)
881    {
882      self.pure_ignored_block_nesting_level = None;
883    }
884  }
885
886  pub fn is_current_local_mode(&self) -> bool {
887    match self.current {
888      Mode::Local | Mode::Pure => true,
889      Mode::Global | Mode::Css => false,
890    }
891  }
892
893  pub fn is_property_local_mode(&self) -> bool {
894    match self.property {
895      Mode::Local | Mode::Pure => true,
896      Mode::Global | Mode::Css => false,
897    }
898  }
899
900  pub fn default_mode(&self) -> Mode {
901    self.default
902  }
903
904  pub fn set_current_mode(&mut self, mode: Mode) {
905    self.current = mode;
906  }
907
908  pub fn set_property_mode(&mut self, mode: Mode) {
909    self.property = mode;
910  }
911
912  pub fn is_inside_mode_function(&self) -> bool {
913    self.inside_mode_function > 0
914  }
915
916  pub fn is_inside_mode_class(&self) -> bool {
917    self.inside_mode_class > 0
918  }
919
920  pub fn is_mode_explicit(&self) -> bool {
921    self.is_inside_mode_function() || self.is_inside_mode_class()
922  }
923}
924
925#[derive(Debug, Default, Clone)]
926struct ComposesLocalClasses<'s> {
927  is_single: SingleLocalClass,
928  local_classes: SmallVec<[&'s str; 2]>,
929}
930
931impl<'s> ComposesLocalClasses<'s> {
932  pub fn get_valid_local_classes(
933    &mut self,
934    lexer: &DependencyLexer<'s>,
935  ) -> Option<SmallVec<[&'s str; 2]>> {
936    if let SingleLocalClass::Single(range) = &self.is_single {
937      let mut local_classes = self.local_classes.clone();
938      local_classes.push(lexer.slice(range.start, range.end)?);
939      Some(local_classes)
940    } else {
941      self.reset_to_initial();
942      None
943    }
944  }
945
946  pub fn invalidate(&mut self) {
947    if !matches!(self.is_single, SingleLocalClass::AtKeyword) {
948      self.is_single = SingleLocalClass::Invalid;
949      self.local_classes.clear();
950    }
951  }
952
953  pub fn find_local_class(&mut self, start: Pos, end: Pos) {
954    match self.is_single {
955      SingleLocalClass::Initial => {
956        self.is_single = SingleLocalClass::Single(Range::new(start, end))
957      }
958      SingleLocalClass::Single(_) => {
959        self.is_single = SingleLocalClass::Invalid;
960        self.local_classes.clear();
961      }
962      _ => {}
963    };
964  }
965
966  pub fn find_at_keyword(&mut self) {
967    self.is_single = SingleLocalClass::AtKeyword;
968    self.local_classes.clear();
969  }
970
971  pub fn reset_to_initial(&mut self) {
972    self.is_single = SingleLocalClass::Initial;
973    self.local_classes.clear();
974  }
975
976  pub fn find_comma(&mut self, lexer: &DependencyLexer<'s>) -> Option<()> {
977    if let SingleLocalClass::Single(range) = &self.is_single {
978      self
979        .local_classes
980        .push(lexer.slice(range.start, range.end)?);
981      self.is_single = SingleLocalClass::Initial
982    } else {
983      self.is_single = SingleLocalClass::Invalid;
984    }
985    Some(())
986  }
987}
988
989#[derive(Debug, Default, Clone)]
990enum SingleLocalClass {
991  #[default]
992  Initial,
993  Single(Range),
994  AtKeyword,
995  Invalid,
996}
997
998#[derive(Debug)]
999struct InProperty<T: ReservedValues> {
1000  reserved: T,
1001  rename: Option<Range>,
1002  balanced_len: usize,
1003}
1004
1005impl<T: ReservedValues> InProperty<T> {
1006  pub fn new(reserved: T, balanced_len: usize) -> Self {
1007    Self {
1008      reserved,
1009      rename: None,
1010      balanced_len,
1011    }
1012  }
1013
1014  fn check_reserved(&mut self, ident: &str, flags: TokenFlags) -> bool {
1015    self.reserved.check(ident, flags)
1016  }
1017
1018  pub fn reset_reserved(&mut self) {
1019    self.reserved.reset();
1020  }
1021
1022  pub fn set_rename(&mut self, ident: &str, flags: TokenFlags, range: Range) {
1023    if self.check_reserved(ident, flags) {
1024      self.rename = Some(range);
1025    }
1026  }
1027
1028  pub fn take_rename(&mut self, balanced_len: usize) -> Option<Range> {
1029    // Don't rename when we in functions
1030    if balanced_len != self.balanced_len {
1031      return None;
1032    }
1033    std::mem::take(&mut self.rename)
1034  }
1035}
1036
1037trait ReservedValues {
1038  fn check(&mut self, ident: &str, flags: TokenFlags) -> bool;
1039  fn reset(&mut self);
1040}
1041
1042#[derive(Debug, Default)]
1043struct AnimationReserved {
1044  bits: u32,
1045}
1046
1047impl ReservedValues for AnimationReserved {
1048  fn check(&mut self, ident: &str, flags: TokenFlags) -> bool {
1049    let mut lowercase = [0; MAX_CSS_KEYWORD_LEN];
1050    let ident = if flags.has_escape() {
1051      decode_css_keyword(ident, &mut lowercase)
1052    } else {
1053      lowercase_ascii_keyword(ident, &mut lowercase)
1054    };
1055    let Some(ident) = ident else {
1056      return true;
1057    };
1058    match ident {
1059            "normal" => self.check_and_update(Self::NORMAL),
1060            "reverse" => self.check_and_update(Self::REVERSE),
1061            "alternate" => self.check_and_update(Self::ALTERNATE),
1062            "alternate-reverse" => self.check_and_update(Self::ALTERNATE_REVERSE),
1063            "forwards" => self.check_and_update(Self::FORWARDS),
1064            "backwards" => self.check_and_update(Self::BACKWARDS),
1065            "both" => self.check_and_update(Self::BOTH),
1066            "infinite" => self.check_and_update(Self::INFINITE),
1067            "paused" => self.check_and_update(Self::PAUSED),
1068            "running" => self.check_and_update(Self::RUNNING),
1069            "ease" => self.check_and_update(Self::EASE),
1070            "ease-in" => self.check_and_update(Self::EASE_IN),
1071            "ease-out" => self.check_and_update(Self::EASE_OUT),
1072            "ease-in-out" => self.check_and_update(Self::EASE_IN_OUT),
1073            "linear" => self.check_and_update(Self::LINEAR),
1074            "step-end" => self.check_and_update(Self::STEP_END),
1075            "step-start" => self.check_and_update(Self::STEP_START),
1076            // keywords values
1077            "none" |
1078            // global values
1079            "initial" | "inherit" | "unset" | "revert" | "revert-layer" => false,
1080            _ => true,
1081        }
1082  }
1083
1084  fn reset(&mut self) {
1085    self.bits = 0;
1086  }
1087}
1088
1089impl AnimationReserved {
1090  const NORMAL: u32 = 1 << 0;
1091  const REVERSE: u32 = 1 << 1;
1092  const ALTERNATE: u32 = 1 << 2;
1093  const ALTERNATE_REVERSE: u32 = 1 << 3;
1094  const FORWARDS: u32 = 1 << 4;
1095  const BACKWARDS: u32 = 1 << 5;
1096  const BOTH: u32 = 1 << 6;
1097  const INFINITE: u32 = 1 << 7;
1098  const PAUSED: u32 = 1 << 8;
1099  const RUNNING: u32 = 1 << 9;
1100  const EASE: u32 = 1 << 10;
1101  const EASE_IN: u32 = 1 << 11;
1102  const EASE_OUT: u32 = 1 << 12;
1103  const EASE_IN_OUT: u32 = 1 << 13;
1104  const LINEAR: u32 = 1 << 14;
1105  const STEP_END: u32 = 1 << 15;
1106  const STEP_START: u32 = 1 << 16;
1107
1108  fn check_and_update(&mut self, bit: u32) -> bool {
1109    if self.bits & bit == bit {
1110      return true;
1111    }
1112    self.bits |= bit;
1113    false
1114  }
1115}
1116
1117#[derive(Debug, Default)]
1118struct ListStyleReserved;
1119
1120impl ReservedValues for ListStyleReserved {
1121  fn check(&mut self, ident: &str, flags: TokenFlags) -> bool {
1122    let mut lowercase = [0; MAX_CSS_KEYWORD_LEN];
1123    let ident = if flags.has_escape() {
1124      decode_css_keyword(ident, &mut lowercase)
1125    } else {
1126      lowercase_ascii_keyword(ident, &mut lowercase)
1127    };
1128    let Some(ident) = ident else {
1129      return true;
1130    };
1131    match ident {
1132            // https://www.w3.org/TR/css-counter-styles-3/#simple-numeric
1133            "decimal"
1134            | "decimal-leading-zero"
1135            | "arabic-indic"
1136            | "armenian"
1137            | "upper-armenian"
1138            | "lower-armenian"
1139            | "bengali"
1140            | "cambodian"
1141            | "khmer"
1142            | "cjk-decimal"
1143            | "devanagari"
1144            | "georgian"
1145            | "gujarati"
1146            | "gurmukhi"
1147            | "hebrew"
1148            | "kannada"
1149            | "lao"
1150            | "malayalam"
1151            | "mongolian"
1152            | "myanmar"
1153            | "oriya"
1154            | "persian"
1155            | "lower-roman"
1156            | "upper-roman"
1157            | "tamil"
1158            | "telugu"
1159            | "thai"
1160            | "tibetan"
1161            // https://www.w3.org/TR/css-counter-styles-3/#simple-alphabetic
1162            | "lower-alpha"
1163            | "lower-latin"
1164            | "upper-alpha"
1165            | "upper-latin"
1166            | "lower-greek"
1167            | "hiragana"
1168            | "hiragana-iroha"
1169            | "katakana"
1170            | "katakana-iroha"
1171            // https://www.w3.org/TR/css-counter-styles-3/#simple-symbolic
1172            | "disc"
1173            | "circle"
1174            | "square"
1175            | "disclosure-open"
1176            | "disclosure-closed"
1177            // https://www.w3.org/TR/css-counter-styles-3/#simple-fixed
1178            | "cjk-earthly-branch"
1179            | "cjk-heavenly-stem"
1180            // https://www.w3.org/TR/css-counter-styles-3/#complex-cjk
1181            | "japanese-informal"
1182            | "japanese-formal"
1183            | "korean-hangul-formal"
1184            | "korean-hanja-informal"
1185            | "korean-hanja-formal"
1186            | "simp-chinese-informal"
1187            | "simp-chinese-formal"
1188            | "trad-chinese-informal"
1189            | "trad-chinese-formal"
1190            | "ethiopic-numeric"
1191            // keywords values
1192            | "none"
1193            // global values
1194            | "initial"
1195            | "inherit"
1196            | "unset"
1197            | "revert"
1198            | "revert-layer" => false,
1199            _ => true,
1200        }
1201  }
1202
1203  fn reset(&mut self) {}
1204}
1205
1206#[derive(Debug, Default)]
1207struct FontPaletteReserved;
1208
1209impl ReservedValues for FontPaletteReserved {
1210  fn check(&mut self, ident: &str, _flags: TokenFlags) -> bool {
1211    ident.starts_with("--")
1212  }
1213
1214  fn reset(&mut self) {}
1215}
1216
1217#[derive(Debug, Default)]
1218struct ContainerReserved;
1219
1220impl ReservedValues for ContainerReserved {
1221  fn check(&mut self, ident: &str, flags: TokenFlags) -> bool {
1222    let mut lowercase = [0; MAX_CSS_KEYWORD_LEN];
1223    let ident = if flags.has_escape() {
1224      decode_css_keyword(ident, &mut lowercase)
1225    } else {
1226      lowercase_ascii_keyword(ident, &mut lowercase)
1227    };
1228    let Some(ident) = ident else {
1229      return true;
1230    };
1231    !matches!(
1232      ident,
1233      "normal"
1234        | "size"
1235        | "inline-size"
1236        | "scroll-state"
1237        | "none"
1238        | "initial"
1239        | "inherit"
1240        | "unset"
1241        | "revert"
1242        | "revert-layer"
1243    )
1244  }
1245
1246  fn reset(&mut self) {}
1247}
1248
1249#[derive(Debug, Clone, Copy)]
1250enum GridPropertyKind {
1251  Generic,
1252  TemplateLike,
1253  TemplateAreas,
1254}
1255
1256fn grid_property_kind(ident: &str) -> Option<GridPropertyKind> {
1257  match ident {
1258    "grid" | "grid-area" | "grid-column" | "grid-column-end" | "grid-column-start" | "grid-row"
1259    | "grid-row-end" | "grid-row-start" => Some(GridPropertyKind::Generic),
1260    "grid-template" | "grid-template-columns" | "grid-template-rows" => {
1261      Some(GridPropertyKind::TemplateLike)
1262    }
1263    "grid-template-areas" => Some(GridPropertyKind::TemplateAreas),
1264    _ => None,
1265  }
1266}
1267
1268fn is_reserved_grid_ident(ident: &str, flags: TokenFlags) -> bool {
1269  let mut lowercase = [0; MAX_CSS_KEYWORD_LEN];
1270  let ident = if flags.has_escape() {
1271    decode_css_keyword(ident, &mut lowercase)
1272  } else {
1273    lowercase_ascii_keyword(ident, &mut lowercase)
1274  };
1275  let Some(ident) = ident else {
1276    return false;
1277  };
1278  matches!(
1279    ident,
1280    "auto"
1281      | "span"
1282      | "auto-flow"
1283      | "dense"
1284      | "row"
1285      | "column"
1286      | "none"
1287      | "subgrid"
1288      | "masonry"
1289      | "max-content"
1290      | "min-content"
1291      | "initial"
1292      | "inherit"
1293      | "unset"
1294      | "revert"
1295      | "revert-layer"
1296  )
1297}
1298
1299/// Decide whether a raw-scanned ident inside a special property value must be
1300/// handed back to the regular tokenizer. The check applies to plain idents and
1301/// function names alike: `url`/`var`/`image-set` and `--`-prefixed functions
1302/// are stopped by the caller's generic dependency-function rule, while any
1303/// other non-reserved function name also stops the scan so the tokenizer can
1304/// push it onto the balanced stack. `icss_symbols` is only consulted for the
1305/// property kinds whose values can reference ICSS symbols.
1306pub(crate) fn special_value_is_candidate(
1307  property: PropertyKind,
1308  ident: &str,
1309  icss_symbols: Option<&FxHashSet<&str>>,
1310) -> bool {
1311  if icss_symbols.is_some_and(|symbols| symbols.contains(ident)) {
1312    return true;
1313  }
1314  match property {
1315    PropertyKind::ListStyle => ListStyleReserved.check(ident, TokenFlags::ascii()),
1316    PropertyKind::FontPalette => ident.starts_with("--"),
1317    PropertyKind::Container => ContainerReserved.check(ident, TokenFlags::ascii()),
1318    PropertyKind::Grid => !is_reserved_grid_ident(ident, TokenFlags::ascii()),
1319    PropertyKind::Animation
1320    | PropertyKind::Generic
1321    | PropertyKind::Composes
1322    | PropertyKind::CustomProperty => false,
1323  }
1324}
1325
1326fn is_reserved_container_query_ident(ident: &str, flags: TokenFlags) -> bool {
1327  let mut lowercase = [0; MAX_CSS_KEYWORD_LEN];
1328  let ident = if flags.has_escape() {
1329    decode_css_keyword(ident, &mut lowercase)
1330  } else {
1331    lowercase_ascii_keyword(ident, &mut lowercase)
1332  };
1333  let Some(ident) = ident else {
1334    return false;
1335  };
1336  matches!(ident, "none" | "and" | "or" | "not")
1337}
1338
1339fn parse_grid_template_area_ranges(input: &str, offset: Pos) -> impl Iterator<Item = Range> + '_ {
1340  let bytes = input.as_bytes();
1341  let mut i = 0usize;
1342
1343  std::iter::from_fn(move || {
1344    loop {
1345      while i < bytes.len() && bytes[i].is_ascii_whitespace() {
1346        i += 1;
1347      }
1348      let start = i;
1349      while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
1350        i += 1;
1351      }
1352      if start == i {
1353        return None;
1354      }
1355      if !input[start..i].bytes().all(|c| c == b'.') {
1356        return Some(Range::new(offset + start as Pos, offset + i as Pos));
1357      }
1358    }
1359  })
1360}
1361
1362#[derive(Debug)]
1363pub struct LexDependencies<'s, W> {
1364  dependency_context: DependencyContext<'s>,
1365  mode_data: Option<ModeData<'s>>,
1366  scope: Scope<'s>,
1367  block_nesting_level: u32,
1368  allow_import_at_rule: bool,
1369  balanced: BalancedStack,
1370  is_next_rule_prelude: bool,
1371  scan_context: ScanContext,
1372  selector_square_depth: u32,
1373  selector_fast_forward_enabled: bool,
1374  property_kind: PropertyKind,
1375  in_animation_property: Option<InProperty<AnimationReserved>>,
1376  in_list_style_property: Option<InProperty<ListStyleReserved>>,
1377  in_font_palette_property: Option<InProperty<FontPaletteReserved>>,
1378  in_container_property: Option<InProperty<ContainerReserved>>,
1379  in_grid_property: Option<GridPropertyKind>,
1380  icss_symbols: FxHashSet<&'s str>,
1381  icss_symbol_filter: [u64; 16],
1382  icss_symbol_min_len: usize,
1383  icss_symbol_max_len: usize,
1384  pending_custom_property: Option<Range>,
1385  pending_grid_property: Option<GridPropertyKind>,
1386  handle_warning: W,
1387}
1388
1389impl<'s, W: HandleWarning<'s>> LexDependencies<'s, W> {
1390  pub fn new(handle_warning: W, mode: Mode) -> Self {
1391    Self::with_context(DependencyContext::new(), handle_warning, mode)
1392  }
1393
1394  pub(crate) fn with_context(
1395    dependency_context: DependencyContext<'s>,
1396    handle_warning: W,
1397    mode: Mode,
1398  ) -> Self {
1399    Self {
1400      dependency_context,
1401      mode_data: if mode == Mode::Css {
1402        None
1403      } else {
1404        Some(ModeData::new(mode))
1405      },
1406      scope: Scope::TopLevel,
1407      block_nesting_level: 0,
1408      allow_import_at_rule: true,
1409      balanced: Default::default(),
1410      is_next_rule_prelude: true,
1411      scan_context: ScanContext::TopLevel,
1412      selector_square_depth: 0,
1413      selector_fast_forward_enabled: false,
1414      property_kind: PropertyKind::Generic,
1415      in_animation_property: None,
1416      in_list_style_property: None,
1417      in_font_palette_property: None,
1418      in_container_property: None,
1419      in_grid_property: None,
1420      icss_symbols: Default::default(),
1421      icss_symbol_filter: [0; 16],
1422      icss_symbol_min_len: usize::MAX,
1423      icss_symbol_max_len: 0,
1424      pending_custom_property: None,
1425      pending_grid_property: None,
1426      handle_warning,
1427    }
1428  }
1429
1430  pub fn dependency_context(&self) -> &DependencyContext<'s> {
1431    &self.dependency_context
1432  }
1433
1434  pub fn into_dependency_context(self) -> DependencyContext<'s> {
1435    self.dependency_context
1436  }
1437
1438  #[inline]
1439  fn set_scan_context(
1440    &mut self,
1441    stream: &mut DependencyTokenStream<'_, 's>,
1442    scan_context: ScanContext,
1443  ) {
1444    if self.scan_context == scan_context {
1445      return;
1446    }
1447    match self.scan_context {
1448      ScanContext::Selector => self.selector_square_depth = 0,
1449      ScanContext::AtRule => stream.reset_at_rule_scan_state(),
1450      ScanContext::SpecialValue(_) => stream.reset_special_value_scan_state(),
1451      _ => {}
1452    }
1453    self.scan_context = scan_context;
1454  }
1455
1456  /// Drive dependency extraction from the forward token stream.
1457  pub fn lex_streaming(&mut self, source: &mut DependencyLexer<'s>) {
1458    self.selector_fast_forward_enabled = source.source_end() >= 256;
1459    let mode = self
1460      .mode_data
1461      .as_ref()
1462      .map_or(Mode::Css, ModeData::default_mode);
1463    let source_len = source.source_end() as usize;
1464    source
1465      .visitor_mut()
1466      .reserve(DependencyContext::estimated_dashed_ident_capacity(
1467        source_len, mode,
1468      ));
1469    self
1470      .dependency_context
1471      .reserve_estimated_capacity(source.source_end() as usize, mode);
1472    {
1473      let mut stream = TokenStream::from_lexer(source);
1474      let keep_comments = self.mode_data.as_ref().is_some_and(ModeData::is_pure_mode);
1475      let has_mode = self.mode_data.is_some();
1476      self.lex_streaming_inner(&mut stream, keep_comments, has_mode);
1477    }
1478    self
1479      .dependency_context
1480      .set_dashed_ident_occurrences(source.visitor_mut().take());
1481  }
1482
1483  #[inline]
1484  fn update_dashed_ident_collection(&self, stream: &mut DependencyTokenStream<'_, 's>) {
1485    let enabled = self
1486      .mode_data
1487      .as_ref()
1488      .is_some_and(ModeData::is_current_local_mode);
1489    stream.lexer_mut().visitor_mut().set_enabled(enabled);
1490  }
1491
1492  #[inline(always)]
1493  fn lex_streaming_inner(
1494    &mut self,
1495    stream: &mut DependencyTokenStream<'_, 's>,
1496    keep_comments: bool,
1497    has_mode: bool,
1498  ) {
1499    loop {
1500      self.update_dashed_ident_collection(stream);
1501      let item = stream.next(keep_comments);
1502      let token = item.token;
1503      if token.kind == TokenKind::Eof {
1504        return;
1505      }
1506
1507      let is_trivia = matches!(token.kind, TokenKind::Comment | TokenKind::BadComment);
1508      if !is_trivia && self.scan_context == ScanContext::BlockItem {
1509        self.is_next_rule_prelude = !matches!(
1510          token.kind,
1511          TokenKind::Ident | TokenKind::Function | TokenKind::RightCurlyBracket
1512        );
1513        if self.is_next_rule_prelude
1514          && self.block_nesting_level == 0
1515          && let Some(mode_data) = &mut self.mode_data
1516        {
1517          mode_data.composes_local_classes.reset_to_initial();
1518        }
1519        let scan_context = if self.is_next_rule_prelude {
1520          ScanContext::Selector
1521        } else {
1522          ScanContext::DeclarationName
1523        };
1524        self.set_scan_context(stream, scan_context);
1525      }
1526
1527      if self.scan_context == ScanContext::TopLevel
1528        && self.is_next_rule_prelude
1529        && token.kind != TokenKind::AtKeyword
1530      {
1531        self.set_scan_context(stream, ScanContext::Selector);
1532      }
1533
1534      let mut colon_next = None;
1535      let mut dot_next = None;
1536      if has_mode
1537        && self.scan_context == ScanContext::Selector
1538        && token.kind == TokenKind::Colon
1539        && stream.lexer().could_start_ident_at(token.range.end)
1540      {
1541        let first = stream.peek_significant_skipping_comments(keep_comments);
1542        if first.token.kind != TokenKind::Eof {
1543          colon_next = Some(first);
1544        }
1545      } else if has_mode
1546        && self.scan_context == ScanContext::Selector
1547        && token.kind == TokenKind::Delim
1548        && stream.lexer().byte_at(token.range.start) == Some(b'.')
1549        && stream.lexer().could_start_ident_at(token.range.end)
1550      {
1551        let next = stream.peek_significant_skipping_comments(keep_comments);
1552        if next.token.kind != TokenKind::Eof {
1553          dot_next = Some(next);
1554        }
1555      }
1556
1557      let mut result = Some(());
1558      match token.kind {
1559        TokenKind::Comment | TokenKind::BadComment => {
1560          result = self.handle_comment(stream.lexer_mut(), token.range.start, token.range.end);
1561        }
1562        TokenKind::WhiteSpace => {}
1563        TokenKind::QuotedString | TokenKind::BadString => {
1564          result = self.handle_string(
1565            stream.lexer_mut(),
1566            token.range.start,
1567            token.range.end,
1568            token.flags,
1569          );
1570        }
1571        TokenKind::Url => {
1572          result = self.handle_url(
1573            stream.lexer_mut(),
1574            token.range.start,
1575            token.range.end,
1576            token.value_range.start,
1577            token.value_range.end,
1578            token.flags,
1579          );
1580        }
1581        TokenKind::Function => {
1582          result = self.handle_function(stream, token.range.start, token.range.end, token.flags);
1583        }
1584        TokenKind::Ident => {
1585          result = self.handle_ident(stream, token.range.start, token.range.end, token.flags);
1586        }
1587        TokenKind::AtKeyword => {
1588          result = self.handle_at_keyword(stream, token.range.start, token.range.end, token.flags);
1589        }
1590        TokenKind::IdHash | TokenKind::Hash
1591          if !has_mode || self.scan_context != ScanContext::Selector => {}
1592        TokenKind::IdHash | TokenKind::Hash => {
1593          let id_end = if token.kind == TokenKind::Hash {
1594            token.range.start + 1
1595          } else {
1596            token.range.end
1597          };
1598          result = self.handle_id(stream.lexer_mut(), token.range.start, id_end, token.flags);
1599        }
1600        TokenKind::Delim
1601          if (!has_mode || self.scan_context != ScanContext::Selector)
1602            && stream.byte_at(token.range.start) == Some(b'#') => {}
1603        TokenKind::Delim if stream.byte_at(token.range.start) == Some(b'#') => {
1604          result = self.handle_id(
1605            stream.lexer_mut(),
1606            token.range.start,
1607            token.range.end,
1608            token.flags,
1609          );
1610        }
1611        TokenKind::Delim
1612          if (!has_mode || self.scan_context != ScanContext::Selector)
1613            && stream.byte_at(token.range.start) == Some(b'.') => {}
1614        TokenKind::Delim if stream.byte_at(token.range.start) == Some(b'.') => {
1615          let mut class_end = token.range.end;
1616          let mut class_flags = token.flags;
1617          let mut consumes_name = false;
1618          if let Some(next) = dot_next
1619            && next.token.kind == TokenKind::Ident
1620            && next.token.range.start == token.range.end
1621          {
1622            class_end = next.token.range.end;
1623            class_flags = next.token.flags;
1624            consumes_name = true;
1625          }
1626          result = self.handle_class(
1627            stream.lexer_mut(),
1628            token.range.start,
1629            class_end,
1630            class_flags,
1631          );
1632          if consumes_name {
1633            stream.next(keep_comments);
1634          }
1635        }
1636        TokenKind::Delim if self.scan_context == ScanContext::Selector => {
1637          if self.block_nesting_level == 0
1638            && let Some(mode_data) = &mut self.mode_data
1639          {
1640            mode_data.composes_local_classes.invalidate();
1641          }
1642        }
1643        TokenKind::Colon if self.scan_context == ScanContext::DeclarationName => {
1644          result = self.enter_property_value(stream);
1645        }
1646        TokenKind::Colon if !has_mode => {}
1647        TokenKind::Colon => {
1648          if let Some(next) = colon_next.filter(|next| {
1649            matches!(next.token.kind, TokenKind::Ident | TokenKind::Function)
1650              && next.token.range.start == token.range.end
1651          }) {
1652            let (end, function) = (next.token.range.end, next.token.kind == TokenKind::Function);
1653            stream.next(keep_comments);
1654            result = if function {
1655              self.handle_pseudo_function(stream, token.range.start, end, next.token.flags)
1656            } else {
1657              self.handle_pseudo_class(stream, token.range.start, end, next.token.flags)
1658            };
1659          }
1660        }
1661        TokenKind::LeftSquareBracket if self.scan_context == ScanContext::Selector => {
1662          self.selector_square_depth += 1;
1663          if self.block_nesting_level == 0
1664            && let Some(mode_data) = &mut self.mode_data
1665          {
1666            mode_data.composes_local_classes.invalidate();
1667          }
1668        }
1669        TokenKind::RightSquareBracket if self.scan_context == ScanContext::Selector => {
1670          self.selector_square_depth = self.selector_square_depth.saturating_sub(1);
1671        }
1672        TokenKind::LeftParenthesis => {
1673          result =
1674            self.handle_left_parenthesis(stream.lexer_mut(), token.range.start, token.range.end);
1675        }
1676        TokenKind::RightParenthesis => {
1677          result = self.handle_right_parenthesis(
1678            stream.lexer_mut(),
1679            item.leading.range.start,
1680            token.range.start,
1681            token.range.end,
1682          );
1683        }
1684        TokenKind::Comma => {
1685          result = self.handle_comma(stream.lexer_mut(), token.range.start, token.range.end);
1686        }
1687        TokenKind::Semicolon => {
1688          result = self.handle_semicolon(stream.lexer_mut(), token.range.start, token.range.end);
1689        }
1690        TokenKind::LeftCurlyBracket => {
1691          result = self.handle_left_curly_bracket(stream, token.range.start, token.range.end);
1692        }
1693        TokenKind::RightCurlyBracket => {
1694          result = self.handle_right_curly_bracket(stream, token.range.start, token.range.end);
1695        }
1696        _ => {}
1697      }
1698
1699      if result.is_none() {
1700        return;
1701      }
1702      if token.kind == TokenKind::Semicolon {
1703        match self.scope {
1704          Scope::TopLevel => {
1705            self.set_scan_context(stream, ScanContext::TopLevel);
1706            self.is_next_rule_prelude = true;
1707          }
1708          Scope::InBlock => self.set_scan_context(stream, ScanContext::BlockItem),
1709          _ => {}
1710        }
1711      }
1712      self.update_dashed_ident_collection(stream);
1713      match self.scan_context {
1714        ScanContext::Selector if self.selector_fast_forward_enabled => {
1715          self.fast_forward_selector(stream, keep_comments, has_mode);
1716        }
1717        ScanContext::AtRule
1718          if !matches!(self.scope, Scope::InAtImport(_) | Scope::AtImportInvalid) =>
1719        {
1720          self.fast_forward_at_rule(stream, keep_comments);
1721        }
1722        ScanContext::SpecialValue(property) => {
1723          self.fast_forward_special_value(stream, property, keep_comments);
1724        }
1725        ScanContext::GenericValue if !is_trivia => {
1726          self.fast_forward_generic_value(stream, keep_comments);
1727        }
1728        _ => {}
1729      }
1730    }
1731  }
1732
1733  #[inline]
1734  fn fast_forward_selector(
1735    &mut self,
1736    stream: &mut DependencyTokenStream<'_, 's>,
1737    keep_comments: bool,
1738    has_mode: bool,
1739  ) {
1740    let mut square_depth = self.selector_square_depth;
1741    let invalidates_composes = stream.fast_forward_selector_if_buffer_empty(
1742      &mut square_depth,
1743      keep_comments,
1744      has_mode,
1745      |ident| self.contains_icss_symbol(ident),
1746      is_css_modules_pure_magic_comment,
1747    );
1748    self.selector_square_depth = square_depth;
1749    if invalidates_composes
1750      && self.block_nesting_level == 0
1751      && let Some(mode_data) = &mut self.mode_data
1752    {
1753      mode_data.composes_local_classes.invalidate();
1754    }
1755  }
1756
1757  #[inline]
1758  fn fast_forward_generic_value(
1759    &self,
1760    stream: &mut DependencyTokenStream<'_, 's>,
1761    keep_comments: bool,
1762  ) {
1763    let preserve_strings = matches!(
1764        self.balanced.last(),
1765        Some(last) if matches!(last.kind, BalancedItemKind::Url | BalancedItemKind::ImageSet)
1766    );
1767    let preserve_delimiters = !self.balanced.is_empty();
1768    if self.icss_symbols.is_empty() && !keep_comments {
1769      stream.fast_forward_generic_value_without_candidates_if_buffer_empty(
1770        preserve_strings,
1771        preserve_delimiters,
1772      );
1773    } else {
1774      stream.fast_forward_generic_value_if_buffer_empty(
1775        keep_comments,
1776        preserve_strings,
1777        preserve_delimiters,
1778        |ident| self.contains_icss_symbol(ident),
1779        is_css_modules_pure_magic_comment,
1780      );
1781    }
1782  }
1783
1784  #[inline]
1785  fn fast_forward_at_rule(&self, stream: &mut DependencyTokenStream<'_, 's>, keep_comments: bool) {
1786    let preserve_strings = matches!(
1787        self.balanced.last(),
1788        Some(last) if matches!(last.kind, BalancedItemKind::Url | BalancedItemKind::ImageSet)
1789    );
1790    let preserve_delimiters = !self.balanced.is_empty();
1791    stream.fast_forward_at_rule_if_buffer_empty(
1792      keep_comments,
1793      preserve_strings,
1794      preserve_delimiters,
1795      |ident| self.contains_icss_symbol(ident),
1796      is_css_modules_pure_magic_comment,
1797    );
1798  }
1799
1800  #[inline]
1801  fn fast_forward_special_value(
1802    &self,
1803    stream: &mut DependencyTokenStream<'_, 's>,
1804    property: PropertyKind,
1805    keep_comments: bool,
1806  ) {
1807    // An animation name may be any top-level identifier, so the raw
1808    // scanner usually stops immediately. Continue tokenizing directly.
1809    if property == PropertyKind::Animation {
1810      return;
1811    }
1812    let preserve_strings = property == PropertyKind::Grid
1813      || matches!(
1814          self.balanced.last(),
1815          Some(last) if matches!(last.kind, BalancedItemKind::Url | BalancedItemKind::ImageSet)
1816      );
1817    let preserve_delimiters = !self.balanced.is_empty();
1818    let icss_symbols = (!self.icss_symbols.is_empty()).then_some(&self.icss_symbols);
1819    stream.fast_forward_special_value_if_buffer_empty(
1820      keep_comments,
1821      preserve_strings,
1822      preserve_delimiters,
1823      property,
1824      icss_symbols,
1825    );
1826  }
1827
1828  #[inline]
1829  fn icss_symbol_filter_bit(value: &str) -> (usize, u64) {
1830    let bytes = value.as_bytes();
1831    let len = bytes.len();
1832    let first = bytes.first().copied().unwrap_or_default() as usize;
1833    let middle = bytes.get(len / 2).copied().unwrap_or_default() as usize;
1834    let last = bytes.last().copied().unwrap_or_default() as usize;
1835    let mut hash = len.wrapping_mul(0x9e37_79b1);
1836    hash ^= first.wrapping_mul(0x85eb_ca6b);
1837    hash ^= middle.wrapping_mul(0xc2b2_ae35);
1838    hash ^= last.wrapping_mul(0x27d4_eb2f);
1839    hash ^= hash >> 16;
1840    let bit = hash & 1023;
1841    (bit >> 6, 1u64 << (bit & 63))
1842  }
1843
1844  #[inline]
1845  fn contains_icss_symbol(&self, value: &str) -> bool {
1846    let len = value.len();
1847    if len < self.icss_symbol_min_len || len > self.icss_symbol_max_len {
1848      return false;
1849    }
1850    let (word, bit) = Self::icss_symbol_filter_bit(value);
1851    self.icss_symbol_filter[word] & bit != 0 && self.icss_symbols.contains(value)
1852  }
1853
1854  fn insert_icss_symbol(&mut self, value: &'s str) {
1855    let len = value.len();
1856    self.icss_symbol_min_len = self.icss_symbol_min_len.min(len);
1857    self.icss_symbol_max_len = self.icss_symbol_max_len.max(len);
1858    let (word, bit) = Self::icss_symbol_filter_bit(value);
1859    self.icss_symbol_filter[word] |= bit;
1860    self.icss_symbols.insert(value);
1861  }
1862
1863  fn enter_property_value(&mut self, stream: &mut DependencyTokenStream<'_, 's>) -> Option<()> {
1864    match self.property_kind {
1865      PropertyKind::Animation => self.enter_animation_property(),
1866      PropertyKind::ListStyle => self.enter_list_style_property(),
1867      PropertyKind::FontPalette => self.enter_font_palette_property(),
1868      PropertyKind::Container => self.enter_container_property(),
1869      PropertyKind::Grid => {
1870        if let Some(kind) = self.pending_grid_property.take() {
1871          self.enter_grid_property(kind);
1872        }
1873      }
1874      PropertyKind::CustomProperty => {
1875        if self
1876          .mode_data
1877          .as_ref()
1878          .is_some_and(ModeData::is_property_local_mode)
1879          && let Some(range) = self.pending_custom_property.take()
1880        {
1881          self
1882            .dependency_context
1883            .push_dependency(Dependency::LocalVarDecl {
1884              name: dashed_ident_name(stream.lexer().slice(range.start, range.end)?)?,
1885              range,
1886            });
1887        }
1888      }
1889      PropertyKind::Generic | PropertyKind::Composes => {}
1890    }
1891    self.pending_custom_property = None;
1892    self.pending_grid_property = None;
1893    self.set_scan_context(stream, ScanContext::for_property(self.property_kind));
1894    Some(())
1895  }
1896
1897  fn classify_property(
1898    name: &str,
1899    flags: TokenFlags,
1900    property_local_mode: bool,
1901  ) -> (PropertyKind, Option<GridPropertyKind>) {
1902    if name.starts_with("--") {
1903      return if property_local_mode {
1904        (PropertyKind::CustomProperty, None)
1905      } else {
1906        (PropertyKind::Generic, None)
1907      };
1908    }
1909    let mut normalized = [0; MAX_CSS_KEYWORD_LEN];
1910    let name = if flags.has_escape() {
1911      let Some(name) = decode_css_keyword(name, &mut normalized) else {
1912        return (PropertyKind::Generic, None);
1913      };
1914      name
1915    } else {
1916      let Some(name) = lowercase_ascii_keyword(name, &mut normalized) else {
1917        return (PropertyKind::Generic, None);
1918      };
1919      name
1920    };
1921    if matches!(name, "composes" | "compose-with") {
1922      return (PropertyKind::Composes, None);
1923    }
1924    if !property_local_mode {
1925      return (PropertyKind::Generic, None);
1926    }
1927    let unprefixed = strip_vendor_prefix(name).unwrap_or(name);
1928    if matches!(unprefixed, "animation" | "animation-name") {
1929      return (PropertyKind::Animation, None);
1930    }
1931    if matches!(name, "list-style" | "list-style-type") {
1932      return (PropertyKind::ListStyle, None);
1933    }
1934    if name == "font-palette" {
1935      return (PropertyKind::FontPalette, None);
1936    }
1937    if matches!(name, "container" | "container-name") {
1938      return (PropertyKind::Container, None);
1939    }
1940    if let Some(grid) = grid_property_kind(name) {
1941      return (PropertyKind::Grid, Some(grid));
1942    }
1943    (PropertyKind::Generic, None)
1944  }
1945
1946  fn classify_at_rule(name: &str, flags: TokenFlags) -> AtRuleKind {
1947    let mut normalized = [0; MAX_CSS_KEYWORD_LEN];
1948    let name = if flags.has_escape() {
1949      let Some(name) = decode_css_keyword(name, &mut normalized) else {
1950        return AtRuleKind::Other;
1951      };
1952      name
1953    } else {
1954      let Some(name) = lowercase_ascii_keyword(name, &mut normalized) else {
1955        return AtRuleKind::Other;
1956      };
1957      name
1958    };
1959    match name {
1960      "@value" => AtRuleKind::Value,
1961      "@scope" => AtRuleKind::Scope,
1962      "@import" => AtRuleKind::Import,
1963      "@charset" => AtRuleKind::Charset,
1964      "@function" => AtRuleKind::Function,
1965      "@property" => AtRuleKind::Property,
1966      "@namespace" => AtRuleKind::Namespace,
1967      "@keyframes" => AtRuleKind::Keyframes,
1968      "@container" => AtRuleKind::Container,
1969      "@counter-style" => AtRuleKind::CounterStyle,
1970      "@font-palette-values" => AtRuleKind::FontPaletteValues,
1971      _ if name.strip_prefix('@').and_then(strip_vendor_prefix) == Some("keyframes") => {
1972        AtRuleKind::Keyframes
1973      }
1974      _ => AtRuleKind::Other,
1975    }
1976  }
1977
1978  fn get_media(&self, lexer: &DependencyLexer<'s>, start: Pos, end: Pos) -> Option<&'s str> {
1979    let media = lexer.slice(start, end)?;
1980    let bytes = media.as_bytes();
1981    let mut position = 0;
1982    loop {
1983      while position < bytes.len() && bytes[position].is_ascii_whitespace() {
1984        position += 1;
1985      }
1986      if position + 1 < bytes.len() && bytes[position] == b'/' && bytes[position + 1] == b'*' {
1987        let Some(relative_end) = media[position + 2..].find("*/") else {
1988          break;
1989        };
1990        position += relative_end + 4;
1991        continue;
1992      }
1993      break;
1994    }
1995    if position == bytes.len() {
1996      return None;
1997    }
1998    Some(media)
1999  }
2000
2001  fn lex_charset_at_rule(
2002    &mut self,
2003    stream: &mut DependencyTokenStream<'_, 's>,
2004    start: Pos,
2005  ) -> Option<()> {
2006    let string = stream.next_parser_token().token;
2007    if string.kind != TokenKind::QuotedString {
2008      return Some(());
2009    }
2010
2011    let next = stream.next_parser_token().token;
2012    if next.kind == TokenKind::Semicolon {
2013      self
2014        .dependency_context
2015        .push_dependency(Dependency::Charset {
2016          value: stream.slice(string.value_range.start, string.value_range.end)?,
2017          range: Range::new(start, next.range.end),
2018        });
2019    }
2020    Some(())
2021  }
2022
2023  fn enter_animation_property(&mut self) {
2024    self.in_animation_property = Some(InProperty::new(
2025      AnimationReserved::default(),
2026      self.balanced.len(),
2027    ));
2028  }
2029
2030  fn exit_animation_property(&mut self) {
2031    self.in_animation_property = None;
2032  }
2033
2034  fn enter_list_style_property(&mut self) {
2035    self.in_list_style_property = Some(InProperty::new(ListStyleReserved, self.balanced.len()));
2036  }
2037
2038  fn exit_list_style_property(&mut self) {
2039    self.in_list_style_property = None;
2040  }
2041
2042  fn enter_font_palette_property(&mut self) {
2043    self.in_font_palette_property = Some(InProperty::new(FontPaletteReserved, self.balanced.len()));
2044  }
2045
2046  fn exit_font_palette_property(&mut self) {
2047    self.in_font_palette_property = None;
2048  }
2049
2050  fn enter_container_property(&mut self) {
2051    self.in_container_property = Some(InProperty::new(ContainerReserved, self.balanced.len()));
2052  }
2053
2054  fn exit_container_property(&mut self) {
2055    self.in_container_property = None;
2056  }
2057
2058  fn enter_grid_property(&mut self, kind: GridPropertyKind) {
2059    self.in_grid_property = Some(kind);
2060  }
2061
2062  fn exit_grid_property(&mut self) {
2063    self.in_grid_property = None;
2064  }
2065
2066  fn lex_icss_import(&mut self, stream: &mut DependencyTokenStream<'_, 's>) -> Option<()> {
2067    let (start, end) = self.consume_icss_import_path(stream)?;
2068    let right_parenthesis = stream.next_parser_token().token;
2069    if right_parenthesis.kind != TokenKind::RightParenthesis {
2070      self.handle_warning.handle_warning(Warning {
2071        range: Range::new(right_parenthesis.range.start, right_parenthesis.range.end),
2072        kind: WarningKind::Unexpected {
2073          message: "Expected ')' during parsing of ':import()'",
2074        },
2075      });
2076      return Some(());
2077    }
2078    self
2079      .dependency_context
2080      .push_dependency(Dependency::ICSSImportFrom {
2081        path: stream.slice(start, end)?,
2082      });
2083    let left_curly = stream.next_parser_token().token;
2084    if left_curly.kind != TokenKind::LeftCurlyBracket {
2085      self.handle_warning.handle_warning(Warning {
2086        range: Range::new(left_curly.range.start, left_curly.range.end),
2087        kind: WarningKind::Unexpected {
2088          message: "Expected '{' during parsing of ':import()'",
2089        },
2090      });
2091      return Some(());
2092    }
2093    loop {
2094      let first = stream.next_parser_token().token;
2095      if first.kind == TokenKind::Eof {
2096        return None;
2097      }
2098      if first.kind == TokenKind::RightCurlyBracket {
2099        break;
2100      }
2101      let prop_start = first.range.start;
2102      let prop_end = self.consume_icss_export_prop(stream, first)?;
2103      let colon = stream.peek_significant_skipping_comments(true).token;
2104      if colon.kind != TokenKind::Colon {
2105        self.handle_warning.handle_warning(Warning {
2106          range: Range::new(colon.range.start, colon.range.end),
2107          kind: WarningKind::Unexpected {
2108            message: "Expected ':' during parsing of ':import'",
2109          },
2110        });
2111        return Some(());
2112      }
2113      stream.next_parser_token();
2114      let value_start_token = stream.next_parser_token().token;
2115      if value_start_token.kind == TokenKind::Eof {
2116        return None;
2117      }
2118      let value_start = value_start_token.range.start;
2119      let value_end = self.consume_icss_value(stream, value_start_token)?;
2120      let delimiter = stream.next_parser_token().token;
2121      self
2122        .dependency_context
2123        .push_dependency(Dependency::ICSSImportValue {
2124          prop: stream
2125            .slice(prop_start, prop_end)?
2126            .trim_end_matches(is_css_white_space_char),
2127          value: stream
2128            .slice(value_start, value_end)?
2129            .trim_end_matches(is_css_white_space_char),
2130        });
2131      self.insert_icss_symbol(
2132        stream
2133          .slice(prop_start, prop_end)?
2134          .trim_end_matches(is_css_white_space_char),
2135      );
2136      if delimiter.kind == TokenKind::RightCurlyBracket {
2137        break;
2138      }
2139    }
2140    Some(())
2141  }
2142
2143  fn consume_icss_import_path(
2144    &self,
2145    stream: &mut DependencyTokenStream<'_, 's>,
2146  ) -> Option<(Pos, Pos)> {
2147    let first = stream.next_parser_token().token;
2148    if first.kind == TokenKind::Eof {
2149      return None;
2150    }
2151    let start = first.range.start;
2152    let mut end = start;
2153    if first.kind == TokenKind::RightParenthesis {
2154      return Some((start, end));
2155    }
2156    end = first.range.end;
2157    loop {
2158      let token = stream.peek_significant_skipping_comments(true).token;
2159      if token.kind == TokenKind::Eof {
2160        return None;
2161      }
2162      if token.kind == TokenKind::RightParenthesis {
2163        return Some((start, end));
2164      }
2165      stream.next_parser_token();
2166      if first.kind != TokenKind::QuotedString {
2167        end = token.range.end;
2168      }
2169    }
2170  }
2171
2172  fn consume_icss_export_prop(
2173    &self,
2174    stream: &mut DependencyTokenStream<'_, 's>,
2175    first: Token,
2176  ) -> Option<Pos> {
2177    if matches!(
2178      first.kind,
2179      TokenKind::Colon | TokenKind::RightCurlyBracket | TokenKind::Semicolon
2180    ) {
2181      return Some(first.range.start);
2182    }
2183    loop {
2184      let token = stream.peek_parser_token();
2185      if token.token.kind == TokenKind::Eof {
2186        return None;
2187      }
2188      if let Some(first_comment_start) = token.leading.first_comment_start {
2189        return Some(first_comment_start);
2190      }
2191      if matches!(
2192        token.token.kind,
2193        TokenKind::Colon | TokenKind::RightCurlyBracket | TokenKind::Semicolon
2194      ) {
2195        return Some(token.token.range.start);
2196      }
2197      stream.next_parser_token();
2198    }
2199  }
2200
2201  fn consume_icss_value(
2202    &self,
2203    stream: &mut DependencyTokenStream<'_, 's>,
2204    first: Token,
2205  ) -> Option<Pos> {
2206    if matches!(
2207      first.kind,
2208      TokenKind::RightCurlyBracket | TokenKind::Semicolon
2209    ) {
2210      return Some(first.range.start);
2211    }
2212    loop {
2213      let token = stream.peek(true).token;
2214      if token.kind == TokenKind::Eof {
2215        return None;
2216      }
2217      if matches!(
2218        token.kind,
2219        TokenKind::RightCurlyBracket | TokenKind::Semicolon
2220      ) {
2221        return Some(token.range.start);
2222      }
2223      stream.next(true);
2224    }
2225  }
2226
2227  fn lex_value_at_rule(
2228    &mut self,
2229    stream: &mut DependencyTokenStream<'_, 's>,
2230    start: Pos,
2231  ) -> Option<()> {
2232    let input = stream.slice_trusted(0, stream.source_end());
2233    let checkpoint = self
2234      .dependency_context
2235      .value_at_rule_import_items_checkpoint();
2236    let mut parser = ValueAtRuleStream::new(input);
2237    loop {
2238      let token = stream.next_parser_token().token;
2239      match token.kind {
2240        TokenKind::Eof => return None,
2241        _ => {
2242          if token.kind == TokenKind::Semicolon && parser.depth == 0 {
2243            parser.params_end = token.range.start;
2244            break;
2245          }
2246          let depth_after = (parser.depth + u32::from(is_open_token(token.kind)))
2247            .saturating_sub(u32::from(is_close_token(token.kind)));
2248          if token.kind == TokenKind::RightCurlyBracket && depth_after == 0 {
2249            parser.params_end = token.range.start;
2250            break;
2251          }
2252          parser.push(&mut self.dependency_context, token);
2253        }
2254      }
2255    }
2256    let at_rule_end = parser.params_end.max(stream.consumed_pos());
2257    let import = parser.last_two().is_some_and(|(penultimate, last)| {
2258      penultimate.kind == TokenKind::Ident
2259        && parser
2260          .input
2261          .get(penultimate.range.start as usize..penultimate.range.end as usize)
2262          .is_some_and(|text| text.eq_ignore_ascii_case("from"))
2263        && parser.from_pos == Some(penultimate.range.start)
2264        && parser
2265          .input
2266          .get(penultimate.range.end as usize..last.range.start as usize)
2267          .is_some_and(trivia_only)
2268    });
2269    if import {
2270      let (_, last) = parser
2271        .last_two()
2272        .expect("an import value must end with at least two tokens");
2273      let from = &parser.input[last.range.start as usize..last.range.end as usize];
2274      let item_end = parser.from_prev_end.unwrap_or(parser.item_end);
2275      parser.finish_item(&mut self.dependency_context, item_end);
2276      let items = self
2277        .dependency_context
2278        .finish_value_at_rule_import_items(checkpoint);
2279      if items.is_empty() {
2280        self.handle_warning.handle_warning(Warning {
2281          range: Range::new(start, at_rule_end),
2282          kind: WarningKind::Unexpected {
2283            message: "Broken '@value' at-rule",
2284          },
2285        });
2286      } else {
2287        self
2288          .dependency_context
2289          .push_dependency(Dependency::ICSSImportFrom { path: from });
2290        for index in items.as_usize_range() {
2291          let item = self.dependency_context.value_at_rule_import_item(index);
2292          self
2293            .dependency_context
2294            .push_dependency(Dependency::ICSSImportValue {
2295              prop: item.local_name(),
2296              value: item.import_name(),
2297            });
2298          self.insert_icss_symbol(item.local_name());
2299          self
2300            .dependency_context
2301            .push_dependency(Dependency::ICSSExportValue {
2302              prop: item.local_name(),
2303              value: item.local_name(),
2304            });
2305        }
2306      }
2307    } else {
2308      self
2309        .dependency_context
2310        .truncate_value_at_rule_import_items(checkpoint);
2311      let local_name;
2312      let value;
2313      let has_colon;
2314      if let Some(colon) = parser.first_colon {
2315        local_name = &parser.input[parser
2316          .first_significant
2317          .unwrap_or((colon.split, colon.split))
2318          .0 as usize..colon.prev_end as usize];
2319        let raw = &parser.input[colon.end as usize..parser.params_end as usize];
2320        value = if parser.first_colon_tokens_after > 0 {
2321          trim_css_whitespace(raw)
2322        } else {
2323          raw
2324        };
2325        has_colon = true;
2326      } else if let Some((first_start, first_end)) = parser.first_significant {
2327        local_name = &parser.input[first_start as usize..first_end as usize];
2328        let raw = &parser.input[first_end as usize..parser.params_end as usize];
2329        value = if parser.significant_count > 1 {
2330          trim_css_whitespace(raw)
2331        } else {
2332          raw
2333        };
2334        has_colon = false;
2335      } else {
2336        local_name = "";
2337        value = "";
2338        has_colon = false;
2339      }
2340      if local_name.is_empty() || (!has_colon && value.is_empty()) {
2341        self.handle_warning.handle_warning(Warning {
2342          range: Range::new(start, at_rule_end),
2343          kind: WarningKind::Unexpected {
2344            message: "Broken '@value' at-rule",
2345          },
2346        });
2347      }
2348      if !local_name.is_empty() {
2349        self
2350          .dependency_context
2351          .push_dependency(Dependency::ICSSExportValue {
2352            prop: local_name,
2353            value,
2354          });
2355        self.insert_icss_symbol(local_name);
2356      }
2357    }
2358    self
2359      .dependency_context
2360      .push_dependency(Dependency::Replace {
2361        content: "",
2362        range: Range::new(start, at_rule_end),
2363      });
2364    Some(())
2365  }
2366
2367  fn lex_icss_export(&mut self, stream: &mut DependencyTokenStream<'_, 's>) -> Option<()> {
2368    let left_curly = stream.next_parser_token().token;
2369    if left_curly.kind != TokenKind::LeftCurlyBracket {
2370      self.handle_warning.handle_warning(Warning {
2371        range: Range::new(left_curly.range.start, left_curly.range.end),
2372        kind: WarningKind::Unexpected {
2373          message: "Expected '{' during parsing of ':export'",
2374        },
2375      });
2376      return Some(());
2377    }
2378    loop {
2379      let first = stream.next_parser_token().token;
2380      if first.kind == TokenKind::Eof {
2381        return None;
2382      }
2383      if first.kind == TokenKind::RightCurlyBracket {
2384        break;
2385      }
2386      let prop_start = first.range.start;
2387      let prop_end = self.consume_icss_export_prop(stream, first)?;
2388      let colon = stream.peek_significant_skipping_comments(true).token;
2389      if colon.kind != TokenKind::Colon {
2390        self.handle_warning.handle_warning(Warning {
2391          range: Range::new(colon.range.start, colon.range.end),
2392          kind: WarningKind::Unexpected {
2393            message: "Expected ':' during parsing of ':export'",
2394          },
2395        });
2396        return Some(());
2397      }
2398      stream.next_parser_token();
2399      let value_start_token = stream.next_parser_token().token;
2400      if value_start_token.kind == TokenKind::Eof {
2401        return None;
2402      }
2403      let value_start = value_start_token.range.start;
2404      let value_end = self.consume_icss_value(stream, value_start_token)?;
2405      let delimiter = stream.next_parser_token().token;
2406      let value = stream
2407        .slice(value_start, value_end)?
2408        .trim_end_matches(is_css_white_space_char);
2409      self
2410        .dependency_context
2411        .push_dependency(Dependency::ICSSExportValue {
2412          prop: stream
2413            .slice(prop_start, prop_end)?
2414            .trim_end_matches(is_css_white_space_char),
2415          value,
2416        });
2417      self.insert_icss_symbol(
2418        stream
2419          .slice(prop_start, prop_end)?
2420          .trim_end_matches(is_css_white_space_char),
2421      );
2422      if delimiter.kind == TokenKind::RightCurlyBracket {
2423        break;
2424      }
2425    }
2426    Some(())
2427  }
2428
2429  fn lex_local_var(&mut self, stream: &mut DependencyTokenStream<'_, 's>) -> Option<()> {
2430    let name_token = stream.next_parser_token().token;
2431    let start = name_token.range.start;
2432    let raw_name = stream.slice(start, name_token.range.end)?;
2433    let name = if name_token.kind == TokenKind::Ident {
2434      dashed_ident_name(raw_name)
2435    } else {
2436      None
2437    };
2438    let Some(name) = name else {
2439      self.handle_warning.handle_warning(Warning {
2440        kind: WarningKind::Unexpected {
2441          message: "Expected starts with '--' during parsing of 'var()'",
2442        },
2443        range: Range::new(start, (start + 2).min(name_token.range.end)),
2444      });
2445      return Some(());
2446    };
2447    let end = name_token.range.end;
2448    let next = stream.peek_significant_skipping_comments(true).token;
2449    let (from, from_is_global) = if next.kind == TokenKind::Ident
2450      && is_ascii_keyword(stream.slice(next.range.start, next.range.end)?, "from")
2451    {
2452      stream.next_parser_token();
2453      let path_token = stream.peek_significant_skipping_comments(true).token;
2454      let path_start = path_token.range.start;
2455      let path_end = path_token.range.end;
2456      if !matches!(path_token.kind, TokenKind::QuotedString | TokenKind::Ident) {
2457        self.handle_warning.handle_warning(Warning {
2458          range: Range::new(path_start, path_end),
2459          kind: WarningKind::Unexpected {
2460            message: "Expected string or ident during parsing of 'var()'",
2461          },
2462        });
2463        return Some(());
2464      }
2465      stream.next_parser_token();
2466      let path = stream.slice(path_start, path_end)?;
2467      (
2468        Some(path),
2469        path_token.kind == TokenKind::Ident && path == "global",
2470      )
2471    } else {
2472      (None, false)
2473    };
2474    if from_is_global {
2475      let name_start = dashed_ident_name_start(raw_name)?;
2476      stream
2477        .lexer_mut()
2478        .visitor_mut()
2479        .discard_last(Range::new(start + name_start as Pos, end));
2480    }
2481    self
2482      .dependency_context
2483      .push_dependency(Dependency::LocalVar {
2484        name,
2485        range: Range::new(start, end),
2486        from,
2487        from_is_global,
2488      });
2489    Some(())
2490  }
2491
2492  fn lex_local_dashed_ident_decl(
2493    &mut self,
2494    stream: &mut DependencyTokenStream<'_, 's>,
2495    local_decl_dependency: impl FnOnce(&'s str, Range) -> Dependency<'s>,
2496    dashed_warning: impl FnOnce(Range) -> Warning<'s>,
2497    left_curly_warning: impl FnOnce(Range) -> Warning<'s>,
2498  ) -> Option<()> {
2499    let name_token = stream.next_parser_token().token;
2500    let start = name_token.range.start;
2501    if name_token.kind != TokenKind::Ident
2502      || !stream.slice(start, name_token.range.end)?.starts_with("--")
2503    {
2504      self
2505        .handle_warning
2506        .handle_warning(dashed_warning(Range::new(
2507          start,
2508          (start + 2).min(name_token.range.end),
2509        )));
2510      return Some(());
2511    }
2512    let end = name_token.range.end;
2513    self
2514      .dependency_context
2515      .push_dependency(local_decl_dependency(
2516        dashed_ident_name(stream.slice(start, end)?)?,
2517        Range::new(start, end),
2518      ));
2519    let left_curly = stream.peek_significant_skipping_comments(true).token;
2520    if left_curly.kind != TokenKind::LeftCurlyBracket {
2521      self
2522        .handle_warning
2523        .handle_warning(left_curly_warning(Range::new(
2524          left_curly.range.start,
2525          left_curly.range.end,
2526        )));
2527      return Some(());
2528    }
2529    Some(())
2530  }
2531
2532  fn lex_local_keyframes_decl(&mut self, stream: &mut DependencyTokenStream<'_, 's>) -> Option<()> {
2533    let mut is_function = false;
2534    let first = stream.next_parser_token().token;
2535    let name_token = if first.kind == TokenKind::Colon {
2536      let pseudo_start = first.range.start;
2537      let pseudo_name = stream.next_parser_token().token;
2538      let pseudo_end = if matches!(pseudo_name.kind, TokenKind::Ident | TokenKind::Function) {
2539        pseudo_name.range.end
2540      } else {
2541        first.range.end
2542      };
2543      let pseudo = stream.slice(pseudo_start, pseudo_end)?;
2544      if pseudo_name.kind == TokenKind::Function {
2545        self.handle_pseudo_function(stream, pseudo_start, pseudo_end, pseudo_name.flags)?;
2546      } else if pseudo_name.kind == TokenKind::Ident {
2547        self.handle_pseudo_class(stream, pseudo_start, pseudo_end, pseudo_name.flags)?;
2548      }
2549      let mode_data = self
2550        .mode_data
2551        .as_ref()
2552        .expect("CSS Modules mode data must exist while parsing keyframes");
2553      if mode_data.is_pure_mode()
2554        && !mode_data.is_pure_check_disabled()
2555        && (pseudo.eq_ignore_ascii_case(":global(") || pseudo.eq_ignore_ascii_case(":global"))
2556      {
2557        self.handle_warning.handle_warning(Warning {
2558          range: Range::new(pseudo_start, pseudo_end),
2559          kind: WarningKind::NotPure {
2560            message: "'@keyframes :global' is not allowed in pure mode",
2561          },
2562        });
2563      }
2564      is_function =
2565        pseudo.eq_ignore_ascii_case(":local(") || pseudo.eq_ignore_ascii_case(":global(");
2566      if !is_function
2567        && !pseudo.eq_ignore_ascii_case(":local")
2568        && !pseudo.eq_ignore_ascii_case(":global")
2569      {
2570        self.handle_warning.handle_warning(Warning {
2571                    range: Range::new(pseudo_start, pseudo_end),
2572                    kind: WarningKind::Unexpected {
2573                        message: "Expected ':local', ':local()', ':global', or ':global()' during parsing of '@keyframes' name",
2574                    }
2575                });
2576        return Some(());
2577      }
2578      stream.next_parser_token().token
2579    } else {
2580      first
2581    };
2582
2583    let start = name_token.range.start;
2584    if name_token.kind != TokenKind::Ident {
2585      self.handle_warning.handle_warning(Warning {
2586        range: Range::new(start, start.saturating_add(2).min(stream.source_end())),
2587        kind: WarningKind::Unexpected {
2588          message: "Expected ident during parsing of '@keyframes' name",
2589        },
2590      });
2591      return Some(());
2592    }
2593    let end = name_token.range.end;
2594    if self
2595      .mode_data
2596      .as_ref()
2597      .expect("CSS Modules mode data must exist while parsing keyframes")
2598      .is_current_local_mode()
2599    {
2600      let name = stream.slice(start, end)?;
2601      self
2602        .dependency_context
2603        .push_dependency(Dependency::LocalKeyframesDecl {
2604          name,
2605          range: Range::new(start, end),
2606        });
2607    }
2608    if is_function {
2609      let right_parenthesis = stream.peek_significant_skipping_comments(true).token;
2610      if right_parenthesis.kind != TokenKind::RightParenthesis {
2611        self.handle_warning.handle_warning(Warning {
2612          range: Range::new(right_parenthesis.range.start, right_parenthesis.range.end),
2613          kind: WarningKind::Unexpected {
2614            message: "Expected ')' during parsing of '@keyframes :local(' or '@keyframes :global('",
2615          },
2616        });
2617        return Some(());
2618      }
2619      stream.next_parser_token();
2620      self
2621        .dependency_context
2622        .push_dependency(Dependency::Replace {
2623          content: "",
2624          range: Range::new(right_parenthesis.range.start, right_parenthesis.range.end),
2625        });
2626      let mode_data = self
2627        .mode_data
2628        .as_mut()
2629        .expect("CSS Modules mode data must exist while leaving a keyframes mode function");
2630      mode_data.inside_mode_function -= 1;
2631      self.balanced.pop_without_moda_data();
2632    }
2633    let left_curly = stream.peek_significant_skipping_comments(true).token;
2634    if left_curly.kind != TokenKind::LeftCurlyBracket {
2635      self.handle_warning.handle_warning(Warning {
2636        range: Range::new(left_curly.range.start, left_curly.range.end),
2637        kind: WarningKind::Unexpected {
2638          message: "Expected '{' during parsing of '@keyframes'",
2639        },
2640      });
2641      return Some(());
2642    }
2643    Some(())
2644  }
2645
2646  fn handle_local_keyframes_dependency(&mut self, lexer: &DependencyLexer<'s>) -> Option<()> {
2647    let animation = self
2648      .in_animation_property
2649      .as_mut()
2650      .expect("animation state must exist while handling an animation dependency");
2651    if let Some(range) = animation.take_rename(self.balanced.len()) {
2652      self
2653        .dependency_context
2654        .push_dependency(Dependency::LocalKeyframes {
2655          name: lexer.slice(range.start, range.end)?,
2656          range,
2657        });
2658    }
2659    animation.reset_reserved();
2660    Some(())
2661  }
2662
2663  fn lex_local_counter_style_decl(
2664    &mut self,
2665    stream: &mut DependencyTokenStream<'_, 's>,
2666  ) -> Option<()> {
2667    let name_token = stream.next_parser_token().token;
2668    let start = name_token.range.start;
2669    if name_token.kind != TokenKind::Ident {
2670      self.handle_warning.handle_warning(Warning {
2671        range: Range::new(start, name_token.range.end),
2672        kind: WarningKind::Unexpected {
2673          message: "Expected ident during parsing of '@counter-style'",
2674        },
2675      });
2676      return Some(());
2677    }
2678    let end = name_token.range.end;
2679    self
2680      .dependency_context
2681      .push_dependency(Dependency::LocalCounterStyleDecl {
2682        name: stream.slice(start, end)?,
2683        range: Range::new(start, end),
2684      });
2685    let left_curly = stream.peek_significant_skipping_comments(true).token;
2686    if left_curly.kind != TokenKind::LeftCurlyBracket {
2687      self.handle_warning.handle_warning(Warning {
2688        range: Range::new(left_curly.range.start, left_curly.range.end),
2689        kind: WarningKind::Unexpected {
2690          message: "Expected '{' during parsing of '@counter-style'",
2691        },
2692      });
2693      return Some(());
2694    }
2695    Some(())
2696  }
2697
2698  fn lex_local_container_at_rule(
2699    &mut self,
2700    stream: &mut DependencyTokenStream<'_, 's>,
2701  ) -> Option<()> {
2702    let name_token = stream.next_parser_token().token;
2703    if name_token.kind == TokenKind::LeftParenthesis {
2704      return Some(());
2705    }
2706    if name_token.kind != TokenKind::Ident {
2707      return Some(());
2708    }
2709    let start = name_token.range.start;
2710    let end = name_token.range.end;
2711    let ident = stream.slice(start, end)?;
2712    if is_reserved_container_query_ident(ident, name_token.flags) {
2713      return Some(());
2714    }
2715    if self
2716      .mode_data
2717      .as_ref()
2718      .expect("CSS Modules mode data must exist while parsing a container")
2719      .is_current_local_mode()
2720    {
2721      self
2722        .dependency_context
2723        .push_dependency(Dependency::LocalContainer {
2724          name: ident,
2725          range: Range::new(start, end),
2726        });
2727    }
2728    Some(())
2729  }
2730
2731  fn lex_local_function_decl(&mut self, stream: &mut DependencyTokenStream<'_, 's>) -> Option<()> {
2732    let mut is_function = false;
2733    let first = stream.next_parser_token().token;
2734    let name_token = if first.kind == TokenKind::Colon {
2735      let pseudo_start = first.range.start;
2736      let pseudo_name = stream.next_parser_token().token;
2737      let pseudo_end = if matches!(pseudo_name.kind, TokenKind::Ident | TokenKind::Function) {
2738        pseudo_name.range.end
2739      } else {
2740        first.range.end
2741      };
2742      let pseudo = stream.slice(pseudo_start, pseudo_end)?;
2743      if pseudo_name.kind == TokenKind::Function {
2744        self.handle_pseudo_function(stream, pseudo_start, pseudo_end, pseudo_name.flags)?;
2745      } else if pseudo_name.kind == TokenKind::Ident {
2746        self.handle_pseudo_class(stream, pseudo_start, pseudo_end, pseudo_name.flags)?;
2747      }
2748      let mode_data = self
2749        .mode_data
2750        .as_ref()
2751        .expect("CSS Modules mode data must exist while parsing a function");
2752      if mode_data.is_pure_mode()
2753        && !mode_data.is_pure_check_disabled()
2754        && (pseudo.eq_ignore_ascii_case(":global(") || pseudo.eq_ignore_ascii_case(":global"))
2755      {
2756        self.handle_warning.handle_warning(Warning {
2757          range: Range::new(pseudo_start, pseudo_end),
2758          kind: WarningKind::NotPure {
2759            message: "'@function :global' is not allowed in pure mode",
2760          },
2761        });
2762      }
2763      is_function =
2764        pseudo.eq_ignore_ascii_case(":local(") || pseudo.eq_ignore_ascii_case(":global(");
2765      if !is_function
2766        && !pseudo.eq_ignore_ascii_case(":local")
2767        && !pseudo.eq_ignore_ascii_case(":global")
2768      {
2769        self.handle_warning.handle_warning(Warning {
2770                    range: Range::new(pseudo_start, pseudo_end),
2771                    kind: WarningKind::Unexpected {
2772                        message:
2773                            "Expected ':local', ':local()', ':global', or ':global()' during parsing of '@function' name",
2774                    },
2775                });
2776        return Some(());
2777      }
2778      stream.next_parser_token().token
2779    } else {
2780      first
2781    };
2782
2783    let name_range = if name_token.kind == TokenKind::Function {
2784      name_token.value_range
2785    } else {
2786      name_token.range
2787    };
2788    let start = name_range.start;
2789    let end = name_range.end;
2790    let name = stream.slice_trusted(start, end);
2791    if name_token.kind != TokenKind::Ident && name_token.kind != TokenKind::Function
2792      || !name.starts_with("--")
2793    {
2794      self.handle_warning.handle_warning(Warning {
2795        range: Range::new(start, start.saturating_add(2).min(stream.source_end())),
2796        kind: WarningKind::Unexpected {
2797          message: "Expected starts with '--' during parsing of '@function' name",
2798        },
2799      });
2800      return Some(());
2801    }
2802    if self
2803      .mode_data
2804      .as_ref()
2805      .expect("CSS Modules mode data must exist while parsing a function")
2806      .is_current_local_mode()
2807    {
2808      self
2809        .dependency_context
2810        .push_dependency(Dependency::LocalFunctionDecl {
2811          name: dashed_ident_name(stream.slice(start, end)?)?,
2812          range: Range::new(start, end),
2813        });
2814    }
2815
2816    if is_function {
2817      let right_parenthesis = stream.peek_significant_skipping_comments(true).token;
2818      if right_parenthesis.kind != TokenKind::RightParenthesis {
2819        self.handle_warning.handle_warning(Warning {
2820          range: Range::new(right_parenthesis.range.start, right_parenthesis.range.end),
2821          kind: WarningKind::Unexpected {
2822            message: "Expected ')' during parsing of '@function :local(' or '@function :global('",
2823          },
2824        });
2825        return Some(());
2826      }
2827      stream.next_parser_token();
2828      self
2829        .dependency_context
2830        .push_dependency(Dependency::Replace {
2831          content: "",
2832          range: Range::new(right_parenthesis.range.start, right_parenthesis.range.end),
2833        });
2834      let mode_data = self
2835        .mode_data
2836        .as_mut()
2837        .expect("CSS Modules mode data must exist while leaving a function mode function");
2838      mode_data.inside_mode_function -= 1;
2839      self.balanced.pop_without_moda_data();
2840    }
2841
2842    if name_token.kind == TokenKind::Function {
2843      return Some(());
2844    }
2845    let left_parenthesis = stream.peek_significant_skipping_comments(true).token;
2846    if left_parenthesis.kind != TokenKind::LeftParenthesis {
2847      self.handle_warning.handle_warning(Warning {
2848        range: Range::new(left_parenthesis.range.start, left_parenthesis.range.end),
2849        kind: WarningKind::Unexpected {
2850          message: "Expected '(' during parsing of '@function'",
2851        },
2852      });
2853    }
2854    Some(())
2855  }
2856
2857  fn handle_local_counter_style_dependency(&mut self, lexer: &DependencyLexer<'s>) -> Option<()> {
2858    let list_style = self
2859      .in_list_style_property
2860      .as_mut()
2861      .expect("list-style state must exist while handling a counter-style dependency");
2862    if let Some(range) = list_style.take_rename(self.balanced.len()) {
2863      self
2864        .dependency_context
2865        .push_dependency(Dependency::LocalCounterStyle {
2866          name: lexer.slice(range.start, range.end)?,
2867          range,
2868        });
2869    }
2870    Some(())
2871  }
2872
2873  fn handle_local_font_palette_dependency(&mut self, lexer: &DependencyLexer<'s>) -> Option<()> {
2874    let font_palette = self
2875      .in_font_palette_property
2876      .as_mut()
2877      .expect("font-palette state must exist while handling a font-palette dependency");
2878    if let Some(range) = font_palette.take_rename(self.balanced.len()) {
2879      self
2880        .dependency_context
2881        .push_dependency(Dependency::LocalFontPalette {
2882          name: dashed_ident_name(lexer.slice(range.start, range.end)?)?,
2883          range,
2884        });
2885    }
2886    Some(())
2887  }
2888
2889  fn lex_composes(
2890    &mut self,
2891    stream: &mut DependencyTokenStream<'_, 's>,
2892    local_classes: SmallVec<[&'s str; 2]>,
2893    start: Pos,
2894  ) -> Option<()> {
2895    let colon = stream.peek_significant_skipping_comments(true).token;
2896    if colon.kind != TokenKind::Colon {
2897      return Some(());
2898    }
2899    stream.next_parser_token();
2900    let mut replacement_end = colon.range.end;
2901    loop {
2902      let first = stream.peek_significant_skipping_comments(true).token;
2903      if first.kind == TokenKind::Eof {
2904        break;
2905      }
2906      if first.kind == TokenKind::RightCurlyBracket {
2907        break;
2908      }
2909      if first.kind == TokenKind::Semicolon {
2910        stream.next_parser_token();
2911        replacement_end = first.range.end;
2912        break;
2913      }
2914
2915      let item_start = first.range.start;
2916      let mut item_end = item_start;
2917      let mut names: SmallVec<[&'s str; 2]> = SmallVec::new();
2918      let mut has_from = false;
2919      let mut delimiter = first;
2920      loop {
2921        if matches!(
2922          delimiter.kind,
2923          TokenKind::Comma | TokenKind::Semicolon | TokenKind::RightCurlyBracket
2924        ) {
2925          break;
2926        }
2927
2928        if delimiter.kind == TokenKind::Function
2929          && stream.slice(delimiter.range.start, delimiter.range.end)? == "global("
2930        {
2931          let global_start = delimiter.range.start;
2932          stream.next_parser_token();
2933          let name_token = stream.next_parser_token().token;
2934          let Some(name_range) = ident_like_range(name_token) else {
2935            self.handle_warning.handle_warning(Warning {
2936              range: Range::new(
2937                name_token.range.start,
2938                name_token
2939                  .range
2940                  .start
2941                  .saturating_add(2)
2942                  .min(stream.source_end()),
2943              ),
2944              kind: WarningKind::Unexpected {
2945                message: "Expected ident during parsing of 'composes'",
2946              },
2947            });
2948            return Some(());
2949          };
2950          let right_parenthesis = stream.peek_significant_skipping_comments(true).token;
2951          if right_parenthesis.kind != TokenKind::RightParenthesis {
2952            self.handle_warning.handle_warning(Warning {
2953              range: Range::new(right_parenthesis.range.start, right_parenthesis.range.end),
2954              kind: WarningKind::Unexpected {
2955                message: "Expected ')' during parsing of 'composes'",
2956              },
2957            });
2958            return Some(());
2959          }
2960          stream.next_parser_token();
2961          item_end = right_parenthesis.range.end;
2962          self.dependency_context.push_composes(
2963            local_classes.iter().copied(),
2964            std::iter::once(stream.slice(name_range.start, name_range.end)?),
2965            Some("global"),
2966            true,
2967            Range::new(global_start, item_end),
2968          );
2969          delimiter = stream.peek_significant_skipping_comments(true).token;
2970          continue;
2971        }
2972
2973        let Some(name_range) = ident_like_range(delimiter) else {
2974          let name_start = delimiter.range.start;
2975          self.handle_warning.handle_warning(Warning {
2976            range: Range::new(
2977              name_start,
2978              name_start.saturating_add(2).min(stream.source_end()),
2979            ),
2980            kind: WarningKind::Unexpected {
2981              message: "Expected ident during parsing of 'composes'",
2982            },
2983          });
2984          return Some(());
2985        };
2986        let ident = stream.slice(name_range.start, name_range.end)?;
2987        if !names.is_empty() && ident.eq_ignore_ascii_case("from") {
2988          stream.next_parser_token();
2989          let path = stream.peek_significant_skipping_comments(true).token;
2990          if matches!(
2991            path.kind,
2992            TokenKind::QuotedString | TokenKind::Ident | TokenKind::Function
2993          ) {
2994            let path_range = if path.kind == TokenKind::Function {
2995              path.value_range
2996            } else {
2997              path.range
2998            };
2999            item_end = path_range.end;
3000            let from_is_global = path.kind == TokenKind::Ident
3001              && is_ascii_keyword(stream.slice(path_range.start, path_range.end)?, "global");
3002            self.dependency_context.push_composes(
3003              local_classes.iter().copied(),
3004              std::mem::take(&mut names),
3005              Some(stream.slice(path_range.start, path_range.end)?),
3006              from_is_global,
3007              Range::new(item_start, item_end),
3008            );
3009            has_from = true;
3010            stream.next_parser_token();
3011            delimiter = stream.peek_significant_skipping_comments(true).token;
3012            break;
3013          }
3014          names.push(ident);
3015          item_end = name_range.end;
3016          delimiter = path;
3017          continue;
3018        }
3019        names.push(ident);
3020        item_end = name_range.end;
3021        stream.next_parser_token();
3022        delimiter = stream.peek_significant_skipping_comments(true).token;
3023      }
3024
3025      if has_from {
3026        if delimiter.kind == TokenKind::Comma {
3027          stream.next_parser_token();
3028          replacement_end = delimiter.range.end;
3029          continue;
3030        }
3031        if delimiter.kind == TokenKind::Semicolon {
3032          stream.next_parser_token();
3033          replacement_end = delimiter.range.end;
3034          break;
3035        }
3036        if delimiter.kind == TokenKind::RightCurlyBracket {
3037          replacement_end = item_end;
3038          break;
3039        }
3040        replacement_end = item_end;
3041        break;
3042      }
3043
3044      if delimiter.kind == TokenKind::Comma {
3045        if !names.is_empty() {
3046          self.dependency_context.push_composes(
3047            local_classes.iter().copied(),
3048            names,
3049            None,
3050            false,
3051            Range::new(item_start, item_end),
3052          );
3053        }
3054        stream.next_parser_token();
3055        replacement_end = delimiter.range.end;
3056        continue;
3057      }
3058
3059      if delimiter.kind == TokenKind::Semicolon {
3060        if !names.is_empty() {
3061          self.dependency_context.push_composes(
3062            local_classes.iter().copied(),
3063            names,
3064            None,
3065            false,
3066            Range::new(item_start, item_end),
3067          );
3068        }
3069        stream.next_parser_token();
3070        replacement_end = delimiter.range.end;
3071        break;
3072      }
3073
3074      if delimiter.kind == TokenKind::RightCurlyBracket {
3075        if !names.is_empty() {
3076          self.dependency_context.push_composes(
3077            local_classes.iter().copied(),
3078            names,
3079            None,
3080            false,
3081            Range::new(item_start, item_end),
3082          );
3083        }
3084        replacement_end = item_end;
3085        break;
3086      }
3087
3088      // An invalid token was encountered after a name.  The next loop
3089      // iteration would produce the same warning, but keeping the
3090      // cursor at that token preserves the legacy recovery point.
3091      self.handle_warning.handle_warning(Warning {
3092        range: Range::new(
3093          delimiter.range.start,
3094          delimiter
3095            .range
3096            .start
3097            .saturating_add(2)
3098            .min(stream.source_end()),
3099        ),
3100        kind: WarningKind::Unexpected {
3101          message: "Expected ident during parsing of 'composes'",
3102        },
3103      });
3104      return Some(());
3105    }
3106    self
3107      .dependency_context
3108      .push_dependency(Dependency::Replace {
3109        content: "",
3110        range: Range::new(start, replacement_end),
3111      });
3112    Some(())
3113  }
3114}
3115
3116impl<'s, W: HandleWarning<'s>> LexDependencies<'s, W> {
3117  fn magic_comments_before(lexer: &DependencyLexer<'s>, start: Pos) -> Option<&'s str> {
3118    let input = lexer.slice(0, start)?;
3119    let range = preceding_comment_range(input)?;
3120    lexer.slice(range.start, range.end)
3121  }
3122
3123  fn handle_comment(
3124    &mut self,
3125    lexer: &mut DependencyLexer<'s>,
3126    start: Pos,
3127    end: Pos,
3128  ) -> Option<()> {
3129    let Some(mode_data) = &mut self.mode_data else {
3130      return Some(());
3131    };
3132    if !mode_data.is_pure_mode() || end < start + 4 {
3133      return Some(());
3134    }
3135
3136    let content = lexer.slice(start + 2, end - 2)?;
3137    if is_css_modules_magic_comment(content, "cssmodules-pure-ignore") {
3138      mode_data.mark_pure_ignore();
3139    } else if matches!(self.scope, Scope::TopLevel)
3140      && self.block_nesting_level == 0
3141      && is_css_modules_magic_comment(content, "cssmodules-pure-no-check")
3142    {
3143      mode_data.mark_pure_no_check();
3144    }
3145    Some(())
3146  }
3147
3148  fn handle_url(
3149    &mut self,
3150    lexer: &mut DependencyLexer<'s>,
3151    start: Pos,
3152    end: Pos,
3153    content_start: Pos,
3154    content_end: Pos,
3155    flags: TokenFlags,
3156  ) -> Option<()> {
3157    let value = lexer.slice(content_start, content_end)?;
3158    let can_be_dependency = match &self.scope {
3159      Scope::InAtImport(import_data) => !import_data.in_supports(),
3160      Scope::InBlock => true,
3161      _ => false,
3162    };
3163    let magic_comments = can_be_dependency
3164      .then(|| Self::magic_comments_before(lexer, start))
3165      .flatten();
3166    match self.scope {
3167      Scope::InAtImport(ref mut import_data) => {
3168        if import_data.in_supports() {
3169          return Some(());
3170        }
3171        if import_data.url.is_some() {
3172          self.handle_warning.handle_warning(Warning {
3173            range: Range::new(import_data.start, end),
3174            kind: WarningKind::DuplicateUrl {
3175              when: lexer.slice(import_data.start, end)?,
3176            },
3177          });
3178          return Some(());
3179        }
3180        import_data.prelude.push(ImportPreludeNode::Url {
3181          range: Range::new(start, end),
3182        });
3183        import_data.url = Some(value);
3184        import_data.url_flags = flags;
3185        import_data.url_range = Some(Range::new(start, end));
3186        import_data.magic_comments = magic_comments;
3187      }
3188      Scope::InBlock => {
3189        self.dependency_context.push_dependency(Dependency::Url {
3190          request: value,
3191          range: Range::new(start, end),
3192          kind: UrlRangeKind::Function,
3193          magic_comments,
3194        });
3195      }
3196      _ => {}
3197    }
3198    Some(())
3199  }
3200
3201  fn handle_string(
3202    &mut self,
3203    lexer: &mut DependencyLexer<'s>,
3204    start: Pos,
3205    end: Pos,
3206    flags: TokenFlags,
3207  ) -> Option<()> {
3208    let inside_url = matches!(
3209      self.balanced.last(),
3210      Some(last) if matches!(last.kind, BalancedItemKind::Url)
3211    );
3212    let can_be_dependency = match &self.scope {
3213      Scope::InAtImport(import_data) => {
3214        !import_data.in_supports() && (inside_url || import_data.url.is_none())
3215      }
3216      Scope::InBlock => matches!(
3217        self.balanced.last(),
3218        Some(last) if matches!(last.kind, BalancedItemKind::Url | BalancedItemKind::ImageSet)
3219      ),
3220      _ => false,
3221    };
3222    let mut magic_comments = can_be_dependency
3223      .then(|| Self::magic_comments_before(lexer, start))
3224      .flatten();
3225    if magic_comments.is_none()
3226      && let Some(range) = self.balanced.last().and_then(|item| item.magic_comments)
3227    {
3228      magic_comments = lexer.slice(range.start, range.end);
3229    }
3230    match self.scope {
3231      Scope::InAtImport(ref mut import_data) => {
3232        // Do not parse URLs in `supports(...)`.
3233        if import_data.in_supports() {
3234          return Some(());
3235        }
3236        // Do not parse other strings if we already have a URL.
3237        if !inside_url && import_data.url.is_some() {
3238          return Some(());
3239        }
3240
3241        if inside_url && import_data.url.is_some() {
3242          self.handle_warning.handle_warning(Warning {
3243            range: Range::new(import_data.start, end),
3244            kind: WarningKind::DuplicateUrl {
3245              when: lexer.slice(import_data.start, end)?,
3246            },
3247          });
3248          return Some(());
3249        }
3250
3251        let value = lexer.slice(start + 1, end - 1)?;
3252        import_data.url = Some(value);
3253        import_data.url_flags = flags;
3254        import_data.magic_comments = magic_comments;
3255        // For url("inside_url") url_range will determined in right_parenthesis
3256        if !inside_url {
3257          import_data.prelude.push(ImportPreludeNode::Url {
3258            range: Range::new(start, end),
3259          });
3260          import_data.url_range = Some(Range::new(start, end));
3261        }
3262      }
3263      Scope::InBlock => {
3264        if let Some(mode_data) = &self.mode_data
3265          && mode_data.is_property_local_mode()
3266          && matches!(
3267            self.in_grid_property,
3268            Some(GridPropertyKind::TemplateLike | GridPropertyKind::TemplateAreas)
3269          )
3270        {
3271          for range in parse_grid_template_area_ranges(lexer.slice(start + 1, end - 1)?, start + 1)
3272          {
3273            self
3274              .dependency_context
3275              .push_dependency(Dependency::LocalGridDecl {
3276                name: lexer.slice(range.start, range.end)?,
3277                range,
3278              });
3279          }
3280        }
3281        let Some(last) = self.balanced.last() else {
3282          return Some(());
3283        };
3284        let kind = match last.kind {
3285          BalancedItemKind::Url => UrlRangeKind::String,
3286          BalancedItemKind::ImageSet => UrlRangeKind::Function,
3287          _ => return Some(()),
3288        };
3289        let value = lexer.slice(start + 1, end - 1)?;
3290        self.dependency_context.push_dependency(Dependency::Url {
3291          request: value,
3292          range: Range::new(start, end),
3293          kind,
3294          magic_comments,
3295        });
3296      }
3297      _ => {}
3298    }
3299    Some(())
3300  }
3301
3302  fn handle_at_keyword(
3303    &mut self,
3304    stream: &mut DependencyTokenStream<'_, 's>,
3305    start: Pos,
3306    end: Pos,
3307    flags: TokenFlags,
3308  ) -> Option<()> {
3309    let name = stream.slice_trusted(start, end);
3310    let kind = Self::classify_at_rule(name, flags);
3311    self.set_scan_context(stream, ScanContext::AtRule);
3312    if kind == AtRuleKind::Namespace {
3313      self.scope = Scope::AtNamespaceInvalid;
3314      self.handle_warning.handle_warning(Warning {
3315        range: Range::new(start, end),
3316        kind: WarningKind::NamespaceNotSupportedInBundledCss,
3317      });
3318    } else if kind == AtRuleKind::Import {
3319      if !self.allow_import_at_rule {
3320        self.scope = Scope::AtImportInvalid;
3321        self.handle_warning.handle_warning(Warning {
3322          range: Range::new(start, end),
3323          kind: WarningKind::NotPrecededAtImport,
3324        });
3325        return Some(());
3326      }
3327      self.scope = Scope::InAtImport(ImportData::new(start));
3328    } else if kind == AtRuleKind::Charset {
3329      self.lex_charset_at_rule(stream, start)?;
3330      self.set_scan_context(stream, ScanContext::TopLevel);
3331      self.is_next_rule_prelude = true;
3332    } else if self.mode_data.is_some() {
3333      let mut can_contain_rules = true;
3334      if kind == AtRuleKind::Value {
3335        self.lex_value_at_rule(stream, start)?;
3336        can_contain_rules = false;
3337        self.set_scan_context(stream, ScanContext::TopLevel);
3338        self.is_next_rule_prelude = true;
3339      } else if kind == AtRuleKind::Keyframes {
3340        self.lex_local_keyframes_decl(stream)?;
3341      } else if kind == AtRuleKind::Container {
3342        self.lex_local_container_at_rule(stream)?;
3343      } else if kind == AtRuleKind::Function {
3344        self.lex_local_function_decl(stream)?;
3345      } else if kind == AtRuleKind::Property {
3346        self.lex_local_dashed_ident_decl(
3347          stream,
3348          |name, range| Dependency::LocalPropertyDecl { name, range },
3349          |range| Warning {
3350            range,
3351            kind: WarningKind::Unexpected {
3352              message: "Expected starts with '--' during parsing of '@property'",
3353            },
3354          },
3355          |range| Warning {
3356            range,
3357            kind: WarningKind::Unexpected {
3358              message: "Expected '{' during parsing of '@property'",
3359            },
3360          },
3361        )?;
3362      } else if kind == AtRuleKind::CounterStyle {
3363        self.lex_local_counter_style_decl(stream)?;
3364      } else if kind == AtRuleKind::FontPaletteValues {
3365        self.lex_local_dashed_ident_decl(
3366          stream,
3367          |name, range| Dependency::LocalFontPaletteDecl { name, range },
3368          |range| Warning {
3369            range,
3370            kind: WarningKind::Unexpected {
3371              message: "Expected starts with '--' during parsing of '@font-palette-values'",
3372            },
3373          },
3374          |range| Warning {
3375            range,
3376            kind: WarningKind::Unexpected {
3377              message: "Expected '{' during parsing of '@font-palette-values'",
3378            },
3379          },
3380        )?;
3381      } else {
3382        self.is_next_rule_prelude = kind == AtRuleKind::Scope;
3383        if self.is_next_rule_prelude {
3384          self.set_scan_context(stream, ScanContext::Selector);
3385        }
3386      }
3387
3388      let mode_data = self
3389        .mode_data
3390        .as_mut()
3391        .expect("CSS Modules mode data must exist while handling an at-rule");
3392      if can_contain_rules && self.block_nesting_level == 0 {
3393        mode_data.composes_local_classes.find_at_keyword();
3394      }
3395
3396      if mode_data.is_pure_mode() {
3397        mode_data.pure_global = None;
3398      }
3399    }
3400    Some(())
3401  }
3402
3403  fn handle_semicolon(
3404    &mut self,
3405    lexer: &mut DependencyLexer<'s>,
3406    start: Pos,
3407    end: Pos,
3408  ) -> Option<()> {
3409    match self.scope {
3410      Scope::InAtImport(ref import_data) => {
3411        let Some(url) = import_data.url else {
3412          if let Some((name, name_range)) = import_data.prelude.icss_import_url() {
3413            self
3414              .dependency_context
3415              .push_dependency(Dependency::ICSSImportUrl {
3416                name,
3417                range: Range::new(import_data.start, end),
3418                name_range: *name_range,
3419              });
3420            self.scope = Scope::TopLevel;
3421            return Some(());
3422          }
3423          self.handle_warning.handle_warning(Warning {
3424            range: Range::new(import_data.start, end),
3425            kind: WarningKind::ExpectedUrl {
3426              when: lexer.slice(import_data.start, end)?,
3427            },
3428          });
3429          self.scope = Scope::TopLevel;
3430          return Some(());
3431        };
3432        let Some(url_range) = &import_data.url_range else {
3433          self.handle_warning.handle_warning(Warning {
3434            range: Range::new(start, end),
3435            kind: WarningKind::Unexpected {
3436              message: "Unexpected ';' during parsing of '@import url()'",
3437            },
3438          });
3439          self.scope = Scope::TopLevel;
3440          return Some(());
3441        };
3442        if let Some(range) = import_data.prelude.first_non_url_before(url_range) {
3443          self.handle_warning.handle_warning(Warning {
3444            range: *url_range,
3445            kind: WarningKind::ExpectedUrlBefore {
3446              when: lexer.slice(range.start, url_range.end)?,
3447            },
3448          });
3449          self.scope = Scope::TopLevel;
3450          return Some(());
3451        }
3452        let layer = match &import_data.layer {
3453          ImportDataLayer::None => None,
3454          ImportDataLayer::EndLayer { value, range } => {
3455            if url_range.start > range.start {
3456              self.handle_warning.handle_warning(Warning {
3457                range: *url_range,
3458                kind: WarningKind::ExpectedUrlBefore {
3459                  when: lexer.slice(range.start, url_range.end)?,
3460                },
3461              });
3462              self.scope = Scope::TopLevel;
3463              return Some(());
3464            }
3465            Some(*value)
3466          }
3467        };
3468        let supports = match &import_data.supports {
3469          ImportDataSupports::None => None,
3470          ImportDataSupports::InSupports => {
3471            self.handle_warning.handle_warning(Warning {
3472              range: Range::new(start, end),
3473              kind: WarningKind::Unexpected {
3474                message: "Unexpected ';' during parsing of 'supports()'",
3475              },
3476            });
3477            None
3478          }
3479          ImportDataSupports::EndSupports { value, range } => {
3480            if url_range.start > range.start {
3481              self.handle_warning.handle_warning(Warning {
3482                range: *url_range,
3483                kind: WarningKind::ExpectedUrlBefore {
3484                  when: lexer.slice(range.start, url_range.end)?,
3485                },
3486              });
3487              self.scope = Scope::TopLevel;
3488              return Some(());
3489            }
3490            Some(*value)
3491          }
3492        };
3493        if let Some(layer_range) = import_data.layer_range()
3494          && let Some(supports_range) = import_data.supports_range()
3495          && layer_range.start > supports_range.start
3496        {
3497          self.handle_warning.handle_warning(Warning {
3498            range: *layer_range,
3499            kind: WarningKind::ExpectedLayerBefore {
3500              when: lexer.slice(supports_range.start, layer_range.end)?,
3501            },
3502          });
3503          self.scope = Scope::TopLevel;
3504          return Some(());
3505        }
3506        let last_end = import_data
3507          .supports_range()
3508          .or_else(|| import_data.layer_range())
3509          .unwrap_or(url_range)
3510          .end;
3511        let media = self.get_media(lexer, last_end, start);
3512        self.dependency_context.push_import(
3513          url,
3514          Range::new(import_data.start, end),
3515          layer,
3516          supports,
3517          media,
3518          import_data.magic_comments,
3519        );
3520        self.scope = Scope::TopLevel;
3521      }
3522      Scope::AtImportInvalid | Scope::AtNamespaceInvalid => {
3523        self.scope = Scope::TopLevel;
3524      }
3525      Scope::InBlock => {
3526        if let Some(mode_data) = &mut self.mode_data {
3527          mode_data.pure_global = Some(end);
3528
3529          if mode_data.is_property_local_mode() {
3530            if self.in_animation_property.is_some() {
3531              self.handle_local_keyframes_dependency(lexer)?;
3532              self.exit_animation_property();
3533            }
3534            if self.in_list_style_property.is_some() {
3535              self.handle_local_counter_style_dependency(lexer)?;
3536              self.exit_list_style_property();
3537            }
3538            if self.in_font_palette_property.is_some() {
3539              self.handle_local_font_palette_dependency(lexer)?;
3540              self.exit_font_palette_property();
3541            }
3542            if self.in_container_property.is_some() {
3543              self.exit_container_property();
3544            }
3545            if self.in_grid_property.is_some() {
3546              self.exit_grid_property();
3547            }
3548          }
3549        }
3550        self.pending_custom_property = None;
3551        self.pending_grid_property = None;
3552        self.property_kind = PropertyKind::Generic;
3553      }
3554      Scope::TopLevel => {
3555        self.is_next_rule_prelude = true;
3556      }
3557    }
3558    Some(())
3559  }
3560
3561  fn handle_function(
3562    &mut self,
3563    stream: &mut DependencyTokenStream<'_, 's>,
3564    start: Pos,
3565    end: Pos,
3566    flags: TokenFlags,
3567  ) -> Option<()> {
3568    let name = stream.slice_trusted(start, end);
3569    let mut normalized = [0; MAX_CSS_KEYWORD_LEN];
3570    let normalized_name = if flags.has_escape() {
3571      decode_css_keyword(name, &mut normalized)
3572    } else {
3573      lowercase_ascii_keyword(name, &mut normalized)
3574    };
3575    let mut item = normalized_name.map_or_else(
3576      || BalancedItem::new_other(start, end),
3577      |name| BalancedItem::new_normalized(name, start, end),
3578    );
3579    if normalized_name == Some("url(") {
3580      item.magic_comments = preceding_comment_range(stream.slice_trusted(0, start));
3581    }
3582    let magic_comments = item.magic_comments;
3583    let at_import_top_level =
3584      matches!(self.scope, Scope::InAtImport(_)) && self.balanced.is_empty();
3585    self.balanced.push(item, self.mode_data.as_mut());
3586
3587    if let Scope::InAtImport(ref mut import_data) = self.scope {
3588      if at_import_top_level && normalized_name == Some("url(") {
3589        import_data.prelude.push(ImportPreludeNode::Url {
3590          range: Range::new(start, end),
3591        });
3592        import_data.magic_comments =
3593          magic_comments.map(|range| stream.slice_trusted(range.start, range.end));
3594      } else if at_import_top_level && normalized_name == Some("layer(") {
3595        import_data.prelude.push(ImportPreludeNode::Layer {
3596          range: Range::new(start, end),
3597        });
3598      } else if at_import_top_level && normalized_name == Some("supports(") {
3599        import_data.prelude.push(ImportPreludeNode::Supports {
3600          range: Range::new(start, end),
3601        });
3602        import_data.supports = ImportDataSupports::InSupports;
3603      } else if at_import_top_level {
3604        import_data.prelude.push(ImportPreludeNode::Other {
3605          range: Range::new(start, end),
3606        });
3607      } else if normalized_name == Some("supports(") {
3608        import_data.supports = ImportDataSupports::InSupports;
3609      }
3610    }
3611
3612    if let Scope::InAtImport(ref mut import_data) = self.scope {
3613      let layer_end = at_import_top_level && normalized_name == Some("layer(");
3614      let supports_end = normalized_name == Some("supports(");
3615      if layer_end || supports_end {
3616        let Some(close) = stream.fast_forward(TokenKind::RightParenthesis) else {
3617          return Some(());
3618        };
3619        self.balanced.pop(self.mode_data.as_mut());
3620        if layer_end {
3621          import_data.layer = ImportDataLayer::EndLayer {
3622            value: stream.slice(end, close.end.saturating_sub(1))?,
3623            range: Range::new(start, close.end),
3624          };
3625        } else {
3626          import_data.supports = ImportDataSupports::EndSupports {
3627            value: stream.slice(end, close.end.saturating_sub(1))?,
3628            range: Range::new(start, close.end),
3629          };
3630        }
3631        return Some(());
3632      }
3633    }
3634
3635    let Some(mode_data) = &self.mode_data else {
3636      return Some(());
3637    };
3638    if mode_data.is_current_local_mode() && name.starts_with("--") && end > start + 1 {
3639      self
3640        .dependency_context
3641        .push_dependency(Dependency::LocalFunction {
3642          name: dashed_ident_name(stream.slice(start, end - 1)?)?,
3643          range: Range::new(start, end - 1),
3644        });
3645    }
3646    if mode_data.is_current_local_mode() && normalized_name == Some("var(") {
3647      self.lex_local_var(stream)?;
3648    }
3649    Some(())
3650  }
3651
3652  fn handle_left_parenthesis(
3653    &mut self,
3654    _: &mut DependencyLexer<'s>,
3655    start: Pos,
3656    end: Pos,
3657  ) -> Option<()> {
3658    self
3659      .balanced
3660      .push(BalancedItem::new_other(start, end), self.mode_data.as_mut());
3661    Some(())
3662  }
3663
3664  fn handle_right_parenthesis(
3665    &mut self,
3666    lexer: &mut DependencyLexer<'s>,
3667    leading_start: Pos,
3668    start: Pos,
3669    end: Pos,
3670  ) -> Option<()> {
3671    let Some(last) = self.balanced.pop(self.mode_data.as_mut()) else {
3672      return Some(());
3673    };
3674    if let Some(mode_data) = &mut self.mode_data {
3675      let mut is_function = last.kind.is_mode_function();
3676      let mut function_end = last.range.end;
3677      if last.kind.is_mode_class() {
3678        self.balanced.pop_mode_pseudo_class(mode_data);
3679        let popped = self
3680          .balanced
3681          .pop_without_moda_data()
3682          .expect("a mode pseudo-class must have a preceding balanced item");
3683        debug_assert!(!matches!(
3684          popped.kind,
3685          BalancedItemKind::GlobalClass | BalancedItemKind::LocalClass
3686        ));
3687        is_function = popped.kind.is_mode_function();
3688        function_end = popped.range.end;
3689      }
3690      if is_function {
3691        let is_empty = start == function_end || trivia_only(lexer.slice(function_end, start)?);
3692        let replacement_start = if is_empty {
3693          function_end
3694        } else {
3695          leading_start
3696        };
3697        if is_empty {
3698          let maybe_left_parenthesis_start = function_end.saturating_sub(1);
3699          self.handle_warning.handle_warning(Warning {
3700            range: Range::new(maybe_left_parenthesis_start, end),
3701            kind: WarningKind::Unexpected {
3702              message: "':global()' or ':local()' can't be empty",
3703            },
3704          });
3705        }
3706        self
3707          .dependency_context
3708          .push_dependency(Dependency::Replace {
3709            content: "",
3710            range: Range::new(replacement_start, end),
3711          });
3712      }
3713    }
3714    if let Scope::InAtImport(ref mut import_data) = self.scope {
3715      let not_in_supports = !import_data.in_supports();
3716      if matches!(last.kind, BalancedItemKind::Url) && not_in_supports {
3717        import_data.url_range = Some(Range::new(last.range.start, end));
3718      } else if matches!(last.kind, BalancedItemKind::Layer) && not_in_supports {
3719        import_data.layer = ImportDataLayer::EndLayer {
3720          value: lexer.slice(last.range.end, end - 1)?,
3721          range: Range::new(last.range.start, end),
3722        };
3723      } else if matches!(last.kind, BalancedItemKind::Supports) {
3724        import_data.supports = ImportDataSupports::EndSupports {
3725          value: lexer.slice(last.range.end, end - 1)?,
3726          range: Range::new(last.range.start, end),
3727        }
3728      }
3729    }
3730    Some(())
3731  }
3732
3733  fn handle_ident(
3734    &mut self,
3735    stream: &mut DependencyTokenStream<'_, 's>,
3736    start: Pos,
3737    end: Pos,
3738    flags: TokenFlags,
3739  ) -> Option<()> {
3740    let ident = stream.slice_trusted(start, end);
3741    // ICSS references can also occur in selectors and at-rule preludes,
3742    // where the scope is not necessarily a declaration block.
3743    if matches!(self.scope, Scope::TopLevel | Scope::InBlock)
3744      && matches!(
3745        self.scan_context,
3746        ScanContext::Selector
3747          | ScanContext::GenericValue
3748          | ScanContext::SpecialValue(_)
3749          | ScanContext::AtRule
3750      )
3751      && (self.scan_context != ScanContext::Selector || self.selector_square_depth == 0)
3752      && self.contains_icss_symbol(ident)
3753    {
3754      self
3755        .dependency_context
3756        .push_dependency(Dependency::ICSSSymbol {
3757          name: ident,
3758          range: Range::new(start, end),
3759        });
3760      if matches!(self.scope, Scope::TopLevel)
3761        && let Some(mode_data) = &mut self.mode_data
3762      {
3763        mode_data.composes_local_classes.invalidate();
3764      }
3765      return Some(());
3766    }
3767    match self.scope {
3768      Scope::InBlock => {
3769        let is_declaration_name = self.scan_context == ScanContext::DeclarationName;
3770        if is_declaration_name {
3771          let property_local_mode = self
3772            .mode_data
3773            .as_ref()
3774            .is_some_and(ModeData::is_property_local_mode);
3775          (self.property_kind, self.pending_grid_property) =
3776            Self::classify_property(ident, flags, property_local_mode);
3777          self.pending_custom_property =
3778            (self.property_kind == PropertyKind::CustomProperty).then_some(Range::new(start, end));
3779          if self.property_kind != PropertyKind::Composes {
3780            return Some(());
3781          }
3782        }
3783        let Some(mode_data) = &mut self.mode_data else {
3784          return Some(());
3785        };
3786        if mode_data.is_current_local_mode()
3787          && self
3788            .balanced
3789            .last()
3790            .is_some_and(|last| last.kind.is_mode_function())
3791          && ident.starts_with("--")
3792          && stream.peek_parser_token().token.kind == TokenKind::RightParenthesis
3793        {
3794          self
3795            .dependency_context
3796            .push_dependency(Dependency::LocalFunction {
3797              name: dashed_ident_name(stream.slice(start, end)?)?,
3798              range: Range::new(start, end),
3799            });
3800          return Some(());
3801        }
3802        if mode_data.is_property_local_mode()
3803          && matches!(self.scan_context, ScanContext::SpecialValue(_))
3804        {
3805          if let Some(animation) = &mut self.in_animation_property {
3806            // Not inside functions
3807            if self.balanced.is_empty() {
3808              animation.set_rename(stream.slice(start, end)?, flags, Range::new(start, end));
3809            }
3810            return Some(());
3811          }
3812
3813          if let Some(list_style) = &mut self.in_list_style_property {
3814            // Not inside functions
3815            if self.balanced.is_empty() {
3816              list_style.set_rename(stream.slice(start, end)?, flags, Range::new(start, end));
3817            }
3818            return Some(());
3819          }
3820
3821          if let Some(font_palette) = &mut self.in_font_palette_property {
3822            // Not inside functions or inside palette-mix()
3823            if self.balanced.is_empty()
3824              || matches!(self.balanced.last(), Some(last) if matches!(last.kind, BalancedItemKind::PaletteMix))
3825            {
3826              font_palette.set_rename(stream.slice(start, end)?, flags, Range::new(start, end));
3827            }
3828            return Some(());
3829          }
3830
3831          if let Some(container) = &mut self.in_container_property {
3832            if self.balanced.is_empty() {
3833              container.set_rename(ident, flags, Range::new(start, end));
3834              if let Some(range) = container.take_rename(self.balanced.len()) {
3835                self
3836                  .dependency_context
3837                  .push_dependency(Dependency::LocalContainerDecl {
3838                    name: stream.slice(range.start, range.end)?,
3839                    range,
3840                  });
3841              }
3842            }
3843            return Some(());
3844          }
3845
3846          if self.in_grid_property.is_some() {
3847            if self.balanced.is_empty() && !is_reserved_grid_ident(ident, flags) {
3848              self
3849                .dependency_context
3850                .push_dependency(Dependency::LocalGrid {
3851                  name: ident,
3852                  range: Range::new(start, end),
3853                });
3854            }
3855            return Some(());
3856          }
3857        }
3858
3859        if is_declaration_name && self.property_kind == PropertyKind::Composes {
3860          if self.block_nesting_level != 1 {
3861            self.handle_warning.handle_warning(Warning {
3862              range: Range::new(start, end),
3863              kind: WarningKind::UnexpectedComposition {
3864                message: "not allowed in nested rule",
3865              },
3866            });
3867            return Some(());
3868          }
3869          let Some(local_classes) = mode_data
3870            .composes_local_classes
3871            .get_valid_local_classes(stream.lexer())
3872          else {
3873            self.handle_warning.handle_warning(Warning {
3874              range: Range::new(start, end),
3875              kind: WarningKind::UnexpectedComposition {
3876                message: "only allowed when selector is single :local class",
3877              },
3878            });
3879            return Some(());
3880          };
3881          return self.lex_composes(stream, local_classes, start);
3882        }
3883      }
3884      Scope::InAtImport(ref mut import_data) => {
3885        if !self.balanced.is_empty() || import_data.in_supports() {
3886          return Some(());
3887        }
3888
3889        let ident = stream.slice_trusted(start, end);
3890        if ident.eq_ignore_ascii_case("layer") {
3891          import_data.prelude.push(ImportPreludeNode::Layer {
3892            range: Range::new(start, end),
3893          });
3894          import_data.layer = ImportDataLayer::EndLayer {
3895            value: "",
3896            range: Range::new(start, end),
3897          }
3898        } else if import_data.url.is_none() && import_data.prelude.is_empty() {
3899          import_data
3900            .prelude
3901            .push(ImportPreludeNode::IcssUrlCandidate {
3902              name: ident,
3903              range: Range::new(start, end),
3904            });
3905        } else if import_data.url.is_none() {
3906          import_data.prelude.push(ImportPreludeNode::Other {
3907            range: Range::new(start, end),
3908          });
3909        }
3910      }
3911      Scope::TopLevel => {
3912        let Some(mode_data) = &mut self.mode_data else {
3913          return Some(());
3914        };
3915
3916        mode_data.composes_local_classes.invalidate();
3917      }
3918      _ => {}
3919    }
3920    Some(())
3921  }
3922
3923  fn handle_class(
3924    &mut self,
3925    lexer: &mut DependencyLexer<'s>,
3926    start: Pos,
3927    end: Pos,
3928    _flags: TokenFlags,
3929  ) -> Option<()> {
3930    let Some(mode_data) = &mut self.mode_data else {
3931      return Some(());
3932    };
3933    let name = lexer.slice_trusted(start, end);
3934    if name == "." {
3935      self.handle_warning.handle_warning(Warning {
3936        range: Range::new(start, end),
3937        kind: WarningKind::Unexpected {
3938          message: "Invalid class selector syntax",
3939        },
3940      });
3941      return Some(());
3942    }
3943    if mode_data.is_current_local_mode() {
3944      self
3945        .dependency_context
3946        .push_dependency(Dependency::LocalClass {
3947          name,
3948          range: Range::new(start, end),
3949          explicit: mode_data.is_mode_explicit(),
3950        });
3951      if self.block_nesting_level == 0 {
3952        mode_data
3953          .composes_local_classes
3954          .find_local_class(start + 1, end);
3955      }
3956
3957      if mode_data.is_pure_mode() {
3958        mode_data.pure_global = None;
3959      }
3960    }
3961    Some(())
3962  }
3963
3964  fn handle_id(
3965    &mut self,
3966    lexer: &mut DependencyLexer<'s>,
3967    start: Pos,
3968    end: Pos,
3969    _flags: TokenFlags,
3970  ) -> Option<()> {
3971    let Some(mode_data) = &mut self.mode_data else {
3972      return Some(());
3973    };
3974    let name = lexer.slice_trusted(start, end);
3975    if name == "#" {
3976      self.handle_warning.handle_warning(Warning {
3977        range: Range::new(start, end),
3978        kind: WarningKind::Unexpected {
3979          message: "Invalid id selector syntax",
3980        },
3981      });
3982      return Some(());
3983    }
3984    if mode_data.is_current_local_mode() {
3985      self
3986        .dependency_context
3987        .push_dependency(Dependency::LocalId {
3988          name,
3989          range: Range::new(start, end),
3990          explicit: mode_data.is_mode_explicit(),
3991        });
3992
3993      if self.block_nesting_level == 0 {
3994        mode_data.composes_local_classes.invalidate();
3995      }
3996
3997      if mode_data.is_pure_mode() {
3998        mode_data.pure_global = None;
3999      }
4000    }
4001    Some(())
4002  }
4003
4004  fn handle_left_curly_bracket(
4005    &mut self,
4006    stream: &mut DependencyTokenStream<'_, 's>,
4007    start: Pos,
4008    _: Pos,
4009  ) -> Option<()> {
4010    if matches!(self.scope, Scope::InBlock)
4011      && self.scan_context != ScanContext::Selector
4012      && !self.balanced.is_empty()
4013    {
4014      self.balanced.push(
4015        BalancedItem::new_curly(start, start + 1),
4016        self.mode_data.as_mut(),
4017      );
4018      return Some(());
4019    }
4020    match self.scope {
4021      Scope::TopLevel => {
4022        self.allow_import_at_rule = false;
4023        self.scope = Scope::InBlock;
4024        if self.mode_data.is_none()
4025          || matches!(&self.mode_data, Some(mode_data) if !matches!(mode_data.composes_local_classes.is_single, SingleLocalClass::AtKeyword))
4026        {
4027          self.block_nesting_level = 1;
4028        }
4029      }
4030      Scope::InBlock => {
4031        let is_at_rule_block = matches!(
4032            &self.mode_data,
4033            Some(mode_data)
4034                if matches!(mode_data.composes_local_classes.is_single, SingleLocalClass::AtKeyword)
4035        );
4036        if !is_at_rule_block {
4037          self.block_nesting_level += 1;
4038        }
4039      }
4040      _ => return Some(()),
4041    }
4042    let (pure_check_disabled_for_selector, enter_pure_ignored_block) =
4043      if let Some(mode_data) = &self.mode_data {
4044        (
4045          mode_data.is_pure_check_disabled(),
4046          mode_data.pure_ignore_pending,
4047        )
4048      } else {
4049        (false, false)
4050      };
4051    if self.mode_data.is_none() {
4052      self.set_scan_context(stream, ScanContext::BlockItem);
4053      return Some(());
4054    }
4055    if let Some(mode_data) = &mut self.mode_data {
4056      if let Some(pure_global_start) = mode_data
4057        .pure_global
4058        .filter(|_| mode_data.is_pure_mode() && !pure_check_disabled_for_selector)
4059      {
4060        self.handle_warning.handle_warning(Warning {
4061                    range: Range::new(pure_global_start, start),
4062                    kind: WarningKind::NotPure {
4063                        message: "Selector is not pure (pure selectors must contain at least one local class or id)",
4064                    }
4065                });
4066      }
4067
4068      if enter_pure_ignored_block {
4069        mode_data.enter_block(self.block_nesting_level);
4070      }
4071
4072      if let Some(resulting_global_start) = mode_data
4073        .resulting_global
4074        .filter(|_| mode_data.is_current_local_mode())
4075      {
4076        self.handle_warning.handle_warning(Warning {
4077          range: Range::new(resulting_global_start, start),
4078          kind: WarningKind::InconsistentModeResult,
4079        });
4080      }
4081      mode_data.resulting_global = None;
4082
4083      self.balanced.update_property_mode(mode_data);
4084      self.balanced.pop_mode_pseudo_class(mode_data);
4085      if self.is_next_rule_prelude && self.block_nesting_level == 0 {
4086        let mode_data = self
4087          .mode_data
4088          .as_mut()
4089          .expect("CSS Modules mode data must exist while finishing a selector");
4090        mode_data.composes_local_classes.reset_to_initial();
4091      }
4092
4093      debug_assert!(
4094        self.balanced.is_empty(),
4095        "balanced should be empty when end of selector"
4096      );
4097    }
4098    self.exit_container_property();
4099    self.exit_grid_property();
4100    self.pending_custom_property = None;
4101    self.pending_grid_property = None;
4102    self.property_kind = PropertyKind::Generic;
4103    self.set_scan_context(stream, ScanContext::BlockItem);
4104    Some(())
4105  }
4106
4107  fn handle_right_curly_bracket(
4108    &mut self,
4109    stream: &mut DependencyTokenStream<'_, 's>,
4110    _: Pos,
4111    end: Pos,
4112  ) -> Option<()> {
4113    if matches!(self.scope, Scope::InBlock) {
4114      if matches!(
4115          self.balanced.last(),
4116          Some(last) if matches!(last.kind, BalancedItemKind::Curly)
4117      ) {
4118        self.balanced.pop(self.mode_data.as_mut());
4119        if self.block_nesting_level == 0 {
4120          self.scope = Scope::TopLevel;
4121          self.is_next_rule_prelude = true;
4122          self.set_scan_context(stream, ScanContext::TopLevel);
4123          if let Some(mode_data) = &mut self.mode_data {
4124            mode_data.composes_local_classes.reset_to_initial();
4125          }
4126        } else {
4127          self.set_scan_context(stream, ScanContext::BlockItem);
4128        }
4129        return Some(());
4130      }
4131
4132      if let Some(mode_data) = &mut self.mode_data {
4133        mode_data.pure_global = Some(end);
4134
4135        if mode_data.is_property_local_mode() {
4136          if self.in_animation_property.is_some() {
4137            self.handle_local_keyframes_dependency(stream.lexer_mut())?;
4138            self.exit_animation_property();
4139          }
4140          if self.in_list_style_property.is_some() {
4141            self.handle_local_counter_style_dependency(stream.lexer_mut())?;
4142            self.exit_list_style_property();
4143          }
4144          if self.in_font_palette_property.is_some() {
4145            self.handle_local_font_palette_dependency(stream.lexer_mut())?;
4146            self.exit_font_palette_property();
4147          }
4148          if self.in_container_property.is_some() {
4149            self.exit_container_property();
4150          }
4151          if self.in_grid_property.is_some() {
4152            self.exit_grid_property();
4153          }
4154        }
4155      }
4156      if self.block_nesting_level > 0 {
4157        self.block_nesting_level -= 1;
4158      }
4159      if let Some(mode_data) = &mut self.mode_data {
4160        mode_data.clear_pure_ignore_pending();
4161        mode_data.exit_block(self.block_nesting_level);
4162      }
4163      if self.block_nesting_level == 0 {
4164        self.scope = Scope::TopLevel;
4165        self.set_scan_context(stream, ScanContext::TopLevel);
4166        self.is_next_rule_prelude = true;
4167        if let Some(mode_data) = &mut self.mode_data {
4168          mode_data.composes_local_classes.reset_to_initial();
4169        }
4170      } else {
4171        self.set_scan_context(stream, ScanContext::BlockItem);
4172      }
4173      self.pending_custom_property = None;
4174      self.pending_grid_property = None;
4175      self.property_kind = PropertyKind::Generic;
4176    }
4177    Some(())
4178  }
4179
4180  fn handle_pseudo_function(
4181    &mut self,
4182    stream: &mut DependencyTokenStream<'_, 's>,
4183    start: Pos,
4184    end: Pos,
4185    flags: TokenFlags,
4186  ) -> Option<()> {
4187    let name = stream.slice_trusted(start, end);
4188    if let Some(mode_data) = &mut self.mode_data {
4189      if name.eq_ignore_ascii_case(":import(") {
4190        self.lex_icss_import(stream);
4191        self
4192          .dependency_context
4193          .push_dependency(Dependency::Replace {
4194            content: "",
4195            range: Range::new(start, stream.consumed_pos()),
4196          });
4197        return Some(());
4198      }
4199      if name.eq_ignore_ascii_case(":global(") || name.eq_ignore_ascii_case(":local(") {
4200        if mode_data.is_inside_mode_function() {
4201          self.handle_warning.handle_warning(Warning {
4202            range: Range::new(start, end),
4203            kind: WarningKind::ExpectedNotInside {
4204              pseudo: stream.slice(start, end)?,
4205            },
4206          });
4207        }
4208
4209        let next = stream.peek_parser_token();
4210        if next.token.kind == TokenKind::Eof {
4211          return None;
4212        }
4213        self
4214          .dependency_context
4215          .push_dependency(Dependency::Replace {
4216            content: "",
4217            range: Range::new(start, next.token.range.start),
4218          });
4219      } else if self.block_nesting_level == 0 {
4220        mode_data.composes_local_classes.invalidate();
4221      }
4222    }
4223    self.balanced.push(
4224      BalancedItem::new(name, flags, start, end),
4225      self.mode_data.as_mut(),
4226    );
4227    Some(())
4228  }
4229
4230  fn handle_pseudo_class(
4231    &mut self,
4232    stream: &mut DependencyTokenStream<'_, 's>,
4233    start: Pos,
4234    end: Pos,
4235    flags: TokenFlags,
4236  ) -> Option<()> {
4237    let Some(mode_data) = &mut self.mode_data else {
4238      return Some(());
4239    };
4240    let name = stream.slice_trusted(start, end);
4241    if name.eq_ignore_ascii_case(":global") || name.eq_ignore_ascii_case(":local") {
4242      if mode_data.is_inside_mode_function() {
4243        self.handle_warning.handle_warning(Warning {
4244          range: Range::new(start, end),
4245          kind: WarningKind::ExpectedNotInside {
4246            pseudo: stream.slice(start, end)?,
4247          },
4248        });
4249      }
4250
4251      let next = stream.peek_parser_token();
4252      if next.token.kind == TokenKind::Eof {
4253        return None;
4254      }
4255      if !next.leading.has_whitespace() {
4256        let missing_whitespace = match stream.byte_at(next.token.range.start) {
4257          Some(b'.' | b'#') => true,
4258          Some(b'{') => next.leading.first_comment_start.is_some(),
4259          _ => false,
4260        };
4261        if missing_whitespace {
4262          self.handle_warning.handle_warning(Warning {
4263            range: Range::new(start, end),
4264            kind: WarningKind::Unexpected {
4265              message: "Missing trailing whitespace",
4266            },
4267          });
4268        }
4269      }
4270      self.balanced.push(
4271        BalancedItem::new(name, flags, start, end),
4272        self.mode_data.as_mut(),
4273      );
4274      self
4275        .dependency_context
4276        .push_dependency(Dependency::Replace {
4277          content: "",
4278          range: Range::new(
4279            start,
4280            next.leading.first_comment_start.unwrap_or(next.leading.end),
4281          ),
4282        });
4283      return Some(());
4284    }
4285    if matches!(self.scope, Scope::TopLevel) && name.eq_ignore_ascii_case(":export") {
4286      self.lex_icss_export(stream)?;
4287      self
4288        .dependency_context
4289        .push_dependency(Dependency::Replace {
4290          content: "",
4291          range: Range::new(start, stream.consumed_pos()),
4292        });
4293      return Some(());
4294    }
4295
4296    if self.block_nesting_level == 0 {
4297      mode_data.composes_local_classes.invalidate();
4298    }
4299    Some(())
4300  }
4301
4302  fn handle_comma(&mut self, lexer: &mut DependencyLexer<'s>, start: Pos, end: Pos) -> Option<()> {
4303    let Some(mode_data) = &mut self.mode_data else {
4304      return Some(());
4305    };
4306
4307    if let Some(pure_global_start) = mode_data
4308      .pure_global
4309      .filter(|_| mode_data.is_pure_mode() && !mode_data.is_pure_check_disabled())
4310    {
4311      self.handle_warning.handle_warning(Warning {
4312                range: Range::new(pure_global_start, start),
4313                kind: WarningKind::NotPure {
4314                    message: "Selector is not pure (pure selectors must contain at least one local class or id)",
4315                }
4316            });
4317    }
4318    mode_data.pure_global = Some(end);
4319
4320    if self.block_nesting_level == 0 {
4321      mode_data.composes_local_classes.find_comma(lexer)?;
4322    }
4323
4324    if let Some(resulting_global_start) = mode_data
4325      .resulting_global
4326      .filter(|_| mode_data.is_current_local_mode())
4327    {
4328      self.handle_warning.handle_warning(Warning {
4329        range: Range::new(resulting_global_start, start),
4330        kind: WarningKind::InconsistentModeResult,
4331      });
4332    }
4333
4334    if self.balanced.len() == 1 {
4335      let last = self
4336        .balanced
4337        .last()
4338        .expect("a balanced item must exist when the stack length is one");
4339      let is_local_class = matches!(last.kind, BalancedItemKind::LocalClass);
4340      let is_global_class = matches!(last.kind, BalancedItemKind::GlobalClass);
4341      if is_local_class || is_global_class {
4342        self.balanced.pop_mode_pseudo_class(mode_data);
4343        if mode_data.resulting_global.is_none() && is_global_class {
4344          mode_data.resulting_global = Some(start);
4345        }
4346      }
4347    }
4348
4349    if matches!(self.scope, Scope::InBlock)
4350      && mode_data.is_property_local_mode()
4351      && self.in_animation_property.is_some()
4352    {
4353      self.handle_local_keyframes_dependency(lexer)?;
4354    }
4355
4356    Some(())
4357  }
4358}