diff --git a/crates/swc_css_ast/src/at_rule.rs b/crates/swc_css_ast/src/at_rule.rs index b55f0ee27b64..8b6cddfef553 100644 --- a/crates/swc_css_ast/src/at_rule.rs +++ b/crates/swc_css_ast/src/at_rule.rs @@ -1,7 +1,7 @@ use is_macro::Is; use string_enum::StringEnum; use swc_atoms::{Atom, JsWord}; -use swc_common::{ast_node, EqIgnoreSpan, Span}; +use swc_common::{ast_node, util::take::Take, EqIgnoreSpan, Span}; use crate::{ CustomIdent, CustomPropertyName, DashedIdent, Declaration, Dimension, FamilyName, Function, @@ -27,6 +27,15 @@ pub enum AtRuleName { Ident(Ident), } +impl PartialEq for AtRuleName { + fn eq(&self, other: &str) -> bool { + match self { + AtRuleName::DashedIdent(v) => *v == *other, + AtRuleName::Ident(v) => *v == *other, + } + } +} + #[ast_node] #[derive(Eq, Hash, Is, EqIgnoreSpan)] pub enum AtRulePrelude { @@ -217,6 +226,19 @@ pub struct MediaQuery { pub condition: Option>, } +impl Take for MediaQuery { + #[inline] + fn dummy() -> Self { + Self { + span: Take::dummy(), + modifier: Take::dummy(), + media_type: Take::dummy(), + keyword: Take::dummy(), + condition: Take::dummy(), + } + } +} + impl EqIgnoreSpan for MediaQuery { fn eq_ignore_span(&self, other: &Self) -> bool { self.modifier.eq_ignore_span(&other.modifier) @@ -813,11 +835,23 @@ pub struct ExtensionName { } impl EqIgnoreSpan for ExtensionName { + #[inline] fn eq_ignore_span(&self, other: &Self) -> bool { self.value == other.value } } +impl Take for ExtensionName { + #[inline] + fn dummy() -> Self { + Self { + span: Take::dummy(), + value: Default::default(), + raw: Take::dummy(), + } + } +} + #[ast_node("CustomMedia")] #[derive(Eq, Hash, EqIgnoreSpan)] pub struct CustomMediaQuery { @@ -826,6 +860,17 @@ pub struct CustomMediaQuery { pub media: CustomMediaQueryMediaType, } +impl Take for CustomMediaQuery { + #[inline] + fn dummy() -> Self { + Self { + span: Take::dummy(), + name: Take::dummy(), + media: Take::dummy(), + } + } +} + #[ast_node] #[derive(Eq, Hash, Is, EqIgnoreSpan)] pub enum CustomMediaQueryMediaType { @@ -834,3 +879,10 @@ pub enum CustomMediaQueryMediaType { #[tag("MediaQueryList")] MediaQueryList(MediaQueryList), } + +impl Take for CustomMediaQueryMediaType { + #[inline] + fn dummy() -> Self { + Self::Ident(Take::dummy()) + } +} diff --git a/crates/swc_css_ast/src/value.rs b/crates/swc_css_ast/src/value.rs index 823d47a877be..a5857b928bd3 100644 --- a/crates/swc_css_ast/src/value.rs +++ b/crates/swc_css_ast/src/value.rs @@ -20,12 +20,21 @@ pub struct Ident { } impl EqIgnoreSpan for Ident { + #[inline] fn eq_ignore_span(&self, other: &Self) -> bool { self.value == other.value } } +impl PartialEq for Ident { + #[inline] + fn eq(&self, other: &str) -> bool { + &*self.value == other + } +} + impl Take for Ident { + #[inline] fn dummy() -> Self { Self { span: Default::default(), @@ -45,6 +54,7 @@ pub struct CustomIdent { } impl EqIgnoreSpan for CustomIdent { + #[inline] fn eq_ignore_span(&self, other: &Self) -> bool { self.value == other.value } @@ -60,11 +70,19 @@ pub struct DashedIdent { } impl EqIgnoreSpan for DashedIdent { + #[inline] fn eq_ignore_span(&self, other: &Self) -> bool { self.value == other.value } } +impl PartialEq for DashedIdent { + #[inline] + fn eq(&self, other: &str) -> bool { + &*self.value == other + } +} + #[ast_node("CustomPropertyName")] #[derive(Eq, Hash)] pub struct CustomPropertyName { diff --git a/crates/swc_css_compat/src/compiler/custom_media.rs b/crates/swc_css_compat/src/compiler/custom_media.rs index 8b137891791f..503c2c3bdeff 100644 --- a/crates/swc_css_compat/src/compiler/custom_media.rs +++ b/crates/swc_css_compat/src/compiler/custom_media.rs @@ -1 +1,399 @@ +use std::mem::take; +use swc_atoms::js_word; +use swc_common::{util::take::Take, DUMMY_SP}; +use swc_css_ast::{ + AtRule, AtRuleName, AtRulePrelude, CustomMediaQuery, CustomMediaQueryMediaType, Ident, + MediaCondition, MediaConditionAllType, MediaConditionType, MediaConditionWithoutOr, + MediaConditionWithoutOrType, MediaFeature, MediaFeatureBoolean, MediaFeatureName, + MediaInParens, MediaOr, MediaQuery, MediaType, Rule, +}; + +#[derive(Debug, Default)] +pub(super) struct CustomMediaHandler { + modifier_and_media_type: Option<(Option, Option)>, + medias: Vec, +} + +impl CustomMediaHandler { + pub(crate) fn store_custom_media(&mut self, n: &mut AtRule) { + if let AtRuleName::Ident(name) = &n.name { + if name.value.eq_ignore_ascii_case(&js_word!("custom-media")) { + if let Some(box AtRulePrelude::CustomMediaPrelude(prelude)) = &mut n.prelude { + self.medias.push(prelude.take()); + } + } + } + } + + pub(crate) fn process_rules(&mut self, n: &mut Vec) { + n.retain(|n| match n { + Rule::AtRule(n) => { + if matches!(&n.name, AtRuleName::Ident(ident) if ident.value.eq_ignore_ascii_case(&js_word!("custom-media"))) { + return false; + } + + true + } + _ => true, + }); + } + + pub(crate) fn process_media_query(&mut self, n: &mut MediaQuery) { + // Limited support for `modifier` and `media_type`, it is impossible to lowering + // syntax for multiple media types, so we handle only case when only one media + // type exists. + if let Some((modifier, media_type)) = self.modifier_and_media_type.take() { + n.modifier = modifier; + n.media_type = media_type; + + if let Some(condition) = &mut n.condition { + match &mut **condition { + MediaConditionType::WithoutOr(condition) => { + if condition.conditions.is_empty() { + n.condition = None; + } + } + MediaConditionType::All(condition) => { + if condition.conditions.is_empty() { + n.condition = None; + } + } + } + } + } + + self.modifier_and_media_type = None; + } + + pub(crate) fn process_media_condition(&mut self, media_condition: &mut MediaCondition) { + let mut remove_rules_list = vec![]; + + for (i, node) in media_condition.conditions.iter_mut().enumerate() { + match node { + MediaConditionAllType::Not(media_not) => { + if let Some(new_media_in_parens) = + self.process_media_in_parens(&media_not.condition) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + media_not.condition = new_media_in_parens; + } + } + } + MediaConditionAllType::And(media_and) => { + if let Some(new_media_in_parens) = + self.process_media_in_parens(&media_and.condition) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + media_and.condition = new_media_in_parens; + } + } + } + MediaConditionAllType::Or(media_or) => { + if let Some(new_media_in_parens) = + self.process_media_in_parens(&media_or.condition) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + media_or.condition = new_media_in_parens; + } + } + } + MediaConditionAllType::MediaInParens(media_in_parens) => { + if let Some(new_media_in_parens) = self.process_media_in_parens(media_in_parens) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + *media_in_parens = new_media_in_parens; + } + } + } + } + } + + if !remove_rules_list.is_empty() { + let mut need_change_next = false; + + media_condition.conditions = take(&mut media_condition.conditions) + .into_iter() + .enumerate() + .filter_map(|(idx, value)| { + if remove_rules_list.contains(&idx) { + if idx == 0 { + need_change_next = true; + } + + None + } else if need_change_next { + need_change_next = false; + + match value { + MediaConditionAllType::And(media_and) => { + Some(MediaConditionAllType::MediaInParens(media_and.condition)) + } + MediaConditionAllType::Or(media_and) => { + Some(MediaConditionAllType::MediaInParens(media_and.condition)) + } + _ => Some(value), + } + } else { + Some(value) + } + }) + .collect::>(); + } + } + + pub(crate) fn process_media_condition_without_or( + &mut self, + media_condition: &mut MediaConditionWithoutOr, + ) { + let mut remove_rules_list = vec![]; + + for (i, node) in media_condition.conditions.iter_mut().enumerate() { + match node { + MediaConditionWithoutOrType::Not(media_not) => { + if let Some(new_media_in_parens) = + self.process_media_in_parens(&media_not.condition) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + media_not.condition = new_media_in_parens; + } + } + } + MediaConditionWithoutOrType::And(media_and) => { + if let Some(new_media_in_parens) = + self.process_media_in_parens(&media_and.condition) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + media_and.condition = new_media_in_parens; + } + } + } + MediaConditionWithoutOrType::MediaInParens(media_in_parens) => { + if let Some(new_media_in_parens) = self.process_media_in_parens(media_in_parens) + { + if self.is_empty_media_parens(&new_media_in_parens) { + remove_rules_list.push(i); + } else { + *media_in_parens = new_media_in_parens; + } + } + } + } + } + + if !remove_rules_list.is_empty() { + let mut need_change_next = false; + + media_condition.conditions = take(&mut media_condition.conditions) + .into_iter() + .enumerate() + .filter_map(|(idx, value)| { + if remove_rules_list.contains(&idx) { + if idx == 0 { + need_change_next = true; + } + + None + } else if need_change_next { + need_change_next = false; + + match value { + MediaConditionWithoutOrType::And(media_and) => Some( + MediaConditionWithoutOrType::MediaInParens(media_and.condition), + ), + _ => Some(value), + } + } else { + Some(value) + } + }) + .collect::>(); + } + } + + pub(crate) fn process_media_in_parens(&mut self, n: &MediaInParens) -> Option { + if let MediaInParens::Feature(box MediaFeature::Boolean(MediaFeatureBoolean { + name: MediaFeatureName::Ident(name), + .. + })) = n + { + if let Some(custom_media) = self.medias.iter().find(|m| m.name.value == name.value) { + let mut new_media_condition = MediaCondition { + span: DUMMY_SP, + conditions: vec![], + }; + + let queries = match &custom_media.media { + CustomMediaQueryMediaType::Ident(_) => { + // TODO make me warning, we should keep code as is in such cases + unimplemented!( + "Boolean logic in @custom-media at-rules is not supported by swc" + ); + } + CustomMediaQueryMediaType::MediaQueryList(media_query_list) => { + &media_query_list.queries + } + }; + + for query in queries { + if query.media_type.is_some() || query.modifier.is_some() { + // TODO throw a warning on multiple media types + self.modifier_and_media_type = + Some((query.modifier.clone(), query.media_type.clone())); + } + + for condition in &query.condition { + match &**condition { + MediaConditionType::All(media_condition) => { + if new_media_condition.conditions.is_empty() { + if media_condition.conditions.len() == 1 { + let media_in_parens = if let Some( + MediaConditionAllType::MediaInParens(inner), + ) = + media_condition.conditions.get(0) + { + inner.clone() + } else { + MediaInParens::MediaCondition(media_condition.clone()) + }; + + new_media_condition.conditions.push( + MediaConditionAllType::MediaInParens(media_in_parens), + ); + } else { + new_media_condition.conditions.push( + MediaConditionAllType::MediaInParens( + MediaInParens::MediaCondition( + media_condition.clone(), + ), + ), + ); + } + } else if let Some(MediaConditionAllType::MediaInParens(inner)) = + media_condition.conditions.get(0) + { + new_media_condition + .conditions + .push(MediaConditionAllType::Or(MediaOr { + span: DUMMY_SP, + keyword: None, + condition: inner.clone(), + })); + } else { + new_media_condition + .conditions + .push(MediaConditionAllType::Or(MediaOr { + span: DUMMY_SP, + keyword: None, + condition: MediaInParens::MediaCondition( + media_condition.clone(), + ), + })); + } + } + MediaConditionType::WithoutOr(media_condition) => { + let mut media_condition = self + .media_condition_without_or_to_media_condition(media_condition); + + if new_media_condition.conditions.is_empty() { + let media_in_parens = if matches!( + media_condition.conditions.get(0), + Some(MediaConditionAllType::MediaInParens(_)) + ) { + match media_condition.conditions.pop() { + Some(MediaConditionAllType::MediaInParens(inner)) => { + inner + } + _ => { + unreachable!(); + } + } + } else { + MediaInParens::MediaCondition(media_condition) + }; + + new_media_condition.conditions.push( + MediaConditionAllType::MediaInParens(media_in_parens), + ); + } else { + new_media_condition + .conditions + .push(MediaConditionAllType::Or(MediaOr { + span: DUMMY_SP, + keyword: None, + condition: MediaInParens::MediaCondition( + media_condition, + ), + })); + } + } + } + } + } + + if new_media_condition.conditions.len() == 1 + && matches!( + new_media_condition.conditions.get(0), + Some(MediaConditionAllType::MediaInParens(_)) + ) + { + let only_one = new_media_condition.conditions.pop().unwrap(); + + if let MediaConditionAllType::MediaInParens(media_in_parens) = only_one { + return Some(media_in_parens); + } + } + + return Some(MediaInParens::MediaCondition(new_media_condition)); + } + } + + None + } + + fn media_condition_without_or_to_media_condition( + &self, + media_condition: &MediaConditionWithoutOr, + ) -> MediaCondition { + let mut new_media_condition = MediaCondition { + span: DUMMY_SP, + conditions: vec![], + }; + + for n in &media_condition.conditions { + let condition = match n { + MediaConditionWithoutOrType::MediaInParens(n) => { + MediaConditionAllType::MediaInParens(n.clone()) + } + MediaConditionWithoutOrType::And(n) => MediaConditionAllType::And(n.clone()), + MediaConditionWithoutOrType::Not(n) => MediaConditionAllType::Not(n.clone()), + }; + + new_media_condition.conditions.push(condition); + } + + new_media_condition + } + + fn is_empty_media_parens(&self, media_in_parens: &MediaInParens) -> bool { + if let MediaInParens::MediaCondition(MediaCondition { conditions, .. }) = media_in_parens { + if conditions.is_empty() { + return true; + } + } + + false + } +} diff --git a/crates/swc_css_compat/src/compiler/mod.rs b/crates/swc_css_compat/src/compiler/mod.rs index 407da9976532..104beee1e7d7 100644 --- a/crates/swc_css_compat/src/compiler/mod.rs +++ b/crates/swc_css_compat/src/compiler/mod.rs @@ -1,5 +1,7 @@ -use swc_css_visit::VisitMut; +use swc_css_ast::{AtRule, MediaCondition, MediaConditionWithoutOr, MediaQuery, Rule}; +use swc_css_visit::{VisitMut, VisitMutWith}; +use self::custom_media::CustomMediaHandler; use crate::feature::Features; mod custom_media; @@ -9,6 +11,7 @@ mod custom_media; pub struct Compiler { #[allow(unused)] c: Config, + custom_media: CustomMediaHandler, } #[derive(Debug)] @@ -19,8 +22,51 @@ pub struct Config { impl Compiler { pub fn new(config: Config) -> Self { - Self { c: config } + Self { + c: config, + custom_media: Default::default(), + } } } -impl VisitMut for Compiler {} +impl VisitMut for Compiler { + fn visit_mut_at_rule(&mut self, n: &mut AtRule) { + n.visit_mut_children_with(self); + + if self.c.process.contains(Features::CUSTOM_MEDIA) { + self.custom_media.store_custom_media(n); + } + } + + fn visit_mut_media_query(&mut self, n: &mut MediaQuery) { + n.visit_mut_children_with(self); + + if self.c.process.contains(Features::CUSTOM_MEDIA) { + self.custom_media.process_media_query(n); + } + } + + fn visit_mut_media_condition(&mut self, n: &mut MediaCondition) { + n.visit_mut_children_with(self); + + if self.c.process.contains(Features::CUSTOM_MEDIA) { + self.custom_media.process_media_condition(n); + } + } + + fn visit_mut_media_condition_without_or(&mut self, n: &mut MediaConditionWithoutOr) { + n.visit_mut_children_with(self); + + if self.c.process.contains(Features::CUSTOM_MEDIA) { + self.custom_media.process_media_condition_without_or(n); + } + } + + fn visit_mut_rules(&mut self, n: &mut Vec) { + n.visit_mut_children_with(self); + + if self.c.process.contains(Features::CUSTOM_MEDIA) { + self.custom_media.process_rules(n); + } + } +} diff --git a/crates/swc_css_compat/src/feature.rs b/crates/swc_css_compat/src/feature.rs index 0d108a34f2b2..a1f5344b9b96 100644 --- a/crates/swc_css_compat/src/feature.rs +++ b/crates/swc_css_compat/src/feature.rs @@ -2,6 +2,7 @@ use bitflags::bitflags; bitflags! { pub struct Features: u64 { - const NESTING = 0b00000001; + const NESTING = 1 << 0; + const CUSTOM_MEDIA = 1 << 1; } } diff --git a/crates/swc_css_compat/src/lib.rs b/crates/swc_css_compat/src/lib.rs index d990d98d57af..2df12b35d269 100644 --- a/crates/swc_css_compat/src/lib.rs +++ b/crates/swc_css_compat/src/lib.rs @@ -1,4 +1,6 @@ +#![feature(box_syntax)] #![feature(box_patterns)] +#![allow(clippy::boxed_local)] #![allow(clippy::vec_box)] pub mod compiler; diff --git a/crates/swc_css_compat/tests/custom-media-query/basic.css b/crates/swc_css_compat/tests/custom-media-query/basic.css new file mode 100644 index 000000000000..292af57e5b3f --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/basic.css @@ -0,0 +1,224 @@ +@custom-media --mq-a (max-width: 30em), (max-height: 30em); +@custom-media --mq-b screen and (max-width: 30em); +@custom-media --not-mq-a not all and (--mq-a); + +@import url("narrow.css") supports(display: flex) screen and (--mq-a); +@import url("narrow.css") supports(display: flex) (--mq-a); +@import url("narrow.css") (--mq-a); + +@media (--mq-a) { + body { + order: 1; + } +} + +@media (--mq-b) { + body { + order: 1; + } +} + +@media (--mq-a), (--mq-a) { + body { + order: 1; + } +} + +@media not all and (--mq-a) { + body { + order: 2; + } +} + +@media (--not-mq-a) { + body { + order: 1; + } +} + +@media not all and (--not-mq-a) { + body { + order: 2; + } +} + +@custom-media --not (not (min-width: 20px)); + +@media (--not) and (width > 1024px) { + .a { color: green; } +} + +@custom-media --not1 (not (min-width: 20px)) and (min-width: 20px); + +@media (--not1) and (width > 1024px) { + .a { color: green; } +} + +@custom-media --or (min-width: 20px) or (min-width: 20px); + +@media (--or) and (width > 1024px) { + .a { color: green; } +} + +@custom-media --or1 (not (min-width: 20px)) or (min-width: 20px); + +@media (--or1) and (width > 1024px) { + .a { color: green; } +} + +@custom-media --and (min-width: 20px) and (min-width: 20px); + +@media (--and) and (width > 1024px) { + .a { color: green; } +} + +@custom-media --and1 (not (min-width: 20px)) and (min-width: 20px); + +@media (--and1) and (width > 1024px) { + .a { color: green; } +} + +/* TODO warning on such cases */ +@custom-media --circular-mq-a (--circular-mq-b); +@custom-media --circular-mq-b (--circular-mq-a); + +@media (--circular-mq-a) { + body { + order: 3; + } +} + +@media (--circular-mq-b) { + body { + order: 4; + } +} + +@media (--unresolved-mq) { + body { + order: 5; + } +} + +@CUSTOM-MEDIA --CASE (max-width: 30em), (max-height: 30em); + +@media (--CASE) { + body { + order: 1; + } +} + +/* TODO do prescan */ +@media (--foo) { + body { + background-color: red; + } +} + +@custom-media --foo print; + +@custom-media --screen only screen; + +@media (--screen) { + body { + background-color: red; + } +} + +@custom-media --screen only screen; + +@media only screen and (--screen) { + body { + background-color: red; + } +} + +@media ((((((--mq-a)))))) { + body { + order: 1; + } +} + +@custom-media --mq-d (max-width: 40em); + +@media ((((((--mq-a))))) or ((((--mq-d))))) { + body { + order: 1; + } +} + +@custom-media --mq-e (max-width: 40em), (max-height: 40em); + +@media ((((((--mq-a))))) or ((((--mq-e))))) { + body { + order: 1; + } +} + + +@media ((((((--mq-a)))))), ((((((--mq-a)))))) { + body { + order: 1; + } +} + +@media ((((((--mq-a))))) or ((((--mq-e))))), ((((((--mq-a))))) or ((((--mq-e))))) { + body { + order: 1; + } +} + +@media (--mq-a) or (--mq-e), (--mq-a) or (--mq-e) { + body { + order: 1; + } +} + +@media ((((((--mq-a) or ((((((--mq-a))))))))))) { + body { + order: 1; + } +} + +@media (not (--mq-a)) or (hover) { + .a { + color: red; + } +} + +@media ((max-width: 30em) or (--mq-a)) { + body { + order: 1; + } +} + +@media ((max-width: 30em) and (--mq-a)) { + body { + order: 1; + } +} + +@media ((max-width: 30em) and (not (--mq-a))) { + body { + order: 1; + } +} + +/* We can't lower the syntax here and should print a warning */ +@custom-media --screen screen and (max-width: 30em); +@custom-media --print print and (max-width: 30em); + +@media (--screen) or (--print) { + .a { + color: red; + } +} + +/* We can't lower the syntax here and should print a warning */ +@custom-media --print print and (max-width: 30em); + +@media screen and (--print) { + .a { + color: red; + } +} diff --git a/crates/swc_css_compat/tests/custom-media-query/basic.expect.css b/crates/swc_css_compat/tests/custom-media-query/basic.expect.css new file mode 100644 index 000000000000..e0c289364ad7 --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/basic.expect.css @@ -0,0 +1,163 @@ +@import url("narrow.css") supports(display: flex) screen and ((max-width: 30em) or (max-height: 30em)); +@import url("narrow.css") supports(display: flex) ((max-width: 30em) or (max-height: 30em)); +@import url("narrow.css") ((max-width: 30em) or (max-height: 30em)); +@media ((max-width: 30em) or (max-height: 30em)) { + body { + order: 1; + } +} +@media screen and (max-width: 30em) { + body { + order: 1; + } +} +@media ((max-width: 30em) or (max-height: 30em)), ((max-width: 30em) or (max-height: 30em)) { + body { + order: 1; + } +} +@media not all and ((max-width: 30em) or (max-height: 30em)) { + body { + order: 2; + } +} +@media not all and ((max-width: 30em) or (max-height: 30em)) { + body { + order: 1; + } +} +@media not all and ((max-width: 30em) or (max-height: 30em)) { + body { + order: 2; + } +} +@media (not (min-width: 20px)) and (width > 1024px) { + .a { + color: green; + } +} +@media ((not (min-width: 20px)) and (min-width: 20px)) and (width > 1024px) { + .a { + color: green; + } +} +@media ((min-width: 20px) or (min-width: 20px)) and (width > 1024px) { + .a { + color: green; + } +} +@media ((not (min-width: 20px)) or (min-width: 20px)) and (width > 1024px) { + .a { + color: green; + } +} +@media ((min-width: 20px) and (min-width: 20px)) and (width > 1024px) { + .a { + color: green; + } +} +@media ((not (min-width: 20px)) and (min-width: 20px)) and (width > 1024px) { + .a { + color: green; + } +} +@media (--circular-mq-b) { + body { + order: 3; + } +} +@media (--circular-mq-b) { + body { + order: 4; + } +} +@media (--unresolved-mq) { + body { + order: 5; + } +} +@media ((max-width: 30em) or (max-height: 30em)) { + body { + order: 1; + } +} +@media (--foo) { + body { + background-color: red; + } +} +@media only screen { + body { + background-color: red; + } +} +@media only screen { + body { + background-color: red; + } +} +@media (((((((max-width: 30em) or (max-height: 30em))))))) { + body { + order: 1; + } +} +@media (((((((max-width: 30em) or (max-height: 30em)))))) or ((((max-width: 40em))))) { + body { + order: 1; + } +} +@media (((((((max-width: 30em) or (max-height: 30em)))))) or (((((max-width: 40em) or (max-height: 40em)))))) { + body { + order: 1; + } +} +@media (((((((max-width: 30em) or (max-height: 30em))))))), (((((((max-width: 30em) or (max-height: 30em))))))) { + body { + order: 1; + } +} +@media (((((((max-width: 30em) or (max-height: 30em)))))) or (((((max-width: 40em) or (max-height: 40em)))))), (((((((max-width: 30em) or (max-height: 30em)))))) or (((((max-width: 40em) or (max-height: 40em)))))) { + body { + order: 1; + } +} +@media ((max-width: 30em) or (max-height: 30em)) or ((max-width: 40em) or (max-height: 40em)), ((max-width: 30em) or (max-height: 30em)) or ((max-width: 40em) or (max-height: 40em)) { + body { + order: 1; + } +} +@media (((((((max-width: 30em) or (max-height: 30em)) or (((((((max-width: 30em) or (max-height: 30em)))))))))))) { + body { + order: 1; + } +} +@media (not ((max-width: 30em) or (max-height: 30em))) or (hover) { + .a { + color: red; + } +} +@media ((max-width: 30em) or ((max-width: 30em) or (max-height: 30em))) { + body { + order: 1; + } +} +@media ((max-width: 30em) and ((max-width: 30em) or (max-height: 30em))) { + body { + order: 1; + } +} +@media ((max-width: 30em) and (not ((max-width: 30em) or (max-height: 30em)))) { + body { + order: 1; + } +} +@media print and (max-width: 30em) { + .a { + color: red; + } +} +@media print and (max-width: 30em) { + .a { + color: red; + } +} diff --git a/crates/swc_css_compat/tests/custom-media-query/basic.import.expect.css b/crates/swc_css_compat/tests/custom-media-query/basic.import.expect.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/swc_css_compat/tests/custom-media-query/basic.preserve.expect.css b/crates/swc_css_compat/tests/custom-media-query/basic.preserve.expect.css new file mode 100644 index 000000000000..4c8aefc634ca --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/basic.preserve.expect.css @@ -0,0 +1,224 @@ +@custom-media --mq-a (max-width: 30em), (max-height: 30em); +@custom-media --mq-b screen and (max-width: 30em); +@custom-media --not-mq-a not all and (--mq-a); + +@media (max-width: 30em),(max-height: 30em) { + body { + order: 1; + } +} + +@media (--mq-a) { + body { + order: 1; + } +} + +@media screen and (max-width: 30em) { + body { + order: 1; + } +} + +@media (--mq-b) { + body { + order: 1; + } +} + +@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { + body { + order: 1; + } +} + +@media (--mq-a), (--mq-a) { + body { + order: 1; + } +} + +@media not all and (max-width: 30em),not all and (max-height: 30em) { + body { + order: 2; + } +} + +@media not all and (--mq-a) { + body { + order: 2; + } +} + +@media not all and (max-width: 30em),not all and (max-height: 30em) { + body { + order: 1; + } +} + +@media (--not-mq-a) { + body { + order: 1; + } +} + +@media all and (max-width: 30em),all and (max-height: 30em) { + body { + order: 2; + } +} + +@media not all and (--not-mq-a) { + body { + order: 2; + } +} + +@custom-media --circular-mq-a (--circular-mq-b); +@custom-media --circular-mq-b (--circular-mq-a); + +@media (--circular-mq-a) { + body { + order: 3; + } +} + +@media (--circular-mq-b) { + body { + order: 4; + } +} + +@media (--unresolved-mq) { + body { + order: 5; + } +} + +@custom-media --min (min-width: 320px); +@custom-media --max (max-width: 640px); + +@media (min-width: 320px) and (max-width: 640px) { + body { + order: 6; + } +} + +@media (--min) and (--max) { + body { + order: 6; + } +} + +@custom-media --concat (min-width: 320px) and (max-width: 640px); + +@media (min-width: 320px) and (max-width: 640px) { + body { + order: 7; + } +} + +@media (--concat) { + body { + order: 7; + } +} + +@media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) { + body { + order: 8; + } +} + +@media (--concat) and (min-aspect-ratio: 16/9) { + body { + order: 8; + } +} + +@media (max-width: 30em),(max-height: 30em) { + body { + order: 1000; + } +} + +@media ( --mq-a ) { + body { + order: 1000; + } +} + +@media (max-width: 30em),(max-height: 30em) { + body { + order: 1001; + } +} + +@media ( --mq-a ) { + body { + order: 1001; + } +} + +@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { + body { + order: 1002; + } +} + +@media ( --mq-a ), ( --mq-a ) { + body { + order: 1002; + } +} + +@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { + body { + order: 1003; + } +} + +@media ( --mq-a ), ( --mq-a ) { + body { + order: 1003; + } +} + +@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { + body { + order: 1004; + } +} + +@media ( --mq-a ), ( --mq-a ) { + body { + order: 1004; + } +} + +@media (max-width: 30em),(max-height: 30em), +(max-width: 30em), +(max-height: 30em) { + body { + order: 1005; + } +} + +@media ( + --mq-a +), +( + --mq-a +) { + body { + order: 1005; + } +} + +@media (trailer--) { + body { + order: 1006; + } +} + +@custom-media trailer-- (min-width: 320px); diff --git a/crates/swc_css_compat/tests/custom-media-query/complex.css b/crates/swc_css_compat/tests/custom-media-query/complex.css new file mode 100644 index 000000000000..aeb2076ff233 --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/complex.css @@ -0,0 +1,110 @@ +@custom-media --🧑🏾‍🎤 (min-width: 1); + +@media (--🧑🏾‍🎤) { + .a { + order: 1; + } +} + +@custom-media --\(\)-escaped (min-width: 2); + +@media (--\(\)-escaped) { + .a { + order: 2; + } +} + +@custom-media --modern (min-width: 3), (min-width: 4); + +@media (--modern) and (width > 1024px) { + .a { order: 3; } +} + +@custom-media --test1 (color), (hover); + +@media (--test1) and (width > 1024px) { + .a { color: green; } +} + +@custom-media --test2 not (color), (hover); + +@media (--test2) and (width > 1024px) { + body { background: red; } +} + +@custom-media --test3 (color) and (min-width: 20px), (hover); + +@media (--test3) and (min-height: 20px) { + .a { color: green; } +} + +@custom-media --screen only screen; +@custom-media --md-and-larger1 (--screen) and (width >= 570px); + +@media (--md-and-larger1) { + body { + background-color: orange; + } +} + +@custom-media --screen only screen; +@custom-media --md-and-larger1 --screen and (width >= 570px); +@custom-media --md-and-larger2 (--screen) and (width >= 570px); +@custom-media --md-and-larger3 only screen and (width >= 570px); +@custom-media --md-larger4 (width >=570px); +@custom-media --md-smaller4 (width < 1000px); + +@media (--md-and-larger1) { + body { + background-color: red; + } +} + +@media (--md-and-larger2) { + body { + background-color: yellow; + } +} + +@media (--md-and-larger3) { + body { + background-color: green; + } +} + +@media (--screen) and (--md-larger4) { + body { + background-color: green; + } +} + +@media (--screen) or (--md-larger4) { + body { + background-color: green; + } +} + +@media not (--md-larger4) { + body { + background-color: green; + } +} + + +@media (--screen) and (not (--md-larger4)) { + body { + background-color: green; + } +} + +@media ((--screen) and (not (--md-larger4))) { + body { + background-color: green; + } +} + +@media (--md-larger4) and (--md-smaller4) { + body { + background-color: green; + } +} diff --git a/crates/swc_css_compat/tests/custom-media-query/complex.expect.css b/crates/swc_css_compat/tests/custom-media-query/complex.expect.css new file mode 100644 index 000000000000..e9a8efb92e4a --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/complex.expect.css @@ -0,0 +1,80 @@ +@media (min-width: 1) { + .a { + order: 1; + } +} +@media (min-width: 2) { + .a { + order: 2; + } +} +@media ((min-width: 3) or (min-width: 4)) and (width > 1024px) { + .a { + order: 3; + } +} +@media ((color) or (hover)) and (width > 1024px) { + .a { + color: green; + } +} +@media ((not (color)) or (hover)) and (width > 1024px) { + body { + background: red; + } +} +@media (((color) and (min-width: 20px)) or (hover)) and (min-height: 20px) { + .a { + color: green; + } +} +@media only screen and (width >= 570px) { + body { + background-color: orange; + } +} +@media only screen and (width >= 570px) { + body { + background-color: red; + } +} +@media only screen and (width >= 570px) { + body { + background-color: yellow; + } +} +@media only screen and (width >= 570px) { + body { + background-color: green; + } +} +@media only screen and (width >= 570px) { + body { + background-color: green; + } +} +@media only screen and (width >= 570px) { + body { + background-color: green; + } +} +@media not (width >= 570px) { + body { + background-color: green; + } +} +@media only screen and (not (width >= 570px)) { + body { + background-color: green; + } +} +@media only screen and ((not (width >= 570px))) { + body { + background-color: green; + } +} +@media (width >= 570px) and (width < 1000px) { + body { + background-color: green; + } +} diff --git a/crates/swc_css_compat/tests/custom-media-query/examples/example.css b/crates/swc_css_compat/tests/custom-media-query/examples/example.css new file mode 100644 index 000000000000..fe7634abb0ca --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/examples/example.css @@ -0,0 +1,5 @@ +@custom-media --small-viewport (max-width: 30em); + +@media (--small-viewport) { + /* styles for small viewport */ +} diff --git a/crates/swc_css_compat/tests/custom-media-query/examples/example.expect.css b/crates/swc_css_compat/tests/custom-media-query/examples/example.expect.css new file mode 100644 index 000000000000..88ae34dc3b9c --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/examples/example.expect.css @@ -0,0 +1 @@ +@media (max-width: 30em) {} diff --git a/crates/swc_css_compat/tests/custom-media-query/examples/example.preserve.expect.css b/crates/swc_css_compat/tests/custom-media-query/examples/example.preserve.expect.css new file mode 100644 index 000000000000..ed814500bc99 --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/examples/example.preserve.expect.css @@ -0,0 +1,9 @@ +@custom-media --small-viewport (max-width: 30em); + +@media (max-width: 30em) { + /* styles for small viewport */ +} + +@media (--small-viewport) { + /* styles for small viewport */ +} diff --git a/crates/swc_css_compat/tests/custom-media-query/export-media.css b/crates/swc_css_compat/tests/custom-media-query/export-media.css new file mode 100644 index 000000000000..f51e88c82e21 --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/export-media.css @@ -0,0 +1,8 @@ +@custom-media --mq-a (max-width: 30em), (max-height: 30em); +@custom-media --mq-b screen and (max-width: 30em); +@custom-media --not-mq-a not all and (--mq-a); +@custom-media --circular-mq-a (--circular-mq-b); +@custom-media --circular-mq-b (--circular-mq-a); +@custom-media --min (min-width: 320px); +@custom-media --max (max-width: 640px); +@custom-media --concat (min-width: 320px) and (max-width: 640px); diff --git a/crates/swc_css_compat/tests/custom-media-query/export-media.json b/crates/swc_css_compat/tests/custom-media-query/export-media.json new file mode 100644 index 000000000000..729bde28eeb0 --- /dev/null +++ b/crates/swc_css_compat/tests/custom-media-query/export-media.json @@ -0,0 +1,12 @@ +{ + "custom-media": { + "--mq-a": "(max-width: 30em), (max-height: 30em)", + "--mq-b": "screen and (max-width: 30em)", + "--not-mq-a": "not all and (--mq-a)", + "--circular-mq-a": "(--circular-mq-b)", + "--circular-mq-b": "(--circular-mq-a)", + "--min": "(min-width: 320px)", + "--max": "(max-width: 640px)", + "--concat": "(min-width: 320px) and (max-width: 640px)" + } +} diff --git a/crates/swc_css_compat/tests/fixture.rs b/crates/swc_css_compat/tests/fixture.rs new file mode 100644 index 000000000000..fc734d203d88 --- /dev/null +++ b/crates/swc_css_compat/tests/fixture.rs @@ -0,0 +1,105 @@ +//! Tests ported from https://github.com/thysultan/stylis.js +//! +//! License is MIT, which is original license at the time of copying. +//! Original test authors have copyright for their work. +#![deny(warnings)] +#![allow(clippy::needless_update)] + +use std::path::PathBuf; + +use swc_common::{errors::HANDLER, sync::Lrc, SourceFile}; +use swc_css_ast::Stylesheet; +use swc_css_codegen::{ + writer::basic::{BasicCssWriter, BasicCssWriterConfig}, + CodegenConfig, Emit, +}; +use swc_css_compat::{ + compiler::{Compiler, Config}, + feature::Features, + nesting::nesting, +}; +use swc_css_parser::{parse_file, parser::ParserConfig}; +use swc_css_visit::VisitMutWith; +use testing::NormalizedOutput; + +fn parse_stylesheet(fm: &Lrc) -> Stylesheet { + let mut errors = vec![]; + let ss: Stylesheet = parse_file( + fm, + ParserConfig { + allow_wrong_line_comments: true, + ..Default::default() + }, + &mut errors, + ) + .unwrap(); + for err in errors { + HANDLER.with(|handler| { + err.to_diagnostics(handler).emit(); + }); + } + + ss +} + +fn print_stylesheet(ss: &Stylesheet) -> String { + let mut s = String::new(); + { + let mut wr = BasicCssWriter::new(&mut s, None, BasicCssWriterConfig::default()); + let mut gen = swc_css_codegen::CodeGenerator::new(&mut wr, CodegenConfig { minify: false }); + + gen.emit(&ss).unwrap(); + } + + s +} + +fn test_nesting(input: PathBuf, suffix: Option<&str>) { + let parent = input.parent().unwrap(); + let output = match suffix { + Some(suffix) => parent.join("output.".to_owned() + suffix + ".css"), + _ => parent.join("output.css"), + }; + + testing::run_test(false, |cm, _| { + // + let fm = cm.load_file(&input).unwrap(); + let mut ss = parse_stylesheet(&fm); + + ss.visit_mut_with(&mut nesting()); + + let s = print_stylesheet(&ss); + + NormalizedOutput::from(s).compare_to_file(&output).unwrap(); + + Ok(()) + }) + .unwrap(); +} + +#[testing::fixture("tests/nesting/**/input.css")] +fn test_nesting_without_env(input: PathBuf) { + test_nesting(input, None) +} + +#[testing::fixture("tests/custom-media-query/**/*.css", exclude("expect.css"))] +fn test_custom_media_query(input: PathBuf) { + let output = input.with_extension("expect.css"); + + testing::run_test(false, |cm, _| { + // + let fm = cm.load_file(&input).unwrap(); + let mut ss = parse_stylesheet(&fm); + + ss.visit_mut_with(&mut Compiler::new(Config { + process: Features::CUSTOM_MEDIA, + })); + + let s = print_stylesheet(&ss); + + NormalizedOutput::from(s).compare_to_file(&output).unwrap(); + + Ok(()) + }) + .unwrap(); +} diff --git a/crates/swc_css_compat/tests/nesting.rs b/crates/swc_css_compat/tests/nesting.rs deleted file mode 100644 index e08d6102d43d..000000000000 --- a/crates/swc_css_compat/tests/nesting.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Tests ported from https://github.com/thysultan/stylis.js -//! -//! License is MIT, which is original license at the time of copying. -//! Original test authors have copyright for their work. -#![deny(warnings)] -#![allow(clippy::needless_update)] - -use std::path::PathBuf; - -use swc_css_ast::Stylesheet; -use swc_css_codegen::{ - writer::basic::{BasicCssWriter, BasicCssWriterConfig}, - CodegenConfig, Emit, -}; -use swc_css_compat::nesting::nesting; -use swc_css_parser::{parse_file, parser::ParserConfig}; -use swc_css_visit::VisitMutWith; -use testing::NormalizedOutput; - -fn test_nesting(input: PathBuf, suffix: Option<&str>) { - let parent = input.parent().unwrap(); - let output = match suffix { - Some(suffix) => parent.join("output.".to_owned() + suffix + ".css"), - _ => parent.join("output.css"), - }; - - testing::run_test2(false, |cm, handler| { - // - let fm = cm.load_file(&input).unwrap(); - let mut errors = vec![]; - let mut ss: Stylesheet = parse_file( - &fm, - ParserConfig { - allow_wrong_line_comments: true, - ..Default::default() - }, - &mut errors, - ) - .unwrap(); - for err in errors { - err.to_diagnostics(&handler).emit(); - } - - ss.visit_mut_with(&mut nesting()); - - let mut s = String::new(); - { - let mut wr = BasicCssWriter::new(&mut s, None, BasicCssWriterConfig::default()); - let mut gen = - swc_css_codegen::CodeGenerator::new(&mut wr, CodegenConfig { minify: false }); - - gen.emit(&ss).unwrap(); - } - - NormalizedOutput::from(s).compare_to_file(&output).unwrap(); - - Ok(()) - }) - .unwrap(); -} - -#[testing::fixture("tests/nesting/**/input.css")] -fn test_without_env(input: PathBuf) { - test_nesting(input, None) -}