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 f8b9aa3ec..95d304311 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,9 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc binary: lightningcss.exe + - os: windows-latest + target: aarch64-pc-windows-msvc + binary: lightningcss.exe # Mac OS - os: macos-latest target: x86_64-apple-darwin @@ -45,7 +48,7 @@ jobs: if: ${{ matrix.strip }} run: ${{ matrix.strip }} *.node ${{ matrix.binary }} - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: | @@ -87,7 +90,7 @@ jobs: - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 run: strip -x *.node lightningcss - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-aarch64-apple-darwin path: | @@ -106,14 +109,17 @@ 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:lts-debian-zig + image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57 - target: aarch64-unknown-linux-musl - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:78c9ab1f117f8c535b93c4b91a2f19063dda6e4dba48a6187df49810625992c1 strip: aarch64-linux-musl-strip - target: x86_64-unknown-linux-musl - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:78c9ab1f117f8c535b93c4b91a2f19063dda6e4dba48a6187df49810625992c1 strip: strip name: build-${{ matrix.target }} @@ -130,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 }} @@ -141,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 @@ -150,7 +167,7 @@ jobs: if: ${{ matrix.strip }} run: ${{ matrix.strip }} *.node lightningcss - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: | @@ -158,11 +175,11 @@ jobs: lightningcss build-freebsd: - runs-on: macos-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build FreeBSD - uses: cross-platform-actions/action@v0.19.0 + uses: cross-platform-actions/action@v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -170,7 +187,10 @@ jobs: RUSTUP_IO_THREADS: 1 with: operating_system: freebsd - version: '13.2' + version: '14.0' + memory: 13G + cpu_count: 3 + environment_variables: 'DEBUG RUSTUP_IO_THREADS' shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm yarn @@ -195,7 +215,7 @@ jobs: rm -rf .yarn/cache - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-x86_64-unknown-freebsd path: | @@ -222,13 +242,11 @@ jobs: curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz tar -xf binaryen-version_111-x86_64-linux.tar.gz - name: Build wasm - run: yarn wasm:build-release - - name: Optimize wasm run: | - ./binaryen-version_111/bin/wasm-opt wasm/lightningcss_node.wasm -Oz -o wasm/lightningcss_node.opt.wasm - mv wasm/lightningcss_node.opt.wasm wasm/lightningcss_node.wasm + export PATH="$PATH:./binaryen-version_111/bin" + yarn wasm:build-release - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: wasm path: wasm/lightningcss_node.wasm @@ -246,9 +264,11 @@ jobs: - uses: actions/checkout@v3 - uses: bahmutov/npm-install@v1.8.32 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts + - name: Show artifacts + run: ls -R artifacts - name: Build npm packages run: | node scripts/build-npm.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8a765fdf..275e55dfc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo fmt - run: cargo test --all-features + test-js: runs-on: ubuntu-latest steps: @@ -31,3 +32,29 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: yarn build - run: yarn test + - run: yarn tsc + + test-wasm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - uses: bahmutov/npm-install@v1.8.32 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - name: Setup rust target + run: rustup target add wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - name: Install wasm-opt + run: | + curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz + tar -xf binaryen-version_111-x86_64-linux.tar.gz + - name: Build wasm + run: | + export PATH="$PATH:./binaryen-version_111/bin" + yarn wasm:build-release + - run: TEST_WASM=node yarn test + - run: TEST_WASM=browser yarn test 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 a17f48874..bb4bb2840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,28 +1,47 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", "once_cell", "serde", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -34,26 +53,21 @@ dependencies = [ [[package]] name = "anstyle" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" - -[[package]] -name = "anyhow" -version = "1.0.70" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "assert_cmd" -version = "2.0.10" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0b2340f55d9661d76793b2bfc2eb0e62689bd79d067a95707ea762afd5e9dd" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", - "predicates 3.0.2", + "libc", + "predicates 3.1.3", "predicates-core", "predicates-tree", "wait-timeout", @@ -61,14 +75,14 @@ dependencies = [ [[package]] name = "assert_fs" -version = "1.0.12" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d5bf7e5441c6393b5a9670a5036abe6b4847612f594b870f7332dbf10cf6fa" +checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" dependencies = [ "anstyle", "doc-comment", "globwalk", - "predicates 3.0.2", + "predicates 3.1.3", "predicates-core", "predicates-tree", "tempfile", @@ -80,16 +94,16 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", "winapi", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64-simd" @@ -108,57 +122,74 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.2.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz 0.5.1", +] + +[[package]] +name = "browserslist-data" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c49471c5ae53cefe3ac4acc4d3c75cb4b68995b70b3bbb864f8e08fae282098c" +dependencies = [ + "ahash 0.8.12", + "chrono", +] [[package]] name = "browserslist-rs" -version = "0.12.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef956561c9a03c35af46714efd0c135e21768a2a012f900ca8a59b28e75d0cd1" +checksum = "8dd48a6ca358df4f7000e3fb5f08738b1b91a0e5d5f862e2f77b2b14647547f5" dependencies = [ - "ahash", - "anyhow", + "ahash 0.8.12", + "browserslist-data", "chrono", "either", - "itertools", - "js-sys", + "itertools 0.13.0", "nom", - "once_cell", - "quote", "serde", - "serde-wasm-bindgen", "serde_json", - "string_cache", - "string_cache_codegen", "thiserror", - "wasm-bindgen", ] [[package]] name = "bstr" -version = "1.4.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", - "once_cell", "regex-automata", "serde", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" -version = "0.6.10" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -167,9 +198,9 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.10" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" dependencies = [ "proc-macro2", "quote", @@ -177,20 +208,20 @@ dependencies = [ ] [[package]] -name = "byteorder" -version = "1.4.3" +name = "bytes" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbindgen" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" +checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" dependencies = [ "clap", "heck", - "indexmap", + "indexmap 1.9.3", "log", "proc-macro2", "quote", @@ -203,9 +234,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -215,30 +249,27 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ + "android-tzdata", "iana-time-zone", - "js-sys", - "num-integer", "num-traits", - "time", - "wasm-bindgen", - "winapi", + "windows-targets", ] [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", "clap_derive", "clap_lex", - "indexmap", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", @@ -247,9 +278,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.18" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ "heck", "proc-macro-error", @@ -267,16 +298,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "const-str" version = "0.3.2" @@ -308,143 +329,95 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-channel" -version = "0.5.7" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" -dependencies = [ - "cfg-if", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "cssparser" -version = "0.29.6" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "matches", "phf", - "proc-macro2", - "quote", "serde", "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ctor" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" -dependencies = [ - "quote", - "syn 1.0.109", ] [[package]] -name = "cxx" -version = "1.0.94" +name = "cssparser-color" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", + "cssparser", ] [[package]] -name = "cxx-build" -version = "1.0.94" +name = "cssparser-macros" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", "quote", - "scratch", - "syn 2.0.13", + "syn 2.0.90", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" +name = "ctor" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ - "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.90", ] [[package]] name = "dashmap" -version = "5.4.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -452,9 +425,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "data-url" @@ -465,6 +438,12 @@ dependencies = [ "matches", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" @@ -479,60 +458,52 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dtoa" -version = "0.4.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.8.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] -name = "errno" -version = "0.3.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.45.0", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "errno" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ - "cc", "libc", + "windows-sys", ] [[package]] name = "fastrand" -version = "1.9.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "float-cmp" @@ -543,12 +514,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "fs_extra" version = "1.3.0" @@ -556,45 +521,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "fxhash" -version = "0.2.1" +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "byteorder", + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.9" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "globset" -version = "0.4.10" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata", + "regex-syntax", ] [[package]] name = "globwalk" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "ignore", "walkdir", ] @@ -605,9 +579,21 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.4.1" @@ -623,58 +609,41 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "ignore" -version = "0.4.20" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ + "crossbeam-deque", "globset", - "lazy_static", "log", "memchr", - "regex", + "regex-automata", "same-file", - "thread_local", "walkdir", "winapi-util", ] @@ -686,49 +655,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] -name = "indoc" -version = "1.0.9" +name = "indexmap" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] [[package]] -name = "instant" -version = "0.1.12" +name = "indoc" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] -name = "io-lifetimes" -version = "1.0.10" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", + "either", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jemalloc-sys" @@ -753,76 +722,101 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "winapi", + "windows-targets", ] [[package]] name = "lightningcss" -version = "1.0.0-alpha.45" +version = "1.0.0-alpha.70" dependencies = [ - "ahash", + "ahash 0.8.12", "assert_cmd", "assert_fs", "atty", - "bitflags 2.2.1", + "bitflags 2.6.0", "browserslist-rs", "clap", "const-str", "cssparser", + "cssparser-color", "dashmap", "data-encoding", + "getrandom 0.3.3", + "indexmap 2.7.0", "indoc", - "itertools", + "itertools 0.10.5", "jemallocator", "lazy_static", "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", ] [[package]] name = "lightningcss-derive" -version = "1.0.0-alpha.39" +version = "1.0.0-alpha.43" dependencies = [ + "convert_case", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "lightningcss-napi" +version = "0.4.7" +dependencies = [ + "crossbeam-channel", + "cssparser", + "lightningcss", + "napi", + "parcel_sourcemap", + "rayon", + "serde", + "serde-content", + "serde-detach", + "serde_bytes", + "smallvec", +] + [[package]] name = "lightningcss_c_bindings" version = "0.1.0" @@ -837,41 +831,24 @@ dependencies = [ name = "lightningcss_node" version = "0.1.0" dependencies = [ - "crossbeam-channel", - "cssparser", "jemallocator", - "lightningcss", + "lightningcss-napi", "napi", "napi-build", "napi-derive", - "parcel_sourcemap", - "rayon", - "serde", - "serde-detach", - "serde_bytes", - "smallvec", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", ] [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -879,12 +856,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matches" @@ -894,18 +868,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memoffset" -version = "0.8.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" -dependencies = [ - "autocfg", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -915,17 +880,17 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "napi" -version = "2.10.3" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a967e17e9ba4e015a7bf9b92f90aa8dc321c6d913f6a6d2afd5b66a8ab36fc81" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "ctor", + "napi-derive", "napi-sys", "once_cell", "serde", "serde_json", - "thread_local", ] [[package]] @@ -936,46 +901,42 @@ checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b" [[package]] name = "napi-derive" -version = "2.9.3" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e44e34e70aa61be9036ae652e27c20db5bca80e006be0f482419f6601352a" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" dependencies = [ + "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "napi-derive-backend" -version = "1.0.40" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17925fff04b6fa636f8e4b4608cc1a4f1360b64ac8ecbfdb7da1be1dc74f6843" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" dependencies = [ "convert_case", "once_cell", "proc-macro2", "quote", "regex", - "syn 1.0.109", + "semver", + "syn 2.0.90", ] [[package]] name = "napi-sys" -version = "2.2.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529671ebfae679f2ce9630b62dd53c72c56b3eb8b2c852e7e2fa91704ff93d67" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - [[package]] name = "nom" version = "7.1.3" @@ -992,46 +953,26 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - [[package]] name = "once_cell" -version = "1.17.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "outref" @@ -1041,18 +982,19 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "parcel_selectors" -version = "0.26.1" +version = "0.28.2" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.6.0", "cssparser", - "fxhash", "log", "phf", "phf_codegen", "precomputed-hash", + "rustc-hash", "schemars", "serde", "smallvec", + "static-self", ] [[package]] @@ -1069,57 +1011,46 @@ dependencies = [ "vlq", ] -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-targets", ] [[package]] -name = "paste" -version = "1.0.12" +name = "pastey" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "phf" -version = "0.10.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", - "proc-macro-hack", ] [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", @@ -1127,9 +1058,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", @@ -1137,33 +1068,26 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", - "proc-macro-hack", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - [[package]] name = "precomputed-hash" version = "0.1.1" @@ -1178,7 +1102,7 @@ checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", "float-cmp", - "itertools", + "itertools 0.10.5", "normalize-line-endings", "predicates-core", "regex", @@ -1186,32 +1110,41 @@ dependencies = [ [[package]] name = "predicates" -version = "3.0.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c575290b64d24745b6c57a12a31465f0a66f3a4799686a6921526a33b0797965" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", - "itertools", "predicates-core", ] [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1236,17 +1169,11 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1273,31 +1200,31 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "radium" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "ppv-lite86", "rand_core", ] @@ -1306,15 +1233,12 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] [[package]] name = "rayon" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1322,110 +1246,114 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.7.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", + "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ "bytecheck", ] [[package]] name = "rkyv" -version = "0.7.41" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21499ed91807f07ae081880aabb2ccc0235e9d88011867d984525e9a4c3cfa3e" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ + "bitvec", "bytecheck", - "hashbrown", + "bytes", + "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", "seahash", + "tinyvec", + "uuid", ] [[package]] name = "rkyv_derive" -version = "0.7.41" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1c672430eb41556291981f45ca900a0239ad007242d1cb4b4167af842db666" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustix" -version = "0.37.8" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -1438,11 +1366,12 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", + "indexmap 2.7.0", "schemars_derive", "serde", "serde_json", @@ -1451,27 +1380,21 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.5" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "seahash" @@ -1479,15 +1402,31 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + [[package]] name = "serde" -version = "1.0.159" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +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" @@ -1495,62 +1434,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c621150da442b6a854bb63c431347bcd4de19219a3e1f06fd744208ded057288" dependencies = [ "serde", - "wyz", + "wyz 0.2.0", ] [[package]] -name = "serde-wasm-bindgen" -version = "0.4.5" +name = "serde_bytes" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ - "js-sys", "serde", - "wasm-bindgen", ] [[package]] -name = "serde_bytes" -version = "0.11.9" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "serde", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.90", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simd-abstraction" version = "0.7.1" @@ -1562,49 +1506,41 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] [[package]] -name = "string_cache" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +name = "static-self" +version = "0.1.2" dependencies = [ - "new_debug_unreachable", - "once_cell", - "parking_lot", - "phf_shared", - "precomputed-hash", - "serde", + "indexmap 2.7.0", + "smallvec", + "static-self-derive", ] [[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +name = "static-self-derive" +version = "0.1.1" dependencies = [ - "phf_generator", - "phf_shared", "proc-macro2", "quote", + "syn 1.0.109", ] [[package]] @@ -1626,89 +1562,89 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.13" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" -version = "3.5.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "once_cell", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "textwrap" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.90", ] [[package]] -name = "thread_local" -version = "1.1.7" +name = "tinyvec" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ - "cfg-if", - "once_cell", + "tinyvec_macros", ] [[package]] -name = "time" -version = "0.1.45" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" @@ -1721,27 +1657,27 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-width" -version = "0.1.10" +name = "uuid" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vlq" @@ -1760,9 +1696,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -1770,46 +1706,49 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1817,22 +1756,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "winapi" @@ -1852,11 +1791,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys", ] [[package]] @@ -1866,148 +1805,133 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnu" -version = "0.48.0" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_msvc" -version = "0.48.0" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "wyz" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" +name = "wyz" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "yansi" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +name = "zerocopy" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] [[package]] -name = "wyz" -version = "0.2.0" +name = "zerocopy-derive" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] diff --git a/Cargo.toml b/Cargo.toml index ae4b05f84..c113a3921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,22 @@ [workspace] members = [ "node", + "napi", "selectors", "c", - "derive" + "derive", + "static-self", + "static-self-derive", ] [package] authors = ["Devon Govett "] name = "lightningcss" -version = "1.0.0-alpha.45" +version = "1.0.0-alpha.70" description = "A CSS parser, transformer, and minifier" license = "MPL-2.0" edition = "2021" -keywords = [ "CSS", "minifier", "Parcel" ] +keywords = ["CSS", "minifier", "Parcel"] repository = "https://github.com/parcel-bundler/lightningcss" [package.metadata.docs.rs] @@ -31,23 +34,37 @@ path = "src/lib.rs" crate-type = ["rlib"] [features] -default = ["bundler", "grid", "nodejs", "sourcemap"] +default = ["bundler", "nodejs", "sourcemap"] browserslist = ["browserslist-rs"] bundler = ["dashmap", "sourcemap", "rayon"] cli = ["atty", "clap", "serde_json", "browserslist", "jemallocator"] -grid = [] jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema"] -nodejs = ["dep:serde"] -serde = ["dep:serde", "smallvec/serde", "cssparser/serde", "parcel_selectors/serde", "into_owned"] +nodejs = ["dep:serde", "dep:serde-content"] +serde = [ + "dep:serde", + "dep:serde-content", + "bitflags/serde", + "smallvec/serde", + "cssparser/serde", + "parcel_selectors/serde", + "into_owned", +] sourcemap = ["parcel_sourcemap"] -visitor = ["lightningcss-derive"] -into_owned = ["lightningcss-derive"] +visitor = [] +into_owned = [ + "static-self", + "static-self/smallvec", + "static-self/indexmap", + "parcel_selectors/into_owned", +] substitute_variables = ["visitor", "into_owned"] [dependencies] -serde = { version = "1.0.123", features = ["derive"], optional = true } -cssparser = "0.29.1" -parcel_selectors = { version = "0.26.1", path = "./selectors" } +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.2", path = "./selectors" } itertools = "0.10.1" smallvec = { version = "1.7.0", features = ["union"] } bitflags = "2.2.1" @@ -56,20 +73,27 @@ data-encoding = "2.3.2" lazy_static = "1.4.0" const-str = "0.3.1" pathdiff = "0.2.1" -ahash = "0.7.6" -paste = "1.0.12" +ahash = "0.8.7" +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.12.3", 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 } -lightningcss-derive = { version = "=1.0.0-alpha.39", path = "./derive", optional = true } -schemars = { version = "0.8.11", features = ["smallvec"], optional = true } +lightningcss-derive = { version = "=1.0.0-alpha.43", path = "./derive" } +schemars = { version = "0.8.19", features = ["smallvec", "indexmap2"], optional = true } +static-self = { version = "0.1.2", path = "static-self", optional = true } [target.'cfg(target_os = "macos")'.dependencies] -jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"], optional = true } +jemallocator = { version = "0.3.2", features = [ + "disable_initial_exec_tls", +], optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.3", default-features = false } [dev-dependencies] indoc = "1.0.3" @@ -77,6 +101,7 @@ assert_cmd = "2.0" assert_fs = "1.0" predicates = "2.1" serde_json = "1" +pretty_assertions = "1.4.0" [[test]] name = "cli_integration_tests" diff --git a/README.md b/README.md index 8cdd2dcdf..f44d7b3cd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An extremely fast CSS parser, transformer, and minifier written in Rust. Use it - **Extremely fast** – Parsing and minifying large files is completed in milliseconds, often with significantly smaller output than other tools. See [benchmarks](#benchmarks) below. - **Typed property values** – many other CSS parsers treat property values as an untyped series of tokens. This means that each transformer that wants to do something with these values must interpret them itself, leading to duplicate work and inconsistencies. Lightning CSS parses all values using the grammar from the CSS specification, and exposes a specific value type for each property. -- **Browser-grade parser** – Lightning CSS is built on the [cssparser](https://github.com/servo/rust-cssparser) and [selectors](https://github.com/servo/servo/tree/master/components/selectors) crates created by Mozilla and used by Firefox and Servo. These provide a solid general purpose CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties. +- **Browser-grade parser** – Lightning CSS is built on the [cssparser](https://github.com/servo/rust-cssparser) and [selectors](https://github.com/servo/stylo/tree/main/selectors) crates created by Mozilla and used by Firefox and Servo. These provide a solid general purpose CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties. - **Minification** – One of the main purposes of Lightning CSS is to minify CSS to make it smaller. This includes many optimizations including: - Combining longhand properties into shorthands where possible. - Merging adjacent rules with the same selectors or declarations when it is safe to do so. @@ -25,7 +25,7 @@ An extremely fast CSS parser, transformer, and minifier written in Rust. Use it - **Vendor prefixing** – Lightning CSS accepts a list of browser targets, and automatically adds (and removes) vendor prefixes. - **Browserslist configuration** – Lightning CSS supports opt-in browserslist configuration discovery to resolve browser targets and integrate with your existing tools and config setup. - **Syntax lowering** – Lightning CSS parses modern CSS syntax, and generates more compatible output where needed, based on browser targets. - - CSS Nesting (draft spec) + - CSS Nesting - Custom media queries (draft spec) - Logical properties * [Color Level 5](https://drafts.csswg.org/css-color-5/) diff --git a/c/Cargo.toml b/c/Cargo.toml index 2b56703a8..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.12.3" } +browserslist-rs = { version = "0.19.0" } [build-dependencies] cbindgen = "0.24.3" diff --git a/c/build.rs b/c/build.rs index 6dac4f91c..02fcb75b1 100644 --- a/c/build.rs +++ b/c/build.rs @@ -1,5 +1,3 @@ -extern crate cbindgen; - use std::env; fn main() { diff --git a/c/src/lib.rs b/c/src/lib.rs index e498fb99c..759a18dbe 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + use std::collections::HashSet; use std::ffi::{CStr, CString}; use std::mem::ManuallyDrop; @@ -256,7 +258,6 @@ pub extern "C" fn lightningcss_stylesheet_parse( let code = unsafe { std::str::from_utf8_unchecked(slice) }; let warnings = Arc::new(RwLock::new(Vec::new())); let mut flags = ParserFlags::empty(); - flags.set(ParserFlags::NESTING, options.nesting); flags.set(ParserFlags::CUSTOM_MEDIA, options.custom_media); let opts = ParserOptions { filename: if options.filename.is_null() { @@ -280,6 +281,7 @@ pub extern "C" fn lightningcss_stylesheet_parse( Some(lightningcss::css_modules::Config { pattern, dashed_idents: options.css_modules_dashed_idents, + ..Default::default() }) } else { None diff --git a/cli/postinstall.js b/cli/postinstall.js index abf9dc191..19dadc796 100644 --- a/cli/postinstall.js +++ b/cli/postinstall.js @@ -3,7 +3,8 @@ let path = require('path'); let parts = [process.platform, process.arch]; if (process.platform === 'linux') { - const {MUSL, family} = require('detect-libc'); + const {MUSL, familySync} = require('detect-libc'); + const family = familySync(); if (family === MUSL) { parts.push('musl'); } else if (process.arch === 'arm') { diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 5b99114f9..ce55e5cca 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Devon Govett "] name = "lightningcss-derive" description = "Derive macros for lightningcss" -version = "1.0.0-alpha.39" +version = "1.0.0-alpha.43" license = "MPL-2.0" edition = "2021" repository = "https://github.com/parcel-bundler/lightningcss" @@ -14,3 +14,4 @@ proc-macro = true syn = { version = "1.0", features = ["extra-traits"] } quote = "1.0" proc-macro2 = "1.0" +convert_case = "0.6.0" diff --git a/derive/src/into_owned.rs b/derive/src/into_owned.rs deleted file mode 100644 index 4da1bc10f..000000000 --- a/derive/src/into_owned.rs +++ /dev/null @@ -1,184 +0,0 @@ -use proc_macro::{self, TokenStream}; -use proc_macro2::Span; -use quote::quote; -use syn::{ - parse_macro_input, Data, DataEnum, DeriveInput, Field, Fields, GenericArgument, Ident, Member, PathArguments, - Type, -}; - -pub(crate) fn derive_into_owned(input: TokenStream) -> TokenStream { - let DeriveInput { - ident: self_name, - data, - generics, - .. - } = parse_macro_input!(input); - - let res = match data { - Data::Struct(s) => { - let fields = s - .fields - .iter() - .enumerate() - .map(|(index, Field { ty, ident, .. })| { - let name = ident - .as_ref() - .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); - - let value = into_owned(ty, quote! { self.#name }); - if let Some(ident) = ident { - quote! { #ident: #value } - } else { - value - } - }) - .collect::>(); - - match s.fields { - Fields::Unnamed(_) => { - quote! { - #self_name(#(#fields),*) - } - } - Fields::Named(_) => { - quote! { - #self_name { #(#fields),* } - } - } - Fields::Unit => quote! {}, - } - } - Data::Enum(DataEnum { variants, .. }) => { - let variants = variants - .iter() - .map(|variant| { - if !variant.fields.iter().any(|f| has_lifetime(&f.ty)) { - return quote! {}; - } - - let name = &variant.ident; - let mut field_names = Vec::new(); - let mut static_fields = Vec::new(); - for (index, Field { ty, ident, .. }) in variant.fields.iter().enumerate() { - let name = ident.as_ref().map_or_else( - || Ident::new(&format!("_{}", index), Span::call_site()), - |ident| ident.clone(), - ); - field_names.push(name.clone()); - let value = into_owned(ty, quote! { #name }); - static_fields.push(if let Some(ident) = ident { - quote! { #ident: #value } - } else { - value - }) - } - - match variant.fields { - Fields::Unnamed(_) => { - quote! { - #self_name::#name(#(#field_names),*) => { - #self_name::#name(#(#static_fields),*) - } - } - } - Fields::Named(_) => { - quote! { - #self_name::#name { #(#field_names),* } => { - #self_name::#name { #(#static_fields),* } - } - } - } - Fields::Unit => quote! {}, - } - }) - .collect::(); - - quote! { - match self { - #variants - _ => unsafe { std::mem::transmute_copy(&self) } - } - } - } - _ => { - panic!("can only derive IntoOwned for enums and structs") - } - }; - - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let into_owned = if generics.lifetimes().next().is_none() { - panic!("can't derive IntoOwned on a type without any lifetimes") - } else { - let params = generics.type_params(); - quote! { - impl #impl_generics #self_name #ty_generics #where_clause { - /// Consumes the value and returns an owned clone. - pub fn into_owned<'x>(self) -> #self_name<'x, #(#params),*> { - #res - } - } - } - }; - - into_owned.into() -} - -fn into_owned(ty: &Type, name: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - if has_lifetime(ty) { - match ty { - Type::Path(path) => { - let last = path.path.segments.last().unwrap(); - if last.ident == "Option" { - let v = quote! { v }; - let into_owned = match &last.arguments { - PathArguments::AngleBracketed(args) => match args.args.first().unwrap() { - GenericArgument::Type(ty) => into_owned(ty, v.clone()), - _ => quote! { #v.into_owned() }, - }, - _ => quote! { #v.into_owned() }, - }; - quote! { #name.map(|#v| #into_owned) } - } else if last.ident == "Vec" - || last.ident == "SmallVec" - || last.ident == "CustomIdentList" - || last.ident == "AnimationList" - || last.ident == "AnimationNameList" - { - let v = quote! { v }; - let into_owned = match &last.arguments { - PathArguments::AngleBracketed(args) => match args.args.first().unwrap() { - GenericArgument::Type(ty) => into_owned(ty, v.clone()), - _ => quote! { #v.into_owned() }, - }, - _ => quote! { #v.into_owned() }, - }; - quote! { #name.into_iter().map(|#v| #into_owned).collect() } - } else if last.ident == "Box" { - quote! { Box::new(#name.into_owned()) } - } else { - quote! { #name.into_owned() } - } - } - Type::Reference(_) => panic!("can't derive IntoOwned on a type with references"), - _ => quote! { #name.into_owned() }, - } - } else { - quote! { #name } - } -} - -fn has_lifetime(ty: &Type) -> bool { - match ty { - Type::Path(path) => path.path.segments.iter().any(|s| match &s.arguments { - PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| match arg { - GenericArgument::Lifetime(..) => true, - GenericArgument::Type(ty) => has_lifetime(ty), - _ => false, - }), - _ => false, - }), - Type::Array(arr) => has_lifetime(&*arr.elem), - _ => false, // TODO - } -} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 835cd9c45..122414911 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,300 +1,20 @@ -use std::collections::HashSet; +use proc_macro::TokenStream; -use proc_macro::{self, TokenStream}; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; -use syn::{ - parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, - GenericParam, Generics, Ident, Member, Token, Type, Visibility, -}; - -mod into_owned; - -#[proc_macro_derive(IntoOwned)] -pub fn derive_into_owned(input: TokenStream) -> TokenStream { - into_owned::derive_into_owned(input) -} +mod parse; +mod to_css; +mod visit; #[proc_macro_derive(Visit, attributes(visit, skip_visit, skip_type, visit_types))] pub fn derive_visit_children(input: TokenStream) -> TokenStream { - let DeriveInput { - ident, - data, - generics, - attrs, - .. - } = parse_macro_input!(input); - - let options: Vec = attrs - .iter() - .filter_map(|attr| { - if attr.path.is_ident("visit") { - let opts: VisitOptions = attr.parse_args().unwrap(); - Some(opts) - } else { - None - } - }) - .collect(); - - let visit_types = if let Some(attr) = attrs.iter().find(|attr| attr.path.is_ident("visit_types")) { - let types: VisitTypes = attr.parse_args().unwrap(); - let types = types.types; - Some(quote! { crate::visit_types!(#(#types)|*) }) - } else { - None - }; - - if options.is_empty() { - derive(&ident, &data, &generics, None, visit_types) - } else { - options - .into_iter() - .map(|options| derive(&ident, &data, &generics, Some(options), visit_types.clone())) - .collect() - } -} - -fn derive( - ident: &Ident, - data: &Data, - generics: &Generics, - options: Option, - visit_types: Option, -) -> TokenStream { - let mut impl_generics = generics.clone(); - let mut type_defs = quote! {}; - let generics = if let Some(VisitOptions { - generic: Some(generic), .. - }) = &options - { - let mappings = generics - .type_params() - .zip(generic.type_params()) - .map(|(a, b)| quote! { type #a = #b; }); - type_defs = quote! { #(#mappings)* }; - impl_generics.params.clear(); - generic - } else { - &generics - }; - - if impl_generics.lifetimes().next().is_none() { - impl_generics.params.insert(0, parse_quote! { 'i }) - } - - let lifetime = impl_generics.lifetimes().next().unwrap().clone(); - let t = impl_generics.type_params().find(|g| &g.ident.to_string() == "R"); - let v = quote! { __V }; - let t = if let Some(t) = t { - GenericParam::Type(t.ident.clone().into()) - } else { - let t: GenericParam = parse_quote! { __T }; - impl_generics - .params - .push(parse_quote! { #t: crate::visitor::Visit<#lifetime, __T, #v> }); - t - }; - - impl_generics - .params - .push(parse_quote! { #v: crate::visitor::Visitor<#lifetime, #t> }); - - for ty in generics.type_params() { - let name = &ty.ident; - impl_generics.make_where_clause().predicates.push(parse_quote! { - #name: Visit<#lifetime, #t, #v> - }) - } - - let mut seen_types = HashSet::new(); - let mut child_types = Vec::new(); - let mut visit = Vec::new(); - match data { - Data::Struct(s) => { - for ( - index, - Field { - vis, ty, ident, attrs, .. - }, - ) in s.fields.iter().enumerate() - { - if attrs.iter().any(|attr| attr.path.is_ident("skip_visit")) { - continue; - } - - if matches!(ty, Type::Reference(_)) || !matches!(vis, Visibility::Public(..)) { - continue; - } - - if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) { - seen_types.insert(ty.clone()); - child_types.push(quote! { - <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() - }); - } - - let name = ident - .as_ref() - .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); - visit.push(quote! { self.#name.visit(visitor)?; }) - } - } - Data::Enum(DataEnum { variants, .. }) => { - let variants = variants - .iter() - .map(|variant| { - let name = &variant.ident; - let mut field_names = Vec::new(); - let mut visit_fields = Vec::new(); - for (index, Field { ty, ident, attrs, .. }) in variant.fields.iter().enumerate() { - let name = ident.as_ref().map_or_else( - || Ident::new(&format!("_{}", index), Span::call_site()), - |ident| ident.clone(), - ); - field_names.push(name.clone()); - - if matches!(ty, Type::Reference(_)) { - continue; - } - - if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) && !skip_type(&variant.attrs) - { - seen_types.insert(ty.clone()); - child_types.push(quote! { - <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() - }); - } - - visit_fields.push(quote! { #name.visit(visitor)?; }) - } - - match variant.fields { - Fields::Unnamed(_) => { - quote! { - Self::#name(#(#field_names),*) => { - #(#visit_fields)* - } - } - } - Fields::Named(_) => { - quote! { - Self::#name { #(#field_names),* } => { - #(#visit_fields)* - } - } - } - Fields::Unit => quote! {}, - } - }) - .collect::(); - - visit.push(quote! { - match self { - #variants - _ => {} - } - }) - } - _ => {} - } - - if visit_types.is_none() && child_types.is_empty() { - child_types.push(quote! { crate::visitor::VisitTypes::empty().bits() }); - } - - let (_, ty_generics, _) = generics.split_for_impl(); - let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); - - let self_visit = if let Some(VisitOptions { - visit: Some(visit), - kind: Some(kind), - .. - }) = &options - { - child_types.push(quote! { crate::visitor::VisitTypes::#kind.bits() }); - - quote! { - fn visit(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { - if visitor.visit_types().contains(crate::visitor::VisitTypes::#kind) { - visitor.#visit(self) - } else { - self.visit_children(visitor) - } - } - } - } else { - quote! {} - }; - - let child_types = visit_types.unwrap_or_else(|| { - quote! { - { - #type_defs - crate::visitor::VisitTypes::from_bits_retain(#(#child_types)|*) - } - } - }); - - let output = quote! { - impl #impl_generics Visit<#lifetime, #t, #v> for #ident #ty_generics #where_clause { - const CHILD_TYPES: crate::visitor::VisitTypes = #child_types; - - #self_visit - - fn visit_children(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { - if !<#lifetime, #t, #v>>::CHILD_TYPES.intersects(visitor.visit_types()) { - return Ok(()) - } - - #(#visit)* - - Ok(()) - } - } - }; - - output.into() -} - -fn skip_type(attrs: &Vec) -> bool { - attrs.iter().any(|attr| attr.path.is_ident("skip_type")) -} - -struct VisitOptions { - visit: Option, - kind: Option, - generic: Option, -} - -impl Parse for VisitOptions { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let (visit, kind, comma) = if input.peek(Ident) { - let visit: Ident = input.parse()?; - let _: Token![,] = input.parse()?; - let kind: Ident = input.parse()?; - let comma: Result = input.parse(); - (Some(visit), Some(kind), comma.is_ok()) - } else { - (None, None, true) - }; - let generic: Option = if comma { Some(input.parse()?) } else { None }; - Ok(Self { visit, kind, generic }) - } + visit::derive_visit_children(input) } -struct VisitTypes { - types: Vec, +#[proc_macro_derive(Parse, attributes(css))] +pub fn derive_parse(input: TokenStream) -> TokenStream { + parse::derive_parse(input) } -impl Parse for VisitTypes { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let first: Ident = input.parse()?; - let mut types = vec![first]; - while input.parse::().is_ok() { - let id: Ident = input.parse()?; - types.push(id); - } - Ok(Self { types }) - } +#[proc_macro_derive(ToCss, attributes(css))] +pub fn derive_to_css(input: TokenStream) -> TokenStream { + to_css::derive_to_css(input) } diff --git a/derive/src/parse.rs b/derive/src/parse.rs new file mode 100644 index 000000000..995b344e3 --- /dev/null +++ b/derive/src/parse.rs @@ -0,0 +1,213 @@ +use convert_case::Casing; +use proc_macro::{self, TokenStream}; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{ + parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Fields, Ident, Token, +}; + +pub fn derive_parse(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + mut generics, + attrs, + .. + } = parse_macro_input!(input); + let opts = CssOptions::parse_attributes(&attrs).unwrap(); + let cloned_generics = generics.clone(); + let (_, ty_generics, _) = cloned_generics.split_for_impl(); + + if generics.lifetimes().next().is_none() { + generics.params.insert(0, parse_quote! { 'i }) + } + + let lifetime = generics.lifetimes().next().unwrap().clone(); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let imp = match &data { + Data::Enum(data) => derive_enum(&data, &ident, &opts), + _ => todo!(), + }; + + let output = quote! { + impl #impl_generics Parse<#lifetime> for #ident #ty_generics #where_clause { + fn parse<'t>(input: &mut Parser<#lifetime, 't>) -> Result<#lifetime, ParserError<#lifetime>>> { + #imp + } + } + }; + + output.into() +} + +fn derive_enum(data: &DataEnum, ident: &Ident, opts: &CssOptions) -> TokenStream2 { + let mut idents = Vec::new(); + let mut non_idents = Vec::new(); + for (index, variant) in data.variants.iter().enumerate() { + let name = &variant.ident; + let fields = variant + .fields + .iter() + .enumerate() + .map(|(index, field)| { + field.ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ) + }) + .collect::<_>>(); + + let mut expr = match &variant.fields { + Fields::Unit => { + idents.push(( + Literal::string(&variant.ident.to_string().to_case(opts.case)), + name.clone(), + )); + continue; + } + Fields::Named(_) => { + quote! { + return Ok(#ident::#name { #(#fields),* }) + } + } + Fields::Unnamed(_) => { + quote! { + return Ok(#ident::#name(#(#fields),*)) + } + } + }; + + // Group multiple ident branches together. + if !idents.is_empty() { + if idents.len() == 1 { + let (s, name) = idents.remove(0); + non_idents.push(quote! { + if input.try_parse(|input| input.expect_ident_matching(#s)).is_ok() { + return Ok(#ident::#name) + } + }); + } else { + let matches = idents + .iter() + .map(|(s, name)| { + quote! { + #s => return Ok(#ident::#name), + } + }) + .collect::<_>>(); + non_idents.push(quote! { + { + let state = input.state(); + if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) { + cssparser::match_ignore_ascii_case! { &*ident, + #(#matches)* + _ => {} + } + input.reset(&state); + } + } + }); + idents.clear(); + } + } + + let is_last = index == data.variants.len() - 1; + + for (index, field) in variant.fields.iter().enumerate().rev() { + let ty = &field.ty; + let field_name = field.ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ); + if is_last { + expr = quote! { + let #field_name = <#ty>::parse(input)?; + #expr + }; + } else { + expr = quote! { + if let Ok(#field_name) = input.try_parse(<#ty>::parse) { + #expr + } + }; + } + } + + non_idents.push(expr); + } + + let idents = if idents.is_empty() { + quote! {} + } else if idents.len() == 1 { + let (s, name) = idents.remove(0); + quote! { + input.expect_ident_matching(#s)?; + Ok(#ident::#name) + } + } else { + let idents = idents + .into_iter() + .map(|(s, name)| { + quote! { + #s => Ok(#ident::#name), + } + }) + .collect::<_>>(); + quote! { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + cssparser::match_ignore_ascii_case! { &*ident, + #(#idents)* + _ => Err(location.new_unexpected_token_error( + cssparser::Token::Ident(ident.clone()) + )) + } + } + }; + + let output = quote! { + #(#non_idents)* + #idents + }; + + output.into() +} + +pub struct CssOptions { + pub case: convert_case::Case, +} + +impl CssOptions { + pub fn parse_attributes(attrs: &Vec) -> syn::Result { + for attr in attrs { + if attr.path.is_ident("css") { + let opts: CssOptions = attr.parse_args()?; + return Ok(opts); + } + } + + Ok(CssOptions { + case: convert_case::Case::Kebab, + }) + } +} + +impl Parse for CssOptions { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut case = convert_case::Case::Kebab; + while !input.is_empty() { + let k: Ident = input.parse()?; + let _: Token![=] = input.parse()?; + let v: Ident = input.parse()?; + + if k == "case" { + if v == "lower" { + case = convert_case::Case::Flat; + } + } + } + + Ok(Self { case }) + } +} diff --git a/derive/src/to_css.rs b/derive/src/to_css.rs new file mode 100644 index 000000000..739a16d40 --- /dev/null +++ b/derive/src/to_css.rs @@ -0,0 +1,156 @@ +use convert_case::Casing; +use proc_macro::{self, TokenStream}; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Type}; + +use crate::parse::CssOptions; + +pub fn derive_to_css(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + attrs, + .. + } = parse_macro_input!(input); + + let opts = CssOptions::parse_attributes(&attrs).unwrap(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let imp = match &data { + Data::Enum(data) => derive_enum(&data, &opts), + _ => todo!(), + }; + + let output = quote! { + impl #impl_generics ToCss for #ident #ty_generics #where_clause { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #imp + } + } + }; + + output.into() +} + +fn derive_enum(data: &DataEnum, opts: &CssOptions) -> TokenStream2 { + let variants = data + .variants + .iter() + .map(|variant| { + let name = &variant.ident; + let fields = variant + .fields + .iter() + .enumerate() + .map(|(index, field)| { + field.ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ) + }) + .collect::<_>>(); + + #[derive(PartialEq)] + enum NeedsSpace { + Yes, + No, + Maybe, + } + + let mut needs_space = NeedsSpace::No; + let mut fields_iter = variant.fields.iter().zip(fields.iter()).peekable(); + let mut writes = Vec::new(); + let mut has_needs_space = false; + while let Some((field, name)) = fields_iter.next() { + writes.push(if fields.len() > 1 { + let space = match needs_space { + NeedsSpace::Yes => quote! { dest.write_char(' ')?; }, + NeedsSpace::No => quote! {}, + NeedsSpace::Maybe => { + has_needs_space = true; + quote! { + if needs_space { + dest.write_char(' ')?; + } + } + } + }; + + if is_option(&field.ty) { + needs_space = NeedsSpace::Maybe; + let after_space = if matches!(fields_iter.peek(), Some((field, _)) if !is_option(&field.ty)) { + // If the next field is non-optional, just insert the space here. + needs_space = NeedsSpace::No; + quote! { dest.write_char(' ')?; } + } else { + quote! {} + }; + quote! { + if let Some(v) = #name { + #space + v.to_css(dest)?; + #after_space + } + } + } else { + needs_space = NeedsSpace::Yes; + quote! { + #space + #name.to_css(dest)?; + } + } + } else { + quote! { #name.to_css(dest) } + }); + } + + if writes.len() > 1 { + writes.push(quote! { Ok(()) }); + } + + if has_needs_space { + writes.insert(0, quote! { let mut needs_space = false }); + } + + match variant.fields { + Fields::Unit => { + let s = Literal::string(&variant.ident.to_string().to_case(opts.case)); + quote! { + Self::#name => dest.write_str(#s) + } + } + Fields::Named(_) => { + quote! { + Self::#name { #(#fields),* } => { + #(#writes)* + } + } + } + Fields::Unnamed(_) => { + quote! { + Self::#name(#(#fields),*) => { + #(#writes)* + } + } + } + } + }) + .collect::<_>>(); + + let output = quote! { + match self { + #(#variants),* + } + }; + + output.into() +} + +fn is_option(ty: &Type) -> bool { + matches!(&ty, Type::Path(p) if p.qself.is_none() && p.path.segments.iter().next().unwrap().ident == "Option") +} diff --git a/derive/src/visit.rs b/derive/src/visit.rs new file mode 100644 index 000000000..020933644 --- /dev/null +++ b/derive/src/visit.rs @@ -0,0 +1,292 @@ +use std::collections::HashSet; + +use proc_macro::{self, TokenStream}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{ + parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, + GenericParam, Generics, Ident, Member, Token, Type, Visibility, +}; + +pub fn derive_visit_children(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + attrs, + .. + } = parse_macro_input!(input); + + let options: Vec = attrs + .iter() + .filter_map(|attr| { + if attr.path.is_ident("visit") { + let opts: VisitOptions = attr.parse_args().unwrap(); + Some(opts) + } else { + None + } + }) + .collect(); + + let visit_types = if let Some(attr) = attrs.iter().find(|attr| attr.path.is_ident("visit_types")) { + let types: VisitTypes = attr.parse_args().unwrap(); + let types = types.types; + Some(quote! { crate::visit_types!(#(#types)|*) }) + } else { + None + }; + + if options.is_empty() { + derive(&ident, &data, &generics, None, visit_types) + } else { + options + .into_iter() + .map(|options| derive(&ident, &data, &generics, Some(options), visit_types.clone())) + .collect() + } +} + +fn derive( + ident: &Ident, + data: &Data, + generics: &Generics, + options: Option, + visit_types: Option, +) -> TokenStream { + let mut impl_generics = generics.clone(); + let mut type_defs = quote! {}; + let generics = if let Some(VisitOptions { + generic: Some(generic), .. + }) = &options + { + let mappings = generics + .type_params() + .zip(generic.type_params()) + .map(|(a, b)| quote! { type #a = #b; }); + type_defs = quote! { #(#mappings)* }; + impl_generics.params.clear(); + generic + } else { + &generics + }; + + if impl_generics.lifetimes().next().is_none() { + impl_generics.params.insert(0, parse_quote! { 'i }) + } + + let lifetime = impl_generics.lifetimes().next().unwrap().clone(); + let t = impl_generics.type_params().find(|g| &g.ident.to_string() == "R"); + let v = quote! { __V }; + let t = if let Some(t) = t { + GenericParam::Type(t.ident.clone().into()) + } else { + let t: GenericParam = parse_quote! { __T }; + impl_generics + .params + .push(parse_quote! { #t: crate::visitor::Visit<#lifetime, __T, #v> }); + t + }; + + impl_generics + .params + .push(parse_quote! { #v: ?Sized + crate::visitor::Visitor<#lifetime, #t> }); + + for ty in generics.type_params() { + let name = &ty.ident; + impl_generics.make_where_clause().predicates.push(parse_quote! { + #name: Visit<#lifetime, #t, #v> + }) + } + + let mut seen_types = HashSet::new(); + let mut child_types = Vec::new(); + let mut visit = Vec::new(); + match data { + Data::Struct(s) => { + for ( + index, + Field { + vis, ty, ident, attrs, .. + }, + ) in s.fields.iter().enumerate() + { + if attrs.iter().any(|attr| attr.path.is_ident("skip_visit")) { + continue; + } + + if matches!(ty, Type::Reference(_)) || !matches!(vis, Visibility::Public(..)) { + continue; + } + + if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) { + seen_types.insert(ty.clone()); + child_types.push(quote! { + <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() + }); + } + + let name = ident + .as_ref() + .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); + visit.push(quote! { self.#name.visit(visitor)?; }) + } + } + Data::Enum(DataEnum { variants, .. }) => { + let variants = variants + .iter() + .map(|variant| { + let name = &variant.ident; + let mut field_names = Vec::new(); + let mut visit_fields = Vec::new(); + for (index, Field { ty, ident, attrs, .. }) in variant.fields.iter().enumerate() { + let name = ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ); + field_names.push(name.clone()); + + if matches!(ty, Type::Reference(_)) { + continue; + } + + if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) && !skip_type(&variant.attrs) + { + seen_types.insert(ty.clone()); + child_types.push(quote! { + <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() + }); + } + + visit_fields.push(quote! { #name.visit(visitor)?; }) + } + + match variant.fields { + Fields::Unnamed(_) => { + quote! { + Self::#name(#(#field_names),*) => { + #(#visit_fields)* + } + } + } + Fields::Named(_) => { + quote! { + Self::#name { #(#field_names),* } => { + #(#visit_fields)* + } + } + } + Fields::Unit => quote! {}, + } + }) + .collect::(); + + visit.push(quote! { + match self { + #variants + _ => {} + } + }) + } + _ => {} + } + + if visit_types.is_none() && child_types.is_empty() { + child_types.push(quote! { crate::visitor::VisitTypes::empty().bits() }); + } + + let (_, ty_generics, _) = generics.split_for_impl(); + let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); + + let self_visit = if let Some(VisitOptions { + visit: Some(visit), + kind: Some(kind), + .. + }) = &options + { + child_types.push(quote! { crate::visitor::VisitTypes::#kind.bits() }); + + quote! { + fn visit(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { + if visitor.visit_types().contains(crate::visitor::VisitTypes::#kind) { + visitor.#visit(self) + } else { + self.visit_children(visitor) + } + } + } + } else { + quote! {} + }; + + let child_types = visit_types.unwrap_or_else(|| { + quote! { + { + #type_defs + crate::visitor::VisitTypes::from_bits_retain(#(#child_types)|*) + } + } + }); + + let output = quote! { + impl #impl_generics Visit<#lifetime, #t, #v> for #ident #ty_generics #where_clause { + const CHILD_TYPES: crate::visitor::VisitTypes = #child_types; + + #self_visit + + fn visit_children(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { + if !<#lifetime, #t, #v>>::CHILD_TYPES.intersects(visitor.visit_types()) { + return Ok(()) + } + + #(#visit)* + + Ok(()) + } + } + }; + + output.into() +} + +fn skip_type(attrs: &Vec) -> bool { + attrs.iter().any(|attr| attr.path.is_ident("skip_type")) +} + +struct VisitOptions { + visit: Option, + kind: Option, + generic: Option, +} + +impl Parse for VisitOptions { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let (visit, kind, comma) = if input.peek(Ident) { + let visit: Ident = input.parse()?; + let _: Token![,] = input.parse()?; + let kind: Ident = input.parse()?; + let comma: Result = input.parse(); + (Some(visit), Some(kind), comma.is_ok()) + } else { + (None, None, true) + }; + let generic: Option = if comma { Some(input.parse()?) } else { None }; + Ok(Self { visit, kind, generic }) + } +} + +struct VisitTypes { + types: Vec, +} + +impl Parse for VisitTypes { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let first: Ident = input.parse()?; + let mut types = vec![first]; + while input.parse::().is_ok() { + let id: Ident = input.parse()?; + types.push(id); + } + Ok(Self { types }) + } +} diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index 3404a701e..9c091e6b0 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -8,10 +8,13 @@ use lightningcss::{ properties::custom::{Token, TokenOrValue}, rules::{style::StyleRule, CssRule, CssRuleList, Location}, selector::{Component, Selector}, - stylesheet::{ParserFlags, ParserOptions, PrinterOptions, StyleSheet}, + stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, targets::Browsers, traits::{AtRuleParser, ToCss}, - values::{color::CssColor, length::LengthValue}, + values::{ + color::{CssColor, RGBA}, + length::LengthValue, + }, vendor_prefix::VendorPrefix, visit_types, visitor::{Visit, VisitTypes, Visitor}, @@ -22,7 +25,6 @@ fn main() { let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { filename: args[1].clone(), - flags: ParserFlags::NESTING, ..Default::default() }; @@ -154,7 +156,9 @@ struct StyleRuleCollector<'i, 'a> { impl<'i, 'a> Visitor<'i, AtRule> for StyleRuleCollector<'i, 'a> { type Error = Infallible; - const TYPES: VisitTypes = VisitTypes::RULES; + fn visit_types(&self) -> VisitTypes { + VisitTypes::RULES + } fn visit_rule(&mut self, rule: &mut lightningcss::rules::CssRule<'i, AtRule>) -> Result<(), Self::Error> { match rule { @@ -187,7 +191,9 @@ struct ApplyVisitor<'a, 'i> { impl<'a, 'i> Visitor<'i, AtRule> for ApplyVisitor<'a, 'i> { type Error = Infallible; - const TYPES: VisitTypes = visit_types!(RULES | COLORS | LENGTHS | DASHED_IDENTS | SELECTORS | TOKENS); + fn visit_types(&self) -> VisitTypes { + visit_types!(RULES | COLORS | LENGTHS | DASHED_IDENTS | SELECTORS | TOKENS) + } fn visit_rule(&mut self, rule: &mut CssRule<'i, AtRule>) -> Result<(), Self::Error> { // Replace @apply rule with nested style rule. @@ -262,8 +268,8 @@ impl<'a, 'i> Visitor<'i, AtRule> for ApplyVisitor<'a, 'i> { match token { TokenOrValue::Function(f) if f.name == "theme" => match f.arguments.0.first() { Some(TokenOrValue::Token(Token::String(s))) => match s.as_ref() { - "blue-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(0, 0, 255, 255))), - "red-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(255, 0, 0, 255))), + "blue-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(0, 0, 255, 1.0))), + "red-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(255, 0, 0, 1.0))), _ => {} }, _ => {} diff --git a/napi/Cargo.toml b/napi/Cargo.toml new file mode 100644 index 000000000..789062ea6 --- /dev/null +++ b/napi/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors = ["Devon Govett "] +name = "lightningcss-napi" +version = "0.4.7" +description = "Node-API bindings for Lightning CSS" +license = "MPL-2.0" +repository = "https://github.com/parcel-bundler/lightningcss" +edition = "2021" + +[features] +default = [] +visitor = ["lightningcss/visitor"] +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.70", path = "../", features = [ + "nodejs", + "serde", +] } +parcel_sourcemap = { version = "2.1.1", features = ["json"] } +serde-detach = "0.0.1" +smallvec = { version = "1.7.0", features = ["union"] } +napi = { version = "2", default-features = false, features = [ + "napi4", + "napi5", + "serde-json", +] } +crossbeam-channel = { version = "0.5.6", optional = true } +rayon = { version = "1.5.1", optional = true } diff --git a/node/src/at_rule_parser.rs b/napi/src/at_rule_parser.rs similarity index 97% rename from node/src/at_rule_parser.rs rename to napi/src/at_rule_parser.rs index 111bd106c..919eda2dc 100644 --- a/node/src/at_rule_parser.rs +++ b/napi/src/at_rule_parser.rs @@ -11,7 +11,6 @@ use lightningcss::{ string::CowArcStr, syntax::{ParsedComponent, SyntaxString}, }, - visitor::{Visit, VisitTypes, Visitor}, }; use serde::{Deserialize, Deserializer, Serialize}; @@ -198,6 +197,10 @@ impl<'i> ToCss for AtRule<'i> { } } +#[cfg(feature = "visitor")] +use lightningcss::visitor::{Visit, VisitTypes, Visitor}; + +#[cfg(feature = "visitor")] impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { const CHILD_TYPES: VisitTypes = VisitTypes::empty(); diff --git a/napi/src/lib.rs b/napi/src/lib.rs new file mode 100644 index 000000000..f43a8d46e --- /dev/null +++ b/napi/src/lib.rs @@ -0,0 +1,1211 @@ +#[cfg(feature = "bundler")] +use at_rule_parser::AtRule; +use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser}; +use lightningcss::bundler::BundleErrorKind; +#[cfg(feature = "bundler")] +use lightningcss::bundler::{Bundler, SourceProvider}; +use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; +use lightningcss::dependencies::{Dependency, DependencyOptions}; +use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; +use lightningcss::stylesheet::{ + MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, +}; +use lightningcss::targets::{Browsers, Features, Targets}; +use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; +use napi::{CallContext, Env, JsObject, JsUnknown}; +use parcel_sourcemap::SourceMap; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +mod at_rule_parser; +#[cfg(feature = "bundler")] +#[cfg(not(target_arch = "wasm32"))] +mod threadsafe_function; +#[cfg(feature = "visitor")] +mod transformer; +mod utils; + +#[cfg(feature = "visitor")] +use transformer::JsVisitor; + +#[cfg(not(feature = "visitor"))] +struct JsVisitor; + +#[cfg(feature = "visitor")] +use lightningcss::visitor::Visit; + +use utils::get_named_property; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct TransformResult<'i> { + #[serde(with = "serde_bytes")] + code: Vec, + #[serde(with = "serde_bytes")] + map: Option>, + exports: Option, + references: Option, + dependencies: Option>, + warnings: Vec<'i>>, +} + +impl<'i> TransformResult<'i> { + fn into_js(self, env: Env) -> napi::Result { + // Manually construct buffers so we avoid a copy and work around + // https://github.com/napi-rs/napi-rs/issues/1124. + let mut obj = env.create_object()?; + let buf = env.create_buffer_with_data(self.code)?; + obj.set_named_property("code", buf.into_raw())?; + obj.set_named_property( + "map", + if let Some(map) = self.map { + let buf = env.create_buffer_with_data(map)?; + buf.into_raw().into_unknown() + } else { + env.get_null()?.into_unknown() + }, + )?; + obj.set_named_property("exports", env.to_js_value(&self.exports)?)?; + obj.set_named_property("references", env.to_js_value(&self.references)?)?; + obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?; + obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?; + Ok(obj.into_unknown()) + } +} + +#[cfg(feature = "visitor")] +fn get_visitor(env: Env, opts: &JsObject) -> Option { + if let Ok(visitor) = get_named_property::(opts, "visitor") { + Some(JsVisitor::new(env, visitor)) + } else { + None + } +} + +#[cfg(not(feature = "visitor"))] +fn get_visitor(_env: Env, _opts: &JsObject) -> Option { + None +} + +pub fn transform(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let config: Config = ctx.env.from_js_value(opts)?; + let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; + let res = compile(code, &config, &mut visitor); + + match res { + Ok(res) => res.into_js(*ctx.env), + Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), + } +} + +pub fn transform_style_attribute(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let config: AttrConfig = ctx.env.from_js_value(opts)?; + let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; + let res = compile_attr(code, &config, &mut visitor); + + match res { + Ok(res) => res.into_js(ctx), + Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), + } +} + +#[cfg(feature = "bundler")] +#[cfg(not(target_arch = "wasm32"))] +mod bundle { + use super::*; + use crossbeam_channel::{self, Receiver, Sender}; + 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; + use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; + + pub fn bundle(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let config: BundleConfig = ctx.env.from_js_value(opts)?; + let fs = FileProvider::new(); + + // This is pretty silly, but works around a rust limitation that you cannot + // explicitly annotate lifetime bounds on closures. + fn annotate<'i, 'o, F>(f: F) -> F + where + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, + { + f + } + + let res = compile_bundle( + &fs, + &config, + visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), + ); + + match res { + Ok(res) => res.into_js(*ctx.env), + Err(err) => Err(err.into_js_error(*ctx.env, None)?), + } + } + + // A SourceProvider which calls JavaScript functions to resolve and read files. + struct JsSourceProvider { + resolve: Option>, + read: Option>, + inputs: Mutex<*mut String>>, + } + + unsafe impl Sync for JsSourceProvider {} + unsafe impl Send for JsSourceProvider {} + + // 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 { + type Error = napi::Error; + + fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { + let source = if let Some(read) = &self.read { + CHANNEL.with(|channel| { + let message = ReadMessage { + file: file.to_str().unwrap().to_owned(), + tx: channel.0.clone(), + }; + + read.call(message, ThreadsafeFunctionCallMode::Blocking); + channel.1.recv().unwrap() + }) + } else { + Ok(std::fs::read_to_string(file)?) + }; + + match source { + Ok(source) => { + // cache the result + let ptr = Box::into_raw(Box::new(source)); + self.inputs.lock().unwrap().push(ptr); + // SAFETY: this is safe because the pointer is not dropped + // until the JsSourceProvider is, and we never remove from the + // list of pointers stored in the vector. + Ok(unsafe { &*ptr }) + } + Err(e) => Err(e), + } + } + + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + if let Some(resolve) = &self.resolve { + return RESOLVER_CHANNEL.with(|channel| { + let message = ResolveMessage { + specifier: specifier.to_owned(), + originating_file: originating_file.to_str().unwrap().to_owned(), + tx: channel.0.clone(), + }; + + resolve.call(message, ThreadsafeFunctionCallMode::Blocking); + channel.1.recv().unwrap() + }); + } + + Ok(originating_file.with_file_name(specifier).into()) + } + } + + struct ResolveMessage { + specifier: String, + originating_file: String, + tx: Sender>, + } + + struct ReadMessage { + file: String, + tx: Sender>, + } + + struct VisitMessage { + stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, + tx: Sender>, + } + + 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()? { + let result: JsObject = result.try_into()?; + let then: JsFunction = get_named_property(&result, "then")?; + let tx2 = tx.clone(); + let cb = env.create_function_from_closure("callback", move |ctx| { + 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| { + let res = ctx.get::(0)?; + tx2.send(Err(napi::Error::from(res))).unwrap(); + ctx.env.get_undefined() + })?; + then.call(Some(&result), &[cb, eb])?; + } else { + let result = parse(result)?; + tx.send(Ok(result)).unwrap(); + } + + Ok(()) + } + + fn resolve_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { + 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, move |unknown| { + ctx.env.from_js_value(unknown) + }) + } + + fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { + match res { + Ok(_) => Ok(()), + Err(e) => { + tx.send(Err(e)).expect("send error"); + Ok(()) + } + } + } + + fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { + let tx = ctx.value.tx.clone(); + handle_error(tx, resolve_on_js_thread(ctx)) + } + + 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, |unknown| { + JsString::try_from(unknown)?.into_utf8()?.into_owned() + }) + } + + fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { + let tx = ctx.value.tx.clone(); + handle_error(tx, read_on_js_thread(ctx)) + } + + pub fn bundle_async(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let visitor = get_visitor(*ctx.env, &opts); + + let config: BundleConfig = ctx.env.from_js_value(&opts)?; + + if let Ok(resolver) = get_named_property::(&opts, "resolver") { + let read = if resolver.has_named_property("read")? { + let read = get_named_property::(&resolver, "read")?; + Some(ThreadsafeFunction::create( + ctx.env.raw(), + unsafe { read.raw() }, + 0, + read_on_js_thread_wrapper, + )?) + } else { + None + }; + + let resolve = if resolver.has_named_property("resolve")? { + let resolve = get_named_property::(&resolver, "resolve")?; + Some(ThreadsafeFunction::create( + ctx.env.raw(), + unsafe { resolve.raw() }, + 0, + resolve_on_js_thread_wrapper, + )?) + } else { + None + }; + + let provider = JsSourceProvider { + resolve, + read, + inputs: Mutex::new(Vec::new()), + }; + + run_bundle_task(provider, config, visitor, *ctx.env) + } else { + let provider = FileProvider::new(); + run_bundle_task(provider, config, visitor, *ctx.env) + } + } + + // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however, + // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile), + // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must + // run bundling from a thread not managed by Node. + fn run_bundle_task( + provider: P, + config: BundleConfig, + visitor: Option, + env: Env, + ) -> napi::Result + where + P::Error: IntoJsError, + { + let (deferred, promise) = env.create_deferred()?; + + let tsfn = if let Some(mut visitor) = visitor { + Some(ThreadsafeFunction::create( + env.raw(), + std::ptr::null_mut(), + 0, + move |ctx: ThreadSafeCallContext| { + if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) { + ctx.value.tx.send(Err(err)).expect("send error"); + return Ok(()); + } + ctx.value.tx.send(Ok(Default::default())).expect("send error"); + Ok(()) + }, + )?) + } else { + None + }; + + // Run bundling task in rayon threadpool. + rayon::spawn(move || { + let res = compile_bundle( + unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, + &config, + tsfn.map(move |tsfn| { + move |stylesheet: &mut StyleSheet| { + CHANNEL.with(|channel| { + let message = VisitMessage { + // SAFETY: we immediately lock the thread until we get a response, + // so stylesheet cannot be dropped in that time. + stylesheet: unsafe { + std::mem::transmute::< + &'_ mut StyleSheet<'_, '_, AtRule>, + &'static mut StyleSheet<'static, 'static, AtRule>, + >(stylesheet) + }, + tx: channel.0.clone(), + }; + + tsfn.call(message, ThreadsafeFunctionCallMode::Blocking); + channel.1.recv().expect("recv error").map(|_| ()) + }) + } + }), + ); + + deferred.resolve(move |env| match res { + Ok(v) => v.into_js(env), + Err(err) => Err(err.into_js_error(env, None)?), + }); + }); + + Ok(promise) + } +} + +#[cfg(feature = "bundler")] +#[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; + + pub fn bundle(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let resolver = get_named_property::(&opts, "resolver")?; + let read = get_named_property::(&resolver, "read")?; + let resolve = if resolver.has_named_property("resolve")? { + let resolve = get_named_property::(&resolver, "resolve")?; + Some(ctx.env.create_reference(resolve)?) + } else { + None + }; + let config: BundleConfig = ctx.env.from_js_value(opts)?; + + let provider = JsSourceProvider { + env: ctx.env.clone(), + resolve, + read: ctx.env.create_reference(read)?, + inputs: UnsafeCell::new(Vec::new()), + }; + + // This is pretty silly, but works around a rust limitation that you cannot + // explicitly annotate lifetime bounds on closures. + fn annotate<'i, 'o, F>(f: F) -> F + where + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, + { + f + } + + let res = compile_bundle( + &provider, + &config, + visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), + ); + + match res { + Ok(res) => res.into_js(*ctx.env), + Err(err) => Err(err.into_js_error(*ctx.env, None)?), + } + } + + struct JsSourceProvider { + env: Env, + resolve: Option<()>>, + read: Ref<()>, + inputs: UnsafeCell<*mut String>>, + } + + impl Drop for JsSourceProvider { + fn drop(&mut self) { + if let Some(resolve) = &mut self.resolve { + drop(resolve.unref(self.env)); + } + drop(self.read.unref(self.env)); + } + } + + unsafe impl Sync for JsSourceProvider {} + unsafe impl Send for JsSourceProvider {} + + // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code. + // See the comments in async.mjs for more details about how this works. + extern "C" { + fn await_promise_sync( + promise: napi::sys::napi_value, + result: *mut napi::sys::napi_value, + error: *mut napi::sys::napi_value, + ); + } + + 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(); + unsafe { await_promise_sync(value.raw(), &mut result, &mut error) }; + if !error.is_null() { + let error = unsafe { JsUnknown::from_raw(env.raw(), error)? }; + return Err(napi::Error::from(error)); + } + if result.is_null() { + return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string())); + } + + value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; + } + + Ok(value) + } + + impl SourceProvider for JsSourceProvider { + type Error = napi::Error; + + fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { + 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)?; + let source: JsString = source.try_into()?; + let source = source.into_utf8()?.into_owned()?; + + // cache the result + let ptr = Box::into_raw(Box::new(source)); + let inputs = unsafe { &mut *self.inputs.get() }; + inputs.push(ptr); + // SAFETY: this is safe because the pointer is not dropped + // until the JsSourceProvider is, and we never remove from the + // list of pointers stored in the vector. + Ok(unsafe { &*ptr }) + } + + 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)?; + let result = self.env.from_js_value(result)?; + Ok(result) + } else { + Ok(ResolveResult::File(originating_file.with_file_name(specifier))) + } + } + } +} + +#[cfg(feature = "bundler")] +pub use bundle::*; + +// --------------------------------------------- + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Config { + pub filename: Option, + pub project_root: Option, + #[serde(with = "serde_bytes")] + pub code: Vec, + pub targets: Option, + #[serde(default)] + pub include: u32, + #[serde(default)] + pub exclude: u32, + pub minify: Option, + pub source_map: Option, + pub input_source_map: Option, + pub drafts: Option, + pub non_standard: Option, + pub css_modules: Option, + pub analyze_dependencies: Option, + pub pseudo_classes: Option, + pub unused_symbols: Option>, + pub error_recovery: Option, + pub custom_at_rules: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum AnalyzeDependenciesOption { + Bool(bool), + Config(AnalyzeDependenciesConfig), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AnalyzeDependenciesConfig { + preserve_imports: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum CssModulesOption { + Bool(bool), + Config(CssModulesConfig), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CssModulesConfig { + pattern: Option, + dashed_idents: Option, + animation: Option, + container: Option, + grid: Option, + custom_idents: Option, + pure: Option, +} + +#[cfg(feature = "bundler")] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BundleConfig { + pub filename: String, + pub project_root: Option, + pub targets: Option, + #[serde(default)] + pub include: u32, + #[serde(default)] + pub exclude: u32, + pub minify: Option, + pub source_map: Option, + pub drafts: Option, + pub non_standard: Option, + pub css_modules: Option, + pub analyze_dependencies: Option, + pub pseudo_classes: Option, + pub unused_symbols: Option>, + pub error_recovery: Option, + pub custom_at_rules: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct OwnedPseudoClasses { + pub hover: Option, + pub active: Option, + pub focus: Option, + pub focus_visible: Option, + pub focus_within: Option, +} + +impl<'a> Into<'a>> for &'a OwnedPseudoClasses { + fn into(self) -> PseudoClasses<'a> { + PseudoClasses { + hover: self.hover.as_deref(), + active: self.active.as_deref(), + focus: self.focus.as_deref(), + focus_visible: self.focus_visible.as_deref(), + focus_within: self.focus_within.as_deref(), + } + } +} + +#[derive(Serialize, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct Drafts { + #[serde(default)] + custom_media: bool, +} + +#[derive(Serialize, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct NonStandard { + #[serde(default)] + deep_selector_combinator: bool, +} + +fn compile<'i>( + code: &'i str, + config: &Config, + #[allow(unused_variables)] visitor: &mut Option, +) -> Result<'i>, CompileError<'i, napi::Error>> { + let drafts = config.drafts.as_ref(); + let non_standard = config.non_standard.as_ref(); + let warnings = Some(Arc::new(RwLock::new(Vec::new()))); + + let filename = config.filename.clone().unwrap_or_default(); + let project_root = config.project_root.as_ref().map(|p| p.as_ref()); + let mut source_map = if config.source_map.unwrap_or_default() { + let mut sm = SourceMap::new(project_root.unwrap_or("/")); + sm.add_source(&filename); + sm.set_source_content(0, code)?; + Some(sm) + } else { + None + }; + + let res = { + let mut flags = ParserFlags::empty(); + flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); + flags.set( + ParserFlags::DEEP_SELECTOR_COMBINATOR, + matches!(non_standard, Some(v) if v.deep_selector_combinator), + ); + + let mut stylesheet = StyleSheet::parse_with( + &code, + ParserOptions { + filename: filename.clone(), + flags, + css_modules: if let Some(css_modules) = &config.css_modules { + match css_modules { + CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), + CssModulesOption::Bool(false) => None, + CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { + pattern: if let Some(pattern) = c.pattern.as_ref() { + match lightningcss::css_modules::Pattern::parse(pattern) { + Ok(p) => p, + Err(e) => return Err(CompileError::PatternError(e)), + } + } else { + Default::default() + }, + dashed_idents: c.dashed_idents.unwrap_or_default(), + animation: c.animation.unwrap_or(true), + container: c.container.unwrap_or(true), + grid: c.grid.unwrap_or(true), + custom_idents: c.custom_idents.unwrap_or(true), + pure: c.pure.unwrap_or_default(), + }), + } + } else { + None + }, + source_index: 0, + error_recovery: config.error_recovery.unwrap_or_default(), + warnings: warnings.clone(), + }, + &mut CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), + }, + )?; + + #[cfg(feature = "visitor")] + if let Some(visitor) = visitor.as_mut() { + stylesheet.visit(visitor).map_err(CompileError::JsError)?; + } + + let targets = Targets { + browsers: config.targets, + include: Features::from_bits_truncate(config.include), + exclude: Features::from_bits_truncate(config.exclude), + }; + + stylesheet.minify(MinifyOptions { + targets, + unused_symbols: config.unused_symbols.clone().unwrap_or_default(), + })?; + + stylesheet.to_css(PrinterOptions { + minify: config.minify.unwrap_or_default(), + source_map: source_map.as_mut(), + project_root, + targets, + analyze_dependencies: if let Some(d) = &config.analyze_dependencies { + match d { + AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), + AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { + remove_imports: !c.preserve_imports, + }), + _ => None, + } + } else { + None + }, + pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), + })? + }; + + let map = if let Some(mut source_map) = source_map { + if let Some(input_source_map) = &config.input_source_map { + if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) { + let _ = source_map.extends(&mut sm); + } + } + + source_map.to_json(None).ok() + } else { + None + }; + + Ok(TransformResult { + code: res.code.into_bytes(), + map: map.map(|m| m.into_bytes()), + exports: res.exports, + references: res.references, + dependencies: res.dependencies, + warnings: warnings.map_or(Vec::new(), |w| { + Arc::try_unwrap(w) + .unwrap() + .into_inner() + .unwrap() + .into_iter() + .map(|w| w.into()) + .collect() + }), + }) +} + +#[cfg(feature = "bundler")] +fn compile_bundle< + 'i, + 'o, + P: SourceProvider, + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, +>( + fs: &'i P, + config: &'o BundleConfig, + visit: Option, +) -> Result<'i>, CompileError<'i, P::Error>> { + use std::path::Path; + + let project_root = config.project_root.as_ref().map(|p| p.as_ref()); + let mut source_map = if config.source_map.unwrap_or_default() { + Some(SourceMap::new(project_root.unwrap_or("/"))) + } else { + None + }; + let warnings = Some(Arc::new(RwLock::new(Vec::new()))); + + let res = { + let drafts = config.drafts.as_ref(); + let non_standard = config.non_standard.as_ref(); + let mut flags = ParserFlags::empty(); + flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); + flags.set( + ParserFlags::DEEP_SELECTOR_COMBINATOR, + matches!(non_standard, Some(v) if v.deep_selector_combinator), + ); + + let parser_options = ParserOptions { + flags, + css_modules: if let Some(css_modules) = &config.css_modules { + match css_modules { + CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), + CssModulesOption::Bool(false) => None, + CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { + pattern: if let Some(pattern) = c.pattern.as_ref() { + match lightningcss::css_modules::Pattern::parse(pattern) { + Ok(p) => p, + Err(e) => return Err(CompileError::PatternError(e)), + } + } else { + Default::default() + }, + dashed_idents: c.dashed_idents.unwrap_or_default(), + animation: c.animation.unwrap_or(true), + container: c.container.unwrap_or(true), + grid: c.grid.unwrap_or(true), + custom_idents: c.custom_idents.unwrap_or(true), + pure: c.pure.unwrap_or_default(), + }), + } + } else { + None + }, + error_recovery: config.error_recovery.unwrap_or_default(), + warnings: warnings.clone(), + filename: String::new(), + source_index: 0, + }; + + let mut at_rule_parser = CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), + }; + + let mut bundler = + Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); + let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; + + if let Some(visit) = visit { + visit(&mut stylesheet).map_err(CompileError::JsError)?; + } + + let targets = Targets { + browsers: config.targets, + include: Features::from_bits_truncate(config.include), + exclude: Features::from_bits_truncate(config.exclude), + }; + + stylesheet.minify(MinifyOptions { + targets, + unused_symbols: config.unused_symbols.clone().unwrap_or_default(), + })?; + + stylesheet.to_css(PrinterOptions { + minify: config.minify.unwrap_or_default(), + source_map: source_map.as_mut(), + project_root, + targets, + analyze_dependencies: if let Some(d) = &config.analyze_dependencies { + match d { + AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), + AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { + remove_imports: !c.preserve_imports, + }), + _ => None, + } + } else { + None + }, + pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), + })? + }; + + let map = if let Some(source_map) = &mut source_map { + source_map.to_json(None).ok() + } else { + None + }; + + Ok(TransformResult { + code: res.code.into_bytes(), + map: map.map(|m| m.into_bytes()), + exports: res.exports, + references: res.references, + dependencies: res.dependencies, + warnings: warnings.map_or(Vec::new(), |w| { + Arc::try_unwrap(w) + .unwrap() + .into_inner() + .unwrap() + .into_iter() + .map(|w| w.into()) + .collect() + }), + }) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AttrConfig { + pub filename: Option, + #[serde(with = "serde_bytes")] + pub code: Vec, + pub targets: Option, + #[serde(default)] + pub include: u32, + #[serde(default)] + pub exclude: u32, + #[serde(default)] + pub minify: bool, + #[serde(default)] + pub analyze_dependencies: bool, + #[serde(default)] + pub error_recovery: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct AttrResult<'i> { + #[serde(with = "serde_bytes")] + code: Vec, + dependencies: Option>, + warnings: Vec<'i>>, +} + +impl<'i> AttrResult<'i> { + fn into_js(self, ctx: CallContext) -> napi::Result { + // Manually construct buffers so we avoid a copy and work around + // https://github.com/napi-rs/napi-rs/issues/1124. + let mut obj = ctx.env.create_object()?; + let buf = ctx.env.create_buffer_with_data(self.code)?; + obj.set_named_property("code", buf.into_raw())?; + obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?; + obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?; + Ok(obj.into_unknown()) + } +} + +fn compile_attr<'i>( + code: &'i str, + config: &AttrConfig, + #[allow(unused_variables)] visitor: &mut Option, +) -> Result<'i>, CompileError<'i, napi::Error>> { + let warnings = if config.error_recovery { + Some(Arc::new(RwLock::new(Vec::new()))) + } else { + None + }; + let res = { + let filename = config.filename.clone().unwrap_or_default(); + let mut attr = StyleAttribute::parse( + &code, + ParserOptions { + filename, + error_recovery: config.error_recovery, + warnings: warnings.clone(), + ..ParserOptions::default() + }, + )?; + + #[cfg(feature = "visitor")] + if let Some(visitor) = visitor.as_mut() { + attr.visit(visitor).unwrap(); + } + + let targets = Targets { + browsers: config.targets, + include: Features::from_bits_truncate(config.include), + exclude: Features::from_bits_truncate(config.exclude), + }; + + attr.minify(MinifyOptions { + targets, + ..MinifyOptions::default() + }); + attr.to_css(PrinterOptions { + minify: config.minify, + source_map: None, + project_root: None, + targets, + analyze_dependencies: if config.analyze_dependencies { + Some(DependencyOptions::default()) + } else { + None + }, + pseudo_classes: None, + })? + }; + Ok(AttrResult { + code: res.code.into_bytes(), + dependencies: res.dependencies, + warnings: warnings.map_or(Vec::new(), |w| { + Arc::try_unwrap(w) + .unwrap() + .into_inner() + .unwrap() + .into_iter() + .map(|w| w.into()) + .collect() + }), + }) +} + +enum CompileError<'i, E: std::error::Error> { + ParseError(Error<'i>>), + MinifyError(Error), + PrinterError(Error), + SourceMapError(parcel_sourcemap::SourceMapError), + BundleError(Error<'i, E>>), + PatternError(PatternParseError), + #[cfg(feature = "visitor")] + JsError(napi::Error), +} + +impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + CompileError::ParseError(err) => err.kind.fmt(f), + CompileError::MinifyError(err) => err.kind.fmt(f), + CompileError::PrinterError(err) => err.kind.fmt(f), + CompileError::BundleError(err) => err.kind.fmt(f), + CompileError::PatternError(err) => err.fmt(f), + CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this + #[cfg(feature = "visitor")] + CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f), + } + } +} + +impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> { + fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result { + let reason = self.to_string(); + let data = match &self { + CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?, + CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?, + CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?, + CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?, + _ => env.get_null()?.into_unknown(), + }; + + let (js_error, loc) = match self { + CompileError::BundleError(Error { + loc, + kind: BundleErrorKind::ResolverError(e), + }) => { + // Add location info to existing JS error if available. + (e.into_js_error(env)?, loc) + } + CompileError::ParseError(Error { loc, .. }) + | CompileError::PrinterError(Error { loc, .. }) + | CompileError::MinifyError(Error { loc, .. }) + | CompileError::BundleError(Error { loc, .. }) => { + // Generate an error with location information. + let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; + let reason = env.create_string_from_std(reason)?; + let obj = syntax_error.new_instance(&[reason])?; + (obj.into_unknown(), loc) + } + _ => return Ok(self.into()), + }; + + if js_error.get_type()? == napi::ValueType::Object { + let mut obj: JsObject = unsafe { js_error.cast() }; + if let Some(loc) = loc { + let line = env.create_int32((loc.line + 1) as i32)?; + let col = env.create_int32(loc.column as i32)?; + let filename = env.create_string_from_std(loc.filename)?; + obj.set_named_property("fileName", filename)?; + if let Some(code) = code { + let source = env.create_string(code)?; + obj.set_named_property("source", source)?; + } + let mut loc = env.create_object()?; + loc.set_named_property("line", line)?; + loc.set_named_property("column", col)?; + obj.set_named_property("loc", loc)?; + } + obj.set_named_property("data", data)?; + Ok(obj.into_unknown().into()) + } else { + Ok(js_error.into()) + } + } +} + +trait IntoJsError { + fn into_js_error(self, env: Env) -> napi::Result; +} + +impl IntoJsError for std::io::Error { + fn into_js_error(self, env: Env) -> napi::Result { + let reason = self.to_string(); + let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; + let reason = env.create_string_from_std(reason)?; + let obj = syntax_error.new_instance(&[reason])?; + Ok(obj.into_unknown()) + } +} + +impl IntoJsError for napi::Error { + fn into_js_error(self, env: Env) -> napi::Result { + unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) } + } +} + +impl<'i, E: std::error::Error> From<'i>>> for CompileError<'i, E> { + fn from(e: Error<'i>>) -> CompileError<'i, E> { + CompileError::ParseError(e) + } +} + +impl<'i, E: std::error::Error> From> for CompileError<'i, E> { + fn from(err: Error) -> CompileError<'i, E> { + CompileError::MinifyError(err) + } +} + +impl<'i, E: std::error::Error> From> for CompileError<'i, E> { + fn from(err: Error) -> CompileError<'i, E> { + CompileError::PrinterError(err) + } +} + +impl<'i, E: std::error::Error> From for CompileError<'i, E> { + fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> { + CompileError::SourceMapError(e) + } +} + +impl<'i, E: std::error::Error> From<'i, E>>> for CompileError<'i, E> { + fn from(e: Error<'i, E>>) -> CompileError<'i, E> { + CompileError::BundleError(e) + } +} + +impl<'i, E: std::error::Error> From<'i, E>> for napi::Error { + fn from(e: CompileError<'i, E>) -> napi::Error { + match e { + CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()), + CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()), + #[cfg(feature = "visitor")] + CompileError::JsError(e) => e, + _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()), + } + } +} + +#[derive(Serialize)] +struct Warning<'i> { + message: String, + #[serde(flatten)] + data: ParserError<'i>, + loc: Option, +} + +impl<'i> From<'i>>> for Warning<'i> { + fn from(mut e: Error<'i>>) -> Self { + // Convert to 1-based line numbers. + if let Some(loc) = &mut e.loc { + loc.line += 1; + } + Warning { + message: e.kind.to_string(), + data: e.kind, + loc: e.loc, + } + } +} diff --git a/node/src/threadsafe_function.rs b/napi/src/threadsafe_function.rs similarity index 100% rename from node/src/threadsafe_function.rs rename to napi/src/threadsafe_function.rs diff --git a/node/src/transformer.rs b/napi/src/transformer.rs similarity index 90% rename from node/src/transformer.rs rename to napi/src/transformer.rs index 857b6fccb..bab931f2d 100644 --- a/node/src/transformer.rs +++ b/napi/src/transformer.rs @@ -19,14 +19,16 @@ use lightningcss::{ }, visitor::{Visit, VisitTypes, Visitor}, }; +use lightningcss::{stylesheet::StyleSheet, traits::IntoOwned}; use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; -use crate::at_rule_parser::AtRule; +use crate::{at_rule_parser::AtRule, utils::get_named_property}; pub struct JsVisitor { env: Env, + visit_stylesheet: VisitorsRef, visit_rule: VisitorsRef, rule_map: VisitorsRef, property_map: VisitorsRef, @@ -97,7 +99,7 @@ impl Visitors { fn named(&self, stage: VisitStage, name: &str) -> Option { self .for_stage(stage) - .and_then(|m| m.get_named_property::(name).ok()) + .and_then(|m| get_named_property::(m, name).ok()) } fn custom(&self, stage: VisitStage, obj: &str, name: &str) -> Option { @@ -110,7 +112,7 @@ impl Visitors { Ok(ValueType::Object) => { let o: napi::Result = v.try_into(); if let Ok(o) = o { - return o.get_named_property::(name).ok(); + return get_named_property::(&o, name).ok(); } } _ => {} @@ -142,6 +144,7 @@ impl Drop for JsVisitor { }; } + drop_tuple!(visit_stylesheet); drop_tuple!(visit_rule); drop_tuple!(rule_map); drop_tuple!(visit_declaration); @@ -174,7 +177,8 @@ impl JsVisitor { let mut types = VisitTypes::empty(); macro_rules! get { ($name: literal, $( $t: ident )|+) => {{ - let res: Option = visitor.get_named_property($name).ok(); + let res: Option = get_named_property(&visitor, $name).ok(); + if res.is_some() { types |= $( VisitTypes::$t )|+; } @@ -187,17 +191,19 @@ impl JsVisitor { macro_rules! map { ($name: literal, $( $t: ident )|+) => {{ - if let Ok(obj) = visitor.get_named_property::($name) { + let obj: Option = get_named_property(&visitor, $name).ok(); + + if obj.is_some() { types |= $( VisitTypes::$t )|+; - env.create_reference(obj).ok() - } else { - None } + + obj.and_then(|obj| env.create_reference(obj).ok()) }}; } Self { env, + visit_stylesheet: VisitorsRef::new(get!("StyleSheet", RULES), get!("StyleSheetExit", RULES)), visit_rule: VisitorsRef::new(get!("Rule", RULES), get!("RuleExit", RULES)), rule_map: VisitorsRef::new(map!("Rule", RULES), get!("RuleExit", RULES)), visit_declaration: VisitorsRef::new(get!("Declaration", PROPERTIES), get!("DeclarationExit", PROPERTIES)), @@ -248,12 +254,30 @@ impl JsVisitor { impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { type Error = napi::Error; - const TYPES: lightningcss::visitor::VisitTypes = VisitTypes::all(); - fn visit_types(&self) -> VisitTypes { self.types } + fn visit_stylesheet<'o>(&mut self, stylesheet: &mut StyleSheet<'i, 'o, AtRule<'i>>) -> Result<(), Self::Error> { + if self.types.contains(VisitTypes::RULES) { + let env = self.env; + let visit_stylesheet = self.visit_stylesheet.get::(&env); + if let Some(visit) = visit_stylesheet.for_stage(VisitStage::Enter) { + call_visitor(&env, stylesheet, visit)? + } + + stylesheet.visit_children(self)?; + + if let Some(visit) = visit_stylesheet.for_stage(VisitStage::Exit) { + call_visitor(&env, stylesheet, visit)? + } + + Ok(()) + } else { + stylesheet.visit_children(self) + } + } + fn visit_rule_list( &mut self, rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>, @@ -274,6 +298,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Keyframes(..) => "keyframes", CssRule::FontFace(..) => "font-face", CssRule::FontPaletteValues(..) => "font-palette-values", + CssRule::FontFeatureValues(..) => "font-feature-values", CssRule::Page(..) => "page", CssRule::Supports(..) => "supports", CssRule::CounterStyle(..) => "counter-style", @@ -283,10 +308,13 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::LayerStatement(..) => "layer-statement", CssRule::Property(..) => "property", CssRule::Container(..) => "container", + CssRule::Scope(..) => "scope", CssRule::MozDocument(..) => "moz-document", CssRule::Nesting(..) => "nesting", + CssRule::NestedDeclarations(..) => "nested-declarations", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", + CssRule::ViewTransition(..) => "view-transition", CssRule::Unknown(v) => { let name = v.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "unknown", name) { @@ -585,13 +613,23 @@ fn visit<'static>>( .as_ref() .and_then(|v| env.get_reference_value_unchecked::(v).ok()) { - let js_value = env.to_js_value(value)?; - let res = visit.call(None, &[js_value])?; - let new_value: Option = env.from_js_value(res).map(serde_detach::detach)?; - match new_value { - Some(new_value) => *value = new_value, - None => {} - } + call_visitor(env, value, &visit)?; + } + + Ok(()) +} + +fn call_visitor<'static>>( + env: &Env, + value: &mut V, + visit: &JsFunction, +) -> napi::Result<()> { + let js_value = env.to_js_value(value)?; + let res = visit.call(None, &[js_value])?; + let new_value: Option = env.from_js_value(res).map(serde_detach::detach)?; + match new_value { + Some(new_value) => *value = new_value, + None => {} } Ok(()) @@ -717,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; @@ -731,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, @@ -773,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()) @@ -790,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/napi/src/utils.rs b/napi/src/utils.rs new file mode 100644 index 000000000..90ac14957 --- /dev/null +++ b/napi/src/utils.rs @@ -0,0 +1,7 @@ +use napi::{Error, JsObject, JsUnknown, Result}; + +// Workaround for https://github.com/napi-rs/napi-rs/issues/1641 +pub fn get_named_property>(obj: &JsObject, property: &str) -> Result { + let unknown = obj.get_named_property::(property)?; + T::try_from(unknown) +} diff --git a/node/Cargo.toml b/node/Cargo.toml index 459488fda..b5c7505c3 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -9,17 +9,14 @@ publish = false crate-type = ["cdylib"] [dependencies] -serde = { version = "1.0.123", features = ["derive"] } -serde_bytes = "0.11.5" -cssparser = "0.29.1" -lightningcss = { path = "../", features = ["nodejs", "serde", "visitor"] } -parcel_sourcemap = { version = "2.1.1", features = ["json"] } -serde-detach = "0.0.1" -smallvec = { version = "1.7.0", features = ["union"] } -napi = {version = "2.10.0", default-features = false, features = ["napi4", "napi5", "compat-mode", "serde-json"]} +lightningcss-napi = { version = "0.4.7", path = "../napi", features = [ + "bundler", + "visitor", +] } +napi = { version = "2.15.4", default-features = false, features = [ + "compat-mode", +] } napi-derive = "2" -crossbeam-channel = "0.5.6" -rayon = "1.5.1" [target.'cfg(target_os = "macos")'.dependencies] jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] } diff --git a/node/ast.d.ts b/node/ast.d.ts index 6a67d020d..28e9d0930 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -1,4 +1,4 @@ -/* tslint:disable */ +/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, @@ -33,6 +33,10 @@ export type Rule = | { type: "font-palette-values"; value: FontPaletteValuesRule; } +| { + type: "font-feature-values"; + value: FontFeatureValuesRule; + } | { type: "page"; value: PageRule; @@ -57,6 +61,10 @@ export type Rule = | { type: "nesting"; value: NestingRule; } +| { + type: "nested-declarations"; + value: NestedDeclarationsRule; + } | { type: "viewport"; value: ViewportRule; @@ -81,10 +89,18 @@ export type Rule = | { type: "container"; value: ContainerRule; } +| { + type: "scope"; + value: ScopeRule; + } | { type: "starting-style"; value: StartingStyleRule; } +| { + type: "view-transition"; + value: ViewTransitionRule; + } | { type: "ignored"; } @@ -118,6 +134,10 @@ export type MediaCondition = */ operator: Operator; type: "operation"; + } + | { + type: "unknown"; + value: TokenOrValue[]; }; /** * A generic media feature or container feature. @@ -500,6 +520,10 @@ export type TokenOrValue = | { type: "dashed-ident"; value: String; + } + | { + type: "animation-name"; + value: AnimationName; }; /** * A raw CSS token. @@ -633,7 +657,7 @@ export type Token = * * Each color space is represented as a struct that implements the `From` and `Into` traits for all other color spaces, so it is possible to convert between color spaces easily. In addition, colors support [interpolation](#method.interpolate) as in the `color-mix()` function. */ -export type CssColor = CurrentColor | RGBColor | LABColor | PredefinedColor | FloatColor; +export type CssColor = CurrentColor | RGBColor | LABColor | PredefinedColor | FloatColor | LightDark | SystemColor; export type CurrentColor = { type: "currentcolor"; }; @@ -953,6 +977,57 @@ export type FloatColor = */ w: number; }; +export type LightDark = { + dark: CssColor; + light: CssColor; + type: "light-dark"; +}; +/** + * A CSS [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword. + */ +export type SystemColor = + | "accentcolor" + | "accentcolortext" + | "activetext" + | "buttonborder" + | "buttonface" + | "buttontext" + | "canvas" + | "canvastext" + | "field" + | "fieldtext" + | "graytext" + | "highlight" + | "highlighttext" + | "linktext" + | "mark" + | "marktext" + | "selecteditem" + | "selecteditemtext" + | "visitedtext" + | "activeborder" + | "activecaption" + | "appworkspace" + | "background" + | "buttonhighlight" + | "buttonshadow" + | "captiontext" + | "inactiveborder" + | "inactivecaption" + | "inactivecaptiontext" + | "infobackground" + | "infotext" + | "menu" + | "menutext" + | "scrollbar" + | "threeddarkshadow" + | "threedface" + | "threedhighlight" + | "threedlightshadow" + | "threedshadow" + | "window" + | "windowframe" + | "windowtext"; /** * A color value with an unresolved alpha value (e.g. a variable). These can be converted from the modern slash syntax to older comma syntax. This can only be done when the only unresolved component is the alpha since variables can resolve to multiple tokens. */ @@ -994,6 +1069,17 @@ export type UnresolvedColor = */ s: number; type: "hsl"; + } + | { + /** + * The dark value. + */ + dark: TokenOrValue[]; + /** + * The light value. + */ + light: TokenOrValue[]; + type: "light-dark"; }; /** * Defines where the class names referenced in the `composes` property are located. @@ -1048,6 +1134,21 @@ export type Time = type: "milliseconds"; value: number; }; +/** + * A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. + */ +export type AnimationName = + | { + type: "none"; + } + | { + type: "ident"; + value: String; + } + | { + type: "string"; + value: String; + }; /** * A CSS environment variable name. */ @@ -1869,6 +1970,21 @@ export type PropertyId = property: "animation-fill-mode"; vendorPrefix: VendorPrefix; } + | { + property: "animation-composition"; + } + | { + property: "animation-timeline"; + } + | { + property: "animation-range-start"; + } + | { + property: "animation-range-end"; + } + | { + property: "animation-range"; + } | { property: "animation"; vendorPrefix: VendorPrefix; @@ -2000,6 +2116,12 @@ export type PropertyId = property: "text-size-adjust"; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + } + | { + property: "unicode-bidi"; + } | { property: "box-decoration-break"; vendorPrefix: VendorPrefix; @@ -2236,6 +2358,19 @@ export type PropertyId = | { property: "view-transition-name"; } + | { + property: "view-transition-class"; + } + | { + property: "view-transition-group"; + } + | { + property: "color-scheme"; + } + | { + property: "print-color-adjust"; + vendorPrefix: VendorPrefix; + } | { property: "all"; } @@ -2387,7 +2522,7 @@ export type Declaration = } | { property: "position"; - value: Position; + value: Position2; } | { property: "top"; @@ -2681,7 +2816,7 @@ export type Declaration = } | { property: "outline"; - value: GenericBorderFor_OutlineStyle; + value: GenericBorderFor_OutlineStyleAnd_11; } | { property: "outline-color"; @@ -3208,6 +3343,26 @@ export type Declaration = value: AnimationFillMode[]; vendorPrefix: VendorPrefix; } + | { + property: "animation-composition"; + value: AnimationComposition[]; + } + | { + property: "animation-timeline"; + value: AnimationTimeline[]; + } + | { + property: "animation-range-start"; + value: AnimationRangeStart[]; + } + | { + property: "animation-range-end"; + value: AnimationRangeEnd[]; + } + | { + property: "animation-range"; + value: AnimationRange[]; + } | { property: "animation"; value: Animation[]; @@ -3376,6 +3531,14 @@ export type Declaration = value: TextSizeAdjust; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + value: Direction2; + } + | { + property: "unicode-bidi"; + value: UnicodeBidi; + } | { property: "box-decoration-break"; value: BoxDecorationBreak; @@ -3682,7 +3845,28 @@ export type Declaration = } | { property: "view-transition-name"; - value: String; + value: ViewTransitionName; + } + | { + property: "view-transition-class"; + value: NoneOrCustomIdentList; + } + | { + property: "view-transition-group"; + value: ViewTransitionGroup; + } + | { + property: "color-scheme"; + value: ColorScheme; + } + | { + property: "print-color-adjust"; + value: PrintColorAdjust; + vendorPrefix: VendorPrefix; + } + | { + property: "all"; + value: CSSWideKeyword; } | { property: "unparsed"; @@ -4085,23 +4269,30 @@ export type PositionComponentFor_VerticalPositionKeyword = * See [RadialGradient](RadialGradient). */ export type EndingShape = - | { - type: "circle"; - value: Circle; - } | { type: "ellipse"; value: Ellipse; + } + | { + type: "circle"; + value: Circle; }; /** - * A circle ending shape for a `radial-gradient()`. + * An ellipse ending shape for a `radial-gradient()`. * * See [RadialGradient](RadialGradient). */ -export type Circle = +export type Ellipse = | { - type: "radius"; - value: Length; + type: "size"; + /** + * The x-radius of the ellipse. + */ + x: DimensionPercentageFor_LengthValue; + /** + * The y-radius of the ellipse. + */ + y: DimensionPercentageFor_LengthValue; } | { type: "extent"; @@ -4114,21 +4305,14 @@ export type Circle = */ export type ShapeExtent = "closest-side" | "farthest-side" | "closest-corner" | "farthest-corner"; /** - * An ellipse ending shape for a `radial-gradient()`. + * A circle ending shape for a `radial-gradient()`. * * See [RadialGradient](RadialGradient). */ -export type Ellipse = +export type Circle = | { - type: "size"; - /** - * The x-radius of the ellipse. - */ - x: DimensionPercentageFor_LengthValue; - /** - * The y-radius of the ellipse. - */ - y: DimensionPercentageFor_LengthValue; + type: "radius"; + value: Length; } | { type: "extent"; @@ -4293,11 +4477,11 @@ export type WebKitGradientPointComponentFor_HorizontalPositionKeyword = */ export type NumberOrPercentage = | { - type: "percentage"; + type: "number"; value: number; } | { - type: "number"; + type: "percentage"; value: number; }; /** @@ -4523,6 +4707,26 @@ export type OverflowKeyword = "visible" | "hidden" | "clip" | "scroll" | "auto"; * A value for the [text-overflow](https://www.w3.org/TR/css-overflow-3/#text-overflow) property. */ export type TextOverflow = "clip" | "ellipsis"; +/** + * A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property. + */ +export type Position2 = + | { + type: "static"; + } + | { + type: "relative"; + } + | { + type: "absolute"; + } + | { + type: "sticky"; + value: VendorPrefix; + } + | { + type: "fixed"; + }; /** * A generic value that represents a value with two components, e.g. a border radius. * @@ -4588,13 +4792,13 @@ export type RectFor_LengthOrNumber = [LengthOrNumber, LengthOrNumber, LengthOrNu * Either a [``](https://www.w3.org/TR/css-values-4/#lengths) or a [``](https://www.w3.org/TR/css-values-4/#numbers). */ export type LengthOrNumber = - | { - type: "length"; - value: Length; - } | { type: "number"; value: number; + } + | { + type: "length"; + value: Length; }; /** * A single [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) keyword. @@ -5027,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 = | { @@ -5331,21 +5535,6 @@ export type StepPosition = | { type: "jump-both"; }; -/** - * A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. - */ -export type AnimationName = - | { - type: "none"; - } - | { - type: "ident"; - value: String; - } - | { - type: "string"; - value: String; - }; /** * A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property. */ @@ -5369,6 +5558,75 @@ export type AnimationPlayState = "running" | "paused"; * A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property. */ export type AnimationFillMode = "none" | "forwards" | "backwards" | "both"; +/** + * A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property. + */ +export type AnimationComposition = "replace" | "add" | "accumulate"; +/** + * A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property. + */ +export type AnimationTimeline = + | { + type: "auto"; + } + | { + type: "none"; + } + | { + type: "dashed-ident"; + value: String; + } + | { + type: "scroll"; + value: ScrollTimeline; + } + | { + type: "view"; + value: ViewTimeline; + }; +/** + * A scroll axis, used in the `scroll()` function. + */ +export type ScrollAxis = "block" | "inline" | "x" | "y"; +/** + * A scroller, used in the `scroll()` function. + */ +export type Scroller = "root" | "nearest" | "self"; +/** + * A generic value that represents a value with two components, e.g. a border radius. + * + * When serialized, only a single component will be written if both are equal. + * + * @minItems 2 + * @maxItems 2 + */ +export type Size2DFor_LengthPercentageOrAuto = [LengthPercentageOrAuto, LengthPercentageOrAuto]; +/** + * A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. + */ +export type AnimationRangeStart = AnimationAttachmentRange; +/** + * A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. + */ +export type AnimationAttachmentRange = + "normal" | DimensionPercentageFor_LengthValue | { + /** + * The name of the timeline range. + */ + name: TimelineRangeName; + /** + * The offset from the start of the named timeline range. + */ + offset: DimensionPercentageFor_LengthValue; + }; +/** + * A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges) + */ +export type TimelineRangeName = "cover" | "contain" | "entry" | "exit" | "entry-crossing" | "exit-crossing"; +/** + * A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. + */ +export type AnimationRangeEnd = AnimationAttachmentRange; /** * An individual [transform function](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions). */ @@ -5484,7 +5742,7 @@ export type Transform = /** * A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property. */ -export type TransformStyle = "flat" | "preserve-3d"; +export type TransformStyle = "flat" | "preserve3d"; /** * A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property. */ @@ -5504,6 +5762,44 @@ export type Perspective = type: "length"; value: Length; }; +/** + * A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. + */ +export type Translate = + | "none" + | { + /** + * The x translation. + */ + x: DimensionPercentageFor_LengthValue; + /** + * The y translation. + */ + y: DimensionPercentageFor_LengthValue; + /** + * The z translation. + */ + z: Length; + }; +/** + * A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. + */ +export type Scale = + | "none" + | { + /** + * Scale on the x axis. + */ + x: NumberOrPercentage; + /** + * Scale on the y axis. + */ + y: NumberOrPercentage; + /** + * Scale on the z axis. + */ + z: NumberOrPercentage; + }; /** * Defines how text case should be transformed in the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. */ @@ -5636,6 +5932,14 @@ export type TextSizeAdjust = type: "percentage"; value: number; }; +/** + * A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. + */ +export type Direction2 = "ltr" | "rtl"; +/** + * A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. + */ +export type UnicodeBidi = "normal" | "embed" | "isolate" | "bidi-override" | "isolate-override" | "plaintext"; /** * A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property. */ @@ -5834,9 +6138,6 @@ export type MarkerSide = "match-self" | "match-parent"; * An SVG [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value used in the `fill` and `stroke` properties. */ export type SVGPaint = - | { - type: "none"; - } | { /** * A fallback to be used used in case the paint server cannot be resolved. @@ -5857,6 +6158,9 @@ export type SVGPaint = } | { type: "context-stroke"; + } + | { + type: "none"; }; /** * A fallback for an SVG paint in case a paint server `url()` cannot be resolved. @@ -5974,11 +6278,11 @@ export type BasicShape = } | { type: "circle"; - value: Circle; + value: Circle2; } | { type: "ellipse"; - value: Ellipse; + value: Ellipse2; } | { type: "polygon"; @@ -5998,6 +6302,20 @@ export type RectFor_DimensionPercentageFor_LengthValue = [ DimensionPercentageFor_LengthValue, DimensionPercentageFor_LengthValue ]; +/** + * A [``](https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius) value that defines the radius of a `circle()` or `ellipse()` shape. + */ +export type ShapeRadius = + | { + type: "length-percentage"; + value: DimensionPercentageFor_LengthValue; + } + | { + type: "closest-side"; + } + | { + type: "farthest-side"; + }; /** * A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property. */ @@ -6115,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. */ @@ -6127,6 +6445,29 @@ export type ContainerNameList = type: "names"; value: String[]; }; +/** + * A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property. + */ +export type ViewTransitionName = + "none" | "auto" | String; +/** + * The `none` keyword, or a space-separated list of custom idents. + */ +export type NoneOrCustomIdentList = + "none" | String[]; +/** + * A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property. + */ +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). + */ +export type CSSWideKeyword = "initial" | "inherit" | "unset" | "revert" | "revert-layer"; /** * A CSS custom property name. */ @@ -6450,6 +6791,23 @@ export type PseudoClass = kind: "autofill"; vendorPrefix: VendorPrefix; } + | { + kind: "active-view-transition"; + } + | { + kind: "active-view-transition-type"; + /** + * A view transition type. + */ + type: String[]; + } + | { + kind: "state"; + /** + * The custom state identifier. + */ + state: String; + } | { kind: "local"; /** @@ -6530,6 +6888,12 @@ export type PseudoElement = | { kind: "first-letter"; } + | { + kind: "details-content"; + } + | { + kind: "target-text"; + } | { kind: "selection"; vendorPrefix: VendorPrefix; @@ -6581,28 +6945,47 @@ export type PseudoElement = /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "view-transition-image-pair"; /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "view-transition-old"; /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "view-transition-new"; /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; + } + | { + /** + * A form control identifier. + */ + identifier: String; + kind: "picker-function"; + } + | { + kind: "picker-icon"; + } + | { + kind: "checkmark"; + } + | { + kind: "grammar-error"; + } + | { + kind: "spelling-error"; } | { kind: "custom"; @@ -6649,6 +7032,10 @@ export type KeyframeSelector = } | { type: "to"; + } + | { + type: "timeline-range-percentage"; + value: TimelineRangePercentage; }; /** * KeyframesName @@ -6678,7 +7065,7 @@ export type FontFaceProperty = } | { type: "font-style"; - value: FontStyle; + value: FontStyle2; } | { type: "font-weight"; @@ -6752,6 +7139,29 @@ export type FontTechnology = | "variations" | "palettes" | "incremental"; +/** + * A value for the [font-style](https://w3c.github.io/csswg-drafts/css-fonts/#descdef-font-face-font-style) descriptor in an `@font-face` rule. + */ +export type FontStyle2 = + | { + type: "normal"; + } + | { + type: "italic"; + } + | { + type: "oblique"; + value: Size2DFor_Angle; + }; +/** + * A generic value that represents a value with two components, e.g. a border radius. + * + * When serialized, only a single component will be written if both are equal. + * + * @minItems 2 + * @maxItems 2 + */ +export type Size2DFor_Angle = [Angle, Angle]; /** * A generic value that represents a value with two components, e.g. a border radius. * @@ -6806,6 +7216,17 @@ export type BasePalette = type: "integer"; value: number; }; +/** + * The name of the `@font-feature-values` sub-rule. font-feature-value-type = <@stylistic> | <@historical-forms> | <@styleset> | <@character-variant> | <@swash> | <@ornaments> | <@annotation> + */ +export type FontFeatureSubruleType = + | "stylistic" + | "historical-forms" + | "styleset" + | "character-variant" + | "swash" + | "ornaments" + | "annotation"; /** * A [page margin box](https://www.w3.org/TR/css-page-3/#margin-boxes). */ @@ -6852,6 +7273,10 @@ export type ParsedComponent = type: "length-percentage"; value: DimensionPercentageFor_LengthValue; } + | { + type: "string"; + value: String; + } | { type: "color"; value: CssColor; @@ -6910,8 +7335,8 @@ export type ParsedComponent = }; } | { - type: "token"; - value: Token; + type: "token-list"; + value: TokenOrValue[]; }; /** * A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated. @@ -6953,6 +7378,9 @@ export type SyntaxComponentKind = | { type: "length-percentage"; } + | { + type: "string"; + } | { type: "color"; } @@ -7012,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. @@ -7085,9 +7521,13 @@ export type ContainerSizeFeatureId = "width" | "height" | "inline-size" | "block * Represents a style query within a container condition. */ export type StyleQuery = | { - type: "feature"; + type: "declaration"; value: D; } +| { + type: "property"; + value: PropertyId; + } | { type: "not"; value: StyleQuery; @@ -7103,6 +7543,119 @@ 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. + * + * See [ViewTransitionRule](ViewTransitionRule). + */ +export type ViewTransitionProperty = + | { + property: "navigation"; + value: Navigation; + } + | { + property: "types"; + value: NoneOrCustomIdentList; + } + | { + property: "custom"; + value: CustomProperty; + }; +/** + * A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) property in a `@view-transition` rule. + */ +export type Navigation = "none" | "auto"; export type DefaultAtRule = null; /** @@ -7147,7 +7700,7 @@ export interface MediaRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The media query list. */ @@ -7158,17 +7711,21 @@ export interface MediaRule { rules: Rule[]; } /** - * A line and column position within a source file. + * A source location. */ -export interface Location { +export interface Location2 { /** - * The column number, starting from 1. + * The column number within a line, starting at 1 for first the character of the line. Column numbers are counted in UTF-16 code units. */ column: number; /** - * The line number, starting from 1. + * The line number, starting at 0. */ line: number; + /** + * The index of the source file within the source map. + */ + source_index: number; } /** * A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list). @@ -7236,6 +7793,19 @@ export interface Url { */ url: String; } +/** + * A line and column position within a source file. + */ +export interface Location { + /** + * The column number, starting from 1. + */ + column: number; + /** + * The line number, starting from 1. + */ + line: number; +} /** * A CSS variable reference. */ @@ -7290,7 +7860,7 @@ export interface ImportRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * A media query. */ @@ -7315,7 +7885,7 @@ export interface StyleRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * Nested rules within the style rule. */ @@ -7809,7 +8379,7 @@ export interface GenericBorderFor_LineStyle { /** * A generic type that represents the `border` and `outline` shorthand properties. */ -export interface GenericBorderFor_OutlineStyle { +export interface GenericBorderFor_OutlineStyleAnd_11 { /** * The border color. */ @@ -7937,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 { @@ -7956,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 { @@ -7985,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 { /** @@ -7998,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 { /** @@ -8011,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 { /** @@ -8273,6 +8847,45 @@ export interface Transition { */ timingFunction: EasingFunction; } +/** + * The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function. + */ +export interface ScrollTimeline { + /** + * Specifies which axis of the scroll container to use as the progress for the timeline. + */ + axis: ScrollAxis; + /** + * Specifies which element to use as the scroll container. + */ + scroller: Scroller; +} +/** + * The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function. + */ +export interface ViewTimeline { + /** + * Specifies which axis of the scroll container to use as the progress for the timeline. + */ + axis: ScrollAxis; + /** + * Provides an adjustment of the view progress visibility range. + */ + inset: Size2DFor_LengthPercentageOrAuto; +} +/** + * A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property. + */ +export interface AnimationRange { + /** + * The end of the animation's attachment range. + */ + end: AnimationRangeEnd; + /** + * The start of the animation's attachment range. + */ + start: AnimationRangeStart; +} /** * A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property. */ @@ -8305,6 +8918,10 @@ export interface Animation { * The current play state of the animation. */ playState: AnimationPlayState; + /** + * The animation timeline. + */ + timeline: AnimationTimeline; /** * The easing function for the animation. */ @@ -8342,23 +8959,6 @@ export interface Matrix3DForFloat { m43: number; m44: number; } -/** - * A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. - */ -export interface Translate { - /** - * The x translation. - */ - x: DimensionPercentageFor_LengthValue; - /** - * The y translation. - */ - y: DimensionPercentageFor_LengthValue; - /** - * The z translation. - */ - z: Length; -} /** * A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. */ @@ -8380,23 +8980,6 @@ export interface Rotate { */ z: number; } -/** - * A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. - */ -export interface Scale { - /** - * Scale on the x axis. - */ - x: NumberOrPercentage; - /** - * Scale on the y axis. - */ - y: NumberOrPercentage; - /** - * Scale on the z axis. - */ - z: NumberOrPercentage; -} /** * A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. */ @@ -8594,6 +9177,36 @@ export interface InsetRect { */ rect: RectFor_DimensionPercentageFor_LengthValue; } +/** + * A [`circle()`](https://www.w3.org/TR/css-shapes-1/#funcdef-circle) shape. + */ +export interface Circle2 { + /** + * The position of the center of the circle. + */ + position: Position; + /** + * The radius of the circle. + */ + radius: ShapeRadius; +} +/** + * An [`ellipse()`](https://www.w3.org/TR/css-shapes-1/#funcdef-ellipse) shape. + */ +export interface Ellipse2 { + /** + * The position of the center of the ellipse. + */ + position: Position; + /** + * The x-radius of the ellipse. + */ + radiusX: ShapeRadius; + /** + * The y-radius of the ellipse. + */ + radiusY: ShapeRadius; +} /** * A [`polygon()`](https://www.w3.org/TR/css-shapes-1/#funcdef-polygon) shape. */ @@ -8722,6 +9335,11 @@ export interface Container { */ name: ContainerNameList; } +export interface ColorScheme { + dark: boolean; + light: boolean; + only: boolean; +} /** * A known property with an unparsed value. * @@ -8755,6 +9373,19 @@ export interface AttrOperation { operator: AttrSelectorOperator; value: string; } +/** + * A [view transition part selector](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). + */ +export interface ViewTransitionPartSelector { + /** + * A list of view transition classes. + */ + classes: String[]; + /** + * The view transition part name. + */ + name?: ViewTransitionPartName | null; +} /** * A [@keyframes](https://drafts.csswg.org/css-animations/#keyframes) rule. */ @@ -8766,7 +9397,7 @@ export interface KeyframesRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The animation name. = | */ @@ -8791,6 +9422,19 @@ export interface Keyframe { */ selectors: KeyframeSelector[]; } +/** + * A percentage of a given timeline range + */ +export interface TimelineRangePercentage { + /** + * The name of the timeline range. + */ + name: TimelineRangeName; + /** + * The percentage progress between the start and end of the range. + */ + percentage: number; +} /** * A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule. */ @@ -8798,7 +9442,7 @@ export interface FontFaceRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * Declarations in the `@font-face` rule. */ @@ -8843,7 +9487,7 @@ export interface FontPaletteValuesRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the font palette. */ @@ -8866,6 +9510,44 @@ export interface OverrideColors { */ index: number; } +/** + * A [@font-feature-values](https://drafts.csswg.org/css-fonts/#font-feature-values) rule. + */ +export interface FontFeatureValuesRule { + /** + * The location of the rule in the source file. + */ + loc: Location2; + /** + * The name of the font feature values. + */ + name: String[]; + /** + * The rules within the `@font-feature-values` rule. + */ + rules: { + [k: string]: FontFeatureSubrule; + }; +} +/** + * A sub-rule of `@font-feature-values` https://drafts.csswg.org/css-fonts/#font-feature-values-syntax + */ +export interface FontFeatureSubrule { + /** + * The declarations within the `@font-feature-values` sub-rules. + */ + declarations: { + [k: string]: number[]; + }; + /** + * The location of the rule in the source file. + */ + loc: Location2; + /** + * The name of the `@font-feature-values` sub-rule. + */ + name: FontFeatureSubruleType; +} /** * A [@page](https://www.w3.org/TR/css-page-3/#at-page-rule) rule. */ @@ -8877,7 +9559,7 @@ export interface PageRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The nested margin rules. */ @@ -8898,7 +9580,7 @@ export interface PageMarginRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The margin box identifier for this rule. */ @@ -8930,7 +9612,7 @@ export interface SupportsRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The rules within the `@supports` rule. */ @@ -8947,7 +9629,7 @@ export interface CounterStyleRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the counter style to declare. */ @@ -8960,7 +9642,7 @@ export interface NamespaceRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * An optional namespace prefix to declare, or `None` to declare the default namespace. */ @@ -8979,7 +9661,7 @@ export interface MozDocumentRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * Nested rules within the `@-moz-document` rule. */ @@ -8992,12 +9674,25 @@ export interface NestingRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The style rule that defines the selector and declarations for the `@nest` rule. */ style: StyleRule; } +/** + * A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule. + */ +export interface NestedDeclarationsRule { + /** + * The style rule that defines the selector and declarations for the `@nest` rule. + */ + declarations: DeclarationBlock; + /** + * The location of the rule in the source file. + */ + loc: Location2; +} /** * A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule. */ @@ -9009,7 +9704,7 @@ export interface ViewportRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The vendor prefix for this rule, e.g. `@-ms-viewport`. */ @@ -9022,7 +9717,7 @@ export interface CustomMediaRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the declared media query. */ @@ -9041,7 +9736,7 @@ export interface LayerStatementRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The layer names to declare. */ @@ -9054,7 +9749,7 @@ export interface LayerBlockRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the layer to declare, or `None` to declare an anonymous layer. */ @@ -9079,7 +9774,7 @@ export interface PropertyRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the custom property to declare. */ @@ -9111,11 +9806,11 @@ export interface ContainerRule { /** * The container condition. */ - condition: ContainerCondition; + condition?: ContainerCondition | null; /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the container. */ @@ -9125,6 +9820,29 @@ export interface ContainerRule { */ rules: Rule[]; } +/** + * A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule. + * + * @scope () [to ()]? { } + */ +export interface ScopeRule { + /** + * The location of the rule in the source file. + */ + loc: Location2; + /** + * Nested rules within the `@scope` rule. + */ + rules: Rule[]; + /** + * A selector list used to identify any scoping limits. + */ + scopeEnd?: SelectorList | null; + /** + * A selector list used to identify the scoping root(s). + */ + scopeStart?: SelectorList | null; +} /** * A [@starting-style](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule) rule. */ @@ -9132,12 +9850,25 @@ export interface StartingStyleRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * Nested rules within the `@starting-style` rule. */ rules: Rule[]; } +/** + * A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule. + */ +export interface ViewTransitionRule { + /** + * The location of the rule in the source file. + */ + loc: Location2; + /** + * Declarations in the `@view-transition` rule. + */ + properties: ViewTransitionProperty[]; +} /** * An unknown at-rule, stored as raw tokens. */ @@ -9149,7 +9880,7 @@ export interface UnknownAtRule { /** * The location of the rule in the source file. */ - loc: Location; + loc: Location2; /** * The name of the at-rule (without the @). */ diff --git a/node/composeVisitors.js b/node/composeVisitors.js index 850058d0b..f29934905 100644 --- a/node/composeVisitors.js +++ b/node/composeVisitors.js @@ -1,20 +1,30 @@ // @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 = {}; - composeObjectVisitors(res, visitors, 'Rule', ruleVisitor, wrapUnknownAtRule); - composeObjectVisitors(res, visitors, 'RuleExit', ruleVisitor, wrapUnknownAtRule); + composeSimpleVisitors(res, visitors, 'StyleSheet'); + composeSimpleVisitors(res, visitors, 'StyleSheetExit'); + composeObjectVisitors(res, visitors, 'Rule', ruleVisitor, wrapCustomAndUnknownAtRule); + composeObjectVisitors(res, visitors, 'RuleExit', ruleVisitor, wrapCustomAndUnknownAtRule); composeObjectVisitors(res, visitors, 'Declaration', declarationVisitor, wrapCustomProperty); composeObjectVisitors(res, visitors, 'DeclarationExit', declarationVisitor, wrapCustomProperty); composeSimpleVisitors(res, visitors, 'Url'); @@ -45,8 +55,14 @@ function composeVisitors(visitors) { module.exports = composeVisitors; -function wrapUnknownAtRule(k, f) { - return k === 'unknown' ? (value => f({ type: 'unknown', value })) : f; +function wrapCustomAndUnknownAtRule(k, f) { + if (k === 'unknown') { + return (value => f({ type: 'unknown', value })); + } + if (k === 'custom') { + return (value => f({ type: 'custom', value })); + } + return f; } function wrapCustomProperty(k, f) { @@ -66,6 +82,13 @@ function ruleVisitor(f, item) { } return v?.(item.value); } + if (item.type === 'custom') { + let v = f.custom; + if (typeof v === 'object') { + v = v[item.value.name]; + } + return v?.(item.value); + } return f[item.type]?.(item); } return f?.(item); @@ -351,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/flags.js b/node/flags.js index 1759b4d97..a636a2045 100644 --- a/node/flags.js +++ b/node/flags.js @@ -21,7 +21,8 @@ exports.Features = { DoublePositionGradients: 131072, VendorPrefixes: 262144, LogicalProperties: 524288, + LightDark: 1048576, Selectors: 31, MediaQueries: 448, - Colors: 64512, + Colors: 1113088, }; diff --git a/node/index.d.ts b/node/index.d.ts index 8bf1cbb12..6d727d75a 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -1,4 +1,4 @@ -import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent, Multiplier } from './ast'; +import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent, Multiplier, StyleSheet, Location2 } from './ast'; import { Targets, Features } from './targets'; export * from './ast'; @@ -24,9 +24,9 @@ export interface TransformOptions { /** The browser targets for the generated code. */ targets?: Targets, /** Features that should always be compiled, even when supported by targets. */ - include?: Features, + include?: number, /** Features that should never be compiled, even when unsupported by targets. */ - exclude?: Features, + exclude?: number, /** Whether to enable parsing various draft syntax. */ drafts?: Drafts, /** Whether to enable various non-standard syntax. */ @@ -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 @@ -138,7 +138,7 @@ interface CustomAtRule { name: N, prelude: R['prelude'] extends keyof MappedPrelude ? MappedPrelude[R['prelude']] : ParsedComponent, body: FindByType>, - loc: Location + loc: Location2 } type CustomAtRuleBody = { @@ -182,6 +182,8 @@ type EnvironmentVariableVisitors = { }; export interface Visitor { + StyleSheet?(stylesheet: StyleSheet): StyleSheet | void; + StyleSheetExit?(stylesheet: StyleSheet): StyleSheet | void; Rule?: RuleVisitor | RuleVisitors; RuleExit?: RuleVisitor | RuleVisitors; Declaration?: DeclarationVisitor | DeclarationVisitors; @@ -211,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 } @@ -259,8 +268,6 @@ export interface Resolver { } export interface Drafts { - /** Whether to enable CSS nesting. */ - nesting?: boolean, /** Whether to enable @custom-media rules. */ customMedia?: boolean } @@ -304,7 +311,17 @@ export interface CSSModulesConfig { /** The pattern to use when renaming class names and other identifiers. Default is `[hash]_[local]`. */ pattern?: string, /** Whether to rename dashed identifiers, e.g. custom properties. */ - dashedIdents?: boolean + dashedIdents?: boolean, + /** Whether to enable hashing for `@keyframes`. */ + animation?: boolean, + /** Whether to enable hashing for CSS grid identifiers. */ + grid?: boolean, + /** Whether to enable hashing for `@container` names. */ + container?: boolean, + /** Whether to enable hashing for custom identifiers. */ + customIdents?: boolean, + /** Whether to require at least one class or id selector in each rule. */ + pure?: boolean } export type CSSModuleExports = { @@ -348,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', @@ -374,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, @@ -428,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 { @@ -464,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 a9f2f6d5f..6fe25aef4 100644 --- a/node/index.js +++ b/node/index.js @@ -1,6 +1,7 @@ let parts = [process.platform, process.arch]; if (process.platform === 'linux') { - const { MUSL, family } = require('detect-libc'); + const { MUSL, familySync } = require('detect-libc'); + const family = familySync(); if (family === MUSL) { parts.push('musl'); } else if (process.arch === 'arm') { @@ -12,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 870b303f6..822944124 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,433 +2,38 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -use at_rule_parser::{AtRule, CustomAtRuleConfig, CustomAtRuleParser}; -use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider, SourceProvider}; -use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; -use lightningcss::dependencies::{Dependency, DependencyOptions}; -use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; -use lightningcss::stylesheet::{ - MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, -}; -use lightningcss::targets::{Browsers, Features, Targets}; -use lightningcss::visitor::Visit; -use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; -use parcel_sourcemap::SourceMap; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::{Arc, Mutex, RwLock}; - -use transformer::JsVisitor; - -mod at_rule_parser; -#[cfg(not(target_arch = "wasm32"))] -mod threadsafe_function; -mod transformer; - -use napi::{CallContext, Env, JsObject, JsUnknown}; -use napi_derive::{js_function, module_exports}; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct TransformResult<'i> { - #[serde(with = "serde_bytes")] - code: Vec, - #[serde(with = "serde_bytes")] - map: Option>, - exports: Option, - references: Option, - dependencies: Option>, - warnings: Vec<'i>>, -} - -impl<'i> TransformResult<'i> { - fn into_js(self, env: Env) -> napi::Result { - // Manually construct buffers so we avoid a copy and work around - // https://github.com/napi-rs/napi-rs/issues/1124. - let mut obj = env.create_object()?; - let buf = env.create_buffer_with_data(self.code)?; - obj.set_named_property("code", buf.into_raw())?; - obj.set_named_property( - "map", - if let Some(map) = self.map { - let buf = env.create_buffer_with_data(map)?; - buf.into_raw().into_unknown() - } else { - env.get_null()?.into_unknown() - }, - )?; - obj.set_named_property("exports", env.to_js_value(&self.exports)?)?; - obj.set_named_property("references", env.to_js_value(&self.references)?)?; - obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?; - obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?; - Ok(obj.into_unknown()) - } -} +use napi::{CallContext, JsObject, JsUnknown}; +use napi_derive::js_function; #[js_function(1)] fn transform(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let config: Config = ctx.env.from_js_value(opts)?; - let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; - let res = compile(code, &config, &mut visitor); - - match res { - Ok(res) => res.into_js(*ctx.env), - Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), - } + lightningcss_napi::transform(ctx) } #[js_function(1)] fn transform_style_attribute(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let config: AttrConfig = ctx.env.from_js_value(opts)?; - let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; - let res = compile_attr(code, &config, &mut visitor); + lightningcss_napi::transform_style_attribute(ctx) +} - match res { - Ok(res) => res.into_js(ctx), - Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), - } +#[js_function(1)] +pub fn bundle(ctx: CallContext) -> napi::Result { + lightningcss_napi::bundle(ctx) } #[cfg(not(target_arch = "wasm32"))] -mod bundle { - use super::*; - use crossbeam_channel::{self, Receiver, Sender}; - use napi::{Env, JsFunction, JsString, NapiRaw}; - use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; - - #[js_function(1)] - pub fn bundle(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let config: BundleConfig = ctx.env.from_js_value(opts)?; - let fs = FileProvider::new(); - - // This is pretty silly, but works around a rust limitation that you cannot - // explicitly annotate lifetime bounds on closures. - fn annotate<'i, 'o, F>(f: F) -> F - where - F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, - { - f - } - - let res = compile_bundle( - &fs, - &config, - visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), - ); - - match res { - Ok(res) => res.into_js(*ctx.env), - Err(err) => Err(err.into_js_error(*ctx.env, None)?), - } - } - - // A SourceProvider which calls JavaScript functions to resolve and read files. - struct JsSourceProvider { - resolve: Option>, - read: Option>, - inputs: Mutex<*mut String>>, - } - - unsafe impl Sync for JsSourceProvider {} - unsafe impl Send for JsSourceProvider {} - - // Allocate a single channel per thread to communicate with the JS thread. - thread_local! { - static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); - } - - impl SourceProvider for JsSourceProvider { - type Error = napi::Error; - - fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { - let source = if let Some(read) = &self.read { - CHANNEL.with(|channel| { - let message = ReadMessage { - file: file.to_str().unwrap().to_owned(), - tx: channel.0.clone(), - }; - - read.call(message, ThreadsafeFunctionCallMode::Blocking); - channel.1.recv().unwrap() - }) - } else { - Ok(std::fs::read_to_string(file)?) - }; - - match source { - Ok(source) => { - // cache the result - let ptr = Box::into_raw(Box::new(source)); - self.inputs.lock().unwrap().push(ptr); - // SAFETY: this is safe because the pointer is not dropped - // until the JsSourceProvider is, and we never remove from the - // list of pointers stored in the vector. - Ok(unsafe { &*ptr }) - } - Err(e) => Err(e), - } - } - - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { - if let Some(resolve) = &self.resolve { - return CHANNEL.with(|channel| { - let message = ResolveMessage { - specifier: specifier.to_owned(), - originating_file: originating_file.to_str().unwrap().to_owned(), - tx: channel.0.clone(), - }; - - 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), - } - }); - } - - Ok(originating_file.with_file_name(specifier)) - } - } - - struct ResolveMessage { - specifier: String, - originating_file: String, - tx: Sender>, - } - - struct ReadMessage { - file: String, - tx: Sender>, - } - - struct VisitMessage { - stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, - tx: Sender>, - } - - fn await_promise(env: Env, result: JsUnknown, tx: Sender>) -> napi::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()? { - let result: JsObject = result.try_into()?; - let then: JsFunction = result.get_named_property("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(); - ctx.env.get_undefined() - })?; - let eb = env.create_function_from_closure("error_callback", move |ctx| { - let res = ctx.get::(0)?; - tx2.send(Err(napi::Error::from(res))).unwrap(); - ctx.env.get_undefined() - })?; - 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(); - } - - Ok(()) - } - - fn resolve_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { - 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) - } - - fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { - match res { - Ok(_) => Ok(()), - Err(e) => { - tx.send(Err(e)).expect("send error"); - Ok(()) - } - } - } - - fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { - let tx = ctx.value.tx.clone(); - handle_error(tx, resolve_on_js_thread(ctx)) - } - - 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) - } - - fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { - let tx = ctx.value.tx.clone(); - handle_error(tx, read_on_js_thread(ctx)) - } - - #[js_function(1)] - pub fn bundle_async(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - let visitor = JsVisitor::new(*ctx.env, visitor); - Some(visitor) - } else { - None - }; - - let config: BundleConfig = ctx.env.from_js_value(&opts)?; - - if let Ok(resolver) = opts.get_named_property::("resolver") { - let read = if resolver.has_named_property("read")? { - let read = resolver.get_named_property::("read")?; - Some(ThreadsafeFunction::create( - ctx.env.raw(), - unsafe { read.raw() }, - 0, - read_on_js_thread_wrapper, - )?) - } else { - None - }; - - let resolve = if resolver.has_named_property("resolve")? { - let resolve = resolver.get_named_property::("resolve")?; - Some(ThreadsafeFunction::create( - ctx.env.raw(), - unsafe { resolve.raw() }, - 0, - resolve_on_js_thread_wrapper, - )?) - } else { - None - }; - - let provider = JsSourceProvider { - resolve, - read, - inputs: Mutex::new(Vec::new()), - }; - - run_bundle_task(provider, config, visitor, *ctx.env) - } else { - let provider = FileProvider::new(); - run_bundle_task(provider, config, visitor, *ctx.env) - } - } - - // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however, - // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile), - // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must - // run bundling from a thread not managed by Node. - fn run_bundle_task( - provider: P, - config: BundleConfig, - visitor: Option, - env: Env, - ) -> napi::Result - where - P::Error: IntoJsError, - { - let (deferred, promise) = env.create_deferred()?; - - let tsfn = if let Some(mut visitor) = visitor { - Some(ThreadsafeFunction::create( - env.raw(), - std::ptr::null_mut(), - 0, - move |ctx: ThreadSafeCallContext| { - if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) { - ctx.value.tx.send(Err(err)).expect("send error"); - return Ok(()); - } - ctx.value.tx.send(Ok(Default::default())).expect("send error"); - Ok(()) - }, - )?) - } else { - None - }; - - // Run bundling task in rayon threadpool. - rayon::spawn(move || { - let res = compile_bundle( - unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, - &config, - tsfn.map(move |tsfn| { - move |stylesheet: &mut StyleSheet| { - CHANNEL.with(|channel| { - let message = VisitMessage { - // SAFETY: we immediately lock the thread until we get a response, - // so stylesheet cannot be dropped in that time. - stylesheet: unsafe { - std::mem::transmute::< - &'_ mut StyleSheet<'_, '_, AtRule>, - &'static mut StyleSheet<'static, 'static, AtRule>, - >(stylesheet) - }, - tx: channel.0.clone(), - }; - - tsfn.call(message, ThreadsafeFunctionCallMode::Blocking); - channel.1.recv().expect("recv error").map(|_| ()) - }) - } - }), - ); - - deferred.resolve(move |env| match res { - Ok(v) => v.into_js(env), - Err(err) => Err(err.into_js_error(env, None)?), - }); - }); - - Ok(promise) - } +#[js_function(1)] +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)?; - + exports.create_named_method("bundle", bundle)?; #[cfg(not(target_arch = "wasm32"))] { - exports.create_named_method("bundle", bundle::bundle)?; - exports.create_named_method("bundleAsync", bundle::bundle_async)?; + exports.create_named_method("bundleAsync", bundle_async)?; } Ok(()) @@ -436,12 +41,15 @@ fn init(mut exports: JsObject) -> napi::Result<()> { #[cfg(target_arch = "wasm32")] #[no_mangle] -pub unsafe fn napi_register_wasm_v1(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) { - use napi::{Env, JsObject, NapiValue}; +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 exports = JsObject::from_raw_unchecked(raw_env, raw_exports); + init(exports) + } - let env = Env::from_raw(raw_env); - let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); - init(exports); + napi::bindgen_prelude::register_module_exports(register) } #[cfg(target_arch = "wasm32")] @@ -466,635 +74,3 @@ pub extern "C" fn napi_wasm_malloc(size: usize) -> *mut u8 { std::process::abort(); } - -// --------------------------------------------- - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Config { - pub filename: Option, - pub project_root: Option, - #[serde(with = "serde_bytes")] - pub code: Vec, - pub targets: Option, - #[serde(default)] - pub include: u32, - #[serde(default)] - pub exclude: u32, - pub minify: Option, - pub source_map: Option, - pub input_source_map: Option, - pub drafts: Option, - pub non_standard: Option, - pub css_modules: Option, - pub analyze_dependencies: Option, - pub pseudo_classes: Option, - pub unused_symbols: Option>, - pub error_recovery: Option, - pub custom_at_rules: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum AnalyzeDependenciesOption { - Bool(bool), - Config(AnalyzeDependenciesConfig), -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AnalyzeDependenciesConfig { - preserve_imports: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum CssModulesOption { - Bool(bool), - Config(CssModulesConfig), -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CssModulesConfig { - pattern: Option, - dashed_idents: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BundleConfig { - pub filename: String, - pub project_root: Option, - pub targets: Option, - #[serde(default)] - pub include: u32, - #[serde(default)] - pub exclude: u32, - pub minify: Option, - pub source_map: Option, - pub drafts: Option, - pub non_standard: Option, - pub css_modules: Option, - pub analyze_dependencies: Option, - pub pseudo_classes: Option, - pub unused_symbols: Option>, - pub error_recovery: Option, - pub custom_at_rules: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct OwnedPseudoClasses { - pub hover: Option, - pub active: Option, - pub focus: Option, - pub focus_visible: Option, - pub focus_within: Option, -} - -impl<'a> Into<'a>> for &'a OwnedPseudoClasses { - fn into(self) -> PseudoClasses<'a> { - PseudoClasses { - hover: self.hover.as_deref(), - active: self.active.as_deref(), - focus: self.focus.as_deref(), - focus_visible: self.focus_visible.as_deref(), - focus_within: self.focus_within.as_deref(), - } - } -} - -#[derive(Serialize, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -struct Drafts { - #[serde(default)] - nesting: bool, - #[serde(default)] - custom_media: bool, -} - -#[derive(Serialize, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -struct NonStandard { - #[serde(default)] - deep_selector_combinator: bool, -} - -fn compile<'i>( - code: &'i str, - config: &Config, - visitor: &mut Option, -) -> Result<'i>, CompileError<'i, napi::Error>> { - let drafts = config.drafts.as_ref(); - let non_standard = config.non_standard.as_ref(); - let warnings = Some(Arc::new(RwLock::new(Vec::new()))); - - let filename = config.filename.clone().unwrap_or_default(); - let project_root = config.project_root.as_ref().map(|p| p.as_ref()); - let mut source_map = if config.source_map.unwrap_or_default() { - let mut sm = SourceMap::new(project_root.unwrap_or("/")); - sm.add_source(&filename); - sm.set_source_content(0, code)?; - Some(sm) - } else { - None - }; - - let res = { - let mut flags = ParserFlags::empty(); - flags.set(ParserFlags::NESTING, matches!(drafts, Some(d) if d.nesting)); - flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); - flags.set( - ParserFlags::DEEP_SELECTOR_COMBINATOR, - matches!(non_standard, Some(v) if v.deep_selector_combinator), - ); - - let mut stylesheet = StyleSheet::parse_with( - &code, - ParserOptions { - filename: filename.clone(), - flags, - css_modules: if let Some(css_modules) = &config.css_modules { - match css_modules { - CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), - CssModulesOption::Bool(false) => None, - CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { - pattern: if let Some(pattern) = c.pattern.as_ref() { - match lightningcss::css_modules::Pattern::parse(pattern) { - Ok(p) => p, - Err(e) => return Err(CompileError::PatternError(e)), - } - } else { - Default::default() - }, - dashed_idents: c.dashed_idents.unwrap_or_default(), - }), - } - } else { - None - }, - source_index: 0, - error_recovery: config.error_recovery.unwrap_or_default(), - warnings: warnings.clone(), - }, - &mut CustomAtRuleParser { - configs: config.custom_at_rules.clone().unwrap_or_default(), - }, - )?; - - if let Some(visitor) = visitor.as_mut() { - stylesheet.visit(visitor).map_err(CompileError::JsError)?; - } - - let targets = Targets { - browsers: config.targets, - include: Features::from_bits_truncate(config.include), - exclude: Features::from_bits_truncate(config.exclude), - }; - - stylesheet.minify(MinifyOptions { - targets, - unused_symbols: config.unused_symbols.clone().unwrap_or_default(), - })?; - - stylesheet.to_css(PrinterOptions { - minify: config.minify.unwrap_or_default(), - source_map: source_map.as_mut(), - project_root, - targets, - analyze_dependencies: if let Some(d) = &config.analyze_dependencies { - match d { - AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), - AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { - remove_imports: !c.preserve_imports, - }), - _ => None, - } - } else { - None - }, - pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), - })? - }; - - let map = if let Some(mut source_map) = source_map { - if let Some(input_source_map) = &config.input_source_map { - if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) { - let _ = source_map.extends(&mut sm); - } - } - - source_map.to_json(None).ok() - } else { - None - }; - - Ok(TransformResult { - code: res.code.into_bytes(), - map: map.map(|m| m.into_bytes()), - exports: res.exports, - references: res.references, - dependencies: res.dependencies, - warnings: warnings.map_or(Vec::new(), |w| { - Arc::try_unwrap(w) - .unwrap() - .into_inner() - .unwrap() - .into_iter() - .map(|w| w.into()) - .collect() - }), - }) -} - -fn compile_bundle< - 'i, - 'o, - P: SourceProvider, - F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, ->( - fs: &'i P, - config: &'o BundleConfig, - visit: Option, -) -> Result<'i>, CompileError<'i, P::Error>> { - let project_root = config.project_root.as_ref().map(|p| p.as_ref()); - let mut source_map = if config.source_map.unwrap_or_default() { - Some(SourceMap::new(project_root.unwrap_or("/"))) - } else { - None - }; - let warnings = Some(Arc::new(RwLock::new(Vec::new()))); - - let res = { - let drafts = config.drafts.as_ref(); - let non_standard = config.non_standard.as_ref(); - let mut flags = ParserFlags::empty(); - flags.set(ParserFlags::NESTING, matches!(drafts, Some(d) if d.nesting)); - flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); - flags.set( - ParserFlags::DEEP_SELECTOR_COMBINATOR, - matches!(non_standard, Some(v) if v.deep_selector_combinator), - ); - - let parser_options = ParserOptions { - flags, - css_modules: if let Some(css_modules) = &config.css_modules { - match css_modules { - CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), - CssModulesOption::Bool(false) => None, - CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { - pattern: if let Some(pattern) = c.pattern.as_ref() { - match lightningcss::css_modules::Pattern::parse(pattern) { - Ok(p) => p, - Err(e) => return Err(CompileError::PatternError(e)), - } - } else { - Default::default() - }, - dashed_idents: c.dashed_idents.unwrap_or_default(), - }), - } - } else { - None - }, - error_recovery: config.error_recovery.unwrap_or_default(), - warnings: warnings.clone(), - filename: String::new(), - source_index: 0, - }; - - let mut at_rule_parser = CustomAtRuleParser { - configs: config.custom_at_rules.clone().unwrap_or_default(), - }; - - let mut bundler = - Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); - let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; - - if let Some(visit) = visit { - visit(&mut stylesheet).map_err(CompileError::JsError)?; - } - - let targets = Targets { - browsers: config.targets, - include: Features::from_bits_truncate(config.include), - exclude: Features::from_bits_truncate(config.exclude), - }; - - stylesheet.minify(MinifyOptions { - targets, - unused_symbols: config.unused_symbols.clone().unwrap_or_default(), - })?; - - stylesheet.to_css(PrinterOptions { - minify: config.minify.unwrap_or_default(), - source_map: source_map.as_mut(), - project_root, - targets, - analyze_dependencies: if let Some(d) = &config.analyze_dependencies { - match d { - AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), - AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { - remove_imports: !c.preserve_imports, - }), - _ => None, - } - } else { - None - }, - pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), - })? - }; - - let map = if let Some(source_map) = &mut source_map { - source_map.to_json(None).ok() - } else { - None - }; - - Ok(TransformResult { - code: res.code.into_bytes(), - map: map.map(|m| m.into_bytes()), - exports: res.exports, - references: res.references, - dependencies: res.dependencies, - warnings: warnings.map_or(Vec::new(), |w| { - Arc::try_unwrap(w) - .unwrap() - .into_inner() - .unwrap() - .into_iter() - .map(|w| w.into()) - .collect() - }), - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AttrConfig { - pub filename: Option, - #[serde(with = "serde_bytes")] - pub code: Vec, - pub targets: Option, - #[serde(default)] - pub include: u32, - #[serde(default)] - pub exclude: u32, - #[serde(default)] - pub minify: bool, - #[serde(default)] - pub analyze_dependencies: bool, - #[serde(default)] - pub error_recovery: bool, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct AttrResult<'i> { - #[serde(with = "serde_bytes")] - code: Vec, - dependencies: Option>, - warnings: Vec<'i>>, -} - -impl<'i> AttrResult<'i> { - fn into_js(self, ctx: CallContext) -> napi::Result { - // Manually construct buffers so we avoid a copy and work around - // https://github.com/napi-rs/napi-rs/issues/1124. - let mut obj = ctx.env.create_object()?; - let buf = ctx.env.create_buffer_with_data(self.code)?; - obj.set_named_property("code", buf.into_raw())?; - obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?; - obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?; - Ok(obj.into_unknown()) - } -} - -fn compile_attr<'i>( - code: &'i str, - config: &AttrConfig, - visitor: &mut Option, -) -> Result<'i>, CompileError<'i, napi::Error>> { - let warnings = if config.error_recovery { - Some(Arc::new(RwLock::new(Vec::new()))) - } else { - None - }; - let res = { - let filename = config.filename.clone().unwrap_or_default(); - let mut attr = StyleAttribute::parse( - &code, - ParserOptions { - filename, - error_recovery: config.error_recovery, - warnings: warnings.clone(), - ..ParserOptions::default() - }, - )?; - - if let Some(visitor) = visitor.as_mut() { - attr.visit(visitor).unwrap(); - } - - let targets = Targets { - browsers: config.targets, - include: Features::from_bits_truncate(config.include), - exclude: Features::from_bits_truncate(config.exclude), - }; - - attr.minify(MinifyOptions { - targets, - ..MinifyOptions::default() - }); - attr.to_css(PrinterOptions { - minify: config.minify, - source_map: None, - project_root: None, - targets, - analyze_dependencies: if config.analyze_dependencies { - Some(DependencyOptions::default()) - } else { - None - }, - pseudo_classes: None, - })? - }; - Ok(AttrResult { - code: res.code.into_bytes(), - dependencies: res.dependencies, - warnings: warnings.map_or(Vec::new(), |w| { - Arc::try_unwrap(w) - .unwrap() - .into_inner() - .unwrap() - .into_iter() - .map(|w| w.into()) - .collect() - }), - }) -} - -enum CompileError<'i, E: std::error::Error> { - ParseError(Error<'i>>), - MinifyError(Error), - PrinterError(Error), - SourceMapError(parcel_sourcemap::SourceMapError), - BundleError(Error<'i, E>>), - PatternError(PatternParseError), - JsError(napi::Error), -} - -impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - CompileError::ParseError(err) => err.kind.fmt(f), - CompileError::MinifyError(err) => err.kind.fmt(f), - CompileError::PrinterError(err) => err.kind.fmt(f), - CompileError::BundleError(err) => err.kind.fmt(f), - CompileError::PatternError(err) => err.fmt(f), - CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this - CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f), - } - } -} - -impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> { - fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result { - let reason = self.to_string(); - let data = match &self { - CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?, - CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?, - CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?, - CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?, - _ => env.get_null()?.into_unknown(), - }; - - let (js_error, loc) = match self { - CompileError::BundleError(Error { - loc, - kind: BundleErrorKind::ResolverError(e), - }) => { - // Add location info to existing JS error if available. - (e.into_js_error(env)?, loc) - } - CompileError::ParseError(Error { loc, .. }) - | CompileError::PrinterError(Error { loc, .. }) - | CompileError::MinifyError(Error { loc, .. }) - | CompileError::BundleError(Error { loc, .. }) => { - // Generate an error with location information. - let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; - let reason = env.create_string_from_std(reason)?; - let obj = syntax_error.new_instance(&[reason])?; - (obj.into_unknown(), loc) - } - _ => return Ok(self.into()), - }; - - if js_error.get_type()? == napi::ValueType::Object { - let mut obj: JsObject = unsafe { js_error.cast() }; - if let Some(loc) = loc { - let line = env.create_int32((loc.line + 1) as i32)?; - let col = env.create_int32(loc.column as i32)?; - let filename = env.create_string_from_std(loc.filename)?; - obj.set_named_property("fileName", filename)?; - if let Some(code) = code { - let source = env.create_string(code)?; - obj.set_named_property("source", source)?; - } - let mut loc = env.create_object()?; - loc.set_named_property("line", line)?; - loc.set_named_property("column", col)?; - obj.set_named_property("loc", loc)?; - } - obj.set_named_property("data", data)?; - Ok(obj.into_unknown().into()) - } else { - Ok(js_error.into()) - } - } -} - -trait IntoJsError { - fn into_js_error(self, env: Env) -> napi::Result; -} - -impl IntoJsError for std::io::Error { - fn into_js_error(self, env: Env) -> napi::Result { - let reason = self.to_string(); - let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; - let reason = env.create_string_from_std(reason)?; - let obj = syntax_error.new_instance(&[reason])?; - Ok(obj.into_unknown()) - } -} - -impl IntoJsError for napi::Error { - fn into_js_error(self, env: Env) -> napi::Result { - unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) } - } -} - -impl<'i, E: std::error::Error> From<'i>>> for CompileError<'i, E> { - fn from(e: Error<'i>>) -> CompileError<'i, E> { - CompileError::ParseError(e) - } -} - -impl<'i, E: std::error::Error> From> for CompileError<'i, E> { - fn from(err: Error) -> CompileError<'i, E> { - CompileError::MinifyError(err) - } -} - -impl<'i, E: std::error::Error> From> for CompileError<'i, E> { - fn from(err: Error) -> CompileError<'i, E> { - CompileError::PrinterError(err) - } -} - -impl<'i, E: std::error::Error> From for CompileError<'i, E> { - fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> { - CompileError::SourceMapError(e) - } -} - -impl<'i, E: std::error::Error> From<'i, E>>> for CompileError<'i, E> { - fn from(e: Error<'i, E>>) -> CompileError<'i, E> { - CompileError::BundleError(e) - } -} - -impl<'i, E: std::error::Error> From<'i, E>> for napi::Error { - fn from(e: CompileError<'i, E>) -> napi::Error { - match e { - CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()), - CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()), - CompileError::JsError(e) => e, - _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()), - } - } -} - -#[derive(Serialize)] -struct Warning<'i> { - message: String, - #[serde(flatten)] - data: ParserError<'i>, - loc: Option, -} - -impl<'i> From<'i>>> for Warning<'i> { - fn from(mut e: Error<'i>>) -> Self { - // Convert to 1-based line numbers. - if let Some(loc) = &mut e.loc { - loc.line += 1; - } - Warning { - message: e.kind.to_string(), - data: e.kind, - loc: e.loc, - } - } -} diff --git a/node/targets.d.ts b/node/targets.d.ts index c962f229f..ccc7c95f0 100644 --- a/node/targets.d.ts +++ b/node/targets.d.ts @@ -33,7 +33,8 @@ export const Features: { DoublePositionGradients: 131072, VendorPrefixes: 262144, LogicalProperties: 524288, + LightDark: 1048576, Selectors: 31, MediaQueries: 448, - Colors: 64512, + Colors: 1113088, }; diff --git a/node/test/bundle.test.mjs b/node/test/bundle.test.mjs index d42979c13..4279e51c8 100644 --- a/node/test/bundle.test.mjs +++ b/node/test/bundle.test.mjs @@ -1,8 +1,31 @@ import path from 'path'; import fs from 'fs'; -import { bundleAsync } from '../index.mjs'; import { test } from 'uvu'; import * as assert from 'uvu/assert'; +import {webcrypto as crypto} from 'node:crypto'; + +let bundleAsync; +if (process.env.TEST_WASM === 'node') { + bundleAsync = (await import('../../wasm/wasm-node.mjs')).bundleAsync; +} else if (process.env.TEST_WASM === 'browser') { + // Define crypto globally for old node. + // @ts-ignore + globalThis.crypto ??= crypto; + let wasm = await import('../../wasm/index.mjs'); + await wasm.default(); + bundleAsync = function (options) { + if (!options.resolver?.read) { + options.resolver = { + ...options.resolver, + read: (filePath) => fs.readFileSync(filePath, 'utf8') + }; + } + + return wasm.bundleAsync(options); + } +} else { + bundleAsync = (await import('../index.mjs')).bundleAsync; +} test('resolver', async () => { const inMemoryFs = new Map(Object.entries({ @@ -342,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, @@ -391,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 f656c53e9..4379cf481 100644 --- a/node/test/composeVisitors.test.mjs +++ b/node/test/composeVisitors.test.mjs @@ -2,7 +2,21 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { transform, composeVisitors } from '../index.mjs'; +import {webcrypto as crypto} from 'node:crypto'; + +let transform, composeVisitors; +if (process.env.TEST_WASM === 'node') { + ({transform, composeVisitors} = await import('../../wasm/wasm-node.mjs')); +} else if (process.env.TEST_WASM === 'browser') { + // Define crypto globally for old node. + // @ts-ignore + globalThis.crypto ??= crypto; + let wasm = await import('../../wasm/index.mjs'); + await wasm.default(); + ({transform, composeVisitors} = wasm); +} else { + ({transform, composeVisitors} = await import('../index.mjs')); +} test('different types', () => { let res = transform({ @@ -499,6 +513,87 @@ test('unknown rules', () => { assert.equal(res.code.toString(), '.menu_link{background:#056ef0}'); }); +test('custom at rules', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @testA; + @testB; + `), + customAtRules: { + testA: {}, + testB: {} + }, + visitor: composeVisitors([ + { + Rule: { + custom: { + testA(rule) { + return { + type: 'style', + value: { + loc: rule.loc, + selectors: [ + [{ type: 'class', name: 'testA' }] + ], + declarations: { + declarations: [ + { + property: 'color', + value: { + type: 'rgb', + r: 0xff, + g: 0x00, + b: 0x00, + alpha: 1, + } + } + ] + } + } + }; + } + } + } + }, + { + Rule: { + custom: { + testB(rule) { + return { + type: 'style', + value: { + loc: rule.loc, + selectors: [ + [{ type: 'class', name: 'testB' }] + ], + declarations: { + declarations: [ + { + property: 'color', + value: { + type: 'rgb', + r: 0x00, + g: 0xff, + b: 0x00, + alpha: 1, + } + } + ] + } + } + }; + } + } + } + } + ]) + }); + + assert.equal(res.code.toString(), '.testA{color:red}.testB{color:#0f0}'); +}); + test('known rules', () => { let declared = new Map(); let res = transform({ @@ -672,4 +767,94 @@ test('variables', () => { assert.equal(res.code.toString(), 'body{padding:20px;width:600px}'); }); +test('StyleSheet', () => { + let styleSheetCalledCount = 0; + let styleSheetExitCalledCount = 0; + transform({ + filename: 'test.css', + code: Buffer.from(` + body { + color: blue; + } + `), + visitor: composeVisitors([ + { + StyleSheet() { + styleSheetCalledCount++ + }, + StyleSheetExit() { + styleSheetExitCalledCount++ + } + }, + { + StyleSheet() { + styleSheetCalledCount++ + }, + StyleSheetExit() { + styleSheetExitCalledCount++ + } + } + ]) + }); + assert.equal(styleSheetCalledCount, 2); + 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/customAtRules.mjs b/node/test/customAtRules.mjs index d0c4ea584..e53f65ad5 100644 --- a/node/test/customAtRules.mjs +++ b/node/test/customAtRules.mjs @@ -2,7 +2,30 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { bundle, transform } from '../index.mjs'; +import fs from 'fs'; +import {webcrypto as crypto} from 'node:crypto'; + +let bundle, transform; +if (process.env.TEST_WASM === 'node') { + ({ bundle, transform } = await import('../../wasm/wasm-node.mjs')); +} else if (process.env.TEST_WASM === 'browser') { + // Define crypto globally for old node. + // @ts-ignore + globalThis.crypto ??= crypto; + let wasm = await import('../../wasm/index.mjs'); + await wasm.default(); + transform = wasm.transform; + bundle = function(options) { + return wasm.bundle({ + ...options, + resolver: { + read: (filePath) => fs.readFileSync(filePath, 'utf8') + } + }); + } +} else { + ({bundle, transform} = await import('../index.mjs')); +} test('declaration list', () => { let definitions = new Map(); @@ -69,7 +92,6 @@ test('mixin', () => { @apply color; } `), - drafts: { nesting: true }, targets: { chrome: 100 << 16 }, customAtRules: { mixin: { @@ -152,9 +174,6 @@ test('style block', () => { } } `), - drafts: { - nesting: true - }, targets: { chrome: 105 << 16 }, @@ -198,9 +217,6 @@ test('style block top level', () => { } } `), - drafts: { - nesting: true - }, customAtRules: { test: { body: 'style-block' @@ -270,7 +286,6 @@ test('bundler', () => { let res = bundle({ filename: 'tests/testdata/apply.css', minify: true, - drafts: { nesting: true }, targets: { chrome: 100 << 16 }, customAtRules: { mixin: { diff --git a/node/test/transform.test.mjs b/node/test/transform.test.mjs index 2eb1d1aed..1b56bbc18 100644 --- a/node/test/transform.test.mjs +++ b/node/test/transform.test.mjs @@ -1,6 +1,20 @@ -import { transform, Features } from 'lightningcss'; import { test } from 'uvu'; import * as assert from 'uvu/assert'; +import {webcrypto as crypto} from 'node:crypto'; + +let transform, Features; +if (process.env.TEST_WASM === 'node') { + ({transform, Features} = await import('../../wasm/wasm-node.mjs')); +} else if (process.env.TEST_WASM === 'browser') { + // Define crypto globally for old node. + // @ts-ignore + globalThis.crypto ??= crypto; + let wasm = await import('../../wasm/index.mjs'); + await wasm.default(); + ({transform, Features} = wasm); +} else { + ({transform, Features} = await import('../index.mjs')); +} test('can enable non-standard syntax', () => { let res = transform({ @@ -20,9 +34,6 @@ test('can enable features without targets', () => { filename: 'test.css', code: Buffer.from('.foo { .bar { color: red }}'), minify: true, - drafts: { - nesting: true - }, include: Features.Nesting }); @@ -56,3 +67,5 @@ test('can disable prefixing', () => { assert.equal(res.code.toString(), '.foo{user-select:none}'); }); + +test.run(); diff --git a/node/test/visitor.test.mjs b/node/test/visitor.test.mjs index 312e98eb3..149825b7d 100644 --- a/node/test/visitor.test.mjs +++ b/node/test/visitor.test.mjs @@ -2,7 +2,41 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { bundle, bundleAsync, transform, transformStyleAttribute } from '../index.mjs'; +import fs from 'fs'; +import {webcrypto as crypto} from 'node:crypto'; + +let bundle, bundleAsync, transform, transformStyleAttribute; +if (process.env.TEST_WASM === 'node') { + ({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../../wasm/wasm-node.mjs')); +} else if (process.env.TEST_WASM === 'browser') { + // Define crypto globally for old node. + // @ts-ignore + globalThis.crypto ??= crypto; + let wasm = await import('../../wasm/index.mjs'); + await wasm.default(); + ({ transform, transformStyleAttribute } = wasm); + bundle = function(options) { + return wasm.bundle({ + ...options, + resolver: { + read: (filePath) => fs.readFileSync(filePath, 'utf8') + } + }); + } + + bundleAsync = function (options) { + if (!options.resolver?.read) { + options.resolver = { + ...options.resolver, + read: (filePath) => fs.readFileSync(filePath, 'utf8') + }; + } + + return wasm.bundleAsync(options); + } +} else { + ({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../index.mjs')); +} test('px to rem', () => { // Similar to https://github.com/cuth/postcss-pxtorem @@ -215,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({ @@ -295,9 +391,6 @@ test('apply', () => { let res = transform({ filename: 'test.css', minify: true, - drafts: { - nesting: true - }, code: Buffer.from(` --toolbar-theme { color: white; @@ -1053,4 +1146,143 @@ test('media query raw', () => { assert.equal(res.code.toString(), '.m-1{margin:10px}@media (width>=500px){.sm\\:m-1{margin:10px}}'); }); +test('visit stylesheet', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + .foo { + width: 32px; + } + + .bar { + width: 80px; + } + `), + visitor: { + StyleSheetExit(stylesheet) { + stylesheet.rules.sort((a, b) => a.value.selectors[0][0].name.localeCompare(b.value.selectors[0][0].name)); + return 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/node/tsconfig.json b/node/tsconfig.json new file mode 100644 index 000000000..9b82eaa4a --- /dev/null +++ b/node/tsconfig.json @@ -0,0 +1,10 @@ +{ + "include": ["*.d.ts"], + "compilerOptions": { + "lib": ["ES2020"], + "moduleResolution": "node", + "isolatedModules": true, + "noEmit": true, + "strict": true + } +} diff --git a/package.json b/package.json index f6cff42f9..af17c0a58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.21.7", + "version": "1.31.1", "license": "MPL-2.0", "description": "A CSS parser, transformer, and minifier written in Rust", "main": "node/index.js", @@ -39,21 +39,22 @@ "node/*.flow" ], "dependencies": { - "detect-libc": "^1.0.3" + "detect-libc": "^2.0.3" }, "devDependencies": { - "@babel/parser": "^7.21.4", - "@babel/traverse": "^7.21.4", + "@babel/parser": "7.21.4", + "@babel/traverse": "7.21.4", "@codemirror/lang-css": "^6.0.1", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/lint": "^6.1.0", "@codemirror/theme-one-dark": "^6.1.0", - "@mdn/browser-compat-data": "^5.2.49", + "@mdn/browser-compat-data": "~7.2.4", "@napi-rs/cli": "^2.14.0", - "autoprefixer": "^10.4.14", + "autoprefixer": "^10.4.23", + "caniuse-lite": "^1.0.30001765", "codemirror": "^6.0.1", - "cssnano": "^5.0.8", - "esbuild": "^0.13.10", + "cssnano": "^7.0.6", + "esbuild": "^0.19.8", "flowgen": "^1.21.0", "jest-diff": "^27.4.2", "json-schema-to-typescript": "^11.0.2", @@ -72,7 +73,8 @@ "process": "^0.11.10", "puppeteer": "^12.0.1", "recast": "^0.22.0", - "sharp": "^0.31.1", + "sharp": "^0.33.5", + "typescript": "^5.7.2", "util": "^0.12.4", "uvu": "^0.5.6" }, @@ -84,11 +86,12 @@ "build": "node scripts/build.js && node scripts/build-flow.js", "build-release": "node scripts/build.js --release && node scripts/build-flow.js", "prepublishOnly": "node scripts/build-flow.js", - "wasm:build": "cargo build --target wasm32-unknown-unknown -p lightningcss_node && cp target/wasm32-unknown-unknown/debug/lightningcss_node.wasm wasm/. && node scripts/build-wasm.js", - "wasm:build-release": "cargo build --target wasm32-unknown-unknown -p lightningcss_node --release && cp target/wasm32-unknown-unknown/release/lightningcss_node.wasm wasm/. && node scripts/build-wasm.js", + "wasm:build": "cargo build --target wasm32-unknown-unknown -p lightningcss_node && wasm-opt target/wasm32-unknown-unknown/debug/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js", + "wasm:build-release": "cargo build --target wasm32-unknown-unknown -p lightningcss_node --release && wasm-opt target/wasm32-unknown-unknown/release/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js", "website:start": "parcel 'website/*.html' website/playground/index.html", "website:build": "yarn wasm:build-release && parcel build 'website/*.html' website/playground/index.html", "build-ast": "cargo run --example schema --features jsonschema && node scripts/build-ast.js", + "tsc": "tsc -p node/tsconfig.json", "test": "uvu node/test" } } diff --git a/patches/@babel+types+7.21.4.patch b/patches/@babel+types+7.26.3.patch similarity index 89% rename from patches/@babel+types+7.21.4.patch rename to patches/@babel+types+7.26.3.patch index 45b21d586..e672fb01f 100644 --- a/patches/@babel+types+7.21.4.patch +++ b/patches/@babel+types+7.26.3.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js -index 19903eb..6bc04a8 100644 +index 31feb1e..a64b83d 100644 --- a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js +++ b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js -@@ -59,6 +59,13 @@ getBindingIdentifiers.keys = { +@@ -66,6 +66,13 @@ const keys = { InterfaceDeclaration: ["id"], TypeAlias: ["id"], OpaqueType: ["id"], diff --git a/patches/json-schema-to-typescript+11.0.2.patch b/patches/json-schema-to-typescript+11.0.5.patch similarity index 99% rename from patches/json-schema-to-typescript+11.0.2.patch rename to patches/json-schema-to-typescript+11.0.5.patch index 37b111733..b1d06ba68 100644 --- a/patches/json-schema-to-typescript+11.0.2.patch +++ b/patches/json-schema-to-typescript+11.0.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/json-schema-to-typescript/dist/src/parser.js b/node_modules/json-schema-to-typescript/dist/src/parser.js -index aec32ab..aafd1b5 100644 +index fa9d2e4..3f65449 100644 --- a/node_modules/json-schema-to-typescript/dist/src/parser.js +++ b/node_modules/json-schema-to-typescript/dist/src/parser.js @@ -1,6 +1,6 @@ diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f15cd1c92..1a2165581 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.70.0" +channel = "1.92.0" components = ["rustfmt", "clippy"] diff --git a/scripts/build-ast.js b/scripts/build-ast.js index 1cd32f8f6..883aa442d 100644 --- a/scripts/build-ast.js +++ b/scripts/build-ast.js @@ -40,6 +40,63 @@ compileFromFile('node/ast.json', { Program(path) { process(path.scope.getBinding('Declaration')); process(path.scope.getBinding('MediaQuery')); + }, + TSInterfaceDeclaration(path) { + // Dedupe. + if (path.node.id.name.startsWith('GenericBorderFor_LineStyleAnd_')) { + if (path.node.id.name.endsWith('_0')) { + path.node.id.name = 'GenericBorderFor_LineStyle'; + } else { + path.remove(); + } + } + }, + ReferencedIdentifier(path) { + if (path.node.name.startsWith('GenericBorderFor_LineStyleAnd_')) { + path.node.name = 'GenericBorderFor_LineStyle'; + } + }, + TSTypeAliasDeclaration(path) { + // Workaround for schemars not supporting untagged variants. + // https://github.com/GREsau/schemars/issues/222 + if ( + (path.node.id.name === 'Translate' || path.node.id.name === 'Scale') && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[1].members[0].key.name === 'xyz' + ) { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } else if (path.node.id.name === 'AnimationAttachmentRange' && path.node.typeAnnotation.type === 'TSUnionType') { + let types = path.node.typeAnnotation.types; + if (types[1].type === 'TSTypeLiteral' && types[1].members[0].key.name === 'lengthpercentage') { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } + + if (types[2].type === 'TSTypeLiteral' && types[2].members[0].key.name === 'timelinerange') { + path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); + } + } else if ( + path.node.id.name === 'NoneOrCustomIdentList' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[1].members[0].key.name === 'idents' + ) { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } else if ( + path.node.id.name === 'ViewTransitionGroup' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[3].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[3].members[0].key.name === 'custom' + ) { + path.get('typeAnnotation.types.3').replaceWith(path.node.typeAnnotation.types[3].members[0].typeAnnotation.typeAnnotation); + } else if ( + path.node.id.name === 'ViewTransitionName' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[2].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[2].members[0].key.name === 'custom' + ) { + path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); + } } }); diff --git a/scripts/build-npm.js b/scripts/build-npm.js index dd4c91bc3..ef447a7ea 100644 --- a/scripts/build-npm.js +++ b/scripts/build-npm.js @@ -15,6 +15,9 @@ const triples = [ { name: 'x86_64-pc-windows-msvc', }, + { + name: 'aarch64-pc-windows-msvc' + }, { name: 'aarch64-apple-darwin', }, @@ -35,6 +38,9 @@ const triples = [ }, { name: 'x86_64-unknown-freebsd' + }, + { + name: 'aarch64-linux-android' } ]; const cpuToNodeArch = { @@ -48,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 da967870d..3a9f7945f 100644 --- a/scripts/build-prefixes.js +++ b/scripts/build-prefixes.js @@ -25,6 +25,7 @@ const MDN_BROWSER_MAPPING = { firefox_android: 'firefox', opera_android: 'opera', safari_ios: 'ios_saf', + webview_ios: 'ios_saf', samsunginternet_android: 'samsung', webview_android: 'android', oculus: null, @@ -70,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(); @@ -253,7 +258,7 @@ for (let feature of cssFeatures) { addValue(compat, {}, 'custom-media-queries'); let mdnFeatures = { - doublePositionGradients: mdn.css.types.image.gradient['radial-gradient'].doubleposition.__compat.support, + doublePositionGradients: mdn.css.types.gradient['radial-gradient'].doubleposition.__compat.support, clampFunction: mdn.css.types.clamp.__compat.support, placeSelf: mdn.css.properties['place-self'].__compat.support, placeContent: mdn.css.properties['place-content'].__compat.support, @@ -282,13 +287,13 @@ let mdnFeatures = { logicalPaddingShorthand: mdn.css.properties['padding-inline'].__compat.support, logicalInset: mdn.css.properties['inset-inline-start'].__compat.support, logicalSize: mdn.css.properties['inline-size'].__compat.support, - logicalTextAlign: mdn.css.properties['text-align']['flow_relative_values_start_and_end'].__compat.support, + logicalTextAlign: mdn.css.properties['text-align'].start.__compat.support, labColors: mdn.css.types.color.lab.__compat.support, oklabColors: mdn.css.types.color.oklab.__compat.support, colorFunction: mdn.css.types.color.color.__compat.support, spaceSeparatedColorNotation: mdn.css.types.color.rgb.space_separated_parameters.__compat.support, textDecorationThicknessPercent: mdn.css.properties['text-decoration-thickness'].percentage.__compat.support, - textDecorationThicknessShorthand: mdn.css.properties['text-decoration']['text-decoration-thickness'].__compat.support, + textDecorationThicknessShorthand: mdn.css.properties['text-decoration'].includes_thickness.__compat.support, cue: mdn.css.selectors.cue.__compat.support, cueFunction: mdn.css.selectors.cue.selector_argument.__compat.support, anyPseudo: Object.fromEntries( @@ -319,7 +324,7 @@ let mdnFeatures = { absFunction: mdn.css.types.abs.__compat.support, signFunction: mdn.css.types.sign.__compat.support, hypotFunction: mdn.css.types.hypot.__compat.support, - gradientInterpolationHints: mdn.css.types.image.gradient['linear-gradient'].interpolation_hints.__compat.support, + gradientInterpolationHints: mdn.css.types.gradient['linear-gradient'].interpolation_hints.__compat.support, borderImageRepeatRound: mdn.css.properties['border-image-repeat'].round.__compat.support, borderImageRepeatSpace: mdn.css.properties['border-image-repeat'].space.__compat.support, fontSizeRem: mdn.css.properties['font-size'].rem_values.__compat.support, @@ -327,6 +332,31 @@ let mdnFeatures = { fontStyleObliqueAngle: mdn.css.properties['font-style']['oblique-angle'].__compat.support, fontWeightNumber: mdn.css.properties['font-weight'].number.__compat.support, fontStretchPercentage: mdn.css.properties['font-stretch'].percentage.__compat.support, + lightDark: mdn.css.types.color['light-dark'].__compat.support, + accentSystemColor: mdn.css.types.color['system-color'].accentcolor_accentcolortext.__compat.support, + animationTimelineShorthand: mdn.css.properties.animation['animation-timeline_included'].__compat.support, + viewTransition: mdn.css.selectors['view-transition'].__compat.support, + detailsContent: mdn.css.selectors['details-content'].__compat.support, + targetText: mdn.css.selectors['target-text'].__compat.support, + 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) { @@ -341,13 +371,13 @@ for (let key in mdn.css.types.length) { mdnFeatures[feat] = mdn.css.types.length[key].__compat.support; } -for (let key in mdn.css.types.image.gradient) { +for (let key in mdn.css.types.gradient) { if (key === '__compat') { continue; } let feat = key.replace(/-([a-z])/g, (_, l) => l.toUpperCase()); - mdnFeatures[feat] = mdn.css.types.image.gradient[key].__compat.support; + mdnFeatures[feat] = mdn.css.types.gradient[key].__compat.support; } const nonStandardListStyleType = new Set([ @@ -374,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; } @@ -382,6 +412,14 @@ for (let key in mdn.css.properties['width']) { mdnFeatures[feat] = mdn.css.properties['width'][key].__compat.support; } +Object.entries(mdn.css.properties.width.stretch.__compat.support) + .filter(([, v]) => v.alternative_name) + .forEach(([k, v]) => { + let name = v.alternative_name.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + 'Size'; + mdnFeatures[name] ??= {}; + mdnFeatures[name][k] = {version_added: v.version_added}; + }); + for (let feature in mdnFeatures) { let browserMap = {}; for (let name in mdnFeatures[feature]) { @@ -453,9 +491,10 @@ let flags = [ 'DoublePositionGradients', 'VendorPrefixes', 'LogicalProperties', + 'LightDark', ['Selectors', ['Nesting', 'NotSelectorList', 'DirSelector', 'LangSelectorList', 'IsSelector']], ['MediaQueries', ['MediaIntervalSyntax', 'MediaRangeSyntax', 'CustomMediaQueries']], - ['Colors', ['ColorFunction', 'OklabColors', 'LabColors', 'P3Colors', 'HexAlphaColors', 'SpaceSeparatedColorNotation']], + ['Colors', ['ColorFunction', 'OklabColors', 'LabColors', 'P3Colors', 'HexAlphaColors', 'SpaceSeparatedColorNotation', 'LightDark']], ]; let enumify = (f) => f.replace(/^@([a-z])/, (_, x) => 'At' + x.toUpperCase()).replace(/^::([a-z])/, (_, x) => 'PseudoElement' + x.toUpperCase()).replace(/^:([a-z])/, (_, x) => 'PseudoClass' + x.toUpperCase()).replace(/(^|-)([a-z])/g, (_, a, x) => x.toUpperCase()) @@ -594,6 +633,7 @@ let c = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT! use crate::targets::Browsers; +#[allow(dead_code)] #[derive(Clone, Copy, PartialEq)] pub enum Feature { ${[...compat.keys()].flat().map(enumify).sort().join(',\n ')} @@ -625,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/scripts/build-wasm.js b/scripts/build-wasm.js index 63394471b..718821da9 100644 --- a/scripts/build-wasm.js +++ b/scripts/build-wasm.js @@ -1,3 +1,4 @@ +const esbuild = require('esbuild'); const exec = require('child_process').execSync; const fs = require('fs'); const pkg = require('../package.json'); @@ -12,6 +13,10 @@ let flags = fs.readFileSync(`${dir}/node/flags.js`, 'utf8'); flags = flags.replace('exports.Features =', 'export const Features ='); fs.writeFileSync(`${dir}/wasm/flags.js`, flags); +let composeVisitors = fs.readFileSync(`${dir}/node/composeVisitors.js`, 'utf8'); +composeVisitors = composeVisitors.replace('module.exports = composeVisitors', 'export { composeVisitors }'); +fs.writeFileSync(`${dir}/wasm/composeVisitors.js`, composeVisitors); + let dts = fs.readFileSync(`${dir}/node/index.d.ts`, 'utf8'); dts = dts.replace(/: Buffer/g, ': Uint8Array'); dts += ` @@ -27,19 +32,47 @@ let readme = fs.readFileSync(`${dir}/README.md`, 'utf8'); readme = readme.replace('# ⚡️ Lightning CSS', '# ⚡️ lightningcss-wasm'); fs.writeFileSync(`${dir}/wasm/README.md`, readme); +const cjsBuild = { + entryPoints: [ + `${dir}/wasm/wasm-node.mjs`, + `${dir}/wasm/index.mjs`, + ], + bundle: true, + format: 'cjs', + platform: 'node', + packages: 'external', + outdir: `${dir}/wasm`, + outExtension: { '.js': '.cjs' }, + inject: [`${dir}/wasm/import.meta.url-polyfill.js`], + define: { 'import.meta.url': 'import_meta_url' }, +}; +esbuild.build(cjsBuild).catch(console.error); + let wasmPkg = { ...pkg }; wasmPkg.name = 'lightningcss-wasm'; wasmPkg.type = 'module'; wasmPkg.main = 'index.mjs'; wasmPkg.module = 'index.mjs'; wasmPkg.exports = { - types: './index.d.ts', - node: './wasm-node.mjs', - default: './index.mjs' + '.': { + types: './index.d.ts', + node: { + import: './wasm-node.mjs', + require: './wasm-node.cjs' + }, + default: { + import: './index.mjs', + require: './index.cjs' + } + }, + // Allow esbuild to import the wasm file + // without copying it in the src directory. + // Simplifies loading it in the browser when used in a library. + './lightningcss_node.wasm': './lightningcss_node.wasm' }; wasmPkg.types = 'index.d.ts'; wasmPkg.sideEffects = false; -wasmPkg.files = ['*.js', '*.mjs', '*.d.ts', '*.flow', '*.wasm']; +wasmPkg.files = ['*.js', '*.cjs', '*.mjs', '*.d.ts', '*.flow', '*.wasm']; wasmPkg.dependencies = { 'napi-wasm': pkg.devDependencies['napi-wasm'] }; diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index adf1d1aaf..824b2f2bd 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parcel_selectors" -version = "0.26.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" @@ -9,6 +9,7 @@ readme = "README.md" keywords = ["css", "selectors"] license = "MPL-2.0" build = "build.rs" +edition = "2021" [lib] name = "parcel_selectors" @@ -17,17 +18,21 @@ path = "lib.rs" [features] bench = [] jsonschema = ["serde", "schemars"] +into_owned = ["static-self"] +smallvec = ["static-self/smallvec"] +serde = ["dep:serde", "smallvec/serde"] [dependencies] bitflags = "2.2.1" -cssparser = "0.29" -fxhash = "0.2" +cssparser = "0.33.0" +rustc-hash = "2" log = "0.4" -phf = "0.10" +phf = "0.11.2" precomputed-hash = "0.1" smallvec = "1.0" -serde = { version = "1.0.123", features = ["derive"], optional = true } -schemars = { version = "0.8.11", features = ["smallvec"], optional = true } +serde = { version = "1.0.201", features = ["derive"], optional = true } +schemars = { version = "0.8.19", features = ["smallvec"], optional = true } +static-self = { version = "0.1.2", path = "../static-self", optional = true } [build-dependencies] -phf_codegen = "0.10" +phf_codegen = "0.11" diff --git a/selectors/attr.rs b/selectors/attr.rs index 5eda9dfa5..0112b0ffa 100644 --- a/selectors/attr.rs +++ b/selectors/attr.rs @@ -15,6 +15,30 @@ pub struct AttrSelectorWithOptionalNamespace<'i, Impl: SelectorImpl<'i>> { pub never_matches: bool, } +#[cfg(feature = "into_owned")] +impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> + for AttrSelectorWithOptionalNamespace<'i, Impl> +where + Impl: static_self::IntoOwned<'any, Owned = NewSel>, + NewSel: SelectorImpl<'any>, + Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>, + Impl::NamespacePrefix: static_self::IntoOwned<'any, Owned = NewSel::NamespacePrefix>, + Impl::NamespaceUrl: static_self::IntoOwned<'any, Owned = NewSel::NamespaceUrl>, + Impl::AttrValue: static_self::IntoOwned<'any, Owned = NewSel::AttrValue>, +{ + type Owned = AttrSelectorWithOptionalNamespace<'any, NewSel>; + + fn into_owned(self) -> Self::Owned { + AttrSelectorWithOptionalNamespace { + namespace: self.namespace.into_owned(), + local_name: self.local_name.into_owned(), + local_name_lower: self.local_name_lower.into_owned(), + operation: self.operation.into_owned(), + never_matches: self.never_matches, + } + } +} + impl<'i, Impl: SelectorImpl<'i>> AttrSelectorWithOptionalNamespace<'i, Impl> { pub fn namespace(&self) -> Option<&Impl::NamespaceUrl>> { self.namespace.as_ref().map(|ns| match ns { @@ -35,6 +59,7 @@ impl<'i, Impl: SelectorImpl<'i>> AttrSelectorWithOptionalNamespace<'i, Impl> { derive(schemars::JsonSchema), schemars(rename = "NamespaceConstraint") )] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum NamespaceConstraint { Any, @@ -43,6 +68,7 @@ pub enum NamespaceConstraint { } #[derive(Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum ParsedAttrSelectorOperation { Exists, WithValue { @@ -84,6 +110,7 @@ impl AttrSelectorOperation { serde(rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum AttrSelectorOperator { Equal, Includes, @@ -146,6 +173,7 @@ pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C']; serde(rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum ParsedCaseSensitivity { // 's' was specified. ExplicitCaseSensitive, diff --git a/selectors/bloom.rs b/selectors/bloom.rs index bbfbee45b..e9d2f7307 100644 --- a/selectors/bloom.rs +++ b/selectors/bloom.rs @@ -283,7 +283,7 @@ fn hash2(hash: u32) -> u32 { #[test] fn create_and_insert_some_stuff() { - use fxhash::FxHasher; + use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; use std::mem::transmute; diff --git a/selectors/build.rs b/selectors/build.rs index 945bb9bb6..787e2d80d 100644 --- a/selectors/build.rs +++ b/selectors/build.rs @@ -2,8 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -extern crate phf_codegen; - use std::env; use std::fs::File; use std::io::{BufWriter, Write}; diff --git a/selectors/lib.rs b/selectors/lib.rs index ed0a35630..2047b4e61 100644 --- a/selectors/lib.rs +++ b/selectors/lib.rs @@ -9,16 +9,8 @@ extern crate bitflags; #[macro_use] extern crate cssparser; -extern crate fxhash; #[macro_use] extern crate log; -extern crate phf; -extern crate precomputed_hash; -#[cfg(feature = "jsonschema")] -extern crate schemars; -#[cfg(feature = "serde")] -extern crate serde; -extern crate smallvec; pub mod attr; pub mod bloom; diff --git a/selectors/matching.rs b/selectors/matching.rs index ce3d7a59f..61f74a85c 100644 --- a/selectors/matching.rs +++ b/selectors/matching.rs @@ -60,7 +60,7 @@ impl ElementSelectorFlags { } /// Holds per-compound-selector data. -struct LocalMatchingContext<'a, 'b: 'a, 'i, Impl: SelectorImpl<'i>> { +struct LocalMatchingContext<'a, 'b, 'i, Impl: SelectorImpl<'i>> { shared: &'a mut MatchingContext<'b, 'i, Impl>, matches_hover_and_active_quirk: MatchesHoverAndActiveQuirk, } diff --git a/selectors/nth_index_cache.rs b/selectors/nth_index_cache.rs index 2ca33e7bb..c5bb8db0a 100644 --- a/selectors/nth_index_cache.rs +++ b/selectors/nth_index_cache.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::tree::OpaqueElement; -use fxhash::FxHashMap; +use rustc_hash::FxHashMap; /// A cache to speed up matching of nth-index-like selectors. /// diff --git a/selectors/parser.rs b/selectors/parser.rs index a59442ebd..19563d5f9 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -197,7 +197,9 @@ pub enum SelectorParseErrorKind<'i> { MissingNestingPrefix, UnexpectedTokenInAttributeSelector(Token<'i>), PseudoElementExpectedIdent(Token<'i>), - UnsupportedPseudoClassOrElement(CowRcStr<'i>), + UnsupportedPseudoElement(CowRcStr<'i>), + UnsupportedPseudoClass(CowRcStr<'i>), + AmbiguousCssModuleClass(CowRcStr<'i>), UnexpectedIdent(CowRcStr<'i>), ExpectedNamespace(CowRcStr<'i>), ExpectedBarInAttr(Token<'i>), @@ -205,6 +207,7 @@ pub enum SelectorParseErrorKind<'i> { InvalidQualNameInAttr(Token<'i>), ExplicitNamespaceUnexpectedToken(Token<'i>), ClassNeedsIdent(Token<'i>), + UnexpectedSelectorAfterPseudoElement(Token<'i>), } macro_rules! with_all_bounds { @@ -310,7 +313,7 @@ pub trait Parser<'i> { location: SourceLocation, name: CowRcStr<'i>, ) -> Result<<'i>>::NonTSPseudoClass, ParseError<'i, Self::Error>> { - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_non_ts_functional_pseudo_class<'t>( @@ -318,7 +321,7 @@ pub trait Parser<'i> { name: CowRcStr<'i>, arguments: &mut CssParser<'i, 't>, ) -> Result<<'i>>::NonTSPseudoClass, ParseError<'i, Self::Error>> { - Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_pseudo_element( @@ -326,7 +329,7 @@ pub trait Parser<'i> { location: SourceLocation, name: CowRcStr<'i>, ) -> Result<<'i>>::PseudoElement, ParseError<'i, Self::Error>> { - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name))) } fn parse_functional_pseudo_element<'t>( @@ -334,7 +337,7 @@ pub trait Parser<'i> { name: CowRcStr<'i>, arguments: &mut CssParser<'i, 't>, ) -> Result<<'i>>::PseudoElement, ParseError<'i, Self::Error>> { - Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name))) } fn default_namespace(&self) -> Option<<'i>>::NamespaceUrl> { @@ -378,6 +381,20 @@ pub struct SelectorList<'i, Impl: SelectorImpl<'i>>( #[cfg_attr(feature = "serde", serde(borrow))] pub SmallVec<[Selector<'i, Impl>; 1]>, ); +#[cfg(feature = "into_owned")] +impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for SelectorList<'i, Impl> +where + Impl: static_self::IntoOwned<'any, Owned = NewSel>, + NewSel: SelectorImpl<'any>, + Component<'i, Impl>: static_self::IntoOwned<'any, Owned = Component<'any, NewSel>>, +{ + type Owned = SelectorList<'any, NewSel>; + + fn into_owned(self) -> Self::Owned { + SelectorList(self.0.into_owned()) + } +} + /// How to treat invalid selectors in a selector list. pub enum ParseErrorRecovery { /// Discard the entire selector list, this is the default behavior for @@ -405,6 +422,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { pub fn parse<'t, P>( parser: &P, input: &mut CssParser<'i, 't>, + error_recovery: ParseErrorRecovery, nesting_requirement: NestingRequirement, ) -> Result<'i, P::Error>> where @@ -414,7 +432,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { parser, input, &mut SelectorParsingState::empty(), - ParseErrorRecovery::DiscardList, + error_recovery, nesting_requirement, ) } @@ -466,6 +484,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { pub fn parse_relative<'t, P>( parser: &P, input: &mut CssParser<'i, 't>, + error_recovery: ParseErrorRecovery, nesting_requirement: NestingRequirement, ) -> Result<'i, P::Error>> where @@ -475,7 +494,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { parser, input, &mut SelectorParsingState::empty(), - ParseErrorRecovery::DiscardList, + error_recovery, nesting_requirement, ) } @@ -694,6 +713,20 @@ pub fn namespace_empty_string<'i, Impl: SelectorImpl<'i>>() -> Impl::NamespaceUr #[derive(Clone, PartialEq, Eq, Hash)] pub struct Selector<'i, Impl: SelectorImpl<'i>>(SpecificityAndFlags, Vec<'i, Impl>>); +#[cfg(feature = "into_owned")] +impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for Selector<'i, Impl> +where + Impl: static_self::IntoOwned<'any, Owned = NewSel>, + NewSel: SelectorImpl<'any>, + Component<'i, Impl>: static_self::IntoOwned<'any, Owned = Component<'any, NewSel>>, +{ + type Owned = Selector<'any, NewSel>; + + fn into_owned(self) -> Self::Owned { + Selector(self.0, self.1.into_owned()) + } +} + impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> { #[inline] pub fn specificity(&self) -> u32 { @@ -847,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<'i, Impl>> { + 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<'i, Impl>> { + pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut<'_, Component<'i, Impl>> { self.1.iter_mut() } @@ -870,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<'i, Impl>>> { + pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev<'_, Component<'i, Impl>>> { self.1[..self.len() - offset].iter().rev() } @@ -976,7 +1009,7 @@ impl<'i, Impl: SelectorImpl<'i>> From<'i, Impl>>> for Selector<'i, } #[derive(Clone)] -pub struct SelectorIter<'a, 'i, Impl: 'a + SelectorImpl<'i>> { +pub struct SelectorIter<'a, 'i, Impl: SelectorImpl<'i>> { iter: slice::Iter<'a, Component<'i, Impl>>, next_combinator: Option, } @@ -1059,7 +1092,7 @@ impl<'a, 'i, Impl: SelectorImpl<'i>> fmt::Debug for SelectorIter<'a, 'i, Impl> { } /// An iterator over all simple selectors belonging to ancestors. -struct AncestorIter<'a, 'i, Impl: 'a + SelectorImpl<'i>>(SelectorIter<'a, 'i, Impl>); +struct AncestorIter<'a, 'i, Impl: SelectorImpl<'i>>(SelectorIter<'a, 'i, Impl>); impl<'a, 'i, Impl: 'a + SelectorImpl<'i>> AncestorIter<'a, 'i, Impl> { /// Creates an AncestorIter. The passed-in iterator is assumed to point to /// the beginning of the child sequence, which will be skipped. @@ -1115,6 +1148,7 @@ impl<'a, 'i, Impl: SelectorImpl<'i>> Iterator for AncestorIter<'a, 'i, Impl> { serde(rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum Combinator { Child, // > Descendant, // space @@ -1177,6 +1211,7 @@ impl Combinator { /// An enum for the different types of :nth- pseudoclasses #[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum NthType { Child, LastChild, @@ -1210,6 +1245,7 @@ impl NthType { /// nth-child(An+B)). /// https://www.w3.org/TR/selectors-3/#nth-child-pseudo #[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct NthSelectorData { pub ty: NthType, pub is_function: bool, @@ -1266,7 +1302,7 @@ impl NthSelectorData { /// Writes the beginning of the selector. #[inline] - fn write_start(&self, dest: &mut W, is_function: bool) -> fmt::Result { + pub fn write_start(&self, dest: &mut W, is_function: bool) -> fmt::Result { dest.write_str(match self.ty { NthType::Child if is_function => ":nth-child(", NthType::Child => ":first-child", @@ -1286,7 +1322,7 @@ impl NthSelectorData { /// Serialize (part of the CSS Syntax spec, but currently only used here). /// #[inline] - fn write_affine(&self, dest: &mut W) -> fmt::Result { + pub fn write_affine(&self, dest: &mut W) -> fmt::Result { match (self.a, self.b) { (0, 0) => dest.write_char('0'), @@ -1310,6 +1346,20 @@ impl NthSelectorData { #[derive(Clone, PartialEq, Eq, Hash)] pub struct NthOfSelectorData<'i, Impl: SelectorImpl<'i>>(NthSelectorData, Box<[Selector<'i, Impl>]>); +#[cfg(feature = "into_owned")] +impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for NthOfSelectorData<'i, Impl> +where + Impl: static_self::IntoOwned<'any, Owned = NewSel>, + NewSel: SelectorImpl<'any>, + Component<'i, Impl>: static_self::IntoOwned<'any, Owned = Component<'any, NewSel>>, +{ + type Owned = NthOfSelectorData<'any, NewSel>; + + fn into_owned(self) -> Self::Owned { + NthOfSelectorData(self.0, self.1.into_owned()) + } +} + impl<'i, Impl: SelectorImpl<'i>> NthOfSelectorData<'i, Impl> { /// Returns selector data for :nth-{,last-}{child,of-type}(An+B [of S]) #[inline] @@ -1429,6 +1479,77 @@ pub enum Component<'i, Impl: SelectorImpl<'i>> { Nesting, } +#[cfg(feature = "into_owned")] +impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for Component<'i, Impl> +where + Impl: static_self::IntoOwned<'any, Owned = NewSel>, + NewSel: SelectorImpl<'any>, + Impl::NamespaceUrl: static_self::IntoOwned<'any, Owned = NewSel::NamespaceUrl>, + Impl::NamespacePrefix: static_self::IntoOwned<'any, Owned = NewSel::NamespacePrefix>, + Impl::Identifier: static_self::IntoOwned<'any, Owned = NewSel::Identifier>, + Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>, + Impl::AttrValue: static_self::IntoOwned<'any, Owned = NewSel::AttrValue>, + Impl::NonTSPseudoClass: static_self::IntoOwned<'any, Owned = NewSel::NonTSPseudoClass>, + Impl::PseudoElement: static_self::IntoOwned<'any, Owned = NewSel::PseudoElement>, + Impl::VendorPrefix: static_self::IntoOwned<'any, Owned = NewSel::VendorPrefix>, +{ + type Owned = Component<'any, NewSel>; + + fn into_owned(self) -> Self::Owned { + match self { + Component::Combinator(c) => Component::Combinator(c.into_owned()), + Component::ExplicitAnyNamespace => Component::ExplicitAnyNamespace, + Component::ExplicitNoNamespace => Component::ExplicitNoNamespace, + Component::DefaultNamespace(c) => Component::DefaultNamespace(c.into_owned()), + Component::Namespace(a, b) => Component::Namespace(a.into_owned(), b.into_owned()), + Component::ExplicitUniversalType => Component::ExplicitUniversalType, + Component::LocalName(c) => Component::LocalName(c.into_owned()), + Component::ID(c) => Component::ID(c.into_owned()), + Component::Class(c) => Component::Class(c.into_owned()), + Component::AttributeInNoNamespaceExists { + local_name, + local_name_lower, + } => Component::AttributeInNoNamespaceExists { + local_name: local_name.into_owned(), + local_name_lower: local_name_lower.into_owned(), + }, + Component::AttributeInNoNamespace { + local_name, + operator, + value, + case_sensitivity, + never_matches, + } => { + let value = value.into_owned(); + Component::AttributeInNoNamespace { + local_name: local_name.into_owned(), + operator, + value, + case_sensitivity, + never_matches, + } + } + Component::AttributeOther(c) => Component::AttributeOther(c.into_owned()), + Component::Negation(c) => Component::Negation(c.into_owned()), + Component::Root => Component::Root, + Component::Empty => Component::Empty, + Component::Scope => Component::Scope, + Component::Nth(c) => Component::Nth(c.into_owned()), + Component::NthOf(c) => Component::NthOf(c.into_owned()), + Component::NonTSPseudoClass(c) => Component::NonTSPseudoClass(c.into_owned()), + Component::Slotted(c) => Component::Slotted(c.into_owned()), + Component::Part(c) => Component::Part(c.into_owned()), + Component::Host(c) => Component::Host(c.into_owned()), + Component::Where(c) => Component::Where(c.into_owned()), + Component::Is(c) => Component::Is(c.into_owned()), + Component::Any(a, b) => Component::Any(a.into_owned(), b.into_owned()), + Component::Has(c) => Component::Has(c.into_owned()), + Component::PseudoElement(c) => Component::PseudoElement(c.into_owned()), + Component::Nesting => Component::Nesting, + } + } +} + impl<'i, Impl: SelectorImpl<'i>> Component<'i, Impl> { /// Returns true if this is a combinator. pub fn is_combinator(&self) -> bool { @@ -1576,6 +1697,23 @@ pub struct LocalName<'i, Impl: SelectorImpl<'i>> { pub lower_name: Impl::LocalName, } +#[cfg(feature = "into_owned")] +impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for LocalName<'i, Impl> +where + Impl: static_self::IntoOwned<'any, Owned = NewSel>, + NewSel: SelectorImpl<'any>, + Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>, +{ + type Owned = LocalName<'any, NewSel>; + + fn into_owned(self) -> Self::Owned { + LocalName { + name: self.name.into_owned(), + lower_name: self.lower_name.into_owned(), + } + } +} + impl<'i, Impl: SelectorImpl<'i>> Debug for Selector<'i, Impl> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("Selector(")?; @@ -1991,18 +2129,6 @@ where input.reset(&state); } - // In the implicit nesting mode, selectors may not start with an ident or function token. - if nesting_requirement == NestingRequirement::Implicit { - let state = input.state(); - match input.next()? { - Token::Ident(..) | Token::Function(..) => { - return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingPrefix)); - } - _ => {} - } - input.reset(&state); - } - let mut builder = SelectorBuilder::default(); 'outer_loop: loop { @@ -2017,6 +2143,14 @@ where } if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + // Input should be exhausted here. + let source_location = input.current_source_location(); + if let Ok(next) = input.next() { + let next = next.clone(); + return Err( + source_location.new_custom_error(SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(next)), + ); + } break; } @@ -2832,6 +2966,7 @@ where Impl: SelectorImpl<'i>, { let start = input.state(); + let token_location = input.current_source_location(); let token = match input.next_including_whitespace().map(|t| t.clone()) { Ok(t) => t, Err(..) => { @@ -2843,14 +2978,18 @@ where Ok(Some(match token { Token::IDHash(id) => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { - return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + return Err(token_location.new_custom_error( + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::IDHash(id)), + )); } let id = Component::ID(id.into()); SimpleSelectorParseResult::SimpleSelector(id) } Token::Delim('.') => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { - return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + return Err(token_location.new_custom_error( + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::Delim('.')), + )); } let location = input.current_source_location(); let class = match *input.next_including_whitespace()? { @@ -2865,7 +3004,9 @@ where } Token::SquareBracketBlock => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { - return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + return Err(token_location.new_custom_error( + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::SquareBracketBlock), + )); } let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?; SimpleSelectorParseResult::SimpleSelector(attr) @@ -3213,7 +3354,7 @@ pub mod tests { "active" => return Ok(PseudoClass::Active), _ => {} } - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_non_ts_functional_pseudo_class<'t>( @@ -3228,7 +3369,7 @@ pub mod tests { }, _ => {} } - Err(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_pseudo_element( @@ -3241,7 +3382,7 @@ pub mod tests { "after" => return Ok(PseudoElement::After), _ => {} } - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name))) } fn default_namespace(&self) -> Option { @@ -3253,7 +3394,7 @@ pub mod tests { } } - fn parse<'i>(input: &'i str) -> Result, SelectorParseError<'i>> { + fn parse<'i>(input: &'i str) -> Result<'i, DummySelectorImpl>, SelectorParseError<'i>> { parse_ns(input, &DummyParser::default()) } @@ -3277,7 +3418,12 @@ pub mod tests { expected: Option<&'a str>, ) -> Result<'i, DummySelectorImpl>, SelectorParseError<'i>> { let mut parser_input = ParserInput::new(input); - let result = SelectorList::parse(parser, &mut CssParser::new(&mut parser_input), NestingRequirement::None); + let result = SelectorList::parse( + parser, + &mut CssParser::new(&mut parser_input), + ParseErrorRecovery::DiscardList, + NestingRequirement::None, + ); if let Ok(ref selectors) = result { assert_eq!(selectors.0.len(), 1); // We can't assume that the serialized parsed selector will equal @@ -3304,6 +3450,7 @@ pub mod tests { let list = SelectorList::parse( &DummyParser::default(), &mut CssParser::new(&mut input), + ParseErrorRecovery::DiscardList, NestingRequirement::None, ); assert!(list.is_ok()); @@ -3782,6 +3929,20 @@ pub mod tests { assert!(parse("foo:where()").is_err()); assert!(parse("foo:where(div, foo, .bar baz)").is_ok()); assert!(parse("foo:where(::before)").is_err()); + + assert!(parse("foo::details-content").is_ok()); + assert!(parse("foo::target-text").is_ok()); + + 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 a7d24083b..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<'i, 'o, T>>, - 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<'a, T::AtRule>> = Vec::new(); - self.inline(&mut rules); + self.inline(&mut rules)?; let sources = self .stylesheets @@ -293,6 +332,23 @@ where .flat_map(|s| s.stylesheet.as_ref().unwrap().license_comments.iter().cloned()) .collect(); + if let Some(config) = &self.options.css_modules { + if config.pattern.has_content_hash() { + stylesheet.content_hashes = Some( + self + .stylesheets + .get_mut() + .unwrap() + .iter() + .flat_map(|s| { + let s = s.stylesheet.as_ref().unwrap(); + s.content_hashes.as_ref().unwrap().iter().cloned() + }) + .collect(), + ); + } + } + Ok(stylesheet) } @@ -411,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() @@ -467,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( @@ -563,7 +622,7 @@ where ) -> Option<'a, P::Error>>>> { 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 { @@ -585,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( @@ -629,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. @@ -642,14 +710,16 @@ where } } - fn inline(&mut self, dest: &mut Vec<'a, T::AtRule>>) { - process(self.stylesheets.get_mut().unwrap(), 0, dest); - - fn process<'a, T>( + fn inline( + &mut self, + dest: &mut Vec<'a, T::AtRule>>, + ) -> Result<(), Error<'a, P::Error>>> { + fn process<'a, T, E: std::error::Error>( stylesheets: &mut Vec<'a, '_, T>>, source_index: u32, dest: &mut Vec<'a, T>>, - ) { + filename: &String, + ) -> Result<(), Error<'a, E>>> { let stylesheet = &mut stylesheets[source_index as usize]; let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0); @@ -661,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(_) => { @@ -722,7 +813,10 @@ where } dest.extend(rules); + Ok(()) } + + process(self.stylesheets.get_mut().unwrap(), 0, dest, &self.options.filename) } } @@ -766,6 +860,10 @@ fn visit_vars<'a, 'b>( UnresolvedColor::RGB { alpha, .. } | UnresolvedColor::HSL { alpha, .. } => { stack.push(alpha.0.iter_mut()); } + UnresolvedColor::LightDark { light, dark } => { + stack.push(light.0.iter_mut()); + stack.push(dark.0.iter_mut()); + } }, None => { stack.pop(); @@ -802,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()) + } } } @@ -822,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, @@ -862,6 +964,15 @@ mod tests { fs: P, entry: &str, project_root: Option<&str>, + ) -> (String, CssModuleExports) { + bundle_css_module_with_pattern(fs, entry, project_root, "[hash]_[local]") + } + + fn bundle_css_module_with_pattern( + fs: P, + entry: &str, + project_root: Option<&str>, + pattern: &'static str, ) -> (String, CssModuleExports) { let mut bundler = Bundler::new( &fs, @@ -869,6 +980,7 @@ mod tests { ParserOptions { css_modules: Some(css_modules::Config { dashed_idents: true, + pattern: css_modules::Pattern::parse(pattern).unwrap(), ..Default::default() }), ..ParserOptions::default() @@ -1517,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! { @@ -1974,6 +2129,35 @@ mod tests { Some("/x/y/z"), ); assert_eq!(code, expected); + + let (code, _) = bundle_css_module_with_pattern( + TestProvider { + map: fs! { + "/a.css": r#" + @import "b.css"; + .a { color: red } + "#, + "/b.css": r#" + .a { color: green } + "# + }, + }, + "/a.css", + None, + "[content-hash]-[local]", + ); + assert_eq!( + code, + indoc! { r#" + .do5n2W-a { + color: green; + } + + .pP97eq-a { + color: red; + } + "#} + ); } #[test] diff --git a/src/compat.rs b/src/compat.rs index 19b95dc13..f0c828d75 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -2,15 +2,25 @@ use crate::targets::Browsers; +#[allow(dead_code)] #[derive(Clone, Copy, PartialEq)] pub enum Feature { AbsFunction, + AccentSystemColor, + AfarListStyleType, + AmharicAbegedeListStyleType, + AmharicListStyleType, + AnchorSizeSize, + AnimationTimelineShorthand, AnyLink, AnyPseudo, ArabicIndicListStyleType, ArmenianListStyleType, + AsterisksListStyleType, + AutoSize, Autofill, BengaliListStyleType, + BinaryListStyleType, BorderImageRepeatRound, BorderImageRepeatSpace, CalcFunction, @@ -18,6 +28,7 @@ pub enum Feature { CapUnit, CaseInsensitive, ChUnit, + Checkmark, CircleListStyleType, CjkDecimalListStyleType, CjkEarthlyBranchListStyleType, @@ -32,6 +43,7 @@ pub enum Feature { DecimalLeadingZeroListStyleType, DecimalListStyleType, DefaultPseudo, + DetailsContent, DevanagariListStyleType, Dialog, DirSelector, @@ -39,10 +51,24 @@ pub enum Feature { DisclosureClosedListStyleType, DisclosureOpenListStyleType, DoublePositionGradients, + EmUnit, + EthiopicAbegedeAmEtListStyleType, + EthiopicAbegedeGezListStyleType, + EthiopicAbegedeListStyleType, + EthiopicAbegedeTiErListStyleType, + EthiopicAbegedeTiEtListStyleType, + EthiopicHalehameAaErListStyleType, + EthiopicHalehameAaEtListStyleType, + EthiopicHalehameAmEtListStyleType, + EthiopicHalehameGezListStyleType, + EthiopicHalehameOmEtListStyleType, + EthiopicHalehameSidEtListStyleType, + EthiopicHalehameSoEtListStyleType, + EthiopicHalehameTigListStyleType, + EthiopicListStyleType, EthiopicNumericListStyleType, ExUnit, ExtendedSystemFonts, - FillSize, FirstLetter, FirstLine, FitContentFunctionSize, @@ -55,11 +81,13 @@ pub enum Feature { FontStretchPercentage, FontStyleObliqueAngle, FontWeightNumber, + FootnotesListStyleType, FormValidation, Fullscreen, Gencontent, GeorgianListStyleType, GradientInterpolationHints, + GrammarError, GujaratiListStyleType, GurmukhiListStyleType, HasSelector, @@ -86,6 +114,7 @@ pub enum Feature { LangSelectorList, LaoListStyleType, LhUnit, + LightDark, LinearGradient, LogicalBorderRadius, LogicalBorderShorthand, @@ -100,7 +129,9 @@ pub enum Feature { LowerAlphaListStyleType, LowerArmenianListStyleType, LowerGreekListStyleType, + LowerHexadecimalListStyleType, LowerLatinListStyleType, + LowerNorwegianListStyleType, LowerRomanListStyleType, MalayalamListStyleType, MarkerPseudo, @@ -115,15 +146,20 @@ pub enum Feature { MyanmarListStyleType, Namespaces, Nesting, + NoneListStyleType, NotSelectorList, NthChildOf, + OctalListStyleType, OklabColors, OptionalPseudo, OriyaListStyleType, + OromoListStyleType, OverflowShorthand, P3Colors, PartPseudo, PersianListStyleType, + Picker, + PickerIcon, PlaceContent, PlaceItems, PlaceSelf, @@ -131,47 +167,65 @@ pub enum Feature { PlaceholderShown, QUnit, RadialGradient, + RcapUnit, + RchUnit, ReadOnlyWrite, RemFunction, RemUnit, RepeatingConicGradient, RepeatingLinearGradient, RepeatingRadialGradient, + RexUnit, + RicUnit, RlhUnit, RoundFunction, Selection, Selectors2, Selectors3, Shadowdomv1, + SidamaListStyleType, SignFunction, SimpChineseFormalListStyleType, SimpChineseInformalListStyleType, + SomaliListStyleType, SpaceSeparatedColorNotation, + SpellingError, SquareListStyleType, + StatePseudoClass, StretchSize, StringListStyleType, SymbolsListStyleType, TamilListStyleType, + TargetText, TeluguListStyleType, TextDecorationThicknessPercent, TextDecorationThicknessShorthand, ThaiListStyleType, TibetanListStyleType, + TigreListStyleType, + TigrinyaErAbegedeListStyleType, + TigrinyaErListStyleType, + TigrinyaEtAbegedeListStyleType, + TigrinyaEtListStyleType, TradChineseFormalListStyleType, TradChineseInformalListStyleType, UpperAlphaListStyleType, UpperArmenianListStyleType, + UpperHexadecimalListStyleType, UpperLatinListStyleType, + UpperNorwegianListStyleType, UpperRomanListStyleType, VbUnit, VhUnit, ViUnit, + ViewTransition, ViewportPercentageUnitsDynamic, ViewportPercentageUnitsLarge, ViewportPercentageUnitsSmall, VmaxUnit, VminUnit, VwUnit, + WebkitFillAvailableSize, XResolutionUnit, } @@ -398,7 +452,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -490,7 +544,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -535,7 +589,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -549,28 +603,47 @@ impl Feature { } } Feature::DirSelector => { + if let Some(version) = browsers.edge { + if version < 7864320 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 3211264 { return false; } } + if let Some(version) = browsers.chrome { + if version < 7864320 { + return false; + } + } if let Some(version) = browsers.safari { if version < 1049600 { return false; } } + if let Some(version) = browsers.opera { + if version < 6946816 { + return false; + } + } if let Some(version) = browsers.ios_saf { if version < 1049600 { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.android { + if version < 9371648 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -606,7 +679,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -651,7 +724,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -696,7 +769,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -741,7 +814,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -833,7 +906,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -878,7 +951,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -942,23 +1015,13 @@ impl Feature { return false; } } - if let Some(version) = browsers.safari { - if version < 721152 { - return false; - } - } if let Some(version) = browsers.opera { if version < 4718592 { return false; } } - if let Some(version) = browsers.ios_saf { - if version < 721664 { - return false; - } - } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -967,7 +1030,7 @@ impl Feature { return false; } } - if browsers.ie.is_some() { + if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() { return false; } } @@ -1003,7 +1066,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1093,7 +1156,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1138,7 +1201,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1152,11 +1215,26 @@ impl Feature { } } Feature::Autofill => { + if let Some(version) = browsers.chrome { + if version < 7208960 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7208960 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 5636096 { return false; } } + if let Some(version) = browsers.opera { + if version < 6291456 { + return false; + } + } if let Some(version) = browsers.safari { if version < 983040 { return false; @@ -1167,13 +1245,17 @@ impl Feature { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.samsung { + if version < 1376256 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 9371648 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -1256,7 +1338,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1301,7 +1383,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1316,35 +1398,46 @@ impl Feature { } Feature::Nesting => { if let Some(version) = browsers.edge { - if version < 7340032 { + if version < 7864320 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7667712 { return false; } } if let Some(version) = browsers.chrome { - if version < 7340032 { + if version < 7864320 { return false; } } if let Some(version) = browsers.safari { - if version < 1049856 { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 6946816 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 1049856 { + if version < 1114624 { return false; } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } - if browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -1380,7 +1473,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1399,6 +1492,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 7929856 { + return false; + } + } if let Some(version) = browsers.chrome { if version < 6881280 { return false; @@ -1420,7 +1518,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1429,7 +1527,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -1465,7 +1563,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1532,7 +1630,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7340032 { + if version < 9371648 { return false; } } @@ -1545,9 +1643,7 @@ impl Feature { return false; } } - Feature::CustomMediaQueries | Feature::RlhUnit | Feature::FitContentFunctionSize | Feature::StretchSize => { - return false - } + Feature::CustomMediaQueries | Feature::FitContentFunctionSize => return false, Feature::DoublePositionGradients => { if let Some(version) = browsers.chrome { if version < 4653056 { @@ -1749,6 +1845,16 @@ impl Feature { return false; } } + if let Some(version) = browsers.safari { + if version < 852224 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 852992 { + return false; + } + } if let Some(version) = browsers.samsung { if version < 655360 { return false; @@ -1759,7 +1865,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; } } @@ -1990,12 +2096,12 @@ impl Feature { } Feature::LogicalMargin | Feature::LogicalPadding => { if let Some(version) = browsers.chrome { - if version < 5701632 { + if version < 4521984 { return false; } } if let Some(version) = browsers.edge { - if version < 5701632 { + if version < 5177344 { return false; } } @@ -2005,7 +2111,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version < 4063232 { + if version < 3145728 { return false; } } @@ -2020,7 +2126,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 917504 { + if version < 655360 { return false; } } @@ -2110,7 +2216,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 458752 { return false; } } @@ -2179,8 +2285,13 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 7405568 { + return false; + } + } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2194,12 +2305,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1441792 { + return false; + } + } if let Some(version) = browsers.android { if version < 7274496 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -2214,8 +2330,13 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 7405568 { + return false; + } + } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2229,12 +2350,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1441792 { + return false; + } + } if let Some(version) = browsers.android { if version < 7274496 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -2249,8 +2375,13 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 7405568 { + return false; + } + } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2264,12 +2395,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1441792 { + return false; + } + } if let Some(version) = browsers.android { if version < 7274496 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -2319,20 +2455,47 @@ impl Feature { } } Feature::TextDecorationThicknessPercent => { + if let Some(version) = browsers.chrome { + if version < 5701632 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5701632 { + return false; + } + } if let Some(version) = browsers.firefox { - if version < 4849664 { + if version < 5177344 { return false; } } - if browsers.android.is_some() - || 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() - { + if let Some(version) = browsers.opera { + if version < 4063232 { + 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 < 917504 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 5701632 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -2357,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; @@ -2367,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; } } @@ -2493,7 +2666,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -2612,6 +2785,16 @@ impl Feature { return false; } } + if let Some(version) = browsers.safari { + if version < 1048576 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1048576 { + return false; + } + } if let Some(version) = browsers.samsung { if version < 655360 { return false; @@ -2622,7 +2805,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; } } @@ -2637,8 +2820,13 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 7405568 { + return false; + } + } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2652,12 +2840,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1441792 { + return false; + } + } if let Some(version) = browsers.android { if version < 7274496 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -2706,12 +2899,27 @@ impl Feature { return false; } } - Feature::RoundFunction - | Feature::RemFunction - | Feature::ModFunction - | Feature::AbsFunction - | Feature::SignFunction - | Feature::HypotFunction => { + Feature::RoundFunction | Feature::RemFunction | Feature::ModFunction => { + 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 < 7733248 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5439488 { + return false; + } + } if let Some(version) = browsers.safari { if version < 984064 { return false; @@ -2722,14 +2930,102 @@ impl Feature { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + 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::AbsFunction | Feature::SignFunction => { + if let Some(version) = browsers.chrome { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5963776 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 9043968 { + return false; + } + } + if browsers.ie.is_some() || browsers.samsung.is_some() { + return false; + } + } + Feature::HypotFunction => { + if let Some(version) = browsers.chrome { + if version < 7864320 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7864320 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5242880 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7864320 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -2820,7 +3116,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -2914,7 +3210,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 262144 { + if version < 2752512 { return false; } } @@ -2931,7 +3227,12 @@ impl Feature { } } if let Some(version) = browsers.firefox { - if version < 4587520 { + if version < 5177344 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 3735552 { return false; } } @@ -2955,25 +3256,52 @@ impl Feature { return false; } } - if browsers.ie.is_some() || browsers.opera.is_some() { + if browsers.ie.is_some() { return false; } } Feature::FontStyleObliqueAngle => { + if let Some(version) = browsers.chrome { + if version < 4063232 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5177344 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 3997696 { return false; } } - if browsers.android.is_some() - || 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() - { + if let Some(version) = browsers.opera { + if version < 3014656 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 721152 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 721664 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 524288 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 4063232 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -3067,44 +3395,64 @@ impl Feature { return false; } } - Feature::QUnit => { + Feature::LightDark => { if let Some(version) = browsers.chrome { - if version < 4128768 { + if version < 8060928 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 8060928 { return false; } } if let Some(version) = browsers.firefox { - if version < 3211264 { + if version < 7864320 { return false; } } if let Some(version) = browsers.opera { - if version < 3014656 { + if version < 5373952 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1115392 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1115392 { return false; } } if let Some(version) = browsers.samsung { - if version < 524288 { + if version < 1769472 { return false; } } if let Some(version) = browsers.android { - if version < 4128768 { + if version < 8060928 { return false; } } - if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() { + if browsers.ie.is_some() { return false; } } - Feature::CapUnit => { + Feature::AccentSystemColor => { if let Some(version) = browsers.firefox { - if version < 6356992 { + if version < 6750208 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1049856 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1049856 { return false; } } @@ -3112,99 +3460,84 @@ 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; } } - Feature::ChUnit => { + Feature::AnimationTimelineShorthand => { if let Some(version) = browsers.chrome { - if version < 1769472 { + if version < 7536640 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 589824 { + if version < 7536640 { return false; } } if let Some(version) = browsers.opera { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { + if version < 5046272 { return false; } } if let Some(version) = browsers.samsung { - if version < 66816 { + if version < 1507328 { return false; } } if let Some(version) = browsers.android { - if version < 263168 { + if version < 7536640 { return false; } } + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.safari.is_some() + { + return false; + } } - Feature::ContainerQueryLengthUnits => { + Feature::ViewTransition => { if let Some(version) = browsers.chrome { - if version < 6881280 { + if version < 7143424 { return false; } } if let Some(version) = browsers.edge { - if version < 6881280 { + if version < 7143424 { return false; } } if let Some(version) = browsers.firefox { - if version < 7208960 { + if version < 9437184 { return false; } } if let Some(version) = browsers.opera { - if version < 4718592 { + if version < 4849664 { return false; } } if let Some(version) = browsers.safari { - if version < 1048576 { + if version < 1179648 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 1048576 { + if version < 1179648 { return false; } } if let Some(version) = browsers.samsung { - if version < 1310720 { + if version < 1376256 { return false; } } if let Some(version) = browsers.android { - if version < 6881280 { + if version < 7143424 { return false; } } @@ -3212,75 +3545,547 @@ impl Feature { return false; } } - Feature::ExUnit - | Feature::CircleListStyleType - | Feature::DecimalListStyleType - | Feature::DiscListStyleType - | Feature::SquareListStyleType => { + Feature::DetailsContent => { if let Some(version) = browsers.chrome { - if version < 1179648 { + if version < 8585216 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 8585216 { return false; } } if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 262144 { + if version < 9371648 { return false; } } if let Some(version) = browsers.opera { - if version < 655616 { + if version < 5701632 { return false; } } if let Some(version) = browsers.safari { - if version < 65536 { + if version < 1180672 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 65536 { + if version < 1180672 { return false; } } if let Some(version) = browsers.samsung { - if version < 65536 { + if version < 1900544 { return false; } } if let Some(version) = browsers.android { - if version < 263168 { + if version < 8585216 { return false; } } + if browsers.ie.is_some() { + return false; + } } - Feature::IcUnit => { + Feature::TargetText => { if let Some(version) = browsers.chrome { - if version < 6946816 { + if version < 5832704 { return false; } } if let Some(version) = browsers.edge { - if version < 6946816 { + if version < 5832704 { return false; } } if let Some(version) = browsers.firefox { - if version < 6356992 { + if version < 8585216 { return false; } } if let Some(version) = browsers.opera { - if version < 4718592 { + if version < 4128768 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1180160 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1180160 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 983040 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 5832704 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::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 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5177344 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 3211264 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 3014656 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 852224 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 852992 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 524288 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 4128768 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::CapUnit => { + if let Some(version) = browsers.chrome { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 6356992 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5177344 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7733248 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::ChUnit => { + if let Some(version) = browsers.chrome { + if version < 1769472 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 786432 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 262144 { + return false; + } + } + if let Some(version) = browsers.ie { + if version < 589824 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 983040 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 458752 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 458752 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 66816 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 263168 { + return false; + } + } + } + Feature::ContainerQueryLengthUnits => { + if let Some(version) = browsers.chrome { + if version < 6881280 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 6881280 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7208960 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4718592 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1048576 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1048576 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1310720 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 6881280 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::EmUnit => { + 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 < 262144 { + return false; + } + } + if let Some(version) = browsers.ie { + if version < 196608 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 655616 { + 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 < 65536 { + return false; + } + } + } + Feature::ExUnit + | Feature::CircleListStyleType + | Feature::DecimalListStyleType + | Feature::DiscListStyleType + | Feature::SquareListStyleType => { + 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 < 262144 { + return false; + } + } + if let Some(version) = browsers.ie { + if version < 262144 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 655616 { + 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::IcUnit => { + if let Some(version) = browsers.chrome { + if version < 6946816 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 6946816 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 6356992 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4718592 { return false; } } @@ -3319,22 +4124,127 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 7864320 { + return false; + } + } if let Some(version) = browsers.opera { if version < 4849664 { return false; } } + if let Some(version) = browsers.safari { + if version < 1049600 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1049600 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1376256 { + return false; + } + } if let Some(version) = browsers.android { if version < 7143424 { return false; } } - if browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() - { + if browsers.ie.is_some() { + return false; + } + } + Feature::RcapUnit => { + if let Some(version) = browsers.chrome { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 9633792 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5177344 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7733248 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::RchUnit | Feature::RexUnit | Feature::RicUnit => { + if let Some(version) = browsers.chrome { + if version < 7274496 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7274496 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 9633792 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4915200 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1114624 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1441792 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7274496 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -3385,6 +4295,51 @@ impl Feature { } } } + Feature::RlhUnit => { + if let Some(version) = browsers.chrome { + if version < 7274496 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7274496 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7864320 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4915200 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1049600 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1049600 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1441792 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7274496 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } Feature::VbUnit | Feature::ViUnit | Feature::ViewportPercentageUnitsDynamic @@ -3420,12 +4375,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1376256 { + return false; + } + } if let Some(version) = browsers.android { if version < 7077888 { return false; } } - if browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -3655,7 +4615,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -3702,34 +4662,246 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { + return false; + } + } + } + Feature::RepeatingRadialGradient => { + 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 < 655360 { + return false; + } + } + if let Some(version) = browsers.ie { + if version < 655360 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 786432 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 327936 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 327680 { + 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::AfarListStyleType + | Feature::AmharicListStyleType + | Feature::AmharicAbegedeListStyleType + | Feature::EthiopicListStyleType + | Feature::EthiopicAbegedeListStyleType + | Feature::EthiopicAbegedeAmEtListStyleType + | Feature::EthiopicAbegedeGezListStyleType + | Feature::EthiopicAbegedeTiErListStyleType + | Feature::EthiopicAbegedeTiEtListStyleType + | Feature::EthiopicHalehameAaErListStyleType + | Feature::EthiopicHalehameAaEtListStyleType + | Feature::EthiopicHalehameAmEtListStyleType + | Feature::EthiopicHalehameGezListStyleType + | Feature::EthiopicHalehameOmEtListStyleType + | Feature::EthiopicHalehameSidEtListStyleType + | Feature::EthiopicHalehameSoEtListStyleType + | Feature::EthiopicHalehameTigListStyleType + | Feature::LowerHexadecimalListStyleType + | Feature::LowerNorwegianListStyleType + | Feature::UpperHexadecimalListStyleType + | Feature::UpperNorwegianListStyleType => { + if let Some(version) = browsers.chrome { + if version < 1179648 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5963776 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 917504 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 327680 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 262656 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 65536 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 196608 { + return false; + } + } + if browsers.firefox.is_some() || browsers.ie.is_some() { + return false; + } + } + Feature::ArabicIndicListStyleType + | Feature::BengaliListStyleType + | Feature::CjkEarthlyBranchListStyleType + | Feature::CjkHeavenlyStemListStyleType + | Feature::DevanagariListStyleType + | Feature::GujaratiListStyleType + | Feature::GurmukhiListStyleType + | Feature::KannadaListStyleType + | Feature::KhmerListStyleType + | Feature::LaoListStyleType + | Feature::MalayalamListStyleType + | Feature::MyanmarListStyleType + | Feature::OriyaListStyleType + | Feature::PersianListStyleType + | Feature::TeluguListStyleType + | Feature::ThaiListStyleType => { + if let Some(version) = browsers.chrome { + if version < 1179648 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5177344 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 262144 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 917504 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 327680 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 262656 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 65536 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 263168 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::ArmenianListStyleType + | Feature::DecimalLeadingZeroListStyleType + | Feature::GeorgianListStyleType + | Feature::LowerAlphaListStyleType + | Feature::LowerGreekListStyleType + | Feature::LowerRomanListStyleType + | Feature::UpperAlphaListStyleType + | Feature::UpperLatinListStyleType + | Feature::UpperRomanListStyleType => { + 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 < 262144 { + return false; + } + } + if let Some(version) = browsers.ie { + if version < 524288 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 655616 { + 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::RepeatingRadialGradient => { + Feature::AsterisksListStyleType | Feature::FootnotesListStyleType => { 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 < 655360 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 655360 { + if version < 5963776 { return false; } } if let Some(version) = browsers.opera { - if version < 786432 { + if version < 917504 { return false; } } @@ -3753,35 +4925,27 @@ impl Feature { return false; } } + if browsers.firefox.is_some() || browsers.ie.is_some() { + return false; + } } - Feature::ArabicIndicListStyleType - | Feature::BengaliListStyleType - | Feature::CjkEarthlyBranchListStyleType - | Feature::CjkHeavenlyStemListStyleType - | Feature::DevanagariListStyleType - | Feature::GujaratiListStyleType - | Feature::GurmukhiListStyleType - | Feature::KannadaListStyleType - | Feature::KhmerListStyleType - | Feature::LaoListStyleType - | Feature::MalayalamListStyleType - | Feature::MyanmarListStyleType - | Feature::OriyaListStyleType - | Feature::PersianListStyleType - | Feature::TeluguListStyleType - | Feature::ThaiListStyleType => { + Feature::BinaryListStyleType + | Feature::OctalListStyleType + | Feature::OromoListStyleType + | Feature::SidamaListStyleType + | Feature::SomaliListStyleType + | Feature::TigreListStyleType + | Feature::TigrinyaErListStyleType + | Feature::TigrinyaErAbegedeListStyleType + | Feature::TigrinyaEtListStyleType + | Feature::TigrinyaEtAbegedeListStyleType => { if let Some(version) = browsers.chrome { if version < 1179648 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { + if version < 5963776 { return false; } } @@ -3810,51 +4974,38 @@ impl Feature { return false; } } - if browsers.ie.is_some() { + if browsers.firefox.is_some() || browsers.ie.is_some() { return false; } } - Feature::ArmenianListStyleType - | Feature::DecimalLeadingZeroListStyleType - | Feature::GeorgianListStyleType - | Feature::LowerAlphaListStyleType - | Feature::LowerGreekListStyleType - | Feature::LowerRomanListStyleType - | Feature::UpperAlphaListStyleType - | Feature::UpperLatinListStyleType - | Feature::UpperRomanListStyleType => { + Feature::CambodianListStyleType | Feature::MongolianListStyleType | Feature::TibetanListStyleType => { if let Some(version) = browsers.chrome { if version < 1179648 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 5177344 { return false; } } if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 524288 { + if version < 2162688 { return false; } } if let Some(version) = browsers.opera { - if version < 655616 { + if version < 917504 { return false; } } if let Some(version) = browsers.safari { - if version < 65536 { + if version < 327680 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 65536 { + if version < 262656 { return false; } } @@ -3868,45 +5019,48 @@ impl Feature { return false; } } + if browsers.ie.is_some() { + return false; + } } - Feature::CambodianListStyleType | Feature::MongolianListStyleType | Feature::TibetanListStyleType => { + Feature::CjkDecimalListStyleType => { if let Some(version) = browsers.chrome { - if version < 1179648 { + if version < 5963776 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 5963776 { return false; } } if let Some(version) = browsers.firefox { - if version < 2162688 { + if version < 1835008 { return false; } } if let Some(version) = browsers.opera { - if version < 917504 { + if version < 4194304 { return false; } } if let Some(version) = browsers.safari { - if version < 327680 { + if version < 983040 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 262656 { + if version < 983040 { return false; } } if let Some(version) = browsers.samsung { - if version < 65536 { + if version < 1048576 { return false; } } if let Some(version) = browsers.android { - if version < 263168 { + if version < 5963776 { return false; } } @@ -3914,24 +5068,6 @@ impl Feature { return false; } } - Feature::CjkDecimalListStyleType => { - if let Some(version) = browsers.firefox { - if version < 1835008 { - return false; - } - } - if browsers.android.is_some() - || 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; - } - } Feature::DisclosureClosedListStyleType | Feature::DisclosureOpenListStyleType => { if let Some(version) = browsers.chrome { if version < 5832704 { @@ -3979,12 +5115,28 @@ impl Feature { } Feature::EthiopicNumericListStyleType | Feature::JapaneseFormalListStyleType - | Feature::JapaneseInformalListStyleType => { + | Feature::JapaneseInformalListStyleType + | Feature::TamilListStyleType => { + if let Some(version) = browsers.chrome { + if version < 5963776 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5963776 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 262144 { return false; } } + if let Some(version) = browsers.opera { + if version < 4194304 { + return false; + } + } if let Some(version) = browsers.safari { if version < 983040 { return false; @@ -3995,13 +5147,17 @@ impl Feature { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.samsung { + if version < 1048576 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 5963776 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -4009,14 +5165,16 @@ impl Feature { | Feature::HiraganaListStyleType | Feature::HiraganaIrohaListStyleType | Feature::KatakanaListStyleType - | Feature::KatakanaIrohaListStyleType => { + | Feature::KatakanaIrohaListStyleType + | Feature::NoneListStyleType + | Feature::AutoSize => { if let Some(version) = browsers.chrome { if version < 1179648 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 786432 { return false; } } @@ -4025,6 +5183,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.ie { + if version < 720896 { + return false; + } + } if let Some(version) = browsers.opera { if version < 917504 { return false; @@ -4050,9 +5213,6 @@ impl Feature { return false; } } - if browsers.ie.is_some() { - return false; - } } Feature::KoreanHangulFormalListStyleType | Feature::KoreanHanjaFormalListStyleType @@ -4258,7 +5418,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version < 4325376 { + if version < 3735552 { return false; } } @@ -4304,61 +5464,48 @@ impl Feature { return false; } } - Feature::TamilListStyleType => { - if let Some(version) = browsers.firefox { - if version < 262144 { + Feature::AnchorSizeSize => { + if let Some(version) = browsers.chrome { + if version < 8192000 { return false; } } - if browsers.android.is_some() - || 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; - } - } - Feature::FillSize => { - if let Some(version) = browsers.chrome { - if version < 3014656 { + if let Some(version) = browsers.edge { + if version < 8192000 { return false; } } - if let Some(version) = browsers.edge { - if version < 5177344 { + if let Some(version) = browsers.firefox { + if version < 9633792 { return false; } } if let Some(version) = browsers.opera { - if version < 2162688 { + if version < 5439488 { return false; } } if let Some(version) = browsers.safari { - if version < 786432 { + if version < 1703936 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 786432 { + if version < 1703936 { return false; } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 1769472 { return false; } } if let Some(version) = browsers.android { - if version < 3014656 { + if version < 8192000 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -4409,7 +5556,7 @@ impl Feature { } Feature::MaxContentSize => { if let Some(version) = browsers.chrome { - if version < 3014656 { + if version < 1638400 { return false; } } @@ -4439,12 +5586,12 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 66816 { return false; } } if let Some(version) = browsers.android { - if version < 3014656 { + if version < 263168 { return false; } } @@ -4497,6 +5644,66 @@ impl Feature { return false; } } + Feature::StretchSize => { + if let Some(version) = browsers.chrome { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5963776 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 9043968 { + return false; + } + } + 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.safari { + if version < 458752 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 458752 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 327680 { + return false; + } + } + if browsers.android.is_some() + || browsers.chrome.is_some() + || browsers.edge.is_some() + || browsers.ie.is_some() + || browsers.opera.is_some() + { + return false; + } + } Feature::P3Colors | Feature::LangSelectorList => { if let Some(version) = browsers.safari { if version < 655616 { @@ -4530,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/context.rs b/src/context.rs index f89a6ae8e..9fb620108 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,12 +2,18 @@ use std::collections::HashSet; use crate::compat::Feature; use crate::declaration::DeclarationBlock; +use crate::media_query::{ + MediaCondition, MediaFeatureId, MediaFeatureName, MediaFeatureValue, MediaList, MediaQuery, MediaType, + QueryFeature, +}; use crate::properties::custom::UnparsedProperty; use crate::properties::Property; +use crate::rules::media::MediaRule; use crate::rules::supports::{SupportsCondition, SupportsRule}; use crate::rules::{style::StyleRule, CssRule, CssRuleList}; use crate::selector::{Direction, PseudoClass}; use crate::targets::Targets; +use crate::values::ident::Ident; use crate::vendor_prefix::VendorPrefix; use parcel_selectors::parser::Component; @@ -33,6 +39,7 @@ pub(crate) struct PropertyHandlerContext<'i, 'o> { supports: Vec<'i>>, ltr: Vec<'i>>, rtl: Vec<'i>>, + dark: Vec<'i>>, pub context: DeclarationContext, pub unused_symbols: &'o HashSet, } @@ -45,6 +52,7 @@ impl<'i, 'o> PropertyHandlerContext<'i, 'o> { supports: Vec::new(), ltr: Vec::new(), rtl: Vec::new(), + dark: Vec::new(), context: DeclarationContext::None, unused_symbols, } @@ -57,6 +65,7 @@ impl<'i, 'o> PropertyHandlerContext<'i, 'o> { supports: Vec::new(), ltr: Vec::new(), rtl: Vec::new(), + dark: Vec::new(), context, unused_symbols: self.unused_symbols, } @@ -77,7 +86,11 @@ impl<'i, 'o> PropertyHandlerContext<'i, 'o> { self.rtl.push(rtl); } - pub fn get_logical_rules(&self, style_rule: &StyleRule<'i, T>) -> Vec<'i, T>> { + pub fn add_dark_rule(&mut self, property: Property<'i>) { + self.dark.push(property); + } + + pub fn get_additional_rules(&self, style_rule: &StyleRule<'i, T>) -> Vec<'i, T>> { // TODO: :dir/:lang raises the specificity of the selector. Use :where to lower it? let mut dest = Vec::new(); @@ -113,6 +126,32 @@ impl<'i, 'o> PropertyHandlerContext<'i, 'o> { rule!(Rtl, rtl); } + if !self.dark.is_empty() { + dest.push(CssRule::Media(MediaRule { + query: MediaList { + media_queries: vec![MediaQuery { + qualifier: None, + media_type: MediaType::All, + condition: Some(MediaCondition::Feature(QueryFeature::Plain { + name: MediaFeatureName::Standard(MediaFeatureId::PrefersColorScheme), + value: MediaFeatureValue::Ident(Ident("dark".into())), + })), + }], + }, + rules: CssRuleList(vec![CssRule::Style(StyleRule { + selectors: style_rule.selectors.clone(), + vendor_prefix: VendorPrefix::None, + declarations: DeclarationBlock { + declarations: self.dark.clone(), + important_declarations: vec![], + }, + rules: CssRuleList(vec![]), + loc: style_rule.loc.clone(), + })]), + loc: style_rule.loc.clone(), + })) + } + dest } @@ -190,5 +229,6 @@ impl<'i, 'o> PropertyHandlerContext<'i, 'o> { self.supports.clear(); self.ltr.clear(); self.rtl.clear(); + self.dark.clear(); } } diff --git a/src/css_modules.rs b/src/css_modules.rs index b794bb431..ce7008df2 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -25,13 +25,41 @@ use std::hash::{Hash, Hasher}; use std::path::Path; /// Configuration for CSS modules. -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct Config<'i> { /// The name pattern to use when renaming class names and other identifiers. /// Default is `[hash]_[local]`. pub pattern: Pattern<'i>, /// Whether to rename dashed identifiers, e.g. custom properties. pub dashed_idents: bool, + /// Whether to scope animation names. + /// Default is `true`. + pub animation: bool, + /// Whether to scope grid names. + /// Default is `true`. + pub grid: bool, + /// Whether to scope custom identifiers + /// Default is `true`. + pub custom_idents: bool, + /// Whether to scope container names. + /// Default is `true`. + pub container: bool, + /// Whether to check for pure CSS modules. + pub pure: bool, +} + +impl<'i> Default for Config<'i> { + fn default() -> Self { + Config { + pattern: Default::default(), + dashed_idents: Default::default(), + animation: true, + grid: true, + container: true, + custom_idents: true, + pure: false, + } + } } /// A CSS modules class name pattern. @@ -86,6 +114,7 @@ impl<'i> Pattern<'i> { "[name]" => Segment::Name, "[local]" => Segment::Local, "[hash]" => Segment::Hash, + "[content-hash]" => Segment::ContentHash, s => return Err(PatternParseError::UnknownPlaceholder(s.into(), start_idx)), }; segments.push(segment); @@ -105,8 +134,20 @@ impl<'i> Pattern<'i> { Ok(Pattern { segments }) } + /// Whether the pattern contains any `[content-hash]` segments. + pub fn has_content_hash(&self) -> bool { + self.segments.iter().any(|s| matches!(s, Segment::ContentHash)) + } + /// Write the substituted pattern to a destination. - pub fn write(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E> + pub fn write( + &self, + hash: &str, + path: &Path, + local: &str, + content_hash: &str, + mut write: W, + ) -> Result<(), E> where W: FnMut(&str) -> Result<(), E>, { @@ -129,6 +170,9 @@ impl<'i> Pattern<'i> { Segment::Hash => { write(hash)?; } + Segment::ContentHash => { + write(content_hash)?; + } } } Ok(()) @@ -141,8 +185,9 @@ impl<'i> Pattern<'i> { hash: &str, path: &Path, local: &str, + content_hash: &str, ) -> Result { - self.write(hash, path, local, |s| res.write_str(s))?; + self.write(hash, path, local, content_hash, |s| res.write_str(s))?; Ok(res) } } @@ -160,6 +205,8 @@ pub enum Segment<'i> { Local, /// A hash of the file name. Hash, + /// A hash of the file contents. + ContentHash, } /// A referenced name within a CSS module, e.g. via the `composes` property. @@ -224,6 +271,7 @@ pub(crate) struct CssModule<'a, 'b, 'c> { pub config: &'a Config<'b>, pub sources: Vec<&'c Path>, pub hashes: Vec, + pub content_hashes: &'a Option>, pub exports_by_source_index: Vec, pub references: &'a mut HashMap, } @@ -234,6 +282,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { sources: &'c Vec, project_root: Option<&'c str>, references: &'a mut HashMap, + content_hashes: &'a Option>, ) -> Self { let project_root = project_root.map(|p| Path::new(p)); let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect(); @@ -258,6 +307,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(), sources, hashes, + content_hashes, references, } } @@ -274,6 +324,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], local, + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -293,6 +348,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], &local[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -315,6 +375,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], name, + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -344,6 +409,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[*source_index as usize], &self.sources[*source_index as usize], &name[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[*source_index as usize] + } else { + "" + }, ) .unwrap(), ) @@ -364,6 +434,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], &name[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -406,6 +481,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], name.0.as_ref(), + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), }, diff --git a/src/declaration.rs b/src/declaration.rs index f965cda88..0dd3da619 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -3,12 +3,14 @@ use std::borrow::Cow; use std::ops::Range; -use crate::context::PropertyHandlerContext; -use crate::error::{ParserError, PrinterError}; +use crate::context::{DeclarationContext, PropertyHandlerContext}; +use crate::error::{ParserError, PrinterError, PrinterErrorKind}; use crate::parser::ParserOptions; use crate::printer::Printer; use crate::properties::box_shadow::BoxShadowHandler; +use crate::properties::custom::{CustomProperty, CustomPropertyName}; use crate::properties::masking::MaskHandler; +use crate::properties::text::{Direction, UnicodeBidi}; use crate::properties::{ align::AlignHandler, animation::AnimationHandler, @@ -29,13 +31,18 @@ use crate::properties::{ text::TextDecorationHandler, transform::TransformHandler, transition::TransitionHandler, + ui::ColorSchemeHandler, }; use crate::properties::{Property, PropertyId}; +use crate::selector::SelectorList; use crate::traits::{PropertyHandler, ToCss}; +use crate::values::ident::DashedIdent; use crate::values::string::CowArcStr; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; +use indexmap::IndexMap; +use smallvec::SmallVec; /// A CSS declaration block. /// @@ -44,7 +51,7 @@ use cssparser::*; /// with storing a boolean along with each property. #[derive(Debug, PartialEq, Clone, Default)] #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_declaration_block, PROPERTIES))] -#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -68,14 +75,12 @@ impl<'i> DeclarationBlock<'i> { ) -> Result<'i, ParserError<'i>>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); - let mut parser = DeclarationListParser::new( - input, - PropertyDeclarationParser { - important_declarations: &mut important_declarations, - declarations: &mut declarations, - options, - }, - ); + let mut decl_parser = PropertyDeclarationParser { + important_declarations: &mut important_declarations, + declarations: &mut declarations, + options, + }; + let mut parser = RuleBodyParser::new(input, &mut decl_parser); while let Some(res) = parser.next() { if let Err((err, _)) = res { if options.error_recovery { @@ -154,18 +159,70 @@ impl<'i> DeclarationBlock<'i> { dest.whitespace()?; dest.write_char('{')?; dest.indent(); + dest.newline()?; + + self.to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0)?; + + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } + pub(crate) fn has_printable_declarations(&self) -> bool { + if self.len() > 1 { + return true; + } + + if self.declarations.len() == 1 { + !matches!(self.declarations[0], crate::properties::Property::Composes(_)) + } else if self.important_declarations.len() == 1 { + !matches!(self.important_declarations[0], crate::properties::Property::Composes(_)) + } else { + false + } + } + + /// Writes the declarations to a CSS declaration block. + pub fn to_css_declarations( + &self, + dest: &mut Printer, + has_nested_rules: bool, + selectors: &SelectorList, + source_index: u32, + ) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { let mut i = 0; let len = self.len(); macro_rules! write { ($decls: expr, $important: literal) => { for decl in &$decls { - dest.newline()?; + // The CSS modules `composes` property is handled specially, and omitted during printing. + // We need to add the classes it references to the list for the selectors in this rule. + if let crate::properties::Property::Composes(composes) = &decl { + if dest.is_nested() && dest.css_module.is_some() { + return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc)); + } + + if let Some(css_module) = &mut dest.css_module { + css_module + .handle_composes(&selectors, &composes, source_index) + .map_err(|e| dest.error(e, composes.loc))?; + continue; + } + } + + if i > 0 { + dest.newline()?; + } + decl.to_css(dest, $important)?; - if i != len - 1 || !dest.minify { + if i != len - 1 || !dest.minify || has_nested_rules { dest.write_char(';')?; } + i += 1; } }; @@ -173,10 +230,7 @@ impl<'i> DeclarationBlock<'i> { write!(self.declarations, false); write!(self.important_declarations, true); - - dest.dedent(); - dest.newline()?; - dest.write_char('}') + Ok(()) } } @@ -435,6 +489,22 @@ impl<'a, 'o, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Error = ParserError<'i>; } +impl<'a, 'o, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { + type Prelude = (); + type QualifiedRule = (); + type Error = ParserError<'i>; +} + +impl<'a, 'o, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for PropertyDeclarationParser<'a, 'o, 'i> { + fn parse_qualified(&self) -> bool { + false + } + + fn parse_declarations(&self) -> bool { + true + } +} + pub(crate) fn parse_declaration<'i, 't>( name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, @@ -442,15 +512,22 @@ pub(crate) fn parse_declaration<'i, 't>( important_declarations: &mut DeclarationList<'i>, options: &ParserOptions<'_, 'i>, ) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> { - let property = input.parse_until_before(Delimiter::Bang, |input| { - Property::parse(PropertyId::from(CowArcStr::from(name)), input, options) - })?; + // Stop if we hit a `{` token in a non-custom property to + // avoid ambiguity between nested rules and declarations. + // https://github.com/w3c/csswg-drafts/issues/9317 + let property_id = PropertyId::from(CowArcStr::from(name)); + let mut delimiters = Delimiter::Bang; + if !matches!(property_id, PropertyId::Custom(CustomPropertyName::Custom(..))) { + delimiters = delimiters | Delimiter::CurlyBracketBlock; + } + let property = input.parse_until_before(delimiters, |input| Property::parse(property_id, input, options))?; let important = input .try_parse(|input| { input.expect_delim('!')?; input.expect_ident_matching("important") }) .is_ok(); + input.expect_exhausted()?; if important { important_declarations.push(property); } else { @@ -487,8 +564,12 @@ pub(crate) struct DeclarationHandler<'i> { box_shadow: BoxShadowHandler, mask: MaskHandler<'i>, container: ContainerHandler<'i>, + color_scheme: ColorSchemeHandler, fallback: FallbackHandler, prefix: PrefixHandler, + direction: Option, + unicode_bidi: Option, + custom_properties: IndexMap<'i>, usize>, decls: DeclarationList<'i>, } @@ -498,12 +579,6 @@ impl<'i> DeclarationHandler<'i> { property: &Property<'i>, context: &mut PropertyHandlerContext<'i, '_>, ) -> bool { - if !context.unused_symbols.is_empty() - && matches!(property, Property::Custom(custom) if context.unused_symbols.contains(custom.name.as_ref())) - { - return true; - } - self.background.handle_property(property, &mut self.decls, context) || self.border.handle_property(property, &mut self.decls, context) || self.outline.handle_property(property, &mut self.decls, context) @@ -528,11 +603,102 @@ impl<'i> DeclarationHandler<'i> { || self.box_shadow.handle_property(property, &mut self.decls, context) || self.mask.handle_property(property, &mut self.decls, context) || self.container.handle_property(property, &mut self.decls, context) + || self.color_scheme.handle_property(property, &mut self.decls, context) || self.fallback.handle_property(property, &mut self.decls, context) || self.prefix.handle_property(property, &mut self.decls, context) + || self.handle_all(property) + || self.handle_custom_property(property, context) + } + + fn handle_custom_property( + &mut self, + property: &Property<'i>, + context: &mut PropertyHandlerContext<'i, '_>, + ) -> bool { + if let Property::Custom(custom) = property { + if context.unused_symbols.contains(custom.name.as_ref()) { + return true; + } + + if let CustomPropertyName::Custom(name) = &custom.name { + if let Some(index) = self.custom_properties.get(name) { + if self.decls[*index] == *property { + return true; + } + let mut custom = custom.clone(); + self.add_conditional_fallbacks(&mut custom, context); + self.decls[*index] = Property::Custom(custom); + } else { + self.custom_properties.insert(name.clone(), self.decls.len()); + let mut custom = custom.clone(); + self.add_conditional_fallbacks(&mut custom, context); + self.decls.push(Property::Custom(custom)); + } + + return true; + } + } + + false + } + + fn handle_all(&mut self, property: &Property<'i>) -> bool { + // The `all` property resets all properies except `unicode-bidi`, `direction`, and custom properties. + // https://drafts.csswg.org/css-cascade-5/#all-shorthand + match property { + Property::UnicodeBidi(bidi) => { + self.unicode_bidi = Some(*bidi); + true + } + Property::Direction(direction) => { + self.direction = Some(*direction); + true + } + Property::All(keyword) => { + let mut handler = DeclarationHandler { + unicode_bidi: self.unicode_bidi.clone(), + direction: self.direction.clone(), + ..Default::default() + }; + for (key, index) in self.custom_properties.drain(..) { + handler.custom_properties.insert(key, handler.decls.len()); + handler.decls.push(self.decls[index].clone()); + } + handler.decls.push(Property::All(keyword.clone())); + *self = handler; + true + } + _ => false, + } + } + + fn add_conditional_fallbacks( + &self, + custom: &mut CustomProperty<'i>, + context: &mut PropertyHandlerContext<'i, '_>, + ) { + if context.context != DeclarationContext::Keyframes { + let fallbacks = custom.value.get_fallbacks(context.targets); + for (condition, fallback) in fallbacks { + context.add_conditional_property( + condition, + Property::Custom(CustomProperty { + name: custom.name.clone(), + value: fallback, + }), + ); + } + } } pub fn finalize(&mut self, context: &mut PropertyHandlerContext<'i, '_>) { + if let Some(direction) = std::mem::take(&mut self.direction) { + self.decls.push(Property::Direction(direction)); + } + if let Some(unicode_bidi) = std::mem::take(&mut self.unicode_bidi) { + self.decls.push(Property::UnicodeBidi(unicode_bidi)); + } + self.background.finalize(&mut self.decls, context); self.border.finalize(&mut self.decls, context); self.outline.finalize(&mut self.decls, context); @@ -557,7 +723,9 @@ impl<'i> DeclarationHandler<'i> { self.box_shadow.finalize(&mut self.decls, context); self.mask.finalize(&mut self.decls, context); self.container.finalize(&mut self.decls, context); + self.color_scheme.finalize(&mut self.decls, context); self.fallback.finalize(&mut self.decls, context); self.prefix.finalize(&mut self.decls, context); + self.custom_properties.clear(); } } diff --git a/src/dependencies.rs b/src/dependencies.rs index 356a8f398..9648a5d93 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -136,6 +136,7 @@ pub struct SourceRange { #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))] #[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct Location { /// The line number, starting from 1. pub line: u32, diff --git a/src/error.rs b/src/error.rs index 274a5caef..b4b1b0ab8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,8 @@ use cssparser::{BasicParseErrorKind, ParseError, ParseErrorKind}; use parcel_selectors::parser::SelectorParseErrorKind; #[cfg(any(feature = "serde", feature = "nodejs"))] use serde::Serialize; +#[cfg(feature = "into_owned")] +use static_self::IntoOwned; use std::fmt; /// An error with a source location. @@ -66,7 +68,7 @@ impl fmt::Display for ErrorLocation { /// A parser error. #[derive(Debug, PartialEq, Clone)] -#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type", content = "value"))] pub enum ParserError<'i> { @@ -82,10 +84,14 @@ 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. DeprecatedNestRule, + /// The @value rule (of CSS modules) is deprecated. + DeprecatedCssModulesValueRule, /// An invalid selector in an `@page` rule. InvalidPageSelector, /// An invalid value was encountered. @@ -114,8 +120,10 @@ 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"), InvalidPageSelector => write!(f, "Invalid page selector"), InvalidValue => write!(f, "Invalid value"), QualifiedRuleInvalid => write!(f, "Invalid qualified rule"), @@ -184,7 +192,7 @@ impl<'i> ParserError<'i> { /// A selector parsing error. #[derive(Debug, PartialEq, Clone)] -#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type", content = "value"))] pub enum SelectorError<'i> { @@ -228,8 +236,20 @@ pub enum SelectorError<'i> { UnexpectedTokenInAttributeSelector( #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>, ), - /// An unsupported pseudo class or pseudo element was encountered. - UnsupportedPseudoClassOrElement(CowArcStr<'i>), + + /// An unsupported pseudo class was encountered. + UnsupportedPseudoClass(CowArcStr<'i>), + + /// An unsupported pseudo element was encountered. + UnsupportedPseudoElement(CowArcStr<'i>), + + /// Ambiguous CSS module class. + AmbiguousCssModuleClass(CowArcStr<'i>), + + /// An unexpected token was encountered after a pseudo element. + UnexpectedSelectorAfterPseudoElement( + #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>, + ), } impl<'i> fmt::Display for SelectorError<'i> { @@ -254,7 +274,15 @@ impl<'i> fmt::Display for SelectorError<'i> { PseudoElementExpectedIdent(token) => write!(f, "Invalid token in pseudo element: {:?}", token), UnexpectedIdent(name) => write!(f, "Unexpected identifier: {}", name), UnexpectedTokenInAttributeSelector(token) => write!(f, "Unexpected token in attribute selector: {:?}", token), - UnsupportedPseudoClassOrElement(name) => write!(f, "Unsupported pseudo class or element: {}", name), + UnsupportedPseudoClass(name) =>write!(f, "'{name}' is not recognized as a valid pseudo-class. Did you mean '::{name}' (pseudo-element) or is this a typo?"), + UnsupportedPseudoElement(name) => write!(f, "'{name}' is not recognized as a valid pseudo-element. Did you mean ':{name}' (pseudo-class) or is this a typo?"), + AmbiguousCssModuleClass(_) => write!(f, "Ambiguous CSS module class not supported"), + UnexpectedSelectorAfterPseudoElement(token) => { + write!( + f, + "Pseudo-elements like '::before' or '::after' can't be followed by selectors like '{token:?}'" + ) + }, } } } @@ -283,9 +311,8 @@ impl<'i> From<'i>> for SelectorError<'i> { SelectorError::UnexpectedTokenInAttributeSelector(t.into()) } SelectorParseErrorKind::PseudoElementExpectedIdent(t) => SelectorError::PseudoElementExpectedIdent(t.into()), - SelectorParseErrorKind::UnsupportedPseudoClassOrElement(t) => { - SelectorError::UnsupportedPseudoClassOrElement(t.into()) - } + SelectorParseErrorKind::UnsupportedPseudoClass(t) => SelectorError::UnsupportedPseudoClass(t.into()), + SelectorParseErrorKind::UnsupportedPseudoElement(t) => SelectorError::UnsupportedPseudoElement(t.into()), SelectorParseErrorKind::UnexpectedIdent(t) => SelectorError::UnexpectedIdent(t.into()), SelectorParseErrorKind::ExpectedNamespace(t) => SelectorError::ExpectedNamespace(t.into()), SelectorParseErrorKind::ExpectedBarInAttr(t) => SelectorError::ExpectedBarInAttr(t.into()), @@ -295,6 +322,10 @@ impl<'i> From<'i>> for SelectorError<'i> { SelectorError::ExplicitNamespaceUnexpectedToken(t.into()) } SelectorParseErrorKind::ClassNeedsIdent(t) => SelectorError::ClassNeedsIdent(t.into()), + SelectorParseErrorKind::AmbiguousCssModuleClass(name) => SelectorError::AmbiguousCssModuleClass(name.into()), + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(t) => { + SelectorError::UnexpectedSelectorAfterPseudoElement(t.into()) + } } } } @@ -335,6 +366,8 @@ pub enum MinifyErrorKind { /// The source location of the `@custom-media` rule with unsupported boolean logic. custom_media_loc: Location, }, + /// A CSS module selector did not contain at least one class or id selector. + ImpureCSSModuleSelector, } impl fmt::Display for MinifyErrorKind { @@ -347,6 +380,10 @@ impl fmt::Display for MinifyErrorKind { f, "Boolean logic with media types in @custom-media rules is not supported by Lightning CSS" ), + ImpureCSSModuleSelector => write!( + f, + "A selector in CSS modules should contain at least one class or ID selector" + ), } } } @@ -376,7 +413,7 @@ pub enum PrinterErrorKind { FmtError, /// The CSS modules `composes` property cannot be used within nested rules. InvalidComposesNesting, - /// The CSS modules `composes` property cannot be used with a simple class selector. + /// The CSS modules `composes` property can only be used with a simple class selector. InvalidComposesSelector, /// The CSS modules pattern must end with `[local]` for use in CSS grid. InvalidCssModulesPatternInGrid, diff --git a/src/lib.rs b/src/lib.rs index 76569a278..2a41655ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,9 @@ mod tests { use crate::vendor_prefix::VendorPrefix; use cssparser::SourceLocation; use indoc::indoc; + use pretty_assertions::assert_eq; use std::collections::HashMap; + use std::sync::{Arc, RwLock}; fn test(source: &str, expected: &str) { test_with_options(source, expected, ParserOptions::default()) @@ -77,10 +79,18 @@ 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()) } + #[track_caller] fn minify_test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) { let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); @@ -93,6 +103,18 @@ mod tests { assert_eq!(res.code, expected); } + fn minify_error_test_with_options<'i, 'o>( + source: &'i str, + error: MinifyErrorKind, + options: ParserOptions<'o, 'i>, + ) { + let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap(); + match stylesheet.minify(MinifyOptions::default()) { + Err(e) => assert_eq!(e.kind, error), + _ => unreachable!(), + } + } + fn prefix_test(source: &str, expected: &str, targets: Browsers) { let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); stylesheet @@ -139,14 +161,7 @@ mod tests { } fn nesting_test_with_targets(source: &str, expected: &str, targets: Targets) { - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - flags: ParserFlags::NESTING, - ..ParserOptions::default() - }, - ) - .unwrap(); + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); stylesheet .minify(MinifyOptions { targets, @@ -163,14 +178,7 @@ mod tests { } fn nesting_test_no_targets(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - flags: ParserFlags::NESTING, - ..ParserOptions::default() - }, - ) - .unwrap(); + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); assert_eq!(res.code, expected); @@ -182,6 +190,7 @@ mod tests { expected_exports: CssModuleExports, expected_references: CssModuleReferences, config: crate::css_modules::Config<'i>, + minify: bool, ) { let mut stylesheet = StyleSheet::parse( &source, @@ -193,7 +202,12 @@ mod tests { ) .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + minify, + ..Default::default() + }) + .unwrap(); assert_eq!(res.code, expected); assert_eq!(res.exports.unwrap(), expected_exports); assert_eq!(res.references.unwrap(), expected_references); @@ -230,12 +244,31 @@ mod tests { } } - fn nesting_error_test(source: &str, error: ParserError) { + 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:?}"), + } + } + Arc::into_inner(warnings).unwrap().into_inner().unwrap() + } + + fn css_modules_error_test(source: &str, error: ParserError) { let res = StyleSheet::parse( &source, ParserOptions { - flags: ParserFlags::NESTING, - ..ParserOptions::default() + css_modules: Some(Default::default()), + ..Default::default() }, ); match res { @@ -362,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( @@ -1034,7 +1183,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left: 2px solid red; } @@ -1060,7 +1213,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-width: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left-width: 2px; } @@ -1086,7 +1243,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: 2px solid red; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-right: 2px solid red; } @@ -1113,7 +1274,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + border-right: 5px solid green; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left: 2px solid red; border-right: 5px solid green; } @@ -1148,7 +1314,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + border-right: 5px solid green; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left: 2px solid red; border-right: 5px solid green; } @@ -1163,7 +1334,12 @@ mod tests { border-right: 2px solid red; } - .bar:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .bar:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 1px dotted gray; + border-right: 1px solid #000; + } + + .bar:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left: 1px dotted gray; border-right: 1px solid #000; } @@ -1268,7 +1444,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--test); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-right: var(--test); } @@ -1295,7 +1475,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: var(--start); + border-right: var(--end); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left: var(--start); border-right: var(--end); } @@ -1434,6 +1619,33 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + &format!( + r#" + @supports (color: lab(0% 0 0)) {{ + .foo {{ + {}: var(--border-width) solid lab(40% 56.6 39); + }} + }} + "#, + prop + ), + &format!( + indoc! {r#" + @supports (color: lab(0% 0 0)) {{ + .foo {{ + {}: var(--border-width) solid lab(40% 56.6 39); + }} + }} + "#}, + prop, + ), + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); } prefix_test( @@ -1443,7 +1655,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left-color: #b32323; border-left-color: lab(40% 56.6 39); } @@ -1471,7 +1688,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-right-color: #b32323; border-right-color: lab(40% 56.6 39); } @@ -1500,7 +1722,14 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); + border-right-color: #ee00be; + border-right-color: lch(50.998% 135.363 338); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left-color: #b32323; border-left-color: lab(40% 56.6 39); border-right-color: #ee00be; @@ -1535,7 +1764,7 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left-color: #b32323; border-left-color: color(display-p3 .643308 .192455 .167712); border-left-color: lab(40% 56.6 39); @@ -1567,7 +1796,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid #b32323; + border-left: 2px solid lab(40% 56.6 39); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-left: 2px solid #b32323; border-left: 2px solid lab(40% 56.6 39); } @@ -1595,7 +1829,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: 2px solid #b32323; + border-right: 2px solid lab(40% 56.6 39); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-right: 2px solid #b32323; border-right: 2px solid lab(40% 56.6 39); } @@ -1623,12 +1862,16 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--border-width) solid #b32323; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-right: var(--border-width) solid #b32323; } @supports (color: lab(0% 0 0)) { - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { border-right: var(--border-width) solid lab(40% 56.6 39); } } @@ -2078,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; } @@ -2099,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; } @@ -2121,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; } @@ -2143,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)); } @@ -2997,7 +3240,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + margin-left: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { margin-left: 2px; } @@ -3024,7 +3271,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + margin-left: 2px; + margin-right: 4px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { margin-left: 2px; margin-right: 4px; } @@ -3360,7 +3612,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { padding-left: 2px; } @@ -3387,7 +3643,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: 2px; + padding-right: 4px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { padding-left: 2px; padding-right: 4px; } @@ -3416,7 +3677,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: var(--padding); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { padding-left: var(--padding); } @@ -3706,6 +3971,32 @@ mod tests { }, ); + prefix_test( + &format!( + r#" + .foo {{ + {}: 100vw; + {}: -webkit-fill-available; + }} + "#, + in_prop, in_prop + ), + &format!( + indoc! {r#" + .foo {{ + {}: 100vw; + {}: -webkit-fill-available; + }} + "#}, + out_prop, out_prop + ), + Browsers { + safari: Some(8 << 16), + firefox: Some(4 << 16), + ..Browsers::default() + }, + ); + prefix_test( &format!( r#" @@ -3865,6 +4156,15 @@ mod tests { minify_test(".foo { aspect-ratio: 2 / 3 }", ".foo{aspect-ratio:2/3}"); minify_test(".foo { aspect-ratio: auto 2 / 3 }", ".foo{aspect-ratio:auto 2/3}"); minify_test(".foo { aspect-ratio: 2 / 3 auto }", ".foo{aspect-ratio:auto 2/3}"); + + minify_test( + ".foo { width: 200px; width: var(--foo); }", + ".foo{width:200px;width:var(--foo)}", + ); + minify_test( + ".foo { width: var(--foo); width: 200px; }", + ".foo{width:var(--foo);width:200px}", + ); } #[test] @@ -4014,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}", @@ -4040,13 +4352,33 @@ mod tests { ".foo { background-position: bottom right }", ".foo{background-position:100% 100%}", ); - minify_test( - ".foo { background: url('img-sprite.png') no-repeat bottom right }", - ".foo{background:url(img-sprite.png) 100% 100% no-repeat}", + ".foo { background-position: center top }", + ".foo{background-position:top}", ); - minify_test(".foo { background: transparent }", ".foo{background:0 0}"); - + 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 }", + ".foo{background:url(img-sprite.png) 100% 100% no-repeat}", + ); + minify_test(".foo { background: transparent }", ".foo{background:0 0}"); + minify_test(".foo { background: url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\") }", ".foo{background:url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\")}"); test( @@ -6400,7 +6732,11 @@ mod tests { prefix_test( ".test:not(.foo, .bar) {color:red}", indoc! {r#" - .test:not(.foo):not(.bar) { + .test:not(:-webkit-any(.foo, .bar)) { + color: red; + } + + .test:not(:is(.foo, .bar)) { color: red; } "#}, @@ -6541,7 +6877,15 @@ mod tests { prefix_test( "a:dir(ltr) {color:red}", indoc! {r#" - a:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + a:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + color: red; + } + + a:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + color: red; + } + + a:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { color: red; } "#}, @@ -6720,6 +7064,19 @@ mod tests { ":root::view-transition {position: fixed}", ":root::view-transition{position:fixed}", ); + minify_test( + ":root:active-view-transition {position: fixed}", + ":root:active-view-transition{position:fixed}", + ); + minify_test( + ":root:active-view-transition-type(slide-in) {position: fixed}", + ":root:active-view-transition-type(slide-in){position:fixed}", + ); + minify_test( + ":root:active-view-transition-type(slide-in, reverse) {position: fixed}", + ":root:active-view-transition-type(slide-in,reverse){position:fixed}", + ); + for name in &[ "view-transition-group", "view-transition-image-pair", @@ -6730,14 +7087,46 @@ mod tests { &format!(":root::{}(*) {{position: fixed}}", name), &format!(":root::{}(*){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(*.class) {{position: fixed}}", name), + &format!(":root::{}(*.class){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(*.class.class) {{position: fixed}}", name), + &format!(":root::{}(*.class.class){{position:fixed}}", name), + ); minify_test( &format!(":root::{}(foo) {{position: fixed}}", name), &format!(":root::{}(foo){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(foo.class) {{position: fixed}}", name), + &format!(":root::{}(foo.class){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name), + &format!(":root::{}(foo.bar.baz){{position:fixed}}", name), + ); minify_test( &format!(":root::{}(foo):only-child {{position: fixed}}", name), &format!(":root::{}(foo):only-child{{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(foo.bar.baz):only-child {{position: fixed}}", name), + &format!(":root::{}(foo.bar.baz):only-child{{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(.foo) {{position: fixed}}", name), + &format!(":root::{}(.foo){{position:fixed}}", name), + ); + minify_test( + &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), @@ -6746,8 +7135,81 @@ mod tests { &format!(":root::{}(foo)::before {{position: fixed}}", name), ParserError::SelectorError(SelectorError::InvalidState), ); + error_test( + &format!(":root::{}(*.*) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*. cls) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(foo .bar) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls. c) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls>cls) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls.foo.*) {{position: fixed}}", name), + 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}"); @@ -6800,6 +7262,159 @@ mod tests { ".foo /deep/ .bar{width:20px}", deep_options.clone(), ); + + let pure_css_module_options = ParserOptions { + css_modules: Some(crate::css_modules::Config { + pure: true, + ..Default::default() + }), + ..ParserOptions::default() + }; + + minify_error_test_with_options( + "div {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + ":global(.foo) {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "[foo=bar] {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "div, .foo {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_test_with_options( + ":local(.foo) {width: 20px}", + "._8Z4fiW_foo{width:20px}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "div.my-class {color: red;}", + "div._8Z4fiW_my-class{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "#id {color: red;}", + "#_8Z4fiW_id{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "a .my-class{color: red;}", + "a ._8Z4fiW_my-class{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".my-class a {color: red;}", + "._8Z4fiW_my-class a{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".my-class:is(a) {color: red;}", + "._8Z4fiW_my-class:is(a){color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "div:has(.my-class) {color: red;}", + "div:has(._8Z4fiW_my-class){color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".foo { html &:hover { a_value: some-value; } }", + "._8Z4fiW_foo{html &:hover{a_value:some-value}}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".foo { span { color: red; } }", + "._8Z4fiW_foo{& span{color:red}}", + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "html { .foo { span { color: red; } } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".foo { div { span { color: red; } } }", + "._8Z4fiW_foo{& div{& span{color:red}}}", + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "@scope (div) { .foo { color: red } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "@scope (.a) to (div) { .foo { color: red } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "@scope (.a) to (.b) { div { color: red } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_test_with_options( + "@scope (.a) to (.b) { .foo { color: red } }", + "@scope(._8Z4fiW_a) to (._8Z4fiW_b){._8Z4fiW_foo{color:red}}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "/* cssmodules-pure-no-check */ :global(.foo) { color: red }", + ".foo{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "/*! some license */ /* cssmodules-pure-no-check */ :global(.foo) { color: red }", + "/*! some license */\n.foo{color:red}", + pure_css_module_options.clone(), + ); + + error_test( + "input.defaultCheckbox::before h1 {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Ident( + "h1".into(), + ))), + ); + error_test( + "input.defaultCheckbox::before .my-class {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.'))), + ); + error_test( + "input.defaultCheckbox::before.my-class {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.'))), + ); + error_test( + "input.defaultCheckbox::before #id {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash( + "id".into(), + ))), + ); + error_test( + "input.defaultCheckbox::before#id {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash( + "id".into(), + ))), + ); + error_test( + "input.defaultCheckbox::before [attr] {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement( + Token::SquareBracketBlock, + )), + ); + error_test( + "input.defaultCheckbox::before[attr] {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement( + Token::SquareBracketBlock, + )), + ); } #[test] @@ -6825,6 +7440,21 @@ mod tests { "@keyframes test{to{background:#00f}}", ); + // named animation range percentages + minify_test( + r#" + @keyframes test { + entry 0% { + background: blue + } + exit 100% { + background: green + } + } + "#, + "@keyframes test{entry 0%{background:#00f}exit 100%{background:green}}", + ); + // CSS-wide keywords and `none` cannot remove quotes. minify_test( r#" @@ -6848,6 +7478,18 @@ mod tests { "@keyframes \"none\"{0%{background:green}}", ); + // named animation ranges cannot be used with to or from + minify_test( + r#" + @keyframes test { + entry to { + background: blue + } + } + "#, + "@keyframes test{}", + ); + // CSS-wide keywords without quotes throws an error. error_test( r#" @@ -7158,6 +7800,39 @@ mod tests { ..Browsers::default() }, ); + + minify_test( + r#" + @keyframes test { + 100% { + background: blue + } + } + + @keyframes test { + 100% { + background: red + } + } + "#, + "@keyframes test{to{background:red}}", + ); + minify_test( + r#" + @keyframes test { + 100% { + background: blue + } + } + + @-webkit-keyframes test { + 100% { + background: red + } + } + "#, + "@keyframes test{to{background:#00f}}@-webkit-keyframes test{to{background:red}}", + ); } #[test] @@ -7379,7 +8054,7 @@ mod tests { ); minify_test( ".foo { border-width: clamp(1em, 2em, 4vh) }", - ".foo{border-width:min(2em,4vh)}", + ".foo{border-width:clamp(1em,2em,4vh)}", ); minify_test( ".foo { border-width: clamp(1em, 2vh, 4vh) }", @@ -7390,6 +8065,10 @@ mod tests { ".foo{border-width:clamp(1px,1px + 2em,4px)}", ); minify_test(".foo { border-width: clamp(1px, 2pt, 1in) }", ".foo{border-width:2pt}"); + minify_test( + ".foo { width: clamp(-100px, 0px, 50% - 50vw); }", + ".foo{width:clamp(-100px,0px,50% - 50vw)}", + ); minify_test( ".foo { top: calc(-1 * clamp(1.75rem, 8vw, 4rem)) }", @@ -7522,6 +8201,46 @@ mod tests { ".foo{transform:rotateX(-40deg)rotateY(50deg)}", ); minify_test(".foo { width: calc(10px * mod(18, 5)) }", ".foo{width:30px}"); + + minify_test( + ".foo { width: calc(100% - 30px - 0) }", + ".foo{width:calc(100% - 30px - 0)}", + ); + minify_test( + ".foo { width: calc(100% - 30px - 1 - 2) }", + ".foo{width:calc(100% - 30px - 3)}", + ); + minify_test( + ".foo { width: calc(1 - 2 - 100% - 30px) }", + ".foo{width:calc(-1 - 100% - 30px)}", + ); + minify_test( + ".foo { width: calc(2 * min(1px, 1vmin) - min(1px, 1vmin)); }", + ".foo{width:calc(2*min(1px,1vmin) - min(1px,1vmin))}", + ); + minify_test( + ".foo { width: calc(100% - clamp(1.125rem, 1.25vw, 1.2375rem) - clamp(1.125rem, 1.25vw, 1.2375rem)); }", + ".foo{width:calc(100% - clamp(1.125rem,1.25vw,1.2375rem) - clamp(1.125rem,1.25vw,1.2375rem))}", + ); + minify_test( + ".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); + } + "#}, + ); } #[test] @@ -7562,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}"); @@ -7581,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] @@ -7613,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] @@ -8034,7 +8759,7 @@ mod tests { } "#, indoc! { r#" - @media (min-color: 3) { + @media not (max-color: 2) { .foo { color: #7fff00; } @@ -8055,7 +8780,7 @@ mod tests { } "#, indoc! { r#" - @media (max-color: 1) { + @media not (min-color: 2) { .foo { color: #7fff00; } @@ -8076,7 +8801,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: 240.001px) { + @media not (max-width: 240px) { .foo { color: #7fff00; } @@ -8139,7 +8864,7 @@ mod tests { } "#, indoc! { r#" - @media (max-width: 239.999px) { + @media not (min-width: 240px) { .foo { color: #7fff00; } @@ -8153,77 +8878,73 @@ mod tests { prefix_test( r#" - @media (100px <= width <= 200px) { + @media not (width < 240px) { .foo { color: chartreuse; } } "#, indoc! { r#" - @media (min-width: 100px) and (max-width: 200px) { + @media (min-width: 240px) { .foo { color: #7fff00; } } "#}, Browsers { - firefox: Some(85 << 16), + firefox: Some(60 << 16), ..Browsers::default() }, ); - prefix_test( + test( r#" - @media not (100px <= width <= 200px) { + @media not (width < 240px) { .foo { color: chartreuse; } } "#, indoc! { r#" - @media not ((min-width: 100px) and (max-width: 200px)) { + @media (width >= 240px) { .foo { color: #7fff00; } } "#}, - Browsers { - firefox: Some(85 << 16), - ..Browsers::default() - }, ); prefix_test( r#" - @media (hover) and (100px <= width <= 200px) { + @media (width < 240px) and (hover) { .foo { color: chartreuse; } } "#, indoc! { r#" - @media (hover) and (min-width: 100px) and (max-width: 200px) { + @media (not (min-width: 240px)) and (hover) { .foo { color: #7fff00; } } "#}, Browsers { - firefox: Some(85 << 16), + firefox: Some(60 << 16), ..Browsers::default() }, ); prefix_test( r#" - @media (hover) or (100px <= width <= 200px) { + @media (100px <= width <= 200px) { .foo { color: chartreuse; } } "#, indoc! { r#" - @media (hover) or ((min-width: 100px) and (max-width: 200px)) { + @media (min-width: 100px) and (max-width: 200px) { .foo { color: #7fff00; } @@ -8237,14 +8958,14 @@ mod tests { prefix_test( r#" - @media (100px < width < 200px) { + @media not (100px <= width <= 200px) { .foo { color: chartreuse; } } "#, indoc! { r#" - @media (min-width: 100.001px) and (max-width: 199.999px) { + @media not ((min-width: 100px) and (max-width: 200px)) { .foo { color: #7fff00; } @@ -8258,14 +8979,14 @@ mod tests { prefix_test( r#" - @media (200px >= width >= 100px) { + @media (hover) and (100px <= width <= 200px) { .foo { color: chartreuse; } } "#, indoc! { r#" - @media (max-width: 200px) and (min-width: 100px) { + @media (hover) and (min-width: 100px) and (max-width: 200px) { .foo { color: #7fff00; } @@ -8277,12 +8998,96 @@ mod tests { }, ); - test( + prefix_test( r#" - @media not all { - .a { - color: green; - } + @media (hover) or (100px <= width <= 200px) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media (hover) or ((min-width: 100px) and (max-width: 200px)) { + .foo { + color: #7fff00; + } + } + "#}, + Browsers { + firefox: Some(85 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @media (100px < width < 200px) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media (not (max-width: 100px)) and (not (min-width: 200px)) { + .foo { + color: #7fff00; + } + } + "#}, + Browsers { + firefox: Some(85 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @media not (100px < width < 200px) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media not ((not (max-width: 100px)) and (not (min-width: 200px))) { + .foo { + color: #7fff00; + } + } + "#}, + Browsers { + firefox: Some(85 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @media (200px >= width >= 100px) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media (max-width: 200px) and (min-width: 100px) { + .foo { + color: #7fff00; + } + } + "#}, + Browsers { + firefox: Some(85 << 16), + ..Browsers::default() + }, + ); + + test( + r#" + @media not all { + .a { + color: green; + } } "#, "\n", @@ -8295,7 +9100,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: calc(1.001px + 1rem)) { + @media not (max-width: calc(1px + 1rem)) { .foo { color: #ff0; } @@ -8313,7 +9118,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: calc(max(10px, 1rem) + .001px)) { + @media not (max-width: max(10px, 1rem)) { .foo { color: #ff0; } @@ -8331,7 +9136,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: .001px) { + @media not (max-width: 0) { .foo { color: #ff0; } @@ -8385,7 +9190,7 @@ mod tests { } "#, indoc! { r#" - @media (-webkit-min-device-pixel-ratio: 2.001), (min-resolution: 2.001dppx) { + @media not (-webkit-max-device-pixel-ratio: 2), not (max-resolution: 2dppx) { .foo { color: #ff0; } @@ -8491,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, @@ -8535,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] @@ -8569,6 +9409,196 @@ mod tests { } "#}, ); + test( + r#" + @layer a {} + @layer b {} + + @layer b { + foo { + color: red; + } + } + + @layer a { + bar { + color: yellow; + } + } + "#, + indoc! {r#" + @layer a { + bar { + color: #ff0; + } + } + + @layer b { + foo { + color: red; + } + } + "#}, + ); + + test( + r#" + @layer a {} + @layer b {} + + @layer b { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a; + + @layer b { + foo { + color: red; + } + } + "#}, + ); + + test( + r#" + @layer a; + @layer b; + @layer c; + "#, + indoc! {r#" + @layer a, b, c; + "#}, + ); + + test( + r#" + @layer a {} + @layer b {} + @layer c {} + "#, + indoc! {r#" + @layer a, b, c; + "#}, + ); + + test( + r#" + @layer a; + @layer b { + .foo { + color: red; + } + } + @layer c {} + "#, + indoc! {r#" + @layer a; + + @layer b { + .foo { + color: red; + } + } + + @layer c; + "#}, + ); + + test( + r#" + @layer a, b; + @layer c {} + + @layer d { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a, b, c; + + @layer d { + foo { + color: red; + } + } + "#}, + ); + + test( + r#" + @layer a; + @layer b; + @import "a.css" layer(x); + @layer c; + + @layer d { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a, b; + @import "a.css" layer(x); + @layer c; + + @layer d { + foo { + color: red; + } + } + "#}, + ); + + test( + r#" + @layer a, b, c; + + @layer a { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a { + foo { + color: red; + } + } + + @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] @@ -8635,6 +9665,22 @@ mod tests { } "#}, ); + test( + r#" + .foo { + --foo: red; + --foo: purple; + } + .foo { + --foo: green; + } + "#, + indoc! {r#" + .foo { + --foo: green; + } + "#}, + ); test( r#" .foo { @@ -9306,15 +10352,15 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi))::-webkit-file-upload-button { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)))::-webkit-file-upload-button { margin-left: 2px; } - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi))::-ms-browse { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)))::-ms-browse { margin-left: 2px; } - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi))::file-selector-button { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)))::file-selector-button { margin-left: 2px; } @@ -9343,11 +10389,15 @@ mod tests { .foo:autofill .baz { color: red; } "#, indoc! {r#" - .foo:placeholder-shown .bar, .foo:-webkit-autofill .baz { + .foo:placeholder-shown .bar { color: red; } - .foo:placeholder-shown .bar, .foo:autofill .baz { + .foo:-webkit-autofill .baz { + color: red; + } + + .foo:autofill .baz { color: red; } "#}, @@ -9362,11 +10412,11 @@ mod tests { .foo:placeholder-shown .bar,.foo:autofill .baz{color:red} "#, indoc! {r#" - .foo:placeholder-shown .bar, .foo:-webkit-autofill .baz { + :-webkit-any(.foo:placeholder-shown .bar, .foo:-webkit-autofill .baz) { color: red; } - .foo:placeholder-shown .bar, .foo:autofill .baz { + :is(.foo:placeholder-shown .bar, .foo:autofill .baz) { color: red; } "#}, @@ -9387,11 +10437,11 @@ mod tests { } "#, indoc! {r#" - .foo:placeholder-shown .bar, .foo:-webkit-autofill .baz { + :-webkit-any(.foo:placeholder-shown .bar, .foo:-webkit-autofill .baz) { color: red; } - .foo:placeholder-shown .bar, .foo:autofill .baz { + :is(.foo:placeholder-shown .bar, .foo:autofill .baz) { color: red; } "#}, @@ -9761,13 +10811,156 @@ mod tests { } "#}, Browsers { - safari: Some(14 << 16), + chrome: Some(33 << 16), ..Browsers::default() }, ); - } - #[test] + test( + r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#, + indoc! {r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#}, + ); + + prefix_test( + r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#, + indoc! {r#" + .foo:is(.bar, .baz):after { + color: red; + } + "#}, + Browsers { + safari: Some(16 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo:-webkit-any(.bar):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#, + indoc! {r#" + .foo:-webkit-any(.bar):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#}, + Browsers { + safari: Some(16 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#, + indoc! {r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:is(.bar, .baz):after { + color: red; + } + "#}, + Browsers { + safari: Some(12 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:-moz-any(.bar, .baz):after { + color: red; + } + "#, + indoc! {r#" + .foo:-webkit-any(.bar, .baz):after { + color: red; + } + + .foo:-moz-any(.bar, .baz):after { + color: red; + } + "#}, + Browsers { + safari: Some(12 << 16), + firefox: Some(67 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .a { + padding-inline: var(--foo); + } + + .a:-webkit-any(.b, .c) { + padding-inline: var(--foo); + } + "#, + indoc! {r#" + .a { + padding-inline: var(--foo); + } + + .a:-webkit-any(.b, .c) { + padding-inline: var(--foo); + } + "#}, + Browsers { + safari: Some(12 << 16), + ..Browsers::default() + }, + ); + } + + #[test] fn test_merge_media_rules() { test( r#" @@ -10323,7 +11516,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + transition-property: margin-left; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { transition-property: margin-left; } @@ -10349,7 +11546,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + transition-property: margin-left, padding-left; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { transition-property: margin-left, padding-left; } @@ -10375,7 +11576,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + transition-property: margin-left, opacity, padding-left, color; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { transition-property: margin-left, opacity, padding-left, color; } @@ -10419,7 +11624,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + transition: margin-left 2s; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { transition: margin-left 2s; } @@ -10445,7 +11654,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + transition: margin-left 2s, padding-left 2s; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { transition: margin-left 2s, padding-left 2s; } @@ -10508,7 +11721,7 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { -webkit-transition: -webkit-border-top-left-radius, border-top-left-radius; transition: -webkit-border-top-left-radius, border-top-left-radius; } @@ -10584,6 +11797,77 @@ mod tests { ..Browsers::default() }, ); + prefix_test( + r#" + .foo { + transition-property: -webkit-backdrop-filter, backdrop-filter; + } + .bar { + transition-property: backdrop-filter; + } + .baz { + transition-property: -webkit-backdrop-filter; + } + "#, + indoc! {r#" + .foo, .bar { + transition-property: -webkit-backdrop-filter, backdrop-filter; + } + + .baz { + transition-property: -webkit-backdrop-filter; + } + "# + }, + Browsers { + safari: Some(15 << 16), + ..Browsers::default() + }, + ); + prefix_test( + r#" + .foo { + transition-property: -webkit-border-radius, -webkit-border-radius, -moz-border-radius; + } + "#, + indoc! {r#" + .foo { + transition-property: -webkit-border-radius, -moz-border-radius; + } + "# + }, + Browsers { + safari: Some(15 << 16), + ..Browsers::default() + }, + ); + prefix_test( + r#" + .foo { + transition: -webkit-backdrop-filter, backdrop-filter; + } + .bar { + transition: backdrop-filter; + } + .baz { + transition: -webkit-backdrop-filter; + } + "#, + indoc! {r#" + .foo, .bar { + transition: -webkit-backdrop-filter, backdrop-filter; + } + + .baz { + transition: -webkit-backdrop-filter; + } + "# + }, + Browsers { + safari: Some(15 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -10760,6 +12044,46 @@ mod tests { ".foo { animation: foo 0s 3s infinite }", ".foo{animation:0s 3s infinite foo}", ); + minify_test(".foo { animation: foo 3s --test }", ".foo{animation:3s foo --test}"); + minify_test(".foo { animation: foo 3s scroll() }", ".foo{animation:3s foo scroll()}"); + minify_test( + ".foo { animation: foo 3s scroll(block) }", + ".foo{animation:3s foo scroll()}", + ); + minify_test( + ".foo { animation: foo 3s scroll(root inline) }", + ".foo{animation:3s foo scroll(root inline)}", + ); + minify_test( + ".foo { animation: foo 3s scroll(inline root) }", + ".foo{animation:3s foo scroll(root inline)}", + ); + minify_test( + ".foo { animation: foo 3s scroll(inline nearest) }", + ".foo{animation:3s foo scroll(inline)}", + ); + minify_test( + ".foo { animation: foo 3s view(block) }", + ".foo{animation:3s foo view()}", + ); + minify_test( + ".foo { animation: foo 3s view(inline) }", + ".foo{animation:3s foo view(inline)}", + ); + minify_test( + ".foo { animation: foo 3s view(inline 10px 10px) }", + ".foo{animation:3s foo view(inline 10px)}", + ); + minify_test( + ".foo { animation: foo 3s view(inline 10px 12px) }", + ".foo{animation:3s foo view(inline 10px 12px)}", + ); + minify_test( + ".foo { animation: foo 3s view(inline auto auto) }", + ".foo{animation:3s foo view(inline)}", + ); + minify_test(".foo { animation: foo 3s auto }", ".foo{animation:3s foo}"); + minify_test(".foo { animation-composition: add }", ".foo{animation-composition:add}"); test( r#" .foo { @@ -10771,6 +12095,7 @@ mod tests { animation-play-state: running; animation-delay: 100ms; animation-fill-mode: forwards; + animation-timeline: auto; } "#, indoc! {r#" @@ -10790,6 +12115,7 @@ mod tests { animation-play-state: running, paused; animation-delay: 100ms, 0s; animation-fill-mode: forwards, none; + animation-timeline: auto, auto; } "#, indoc! {r#" @@ -10836,6 +12162,7 @@ mod tests { animation-play-state: running; animation-delay: 100ms; animation-fill-mode: forwards; + animation-timeline: auto; } "#, indoc! {r#" @@ -10848,6 +12175,55 @@ mod tests { animation-play-state: running; animation-delay: .1s; animation-fill-mode: forwards; + animation-timeline: auto; + } + "#}, + ); + test( + r#" + .foo { + animation-name: foo; + animation-duration: 0.09s; + animation-timing-function: ease-in-out; + animation-iteration-count: 2; + animation-direction: alternate; + animation-play-state: running; + animation-delay: 100ms; + animation-fill-mode: forwards; + animation-timeline: scroll(); + } + "#, + indoc! {r#" + .foo { + animation: 90ms ease-in-out .1s 2 alternate forwards foo scroll(); + } + "#}, + ); + test( + r#" + .foo { + animation-name: foo; + animation-duration: 0.09s; + animation-timing-function: ease-in-out; + animation-iteration-count: 2; + animation-direction: alternate; + animation-play-state: running; + animation-delay: 100ms; + animation-fill-mode: forwards; + animation-timeline: scroll(), view(); + } + "#, + indoc! {r#" + .foo { + animation-name: foo; + animation-duration: 90ms; + animation-timing-function: ease-in-out; + animation-iteration-count: 2; + animation-direction: alternate; + animation-play-state: running; + animation-delay: .1s; + animation-fill-mode: forwards; + animation-timeline: scroll(), view(); } "#}, ); @@ -10958,25 +12334,349 @@ mod tests { ..Browsers::default() }, ); - } - #[test] - fn test_transform() { - minify_test( - ".foo { transform: translate(2px, 3px)", - ".foo{transform:translate(2px,3px)}", - ); - minify_test( - ".foo { transform: translate(2px, 0px)", - ".foo{transform:translate(2px)}", - ); - minify_test( - ".foo { transform: translate(0px, 2px)", - ".foo{transform:translateY(2px)}", + prefix_test( + r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#, + indoc! {r#" + .foo { + animation: .2s ease-in-out bar; + animation-timeline: scroll(); + } + "#}, + Browsers { + safari: Some(16 << 16), + ..Browsers::default() + }, ); - minify_test(".foo { transform: translateX(2px)", ".foo{transform:translate(2px)}"); - minify_test(".foo { transform: translateY(2px)", ".foo{transform:translateY(2px)}"); - minify_test(".foo { transform: translateZ(2px)", ".foo{transform:translateZ(2px)}"); + prefix_test( + r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#, + indoc! {r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#}, + Browsers { + chrome: Some(120 << 16), + ..Browsers::default() + }, + ); + prefix_test( + r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#, + indoc! {r#" + .foo { + -webkit-animation: .2s ease-in-out bar; + animation: .2s ease-in-out bar; + animation-timeline: scroll(); + } + "#}, + Browsers { + safari: Some(6 << 16), + ..Browsers::default() + }, + ); + + minify_test( + ".foo { animation-range-start: entry 10% }", + ".foo{animation-range-start:entry 10%}", + ); + minify_test( + ".foo { animation-range-start: entry 0% }", + ".foo{animation-range-start:entry}", + ); + minify_test( + ".foo { animation-range-start: entry }", + ".foo{animation-range-start:entry}", + ); + minify_test(".foo { animation-range-start: 50% }", ".foo{animation-range-start:50%}"); + minify_test( + ".foo { animation-range-end: exit 10% }", + ".foo{animation-range-end:exit 10%}", + ); + minify_test( + ".foo { animation-range-end: exit 100% }", + ".foo{animation-range-end:exit}", + ); + minify_test(".foo { animation-range-end: exit }", ".foo{animation-range-end:exit}"); + minify_test(".foo { animation-range-end: 50% }", ".foo{animation-range-end:50%}"); + minify_test( + ".foo { animation-range: entry 10% exit 90% }", + ".foo{animation-range:entry 10% exit 90%}", + ); + minify_test( + ".foo { animation-range: entry 0% exit 100% }", + ".foo{animation-range:entry exit}", + ); + minify_test(".foo { animation-range: entry }", ".foo{animation-range:entry}"); + minify_test( + ".foo { animation-range: entry 0% entry 100% }", + ".foo{animation-range:entry}", + ); + minify_test(".foo { animation-range: 50% normal }", ".foo{animation-range:50%}"); + minify_test( + ".foo { animation-range: normal normal }", + ".foo{animation-range:normal}", + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 0%; + animation-range-end: entry 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 0%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: normal; + } + "#, + indoc! {r#" + .foo { + animation-range: 10%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%, exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit 90%, entry 50% exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + animation: spin 100ms; + } + "#, + indoc! {r#" + .foo { + animation: .1s spin; + } + "#}, + ); + test( + r#" + .foo { + animation: spin 100ms; + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation: .1s spin; + animation-range: entry 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + animation: var(--animation) 100ms; + } + "#, + indoc! {r#" + .foo { + animation: var(--animation) .1s; + } + "#}, + ); + } + + #[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)}", + ); + minify_test( + ".foo { transform: translate(2px, 0px)", + ".foo{transform:translate(2px)}", + ); + minify_test( + ".foo { transform: translate(0px, 2px)", + ".foo{transform:translateY(2px)}", + ); + minify_test(".foo { transform: translateX(2px)", ".foo{transform:translate(2px)}"); + minify_test(".foo { transform: translateY(2px)", ".foo{transform:translateY(2px)}"); + minify_test(".foo { transform: translateZ(2px)", ".foo{transform:translateZ(2px)}"); minify_test( ".foo { transform: translate3d(2px, 3px, 4px)", ".foo{transform:translate3d(2px,3px,4px)}", @@ -11070,14 +12770,16 @@ mod tests { ".foo { transform: matrix3d(1, 0, 0, 0, 0, 1, 6, 0, 0, 0, 1, 0, 50, 100, 0, 1.1)", ".foo{transform:matrix3d(1,0,0,0,0,1,6,0,0,0,1,0,50,100,0,1.1)}", ); - minify_test( - ".foo{transform:translate(100px,200px) rotate(45deg) skew(10deg) scale(2)}", - ".foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)}", - ); - minify_test( - ".foo{transform:translate(200px,300px) translate(100px,200px) scale(2)}", - ".foo{transform:matrix(2,0,0,2,300,500)}", - ); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test( + // ".foo{transform:translate(100px,200px) rotate(45deg) skew(10deg) scale(2)}", + // ".foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)}", + // ); + // minify_test( + // ".foo{transform:translate(200px,300px) translate(100px,200px) scale(2)}", + // ".foo{transform:matrix(2,0,0,2,300,500)}", + // ); minify_test( ".foo{transform:translate(100px,200px) rotate(45deg)}", ".foo{transform:translate(100px,200px)rotate(45deg)}", @@ -11086,34 +12788,36 @@ mod tests { ".foo{transform:rotate3d(1, 1, 1, 45deg) translate3d(100px, 100px, 10px)}", ".foo{transform:rotate3d(1,1,1,45deg)translate3d(100px,100px,10px)}", ); - minify_test( - ".foo{transform:translate3d(100px, 100px, 10px) skew(10deg) scale3d(2, 3, 4)}", - ".foo{transform:matrix3d(2,0,0,0,.528981,3,0,0,0,0,4,0,100,100,10,1)}", - ); - minify_test( - ".foo{transform:matrix3d(0.804737854124365, 0.5058793634016805, -0.31061721752604554, 0, -0.31061721752604554, 0.804737854124365, 0.5058793634016805, 0, 0.5058793634016805, -0.31061721752604554, 0.804737854124365, 0, 100, 100, 10, 1)}", - ".foo{transform:translate3d(100px,100px,10px)rotate3d(1,1,1,45deg)}" - ); - minify_test( - ".foo{transform:matrix3d(1, 0, 0, 0, 0, 0.7071067811865476, 0.7071067811865475, 0, 0, -0.7071067811865475, 0.7071067811865476, 0, 100, 100, 10, 1)}", - ".foo{transform:translate3d(100px,100px,10px)rotateX(45deg)}" - ); - minify_test( - ".foo{transform:translate3d(100px, 200px, 10px) translate(100px, 100px)}", - ".foo{transform:translate3d(200px,300px,10px)}", - ); - minify_test( - ".foo{transform:rotate(45deg) rotate(45deg)}", - ".foo{transform:rotate(90deg)}", - ); - minify_test( - ".foo{transform:matrix(0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 100, 100)}", - ".foo{transform:translate(100px,100px)rotate(45deg)}" - ); - minify_test( - ".foo{transform:translateX(2in) translateX(50px)}", - ".foo{transform:translate(242px)}", - ); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test( + // ".foo{transform:translate3d(100px, 100px, 10px) skew(10deg) scale3d(2, 3, 4)}", + // ".foo{transform:matrix3d(2,0,0,0,.528981,3,0,0,0,0,4,0,100,100,10,1)}", + // ); + // minify_test( + // ".foo{transform:matrix3d(0.804737854124365, 0.5058793634016805, -0.31061721752604554, 0, -0.31061721752604554, 0.804737854124365, 0.5058793634016805, 0, 0.5058793634016805, -0.31061721752604554, 0.804737854124365, 0, 100, 100, 10, 1)}", + // ".foo{transform:translate3d(100px,100px,10px)rotate3d(1,1,1,45deg)}" + // ); + // minify_test( + // ".foo{transform:matrix3d(1, 0, 0, 0, 0, 0.7071067811865476, 0.7071067811865475, 0, 0, -0.7071067811865475, 0.7071067811865476, 0, 100, 100, 10, 1)}", + // ".foo{transform:translate3d(100px,100px,10px)rotateX(45deg)}" + // ); + // minify_test( + // ".foo{transform:translate3d(100px, 200px, 10px) translate(100px, 100px)}", + // ".foo{transform:translate3d(200px,300px,10px)}", + // ); + // minify_test( + // ".foo{transform:rotate(45deg) rotate(45deg)}", + // ".foo{transform:rotate(90deg)}", + // ); + // minify_test( + // ".foo{transform:matrix(0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 100, 100)}", + // ".foo{transform:translate(100px,100px)rotate(45deg)}" + // ); + // minify_test( + // ".foo{transform:translateX(2in) translateX(50px)}", + // ".foo{transform:translate(242px)}", + // ); minify_test( ".foo{transform:translateX(calc(2in + 50px))}", ".foo{transform:translate(242px)}", @@ -11159,26 +12863,43 @@ mod tests { minify_test(".foo { translate: 1px 0px 0px }", ".foo{translate:1px}"); 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:0}"); + 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}"); - minify_test(".foo { scale: none }", ".foo{scale:1}"); + minify_test(".foo { scale: none }", ".foo{scale:none}"); minify_test(".foo { scale: 1 0 }", ".foo{scale:1 0}"); minify_test(".foo { scale: 1 0 1 }", ".foo{scale:1 0}"); minify_test(".foo { scale: 1 0 0 }", ".foo{scale:1 0 0}"); - minify_test(".foo { transform: scale(3); scale: 0.5 }", ".foo{transform:scale(1.5)}"); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test(".foo { transform: scale(3); scale: 0.5 }", ".foo{transform:scale(1.5)}"); minify_test(".foo { scale: 0.5; transform: scale(3); }", ".foo{transform:scale(3)}"); prefix_test( @@ -11591,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); } "#}, @@ -11609,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); } "#}, @@ -11627,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); } "#}, @@ -11645,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); } "#}, @@ -11663,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); } "#}, @@ -11681,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); } "#}, @@ -11699,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); } "#}, @@ -11733,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); } "#}, @@ -11857,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"); } "#}, @@ -11939,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)); } @@ -11955,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)); } @@ -12026,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)); } @@ -12042,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)); } @@ -12078,6 +13799,357 @@ mod tests { ..Browsers::default() }, ); + + // Test cases from https://github.com/postcss/autoprefixer/blob/541295c0e6dd348db2d3f52772b59cd403c59d29/test/cases/gradient.css + prefix_test( + r#" + a { + background: linear-gradient(350.5deg, white, black), linear-gradient(-130deg, black, white), linear-gradient(45deg, black, white); + } + b { + background-image: linear-gradient(rgba(0,0,0,1), white), linear-gradient(white, black); + } + strong { + background: linear-gradient(to top, transparent, rgba(0, 0, 0, 0.8) 20px, #000 30px, #000) no-repeat; + } + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + .simple1 { + background: linear-gradient(black, white); + } + .simple2 { + background: linear-gradient(to left, black 0%, rgba(0, 0, 0, 0.5)50%, white 100%); + } + .simple3 { + background: linear-gradient(to left, black 50%, white 100%); + } + .simple4 { + background: linear-gradient(to right top, black, white); + } + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, 0.5), white); + } + .silent { + background: -webkit-linear-gradient(top left, black, white); + } + .radial { + background: radial-gradient(farthest-side at 0 50%, white, black); + } + .second { + background: red linear-gradient(red, blue); + background: url('logo.png'), linear-gradient(#fff, #000); + } + .px { + background: linear-gradient(black 0, white 100px); + } + .list { + list-style-image: linear-gradient(white, black); + } + .mask { + mask: linear-gradient(white, black); + } + .newline { + background-image: + linear-gradient( white, black ), + linear-gradient( black, white ); + } + .convert { + background: linear-gradient(0deg, white, black); + background: linear-gradient(90deg, white, black); + background: linear-gradient(180deg, white, black); + background: linear-gradient(270deg, white, black); + } + .grad { + background: linear-gradient(1grad, white, black); + } + .rad { + background: linear-gradient(1rad, white, black); + } + .turn { + background: linear-gradient(0.3turn, white, black); + } + .norm { + background: linear-gradient(-90deg, white, black); + } + .mask { + mask-image: radial-gradient(circle at 86% 86%, transparent 8px, black 8px); + } + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + .contain { + background: radial-gradient(contain at center, white, black); + } + .no-div { + background: linear-gradient(black); + } + .background-shorthand { + background: radial-gradient(#FFF, transparent) 0 0 / cover no-repeat #F0F; + } + .background-advanced { + background: radial-gradient(ellipse farthest-corner at 5px 15px, rgba(214, 168, 18, 0.7) 0%, rgba(255, 21, 177, 0.7) 50%, rgba(210, 7, 148, 0.7) 95%), + radial-gradient(#FFF, transparent), + url(path/to/image.jpg) 50%/cover; + } + .multiradial { + mask-image: radial-gradient(circle closest-corner at 100% 50%, #000, transparent); + } + .broken { + mask-image: radial-gradient(white, black); + } + .loop { + background-image: url("https://test.com/lol(test.png"), radial-gradient(yellow, black, yellow); + } + .unitless-zero { + background-image: linear-gradient(0, green, blue); + background: repeating-linear-gradient(0, blue, red 33.3%) + } + .zero-grad { + background: linear-gradient(0grad, green, blue); + background-image: repeating-linear-gradient(0grad, blue, red 33.3%) + } + .zero-rad { + background: linear-gradient(0rad, green, blue); + } + .zero-turn { + background: linear-gradient(0turn, green, blue); + } + "#, + indoc! { r#" + a { + background: -webkit-linear-gradient(99.5deg, #fff, #000), -webkit-linear-gradient(220deg, #000, #fff), -webkit-linear-gradient(45deg, #000, #fff); + background: -o-linear-gradient(99.5deg, #fff, #000), -o-linear-gradient(220deg, #000, #fff), -o-linear-gradient(45deg, #000, #fff); + background: linear-gradient(350.5deg, #fff, #000), linear-gradient(-130deg, #000, #fff), linear-gradient(45deg, #000, #fff); + } + + b { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)), -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + background-image: -webkit-linear-gradient(top, #000, #fff), -webkit-linear-gradient(top, #fff, #000); + background-image: -o-linear-gradient(top, #000, #fff), -o-linear-gradient(top, #fff, #000); + background-image: linear-gradient(#000, #fff), linear-gradient(#fff, #000); + } + + strong { + background: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: -o-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + } + + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + + .simple1 { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background: -webkit-linear-gradient(top, #000, #fff); + background: -o-linear-gradient(top, #000, #fff); + background: linear-gradient(#000, #fff); + } + + .simple2 { + background: -webkit-gradient(linear, 100% 0, 0 0, from(#000), color-stop(.5, rgba(0, 0, 0, .5)), to(#fff)); + background: -webkit-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: -o-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: linear-gradient(to left, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + } + + .simple3 { + background: -webkit-gradient(linear, 100% 0, 0 0, color-stop(.5, #000), to(#fff)); + background: -webkit-linear-gradient(right, #000 50%, #fff 100%); + background: -o-linear-gradient(right, #000 50%, #fff 100%); + background: linear-gradient(to left, #000 50%, #fff 100%); + } + + .simple4 { + background: -webkit-gradient(linear, 0 100%, 100% 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(bottom left, #000, #fff); + background: -o-linear-gradient(bottom left, #000, #fff); + background: linear-gradient(to top right, #000, #fff); + } + + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, .5), white); + } + + .silent { + background: -webkit-gradient(linear, 100% 100%, 0 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(top left, #000, #fff); + } + + .radial { + background: -webkit-radial-gradient(farthest-side at 0, #fff, #000); + background: -o-radial-gradient(farthest-side at 0, #fff, #000); + background: radial-gradient(farthest-side at 0, #fff, #000); + } + + .second { + background: red -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f)); + background: red -webkit-linear-gradient(top, red, #00f); + background: red -o-linear-gradient(top, red, #00f); + background: red linear-gradient(red, #00f); + background: url("logo.png"), linear-gradient(#fff, #000); + } + + .px { + background: -webkit-linear-gradient(top, #000 0, #fff 100px); + background: -o-linear-gradient(top, #000 0, #fff 100px); + background: linear-gradient(#000 0, #fff 100px); + } + + .list { + list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + list-style-image: -webkit-linear-gradient(top, #fff, #000); + list-style-image: -o-linear-gradient(top, #fff, #000); + list-style-image: linear-gradient(#fff, #000); + } + + .mask { + -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + -webkit-mask: -webkit-linear-gradient(top, #fff, #000); + -webkit-mask: -o-linear-gradient(top, #fff, #000); + mask: -o-linear-gradient(top, #fff, #000); + -webkit-mask: linear-gradient(#fff, #000); + mask: linear-gradient(#fff, #000); + } + + .newline { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)), -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background-image: -webkit-linear-gradient(top, #fff, #000), -webkit-linear-gradient(top, #000, #fff); + background-image: -o-linear-gradient(top, #fff, #000), -o-linear-gradient(top, #000, #fff); + background-image: linear-gradient(#fff, #000), linear-gradient(#000, #fff); + } + + .convert { + background: -webkit-gradient(linear, 0 100%, 0 0, from(#fff), to(#000)); + background: -webkit-linear-gradient(90deg, #fff, #000); + background: -o-linear-gradient(90deg, #fff, #000); + background: linear-gradient(0deg, #fff, #000); + background: linear-gradient(90deg, #fff, #000); + background: linear-gradient(#fff, #000); + background: linear-gradient(270deg, #fff, #000); + } + + .grad { + background: -webkit-linear-gradient(89.1deg, #fff, #000); + background: -o-linear-gradient(89.1deg, #fff, #000); + background: linear-gradient(1grad, #fff, #000); + } + + .rad { + background: -webkit-linear-gradient(32.704deg, #fff, #000); + background: -o-linear-gradient(32.704deg, #fff, #000); + background: linear-gradient(57.2958deg, #fff, #000); + } + + .turn { + background: -webkit-linear-gradient(342deg, #fff, #000); + background: -o-linear-gradient(342deg, #fff, #000); + background: linear-gradient(.3turn, #fff, #000); + } + + .norm { + background: -webkit-linear-gradient(#fff, #000); + background: -o-linear-gradient(#fff, #000); + background: linear-gradient(-90deg, #fff, #000); + } + + .mask { + -webkit-mask-image: -webkit-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + } + + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + + .contain { + background: radial-gradient(contain at center, white, black); + } + + .no-div { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000)); + background: -webkit-linear-gradient(top, #000); + background: -o-linear-gradient(top, #000); + background: linear-gradient(#000); + } + + .background-shorthand { + background: #f0f -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f -o-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + } + + .background-advanced { + background: url("path/to/image.jpg") 50% / cover; + background: -webkit-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: -o-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -o-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + } + + .multiradial { + -webkit-mask-image: -webkit-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + } + + .broken { + -webkit-mask-image: -webkit-radial-gradient(#fff, #000); + -webkit-mask-image: -o-radial-gradient(#fff, #000); + mask-image: -o-radial-gradient(#fff, #000); + -webkit-mask-image: radial-gradient(#fff, #000); + mask-image: radial-gradient(#fff, #000); + } + + .loop { + background-image: url("https://test.com/lol(test.png"); + background-image: url("https://test.com/lol(test.png"), -webkit-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), -o-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), radial-gradient(#ff0, #000, #ff0); + } + + .unitless-zero { + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background-image: -webkit-linear-gradient(90deg, green, #00f); + background-image: -o-linear-gradient(90deg, green, #00f); + background-image: linear-gradient(0deg, green, #00f); + background: repeating-linear-gradient(0deg, #00f, red 33.3%); + } + + .zero-grad { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0grad, green, #00f); + background-image: repeating-linear-gradient(0grad, #00f, red 33.3%); + } + + .zero-rad, .zero-turn { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0deg, green, #00f); + } + "#}, + Browsers { + chrome: Some(25 << 16), + opera: Some(12 << 16), + android: Some(2 << 16 | 3 << 8), + ..Browsers::default() + }, + ); } #[test] @@ -12148,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 @@ -12170,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}"); @@ -12268,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; @@ -12312,9 +14384,149 @@ mod tests { ..Browsers::default() }, ); + prefix_test( + r#"@supports (color: lab(0% 0 0)) { + @font-palette-values --Cooler { + font-family: Handover Sans; + base-palette: 3; + override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078); + } + }"#, + indoc! {r#"@supports (color: lab(0% 0 0)) { + @font-palette-values --Cooler { + font-family: Handover Sans; + base-palette: 3; + override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); minify_test(".foo { font-palette: --Custom; }", ".foo{font-palette:--Custom}"); } + #[test] + fn test_font_feature_values() { + // https://github.com/clagnut/TODS/blob/e693d52ad411507b960cf01a9734265e3efab102/tods.css#L116-L142 + minify_test( + r#" +@font-feature-values "Fancy Font Name" { + @styleset { cursive: 1; swoopy: 7 16; } + @character-variant { ampersand: 1; capital-q: 2; } + @stylistic { two-story-g: 1; straight-y: 2; } + @swash { swishy: 1; flowing: 2; } + @ornaments { clover: 1; fleuron: 2; } + @annotation { circled: 1; boxed: 2; } +} + "#, + r#"@font-feature-values Fancy Font Name{@styleset{cursive:1;swoopy:7 16}@character-variant{ampersand:1;capital-q:2}@stylistic{two-story-g:1;straight-y:2}@swash{swishy:1;flowing:2}@ornaments{clover:1;fleuron:2}@annotation{circled:1;boxed:2}}"#, + ); + + // https://github.com/Sorixelle/srxl.me/blob/4eb4f4a15cb2d21356df24c096d6a819cfdc1a99/public/fonts/inter/inter.css#L201-L222 + minify_test( + r#" +@font-feature-values "Inter", "Inter var", "Inter var experimental" { + @styleset { + open-digits: 1; + disambiguation: 2; + curved-r: 3; + disambiguation-without-zero: 4; + } + + @character-variant { + alt-one: 1; + open-four: 2; + open-six: 3; + open-nine: 4; + lower-l-with-tail: 5; + curved-lower-r: 6; + german-double-s: 7; + upper-i-with-serif: 8; + flat-top-three: 9; + upper-g-with-spur: 10; + single-storey-a: 11; + } +} + "#, + r#"@font-feature-values Inter,Inter var,Inter var experimental{@styleset{open-digits:1;disambiguation:2;curved-r:3;disambiguation-without-zero:4}@character-variant{alt-one:1;open-four:2;open-six:3;open-nine:4;lower-l-with-tail:5;curved-lower-r:6;german-double-s:7;upper-i-with-serif:8;flat-top-three:9;upper-g-with-spur:10;single-storey-a:11}}"#, + ); + + // https://github.com/MihailJP/Inconsolata-LGC/blob/7c53cf455787096c93d82d9a51018f12ec39a6e9/Inconsolata-LGC.css#L65-L91 + minify_test( + r#" +@font-feature-values "Inconsolata LGC" { + @styleset { + alternative-umlaut: 1; + } + @character-variant { + zero-plain: 1 1; + zero-dotted: 1 2; + zero-longslash: 1 3; + r-with-serif: 2 1; + eng-descender: 3 1; + eng-uppercase: 3 2; + dollar-open: 4 1; + dollar-oldstyle: 4 2; + dollar-cifrao: 4 2; + ezh-no-descender: 5 1; + ezh-reversed-sigma: 5 2; + triangle-text-form: 6 1; + el-with-hook-old: 7 1; + qa-enlarged-lowercase: 8 1; + qa-reversed-p: 8 2; + che-with-hook: 9 1; + che-with-hook-alt: 9 2; + ge-with-hook: 10 1; + ge-with-hook-alt: 10 2; + ge-with-stroke-and-descender: 11 1; + } +} + "#, + r#"@font-feature-values Inconsolata LGC{@styleset{alternative-umlaut:1}@character-variant{zero-plain:1 1;zero-dotted:1 2;zero-longslash:1 3;r-with-serif:2 1;eng-descender:3 1;eng-uppercase:3 2;dollar-open:4 1;dollar-oldstyle:4 2;dollar-cifrao:4 2;ezh-no-descender:5 1;ezh-reversed-sigma:5 2;triangle-text-form:6 1;el-with-hook-old:7 1;qa-enlarged-lowercase:8 1;qa-reversed-p:8 2;che-with-hook:9 1;che-with-hook-alt:9 2;ge-with-hook:10 1;ge-with-hook-alt:10 2;ge-with-stroke-and-descender:11 1}}"#, + ); + + minify_test( + r#" + @font-feature-values "Fancy Font Name" { + @styleset { cursive: 1; swoopy: 7 16; } + @character-variant { ampersand: 1; capital-q: 2; } + } + "#, + r#"@font-feature-values Fancy Font Name{@styleset{cursive:1;swoopy:7 16}@character-variant{ampersand:1;capital-q:2}}"#, + ); + minify_test( + r#" + @font-feature-values foo { + @swash { pretty: 0; pretty: 1; cool: 2; } + } + "#, + "@font-feature-values foo{@swash{pretty:1;cool:2}}", + ); + minify_test( + r#" + @font-feature-values foo { + @swash { pretty: 1; } + @swash { cool: 2; } + } + "#, + "@font-feature-values foo{@swash{pretty:1;cool:2}}", + ); + minify_test( + r#" + @font-feature-values foo { + @swash { pretty: 1; } + } + @font-feature-values foo { + @swash { cool: 2; } + } + "#, + "@font-feature-values foo{@swash{pretty:1;cool:2}}", + ); + } + #[test] fn test_page_rule() { minify_test("@page {margin: 0.5cm}", "@page{margin:.5cm}"); @@ -12635,44 +14847,106 @@ mod tests { ..Default::default() }, ); - minify_test( + prefix_test( r#" - @supports (width: calc(10px * 2)) { - .test { - width: calc(10px * 2); + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); } } "#, - "@supports (width:calc(10px * 2)){.test{width:20px}}", - ); - minify_test( - r#" - @supports (color: hsl(0deg, 0%, 0%)) { - .test { - color: hsl(0deg, 0%, 0%); + indoc! { r#" + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); } } - "#, - "@supports (color:hsl(0deg, 0%, 0%)){.test{color:#000}}", + "#}, + Browsers { + safari: Some(14 << 16), + ..Default::default() + }, ); - } - - #[test] - fn test_counter_style() { - test( + prefix_test( r#" - @counter-style circled-alpha { - system: fixed; - symbols: Ⓐ Ⓑ Ⓒ; - suffix: " "; + @supports ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } } "#, indoc! { r#" - @counter-style circled-alpha { - system: fixed; - symbols: Ⓐ Ⓑ Ⓒ; - suffix: " "; - } + @supports ((-webkit-backdrop-filter: blur(20px))) or ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Default::default() + }, + ); + prefix_test( + r#" + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } + } + "#, + indoc! { r#" + @supports (backdrop-filter: blur(10px)) { + div { + backdrop-filter: blur(10px); + } + } + "#}, + Browsers { + chrome: Some(80 << 16), + ..Default::default() + }, + ); + minify_test( + r#" + @supports (width: calc(10px * 2)) { + .test { + width: calc(10px * 2); + } + } + "#, + "@supports (width:calc(10px * 2)){.test{width:20px}}", + ); + minify_test( + r#" + @supports (color: hsl(0deg, 0%, 0%)) { + .test { + color: hsl(0deg, 0%, 0%); + } + } + "#, + "@supports (color:hsl(0deg, 0%, 0%)){.test{color:#000}}", + ); + } + + #[test] + fn test_counter_style() { + test( + r#" + @counter-style circled-alpha { + system: fixed; + symbols: Ⓐ Ⓑ Ⓒ; + suffix: " "; + } + "#, + indoc! { r#" + @counter-style circled-alpha { + system: fixed; + symbols: Ⓐ Ⓑ Ⓒ; + suffix: " "; + } "#}, ); } @@ -12838,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] @@ -13270,7 +15546,7 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { text-align: left; } @@ -13292,7 +15568,7 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { text-align: right; } @@ -13332,7 +15608,7 @@ mod tests { } "#, indoc! {r#" - .foo > .bar:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo > .bar:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { text-align: left; } @@ -13354,7 +15630,7 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)):after { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):after { text-align: left; } @@ -13376,7 +15652,7 @@ mod tests { } "#, indoc! {r#" - .foo:hover:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:hover:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { text-align: left; } @@ -13940,6 +16216,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -14307,6 +16604,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-emphasis: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-emphasis: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + safari: Some(8 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -14394,6 +16712,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-shadow: var(--foo) 12px lab(40% 56.6 39); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-shadow: var(--foo) 12px lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -14608,7 +16947,11 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + left: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { left: 2px; } @@ -14635,7 +16978,12 @@ mod tests { } "#, indoc! {r#" - .foo:not(:lang(ae)):not(:lang(ar)):not(:lang(arc)):not(:lang(bcc)):not(:lang(bqi)):not(:lang(ckb)):not(:lang(dv)):not(:lang(fa)):not(:lang(glk)):not(:lang(he)):not(:lang(ku)):not(:lang(mzn)):not(:lang(nqo)):not(:lang(pnb)):not(:lang(ps)):not(:lang(sd)):not(:lang(ug)):not(:lang(ur)):not(:lang(yi)) { + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + left: 2px; + right: 4px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { left: 2px; right: 4px; } @@ -15095,6 +17443,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + caret: lab(50.998% 125.506 -50.7078) var(--foo); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + caret: lab(50.998% 125.506 -50.7078) var(--foo); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -15127,7 +17496,18 @@ mod tests { ); minify_test( ".foo { list-style: \"★\" url(ellipse.png) outside; }", - ".foo{list-style:\"★\" url(ellipse.png)}", + ".foo{list-style:url(ellipse.png) \"★\"}", + ); + minify_test(".foo { list-style: none; }", ".foo{list-style:none}"); + minify_test(".foo { list-style: none none outside; }", ".foo{list-style:none}"); + minify_test(".foo { list-style: none none inside; }", ".foo{list-style:inside none}"); + minify_test(".foo { list-style: none inside; }", ".foo{list-style:inside none}"); + minify_test(".foo { list-style: none disc; }", ".foo{list-style:outside}"); + minify_test(".foo { list-style: none inside disc; }", ".foo{list-style:inside}"); + minify_test(".foo { list-style: none \"★\"; }", ".foo{list-style:\"★\"}"); + minify_test( + ".foo { list-style: none url(foo.png); }", + ".foo{list-style:url(foo.png) none}", ); test( @@ -15168,7 +17548,7 @@ mod tests { "#, indoc! {r#" .foo { - list-style: \"★\" url("ellipse.png"); + list-style: url("ellipse.png") \"★\"; list-style-image: var(--img); } "#}, @@ -15179,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)); } @@ -15194,8 +17574,8 @@ mod tests { ".foo { list-style: \"★\" linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", indoc! { r#" .foo { - list-style: "★" linear-gradient(#ff0f0e, #7773ff); - list-style: "★" linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + list-style: linear-gradient(#ff0f0e, #7773ff) "★"; + list-style: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) "★"; } "#}, Browsers { @@ -15222,6 +17602,54 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + test( + r#" + .foo { + list-style: inside; + list-style-type: disc; + } + "#, + indoc! {r#" + .foo { + list-style: inside; + } + "#}, + ); + test( + r#" + .foo { + list-style: inside; + list-style-type: decimal; + } + "#, + indoc! {r#" + .foo { + list-style: inside decimal; + } + "#}, + ); } #[test] @@ -15422,22 +17850,33 @@ 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}"); minify_test(".foo { color: hsla(100 100% 50% / .8) }", ".foo{color:#5f0c}"); minify_test(".foo { color: transparent }", ".foo{color:#0000}"); minify_test(".foo { color: currentColor }", ".foo{color:currentColor}"); + minify_test(".foo { color: ButtonBorder }", ".foo{color:buttonborder}"); minify_test(".foo { color: hwb(194 0% 0%) }", ".foo{color:#00c4ff}"); 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)}", @@ -15450,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)}", @@ -15470,21 +17917,37 @@ 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)}", ); minify_test( ".foo { color: color(display-p3 1 0.5 0); }", - ".foo{color:color(display-p3 1 .5)}", + ".foo{color:color(display-p3 1 .5 0)}", ); minify_test( ".foo { color: color(display-p3 100% 50% 0%); }", - ".foo{color:color(display-p3 1 .5)}", + ".foo{color:color(display-p3 1 .5 0)}", ); minify_test( ".foo { color: color(xyz-d50 0.2005 0.14089 0.4472); }", @@ -15510,24 +17973,27 @@ mod tests { ".foo { color: color(xyz 20.05% 14.089% 44.72%); }", ".foo{color:color(xyz .2005 .14089 .4472)}", ); - minify_test(".foo { color: color(xyz 0.2005 0 0); }", ".foo{color:color(xyz .2005)}"); - minify_test(".foo { color: color(xyz 0 0 0); }", ".foo{color:color(xyz)}"); - minify_test(".foo { color: color(xyz 0 1 0); }", ".foo{color:color(xyz 0 1)}"); - minify_test(".foo { color: color(xyz 0 1); }", ".foo{color:color(xyz 0 1)}"); - minify_test(".foo { color: color(xyz 1); }", ".foo{color:color(xyz 1)}"); - minify_test(".foo { color: color(xyz); }", ".foo{color:color(xyz)}"); + minify_test( + ".foo { color: color(xyz 0.2005 0 0); }", + ".foo{color:color(xyz .2005 0 0)}", + ); + minify_test(".foo { color: color(xyz 0 0 0); }", ".foo{color:color(xyz 0 0 0)}"); + minify_test(".foo { color: color(xyz 0 1 0); }", ".foo{color:color(xyz 0 1 0)}"); minify_test( ".foo { color: color(xyz 0 1 0 / 20%); }", - ".foo{color:color(xyz 0 1/.2)}", + ".foo{color:color(xyz 0 1 0/.2)}", + ); + minify_test( + ".foo { color: color(xyz 0 0 0 / 20%); }", + ".foo{color:color(xyz 0 0 0/.2)}", ); - minify_test(".foo { color: color(xyz / 20%); }", ".foo{color:color(xyz/.2)}"); minify_test( ".foo { color: color(display-p3 100% 50% 0 / 20%); }", - ".foo{color:color(display-p3 1 .5/.2)}", + ".foo{color:color(display-p3 1 .5 0/.2)}", ); minify_test( - ".foo { color: color(display-p3 100% / 20%); }", - ".foo{color:color(display-p3 1/.2)}", + ".foo { color: color(display-p3 100% 0 0 / 20%); }", + ".foo{color:color(display-p3 1 0 0/.2)}", ); minify_test(".foo { color: hsl(none none none) }", ".foo{color:#000}"); minify_test(".foo { color: hwb(none none none) }", ".foo{color:red}"); @@ -16013,6 +18479,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + background: var(--image) lab(40% 56.6 39); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + background: var(--image) lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -16117,16 +18604,39 @@ mod tests { prefix_test( r#" - .foo { - --a: rgb(0 0 0 / var(--alpha)); - --b: rgb(50% 50% 50% / var(--alpha)); - --c: rgb(var(--x) 0 0); - --d: rgb(0 var(--x) 0); - --e: rgb(0 0 var(--x)); + @supports (color: lab(0% 0 0)) { + .foo { + color: var(--foo, lab(40% 56.6 39)); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + color: var(--foo, lab(40% 56.6 39)); + } + } + "# + }, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + --a: rgb(0 0 0 / var(--alpha)); + --b: rgb(50% 50% 50% / var(--alpha)); + --c: rgb(var(--x) 0 0); + --d: rgb(0 var(--x) 0); + --e: rgb(0 0 var(--x)); --f: rgb(var(--x) 0 0 / var(--alpha)); --g: rgb(0 var(--x) 0 / var(--alpha)); --h: rgb(0 0 var(--x) / var(--alpha)); --i: rgb(none 0 0 / var(--alpha)); + --j: rgb(from yellow r g b / var(--alpha)); } "#, indoc! { r#" @@ -16140,6 +18650,7 @@ mod tests { --g: rgb(0 var(--x) 0 / var(--alpha)); --h: rgb(0 0 var(--x) / var(--alpha)); --i: rgb(none 0 0 / var(--alpha)); + --j: rgba(255, 255, 0, var(--alpha)); } "#}, Browsers { @@ -16160,6 +18671,7 @@ mod tests { --g: rgb(0 var(--x) 0 / var(--alpha)); --h: rgb(0 0 var(--x) / var(--alpha)); --i: rgb(none 0 0 / var(--alpha)); + --j: rgb(from yellow r g b / var(--alpha)); } "#, indoc! { r#" @@ -16173,6 +18685,7 @@ mod tests { --g: rgb(0 var(--x) 0 / var(--alpha)); --h: rgb(0 0 var(--x) / var(--alpha)); --i: rgb(none 0 0 / var(--alpha)); + --j: rgb(255 255 0 / var(--alpha)); } "#}, Browsers { @@ -16193,6 +18706,7 @@ mod tests { --g: hsl(0 0 var(--x) / var(--alpha)); --h: hsl(270 100% 50% / calc(var(--alpha) / 2)); --i: hsl(none 100% 50% / var(--alpha)); + --j: hsl(from yellow h s l / var(--alpha)); } "#, indoc! { r#" @@ -16206,6 +18720,7 @@ mod tests { --g: hsl(0 0 var(--x) / var(--alpha)); --h: hsla(270, 100%, 50%, calc(var(--alpha) / 2)); --i: hsl(none 100% 50% / var(--alpha)); + --j: hsla(60, 100%, 50%, var(--alpha)); } "#}, Browsers { @@ -16286,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))", @@ -16302,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))}", @@ -16421,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). @@ -16531,18 +19051,18 @@ mod tests { test("hsl(from rgb(20%, 40%, 60%, 80%) h s l / 0)", "rgba(51, 102, 153, 0)"); // Testing replacement with a constant. - test("hsl(from rebeccapurple 25 s l / alpha)", "rgb(153, 93, 51)"); - test("hsl(from rebeccapurple 25deg s l / alpha)", "rgb(153, 93, 51)"); + test("hsl(from rebeccapurple 25 s l / alpha)", "rgb(153, 94, 51)"); + test("hsl(from rebeccapurple 25deg s l / alpha)", "rgb(153, 94, 51)"); test("hsl(from rebeccapurple h 20% l / alpha)", "rgb(102, 82, 122)"); test("hsl(from rebeccapurple h s 20% / alpha)", "rgb(51, 25, 77)"); test("hsl(from rebeccapurple h s l / .25)", "rgba(102, 51, 153, 0.25)"); test( "hsl(from rgb(20%, 40%, 60%, 80%) 25 s l / alpha)", - "rgba(153, 93, 51, 0.8)", + "rgba(153, 94, 51, 0.8)", ); test( "hsl(from rgb(20%, 40%, 60%, 80%) 25deg s l / alpha)", - "rgba(153, 93, 51, 0.8)", + "rgba(153, 94, 51, 0.8)", ); test( "hsl(from rgb(20%, 40%, 60%, 80%) h 20% l / alpha)", @@ -16559,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)", ); @@ -16669,18 +19201,18 @@ mod tests { test("hwb(from rgb(20%, 40%, 60%, 80%) h w b / 0)", "rgba(51, 102, 153, 0)"); // Testing replacement with a constant. - test("hwb(from rebeccapurple 25 w b / alpha)", "rgb(153, 93, 51)"); - test("hwb(from rebeccapurple 25deg w b / alpha)", "rgb(153, 93, 51)"); + test("hwb(from rebeccapurple 25 w b / alpha)", "rgb(153, 94, 51)"); + test("hwb(from rebeccapurple 25deg w b / alpha)", "rgb(153, 94, 51)"); test("hwb(from rebeccapurple h 20% b / alpha)", "rgb(102, 51, 153)"); test("hwb(from rebeccapurple h w 20% / alpha)", "rgb(128, 51, 204)"); test("hwb(from rebeccapurple h w b / .2)", "rgba(102, 51, 153, 0.2)"); test( "hwb(from rgb(20%, 40%, 60%, 80%) 25 w b / alpha)", - "rgba(153, 93, 51, 0.8)", + "rgba(153, 94, 51, 0.8)", ); test( "hwb(from rgb(20%, 40%, 60%, 80%) 25deg w b / alpha)", - "rgba(153, 93, 51, 0.8)", + "rgba(153, 94, 51, 0.8)", ); test( "hwb(from rgb(20%, 40%, 60%, 80%) h 20% b / alpha)", @@ -16697,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)", ); @@ -17160,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), @@ -17168,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), @@ -17187,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(). @@ -18056,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}", ); } } @@ -18165,11 +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)}", + ); + minify_test( + ".foo { color: color-mix(in srgb, blue, accentcolor); }", + ".foo{color:color-mix(in srgb, blue, accentcolor)}", ); // regex for converting web platform tests: @@ -19468,7 +22025,7 @@ mod tests { ".foo {{ color: color-mix(in {0}, color({0} -2 -3 -4 / -5), color({0} -4 -6 -8 / -10)) }}", color_space ), - &format!(".foo{{color:color({}/0)}}", result_color_space), + &format!(".foo{{color:color({} 0 0 0/0)}}", result_color_space), ); minify_test( @@ -19537,7 +22094,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -19685,6 +22241,214 @@ 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#" + .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#" + .test-miss-areas-2 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, + ); + + test( + r#" + .test-miss-areas-3 { + grid-template: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; + } + "#, + indoc! { r#" + .test-miss-areas-3 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, + ); + + test( + r#" + .test-miss-areas-4 { + grid: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; + } + "#, + 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( + r#" + .grid-shorthand-areas { + grid: auto / 1fr 3fr; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas{grid:\".content.\"/1fr 3fr}", + ); + minify_test( + 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 { @@ -20364,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)); }", @@ -20384,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#" @@ -20415,18 +23240,16 @@ mod tests { prefix_test( r#" - .foo { - --custom: lab(40% 56.6 39) !important; + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } } "#, indoc! {r#" - .foo { - --custom: #b32323 !important; - } - @supports (color: lab(0% 0 0)) { .foo { - --custom: lab(40% 56.6 39) !important; + --custom: lab(40% 56.6 39); } } "#}, @@ -20439,52 +23262,43 @@ mod tests { prefix_test( r#" .foo { - --custom: lab(40% 56.6 39); + --custom: lab(40% 56.6 39) !important; } "#, indoc! {r#" .foo { - --custom: #b32323; - } - - @supports (color: color(display-p3 0 0 0)) { - .foo { - --custom: color(display-p3 .643308 .192455 .167712); - } + --custom: #b32323 !important; } @supports (color: lab(0% 0 0)) { .foo { - --custom: lab(40% 56.6 39); + --custom: lab(40% 56.6 39) !important; } } "#}, Browsers { chrome: Some(90 << 16), - safari: Some(14 << 16), ..Browsers::default() }, ); prefix_test( r#" - .foo { - --custom: lab(40% 56.6 39); + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39) !important; + } } "#, indoc! {r#" - .foo { - --custom: color(display-p3 .643308 .192455 .167712); - } - @supports (color: lab(0% 0 0)) { .foo { - --custom: lab(40% 56.6 39); + --custom: lab(40% 56.6 39) !important; } } "#}, Browsers { - safari: Some(14 << 16), + chrome: Some(90 << 16), ..Browsers::default() }, ); @@ -20497,12 +23311,99 @@ mod tests { "#, indoc! {r#" .foo { - --custom: lab(40% 56.6 39); + --custom: #b32323; } - "#}, - Browsers { - safari: Some(15 << 16), - ..Browsers::default() + + @supports (color: color(display-p3 0 0 0)) { + .foo { + --custom: color(display-p3 .643308 .192455 .167712); + } + } + + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + --custom: color(display-p3 .643308 .192455 .167712); + } + } + + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + --custom: color(display-p3 .643308 .192455 .167712); + } + } + + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + --custom: lab(40% 56.6 39); + } + "#, + indoc! {r#" + .foo { + --custom: color(display-p3 .643308 .192455 .167712); + } + + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + --custom: lab(40% 56.6 39); + } + "#, + indoc! {r#" + .foo { + --custom: lab(40% 56.6 39); + } + "#}, + Browsers { + safari: Some(15 << 16), + ..Browsers::default() }, ); @@ -20634,6 +23535,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + --foo: color(display-p3 0 1 0); + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + --foo: color(display-p3 0 1 0); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -20798,6 +23721,39 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" @keyframes foo { @@ -20852,6 +23808,64 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + @keyframes foo { + from { + --custom: color(display-p3 .643308 .192455 .167712); + } + + to { + --custom: color(display-p3 .972962 -.362078 .804206); + } + } + } + + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + @keyframes foo { + from { + --custom: color(display-p3 .643308 .192455 .167712); + } + + to { + --custom: color(display-p3 .972962 -.362078 .804206); + } + } + } + + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" @keyframes foo { @@ -20990,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), @@ -21211,7 +24225,7 @@ mod tests { grid-auto-flow: column; } - @media (min-width: 1024px) { + @media not (max-width: 1024px) { .foo { max-inline-size: 1024px; } @@ -21834,18 +24848,6 @@ mod tests { "#}, ); - nesting_error_test( - r#" - .foo { - color: blue; - div { - color: red; - } - } - "#, - ParserError::UnexpectedToken(crate::properties::custom::Token::CurlyBracketBlock), - ); - nesting_test( r#" .foo { @@ -22090,13 +25092,13 @@ mod tests { } "#, indoc! {r#" - .foo { - color: red; - } - .foo .bar { color: #00f; } + + .foo { + color: red; + } "#}, ); @@ -22110,12 +25112,16 @@ mod tests { "#, indoc! {r#" article { - color: red; + color: green; } article { color: #00f; } + + article { + color: red; + } "#}, ); @@ -22171,48 +25177,131 @@ mod tests { "#}, ); - nesting_test_no_targets( + nesting_test( r#" .foo { color: blue; - @nest .bar & { + div { color: red; - &.baz { - color: green; - } } } "#, indoc! {r#" .foo { color: #00f; + } - @nest .bar & { - color: red; - - &.baz { - color: green; - } - } + .foo div { + color: red; } "#}, ); - nesting_test_no_targets( + nesting_test( r#" - .foo { + div { color: blue; - &div { - color: red; - } - &span { - color: purple; + button:focus { + color: red; } } "#, indoc! {r#" - .foo { + div { + color: #00f; + } + + div button:focus { + color: red; + } + "#}, + ); + nesting_test( + r#" + div { + color: blue; + + --button:focus { + color: red; + } + } + "#, + indoc! {r#" + div { + color: #00f; + --button: focus { + color: red; + }; + } + "#}, + ); + nesting_test( + r#" + .foo { + &::before, &::after { + background: blue; + @media screen { + background: orange; + } + } + } + "#, + indoc! {r#" + .foo:before, .foo:after { + background: #00f; + } + + @media screen { + .foo:before, .foo:after { + background: orange; + } + } + "#}, + ); + + nesting_test_no_targets( + r#" + .foo { + color: blue; + @nest .bar & { + color: red; + &.baz { + color: green; + } + } + } + "#, + indoc! {r#" + .foo { + color: #00f; + + @nest .bar & { + color: red; + + &.baz { + color: green; + } + } + } + "#}, + ); + + nesting_test_no_targets( + r#" + .foo { + color: blue; + &div { + color: red; + } + + &span { + color: purple; + } + } + "#, + indoc! {r#" + .foo { color: #00f; &div { @@ -22301,10 +25390,7 @@ mod tests { } } "#, - ParserOptions { - flags: ParserFlags::NESTING, - ..ParserOptions::default() - }, + ParserOptions::default(), ) .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); @@ -22315,6 +25401,86 @@ mod tests { }) .unwrap(); assert_eq!(res.code, ".foo{color:#00f;& .bar{color:red}}"); + + nesting_test_with_targets( + r#" + .a { + &.b, + &.c { + &.d { + color: red; + } + } + } + "#, + indoc! {r#" + .a.b.d { + color: red; + } + + .a.c.d { + color: red; + } + "#}, + Targets { + browsers: Some(Browsers { + safari: Some(13 << 16), + ..Browsers::default() + }), + include: Features::Nesting, + 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] + fn test_nesting_error_recovery() { + error_recovery_test( + " + .container { + padding: 3rem; + @media (max-width: --styled-jsx-placeholder-0__) { + .responsive { + color: purple; + } + } + } + ", + ); + } + + #[test] + fn test_css_variable_error_recovery() { + error_recovery_test(" + .container { + --local-var: --styled-jsx-placeholder-0__; + color: var(--text-color); + background: linear-gradient(to right, --styled-jsx-placeholder-1__, --styled-jsx-placeholder-2__); + + .item { + transform: translate(calc(var(--x) + --styled-jsx-placeholder-3__px), calc(var(--y) + --styled-jsx-placeholder-4__px)); + } + + div { + margin: calc(10px + --styled-jsx-placeholder-5__px); + } + } + "); } #[test] @@ -22409,9 +25575,102 @@ mod tests { }, HashMap::new(), Default::default(), + false, + ); + + css_modules_test( + r#" + .foo { + color: red; + } + + #id { + animation: 2s test; + } + + @keyframes test { + from { color: red } + to { color: yellow } + } + "#, + indoc! {r#" + .EgL3uq_foo { + color: red; + } + + #EgL3uq_id { + animation: 2s test; + } + + @keyframes test { + from { + color: red; + } + + to { + color: #ff0; + } + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "id" => "EgL3uq_id" + }, + HashMap::new(), + crate::css_modules::Config { + animation: false, + // custom_idents: false, + ..Default::default() + }, + false, + ); + + css_modules_test( + r#" + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#, + indoc! {r#" + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#}, + map! { + "circles" => "EgL3uq_circles" referenced: true + }, + HashMap::new(), + crate::css_modules::Config { + custom_idents: false, + ..Default::default() + }, + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" body { @@ -22453,9 +25712,9 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -22491,33 +25750,75 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" - test { - transition-property: opacity; - } - "#, - indoc! {r#" - test { - transition-property: opacity; - } - "#}, - map! {}, - HashMap::new(), - Default::default(), - ); - - css_modules_test( - r#" - :global(.foo) { - color: red; - } + .grid { + grid-template-areas: "foo"; + } - :local(.bar) { - color: yellow; - } + .foo { + grid-area: foo; + } + + .bar { + grid-column-start: foo-start; + } + "#, + indoc! {r#" + .EgL3uq_grid { + grid-template-areas: "foo"; + } + + .EgL3uq_foo { + grid-area: foo; + } + + .EgL3uq_bar { + grid-column-start: foo-start; + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "grid" => "EgL3uq_grid", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + crate::css_modules::Config { + grid: false, + ..Default::default() + }, + false, + ); + + css_modules_test( + r#" + test { + transition-property: opacity; + } + "#, + indoc! {r#" + test { + transition-property: opacity; + } + "#}, + map! {}, + HashMap::new(), + Default::default(), + false, + ); + + css_modules_test( + r#" + :global(.foo) { + color: red; + } + + :local(.bar) { + color: yellow; + } .bar :global(.baz) { color: purple; @@ -22541,6 +25842,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); // :global(:local(.hi)) { @@ -22573,6 +25875,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22602,6 +25905,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22639,6 +25943,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22658,6 +25963,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22677,6 +25983,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22696,6 +26003,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22715,6 +26023,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22745,6 +26054,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22766,6 +26076,7 @@ mod tests { pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), ..Default::default() }, + false, ); let stylesheet = StyleSheet::parse( @@ -22827,6 +26138,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -22896,246 +26208,613 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); - // Stable hashes between project roots. - fn test_project_root(project_root: &str, filename: &str, hash: &str) { - let stylesheet = StyleSheet::parse( - r#" - .foo { - background: red; - } - "#, - ParserOptions { - filename: filename.into(), - css_modules: Some(Default::default()), - ..ParserOptions::default() - }, - ) - .unwrap(); - let res = stylesheet - .to_css(PrinterOptions { - project_root: Some(project_root), - ..PrinterOptions::default() - }) - .unwrap(); - assert_eq!( - res.code, - format!( - indoc! {r#" - .{}_foo {{ - background: red; - }} - "#}, - hash - ) - ); - } - - test_project_root("/foo/bar", "/foo/bar/test.css", "EgL3uq"); - test_project_root("/foo", "/foo/test.css", "EgL3uq"); - test_project_root("/foo/bar", "/foo/bar/baz/test.css", "xLEkNW"); - test_project_root("/foo", "/foo/baz/test.css", "xLEkNW"); - } - - #[test] - fn test_pseudo_replacement() { - let source = r#" - .foo:hover { - color: red; + css_modules_test( + r#" + .test { + animation: rotate var(--duration) linear infinite; } - - .foo:active { - color: yellow; + "#, + indoc! {r#" + .EgL3uq_test { + animation: EgL3uq_rotate var(--duration) linear infinite; } - - .foo:focus-visible { - color: purple; + "#}, + map! { + "test" => "EgL3uq_test", + "rotate" => "EgL3uq_rotate" referenced: true + }, + HashMap::new(), + Default::default(), + false, + ); + css_modules_test( + r#" + .test { + animation: none var(--duration); } - "#; - - let expected = indoc! { r#" - .foo.is-hovered { - color: red; + "#, + indoc! {r#" + .EgL3uq_test { + animation: none var(--duration); } - - .foo.is-active { - color: #ff0; + "#}, + map! { + "test" => "EgL3uq_test" + }, + HashMap::new(), + Default::default(), + false, + ); + css_modules_test( + r#" + .test { + animation: var(--animation); } - - .foo.focus-visible { - color: purple; + "#, + indoc! {r#" + .EgL3uq_test { + animation: var(--animation); } - "#}; - - let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); - let res = stylesheet - .to_css(PrinterOptions { - pseudo_classes: Some(PseudoClasses { - hover: Some("is-hovered"), - active: Some("is-active"), - focus_visible: Some("focus-visible"), - ..PseudoClasses::default() - }), - ..PrinterOptions::default() - }) - .unwrap(); - assert_eq!(res.code, expected); - - let source = r#" - .foo:hover { - color: red; + "#}, + map! { + "test" => "EgL3uq_test" + }, + HashMap::new(), + Default::default(), + false, + ); + css_modules_test( + r#" + .test { + animation: rotate var(--duration); } - "#; - - let expected = indoc! { r#" - .EgL3uq_foo.EgL3uq_is-hovered { - color: red; + "#, + indoc! {r#" + .EgL3uq_test { + animation: rotate var(--duration); } - "#}; - - let stylesheet = StyleSheet::parse( - &source, - ParserOptions { - filename: "test.css".into(), - css_modules: Some(Default::default()), - ..ParserOptions::default() + "#}, + map! { + "test" => "EgL3uq_test" }, - ) - .unwrap(); - let res = stylesheet - .to_css(PrinterOptions { - pseudo_classes: Some(PseudoClasses { - hover: Some("is-hovered"), - ..PseudoClasses::default() - }), - ..PrinterOptions::default() - }) - .unwrap(); - assert_eq!(res.code, expected); - } - - #[test] - fn test_unused_symbols() { - let source = r#" - .foo { - color: red; + HashMap::new(), + crate::css_modules::Config { + animation: false, + ..Default::default() + }, + false, + ); + css_modules_test( + r#" + .test { + animation: "rotate" var(--duration); } - - .bar { - color: green; + "#, + indoc! {r#" + .EgL3uq_test { + animation: EgL3uq_rotate var(--duration); } + "#}, + map! { + "test" => "EgL3uq_test", + "rotate" => "EgL3uq_rotate" referenced: true + }, + HashMap::new(), + crate::css_modules::Config { ..Default::default() }, + false, + ); - .bar:hover { - color: purple; + css_modules_test( + r#" + .test { + composes: foo bar from "foo.css"; + background: white; } - - .bar .baz { - background: red; + "#, + indoc! {r#" + ._5h2kwG-test { + background: #fff; } + "#}, + map! { + "test" => "_5h2kwG-test" "foo" from "foo.css" "bar" from "foo.css" + }, + HashMap::new(), + crate::css_modules::Config { + pattern: crate::css_modules::Pattern::parse("[content-hash]-[local]").unwrap(), + ..Default::default() + }, + false, + ); - .baz:is(.bar) { - background: green; + css_modules_test( + r#" + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } - - #id { - animation: 2s test; + "#, + indoc! {r#" + .EgL3uq_box2 { + @container EgL3uq_main (width >= 0) { + background-color: #90ee90; + } } + "#}, + map! { + "main" => "EgL3uq_main", + "box2" => "EgL3uq_box2" + }, + HashMap::new(), + crate::css_modules::Config { ..Default::default() }, + false, + ); - #other_id { - color: red; + css_modules_test( + r#" + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } - - @keyframes test { - from { color: red } - to { color: yellow } + "#, + indoc! {r#" + .EgL3uq_box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } + "#}, + map! { + "box2" => "EgL3uq_box2" + }, + HashMap::new(), + crate::css_modules::Config { + container: false, + ..Default::default() + }, + false, + ); - @counter-style circles { - symbols: Ⓐ Ⓑ Ⓒ; - } + css_modules_test( + ".foo { view-transition-name: bar }", + ".EgL3uq_foo{view-transition-name:EgL3uq_bar}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ".foo { view-transition-name: none }", + ".EgL3uq_foo{view-transition-name:none}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ".foo { view-transition-name: auto }", + ".EgL3uq_foo{view-transition-name:auto}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); - @keyframes fade { - from { opacity: 0 } - to { opacity: 1 } - } - "#; + css_modules_test( + ".foo { view-transition-class: bar baz qux }", + ".EgL3uq_foo{view-transition-class:EgL3uq_bar EgL3uq_baz EgL3uq_qux}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar", + "baz" => "EgL3uq_baz", + "qux" => "EgL3uq_qux" + }, + HashMap::new(), + Default::default(), + true, + ); - let expected = indoc! {r#" - .foo { - color: red; - } + css_modules_test( + ".foo { view-transition-group: contain }", + ".EgL3uq_foo{view-transition-group:contain}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ".foo { view-transition-group: bar }", + ".EgL3uq_foo{view-transition-group:EgL3uq_bar}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); - #id { - animation: 2s test; - } + css_modules_test( + "@view-transition { types: foo bar baz }", + "@view-transition{types:EgL3uq_foo EgL3uq_bar EgL3uq_baz}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar", + "baz" => "EgL3uq_baz" + }, + HashMap::new(), + Default::default(), + true, + ); - @keyframes test { - from { - color: red; - } + css_modules_test( + ":root:active-view-transition-type(foo, bar) { color: red }", + ":root:active-view-transition-type(EgL3uq_foo,EgL3uq_bar){color:red}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); - to { - color: #ff0; - } - } - "#}; + for name in &[ + "view-transition-group", + "view-transition-image-pair", + "view-transition-new", + "view-transition-old", + ] { + css_modules_test( + &format!(":root::{}(foo) {{position: fixed}}", name), + &format!(":root::{}(EgL3uq_foo){{position:fixed}}", name), + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + &format!(":root::{}(.bar) {{position: fixed}}", name), + &format!(":root::{}(.EgL3uq_bar){{position:fixed}}", name), + map! { + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name), + &format!(":root::{}(EgL3uq_foo.EgL3uq_bar.EgL3uq_baz){{position:fixed}}", name), + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar", + "baz" => "EgL3uq_baz" + }, + HashMap::new(), + Default::default(), + true, + ); - let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); - stylesheet - .minify(MinifyOptions { - unused_symbols: vec!["bar", "other_id", "fade", "circles"] - .iter() - .map(|s| String::from(*s)) - .collect(), - ..MinifyOptions::default() - }) + css_modules_test( + ":nth-child(1 of .foo) {width: 20px}", + ":nth-child(1 of .EgL3uq_foo){width:20px}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ":nth-last-child(1 of .foo) {width: 20px}", + ":nth-last-child(1 of .EgL3uq_foo){width:20px}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + } + + // Stable hashes between project roots. + fn test_project_root(project_root: &str, filename: &str, hash: &str) { + let stylesheet = StyleSheet::parse( + r#" + .foo { + background: red; + } + "#, + ParserOptions { + filename: filename.into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) .unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); - assert_eq!(res.code, expected); + let res = stylesheet + .to_css(PrinterOptions { + project_root: Some(project_root), + ..PrinterOptions::default() + }) + .unwrap(); + assert_eq!( + res.code, + format!( + indoc! {r#" + .{}_foo {{ + background: red; + }} + "#}, + hash + ) + ); + } - let source = r#" + test_project_root("/foo/bar", "/foo/bar/test.css", "EgL3uq"); + test_project_root("/foo", "/foo/test.css", "EgL3uq"); + test_project_root("/foo/bar", "/foo/bar/baz/test.css", "xLEkNW"); + test_project_root("/foo", "/foo/baz/test.css", "xLEkNW"); + + let mut stylesheet = StyleSheet::parse( + r#" .foo { color: red; - - &.bar { + .bar { color: green; } + composes: test from "foo.css"; } - "#; - - let expected = indoc! {r#" - .foo { - color: red; - } - "#}; - - let mut stylesheet = StyleSheet::parse( - &source, + "#, ParserOptions { - flags: ParserFlags::NESTING, + filename: "test.css".into(), + css_modules: Some(Default::default()), ..ParserOptions::default() }, ) .unwrap(); - stylesheet - .minify(MinifyOptions { - unused_symbols: vec!["bar"].iter().map(|s| String::from(*s)).collect(), - ..MinifyOptions::default() + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..Default::default() }) .unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); - assert_eq!(res.code, expected); + assert_eq!( + res.code, + indoc! {r#" + .EgL3uq_foo { + color: red; + } - let source = r#" - .foo { - color: red; + .EgL3uq_foo .EgL3uq_bar { + color: green; + } - &.bar { - color: purple; - } - @nest &.bar { + "#} + ); + assert_eq!( + res.exports.unwrap(), + map! { + "foo" => "EgL3uq_foo" "test" from "foo.css", + "bar" => "EgL3uq_bar" + } + ); + } + + #[test] + fn test_pseudo_replacement() { + let source = r#" + .foo:hover { + color: red; + } + + .foo:active { + color: yellow; + } + + .foo:focus-visible { + color: purple; + } + "#; + + let expected = indoc! { r#" + .foo.is-hovered { + color: red; + } + + .foo.is-active { + color: #ff0; + } + + .foo.focus-visible { + color: purple; + } + "#}; + + let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + pseudo_classes: Some(PseudoClasses { + hover: Some("is-hovered"), + active: Some("is-active"), + focus_visible: Some("focus-visible"), + ..PseudoClasses::default() + }), + ..PrinterOptions::default() + }) + .unwrap(); + assert_eq!(res.code, expected); + + let source = r#" + .foo:hover { + color: red; + } + "#; + + let expected = indoc! { r#" + .EgL3uq_foo.EgL3uq_is-hovered { + color: red; + } + "#}; + + let stylesheet = StyleSheet::parse( + &source, + ParserOptions { + filename: "test.css".into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) + .unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + pseudo_classes: Some(PseudoClasses { + hover: Some("is-hovered"), + ..PseudoClasses::default() + }), + ..PrinterOptions::default() + }) + .unwrap(); + assert_eq!(res.code, expected); + } + + #[test] + fn test_unused_symbols() { + let source = r#" + .foo { + color: red; + } + + .bar { + color: green; + } + + .bar:hover { + color: purple; + } + + .bar .baz { + background: red; + } + + .baz:is(.bar) { + background: green; + } + + #id { + animation: 2s test; + } + + #other_id { + color: red; + } + + @keyframes test { + from { color: red } + to { color: yellow } + } + + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + @keyframes fade { + from { opacity: 0 } + to { opacity: 1 } + } + "#; + + let expected = indoc! {r#" + .foo { + color: red; + } + + #id { + animation: 2s test; + } + + @keyframes test { + from { + color: red; + } + + to { + color: #ff0; + } + } + "#}; + + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); + stylesheet + .minify(MinifyOptions { + unused_symbols: vec!["bar", "other_id", "fade", "circles"] + .iter() + .map(|s| String::from(*s)) + .collect(), + ..MinifyOptions::default() + }) + .unwrap(); + let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + assert_eq!(res.code, expected); + + let source = r#" + .foo { + color: red; + + &.bar { + color: green; + } + } + "#; + + let expected = indoc! {r#" + .foo { + color: red; + } + "#}; + + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); + stylesheet + .minify(MinifyOptions { + unused_symbols: vec!["bar"].iter().map(|s| String::from(*s)).collect(), + ..MinifyOptions::default() + }) + .unwrap(); + let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + assert_eq!(res.code, expected); + + let source = r#" + .foo { + color: red; + + &.bar { + color: purple; + } + + @nest &.bar { color: orange; } @@ -23163,14 +26842,7 @@ mod tests { } "#}; - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - flags: ParserFlags::NESTING, - ..ParserOptions::default() - }, - ) - .unwrap(); + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); stylesheet .minify(MinifyOptions { unused_symbols: vec!["foo", "x"].iter().map(|s| String::from(*s)).collect(), @@ -23217,14 +26889,7 @@ mod tests { } "#}; - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - flags: ParserFlags::NESTING, - ..ParserOptions::default() - }, - ) - .unwrap(); + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); stylesheet .minify(MinifyOptions { unused_symbols: vec!["--EgL3uq_foo", "--EgL3uq_Cooler"] @@ -23240,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}"); @@ -23525,12 +27192,33 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", indoc! { r#" .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)); @@ -23592,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; @@ -23641,14 +27329,36 @@ mod tests { ); prefix_test( - ".foo { mask: url(masks.svg#star) luminance }", + r#" + @supports (color: lab(0% 0 0)) { + .foo { + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#, indoc! { r#" - .foo { - -webkit-mask: url("masks.svg#star"); - -webkit-mask-source-type: luminance; - mask: url("masks.svg#star") luminance; + @supports (color: lab(0% 0 0)) { + .foo { + -webkit-mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } } - "#}, + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { mask: url(masks.svg#star) luminance }", + indoc! { r#" + .foo { + -webkit-mask: url("masks.svg#star"); + -webkit-mask-source-type: luminance; + mask: url("masks.svg#star") luminance; + } + "#}, Browsers { chrome: Some(90 << 16), ..Browsers::default() @@ -24025,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] @@ -24196,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( @@ -24214,6 +28015,120 @@ mod tests { ); } + #[test] + fn test_at_scope() { + minify_test( + r#" + @scope { + .foo { + display: flex; + } + } + "#, + "@scope{.foo{display:flex}}", + ); + minify_test( + r#" + @scope { + :scope { + display: flex; + color: lightblue; + } + }"#, + "@scope{:scope{color:#add8e6;display:flex}}", + ); + minify_test( + r#" + @scope (.light-scheme) { + a { color: yellow; } + } + "#, + "@scope(.light-scheme){a{color:#ff0}}", + ); + minify_test( + r#" + @scope (.media-object) to (.content > *) { + a { color: yellow; } + } + "#, + "@scope(.media-object) to (.content>*){a{color:#ff0}}", + ); + minify_test( + r#" + @scope to (.content > *) { + a { color: yellow; } + } + "#, + "@scope to (.content>*){a{color:#ff0}}", + ); + minify_test( + r#" + @scope (#my-component) { + & { color: yellow; } + } + "#, + "@scope(#my-component){&{color:#ff0}}", + ); + minify_test( + r#" + @scope (.parent-scope) { + @scope (:scope > .child-scope) to (:scope .limit) { + .content { color: yellow; } + } + } + "#, + "@scope(.parent-scope){@scope(:scope>.child-scope) to (:scope .limit){.content{color:#ff0}}}", + ); + minify_test( + r#" + .foo { + @scope (.bar) { + color: yellow; + } + } + "#, + ".foo{@scope(.bar){color:#ff0}}", + ); + nesting_test( + r#" + .foo { + @scope (.bar) { + color: yellow; + } + } + "#, + indoc! {r#" + @scope (.bar) { + color: #ff0; + } + "#}, + ); + nesting_test( + r#" + .parent { + color: blue; + + @scope (& > .scope) to (& .limit) { + & .content { + color: yellow; + } + } + } + "#, + indoc! {r#" + .parent { + color: #00f; + } + + @scope (.parent > .scope) to (.parent > .scope .limit) { + :scope .content { + color: #ff0; + } + } + "#}, + ); + } + #[test] fn test_custom_media() { custom_media_test( @@ -24574,7 +28489,7 @@ mod tests { } "#, indoc! {r#" - @media screen and ((prefers-color-scheme: dark) or (not (width >= 300px))) { + @media screen and ((prefers-color-scheme: dark) or ((width < 300px))) { .foo { order: 6; } @@ -24893,1355 +28808,2195 @@ mod tests { vec![("/foo.png", "lDnnrG")], ); - dep_test( - ".foo { --test: url(\"http://example.com/foo.png\") }", - ".foo{--test:url(\"3X1zSW\")}", - vec![("http://example.com/foo.png", "3X1zSW")], + dep_test( + ".foo { --test: url(\"http://example.com/foo.png\") }", + ".foo{--test:url(\"3X1zSW\")}", + vec![("http://example.com/foo.png", "3X1zSW")], + ); + + dep_test( + ".foo { --test: url(\"data:image/svg+xml;utf8,\") }", + ".foo{--test:url(\"-vl-rG\")}", + vec![("data:image/svg+xml;utf8,", "-vl-rG")], + ); + + dep_test( + ".foo { background: url(\"foo.png\") var(--test) }", + ".foo{background:url(\"Vwkwkq\") var(--test)}", + vec![("foo.png", "Vwkwkq")], + ); + + dep_error_test( + ".foo { --test: url(\"foo.png\") }", + PrinterErrorKind::AmbiguousUrlInCustomProperty { url: "foo.png".into() }, + ); + + dep_error_test( + ".foo { --test: url(foo.png) }", + PrinterErrorKind::AmbiguousUrlInCustomProperty { url: "foo.png".into() }, + ); + + dep_error_test( + ".foo { --test: url(./foo.png) }", + PrinterErrorKind::AmbiguousUrlInCustomProperty { + url: "./foo.png".into(), + }, + ); + + dep_test( + ".foo { behavior: url(#foo) }", + ".foo{behavior:url(\"Zn9-2q\")}", + vec![("#foo", "Zn9-2q")], + ); + + dep_test( + ".foo { --foo: url(#foo) }", + ".foo{--foo:url(\"Zn9-2q\")}", + vec![("#foo", "Zn9-2q")], + ); + + dep_test( + "@import \"test.css\"; .foo { color: red }", + "@import \"hHsogW\";.foo{color:red}", + vec![("test.css", "hHsogW")], + ); + } + + #[test] + fn test_api() { + let stylesheet = StyleSheet::parse(".foo:hover { color: red }", ParserOptions::default()).unwrap(); + match &stylesheet.rules.0[0] { + CssRule::Style(s) => { + assert_eq!(&s.selectors.to_string(), ".foo:hover"); + } + _ => unreachable!(), + } + + let color = CssColor::parse_string("#f0f").unwrap(); + assert_eq!(color.to_css_string(PrinterOptions::default()).unwrap(), "#f0f"); + + let rule = CssRule::parse_string(".foo { color: red }", ParserOptions::default()).unwrap(); + assert_eq!( + rule.to_css_string(PrinterOptions::default()).unwrap(), + indoc! {r#" + .foo { + color: red; + }"#} + ); + + let property = Property::parse_string("color".into(), "#f0f", ParserOptions::default()).unwrap(); + assert_eq!( + property.to_css_string(false, PrinterOptions::default()).unwrap(), + "color: #f0f" + ); + assert_eq!( + property.to_css_string(true, PrinterOptions::default()).unwrap(), + "color: #f0f !important" + ); + + let code = indoc! { r#" + .foo { + color: green; + } + + .bar { + color: red; + background: pink; + } + + @media print { + .baz { + color: green; + } + } + "#}; + let stylesheet = StyleSheet::parse(code, ParserOptions::default()).unwrap(); + if let CssRule::Style(style) = &stylesheet.rules.0[1] { + let (key, val) = style.property_location(code, 0).unwrap(); + assert_eq!( + key, + SourceLocation { line: 5, column: 3 }..SourceLocation { line: 5, column: 8 } + ); + assert_eq!( + val, + SourceLocation { line: 5, column: 10 }..SourceLocation { line: 5, column: 13 } + ); + } + + if let CssRule::Style(style) = &stylesheet.rules.0[1] { + let (key, val) = style.property_location(code, 1).unwrap(); + assert_eq!( + key, + SourceLocation { line: 6, column: 3 }..SourceLocation { line: 6, column: 13 } + ); + assert_eq!( + val, + SourceLocation { line: 6, column: 15 }..SourceLocation { line: 6, column: 19 } + ); + } + if let CssRule::Media(media) = &stylesheet.rules.0[2] { + if let CssRule::Style(style) = &media.rules.0[0] { + let (key, val) = style.property_location(code, 0).unwrap(); + assert_eq!( + key, + SourceLocation { line: 11, column: 5 }..SourceLocation { line: 11, column: 10 } + ); + assert_eq!( + val, + SourceLocation { line: 11, column: 12 }..SourceLocation { line: 11, column: 17 } + ); + } + } + + let mut property = Property::Transform(Default::default(), VendorPrefix::WebKit); + property.set_prefix(VendorPrefix::None); + assert_eq!(property, Property::Transform(Default::default(), VendorPrefix::None)); + property.set_prefix(VendorPrefix::Moz); + assert_eq!(property, Property::Transform(Default::default(), VendorPrefix::Moz)); + property.set_prefix(VendorPrefix::WebKit | VendorPrefix::Moz); + assert_eq!( + property, + Property::Transform(Default::default(), VendorPrefix::WebKit | VendorPrefix::Moz) + ); + + let mut property = Property::TextDecorationLine(Default::default(), VendorPrefix::None); + property.set_prefix(VendorPrefix::Ms); + assert_eq!( + property, + Property::TextDecorationLine(Default::default(), VendorPrefix::None) + ); + property.set_prefix(VendorPrefix::WebKit | VendorPrefix::Moz | VendorPrefix::Ms); + assert_eq!( + property, + Property::TextDecorationLine(Default::default(), VendorPrefix::WebKit | VendorPrefix::Moz) + ); + + let mut property = Property::AccentColor(Default::default()); + property.set_prefix(VendorPrefix::WebKit); + assert_eq!(property, Property::AccentColor(Default::default())); + } + + #[cfg(feature = "substitute_variables")] + #[test] + fn test_substitute_vars() { + use crate::properties::custom::TokenList; + use crate::traits::ParseWithOptions; + + fn test(property: Property, vars: HashMap<&str, &str>, expected: &str) { + if let Property::Unparsed(unparsed) = property { + let vars = vars + .into_iter() + .map(|(k, v)| { + ( + k, + TokenList::parse_string_with_options(v, ParserOptions::default()).unwrap(), + ) + }) + .collect(); + let substituted = unparsed.substitute_variables(&vars).unwrap(); + assert_eq!( + substituted.to_css_string(false, PrinterOptions::default()).unwrap(), + expected + ); + } else { + panic!("Not an unparsed property"); + } + } + + let property = Property::parse_string("color".into(), "var(--test)", ParserOptions::default()).unwrap(); + test(property, HashMap::from([("--test", "yellow")]), "color: #ff0"); + + let property = + Property::parse_string("color".into(), "var(--test, var(--foo))", ParserOptions::default()).unwrap(); + test(property, HashMap::from([("--foo", "yellow")]), "color: #ff0"); + let property = Property::parse_string( + "color".into(), + "var(--test, var(--foo, yellow))", + ParserOptions::default(), + ) + .unwrap(); + test(property, HashMap::new(), "color: #ff0"); + + let property = + Property::parse_string("width".into(), "calc(var(--a) + var(--b))", ParserOptions::default()).unwrap(); + test(property, HashMap::from([("--a", "2px"), ("--b", "4px")]), "width: 6px"); + + let property = Property::parse_string("color".into(), "var(--a)", ParserOptions::default()).unwrap(); + test( + property, + HashMap::from([("--a", "var(--b)"), ("--b", "yellow")]), + "color: #ff0", + ); + + let property = Property::parse_string("color".into(), "var(--a)", ParserOptions::default()).unwrap(); + test( + property, + HashMap::from([("--a", "var(--b)"), ("--b", "var(--c)"), ("--c", "var(--a)")]), + "color: var(--a)", + ); + } + + #[test] + fn test_layer() { + minify_test("@layer foo;", "@layer foo;"); + minify_test("@layer foo, bar;", "@layer foo,bar;"); + minify_test("@layer foo.bar;", "@layer foo.bar;"); + minify_test("@layer foo.bar, baz;", "@layer foo.bar,baz;"); + + minify_test( + r#" + @layer foo { + .bar { + color: red; + } + } + "#, + "@layer foo{.bar{color:red}}", + ); + minify_test( + r#" + @layer foo.bar { + .bar { + color: red; + } + } + "#, + "@layer foo.bar{.bar{color:red}}", + ); + minify_test(r#" + @layer base { + p { max-width: 70ch; } + } + + @layer framework { + @layer base { + p { margin-block: 0.75em; } + } + + @layer theme { + p { color: #222; } + } + } + "#, "@layer base{p{max-width:70ch}}@layer framework{@layer base{p{margin-block:.75em}}@layer theme{p{color:#222}}}"); + minify_test( + r#" + @layer { + .bar { + color: red; + } + } + "#, + "@layer{.bar{color:red}}", + ); + minify_test( + r#" + @layer foo\20 bar, baz; + "#, + "@layer foo\\ bar,baz;", + ); + minify_test( + r#" + @layer one.two\20 three\#four\.five { + .bar { + color: red; + } + } + "#, + "@layer one.two\\ three\\#four\\.five{.bar{color:red}}", + ); + + error_test("@layer;", ParserError::UnexpectedToken(Token::Semicolon)); + error_test("@layer foo, bar {};", ParserError::AtRuleBodyInvalid); + minify_test("@import 'test.css' layer;", "@import \"test.css\" layer;"); + minify_test("@import 'test.css' layer(foo);", "@import \"test.css\" layer(foo);"); + minify_test( + "@import 'test.css' layer(foo.bar);", + "@import \"test.css\" layer(foo.bar);", + ); + minify_test( + "@import 'test.css' layer(foo\\20 bar);", + "@import \"test.css\" layer(foo\\ bar);", + ); + error_test( + "@import 'test.css' layer(foo, bar) {};", + ParserError::UnexpectedToken(Token::Comma), + ); + minify_test( + r#" + @layer one { + body { + background: red; + } + } + + body { + background: red; + } + + @layer two { + body { + background: green; + } + } + + @layer one { + body { + background: yellow; + } + } + "#, + "@layer one{body{background:#ff0}}body{background:red}@layer two{body{background:green}}", + ); + } + + #[test] + fn test_property() { + minify_test( + r#" + @property --property-name { + syntax: ''; + inherits: false; + initial-value: yellow; + } + "#, + "@property --property-name{syntax:\"\";inherits:false;initial-value:#ff0}", + ); + + test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; + initial-value: ; + } + "#, + indoc! {r#" + @property --property-name { + syntax: "*"; + inherits: false; + initial-value: ; + } + "#}, + ); + + minify_test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; + initial-value: ; + } + "#, + "@property --property-name{syntax:\"*\";inherits:false;initial-value:}", + ); + + test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; + initial-value:; + } + "#, + indoc! {r#" + @property --property-name { + syntax: "*"; + inherits: false; + initial-value: ; + } + "#}, + ); + + minify_test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; + initial-value:; + } + "#, + "@property --property-name{syntax:\"*\";inherits:false;initial-value:}", + ); + minify_test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; + initial-value: foo bar; + } + "#, + "@property --property-name{syntax:\"*\";inherits:false;initial-value:foo bar}", + ); + + minify_test( + r#" + @property --property-name { + syntax: ''; + inherits: true; + initial-value: 25px; + } + "#, + "@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 { + syntax: ''; + inherits: false; + initial-value: 25px; + } + "#, + ParserError::UnexpectedToken(crate::properties::custom::Token::Dimension { + has_sign: false, + value: 25.0, + int_value: Some(25), + unit: "px".into(), + }), + ); + + error_test( + r#" + @property --property-name { + syntax: ''; + inherits: false; + initial-value: var(--some-value); + } + "#, + ParserError::UnexpectedToken(crate::properties::custom::Token::Function("var".into())), + ); + + error_test( + r#" + @property --property-name { + syntax: ''; + inherits: false; + } + "#, + ParserError::AtRuleBodyInvalid, + ); + + minify_test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; + } + "#, + "@property --property-name{syntax:\"*\";inherits:false}", + ); + + error_test( + r#" + @property --property-name { + syntax: '*'; + } + "#, + ParserError::AtRuleBodyInvalid, + ); + + error_test( + r#" + @property --property-name { + inherits: false; + } + "#, + ParserError::AtRuleBodyInvalid, + ); + + error_test( + r#" + @property property-name { + syntax: '*'; + inherits: false; + } + "#, + ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("property-name".into())), + ); + + minify_test( + r#" + @property --property-name { + syntax: 'custom | '; + inherits: false; + initial-value: yellow; + } + "#, + "@property --property-name{syntax:\"custom|\";inherits:false;initial-value:#ff0}", + ); + + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test(r#" + // @property --property-name { + // syntax: ''; + // inherits: false; + // initial-value: translate(200px,300px) translate(100px,200px) scale(2); + // } + // "#, "@property --property-name{syntax:\"\";inherits:false;initial-value:matrix(2,0,0,2,300,500)}"); + + minify_test( + r#" + @property --property-name { + syntax: '