1use 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#[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#[derive(Debug, Clone, Copy)]
515struct ValueAtRuleSplit {
516 split: Pos,
517 end: Pos,
518 prev_end: Pos,
519 next_start: Option<Pos>,
520}
521
522struct 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 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 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 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 "none" |
1078 "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 "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 | "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 | "disc"
1173 | "circle"
1174 | "square"
1175 | "disclosure-open"
1176 | "disclosure-closed"
1177 | "cjk-earthly-branch"
1179 | "cjk-heavenly-stem"
1180 | "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 | "none"
1193 | "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
1299pub(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 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 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 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 if import_data.in_supports() {
3234 return Some(());
3235 }
3236 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 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 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 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 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 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}