diff --git a/src/dependencies.rs b/src/dependencies.rs index da35e39..f3ceb9b 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -101,12 +101,12 @@ impl BalancedStack { item.kind, BalancedItemKind::LocalFn | BalancedItemKind::LocalClass ) { - mode_data.set_local(); + mode_data.set_current_mode(CssModulesMode::Local); } else if matches!( item.kind, BalancedItemKind::GlobalFn | BalancedItemKind::GlobalClass ) { - mode_data.set_global(); + mode_data.set_current_mode(CssModulesMode::Global); } } self.0.push(item); @@ -115,27 +115,7 @@ impl BalancedStack { pub fn pop(&mut self, mode_data: Option<&mut CssModulesModeData>) -> Option { let item = self.0.pop()?; if let Some(mode_data) = mode_data { - let mut iter = self.0.iter(); - loop { - if let Some(last) = iter.next_back() { - if matches!( - last.kind, - BalancedItemKind::LocalFn | BalancedItemKind::LocalClass - ) { - mode_data.set_local(); - break; - } else if matches!( - last.kind, - BalancedItemKind::GlobalFn | BalancedItemKind::GlobalClass - ) { - mode_data.set_global(); - break; - } - } else { - mode_data.set_default(); - break; - } - } + self.update_current_modules_mode(mode_data); } Some(item) } @@ -157,7 +137,36 @@ impl BalancedStack { } break; } - mode_data.set_default(); + self.update_current_modules_mode(mode_data); + } + + pub fn update_current_modules_mode(&self, mode_data: &mut CssModulesModeData) { + mode_data.set_current_mode(self.topmost_modules_mode(mode_data)); + } + + pub fn update_property_modules_mode(&self, mode_data: &mut CssModulesModeData) { + mode_data.set_property_mode(self.topmost_modules_mode(mode_data)); + } + + fn topmost_modules_mode(&self, mode_data: &CssModulesModeData) -> CssModulesMode { + let mut iter = self.0.iter(); + loop { + if let Some(last) = iter.next_back() { + if matches!( + last.kind, + BalancedItemKind::LocalFn | BalancedItemKind::LocalClass + ) { + return CssModulesMode::Local; + } else if matches!( + last.kind, + BalancedItemKind::GlobalFn | BalancedItemKind::GlobalClass + ) { + return CssModulesMode::Global; + } + } else { + return mode_data.default_mode(); + } + } } } @@ -224,28 +233,30 @@ impl Range { } } -#[derive(Debug)] -enum CssModulesMode { +#[derive(Debug, Clone, Copy)] +pub enum CssModulesMode { Local, Global, - None, } #[derive(Debug)] pub struct CssModulesModeData { default: CssModulesMode, current: CssModulesMode, + property: CssModulesMode, } impl CssModulesModeData { pub fn new(local: bool) -> Self { + let default = if local { + CssModulesMode::Local + } else { + CssModulesMode::Global + }; Self { - default: if local { - CssModulesMode::Local - } else { - CssModulesMode::Global - }, - current: CssModulesMode::None, + default, + current: default, + property: default, } } @@ -253,24 +264,26 @@ impl CssModulesModeData { match self.current { CssModulesMode::Local => true, CssModulesMode::Global => false, - CssModulesMode::None => match self.default { - CssModulesMode::Local => true, - CssModulesMode::Global => false, - CssModulesMode::None => false, - }, } } - pub fn set_local(&mut self) { - self.current = CssModulesMode::Local; + pub fn is_property_local_mode(&self) -> bool { + match self.property { + CssModulesMode::Local => true, + CssModulesMode::Global => false, + } + } + + pub fn default_mode(&self) -> CssModulesMode { + self.default } - pub fn set_global(&mut self) { - self.current = CssModulesMode::Global; + pub fn set_current_mode(&mut self, mode: CssModulesMode) { + self.current = mode; } - pub fn set_default(&mut self) { - self.current = CssModulesMode::None; + pub fn set_property_mode(&mut self, mode: CssModulesMode) { + self.property = mode; } } @@ -294,6 +307,10 @@ impl AnimationProperty { self.keywords.check(ident) } + pub fn reset_keywords(&mut self) { + self.keywords.reset(); + } + pub fn set_keyframes(&mut self, ident: &str, range: Range) { if self.check_keywords(ident) { self.keyframes = Some(range); @@ -338,7 +355,7 @@ impl AnimationKeywords { "normal" => self.keyword_check(Self::NORMAL), "reverse" => self.keyword_check(Self::REVERSE), "alternate" => self.keyword_check(Self::ALTERNATE), - "alternate_reverse" => self.keyword_check(Self::ALTERNATE_REVERSE), + "alternate-reverse" => self.keyword_check(Self::ALTERNATE_REVERSE), "forwards" => self.keyword_check(Self::FORWARDS), "backwards" => self.keyword_check(Self::BACKWARDS), "both" => self.keyword_check(Self::BOTH), @@ -346,12 +363,12 @@ impl AnimationKeywords { "paused" => self.keyword_check(Self::PAUSED), "running" => self.keyword_check(Self::RUNNING), "ease" => self.keyword_check(Self::EASE), - "ease_in" => self.keyword_check(Self::EASE_IN), - "ease_out" => self.keyword_check(Self::EASE_OUT), - "ease_in_out" => self.keyword_check(Self::EASE_IN_OUT), + "ease-in" => self.keyword_check(Self::EASE_IN), + "ease-out" => self.keyword_check(Self::EASE_OUT), + "ease-in-out" => self.keyword_check(Self::EASE_IN_OUT), "linear" => self.keyword_check(Self::LINEAR), - "step_end" => self.keyword_check(Self::STEP_END), - "step_start" => self.keyword_check(Self::STEP_START), + "step-end" => self.keyword_check(Self::STEP_END), + "step-start" => self.keyword_check(Self::STEP_START), "none" | "initial" | "inherit" | "unset" | "revert" | "revert-layer" => false, _ => true, } @@ -364,6 +381,10 @@ impl AnimationKeywords { self.bits |= bit; false } + + pub fn reset(&mut self) { + self.bits = 0; + } } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -707,6 +728,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> LexDependencies<'s, D, W range, }); } + animation.reset_keywords(); } Some(()) } @@ -1007,7 +1029,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen } Scope::InBlock => { if let Some(mode_data) = &self.mode_data { - if mode_data.is_local_mode() { + if mode_data.is_property_local_mode() { self.handle_local_keyframes_dependency(lexer)?; self.exit_animation_property(); } @@ -1053,7 +1075,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen let Some(last) = self.balanced.pop(self.mode_data.as_mut()) else { return Some(()); }; - if self.mode_data.is_some() { + if let Some(mode_data) = &mut self.mode_data { let is_function = matches!( last.kind, BalancedItemKind::LocalFn | BalancedItemKind::GlobalFn @@ -1071,6 +1093,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen range: Range::new(start, end), }); } else { + self.balanced.pop_modules_mode_pseudo_class(mode_data); let popped = self.balanced.pop_without_moda_data(); debug_assert!(matches!(popped.unwrap().kind, BalancedItemKind::Other)); } @@ -1102,7 +1125,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen let Some(mode_data) = &mut self.mode_data else { return Some(()); }; - if mode_data.is_local_mode() { + if mode_data.is_property_local_mode() { if let Some(animation) = &mut self.in_animation_property { // Not inside functions if self.balanced.is_empty() { @@ -1112,14 +1135,18 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen return Some(()); } let ident = lexer.slice(start, end)?; - if let Some(name) = ident.strip_prefix("--") { - return self.lex_local_var_decl(lexer, name, start, end); - } let ident = ident.to_ascii_lowercase(); if ident == "animation" || ident == "animation-name" { self.enter_animation_property(); return Some(()); } + } + if mode_data.is_local_mode() { + let ident = lexer.slice(start, end)?; + if let Some(name) = ident.strip_prefix("--") { + return self.lex_local_var_decl(lexer, name, start, end); + } + let ident = ident.to_ascii_lowercase(); if ident == "composes" { return self.lex_composes(lexer, start); } @@ -1181,6 +1208,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen _ => return Some(()), } if let Some(mode_data) = &mut self.mode_data { + self.balanced.update_property_modules_mode(mode_data); self.balanced.pop_modules_mode_pseudo_class(mode_data); self.is_next_rule_prelude = self.is_next_nested_syntax(lexer)?; } @@ -1194,7 +1222,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen fn right_curly_bracket(&mut self, lexer: &mut Lexer<'s>, _: Pos, _: Pos) -> Option<()> { if matches!(self.scope, Scope::InBlock) { if let Some(mode_data) = &self.mode_data { - if mode_data.is_local_mode() { + if mode_data.is_property_local_mode() { self.handle_local_keyframes_dependency(lexer)?; self.exit_animation_property(); } @@ -1272,7 +1300,7 @@ impl<'s, D: HandleDependency<'s>, W: HandleWarning<'s>> Visitor<'s> for LexDepen self.balanced.pop_modules_mode_pseudo_class(mode_data); } } - if matches!(self.scope, Scope::InBlock) && mode_data.is_local_mode() { + if matches!(self.scope, Scope::InBlock) && mode_data.is_property_local_mode() { self.handle_local_keyframes_dependency(lexer)?; } Some(()) diff --git a/tests/postcss_plugins/modules_local_by_default_test.rs b/tests/postcss_plugins/modules_local_by_default_test.rs index 48d91e9..34a482d 100644 --- a/tests/postcss_plugins/modules_local_by_default_test.rs +++ b/tests/postcss_plugins/modules_local_by_default_test.rs @@ -1,6 +1,7 @@ use css_module_lexer::collect_css_modules_dependencies; use css_module_lexer::Dependency; use css_module_lexer::Range; +use indoc::indoc; use crate::slice_range; @@ -379,3 +380,147 @@ fn localize_animation() { ":local(.foo) { animation: 0s ease 0s 1 normal none :local(test) running; }", ); } + +// #[test] +// fn localize_animation_with_vendor_prefix() { +// test( +// ".foo { -webkit-animation: bar; animation: bar; }", +// ":local(.foo) { -webkit-animation: :local(bar); animation: :local(bar); }", +// ); +// } + +#[test] +fn not_localize_other_rules() { + test( + ".foo { content: \"animation: bar;\" }", + ":local(.foo) { content: \"animation: bar;\" }", + ); +} + +#[test] +fn not_localize_global_rules() { + test( + ":global .foo { animation: foo; animation-name: bar; }", + ".foo { animation: foo; animation-name: bar; }", + ); +} + +#[test] +fn handle_nested_global() { + test(":global .a:not(:global .b) {}", ".a:not(.b) {}"); + test( + ":global .a:not(:global .b:not(:global .c)) {}", + ".a:not(.b:not(.c)) {}", + ); + test( + ":local .a:not(:not(:not(:global .c))) {}", + ":local(.a):not(:not(:not(.c))) {}", + ); + test( + ":global .a:not(:global .b, :global .c) {}", + ".a:not(.b, .c) {}", + ); + test( + ":local .a:not(:global .b, :local .c) {}", + ":local(.a):not(.b, :local(.c)) {}", + ); + test( + ":global .a:not(:local .b, :global .c) {}", + ".a:not(:local(.b), .c) {}", + ); + test(":global .a:not(.b, .c) {}", ".a:not(.b, .c) {}"); + test( + ":local .a:not(.b, .c) {}", + ":local(.a):not(:local(.b), :local(.c)) {}", + ); + test( + ":global .a:not(:local .b, .c) {}", + ".a:not(:local(.b), :local(.c)) {}", + ); +} + +#[test] +fn handle_a_complex_animation_rule() { + test( + ".foo { animation: foo, bar 5s linear 2s infinite alternate, barfoo 1s; }", + ":local(.foo) { animation: :local(foo), :local(bar) 5s linear 2s infinite alternate, :local(barfoo) 1s; }", + ); +} + +#[test] +fn handle_animations_where_the_first_value_is_not_the_animation_name() { + test( + ".foo { animation: 1s foo; }", + ":local(.foo) { animation: 1s :local(foo); }", + ); +} + +#[test] +fn handle_animations_where_the_first_value_is_not_the_animation_name_whilst_also_using_keywords() { + test( + ".foo { animation: 1s normal ease-out infinite foo; }", + ":local(.foo) { animation: 1s normal ease-out infinite :local(foo); }", + ); +} + +#[test] +fn not_treat_animation_curve_as_identifier_of_animation_name_even_if_it_separated_by_comma() { + test( + ".foo { animation: slide-right 300ms forwards ease-out, fade-in 300ms forwards ease-out; }", + ":local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }", + ); +} + +#[test] +fn not_treat_start_and_end_keywords_in_steps_function_as_identifiers() { + test( + indoc! {r#" + .foo { animation: spin 1s steps(12, end) infinite; } + .foo { animation: spin 1s STEPS(12, start) infinite; } + .foo { animation: spin 1s steps(12, END) infinite; } + .foo { animation: spin 1s steps(12, START) infinite; } + "#}, + indoc! {r#" + :local(.foo) { animation: :local(spin) 1s steps(12, end) infinite; } + :local(.foo) { animation: :local(spin) 1s STEPS(12, start) infinite; } + :local(.foo) { animation: :local(spin) 1s steps(12, END) infinite; } + :local(.foo) { animation: :local(spin) 1s steps(12, START) infinite; } + "#}, + ); +} + +#[test] +fn handle_animations_with_custom_timing_functions() { + test( + ".foo { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) foo; }", + ":local(.foo) { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) :local(foo); }", + ); +} + +#[test] +fn handle_animations_whose_names_are_keywords() { + test( + ".foo { animation: 1s infinite infinite; }", + ":local(.foo) { animation: 1s infinite :local(infinite); }", + ); +} + +#[test] +fn handle_not_localize_an_animation_shorthand_value_of_inherit() { + test( + ".foo { animation: inherit; }", + ":local(.foo) { animation: inherit; }", + ); +} + +#[test] +fn handle_constructor_as_animation_name() { + test( + ".foo { animation: constructor constructor; }", + // should be ":local(.foo) { animation: :local(constructor) :local(constructor); }" + // but the output of postcss-modules-scope is "._local_foo) { animation: _local_constructor :local(constructor); }" + // postcss-modules-scope only process the first :local in decl.value + // therefore seems our output is more appropriate and it's fine to have different output here + ":local(.foo) { animation: constructor :local(constructor); }", + ); +}