feat(css/compat): Implement transform for nested css (#6077)

This commit is contained in:
Donny/강동윤 2022-10-07 14:18:14 +09:00 committed by GitHub
parent 5c486c5234
commit 347d4b7602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 577 additions and 40 deletions

View File

@ -269,6 +269,9 @@ jobs:
- crate: swc_css_codegen_macros
os: ubuntu-latest
runner: ubuntu-latest
- crate: swc_css_compat
os: ubuntu-latest
runner: ubuntu-latest
- crate: swc_css_lints
os: ubuntu-latest
runner: ubuntu-latest

20
Cargo.lock generated
View File

@ -3196,9 +3196,11 @@ version = "0.128.0"
dependencies = [
"swc_css_ast",
"swc_css_codegen",
"swc_css_compat",
"swc_css_minifier",
"swc_css_modules",
"swc_css_parser",
"swc_css_prefixer",
"swc_css_utils",
"swc_css_visit",
]
@ -3245,6 +3247,24 @@ dependencies = [
"syn",
]
[[package]]
name = "swc_css_compat"
version = "0.1.0"
dependencies = [
"once_cell",
"preset_env_base",
"serde",
"serde_json",
"swc_atoms",
"swc_common",
"swc_css_ast",
"swc_css_codegen",
"swc_css_parser",
"swc_css_utils",
"swc_css_visit",
"testing",
]
[[package]]
name = "swc_css_lints"
version = "0.34.0"

View File

@ -1,29 +1,33 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "CSS apis for rust"
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "CSS apis for rust"
documentation = "https://rustdoc.swc.rs/swc_css/"
edition = "2021"
license = "Apache-2.0"
name = "swc_css"
repository = "https://github.com/swc-project/swc.git"
version = "0.128.0"
edition = "2021"
license = "Apache-2.0"
name = "swc_css"
repository = "https://github.com/swc-project/swc.git"
version = "0.128.0"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lib]
bench = false
[features]
compat = ["swc_css_compat"]
minifier = ["swc_css_minifier"]
modules = ["swc_css_modules"]
modules = ["swc_css_modules"]
prefixer = ["swc_css_prefixer"]
[dependencies]
swc_css_ast = { version = "0.115.0", path = "../swc_css_ast" }
swc_css_codegen = { version = "0.125.0", path = "../swc_css_codegen" }
swc_css_minifier = { version = "0.90.0", path = "../swc_css_minifier", optional = true }
swc_css_modules = { version = "0.1.0", path = "../swc_css_modules", optional = true }
swc_css_parser = { version = "0.124.0", path = "../swc_css_parser" }
swc_css_utils = { version = "0.112.0", path = "../swc_css_utils/" }
swc_css_visit = { version = "0.114.0", path = "../swc_css_visit" }
swc_css_ast = {version = "0.115.0", path = "../swc_css_ast"}
swc_css_codegen = {version = "0.125.0", path = "../swc_css_codegen"}
swc_css_compat = {version = "0.1.0", path = "../swc_css_compat", optional = true}
swc_css_minifier = {version = "0.90.0", path = "../swc_css_minifier", optional = true}
swc_css_modules = {version = "0.1.0", path = "../swc_css_modules", optional = true}
swc_css_parser = {version = "0.124.0", path = "../swc_css_parser"}
swc_css_prefixer = {version = "0.126.0", path = "../swc_css_prefixer", optional = true}
swc_css_utils = {version = "0.112.0", path = "../swc_css_utils/"}
swc_css_visit = {version = "0.114.0", path = "../swc_css_visit"}

View File

@ -1,5 +1,8 @@
pub extern crate swc_css_ast as ast;
pub extern crate swc_css_codegen as codegen;
#[cfg(feature = "swc_css_compat")]
#[cfg_attr(docsrs, doc(cfg(feature = "compat")))]
pub extern crate swc_css_compat as compat;
#[cfg(feature = "swc_css_minifier")]
#[cfg_attr(docsrs, doc(cfg(feature = "minifier")))]
pub extern crate swc_css_minifier as minifier;
@ -7,5 +10,8 @@ pub extern crate swc_css_minifier as minifier;
#[cfg_attr(docsrs, doc(cfg(feature = "modules")))]
pub extern crate swc_css_modules as modules;
pub extern crate swc_css_parser as parser;
#[cfg(feature = "swc_css_prefixer")]
#[cfg_attr(docsrs, doc(cfg(feature = "prefixer")))]
pub extern crate swc_css_prefixer as prefixer;
pub extern crate swc_css_utils as utils;
pub extern crate swc_css_visit as visit;

View File

@ -0,0 +1,29 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Port of stylis"
documentation = "https://rustdoc.swc.rs/swc_css_compat/"
edition = "2021"
include = ["Cargo.toml", "src/**/*.rs", "src/**/*.json", "data/**/*.json"]
license = "Apache-2.0"
name = "swc_css_compat"
repository = "https://github.com/swc-project/swc.git"
version = "0.1.0"
[lib]
bench = false
[dependencies]
once_cell = "1.10.0"
preset_env_base = { version = "0.3.2", path = "../preset_env_base" }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61"
swc_atoms = { version = "0.4.18", path = "../swc_atoms" }
swc_common = { version = "0.29.5", path = "../swc_common" }
swc_css_ast = { version = "0.115.0", path = "../swc_css_ast" }
swc_css_utils = { version = "0.112.0", path = "../swc_css_utils/" }
swc_css_visit = { version = "0.114.0", path = "../swc_css_visit" }
[dev-dependencies]
swc_css_codegen = { version = "0.125.0", path = "../swc_css_codegen" }
swc_css_parser = { version = "0.124.0", path = "../swc_css_parser" }
testing = { version = "0.31.5", path = "../testing" }

View File

@ -0,0 +1,3 @@
#![allow(clippy::vec_box)]
pub mod nesting;

View File

@ -0,0 +1,227 @@
use swc_common::util::take::Take;
use swc_css_ast::{
ComplexSelector, ComplexSelectorChildren, ComponentValue, CompoundSelector,
ForgivingComplexSelector, ForgivingSelectorList, PseudoClassSelector,
PseudoClassSelectorChildren, QualifiedRule, QualifiedRulePrelude, Rule, SelectorList,
StyleBlock, SubclassSelector,
};
use swc_css_visit::{VisitMut, VisitMutWith};
pub fn nesting() -> impl VisitMut {
NestingHandler {}
}
struct NestingHandler {}
impl NestingHandler {
fn append_compound(
&mut self,
prelude: &SelectorList,
to: &mut ComplexSelector,
base: &ComplexSelector,
c: &CompoundSelector,
) {
if c.nesting_selector.is_some() {
let len = base.children.len();
to.children
.extend(
base.children
.iter()
.cloned()
.enumerate()
.map(|(idx, mut children)| {
if idx == len - 1 {
if let ComplexSelectorChildren::CompoundSelector(compound) =
&mut children
{
if c.type_selector.is_some() {
compound.type_selector = c.type_selector.clone();
}
let mut subclass = c.subclass_selectors.clone();
self.process_subclass_selectors(prelude, &mut subclass);
compound.subclass_selectors.extend(subclass);
}
}
children
}),
);
} else {
to.children
.push(ComplexSelectorChildren::CompoundSelector(c.clone()));
}
}
fn process_subclass_selectors(
&mut self,
prelude: &SelectorList,
subclass: &mut Vec<SubclassSelector>,
) {
for sel in subclass {
if let SubclassSelector::PseudoClass(PseudoClassSelector {
children: Some(children),
..
}) = sel
{
for c in children {
if let PseudoClassSelectorChildren::ForgivingSelectorList(c) = c {
let mut selectors = vec![];
for sel in &mut c.children {
match sel {
ForgivingComplexSelector::ComplexSelector(sel) => {
selectors.push(sel.clone());
}
ForgivingComplexSelector::ListOfComponentValues(_) => {
// Abort
return;
}
}
}
self.process_complex_selectors(prelude, &mut selectors);
*c = ForgivingSelectorList {
children: selectors
.into_iter()
.map(ForgivingComplexSelector::ComplexSelector)
.collect(),
..*c
};
}
}
}
}
}
fn process_complex_selectors(
&mut self,
prelude: &SelectorList,
selectors: &mut Vec<ComplexSelector>,
) {
let mut new_selectors = vec![];
//
'complex: for complex in selectors.take() {
for compound in &complex.children {
match compound {
ComplexSelectorChildren::CompoundSelector(compound) => {
if compound.nesting_selector.is_some() {
//
for prelude_children in &prelude.children {
let mut new = ComplexSelector {
span: Default::default(),
children: Default::default(),
};
for compound in &complex.children {
match compound {
ComplexSelectorChildren::CompoundSelector(compound) => {
self.append_compound(
prelude,
&mut new,
prelude_children,
compound,
);
}
ComplexSelectorChildren::Combinator(_) => {
new.children.push(compound.clone());
}
}
}
new_selectors.push(new);
}
continue 'complex;
}
}
ComplexSelectorChildren::Combinator(_) => {}
}
}
new_selectors.push(complex);
}
*selectors = new_selectors;
}
/// Prepend current selector
fn process_prelude(&mut self, prelude: &QualifiedRulePrelude, to: &mut QualifiedRulePrelude) {
if let (
QualifiedRulePrelude::SelectorList(prelude),
QualifiedRulePrelude::SelectorList(selectors),
) = (prelude, to)
{
self.process_complex_selectors(prelude, &mut selectors.children);
}
}
fn extract_nested_rules(&mut self, rule: &mut QualifiedRule) -> Vec<Box<QualifiedRule>> {
let mut rules = vec![];
let mut block_values = vec![];
for value in rule.block.value.take() {
match value {
ComponentValue::StyleBlock(StyleBlock::QualifiedRule(mut q)) => {
self.process_prelude(&rule.prelude, &mut q.prelude);
rules.push(q);
}
_ => {
block_values.push(value);
}
}
}
rule.block.value = block_values;
rules
}
}
impl VisitMut for NestingHandler {
fn visit_mut_rules(&mut self, n: &mut Vec<Rule>) {
n.visit_mut_children_with(self);
let mut new = vec![];
for n in n.take() {
match n {
Rule::QualifiedRule(mut n) => {
let rules = self.extract_nested_rules(&mut n);
new.push(Rule::QualifiedRule(n));
new.extend(rules.into_iter().map(Rule::QualifiedRule));
}
_ => {
new.push(n);
}
}
}
*n = new;
}
fn visit_mut_component_values(&mut self, n: &mut Vec<ComponentValue>) {
n.visit_mut_children_with(self);
let mut new = vec![];
for n in n.take() {
match n {
ComponentValue::StyleBlock(StyleBlock::QualifiedRule(mut n)) => {
let rules = self.extract_nested_rules(&mut n);
new.push(ComponentValue::StyleBlock(StyleBlock::QualifiedRule(n)));
new.extend(
rules
.into_iter()
.map(StyleBlock::QualifiedRule)
.map(ComponentValue::StyleBlock),
);
}
_ => {
new.push(n);
}
}
}
*n = new;
}
}

View File

@ -0,0 +1,65 @@
//! 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)
}

View File

@ -0,0 +1,13 @@
table.colortable {
& td {
text-align: center;
&.c { text-transform:uppercase }
&:first-child, &:first-child + td { border:1px solid black }
}
& th {
text-align:center;
background:black;
color:white;
}
}

View File

@ -0,0 +1,16 @@
table.colortable {}
table.colortable td {
text-align: center;
}
table.colortable td.c {
text-transform: uppercase;
}
table.colortable td:first-child,
table.colortable td:first-child + td {
border: 1px solid black;
}
table.colortable th {
text-align: center;
background: black;
color: white;
}

View File

@ -0,0 +1,69 @@
table.colortable {
& td {
text-align: center;
&.c { text-transform:uppercase }
&:first-child, &:first-child + td { border:1px solid black }
}
& th {
text-align:center;
background:black;
color:white;
}
}
.foo {
color: blue;
& > .bar { color: red; }
}
.foo {
color: blue;
&.bar { color: red; }
}
.foo, .bar {
color: blue;
& + .baz, &.qux { color: red; }
}
.foo {
color: blue;
& .bar & .baz & .qux { color: red; }
}
.foo {
color: blue;
& { padding: 2ch; }
}
/* TODO fix me */
/*.foo {*/
/* color: blue;*/
/* && { padding: 2ch; }*/
/*}*/
.error, #test {
&:hover > .baz { color: red; }
}
.foo {
&:is(.bar, &.baz) { color: red; }
}
figure {
margin: 0;
& > figcaption {
background: hsl(0 0% 0% / 50%);
& > p {
font-size: .9rem;
}
}
}
.foo {
color: blue;
&__bar { color: red; }
}

View File

@ -0,0 +1,75 @@
table.colortable {}
table.colortable td {
text-align: center;
}
table.colortable td.c {
text-transform: uppercase;
}
table.colortable td:first-child,
table.colortable td:first-child + td {
border: 1px solid black;
}
table.colortable th {
text-align: center;
background: black;
color: white;
}
.foo {
color: blue;
}
.foo > .bar {
color: red;
}
.foo {
color: blue;
}
.foo.bar {
color: red;
}
.foo,
.bar {
color: blue;
}
.foo + .baz,
.bar + .baz,
.foo.qux,
.bar.qux {
color: red;
}
.foo {
color: blue;
}
.foo .bar .foo .baz .foo .qux {
color: red;
}
.foo {
color: blue;
}
.foo {
padding: 2ch;
}
.error,
#test {}
.error:hover > .baz,
#test:hover > .baz {
color: red;
}
.foo {}
.foo:is(.bar, .foo.baz) {
color: red;
}
figure {
margin: 0;
}
figure > figcaption {
background: hsl(0 0% 0% / 50%);
}
figure > figcaption > p {
font-size: .9rem;
}
.foo {
color: blue;
}
__bar.foo {
color: red;
}

View File

@ -0,0 +1,3 @@
.foo {
&:is(.bar, &.baz) { color: red; }
}

View File

@ -0,0 +1,4 @@
.foo {}
.foo:is(.bar, .foo.baz) {
color: red;
}

View File

@ -1,29 +1,29 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Port of stylis"
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Port of stylis"
documentation = "https://rustdoc.swc.rs/swc_stylis/"
edition = "2021"
include = ["Cargo.toml", "src/**/*.rs", "src/**/*.json", "data/**/*.json"]
license = "Apache-2.0"
name = "swc_css_prefixer"
repository = "https://github.com/swc-project/swc.git"
version = "0.126.0"
edition = "2021"
include = ["Cargo.toml", "src/**/*.rs", "src/**/*.json", "data/**/*.json"]
license = "Apache-2.0"
name = "swc_css_prefixer"
repository = "https://github.com/swc-project/swc.git"
version = "0.126.0"
[lib]
bench = false
[dependencies]
once_cell = "1.10.0"
preset_env_base = { version = "0.3.2", path = "../preset_env_base" }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61"
swc_atoms = { version = "0.4.18", path = "../swc_atoms" }
swc_common = { version = "0.29.5", path = "../swc_common" }
swc_css_ast = { version = "0.115.0", path = "../swc_css_ast" }
swc_css_utils = { version = "0.112.0", path = "../swc_css_utils/" }
swc_css_visit = { version = "0.114.0", path = "../swc_css_visit" }
once_cell = "1.10.0"
preset_env_base = {version = "0.3.2", path = "../preset_env_base"}
serde = {version = "1.0.118", features = ["derive"]}
serde_json = "1.0.61"
swc_atoms = {version = "0.4.18", path = "../swc_atoms"}
swc_common = {version = "0.29.5", path = "../swc_common"}
swc_css_ast = {version = "0.115.0", path = "../swc_css_ast"}
swc_css_utils = {version = "0.112.0", path = "../swc_css_utils/"}
swc_css_visit = {version = "0.114.0", path = "../swc_css_visit"}
[dev-dependencies]
swc_css_codegen = { version = "0.125.0", path = "../swc_css_codegen" }
swc_css_parser = { version = "0.124.0", path = "../swc_css_parser" }
testing = { version = "0.31.5", path = "../testing" }
swc_css_codegen = {version = "0.125.0", path = "../swc_css_codegen"}
swc_css_parser = {version = "0.124.0", path = "../swc_css_parser"}
testing = {version = "0.31.5", path = "../testing"}

View File

@ -30,9 +30,9 @@ check:
swc_bundler:
- "cargo hack check --feature-powerset --no-dev-deps"
swc_common:
- "cargo hack check --feature-powerset --no-dev-deps --exclude-features plugin_transform_schema_vtest"
- "cargo hack check --feature-powerset --no-dev-deps --exclude-features plugin_transform_schema_vtest --exclude-features __plugin --exclude-features __plugin_mode --exclude-features __plugin_rt --exclude-features __rkyv --exclude-features plugin-bytecheck-base --exclude-features plugin-bytecheck-mode --exclude-features plugin-bytecheck-rt --exclude-features rkyv-bytecheck-impl --exclude-features rkyv-impl"
swc_ecma_ast:
- "cargo hack check --feature-powerset --no-dev-deps"
- "cargo hack check --feature-powerset --no-dev-deps --exclude-features __rkyv --exclude-features rkyv-bytecheck-impl"
swc_ecma_loader:
- "cargo hack check --feature-powerset --no-dev-deps"
swc_ecma_transforms: