From c35a5fcaa6d6e233256f4c06836a0f602a4968cc Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Wed, 5 Mar 2025 00:53:26 -0700 Subject: [PATCH 01/75] upgrade detect-libc (#923) --- cli/postinstall.js | 3 ++- node/index.js | 3 ++- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cli/postinstall.js b/cli/postinstall.js index abf9dc19..19dadc79 100644 --- a/cli/postinstall.js +++ b/cli/postinstall.js @@ -3,7 +3,8 @@ let path = require('path'); let parts = [process.platform, process.arch]; if (process.platform === 'linux') { - const {MUSL, family} = require('detect-libc'); + const {MUSL, familySync} = require('detect-libc'); + const family = familySync(); if (family === MUSL) { parts.push('musl'); } else if (process.arch === 'arm') { diff --git a/node/index.js b/node/index.js index a9f2f6d5..011d04b4 100644 --- a/node/index.js +++ b/node/index.js @@ -1,6 +1,7 @@ let parts = [process.platform, process.arch]; if (process.platform === 'linux') { - const { MUSL, family } = require('detect-libc'); + const { MUSL, familySync } = require('detect-libc'); + const family = familySync(); if (family === MUSL) { parts.push('musl'); } else if (process.arch === 'arm') { diff --git a/package.json b/package.json index de384950..bffb0446 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "node/*.flow" ], "dependencies": { - "detect-libc": "^1.0.3" + "detect-libc": "^2.0.3" }, "devDependencies": { "@babel/parser": "7.21.4", From b15341333a3a4825c578c2523837f54efff4c366 Mon Sep 17 00:00:00 2001 From: Liam Esparraguera Date: Tue, 4 Mar 2025 23:54:05 -0800 Subject: [PATCH 02/75] fix double hyphen when number negative and in scientific notation (#901) --- src/values/number.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/values/number.rs b/src/values/number.rs index bac4d23d..60506eac 100644 --- a/src/values/number.rs +++ b/src/values/number.rs @@ -40,7 +40,7 @@ impl ToCss for CSSNumber { cssparser::ToCss::to_css(self, &mut s)?; if number < 0.0 { dest.write_char('-')?; - dest.write_str(s.trim_start_matches("-0")) + dest.write_str(s.trim_start_matches("-").trim_start_matches("0")) } else { dest.write_str(s.trim_start_matches('0')) } From 22846530a008baf7a0985e9b87d90c6ae5c4047b Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 5 Mar 2025 21:45:23 -0800 Subject: [PATCH 03/75] Fix parsing style container queries without a value Closes #915 --- node/ast.d.ts | 6 +++++- src/lib.rs | 21 ++++++++++++++++++++- src/rules/container.rs | 26 +++++++++++++++++--------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/node/ast.d.ts b/node/ast.d.ts index cc198e2e..4af77f67 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -7453,9 +7453,13 @@ export type ContainerSizeFeatureId = "width" | "height" | "inline-size" | "block * Represents a style query within a container condition. */ export type StyleQuery = | { - type: "feature"; + type: "declaration"; value: D; } +| { + type: "property"; + value: PropertyId; + } | { type: "not"; value: StyleQuery; diff --git a/src/lib.rs b/src/lib.rs index 508b1984..88630424 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28270,6 +28270,26 @@ mod tests { "#, "@container style(--my-prop:foo - bar ()){.foo{color:red}}", ); + minify_test( + r#" + @container style(--test) { + .foo { + color: red; + } + } + "#, + "@container style(--test){.foo{color:red}}", + ); + minify_test( + r#" + @container style(width) { + .foo { + color: red; + } + } + "#, + "@container style(width){.foo{color:red}}", + ); // Disallow 'none', 'not', 'and', 'or' as a `` // https://github.com/w3c/csswg-drafts/issues/7203#issuecomment-1144257312 @@ -28310,7 +28330,6 @@ mod tests { error_test("@container (inline-size <= foo) {}", ParserError::InvalidMediaQuery); error_test("@container (orientation <= 10px) {}", ParserError::InvalidMediaQuery); - error_test("@container style(width) {}", ParserError::EndOfInput); error_test( "@container style(style(--foo: bar)) {}", ParserError::UnexpectedToken(crate::properties::custom::Token::Function("style".into())), diff --git a/src/rules/container.rs b/src/rules/container.rs index a911d33b..b5f5bd1a 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -112,9 +112,13 @@ impl FeatureToCss for ContainerSizeFeatureId { )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum StyleQuery<'i> { - /// A style feature, implicitly parenthesized. + /// A property declaration. #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] - Feature(Property<'i>), + Declaration(Property<'i>), + /// A property name, without a value. + /// This matches if the property value is different from the initial value. + #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::"))] + Property(PropertyId<'i>), /// A negation of a condition. #[cfg_attr(feature = "visitor", skip_type)] #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] @@ -170,11 +174,14 @@ impl<'i> QueryCondition<'i> for StyleQuery<'i> { #[inline] fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>> { let property_id = PropertyId::parse(input)?; - input.expect_colon()?; - input.skip_whitespace(); - let feature = Self::Feature(Property::parse(property_id, input, &Default::default())?); - let _ = input.try_parse(|input| parse_important(input)); - Ok(feature) + if input.try_parse(|input| input.expect_colon()).is_ok() { + input.skip_whitespace(); + let feature = Self::Declaration(Property::parse(property_id, input, &Default::default())?); + let _ = input.try_parse(|input| parse_important(input)); + Ok(feature) + } else { + Ok(Self::Property(property_id)) + } } #[inline] @@ -191,7 +198,7 @@ impl<'i> QueryCondition<'i> for StyleQuery<'i> { match self { StyleQuery::Not(_) => true, StyleQuery::Operation { operator, .. } => Some(*operator) != parent_operator, - StyleQuery::Feature(_) => true, + StyleQuery::Declaration(_) | StyleQuery::Property(_) => true, } } } @@ -232,7 +239,8 @@ impl<'i> ToCss for StyleQuery<'i> { W: std::fmt::Write, { match *self { - StyleQuery::Feature(ref f) => f.to_css(dest, false), + StyleQuery::Declaration(ref f) => f.to_css(dest, false), + StyleQuery::Property(ref f) => f.to_css(dest), StyleQuery::Not(ref c) => { dest.write_str("not ")?; to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) From 50a956bdadf15c504ce568e6c42a1d5a7b0ce940 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 6 Mar 2025 13:52:07 +0800 Subject: [PATCH 04/75] Bump browserslist-rs 0.17.0 (#912) --- Cargo.lock | 5 ++--- Cargo.toml | 2 +- c/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa68d332..6d0e8466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "browserslist-rs" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf0ca73de70c3da94e4194e4a01fe732378f55d47cf4c0588caab22a0dbfa14" +checksum = "74c973b79d9b6b89854493185ab760c6ef8e54bcfad10ad4e33991e46b374ac8" dependencies = [ "ahash 0.8.11", "chrono", @@ -150,7 +150,6 @@ dependencies = [ "indexmap 2.7.0", "itertools 0.13.0", "nom", - "once_cell", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 82802a65..3ab9bf27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ indexmap = { version = "2.2.6", features = ["serde"] } # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } -browserslist-rs = { version = "0.16.0", optional = true } +browserslist-rs = { version = "0.17.0", optional = true } rayon = { version = "1.5.1", optional = true } dashmap = { version = "5.0.0", optional = true } serde_json = { version = "1.0.78", optional = true } diff --git a/c/Cargo.toml b/c/Cargo.toml index 96641dc7..3d20add0 100644 --- a/c/Cargo.toml +++ b/c/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] [dependencies] lightningcss = { path = "../", features = ["browserslist"] } parcel_sourcemap = { version = "2.1.1", features = ["json"] } -browserslist-rs = { version = "0.16.0" } +browserslist-rs = { version = "0.17.0" } [build-dependencies] cbindgen = "0.24.3" From d7b1cea9991d0f9785353c5aff09ffe8269ac320 Mon Sep 17 00:00:00 2001 From: CPunisher <1343316114@qq.com> Date: Thu, 6 Mar 2025 13:52:35 +0800 Subject: [PATCH 05/75] Add wasm compilation cfg for (#895) --- src/targets.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/targets.rs b/src/targets.rs index 6c1f429a..8180852f 100644 --- a/src/targets.rs +++ b/src/targets.rs @@ -51,6 +51,7 @@ impl Browsers { Self::from_distribs(resolve(query, &Opts::default())?) } + #[cfg(not(target_arch = "wasm32"))] /// Finds browserslist configuration, selects queries by environment and loads the resulting queries into LightningCSS targets. /// /// Configuration resolution is modeled after the original `browserslist` nodeJS package. From eccb67397dd8d2ba6f04287d650ca420a21fdd8b Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 5 Mar 2025 21:55:30 -0800 Subject: [PATCH 06/75] bump browser compat data --- package.json | 7 +++--- src/compat.rs | 44 ++++++++++++++++++------------------ src/prefixes.rs | 60 ++++++++++++++++++++++++------------------------- yarn.lock | 15 ++++++++----- 4 files changed, 65 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index bffb0446..794b78fa 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ "@codemirror/lang-javascript": "^6.1.2", "@codemirror/lint": "^6.1.0", "@codemirror/theme-one-dark": "^6.1.0", - "@mdn/browser-compat-data": "~5.6.26", + "@mdn/browser-compat-data": "~5.7.0", "@napi-rs/cli": "^2.14.0", "autoprefixer": "^10.4.20", - "caniuse-lite": "^1.0.30001690", + "caniuse-lite": "^1.0.30001702", "codemirror": "^6.0.1", "cssnano": "^7.0.6", "esbuild": "^0.19.8", @@ -79,8 +79,7 @@ "uvu": "^0.5.6" }, "resolutions": { - "lightningcss": "link:.", - "caniuse-lite": "^1.0.30001677" + "lightningcss": "link:." }, "scripts": { "prepare": "patch-package", diff --git a/src/compat.rs b/src/compat.rs index 6f360bb1..6ed9ee88 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -446,7 +446,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -538,7 +538,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -583,7 +583,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -628,7 +628,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -673,7 +673,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -718,7 +718,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -763,7 +763,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -808,7 +808,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -900,7 +900,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -945,7 +945,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1015,7 +1015,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1060,7 +1060,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1150,7 +1150,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1195,7 +1195,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1245,7 +1245,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1332,7 +1332,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1377,7 +1377,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1422,7 +1422,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1462,7 +1462,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1507,7 +1507,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1552,7 +1552,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } @@ -1619,7 +1619,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8585216 { + if version < 8716288 { return false; } } diff --git a/src/prefixes.rs b/src/prefixes.rs index 8735011a..803b9f98 100644 --- a/src/prefixes.rs +++ b/src/prefixes.rs @@ -562,7 +562,7 @@ impl Feature { } Feature::Element => { if let Some(version) = browsers.firefox { - if version >= 131072 && version <= 8847360 { + if version >= 131072 && version <= 8912896 { prefixes |= VendorPrefix::Moz; } } @@ -672,7 +672,7 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 197120 && version <= 1179904 { + if version >= 197120 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -682,7 +682,7 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 196864 && version <= 1179904 { + if version >= 196864 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -1190,17 +1190,17 @@ impl Feature { } Feature::Fill | Feature::FillAvailable => { if let Some(version) = browsers.chrome { - if version >= 1441792 && version <= 8716288 { + if version >= 1441792 && version <= 8781824 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8519680 { + if version >= 263168 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8519680 { + if version >= 5177344 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } @@ -1225,7 +1225,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 && version <= 1703936 { + if version >= 262144 { prefixes |= VendorPrefix::WebKit; } } @@ -1269,27 +1269,27 @@ impl Feature { } Feature::Stretch => { if let Some(version) = browsers.chrome { - if version >= 1441792 && version <= 8716288 { + if version >= 1441792 && version <= 8781824 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.firefox { - if version >= 196608 && version <= 8847360 { + if version >= 196608 && version <= 8912896 { prefixes |= VendorPrefix::Moz; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8519680 { + if version >= 263168 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8519680 { + if version >= 5177344 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.ios_saf { - if version >= 458752 && version <= 1179904 { + if version >= 458752 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -1299,12 +1299,12 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 458752 && version <= 1179904 { + if version >= 458752 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.samsung { - if version >= 327680 && version <= 1703936 { + if version >= 327680 { prefixes |= VendorPrefix::WebKit; } } @@ -1374,7 +1374,7 @@ impl Feature { } Feature::TextDecorationSkip | Feature::TextDecorationSkipInk => { if let Some(version) = browsers.ios_saf { - if version >= 524288 && version <= 1179904 { + if version >= 524288 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -1386,12 +1386,12 @@ impl Feature { } Feature::TextDecoration => { if let Some(version) = browsers.ios_saf { - if version >= 524288 && version <= 1179904 { + if version >= 524288 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 524288 && version <= 1179904 { + if version >= 524288 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -1415,7 +1415,7 @@ impl Feature { } Feature::TextSizeAdjust => { if let Some(version) = browsers.firefox { - if version <= 8519680 { + if version <= 8650752 { prefixes |= VendorPrefix::Moz; } } @@ -1430,7 +1430,7 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 327680 && version <= 1179904 { + if version >= 327680 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -1534,7 +1534,7 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 458752 && version <= 1179904 { + if version >= 458752 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } @@ -1544,12 +1544,12 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 393472 && version <= 1179904 { + if version >= 393472 && version <= 1180416 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.samsung { - if version >= 262144 && version <= 1703936 { + if version >= 262144 { prefixes |= VendorPrefix::WebKit; } } @@ -1921,17 +1921,17 @@ impl Feature { } Feature::CrossFade => { if let Some(version) = browsers.chrome { - if version >= 1114112 && version <= 8716288 { + if version >= 1114112 && version <= 8781824 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8519680 { + if version >= 263168 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8519680 { + if version >= 5177344 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } @@ -1951,7 +1951,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 && version <= 1703936 { + if version >= 262144 { prefixes |= VendorPrefix::WebKit; } } @@ -2156,17 +2156,17 @@ impl Feature { } Feature::PrintColorAdjust | Feature::ColorAdjust => { if let Some(version) = browsers.chrome { - if version >= 1114112 && version <= 8716288 { + if version >= 1114112 && version <= 8781824 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8519680 { + if version >= 263168 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8519680 { + if version >= 5177344 && version <= 8585216 { prefixes |= VendorPrefix::WebKit; } } @@ -2191,7 +2191,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 && version <= 1703936 { + if version >= 262144 { prefixes |= VendorPrefix::WebKit; } } diff --git a/yarn.lock b/yarn.lock index 638e0a6b..fc76b92c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -570,10 +570,10 @@ resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8" integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== -"@mdn/browser-compat-data@~5.6.26": - version "5.6.26" - resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.6.26.tgz#0d1a64cf0a04a29f4283bbc1f5a313f355f9c73d" - integrity sha512-7NdgdOR7lkzrN70zGSULmrcvKyi/aJjpTJRCbuy8IZuHiLkPTvsr10jW0MJgWzK2l2wTmhdQvegTw6yNU5AVNQ== +"@mdn/browser-compat-data@~5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.7.0.tgz#3f08f76234cb075e8377bbb822d93ebc2003bee5" + integrity sha512-Z96UeDGT80sBRJAY67xKnbDEdt5F/8wduy6aU5NeEzLwFqWcyx1oNcj9XapZNMp/i0Nz9iTWZqht+1zcq48j5Q== "@mischnic/json-sourcemap@^0.1.0": version "0.1.1" @@ -1639,11 +1639,16 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001677, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001690: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: version "1.0.30001690" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8" integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== +caniuse-lite@^1.0.30001702: + version "1.0.30001702" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz#cde16fa8adaa066c04aec2967b6cde46354644c4" + integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== + chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" From f2303df29448a55c5f4f284f747eb53760769fe4 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 5 Mar 2025 22:02:46 -0800 Subject: [PATCH 07/75] v1.29.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- napi/Cargo.toml | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d0e8466..5dbeb181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -729,7 +729,7 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.63" +version = "1.0.0-alpha.64" dependencies = [ "ahash 0.8.11", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 3ab9bf27..8e58f5f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ [package] authors = ["Devon Govett "] name = "lightningcss" -version = "1.0.0-alpha.63" +version = "1.0.0-alpha.64" description = "A CSS parser, transformer, and minifier" license = "MPL-2.0" edition = "2021" diff --git a/napi/Cargo.toml b/napi/Cargo.toml index f2019e4b..ae96485c 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -16,7 +16,7 @@ bundler = ["dep:crossbeam-channel", "dep:rayon"] serde = { version = "1.0.201", features = ["derive"] } serde_bytes = "0.11.5" cssparser = "0.33.0" -lightningcss = { version = "1.0.0-alpha.63", path = "../", features = [ +lightningcss = { version = "1.0.0-alpha.64", path = "../", features = [ "nodejs", "serde", ] } diff --git a/package.json b/package.json index 794b78fa..19a5e180 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.29.1", + "version": "1.29.2", "license": "MPL-2.0", "description": "A CSS parser, transformer, and minifier written in Rust", "main": "node/index.js", From 37c4f4df4a7ea8503a7fa5f0c5455dc405f434cd Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 14 Mar 2025 18:05:04 +0100 Subject: [PATCH 08/75] Add support for `::details-content` and `::target-text` (#930) --- scripts/build-prefixes.js | 2 + selectors/parser.rs | 3 ++ src/compat.rs | 82 +++++++++++++++++++++++++++++++++++++++ src/selector.rs | 10 +++++ 4 files changed, 97 insertions(+) diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js index 47a42692..32951a63 100644 --- a/scripts/build-prefixes.js +++ b/scripts/build-prefixes.js @@ -332,6 +332,8 @@ let mdnFeatures = { accentSystemColor: mdn.css.types.color['system-color'].accentcolor_accentcolortext.__compat.support, animationTimelineShorthand: mdn.css.properties.animation['animation-timeline_included'].__compat.support, viewTransition: mdn.css.selectors['view-transition'].__compat.support, + detailsContent: mdn.css.selectors['details-content'].__compat.support, + targetText: mdn.css.selectors['target-text'].__compat.support, }; for (let key in mdn.css.types.length) { diff --git a/selectors/parser.rs b/selectors/parser.rs index d3e18f74..e97d5347 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -3929,6 +3929,9 @@ pub mod tests { assert!(parse("foo:where()").is_err()); assert!(parse("foo:where(div, foo, .bar baz)").is_ok()); assert!(parse("foo:where(::before)").is_err()); + + assert!(parse("foo::details-content").is_ok()); + assert!(parse("foo::target-text").is_ok()); } #[test] diff --git a/src/compat.rs b/src/compat.rs index 6ed9ee88..4a15ee56 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -42,6 +42,7 @@ pub enum Feature { DecimalLeadingZeroListStyleType, DecimalListStyleType, DefaultPseudo, + DetailsContent, DevanagariListStyleType, Dialog, DirSelector, @@ -191,6 +192,7 @@ pub enum Feature { StringListStyleType, SymbolsListStyleType, TamilListStyleType, + TargetText, TeluguListStyleType, TextDecorationThicknessPercent, TextDecorationThicknessShorthand, @@ -3505,6 +3507,86 @@ impl Feature { return false; } } + Feature::DetailsContent => { + if let Some(version) = browsers.chrome { + if version < 8585216 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8585216 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5701632 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1180672 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1180672 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8585216 { + return false; + } + } + if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() { + return false; + } + } + Feature::TargetText => { + if let Some(version) = browsers.chrome { + if version < 5832704 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5832704 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 8585216 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4128768 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1180160 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1180160 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 983040 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 5832704 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } Feature::QUnit => { if let Some(version) = browsers.chrome { if version < 4128768 { diff --git a/src/selector.rs b/src/selector.rs index e624b199..add55dcf 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -267,6 +267,8 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "after" => After, "first-line" => FirstLine, "first-letter" => FirstLetter, + "details-content" => DetailsContent, + "target-text" => TargetText, "cue" => Cue, "cue-region" => CueRegion, "selection" => Selection(VendorPrefix::None), @@ -887,6 +889,10 @@ pub enum PseudoElement<'i> { FirstLine, /// The [::first-letter](https://drafts.csswg.org/css-pseudo-4/#first-letter-pseudo) pseudo element. FirstLetter, + /// The [::details-content](https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo) + DetailsContent, + /// The [::target-text](https://drafts.csswg.org/css-pseudo-4/#selectordef-target-text) + TargetText, /// The [::selection](https://drafts.csswg.org/css-pseudo-4/#selectordef-selection) pseudo element. #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] Selection(VendorPrefix), @@ -1139,6 +1145,8 @@ where Before => dest.write_str(":before"), FirstLine => dest.write_str(":first-line"), FirstLetter => dest.write_str(":first-letter"), + DetailsContent => dest.write_str("::details-content"), + TargetText => dest.write_str("::target-text"), Marker => dest.write_str("::marker"), Selection(prefix) => write_prefixed!(prefix, "selection"), Cue => dest.write_str("::cue"), @@ -1903,6 +1911,8 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { PseudoElement::After | PseudoElement::Before => Feature::Gencontent, PseudoElement::FirstLine => Feature::FirstLine, PseudoElement::FirstLetter => Feature::FirstLetter, + PseudoElement::DetailsContent => Feature::DetailsContent, + PseudoElement::TargetText => Feature::TargetText, PseudoElement::Selection(prefix) if *prefix == VendorPrefix::None => Feature::Selection, PseudoElement::Placeholder(prefix) if *prefix == VendorPrefix::None => Feature::Placeholder, PseudoElement::Marker => Feature::MarkerPseudo, From b93c9c573e6bb5c6d4e13ad401d972f32ab9da84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=9E=E3=83=AB=E3=82=B3=E3=83=A1?= Date: Sat, 15 Mar 2025 02:17:57 +0900 Subject: [PATCH 09/75] fix: update `placeholder-shown` property (#929) --- src/selector.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index add55dcf..f376048c 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -158,8 +158,8 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "read-write" => ReadWrite(VendorPrefix::None), "-moz-read-write" => ReadWrite(VendorPrefix::Moz), "placeholder-shown" => PlaceholderShown(VendorPrefix::None), - "-moz-placeholder-shown" => PlaceholderShown(VendorPrefix::Moz), - "-ms-placeholder-shown" => PlaceholderShown(VendorPrefix::Ms), + "-moz-placeholder" => PlaceholderShown(VendorPrefix::Moz), + "-ms-input-placeholder" => PlaceholderShown(VendorPrefix::Ms), "default" => Default, "checked" => Checked, "indeterminate" => Indeterminate, From 3122b6519c6888a21774998821220ee5db4b95a7 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Fri, 14 Mar 2025 18:18:03 +0100 Subject: [PATCH 10/75] allow to opt-out from css pure linting (#898) --- src/lib.rs | 10 ++++++++++ src/stylesheet.rs | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 88630424..47e6bbf1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7111,6 +7111,16 @@ mod tests { "@scope(._8Z4fiW_a) to (._8Z4fiW_b){._8Z4fiW_foo{color:red}}", pure_css_module_options.clone(), ); + minify_test_with_options( + "/* cssmodules-pure-no-check */ :global(.foo) { color: red }", + ".foo{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "/*! some license */ /* cssmodules-pure-no-check */ :global(.foo) { color: red }", + "/*! some license */\n.foo{color:red}", + pure_css_module_options.clone(), + ); error_test( "input.defaultCheckbox::before h1 {width: 20px}", diff --git a/src/stylesheet.rs b/src/stylesheet.rs index bb5a704f..d1746720 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -172,6 +172,11 @@ where cssparser::Token::Comment(comment) if comment.starts_with('!') => { license_comments.push((*comment).into()); } + cssparser::Token::Comment(comment) if comment.contains("cssmodules-pure-no-check") => { + if let Some(css_modules) = &mut options.css_modules { + css_modules.pure = false; + } + } _ => break, } state = parser.state(); From 48332fe12fd23a08c931bebc33c34d0aaf797066 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 14 Mar 2025 10:20:31 -0700 Subject: [PATCH 11/75] update ast --- node/ast.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/node/ast.d.ts b/node/ast.d.ts index 4af77f67..5e7ad008 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -6860,6 +6860,12 @@ export type PseudoElement = | { kind: "first-letter"; } + | { + kind: "details-content"; + } + | { + kind: "target-text"; + } | { kind: "selection"; vendorPrefix: VendorPrefix; From c03a7e7f7cfe7a8d487f9402e3194a1d732f9a04 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 14 Mar 2025 10:23:36 -0700 Subject: [PATCH 12/75] bump compat data --- package.json | 6 +++--- src/compat.rs | 23 ++++++++++++++------- src/prefixes.rs | 54 ++++++++++++++++++++++++------------------------- yarn.lock | 44 ++++++++++++++++++++++++---------------- 4 files changed, 72 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 19a5e180..5398e42c 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ "@codemirror/lang-javascript": "^6.1.2", "@codemirror/lint": "^6.1.0", "@codemirror/theme-one-dark": "^6.1.0", - "@mdn/browser-compat-data": "~5.7.0", + "@mdn/browser-compat-data": "~5.7.3", "@napi-rs/cli": "^2.14.0", - "autoprefixer": "^10.4.20", - "caniuse-lite": "^1.0.30001702", + "autoprefixer": "^10.4.21", + "caniuse-lite": "^1.0.30001704", "codemirror": "^6.0.1", "cssnano": "^7.0.6", "esbuild": "^0.19.8", diff --git a/src/compat.rs b/src/compat.rs index 4a15ee56..4c237c4e 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -2926,6 +2926,16 @@ impl Feature { } } Feature::AbsFunction | Feature::SignFunction => { + if let Some(version) = browsers.chrome { + if version < 8847360 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8847360 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 7733248 { return false; @@ -2941,13 +2951,12 @@ impl Feature { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.android { + if version < 8847360 { + return false; + } + } + if browsers.ie.is_some() || browsers.opera.is_some() || browsers.samsung.is_some() { return false; } } diff --git a/src/prefixes.rs b/src/prefixes.rs index 803b9f98..c967bdc6 100644 --- a/src/prefixes.rs +++ b/src/prefixes.rs @@ -562,7 +562,7 @@ impl Feature { } Feature::Element => { if let Some(version) = browsers.firefox { - if version >= 131072 && version <= 8912896 { + if version >= 131072 { prefixes |= VendorPrefix::Moz; } } @@ -672,7 +672,7 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 197120 && version <= 1180416 { + if version >= 197120 { prefixes |= VendorPrefix::WebKit; } } @@ -682,7 +682,7 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 196864 && version <= 1180416 { + if version >= 196864 { prefixes |= VendorPrefix::WebKit; } } @@ -1190,17 +1190,17 @@ impl Feature { } Feature::Fill | Feature::FillAvailable => { if let Some(version) = browsers.chrome { - if version >= 1441792 && version <= 8781824 { + if version >= 1441792 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8585216 { + if version >= 263168 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8585216 { + if version >= 5177344 { prefixes |= VendorPrefix::WebKit; } } @@ -1269,27 +1269,27 @@ impl Feature { } Feature::Stretch => { if let Some(version) = browsers.chrome { - if version >= 1441792 && version <= 8781824 { + if version >= 1441792 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.firefox { - if version >= 196608 && version <= 8912896 { + if version >= 196608 { prefixes |= VendorPrefix::Moz; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8585216 { + if version >= 263168 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8585216 { + if version >= 5177344 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.ios_saf { - if version >= 458752 && version <= 1180416 { + if version >= 458752 { prefixes |= VendorPrefix::WebKit; } } @@ -1299,7 +1299,7 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 458752 && version <= 1180416 { + if version >= 458752 { prefixes |= VendorPrefix::WebKit; } } @@ -1374,7 +1374,7 @@ impl Feature { } Feature::TextDecorationSkip | Feature::TextDecorationSkipInk => { if let Some(version) = browsers.ios_saf { - if version >= 524288 && version <= 1180416 { + if version >= 524288 { prefixes |= VendorPrefix::WebKit; } } @@ -1386,12 +1386,12 @@ impl Feature { } Feature::TextDecoration => { if let Some(version) = browsers.ios_saf { - if version >= 524288 && version <= 1180416 { + if version >= 524288 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 524288 && version <= 1180416 { + if version >= 524288 { prefixes |= VendorPrefix::WebKit; } } @@ -1414,10 +1414,8 @@ impl Feature { } } Feature::TextSizeAdjust => { - if let Some(version) = browsers.firefox { - if version <= 8650752 { - prefixes |= VendorPrefix::Moz; - } + if browsers.firefox.is_some() { + prefixes |= VendorPrefix::Moz; } if let Some(version) = browsers.edge { if version >= 786432 && version <= 1179648 { @@ -1430,7 +1428,7 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 327680 && version <= 1180416 { + if version >= 327680 { prefixes |= VendorPrefix::WebKit; } } @@ -1534,7 +1532,7 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 458752 && version <= 1180416 { + if version >= 458752 { prefixes |= VendorPrefix::WebKit; } } @@ -1544,7 +1542,7 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 393472 && version <= 1180416 { + if version >= 393472 { prefixes |= VendorPrefix::WebKit; } } @@ -1921,17 +1919,17 @@ impl Feature { } Feature::CrossFade => { if let Some(version) = browsers.chrome { - if version >= 1114112 && version <= 8781824 { + if version >= 1114112 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8585216 { + if version >= 263168 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8585216 { + if version >= 5177344 { prefixes |= VendorPrefix::WebKit; } } @@ -2156,17 +2154,17 @@ impl Feature { } Feature::PrintColorAdjust | Feature::ColorAdjust => { if let Some(version) = browsers.chrome { - if version >= 1114112 && version <= 8781824 { + if version >= 1114112 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.android { - if version >= 263168 && version <= 8585216 { + if version >= 263168 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 && version <= 8585216 { + if version >= 5177344 { prefixes |= VendorPrefix::WebKit; } } diff --git a/yarn.lock b/yarn.lock index fc76b92c..2c0ecdf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -570,10 +570,10 @@ resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8" integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== -"@mdn/browser-compat-data@~5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.7.0.tgz#3f08f76234cb075e8377bbb822d93ebc2003bee5" - integrity sha512-Z96UeDGT80sBRJAY67xKnbDEdt5F/8wduy6aU5NeEzLwFqWcyx1oNcj9XapZNMp/i0Nz9iTWZqht+1zcq48j5Q== +"@mdn/browser-compat-data@~5.7.3": + version "5.7.3" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.7.3.tgz#1c4abb3a0ff9e631dd23eed1435d388ccc999f4b" + integrity sha512-ckygcngv0i7Qe0yOzzge/K7Gr5dnk2jNm/AYdqUd1ZTGa9pIEdDuVyWmL3bDU/NdJ8FtdSAjng98YfUuou9Csw== "@mischnic/json-sourcemap@^0.1.0": version "0.1.1" @@ -1505,16 +1505,16 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.4.20: - version "10.4.20" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" - integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== +autoprefixer@^10.4.21: + version "10.4.21" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" + integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== dependencies: - browserslist "^4.23.3" - caniuse-lite "^1.0.30001646" + browserslist "^4.24.4" + caniuse-lite "^1.0.30001702" fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.1" + picocolors "^1.1.1" postcss-value-parser "^4.2.0" available-typed-arrays@^1.0.7: @@ -1580,6 +1580,16 @@ browserslist@^4.0.0, browserslist@^4.23.3, browserslist@^4.6.6: node-releases "^2.0.19" update-browserslist-db "^1.1.1" +browserslist@^4.24.4: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -1639,15 +1649,15 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688: version "1.0.30001690" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8" integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== -caniuse-lite@^1.0.30001702: - version "1.0.30001702" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz#cde16fa8adaa066c04aec2967b6cde46354644c4" - integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001704: + version "1.0.30001704" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz#6644fe909d924ac3a7125e8a0ab6af95b1f32990" + integrity sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew== chalk@^2.4.2: version "2.4.2" @@ -3182,7 +3192,7 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== From 80eb8617c5f8f5519ed85bd3eb6d8953f4d32493 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 14 Mar 2025 10:23:52 -0700 Subject: [PATCH 13/75] v1.29.3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- napi/Cargo.toml | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dbeb181..4c4e0557 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -729,7 +729,7 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.64" +version = "1.0.0-alpha.65" dependencies = [ "ahash 0.8.11", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 8e58f5f5..3c87e362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ [package] authors = ["Devon Govett "] name = "lightningcss" -version = "1.0.0-alpha.64" +version = "1.0.0-alpha.65" description = "A CSS parser, transformer, and minifier" license = "MPL-2.0" edition = "2021" diff --git a/napi/Cargo.toml b/napi/Cargo.toml index ae96485c..0aa3a14d 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -16,7 +16,7 @@ bundler = ["dep:crossbeam-channel", "dep:rayon"] serde = { version = "1.0.201", features = ["derive"] } serde_bytes = "0.11.5" cssparser = "0.33.0" -lightningcss = { version = "1.0.0-alpha.64", path = "../", features = [ +lightningcss = { version = "1.0.0-alpha.65", path = "../", features = [ "nodejs", "serde", ] } diff --git a/package.json b/package.json index 5398e42c..b4c1f4bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.29.2", + "version": "1.29.3", "license": "MPL-2.0", "description": "A CSS parser, transformer, and minifier written in Rust", "main": "node/index.js", From 0daccac88ff2c63183a8f1ed333d900e87ecd579 Mon Sep 17 00:00:00 2001 From: Samuel Cormier-Iijima Date: Sun, 6 Apr 2025 14:19:39 -0400 Subject: [PATCH 14/75] Fix error message for invalid composes selectors (#948) The description for this error message is backwards. --- src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index d936474e..4cca1069 100644 --- a/src/error.rs +++ b/src/error.rs @@ -410,7 +410,7 @@ pub enum PrinterErrorKind { FmtError, /// The CSS modules `composes` property cannot be used within nested rules. InvalidComposesNesting, - /// The CSS modules `composes` property cannot be used with a simple class selector. + /// The CSS modules `composes` property can only be used with a simple class selector. InvalidComposesSelector, /// The CSS modules pattern must end with `[local]` for use in CSS grid. InvalidCssModulesPatternInGrid, From 3ffe09e810cb56e37ab1b35265039bb324b78bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0=20/=20green?= Date: Mon, 7 Apr 2025 05:47:32 +0900 Subject: [PATCH 15/75] Skip generating unnecessary `@supports` (#878) --- src/lib.rs | 555 ++++++++++++++++++++++++++++++- src/rules/font_palette_values.rs | 2 +- src/rules/media.rs | 2 +- src/rules/mod.rs | 29 +- src/rules/style.rs | 4 +- src/rules/supports.rs | 43 ++- src/stylesheet.rs | 4 +- src/targets.rs | 77 ++++- src/values/color.rs | 11 +- 9 files changed, 694 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 47e6bbf1..104ebf62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1476,6 +1476,33 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + &format!( + r#" + @supports (color: lab(0% 0 0)) {{ + .foo {{ + {}: var(--border-width) solid lab(40% 56.6 39); + }} + }} + "#, + prop + ), + &format!( + indoc! {r#" + @supports (color: lab(0% 0 0)) {{ + .foo {{ + {}: var(--border-width) solid lab(40% 56.6 39); + }} + }} + "#}, + prop, + ), + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); } prefix_test( @@ -13655,6 +13682,27 @@ mod tests { ..Browsers::default() }, ); + prefix_test( + r#"@supports (color: lab(0% 0 0)) { + @font-palette-values --Cooler { + font-family: Handover Sans; + base-palette: 3; + override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078); + } + }"#, + indoc! {r#"@supports (color: lab(0% 0 0)) { + @font-palette-values --Cooler { + font-family: Handover Sans; + base-palette: 3; + override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); minify_test(".foo { font-palette: --Custom; }", ".foo{font-palette:--Custom}"); } @@ -15464,6 +15512,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -15831,6 +15900,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-emphasis: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-emphasis: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + safari: Some(8 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -15918,6 +16008,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-shadow: var(--foo) 12px lab(40% 56.6 39); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-shadow: var(--foo) 12px lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -16628,6 +16739,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + caret: lab(50.998% 125.506 -50.7078) var(--foo); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + caret: lab(50.998% 125.506 -50.7078) var(--foo); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -16767,6 +16899,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + test( r#" .foo { @@ -17588,6 +17741,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + background: var(--image) lab(40% 56.6 39); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + background: var(--image) lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -17690,6 +17864,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + color: var(--foo, lab(40% 56.6 39)); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + color: var(--foo, lab(40% 56.6 39)); + } + } + "# + }, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -22002,6 +22198,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -22025,6 +22242,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39) !important; + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39) !important; + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -22057,13 +22295,23 @@ mod tests { prefix_test( r#" - .foo { - --custom: lab(40% 56.6 39); + @supports (color: color(display-p3 0 0 0)) { + .foo { + --custom: color(display-p3 .643308 .192455 .167712); + } + } + + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } } "#, indoc! {r#" - .foo { - --custom: color(display-p3 .643308 .192455 .167712); + @supports (color: color(display-p3 0 0 0)) { + .foo { + --custom: color(display-p3 .643308 .192455 .167712); + } } @supports (color: lab(0% 0 0)) { @@ -22073,6 +22321,7 @@ mod tests { } "#}, Browsers { + chrome: Some(90 << 16), safari: Some(14 << 16), ..Browsers::default() }, @@ -22086,7 +22335,30 @@ mod tests { "#, indoc! {r#" .foo { - --custom: lab(40% 56.6 39); + --custom: color(display-p3 .643308 .192455 .167712); + } + + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + --custom: lab(40% 56.6 39); + } + "#, + indoc! {r#" + .foo { + --custom: lab(40% 56.6 39); } "#}, Browsers { @@ -22223,6 +22495,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + --foo: color(display-p3 0 1 0); + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + --foo: color(display-p3 0 1 0); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -22387,6 +22681,39 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" @keyframes foo { @@ -22441,6 +22768,64 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + @keyframes foo { + from { + --custom: color(display-p3 .643308 .192455 .167712); + } + + to { + --custom: color(display-p3 .972962 -.362078 .804206); + } + } + } + + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + @keyframes foo { + from { + --custom: color(display-p3 .643308 .192455 .167712); + } + + to { + --custom: color(display-p3 .972962 -.362078 .804206); + } + } + } + + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" @keyframes foo { @@ -25643,6 +26028,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", indoc! { r#" @@ -25758,6 +26164,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + -webkit-mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( ".foo { mask: url(masks.svg#star) luminance }", indoc! { r#" @@ -28529,6 +28957,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + color: env(--brand-color, color(display-p3 0 1 0)); + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + color: env(--brand-color, color(display-p3 0 1 0)); + } + } + "#}, + Browsers { + safari: Some(15 << 16), + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + css_modules_test( r#" @media (max-width: env(--branding-small)) { @@ -28922,4 +29372,99 @@ mod tests { "@layer{@view-transition{navigation:auto;types:foo bar}}", ); } + + #[test] + fn test_skip_generating_unnecessary_fallbacks() { + prefix_test( + r#" + @supports (color: lab(0% 0 0)) and (color: color(display-p3 0 0 0)) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) and (color: color(display-p3 0 0 0)) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + // NOTE: fallback for lab is not necessary + prefix_test( + r#" + @supports (color: lab(0% 0 0)) and (not (color: color(display-p3 0 0 0))) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) and (not (color: color(display-p3 0 0 0))) { + .foo { + color: #b32323; + color: lab(40% 56.6 39); + } + + .bar { + color: #b32323; + color: color(display-p3 .643308 .192455 .167712); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) or (color: color(display-p3 0 0 0)) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) or (color: color(display-p3 0 0 0)) { + .foo { + color: #b32323; + color: lab(40% 56.6 39); + } + + .bar { + color: #b32323; + color: color(display-p3 .643308 .192455 .167712); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + } } diff --git a/src/rules/font_palette_values.rs b/src/rules/font_palette_values.rs index 95ca014c..af06c487 100644 --- a/src/rules/font_palette_values.rs +++ b/src/rules/font_palette_values.rs @@ -261,7 +261,7 @@ impl<'i> FontPaletteValuesRule<'i> { // Generate color fallbacks. let mut fallbacks = ColorFallbackKind::empty(); for o in override_colors { - fallbacks |= o.color.get_necessary_fallbacks(*context.targets); + fallbacks |= o.color.get_necessary_fallbacks(context.targets.current); } if fallbacks.contains(ColorFallbackKind::RGB) { diff --git a/src/rules/media.rs b/src/rules/media.rs index 1340bea3..c398b9b0 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -39,7 +39,7 @@ impl<'i, T: Clone> MediaRule<'i, T> { self.query.transform_custom_media(self.loc, custom_media)?; } - self.query.transform_resolution(*context.targets); + self.query.transform_resolution(context.targets.current); Ok(self.rules.0.is_empty() || self.query.never_matches()) } } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4909961a..8f815bbe 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -73,7 +73,7 @@ use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; use crate::selector::{is_compatible, is_equivalent, Component, Selector, SelectorList}; use crate::stylesheet::ParserOptions; -use crate::targets::Targets; +use crate::targets::TargetsWithSupportsScope; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -499,7 +499,7 @@ impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for Css } pub(crate) struct MinifyContext<'a, 'i> { - pub targets: &'a Targets, + pub targets: TargetsWithSupportsScope, pub handler: &'a mut DeclarationHandler<'i>, pub important_handler: &'a mut DeclarationHandler<'i>, pub handler_context: PropertyHandlerContext<'i, 'a>, @@ -535,7 +535,8 @@ impl<'i, T: Clone> CssRuleList<'i, T> { macro_rules! set_prefix { ($keyframes: ident) => { - $keyframes.vendor_prefix = context.targets.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes); + $keyframes.vendor_prefix = + context.targets.current.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes); }; } @@ -559,7 +560,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { set_prefix!(keyframes); keyframe_rules.insert(keyframes.name.clone(), rules.len()); - let fallbacks = keyframes.get_fallbacks(context.targets); + let fallbacks = keyframes.get_fallbacks(&context.targets.current); rules.push(rule); rules.extend(fallbacks); continue; @@ -647,14 +648,14 @@ impl<'i, T: Clone> CssRuleList<'i, T> { // If some of the selectors in this rule are not compatible with the targets, // we need to either wrap in :is() or split them into multiple rules. let incompatible = if style.selectors.0.len() > 1 - && context.targets.should_compile_selectors() - && !style.is_compatible(*context.targets) + && context.targets.current.should_compile_selectors() + && !style.is_compatible(context.targets.current) { // The :is() selector accepts a forgiving selector list, so use that if possible. // Note that :is() does not allow pseudo elements, so we need to check for that. // In addition, :is() takes the highest specificity of its arguments, so if the selectors // have different weights, we need to split them into separate rules as well. - if context.targets.is_compatible(crate::compat::Feature::IsSelector) + if context.targets.current.is_compatible(crate::compat::Feature::IsSelector) && !style.selectors.0.iter().any(|selector| selector.has_pseudo_element()) && style.selectors.0.iter().map(|selector| selector.specificity()).all_equal() { @@ -673,7 +674,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { .cloned() .partition::, _>(|selector| { let list = SelectorList::new(smallvec![selector.clone()]); - is_compatible(&list.0, *context.targets) + is_compatible(&list.0, context.targets.current) }); style.selectors = SelectorList::new(compatible); incompatible @@ -827,7 +828,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { f.minify(context, parent_is_unused); - let fallbacks = f.get_fallbacks(*context.targets); + let fallbacks = f.get_fallbacks(context.targets.current); rules.push(rule); rules.extend(fallbacks); continue; @@ -931,8 +932,8 @@ fn merge_style_rules<'i, T>( ) -> bool { // Merge declarations if the selectors are equivalent, and both are compatible with all targets. if style.selectors == last_style_rule.selectors - && style.is_compatible(*context.targets) - && last_style_rule.is_compatible(*context.targets) + && style.is_compatible(context.targets.current) + && last_style_rule.is_compatible(context.targets.current) && style.rules.0.is_empty() && last_style_rule.rules.0.is_empty() && (!context.css_modules || style.loc.source_index == last_style_rule.loc.source_index) @@ -961,7 +962,7 @@ fn merge_style_rules<'i, T>( { // If the new rule is unprefixed, replace the prefixes of the last rule. // Otherwise, add the new prefix. - if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() { + if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { last_style_rule.vendor_prefix = style.vendor_prefix; } else { last_style_rule.vendor_prefix |= style.vendor_prefix; @@ -970,9 +971,9 @@ fn merge_style_rules<'i, T>( } // Append the selectors to the last rule if the declarations are the same, and all selectors are compatible. - if style.is_compatible(*context.targets) && last_style_rule.is_compatible(*context.targets) { + if style.is_compatible(context.targets.current) && last_style_rule.is_compatible(context.targets.current) { last_style_rule.selectors.0.extend(style.selectors.0.drain(..)); - if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() { + if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { last_style_rule.vendor_prefix = style.vendor_prefix; } else { last_style_rule.vendor_prefix |= style.vendor_prefix; diff --git a/src/rules/style.rs b/src/rules/style.rs index d18ec4f9..851a918b 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -173,8 +173,8 @@ impl<'i, T> StyleRule<'i, T> { pub(crate) fn update_prefix(&mut self, context: &mut MinifyContext<'_, 'i>) { self.vendor_prefix = get_prefix(&self.selectors); - if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() { - self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), *context.targets); + if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { + self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), context.targets.current); } } } diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 04f1fdd3..904da503 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -8,8 +8,9 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::PropertyId; -use crate::targets::Targets; +use crate::targets::{Features, FeaturesIterator, Targets}; use crate::traits::{Parse, ToCss}; +use crate::values::color::ColorFallbackKind; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] @@ -42,8 +43,19 @@ impl<'i, T: Clone> SupportsRule<'i, T> { context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool, ) -> Result<(), MinifyError> { - self.condition.set_prefixes_for_targets(&context.targets); - self.rules.minify(context, parent_is_unused) + let inserted = context.targets.enter_supports(self.condition.get_supported_features()); + if inserted { + context.handler_context.targets = context.targets.current; + } + + self.condition.set_prefixes_for_targets(&context.targets.current); + let result = self.rules.minify(context, parent_is_unused); + + if inserted { + context.targets.exit_supports(); + context.handler_context.targets = context.targets.current; + } + result } } @@ -149,6 +161,31 @@ impl<'i> SupportsCondition<'i> { _ => {} } } + + fn get_supported_features(&self) -> Features { + const COLOR_P3_SUPPORTS_CONDITION: &str = ColorFallbackKind::P3.supports_condition_value(); + const COLOR_LAB_SUPPORTS_CONDITION: &str = ColorFallbackKind::LAB.supports_condition_value(); + fn get_supported_features_internal(value: &SupportsCondition) -> Option { + match value { + SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(), + SupportsCondition::Declaration { property_id, value } => match property_id { + PropertyId::Color => Some(match value.as_ref() { + COLOR_P3_SUPPORTS_CONDITION => Features::P3Colors | Features::ColorFunction, + COLOR_LAB_SUPPORTS_CONDITION => Features::LabColors, + _ => Features::empty(), + }), + _ => Some(Features::empty()), + }, + // bail out if "not" or "or" exists for now + SupportsCondition::Not(_) | SupportsCondition::Or(_) => None, + SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => { + Some(Features::empty()) + } + } + } + + get_supported_features_internal(self).unwrap_or(Features::empty()) + } } impl<'i> Parse<'i> for SupportsCondition<'i> { diff --git a/src/stylesheet.rs b/src/stylesheet.rs index d1746720..dcf87f1c 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -11,7 +11,7 @@ use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterEr use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; use crate::rules::{CssRule, CssRuleList, MinifyContext}; -use crate::targets::{should_compile, Targets}; +use crate::targets::{should_compile, Targets, TargetsWithSupportsScope}; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; #[cfg(feature = "visitor")] @@ -247,7 +247,7 @@ where }; let mut ctx = MinifyContext { - targets: &options.targets, + targets: TargetsWithSupportsScope::new(options.targets), handler: &mut handler, important_handler: &mut important_handler, handler_context: context, diff --git a/src/targets.rs b/src/targets.rs index 8180852f..c568e285 100644 --- a/src/targets.rs +++ b/src/targets.rs @@ -2,6 +2,8 @@ #![allow(missing_docs)] +use std::borrow::Borrow; + use crate::vendor_prefix::VendorPrefix; use bitflags::bitflags; #[cfg(any(feature = "serde", feature = "nodejs"))] @@ -137,7 +139,7 @@ fn parse_version(version: &str) -> Option { bitflags! { /// Features to explicitly enable or disable. - #[derive(Debug, Default, Clone, Copy)] + #[derive(Debug, Default, Clone, Copy, Hash, Eq, PartialEq)] pub struct Features: u32 { const Nesting = 1 << 0; const NotSelectorList = 1 << 1; @@ -166,6 +168,18 @@ bitflags! { } } +pub(crate) trait FeaturesIterator: Sized + Iterator { + fn try_union_all(&mut self) -> Option + where + Self: Iterator>, + T: Borrow, + { + self.try_fold(Features::empty(), |a, b| b.map(|b| a | *b.borrow())) + } +} + +impl FeaturesIterator for I where I: Iterator {} + /// Target browsers and features to compile. #[derive(Debug, Clone, Copy, Default)] pub struct Targets { @@ -226,6 +240,67 @@ impl Targets { } } +#[derive(Debug)] +pub(crate) struct TargetsWithSupportsScope { + stack: Vec, + pub(crate) current: Targets, +} + +impl TargetsWithSupportsScope { + pub fn new(targets: Targets) -> Self { + Self { + stack: Vec::new(), + current: targets, + } + } + + /// Returns true if inserted + pub fn enter_supports(&mut self, features: Features) -> bool { + if features.is_empty() || self.current.exclude.contains(features) { + // Already excluding all features + return false; + } + + let newly_excluded = features - self.current.exclude; + self.stack.push(newly_excluded); + self.current.exclude.insert(newly_excluded); + true + } + + /// Should be only called if inserted + pub fn exit_supports(&mut self) { + if let Some(last) = self.stack.pop() { + self.current.exclude.remove(last); + } + } +} + +#[test] +fn supports_scope_correctly() { + let mut targets = TargetsWithSupportsScope::new(Targets::default()); + assert!(!targets.current.exclude.contains(Features::OklabColors)); + assert!(!targets.current.exclude.contains(Features::LabColors)); + assert!(!targets.current.exclude.contains(Features::P3Colors)); + + targets.enter_supports(Features::OklabColors | Features::LabColors); + assert!(targets.current.exclude.contains(Features::OklabColors)); + assert!(targets.current.exclude.contains(Features::LabColors)); + + targets.enter_supports(Features::P3Colors | Features::LabColors); + assert!(targets.current.exclude.contains(Features::OklabColors)); + assert!(targets.current.exclude.contains(Features::LabColors)); + assert!(targets.current.exclude.contains(Features::P3Colors)); + + targets.exit_supports(); + assert!(targets.current.exclude.contains(Features::OklabColors)); + assert!(targets.current.exclude.contains(Features::LabColors)); + assert!(!targets.current.exclude.contains(Features::P3Colors)); + + targets.exit_supports(); + assert!(!targets.current.exclude.contains(Features::OklabColors)); + assert!(!targets.current.exclude.contains(Features::LabColors)); +} + macro_rules! should_compile { ($targets: expr, $feature: ident) => { $targets.should_compile( diff --git a/src/values/color.rs b/src/values/color.rs index 093d5c12..8d76ba72 100644 --- a/src/values/color.rs +++ b/src/values/color.rs @@ -231,7 +231,7 @@ pub enum FloatColor { bitflags! { /// A color type that is used as a fallback when compiling colors for older browsers. - #[derive(PartialEq, Eq, Clone, Copy)] + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct ColorFallbackKind: u8 { /// An RGB color fallback. const RGB = 0b01; @@ -303,13 +303,16 @@ impl ColorFallbackKind { *self | ColorFallbackKind::from_bits_truncate(self.bits() - 1) } - pub(crate) fn supports_condition<'i>(&self) -> SupportsCondition<'i> { - let s = match *self { + pub(crate) const fn supports_condition_value(&self) -> &'static str { + match *self { ColorFallbackKind::P3 => "color(display-p3 0 0 0)", ColorFallbackKind::LAB => "lab(0% 0 0)", _ => unreachable!(), - }; + } + } + pub(crate) fn supports_condition<'i>(&self) -> SupportsCondition<'i> { + let s = self.supports_condition_value(); SupportsCondition::Declaration { property_id: PropertyId::Color, value: s.into(), From d398c1b86439dfd243a6c2ba627a8e7981118a44 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 6 Apr 2025 14:02:41 -0700 Subject: [PATCH 16/75] Detect @supports features from value, and apply exclusions during printing --- selectors/parser.rs | 2 +- src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++++++ src/media_query.rs | 10 +++---- src/printer.rs | 6 ++-- src/properties/custom.rs | 46 ++++++++++++++++++++++++++--- src/rules/container.rs | 10 +++---- src/rules/style.rs | 2 +- src/rules/supports.rs | 31 +++++++++++--------- src/selector.rs | 4 +-- src/values/calc.rs | 2 +- src/values/color.rs | 51 +++++++++++++++++++++++++------- src/values/gradient.rs | 2 +- src/values/image.rs | 4 +-- src/values/resolution.rs | 2 +- 14 files changed, 185 insertions(+), 50 deletions(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index e97d5347..85c118bc 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -3394,7 +3394,7 @@ pub mod tests { } } - fn parse<'i>(input: &'i str) -> Result, SelectorParseError<'i>> { + fn parse<'i>(input: &'i str) -> Result, SelectorParseError<'i>> { parse_ns(input, &DummyParser::default()) } diff --git a/src/lib.rs b/src/lib.rs index 104ebf62..64441fab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29404,6 +29404,69 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(40% 56.6 39)) { + .foo { + color: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (color: lab(40% 56.6 39)) { + .foo { + color: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (background-color: lab(40% 56.6 39)) { + .foo { + background-color: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (background-color: lab(40% 56.6 39)) { + .foo { + background-color: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (color: light-dark(#f00, #00f)) { + .foo { + color: light-dark(#ff0, #0ff); + } + } + "#, + indoc! {r#" + @supports (color: light-dark(#f00, #00f)) { + .foo { + color: light-dark(#ff0, #0ff); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + // NOTE: fallback for lab is not necessary prefix_test( r#" diff --git a/src/media_query.rs b/src/media_query.rs index d90d3b0b..dbb42f40 100644 --- a/src/media_query.rs +++ b/src/media_query.rs @@ -754,12 +754,12 @@ where { let mut iter = conditions.iter(); let first = iter.next().unwrap(); - to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets))?; + to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets.current))?; for item in iter { dest.write_char(' ')?; operator.to_css(dest)?; dest.write_char(' ')?; - to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets))?; + to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets.current))?; } Ok(()) @@ -787,7 +787,7 @@ impl<'i> ToCss for MediaCondition<'i> { negated.to_css(dest) } else { dest.write_str("not ")?; - to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) } } MediaCondition::Operation { @@ -1087,7 +1087,7 @@ impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> { } QueryFeature::Range { name, operator, value } => { // If range syntax is unsupported, use min/max prefix if possible. - if should_compile!(dest.targets, MediaRangeSyntax) { + if should_compile!(dest.targets.current, MediaRangeSyntax) { return write_min_max(operator, name, value, dest, false); } @@ -1103,7 +1103,7 @@ impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> { end, end_operator, } => { - if should_compile!(dest.targets, MediaIntervalSyntax) { + if should_compile!(dest.targets.current, MediaIntervalSyntax) { write_min_max(&start_operator.opposite(), name, start, dest, true)?; dest.write_str(" and ")?; return write_min_max(end_operator, name, end, dest, true); diff --git a/src/printer.rs b/src/printer.rs index a51c1491..8b5e8f48 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -5,7 +5,7 @@ use crate::dependencies::{Dependency, DependencyOptions}; use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind}; use crate::rules::{Location, StyleContext}; use crate::selector::SelectorList; -use crate::targets::Targets; +use crate::targets::{Targets, TargetsWithSupportsScope}; use crate::vendor_prefix::VendorPrefix; use cssparser::{serialize_identifier, serialize_name}; #[cfg(feature = "sourcemap")] @@ -77,7 +77,7 @@ pub struct Printer<'a, 'b, 'c, W> { line: u32, col: u32, pub(crate) minify: bool, - pub(crate) targets: Targets, + pub(crate) targets: TargetsWithSupportsScope, /// Vendor prefix override. When non-empty, it overrides /// the vendor prefix of whatever is being printed. pub(crate) vendor_prefix: VendorPrefix, @@ -108,7 +108,7 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { line: 0, col: 0, minify: options.minify, - targets: options.targets, + targets: TargetsWithSupportsScope::new(options.targets), vendor_prefix: VendorPrefix::empty(), in_calc: false, css_module: None, diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 702fb724..824bc5b7 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -7,7 +7,7 @@ use crate::printer::Printer; use crate::properties::PropertyId; use crate::rules::supports::SupportsCondition; use crate::stylesheet::ParserOptions; -use crate::targets::{should_compile, Targets}; +use crate::targets::{should_compile, Features, Targets}; use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::angle::Angle; use crate::values::color::{ @@ -1176,6 +1176,44 @@ impl<'i> TokenList<'i> { res } + pub(crate) fn get_features(&self) -> Features { + let mut features = Features::empty(); + for token in &self.0 { + match token { + TokenOrValue::Color(color) => { + features |= color.get_features(); + } + TokenOrValue::UnresolvedColor(unresolved_color) => { + features |= Features::SpaceSeparatedColorNotation; + match unresolved_color { + UnresolvedColor::LightDark { light, dark } => { + features |= Features::LightDark; + features |= light.get_features(); + features |= dark.get_features(); + } + _ => {} + } + } + TokenOrValue::Function(f) => { + features |= f.arguments.get_features(); + } + TokenOrValue::Var(v) => { + if let Some(fallback) = &v.fallback { + features |= fallback.get_features(); + } + } + TokenOrValue::Env(v) => { + if let Some(fallback) = &v.fallback { + features |= fallback.get_features(); + } + } + _ => {} + } + } + + features + } + /// Substitutes variables with the provided values. #[cfg(feature = "substitute_variables")] #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))] @@ -1605,7 +1643,7 @@ impl<'i> UnresolvedColor<'i> { match self { UnresolvedColor::RGB { r, g, b, alpha } => { - if should_compile!(dest.targets, SpaceSeparatedColorNotation) { + if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) { dest.write_str("rgba(")?; c(r).to_css(dest)?; dest.delim(',', false)?; @@ -1629,7 +1667,7 @@ impl<'i> UnresolvedColor<'i> { dest.write_char(')') } UnresolvedColor::HSL { h, s, l, alpha } => { - if should_compile!(dest.targets, SpaceSeparatedColorNotation) { + if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) { dest.write_str("hsla(")?; h.to_css(dest)?; dest.delim(',', false)?; @@ -1653,7 +1691,7 @@ impl<'i> UnresolvedColor<'i> { dest.write_char(')') } UnresolvedColor::LightDark { light, dark } => { - if should_compile!(dest.targets, LightDark) { + if should_compile!(dest.targets.current, LightDark) { dest.write_str("var(--lightningcss-light")?; dest.delim(',', false)?; light.to_css(dest, is_custom_property)?; diff --git a/src/rules/container.rs b/src/rules/container.rs index b5f5bd1a..75378c3c 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -218,7 +218,7 @@ impl<'i> ToCss for ContainerCondition<'i> { ContainerCondition::Feature(ref f) => f.to_css(dest), ContainerCondition::Not(ref c) => { dest.write_str("not ")?; - to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) } ContainerCondition::Operation { ref conditions, @@ -243,7 +243,7 @@ impl<'i> ToCss for StyleQuery<'i> { StyleQuery::Property(ref f) => f.to_css(dest), StyleQuery::Not(ref c) => { dest.write_str("not ")?; - to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) } StyleQuery::Operation { ref conditions, @@ -313,10 +313,10 @@ impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> { } // Don't downlevel range syntax in container queries. - let exclude = dest.targets.exclude; - dest.targets.exclude.insert(Features::MediaQueries); + let exclude = dest.targets.current.exclude; + dest.targets.current.exclude.insert(Features::MediaQueries); self.condition.to_css(dest)?; - dest.targets.exclude = exclude; + dest.targets.current.exclude = exclude; dest.whitespace()?; dest.write_char('{')?; diff --git a/src/rules/style.rs b/src/rules/style.rs index 851a918b..36a629f6 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -245,7 +245,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { W: std::fmt::Write, { // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. - let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets, Nesting); + let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets.current, Nesting); let len = self.declarations.declarations.len() + self.declarations.important_declarations.len(); let has_declarations = supports_nesting || len > 0 || self.rules.0.is_empty(); diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 904da503..a8deccd1 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -7,10 +7,10 @@ use super::{CssRuleList, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; +use crate::properties::custom::TokenList; use crate::properties::PropertyId; use crate::targets::{Features, FeaturesIterator, Targets}; use crate::traits::{Parse, ToCss}; -use crate::values::color::ColorFallbackKind; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] @@ -72,7 +72,13 @@ impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; + + let inserted = dest.targets.enter_supports(self.condition.get_supported_features()); self.rules.to_css(dest)?; + if inserted { + dest.targets.exit_supports(); + } + dest.dedent(); dest.newline()?; dest.write_char('}') @@ -163,24 +169,21 @@ impl<'i> SupportsCondition<'i> { } fn get_supported_features(&self) -> Features { - const COLOR_P3_SUPPORTS_CONDITION: &str = ColorFallbackKind::P3.supports_condition_value(); - const COLOR_LAB_SUPPORTS_CONDITION: &str = ColorFallbackKind::LAB.supports_condition_value(); fn get_supported_features_internal(value: &SupportsCondition) -> Option { match value { SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(), - SupportsCondition::Declaration { property_id, value } => match property_id { - PropertyId::Color => Some(match value.as_ref() { - COLOR_P3_SUPPORTS_CONDITION => Features::P3Colors | Features::ColorFunction, - COLOR_LAB_SUPPORTS_CONDITION => Features::LabColors, - _ => Features::empty(), - }), - _ => Some(Features::empty()), - }, + SupportsCondition::Declaration { value, .. } => { + let mut input = ParserInput::new(&value); + let mut parser = Parser::new(&mut input); + if let Ok(tokens) = TokenList::parse(&mut parser, &Default::default(), 0) { + Some(tokens.get_features()) + } else { + Some(Features::empty()) + } + } // bail out if "not" or "or" exists for now SupportsCondition::Not(_) | SupportsCondition::Or(_) => None, - SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => { - Some(Features::empty()) - } + SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => Some(Features::empty()), } } diff --git a/src/selector.rs b/src/selector.rs index f376048c..28cb1836 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1371,7 +1371,7 @@ where let mut combinators = selector.iter_raw_match_order().rev().filter_map(|x| x.as_combinator()); let compound_selectors = selector.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev(); - let should_compile_nesting = should_compile!(dest.targets, Nesting); + let should_compile_nesting = should_compile!(dest.targets.current, Nesting); let mut first = true; let mut combinators_exhausted = false; @@ -1681,7 +1681,7 @@ where } else { // If there is no context, we are at the root if nesting is supported. This is equivalent to :scope. // Otherwise, if nesting is supported, serialize the nesting selector directly. - if should_compile!(dest.targets, Nesting) { + if should_compile!(dest.targets.current, Nesting) { dest.write_str(":scope") } else { dest.write_char('&') diff --git a/src/values/calc.rs b/src/values/calc.rs index 8c0c5e4f..22303967 100644 --- a/src/values/calc.rs +++ b/src/values/calc.rs @@ -161,7 +161,7 @@ impl + TrySign + Clone + std::fmt::Deb } MathFunction::Clamp(a, b, c) => { // If clamp() is unsupported by targets, output min()/max() - if should_compile!(dest.targets, ClampFunction) { + if should_compile!(dest.targets.current, ClampFunction) { dest.write_str("max(")?; a.to_css(dest)?; dest.delim(',', false)?; diff --git a/src/values/color.rs b/src/values/color.rs index 8d76ba72..3edf8447 100644 --- a/src/values/color.rs +++ b/src/values/color.rs @@ -12,7 +12,7 @@ use crate::macros::enum_property; use crate::printer::Printer; use crate::properties::PropertyId; use crate::rules::supports::SupportsCondition; -use crate::targets::{should_compile, Browsers, Targets}; +use crate::targets::{should_compile, Browsers, Features, Targets}; use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss}; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; @@ -231,7 +231,7 @@ pub enum FloatColor { bitflags! { /// A color type that is used as a fallback when compiling colors for older browsers. - #[derive(Debug, PartialEq, Eq, Clone, Copy)] + #[derive(PartialEq, Eq, Clone, Copy)] pub struct ColorFallbackKind: u8 { /// An RGB color fallback. const RGB = 0b01; @@ -303,16 +303,13 @@ impl ColorFallbackKind { *self | ColorFallbackKind::from_bits_truncate(self.bits() - 1) } - pub(crate) const fn supports_condition_value(&self) -> &'static str { - match *self { + pub(crate) fn supports_condition<'i>(&self) -> SupportsCondition<'i> { + let s = match *self { ColorFallbackKind::P3 => "color(display-p3 0 0 0)", ColorFallbackKind::LAB => "lab(0% 0 0)", _ => unreachable!(), - } - } + }; - pub(crate) fn supports_condition<'i>(&self) -> SupportsCondition<'i> { - let s = self.supports_condition_value(); SupportsCondition::Declaration { property_id: PropertyId::Color, value: s.into(), @@ -447,6 +444,40 @@ impl CssColor { _ => unreachable!(), } } + + pub(crate) fn get_features(&self) -> Features { + let mut features = Features::empty(); + match self { + CssColor::LAB(labcolor) => match &**labcolor { + LABColor::LAB(_) | LABColor::LCH(_) => { + features |= Features::LabColors; + } + LABColor::OKLAB(_) | LABColor::OKLCH(_) => { + features |= Features::OklabColors; + } + }, + CssColor::Predefined(predefined_color) => { + features |= Features::ColorFunction; + match &**predefined_color { + PredefinedColor::DisplayP3(_) => { + features |= Features::P3Colors; + } + _ => {} + } + } + CssColor::Float(_) => { + features |= Features::SpaceSeparatedColorNotation; + } + CssColor::LightDark(light, dark) => { + features |= Features::LightDark; + features |= light.get_features(); + features |= dark.get_features(); + } + _ => {} + } + + features + } } impl IsCompatible for CssColor { @@ -545,7 +576,7 @@ impl ToCss for CssColor { } } else { // If the #rrggbbaa syntax is not supported by the browser targets, output rgba() - if should_compile!(dest.targets, HexAlphaColors) { + if should_compile!(dest.targets.current, HexAlphaColors) { // If the browser doesn't support `#rrggbbaa` color syntax, it is converted to `transparent` when compressed(minify = true). // https://www.w3.org/TR/css-color-4/#transparent-black if dest.minify && color.red == 0 && color.green == 0 && color.blue == 0 && color.alpha == 0 { @@ -598,7 +629,7 @@ impl ToCss for CssColor { CssColor::from(srgb).to_css(dest) } CssColor::LightDark(light, dark) => { - if should_compile!(dest.targets, LightDark) { + if should_compile!(dest.targets.current, LightDark) { dest.write_str("var(--lightningcss-light")?; dest.delim(',', false)?; light.to_css(dest)?; diff --git a/src/values/gradient.rs b/src/values/gradient.rs index fec51938..6ee45f52 100644 --- a/src/values/gradient.rs +++ b/src/values/gradient.rs @@ -971,7 +971,7 @@ where // Use double position stop if the last stop is the same color and all targets support it. if let Some(prev) = last { - if !should_compile!(dest.targets, DoublePositionGradients) { + if !should_compile!(dest.targets.current, DoublePositionGradients) { match (prev, item) { ( GradientItem::ColorStop(ColorStop { diff --git a/src/values/image.rs b/src/values/image.rs index e6ca26c5..4964c98c 100644 --- a/src/values/image.rs +++ b/src/values/image.rs @@ -478,9 +478,9 @@ impl<'i> ImageSetOption<'i> { // Safari only supports the x resolution unit in image-set(). // In other places, x was added as an alias later. // Temporarily ignore the targets while printing here. - let targets = std::mem::take(&mut dest.targets); + let targets = std::mem::take(&mut dest.targets.current); self.resolution.to_css(dest)?; - dest.targets = targets; + dest.targets.current = targets; if let Some(file_type) = &self.file_type { dest.write_str(" type(")?; diff --git a/src/values/resolution.rs b/src/values/resolution.rs index b28e531c..ddd78dce 100644 --- a/src/values/resolution.rs +++ b/src/values/resolution.rs @@ -73,7 +73,7 @@ impl ToCss for Resolution { Resolution::Dpi(dpi) => (*dpi, "dpi"), Resolution::Dpcm(dpcm) => (*dpcm, "dpcm"), Resolution::Dppx(dppx) => { - if dest.targets.is_compatible(Feature::XResolutionUnit) { + if dest.targets.current.is_compatible(Feature::XResolutionUnit) { (*dppx, "x") } else { (*dppx, "dppx") From 6c465c123deb549dfb45fd3558f25dca0dbde0d6 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 13 Apr 2025 16:15:58 -0700 Subject: [PATCH 17/75] Update nesting implementation for new spec Fixes #806, fixes #941, fixes #781, fixes #612 The spec now allows mixing declarations and rules (https://drafts.csswg.org/css-nesting/#mixing), and nesting declarations in at-rules when the parent style rule has pseudo elements (https://drafts.csswg.org/css-nesting/#nested-declarations-rule). --- napi/src/transformer.rs | 1 + node/ast.d.ts | 17 +++++++ src/declaration.rs | 65 +++++++++++++++++++++++--- src/lib.rs | 101 ++++++++++++++++++++++++++++++++++------ src/parser.rs | 54 ++++++++++++++------- src/printer.rs | 13 ++++++ src/rules/mod.rs | 14 +++++- src/rules/nesting.rs | 74 +++++++++++++++++++++++++++++ src/rules/style.rs | 41 ++++------------ 9 files changed, 309 insertions(+), 71 deletions(-) diff --git a/napi/src/transformer.rs b/napi/src/transformer.rs index de4f5075..29875b87 100644 --- a/napi/src/transformer.rs +++ b/napi/src/transformer.rs @@ -311,6 +311,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Scope(..) => "scope", CssRule::MozDocument(..) => "moz-document", CssRule::Nesting(..) => "nesting", + CssRule::NestedDeclarations(..) => "nested-declarations", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", CssRule::ViewTransition(..) => "view-transition", diff --git a/node/ast.d.ts b/node/ast.d.ts index 5e7ad008..08d9d786 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -61,6 +61,10 @@ export type Rule = | { type: "nesting"; value: NestingRule; } +| { + type: "nested-declarations"; + value: NestedDeclarationsRule; + } | { type: "viewport"; value: ViewportRule; @@ -9523,6 +9527,19 @@ export interface NestingRule { */ style: StyleRule; } +/** + * A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule. + */ +export interface NestedDeclarationsRule { + /** + * The style rule that defines the selector and declarations for the `@nest` rule. + */ + declarations: DeclarationBlock; + /** + * The location of the rule in the source file. + */ + loc: Location2; +} /** * A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule. */ diff --git a/src/declaration.rs b/src/declaration.rs index 2b211999..0dd3da61 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -4,7 +4,7 @@ use std::borrow::Cow; use std::ops::Range; use crate::context::{DeclarationContext, PropertyHandlerContext}; -use crate::error::{ParserError, PrinterError}; +use crate::error::{ParserError, PrinterError, PrinterErrorKind}; use crate::parser::ParserOptions; use crate::printer::Printer; use crate::properties::box_shadow::BoxShadowHandler; @@ -34,6 +34,7 @@ use crate::properties::{ ui::ColorSchemeHandler, }; use crate::properties::{Property, PropertyId}; +use crate::selector::SelectorList; use crate::traits::{PropertyHandler, ToCss}; use crate::values::ident::DashedIdent; use crate::values::string::CowArcStr; @@ -41,6 +42,7 @@ use crate::values::string::CowArcStr; use crate::visitor::Visit; use cssparser::*; use indexmap::IndexMap; +use smallvec::SmallVec; /// A CSS declaration block. /// @@ -157,18 +159,70 @@ impl<'i> DeclarationBlock<'i> { dest.whitespace()?; dest.write_char('{')?; dest.indent(); + dest.newline()?; + + self.to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0)?; + + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } + pub(crate) fn has_printable_declarations(&self) -> bool { + if self.len() > 1 { + return true; + } + + if self.declarations.len() == 1 { + !matches!(self.declarations[0], crate::properties::Property::Composes(_)) + } else if self.important_declarations.len() == 1 { + !matches!(self.important_declarations[0], crate::properties::Property::Composes(_)) + } else { + false + } + } + + /// Writes the declarations to a CSS declaration block. + pub fn to_css_declarations( + &self, + dest: &mut Printer, + has_nested_rules: bool, + selectors: &SelectorList, + source_index: u32, + ) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { let mut i = 0; let len = self.len(); macro_rules! write { ($decls: expr, $important: literal) => { for decl in &$decls { - dest.newline()?; + // The CSS modules `composes` property is handled specially, and omitted during printing. + // We need to add the classes it references to the list for the selectors in this rule. + if let crate::properties::Property::Composes(composes) = &decl { + if dest.is_nested() && dest.css_module.is_some() { + return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc)); + } + + if let Some(css_module) = &mut dest.css_module { + css_module + .handle_composes(&selectors, &composes, source_index) + .map_err(|e| dest.error(e, composes.loc))?; + continue; + } + } + + if i > 0 { + dest.newline()?; + } + decl.to_css(dest, $important)?; - if i != len - 1 || !dest.minify { + if i != len - 1 || !dest.minify || has_nested_rules { dest.write_char(';')?; } + i += 1; } }; @@ -176,10 +230,7 @@ impl<'i> DeclarationBlock<'i> { write!(self.declarations, false); write!(self.important_declarations, true); - - dest.dedent(); - dest.newline()?; - dest.write_char('}') + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 64441fab..86c1500e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24052,13 +24052,13 @@ mod tests { } "#, indoc! {r#" - .foo { - color: red; - } - .foo .bar { color: #00f; } + + .foo { + color: red; + } "#}, ); @@ -24072,12 +24072,16 @@ mod tests { "#, indoc! {r#" article { - color: red; + color: green; } article { color: #00f; } + + article { + color: red; + } "#}, ); @@ -24190,6 +24194,29 @@ mod tests { } "#}, ); + nesting_test( + r#" + .foo { + &::before, &::after { + background: blue; + @media screen { + background: orange; + } + } + } + "#, + indoc! {r#" + .foo:before, .foo:after { + background: #00f; + } + + @media screen { + .foo:before, .foo:after { + background: orange; + } + } + "#}, + ); nesting_test_no_targets( r#" @@ -25225,9 +25252,7 @@ mod tests { indoc! {r#" .EgL3uq_box2 { @container EgL3uq_main (width >= 0) { - & { - background-color: #90ee90; - } + background-color: #90ee90; } } "#}, @@ -25251,9 +25276,7 @@ mod tests { indoc! {r#" .EgL3uq_box2 { @container main (width >= 0) { - & { - background-color: #90ee90; - } + background-color: #90ee90; } } "#}, @@ -25460,6 +25483,56 @@ mod tests { test_project_root("/foo", "/foo/test.css", "EgL3uq"); test_project_root("/foo/bar", "/foo/bar/baz/test.css", "xLEkNW"); test_project_root("/foo", "/foo/baz/test.css", "xLEkNW"); + + let mut stylesheet = StyleSheet::parse( + r#" + .foo { + color: red; + .bar { + color: green; + } + composes: test from "foo.css"; + } + "#, + ParserOptions { + filename: "test.css".into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..Default::default() + }) + .unwrap(); + assert_eq!( + res.code, + indoc! {r#" + .EgL3uq_foo { + color: red; + } + + .EgL3uq_foo .EgL3uq_bar { + color: green; + } + + + "#} + ); + assert_eq!( + res.exports.unwrap(), + map! { + "foo" => "EgL3uq_foo" "test" from "foo.css", + "bar" => "EgL3uq_bar" + } + ); } #[test] @@ -26832,7 +26905,7 @@ mod tests { } } "#, - ".foo{@scope(.bar){&{color:#ff0}}}", + ".foo{@scope(.bar){color:#ff0}}", ); nesting_test( r#" @@ -26844,9 +26917,7 @@ mod tests { "#, indoc! {r#" @scope (.bar) { - :scope { - color: #ff0; - } + color: #ff0; } "#}, ); diff --git a/src/parser.rs b/src/parser.rs index 15b78f33..b5e954bf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,6 +7,7 @@ use crate::rules::container::{ContainerCondition, ContainerName, ContainerRule}; use crate::rules::font_feature_values::FontFeatureValuesRule; use crate::rules::font_palette_values::FontPaletteValuesRule; use crate::rules::layer::{LayerBlockRule, LayerStatementRule}; +use crate::rules::nesting::NestedDeclarationsRule; use crate::rules::property::PropertyRule; use crate::rules::scope::ScopeRule; use crate::rules::starting_style::StartingStyleRule; @@ -31,7 +32,7 @@ use crate::rules::{ unknown::UnknownAtRule, CssRule, CssRuleList, Location, }; -use crate::selector::{Component, SelectorList, SelectorParser}; +use crate::selector::{SelectorList, SelectorParser}; use crate::traits::Parse; use crate::values::ident::{CustomIdent, DashedIdent}; use crate::values::string::CowArcStr; @@ -517,19 +518,13 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o }; // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. - // These act the same way as if they were nested within a `& { ... }` block. + // These are wrapped in an (invisible) NestedDeclarationsRule. let (declarations, mut rules) = self.parse_nested(input, false)?; if declarations.len() > 0 { rules.0.insert( 0, - CssRule::Style(StyleRule { - selectors: Component::Nesting.into(), - declarations, - vendor_prefix: VendorPrefix::empty(), - rules: CssRuleList(vec![]), - loc, - }), + CssRule::NestedDeclarations(NestedDeclarationsRule { declarations, loc }), ) } @@ -1002,13 +997,40 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> cssparser::DeclarationParse name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result> { - parse_declaration( - name, - input, - &mut self.declarations, - &mut self.important_declarations, - &self.options, - ) + if self.rules.0.is_empty() { + parse_declaration( + name, + input, + &mut self.declarations, + &mut self.important_declarations, + &self.options, + ) + } else if let Some(CssRule::NestedDeclarations(last)) = self.rules.0.last_mut() { + parse_declaration( + name, + input, + &mut last.declarations.declarations, + &mut last.declarations.important_declarations, + &self.options, + ) + } else { + let loc = self.loc(&input.state()); + let mut nested = NestedDeclarationsRule { + declarations: DeclarationBlock::new(), + loc, + }; + + parse_declaration( + name, + input, + &mut nested.declarations.declarations, + &mut nested.declarations.important_declarations, + &self.options, + )?; + + self.rules.0.push(CssRule::NestedDeclarations(nested)); + Ok(()) + } } } diff --git a/src/printer.rs b/src/printer.rs index 8b5e8f48..7061485e 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -376,6 +376,19 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { res } + pub(crate) fn with_parent_context) -> Result>( + &mut self, + f: F, + ) -> Result { + let parent = std::mem::take(&mut self.context); + if let Some(parent) = parent { + self.context = parent.parent; + } + let res = f(self); + self.context = parent; + res + } + pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> { self.context.clone() } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 8f815bbe..4413b985 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -90,7 +90,7 @@ use itertools::Itertools; use keyframes::KeyframesRule; use media::MediaRule; use namespace::NamespaceRule; -use nesting::NestingRule; +use nesting::{NestedDeclarationsRule, NestingRule}; use page::PageRule; use scope::ScopeRule; use smallvec::{smallvec, SmallVec}; @@ -164,6 +164,8 @@ pub enum CssRule<'i, R = DefaultAtRule> { MozDocument(MozDocumentRule<'i, R>), /// A `@nest` rule. Nesting(NestingRule<'i, R>), + /// A nested declarations rule. + NestedDeclarations(NestedDeclarationsRule<'i>), /// A `@viewport` rule. Viewport(ViewportRule<'i>), /// A `@custom-media` rule. @@ -298,6 +300,10 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul let rule = NestingRule::deserialize(deserializer)?; Ok(CssRule::Nesting(rule)) } + "nested-declarations" => { + let rule = NestedDeclarationsRule::deserialize(deserializer)?; + Ok(CssRule::NestedDeclarations(rule)) + } "viewport" => { let rule = ViewportRule::deserialize(deserializer)?; Ok(CssRule::Viewport(rule)) @@ -367,6 +373,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { CssRule::Namespace(namespace) => namespace.to_css(dest), CssRule::MozDocument(document) => document.to_css(dest), CssRule::Nesting(nesting) => nesting.to_css(dest), + CssRule::NestedDeclarations(nested) => nested.to_css(dest), CssRule::Viewport(viewport) => viewport.to_css(dest), CssRule::CustomMedia(custom_media) => custom_media.to_css(dest), CssRule::LayerStatement(layer) => layer.to_css(dest), @@ -816,6 +823,11 @@ impl<'i, T: Clone> CssRuleList<'i, T> { continue; } } + CssRule::NestedDeclarations(nested) => { + if nested.minify(context, parent_is_unused) { + continue; + } + } CssRule::StartingStyle(rule) => { if rule.minify(context, parent_is_unused)? { continue; diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index 2473cc2a..679e586a 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -1,14 +1,20 @@ //! The `@nest` rule. +use smallvec::SmallVec; + use super::style::StyleRule; use super::Location; use super::MinifyContext; +use crate::context::DeclarationContext; +use crate::declaration::DeclarationBlock; use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; +use crate::targets::should_compile; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; + /// A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule. #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -47,3 +53,71 @@ impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> { self.style.to_css(dest) } } + +/// A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule. +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub struct NestedDeclarationsRule<'i> { + /// The style rule that defines the selector and declarations for the `@nest` rule. + #[cfg_attr(feature = "serde", serde(borrow))] + pub declarations: DeclarationBlock<'i>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +impl<'i> NestedDeclarationsRule<'i> { + pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool) -> bool { + if parent_is_unused { + return true; + } + + context.handler_context.context = DeclarationContext::StyleRule; + self + .declarations + .minify(context.handler, context.important_handler, &mut context.handler_context); + context.handler_context.context = DeclarationContext::None; + return false; + } +} + +impl<'i> ToCss for NestedDeclarationsRule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + + if should_compile!(dest.targets.current, Nesting) { + if let Some(context) = dest.context() { + let has_printable_declarations = self.declarations.has_printable_declarations(); + if has_printable_declarations { + dest.with_parent_context(|dest| context.selectors.to_css(dest))?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + dest.newline()?; + } + + self + .declarations + .to_css_declarations(dest, false, &context.selectors, self.loc.source_index)?; + + if has_printable_declarations { + dest.dedent(); + dest.newline()?; + dest.write_char('}')?; + } + return Ok(()); + } + } + + self + .declarations + .to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0) + } +} diff --git a/src/rules/style.rs b/src/rules/style.rs index 36a629f6..ce3cd459 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -8,7 +8,7 @@ use super::MinifyContext; use crate::context::DeclarationContext; use crate::declaration::DeclarationBlock; use crate::error::ParserError; -use crate::error::{MinifyError, PrinterError, PrinterErrorKind}; +use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::rules::CssRuleList; @@ -256,39 +256,16 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { dest.whitespace()?; dest.write_char('{')?; dest.indent(); - - let mut i = 0; - macro_rules! write { - ($decls: ident, $important: literal) => { - for decl in &self.declarations.$decls { - // The CSS modules `composes` property is handled specially, and omitted during printing. - // We need to add the classes it references to the list for the selectors in this rule. - if let crate::properties::Property::Composes(composes) = &decl { - if dest.is_nested() && dest.css_module.is_some() { - return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc)); - } - - if let Some(css_module) = &mut dest.css_module { - css_module - .handle_composes(&self.selectors, &composes, self.loc.source_index) - .map_err(|e| dest.error(e, composes.loc))?; - continue; - } - } - - dest.newline()?; - decl.to_css(dest, $important)?; - if i != len - 1 || !dest.minify || (supports_nesting && !self.rules.0.is_empty()) { - dest.write_char(';')?; - } - - i += 1; - } - }; + if len > 0 { + dest.newline()?; } - write!(declarations, false); - write!(important_declarations, true); + self.declarations.to_css_declarations( + dest, + supports_nesting && !self.rules.0.is_empty(), + &self.selectors, + self.loc.source_index, + )?; } macro_rules! newline { From b08d7afcd63494884c619e289c8b7576327663c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Mon, 21 Apr 2025 20:15:52 -0700 Subject: [PATCH 18/75] feat(parser): Improve error recovery of parser (#954) --- src/lib.rs | 52 ++++++++++++++++++ src/media_query.rs | 119 +++++++++++++++++++++++++++++------------ src/parser.rs | 12 ++--- src/rules/container.rs | 42 ++++++++++----- 4 files changed, 174 insertions(+), 51 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 86c1500e..c89bbd76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,7 @@ mod tests { use indoc::indoc; use pretty_assertions::assert_eq; use std::collections::HashMap; + use std::sync::{Arc, RwLock}; fn test(source: &str, expected: &str) { test_with_options(source, expected, ParserOptions::default()) @@ -236,6 +237,22 @@ mod tests { } } + fn error_recovery_test(source: &str) { + let warnings = Arc::new(RwLock::default()); + let res = StyleSheet::parse( + &source, + ParserOptions { + error_recovery: true, + warnings: Some(warnings.clone()), + ..Default::default() + }, + ); + match res { + Ok(..) => {} + Err(e) => unreachable!("parser error should be recovered, but got {e:?}"), + } + } + fn css_modules_error_test(source: &str, error: ParserError) { let res = StyleSheet::parse( &source, @@ -24391,6 +24408,41 @@ mod tests { ); } + #[test] + fn test_nesting_error_recovery() { + error_recovery_test( + " + .container { + padding: 3rem; + @media (max-width: --styled-jsx-placeholder-0__) { + .responsive { + color: purple; + } + } + } + ", + ); + } + + #[test] + fn test_css_variable_error_recovery() { + error_recovery_test(" + .container { + --local-var: --styled-jsx-placeholder-0__; + color: var(--text-color); + background: linear-gradient(to right, --styled-jsx-placeholder-1__, --styled-jsx-placeholder-2__); + + .item { + transform: translate(calc(var(--x) + --styled-jsx-placeholder-3__px), calc(var(--y) + --styled-jsx-placeholder-4__px)); + } + + div { + margin: calc(10px + --styled-jsx-placeholder-5__px); + } + } + "); + } + #[test] fn test_css_modules() { css_modules_test( diff --git a/src/media_query.rs b/src/media_query.rs index dbb42f40..10657c16 100644 --- a/src/media_query.rs +++ b/src/media_query.rs @@ -10,7 +10,7 @@ use crate::rules::custom_media::CustomMediaRule; use crate::rules::Location; use crate::stylesheet::ParserOptions; use crate::targets::{should_compile, Targets}; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::{DashedIdent, Ident}; use crate::values::number::{CSSInteger, CSSNumber}; use crate::values::string::CowArcStr; @@ -51,10 +51,13 @@ impl<'i> MediaList<'i> { } /// Parse a media query list from CSS. - pub fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + pub fn parse<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { let mut media_queries = vec![]; loop { - match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(i)) { + match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse_with_options(i, options)) { Ok(mq) => { media_queries.push(mq); } @@ -269,8 +272,11 @@ pub struct MediaQuery<'i> { pub condition: Option>, } -impl<'i> Parse<'i> for MediaQuery<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { +impl<'i> ParseWithOptions<'i> for MediaQuery<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { let (qualifier, explicit_media_type) = input .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> { let qualifier = input.try_parse(Qualifier::parse).ok(); @@ -280,9 +286,17 @@ impl<'i> Parse<'i> for MediaQuery<'i> { .unwrap_or_default(); let condition = if explicit_media_type.is_none() { - Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::ALLOW_OR)?) + Some(MediaCondition::parse_with_flags( + input, + QueryConditionFlags::ALLOW_OR, + options, + )?) } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { - Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::empty())?) + Some(MediaCondition::parse_with_flags( + input, + QueryConditionFlags::empty(), + options, + )?) } else { None }; @@ -476,8 +490,8 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> { condition, }), MediaQueryOrRaw::Raw { raw } => { - let res = - MediaQuery::parse_string(raw.as_ref()).map_err(|_| serde::de::Error::custom("Could not parse value"))?; + let res = MediaQuery::parse_string_with_options(raw.as_ref(), ParserOptions::default()) + .map_err(|_| serde::de::Error::custom("Could not parse value"))?; Ok(res.into_owned()) } } @@ -524,10 +538,16 @@ pub enum MediaCondition<'i> { /// A trait for conditions such as media queries and container queries. pub(crate) trait QueryCondition<'i>: Sized { - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>>; + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>>; fn create_negation(condition: Box) -> Self; fn create_operation(operator: Operator, conditions: Vec) -> Self; - fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result>> { + fn parse_style_query<'t>( + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, + ) -> Result>> { Err(input.new_error_for_next_token()) } @@ -536,8 +556,11 @@ pub(crate) trait QueryCondition<'i>: Sized { impl<'i> QueryCondition<'i> for MediaCondition<'i> { #[inline] - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>> { - let feature = MediaFeature::parse(input)?; + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + let feature = MediaFeature::parse_with_options(input, options)?; Ok(Self::Feature(feature)) } @@ -576,8 +599,9 @@ impl<'i> MediaCondition<'i> { fn parse_with_flags<'t>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result>> { - parse_query_condition(input, flags) + parse_query_condition(input, flags, options) } fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix { @@ -633,9 +657,12 @@ impl<'i> MediaCondition<'i> { } } -impl<'i> Parse<'i> for MediaCondition<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { - Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR) +impl<'i> ParseWithOptions<'i> for MediaCondition<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR, options) } } @@ -643,6 +670,7 @@ impl<'i> Parse<'i> for MediaCondition<'i> { pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let location = input.current_source_location(); let (is_negation, is_style) = match *input.next()? { @@ -658,15 +686,15 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( let first_condition = match (is_negation, is_style) { (true, false) => { - let inner_condition = parse_parens_or_function(input, flags)?; + let inner_condition = parse_parens_or_function(input, flags, options)?; return Ok(P::create_negation(Box::new(inner_condition))); } (true, true) => { - let inner_condition = P::parse_style_query(input)?; + let inner_condition = P::parse_style_query(input, options)?; return Ok(P::create_negation(Box::new(inner_condition))); } - (false, false) => parse_paren_block(input, flags)?, - (false, true) => P::parse_style_query(input)?, + (false, false) => parse_paren_block(input, flags, options)?, + (false, true) => P::parse_style_query(input, options)?, }; let operator = match input.try_parse(Operator::parse) { @@ -680,7 +708,7 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( let mut conditions = vec![]; conditions.push(first_condition); - conditions.push(parse_parens_or_function(input, flags)?); + conditions.push(parse_parens_or_function(input, flags, options)?); let delim = match operator { Operator::And => "and", @@ -692,7 +720,7 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( return Ok(P::create_operation(operator, conditions)); } - conditions.push(parse_parens_or_function(input, flags)?); + conditions.push(parse_parens_or_function(input, flags, options)?); } } @@ -700,14 +728,15 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let location = input.current_source_location(); match *input.next()? { - Token::ParenthesisBlock => parse_paren_block(input, flags), + Token::ParenthesisBlock => parse_paren_block(input, flags, options), Token::Function(ref f) if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") => { - P::parse_style_query(input) + P::parse_style_query(input, options) } ref t => return Err(location.new_unexpected_token_error(t.clone())), } @@ -716,13 +745,16 @@ fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>( fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result>> { input.parse_nested_block(|input| { - if let Ok(inner) = input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR)) { + if let Ok(inner) = + input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR, options)) + { return Ok(inner); } - P::parse_feature(input) + P::parse_feature(input, options) }) } @@ -924,12 +956,15 @@ pub enum QueryFeature<'i, FeatureId> { /// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature) pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>; -impl<'i, FeatureId> Parse<'i> for QueryFeature<'i, FeatureId> +impl<'i, FeatureId> ParseWithOptions<'i> for QueryFeature<'i, FeatureId> where FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone, { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { - match input.try_parse(Self::parse_name_first) { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + match input.try_parse(|input| Self::parse_name_first(input, options)) { Ok(res) => Ok(res), Err( err @ ParseError { @@ -946,7 +981,10 @@ impl<'i, FeatureId> QueryFeature<'i, FeatureId> where FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone, { - fn parse_name_first<'t>(input: &mut Parser<'i, 't>) -> Result>> { + fn parse_name_first<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { let (name, legacy_op) = MediaFeatureName::parse(input)?; let operator = input.try_parse(|input| consume_operation_or_colon(input, true)); @@ -956,16 +994,26 @@ where }; if operator.is_some() && legacy_op.is_some() { + dbg!(); return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } let value = MediaFeatureValue::parse(input, name.value_type())?; if !value.check_type(name.value_type()) { - return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); + if options.error_recovery { + options.warn(ParseError { + kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery), + location: input.current_source_location(), + }); + } else { + return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); + } } if let Some(operator) = operator.or(legacy_op) { if !name.value_type().allows_ranges() { + dbg!(); + return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } @@ -981,11 +1029,15 @@ where let name = loop { if let Ok((name, legacy_op)) = MediaFeatureName::parse(input) { if legacy_op.is_some() { + dbg!(); + return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } break name; } if input.is_exhausted() { + dbg!(); + return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } }; @@ -1003,6 +1055,7 @@ where } if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) { + dbg!(); return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } @@ -1802,7 +1855,7 @@ mod tests { fn parse(s: &str) -> MediaQuery { let mut input = ParserInput::new(&s); let mut parser = Parser::new(&mut input); - MediaQuery::parse(&mut parser).unwrap() + MediaQuery::parse_with_options(&mut parser, &ParserOptions::default()).unwrap() } fn and(a: &str, b: &str) -> String { diff --git a/src/parser.rs b/src/parser.rs index b5e954bf..af93d978 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -33,7 +33,7 @@ use crate::rules::{ CssRule, CssRuleList, Location, }; use crate::selector::{SelectorList, SelectorParser}; -use crate::traits::Parse; +use crate::traits::{Parse, ParseWithOptions}; use crate::values::ident::{CustomIdent, DashedIdent}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -295,7 +295,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLev } else { None }; - let media = MediaList::parse(input)?; + let media = MediaList::parse(input, &self.options)?; return Ok(AtRulePrelude::Import(url_string, media, supports, layer)); }, "namespace" => { @@ -317,7 +317,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLev }, "custom-media" if self.options.flags.contains(ParserFlags::CUSTOM_MEDIA) => { let name = DashedIdent::parse(input)?; - let media = MediaList::parse(input)?; + let media = MediaList::parse(input, &self.options)?; return Ok(AtRulePrelude::CustomMedia(name, media)) }, "property" => { @@ -553,11 +553,11 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne ) -> Result> { let result = match_ignore_ascii_case! { &*name, "media" => { - let media = MediaList::parse(input)?; + let media = MediaList::parse(input, &self.options)?; AtRulePrelude::Media(media) }, "supports" => { - let cond = SupportsCondition::parse(input)?; + let cond = SupportsCondition::parse(input, )?; AtRulePrelude::Supports(cond) }, "font-face" => { @@ -645,7 +645,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne }, "container" => { let name = input.try_parse(ContainerName::parse).ok(); - let condition = ContainerCondition::parse(input)?; + let condition = ContainerCondition::parse_with_options(input, &self.options)?; AtRulePrelude::Container(name, condition) }, "starting-style" => { diff --git a/src/rules/container.rs b/src/rules/container.rs index 75378c3c..a3ac87fe 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -9,13 +9,13 @@ use crate::media_query::{ define_query_features, operation_to_css, parse_query_condition, to_css_with_parens_if_needed, FeatureToCss, MediaFeatureType, Operator, QueryCondition, QueryConditionFlags, QueryFeature, ValueType, }; -use crate::parser::DefaultAtRule; +use crate::parser::{DefaultAtRule, ParserOptions}; use crate::printer::Printer; use crate::properties::{Property, PropertyId}; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; use crate::targets::{Features, Targets}; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::CustomIdent; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -135,8 +135,11 @@ pub enum StyleQuery<'i> { impl<'i> QueryCondition<'i> for ContainerCondition<'i> { #[inline] - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>> { - let feature = QueryFeature::parse(input)?; + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + let feature = QueryFeature::parse_with_options(input, options)?; Ok(Self::Feature(feature)) } @@ -150,13 +153,18 @@ impl<'i> QueryCondition<'i> for ContainerCondition<'i> { Self::Operation { operator, conditions } } - fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result>> { + fn parse_style_query<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { input.parse_nested_block(|input| { - if let Ok(res) = input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR)) { + if let Ok(res) = + input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR, options)) + { return Ok(Self::Style(res)); } - Ok(Self::Style(StyleQuery::parse_feature(input)?)) + Ok(Self::Style(StyleQuery::parse_feature(input, options)?)) }) } @@ -172,11 +180,14 @@ impl<'i> QueryCondition<'i> for ContainerCondition<'i> { impl<'i> QueryCondition<'i> for StyleQuery<'i> { #[inline] - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>> { + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { let property_id = PropertyId::parse(input)?; if input.try_parse(|input| input.expect_colon()).is_ok() { input.skip_whitespace(); - let feature = Self::Declaration(Property::parse(property_id, input, &Default::default())?); + let feature = Self::Declaration(Property::parse(property_id, input, options)?); let _ = input.try_parse(|input| parse_important(input)); Ok(feature) } else { @@ -203,9 +214,16 @@ impl<'i> QueryCondition<'i> for StyleQuery<'i> { } } -impl<'i> Parse<'i> for ContainerCondition<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { - parse_query_condition(input, QueryConditionFlags::ALLOW_OR | QueryConditionFlags::ALLOW_STYLE) +impl<'i> ParseWithOptions<'i> for ContainerCondition<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + parse_query_condition( + input, + QueryConditionFlags::ALLOW_OR | QueryConditionFlags::ALLOW_STYLE, + options, + ) } } From 241df3a82b9779cb0df3bf01cd99871a2498405a Mon Sep 17 00:00:00 2001 From: hardfist Date: Tue, 22 Apr 2025 11:46:25 +0800 Subject: [PATCH 19/75] fix: upgrade browserslist (#961) --- Cargo.lock | 21 +++++++++++++++++++-- Cargo.toml | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c4e0557..a55c3e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "browserslist-rs" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95aff901882c66e4b642f3f788ceee152ef44f8a5ef12cb1ddee5479c483be" +dependencies = [ + "ahash 0.8.11", + "chrono", + "either", + "indexmap 2.7.0", + "itertools 0.13.0", + "nom", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "bstr" version = "1.11.1" @@ -736,7 +753,7 @@ dependencies = [ "assert_fs", "atty", "bitflags 2.6.0", - "browserslist-rs", + "browserslist-rs 0.18.1", "clap", "const-str", "cssparser", @@ -794,7 +811,7 @@ dependencies = [ name = "lightningcss_c_bindings" version = "0.1.0" dependencies = [ - "browserslist-rs", + "browserslist-rs 0.17.0", "cbindgen", "lightningcss", "parcel_sourcemap", diff --git a/Cargo.toml b/Cargo.toml index 3c87e362..3f4eef4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ indexmap = { version = "2.2.6", features = ["serde"] } # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } -browserslist-rs = { version = "0.17.0", optional = true } +browserslist-rs = { version = "0.18.1", optional = true } rayon = { version = "1.5.1", optional = true } dashmap = { version = "5.0.0", optional = true } serde_json = { version = "1.0.78", optional = true } From c5cbcfa4aa952d1d662551f85af972495249c24c Mon Sep 17 00:00:00 2001 From: Lucas Weng <30640930+lucasweng@users.noreply.github.com> Date: Sat, 26 Apr 2025 00:21:00 +0800 Subject: [PATCH 20/75] Fix linear-gradient direction conversion for legacy vendor-prefixed values (#936) --- src/lib.rs | 399 ++++++++++++++++++++++++++++++++++++++--- src/values/gradient.rs | 86 ++++++++- 2 files changed, 453 insertions(+), 32 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c89bbd76..3def1948 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2195,7 +2195,7 @@ mod tests { indoc! {r#" .foo { -webkit-border-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 60; - -webkit-border-image: -webkit-linear-gradient(#ff0f0e, #7773ff) 60; + -webkit-border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60; border-image: linear-gradient(#ff0f0e, #7773ff) 60; border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60; } @@ -2216,8 +2216,8 @@ mod tests { indoc! {r#" .foo { -webkit-border-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 60; - -webkit-border-image: -webkit-linear-gradient(#ff0f0e, #7773ff) 60; - -moz-border-image: -moz-linear-gradient(#ff0f0e, #7773ff) 60; + -webkit-border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60; + -moz-border-image: -moz-linear-gradient(top, #ff0f0e, #7773ff) 60; border-image: linear-gradient(#ff0f0e, #7773ff) 60; border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60; } @@ -2238,8 +2238,8 @@ mod tests { "#, indoc! {r#" .foo { - border-image: -webkit-linear-gradient(#ff0f0e, #7773ff) 60; - border-image: -moz-linear-gradient(#ff0f0e, #7773ff) 60; + border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60; + border-image: -moz-linear-gradient(top, #ff0f0e, #7773ff) 60; border-image: linear-gradient(#ff0f0e, #7773ff) 60; border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60; } @@ -2260,7 +2260,7 @@ mod tests { "#, indoc! {r#" .foo { - border-image-source: -webkit-linear-gradient(#ff0f0e, #7773ff); + border-image-source: -webkit-linear-gradient(top, #ff0f0e, #7773ff); border-image-source: linear-gradient(#ff0f0e, #7773ff); border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -12978,7 +12978,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f)); - background-image: -webkit-linear-gradient(red, #00f); + background-image: -webkit-linear-gradient(top, red, #00f); background-image: linear-gradient(red, #00f); } "#}, @@ -12996,7 +12996,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 0 0, 100% 0, from(red), to(#00f)); - background-image: -webkit-linear-gradient(right, red, #00f); + background-image: -webkit-linear-gradient(left, red, #00f); background-image: linear-gradient(to right, red, #00f); } "#}, @@ -13014,7 +13014,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 0 100%, 0 0, from(red), to(#00f)); - background-image: -webkit-linear-gradient(top, red, #00f); + background-image: -webkit-linear-gradient(red, #00f); background-image: linear-gradient(to top, red, #00f); } "#}, @@ -13032,7 +13032,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 100% 0, 0 0, from(red), to(#00f)); - background-image: -webkit-linear-gradient(left, red, #00f); + background-image: -webkit-linear-gradient(right, red, #00f); background-image: linear-gradient(to left, red, #00f); } "#}, @@ -13050,7 +13050,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 100% 0, 0 100%, from(red), to(#00f)); - background-image: -webkit-linear-gradient(bottom left, red, #00f); + background-image: -webkit-linear-gradient(top right, red, #00f); background-image: linear-gradient(to bottom left, red, #00f); } "#}, @@ -13068,7 +13068,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 0 100%, 100% 0, from(red), to(#00f)); - background-image: -webkit-linear-gradient(top right, red, #00f); + background-image: -webkit-linear-gradient(bottom left, red, #00f); background-image: linear-gradient(to top right, red, #00f); } "#}, @@ -13086,7 +13086,7 @@ mod tests { indoc! {r#" .foo { background-image: -webkit-gradient(linear, 0 0, 100% 0, from(red), to(#00f)); - background-image: -webkit-linear-gradient(90deg, red, #00f); + background-image: -webkit-linear-gradient(0deg, red, #00f); background-image: linear-gradient(90deg, red, #00f); } "#}, @@ -13120,7 +13120,7 @@ mod tests { "#, indoc! {r#" .foo { - background-image: -webkit-linear-gradient(red, #00f); + background-image: -webkit-linear-gradient(top, red, #00f); background-image: linear-gradient(red, #00f); } "#}, @@ -13244,9 +13244,9 @@ mod tests { indoc! {r#" .foo { background: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0), to(red)), url("bg.jpg"); - background: -webkit-radial-gradient(red, #00f), -webkit-linear-gradient(#ff0, red), url("bg.jpg"); - background: -moz-radial-gradient(red, #00f), -moz-linear-gradient(#ff0, red), url("bg.jpg"); - background: -o-radial-gradient(red, #00f), -o-linear-gradient(#ff0, red), url("bg.jpg"); + background: -webkit-radial-gradient(red, #00f), -webkit-linear-gradient(top, #ff0, red), url("bg.jpg"); + background: -moz-radial-gradient(red, #00f), -moz-linear-gradient(top, #ff0, red), url("bg.jpg"); + background: -o-radial-gradient(red, #00f), -o-linear-gradient(top, #ff0, red), url("bg.jpg"); background: radial-gradient(red, #00f), linear-gradient(#ff0, red), url("bg.jpg"); } "#}, @@ -13326,7 +13326,7 @@ mod tests { ".foo { background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", indoc! { r#" .foo { - background: -webkit-linear-gradient(#ff0f0e, #7773ff); + background: -webkit-linear-gradient(top, #ff0f0e, #7773ff); background: linear-gradient(#ff0f0e, #7773ff); background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -13342,7 +13342,7 @@ mod tests { indoc! { r#" .foo { background: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)); - background: -webkit-linear-gradient(#ff0f0e, #7773ff); + background: -webkit-linear-gradient(top, #ff0f0e, #7773ff); background: linear-gradient(#ff0f0e, #7773ff); background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -13413,7 +13413,7 @@ mod tests { ".foo { background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", indoc! { r#" .foo { - background-image: -webkit-linear-gradient(#ff0f0e, #7773ff); + background-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff); background-image: linear-gradient(#ff0f0e, #7773ff); background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -13429,7 +13429,7 @@ mod tests { indoc! { r#" .foo { background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)); - background-image: -webkit-linear-gradient(#ff0f0e, #7773ff); + background-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff); background-image: linear-gradient(#ff0f0e, #7773ff); background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -13465,6 +13465,357 @@ mod tests { ..Browsers::default() }, ); + + // Test cases from https://github.com/postcss/autoprefixer/blob/541295c0e6dd348db2d3f52772b59cd403c59d29/test/cases/gradient.css + prefix_test( + r#" + a { + background: linear-gradient(350.5deg, white, black), linear-gradient(-130deg, black, white), linear-gradient(45deg, black, white); + } + b { + background-image: linear-gradient(rgba(0,0,0,1), white), linear-gradient(white, black); + } + strong { + background: linear-gradient(to top, transparent, rgba(0, 0, 0, 0.8) 20px, #000 30px, #000) no-repeat; + } + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + .simple1 { + background: linear-gradient(black, white); + } + .simple2 { + background: linear-gradient(to left, black 0%, rgba(0, 0, 0, 0.5)50%, white 100%); + } + .simple3 { + background: linear-gradient(to left, black 50%, white 100%); + } + .simple4 { + background: linear-gradient(to right top, black, white); + } + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, 0.5), white); + } + .silent { + background: -webkit-linear-gradient(top left, black, white); + } + .radial { + background: radial-gradient(farthest-side at 0 50%, white, black); + } + .second { + background: red linear-gradient(red, blue); + background: url('logo.png'), linear-gradient(#fff, #000); + } + .px { + background: linear-gradient(black 0, white 100px); + } + .list { + list-style-image: linear-gradient(white, black); + } + .mask { + mask: linear-gradient(white, black); + } + .newline { + background-image: + linear-gradient( white, black ), + linear-gradient( black, white ); + } + .convert { + background: linear-gradient(0deg, white, black); + background: linear-gradient(90deg, white, black); + background: linear-gradient(180deg, white, black); + background: linear-gradient(270deg, white, black); + } + .grad { + background: linear-gradient(1grad, white, black); + } + .rad { + background: linear-gradient(1rad, white, black); + } + .turn { + background: linear-gradient(0.3turn, white, black); + } + .norm { + background: linear-gradient(-90deg, white, black); + } + .mask { + mask-image: radial-gradient(circle at 86% 86%, transparent 8px, black 8px); + } + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + .contain { + background: radial-gradient(contain at center, white, black); + } + .no-div { + background: linear-gradient(black); + } + .background-shorthand { + background: radial-gradient(#FFF, transparent) 0 0 / cover no-repeat #F0F; + } + .background-advanced { + background: radial-gradient(ellipse farthest-corner at 5px 15px, rgba(214, 168, 18, 0.7) 0%, rgba(255, 21, 177, 0.7) 50%, rgba(210, 7, 148, 0.7) 95%), + radial-gradient(#FFF, transparent), + url(path/to/image.jpg) 50%/cover; + } + .multiradial { + mask-image: radial-gradient(circle closest-corner at 100% 50%, #000, transparent); + } + .broken { + mask-image: radial-gradient(white, black); + } + .loop { + background-image: url("https://test.com/lol(test.png"), radial-gradient(yellow, black, yellow); + } + .unitless-zero { + background-image: linear-gradient(0, green, blue); + background: repeating-linear-gradient(0, blue, red 33.3%) + } + .zero-grad { + background: linear-gradient(0grad, green, blue); + background-image: repeating-linear-gradient(0grad, blue, red 33.3%) + } + .zero-rad { + background: linear-gradient(0rad, green, blue); + } + .zero-turn { + background: linear-gradient(0turn, green, blue); + } + "#, + indoc! { r#" + a { + background: -webkit-linear-gradient(99.5deg, #fff, #000), -webkit-linear-gradient(220deg, #000, #fff), -webkit-linear-gradient(45deg, #000, #fff); + background: -o-linear-gradient(99.5deg, #fff, #000), -o-linear-gradient(220deg, #000, #fff), -o-linear-gradient(45deg, #000, #fff); + background: linear-gradient(350.5deg, #fff, #000), linear-gradient(-130deg, #000, #fff), linear-gradient(45deg, #000, #fff); + } + + b { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)), -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + background-image: -webkit-linear-gradient(top, #000, #fff), -webkit-linear-gradient(top, #fff, #000); + background-image: -o-linear-gradient(top, #000, #fff), -o-linear-gradient(top, #fff, #000); + background-image: linear-gradient(#000, #fff), linear-gradient(#fff, #000); + } + + strong { + background: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: -o-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + } + + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + + .simple1 { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background: -webkit-linear-gradient(top, #000, #fff); + background: -o-linear-gradient(top, #000, #fff); + background: linear-gradient(#000, #fff); + } + + .simple2 { + background: -webkit-gradient(linear, 100% 0, 0 0, from(#000), color-stop(.5, rgba(0, 0, 0, .5)), to(#fff)); + background: -webkit-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: -o-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: linear-gradient(to left, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + } + + .simple3 { + background: -webkit-gradient(linear, 100% 0, 0 0, color-stop(.5, #000), to(#fff)); + background: -webkit-linear-gradient(right, #000 50%, #fff 100%); + background: -o-linear-gradient(right, #000 50%, #fff 100%); + background: linear-gradient(to left, #000 50%, #fff 100%); + } + + .simple4 { + background: -webkit-gradient(linear, 0 100%, 100% 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(bottom left, #000, #fff); + background: -o-linear-gradient(bottom left, #000, #fff); + background: linear-gradient(to top right, #000, #fff); + } + + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, .5), white); + } + + .silent { + background: -webkit-gradient(linear, 100% 100%, 0 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(top left, #000, #fff); + } + + .radial { + background: -webkit-radial-gradient(farthest-side at 0, #fff, #000); + background: -o-radial-gradient(farthest-side at 0, #fff, #000); + background: radial-gradient(farthest-side at 0, #fff, #000); + } + + .second { + background: red -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f)); + background: red -webkit-linear-gradient(top, red, #00f); + background: red -o-linear-gradient(top, red, #00f); + background: red linear-gradient(red, #00f); + background: url("logo.png"), linear-gradient(#fff, #000); + } + + .px { + background: -webkit-linear-gradient(top, #000 0, #fff 100px); + background: -o-linear-gradient(top, #000 0, #fff 100px); + background: linear-gradient(#000 0, #fff 100px); + } + + .list { + list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + list-style-image: -webkit-linear-gradient(top, #fff, #000); + list-style-image: -o-linear-gradient(top, #fff, #000); + list-style-image: linear-gradient(#fff, #000); + } + + .mask { + -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + -webkit-mask: -webkit-linear-gradient(top, #fff, #000); + -webkit-mask: -o-linear-gradient(top, #fff, #000); + mask: -o-linear-gradient(top, #fff, #000); + -webkit-mask: linear-gradient(#fff, #000); + mask: linear-gradient(#fff, #000); + } + + .newline { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)), -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background-image: -webkit-linear-gradient(top, #fff, #000), -webkit-linear-gradient(top, #000, #fff); + background-image: -o-linear-gradient(top, #fff, #000), -o-linear-gradient(top, #000, #fff); + background-image: linear-gradient(#fff, #000), linear-gradient(#000, #fff); + } + + .convert { + background: -webkit-gradient(linear, 0 100%, 0 0, from(#fff), to(#000)); + background: -webkit-linear-gradient(90deg, #fff, #000); + background: -o-linear-gradient(90deg, #fff, #000); + background: linear-gradient(0deg, #fff, #000); + background: linear-gradient(90deg, #fff, #000); + background: linear-gradient(#fff, #000); + background: linear-gradient(270deg, #fff, #000); + } + + .grad { + background: -webkit-linear-gradient(89.1deg, #fff, #000); + background: -o-linear-gradient(89.1deg, #fff, #000); + background: linear-gradient(1grad, #fff, #000); + } + + .rad { + background: -webkit-linear-gradient(32.704deg, #fff, #000); + background: -o-linear-gradient(32.704deg, #fff, #000); + background: linear-gradient(57.2958deg, #fff, #000); + } + + .turn { + background: -webkit-linear-gradient(342deg, #fff, #000); + background: -o-linear-gradient(342deg, #fff, #000); + background: linear-gradient(.3turn, #fff, #000); + } + + .norm { + background: -webkit-linear-gradient(#fff, #000); + background: -o-linear-gradient(#fff, #000); + background: linear-gradient(-90deg, #fff, #000); + } + + .mask { + -webkit-mask-image: -webkit-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + } + + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + + .contain { + background: radial-gradient(contain at center, white, black); + } + + .no-div { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000)); + background: -webkit-linear-gradient(top, #000); + background: -o-linear-gradient(top, #000); + background: linear-gradient(#000); + } + + .background-shorthand { + background: #f0f -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f -o-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + } + + .background-advanced { + background: url("path/to/image.jpg") 50% / cover; + background: -webkit-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: -o-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -o-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + } + + .multiradial { + -webkit-mask-image: -webkit-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + } + + .broken { + -webkit-mask-image: -webkit-radial-gradient(#fff, #000); + -webkit-mask-image: -o-radial-gradient(#fff, #000); + mask-image: -o-radial-gradient(#fff, #000); + -webkit-mask-image: radial-gradient(#fff, #000); + mask-image: radial-gradient(#fff, #000); + } + + .loop { + background-image: url("https://test.com/lol(test.png"); + background-image: url("https://test.com/lol(test.png"), -webkit-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), -o-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), radial-gradient(#ff0, #000, #ff0); + } + + .unitless-zero { + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background-image: -webkit-linear-gradient(90deg, green, #00f); + background-image: -o-linear-gradient(90deg, green, #00f); + background-image: linear-gradient(0deg, green, #00f); + background: repeating-linear-gradient(0deg, #00f, red 33.3%); + } + + .zero-grad { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0grad, green, #00f); + background-image: repeating-linear-gradient(0grad, #00f, red 33.3%); + } + + .zero-rad, .zero-turn { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0deg, green, #00f); + } + "#}, + Browsers { + chrome: Some(25 << 16), + opera: Some(12 << 16), + android: Some(2 << 16 | 3 << 8), + ..Browsers::default() + }, + ); } #[test] @@ -16872,7 +17223,7 @@ mod tests { indoc! { r#" .foo { list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)); - list-style-image: -webkit-linear-gradient(#ff0f0e, #7773ff); + list-style-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff); list-style-image: linear-gradient(#ff0f0e, #7773ff); list-style-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -26179,7 +26530,7 @@ mod tests { indoc! { r#" .foo { -webkit-mask-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)); - -webkit-mask-image: -webkit-linear-gradient(#ff0f0e, #7773ff); + -webkit-mask-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff); -webkit-mask-image: linear-gradient(#ff0f0e, #7773ff); mask-image: linear-gradient(#ff0f0e, #7773ff); -webkit-mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); @@ -26241,7 +26592,7 @@ mod tests { indoc! { r#" .foo { -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 40px 20px; - -webkit-mask: -webkit-linear-gradient(#ff0f0e, #7773ff) 40px 20px; + -webkit-mask: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 40px 20px; -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 40px 20px; mask: linear-gradient(#ff0f0e, #7773ff) 40px 20px; -webkit-mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px; diff --git a/src/values/gradient.rs b/src/values/gradient.rs index 6ee45f52..22d748eb 100644 --- a/src/values/gradient.rs +++ b/src/values/gradient.rs @@ -18,6 +18,7 @@ use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; +use std::f32::consts::PI; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; @@ -83,14 +84,24 @@ impl Gradient { /// Returns a copy of the gradient with the given vendor prefix. pub fn get_prefixed(&self, prefix: VendorPrefix) -> Gradient { match self { - Gradient::Linear(linear) => Gradient::Linear(LinearGradient { - vendor_prefix: prefix, - ..linear.clone() - }), - Gradient::RepeatingLinear(linear) => Gradient::RepeatingLinear(LinearGradient { - vendor_prefix: prefix, - ..linear.clone() - }), + Gradient::Linear(linear) => { + let mut new_linear = linear.clone(); + let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None; + if needs_legacy_direction { + new_linear.direction = convert_to_legacy_direction(&new_linear.direction); + } + new_linear.vendor_prefix = prefix; + Gradient::Linear(new_linear) + } + Gradient::RepeatingLinear(linear) => { + let mut new_linear = linear.clone(); + let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None; + if needs_legacy_direction { + new_linear.direction = convert_to_legacy_direction(&new_linear.direction); + } + new_linear.vendor_prefix = prefix; + Gradient::RepeatingLinear(new_linear) + } Gradient::Radial(radial) => Gradient::Radial(RadialGradient { vendor_prefix: prefix, ..radial.clone() @@ -530,6 +541,65 @@ impl LineDirection { } } +/// Converts a standard gradient direction to its legacy vendor-prefixed form. +/// +/// Inverts keyword-based directions (e.g., `to bottom` → `top`) for compatibility +/// with legacy prefixed syntaxes. +/// +/// See: https://github.com/parcel-bundler/lightningcss/issues/918 +fn convert_to_legacy_direction(direction: &LineDirection) -> LineDirection { + match direction { + LineDirection::Horizontal(HorizontalPositionKeyword::Left) => { + LineDirection::Horizontal(HorizontalPositionKeyword::Right) + } + LineDirection::Horizontal(HorizontalPositionKeyword::Right) => { + LineDirection::Horizontal(HorizontalPositionKeyword::Left) + } + LineDirection::Vertical(VerticalPositionKeyword::Top) => { + LineDirection::Vertical(VerticalPositionKeyword::Bottom) + } + LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { + LineDirection::Vertical(VerticalPositionKeyword::Top) + } + LineDirection::Corner { horizontal, vertical } => LineDirection::Corner { + horizontal: match horizontal { + HorizontalPositionKeyword::Left => HorizontalPositionKeyword::Right, + HorizontalPositionKeyword::Right => HorizontalPositionKeyword::Left, + }, + vertical: match vertical { + VerticalPositionKeyword::Top => VerticalPositionKeyword::Bottom, + VerticalPositionKeyword::Bottom => VerticalPositionKeyword::Top, + }, + }, + LineDirection::Angle(angle) => { + let angle = angle.clone(); + let deg = match angle { + Angle::Deg(n) => convert_to_legacy_degree(n), + Angle::Rad(n) => { + let n = n / (2.0 * PI) * 360.0; + convert_to_legacy_degree(n) + } + Angle::Grad(n) => { + let n = n / 400.0 * 360.0; + convert_to_legacy_degree(n) + } + Angle::Turn(n) => { + let n = n * 360.0; + convert_to_legacy_degree(n) + } + }; + LineDirection::Angle(Angle::Deg(deg)) + } + } +} + +fn convert_to_legacy_degree(degree: f32) -> f32 { + // Add 90 degrees + let n = (450.0 - degree).abs() % 360.0; + // Round the number to 3 decimal places + (n * 1000.0).round() / 1000.0 +} + /// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape). /// /// See [RadialGradient](RadialGradient). From 3e27005b012d885d531371a3815d0ad2b1599b0f Mon Sep 17 00:00:00 2001 From: Lucas Weng <30640930+lucasweng@users.noreply.github.com> Date: Sat, 26 Apr 2025 00:21:25 +0800 Subject: [PATCH 21/75] Add support for `::picker`, `::picker-icon` and `::checkmark` (#957) --- scripts/build-prefixes.js | 3 +++ selectors/parser.rs | 6 ++++++ src/compat.rs | 29 +++++++++++++++++++++++++++++ src/selector.rs | 23 +++++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js index 32951a63..029fa54a 100644 --- a/scripts/build-prefixes.js +++ b/scripts/build-prefixes.js @@ -334,6 +334,9 @@ let mdnFeatures = { viewTransition: mdn.css.selectors['view-transition'].__compat.support, detailsContent: mdn.css.selectors['details-content'].__compat.support, targetText: mdn.css.selectors['target-text'].__compat.support, + picker: mdn.css.selectors.picker.__compat.support, + pickerIcon: mdn.css.selectors['picker-icon'].__compat.support, + checkmark: mdn.css.selectors.checkmark.__compat.support, }; for (let key in mdn.css.types.length) { diff --git a/selectors/parser.rs b/selectors/parser.rs index 85c118bc..ed7b97f1 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -3932,6 +3932,12 @@ pub mod tests { assert!(parse("foo::details-content").is_ok()); assert!(parse("foo::target-text").is_ok()); + + assert!(parse("select::picker").is_err()); + assert!(parse("::picker()").is_err()); + assert!(parse("::picker(select)").is_ok()); + assert!(parse("select::picker-icon").is_ok()); + assert!(parse("option::checkmark").is_ok()); } #[test] diff --git a/src/compat.rs b/src/compat.rs index 4c237c4e..fb6dd6d2 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -28,6 +28,7 @@ pub enum Feature { CapUnit, CaseInsensitive, ChUnit, + Checkmark, CircleListStyleType, CjkDecimalListStyleType, CjkEarthlyBranchListStyleType, @@ -158,6 +159,8 @@ pub enum Feature { P3Colors, PartPseudo, PersianListStyleType, + Picker, + PickerIcon, PlaceContent, PlaceItems, PlaceSelf, @@ -3596,6 +3599,32 @@ impl Feature { return false; } } + Feature::Picker | Feature::PickerIcon | Feature::Checkmark => { + if let Some(version) = browsers.chrome { + if version < 8781824 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8781824 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8781824 { + return false; + } + } + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.opera.is_some() + || browsers.safari.is_some() + || browsers.samsung.is_some() + { + return false; + } + } Feature::QUnit => { if let Some(version) = browsers.chrome { if version < 4128768 { diff --git a/src/selector.rs b/src/selector.rs index 28cb1836..d5011076 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -292,6 +292,9 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "-webkit-scrollbar-corner" => WebKitScrollbar(WebKitScrollbarPseudoElement::Corner), "-webkit-resizer" => WebKitScrollbar(WebKitScrollbarPseudoElement::Resizer), + "picker-icon" => PickerIcon, + "checkmark" => Checkmark, + "view-transition" => ViewTransition, _ => { @@ -314,6 +317,7 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, let pseudo_element = match_ignore_ascii_case! { &name, "cue" => CueFunction { selector: Box::new(Selector::parse(self, arguments)?) }, "cue-region" => CueRegionFunction { selector: Box::new(Selector::parse(self, arguments)?) }, + "picker" => PickerFunction { identifier: Ident::parse(arguments)? }, "view-transition-group" => ViewTransitionGroup { part: ViewTransitionPartSelector::parse(arguments)? }, "view-transition-image-pair" => ViewTransitionImagePair { part: ViewTransitionPartSelector::parse(arguments)? }, "view-transition-old" => ViewTransitionOld { part: ViewTransitionPartSelector::parse(arguments)? }, @@ -953,6 +957,15 @@ pub enum PseudoElement<'i> { /// A part name selector. part: ViewTransitionPartSelector<'i>, }, + /// The [::picker()](https://drafts.csswg.org/css-forms-1/#the-picker-pseudo-element) functional pseudo element. + PickerFunction { + /// A form control identifier. + identifier: Ident<'i>, + }, + /// The [::picker-icon](https://drafts.csswg.org/css-forms-1/#picker-opener-icon-the-picker-icon-pseudo-element) pseudo element. + PickerIcon, + /// The [::checkmark](https://drafts.csswg.org/css-forms-1/#styling-checkmarks-the-checkmark-pseudo-element) pseudo element. + Checkmark, /// An unknown pseudo element. Custom { /// The name of the pseudo element. @@ -1213,6 +1226,13 @@ where part.to_css(dest)?; dest.write_char(')') } + PickerFunction { identifier } => { + dest.write_str("::picker(")?; + identifier.to_css(dest)?; + dest.write_char(')') + } + PickerIcon => dest.write_str("::picker-icon"), + Checkmark => dest.write_str("::checkmark"), Custom { name: val } => { dest.write_str("::")?; return dest.write_str(val); @@ -1924,6 +1944,9 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { | PseudoElement::ViewTransitionOld { .. } | PseudoElement::ViewTransitionGroup { .. } | PseudoElement::ViewTransitionImagePair { .. } => Feature::ViewTransition, + PseudoElement::PickerFunction { identifier: _ } => Feature::Picker, + PseudoElement::PickerIcon => Feature::PickerIcon, + PseudoElement::Checkmark => Feature::Checkmark, PseudoElement::Custom { name: _ } | _ => return false, }, From 496f4f6843150165e467026151504003b9c1e1f2 Mon Sep 17 00:00:00 2001 From: ozip <41739417+ozipoetra@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:16:12 +0700 Subject: [PATCH 22/75] Add build support for Android (#932) --- .github/workflows/release.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2eeb96a..95d30431 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,6 +109,9 @@ jobs: - target: aarch64-unknown-linux-gnu strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + - target: aarch64-linux-android + strip: llvm-strip + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - target: armv7-unknown-linux-gnueabihf strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57 @@ -133,6 +136,14 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Setup Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} + run: | + sudo apt update && sudo apt install unzip -y + cd /tmp + wget -q https://dl.google.com/android/repository/android-ndk-r28-linux.zip -O /tmp/ndk.zip + unzip ndk.zip + - name: Setup cross compile toolchain if: ${{ matrix.setup }} run: ${{ matrix.setup }} @@ -144,8 +155,11 @@ jobs: - name: Build release run: yarn build-release env: + ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 RUST_TARGET: ${{ matrix.target }} - name: Build CLI + env: + ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 run: | yarn napi build --bin lightningcss --release --features cli --target ${{ matrix.target }} mv target/${{ matrix.target }}/release/lightningcss lightningcss From 16fdfd5906e8982ecaf099e3990cbbc056b2e74c Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Sun, 11 May 2025 08:26:31 +0900 Subject: [PATCH 23/75] fix: remove grid feature (#972) --- Cargo.toml | 3 +-- src/lib.rs | 4 ---- src/properties/mod.rs | 32 -------------------------------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3f4eef4e..29d24d8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,10 @@ path = "src/lib.rs" crate-type = ["rlib"] [features] -default = ["bundler", "grid", "nodejs", "sourcemap"] +default = ["bundler", "nodejs", "sourcemap"] browserslist = ["browserslist-rs"] bundler = ["dashmap", "sourcemap", "rayon"] cli = ["atty", "clap", "serde_json", "browserslist", "jemallocator"] -grid = [] jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema"] nodejs = ["dep:serde"] serde = [ diff --git a/src/lib.rs b/src/lib.rs index 3def1948..43581946 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21690,7 +21690,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -24982,7 +24981,6 @@ mod tests { false, ); - #[cfg(feature = "grid")] css_modules_test( r#" body { @@ -25027,7 +25025,6 @@ mod tests { false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -25066,7 +25063,6 @@ mod tests { false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 00667a30..fb552773 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -104,7 +104,6 @@ pub mod display; pub mod effects; pub mod flex; pub mod font; -#[cfg(feature = "grid")] pub mod grid; pub mod list; pub(crate) mod margin_padding; @@ -154,7 +153,6 @@ use display::*; use effects::*; use flex::*; use font::*; -#[cfg(feature = "grid")] use grid::*; use list::*; use margin_padding::*; @@ -1372,50 +1370,20 @@ define_properties! { "flex-negative": FlexNegative(CSSNumber, VendorPrefix) / Ms unprefixed: false, "flex-preferred-size": FlexPreferredSize(LengthPercentageOrAuto, VendorPrefix) / Ms unprefixed: false, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template-columns": GridTemplateColumns(TrackSizing<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template-rows": GridTemplateRows(TrackSizing<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-auto-columns": GridAutoColumns(TrackSizeList), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-auto-rows": GridAutoRows(TrackSizeList), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-auto-flow": GridAutoFlow(GridAutoFlow), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template-areas": GridTemplateAreas(GridTemplateAreas), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template": GridTemplate(GridTemplate<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid": Grid(Grid<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-row-start": GridRowStart(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-row-end": GridRowEnd(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-column-start": GridColumnStart(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-column-end": GridColumnEnd(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-row": GridRow(GridRow<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-column": GridColumn(GridColumn<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-area": GridArea(GridArea<'i>) shorthand: true, "margin-top": MarginTop(LengthPercentageOrAuto) [logical_group: Margin, category: Physical], From 0f064abacbe0076d162d3f18ce685baf680117f2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 10 May 2025 19:42:52 -0400 Subject: [PATCH 24/75] Prevent new lines written by `write_str` from breaking source maps (#971) --- src/lib.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/printer.rs | 19 +++++++++++++++++++ src/stylesheet.rs | 4 ++-- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 43581946..96b640f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28738,6 +28738,48 @@ mod tests { ); } + #[test] + #[cfg(feature = "sourcemap")] + fn test_source_maps_with_license_comments() { + let source = r#"/*! a single line comment */ + /*! + a comment + containing + multiple + lines + */ + .a { + display: flex; + } + + .b { + display: hidden; + } + "#; + + let mut sm = parcel_sourcemap::SourceMap::new("/"); + let source_index = sm.add_source("input.css"); + sm.set_source_content(source_index as usize, source).unwrap(); + + let mut stylesheet = StyleSheet::parse(&source, ParserOptions { + source_index, + ..Default::default() + }).unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + stylesheet + .to_css(PrinterOptions { + source_map: Some(&mut sm), + minify: true, + ..PrinterOptions::default() + }) + .unwrap(); + let map = sm.to_json(None).unwrap(); + assert_eq!( + map, + r#"{"version":3,"sourceRoot":null,"mappings":";;;;;;;AAOI,gBAIA","sources":["input.css"],"sourcesContent":["/*! a single line comment */\n /*!\n a comment\n containing\n multiple\n lines\n */\n .a {\n display: flex;\n }\n\n .b {\n display: hidden;\n }\n "],"names":[]}"# + ); + } + #[test] fn test_error_recovery() { use std::sync::{Arc, RwLock}; diff --git a/src/printer.rs b/src/printer.rs index 7061485e..b231fbec 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -146,6 +146,25 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { Ok(()) } + /// Writes a raw string which may contain newlines to the underlying destination. + pub fn write_str_with_newlines(&mut self, s: &str) -> Result<(), PrinterError> { + let mut last_line_start: usize = 0; + + for (idx, n) in s.char_indices() { + if n == '\n' { + self.line += 1; + self.col = 0; + + // Keep track of where the *next* line starts + last_line_start = idx + 1; + } + } + + self.col += (s.len() - last_line_start) as u32; + self.dest.write_str(s)?; + Ok(()) + } + /// Write a single character to the underlying destination. pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> { if c == '\n' { diff --git a/src/stylesheet.rs b/src/stylesheet.rs index dcf87f1c..990a09be 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -287,8 +287,8 @@ where for comment in &self.license_comments { printer.write_str("/*")?; - printer.write_str(comment)?; - printer.write_str("*/\n")?; + printer.write_str_with_newlines(comment)?; + printer.write_str_with_newlines("*/\n")?; } if let Some(config) = &self.options.css_modules { From 4ebcb45600ce4a15db0d7c4b3861013eb0adf077 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 10 May 2025 22:43:35 -0700 Subject: [PATCH 25/75] Update relative color parsing to latest spec (#465) --- src/lib.rs | 158 +++++++++---- src/properties/custom.rs | 29 +-- src/values/angle.rs | 17 +- src/values/calc.rs | 35 +-- src/values/color.rs | 396 ++++++++++++++++++++------------- src/values/length.rs | 2 +- src/values/percentage.rs | 24 +- src/values/time.rs | 10 +- website/pages/transpilation.md | 4 +- 9 files changed, 423 insertions(+), 252 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 96b640f2..04cb0e49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17514,6 +17514,7 @@ mod tests { minify_test(".foo { color: hsl(100deg, 100%, 50%) }", ".foo{color:#5f0}"); minify_test(".foo { color: hsl(100, 100%, 50%) }", ".foo{color:#5f0}"); minify_test(".foo { color: hsl(100 100% 50%) }", ".foo{color:#5f0}"); + minify_test(".foo { color: hsl(100 100 50) }", ".foo{color:#5f0}"); minify_test(".foo { color: hsl(100, 100%, 50%, .8) }", ".foo{color:#5f0c}"); minify_test(".foo { color: hsl(100 100% 50% / .8) }", ".foo{color:#5f0c}"); minify_test(".foo { color: hsla(100, 100%, 50%, .8) }", ".foo{color:#5f0c}"); @@ -17525,12 +17526,21 @@ mod tests { minify_test(".foo { color: hwb(194 0% 0% / 50%) }", ".foo{color:#00c4ff80}"); minify_test(".foo { color: hwb(194 0% 50%) }", ".foo{color:#006280}"); minify_test(".foo { color: hwb(194 50% 0%) }", ".foo{color:#80e1ff}"); + minify_test(".foo { color: hwb(194 50 0) }", ".foo{color:#80e1ff}"); minify_test(".foo { color: hwb(194 50% 50%) }", ".foo{color:gray}"); // minify_test(".foo { color: ActiveText }", ".foo{color:ActiveTet}"); minify_test( ".foo { color: lab(29.2345% 39.3825 20.0664); }", ".foo{color:lab(29.2345% 39.3825 20.0664)}", ); + minify_test( + ".foo { color: lab(29.2345 39.3825 20.0664); }", + ".foo{color:lab(29.2345% 39.3825 20.0664)}", + ); + minify_test( + ".foo { color: lab(29.2345% 39.3825% 20.0664%); }", + ".foo{color:lab(29.2345% 49.2281 25.083)}", + ); minify_test( ".foo { color: lab(29.2345% 39.3825 20.0664 / 100%); }", ".foo{color:lab(29.2345% 39.3825 20.0664)}", @@ -17543,6 +17553,14 @@ mod tests { ".foo { color: lch(29.2345% 44.2 27); }", ".foo{color:lch(29.2345% 44.2 27)}", ); + minify_test( + ".foo { color: lch(29.2345 44.2 27); }", + ".foo{color:lch(29.2345% 44.2 27)}", + ); + minify_test( + ".foo { color: lch(29.2345% 44.2% 27deg); }", + ".foo{color:lch(29.2345% 66.3 27)}", + ); minify_test( ".foo { color: lch(29.2345% 44.2 45deg); }", ".foo{color:lch(29.2345% 44.2 45)}", @@ -17563,10 +17581,26 @@ mod tests { ".foo { color: oklab(40.101% 0.1147 0.0453); }", ".foo{color:oklab(40.101% .1147 .0453)}", ); + minify_test( + ".foo { color: oklab(.40101 0.1147 0.0453); }", + ".foo{color:oklab(40.101% .1147 .0453)}", + ); + minify_test( + ".foo { color: oklab(40.101% 0.1147% 0.0453%); }", + ".foo{color:oklab(40.101% .0004588 .0001812)}", + ); minify_test( ".foo { color: oklch(40.101% 0.12332 21.555); }", ".foo{color:oklch(40.101% .12332 21.555)}", ); + minify_test( + ".foo { color: oklch(.40101 0.12332 21.555); }", + ".foo{color:oklch(40.101% .12332 21.555)}", + ); + minify_test( + ".foo { color: oklch(40.101% 0.12332% 21.555); }", + ".foo{color:oklch(40.101% .00049328 21.555)}", + ); minify_test( ".foo { color: oklch(40.101% 0.12332 .5turn); }", ".foo{color:oklch(40.101% .12332 180)}", @@ -18431,7 +18465,7 @@ mod tests { } test("lab(from indianred calc(l * .8) a b)", "lab(43.1402% 45.7516 23.1557)"); - test("lch(from indianred calc(l + 10%) c h)", "lch(63.9252% 51.2776 26.8448)"); + test("lch(from indianred calc(l + 10) c h)", "lch(63.9252% 51.2776 26.8448)"); test("lch(from indianred l calc(c - 50) h)", "lch(53.9252% 1.27763 26.8448)"); test( "lch(from indianred l c calc(h + 180deg))", @@ -18447,12 +18481,23 @@ mod tests { "rgba(205, 92, 92, .7)", ); test( - "rgb(from rgba(205, 92, 92, .5) r g b / calc(alpha + 20%))", + "rgb(from rgba(205, 92, 92, .5) r g b / calc(alpha + .2))", "rgba(205, 92, 92, .7)", ); test("lch(from indianred l sin(c) h)", "lch(53.9252% .84797 26.8448)"); test("lch(from indianred l sqrt(c) h)", "lch(53.9252% 7.16084 26.8448)"); - test("lch(from indianred l c sin(h))", "lch(53.9252% 51.2776 .990043)"); + test("lch(from indianred l c sin(h))", "lch(53.9252% 51.2776 .451575)"); + test("lch(from indianred calc(10% + 20%) c h)", "lch(30% 51.2776 26.8448)"); + test("lch(from indianred calc(10 + 20) c h)", "lch(30% 51.2776 26.8448)"); + test("lch(from indianred l c calc(10 + 20))", "lch(53.9252% 51.2776 30)"); + test( + "lch(from indianred l c calc(10deg + 20deg))", + "lch(53.9252% 51.2776 30)", + ); + test( + "lch(from indianred l c calc(10deg + 0.35rad))", + "lch(53.9252% 51.2776 30.0535)", + ); minify_test( ".foo{color:lch(from currentColor l c sin(h))}", ".foo{color:lch(from currentColor l c sin(h))}", @@ -18566,21 +18611,15 @@ mod tests { // Testing permutation. test("rgb(from rebeccapurple g b r)", "rgb(51, 153, 102)"); - test("rgb(from rebeccapurple b alpha r / g)", "rgba(153, 255, 102, 0.2)"); - test("rgb(from rebeccapurple r r r / r)", "rgba(102, 102, 102, 0.4)"); - test( - "rgb(from rebeccapurple alpha alpha alpha / alpha)", - "rgb(255, 255, 255)", - ); + test("rgb(from rebeccapurple b alpha r / g)", "rgba(153, 1, 102, 1)"); + test("rgb(from rebeccapurple r r r / r)", "rgba(102, 102, 102, 1)"); + test("rgb(from rebeccapurple alpha alpha alpha / alpha)", "rgb(1, 1, 1)"); test("rgb(from rgb(20%, 40%, 60%, 80%) g b r)", "rgb(102, 153, 51)"); - test( - "rgb(from rgb(20%, 40%, 60%, 80%) b alpha r / g)", - "rgba(153, 204, 51, 0.4)", - ); - test("rgb(from rgb(20%, 40%, 60%, 80%) r r r / r)", "rgba(51, 51, 51, 0.2)"); + test("rgb(from rgb(20%, 40%, 60%, 80%) b alpha r / g)", "rgba(153, 1, 51, 1)"); + test("rgb(from rgb(20%, 40%, 60%, 80%) r r r / r)", "rgba(51, 51, 51, 1)"); test( "rgb(from rgb(20%, 40%, 60%, 80%) alpha alpha alpha / alpha)", - "rgba(204, 204, 204, 0.8)", + "rgba(1, 1, 1, 0.8)", ); // Testing mixes of number and percentage. (These would not be allowed in the non-relative syntax). @@ -18704,17 +18743,29 @@ mod tests { // Testing valid permutation (types match). test("hsl(from rebeccapurple h l s)", "rgb(128, 77, 179)"); - test("hsl(from rebeccapurple h alpha l / s)", "rgba(102, 0, 204, 0.5)"); - test("hsl(from rebeccapurple h l l / l)", "rgba(102, 61, 143, 0.4)"); - test("hsl(from rebeccapurple h alpha alpha / alpha)", "rgb(255, 255, 255)"); + test( + "hsl(from rebeccapurple h calc(alpha * 100) l / calc(s / 100))", + "rgba(102, 0, 204, 0.5)", + ); + test( + "hsl(from rebeccapurple h l l / calc(l / 100))", + "rgba(102, 61, 143, 0.4)", + ); + test( + "hsl(from rebeccapurple h calc(alpha * 100) calc(alpha * 100) / calc(alpha * 100))", + "rgb(255, 255, 255)", + ); test("hsl(from rgb(20%, 40%, 60%, 80%) h l s)", "rgb(77, 128, 179)"); test( - "hsl(from rgb(20%, 40%, 60%, 80%) h alpha l / s)", + "hsl(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) l / calc(s / 100))", "rgba(20, 102, 184, 0.5)", ); - test("hsl(from rgb(20%, 40%, 60%, 80%) h l l / l)", "rgba(61, 102, 143, 0.4)"); test( - "hsl(from rgb(20%, 40%, 60%, 80%) h alpha alpha / alpha)", + "hsl(from rgb(20%, 40%, 60%, 80%) h l l / calc(l / 100))", + "rgba(61, 102, 143, 0.4)", + ); + test( + "hsl(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) calc(alpha * 100) / alpha)", "rgba(163, 204, 245, 0.8)", ); @@ -18842,17 +18893,29 @@ mod tests { // Testing valid permutation (types match). test("hwb(from rebeccapurple h b w)", "rgb(153, 102, 204)"); - test("hwb(from rebeccapurple h alpha w / b)", "rgba(213, 213, 213, 0.4)"); - test("hwb(from rebeccapurple h w w / w)", "rgba(128, 51, 204, 0.2)"); - test("hwb(from rebeccapurple h alpha alpha / alpha)", "rgb(128, 128, 128)"); + test( + "hwb(from rebeccapurple h calc(alpha * 100) w / calc(b / 100))", + "rgba(213, 213, 213, 0.4)", + ); + test( + "hwb(from rebeccapurple h w w / calc(w / 100))", + "rgba(128, 51, 204, 0.2)", + ); + test( + "hwb(from rebeccapurple h calc(alpha * 100) calc(alpha * 100) / alpha)", + "rgb(128, 128, 128)", + ); test("hwb(from rgb(20%, 40%, 60%, 80%) h b w)", "rgb(102, 153, 204)"); test( - "hwb(from rgb(20%, 40%, 60%, 80%) h alpha w / b)", + "hwb(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) w / calc(b / 100))", "rgba(204, 204, 204, 0.4)", ); - test("hwb(from rgb(20%, 40%, 60%, 80%) h w w / w)", "rgba(51, 128, 204, 0.2)"); test( - "hwb(from rgb(20%, 40%, 60%, 80%) h alpha alpha / alpha)", + "hwb(from rgb(20%, 40%, 60%, 80%) h w w / calc(w / 100))", + "rgba(51, 128, 204, 0.2)", + ); + test( + "hwb(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) calc(alpha * 100) / alpha)", "rgba(128, 128, 128, 0.8)", ); @@ -19305,7 +19368,11 @@ mod tests { // NOTE: 'c' is a valid hue, as hue is |. test( &format!("{}(from {}(70% 45 30) alpha c h / l)", color_space, color_space), - &format!("{}(100% 45 30 / 0.7)", color_space), + &format!( + "{}(1 45 30 / {})", + color_space, + if *color_space == "lch" { "1" } else { ".7" } + ), ); test( &format!("{}(from {}(70% 45 30) l c c / alpha)", color_space, color_space), @@ -19313,15 +19380,19 @@ mod tests { ); test( &format!("{}(from {}(70% 45 30) alpha c h / alpha)", color_space, color_space), - &format!("{}(100% 45 30)", color_space), + &format!("{}(1 45 30)", color_space), ); test( &format!("{}(from {}(70% 45 30) alpha c c / alpha)", color_space, color_space), - &format!("{}(100% 45 45)", color_space), + &format!("{}(1 45 45)", color_space), ); test( &format!("{}(from {}(70% 45 30 / 40%) alpha c h / l)", color_space, color_space), - &format!("{}(40% 45 30 / 0.7)", color_space), + &format!( + "{}(.4 45 30 / {})", + color_space, + if *color_space == "lch" { "1" } else { ".7" } + ), ); test( &format!("{}(from {}(70% 45 30 / 40%) l c c / alpha)", color_space, color_space), @@ -19332,14 +19403,14 @@ mod tests { "{}(from {}(70% 45 30 / 40%) alpha c h / alpha)", color_space, color_space ), - &format!("{}(40% 45 30 / 0.4)", color_space), + &format!("{}(.4 45 30 / 0.4)", color_space), ); test( &format!( "{}(from {}(70% 45 30 / 40%) alpha c c / alpha)", color_space, color_space ), - &format!("{}(40% 45 45 / 0.4)", color_space), + &format!("{}(.4 45 45 / 0.4)", color_space), ); // Testing with calc(). @@ -20201,13 +20272,10 @@ mod tests { ".foo{color:hsl(from rebeccapurple s h l)}", ".foo{color:hsl(from rebeccapurple s h l)}", ); + minify_test(".foo{color:hsl(from rebeccapurple s s s / s)}", ".foo{color:#bfaa40}"); minify_test( - ".foo{color:hsl(from rebeccapurple s s s / s)}", - ".foo{color:hsl(from rebeccapurple s s s/s)}", - ); - minify_test( - ".foo{color:hsl(from rebeccapurple alpha alpha alpha / alpha)}", - ".foo{color:hsl(from rebeccapurple alpha alpha alpha/alpha)}", + ".foo{color:hsl(from rebeccapurple calc(alpha * 100) calc(alpha * 100) calc(alpha * 100) / alpha)}", + ".foo{color:#fff}", ); } } @@ -28761,10 +28829,14 @@ mod tests { let source_index = sm.add_source("input.css"); sm.set_source_content(source_index as usize, source).unwrap(); - let mut stylesheet = StyleSheet::parse(&source, ParserOptions { - source_index, - ..Default::default() - }).unwrap(); + let mut stylesheet = StyleSheet::parse( + &source, + ParserOptions { + source_index, + ..Default::default() + }, + ) + .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); stylesheet .to_css(PrinterOptions { diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 824bc5b7..83a29273 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -12,7 +12,7 @@ use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::angle::Angle; use crate::values::color::{ parse_hsl_hwb_components, parse_rgb_components, ColorFallbackKind, ComponentParser, CssColor, LightDarkColor, - HSL, RGBA, SRGB, + HSL, RGB, RGBA, }; use crate::values::ident::{CustomIdent, DashedIdent, DashedIdentReference, Ident}; use crate::values::length::{serialize_dimension, LengthValue}; @@ -1594,7 +1594,7 @@ impl<'i> UnresolvedColor<'i> { match_ignore_ascii_case! { &*f, "rgb" => { input.parse_nested_block(|input| { - parser.parse_relative::(input, |input, parser| { + parser.parse_relative::(input, |input, parser| { let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?; if is_legacy { return Err(input.new_custom_error(ParserError::InvalidValue)) @@ -1636,20 +1636,15 @@ impl<'i> UnresolvedColor<'i> { where W: std::fmt::Write, { - #[inline] - fn c(c: &f32) -> i32 { - (c * 255.0).round().clamp(0.0, 255.0) as i32 - } - match self { UnresolvedColor::RGB { r, g, b, alpha } => { if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) { dest.write_str("rgba(")?; - c(r).to_css(dest)?; + r.to_css(dest)?; dest.delim(',', false)?; - c(g).to_css(dest)?; + g.to_css(dest)?; dest.delim(',', false)?; - c(b).to_css(dest)?; + b.to_css(dest)?; dest.delim(',', false)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')')?; @@ -1657,11 +1652,11 @@ impl<'i> UnresolvedColor<'i> { } dest.write_str("rgb(")?; - c(r).to_css(dest)?; + r.to_css(dest)?; dest.write_char(' ')?; - c(g).to_css(dest)?; + g.to_css(dest)?; dest.write_char(' ')?; - c(b).to_css(dest)?; + b.to_css(dest)?; dest.delim('/', true)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')') @@ -1671,9 +1666,9 @@ impl<'i> UnresolvedColor<'i> { dest.write_str("hsla(")?; h.to_css(dest)?; dest.delim(',', false)?; - Percentage(*s).to_css(dest)?; + Percentage(*s / 100.0).to_css(dest)?; dest.delim(',', false)?; - Percentage(*l).to_css(dest)?; + Percentage(*l / 100.0).to_css(dest)?; dest.delim(',', false)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')')?; @@ -1683,9 +1678,9 @@ impl<'i> UnresolvedColor<'i> { dest.write_str("hsl(")?; h.to_css(dest)?; dest.write_char(' ')?; - Percentage(*s).to_css(dest)?; + Percentage(*s / 100.0).to_css(dest)?; dest.write_char(' ')?; - Percentage(*l).to_css(dest)?; + Percentage(*l / 100.0).to_css(dest)?; dest.delim('/', true)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')') diff --git a/src/values/angle.rs b/src/values/angle.rs index a1bc6a00..dff23a28 100644 --- a/src/values/angle.rs +++ b/src/values/angle.rs @@ -183,11 +183,13 @@ impl Into> for Angle { } } -impl From> for Angle { - fn from(calc: Calc) -> Angle { +impl TryFrom> for Angle { + type Error = (); + + fn try_from(calc: Calc) -> Result { match calc { - Calc::Value(v) => *v, - _ => unreachable!(), + Calc::Value(v) => Ok(*v), + _ => Err(()), } } } @@ -285,6 +287,13 @@ macro_rules! impl_try_from_angle { Err(()) } } + + impl TryInto for $t { + type Error = (); + fn try_into(self) -> Result { + Err(()) + } + } }; } diff --git a/src/values/calc.rs b/src/values/calc.rs index 22303967..6022cf2f 100644 --- a/src/values/calc.rs +++ b/src/values/calc.rs @@ -314,8 +314,9 @@ impl< + TrySign + std::cmp::PartialOrd + Into> - + From> + + TryFrom> + TryFrom + + TryInto + Clone + std::fmt::Debug, > Parse<'i> for Calc @@ -335,8 +336,9 @@ impl< + TrySign + std::cmp::PartialOrd + Into> - + From> + + TryFrom> + TryFrom + + TryInto + Clone + std::fmt::Debug, > Calc @@ -550,12 +552,12 @@ impl< match *input.next()? { Token::Delim('+') => { let next = Calc::parse_product(input, parse_ident)?; - cur = cur.add(next); + cur = cur.add(next).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?; } Token::Delim('-') => { let mut rhs = Calc::parse_product(input, parse_ident)?; rhs = rhs * -1.0; - cur = cur.add(rhs); + cur = cur.add(rhs).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?; } ref t => { let t = t.clone(); @@ -744,9 +746,12 @@ impl< ) -> Result>> { input.parse_nested_block(|input| { let v: Calc = Calc::parse_sum(input, |v| { - parse_ident(v).and_then(|v| match v { - Calc::Number(v) => Some(Calc::Number(v)), - _ => None, + parse_ident(v).and_then(|v| -> Option> { + match v { + Calc::Number(v) => Some(Calc::Number(v)), + Calc::Value(v) => (*v).try_into().ok().map(|v| Calc::Value(Box::new(v))), + _ => None, + } }) })?; let rad = match v { @@ -903,11 +908,9 @@ impl> std::ops::Mul for Calc { } } -impl> + std::convert::From> + std::fmt::Debug> AddInternal - for Calc -{ - fn add(self, other: Calc) -> Calc { - match (self, other) { +impl> + std::convert::TryFrom> + std::fmt::Debug> Calc { + pub(crate) fn add(self, other: Calc) -> Result, >>::Error> { + Ok(match (self, other) { (Calc::Value(a), Calc::Value(b)) => (a.add(*b)).into(), (Calc::Number(a), Calc::Number(b)) => Calc::Number(a + b), (Calc::Sum(a, b), Calc::Number(c)) => { @@ -934,10 +937,10 @@ impl> + std::convert::From> | (a, b @ Calc::Product(..)) => Calc::Sum(Box::new(a), Box::new(b)), (Calc::Function(a), b) => Calc::Sum(Box::new(Calc::Function(a)), Box::new(b)), (a, Calc::Function(b)) => Calc::Sum(Box::new(a), Box::new(Calc::Function(b))), - (Calc::Value(a), b) => (a.add(V::from(b))).into(), - (a, Calc::Value(b)) => (V::from(a).add(*b)).into(), - (a @ Calc::Sum(..), b @ Calc::Sum(..)) => V::from(a).add(V::from(b)).into(), - } + (Calc::Value(a), b) => (a.add(V::try_from(b)?)).into(), + (a, Calc::Value(b)) => (V::try_from(a)?.add(*b)).into(), + (a @ Calc::Sum(..), b @ Calc::Sum(..)) => V::try_from(a)?.add(V::try_from(b)?).into(), + }) } } diff --git a/src/values/color.rs b/src/values/color.rs index 3edf8447..b21eb196 100644 --- a/src/values/color.rs +++ b/src/values/color.rs @@ -101,7 +101,7 @@ impl CurrentColor { #[serde(tag = "type", rename_all = "lowercase")] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] enum RGBColor { - RGB(SRGB), + RGB(RGB), } #[cfg(feature = "serde")] @@ -222,7 +222,7 @@ pub enum PredefinedColor { #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum FloatColor { /// An RGB color. - RGB(SRGB), + RGB(RGB), /// An HSL color. HSL(HSL), /// An HWB color. @@ -617,16 +617,16 @@ impl ToCss for CssColor { Ok(()) } CssColor::LAB(lab) => match &**lab { - LABColor::LAB(lab) => write_components("lab", lab.l, lab.a, lab.b, lab.alpha, dest), - LABColor::LCH(lch) => write_components("lch", lch.l, lch.c, lch.h, lch.alpha, dest), + LABColor::LAB(lab) => write_components("lab", lab.l / 100.0, lab.a, lab.b, lab.alpha, dest), + LABColor::LCH(lch) => write_components("lch", lch.l / 100.0, lch.c, lch.h, lch.alpha, dest), LABColor::OKLAB(lab) => write_components("oklab", lab.l, lab.a, lab.b, lab.alpha, dest), LABColor::OKLCH(lch) => write_components("oklch", lch.l, lch.c, lch.h, lch.alpha, dest), }, CssColor::Predefined(predefined) => write_predefined(predefined, dest), CssColor::Float(float) => { // Serialize as hex. - let srgb = SRGB::from(**float); - CssColor::from(srgb).to_css(dest) + let rgb = RGB::from(**float); + CssColor::from(rgb).to_css(dest) } CssColor::LightDark(light, dark) => { if should_compile!(dest.targets.current, LightDark) { @@ -718,21 +718,23 @@ impl RelativeComponentParser { } } - fn get_ident(&self, ident: &str, allowed_types: ChannelType) -> Option { + fn get_ident(&self, ident: &str, allowed_types: ChannelType) -> Option<(f32, ChannelType)> { if ident.eq_ignore_ascii_case(self.names.0) && allowed_types.intersects(self.types.0) { - return Some(self.components.0); + return Some((self.components.0, self.types.0)); } if ident.eq_ignore_ascii_case(self.names.1) && allowed_types.intersects(self.types.1) { - return Some(self.components.1); + return Some((self.components.1, self.types.1)); } if ident.eq_ignore_ascii_case(self.names.2) && allowed_types.intersects(self.types.2) { - return Some(self.components.2); + return Some((self.components.2, self.types.2)); } - if ident.eq_ignore_ascii_case("alpha") && allowed_types.intersects(ChannelType::Percentage) { - return Some(self.components.3); + if ident.eq_ignore_ascii_case("alpha") + && allowed_types.intersects(ChannelType::Number | ChannelType::Percentage) + { + return Some((self.components.3, ChannelType::Number)); } None @@ -742,24 +744,12 @@ impl RelativeComponentParser { &self, input: &mut Parser<'i, 't>, allowed_types: ChannelType, - ) -> Result>> { + ) -> Result<(f32, ChannelType), ParseError<'i, ParserError<'i>>> { match self.get_ident(input.expect_ident()?.as_ref(), allowed_types) { Some(v) => Ok(v), None => Err(input.new_error_for_next_token()), } } - - fn parse_calc<'i, 't>( - &self, - input: &mut Parser<'i, 't>, - allowed_types: ChannelType, - ) -> Result>> { - match Calc::parse_with(input, |ident| self.get_ident(ident, allowed_types).map(Calc::Number)) { - Ok(Calc::Value(v)) => Ok(*v), - Ok(Calc::Number(n)) => Ok(n), - _ => Err(input.new_custom_error(ParserError::InvalidValue)), - } - } } impl<'i> ColorParser<'i> for RelativeComponentParser { @@ -770,46 +760,55 @@ impl<'i> ColorParser<'i> for RelativeComponentParser { &self, input: &mut Parser<'i, 't>, ) -> Result> { - if let Ok(value) = input.try_parse(|input| self.parse_ident(input, ChannelType::Angle | ChannelType::Number)) { - return Ok(AngleOrNumber::Number { value }); - } - - if let Ok(value) = input.try_parse(|input| self.parse_calc(input, ChannelType::Angle | ChannelType::Number)) { - return Ok(AngleOrNumber::Number { value }); + if let Ok((value, ty)) = + input.try_parse(|input| self.parse_ident(input, ChannelType::Angle | ChannelType::Number)) + { + return Ok(match ty { + ChannelType::Angle => AngleOrNumber::Angle { degrees: value }, + ChannelType::Number => AngleOrNumber::Number { value }, + _ => unreachable!(), + }); } - if let Ok(value) = input.try_parse(|input| -> Result>> { + if let Ok(value) = input.try_parse(|input| -> Result>> { match Calc::parse_with(input, |ident| { self .get_ident(ident, ChannelType::Angle | ChannelType::Number) - .map(|v| Calc::Value(Box::new(Angle::Deg(v)))) + .map(|(value, ty)| match ty { + ChannelType::Angle => Calc::Value(Box::new(Angle::Deg(value))), + ChannelType::Number => Calc::Number(value), + _ => unreachable!(), + }) }) { - Ok(Calc::Value(v)) => Ok(*v), + Ok(Calc::Value(v)) => Ok(AngleOrNumber::Angle { + degrees: v.to_degrees(), + }), + Ok(Calc::Number(v)) => Ok(AngleOrNumber::Number { value: v }), _ => Err(input.new_custom_error(ParserError::InvalidValue)), } }) { - return Ok(AngleOrNumber::Angle { - degrees: value.to_degrees(), - }); + return Ok(value); } Err(input.new_error_for_next_token()) } fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result> { - if let Ok(value) = input.try_parse(|input| self.parse_ident(input, ChannelType::Number)) { + if let Ok((value, _)) = input.try_parse(|input| self.parse_ident(input, ChannelType::Number)) { return Ok(value); } - if let Ok(value) = input.try_parse(|input| self.parse_calc(input, ChannelType::Number)) { - return Ok(value); + match Calc::parse_with(input, |ident| { + self.get_ident(ident, ChannelType::Number).map(|(v, _)| Calc::Number(v)) + }) { + Ok(Calc::Value(v)) => Ok(*v), + Ok(Calc::Number(n)) => Ok(n), + _ => Err(input.new_error_for_next_token()), } - - Err(input.new_error_for_next_token()) } fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result> { - if let Ok(value) = input.try_parse(|input| self.parse_ident(input, ChannelType::Percentage)) { + if let Ok((value, _)) = input.try_parse(|input| self.parse_ident(input, ChannelType::Percentage)) { return Ok(value); } @@ -817,7 +816,7 @@ impl<'i> ColorParser<'i> for RelativeComponentParser { match Calc::parse_with(input, |ident| { self .get_ident(ident, ChannelType::Percentage) - .map(|v| Calc::Value(Box::new(Percentage(v)))) + .map(|(v, _)| Calc::Value(Box::new(Percentage(v)))) }) { Ok(Calc::Value(v)) => Ok(*v), _ => Err(input.new_custom_error(ParserError::InvalidValue)), @@ -833,29 +832,32 @@ impl<'i> ColorParser<'i> for RelativeComponentParser { &self, input: &mut Parser<'i, 't>, ) -> Result> { - if let Ok(value) = + if let Ok((value, ty)) = input.try_parse(|input| self.parse_ident(input, ChannelType::Percentage | ChannelType::Number)) { - return Ok(NumberOrPercentage::Percentage { unit_value: value }); - } - - if let Ok(value) = - input.try_parse(|input| self.parse_calc(input, ChannelType::Percentage | ChannelType::Number)) - { - return Ok(NumberOrPercentage::Percentage { unit_value: value }); + return Ok(match ty { + ChannelType::Percentage => NumberOrPercentage::Percentage { unit_value: value }, + ChannelType::Number => NumberOrPercentage::Number { value }, + _ => unreachable!(), + }); } - if let Ok(value) = input.try_parse(|input| -> Result>> { + if let Ok(value) = input.try_parse(|input| -> Result>> { match Calc::parse_with(input, |ident| { self .get_ident(ident, ChannelType::Percentage | ChannelType::Number) - .map(|v| Calc::Value(Box::new(Percentage(v)))) + .map(|(value, ty)| match ty { + ChannelType::Percentage => Calc::Value(Box::new(Percentage(value))), + ChannelType::Number => Calc::Number(value), + _ => unreachable!(), + }) }) { - Ok(Calc::Value(v)) => Ok(*v), + Ok(Calc::Value(v)) => Ok(NumberOrPercentage::Percentage { unit_value: v.0 }), + Ok(Calc::Number(v)) => Ok(NumberOrPercentage::Number { value: v }), _ => Err(input.new_custom_error(ParserError::InvalidValue)), } }) { - return Ok(NumberOrPercentage::Percentage { unit_value: value.0 }); + return Ok(value); } Err(input.new_error_for_next_token()) @@ -1026,22 +1028,22 @@ fn parse_color_function<'i, 't>( match_ignore_ascii_case! {&*function, "lab" => { - parse_lab::(input, &mut parser, |l, a, b, alpha| { + parse_lab::(input, &mut parser, 100.0, 125.0, |l, a, b, alpha| { LABColor::LAB(LAB { l, a, b, alpha }) }) }, "oklab" => { - parse_lab::(input, &mut parser, |l, a, b, alpha| { + parse_lab::(input, &mut parser, 1.0, 0.4, |l, a, b, alpha| { LABColor::OKLAB(OKLAB { l, a, b, alpha }) }) }, "lch" => { - parse_lch::(input, &mut parser, |l, c, h, alpha| { + parse_lch::(input, &mut parser, 100.0, 150.0, |l, c, h, alpha| { LABColor::LCH(LCH { l, c, h, alpha }) }) }, "oklch" => { - parse_lch::(input, &mut parser, |l, c, h, alpha| { + parse_lch::(input, &mut parser, 1.0, 0.4, |l, c, h, alpha| { LABColor::OKLCH(OKLCH { l, c, h, alpha }) }) }, @@ -1100,15 +1102,17 @@ fn parse_color_function<'i, 't>( fn parse_lab<'i, 't, T: TryFrom + ColorSpace, F: Fn(f32, f32, f32, f32) -> LABColor>( input: &mut Parser<'i, 't>, parser: &mut ComponentParser, + l_basis: f32, + ab_basis: f32, f: F, ) -> Result>> { // https://www.w3.org/TR/css-color-4/#funcdef-lab input.parse_nested_block(|input| { parser.parse_relative::(input, |input, parser| { // f32::max() does not propagate NaN, so use clamp for now until f32::maximum() is stable. - let l = parser.parse_percentage(input)?.clamp(0.0, f32::MAX); - let a = parser.parse_number(input)?; - let b = parser.parse_number(input)?; + let l = parse_number_or_percentage(input, parser, l_basis)?.clamp(0.0, f32::MAX); + let a = parse_number_or_percentage(input, parser, ab_basis)?; + let b = parse_number_or_percentage(input, parser, ab_basis)?; let alpha = parse_alpha(input, parser)?; let lab = f(l, a, b, alpha); @@ -1122,6 +1126,8 @@ fn parse_lab<'i, 't, T: TryFrom + ColorSpace, F: Fn(f32, f32, f32, f32 fn parse_lch<'i, 't, T: TryFrom + ColorSpace, F: Fn(f32, f32, f32, f32) -> LABColor>( input: &mut Parser<'i, 't>, parser: &mut ComponentParser, + l_basis: f32, + c_basis: f32, f: F, ) -> Result>> { // https://www.w3.org/TR/css-color-4/#funcdef-lch @@ -1136,8 +1142,8 @@ fn parse_lch<'i, 't, T: TryFrom + ColorSpace, F: Fn(f32, f32, f32, f32 } } - let l = parser.parse_percentage(input)?.clamp(0.0, f32::MAX); - let c = parser.parse_number(input)?.clamp(0.0, f32::MAX); + let l = parse_number_or_percentage(input, parser, l_basis)?.clamp(0.0, f32::MAX); + let c = parse_number_or_percentage(input, parser, c_basis)?.clamp(0.0, f32::MAX); let h = parse_angle_or_number(input, parser)?; let alpha = parse_alpha(input, parser)?; let lab = f(l, c, h, alpha); @@ -1203,9 +1209,9 @@ fn parse_predefined_relative<'i, 't>( // Out of gamut values should not be clamped, i.e. values < 0 or > 1 should be preserved. // The browser will gamut-map the color for the target device that it is rendered on. - let a = input.try_parse(|input| parse_number_or_percentage(input, parser))?; - let b = input.try_parse(|input| parse_number_or_percentage(input, parser))?; - let c = input.try_parse(|input| parse_number_or_percentage(input, parser))?; + let a = input.try_parse(|input| parse_number_or_percentage(input, parser, 1.0))?; + let b = input.try_parse(|input| parse_number_or_percentage(input, parser, 1.0))?; + let c = input.try_parse(|input| parse_number_or_percentage(input, parser, 1.0))?; let alpha = parse_alpha(input, parser)?; let res = match_ignore_ascii_case! { &*&colorspace, @@ -1258,11 +1264,11 @@ pub(crate) fn parse_hsl_hwb_components<'i, 't, T: TryFrom + ColorSpace let h = parse_angle_or_number(input, parser)?; let is_legacy_syntax = allows_legacy && parser.from.is_none() && !h.is_nan() && input.try_parse(|p| p.expect_comma()).is_ok(); - let a = parser.parse_percentage(input)?.clamp(0.0, 1.0); + let a = parse_number_or_percentage(input, parser, 100.0)?.clamp(0.0, 100.0); if is_legacy_syntax { input.expect_comma()?; } - let b = parser.parse_percentage(input)?.clamp(0.0, 1.0); + let b = parse_number_or_percentage(input, parser, 100.0)?.clamp(0.0, 100.0); if is_legacy_syntax && (a.is_nan() || b.is_nan()) { return Err(input.new_custom_error(ParserError::InvalidValue)); } @@ -1276,7 +1282,7 @@ fn parse_rgb<'i, 't>( ) -> Result>> { // https://drafts.csswg.org/css-color-4/#rgb-functions input.parse_nested_block(|input| { - parser.parse_relative::(input, |input, parser| { + parser.parse_relative::(input, |input, parser| { let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?; let alpha = if is_legacy { parse_legacy_alpha(input, parser)? @@ -1288,10 +1294,15 @@ fn parse_rgb<'i, 't>( if is_legacy { Ok(CssColor::RGBA(RGBA::new(r as u8, g as u8, b as u8, alpha))) } else { - Ok(CssColor::RGBA(RGBA::from_floats(r, g, b, alpha))) + Ok(CssColor::RGBA(RGBA::from_floats( + r / 255.0, + g / 255.0, + b / 255.0, + alpha, + ))) } } else { - Ok(CssColor::Float(Box::new(FloatColor::RGB(SRGB { r, g, b, alpha })))) + Ok(CssColor::Float(Box::new(FloatColor::RGB(RGB { r, g, b, alpha })))) } }) }) @@ -1327,8 +1338,8 @@ pub(crate) fn parse_rgb_components<'i, 't>( fn get_component<'i, 't>(value: NumberOrPercentage) -> f32 { match value { NumberOrPercentage::Number { value } if value.is_nan() => value, - NumberOrPercentage::Number { value } => value.round().clamp(0.0, 255.0) / 255.0, - NumberOrPercentage::Percentage { unit_value } => unit_value.clamp(0.0, 1.0), + NumberOrPercentage::Number { value } => value.round().clamp(0.0, 255.0), + NumberOrPercentage::Percentage { unit_value } => (unit_value * 255.0).round().clamp(0.0, 255.0), } } @@ -1359,10 +1370,11 @@ fn parse_angle_or_number<'i, 't>( fn parse_number_or_percentage<'i, 't>( input: &mut Parser<'i, 't>, parser: &ComponentParser, + percent_basis: f32, ) -> Result>> { Ok(match parser.parse_number_or_percentage(input)? { NumberOrPercentage::Number { value } => value, - NumberOrPercentage::Percentage { unit_value } => unit_value, + NumberOrPercentage::Percentage { unit_value } => unit_value * percent_basis, }) } @@ -1372,7 +1384,7 @@ fn parse_alpha<'i, 't>( parser: &ComponentParser, ) -> Result>> { let res = if input.try_parse(|input| input.expect_delim('/')).is_ok() { - parse_number_or_percentage(input, parser)?.clamp(0.0, 1.0) + parse_number_or_percentage(input, parser, 1.0)?.clamp(0.0, 1.0) } else { 1.0 }; @@ -1386,7 +1398,7 @@ fn parse_legacy_alpha<'i, 't>( ) -> Result>> { Ok(if !input.is_exhausted() { input.expect_comma()?; - parse_number_or_percentage(input, parser)?.clamp(0.0, 1.0) + parse_number_or_percentage(input, parser, 1.0)?.clamp(0.0, 1.0) } else { 1.0 }) @@ -1566,41 +1578,14 @@ define_colorspace! { /// A color in the [`sRGB`](https://www.w3.org/TR/css-color-4/#predefined-sRGB) color space. pub struct SRGB { /// The red component. - #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_rgb_component", deserialize_with = "deserialize_rgb_component"))] - r: Percentage, + r: Number, /// The green component. - #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_rgb_component", deserialize_with = "deserialize_rgb_component"))] - g: Percentage, + g: Number, /// The blue component. - #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_rgb_component", deserialize_with = "deserialize_rgb_component"))] - b: Percentage + b: Number } } -// serialize RGB components in the 0-255 range as it is more common. -#[cfg(feature = "serde")] -fn serialize_rgb_component(v: &f32, serializer: S) -> Result -where - S: serde::Serializer, -{ - let v = if !v.is_nan() { - (v * 255.0).round().max(0.0).min(255.0) - } else { - *v - }; - - serializer.serialize_f32(v) -} - -#[cfg(feature = "serde")] -fn deserialize_rgb_component<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let v: f32 = serde::Deserialize::deserialize(deserializer)?; - Ok(v / 255.0) -} - // Copied from an older version of cssparser. /// A color with red, green, blue, and alpha components, in a byte each. #[derive(Clone, Copy, PartialEq, Debug)] @@ -1688,15 +1673,28 @@ fn clamp_floor_256_f32(val: f32) -> u8 { val.round().max(0.).min(255.) as u8 } +define_colorspace! { + /// A color in the [`RGB`](https://w3c.github.io/csswg-drafts/css-color-4/#rgb-functions) color space. + /// Components are in the 0-255 range. + pub struct RGB { + /// The red component. + r: Number, + /// The green component. + g: Number, + /// The blue component. + b: Number + } +} + define_colorspace! { /// A color in the [`sRGB-linear`](https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear) color space. pub struct SRGBLinear { /// The red component. - r: Percentage, + r: Number, /// The green component. - g: Percentage, + g: Number, /// The blue component. - b: Percentage + b: Number } } @@ -1704,11 +1702,11 @@ define_colorspace! { /// A color in the [`display-p3`](https://www.w3.org/TR/css-color-4/#predefined-display-p3) color space. pub struct P3 { /// The red component. - r: Percentage, + r: Number, /// The green component. - g: Percentage, + g: Number, /// The blue component. - b: Percentage + b: Number } } @@ -1716,11 +1714,11 @@ define_colorspace! { /// A color in the [`a98-rgb`](https://www.w3.org/TR/css-color-4/#predefined-a98-rgb) color space. pub struct A98 { /// The red component. - r: Percentage, + r: Number, /// The green component. - g: Percentage, + g: Number, /// The blue component. - b: Percentage + b: Number } } @@ -1728,11 +1726,11 @@ define_colorspace! { /// A color in the [`prophoto-rgb`](https://www.w3.org/TR/css-color-4/#predefined-prophoto-rgb) color space. pub struct ProPhoto { /// The red component. - r: Percentage, + r: Number, /// The green component. - g: Percentage, + g: Number, /// The blue component. - b: Percentage + b: Number } } @@ -1740,11 +1738,11 @@ define_colorspace! { /// A color in the [`rec2020`](https://www.w3.org/TR/css-color-4/#predefined-rec2020) color space. pub struct Rec2020 { /// The red component. - r: Percentage, + r: Number, /// The green component. - g: Percentage, + g: Number, /// The blue component. - b: Percentage + b: Number } } @@ -1752,7 +1750,7 @@ define_colorspace! { /// A color in the [CIE Lab](https://www.w3.org/TR/css-color-4/#cie-lab) color space. pub struct LAB { /// The lightness component. - l: Percentage, + l: Number, /// The a component. a: Number, /// The b component. @@ -1764,7 +1762,7 @@ define_colorspace! { /// A color in the [CIE LCH](https://www.w3.org/TR/css-color-4/#cie-lab) color space. pub struct LCH { /// The lightness component. - l: Percentage, + l: Number, /// The chroma component. c: Number, /// The hue component. @@ -1776,7 +1774,7 @@ define_colorspace! { /// A color in the [OKLab](https://www.w3.org/TR/css-color-4/#ok-lab) color space. pub struct OKLAB { /// The lightness component. - l: Percentage, + l: Number, /// The a component. a: Number, /// The b component. @@ -1788,7 +1786,7 @@ define_colorspace! { /// A color in the [OKLCH](https://www.w3.org/TR/css-color-4/#ok-lab) color space. pub struct OKLCH { /// The lightness component. - l: Percentage, + l: Number, /// The chroma component. c: Number, /// The hue component. @@ -1800,11 +1798,11 @@ define_colorspace! { /// A color in the [`xyz-d50`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space. pub struct XYZd50 { /// The x component. - x: Percentage, + x: Number, /// The y component. - y: Percentage, + y: Number, /// The z component. - z: Percentage + z: Number } } @@ -1812,11 +1810,11 @@ define_colorspace! { /// A color in the [`xyz-d65`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space. pub struct XYZd65 { /// The x component. - x: Percentage, + x: Number, /// The y component. - y: Percentage, + y: Number, /// The z component. - z: Percentage + z: Number } } @@ -1826,9 +1824,9 @@ define_colorspace! { /// The hue component. h: Angle, /// The saturation component. - s: Percentage, + s: Number, /// The lightness component. - l: Percentage + l: Number } } @@ -1838,9 +1836,9 @@ define_colorspace! { /// The hue component. h: Angle, /// The whiteness component. - w: Percentage, + w: Number, /// The blackness component. - b: Percentage + b: Number } } @@ -1945,7 +1943,7 @@ impl From for XYZd50 { const E: f32 = 216.0 / 24389.0; // 6^3/29^3 let lab = lab.resolve_missing(); - let l = lab.l * 100.0; + let l = lab.l; let a = lab.a; let b = lab.b; @@ -2204,7 +2202,7 @@ impl From for LAB { let f2 = if z > E { z.cbrt() } else { (K * z + 16.0) / 116.0 }; - let l = ((116.0 * f1) - 16.0) / 100.0; + let l = (116.0 * f1) - 16.0; let a = 500.0 * (f0 - f1); let b = 200.0 * (f1 - f2); LAB { @@ -2657,8 +2655,8 @@ impl From for HSL { HSL { h, - s, - l, + s: s * 100.0, + l: l * 100.0, alpha: rgb.alpha, } } @@ -2669,7 +2667,7 @@ impl From for SRGB { // https://drafts.csswg.org/css-color/#hsl-to-rgb let hsl = hsl.resolve_missing(); let h = (hsl.h - 360.0 * (hsl.h / 360.0).floor()) / 360.0; - let (r, g, b) = hsl_to_rgb(h, hsl.s, hsl.l); + let (r, g, b) = hsl_to_rgb(h, hsl.s / 100.0, hsl.l / 100.0); SRGB { r, g, @@ -2690,8 +2688,8 @@ impl From for HWB { let b = 1.0 - r.max(g).max(b); HWB { h: hsl.h, - w, - b, + w: w * 100.0, + b: b * 100.0, alpha: rgb.alpha, } } @@ -2702,8 +2700,8 @@ impl From for SRGB { // https://drafts.csswg.org/css-color/#hwb-to-rgb let hwb = hwb.resolve_missing(); let h = hwb.h; - let w = hwb.w; - let b = hwb.b; + let w = hwb.w / 100.0; + let b = hwb.b / 100.0; if w + b >= 1.0 { let gray = w / (w + b); @@ -2717,8 +2715,8 @@ impl From for SRGB { let mut rgba = SRGB::from(HSL { h, - s: 1.0, - l: 0.5, + s: 100.0, + l: 50.0, alpha: hwb.alpha, }); let x = 1.0 - w - b; @@ -2730,13 +2728,7 @@ impl From for SRGB { } impl From for SRGB { - fn from(rgb: RGBA) -> Self { - Self::from(&rgb) - } -} - -impl From<&RGBA> for SRGB { - fn from(rgb: &RGBA) -> SRGB { + fn from(rgb: RGBA) -> SRGB { SRGB { r: rgb.red_f32(), g: rgb.green_f32(), @@ -2753,6 +2745,57 @@ impl From for RGBA { } } +impl From for RGB { + fn from(rgb: SRGB) -> Self { + RGB { + r: rgb.r * 255.0, + g: rgb.g * 255.0, + b: rgb.b * 255.0, + alpha: rgb.alpha, + } + } +} + +impl From for SRGB { + fn from(rgb: RGB) -> Self { + SRGB { + r: rgb.r / 255.0, + g: rgb.g / 255.0, + b: rgb.b / 255.0, + alpha: rgb.alpha, + } + } +} + +impl From for RGB { + fn from(rgb: RGBA) -> Self { + RGB::from(&rgb) + } +} + +impl From<&RGBA> for RGB { + fn from(rgb: &RGBA) -> Self { + RGB { + r: rgb.red as f32, + g: rgb.green as f32, + b: rgb.blue as f32, + alpha: rgb.alpha_f32(), + } + } +} + +impl From for RGBA { + fn from(rgb: RGB) -> Self { + let rgb = rgb.resolve(); + RGBA::new( + clamp_floor_256_f32(rgb.r), + clamp_floor_256_f32(rgb.g), + clamp_floor_256_f32(rgb.b), + rgb.alpha, + ) + } +} + // Once Rust specialization is stable, this could be simplified. via!(LAB -> XYZd50 -> XYZd65); via!(ProPhoto -> XYZd50 -> XYZd65); @@ -2844,6 +2887,20 @@ via!(HWB -> SRGB -> XYZd65); via!(HWB -> XYZd65 -> OKLAB); via!(HWB -> XYZd65 -> OKLCH); +via!(RGB -> SRGB -> LAB); +via!(RGB -> SRGB -> LCH); +via!(RGB -> SRGB -> OKLAB); +via!(RGB -> SRGB -> OKLCH); +via!(RGB -> SRGB -> P3); +via!(RGB -> SRGB -> SRGBLinear); +via!(RGB -> SRGB -> A98); +via!(RGB -> SRGB -> ProPhoto); +via!(RGB -> SRGB -> XYZd50); +via!(RGB -> SRGB -> XYZd65); +via!(RGB -> SRGB -> Rec2020); +via!(RGB -> SRGB -> HSL); +via!(RGB -> SRGB -> HWB); + // RGBA is an 8-bit version. Convert to SRGB, which is a // more accurate floating point representation for all operations. via!(RGBA -> SRGB -> LAB); @@ -2950,6 +3007,7 @@ color_space!(ProPhoto); color_space!(Rec2020); color_space!(HSL); color_space!(HWB); +color_space!(RGB); color_space!(RGBA); macro_rules! predefined { @@ -3012,6 +3070,7 @@ macro_rules! rgb { rgb!(SRGB); rgb!(HSL); rgb!(HWB); +rgb!(RGB); impl From for CssColor { fn from(color: RGBA) -> CssColor { @@ -3069,15 +3128,15 @@ macro_rules! hsl_hwb_color_gamut { impl ColorGamut for $t { #[inline] fn in_gamut(&self) -> bool { - self.$a >= 0.0 && self.$a <= 1.0 && self.$b >= 0.0 && self.$b <= 1.0 + self.$a >= 0.0 && self.$a <= 100.0 && self.$b >= 0.0 && self.$b <= 100.0 } #[inline] fn clip(&self) -> Self { Self { h: self.h % 360.0, - $a: self.$a.clamp(0.0, 1.0), - $b: self.$b.clamp(0.0, 1.0), + $a: self.$a.clamp(0.0, 100.0), + $b: self.$b.clamp(0.0, 100.0), alpha: self.alpha.clamp(0.0, 1.0), } } @@ -3100,6 +3159,23 @@ unbounded_color_gamut!(OKLCH, l, c, h); hsl_hwb_color_gamut!(HSL, s, l); hsl_hwb_color_gamut!(HWB, w, b); +impl ColorGamut for RGB { + #[inline] + fn in_gamut(&self) -> bool { + self.r >= 0.0 && self.r <= 255.0 && self.g >= 0.0 && self.g <= 255.0 && self.b >= 0.0 && self.b <= 255.0 + } + + #[inline] + fn clip(&self) -> Self { + Self { + r: self.r.clamp(0.0, 255.0), + g: self.g.clamp(0.0, 255.0), + b: self.b.clamp(0.0, 255.0), + alpha: self.alpha.clamp(0.0, 1.0), + } + } +} + fn delta_eok>(a: T, b: OKLCH) -> f32 { // https://www.w3.org/TR/css-color-4/#color-difference-OK let a: OKLAB = a.into(); @@ -3532,7 +3608,7 @@ impl Interpolate for HSL { self.h = f32::NAN; } - if self.l.abs() < f32::EPSILON || (self.l - 1.0).abs() < f32::EPSILON { + if self.l.abs() < f32::EPSILON || (self.l - 100.0).abs() < f32::EPSILON { self.h = f32::NAN; self.s = f32::NAN; } @@ -3551,7 +3627,7 @@ impl Interpolate for HWB { fn adjust_powerless_components(&mut self) { // If white+black is equal to 100% (after normalization), it defines an achromatic color, // i.e. some shade of gray, without any hint of the chosen hue. In this case, the hue component is powerless. - if (self.w + self.b - 1.0).abs() < f32::EPSILON { + if (self.w + self.b - 100.0).abs() < f32::EPSILON { self.h = f32::NAN; } } diff --git a/src/values/length.rs b/src/values/length.rs index 149f1dd1..2ce01b06 100644 --- a/src/values/length.rs +++ b/src/values/length.rs @@ -656,7 +656,7 @@ impl Length { } match (a, b) { - (Length::Calc(a), Length::Calc(b)) => return Length::Calc(Box::new(a.add(*b))), + (Length::Calc(a), Length::Calc(b)) => return Length::Calc(Box::new(a.add(*b).unwrap())), (Length::Calc(calc), b) => { if let Calc::Value(a) = *calc { a.add(b) diff --git a/src/values/percentage.rs b/src/values/percentage.rs index 54352750..5e059457 100644 --- a/src/values/percentage.rs +++ b/src/values/percentage.rs @@ -74,11 +74,13 @@ impl std::convert::Into> for Percentage { } } -impl std::convert::From> for Percentage { - fn from(calc: Calc) -> Percentage { +impl std::convert::TryFrom> for Percentage { + type Error = (); + + fn try_from(calc: Calc) -> Result { match calc { - Calc::Value(v) => *v, - _ => unreachable!(), + Calc::Value(v) => Ok(*v), + _ => Err(()), } } } @@ -203,6 +205,7 @@ impl< + Zero + TrySign + TryFrom + + TryInto + PartialOrd + std::fmt::Debug, > Parse<'i> for DimensionPercentage @@ -349,7 +352,7 @@ impl + Clone + Zero + TrySign + std::fmt::Debug> DimensionPercentag match (a, b) { (DimensionPercentage::Calc(a), DimensionPercentage::Calc(b)) => { - DimensionPercentage::Calc(Box::new(a.add(*b))) + DimensionPercentage::Calc(Box::new(a.add(*b).unwrap())) } (DimensionPercentage::Calc(calc), b) => { if let Calc::Value(a) = *calc { @@ -434,6 +437,17 @@ impl> TryFrom for DimensionPercentage } } +impl> TryInto for DimensionPercentage { + type Error = (); + + fn try_into(self) -> Result { + match self { + DimensionPercentage::Dimension(d) => d.try_into().map_err(|_| ()), + _ => Err(()), + } + } +} + impl Zero for DimensionPercentage { fn zero() -> Self { DimensionPercentage::Dimension(D::zero()) diff --git a/src/values/time.rs b/src/values/time.rs index 20408e8a..11e67b31 100644 --- a/src/values/time.rs +++ b/src/values/time.rs @@ -130,11 +130,13 @@ impl std::convert::Into> for Time { } } -impl std::convert::From> for Time { - fn from(calc: Calc