diff --git a/.cargo/config.toml b/.cargo/config.toml index a05c05ab5..10746d766 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,5 @@ +[target.'cfg(target_env = "gnu")'] +rustflags = ["-C", "link-args=-Wl,-z,nodelete"] [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" @@ -10,4 +12,16 @@ linker = "aarch64-linux-musl-gcc" rustflags = ["-C", "target-feature=-crt-static"] [target.wasm32-unknown-unknown] -rustflags = ["-C", "link-arg=--export-table"] +rustflags = [ + "-C", + "link-arg=--export-table", + '--cfg', + 'getrandom_backend="custom"', +] + + +# Statically link Visual Studio redistributables on Windows builds +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2eeb96aa..95d304311 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e81411540..10868f4a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,13 @@ yarn wasm:build yarn wasm:build-release ``` +Note: If you plan to build the WASM target, ensure that you have the required toolchain and binaries installed. + +```sh +rustup target add wasm32-unknown-unknown +cargo install wasm-opt +``` + ## Website The website is built using [Parcel](https://parceljs.org). You can start the development server by running: diff --git a/Cargo.lock b/Cargo.lock index a55c3e633..bb4bb2840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,19 +8,19 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.3", "once_cell", "serde", "version_check", @@ -125,6 +125,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -139,32 +142,25 @@ dependencies = [ ] [[package]] -name = "browserslist-rs" -version = "0.17.0" +name = "browserslist-data" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c973b79d9b6b89854493185ab760c6ef8e54bcfad10ad4e33991e46b374ac8" +checksum = "c49471c5ae53cefe3ac4acc4d3c75cb4b68995b70b3bbb864f8e08fae282098c" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "chrono", - "either", - "indexmap 2.7.0", - "itertools 0.13.0", - "nom", - "serde", - "serde_json", - "thiserror", ] [[package]] name = "browserslist-rs" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95aff901882c66e4b642f3f788ceee152ef44f8a5ef12cb1ddee5479c483be" +checksum = "8dd48a6ca358df4f7000e3fb5f08738b1b91a0e5d5f862e2f77b2b14647547f5" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", + "browserslist-data", "chrono", "either", - "indexmap 2.7.0", "itertools 0.13.0", "nom", "serde", @@ -538,7 +534,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -746,21 +754,21 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.65" +version = "1.0.0-alpha.70" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "assert_cmd", "assert_fs", "atty", "bitflags 2.6.0", - "browserslist-rs 0.18.1", + "browserslist-rs", "clap", "const-str", "cssparser", "cssparser-color", "dashmap", "data-encoding", - "getrandom", + "getrandom 0.3.3", "indexmap 2.7.0", "indoc", "itertools 0.10.5", @@ -769,13 +777,14 @@ dependencies = [ "lightningcss-derive", "parcel_selectors", "parcel_sourcemap", - "paste", + "pastey", "pathdiff", "predicates 2.1.5", "pretty_assertions", "rayon", "schemars", "serde", + "serde-content", "serde_json", "smallvec", "static-self", @@ -793,7 +802,7 @@ dependencies = [ [[package]] name = "lightningcss-napi" -version = "0.4.3" +version = "0.4.7" dependencies = [ "crossbeam-channel", "cssparser", @@ -802,6 +811,7 @@ dependencies = [ "parcel_sourcemap", "rayon", "serde", + "serde-content", "serde-detach", "serde_bytes", "smallvec", @@ -811,7 +821,7 @@ dependencies = [ name = "lightningcss_c_bindings" version = "0.1.0" dependencies = [ - "browserslist-rs 0.17.0", + "browserslist-rs", "cbindgen", "lightningcss", "parcel_sourcemap", @@ -972,7 +982,7 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "parcel_selectors" -version = "0.28.1" +version = "0.28.2" dependencies = [ "bitflags 2.6.0", "cssparser", @@ -1015,10 +1025,10 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261" [[package]] name = "pathdiff" @@ -1197,6 +1207,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -1394,13 +1410,23 @@ checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] +[[package]] +name = "serde-content" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" +dependencies = [ + "serde", +] + [[package]] name = "serde-detach" version = "0.0.1" @@ -1420,11 +1446,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1675,6 +1710,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.99" @@ -1842,6 +1886,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "wyz" version = "0.2.0" @@ -1865,18 +1918,18 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3f4eef4e8..c113a3921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ [package] authors = ["Devon Govett "] name = "lightningcss" -version = "1.0.0-alpha.65" +version = "1.0.0-alpha.70" description = "A CSS parser, transformer, and minifier" license = "MPL-2.0" edition = "2021" @@ -34,15 +34,16 @@ 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"] +nodejs = ["dep:serde", "dep:serde-content"] serde = [ "dep:serde", + "dep:serde-content", + "bitflags/serde", "smallvec/serde", "cssparser/serde", "parcel_selectors/serde", @@ -59,10 +60,11 @@ into_owned = [ substitute_variables = ["visitor", "into_owned"] [dependencies] -serde = { version = "1.0.201", features = ["derive"], optional = true } +serde = { version = "1.0.228", features = ["derive"], optional = true } +serde-content = { version = "0.1.2", features = ["serde"], optional = true } cssparser = "0.33.0" cssparser-color = "0.1.0" -parcel_selectors = { version = "0.28.1", path = "./selectors" } +parcel_selectors = { version = "0.28.2", path = "./selectors" } itertools = "0.10.1" smallvec = { version = "1.7.0", features = ["union"] } bitflags = "2.2.1" @@ -72,12 +74,12 @@ lazy_static = "1.4.0" const-str = "0.3.1" pathdiff = "0.2.1" ahash = "0.8.7" -paste = "1.0.12" +pastey = "0.1.0" 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.18.1", optional = true } +browserslist-rs = { version = "0.19.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 } @@ -91,7 +93,7 @@ jemallocator = { version = "0.3.2", features = [ ], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["custom"], default-features = false } +getrandom = { version = "0.3", default-features = false } [dev-dependencies] indoc = "1.0.3" diff --git a/c/Cargo.toml b/c/Cargo.toml index 3d20add03..f9a9d167f 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.17.0" } +browserslist-rs = { version = "0.19.0" } [build-dependencies] cbindgen = "0.24.3" diff --git a/napi/Cargo.toml b/napi/Cargo.toml index 0aa3a14d4..789062ea6 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Devon Govett "] name = "lightningcss-napi" -version = "0.4.3" +version = "0.4.7" description = "Node-API bindings for Lightning CSS" license = "MPL-2.0" repository = "https://github.com/parcel-bundler/lightningcss" @@ -14,9 +14,10 @@ bundler = ["dep:crossbeam-channel", "dep:rayon"] [dependencies] serde = { version = "1.0.201", features = ["derive"] } +serde-content = { version = "0.1.2", features = ["serde"] } serde_bytes = "0.11.5" cssparser = "0.33.0" -lightningcss = { version = "1.0.0-alpha.65", path = "../", features = [ +lightningcss = { version = "1.0.0-alpha.70", path = "../", features = [ "nodejs", "serde", ] } diff --git a/napi/src/lib.rs b/napi/src/lib.rs index dff488056..f43a8d46e 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -121,8 +121,8 @@ pub fn transform_style_attribute(ctx: CallContext) -> napi::Result { mod bundle { use super::*; use crossbeam_channel::{self, Receiver, Sender}; - use lightningcss::bundler::FileProvider; - use napi::{Env, JsFunction, JsString, NapiRaw}; + use lightningcss::bundler::{FileProvider, ResolveResult}; + use napi::{Env, JsBoolean, JsFunction, JsString, NapiRaw}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Mutex; @@ -169,6 +169,7 @@ mod bundle { // Allocate a single channel per thread to communicate with the JS thread. thread_local! { static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); + static RESOLVER_CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); } impl SourceProvider for JsSourceProvider { @@ -203,9 +204,9 @@ mod bundle { } } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { - return CHANNEL.with(|channel| { + return RESOLVER_CHANNEL.with(|channel| { let message = ResolveMessage { specifier: specifier.to_owned(), originating_file: originating_file.to_str().unwrap().to_owned(), @@ -213,22 +214,18 @@ mod bundle { }; resolve.call(message, ThreadsafeFunctionCallMode::Blocking); - let result = channel.1.recv().unwrap(); - match result { - Ok(result) => Ok(PathBuf::from_str(&result).unwrap()), - Err(e) => Err(e), - } + channel.1.recv().unwrap() }); } - Ok(originating_file.with_file_name(specifier)) + Ok(originating_file.with_file_name(specifier).into()) } } struct ResolveMessage { specifier: String, originating_file: String, - tx: Sender>, + tx: Sender>, } struct ReadMessage { @@ -241,7 +238,11 @@ mod bundle { tx: Sender>, } - fn await_promise(env: Env, result: JsUnknown, tx: Sender>) -> napi::Result<()> { + fn await_promise(env: Env, result: JsUnknown, tx: Sender>, parse: Cb) -> napi::Result<()> + where + T: 'static, + Cb: 'static + Fn(JsUnknown) -> Result, + { // If the result is a promise, wait for it to resolve, and send the result to the channel. // Otherwise, send the result immediately. if result.is_promise()? { @@ -249,9 +250,8 @@ mod bundle { let then: JsFunction = get_named_property(&result, "then")?; let tx2 = tx.clone(); let cb = env.create_function_from_closure("callback", move |ctx| { - let res = ctx.get::(0)?.into_utf8()?; - let s = res.into_owned()?; - tx.send(Ok(s)).unwrap(); + let res = parse(ctx.get::(0)?)?; + tx.send(Ok(res)).unwrap(); ctx.env.get_undefined() })?; let eb = env.create_function_from_closure("error_callback", move |ctx| { @@ -261,10 +261,8 @@ mod bundle { })?; then.call(Some(&result), &[cb, eb])?; } else { - let result: JsString = result.try_into()?; - let utf8 = result.into_utf8()?; - let s = utf8.into_owned()?; - tx.send(Ok(s)).unwrap(); + let result = parse(result)?; + tx.send(Ok(result)).unwrap(); } Ok(()) @@ -274,10 +272,12 @@ mod bundle { let specifier = ctx.env.create_string(&ctx.value.specifier)?; let originating_file = ctx.env.create_string(&ctx.value.originating_file)?; let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?; - await_promise(ctx.env, result, ctx.value.tx) + await_promise(ctx.env, result, ctx.value.tx, move |unknown| { + ctx.env.from_js_value(unknown) + }) } - fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { + fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { match res { Ok(_) => Ok(()), Err(e) => { @@ -295,7 +295,9 @@ mod bundle { fn read_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { let file = ctx.env.create_string(&ctx.value.file)?; let result = ctx.callback.unwrap().call(None, &[file])?; - await_promise(ctx.env, result, ctx.value.tx) + await_promise(ctx.env, result, ctx.value.tx, |unknown| { + JsString::try_from(unknown)?.into_utf8()?.into_owned() + }) } fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { @@ -421,10 +423,10 @@ mod bundle { #[cfg(target_arch = "wasm32")] mod bundle { use super::*; + use lightningcss::bundler::ResolveResult; use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; use std::cell::UnsafeCell; - use std::path::{Path, PathBuf}; - use std::str::FromStr; + use std::path::Path; pub fn bundle(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; @@ -497,7 +499,7 @@ mod bundle { ); } - fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { + fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { if value.is_promise()? { let mut result = std::ptr::null_mut(); let mut error = std::ptr::null_mut(); @@ -513,7 +515,7 @@ mod bundle { value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; } - value.try_into() + Ok(value) } impl SourceProvider for JsSourceProvider { @@ -523,7 +525,9 @@ mod bundle { let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?; let file = self.env.create_string(file.to_str().unwrap())?; let source: JsUnknown = read.call(None, &[file])?; - let source = get_result(self.env, source)?.into_utf8()?.into_owned()?; + let source = get_result(self.env, source)?; + let source: JsString = source.try_into()?; + let source = source.into_utf8()?.into_owned()?; // cache the result let ptr = Box::into_raw(Box::new(source)); @@ -535,16 +539,17 @@ mod bundle { Ok(unsafe { &*ptr }) } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?; let specifier = self.env.create_string(specifier)?; let originating_file = self.env.create_string(originating_file.to_str().unwrap())?; let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?; - let result = get_result(self.env, result)?.into_utf8()?; - Ok(PathBuf::from_str(result.as_str()?).unwrap()) + let result = get_result(self.env, result)?; + let result = self.env.from_js_value(result)?; + Ok(result) } else { - Ok(originating_file.with_file_name(specifier)) + Ok(ResolveResult::File(originating_file.with_file_name(specifier))) } } } diff --git a/napi/src/transformer.rs b/napi/src/transformer.rs index 29875b87c..bab931f2d 100644 --- a/napi/src/transformer.rs +++ b/napi/src/transformer.rs @@ -755,9 +755,8 @@ impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de D: serde::Deserializer<'de>, { use serde::Deserializer; - let content = serde::__private::de::Content::deserialize(deserializer)?; - let de: serde::__private::de::ContentRefDeserializer = - serde::__private::de::ContentRefDeserializer::new(&content); + let content = serde_content::Value::deserialize(deserializer)?; + let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); // Try to deserialize as a sequence first. let mut was_seq = false; @@ -769,13 +768,15 @@ impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de if was_seq { // Allow fallback if we know the value is also a list (e.g. selector). if res.is_ok() || !IS_VEC { - return res.map(ValueOrVec::Vec); + return res.map_err(|e| serde::de::Error::custom(e.to_string())).map(ValueOrVec::Vec); } } // If it wasn't a sequence, try a value. - let de = serde::__private::de::ContentRefDeserializer::new(&content); - return V::deserialize(de).map(ValueOrVec::Value); + let de = serde_content::Deserializer::new(content).coerce_numbers(); + return V::deserialize(de) + .map_err(|e| serde::de::Error::custom(e.to_string())) + .map(ValueOrVec::Value); struct SeqVisitor<'a, V> { was_seq: &'a mut bool, @@ -811,16 +812,14 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { where D: serde::Deserializer<'de>, { - use serde::__private::de::ContentRefDeserializer; - #[derive(serde::Deserialize)] struct Raw<'i> { #[serde(borrow)] raw: CowArcStr<'i>, } - let content = serde::__private::de::Content::deserialize(deserializer)?; - let de: ContentRefDeserializer = ContentRefDeserializer::new(&content); + let content = serde_content::Value::deserialize(deserializer)?; + let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); if let Ok(res) = Raw::deserialize(de) { let res = TokenList::parse_string_with_options(res.raw.as_ref(), ParserOptions::default()) @@ -828,8 +827,10 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { return Ok(TokensOrRaw(ValueOrVec::Vec(res.into_owned().0))); } - let de = ContentRefDeserializer::new(&content); - Ok(TokensOrRaw(ValueOrVec::deserialize(de)?)) + let de = serde_content::Deserializer::new(content).coerce_numbers(); + Ok(TokensOrRaw( + ValueOrVec::deserialize(de).map_err(|e| serde::de::Error::custom(e.to_string()))?, + )) } } diff --git a/node/Cargo.toml b/node/Cargo.toml index bc7df82ba..b5c7505c3 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -9,7 +9,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -lightningcss-napi = { version = "0.4.3", path = "../napi", features = [ +lightningcss-napi = { version = "0.4.7", path = "../napi", features = [ "bundler", "visitor", ] } diff --git a/node/ast.d.ts b/node/ast.d.ts index 08d9d786e..28e9d0930 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -134,6 +134,10 @@ export type MediaCondition = */ operator: Operator; type: "operation"; + } + | { + type: "unknown"; + value: TokenOrValue[]; }; /** * A generic media feature or container feature. @@ -2363,6 +2367,10 @@ export type PropertyId = | { property: "color-scheme"; } + | { + property: "print-color-adjust"; + vendorPrefix: VendorPrefix; + } | { property: "all"; } @@ -3851,6 +3859,11 @@ export type Declaration = property: "color-scheme"; value: ColorScheme; } + | { + property: "print-color-adjust"; + value: PrintColorAdjust; + vendorPrefix: VendorPrefix; + } | { property: "all"; value: CSSWideKeyword; @@ -5218,7 +5231,7 @@ export type RepeatCount = }; export type AutoFlowDirection = "row" | "column"; /** - * A value for the [grid-template-areas](https://drafts.csswg.org/css-grid-2/#grid-template-areas-property) property. + * A value for the [grid-template-areas](https://drafts.csswg.org/css-grid-2/#grid-template-areas-property) property. none | + */ export type GridTemplateAreas = | { @@ -6420,7 +6433,7 @@ export type ZIndex = /** * A value for the [container-type](https://drafts.csswg.org/css-contain-3/#container-type) property. Establishes the element as a query container for the purpose of container queries. */ -export type ContainerType = "normal" | "inline-size" | "size"; +export type ContainerType = "normal" | "inline-size" | "size" | "scroll-state"; /** * A value for the [container-name](https://drafts.csswg.org/css-contain-3/#container-name) property. */ @@ -6447,6 +6460,10 @@ export type NoneOrCustomIdentList = */ export type ViewTransitionGroup = "normal" | "contain" | "nearest" | String; +/** + * A value for the [print-color-adjust](https://drafts.csswg.org/css-color-adjust/#propdef-print-color-adjust) property. + */ +export type PrintColorAdjust = "economy" | "exact"; /** * A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). */ @@ -6784,6 +6801,13 @@ export type PseudoClass = */ type: String[]; } + | { + kind: "state"; + /** + * The custom state identifier. + */ + state: String; + } | { kind: "local"; /** @@ -6944,6 +6968,25 @@ export type PseudoElement = */ part: ViewTransitionPartSelector; } + | { + /** + * A form control identifier. + */ + identifier: String; + kind: "picker-function"; + } + | { + kind: "picker-icon"; + } + | { + kind: "checkmark"; + } + | { + kind: "grammar-error"; + } + | { + kind: "spelling-error"; + } | { kind: "custom"; /** @@ -7230,6 +7273,10 @@ export type ParsedComponent = type: "length-percentage"; value: DimensionPercentageFor_LengthValue; } + | { + type: "string"; + value: String; + } | { type: "color"; value: CssColor; @@ -7331,6 +7378,9 @@ export type SyntaxComponentKind = | { type: "length-percentage"; } + | { + type: "string"; + } | { type: "color"; } @@ -7390,6 +7440,14 @@ export type ContainerCondition = | { | { type: "style"; value: StyleQuery; + } +| { + type: "scroll-state"; + value: ScrollStateQuery; + } +| { + type: "unknown"; + value: TokenOrValue[]; }; /** * A generic media feature or container feature. @@ -7485,6 +7543,97 @@ export type StyleQuery = | { operator: Operator; type: "operation"; }; +/** + * Represents a scroll state query within a container condition. + */ +export type ScrollStateQuery = + | { + type: "feature"; + value: QueryFeatureFor_ScrollStateFeatureId; + } + | { + type: "not"; + value: ScrollStateQuery; + } + | { + /** + * The conditions for the operator. + */ + conditions: ScrollStateQuery[]; + /** + * The operator for the conditions. + */ + operator: Operator; + type: "operation"; + }; +/** + * A generic media feature or container feature. + */ +export type QueryFeatureFor_ScrollStateFeatureId = + | { + /** + * The name of the feature. + */ + name: MediaFeatureNameFor_ScrollStateFeatureId; + type: "plain"; + /** + * The feature value. + */ + value: MediaFeatureValue; + } + | { + /** + * The name of the feature. + */ + name: MediaFeatureNameFor_ScrollStateFeatureId; + type: "boolean"; + } + | { + /** + * The name of the feature. + */ + name: MediaFeatureNameFor_ScrollStateFeatureId; + /** + * A comparator. + */ + operator: MediaFeatureComparison; + type: "range"; + /** + * The feature value. + */ + value: MediaFeatureValue; + } + | { + /** + * The end value. + */ + end: MediaFeatureValue; + /** + * A comparator for the end value. + */ + endOperator: MediaFeatureComparison; + /** + * The name of the feature. + */ + name: MediaFeatureNameFor_ScrollStateFeatureId; + /** + * A start value. + */ + start: MediaFeatureValue; + /** + * A comparator for the start value. + */ + startOperator: MediaFeatureComparison; + type: "interval"; + }; +/** + * A media feature name. + */ +export type MediaFeatureNameFor_ScrollStateFeatureId = ScrollStateFeatureId | String | String; +/** + * A container query scroll state feature identifier. + */ +export type ScrollStateFeatureId = "stuck" | "snapped" | "scrollable" | "scrolled"; /** * A property within a `@view-transition` rule. * @@ -8358,6 +8507,8 @@ export interface GridAutoFlow { /** * A value for the [grid-template](https://drafts.csswg.org/css-grid-2/#explicit-grid-shorthand) shorthand property. * + * none | [ <'grid-template-rows'> / <'grid-template-columns'> ] | [ ? ? ? ]+ [ / ]? + * * If `areas` is not `None`, then `rows` must also not be `None`. */ export interface GridTemplate { @@ -8377,6 +8528,8 @@ export interface GridTemplate { /** * A value for the [grid](https://drafts.csswg.org/css-grid-2/#grid-shorthand) shorthand property. * + * <'grid-template'> | <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? | [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'> + * * Explicit and implicit values may not be combined. */ export interface Grid { @@ -8406,7 +8559,7 @@ export interface Grid { rows: TrackSizing; } /** - * A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-row) shorthand property. + * A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-row) shorthand property. [ / ]? */ export interface GridRow { /** @@ -8419,7 +8572,7 @@ export interface GridRow { start: GridLine; } /** - * A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property. + * A value for the [grid-column](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property. [ / ]? */ export interface GridColumn { /** @@ -8432,7 +8585,7 @@ export interface GridColumn { start: GridLine; } /** - * A value for the [grid-area](https://drafts.csswg.org/css-grid-2/#propdef-grid-area) shorthand property. + * A value for the [grid-area](https://drafts.csswg.org/css-grid-2/#propdef-grid-area) shorthand property. [ / ]{0,3} */ export interface GridArea { /** @@ -9653,7 +9806,7 @@ export interface ContainerRule { /** * The container condition. */ - condition: ContainerCondition; + condition?: ContainerCondition | null; /** * The location of the rule in the source file. */ diff --git a/node/composeVisitors.js b/node/composeVisitors.js index 9d5796e3b..f29934905 100644 --- a/node/composeVisitors.js +++ b/node/composeVisitors.js @@ -1,15 +1,23 @@ // @ts-check /** @typedef {import('./index').Visitor} Visitor */ +/** @typedef {import('./index').VisitorFunction} VisitorFunction */ /** * Composes multiple visitor objects into a single one. - * @param {Visitor[]} visitors - * @return {Visitor} + * @param {(Visitor | VisitorFunction)[]} visitors + * @return {Visitor | VisitorFunction} */ function composeVisitors(visitors) { if (visitors.length === 1) { return visitors[0]; } + + if (visitors.some(v => typeof v === 'function')) { + return (opts) => { + let v = visitors.map(v => typeof v === 'function' ? v(opts) : v); + return composeVisitors(v); + }; + } /** @type Visitor */ let res = {}; @@ -366,7 +374,7 @@ function createArrayVisitor(visitors, apply) { // For each value, call all visitors. If a visitor returns a new value, // we start over, but skip the visitor that generated the value or saw // it before (to avoid cycles). This way, visitors can be composed in any order. - for (let v = 0; v < visitors.length;) { + for (let v = 0; v < visitors.length && i < arr.length;) { if (seen.get(v)) { v++; continue; diff --git a/node/index.d.ts b/node/index.d.ts index 76d405729..6d727d75a 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -63,7 +63,7 @@ export interface TransformOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor, + visitor?: Visitor | VisitorFunction, /** * Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS * [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and @@ -213,6 +213,13 @@ export interface Visitor { EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors; } +export type VisitorDependency = FileDependency | GlobDependency; +export interface VisitorOptions { + addDependency: (dep: VisitorDependency) => void +} + +export type VisitorFunction = (options: VisitorOptions) => Visitor; + export interface CustomAtRules { [name: string]: CustomAtRuleDefinition } @@ -358,7 +365,7 @@ export interface DependencyCSSModuleReference { specifier: string } -export type Dependency = ImportDependency | UrlDependency; +export type Dependency = ImportDependency | UrlDependency | FileDependency | GlobDependency; export interface ImportDependency { type: 'import', @@ -384,6 +391,16 @@ export interface UrlDependency { placeholder: string } +export interface FileDependency { + type: 'file', + filePath: string +} + +export interface GlobDependency { + type: 'glob', + glob: string +} + export interface SourceLocation { /** The file path in which the dependency exists. */ filePath: string, @@ -438,7 +455,7 @@ export interface TransformAttributeOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor + visitor?: Visitor | VisitorFunction } export interface TransformAttributeResult { @@ -474,4 +491,4 @@ export declare function bundleAsync(options: BundleAsyn /** * Composes multiple visitor objects into a single one. */ -export declare function composeVisitors(visitors: Visitor[]): Visitor; +export declare function composeVisitors(visitors: (Visitor | VisitorFunction)[]): Visitor | VisitorFunction; diff --git a/node/index.js b/node/index.js index 011d04b45..6fe25aef4 100644 --- a/node/index.js +++ b/node/index.js @@ -13,16 +13,47 @@ if (process.platform === 'linux') { parts.push('msvc'); } -if (process.env.CSS_TRANSFORMER_WASM) { - module.exports = require(`../pkg`); -} else { - try { - module.exports = require(`lightningcss-${parts.join('-')}`); - } catch (err) { - module.exports = require(`../lightningcss.${parts.join('-')}.node`); - } +let native; +try { + native = require(`lightningcss-${parts.join('-')}`); +} catch (err) { + native = require(`../lightningcss.${parts.join('-')}.node`); } +module.exports.transform = wrap(native.transform); +module.exports.transformStyleAttribute = wrap(native.transformStyleAttribute); +module.exports.bundle = wrap(native.bundle); +module.exports.bundleAsync = wrap(native.bundleAsync); module.exports.browserslistToTargets = require('./browserslistToTargets'); module.exports.composeVisitors = require('./composeVisitors'); module.exports.Features = require('./flags').Features; + +function wrap(call) { + return (options) => { + if (typeof options.visitor === 'function') { + let deps = []; + options.visitor = options.visitor({ + addDependency(dep) { + deps.push(dep); + } + }); + + let result = call(options); + if (result instanceof Promise) { + result = result.then(res => { + if (deps.length) { + res.dependencies ??= []; + res.dependencies.push(...deps); + } + return res; + }); + } else if (deps.length) { + result.dependencies ??= []; + result.dependencies.push(...deps); + } + return result; + } else { + return call(options); + } + }; +} diff --git a/node/src/lib.rs b/node/src/lib.rs index e429b0f2e..822944124 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -3,7 +3,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; use napi::{CallContext, JsObject, JsUnknown}; -use napi_derive::{js_function, module_exports}; +use napi_derive::js_function; #[js_function(1)] fn transform(ctx: CallContext) -> napi::Result { @@ -26,7 +26,7 @@ pub fn bundle_async(ctx: CallContext) -> napi::Result { lightningcss_napi::bundle_async(ctx) } -#[cfg_attr(not(target_arch = "wasm32"), module_exports)] +#[cfg_attr(not(target_arch = "wasm32"), napi_derive::module_exports)] fn init(mut exports: JsObject) -> napi::Result<()> { exports.create_named_method("transform", transform)?; exports.create_named_method("transformStyleAttribute", transform_style_attribute)?; @@ -45,7 +45,6 @@ pub fn register_module() { unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> { use napi::{Env, JsObject, NapiValue}; - let env = Env::from_raw(raw_env); let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); init(exports) } diff --git a/node/test/bundle.test.mjs b/node/test/bundle.test.mjs index 50d113b57..4279e51c8 100644 --- a/node/test/bundle.test.mjs +++ b/node/test/bundle.test.mjs @@ -365,7 +365,7 @@ test('resolve return non-string', async () => { } if (!error) throw new Error(`\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`); - assert.equal(error.message, 'expect String, got: Number'); + assert.equal(error.message, 'data did not match any variant of untagged enum ResolveResult'); assert.equal(error.fileName, 'tests/testdata/foo.css'); assert.equal(error.loc, { line: 1, @@ -414,4 +414,29 @@ test('should support throwing in visitors', async () => { assert.equal(error.message, 'Some error'); }); +test('external import', async () => { + const { code: buffer } = await bundleAsync(/** @type {import('../index').BundleAsyncOptions} */ ({ + filename: 'tests/testdata/has_external.css', + resolver: { + resolve(specifier, originatingFile) { + if (specifier === './does_not_exist.css' || specifier.startsWith('https:')) { + return {external: specifier}; + } + return path.resolve(path.dirname(originatingFile), specifier); + } + } + })); + const code = buffer.toString('utf-8').trim(); + + const expected = ` +@import "https://fonts.googleapis.com/css2?family=Roboto&display=swap"; +@import "./does_not_exist.css"; + +.b { + height: calc(100vh - 64px); +} + `.trim(); + if (code !== expected) throw new Error(`\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`); +}); + test.run(); diff --git a/node/test/composeVisitors.test.mjs b/node/test/composeVisitors.test.mjs index 7718ec067..4379cf481 100644 --- a/node/test/composeVisitors.test.mjs +++ b/node/test/composeVisitors.test.mjs @@ -800,4 +800,61 @@ test('StyleSheet', () => { assert.equal(styleSheetExitCalledCount, 2); }); +test('visitor function', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @dep "foo.js"; + @dep2 "bar.js"; + + .foo { + width: 32px; + } + `), + visitor: composeVisitors([ + ({addDependency}) => ({ + Rule: { + unknown: { + dep(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }), + ({addDependency}) => ({ + Rule: { + unknown: { + dep2(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }) + ]) + }); + + assert.equal(res.code.toString(), '.foo{width:32px}'); + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'foo.js' + }, + { + type: 'file', + filePath: 'bar.js' + } + ]); +}); + test.run(); diff --git a/node/test/visitor.test.mjs b/node/test/visitor.test.mjs index 3a42a696b..149825b7d 100644 --- a/node/test/visitor.test.mjs +++ b/node/test/visitor.test.mjs @@ -249,6 +249,68 @@ test('specific environment variables', () => { assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}'); }); +test('spacing with env substitution', () => { + // Test spacing for different cases when `env()` functions are replaced with actual values. + /** @type {Record} */ + let tokens = { + '--var1': 'var(--foo)', + '--var2': 'var(--bar)', + '--function': 'scale(1.5)', + '--length1': '10px', + '--length2': '20px', + '--x': '4', + '--y': '12', + '--num1': '5', + '--num2': '10', + '--num3': '15', + '--counter': '2', + '--ident1': 'solid', + '--ident2': 'auto', + '--rotate': '45deg', + '--percentage1': '25%', + '--percentage2': '75%', + '--color': 'red', + '--color1': '#ff1234', + '--string1': '"hello"', + '--string2': '" world"' + }; + + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + .test { + /* Asymmetric spacing - no space after var(). */ + background: env(--var1) env(--var2); + border: env(--var1)env(--ident1); + transform: env(--function) env(--function); + /* Normal spacing between values. */ + padding: env(--length1) env(--length2); + margin: env(--length1) env(--ident2); + outline: env(--color) env(--ident1); + /* Raw numbers that need spacing. */ + cursor: url(cursor.png) env(--x) env(--y), auto; + stroke-dasharray: env(--num1) env(--num2) env(--num3); + counter-increment: myCounter env(--counter); + /* Mixed token types. */ + background: linear-gradient(red env(--percentage1), blue env(--percentage2)); + content: env(--string1) env(--string2); + /* Inside calc expressions. */ + width: calc(env(--length1) - env(--length2)); + } + `), + visitor: { + EnvironmentVariable(env) { + if (env.name.type === 'custom' && tokens[env.name.ident]) { + return { raw: tokens[env.name.ident] }; + } + } + } + }); + + assert.equal(res.code.toString(), '.test{background:var(--foo) var(--bar);border:var(--foo)solid;transform:scale(1.5) scale(1.5);padding:10px 20px;margin:10px auto;outline:red solid;cursor:url(cursor.png) 4 12, auto;stroke-dasharray:5 10 15;counter-increment:myCounter 2;background:linear-gradient(red 25%, blue 75%);content:"hello" " world";width:calc(10px - 20px)}'); +}); + test('url', () => { // https://www.npmjs.com/package/postcss-url let res = transform({ @@ -1108,4 +1170,119 @@ test('visit stylesheet', () => { assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}'); }); +test('visitor function', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @dep "foo.js"; + + .foo { + width: 32px; + } + `), + visitor: ({addDependency}) => ({ + Rule: { + unknown: { + dep(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }) + }); + + assert.equal(res.code.toString(), '.foo{width:32px}'); + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'foo.js' + }]); +}); + +test('visitor function works with style attributes', () => { + let res = transformStyleAttribute({ + filename: 'test.css', + minify: true, + code: Buffer.from('height: 12px'), + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'test.json' + }]); +}); + +test('visitor function works with bundler', () => { + let res = bundle({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + +test('works with async bundler', async () => { + let res = await bundleAsync({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + test.run(); diff --git a/package.json b/package.json index b4c1f4bff..af17c0a58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.29.3", + "version": "1.31.1", "license": "MPL-2.0", "description": "A CSS parser, transformer, and minifier written in Rust", "main": "node/index.js", @@ -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.3", + "@mdn/browser-compat-data": "~7.2.4", "@napi-rs/cli": "^2.14.0", - "autoprefixer": "^10.4.21", - "caniuse-lite": "^1.0.30001704", + "autoprefixer": "^10.4.23", + "caniuse-lite": "^1.0.30001765", "codemirror": "^6.0.1", "cssnano": "^7.0.6", "esbuild": "^0.19.8", diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 80afd2d32..1a2165581 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.83.0" +channel = "1.92.0" components = ["rustfmt", "clippy"] diff --git a/scripts/build-npm.js b/scripts/build-npm.js index ca89d669e..ef447a7ea 100644 --- a/scripts/build-npm.js +++ b/scripts/build-npm.js @@ -38,6 +38,9 @@ const triples = [ }, { name: 'x86_64-unknown-freebsd' + }, + { + name: 'aarch64-linux-android' } ]; const cpuToNodeArch = { @@ -51,6 +54,7 @@ const sysToNodePlatform = { freebsd: 'freebsd', darwin: 'darwin', windows: 'win32', + android: 'android' }; let optionalDependencies = {}; diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js index 32951a639..3a9f7945f 100644 --- a/scripts/build-prefixes.js +++ b/scripts/build-prefixes.js @@ -71,6 +71,10 @@ prefixes['any-pseudo'] = { }) } +// Safari 4-13 supports background-clip: text with a prefix. +prefixes['background-clip'].browsers.push('safari 13'); +prefixes['background-clip'].browsers.push('ios_saf 4', 'ios_saf 13'); + let flexSpec = {}; let oldGradient = {}; let p = new Map(); @@ -334,6 +338,25 @@ 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, + grammarError: mdn.css.selectors['grammar-error'].__compat.support, + spellingError: mdn.css.selectors['spelling-error'].__compat.support, + statePseudoClass: Object.fromEntries( + Object.entries(mdn.css.selectors.state.__compat.support) + .map(([browser, value]) => { + // Chrome/Edge 90-124 supported old :--foo syntax which was removed. + // Only include full :state(foo) support from 125+. + if (Array.isArray(value)) { + value = value.filter(v => !v.partial_implementation) + } else if (value.partial_implementation) { + value = undefined; + } + + return [browser, value]; + }) + ), }; for (let key in mdn.css.types.length) { @@ -381,7 +404,7 @@ for (let key in mdn.css.properties['list-style-type']) { } for (let key in mdn.css.properties['width']) { - if (key === '__compat' || key === 'animatable') { + if (key === '__compat' || key === 'is_animatable') { continue; } @@ -642,7 +665,10 @@ impl Feature { if self.is_compatible(browsers) { return true } - browsers.${browser} = None; + #[allow(unused_assignments)] + { + browsers.${browser} = None; + } }\n`).join(' ')} false } diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index 2253b7973..824b2f2bd 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parcel_selectors" -version = "0.28.1" +version = "0.28.2" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/parcel_selectors/" description = "CSS Selectors matching for Rust - forked for lightningcss" diff --git a/selectors/parser.rs b/selectors/parser.rs index 85c118bc3..19563d5f9 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -880,12 +880,12 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> { /// Returns an iterator over the entire sequence of simple selectors and /// combinators, in matching order (from right to left). #[inline] - pub fn iter_raw_match_order(&self) -> slice::Iter> { + pub fn iter_raw_match_order(&self) -> slice::Iter<'_, Component<'i, Impl>> { self.1.iter() } #[inline] - pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut> { + pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut<'_, Component<'i, Impl>> { self.1.iter_mut() } @@ -903,7 +903,7 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> { /// combinators, in parse order (from left to right), starting from /// `offset`. #[inline] - pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev>> { + pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev>> { self.1[..self.len() - offset].iter().rev() } @@ -3932,6 +3932,17 @@ 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()); + + assert!(parse("::grammar-error").is_ok()); + assert!(parse("::spelling-error").is_ok()); + assert!(parse("::part(mypart)::grammar-error").is_ok()); + assert!(parse("::part(mypart)::spelling-error").is_ok()); } #[test] diff --git a/src/bundler.rs b/src/bundler.rs index 00988487c..e5009a863 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -79,7 +79,7 @@ enum AtRuleParserValue<'a, T> { struct BundleStyleSheet<'i, 'o, T> { stylesheet: Option>, - dependencies: Vec, + dependencies: Vec, css_modules_deps: Vec, parent_source_index: u32, parent_dep_index: u32, @@ -89,6 +89,33 @@ struct BundleStyleSheet<'i, 'o, T> { loc: Location, } +#[derive(Debug, Clone)] +enum Dependency { + File(u32), + External(String), +} + +/// The result of [SourceProvider::resolve]. +#[derive(Debug)] +#[cfg_attr( + any(feature = "serde", feature = "nodejs"), + derive(serde::Deserialize), + serde(rename_all = "lowercase") +)] +pub enum ResolveResult { + /// An external URL. + External(String), + /// A file path. + #[serde(untagged)] + File(PathBuf), +} + +impl From for ResolveResult { + fn from(path: PathBuf) -> Self { + ResolveResult::File(path) + } +} + /// A trait to provide the contents of files to a Bundler. /// /// See [FileProvider](FileProvider) for an implementation that uses the @@ -102,7 +129,7 @@ pub trait SourceProvider: Send + Sync { /// Resolves the given import specifier to a file path given the file /// which the import originated from. - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result; + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result; } /// Provides an implementation of [SourceProvider](SourceProvider) @@ -136,9 +163,9 @@ impl SourceProvider for FileProvider { Ok(unsafe { &*ptr }) } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { // Assume the specifier is a relative file path and join it with current path. - Ok(originating_file.with_file_name(specifier)) + Ok(originating_file.with_file_name(specifier).into()) } } @@ -162,6 +189,11 @@ pub enum BundleErrorKind<'i, T: std::error::Error> { UnsupportedLayerCombination, /// Unsupported media query boolean logic was encountered. UnsupportedMediaBooleanLogic, + /// An external module was referenced with a CSS module "from" clause. + ReferencedExternalModuleWithCssModuleFrom, + /// An external `@import` was found after a bundled `@import`. + /// This may result in unintended selector order. + ExternalImportAfterBundledImport, /// A custom resolver error. ResolverError(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] T), } @@ -183,6 +215,13 @@ impl<'i, T: std::error::Error> std::fmt::Display for BundleErrorKind<'i, T> { UnsupportedImportCondition => write!(f, "Unsupported import condition"), UnsupportedLayerCombination => write!(f, "Unsupported layer combination in @import"), UnsupportedMediaBooleanLogic => write!(f, "Unsupported boolean logic in @import media query"), + ReferencedExternalModuleWithCssModuleFrom => { + write!(f, "Referenced external module with CSS module \"from\" clause") + } + ExternalImportAfterBundledImport => write!( + f, + "An external `@import` was found after a bundled `@import`. This may result in unintended selector order." + ), ResolverError(err) => std::fmt::Display::fmt(&err, f), } } @@ -265,7 +304,7 @@ where // Phase 3: concatenate. let mut rules: Vec> = Vec::new(); - self.inline(&mut rules); + self.inline(&mut rules)?; let sources = self .stylesheets @@ -428,7 +467,7 @@ where } // Collect and load dependencies for this stylesheet in parallel. - let dependencies: Result, _> = stylesheet + let dependencies: Result, _> = stylesheet .rules .0 .par_iter_mut() @@ -484,16 +523,19 @@ where }; let result = match self.fs.resolve(&specifier, file) { - Ok(path) => self.load_file( - &path, - ImportRule { - layer, - media, - supports: combine_supports(rule.supports.clone(), &import.supports), - url: "".into(), - loc: import.loc, - }, - ), + Ok(ResolveResult::File(path)) => self + .load_file( + &path, + ImportRule { + layer, + media, + supports: combine_supports(rule.supports.clone(), &import.supports), + url: "".into(), + loc: import.loc, + }, + ) + .map(Dependency::File), + Ok(ResolveResult::External(url)) => Ok(Dependency::External(url)), Err(err) => Err(Error { kind: BundleErrorKind::ResolverError(err), loc: Some(ErrorLocation::new( @@ -580,7 +622,7 @@ where ) -> Option>>> { if let Some(Specifier::File(f)) = specifier { let result = match self.fs.resolve(&f, file) { - Ok(path) => { + Ok(ResolveResult::File(path)) => { let res = self.load_file( &path, ImportRule { @@ -602,6 +644,13 @@ where res } + Ok(ResolveResult::External(_)) => Err(Error { + kind: BundleErrorKind::ReferencedExternalModuleWithCssModuleFrom, + loc: Some(ErrorLocation::new( + style_loc, + self.find_filename(style_loc.source_index), + )), + }), Err(err) => Err(Error { kind: BundleErrorKind::ResolverError(err), loc: Some(ErrorLocation::new( @@ -646,7 +695,9 @@ where } for i in 0..stylesheets[source_index as usize].dependencies.len() { - let dep_source_index = stylesheets[source_index as usize].dependencies[i]; + let Dependency::File(dep_source_index) = stylesheets[source_index as usize].dependencies[i] else { + continue; + }; let resolved = &mut stylesheets[dep_source_index as usize]; // In browsers, every instance of an @import is evaluated, so we preserve the last. @@ -659,14 +710,16 @@ where } } - fn inline(&mut self, dest: &mut Vec>) { - process(self.stylesheets.get_mut().unwrap(), 0, dest); - - fn process<'a, T>( + fn inline( + &mut self, + dest: &mut Vec>, + ) -> Result<(), Error>> { + fn process<'a, T, E: std::error::Error>( stylesheets: &mut Vec>, source_index: u32, dest: &mut Vec>, - ) { + filename: &String, + ) -> Result<(), Error>> { let stylesheet = &mut stylesheets[source_index as usize]; let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0); @@ -678,26 +731,47 @@ where // Include the dependency if this is the first instance as computed earlier. if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index as u32 { - process(stylesheets, dep_source_index, dest); + process(stylesheets, dep_source_index, dest, filename)?; } dep_index += 1; } let mut import_index = 0; + let mut has_bundled_import = false; for rule in &mut rules { match rule { - CssRule::Import(_) => { - let dep_source_index = stylesheets[source_index as usize].dependencies[import_index]; - let resolved = &stylesheets[dep_source_index as usize]; - - // Include the dependency if this is the last instance as computed earlier. - if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index { - process(stylesheets, dep_source_index, dest); + CssRule::Import(import_rule) => { + let dep_source = &stylesheets[source_index as usize].dependencies[import_index]; + match dep_source { + Dependency::File(dep_source_index) => { + let resolved = &stylesheets[*dep_source_index as usize]; + + // Include the dependency if this is the last instance as computed earlier. + if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index { + has_bundled_import = true; + process(stylesheets, *dep_source_index, dest, filename)?; + } + + *rule = CssRule::Ignored; + dep_index += 1; + } + Dependency::External(url) => { + if has_bundled_import { + return Err(Error { + kind: BundleErrorKind::ExternalImportAfterBundledImport, + loc: Some(ErrorLocation { + filename: filename.clone(), + line: import_rule.loc.line, + column: import_rule.loc.column, + }), + }); + } + import_rule.url = url.to_owned().into(); + let imp = std::mem::replace(rule, CssRule::Ignored); + dest.push(imp); + } } - - *rule = CssRule::Ignored; - dep_index += 1; import_index += 1; } CssRule::LayerStatement(_) => { @@ -739,7 +813,10 @@ where } dest.extend(rules); + Ok(()) } + + process(self.stylesheets.get_mut().unwrap(), 0, dest, &self.options.filename) } } @@ -823,8 +900,12 @@ mod tests { Ok(self.map.get(file).unwrap()) } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { - Ok(originating_file.with_file_name(specifier)) + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + if specifier.starts_with("https:") { + Ok(ResolveResult::External(specifier.to_owned())) + } else { + Ok(originating_file.with_file_name(specifier).into()) + } } } @@ -843,9 +924,9 @@ mod tests { /// Resolve by stripping a `foo:` prefix off any import. Specifiers without /// this prefix fail with an error. - fn resolve(&self, specifier: &str, _originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, _originating_file: &Path) -> Result { if specifier.starts_with("foo:") { - Ok(Path::new(&specifier["foo:".len()..]).to_path_buf()) + Ok(Path::new(&specifier["foo:".len()..]).to_path_buf().into()) } else { let err = std::io::Error::new( std::io::ErrorKind::NotFound, @@ -1548,6 +1629,49 @@ mod tests { "#} ); + let res = bundle( + TestProvider { + map: fs! { + "/a.css": r#" + @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); + @import './b.css'; + "#, + "/b.css": r#" + .b { color: green } + "# + }, + }, + "/a.css", + ); + assert_eq!( + res, + indoc! { r#" + @import "https://fonts.googleapis.com/css2?family=Roboto&display=swap"; + + .b { + color: green; + } + "#} + ); + + error_test( + TestProvider { + map: fs! { + "/a.css": r#" + @import './b.css'; + @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); + "#, + "/b.css": r#" + .b { color: green } + "# + }, + }, + "/a.css", + Some(Box::new(|err| { + assert!(matches!(err, BundleErrorKind::ExternalImportAfterBundledImport)); + })), + ); + error_test( TestProvider { map: fs! { diff --git a/src/compat.rs b/src/compat.rs index 4c237c4ec..f0c828d75 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -28,6 +28,7 @@ pub enum Feature { CapUnit, CaseInsensitive, ChUnit, + Checkmark, CircleListStyleType, CjkDecimalListStyleType, CjkEarthlyBranchListStyleType, @@ -86,6 +87,7 @@ pub enum Feature { Gencontent, GeorgianListStyleType, GradientInterpolationHints, + GrammarError, GujaratiListStyleType, GurmukhiListStyleType, HasSelector, @@ -98,7 +100,6 @@ pub enum Feature { ImageSet, InOutOfRange, IndeterminatePseudo, - IsAnimatableSize, IsSelector, JapaneseFormalListStyleType, JapaneseInformalListStyleType, @@ -142,7 +143,6 @@ pub enum Feature { MinFunction, ModFunction, MongolianListStyleType, - MozAvailableSize, MyanmarListStyleType, Namespaces, Nesting, @@ -158,6 +158,8 @@ pub enum Feature { P3Colors, PartPseudo, PersianListStyleType, + Picker, + PickerIcon, PlaceContent, PlaceItems, PlaceSelf, @@ -187,7 +189,9 @@ pub enum Feature { SimpChineseInformalListStyleType, SomaliListStyleType, SpaceSeparatedColorNotation, + SpellingError, SquareListStyleType, + StatePseudoClass, StretchSize, StringListStyleType, SymbolsListStyleType, @@ -448,7 +452,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -540,7 +544,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -585,7 +589,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -630,7 +634,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -675,7 +679,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -720,7 +724,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -765,7 +769,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -810,7 +814,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -902,7 +906,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -947,7 +951,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1017,7 +1021,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1062,7 +1066,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1152,7 +1156,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1197,7 +1201,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1247,7 +1251,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1334,7 +1338,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1379,7 +1383,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1424,11 +1428,16 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } - if browsers.ie.is_some() || browsers.samsung.is_some() { + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -1464,7 +1473,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1509,7 +1518,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1554,7 +1563,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1621,7 +1630,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8716288 { + if version < 9371648 { return false; } } @@ -1634,7 +1643,7 @@ impl Feature { return false; } } - Feature::CustomMediaQueries | Feature::FitContentFunctionSize | Feature::StretchSize => return false, + Feature::CustomMediaQueries | Feature::FitContentFunctionSize => return false, Feature::DoublePositionGradients => { if let Some(version) = browsers.chrome { if version < 4653056 { @@ -2207,7 +2216,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 458752 { return false; } } @@ -2511,6 +2520,16 @@ impl Feature { return false; } } + if let Some(version) = browsers.safari { + if version < 1704448 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1704448 { + return false; + } + } if let Some(version) = browsers.samsung { if version < 917504 { return false; @@ -2521,7 +2540,7 @@ impl Feature { return false; } } - if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() { + if browsers.ie.is_some() { return false; } } @@ -2647,7 +2666,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -2927,12 +2946,12 @@ impl Feature { } Feature::AbsFunction | Feature::SignFunction => { if let Some(version) = browsers.chrome { - if version < 8847360 { + if version < 9043968 { return false; } } if let Some(version) = browsers.edge { - if version < 8847360 { + if version < 9043968 { return false; } } @@ -2941,6 +2960,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.opera { + if version < 5963776 { + return false; + } + } if let Some(version) = browsers.safari { if version < 984064 { return false; @@ -2952,11 +2976,11 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8847360 { + if version < 9043968 { return false; } } - if browsers.ie.is_some() || browsers.opera.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() || browsers.samsung.is_some() { return false; } } @@ -3487,6 +3511,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 9437184 { + return false; + } + } if let Some(version) = browsers.opera { if version < 4849664 { return false; @@ -3512,7 +3541,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -3527,6 +3556,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 9371648 { + return false; + } + } if let Some(version) = browsers.opera { if version < 5701632 { return false; @@ -3542,12 +3576,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1900544 { + 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() { + if browsers.ie.is_some() { return false; } } @@ -3596,6 +3635,159 @@ impl Feature { return false; } } + Feature::Picker => { + 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.opera { + if version < 5832704 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1900544 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8847360 { + return false; + } + } + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.safari.is_some() + { + return false; + } + } + Feature::PickerIcon | Feature::Checkmark => { + if let Some(version) = browsers.chrome { + if version < 8716288 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8716288 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5767168 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1900544 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8716288 { + return false; + } + } + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.safari.is_some() + { + return false; + } + } + Feature::GrammarError | Feature::SpellingError => { + if let Some(version) = browsers.chrome { + if version < 7929856 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7929856 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5308416 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1115136 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1115136 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7929856 { + return false; + } + } + if browsers.firefox.is_some() || browsers.ie.is_some() { + return false; + } + } + Feature::StatePseudoClass => { + if let Some(version) = browsers.chrome { + if version < 8192000 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8192000 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 8257536 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5439488 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1115136 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1115136 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1769472 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8192000 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } Feature::QUnit => { if let Some(version) = browsers.chrome { if version < 4128768 { @@ -3977,6 +4169,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 9633792 { + return false; + } + } if let Some(version) = browsers.opera { if version < 5177344 { return false; @@ -4002,7 +4199,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -4017,6 +4214,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 9633792 { + return false; + } + } if let Some(version) = browsers.opera { if version < 4915200 { return false; @@ -4042,7 +4244,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -4413,7 +4615,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4460,7 +4662,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4964,6 +5166,7 @@ impl Feature { | Feature::HiraganaIrohaListStyleType | Feature::KatakanaListStyleType | Feature::KatakanaIrohaListStyleType + | Feature::NoneListStyleType | Feature::AutoSize => { if let Some(version) = browsers.chrome { if version < 1179648 { @@ -5150,53 +5353,6 @@ impl Feature { } } } - Feature::NoneListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } Feature::SimpChineseFormalListStyleType | Feature::SimpChineseInformalListStyleType | Feature::TradChineseFormalListStyleType @@ -5319,67 +5475,33 @@ impl Feature { return false; } } - if let Some(version) = browsers.opera { - if version < 5439488 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1769472 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8192000 { - return false; - } - } - if browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.safari.is_some() - { - return false; - } - } - Feature::FitContentSize => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } if let Some(version) = browsers.firefox { - if version < 262144 { + if version < 9633792 { return false; } } if let Some(version) = browsers.opera { - if version < 917504 { + if version < 5439488 { return false; } } if let Some(version) = browsers.safari { - if version < 458752 { + if version < 1703936 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 458752 { + if version < 1703936 { return false; } } if let Some(version) = browsers.samsung { - if version < 66816 { + if version < 1769472 { return false; } } if let Some(version) = browsers.android { - if version < 263168 { + if version < 8192000 { return false; } } @@ -5387,24 +5509,19 @@ impl Feature { return false; } } - Feature::IsAnimatableSize => { + Feature::FitContentSize => { if let Some(version) = browsers.chrome { - if version < 1703936 { + if version < 1638400 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 5177344 { return false; } } if let Some(version) = browsers.firefox { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { + if version < 262144 { return false; } } @@ -5433,6 +5550,9 @@ impl Feature { return false; } } + if browsers.ie.is_some() { + return false; + } } Feature::MaxContentSize => { if let Some(version) = browsers.chrome { @@ -5524,49 +5644,54 @@ impl Feature { return false; } } - Feature::WebkitFillAvailableSize => { + Feature::StretchSize => { if let Some(version) = browsers.chrome { - if version < 1638400 { + if version < 9043968 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 9043968 { return false; } } if let Some(version) = browsers.opera { - if version < 917504 { + if version < 5963776 { return false; } } - if let Some(version) = browsers.safari { - if version < 458752 { + if let Some(version) = browsers.android { + if version < 9043968 { return false; } } - if let Some(version) = browsers.ios_saf { - if version < 458752 { + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.safari.is_some() + || browsers.samsung.is_some() + { + return false; + } + } + Feature::WebkitFillAvailableSize => { + if let Some(version) = browsers.firefox { + if version < 9568256 { return false; } } - if let Some(version) = browsers.samsung { - if version < 327680 { + if let Some(version) = browsers.safari { + if version < 458752 { return false; } } - if let Some(version) = browsers.android { - if version < 263168 { + if let Some(version) = browsers.ios_saf { + if version < 458752 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::MozAvailableSize => { - if let Some(version) = browsers.firefox { - if version < 262144 { + if let Some(version) = browsers.samsung { + if version < 327680 { return false; } } @@ -5574,10 +5699,7 @@ impl Feature { || browsers.chrome.is_some() || browsers.edge.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; } @@ -5615,63 +5737,90 @@ impl Feature { if self.is_compatible(browsers) { return true; } - browsers.android = None; + #[allow(unused_assignments)] + { + browsers.android = None; + } } if targets.chrome.is_some() { browsers.chrome = targets.chrome; if self.is_compatible(browsers) { return true; } - browsers.chrome = None; + #[allow(unused_assignments)] + { + browsers.chrome = None; + } } if targets.edge.is_some() { browsers.edge = targets.edge; if self.is_compatible(browsers) { return true; } - browsers.edge = None; + #[allow(unused_assignments)] + { + browsers.edge = None; + } } if targets.firefox.is_some() { browsers.firefox = targets.firefox; if self.is_compatible(browsers) { return true; } - browsers.firefox = None; + #[allow(unused_assignments)] + { + browsers.firefox = None; + } } if targets.ie.is_some() { browsers.ie = targets.ie; if self.is_compatible(browsers) { return true; } - browsers.ie = None; + #[allow(unused_assignments)] + { + browsers.ie = None; + } } if targets.ios_saf.is_some() { browsers.ios_saf = targets.ios_saf; if self.is_compatible(browsers) { return true; } - browsers.ios_saf = None; + #[allow(unused_assignments)] + { + browsers.ios_saf = None; + } } if targets.opera.is_some() { browsers.opera = targets.opera; if self.is_compatible(browsers) { return true; } - browsers.opera = None; + #[allow(unused_assignments)] + { + browsers.opera = None; + } } if targets.safari.is_some() { browsers.safari = targets.safari; if self.is_compatible(browsers) { return true; } - browsers.safari = None; + #[allow(unused_assignments)] + { + browsers.safari = None; + } } if targets.samsung.is_some() { browsers.samsung = targets.samsung; if self.is_compatible(browsers) { return true; } - browsers.samsung = None; + #[allow(unused_assignments)] + { + browsers.samsung = None; + } } false diff --git a/src/error.rs b/src/error.rs index 4cca10692..b4b1b0ab8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -84,6 +84,8 @@ pub enum ParserError<'i> { InvalidDeclaration, /// A media query was invalid. InvalidMediaQuery, + /// The brackets in a condition cannot be empty. + EmptyBracketInCondition, /// Invalid CSS nesting. InvalidNesting, /// The @nest rule is deprecated. @@ -118,6 +120,7 @@ impl<'i> fmt::Display for ParserError<'i> { EndOfInput => write!(f, "Unexpected end of input"), InvalidDeclaration => write!(f, "Invalid declaration"), InvalidMediaQuery => write!(f, "Invalid media query"), + EmptyBracketInCondition => write!(f, "The brackets cannot be empty"), InvalidNesting => write!(f, "Invalid nesting"), DeprecatedNestRule => write!(f, "The @nest rule is deprecated"), DeprecatedCssModulesValueRule => write!(f, "The @value rule is deprecated"), diff --git a/src/lib.rs b/src/lib.rs index c89bbd764..2a41655ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,13 @@ mod tests { assert_eq!(res.code, expected); } + fn test_with_printer_options<'i, 'o>(source: &'i str, expected: &'i str, options: PrinterOptions<'o>) { + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet.to_css(options).unwrap(); + assert_eq!(res.code, expected); + } + fn minify_test(source: &str, expected: &str) { minify_test_with_options(source, expected, ParserOptions::default()) } @@ -237,20 +244,23 @@ mod tests { } } - fn error_recovery_test(source: &str) { + fn error_recovery_test(source: &str) -> Vec>> { 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:?}"), + { + 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:?}"), + } } + Arc::into_inner(warnings).unwrap().into_inner().unwrap() } fn css_modules_error_test(source: &str, error: ParserError) { @@ -385,6 +395,122 @@ mod tests { ); } + #[test] + pub fn test_math_fn() { + // max() + minify_test( + r#" + .foo { + color: rgb(max(255, 100), 0, 0); + } + "#, + indoc! {".foo{color:red}" + }, + ); + // min() + minify_test( + r#" + .foo { + color: rgb(min(255, 500), 0, 0); + } + "#, + indoc! {".foo{color:red}" + }, + ); + // abs() + minify_test( + r#" + .foo { + color: rgb(abs(-255), 0, 0); + } + "#, + indoc! {".foo{color:red}" + }, + ); + // clamp() + minify_test( + r#" + .foo { + flex: clamp(1, 5.20, 20); + color: rgb(clamp(0, 255, 300), 0, 0); + } + "#, + indoc! {".foo{color:red;flex:5.2}" + }, + ); + // round() + minify_test( + r#" + .round-color { + color: rgb(round(down, 255.6, 1), 0, 0); + } + "#, + indoc! {".round-color{color:red}" + }, + ); + // hypot() + minify_test( + r#" + .hypot-color { + color: rgb(hypot(255, 0), 0, 0); + } + "#, + indoc! {".hypot-color{color:red}" + }, + ); + // sign(), sign(50) = 1 + minify_test( + r#" + .sign-color { + color: rgb(sign(50), 0, 0); + } + "#, + indoc! {".sign-color{color:#010000}" + }, + ); + // rem(), rem(21, 2) = 1 + minify_test( + r#" + .rem-color { + color: rgb(rem(21, 2), 0, 0); + } + "#, + indoc! {".rem-color{color:#010000}" + }, + ); + // max() in width + minify_test( + r#" + .foo { + width: max(200px, 5px); + } + "#, + indoc! {".foo{width:200px}" + }, + ); + // max() in opacity + minify_test( + r#" + .foo { + opacity: max(1, 0.2); + filter: invert(min(1, 0.5)); + } + "#, + indoc! {".foo{opacity:1;filter:invert(.5)}" + }, + ); + // TODO: support calc in Integer + // minify_test( + // r#" + // .foo { + // z-index: max(100, 20); + // } + // "#, + // indoc! {".foo{z-index:100}" + // }, + // ); + } + #[test] pub fn test_border() { test( @@ -2195,7 +2321,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 +2342,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 +2364,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 +2386,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)); } @@ -4188,12 +4314,24 @@ mod tests { ); minify_test( ".foo { background-position: left 10px center }", - ".foo{background-position:10px 50%}", + ".foo{background-position:10px}", ); minify_test( ".foo { background-position: right 10px center }", ".foo{background-position:right 10px center}", ); + minify_test( + ".foo { background-position: center top 10px }", + ".foo{background-position:50% 10px}", + ); + minify_test( + ".foo { background-position: center bottom 10px }", + ".foo{background-position:center bottom 10px}", + ); + minify_test( + ".foo { background-position: center 10px }", + ".foo{background-position:50% 10px}", + ); minify_test( ".foo { background-position: right 10px top 20px }", ".foo{background-position:right 10px top 20px}", @@ -4214,6 +4352,26 @@ mod tests { ".foo { background-position: bottom right }", ".foo{background-position:100% 100%}", ); + minify_test( + ".foo { background-position: center top }", + ".foo{background-position:top}", + ); + minify_test( + ".foo { background-position: center bottom }", + ".foo{background-position:bottom}", + ); + minify_test( + ".foo { background-position: left center }", + ".foo{background-position:0}", + ); + minify_test( + ".foo { background-position: right center }", + ".foo{background-position:100%}", + ); + minify_test( + ".foo { background-position: 20px center }", + ".foo{background-position:20px}", + ); minify_test( ".foo { background: url('img-sprite.png') no-repeat bottom right }", @@ -6965,6 +7123,10 @@ mod tests { &format!(":root::{}(.foo.bar) {{position: fixed}}", name), &format!(":root::{}(.foo.bar){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}( .foo.bar ) {{position: fixed}}", name), + &format!(":root::{}(.foo.bar){{position:fixed}}", name), + ); error_test( &format!(":root::{}(foo):first-child {{position: fixed}}", name), ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement), @@ -6998,7 +7160,56 @@ mod tests { ParserError::SelectorError(SelectorError::InvalidState), ); } + minify_test( + "wa-checkbox:state(disabled) {color:red}", + "wa-checkbox:state(disabled){color:red}", + ); + minify_test( + "button:state(checked) {background:blue}", + "button:state(checked){background:#00f}", + ); + minify_test( + "input:state(custom-state) {border:1px solid}", + "input:state(custom-state){border:1px solid}", + ); + minify_test( + "button:active:not(:state(disabled))::part(control) {border:1px solid}", + "button:active:not(:state(disabled))::part(control){border:1px solid}", + ); + // Test nested CSS with :state() selector + nesting_test( + r#" + custom-element { + color: blue; + &:state(loading) { + opacity: 0.5; + & .spinner { + display: block; + } + } + &:state(error) { + border: 2px solid red; + } + } + "#, + indoc! {r#" + custom-element { + color: #00f; + } + + custom-element:state(loading) { + opacity: .5; + } + custom-element:state(loading) .spinner { + display: block; + } + + custom-element:state(error) { + border: 2px solid red; + } + "#}, + ); minify_test(".foo ::deep .bar {width: 20px}", ".foo ::deep .bar{width:20px}"); minify_test(".foo::deep .bar {width: 20px}", ".foo::deep .bar{width:20px}"); minify_test(".foo ::deep.bar {width: 20px}", ".foo ::deep.bar{width:20px}"); @@ -8013,7 +8224,22 @@ mod tests { ); minify_test( ".foo { width: calc(100% - 2 (2 * var(--card-margin))); }", - ".foo{width:calc(100% - 2 (2*var(--card-margin)))}", + ".foo{width:calc(100% - 2 (2 * var(--card-margin)))}", + ); + + test( + indoc! {r#" + .test { + width: calc(var(--test) + 2px); + width: calc(var(--test) - 2px); + } + "#}, + indoc! {r#" + .test { + width: calc(var(--test) + 2px); + width: calc(var(--test) - 2px); + } + "#}, ); } @@ -8055,13 +8281,13 @@ mod tests { minify_test(".foo { rotate: acos(cos(45deg))", ".foo{rotate:45deg}"); minify_test(".foo { rotate: acos(-1)", ".foo{rotate:180deg}"); minify_test(".foo { rotate: acos(0)", ".foo{rotate:90deg}"); - minify_test(".foo { rotate: acos(1)", ".foo{rotate:none}"); + minify_test(".foo { rotate: acos(1)", ".foo{rotate:0deg}"); minify_test(".foo { rotate: acos(45deg)", ".foo{rotate:acos(45deg)}"); // invalid minify_test(".foo { rotate: acos(-20)", ".foo{rotate:acos(-20)}"); // evaluates to NaN minify_test(".foo { rotate: atan(tan(45deg))", ".foo{rotate:45deg}"); minify_test(".foo { rotate: atan(1)", ".foo{rotate:45deg}"); - minify_test(".foo { rotate: atan(0)", ".foo{rotate:none}"); + minify_test(".foo { rotate: atan(0)", ".foo{rotate:0deg}"); minify_test(".foo { rotate: atan(45deg)", ".foo{rotate:atan(45deg)}"); // invalid minify_test(".foo { rotate: atan2(1px, -1px)", ".foo{rotate:135deg}"); @@ -8074,7 +8300,10 @@ mod tests { minify_test(".foo { rotate: atan2(0, -1)", ".foo{rotate:180deg}"); minify_test(".foo { rotate: atan2(-1, 1)", ".foo{rotate:-45deg}"); // incompatible units - minify_test(".foo { rotate: atan2(1px, -1vw)", ".foo{rotate:atan2(1px,-1vw)}"); + minify_test(".foo { rotate: atan2(1px, -1vw)", ".foo{rotate:atan2(1px, -1vw)}"); + + minify_test(".foo { transform: rotate(acos(1)) }", ".foo{transform:rotate(0)}"); + minify_test(".foo { transform: rotate(atan(0)) }", ".foo{transform:rotate(0)}"); } #[test] @@ -8106,7 +8335,10 @@ mod tests { minify_test(".foo { width: abs(1%)", ".foo{width:abs(1%)}"); // spec says percentages must be against resolved value minify_test(".foo { width: calc(10px * sign(-1vw)", ".foo{width:-10px}"); - minify_test(".foo { width: calc(10px * sign(1%)", ".foo{width:calc(10px*sign(1%))}"); + minify_test( + ".foo { width: calc(10px * sign(1%)", + ".foo{width:calc(10px * sign(1%))}", + ); } #[test] @@ -9064,6 +9296,31 @@ mod tests { }, ); + test_with_printer_options( + r#" + @media (width < 256px) or (hover: none) { + .foo { + color: #fff; + } + } + "#, + indoc! { r#" + @media (not (min-width: 256px)) or (hover: none) { + .foo { + color: #fff; + } + } + "#}, + PrinterOptions { + targets: Targets { + browsers: None, + include: Features::MediaRangeSyntax, + exclude: Features::empty(), + }, + ..Default::default() + }, + ); + error_test( "@media (min-width: hi) { .foo { color: chartreuse }}", ParserError::InvalidMediaQuery, @@ -9108,6 +9365,16 @@ mod tests { "@media (prefers-color-scheme = dark) { .foo { color: chartreuse }}", ParserError::InvalidMediaQuery, ); + error_test( + "@media unknown(foo) {}", + ParserError::UnexpectedToken(crate::properties::custom::Token::Function("unknown".into())), + ); + + // empty brackets should return a clearer error message + error_test("@media () {}", ParserError::EmptyBracketInCondition); + error_test("@media screen and () {}", ParserError::EmptyBracketInCondition); + + error_recovery_test("@media unknown(foo) {}"); } #[test] @@ -9309,6 +9576,29 @@ mod tests { @layer b, c; "#}, ); + + test( + r#" + @layer a; + @import "foo.css"; + + @layer a { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a; + @import "foo.css"; + + @layer a { + foo { + color: red; + } + } + "#}, + ); } #[test] @@ -12343,6 +12633,35 @@ mod tests { #[test] fn test_transform() { + test( + ".foo { transform: perspective(500px)translate3d(10px, 0, 20px)rotateY(30deg) }", + indoc! {r#" + .foo { + transform: perspective(500px) translate3d(10px, 0, 20px) rotateY(30deg); + } + "#}, + ); + test( + ".foo { transform: translate3d(12px,50%,3em)scale(2,.5) }", + indoc! {r#" + .foo { + transform: translate3d(12px, 50%, 3em) scale(2, .5); + } + "#}, + ); + test( + ".foo { transform:matrix(1,2,-1,1,80,80) }", + indoc! {r#" + .foo { + transform: matrix(1, 2, -1, 1, 80, 80); + } + "#}, + ); + + minify_test( + ".foo { transform: scale( 0.5 )translateX(10px ) }", + ".foo{transform:scale(.5)translate(10px)}", + ); minify_test( ".foo { transform: translate(2px, 3px)", ".foo{transform:translate(2px,3px)}", @@ -12545,16 +12864,31 @@ mod tests { minify_test(".foo { translate: 1px 2px 0px }", ".foo{translate:1px 2px}"); minify_test(".foo { translate: 1px 0px 2px }", ".foo{translate:1px 0 2px}"); minify_test(".foo { translate: none }", ".foo{translate:none}"); + minify_test(".foo { rotate: none }", ".foo{rotate:none}"); + minify_test(".foo { rotate: 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: -0deg }", ".foo{rotate:0deg}"); minify_test(".foo { rotate: 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: z 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: 0 0 1 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: x 10deg }", ".foo{rotate:x 10deg}"); minify_test(".foo { rotate: 1 0 0 10deg }", ".foo{rotate:x 10deg}"); - minify_test(".foo { rotate: y 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: 2 0 0 10deg }", ".foo{rotate:x 10deg}"); + minify_test(".foo { rotate: 0 2 0 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: 0 0 2 10deg }", ".foo{rotate:10deg}"); + minify_test(".foo { rotate: 0 0 5.3 10deg }", ".foo{rotate:10deg}"); + minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 10deg 0 0 -1 }", ".foo{rotate:-10deg}"); + minify_test(".foo { rotate: 10deg 0 0 -233 }", ".foo{rotate:-10deg}"); + minify_test(".foo { rotate: -1 0 0 0deg }", ".foo{rotate:x 0deg}"); + minify_test(".foo { rotate: 0deg 0 0 1 }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 0deg 0 0 -1 }", ".foo{rotate:0deg}"); minify_test(".foo { rotate: 0 1 0 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: x 0rad }", ".foo{rotate:x 0deg}"); + // TODO: In minify mode, convert units to the shortest form. + // minify_test(".foo { rotate: y 0turn }", ".foo{rotate:y 0deg}"); + minify_test(".foo { rotate: z 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 10deg y }", ".foo{rotate:y 10deg}"); minify_test(".foo { rotate: 1 1 1 10deg }", ".foo{rotate:1 1 1 10deg}"); - minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:none}"); - minify_test(".foo { rotate: none }", ".foo{rotate:none}"); minify_test(".foo { scale: 1 }", ".foo{scale:1}"); minify_test(".foo { scale: 1 1 }", ".foo{scale:1}"); minify_test(".foo { scale: 1 1 1 }", ".foo{scale:1}"); @@ -12978,7 +13312,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 +13330,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 +13348,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 +13366,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 +13384,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 +13402,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 +13420,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 +13454,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 +13578,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 +13660,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 +13676,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 +13747,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 +13763,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,37 +13799,388 @@ mod tests { ..Browsers::default() }, ); - } - - #[test] - fn test_font_face() { - minify_test( - r#"@font-face { - src: url("test.woff"); - font-family: "Helvetica"; - font-weight: bold; - font-style: italic; - }"#, - "@font-face{src:url(test.woff);font-family:Helvetica;font-weight:700;font-style:italic}", - ); - minify_test("@font-face {src: url(test.woff);}", "@font-face{src:url(test.woff)}"); - minify_test("@font-face {src: local(\"Test\");}", "@font-face{src:local(Test)}"); - minify_test( - "@font-face {src: local(\"Foo Bar\");}", - "@font-face{src:local(Foo Bar)}", - ); - minify_test("@font-face {src: local(Test);}", "@font-face{src:local(Test)}"); - minify_test("@font-face {src: local(Foo Bar);}", "@font-face{src:local(Foo Bar)}"); - minify_test( - "@font-face {src: url(\"test.woff\") format(woff);}", - "@font-face{src:url(test.woff)format(\"woff\")}", - ); - minify_test( - "@font-face {src: url(\"test.ttc\") format(collection), url(test.ttf) format(truetype);}", - "@font-face{src:url(test.ttc)format(\"collection\"),url(test.ttf)format(\"truetype\")}", - ); - minify_test( + // 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] + fn test_font_face() { + minify_test( + r#"@font-face { + src: url("test.woff"); + font-family: "Helvetica"; + font-weight: bold; + font-style: italic; + }"#, + "@font-face{src:url(test.woff);font-family:Helvetica;font-weight:700;font-style:italic}", + ); + minify_test("@font-face {src: url(test.woff);}", "@font-face{src:url(test.woff)}"); + minify_test("@font-face {src: local(\"Test\");}", "@font-face{src:local(Test)}"); + minify_test( + "@font-face {src: local(\"Foo Bar\");}", + "@font-face{src:local(Foo Bar)}", + ); + minify_test("@font-face {src: local(Test);}", "@font-face{src:local(Test)}"); + minify_test("@font-face {src: local(Foo Bar);}", "@font-face{src:local(Foo Bar)}"); + + minify_test( + "@font-face {src: url(\"test.woff\") format(woff);}", + "@font-face{src:url(test.woff)format(\"woff\")}", + ); + minify_test( + "@font-face {src: url(\"test.ttc\") format(collection), url(test.ttf) format(truetype);}", + "@font-face{src:url(test.ttc)format(\"collection\"),url(test.ttf)format(\"truetype\")}", + ); + minify_test( "@font-face {src: url(\"test.otf\") format(opentype) tech(features-aat);}", "@font-face{src:url(test.otf)format(\"opentype\")tech(features-aat)}", ); @@ -13535,7 +14220,7 @@ mod tests { // ref: https://github.com/parcel-bundler/lightningcss/pull/255#issuecomment-1219049998 minify_test( "@font-face {src: url(\"foo.ttf\") tech(palettes color-colrv0 variations) format(opentype);}", - "@font-face{src:url(foo.ttf) tech(palettes color-colrv0 variations)format(opentype)}", + "@font-face{src:url(foo.ttf) tech(palettes color-colrv0 variations) format(opentype)}", ); // TODO(CGQAQ): make this test pass when we have strict mode // ref: https://github.com/web-platform-tests/wpt/blob/9f8a6ccc41aa725e8f51f4f096f686313bb88d8d/css/css-fonts/parsing/font-face-src-tech.html#L45 @@ -13557,7 +14242,7 @@ mod tests { // ); minify_test( "@font-face {src: local(\"\") url(\"test.woff\");}", - "@font-face{src:local(\"\")url(test.woff)}", + "@font-face{src:local(\"\") url(test.woff)}", ); minify_test("@font-face {font-weight: 200 400}", "@font-face{font-weight:200 400}"); minify_test("@font-face {font-weight: 400 400}", "@font-face{font-weight:400}"); @@ -13655,7 +14340,7 @@ mod tests { font-family: Handover Sans; base-palette: 3; override-colors: 1 rgb(43, 12, 9), 3 var(--highlight); - }"#, "@font-palette-values --Cooler{font-family:Handover Sans;base-palette:3;override-colors:1 #2b0c09,3 var(--highlight)}"); + }"#, "@font-palette-values --Cooler{font-family:Handover Sans;base-palette:3;override-colors:1 #2b0c09, 3 var(--highlight)}"); prefix_test( r#"@font-palette-values --Cooler { font-family: Handover Sans; @@ -14427,6 +15112,8 @@ mod tests { "@layer foo; @import url(foo.css); @layer bar; @import url(bar.css)", ParserError::UnexpectedImportRule, ); + let warnings = error_recovery_test("@import './actual-styles.css';"); + assert_eq!(warnings, vec![]); } #[test] @@ -16872,7 +17559,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)); } @@ -17163,6 +17850,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}"); @@ -17174,12 +17862,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)}", @@ -17192,6 +17889,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)}", @@ -17212,10 +17917,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)}", @@ -18080,7 +18801,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))", @@ -18096,12 +18817,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))}", @@ -18215,21 +18947,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). @@ -18353,17 +19079,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)", ); @@ -18491,17 +19229,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)", ); @@ -18954,7 +19704,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), @@ -18962,15 +19716,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), @@ -18981,14 +19739,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(). @@ -19850,13 +20608,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}", ); } } @@ -19959,19 +20714,19 @@ mod tests { ); minify_test( ".foo { color: color-mix(in srgb, currentColor, blue); }", - ".foo{color:color-mix(in srgb,currentColor,blue)}", + ".foo{color:color-mix(in srgb, currentColor, blue)}", ); minify_test( ".foo { color: color-mix(in srgb, blue, currentColor); }", - ".foo{color:color-mix(in srgb,blue,currentColor)}", + ".foo{color:color-mix(in srgb, blue, currentColor)}", ); minify_test( ".foo { color: color-mix(in srgb, accentcolor, blue); }", - ".foo{color:color-mix(in srgb,accentcolor,blue)}", + ".foo{color:color-mix(in srgb, accentcolor, blue)}", ); minify_test( ".foo { color: color-mix(in srgb, blue, accentcolor); }", - ".foo{color:color-mix(in srgb,blue,accentcolor)}", + ".foo{color:color-mix(in srgb, blue, accentcolor)}", ); // regex for converting web platform tests: @@ -21339,7 +22094,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -21487,71 +22241,279 @@ mod tests { ".foo{grid-template-areas:\"head head\"\"nav main\"\". .\"}", ); + // to grid-* shorthand + minify_test( + r#" + .test-miss-areas { + grid-template-columns: 1fr 90px; + grid-template-rows: auto 80px; + grid-template-areas: "one"; + } + "#, + ".test-miss-areas{grid-template:\"one\"\".\"80px/1fr 90px}", + ); test( r#" - .foo { - grid-template-areas: "head head" "nav main" "foot ...."; + .test-miss-areas-2 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 30px 60px 100px; + grid-template-areas: "a a a" "b c c"; } "#, indoc! { r#" - .foo { - grid-template-areas: "head head" - "nav main" - "foot ."; + .test-miss-areas-2 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; } "#}, ); - minify_test( + test( r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom]; + .test-miss-areas-3 { + grid-template: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", - ); - minify_test( - r#" - .foo { - grid-template: "head head" - "nav main" 1fr - "foot ...."; + indoc! { r#" + .test-miss-areas-3 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; } - "#, - ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}", + "#}, ); - minify_test( + + test( r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom] - / auto 1fr auto; + .test-miss-areas-4 { + grid: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", + indoc! { r#" + .test-miss-areas-4 { + grid: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, ); + // test no unreachable error minify_test( - ".foo { grid-template: auto 1fr / auto 1fr auto; }", - ".foo{grid-template:auto 1fr/auto 1fr auto}", + r#" + .grid-shorthand-areas { + grid: auto / 1fr 3fr; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas{grid:\".content.\"/1fr 3fr}", ); minify_test( - ".foo { grid-template: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3] / [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }", - ".foo{grid-template:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]/[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}" + r#" + .grid-shorthand-areas-rows { + grid: auto / 1fr 3fr; + grid-template-rows: 20px; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas-rows{grid:\".content.\"20px/1fr 3fr}", ); + // test grid-auto-flow: row in grid shorthand + test( + r#" + .test-auto-flow-row-1 { + grid: auto-flow / 1fr 2fr 1fr; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-row-1 { + grid: auto-flow / 1fr 2fr 1fr; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-row-2 { + grid: auto-flow auto / 100px 100px; + grid-template-areas: " one two "; + } + "#, + indoc! { r#" + .test-auto-flow-row-2 { + grid: auto-flow / 100px 100px; + grid-template-areas: "one two"; + } + "#}, + ); + test( + r#" + .test-auto-flow-dense { + grid: dense auto-flow / 1fr 2fr; + grid-template-areas: " . content . "; + } + "#, + indoc! { r#" + .test-auto-flow-dense { + grid: auto-flow dense / 1fr 2fr; + grid-template-areas: ". content ."; + } + "#}, + ); + minify_test( + r#" + .grid-auto-flow-row-auto-rows { + grid: auto-flow 40px / 1fr 90px; + grid-template-areas: "a"; + } + "#, + ".grid-auto-flow-row-auto-rows{grid:auto-flow 40px/1fr 90px;grid-template-areas:\"a\"}", + ); + minify_test( + r#" + .grid-auto-flow-row-auto-rows-multiple { + grid: auto-flow 40px max-content / 1fr; + grid-template-areas: ". a"; + } + "#, + ".grid-auto-flow-row-auto-rows-multiple{grid:auto-flow 40px max-content/1fr;grid-template-areas:\".a\"}", + ); + + // test grid-auto-flow: column in grid shorthand + test( + r#" + .test-auto-flow-column-1 { + grid: 300px / auto-flow; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-1 { + grid: 300px / auto-flow; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-column-2 { + grid: 200px 1fr / auto-flow auto; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-2 { + grid: 200px 1fr / auto-flow; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-column-dense { + grid: 1fr 2fr / dense auto-flow; + grid-template-areas: " . content . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-dense { + grid: 1fr 2fr / auto-flow dense; + grid-template-areas: ". content ."; + } + "#}, + ); + minify_test( + r#" + .grid-auto-flow-column-auto-rows { + grid: 1fr 3fr / auto-flow 40px; + grid-template-areas: "a"; + } + "#, + ".grid-auto-flow-column-auto-rows{grid:1fr 3fr/auto-flow 40px;grid-template-areas:\"a\"}", + ); + minify_test( + r#" + .grid-auto-flow-column-auto-rows-multiple { + grid: 1fr / auto-flow 40px max-content ; + grid-template-areas: ". a"; + } + "#, + ".grid-auto-flow-column-auto-rows-multiple{grid:1fr/auto-flow 40px max-content;grid-template-areas:\".a\"}", + ); + + test( + r#" + .foo { + grid-template-areas: "head head" "nav main" "foot ...."; + } + "#, + indoc! { r#" + .foo { + grid-template-areas: "head head" + "nav main" + "foot ."; + } + "#}, + ); + + minify_test( + r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom]; + } + "#, + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", + ); + minify_test( + r#" + .foo { + grid-template: "head head" + "nav main" 1fr + "foot ...."; + } + "#, + ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}", + ); + minify_test( + r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom] + / auto 1fr auto; + } + "#, + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", + ); + + minify_test( + ".foo { grid-template: auto 1fr / auto 1fr auto; }", + ".foo{grid-template:auto 1fr/auto 1fr auto}", + ); + minify_test( + ".foo { grid-template: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3] / [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }", + ".foo{grid-template:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]/[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}" + ); + + test( + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", + indoc! {r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom] + / auto 1fr auto; + } + "#}, + ); test( - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", - indoc! {r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom] - / auto 1fr auto; - } - "#}, - ); - test( ".foo{grid-template:[header-top]\"a a a\"[main-top]\"b b b\"1fr/auto 1fr auto}", indoc! {r#" .foo { @@ -22166,15 +23128,15 @@ mod tests { minify_test(".foo { --test: var(--foo, 20px); }", ".foo{--test:var(--foo,20px)}"); minify_test( ".foo { transition: var(--foo, 20px),\nvar(--bar, 40px); }", - ".foo{transition:var(--foo,20px),var(--bar,40px)}", + ".foo{transition:var(--foo,20px), var(--bar,40px)}", ); minify_test( ".foo { background: var(--color) var(--image); }", - ".foo{background:var(--color)var(--image)}", + ".foo{background:var(--color) var(--image)}", ); minify_test( ".foo { height: calc(var(--spectrum-global-dimension-size-300) / 2);", - ".foo{height:calc(var(--spectrum-global-dimension-size-300)/2)}", + ".foo{height:calc(var(--spectrum-global-dimension-size-300) / 2)}", ); minify_test( ".foo { color: var(--color, rgb(255, 255, 0)); }", @@ -22186,11 +23148,72 @@ mod tests { ); minify_test( ".foo { color: var(--color, rgb(var(--red), var(--green), 0)); }", - ".foo{color:var(--color,rgb(var(--red),var(--green),0))}", + ".foo{color:var(--color,rgb(var(--red), var(--green), 0))}", ); minify_test(".foo { --test: .5s; }", ".foo{--test:.5s}"); minify_test(".foo { --theme-sizes-1\\/12: 2 }", ".foo{--theme-sizes-1\\/12:2}"); minify_test(".foo { --test: 0px; }", ".foo{--test:0px}"); + test( + ".foo { transform: var(--bar, ) }", + indoc! {r#" + .foo { + transform: var(--bar, ); + } + "#}, + ); + test( + ".foo { transform: env(--bar, ) }", + indoc! {r#" + .foo { + transform: env(--bar, ); + } + "#}, + ); + + // Test attr() function with type() syntax - minified + minify_test( + ".foo { background-color: attr(data-color type()); }", + ".foo{background-color:attr(data-color type())}", + ); + minify_test( + ".foo { width: attr(data-width type(), 100px); }", + ".foo{width:attr(data-width type(), 100px)}", + ); + + minify_test(".foo { width: attr( data-foo % ); }", ".foo{width:attr(data-foo %)}"); + + // = attr( , ? ) + // Like var(), a bare comma can be used with nothing following it, indicating that the second was passed, just as an empty sequence. + // Spec: https://drafts.csswg.org/css-values-5/#funcdef-attr + minify_test( + ".foo { width: attr( data-foo %, ); }", + ".foo{width:attr(data-foo %,)}", + ); + + minify_test( + ".foo { width: attr( data-foo px ); }", + ".foo{width:attr(data-foo px)}", + ); + + minify_test( + ".foo { width: attr(data-foo number ); }", + ".foo{width:attr(data-foo number)}", + ); + + minify_test( + ".foo { width: attr(data-foo raw-string); }", + ".foo{width:attr(data-foo raw-string)}", + ); + + // Test attr() function with type() syntax - non-minified (issue with extra spaces) + test( + ".foo { background-color: attr(data-color type()); }", + ".foo {\n background-color: attr(data-color type());\n}\n", + ); + test( + ".foo { width: attr(data-width type(), 100px); }", + ".foo {\n width: attr(data-width type(), 100px);\n}\n", + ); prefix_test( r#" @@ -22981,7 +24004,7 @@ mod tests { ); attr_test( "text-decoration: var(--foo) lab(40% 56.6 39);", - "text-decoration:var(--foo)#b32323", + "text-decoration:var(--foo) #b32323", true, Some(Browsers { chrome: Some(90 << 16), @@ -24207,7 +25230,9 @@ mod tests { indoc! {r#" div { color: #00f; - --button: focus { color: red; }; + --button: focus { + color: red; + }; } "#}, ); @@ -24406,6 +25431,21 @@ mod tests { exclude: Features::empty(), }, ); + + minify_test( + r#" + .foo { + color: red; + .bar { + color: green; + } + color: blue; + .baz { + color: pink; + } + }"#, + ".foo{color:red;& .bar{color:green}color:#00f;& .baz{color:pink}}", + ); } #[test] @@ -24631,7 +25671,6 @@ mod tests { false, ); - #[cfg(feature = "grid")] css_modules_test( r#" body { @@ -24676,7 +25715,6 @@ mod tests { false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -24715,7 +25753,6 @@ mod tests { false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -25868,6 +26905,8 @@ mod tests { #[test] fn test_svg() { + use crate::properties::svg; + minify_test(".foo { fill: yellow; }", ".foo{fill:#ff0}"); minify_test(".foo { fill: url(#foo); }", ".foo{fill:url(#foo)}"); minify_test(".foo { fill: url(#foo) none; }", ".foo{fill:url(#foo) none}"); @@ -26179,7 +27218,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 +27280,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; @@ -26696,6 +27735,21 @@ mod tests { ..Browsers::default() }, ); + + let property = + Property::parse_string("text-rendering".into(), "geometricPrecision", ParserOptions::default()).unwrap(); + assert_eq!( + property, + Property::TextRendering(svg::TextRendering::GeometricPrecision) + ); + let property = + Property::parse_string("shape-rendering".into(), "geometricPrecision", ParserOptions::default()).unwrap(); + assert_eq!( + property, + Property::ShapeRendering(svg::ShapeRendering::GeometricPrecision) + ); + let property = Property::parse_string("color-interpolation".into(), "sRGB", ParserOptions::default()).unwrap(); + assert_eq!(property, Property::ColorInterpolation(svg::ColorInterpolation::SRGB)); } #[test] @@ -26867,6 +27921,82 @@ mod tests { ); } + #[test] + fn test_mix_blend_mode() { + minify_test( + ".foo { mix-blend-mode: normal }", + ".foo{mix-blend-mode:normal}", + ); + minify_test( + ".foo { mix-blend-mode: multiply }", + ".foo{mix-blend-mode:multiply}", + ); + minify_test( + ".foo { mix-blend-mode: screen }", + ".foo{mix-blend-mode:screen}", + ); + minify_test( + ".foo { mix-blend-mode: overlay }", + ".foo{mix-blend-mode:overlay}", + ); + minify_test( + ".foo { mix-blend-mode: darken }", + ".foo{mix-blend-mode:darken}", + ); + minify_test( + ".foo { mix-blend-mode: lighten }", + ".foo{mix-blend-mode:lighten}", + ); + minify_test( + ".foo { mix-blend-mode: color-dodge }", + ".foo{mix-blend-mode:color-dodge}", + ); + minify_test( + ".foo { mix-blend-mode: color-burn }", + ".foo{mix-blend-mode:color-burn}", + ); + minify_test( + ".foo { mix-blend-mode: hard-light }", + ".foo{mix-blend-mode:hard-light}", + ); + minify_test( + ".foo { mix-blend-mode: soft-light }", + ".foo{mix-blend-mode:soft-light}", + ); + minify_test( + ".foo { mix-blend-mode: difference }", + ".foo{mix-blend-mode:difference}", + ); + minify_test( + ".foo { mix-blend-mode: exclusion }", + ".foo{mix-blend-mode:exclusion}", + ); + minify_test( + ".foo { mix-blend-mode: hue }", + ".foo{mix-blend-mode:hue}", + ); + minify_test( + ".foo { mix-blend-mode: saturation }", + ".foo{mix-blend-mode:saturation}", + ); + minify_test( + ".foo { mix-blend-mode: color }", + ".foo{mix-blend-mode:color}", + ); + minify_test( + ".foo { mix-blend-mode: luminosity }", + ".foo{mix-blend-mode:luminosity}", + ); + minify_test( + ".foo { mix-blend-mode: plus-darker }", + ".foo{mix-blend-mode:plus-darker}", + ); + minify_test( + ".foo { mix-blend-mode: plus-lighter }", + ".foo{mix-blend-mode:plus-lighter}", + ); + } + #[test] fn test_viewport() { minify_test( @@ -28109,6 +29239,17 @@ mod tests { "@property --property-name{syntax:\"\";inherits:true;initial-value:25px}", ); + minify_test( + r#" + @property --property-name { + syntax: ''; + inherits: true; + initial-value: "hi"; + } + "#, + "@property --property-name{syntax:\"\";inherits:true;initial-value:\"hi\"}", + ); + error_test( r#" @property --property-name { @@ -28304,6 +29445,44 @@ mod tests { "#, "@property --property-name{syntax:\"\";inherits:true;initial-value:#00f}.foo{color:var(--property-name)}", ); + + test( + r#" + @media (width < 800px) { + @property --property-name { + syntax: '*'; + inherits: false; + } + } + "#, + indoc! {r#" + @media (width < 800px) { + @property --property-name { + syntax: "*"; + inherits: false + } + } + "#}, + ); + + test( + r#" + @layer foo { + @property --property-name { + syntax: '*'; + inherits: false; + } + } + "#, + indoc! {r#" + @layer foo { + @property --property-name { + syntax: "*"; + inherits: false + } + } + "#}, + ); } #[test] @@ -28391,6 +29570,52 @@ 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}; @@ -28521,6 +29746,18 @@ mod tests { #[test] fn test_container_queries() { + // name only (no condition) - new syntax + minify_test( + r#" + @container foo { + .inner { + background: green; + } + } + "#, + "@container foo{.inner{background:green}}", + ); + // with name minify_test( r#" @@ -28851,6 +30088,56 @@ mod tests { "#, "@container style(width){.foo{color:red}}", ); + minify_test( + r#" + @container scroll-state(scrollable: top) { + .foo { + color: red; + } + } + "#, + "@container scroll-state(scrollable:top){.foo{color:red}}", + ); + minify_test( + r#" + @container scroll-state((stuck: top) and (stuck: left)) { + .foo { + color: red; + } + } + "#, + "@container scroll-state((stuck:top) and (stuck:left)){.foo{color:red}}", + ); + minify_test( + r#" + @container scroll-state(not ((scrollable: bottom) and (scrollable: right))) { + .foo { + color: red; + } + } + "#, + "@container scroll-state(not ((scrollable:bottom) and (scrollable:right))){.foo{color:red}}", + ); + minify_test( + r#" + @container (scroll-state(scrollable: inline-end)) { + .foo { + color: red; + } + } + "#, + "@container scroll-state(scrollable:inline-end){.foo{color:red}}", + ); + minify_test( + r#" + @container not scroll-state(scrollable: top) { + .foo { + color: red; + } + } + "#, + "@container not scroll-state(scrollable:top){.foo{color:red}}", + ); // Disallow 'none', 'not', 'and', 'or' as a `` // https://github.com/w3c/csswg-drafts/issues/7203#issuecomment-1144257312 @@ -28895,6 +30182,29 @@ mod tests { "@container style(style(--foo: bar)) {}", ParserError::UnexpectedToken(crate::properties::custom::Token::Function("style".into())), ); + error_test( + "@container scroll-state(scroll-state(scrollable: top)) {}", + ParserError::InvalidMediaQuery, + ); + error_test( + "@container unknown(foo) {}", + ParserError::UnexpectedToken(crate::properties::custom::Token::Function("unknown".into())), + ); + + // empty container (no name and no condition) should error + error_test("@container {}", ParserError::EndOfInput); + + // empty brackets should return a clearer error message + error_test("@container () {}", ParserError::EmptyBracketInCondition); + + // invalid condition after a name should error + error_test("@container foo () {}", ParserError::EmptyBracketInCondition); + error_test( + "@container foo bar {}", + ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("bar".into())), + ); + + error_recovery_test("@container unknown(foo) {}"); } #[test] @@ -28916,11 +30226,12 @@ mod tests { color: red; } }"#, - indoc! {r#" - @foo test { - div { color: red; } - } - "#}, + indoc! { r#"@foo test { + div { + color: red; + } + } + "#}, ); minify_test( r#"@foo test { @@ -29207,14 +30518,20 @@ mod tests { minify_test(".foo { color-scheme: dark light; }", ".foo{color-scheme:light dark}"); minify_test(".foo { color-scheme: only light; }", ".foo{color-scheme:light only}"); minify_test(".foo { color-scheme: only dark; }", ".foo{color-scheme:dark only}"); + minify_test(".foo { color-scheme: inherit; }", ".foo{color-scheme:inherit}"); + minify_test(":root { color-scheme: unset; }", ":root{color-scheme:unset}"); + minify_test(".foo { color-scheme: unknow; }", ".foo{color-scheme:unknow}"); + minify_test(".foo { color-scheme: only; }", ".foo{color-scheme:only}"); + minify_test(".foo { color-scheme: dark foo; }", ".foo{color-scheme:dark foo}"); + minify_test(".foo { color-scheme: normal dark; }", ".foo{color-scheme:normal dark}"); minify_test( ".foo { color-scheme: dark light only; }", ".foo{color-scheme:light dark only}", ); - minify_test(".foo { color-scheme: foo bar light; }", ".foo{color-scheme:light}"); + minify_test(".foo { color-scheme: foo bar light; }", ".foo{color-scheme:foo bar light}"); minify_test( ".foo { color-scheme: only foo dark bar; }", - ".foo{color-scheme:dark only}", + ".foo{color-scheme:only foo dark bar}", ); prefix_test( ".foo { color-scheme: dark; }", @@ -29440,6 +30757,35 @@ mod tests { ); } + #[test] + fn test_print_color_adjust() { + prefix_test( + ".foo { print-color-adjust: exact; }", + indoc! { r#" + .foo { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + "#}, + Browsers { + chrome: Some(135 << 16), + ..Browsers::default() + }, + ); + prefix_test( + ".foo { print-color-adjust: exact; }", + indoc! { r#" + .foo { + print-color-adjust: exact; + } + "#}, + Browsers { + chrome: Some(137 << 16), + ..Browsers::default() + }, + ); + } + #[test] fn test_all() { minify_test(".foo { all: initial; all: initial }", ".foo{all:initial}"); diff --git a/src/macros.rs b/src/macros.rs index 8019c58cb..a50d54f5a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -189,7 +189,7 @@ macro_rules! shorthand_property_bitflags { crate::macros::shorthand_property_bitflags!($name, [$($all),*] $($rest),* ; $last_index + 1; $($var = $index)* $cur = $last_index + 1); }; ($name:ident, [$($all:ident),*] $cur:ident; $last_index:expr ; $($var:ident = $index:expr)+) => { - paste::paste! { + pastey::paste! { crate::macros::property_bitflags! { #[derive(Default, Debug)] struct [<$name Property>]: u8 { @@ -216,7 +216,7 @@ macro_rules! shorthand_handler { $( pub $key: Option<$type>, )* - flushed_properties: paste::paste!([<$shorthand Property>]), + flushed_properties: pastey::paste!([<$shorthand Property>]), has_any: bool } @@ -250,7 +250,7 @@ macro_rules! shorthand_handler { let mut unparsed = val.clone(); context.add_unparsed_fallbacks(&mut unparsed); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::try_from(&unparsed.property_id).unwrap()); }; dest.push(Property::Unparsed(unparsed)); @@ -263,7 +263,7 @@ macro_rules! shorthand_handler { fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { self.flush(dest, context); - self.flushed_properties = paste::paste!([<$shorthand Property>]::empty()); + self.flushed_properties = pastey::paste!([<$shorthand Property>]::empty()); } } @@ -289,7 +289,7 @@ macro_rules! shorthand_handler { }; $( - if $shorthand_fallback && !self.flushed_properties.intersects(paste::paste!([<$shorthand Property>]::$shorthand)) { + if $shorthand_fallback && !self.flushed_properties.intersects(pastey::paste!([<$shorthand Property>]::$shorthand)) { let fallbacks = shorthand.get_fallbacks(context.targets); for fallback in fallbacks { dest.push(Property::$shorthand(fallback)); @@ -298,7 +298,7 @@ macro_rules! shorthand_handler { )? dest.push(Property::$shorthand(shorthand)); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::$shorthand); }; } else { @@ -306,7 +306,7 @@ macro_rules! shorthand_handler { #[allow(unused_mut)] if let Some(mut val) = $key { $( - if $fallback && !self.flushed_properties.intersects(paste::paste!([<$shorthand Property>]::$prop)) { + if $fallback && !self.flushed_properties.intersects(pastey::paste!([<$shorthand Property>]::$prop)) { let fallbacks = val.get_fallbacks(context.targets); for fallback in fallbacks { dest.push(Property::$prop(fallback)); @@ -315,7 +315,7 @@ macro_rules! shorthand_handler { )? dest.push(Property::$prop(val)); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::$prop); }; } @@ -390,7 +390,7 @@ macro_rules! impl_shorthand { impl<'i> Shorthand<'i> for $t { #[allow(unused_variables)] fn from_longhands(decls: &DeclarationBlock<'i>, vendor_prefix: crate::vendor_prefix::VendorPrefix) -> Option<(Self, bool)> { - use paste::paste; + use pastey::paste; $( $( diff --git a/src/media_query.rs b/src/media_query.rs index 10657c160..2f0ee9de6 100644 --- a/src/media_query.rs +++ b/src/media_query.rs @@ -3,9 +3,9 @@ use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, use crate::macros::enum_property; use crate::parser::starts_with_ignore_ascii_case; use crate::printer::Printer; -use crate::properties::custom::EnvironmentVariable; +use crate::properties::custom::{EnvironmentVariable, TokenList}; #[cfg(feature = "visitor")] -use crate::rules::container::ContainerSizeFeatureId; +use crate::rules::container::{ContainerSizeFeatureId, ScrollStateFeatureId}; use crate::rules::custom_media::CustomMediaRule; use crate::rules::Location; use crate::stylesheet::ParserOptions; @@ -56,6 +56,10 @@ impl<'i> MediaList<'i> { options: &ParserOptions<'_, 'i>, ) -> Result>> { let mut media_queries = vec![]; + if input.is_exhausted() { + return Ok(MediaList { media_queries }); + } + loop { match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse_with_options(i, options)) { Ok(mq) => { @@ -534,6 +538,9 @@ pub enum MediaCondition<'i> { /// The conditions for the operator. conditions: Vec>, }, + /// Unknown tokens. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + Unknown(TokenList<'i>), } /// A trait for conditions such as media queries and container queries. @@ -551,6 +558,13 @@ pub(crate) trait QueryCondition<'i>: Sized { Err(input.new_error_for_next_token()) } + fn parse_scroll_state_query<'t>( + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Err(input.new_error_for_next_token()) + } + fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool; } @@ -579,6 +593,7 @@ impl<'i> QueryCondition<'i> for MediaCondition<'i> { MediaCondition::Not(_) => true, MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator, MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets), + MediaCondition::Unknown(_) => false, } } } @@ -591,6 +606,8 @@ bitflags! { const ALLOW_OR = 1 << 0; /// Whether to allow style container queries. const ALLOW_STYLE = 1 << 1; + /// Whether to allow scroll state container queries. + const ALLOW_SCROLL_STATE = 1 << 2; } } @@ -601,7 +618,16 @@ impl<'i> MediaCondition<'i> { flags: QueryConditionFlags, options: &ParserOptions<'_, 'i>, ) -> Result>> { - parse_query_condition(input, flags, options) + input + .try_parse(|input| parse_query_condition(input, flags, options)) + .or_else(|e| { + if options.error_recovery { + options.warn(e); + Ok(MediaCondition::Unknown(TokenList::parse(input, options, 0)?)) + } else { + Err(e) + } + }) } fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix { @@ -673,28 +699,44 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( options: &ParserOptions<'_, 'i>, ) -> Result>> { let location = input.current_source_location(); - let (is_negation, is_style) = match *input.next()? { - Token::ParenthesisBlock => (false, false), - Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false), + enum QueryFunction { + None, + Style, + ScrollState, + } + + let (is_negation, function) = match *input.next()? { + Token::ParenthesisBlock => (false, QueryFunction::None), + Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, QueryFunction::None), Token::Function(ref f) if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") => { - (false, true) + (false, QueryFunction::Style) + } + Token::Function(ref f) + if flags.contains(QueryConditionFlags::ALLOW_SCROLL_STATE) && f.eq_ignore_ascii_case("scroll-state") => + { + (false, QueryFunction::ScrollState) } ref t => return Err(location.new_unexpected_token_error(t.clone())), }; - let first_condition = match (is_negation, is_style) { - (true, false) => { + let first_condition = match (is_negation, function) { + (true, QueryFunction::None) => { let inner_condition = parse_parens_or_function(input, flags, options)?; return Ok(P::create_negation(Box::new(inner_condition))); } - (true, true) => { + (true, QueryFunction::Style) => { 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, options)?, - (false, true) => P::parse_style_query(input, options)?, + (true, QueryFunction::ScrollState) => { + let inner_condition = P::parse_scroll_state_query(input, options)?; + return Ok(P::create_negation(Box::new(inner_condition))); + } + (false, QueryFunction::None) => parse_paren_block(input, flags, options)?, + (false, QueryFunction::Style) => P::parse_style_query(input, options)?, + (false, QueryFunction::ScrollState) => P::parse_scroll_state_query(input, options)?, }; let operator = match input.try_parse(Operator::parse) { @@ -738,6 +780,11 @@ fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>( { P::parse_style_query(input, options) } + Token::Function(ref f) + if flags.contains(QueryConditionFlags::ALLOW_SCROLL_STATE) && f.eq_ignore_ascii_case("scroll-state") => + { + P::parse_scroll_state_query(input, options) + } ref t => return Err(location.new_unexpected_token_error(t.clone())), } } @@ -748,6 +795,11 @@ fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>( options: &ParserOptions<'_, 'i>, ) -> Result>> { input.parse_nested_block(|input| { + // Detect empty brackets and provide a clearer error message. + if input.is_exhausted() { + return Err(input.new_custom_error(ParserError::EmptyBracketInCondition)); + } + if let Ok(inner) = input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR, options)) { @@ -826,6 +878,7 @@ impl<'i> ToCss for MediaCondition<'i> { ref conditions, operator, } => operation_to_css(operator, conditions, dest), + MediaCondition::Unknown(ref tokens) => tokens.to_css(dest, false), } } } @@ -905,7 +958,8 @@ impl MediaFeatureComparison { feature = "visitor", derive(Visit), visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>), - visit(<'i, ContainerSizeFeatureId>) + visit(<'i, ContainerSizeFeatureId>), + visit(<'i, ScrollStateFeatureId>) )] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -994,7 +1048,6 @@ where }; if operator.is_some() && legacy_op.is_some() { - dbg!(); return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } @@ -1012,8 +1065,6 @@ where if let Some(operator) = operator.or(legacy_op) { if !name.value_type().allows_ranges() { - dbg!(); - return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } @@ -1029,15 +1080,11 @@ 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)); } }; @@ -1055,7 +1102,6 @@ where } if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) { - dbg!(); return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } @@ -1094,17 +1140,16 @@ where } pub(crate) fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { - if !should_compile!(targets, MediaIntervalSyntax) { - return false; - } - match self { - QueryFeature::Interval { .. } => parent_operator != Some(Operator::And), + QueryFeature::Interval { .. } => { + should_compile!(targets, MediaIntervalSyntax) && parent_operator != Some(Operator::And) + } QueryFeature::Range { operator, .. } => { - matches!( - operator, - MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan - ) + should_compile!(targets, MediaRangeSyntax) + && matches!( + operator, + MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan + ) } _ => false, } @@ -1852,7 +1897,7 @@ mod tests { targets::{Browsers, Targets}, }; - fn parse(s: &str) -> MediaQuery { + fn parse(s: &str) -> MediaQuery<'_> { let mut input = ParserInput::new(&s); let mut parser = Parser::new(&mut input); MediaQuery::parse_with_options(&mut parser, &ParserOptions::default()).unwrap() diff --git a/src/parser.rs b/src/parser.rs index af93d978e..0dd76041c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -213,7 +213,9 @@ pub enum AtRulePrelude<'i, T> { /// An @property prelude. Property(DashedIdent<'i>), /// A @container prelude. - Container(Option>, ContainerCondition<'i>), + /// Spec: https://drafts.csswg.org/css-conditional-5/#container-rule + /// @container [ ? ? ]! + Container(Option>, Option>), /// A @starting-style prelude. StartingStyle, /// A @scope rule prelude. @@ -320,10 +322,6 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLev let media = MediaList::parse(input, &self.options)?; return Ok(AtRulePrelude::CustomMedia(name, media)) }, - "property" => { - let name = DashedIdent::parse(input)?; - return Ok(AtRulePrelude::Property(name)) - }, _ => {} } @@ -645,8 +643,18 @@ 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_with_options(input, &self.options)?; - AtRulePrelude::Container(name, condition) + match input.try_parse(|input| ContainerCondition::parse_with_options(input, &self.options)) { + Ok(condition) => AtRulePrelude::Container(name, Some(condition)), + Err(e) => { + if name.is_some() && input.is_exhausted() { + // name only, no condition - allowed by new syntax + AtRulePrelude::Container(name, None) + } else { + // condition parsing failed (e.g., empty brackets or invalid tokens) + return Err(e); + } + } + } }, "starting-style" => { AtRulePrelude::StartingStyle @@ -695,6 +703,10 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne return Err(input.new_custom_error(ParserError::DeprecatedCssModulesValueRule)); }, + "property" => { + let name = DashedIdent::parse(input)?; + return Ok(AtRulePrelude::Property(name)) + }, _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser)? }; diff --git a/src/prefixes.rs b/src/prefixes.rs index c967bdc6d..63792255e 100644 --- a/src/prefixes.rs +++ b/src/prefixes.rs @@ -800,18 +800,13 @@ impl Feature { prefixes |= VendorPrefix::WebKit; } } - if let Some(version) = browsers.ios_saf { - if version >= 393216 && version <= 852992 { - prefixes |= VendorPrefix::WebKit; - } - } if let Some(version) = browsers.opera { if version >= 983040 && version <= 6881280 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 262144 && version <= 852224 { + if version >= 197120 && version <= 851968 { prefixes |= VendorPrefix::WebKit; } } @@ -820,6 +815,11 @@ impl Feature { prefixes |= VendorPrefix::WebKit; } } + if let Some(version) = browsers.ios_saf { + if version >= 262144 && version <= 851968 { + prefixes |= VendorPrefix::WebKit; + } + } } Feature::FontFeatureSettings | Feature::FontVariantLigatures | Feature::FontLanguageOverride => { if let Some(version) = browsers.android { @@ -1268,23 +1268,23 @@ impl Feature { } } Feature::Stretch => { - if let Some(version) = browsers.chrome { - if version >= 1441792 { - prefixes |= VendorPrefix::WebKit; - } - } if let Some(version) = browsers.firefox { if version >= 196608 { prefixes |= VendorPrefix::Moz; } } if let Some(version) = browsers.android { - if version >= 263168 { + if version >= 263168 && version <= 263171 { + prefixes |= VendorPrefix::WebKit; + } + } + if let Some(version) = browsers.chrome { + if version >= 1441792 && version <= 8978432 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 { + if version >= 5177344 && version <= 8978432 { prefixes |= VendorPrefix::WebKit; } } @@ -1299,12 +1299,12 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 458752 { + if version >= 393472 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.samsung { - if version >= 327680 { + if version >= 262144 { prefixes |= VendorPrefix::WebKit; } } @@ -1386,12 +1386,12 @@ impl Feature { } Feature::TextDecoration => { if let Some(version) = browsers.ios_saf { - if version >= 524288 { + if version >= 524288 && version <= 1704192 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 524288 { + if version >= 524288 && version <= 1704192 { prefixes |= VendorPrefix::WebKit; } } @@ -2153,18 +2153,18 @@ impl Feature { } } Feature::PrintColorAdjust | Feature::ColorAdjust => { - if let Some(version) = browsers.chrome { - if version >= 1114112 { + if let Some(version) = browsers.android { + if version >= 263168 && version <= 263171 { prefixes |= VendorPrefix::WebKit; } } - if let Some(version) = browsers.android { - if version >= 263168 { + if let Some(version) = browsers.chrome { + if version >= 1114112 && version <= 8847360 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 { + if version >= 5177344 && version <= 8847360 { prefixes |= VendorPrefix::WebKit; } } @@ -2189,7 +2189,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 1835008 { prefixes |= VendorPrefix::WebKit; } } @@ -2231,7 +2231,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version >= 2424832 && version <= 5701632 { + if version >= 263168 && version <= 5701632 { prefixes |= VendorPrefix::WebKit; } } diff --git a/src/printer.rs b/src/printer.rs index 7061485ed..b231fbecc 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/properties/contain.rs b/src/properties/contain.rs index 1c57bab49..27ad2bb95 100644 --- a/src/properties/contain.rs +++ b/src/properties/contain.rs @@ -30,6 +30,8 @@ enum_property! { InlineSize, /// Establishes a query container for container size queries on both the inline and block axis. Size, + /// Establishes a query container for container scroll-state queries + ScrollState, } } diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 824bc5b7f..26da05708 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}; @@ -370,59 +370,37 @@ impl<'i> TokenList<'i> { return Err(input.new_custom_error(ParserError::MaximumNestingDepth)); } - let mut last_is_delim = false; - let mut last_is_whitespace = false; loop { let state = input.state(); match input.next_including_whitespace_and_comments() { - Ok(&cssparser::Token::WhiteSpace(..)) | Ok(&cssparser::Token::Comment(..)) => { - // Skip whitespace if the last token was a delimiter. - // Otherwise, replace all whitespace and comments with a single space character. - if !last_is_delim { - tokens.push(Token::WhiteSpace(" ".into()).into()); - last_is_whitespace = true; - } - } Ok(&cssparser::Token::Function(ref f)) => { // Attempt to parse embedded color values into hex tokens. let f = f.into(); if let Some(color) = try_parse_color_token(&f, &state, input) { tokens.push(TokenOrValue::Color(color)); - last_is_delim = false; - last_is_whitespace = false; } else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) { tokens.push(TokenOrValue::UnresolvedColor(color)); - last_is_delim = true; - last_is_whitespace = false; } else if f == "url" { input.reset(&state); tokens.push(TokenOrValue::Url(Url::parse(input)?)); - last_is_delim = false; - last_is_whitespace = false; } else if f == "var" { let var = input.parse_nested_block(|input| { let var = Variable::parse(input, options, depth + 1)?; Ok(TokenOrValue::Var(var)) })?; tokens.push(var); - last_is_delim = true; - last_is_whitespace = false; } else if f == "env" { let env = input.parse_nested_block(|input| { let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?; Ok(TokenOrValue::Env(env)) })?; tokens.push(env); - last_is_delim = true; - last_is_whitespace = false; } else { let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?; tokens.push(TokenOrValue::Function(Function { name: Ident(f), arguments, })); - last_is_delim = true; // Whitespace is not required after any of these chars. - last_is_whitespace = false; } } Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => { @@ -431,19 +409,13 @@ impl<'i> TokenList<'i> { } else { tokens.push(Token::Hash(h.into()).into()); } - last_is_delim = false; - last_is_whitespace = false; } Ok(&cssparser::Token::UnquotedUrl(_)) => { input.reset(&state); tokens.push(TokenOrValue::Url(Url::parse(input)?)); - last_is_delim = false; - last_is_whitespace = false; } Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => { tokens.push(TokenOrValue::DashedIdent(name.into())); - last_is_delim = false; - last_is_whitespace = false; } Ok(token @ &cssparser::Token::ParenthesisBlock) | Ok(token @ &cssparser::Token::SquareBracketBlock) @@ -459,8 +431,6 @@ impl<'i> TokenList<'i> { input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?; tokens.push(closing_delimiter.into()); - last_is_delim = true; // Whitespace is not required after any of these chars. - last_is_whitespace = false; } Ok(token @ cssparser::Token::Dimension { .. }) => { let value = if let Ok(length) = LengthValue::try_from(token) { @@ -475,8 +445,6 @@ impl<'i> TokenList<'i> { TokenOrValue::Token(token.into()) }; tokens.push(value); - last_is_delim = false; - last_is_whitespace = false; } Ok(token) if token.is_parse_error() => { return Err(ParseError { @@ -485,18 +453,7 @@ impl<'i> TokenList<'i> { }) } Ok(token) => { - last_is_delim = matches!(token, cssparser::Token::Delim(_) | cssparser::Token::Comma); - - // If this is a delimiter, and the last token was whitespace, - // replace the whitespace with the delimiter since both are not required. - if last_is_delim && last_is_whitespace { - let last = tokens.last_mut().unwrap(); - *last = Token::from(token).into(); - } else { - tokens.push(Token::from(token).into()); - } - - last_is_whitespace = false; + tokens.push(Token::from(token).into()); } Err(_) => break, } @@ -532,20 +489,13 @@ impl<'i> TokenList<'i> { where W: std::fmt::Write, { - if !dest.minify && self.0.len() == 1 && matches!(self.0.first(), Some(token) if token.is_whitespace()) { - return Ok(()); - } - - let mut has_whitespace = false; - for (i, token_or_value) in self.0.iter().enumerate() { - has_whitespace = match token_or_value { + for token_or_value in self.0.iter() { + match token_or_value { TokenOrValue::Color(color) => { color.to_css(dest)?; - false } TokenOrValue::UnresolvedColor(color) => { color.to_css(dest, is_custom_property)?; - false } TokenOrValue::Url(url) => { if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() { @@ -557,77 +507,45 @@ impl<'i> TokenList<'i> { )); } url.to_css(dest)?; - false } TokenOrValue::Var(var) => { var.to_css(dest, is_custom_property)?; - self.write_whitespace_if_needed(i, dest)? } TokenOrValue::Env(env) => { env.to_css(dest, is_custom_property)?; - self.write_whitespace_if_needed(i, dest)? } TokenOrValue::Function(f) => { f.to_css(dest, is_custom_property)?; - self.write_whitespace_if_needed(i, dest)? } TokenOrValue::Length(v) => { // Do not serialize unitless zero lengths in custom properties as it may break calc(). let (value, unit) = v.to_unit_value(); serialize_dimension(value, unit, dest)?; - false } TokenOrValue::Angle(v) => { v.to_css(dest)?; - false } TokenOrValue::Time(v) => { v.to_css(dest)?; - false } TokenOrValue::Resolution(v) => { v.to_css(dest)?; - false } TokenOrValue::DashedIdent(v) => { v.to_css(dest)?; - false } TokenOrValue::AnimationName(v) => { v.to_css(dest)?; - false } TokenOrValue::Token(token) => match token { - Token::Delim(d) => { - if *d == '+' || *d == '-' { - dest.write_char(' ')?; - dest.write_char(*d)?; - dest.write_char(' ')?; - } else { - let ws_before = !has_whitespace && (*d == '/' || *d == '*'); - dest.delim(*d, ws_before)?; - } - true - } - Token::Comma => { - dest.delim(',', false)?; - true - } - Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => { - token.to_css(dest)?; - self.write_whitespace_if_needed(i, dest)? - } Token::Dimension { value, unit, .. } => { serialize_dimension(*value, unit, dest)?; - false } Token::Number { value, .. } => { value.to_css(dest)?; - false } _ => { token.to_css(dest)?; - matches!(token, Token::WhiteSpace(..)) } }, }; @@ -657,24 +575,8 @@ impl<'i> TokenList<'i> { Ok(()) } - #[inline] - fn write_whitespace_if_needed(&self, i: usize, dest: &mut Printer) -> Result - where - W: std::fmt::Write, - { - if !dest.minify - && i != self.0.len() - 1 - && !matches!( - self.0[i + 1], - TokenOrValue::Token(Token::Comma) | TokenOrValue::Token(Token::CloseParenthesis) - ) - { - // Whitespace is removed during parsing, so add it back if we aren't minifying. - dest.write_char(' ')?; - Ok(true) - } else { - Ok(false) - } + pub(crate) fn starts_with_whitespace(&self) -> bool { + matches!(self.0.get(0), Some(TokenOrValue::Token(Token::WhiteSpace(_)))) } } @@ -986,8 +888,18 @@ impl<'a> ToCss for Token<'a> { int_value: *int_value, } .to_css(dest)?, - Token::WhiteSpace(w) => cssparser::Token::WhiteSpace(w).to_css(dest)?, - Token::Comment(c) => cssparser::Token::Comment(c).to_css(dest)?, + Token::WhiteSpace(w) => { + if dest.minify { + dest.write_char(' ')?; + } else { + dest.write_str(&w)?; + } + } + Token::Comment(c) => { + if !dest.minify { + cssparser::Token::Comment(c).to_css(dest)?; + } + } Token::Colon => cssparser::Token::Colon.to_css(dest)?, Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?, Token::Comma => cssparser::Token::Comma.to_css(dest)?, @@ -1081,7 +993,7 @@ impl<'a> std::hash::Hash for Token<'a> { /// Converts a floating point value into its mantissa, exponent, /// and sign components so that it can be hashed. fn integer_decode(v: f32) -> (u32, i16, i8) { - let bits: u32 = unsafe { std::mem::transmute(v) }; + let bits: u32 = f32::to_bits(v); let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 }; let mut exponent: i16 = ((bits >> 23) & 0xff) as i16; let mantissa = if exponent == 0 { @@ -1303,7 +1215,10 @@ impl<'i> Variable<'i> { dest.write_str("var(")?; self.name.to_css(dest)?; if let Some(fallback) = &self.fallback { - dest.delim(',', false)?; + dest.write_char(',')?; + if !fallback.starts_with_whitespace() { + dest.whitespace()?; + } fallback.to_css(dest, is_custom_property)?; } dest.write_char(')') @@ -1477,7 +1392,10 @@ impl<'i> EnvironmentVariable<'i> { } if let Some(fallback) = &self.fallback { - dest.delim(',', false)?; + dest.write_char(',')?; + if !fallback.starts_with_whitespace() { + dest.whitespace()?; + } fallback.to_css(dest, is_custom_property)?; } dest.write_char(')') @@ -1594,7 +1512,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 +1554,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 +1570,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 +1584,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 +1596,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/properties/effects.rs b/src/properties/effects.rs index 1ba69f6a6..95d403d43 100644 --- a/src/properties/effects.rs +++ b/src/properties/effects.rs @@ -1,5 +1,6 @@ //! CSS properties related to filters and effects. +use crate::macros::enum_property; use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::targets::{Browsers, Targets}; @@ -410,3 +411,45 @@ impl IsCompatible for FilterList<'_> { true } } + +enum_property! { + /// A [``](https://www.w3.org/TR/compositing-1/#ltblendmodegt) value. + pub enum BlendMode { + /// The default blend mode; the top layer is drawn over the bottom layer. + Normal, + /// The source and destination are multiplied. + Multiply, + /// Multiplies the complements of the backdrop and source, then complements the result. + Screen, + /// Multiplies or screens, depending on the backdrop color. + Overlay, + /// Selects the darker of the backdrop and source. + Darken, + /// Selects the lighter of the backdrop and source. + Lighten, + /// Brightens the backdrop to reflect the source. + ColorDodge, + /// Darkens the backdrop to reflect the source. + ColorBurn, + /// Multiplies or screens, depending on the source color. + HardLight, + /// Darkens or lightens, depending on the source color. + SoftLight, + /// Subtracts the darker from the lighter. + Difference, + /// Similar to difference, but with lower contrast. + Exclusion, + /// The hue of the source with the saturation and luminosity of the backdrop. + Hue, + /// The saturation of the source with the hue and luminosity of the backdrop. + Saturation, + /// The hue and saturation of the source with the luminosity of the backdrop. + Color, + /// The luminosity of the source with the hue and saturation of the backdrop. + Luminosity, + /// Adds the source to the backdrop, producing a darker result. + PlusDarker, + /// Adds the source to the backdrop, producing a lighter result. + PlusLighter, + } +} diff --git a/src/properties/font.rs b/src/properties/font.rs index 55581d886..8b3031a9f 100644 --- a/src/properties/font.rs +++ b/src/properties/font.rs @@ -421,6 +421,13 @@ impl<'i> ToCss for FamilyName<'i> { // https://www.w3.org/TR/css-fonts-4/#family-name-syntax let val = &self.0; if !val.is_empty() && !GenericFontFamily::parse_string(val).is_ok() { + // Family names with two or more consecutive spaces must be quoted to preserve the spaces. + let needs_quotes = val.contains(" "); + if needs_quotes { + serialize_string(&val, dest)?; + return Ok(()); + } + let mut id = String::new(); let mut first = true; for slice in val.split(' ') { diff --git a/src/properties/grid.rs b/src/properties/grid.rs index 6e7063197..553cdc542 100644 --- a/src/properties/grid.rs +++ b/src/properties/grid.rs @@ -533,6 +533,7 @@ impl ToCss for TrackSizeList { } /// A value for the [grid-template-areas](https://drafts.csswg.org/css-grid-2/#grid-template-areas-property) property. +/// none | + #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( @@ -704,6 +705,8 @@ impl GridTemplateAreas { /// A value for the [grid-template](https://drafts.csswg.org/css-grid-2/#explicit-grid-shorthand) shorthand property. /// +/// none | [ <'grid-template-rows'> / <'grid-template-columns'> ] | [ ? ? ? ]+ [ / ]? +/// /// If `areas` is not `None`, then `rows` must also not be `None`. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -935,6 +938,8 @@ impl_shorthand! { bitflags! { /// A value for the [grid-auto-flow](https://drafts.csswg.org/css-grid-2/#grid-auto-flow-property) property. /// + /// [ row | column ] || dense + /// /// The `Row` or `Column` flags may be combined with the `Dense` flag, but the `Row` and `Column` flags may /// not be combined. #[cfg_attr(feature = "visitor", derive(Visit))] @@ -1101,6 +1106,8 @@ impl ToCss for GridAutoFlow { /// A value for the [grid](https://drafts.csswg.org/css-grid-2/#grid-shorthand) shorthand property. /// +/// <'grid-template'> | <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? | [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'> +/// /// Explicit and implicit values may not be combined. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -1199,6 +1206,41 @@ impl ToCss for Grid<'_> { && self.auto_columns == TrackSizeList::default() && self.auto_flow == GridAutoFlow::default(); + // Handle the case where areas is set but rows is None (auto-flow syntax). + // In this case, output "auto-flow / columns" format. + if self.areas != GridTemplateAreas::None && self.rows == TrackSizing::None { + dest.write_str("auto-flow")?; + if self.auto_flow.contains(GridAutoFlow::Dense) { + dest.write_str(" dense")?; + } + if self.auto_rows != TrackSizeList::default() { + dest.write_char(' ')?; + self.auto_rows.to_css(dest)?; + } + dest.delim('/', true)?; + self.columns.to_css(dest)?; + return Ok(()); + } + + // Handle the case where areas is set but columns is None (auto-flow column syntax). + // In this case, output "rows / auto-flow" format. + if self.areas != GridTemplateAreas::None + && self.columns == TrackSizing::None + && self.auto_flow.direction() == GridAutoFlow::Column + { + self.rows.to_css(dest)?; + dest.delim('/', true)?; + dest.write_str("auto-flow")?; + if self.auto_flow.contains(GridAutoFlow::Dense) { + dest.write_str(" dense")?; + } + if self.auto_columns != TrackSizeList::default() { + dest.write_char(' ')?; + self.auto_columns.to_css(dest)?; + } + return Ok(()); + } + if self.areas != GridTemplateAreas::None || (self.rows != TrackSizing::None && self.columns != TrackSizing::None) || (self.areas == GridTemplateAreas::None && is_auto_initial) @@ -1256,16 +1298,28 @@ impl<'i> Grid<'i> { auto_columns: &TrackSizeList, auto_flow: &GridAutoFlow, ) -> bool { + let default_track_size_list = TrackSizeList::default(); + + // When areas is set but rows is None (auto-flow syntax like "grid: auto-flow / 1fr"), + // we can output the auto-flow shorthand along with "grid-template-areas" separately. + // ⚠️ The case of `grid: 1fr / auto-flow` does not require such handling. + if *areas != GridTemplateAreas::None && *rows == TrackSizing::None { + return auto_flow.direction() == GridAutoFlow::Row; + } + // The `grid` shorthand can either be fully explicit (e.g. same as `grid-template`), // or explicit along a single axis. If there are auto rows, then there cannot be explicit rows, for example. let is_template = GridTemplate::is_valid(rows, columns, areas); - let default_track_size_list = TrackSizeList::default(); let is_explicit = *auto_rows == default_track_size_list && *auto_columns == default_track_size_list && *auto_flow == GridAutoFlow::default(); + // grid-auto-flow: row shorthand syntax: + // [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'> let is_auto_rows = auto_flow.direction() == GridAutoFlow::Row && *rows == TrackSizing::None && *auto_columns == default_track_size_list; + // grid-auto-flow: column shorthand syntax: + // <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? let is_auto_columns = auto_flow.direction() == GridAutoFlow::Column && *columns == TrackSizing::None && *auto_rows == default_track_size_list; @@ -1274,6 +1328,7 @@ impl<'i> Grid<'i> { } } +// TODO: shorthand `grid: auto-flow 1fr / 100px` https://drafts.csswg.org/css-grid/#example-dec34e0f impl_shorthand! { Grid(Grid<'i>) { rows: [GridTemplateRows], @@ -1461,6 +1516,7 @@ macro_rules! impl_grid_placement { define_shorthand! { /// A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-row) shorthand property. + /// [ / ]? pub struct GridRow<'i> { /// The starting line. #[cfg_attr(feature = "serde", serde(borrow))] @@ -1471,7 +1527,8 @@ define_shorthand! { } define_shorthand! { - /// A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property. + /// A value for the [grid-column](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property. + /// [ / ]? pub struct GridColumn<'i> { /// The starting line. #[cfg_attr(feature = "serde", serde(borrow))] @@ -1486,6 +1543,7 @@ impl_grid_placement!(GridColumn); define_shorthand! { /// A value for the [grid-area](https://drafts.csswg.org/css-grid-2/#propdef-grid-area) shorthand property. + /// [ / ]{0,3} pub struct GridArea<'i> { /// The grid row start placement. #[cfg_attr(feature = "serde", serde(borrow))] @@ -1684,10 +1742,24 @@ impl<'i> PropertyHandler<'i> for GridHandler<'i> { auto_columns_val, auto_flow_val, ) { + let needs_separate_areas = *areas_val != GridTemplateAreas::None + && ((*rows_val == TrackSizing::None && auto_flow_val.direction() == GridAutoFlow::Row) + || (*columns_val == TrackSizing::None && auto_flow_val.direction() == GridAutoFlow::Column)); + + // Pad areas with "." for missing rows. But don't pad if we're using auto-flow syntax, + // because grid-template-areas should remain as-is in that case. + // Use tuple to avoid double cloning when needs_separate_areas is true. + let (areas_for_grid, areas_for_output) = if needs_separate_areas { + // Take the original areas directly to avoid cloning when needs_separate_areas is true + (areas_val.clone(), Some(areas_val.clone())) + } else { + (GridHandler::pad_grid_template_areas(rows_val, areas_val.clone()), None) + }; + dest.push(Property::Grid(Grid { rows: rows_val.clone(), columns: columns_val.clone(), - areas: areas_val.clone(), + areas: areas_for_grid, auto_rows: auto_rows_val.clone(), auto_columns: auto_columns_val.clone(), auto_flow: auto_flow_val.clone(), @@ -1697,16 +1769,25 @@ impl<'i> PropertyHandler<'i> for GridHandler<'i> { auto_rows = None; auto_columns = None; auto_flow = None; + + // When areas is set but rows/columns is None (auto-flow syntax), also output + // grid-template-areas separately since grid shorthand can't represent this combination. + if let Some(areas) = areas_for_output { + dest.push(Property::GridTemplateAreas(areas)); + } } } // The `grid-template` shorthand supports only explicit track values (i.e. no `repeat()`) // combined with grid-template-areas. If there are no areas, then any track values are allowed. if has_template && GridTemplate::is_valid(rows_val, columns_val, areas_val) { + // Pad areas with "." for missing rows + let padded_areas = GridHandler::pad_grid_template_areas(rows_val, areas_val.clone()); + dest.push(Property::GridTemplate(GridTemplate { rows: rows_val.clone(), columns: columns_val.clone(), - areas: areas_val.clone(), + areas: padded_areas, })); has_template = false; @@ -1763,6 +1844,39 @@ impl<'i> PropertyHandler<'i> for GridHandler<'i> { } } +/// Pads grid template areas with "." (None) for missing rows. +/// All the remaining unnamed areas in a grid can be referred using null cell +/// tokens. A null cell token is a sequence of one or more . (U+002E FULL STOP) +/// characters, e.g., ., ..., or ..... etc. A null cell token can be used to +/// create empty spaces in the grid. +/// Spec: https://drafts.csswg.org/css-grid/#ref-for-string-value① +impl GridHandler<'_> { + fn pad_grid_template_areas(rows: &TrackSizing, areas: GridTemplateAreas) -> GridTemplateAreas { + match (rows, areas) { + (TrackSizing::TrackList(rows_list), GridTemplateAreas::Areas { columns, areas }) => { + let rows_count = rows_list.items.len(); + let areas_rows_count = areas.len() / columns as usize; + if areas_rows_count < rows_count { + let mut padded_areas = areas; + // Fill each missing row with "." (represented as None) + for _ in areas_rows_count..rows_count { + for _ in 0..columns { + padded_areas.push(None); + } + } + GridTemplateAreas::Areas { + columns, + areas: padded_areas, + } + } else { + GridTemplateAreas::Areas { columns, areas } + } + } + (_, areas) => areas, + } + } +} + #[inline] fn is_grid_property(property_id: &PropertyId) -> bool { match property_id { diff --git a/src/properties/list.rs b/src/properties/list.rs index e411667de..e04ed7477 100644 --- a/src/properties/list.rs +++ b/src/properties/list.rs @@ -108,7 +108,7 @@ macro_rules! counter_styles { fn is_compatible(&self, browsers: Browsers) -> bool { match self { $( - PredefinedCounterStyle::$id => paste::paste! { + PredefinedCounterStyle::$id => pastey::paste! { crate::compat::Feature::[<$id ListStyleType>].is_compatible(browsers) }, )+ diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 00667a307..c39b618ba 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::*; @@ -854,7 +852,10 @@ macro_rules! define_properties { }, Custom(custom) => { custom.name.to_css(dest)?; - dest.delim(':', false)?; + dest.write_char(':')?; + if !custom.value.starts_with_whitespace() { + dest.whitespace()?; + } self.value_to_css(dest)?; write_important!(); return Ok(()) @@ -991,7 +992,7 @@ macro_rules! define_properties { D: serde::Deserializer<'de>, { enum ContentOrRaw<'de> { - Content(serde::__private::de::Content<'de>), + Content(serde_content::Value<'de>), Raw(CowArcStr<'de>) } @@ -1064,26 +1065,26 @@ macro_rules! define_properties { ContentOrRaw::Content(content) => content }; - let deserializer = serde::__private::de::ContentDeserializer::new(content); + let deserializer = serde_content::Deserializer::new(content).coerce_numbers(); match partial.property_id { $( $(#[$meta])* PropertyId::$property$((vp_name!($vp, prefix)))? => { - let value = <$type>::deserialize(deserializer)?; + let value = <$type>::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::$property(value $(, vp_name!($vp, prefix))?)) }, )+ PropertyId::Custom(name) => { if name.as_ref() == "unparsed" { - let value = UnparsedProperty::deserialize(deserializer)?; + let value = UnparsedProperty::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::Unparsed(value)) } else { - let value = CustomProperty::deserialize(deserializer)?; + let value = CustomProperty::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::Custom(value)) } } PropertyId::All => { - let value = CSSWideKeyword::deserialize(deserializer)?; + let value = CSSWideKeyword::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::All(value)) } } @@ -1372,50 +1373,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], @@ -1629,6 +1600,9 @@ define_properties! { "filter": Filter(FilterList<'i>, VendorPrefix) / WebKit, "backdrop-filter": BackdropFilter(FilterList<'i>, VendorPrefix) / WebKit, + // https://www.w3.org/TR/compositing-1/ + "mix-blend-mode": MixBlendMode(BlendMode), + // https://drafts.csswg.org/css2/ "z-index": ZIndex(position::ZIndex), @@ -1645,6 +1619,7 @@ define_properties! { // https://drafts.csswg.org/css-color-adjust/ "color-scheme": ColorScheme(ColorScheme), + "print-color-adjust": PrintColorAdjust(PrintColorAdjust, VendorPrefix) / WebKit, } impl<'i, T: smallvec::Array, V: Parse<'i>> Parse<'i> for SmallVec { diff --git a/src/properties/prefix_handler.rs b/src/properties/prefix_handler.rs index 7ad2dfe8e..b42234777 100644 --- a/src/properties/prefix_handler.rs +++ b/src/properties/prefix_handler.rs @@ -73,13 +73,14 @@ define_prefixes! { ClipPath, BoxDecorationBreak, TextSizeAdjust, + PrintColorAdjust, } macro_rules! define_fallbacks { ( $( $name: ident$(($p: ident))?, )+ ) => { - paste::paste! { + pastey::paste! { #[derive(Default)] pub(crate) struct FallbackHandler { $( @@ -97,7 +98,7 @@ macro_rules! define_fallbacks { $( $p = context.targets.prefixes($p, Feature::$name); )? - if paste::paste! { self.[<$name:snake>] }.is_none() { + if pastey::paste! { self.[<$name:snake>] }.is_none() { let fallbacks = val.get_fallbacks(context.targets); #[allow(unused_variables)] let has_fallbacks = !fallbacks.is_empty(); @@ -112,10 +113,10 @@ macro_rules! define_fallbacks { )? } - if paste::paste! { self.[<$name:snake>] }.is_none() || matches!(context.targets.browsers, Some(targets) if !val.is_compatible(targets)) { - paste::paste! { self.[<$name:snake>] = Some(dest.len()) }; + if pastey::paste! { self.[<$name:snake>] }.is_none() || matches!(context.targets.browsers, Some(targets) if !val.is_compatible(targets)) { + pastey::paste! { self.[<$name:snake>] = Some(dest.len()) }; dest.push(Property::$name(val $(, $p)?)); - } else if let Some(index) = paste::paste! { self.[<$name:snake>] } { + } else if let Some(index) = pastey::paste! { self.[<$name:snake>] } { dest[index] = Property::$name(val $(, $p)?); } } @@ -138,7 +139,7 @@ macro_rules! define_fallbacks { } let val = get_prefixed!($($p)?); - (val, paste::paste! { &mut self.[<$name:snake>] }) + (val, pastey::paste! { &mut self.[<$name:snake>] }) } )+ _ => return false @@ -161,7 +162,7 @@ macro_rules! define_fallbacks { fn finalize(&mut self, _: &mut DeclarationList, _: &mut PropertyHandlerContext) { $( - paste::paste! { self.[<$name:snake>] = None }; + pastey::paste! { self.[<$name:snake>] = None }; )+ } } diff --git a/src/properties/size.rs b/src/properties/size.rs index 0c8733572..c6a5eb2e6 100644 --- a/src/properties/size.rs +++ b/src/properties/size.rs @@ -144,8 +144,7 @@ impl IsCompatible for Size { } Stretch(vp) => match *vp { VendorPrefix::None => Feature::StretchSize, - VendorPrefix::WebKit => Feature::WebkitFillAvailableSize, - VendorPrefix::Moz => Feature::MozAvailableSize, + VendorPrefix::WebKit | VendorPrefix::Moz => Feature::WebkitFillAvailableSize, _ => return false, } .is_compatible(browsers), @@ -278,8 +277,7 @@ impl IsCompatible for MaxSize { } Stretch(vp) => match *vp { VendorPrefix::None => Feature::StretchSize, - VendorPrefix::WebKit => Feature::WebkitFillAvailableSize, - VendorPrefix::Moz => Feature::MozAvailableSize, + VendorPrefix::WebKit | VendorPrefix::Moz => Feature::WebkitFillAvailableSize, _ => return false, } .is_compatible(browsers), diff --git a/src/properties/svg.rs b/src/properties/svg.rs index 16b384568..552dd2d25 100644 --- a/src/properties/svg.rs +++ b/src/properties/svg.rs @@ -211,6 +211,7 @@ pub enum Marker<'i> { /// A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property. #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[css(case = lower)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -249,6 +250,7 @@ pub enum ColorRendering { /// A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property. #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[css(case = lower)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -270,6 +272,7 @@ pub enum ShapeRendering { /// A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property. #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[css(case = lower)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", diff --git a/src/properties/transform.rs b/src/properties/transform.rs index c8f5a7f72..cc00e5783 100644 --- a/src/properties/transform.rs +++ b/src/properties/transform.rs @@ -7,7 +7,6 @@ use crate::error::{ParserError, PrinterError}; use crate::macros::enum_property; use crate::prefixes::Feature; use crate::printer::Printer; -use crate::stylesheet::PrinterOptions; use crate::traits::{Parse, PropertyHandler, ToCss, Zero}; use crate::values::{ angle::Angle, @@ -59,20 +58,6 @@ impl ToCss for TransformList { // TODO: Re-enable with a better solution // See: https://github.com/parcel-bundler/lightningcss/issues/288 - if dest.minify { - let mut base = String::new(); - self.to_css_base(&mut Printer::new( - &mut base, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))?; - - dest.write_str(&base)?; - - return Ok(()); - } // if dest.minify { // // Combine transforms into a single matrix. // if let Some(matrix) = self.to_matrix() { @@ -141,7 +126,13 @@ impl TransformList { where W: std::fmt::Write, { + let mut first = true; for item in &self.0 { + if first { + first = false; + } else { + dest.whitespace()?; + } item.to_css(dest)?; } Ok(()) @@ -1518,29 +1509,39 @@ impl Translate { /// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "lowercase") +)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] -pub struct Rotate { - /// Rotation around the x axis. - pub x: f32, - /// Rotation around the y axis. - pub y: f32, - /// Rotation around the z axis. - pub z: f32, - /// The angle of rotation. - pub angle: Angle, +pub enum Rotate { + /// The `none` keyword. + None, + + /// Rotation on the x, y, and z axes. + #[cfg_attr(feature = "serde", serde(untagged))] + XYZ { + /// Rotation around the x axis. + x: f32, + /// Rotation around the y axis. + y: f32, + /// Rotation around the z axis. + z: f32, + /// The angle of rotation. + angle: Angle, + }, } impl<'i> Parse<'i> for Rotate { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + // CSS Transforms 2 §5.1: + // "It must serialize as the keyword none if and only if none was originally specified." + // Keep `none` explicit so identity rotations (e.g. `0deg`) do not round-trip to `none`. + // https://drafts.csswg.org/css-transforms-2/#individual-transforms if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { - return Ok(Rotate { - x: 0.0, - y: 0.0, - z: 1.0, - angle: Angle::Deg(0.0), - }); + return Ok(Rotate::None); } let angle = input.try_parse(Angle::parse); @@ -1564,7 +1565,7 @@ impl<'i> Parse<'i> for Rotate { ) .unwrap_or((0.0, 0.0, 1.0)); let angle = angle.or_else(|_| Angle::parse(input))?; - Ok(Rotate { x, y, z, angle }) + Ok(Rotate::XYZ { x, y, z, angle }) } } @@ -1573,32 +1574,46 @@ impl ToCss for Rotate { where W: std::fmt::Write, { - if self.x == 0.0 && self.y == 0.0 && self.z == 1.0 && self.angle.is_zero() { - dest.write_str("none")?; - return Ok(()); - } - - if self.x == 1.0 && self.y == 0.0 && self.z == 0.0 { - dest.write_str("x ")?; - } else if self.x == 0.0 && self.y == 1.0 && self.z == 0.0 { - dest.write_str("y ")?; - } else if !(self.x == 0.0 && self.y == 0.0 && self.z == 1.0) { - self.x.to_css(dest)?; - dest.write_char(' ')?; - self.y.to_css(dest)?; - dest.write_char(' ')?; - self.z.to_css(dest)?; - dest.write_char(' ')?; + match self { + Rotate::None => dest.write_str("none"), + Rotate::XYZ { x, y, z, angle } => { + // CSS Transforms 2 §5.1: + // "If the axis is parallel with the x or y axes, it must serialize as the appropriate keyword." + // "If a rotation about the z axis ... must serialize as just an ." + // Normalize parallel vectors (including non-unit vectors); flip the angle for negative axis directions. + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + if *y == 0.0 && *z == 0.0 && *x != 0.0 { + let angle = if *x < 0.0 { angle.clone() * -1.0 } else { angle.clone() }; + dest.write_str("x ")?; + angle.to_css(dest) + } else if *x == 0.0 && *z == 0.0 && *y != 0.0 { + let angle = if *y < 0.0 { angle.clone() * -1.0 } else { angle.clone() }; + dest.write_str("y ")?; + angle.to_css(dest) + } else if *x == 0.0 && *y == 0.0 && *z != 0.0 { + let angle = if *z < 0.0 { angle.clone() * -1.0 } else { angle.clone() }; + angle.to_css(dest) + } else { + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest)?; + dest.write_char(' ')?; + z.to_css(dest)?; + dest.write_char(' ')?; + angle.to_css(dest) + } + } } - - self.angle.to_css(dest) } } impl Rotate { /// Converts the rotation to a transform function. pub fn to_transform(&self) -> Transform { - Transform::Rotate3d(self.x, self.y, self.z, self.angle.clone()) + match self { + Rotate::None => Transform::Rotate3d(0.0, 0.0, 1.0, Angle::Deg(0.0)), + Rotate::XYZ { x, y, z, angle } => Transform::Rotate3d(*x, *y, *z, angle.clone()), + } } } diff --git a/src/properties/ui.rs b/src/properties/ui.rs index 9b0bb7149..e0802c500 100644 --- a/src/properties/ui.rs +++ b/src/properties/ui.rs @@ -433,33 +433,44 @@ bitflags! { impl<'i> Parse<'i> for ColorScheme { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let mut res = ColorScheme::empty(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &ident, - "normal" => return Ok(res), - "only" => res |= ColorScheme::Only, - "light" => res |= ColorScheme::Light, - "dark" => res |= ColorScheme::Dark, - _ => {} - }; + let mut has_any = false; - while let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) { - match_ignore_ascii_case! { &ident, - "normal" => return Err(input.new_custom_error(ParserError::InvalidValue)), - "only" => { - // Only must be at the start or the end, not in the middle. - if res.contains(ColorScheme::Only) { - return Err(input.new_custom_error(ParserError::InvalidValue)); - } - res |= ColorScheme::Only; - return Ok(res); - }, - "light" => res |= ColorScheme::Light, - "dark" => res |= ColorScheme::Dark, - _ => {} - }; + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + return Ok(res); } - Ok(res) + if input.try_parse(|input| input.expect_ident_matching("only")).is_ok() { + res |= ColorScheme::Only; + has_any = true; + } + + loop { + if input.try_parse(|input| input.expect_ident_matching("light")).is_ok() { + res |= ColorScheme::Light; + has_any = true; + continue; + } + + if input.try_parse(|input| input.expect_ident_matching("dark")).is_ok() { + res |= ColorScheme::Dark; + has_any = true; + continue; + } + + break; + } + + // Only is allowed at the start or the end. + if !res.contains(ColorScheme::Only) && input.try_parse(|input| input.expect_ident_matching("only")).is_ok() { + res |= ColorScheme::Only; + has_any = true; + } + + if has_any { + return Ok(res); + } + + Err(input.new_custom_error(ParserError::InvalidValue)) } } @@ -484,6 +495,10 @@ impl ToCss for ColorScheme { } if self.contains(ColorScheme::Only) { + // Avoid parsing `color-scheme: only` as `color-scheme: only` + if !self.intersects(ColorScheme::Light | ColorScheme::Dark) { + return dest.write_str("only"); + } dest.write_str(" only")?; } @@ -571,6 +586,16 @@ impl<'i> PropertyHandler<'i> for ColorSchemeHandler { fn finalize(&mut self, _: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {} } +enum_property! { + /// A value for the [print-color-adjust](https://drafts.csswg.org/css-color-adjust/#propdef-print-color-adjust) property. + pub enum PrintColorAdjust { + /// The user agent is allowed to make adjustments to the element as it deems appropriate. + Economy, + /// The user agent is not allowed to make adjustments to the element. + Exact, + } +} + #[inline] fn define_var<'i>(name: &'static str, value: Token<'static>) -> Property<'i> { Property::Custom(CustomProperty { diff --git a/src/rules/container.rs b/src/rules/container.rs index a3ac87feb..97bb1b3a4 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -11,6 +11,7 @@ use crate::media_query::{ }; use crate::parser::{DefaultAtRule, ParserOptions}; use crate::printer::Printer; +use crate::properties::custom::TokenList; use crate::properties::{Property, PropertyId}; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; @@ -31,7 +32,7 @@ pub struct ContainerRule<'i, R = DefaultAtRule> { #[cfg_attr(feature = "serde", serde(borrow))] pub name: Option>, /// The container condition. - pub condition: ContainerCondition<'i>, + pub condition: Option>, /// The rules within the `@container` rule. pub rules: CssRuleList<'i, R>, /// The location of the rule in the source file. @@ -68,6 +69,12 @@ pub enum ContainerCondition<'i> { /// A style query. #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] Style(StyleQuery<'i>), + /// A scroll state query. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + ScrollState(ScrollStateQuery<'i>), + /// Unknown tokens. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + Unknown(TokenList<'i>), } /// A container query size feature. @@ -133,6 +140,61 @@ pub enum StyleQuery<'i> { }, } +/// Represents a scroll state query within a container condition. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type", rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum ScrollStateQuery<'i> { + /// A size container feature, implicitly parenthesized. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + Feature(ScrollStateFeature<'i>), + /// A negation of a condition. + #[cfg_attr(feature = "visitor", skip_type)] + #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] + Not(Box>), + /// A set of joint operations. + #[cfg_attr(feature = "visitor", skip_type)] + Operation { + /// The operator for the conditions. + operator: Operator, + /// The conditions for the operator. + conditions: Vec>, + }, +} + +/// A container query size feature. +pub type ScrollStateFeature<'i> = QueryFeature<'i, ScrollStateFeatureId>; + +define_query_features! { + /// A container query scroll state feature identifier. + pub enum ScrollStateFeatureId { + /// The [stuck](https://drafts.csswg.org/css-conditional-5/#stuck) scroll state feature. + "stuck": Stuck = Ident, + /// The [snapped](https://drafts.csswg.org/css-conditional-5/#snapped) scroll state feature. + "snapped": Snapped = Ident, + /// The [scrollable](https://drafts.csswg.org/css-conditional-5/#scrollable) scroll state feature. + "scrollable": Scrollable = Ident, + /// The [scrolled](https://drafts.csswg.org/css-conditional-5/#scrolled) scroll state feature. + "scrolled": Scrolled = Ident, + } +} + +impl FeatureToCss for ScrollStateFeatureId { + fn to_css_with_prefix(&self, prefix: &str, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + dest.write_str(prefix)?; + self.to_css(dest) + } +} + impl<'i> QueryCondition<'i> for ContainerCondition<'i> { #[inline] fn parse_feature<'t>( @@ -168,12 +230,58 @@ impl<'i> QueryCondition<'i> for ContainerCondition<'i> { }) } + fn parse_scroll_state_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, options)) + { + return Ok(Self::ScrollState(res)); + } + + Ok(Self::ScrollState(ScrollStateQuery::parse_feature(input, options)?)) + }) + } + fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { match self { ContainerCondition::Not(_) => true, ContainerCondition::Operation { operator, .. } => Some(*operator) != parent_operator, ContainerCondition::Feature(f) => f.needs_parens(parent_operator, targets), ContainerCondition::Style(_) => false, + ContainerCondition::ScrollState(_) => false, + ContainerCondition::Unknown(_) => false, + } + } +} + +impl<'i> QueryCondition<'i> for ScrollStateQuery<'i> { + #[inline] + 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)) + } + + #[inline] + fn create_negation(condition: Box) -> Self { + Self::Not(condition) + } + + #[inline] + fn create_operation(operator: Operator, conditions: Vec) -> Self { + Self::Operation { operator, conditions } + } + + fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { + match self { + ScrollStateQuery::Not(_) => true, + ScrollStateQuery::Operation { operator, .. } => Some(*operator) != parent_operator, + ScrollStateQuery::Feature(f) => f.needs_parens(parent_operator, targets), } } } @@ -219,11 +327,24 @@ impl<'i> ParseWithOptions<'i> for ContainerCondition<'i> { input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, ) -> Result>> { - parse_query_condition( - input, - QueryConditionFlags::ALLOW_OR | QueryConditionFlags::ALLOW_STYLE, - options, - ) + input + .try_parse(|input| { + parse_query_condition( + input, + QueryConditionFlags::ALLOW_OR + | QueryConditionFlags::ALLOW_STYLE + | QueryConditionFlags::ALLOW_SCROLL_STATE, + options, + ) + }) + .or_else(|e| { + if options.error_recovery { + options.warn(e); + Ok(ContainerCondition::Unknown(TokenList::parse(input, options, 0)?)) + } else { + Err(e) + } + }) } } @@ -247,6 +368,19 @@ impl<'i> ToCss for ContainerCondition<'i> { query.to_css(dest)?; dest.write_char(')') } + ContainerCondition::ScrollState(ref query) => { + let needs_parens = !matches!(query, ScrollStateQuery::Feature(_)); + dest.write_str("scroll-state")?; + if needs_parens { + dest.write_char('(')?; + } + query.to_css(dest)?; + if needs_parens { + dest.write_char(')')?; + } + Ok(()) + } + ContainerCondition::Unknown(ref tokens) => tokens.to_css(dest, false), } } } @@ -271,6 +405,25 @@ impl<'i> ToCss for StyleQuery<'i> { } } +impl<'i> ToCss for ScrollStateQuery<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match *self { + ScrollStateQuery::Feature(ref f) => f.to_css(dest), + ScrollStateQuery::Not(ref c) => { + dest.write_str("not ")?; + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) + } + ScrollStateQuery::Operation { + ref conditions, + operator, + } => operation_to_css(operator, conditions, dest), + } + } +} + /// A [``](https://drafts.csswg.org/css-contain-3/#typedef-container-name) in a `@container` rule. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -325,16 +478,22 @@ impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); dest.write_str("@container ")?; + let has_condition = self.condition.is_some(); + if let Some(name) = &self.name { name.to_css(dest)?; - dest.write_char(' ')?; + if has_condition { + dest.write_char(' ')?; + } } - // Don't downlevel range syntax in container queries. - let exclude = dest.targets.current.exclude; - dest.targets.current.exclude.insert(Features::MediaQueries); - self.condition.to_css(dest)?; - dest.targets.current.exclude = exclude; + if let Some(condition) = &self.condition { + // Don't downlevel range syntax in container queries. + let exclude = dest.targets.current.exclude; + dest.targets.current.exclude.insert(Features::MediaQueries); + condition.to_css(dest)?; + dest.targets.current.exclude = exclude; + } dest.whitespace()?; dest.write_char('{')?; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4413b985b..5598c96f5 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::TargetsWithSupportsScope; +use crate::targets::{should_compile, TargetsWithSupportsScope}; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -209,7 +209,7 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul struct PartialRule<'de> { rule_type: CowArcStr<'de>, - content: serde::__private::de::Content<'de>, + content: serde_content::Value<'de>, } struct CssRuleVisitor; @@ -226,7 +226,7 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul A: serde::de::MapAccess<'de>, { let mut rule_type: Option> = None; - let mut value: Option = None; + let mut value: Option = None; while let Some(key) = map.next_key()? { match key { Field::Type => { @@ -245,108 +245,122 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul } let partial = deserializer.deserialize_map(CssRuleVisitor)?; - let deserializer = serde::__private::de::ContentDeserializer::new(partial.content); + let deserializer = serde_content::Deserializer::new(partial.content).coerce_numbers(); match partial.rule_type.as_ref() { "media" => { - let rule = MediaRule::deserialize(deserializer)?; + let rule = MediaRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Media(rule)) } "import" => { - let rule = ImportRule::deserialize(deserializer)?; + let rule = ImportRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Import(rule)) } "style" => { - let rule = StyleRule::deserialize(deserializer)?; + let rule = StyleRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Style(rule)) } "keyframes" => { - let rule = KeyframesRule::deserialize(deserializer)?; + let rule = + KeyframesRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Keyframes(rule)) } "font-face" => { - let rule = FontFaceRule::deserialize(deserializer)?; + let rule = FontFaceRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::FontFace(rule)) } "font-palette-values" => { - let rule = FontPaletteValuesRule::deserialize(deserializer)?; + let rule = + FontPaletteValuesRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::FontPaletteValues(rule)) } "font-feature-values" => { - let rule = FontFeatureValuesRule::deserialize(deserializer)?; + let rule = + FontFeatureValuesRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::FontFeatureValues(rule)) } "page" => { - let rule = PageRule::deserialize(deserializer)?; + let rule = PageRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Page(rule)) } "supports" => { - let rule = SupportsRule::deserialize(deserializer)?; + let rule = SupportsRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Supports(rule)) } "counter-style" => { - let rule = CounterStyleRule::deserialize(deserializer)?; + let rule = + CounterStyleRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::CounterStyle(rule)) } "namespace" => { - let rule = NamespaceRule::deserialize(deserializer)?; + let rule = + NamespaceRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Namespace(rule)) } "moz-document" => { - let rule = MozDocumentRule::deserialize(deserializer)?; + let rule = + MozDocumentRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::MozDocument(rule)) } "nesting" => { - let rule = NestingRule::deserialize(deserializer)?; + let rule = NestingRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Nesting(rule)) } "nested-declarations" => { - let rule = NestedDeclarationsRule::deserialize(deserializer)?; + let rule = NestedDeclarationsRule::deserialize(deserializer) + .map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::NestedDeclarations(rule)) } "viewport" => { - let rule = ViewportRule::deserialize(deserializer)?; + let rule = ViewportRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Viewport(rule)) } "custom-media" => { - let rule = CustomMediaRule::deserialize(deserializer)?; + let rule = + CustomMediaRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::CustomMedia(rule)) } "layer-statement" => { - let rule = LayerStatementRule::deserialize(deserializer)?; + let rule = + LayerStatementRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::LayerStatement(rule)) } "layer-block" => { - let rule = LayerBlockRule::deserialize(deserializer)?; + let rule = + LayerBlockRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::LayerBlock(rule)) } "property" => { - let rule = PropertyRule::deserialize(deserializer)?; + let rule = PropertyRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Property(rule)) } "container" => { - let rule = ContainerRule::deserialize(deserializer)?; + let rule = + ContainerRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Container(rule)) } "scope" => { - let rule = ScopeRule::deserialize(deserializer)?; + let rule = ScopeRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Scope(rule)) } "starting-style" => { - let rule = StartingStyleRule::deserialize(deserializer)?; + let rule = + StartingStyleRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::StartingStyle(rule)) } "view-transition" => { - let rule = ViewTransitionRule::deserialize(deserializer)?; + let rule = + ViewTransitionRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::ViewTransition(rule)) } "ignored" => Ok(CssRule::Ignored), "unknown" => { - let rule = UnknownAtRule::deserialize(deserializer)?; + let rule = + UnknownAtRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Unknown(rule)) } "custom" => { - let rule = R::deserialize(deserializer)?; + let rule = R::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Custom(rule)) } t => Err(serde::de::Error::unknown_variant(t, &[])), @@ -524,6 +538,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { ) -> Result<(), MinifyError> { let mut keyframe_rules = HashMap::new(); let mut layer_rules = HashMap::new(); + let mut has_layers = false; let mut property_rules = HashMap::new(); let mut font_feature_values_rules = Vec::new(); let mut style_rules = @@ -629,6 +644,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { } layer_rules.insert(name.clone(), rules.len()); + has_layers = true; } } CssRule::LayerStatement(layer) => { @@ -637,6 +653,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { for name in &layer.names { if !layer_rules.contains_key(name) { layer_rules.insert(name.clone(), rules.len()); + has_layers = true; rules.push(CssRule::LayerBlock(LayerBlockRule { name: Some(name.clone()), rules: CssRuleList(vec![]), @@ -870,6 +887,10 @@ impl<'i, T: Clone> CssRuleList<'i, T> { property_rules.insert(property.name.clone(), rules.len()); } } + CssRule::Import(_) => { + // @layer blocks can't be inlined into layers declared before imports. + layer_rules.clear(); + } _ => {} } @@ -878,7 +899,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { // Optimize @layer rules. Combine subsequent empty layer blocks into a single @layer statement // so that layers are declared in the correct order. - if !layer_rules.is_empty() { + if has_layers { let mut declared_layers = HashSet::new(); let mut layer_statement = None; for index in 0..rules.len() { @@ -1004,7 +1025,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { let mut first = true; let mut last_without_block = false; - for rule in &self.0 { + for (i, rule) in self.0.iter().enumerate() { if let CssRule::Ignored = &rule { continue; } @@ -1040,6 +1061,16 @@ impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { dest.newline()?; } rule.to_css(dest)?; + + // If this is an invisible nested declarations rule, and not the last rule in the block, add a semicolon. + if dest.minify + && !should_compile!(dest.targets.current, Nesting) + && matches!(rule, CssRule::NestedDeclarations(_)) + && i != self.0.len() - 1 + { + dest.write_char(';')?; + } + last_without_block = matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) diff --git a/src/selector.rs b/src/selector.rs index 28cb1836b..355ab66e3 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -230,6 +230,10 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, let kind = Parse::parse(parser)?; ActiveViewTransitionType { kind } }, + "state" => { + let state = CustomIdent::parse(parser)?; + State { state } + }, "local" if self.options.css_modules.is_some() => Local { selector: Box::new(Selector::parse(self, parser)?) }, "global" if self.options.css_modules.is_some() => Global { selector: Box::new(Selector::parse(self, parser)?) }, _ => { @@ -292,8 +296,14 @@ 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, + "grammar-error" => GrammarError, + "spelling-error" => SpellingError, + _ => { if !name.starts_with('-') { self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone()))); @@ -314,6 +324,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)? }, @@ -527,6 +538,12 @@ pub enum PseudoClass<'i> { kind: SmallVec<[CustomIdent<'i>; 1]>, }, + /// The [:state()](https://developer.mozilla.org/en-US/docs/Web/CSS/:state) pseudo class for custom element states. + State { + /// The custom state identifier. + state: CustomIdent<'i>, + }, + // CSS modules /// The CSS modules :local() pseudo class. Local { @@ -669,6 +686,11 @@ where dir.to_css(dest)?; return dest.write_str(")"); } + State { state } => { + dest.write_str(":state(")?; + state.to_css(dest)?; + return dest.write_str(")"); + } _ => {} } @@ -816,7 +838,7 @@ where }) } - Lang { languages: _ } | Dir { direction: _ } => unreachable!(), + Lang { languages: _ } | Dir { direction: _ } | State { .. } => unreachable!(), Custom { name } => { dest.write_char(':')?; return dest.write_str(&name); @@ -953,6 +975,19 @@ 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, + /// The [::grammar-error](https://drafts.csswg.org/css-pseudo/#selectordef-grammar-error) pseudo element. + GrammarError, + /// The [::spelling-error](https://drafts.csswg.org/css-pseudo/#selectordef-spelling-error) pseudo element. + SpellingError, /// An unknown pseudo element. Custom { /// The name of the pseudo element. @@ -1070,10 +1105,14 @@ impl<'i> Parse<'i> for ViewTransitionPartSelector<'i> { _ => return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))), } } else { - return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))); + break; } } + if !input.is_exhausted() || (name.is_none() && classes.is_empty()) { + return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))); + } + Ok(ViewTransitionPartSelector { name, classes }) } } @@ -1213,6 +1252,15 @@ 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"), + GrammarError => dest.write_str("::grammar-error"), + SpellingError => dest.write_str("::spelling-error"), Custom { name: val } => { dest.write_str("::")?; return dest.write_str(val); @@ -1883,6 +1931,8 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { PseudoClass::Autofill(prefix) if *prefix == VendorPrefix::None => Feature::Autofill, + PseudoClass::State { .. } => Feature::StatePseudoClass, + // Experimental, no browser support. PseudoClass::Current | PseudoClass::Past @@ -1924,6 +1974,11 @@ 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::GrammarError => Feature::GrammarError, + PseudoElement::SpellingError => Feature::SpellingError, PseudoElement::Custom { name: _ } | _ => return false, }, diff --git a/src/stylesheet.rs b/src/stylesheet.rs index dcf87f1c9..990a09be1 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 { diff --git a/src/targets.rs b/src/targets.rs index c568e2852..91ff8c677 100644 --- a/src/targets.rs +++ b/src/targets.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; /// ..Browsers::default() /// }; /// ``` -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize, Deserialize))] #[allow(missing_docs)] pub struct Browsers { @@ -41,6 +41,9 @@ pub struct Browsers { pub samsung: Option, } +#[cfg(feature = "browserslist")] +pub use browserslist::Opts as BrowserslistConfig; + #[cfg(feature = "browserslist")] #[cfg_attr(docsrs, doc(cfg(feature = "browserslist")))] impl Browsers { @@ -48,9 +51,17 @@ impl Browsers { pub fn from_browserslist, I: IntoIterator>( query: I, ) -> Result, browserslist::Error> { - use browserslist::{resolve, Opts}; + Self::from_browserslist_with_config(query, BrowserslistConfig::default()) + } + + /// Parses a list of browserslist queries into Lightning CSS targets. + pub fn from_browserslist_with_config, I: IntoIterator>( + query: I, + config: BrowserslistConfig, + ) -> Result, browserslist::Error> { + use browserslist::resolve; - Self::from_distribs(resolve(query, &Opts::default())?) + Self::from_distribs(resolve(query, &config)?) } #[cfg(not(target_arch = "wasm32"))] @@ -140,6 +151,7 @@ fn parse_version(version: &str) -> Option { bitflags! { /// Features to explicitly enable or disable. #[derive(Debug, Default, Clone, Copy, Hash, Eq, PartialEq)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))] pub struct Features: u32 { const Nesting = 1 << 0; const NotSelectorList = 1 << 1; @@ -181,7 +193,8 @@ pub(crate) trait FeaturesIterator: Sized + Iterator { impl FeaturesIterator for I where I: Iterator {} /// Target browsers and features to compile. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Targets { /// Browser targets to compile the CSS for. pub browsers: Option, diff --git a/src/values/angle.rs b/src/values/angle.rs index a1bc6a00a..b7fb62329 100644 --- a/src/values/angle.rs +++ b/src/values/angle.rs @@ -121,6 +121,8 @@ impl ToCss for Angle { } Angle::Turn(val) => (*val, "turn"), }; + // Canonicalize negative zero so serialization is stable (`0deg` instead of `-0deg`). + let value = if value == 0.0 { 0.0 } else { value }; serialize_dimension(value, unit, dest) } @@ -183,11 +185,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 +289,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 22303967a..c57cfb044 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 @@ -382,10 +384,10 @@ impl< })?; // According to the spec, the minimum should "win" over the maximum if they are in the wrong order. - let cmp = if let (Some(Calc::Value(max_val)), Calc::Value(center_val)) = (&max, ¢er) { - center_val.partial_cmp(&max_val) - } else { - None + let cmp = match (&max, ¢er) { + (Some(Calc::Value(max_val)), Calc::Value(center_val)) => center_val.partial_cmp(&max_val), + (Some(Calc::Number(max_val)), Calc::Number(center_val)) => center_val.partial_cmp(max_val), + _ => None, }; // If center is known to be greater than the maximum, replace it with maximum and remove the max argument. @@ -401,10 +403,10 @@ impl< } if cmp.is_some() { - let cmp = if let (Some(Calc::Value(min_val)), Calc::Value(center_val)) = (&min, ¢er) { - center_val.partial_cmp(&min_val) - } else { - None + let cmp = match (&min, ¢er) { + (Some(Calc::Value(min_val)), Calc::Value(center_val)) => center_val.partial_cmp(&min_val), + (Some(Calc::Number(min_val)), Calc::Number(center_val)) => center_val.partial_cmp(min_val), + _ => None, }; // If center is known to be less than the minimum, replace it with minimum and remove the min argument. @@ -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(); @@ -656,6 +658,7 @@ impl< fn reduce_args(args: &mut Vec>, cmp: std::cmp::Ordering) -> Vec> { // Reduces the arguments of a min() or max() expression, combining compatible values. // e.g. min(1px, 1em, 2px, 3in) => min(1px, 1em) + // Also handles plain numbers: min(1, 2, 3) => min(1, 2) let mut reduced: Vec> = vec![]; for arg in args.drain(..) { let mut found = None; @@ -677,6 +680,23 @@ impl< } } } + Calc::Number(val) => { + for b in reduced.iter_mut() { + if let Calc::Number(v) = b { + match val.partial_cmp(v) { + Some(ord) if ord == cmp => { + found = Some(Some(b)); + break; + } + Some(_) => { + found = Some(None); + break; + } + None => {} + } + } + } + } _ => {} } if let Some(r) = found { @@ -744,9 +764,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 +926,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 +955,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 3edf84474..b21eb1968 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/gradient.rs b/src/values/gradient.rs index 6ee45f525..22d748eb5 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). diff --git a/src/values/length.rs b/src/values/length.rs index 149f1dd18..2ce01b06e 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 54352750e..5e0594570 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/position.rs b/src/values/position.rs index 61373cb87..32e4788d4 100644 --- a/src/values/position.rs +++ b/src/values/position.rs @@ -203,11 +203,32 @@ impl ToCss for Position { // `center` is assumed if omitted. x_lp.to_css(dest) } + ( + &HorizontalPosition::Side { + side: HorizontalPositionKeyword::Left, + offset: Some(ref x_lp), + }, + y, + ) if y.is_center() => { + // `left 10px center` => `10px` (omit Y when center) + x_lp.to_css(dest) + } (&HorizontalPosition::Side { side, offset: None }, y) if y.is_center() => { let p: LengthPercentage = side.into(); p.to_css(dest) } (x, y_pos @ &VerticalPosition::Side { offset: None, .. }) if x.is_center() => y_pos.to_css(dest), + ( + &HorizontalPosition::Center, + y_pos @ &VerticalPosition::Side { + side: VerticalPositionKeyword::Bottom, + offset: Some(_), + }, + ) => { + // `center bottom 10px` must keep the keyword form + dest.write_str("center ")?; + y_pos.to_css(dest) + } (&HorizontalPosition::Side { side: x, offset: None }, &VerticalPosition::Side { side: y, offset: None }) => { let x: LengthPercentage = x.into(); let y: LengthPercentage = y.into(); diff --git a/src/values/syntax.rs b/src/values/syntax.rs index 42dfe485c..306f5e413 100644 --- a/src/values/syntax.rs +++ b/src/values/syntax.rs @@ -63,6 +63,8 @@ pub enum SyntaxComponentKind { Percentage, /// A `` component. LengthPercentage, + /// A `` component. + String, /// A `` component. Color, /// An `` component. @@ -126,6 +128,8 @@ pub enum ParsedComponent<'i> { Percentage(values::percentage::Percentage), /// A `` value. LengthPercentage(values::length::LengthPercentage), + /// A `` value. + String(values::string::CSSString<'i>), /// A `` value. Color(values::color::CssColor), /// An `` value. @@ -222,6 +226,7 @@ impl<'i> SyntaxString { SyntaxComponentKind::LengthPercentage => { ParsedComponent::LengthPercentage(values::length::LengthPercentage::parse(input)?) } + SyntaxComponentKind::String => ParsedComponent::String(values::string::CSSString::parse(input)?), SyntaxComponentKind::Color => ParsedComponent::Color(values::color::CssColor::parse(input)?), SyntaxComponentKind::Image => ParsedComponent::Image(values::image::Image::parse(input)?), SyntaxComponentKind::Url => ParsedComponent::Url(values::url::Url::parse(input)?), @@ -343,6 +348,7 @@ impl SyntaxComponentKind { "number" => SyntaxComponentKind::Number, "percentage" => SyntaxComponentKind::Percentage, "length-percentage" => SyntaxComponentKind::LengthPercentage, + "string" => SyntaxComponentKind::String, "color" => SyntaxComponentKind::Color, "image" => SyntaxComponentKind::Image, "url" => SyntaxComponentKind::Url, @@ -440,6 +446,7 @@ impl ToCss for SyntaxComponentKind { Number => "", Percentage => "", LengthPercentage => "", + String => "", Color => "", Image => "", Url => "", @@ -467,6 +474,7 @@ impl<'i> ToCss for ParsedComponent<'i> { Number(v) => v.to_css(dest), Percentage(v) => v.to_css(dest), LengthPercentage(v) => v.to_css(dest), + String(v) => v.to_css(dest), Color(v) => v.to_css(dest), Image(v) => v.to_css(dest), Url(v) => v.to_css(dest), @@ -640,6 +648,12 @@ mod tests { test("foo | bar | baz", "bar", ParsedComponent::Literal("bar".into())); + test( + "", + "'foo'", + ParsedComponent::String(values::string::CSSString("foo".into())), + ); + test( "", "hi", diff --git a/src/values/time.rs b/src/values/time.rs index 20408e8ad..11e67b312 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