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 918afd74a..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,6 +109,9 @@ jobs: - target: aarch64-unknown-linux-gnu strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + - target: aarch64-linux-android + strip: llvm-strip + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - target: armv7-unknown-linux-gnueabihf strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57 @@ -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.22.0 + uses: cross-platform-actions/action@v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -170,7 +187,7 @@ 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' @@ -198,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: | @@ -229,7 +246,7 @@ jobs: 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 @@ -247,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/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 90fbcd7c2..bb4bb2840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,26 +1,26 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.3", "once_cell", "serde", "version_check", @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -53,26 +53,21 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "assert_cmd" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", - "predicates 3.0.4", + "libc", + "predicates 3.1.3", "predicates-core", "predicates-tree", "wait-timeout", @@ -80,14 +75,14 @@ dependencies = [ [[package]] name = "assert_fs" -version = "1.0.13" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" +checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" dependencies = [ "anstyle", "doc-comment", "globwalk", - "predicates 3.0.4", + "predicates 3.1.3", "predicates-core", "predicates-tree", "tempfile", @@ -106,9 +101,9 @@ dependencies = [ [[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" @@ -127,9 +122,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -143,36 +141,38 @@ dependencies = [ "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.4" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bda9b4595376bf255f68dafb5dcc5b0e2842b38dc2a7b52c4e0bfe9fd1c651" +checksum = "8dd48a6ca358df4f7000e3fb5f08738b1b91a0e5d5f862e2f77b2b14647547f5" dependencies = [ - "ahash 0.8.6", - "anyhow", + "ahash 0.8.12", + "browserslist-data", "chrono", "either", - "getrandom", - "itertools 0.10.5", - "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.7.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "regex-automata", @@ -181,15 +181,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" dependencies = [ "proc-macro2", "quote", @@ -208,10 +208,10 @@ dependencies = [ ] [[package]] -name = "byteorder" -version = "1.5.0" +name = "bytes" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbindgen" @@ -221,7 +221,7 @@ checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" dependencies = [ "clap", "heck", - "indexmap", + "indexmap 1.9.3", "log", "proc-macro2", "quote", @@ -234,11 +234,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -249,15 +249,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", - "wasm-bindgen", "windows-targets", ] @@ -271,7 +269,7 @@ dependencies = [ "bitflags 1.3.2", "clap_derive", "clap_lex", - "indexmap", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", @@ -331,52 +329,43 @@ 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.8" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +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.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "cssparser" @@ -387,7 +376,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf", "serde", "smallvec", ] @@ -408,17 +397,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] name = "ctor" -version = "0.1.26" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] @@ -428,7 +417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.2", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -436,9 +425,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "data-url" @@ -449,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" @@ -469,30 +464,36 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys", @@ -500,9 +501,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "float-cmp" @@ -513,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" @@ -532,47 +527,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] -name = "fxhash" -version = "0.2.1" +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.10" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", - "wasi", - "wasm-bindgen", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "globset" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +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", ] @@ -583,14 +579,20 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", ] [[package]] name = "hashbrown" -version = "0.14.2" +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 = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -609,9 +611,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -632,17 +634,16 @@ dependencies = [ [[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", ] @@ -657,6 +658,17 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + [[package]] name = "indoc" version = "1.0.9" @@ -674,18 +686,18 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jemalloc-sys" @@ -710,44 +722,45 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +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.150" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-sys", + "windows-targets", ] [[package]] name = "lightningcss" -version = "1.0.0-alpha.52" +version = "1.0.0-alpha.70" dependencies = [ - "ahash 0.7.7", + "ahash 0.8.12", "assert_cmd", "assert_fs", "atty", - "bitflags 2.4.1", + "bitflags 2.6.0", "browserslist-rs", "clap", "const-str", @@ -755,6 +768,8 @@ dependencies = [ "cssparser-color", "dashmap", "data-encoding", + "getrandom 0.3.3", + "indexmap 2.7.0", "indoc", "itertools 0.10.5", "jemallocator", @@ -762,12 +777,14 @@ dependencies = [ "lightningcss-derive", "parcel_selectors", "parcel_sourcemap", - "paste", + "pastey", "pathdiff", "predicates 2.1.5", + "pretty_assertions", "rayon", "schemars", "serde", + "serde-content", "serde_json", "smallvec", "static-self", @@ -775,13 +792,31 @@ dependencies = [ [[package]] name = "lightningcss-derive" -version = "1.0.0-alpha.42" +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" @@ -796,32 +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 = "linux-raw-sys" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -829,9 +856,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matches" @@ -841,18 +868,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.9.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -862,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]] @@ -883,23 +901,23 @@ checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b" [[package]] name = "napi-derive" -version = "2.14.0" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01328a2da5c52a77fe839ec8576ddf063529cf23db8958b1030005675f32c45" +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.53" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2470ee27b89da8973defd10dec6def6b412f2873ecc173c9cdc7f44e324d83dc" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" dependencies = [ "convert_case", "once_cell", @@ -907,24 +925,18 @@ dependencies = [ "quote", "regex", "semver", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "napi-sys" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" +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" @@ -943,18 +955,18 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "os_str_bytes" @@ -970,15 +982,15 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "parcel_selectors" -version = "0.26.4" +version = "0.28.2" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "cssparser", - "fxhash", "log", - "phf 0.10.1", + "phf", "phf_codegen", "precomputed-hash", + "rustc-hash", "schemars", "serde", "smallvec", @@ -999,21 +1011,11 @@ 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.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", @@ -1023,25 +1025,16 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.14" +name = "pastey" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +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" - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "phf" @@ -1050,27 +1043,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", - "phf_shared 0.11.2", + "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - -[[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 = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ - "phf_shared 0.10.0", - "rand", + "phf_generator", + "phf_shared", ] [[package]] @@ -1079,7 +1062,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared 0.11.2", + "phf_shared", "rand", ] @@ -1089,20 +1072,11 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.39", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", + "syn 2.0.90", ] [[package]] @@ -1114,12 +1088,6 @@ 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" @@ -1142,32 +1110,41 @@ dependencies = [ [[package]] name = "predicates" -version = "3.0.4" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", - "itertools 0.11.0", "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" @@ -1194,9 +1171,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1223,13 +1200,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -1242,18 +1225,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", "rand_core", ] @@ -1262,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.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1278,9 +1246,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1288,18 +1256,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1309,9 +1277,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1320,27 +1288,28 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ "bytecheck", ] [[package]] name = "rkyv" -version = "0.7.42" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", + "bytes", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -1352,22 +1321,28 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.42" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +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.38.21" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1376,9 +1351,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -1391,11 +1366,12 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.15" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", + "indexmap 2.7.0", "schemars_derive", "serde", "serde_json", @@ -1404,14 +1380,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.15" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] @@ -1428,19 +1404,29 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "semver" -version = "1.0.20" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +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" @@ -1452,58 +1438,63 @@ dependencies = [ ] [[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.12" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "serde", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "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.108" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +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" @@ -1515,9 +1506,9 @@ 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" @@ -1527,17 +1518,18 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] [[package]] name = "static-self" -version = "0.1.1" +version = "0.1.2" dependencies = [ + "indexmap 2.7.0", "smallvec", "static-self-derive", ] @@ -1551,32 +1543,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "string_cache" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" -dependencies = [ - "new_debug_unreachable", - "once_cell", - "parking_lot", - "phf_shared 0.10.0", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", -] - [[package]] name = "strsim" version = "0.10.0" @@ -1596,9 +1562,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1613,73 +1579,63 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "once_cell", "rustix", "windows-sys", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +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.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", + "syn 2.0.90", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -1701,27 +1657,27 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +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 = "uuid" -version = "1.5.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +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" @@ -1740,9 +1696,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -1754,36 +1710,45 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1791,22 +1756,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "winapi" @@ -1826,11 +1791,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys", ] [[package]] @@ -1841,31 +1806,32 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -1874,45 +1840,60 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] [[package]] name = "wyz" @@ -1929,22 +1910,28 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index b4e6454f6..c113a3921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,22 @@ [workspace] members = [ "node", + "napi", "selectors", "c", "derive", "static-self", - "static-self-derive" + "static-self-derive", ] [package] authors = ["Devon Govett "] name = "lightningcss" -version = "1.0.0-alpha.52" +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] @@ -33,24 +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 = ["static-self", "static-self/smallvec", "parcel_selectors/into_owned"] +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 } +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.26.4", path = "./selectors" } +parcel_selectors = { version = "0.28.2", path = "./selectors" } itertools = "0.10.1" smallvec = { version = "1.7.0", features = ["union"] } bitflags = "2.2.1" @@ -59,21 +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.42", path = "./derive", optional = true } -schemars = { version = "0.8.11", features = ["smallvec"], optional = true } -static-self = { version = "0.1.0", path = "static-self", 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" @@ -81,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 035544626..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. 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 68bfc7e48..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; @@ -279,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 1864748e9..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.42" +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/lib.rs b/derive/src/lib.rs index 00b77a266..122414911 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,293 +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 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: ?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 }) - } + 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/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 94% rename from node/src/transformer.rs rename to napi/src/transformer.rs index 111ff2cd1..bab931f2d 100644 --- a/node/src/transformer.rs +++ b/napi/src/transformer.rs @@ -24,7 +24,7 @@ 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, @@ -99,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 { @@ -112,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(); } } _ => {} @@ -177,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 )|+; } @@ -190,12 +191,13 @@ 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()) }}; } @@ -296,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", @@ -308,8 +311,10 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Scope(..) => "scope", CssRule::MozDocument(..) => "moz-document", CssRule::Nesting(..) => "nesting", + CssRule::NestedDeclarations(..) => "nested-declarations", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", + CssRule::ViewTransition(..) => "view-transition", CssRule::Unknown(v) => { let name = v.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "unknown", name) { @@ -750,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; @@ -764,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, @@ -806,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()) @@ -823,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 5b26eea75..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.33.0" -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.3", 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 3ad4ee7ce..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; @@ -89,6 +97,10 @@ export type Rule = | { type: "starting-style"; value: StartingStyleRule; } +| { + type: "view-transition"; + value: ViewTransitionRule; + } | { type: "ignored"; } @@ -122,6 +134,10 @@ export type MediaCondition = */ operator: Operator; type: "operation"; + } + | { + type: "unknown"; + value: TokenOrValue[]; }; /** * A generic media feature or container feature. @@ -504,6 +520,10 @@ export type TokenOrValue = | { type: "dashed-ident"; value: String; + } + | { + type: "animation-name"; + value: AnimationName; }; /** * A raw CSS token. @@ -637,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"; }; @@ -957,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. */ @@ -998,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. @@ -1052,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. */ @@ -1873,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; @@ -2004,6 +2116,12 @@ export type PropertyId = property: "text-size-adjust"; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + } + | { + property: "unicode-bidi"; + } | { property: "box-decoration-break"; vendorPrefix: VendorPrefix; @@ -2240,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"; } @@ -3212,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[]; @@ -3380,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; @@ -3686,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"; @@ -4089,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"; @@ -4118,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"; @@ -4297,11 +4477,11 @@ export type WebKitGradientPointComponentFor_HorizontalPositionKeyword = */ export type NumberOrPercentage = | { - type: "percentage"; + type: "number"; value: number; } | { - type: "number"; + type: "percentage"; value: number; }; /** @@ -4612,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. @@ -5051,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 = | { @@ -5355,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. */ @@ -5393,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). */ @@ -5508,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. */ @@ -5528,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. */ @@ -5660,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. */ @@ -5858,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. @@ -5881,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. @@ -6153,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. */ @@ -6165,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. */ @@ -6488,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"; /** @@ -6568,6 +6888,12 @@ export type PseudoElement = | { kind: "first-letter"; } + | { + kind: "details-content"; + } + | { + kind: "target-text"; + } | { kind: "selection"; vendorPrefix: VendorPrefix; @@ -6619,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"; @@ -6687,6 +7032,10 @@ export type KeyframeSelector = } | { type: "to"; + } + | { + type: "timeline-range-percentage"; + value: TimelineRangePercentage; }; /** * KeyframesName @@ -6867,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). */ @@ -6913,6 +7273,10 @@ export type ParsedComponent = type: "length-percentage"; value: DimensionPercentageFor_LengthValue; } + | { + type: "string"; + value: String; + } | { type: "color"; value: CssColor; @@ -6971,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. @@ -7014,6 +7378,9 @@ export type SyntaxComponentKind = | { type: "length-percentage"; } + | { + type: "string"; + } | { type: "color"; } @@ -7073,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. @@ -7146,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; @@ -7164,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; /** @@ -8015,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 { @@ -8034,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 { @@ -8063,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 { /** @@ -8076,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 { /** @@ -8089,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 { /** @@ -8351,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. */ @@ -8383,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. */ @@ -8420,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. */ @@ -8458,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. */ @@ -8830,6 +9335,11 @@ export interface Container { */ name: ContainerNameList; } +export interface ColorScheme { + dark: boolean; + light: boolean; + only: boolean; +} /** * A known property with an unparsed value. * @@ -8863,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. */ @@ -8899,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. */ @@ -8974,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. */ @@ -9106,6 +9680,19 @@ export interface NestingRule { */ style: StyleRule; } +/** + * A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule. + */ +export interface NestedDeclarationsRule { + /** + * The style rule that defines the selector and declarations for the `@nest` rule. + */ + declarations: DeclarationBlock; + /** + * The location of the rule in the source file. + */ + loc: Location2; +} /** * A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule. */ @@ -9219,7 +9806,7 @@ export interface ContainerRule { /** * The container condition. */ - condition: ContainerCondition; + condition?: ContainerCondition | null; /** * The location of the rule in the source file. */ @@ -9269,6 +9856,19 @@ export interface StartingStyleRule { */ 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. */ 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 d138359b6..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, StyleSheet } 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'; @@ -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 = { @@ -213,6 +213,13 @@ export interface Visitor { EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors; } +export type VisitorDependency = FileDependency | GlobDependency; +export interface VisitorOptions { + addDependency: (dep: VisitorDependency) => void +} + +export type VisitorFunction = (options: VisitorOptions) => Visitor; + export interface CustomAtRules { [name: string]: CustomAtRuleDefinition } @@ -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 cb0d78588..822944124 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,570 +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); - - match res { - Ok(res) => res.into_js(ctx), - Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), - } + lightningcss_napi::transform_style_attribute(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(ctx: CallContext) -> napi::Result { + lightningcss_napi::bundle(ctx) } -#[cfg(target_arch = "wasm32")] -mod bundle { - use super::*; - use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; - use std::cell::UnsafeCell; - - #[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 resolver = opts.get_named_property::("resolver")?; - let read = resolver.get_named_property::("read")?; - let resolve = if resolver.has_named_property("resolve")? { - let resolve = resolver.get_named_property::("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".into())); - } - - value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; - } - - value.try_into() - } - - 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 mut source: JsUnknown = read.call(None, &[file])?; - let source = get_result(self.env, 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)?.into_utf8()?; - Ok(PathBuf::from_str(result.as_str()?).unwrap()) - } else { - Ok(originating_file.with_file_name(specifier)) - } - } - } +#[cfg(not(target_arch = "wasm32"))] +#[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::bundle)?; - + exports.create_named_method("bundle", bundle)?; #[cfg(not(target_arch = "wasm32"))] { - exports.create_named_method("bundleAsync", bundle::bundle_async)?; + exports.create_named_method("bundleAsync", bundle_async)?; } Ok(()) @@ -573,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")] @@ -603,631 +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)] - 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::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::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 7199c3272..4279e51c8 100644 --- a/node/test/bundle.test.mjs +++ b/node/test/bundle.test.mjs @@ -2,11 +2,15 @@ import path from 'path'; import fs from 'fs'; 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) { @@ -361,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, @@ -410,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 af5fd99c3..4379cf481 100644 --- a/node/test/composeVisitors.test.mjs +++ b/node/test/composeVisitors.test.mjs @@ -2,11 +2,15 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; +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); @@ -509,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({ @@ -682,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 ed0734791..e53f65ad5 100644 --- a/node/test/customAtRules.mjs +++ b/node/test/customAtRules.mjs @@ -3,11 +3,15 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; 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; diff --git a/node/test/transform.test.mjs b/node/test/transform.test.mjs index d68062995..1b56bbc18 100644 --- a/node/test/transform.test.mjs +++ b/node/test/transform.test.mjs @@ -1,10 +1,14 @@ 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); diff --git a/node/test/visitor.test.mjs b/node/test/visitor.test.mjs index f57a2b973..149825b7d 100644 --- a/node/test/visitor.test.mjs +++ b/node/test/visitor.test.mjs @@ -3,11 +3,15 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; 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); @@ -245,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({ @@ -1104,4 +1170,119 @@ test('visit stylesheet', () => { assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}'); }); +test('visitor function', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @dep "foo.js"; + + .foo { + width: 32px; + } + `), + visitor: ({addDependency}) => ({ + Rule: { + unknown: { + dep(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }) + }); + + assert.equal(res.code.toString(), '.foo{width:32px}'); + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'foo.js' + }]); +}); + +test('visitor function works with style attributes', () => { + let res = transformStyleAttribute({ + filename: 'test.css', + minify: true, + code: Buffer.from('height: 12px'), + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'test.json' + }]); +}); + +test('visitor function works with bundler', () => { + let res = bundle({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + +test('works with async bundler', async () => { + let res = await bundleAsync({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + test.run(); diff --git a/package.json b/package.json index 4bdbf22c1..af17c0a58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.23.0", + "version": "1.31.1", "license": "MPL-2.0", "description": "A CSS parser, transformer, and minifier written in Rust", "main": "node/index.js", @@ -39,20 +39,21 @@ "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.3.29", + "@mdn/browser-compat-data": "~7.2.4", "@napi-rs/cli": "^2.14.0", - "autoprefixer": "^10.4.16", + "autoprefixer": "^10.4.23", + "caniuse-lite": "^1.0.30001765", "codemirror": "^6.0.1", - "cssnano": "^5.0.8", + "cssnano": "^7.0.6", "esbuild": "^0.19.8", "flowgen": "^1.21.0", "jest-diff": "^27.4.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" }, 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 4e6bbebf5..883aa442d 100644 --- a/scripts/build-ast.js +++ b/scripts/build-ast.js @@ -55,6 +55,48 @@ compileFromFile('node/ast.json', { 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 bb85e84f9..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; } @@ -461,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()) @@ -634,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 9f69a9af8..718821da9 100644 --- a/scripts/build-wasm.js +++ b/scripts/build-wasm.js @@ -54,15 +54,21 @@ wasmPkg.type = 'module'; wasmPkg.main = 'index.mjs'; wasmPkg.module = 'index.mjs'; wasmPkg.exports = { - types: './index.d.ts', - node: { - import: './wasm-node.mjs', - require: './wasm-node.cjs', + '.': { + types: './index.d.ts', + node: { + import: './wasm-node.mjs', + require: './wasm-node.cjs' + }, + default: { + import: './index.mjs', + require: './index.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; diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index c3dd94026..824b2f2bd 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parcel_selectors" -version = "0.26.4" +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" @@ -24,14 +25,14 @@ serde = ["dep:serde", "smallvec/serde"] [dependencies] bitflags = "2.2.1" cssparser = "0.33.0" -fxhash = "0.2" +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 } -static-self = { version = "0.1.0", path = "../static-self", 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/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 56217d284..2047b4e61 100644 --- a/selectors/lib.rs +++ b/selectors/lib.rs @@ -9,18 +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; -#[cfg(feature = "into_owned")] -extern crate static_self; 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 25b1445b5..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> { @@ -877,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() } @@ -900,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() } @@ -1006,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, } @@ -1089,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. @@ -1299,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", @@ -1319,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'), @@ -2140,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; } @@ -2955,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(..) => { @@ -2966,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()? { @@ -2988,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) @@ -3336,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>( @@ -3351,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( @@ -3364,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 { @@ -3376,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()) } @@ -3911,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 d62014096..f0c828d75 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -6,14 +6,18 @@ use crate::targets::Browsers; #[derive(Clone, Copy, PartialEq)] pub enum Feature { AbsFunction, + AccentSystemColor, AfarListStyleType, AmharicAbegedeListStyleType, AmharicListStyleType, + AnchorSizeSize, + AnimationTimelineShorthand, AnyLink, AnyPseudo, ArabicIndicListStyleType, ArmenianListStyleType, AsterisksListStyleType, + AutoSize, Autofill, BengaliListStyleType, BinaryListStyleType, @@ -24,6 +28,7 @@ pub enum Feature { CapUnit, CaseInsensitive, ChUnit, + Checkmark, CircleListStyleType, CjkDecimalListStyleType, CjkEarthlyBranchListStyleType, @@ -38,6 +43,7 @@ pub enum Feature { DecimalLeadingZeroListStyleType, DecimalListStyleType, DefaultPseudo, + DetailsContent, DevanagariListStyleType, Dialog, DirSelector, @@ -45,6 +51,7 @@ pub enum Feature { DisclosureClosedListStyleType, DisclosureOpenListStyleType, DoublePositionGradients, + EmUnit, EthiopicAbegedeAmEtListStyleType, EthiopicAbegedeGezListStyleType, EthiopicAbegedeListStyleType, @@ -80,6 +87,7 @@ pub enum Feature { Gencontent, GeorgianListStyleType, GradientInterpolationHints, + GrammarError, GujaratiListStyleType, GurmukhiListStyleType, HasSelector, @@ -106,6 +114,7 @@ pub enum Feature { LangSelectorList, LaoListStyleType, LhUnit, + LightDark, LinearGradient, LogicalBorderRadius, LogicalBorderShorthand, @@ -134,10 +143,10 @@ pub enum Feature { MinFunction, ModFunction, MongolianListStyleType, - MozAvailableSize, MyanmarListStyleType, Namespaces, Nesting, + NoneListStyleType, NotSelectorList, NthChildOf, OctalListStyleType, @@ -149,6 +158,8 @@ pub enum Feature { P3Colors, PartPseudo, PersianListStyleType, + Picker, + PickerIcon, PlaceContent, PlaceItems, PlaceSelf, @@ -156,12 +167,16 @@ pub enum Feature { PlaceholderShown, QUnit, RadialGradient, + RcapUnit, + RchUnit, ReadOnlyWrite, RemFunction, RemUnit, RepeatingConicGradient, RepeatingLinearGradient, RepeatingRadialGradient, + RexUnit, + RicUnit, RlhUnit, RoundFunction, Selection, @@ -174,11 +189,14 @@ pub enum Feature { SimpChineseInformalListStyleType, SomaliListStyleType, SpaceSeparatedColorNotation, + SpellingError, SquareListStyleType, + StatePseudoClass, StretchSize, StringListStyleType, SymbolsListStyleType, TamilListStyleType, + TargetText, TeluguListStyleType, TextDecorationThicknessPercent, TextDecorationThicknessShorthand, @@ -200,6 +218,7 @@ pub enum Feature { VbUnit, VhUnit, ViUnit, + ViewTransition, ViewportPercentageUnitsDynamic, ViewportPercentageUnitsLarge, ViewportPercentageUnitsSmall, @@ -433,7 +452,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -525,7 +544,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -570,7 +589,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -584,6 +603,11 @@ 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; @@ -599,17 +623,27 @@ impl Feature { 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.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; } } @@ -645,7 +679,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -690,7 +724,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -735,7 +769,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -780,7 +814,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -872,7 +906,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -917,7 +951,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -956,17 +990,12 @@ impl Feature { return false; } } - if let Some(version) = browsers.ios_saf { - if version < 1114624 { - return false; - } - } if let Some(version) = browsers.samsung { if version < 655616 { return false; } } - if browsers.android.is_some() || browsers.ie.is_some() { + if browsers.android.is_some() || browsers.ie.is_some() || browsers.ios_saf.is_some() { return false; } } @@ -986,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 < 7798784 { + if version < 9371648 { return false; } } @@ -1011,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; } } @@ -1047,7 +1066,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1137,7 +1156,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1182,7 +1201,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1196,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; @@ -1211,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; } } @@ -1300,7 +1338,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1345,7 +1383,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1359,6 +1397,11 @@ impl Feature { } } Feature::Nesting => { + if let Some(version) = browsers.edge { + if version < 7864320 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 7667712 { return false; @@ -1374,17 +1417,27 @@ impl Feature { return false; } } + if let Some(version) = browsers.opera { + if version < 6946816 { + return false; + } + } if let Some(version) = browsers.ios_saf { if version < 1114624 { return false; } } - if browsers.android.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; } } @@ -1420,7 +1473,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1465,7 +1518,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1510,7 +1563,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1577,7 +1630,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7798784 { + if version < 9371648 { return false; } } @@ -1590,7 +1643,7 @@ impl Feature { return false; } } - Feature::CustomMediaQueries | Feature::FitContentFunctionSize | Feature::StretchSize => return false, + Feature::CustomMediaQueries | Feature::FitContentFunctionSize => return false, Feature::DoublePositionGradients => { if let Some(version) = browsers.chrome { if version < 4653056 { @@ -2163,7 +2216,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 458752 { return false; } } @@ -2238,7 +2291,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2283,7 +2336,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2328,7 +2381,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2401,7 +2454,7 @@ impl Feature { return false; } } - Feature::TextDecorationThicknessPercent | Feature::TextDecorationThicknessShorthand => { + Feature::TextDecorationThicknessPercent => { if let Some(version) = browsers.chrome { if version < 5701632 { return false; @@ -2422,6 +2475,16 @@ impl Feature { 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; @@ -2432,7 +2495,52 @@ 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; + } + } + Feature::TextDecorationThicknessShorthand => { + 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 < 5177344 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4063232 { + 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; + } + } + if let Some(version) = browsers.android { + if version < 5701632 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -2558,7 +2666,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -2677,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; @@ -2687,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; } } @@ -2708,7 +2826,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version < 6356992 { + if version < 4915200 { return false; } } @@ -2781,17 +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; @@ -2802,121 +2930,210 @@ 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 < 1769472 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8192000 { + return false; + } + } + if browsers.ie.is_some() { return false; } } - Feature::GradientInterpolationHints => { + Feature::AbsFunction | Feature::SignFunction => { if let Some(version) = browsers.chrome { - if version < 2621440 { + if version < 9043968 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 9043968 { return false; } } if let Some(version) = browsers.firefox { - if version < 2359296 { + if version < 7733248 { return false; } } if let Some(version) = browsers.opera { - if version < 1769472 { + if version < 5963776 { return false; } } if let Some(version) = browsers.safari { - if version < 458752 { + if version < 984064 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { + if version < 984064 { return false; } } if let Some(version) = browsers.android { - if version < 2621440 { + if version < 9043968 { return false; } } - if browsers.ie.is_some() { + if browsers.ie.is_some() || browsers.samsung.is_some() { return false; } } - Feature::BorderImageRepeatRound => { + Feature::HypotFunction => { if let Some(version) = browsers.chrome { - if version < 1966080 { + if version < 7864320 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 7864320 { return false; } } if let Some(version) = browsers.firefox { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { + if version < 7733248 { return false; } } if let Some(version) = browsers.opera { - if version < 1179648 { + if version < 5242880 { return false; } } if let Some(version) = browsers.safari { - if version < 590080 { + if version < 984064 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 590592 { + if version < 984064 { return false; } } if let Some(version) = browsers.samsung { - if version < 131072 { + if version < 1638400 { return false; } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 7864320 { return false; } } + if browsers.ie.is_some() { + return false; + } } - Feature::BorderImageRepeatSpace => { + Feature::GradientInterpolationHints => { if let Some(version) = browsers.chrome { - if version < 3670016 { + if version < 2621440 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 5177344 { return false; } } if let Some(version) = browsers.firefox { - if version < 3276800 { + if version < 2359296 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 1769472 { + 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 < 262144 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 2621440 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::BorderImageRepeatRound => { + if let Some(version) = browsers.chrome { + if version < 1966080 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 786432 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 983040 { + return false; + } + } + if let Some(version) = browsers.ie { + if version < 720896 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 1179648 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 590080 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 590592 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 131072 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 263168 { + return false; + } + } + } + Feature::BorderImageRepeatSpace => { + if let Some(version) = browsers.chrome { + if version < 3670016 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 786432 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 3276800 { return false; } } @@ -2993,7 +3210,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 262144 { + if version < 2752512 { return false; } } @@ -3178,72 +3395,635 @@ 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 < 852224 { + if version < 1115392 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 852992 { + 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 < 8060928 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::AccentSystemColor => { + if let Some(version) = browsers.firefox { + 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; + } + } + 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() + { + return false; + } + } + Feature::AnimationTimelineShorthand => { + if let Some(version) = browsers.chrome { + if version < 7536640 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7536640 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5046272 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1507328 { + return false; + } + } + if let Some(version) = browsers.android { + 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::ViewTransition => { + if let Some(version) = browsers.chrome { + if version < 7143424 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7143424 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 9437184 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4849664 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1179648 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1179648 { + 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.ie.is_some() { + return false; + } + } + Feature::DetailsContent => { + if let Some(version) = browsers.chrome { + if version < 8585216 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8585216 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 9371648 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5701632 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1180672 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1180672 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1900544 { return false; } } if let Some(version) = browsers.android { + if version < 8585216 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::TargetText => { + if let Some(version) = browsers.chrome { + if version < 5832704 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5832704 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 8585216 { + return false; + } + } + if let Some(version) = browsers.opera { if version < 4128768 { return false; } } - if browsers.ie.is_some() { - return false; - } - } - Feature::CapUnit => { - if let Some(version) = browsers.firefox { - if version < 6356992 { + 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; } } - 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::ChUnit => { + Feature::ExUnit + | Feature::CircleListStyleType + | Feature::DecimalListStyleType + | Feature::DiscListStyleType + | Feature::SquareListStyleType => { if let Some(version) = browsers.chrome { - if version < 1769472 { + if version < 1179648 { return false; } } @@ -3258,27 +4038,27 @@ impl Feature { } } if let Some(version) = browsers.ie { - if version < 589824 { + if version < 262144 { return false; } } if let Some(version) = browsers.opera { - if version < 983040 { + if version < 655616 { return false; } } if let Some(version) = browsers.safari { - if version < 458752 { + if version < 65536 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 458752 { + if version < 65536 { return false; } } if let Some(version) = browsers.samsung { - if version < 66816 { + if version < 65536 { return false; } } @@ -3288,19 +4068,19 @@ impl Feature { } } } - Feature::ContainerQueryLengthUnits => { + Feature::IcUnit => { if let Some(version) = browsers.chrome { - if version < 6881280 { + if version < 6946816 { return false; } } if let Some(version) = browsers.edge { - if version < 6881280 { + if version < 6946816 { return false; } } if let Some(version) = browsers.firefox { - if version < 7208960 { + if version < 6356992 { return false; } } @@ -3310,12 +4090,12 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version < 1048576 { + if version < 984064 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 1048576 { + if version < 984064 { return false; } } @@ -3325,7 +4105,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 6881280 { + if version < 6946816 { return false; } } @@ -3333,95 +4113,89 @@ impl Feature { return false; } } - Feature::ExUnit - | Feature::CircleListStyleType - | Feature::DecimalListStyleType - | Feature::DiscListStyleType - | Feature::SquareListStyleType => { + Feature::LhUnit => { if let Some(version) = browsers.chrome { - if version < 1179648 { + if version < 7143424 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 7143424 { return false; } } if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 262144 { + if version < 7864320 { return false; } } if let Some(version) = browsers.opera { - if version < 655616 { + if version < 4849664 { return false; } } if let Some(version) = browsers.safari { - if version < 65536 { + if version < 1049600 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 65536 { + if version < 1049600 { return false; } } if let Some(version) = browsers.samsung { - if version < 65536 { + if version < 1376256 { return false; } } if let Some(version) = browsers.android { - if version < 263168 { + if version < 7143424 { return false; } } + if browsers.ie.is_some() { + return false; + } } - Feature::IcUnit => { + Feature::RcapUnit => { if let Some(version) = browsers.chrome { - if version < 6946816 { + if version < 7733248 { return false; } } if let Some(version) = browsers.edge { - if version < 6946816 { + if version < 7733248 { return false; } } if let Some(version) = browsers.firefox { - if version < 6356992 { + if version < 9633792 { return false; } } if let Some(version) = browsers.opera { - if version < 4718592 { + if version < 5177344 { return false; } } if let Some(version) = browsers.safari { - if version < 984064 { + if version < 1114624 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 984064 { + if version < 1114624 { return false; } } if let Some(version) = browsers.samsung { - if version < 1310720 { + if version < 1638400 { return false; } } if let Some(version) = browsers.android { - if version < 6946816 { + if version < 7733248 { return false; } } @@ -3429,44 +4203,44 @@ impl Feature { return false; } } - Feature::LhUnit => { + Feature::RchUnit | Feature::RexUnit | Feature::RicUnit => { if let Some(version) = browsers.chrome { - if version < 7143424 { + if version < 7274496 { return false; } } if let Some(version) = browsers.edge { - if version < 7143424 { + if version < 7274496 { return false; } } if let Some(version) = browsers.firefox { - if version < 7864320 { + if version < 9633792 { return false; } } if let Some(version) = browsers.opera { - if version < 4849664 { + if version < 4915200 { return false; } } if let Some(version) = browsers.safari { - if version < 1049600 { + if version < 1114624 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 1049600 { + if version < 1114624 { return false; } } if let Some(version) = browsers.samsung { - if version < 1376256 { + if version < 1441792 { return false; } } if let Some(version) = browsers.android { - if version < 7143424 { + if version < 7274496 { return false; } } @@ -3522,11 +4296,26 @@ 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; @@ -3537,13 +4326,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 < 1441792 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7274496 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -3822,7 +4615,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -3869,7 +4662,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4372,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; } } @@ -4388,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; @@ -4413,9 +5213,6 @@ impl Feature { return false; } } - if browsers.ie.is_some() { - return false; - } } Feature::KoreanHangulFormalListStyleType | Feature::KoreanHanjaFormalListStyleType @@ -4667,6 +5464,51 @@ impl Feature { return false; } } + Feature::AnchorSizeSize => { + 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 < 9633792 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5439488 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1703936 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1703936 { + 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::FitContentSize => { if let Some(version) = browsers.chrome { if version < 1638400 { @@ -4714,7 +5556,7 @@ impl Feature { } Feature::MaxContentSize => { if let Some(version) = browsers.chrome { - if version < 3014656 { + if version < 1638400 { return false; } } @@ -4744,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; } } @@ -4802,49 +5644,54 @@ impl Feature { return false; } } - Feature::WebkitFillAvailableSize => { + Feature::StretchSize => { if let Some(version) = browsers.chrome { - if version < 1638400 { + if version < 9043968 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 9043968 { return false; } } if let Some(version) = browsers.opera { - if version < 917504 { + if version < 5963776 { return false; } } - if let Some(version) = browsers.safari { - if version < 458752 { + if let Some(version) = browsers.android { + if version < 9043968 { return false; } } - if let Some(version) = browsers.ios_saf { - if version < 458752 { + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.safari.is_some() + || browsers.samsung.is_some() + { + return false; + } + } + Feature::WebkitFillAvailableSize => { + if let Some(version) = browsers.firefox { + if version < 9568256 { return false; } } - if let Some(version) = browsers.samsung { - if version < 327680 { + if let Some(version) = browsers.safari { + if version < 458752 { return false; } } - if let Some(version) = browsers.android { - if version < 263168 { + if let Some(version) = browsers.ios_saf { + if version < 458752 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::MozAvailableSize => { - if let Some(version) = browsers.firefox { - if version < 262144 { + if let Some(version) = browsers.samsung { + if version < 327680 { return false; } } @@ -4852,10 +5699,7 @@ impl Feature { || browsers.chrome.is_some() || browsers.edge.is_some() || browsers.ie.is_some() - || browsers.ios_saf.is_some() || browsers.opera.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() { return false; } @@ -4893,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 e6fec2f16..0dd3da619 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -3,13 +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::CustomPropertyName; +use crate::properties::custom::{CustomProperty, CustomPropertyName}; use crate::properties::masking::MaskHandler; +use crate::properties::text::{Direction, UnicodeBidi}; use crate::properties::{ align::AlignHandler, animation::AnimationHandler, @@ -30,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. /// @@ -153,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; } }; @@ -172,10 +230,7 @@ impl<'i> DeclarationBlock<'i> { write!(self.declarations, false); write!(self.important_declarations, true); - - dest.dedent(); - dest.newline()?; - dest.write_char('}') + Ok(()) } } @@ -509,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>, } @@ -520,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) @@ -550,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); @@ -579,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/error.rs b/src/error.rs index 42576a332..b4b1b0ab8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,13 +2,13 @@ use crate::properties::custom::Token; use crate::rules::Location; -#[cfg(feature = "into_owned")] -use static_self::IntoOwned; use crate::values::string::CowArcStr; 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. @@ -84,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. @@ -116,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"), @@ -230,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> { @@ -256,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:?}'" + ) + }, } } } @@ -285,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()), @@ -297,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()) + } } } } @@ -337,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 { @@ -349,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" + ), } } } @@ -378,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 46a8621fb..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 @@ -168,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, @@ -179,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); @@ -216,6 +244,39 @@ mod tests { } } + 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 { + css_modules: Some(Default::default()), + ..Default::default() + }, + ); + match res { + Ok(_) => unreachable!(), + Err(e) => assert_eq!(e.kind, error), + } + } + macro_rules! map( { $($key:expr => $name:literal $(referenced: $referenced: literal)? $($value:literal $(global: $global: literal)? $(from $from:literal)?)*),* } => { { @@ -334,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( @@ -1442,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( @@ -2117,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; } @@ -2138,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; } @@ -2160,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; } @@ -2182,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)); } @@ -3952,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] @@ -4101,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}", @@ -4127,6 +4352,26 @@ mod tests { ".foo { background-position: bottom right }", ".foo{background-position:100% 100%}", ); + minify_test( + ".foo { background-position: center top }", + ".foo{background-position:top}", + ); + minify_test( + ".foo { background-position: center bottom }", + ".foo{background-position:bottom}", + ); + minify_test( + ".foo { background-position: left center }", + ".foo{background-position:0}", + ); + minify_test( + ".foo { background-position: right center }", + ".foo{background-position:100%}", + ); + minify_test( + ".foo { background-position: 20px center }", + ".foo{background-position:20px}", + ); minify_test( ".foo { background: url('img-sprite.png') no-repeat bottom right }", @@ -6819,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", @@ -6829,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), @@ -6845,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}"); @@ -6899,33 +7262,201 @@ mod tests { ".foo /deep/ .bar{width:20px}", deep_options.clone(), ); - } - #[test] - fn test_keyframes() { - minify_test( - r#" - @keyframes "test" { - 100% { - background: blue - } - } - "#, - "@keyframes test{to{background:#00f}}", + 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_test( - r#" - @keyframes test { - 100% { - background: blue - } - } - "#, - "@keyframes test{to{background:#00f}}", + minify_error_test_with_options( + ":global(.foo) {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), ); - - // CSS-wide keywords and `none` cannot remove quotes. - minify_test( + 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] + fn test_keyframes() { + minify_test( + r#" + @keyframes "test" { + 100% { + background: blue + } + } + "#, + "@keyframes test{to{background:#00f}}", + ); + minify_test( + r#" + @keyframes test { + 100% { + background: blue + } + } + "#, + "@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#" @keyframes "revert" { from { @@ -6947,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#" @@ -7257,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] @@ -7478,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) }", @@ -7489,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)) }", @@ -7621,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] @@ -7661,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}"); @@ -7680,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] @@ -7712,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] @@ -8133,7 +8759,7 @@ mod tests { } "#, indoc! { r#" - @media (min-color: 3) { + @media not (max-color: 2) { .foo { color: #7fff00; } @@ -8154,7 +8780,7 @@ mod tests { } "#, indoc! { r#" - @media (max-color: 1) { + @media not (min-color: 2) { .foo { color: #7fff00; } @@ -8175,7 +8801,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: 240.001px) { + @media not (max-width: 240px) { .foo { color: #7fff00; } @@ -8238,7 +8864,66 @@ mod tests { } "#, indoc! { r#" - @media (max-width: 239.999px) { + @media not (min-width: 240px) { + .foo { + color: #7fff00; + } + } + "#}, + Browsers { + firefox: Some(60 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @media not (width < 240px) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media (min-width: 240px) { + .foo { + color: #7fff00; + } + } + "#}, + Browsers { + firefox: Some(60 << 16), + ..Browsers::default() + }, + ); + + test( + r#" + @media not (width < 240px) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media (width >= 240px) { + .foo { + color: #7fff00; + } + } + "#}, + ); + + prefix_test( + r#" + @media (width < 240px) and (hover) { + .foo { + color: chartreuse; + } + } + "#, + indoc! { r#" + @media (not (min-width: 240px)) and (hover) { .foo { color: #7fff00; } @@ -8343,7 +9028,28 @@ mod tests { } "#, indoc! { r#" - @media (min-width: 100.001px) and (max-width: 199.999px) { + @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; } @@ -8394,7 +9100,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: calc(1.001px + 1rem)) { + @media not (max-width: calc(1px + 1rem)) { .foo { color: #ff0; } @@ -8412,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; } @@ -8430,7 +9136,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: .001px) { + @media not (max-width: 0) { .foo { color: #ff0; } @@ -8484,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; } @@ -8590,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, @@ -8634,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] @@ -8835,17 +9576,40 @@ mod tests { @layer b, c; "#}, ); - } - #[test] - fn test_merge_rules() { test( r#" - .foo { - color: red; - } - .bar { - color: red; + @layer a; + @import "foo.css"; + + @layer a { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a; + @import "foo.css"; + + @layer a { + foo { + color: red; + } + } + "#}, + ); + } + + #[test] + fn test_merge_rules() { + test( + r#" + .foo { + color: red; + } + .bar { + color: red; } "#, indoc! {r#" @@ -8901,6 +9665,22 @@ mod tests { } "#}, ); + test( + r#" + .foo { + --foo: red; + --foo: purple; + } + .foo { + --foo: green; + } + "#, + indoc! {r#" + .foo { + --foo: green; + } + "#}, + ); test( r#" .foo { @@ -11017,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] @@ -11193,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 { @@ -11204,6 +12095,7 @@ mod tests { animation-play-state: running; animation-delay: 100ms; animation-fill-mode: forwards; + animation-timeline: auto; } "#, indoc! {r#" @@ -11223,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#" @@ -11269,6 +12162,7 @@ mod tests { animation-play-state: running; animation-delay: 100ms; animation-fill-mode: forwards; + animation-timeline: auto; } "#, indoc! {r#" @@ -11281,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(); } "#}, ); @@ -11391,436 +12334,781 @@ 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)}", + 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: 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 scroll(); + } + "#}, + Browsers { + chrome: Some(120 << 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)}"); - minify_test( - ".foo { transform: translate3d(2px, 3px, 4px)", - ".foo{transform:translate3d(2px,3px,4px)}", + 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 { transform: translate3d(10%, 20%, 4px)", - ".foo{transform:translate3d(10%,20%,4px)}", + ".foo { animation-range-start: entry 10% }", + ".foo{animation-range-start:entry 10%}", ); minify_test( - ".foo { transform: translate3d(2px, 0px, 0px)", - ".foo{transform:translate(2px)}", + ".foo { animation-range-start: entry 0% }", + ".foo{animation-range-start:entry}", ); minify_test( - ".foo { transform: translate3d(0px, 2px, 0px)", - ".foo{transform:translateY(2px)}", + ".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 { transform: translate3d(0px, 0px, 2px)", - ".foo{transform:translateZ(2px)}", + ".foo { animation-range-end: exit 10% }", + ".foo{animation-range-end:exit 10%}", ); minify_test( - ".foo { transform: translate3d(2px, 3px, 0px)", - ".foo{transform:translate(2px,3px)}", + ".foo { animation-range-end: exit 100% }", + ".foo{animation-range-end:exit}", ); - minify_test(".foo { transform: scale(2, 3)", ".foo{transform:scale(2,3)}"); - minify_test(".foo { transform: scale(10%, 20%)", ".foo{transform:scale(.1,.2)}"); - minify_test(".foo { transform: scale(2, 2)", ".foo{transform:scale(2)}"); - minify_test(".foo { transform: scale(2, 1)", ".foo{transform:scaleX(2)}"); - minify_test(".foo { transform: scale(1, 2)", ".foo{transform:scaleY(2)}"); - minify_test(".foo { transform: scaleX(2)", ".foo{transform:scaleX(2)}"); - minify_test(".foo { transform: scaleY(2)", ".foo{transform:scaleY(2)}"); - minify_test(".foo { transform: scaleZ(2)", ".foo{transform:scaleZ(2)}"); - minify_test(".foo { transform: scale3d(2, 3, 4)", ".foo{transform:scale3d(2,3,4)}"); - minify_test(".foo { transform: scale3d(2, 1, 1)", ".foo{transform:scaleX(2)}"); - minify_test(".foo { transform: scale3d(1, 2, 1)", ".foo{transform:scaleY(2)}"); - minify_test(".foo { transform: scale3d(1, 1, 2)", ".foo{transform:scaleZ(2)}"); - minify_test(".foo { transform: scale3d(2, 2, 1)", ".foo{transform:scale(2)}"); - minify_test(".foo { transform: rotate(20deg)", ".foo{transform:rotate(20deg)}"); - minify_test(".foo { transform: rotateX(20deg)", ".foo{transform:rotateX(20deg)}"); - minify_test(".foo { transform: rotateY(20deg)", ".foo{transform:rotateY(20deg)}"); - minify_test(".foo { transform: rotateZ(20deg)", ".foo{transform:rotate(20deg)}"); - minify_test(".foo { transform: rotate(360deg)", ".foo{transform:rotate(360deg)}"); + 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 { transform: rotate3d(2, 3, 4, 20deg)", - ".foo{transform:rotate3d(2,3,4,20deg)}", + ".foo { animation-range: entry 10% exit 90% }", + ".foo{animation-range:entry 10% exit 90%}", ); minify_test( - ".foo { transform: rotate3d(1, 0, 0, 20deg)", - ".foo{transform:rotateX(20deg)}", + ".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 { transform: rotate3d(0, 1, 0, 20deg)", - ".foo{transform:rotateY(20deg)}", + ".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 { transform: rotate3d(0, 0, 1, 20deg)", - ".foo{transform:rotate(20deg)}", + ".foo { animation-range: normal normal }", + ".foo{animation-range:normal}", ); - minify_test(".foo { transform: rotate(405deg)}", ".foo{transform:rotate(405deg)}"); - minify_test(".foo { transform: rotateX(405deg)}", ".foo{transform:rotateX(405deg)}"); - minify_test(".foo { transform: rotateY(405deg)}", ".foo{transform:rotateY(405deg)}"); - minify_test(".foo { transform: rotate(-200deg)}", ".foo{transform:rotate(-200deg)}"); - minify_test(".foo { transform: rotate(0)", ".foo{transform:rotate(0)}"); - minify_test(".foo { transform: rotate(0deg)", ".foo{transform:rotate(0)}"); - minify_test( - ".foo { transform: rotateX(-200deg)}", - ".foo{transform:rotateX(-200deg)}", + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit 90%; + } + "#}, ); - minify_test( - ".foo { transform: rotateY(-200deg)}", - ".foo{transform:rotateY(-200deg)}", + test( + r#" + .foo { + animation-range-start: entry 0%; + animation-range-end: entry 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + } + "#}, ); - minify_test( - ".foo { transform: rotate3d(1, 1, 0, -200deg)", - ".foo{transform:rotate3d(1,1,0,-200deg)}", - ); - minify_test(".foo { transform: skew(20deg)", ".foo{transform:skew(20deg)}"); - minify_test(".foo { transform: skew(20deg, 0deg)", ".foo{transform:skew(20deg)}"); - minify_test(".foo { transform: skew(0deg, 20deg)", ".foo{transform:skewY(20deg)}"); - minify_test(".foo { transform: skewX(20deg)", ".foo{transform:skew(20deg)}"); - minify_test(".foo { transform: skewY(20deg)", ".foo{transform:skewY(20deg)}"); - minify_test( - ".foo { transform: perspective(10px)", - ".foo{transform:perspective(10px)}", - ); - minify_test( - ".foo { transform: matrix(1, 2, -1, 1, 80, 80)", - ".foo{transform:matrix(1,2,-1,1,80,80)}", - ); - minify_test( - ".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)}", - ); - minify_test( - ".foo{transform:translate(100px,200px) rotate(45deg)}", - ".foo{transform:translate(100px,200px)rotate(45deg)}", - ); - minify_test( - ".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)}", + test( + r#" + .foo { + animation-range-start: entry 0%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry exit; + } + "#}, ); - minify_test( - ".foo{transform:translateX(calc(2in + 50px))}", - ".foo{transform:translate(242px)}", + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: normal; + } + "#, + indoc! {r#" + .foo { + animation-range: 10%; + } + "#}, ); - minify_test(".foo{transform:translateX(50%)}", ".foo{transform:translate(50%)}"); - minify_test( - ".foo{transform:translateX(calc(50% - 100px + 20px))}", - ".foo{transform:translate(calc(50% - 80px))}", + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% 90%; + } + "#}, ); - minify_test( - ".foo{transform:rotate(calc(10deg + 20deg))}", - ".foo{transform:rotate(30deg)}", + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit; + } + "#}, ); - minify_test( - ".foo{transform:rotate(calc(10deg + 0.349066rad))}", - ".foo{transform:rotate(30deg)}", + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% exit 90%; + } + "#}, ); - minify_test( - ".foo{transform:rotate(calc(10deg + 1.5turn))}", - ".foo{transform:rotate(550deg)}", + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% 90%; + } + "#}, ); - minify_test( - ".foo{transform:rotate(calc(10deg * 2))}", - ".foo{transform:rotate(20deg)}", + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 90%; + } + "#}, ); - minify_test( - ".foo{transform:rotate(calc(-10deg * 2))}", - ".foo{transform:rotate(-20deg)}", + test( + r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#}, ); - minify_test( - ".foo{transform:rotate(calc(10deg + var(--test)))}", - ".foo{transform:rotate(calc(10deg + var(--test)))}", + 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%; + } + "#}, ); - minify_test(".foo { transform: scale(calc(10% + 20%))", ".foo{transform:scale(.3)}"); - minify_test(".foo { transform: scale(calc(.1 + .2))", ".foo{transform:scale(.3)}"); - - minify_test( - ".foo { -webkit-transform: scale(calc(10% + 20%))", - ".foo{-webkit-transform:scale(.3)}", + 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; + } + "#}, ); - - minify_test(".foo { translate: 1px 2px 3px }", ".foo{translate:1px 2px 3px}"); - 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 { 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: 0 1 0 10deg }", ".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: 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)}"); - minify_test(".foo { scale: 0.5; transform: scale(3); }", ".foo{transform:scale(3)}"); - - prefix_test( + test( r#" .foo { - transform: scale(0.5); + animation-range: entry; + animation-range-end: 90%; + animation: spin 100ms; } - "#, + "#, indoc! {r#" .foo { - -webkit-transform: scale(.5); - -moz-transform: scale(.5); - transform: scale(.5); + animation: .1s spin; } - "#}, - Browsers { - firefox: Some(6 << 16), - safari: Some(6 << 16), - ..Browsers::default() - }, + "#}, ); - - prefix_test( + test( r#" .foo { - transform: var(--transform); + animation: spin 100ms; + animation-range: entry; + animation-range-end: 90%; } - "#, + "#, indoc! {r#" .foo { - -webkit-transform: var(--transform); - -moz-transform: var(--transform); - transform: var(--transform); + animation: .1s spin; + animation-range: entry 90%; } - "#}, - Browsers { - firefox: Some(6 << 16), - safari: Some(6 << 16), - ..Browsers::default() - }, + "#}, ); - test( r#" .foo { - transform: translateX(-50%); - transform: translateX(20px); + animation-range: entry; + animation-range-end: 90%; + animation: var(--animation) 100ms; } "#, indoc! {r#" .foo { - transform: translateX(20px); + animation: var(--animation) .1s; } "#}, ); } #[test] - pub fn test_gradients() { - minify_test( - ".foo { background: linear-gradient(yellow, blue) }", - ".foo{background:linear-gradient(#ff0,#00f)}", + 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); + } + "#}, ); - minify_test( - ".foo { background: linear-gradient(to bottom, yellow, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", + test( + ".foo { transform: translate3d(12px,50%,3em)scale(2,.5) }", + indoc! {r#" + .foo { + transform: translate3d(12px, 50%, 3em) scale(2, .5); + } + "#}, ); - minify_test( - ".foo { background: linear-gradient(180deg, yellow, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", + test( + ".foo { transform:matrix(1,2,-1,1,80,80) }", + indoc! {r#" + .foo { + transform: matrix(1, 2, -1, 1, 80, 80); + } + "#}, ); + minify_test( - ".foo { background: linear-gradient(0.5turn, yellow, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", + ".foo { transform: scale( 0.5 )translateX(10px ) }", + ".foo{transform:scale(.5)translate(10px)}", ); minify_test( - ".foo { background: linear-gradient(yellow 10%, blue 20%) }", - ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", + ".foo { transform: translate(2px, 3px)", + ".foo{transform:translate(2px,3px)}", ); minify_test( - ".foo { background: linear-gradient(to top, blue, yellow); }", - ".foo{background:linear-gradient(#ff0,#00f)}", + ".foo { transform: translate(2px, 0px)", + ".foo{transform:translate(2px)}", ); minify_test( - ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", - ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", + ".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 { background: linear-gradient(to top, blue 10px, yellow 20px); }", - ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", + ".foo { transform: translate3d(2px, 3px, 4px)", + ".foo{transform:translate3d(2px,3px,4px)}", ); minify_test( - ".foo { background: linear-gradient(135deg, yellow, blue); }", - ".foo{background:linear-gradient(135deg,#ff0,#00f)}", + ".foo { transform: translate3d(10%, 20%, 4px)", + ".foo{transform:translate3d(10%,20%,4px)}", ); minify_test( - ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }", - ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ".foo { transform: translate3d(2px, 0px, 0px)", + ".foo{transform:translate(2px)}", ); minify_test( - ".foo { background: linear-gradient(to top right, red, white, blue) }", - ".foo{background:linear-gradient(to top right,red,#fff,#00f)}", + ".foo { transform: translate3d(0px, 2px, 0px)", + ".foo{transform:translateY(2px)}", ); minify_test( - ".foo { background: linear-gradient(yellow, blue calc(10% * 2), #0f0); }", - ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ".foo { transform: translate3d(0px, 0px, 2px)", + ".foo{transform:translateZ(2px)}", ); minify_test( - ".foo { background: linear-gradient(yellow, 20%, blue); }", - ".foo{background:linear-gradient(#ff0,20%,#00f)}", + ".foo { transform: translate3d(2px, 3px, 0px)", + ".foo{transform:translate(2px,3px)}", ); + minify_test(".foo { transform: scale(2, 3)", ".foo{transform:scale(2,3)}"); + minify_test(".foo { transform: scale(10%, 20%)", ".foo{transform:scale(.1,.2)}"); + minify_test(".foo { transform: scale(2, 2)", ".foo{transform:scale(2)}"); + minify_test(".foo { transform: scale(2, 1)", ".foo{transform:scaleX(2)}"); + minify_test(".foo { transform: scale(1, 2)", ".foo{transform:scaleY(2)}"); + minify_test(".foo { transform: scaleX(2)", ".foo{transform:scaleX(2)}"); + minify_test(".foo { transform: scaleY(2)", ".foo{transform:scaleY(2)}"); + minify_test(".foo { transform: scaleZ(2)", ".foo{transform:scaleZ(2)}"); + minify_test(".foo { transform: scale3d(2, 3, 4)", ".foo{transform:scale3d(2,3,4)}"); + minify_test(".foo { transform: scale3d(2, 1, 1)", ".foo{transform:scaleX(2)}"); + minify_test(".foo { transform: scale3d(1, 2, 1)", ".foo{transform:scaleY(2)}"); + minify_test(".foo { transform: scale3d(1, 1, 2)", ".foo{transform:scaleZ(2)}"); + minify_test(".foo { transform: scale3d(2, 2, 1)", ".foo{transform:scale(2)}"); + minify_test(".foo { transform: rotate(20deg)", ".foo{transform:rotate(20deg)}"); + minify_test(".foo { transform: rotateX(20deg)", ".foo{transform:rotateX(20deg)}"); + minify_test(".foo { transform: rotateY(20deg)", ".foo{transform:rotateY(20deg)}"); + minify_test(".foo { transform: rotateZ(20deg)", ".foo{transform:rotate(20deg)}"); + minify_test(".foo { transform: rotate(360deg)", ".foo{transform:rotate(360deg)}"); minify_test( - ".foo { background: linear-gradient(yellow, 50%, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", + ".foo { transform: rotate3d(2, 3, 4, 20deg)", + ".foo{transform:rotate3d(2,3,4,20deg)}", ); minify_test( - ".foo { background: linear-gradient(yellow, 20px, blue); }", - ".foo{background:linear-gradient(#ff0,20px,#00f)}", + ".foo { transform: rotate3d(1, 0, 0, 20deg)", + ".foo{transform:rotateX(20deg)}", ); minify_test( - ".foo { background: linear-gradient(yellow, 50px, blue); }", - ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ".foo { transform: rotate3d(0, 1, 0, 20deg)", + ".foo{transform:rotateY(20deg)}", ); minify_test( - ".foo { background: linear-gradient(yellow, 50px, blue); }", - ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ".foo { transform: rotate3d(0, 0, 1, 20deg)", + ".foo{transform:rotate(20deg)}", ); + minify_test(".foo { transform: rotate(405deg)}", ".foo{transform:rotate(405deg)}"); + minify_test(".foo { transform: rotateX(405deg)}", ".foo{transform:rotateX(405deg)}"); + minify_test(".foo { transform: rotateY(405deg)}", ".foo{transform:rotateY(405deg)}"); + minify_test(".foo { transform: rotate(-200deg)}", ".foo{transform:rotate(-200deg)}"); + minify_test(".foo { transform: rotate(0)", ".foo{transform:rotate(0)}"); + minify_test(".foo { transform: rotate(0deg)", ".foo{transform:rotate(0)}"); minify_test( - ".foo { background: linear-gradient(yellow, red 30% 40%, blue); }", - ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ".foo { transform: rotateX(-200deg)}", + ".foo{transform:rotateX(-200deg)}", ); minify_test( - ".foo { background: linear-gradient(yellow, red 30%, red 40%, blue); }", - ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ".foo { transform: rotateY(-200deg)}", + ".foo{transform:rotateY(-200deg)}", ); minify_test( - ".foo { background: linear-gradient(0, yellow, blue); }", - ".foo{background:linear-gradient(#00f,#ff0)}", + ".foo { transform: rotate3d(1, 1, 0, -200deg)", + ".foo{transform:rotate3d(1,1,0,-200deg)}", ); + minify_test(".foo { transform: skew(20deg)", ".foo{transform:skew(20deg)}"); + minify_test(".foo { transform: skew(20deg, 0deg)", ".foo{transform:skew(20deg)}"); + minify_test(".foo { transform: skew(0deg, 20deg)", ".foo{transform:skewY(20deg)}"); + minify_test(".foo { transform: skewX(20deg)", ".foo{transform:skew(20deg)}"); + minify_test(".foo { transform: skewY(20deg)", ".foo{transform:skewY(20deg)}"); minify_test( - ".foo { background: -webkit-linear-gradient(yellow, blue) }", - ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ".foo { transform: perspective(10px)", + ".foo{transform:perspective(10px)}", ); minify_test( - ".foo { background: -webkit-linear-gradient(bottom, yellow, blue); }", - ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ".foo { transform: matrix(1, 2, -1, 1, 80, 80)", + ".foo{transform:matrix(1,2,-1,1,80,80)}", ); minify_test( - ".foo { background: -webkit-linear-gradient(top right, red, white, blue) }", - ".foo{background:-webkit-linear-gradient(top right,red,#fff,#00f)}", + ".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)}", ); + // 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 { background: -moz-linear-gradient(yellow, blue) }", - ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ".foo{transform:translate(100px,200px) rotate(45deg)}", + ".foo{transform:translate(100px,200px)rotate(45deg)}", ); minify_test( - ".foo { background: -moz-linear-gradient(bottom, yellow, blue); }", - ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ".foo{transform:rotate3d(1, 1, 1, 45deg) translate3d(100px, 100px, 10px)}", + ".foo{transform:rotate3d(1,1,1,45deg)translate3d(100px,100px,10px)}", ); + // 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 { background: -moz-linear-gradient(top right, red, white, blue) }", - ".foo{background:-moz-linear-gradient(top right,red,#fff,#00f)}", + ".foo{transform:translateX(calc(2in + 50px))}", + ".foo{transform:translate(242px)}", ); + minify_test(".foo{transform:translateX(50%)}", ".foo{transform:translate(50%)}"); minify_test( - ".foo { background: -o-linear-gradient(yellow, blue) }", - ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ".foo{transform:translateX(calc(50% - 100px + 20px))}", + ".foo{transform:translate(calc(50% - 80px))}", ); minify_test( - ".foo { background: -o-linear-gradient(bottom, yellow, blue); }", - ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ".foo{transform:rotate(calc(10deg + 20deg))}", + ".foo{transform:rotate(30deg)}", ); minify_test( - ".foo { background: -o-linear-gradient(top right, red, white, blue) }", - ".foo{background:-o-linear-gradient(top right,red,#fff,#00f)}", + ".foo{transform:rotate(calc(10deg + 0.349066rad))}", + ".foo{transform:rotate(30deg)}", ); minify_test( - ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), to(yellow)) }", - ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),to(#ff0))}", + ".foo{transform:rotate(calc(10deg + 1.5turn))}", + ".foo{transform:rotate(550deg)}", ); minify_test( - ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), color-stop(50%, red), to(yellow)) }", - ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}" + ".foo{transform:rotate(calc(10deg * 2))}", + ".foo{transform:rotate(20deg)}", ); minify_test( - ".foo { background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, blue), color-stop(50%, red), color-stop(100%, yellow)) }", - ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}" + ".foo{transform:rotate(calc(-10deg * 2))}", + ".foo{transform:rotate(-20deg)}", ); minify_test( - ".foo { background: repeating-linear-gradient(yellow 10px, blue 50px) }", - ".foo{background:repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ".foo{transform:rotate(calc(10deg + var(--test)))}", + ".foo{transform:rotate(calc(10deg + var(--test)))}", ); + minify_test(".foo { transform: scale(calc(10% + 20%))", ".foo{transform:scale(.3)}"); + minify_test(".foo { transform: scale(calc(.1 + .2))", ".foo{transform:scale(.3)}"); + minify_test( - ".foo { background: -webkit-repeating-linear-gradient(yellow 10px, blue 50px) }", - ".foo{background:-webkit-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ".foo { -webkit-transform: scale(calc(10% + 20%))", + ".foo{-webkit-transform:scale(.3)}", ); - minify_test( - ".foo { background: -moz-repeating-linear-gradient(yellow 10px, blue 50px) }", - ".foo{background:-moz-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + + minify_test(".foo { translate: 1px 2px 3px }", ".foo{translate:1px 2px 3px}"); + 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: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: 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 { 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: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}"); + + // 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( + r#" + .foo { + transform: scale(0.5); + } + "#, + indoc! {r#" + .foo { + -webkit-transform: scale(.5); + -moz-transform: scale(.5); + transform: scale(.5); + } + "#}, + Browsers { + firefox: Some(6 << 16), + safari: Some(6 << 16), + ..Browsers::default() + }, ); - minify_test( - ".foo { background: -o-repeating-linear-gradient(yellow 10px, blue 50px) }", - ".foo{background:-o-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + + prefix_test( + r#" + .foo { + transform: var(--transform); + } + "#, + indoc! {r#" + .foo { + -webkit-transform: var(--transform); + -moz-transform: var(--transform); + transform: var(--transform); + } + "#}, + Browsers { + firefox: Some(6 << 16), + safari: Some(6 << 16), + ..Browsers::default() + }, ); - minify_test( - ".foo { background: radial-gradient(yellow, blue) }", - ".foo{background:radial-gradient(#ff0,#00f)}", + + test( + r#" + .foo { + transform: translateX(-50%); + transform: translateX(20px); + } + "#, + indoc! {r#" + .foo { + transform: translateX(20px); + } + "#}, ); + } + + #[test] + pub fn test_gradients() { minify_test( - ".foo { background: radial-gradient(at top left, yellow, blue) }", + ".foo { background: linear-gradient(yellow, blue) }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(to bottom, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(180deg, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(0.5turn, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow 10%, blue 20%) }", + ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", + ); + minify_test( + ".foo { background: linear-gradient(to top, blue, yellow); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", + ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", + ); + minify_test( + ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }", + ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", + ); + minify_test( + ".foo { background: linear-gradient(135deg, yellow, blue); }", + ".foo{background:linear-gradient(135deg,#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }", + ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ); + minify_test( + ".foo { background: linear-gradient(to top right, red, white, blue) }", + ".foo{background:linear-gradient(to top right,red,#fff,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, blue calc(10% * 2), #0f0); }", + ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, 20%, blue); }", + ".foo{background:linear-gradient(#ff0,20%,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, 50%, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, 20px, blue); }", + ".foo{background:linear-gradient(#ff0,20px,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, 50px, blue); }", + ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, 50px, blue); }", + ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, red 30% 40%, blue); }", + ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(yellow, red 30%, red 40%, blue); }", + ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ); + minify_test( + ".foo { background: linear-gradient(0, yellow, blue); }", + ".foo{background:linear-gradient(#00f,#ff0)}", + ); + minify_test( + ".foo { background: -webkit-linear-gradient(yellow, blue) }", + ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: -webkit-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: -webkit-linear-gradient(top right, red, white, blue) }", + ".foo{background:-webkit-linear-gradient(top right,red,#fff,#00f)}", + ); + minify_test( + ".foo { background: -moz-linear-gradient(yellow, blue) }", + ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: -moz-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: -moz-linear-gradient(top right, red, white, blue) }", + ".foo{background:-moz-linear-gradient(top right,red,#fff,#00f)}", + ); + minify_test( + ".foo { background: -o-linear-gradient(yellow, blue) }", + ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: -o-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: -o-linear-gradient(top right, red, white, blue) }", + ".foo{background:-o-linear-gradient(top right,red,#fff,#00f)}", + ); + minify_test( + ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), to(yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),to(#ff0))}", + ); + minify_test( + ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), color-stop(50%, red), to(yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}" + ); + minify_test( + ".foo { background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, blue), color-stop(50%, red), color-stop(100%, yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}" + ); + minify_test( + ".foo { background: repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minify_test( + ".foo { background: -webkit-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-webkit-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minify_test( + ".foo { background: -moz-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-moz-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minify_test( + ".foo { background: -o-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-o-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minify_test( + ".foo { background: radial-gradient(yellow, blue) }", + ".foo{background:radial-gradient(#ff0,#00f)}", + ); + minify_test( + ".foo { background: radial-gradient(at top left, yellow, blue) }", ".foo{background:radial-gradient(at 0 0,#ff0,#00f)}", ); minify_test( @@ -12024,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); } "#}, @@ -12042,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); } "#}, @@ -12060,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); } "#}, @@ -12078,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); } "#}, @@ -12096,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); } "#}, @@ -12114,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); } "#}, @@ -12132,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); } "#}, @@ -12166,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); } "#}, @@ -12290,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"); } "#}, @@ -12372,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)); } @@ -12388,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)); } @@ -12459,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)); } @@ -12475,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)); } @@ -12511,60 +13799,411 @@ mod tests { ..Browsers::default() }, ); - } - #[test] - fn test_font_face() { - minify_test( - r#"@font-face { - src: url("test.woff"); - font-family: "Helvetica"; - font-weight: bold; - font-style: italic; - }"#, - "@font-face{src:url(test.woff);font-family:Helvetica;font-weight:700;font-style:italic}", - ); - minify_test("@font-face {src: url(test.woff);}", "@font-face{src:url(test.woff)}"); - minify_test("@font-face {src: local(\"Test\");}", "@font-face{src:local(Test)}"); - minify_test( - "@font-face {src: local(\"Foo Bar\");}", - "@font-face{src:local(Foo Bar)}", - ); - minify_test("@font-face {src: local(Test);}", "@font-face{src:local(Test)}"); - minify_test("@font-face {src: local(Foo Bar);}", "@font-face{src:local(Foo Bar)}"); + // 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); + } - minify_test( - "@font-face {src: url(\"test.woff\") format(woff);}", - "@font-face{src:url(test.woff)format(\"woff\")}", - ); - minify_test( - "@font-face {src: url(\"test.ttc\") format(collection), url(test.ttf) format(truetype);}", - "@font-face{src:url(test.ttc)format(\"collection\"),url(test.ttf)format(\"truetype\")}", - ); - minify_test( - "@font-face {src: url(\"test.otf\") format(opentype) tech(features-aat);}", - "@font-face{src:url(test.otf)format(\"opentype\")tech(features-aat)}", - ); - minify_test( - "@font-face {src: url(\"test.woff\") format(woff) tech(color-colrv1);}", - "@font-face{src:url(test.woff)format(\"woff\")tech(color-colrv1)}", - ); - minify_test( - "@font-face {src: url(\"test.woff2\") format(woff2) tech(variations);}", - "@font-face{src:url(test.woff2)format(\"woff2\")tech(variations)}", - ); - minify_test( - "@font-face {src: url(\"test.woff\") format(woff) tech(palettes);}", - "@font-face{src:url(test.woff)format(\"woff\")tech(palettes)}", - ); - // multiple tech - minify_test( - "@font-face {src: url(\"test.woff\") format(woff) tech(features-opentype, color-sbix);}", - "@font-face{src:url(test.woff)format(\"woff\")tech(features-opentype,color-sbix)}", - ); - minify_test( - "@font-face {src: url(\"test.woff\") format(woff) tech(incremental, color-svg, features-graphite, features-aat);}", - "@font-face{src:url(test.woff)format(\"woff\")tech(incremental,color-svg,features-graphite,features-aat)}", + b { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)), -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + background-image: -webkit-linear-gradient(top, #000, #fff), -webkit-linear-gradient(top, #fff, #000); + background-image: -o-linear-gradient(top, #000, #fff), -o-linear-gradient(top, #fff, #000); + background-image: linear-gradient(#000, #fff), linear-gradient(#fff, #000); + } + + strong { + background: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: -o-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + } + + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + + .simple1 { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background: -webkit-linear-gradient(top, #000, #fff); + background: -o-linear-gradient(top, #000, #fff); + background: linear-gradient(#000, #fff); + } + + .simple2 { + background: -webkit-gradient(linear, 100% 0, 0 0, from(#000), color-stop(.5, rgba(0, 0, 0, .5)), to(#fff)); + background: -webkit-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: -o-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: linear-gradient(to left, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + } + + .simple3 { + background: -webkit-gradient(linear, 100% 0, 0 0, color-stop(.5, #000), to(#fff)); + background: -webkit-linear-gradient(right, #000 50%, #fff 100%); + background: -o-linear-gradient(right, #000 50%, #fff 100%); + background: linear-gradient(to left, #000 50%, #fff 100%); + } + + .simple4 { + background: -webkit-gradient(linear, 0 100%, 100% 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(bottom left, #000, #fff); + background: -o-linear-gradient(bottom left, #000, #fff); + background: linear-gradient(to top right, #000, #fff); + } + + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, .5), white); + } + + .silent { + background: -webkit-gradient(linear, 100% 100%, 0 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(top left, #000, #fff); + } + + .radial { + background: -webkit-radial-gradient(farthest-side at 0, #fff, #000); + background: -o-radial-gradient(farthest-side at 0, #fff, #000); + background: radial-gradient(farthest-side at 0, #fff, #000); + } + + .second { + background: red -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f)); + background: red -webkit-linear-gradient(top, red, #00f); + background: red -o-linear-gradient(top, red, #00f); + background: red linear-gradient(red, #00f); + background: url("logo.png"), linear-gradient(#fff, #000); + } + + .px { + background: -webkit-linear-gradient(top, #000 0, #fff 100px); + background: -o-linear-gradient(top, #000 0, #fff 100px); + background: linear-gradient(#000 0, #fff 100px); + } + + .list { + list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + list-style-image: -webkit-linear-gradient(top, #fff, #000); + list-style-image: -o-linear-gradient(top, #fff, #000); + list-style-image: linear-gradient(#fff, #000); + } + + .mask { + -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + -webkit-mask: -webkit-linear-gradient(top, #fff, #000); + -webkit-mask: -o-linear-gradient(top, #fff, #000); + mask: -o-linear-gradient(top, #fff, #000); + -webkit-mask: linear-gradient(#fff, #000); + mask: linear-gradient(#fff, #000); + } + + .newline { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)), -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background-image: -webkit-linear-gradient(top, #fff, #000), -webkit-linear-gradient(top, #000, #fff); + background-image: -o-linear-gradient(top, #fff, #000), -o-linear-gradient(top, #000, #fff); + background-image: linear-gradient(#fff, #000), linear-gradient(#000, #fff); + } + + .convert { + background: -webkit-gradient(linear, 0 100%, 0 0, from(#fff), to(#000)); + background: -webkit-linear-gradient(90deg, #fff, #000); + background: -o-linear-gradient(90deg, #fff, #000); + background: linear-gradient(0deg, #fff, #000); + background: linear-gradient(90deg, #fff, #000); + background: linear-gradient(#fff, #000); + background: linear-gradient(270deg, #fff, #000); + } + + .grad { + background: -webkit-linear-gradient(89.1deg, #fff, #000); + background: -o-linear-gradient(89.1deg, #fff, #000); + background: linear-gradient(1grad, #fff, #000); + } + + .rad { + background: -webkit-linear-gradient(32.704deg, #fff, #000); + background: -o-linear-gradient(32.704deg, #fff, #000); + background: linear-gradient(57.2958deg, #fff, #000); + } + + .turn { + background: -webkit-linear-gradient(342deg, #fff, #000); + background: -o-linear-gradient(342deg, #fff, #000); + background: linear-gradient(.3turn, #fff, #000); + } + + .norm { + background: -webkit-linear-gradient(#fff, #000); + background: -o-linear-gradient(#fff, #000); + background: linear-gradient(-90deg, #fff, #000); + } + + .mask { + -webkit-mask-image: -webkit-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + } + + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + + .contain { + background: radial-gradient(contain at center, white, black); + } + + .no-div { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000)); + background: -webkit-linear-gradient(top, #000); + background: -o-linear-gradient(top, #000); + background: linear-gradient(#000); + } + + .background-shorthand { + background: #f0f -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f -o-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + } + + .background-advanced { + background: url("path/to/image.jpg") 50% / cover; + background: -webkit-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: -o-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -o-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + } + + .multiradial { + -webkit-mask-image: -webkit-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + } + + .broken { + -webkit-mask-image: -webkit-radial-gradient(#fff, #000); + -webkit-mask-image: -o-radial-gradient(#fff, #000); + mask-image: -o-radial-gradient(#fff, #000); + -webkit-mask-image: radial-gradient(#fff, #000); + mask-image: radial-gradient(#fff, #000); + } + + .loop { + background-image: url("https://test.com/lol(test.png"); + background-image: url("https://test.com/lol(test.png"), -webkit-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), -o-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), radial-gradient(#ff0, #000, #ff0); + } + + .unitless-zero { + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background-image: -webkit-linear-gradient(90deg, green, #00f); + background-image: -o-linear-gradient(90deg, green, #00f); + background-image: linear-gradient(0deg, green, #00f); + background: repeating-linear-gradient(0deg, #00f, red 33.3%); + } + + .zero-grad { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0grad, green, #00f); + background-image: repeating-linear-gradient(0grad, #00f, red 33.3%); + } + + .zero-rad, .zero-turn { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0deg, green, #00f); + } + "#}, + Browsers { + chrome: Some(25 << 16), + opera: Some(12 << 16), + android: Some(2 << 16 | 3 << 8), + ..Browsers::default() + }, + ); + } + + #[test] + fn test_font_face() { + minify_test( + r#"@font-face { + src: url("test.woff"); + font-family: "Helvetica"; + font-weight: bold; + font-style: italic; + }"#, + "@font-face{src:url(test.woff);font-family:Helvetica;font-weight:700;font-style:italic}", + ); + minify_test("@font-face {src: url(test.woff);}", "@font-face{src:url(test.woff)}"); + minify_test("@font-face {src: local(\"Test\");}", "@font-face{src:local(Test)}"); + minify_test( + "@font-face {src: local(\"Foo Bar\");}", + "@font-face{src:local(Foo Bar)}", + ); + minify_test("@font-face {src: local(Test);}", "@font-face{src:local(Test)}"); + minify_test("@font-face {src: local(Foo Bar);}", "@font-face{src:local(Foo Bar)}"); + + minify_test( + "@font-face {src: url(\"test.woff\") format(woff);}", + "@font-face{src:url(test.woff)format(\"woff\")}", + ); + minify_test( + "@font-face {src: url(\"test.ttc\") format(collection), url(test.ttf) format(truetype);}", + "@font-face{src:url(test.ttc)format(\"collection\"),url(test.ttf)format(\"truetype\")}", + ); + minify_test( + "@font-face {src: url(\"test.otf\") format(opentype) tech(features-aat);}", + "@font-face{src:url(test.otf)format(\"opentype\")tech(features-aat)}", + ); + minify_test( + "@font-face {src: url(\"test.woff\") format(woff) tech(color-colrv1);}", + "@font-face{src:url(test.woff)format(\"woff\")tech(color-colrv1)}", + ); + minify_test( + "@font-face {src: url(\"test.woff2\") format(woff2) tech(variations);}", + "@font-face{src:url(test.woff2)format(\"woff2\")tech(variations)}", + ); + minify_test( + "@font-face {src: url(\"test.woff\") format(woff) tech(palettes);}", + "@font-face{src:url(test.woff)format(\"woff\")tech(palettes)}", + ); + // multiple tech + minify_test( + "@font-face {src: url(\"test.woff\") format(woff) tech(features-opentype, color-sbix);}", + "@font-face{src:url(test.woff)format(\"woff\")tech(features-opentype,color-sbix)}", + ); + minify_test( + "@font-face {src: url(\"test.woff\") format(woff) tech(incremental, color-svg, features-graphite, features-aat);}", + "@font-face{src:url(test.woff)format(\"woff\")tech(incremental,color-svg,features-graphite,features-aat)}", ); // format() function must precede tech() if both are present minify_test( @@ -12581,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 @@ -12603,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}"); @@ -12701,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; @@ -12745,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}"); @@ -13068,6 +14847,68 @@ mod tests { ..Default::default() }, ); + prefix_test( + r#" + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } + } + "#, + indoc! { r#" + @supports ((-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(20px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } + } + "#, + indoc! { r#" + @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)) { @@ -13271,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] @@ -14375,14 +16218,35 @@ mod tests { prefix_test( r#" - .foo { - text-decoration: underline 10px; + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } } "#, indoc! {r#" - .foo { - text-decoration: underline; - text-decoration-thickness: 10px; + @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 { + text-decoration: underline 10px; + } + "#, + indoc! {r#" + .foo { + text-decoration: underline; + text-decoration-thickness: 10px; } "#}, Browsers { @@ -14740,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] @@ -14827,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] @@ -15537,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] @@ -15569,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( @@ -15610,7 +17548,7 @@ mod tests { "#, indoc! {r#" .foo { - list-style: \"★\" url("ellipse.png"); + list-style: url("ellipse.png") \"★\"; list-style-image: var(--img); } "#}, @@ -15621,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)); } @@ -15636,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 { @@ -15664,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] @@ -15864,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)}", @@ -15892,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)}", @@ -15912,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); }", @@ -15952,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}"); @@ -16455,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 { @@ -16557,6 +18602,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + color: var(--foo, lab(40% 56.6 39)); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + color: var(--foo, lab(40% 56.6 39)); + } + } + "# + }, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -16734,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))", @@ -16750,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))}", @@ -16869,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). @@ -17007,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)", ); @@ -17145,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)", ); @@ -17608,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), @@ -17616,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), @@ -17635,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(). @@ -18504,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}", ); } } @@ -18613,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: @@ -19916,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( @@ -19985,7 +22094,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -20133,32 +22241,240 @@ mod tests { ".foo{grid-template-areas:\"head head\"\"nav main\"\". .\"}", ); + // to grid-* shorthand + minify_test( + r#" + .test-miss-areas { + grid-template-columns: 1fr 90px; + grid-template-rows: auto 80px; + grid-template-areas: "one"; + } + "#, + ".test-miss-areas{grid-template:\"one\"\".\"80px/1fr 90px}", + ); test( r#" - .foo { - grid-template-areas: "head head" "nav main" "foot ...."; + .test-miss-areas-2 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 30px 60px 100px; + grid-template-areas: "a a a" "b c c"; } "#, indoc! { r#" - .foo { - grid-template-areas: "head head" - "nav main" - "foot ."; + .test-miss-areas-2 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; } "#}, ); - minify_test( + test( r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom]; + .test-miss-areas-3 { + grid-template: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", + indoc! { r#" + .test-miss-areas-3 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, ); - minify_test( - r#" + + 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 { + grid-template-areas: "head head" "nav main" "foot ...."; + } + "#, + indoc! { r#" + .foo { + grid-template-areas: "head head" + "nav main" + "foot ."; + } + "#}, + ); + + minify_test( + r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom]; + } + "#, + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", + ); + minify_test( + r#" .foo { grid-template: "head head" "nav main" 1fr @@ -20812,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)); }", @@ -20832,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#" @@ -20861,6 +23238,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -20884,6 +23282,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39) !important; + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + --custom: lab(40% 56.6 39) !important; + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -20914,6 +23333,40 @@ mod tests { }, ); + 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 { @@ -21082,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 { @@ -21248,14 +23723,47 @@ mod tests { prefix_test( r#" - @keyframes foo { - from { - --custom: lab(40% 56.6 39); - } - - to { - --custom: lch(50.998% 135.363 338); - } + @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 { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lch(50.998% 135.363 338); + } } "#, indoc! {r#" @@ -21300,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 { @@ -21438,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), @@ -21659,7 +24225,7 @@ mod tests { grid-auto-flow: column; } - @media (min-width: 1024px) { + @media not (max-width: 1024px) { .foo { max-inline-size: 1024px; } @@ -22526,13 +25092,13 @@ mod tests { } "#, indoc! {r#" - .foo { - color: red; - } - .foo .bar { color: #00f; } + + .foo { + color: red; + } "#}, ); @@ -22546,12 +25112,16 @@ mod tests { "#, indoc! {r#" article { - color: red; + color: green; } article { color: #00f; } + + article { + color: red; + } "#}, ); @@ -22660,8 +25230,33 @@ mod tests { indoc! {r#" div { color: #00f; - --button: focus { color: red; }; + --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; } + } "#}, ); @@ -22836,6 +25431,56 @@ mod tests { exclude: Features::empty(), }, ); + + minify_test( + r#" + .foo { + color: red; + .bar { + color: green; + } + color: blue; + .baz { + color: pink; + } + }"#, + ".foo{color:red;& .bar{color:green}color:#00f;& .baz{color:pink}}", + ); + } + + #[test] + 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] @@ -22930,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 { @@ -22974,9 +25712,9 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -23012,22 +25750,64 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" - test { - transition-property: opacity; - } - "#, - indoc! {r#" - test { - transition-property: opacity; + .grid { + grid-template-areas: "foo"; + } + + .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( @@ -23062,6 +25842,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); // :global(:local(.hi)) { @@ -23094,6 +25875,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23123,6 +25905,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23160,6 +25943,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23179,6 +25963,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23198,6 +25983,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23217,6 +26003,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23236,6 +26023,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23266,6 +26054,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23287,6 +26076,7 @@ mod tests { pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), ..Default::default() }, + false, ); let stylesheet = StyleSheet::parse( @@ -23348,6 +26138,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23417,241 +26208,615 @@ 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; - } - - .baz:is(.bar) { - background: green; + "#, + 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, + ); - #id { - animation: 2s test; + css_modules_test( + r#" + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } - - #other_id { - color: red; + "#, + 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, + ); - @keyframes test { - from { color: red } - to { color: yellow } + css_modules_test( + r#" + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } - - @counter-style circles { - symbols: Ⓐ Ⓑ Ⓒ; + "#, + 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, + ); - @keyframes fade { - from { opacity: 0 } - to { opacity: 1 } - } - "#; + 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, + ); - let expected = indoc! {r#" - .foo { - color: red; - } + 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, + ); - #id { - animation: 2s test; - } + 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, + ); - @keyframes test { - from { - color: red; - } + 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, + ); - to { - color: #ff0; - } - } - "#}; + 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, + ); - 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); + 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 source = r#" - .foo { - color: red; + 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, + ); + } - &.bar { - color: green; + // 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 + ) + ); + } - let expected = indoc! {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 { + color: green; + } + composes: test from "foo.css"; } - "#}; - - 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() + "#, + ParserOptions { + filename: "test.css".into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..Default::default() }) .unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); - assert_eq!(res.code, expected); - - let source = r#" - .foo { - color: red; + assert_eq!( + res.code, + indoc! {r#" + .EgL3uq_foo { + color: red; + } - &.bar { - color: purple; - } + .EgL3uq_foo .EgL3uq_bar { + color: green; + } - @nest &.bar { - color: orange; - } + + "#} + ); + 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; + } @nest :not(&) { color: green; @@ -23740,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}"); @@ -23899,126 +27066,653 @@ mod tests { ".foo { clip-path: circle(50px at 0 100px) padding-box; }", ".foo{clip-path:circle(50px at 0 100px) padding-box}", ); - minify_test( - ".foo { clip-path: circle(50px at 0 100px) border-box; }", - ".foo{clip-path:circle(50px at 0 100px)}", + minify_test( + ".foo { clip-path: circle(50px at 0 100px) border-box; }", + ".foo{clip-path:circle(50px at 0 100px)}", + ); + + prefix_test( + ".foo { clip-path: circle(50px); }", + indoc! { r#" + .foo { + -webkit-clip-path: circle(50px); + clip-path: circle(50px); + } + "#}, + Browsers { + chrome: Some(30 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { clip-path: circle(50px); }", + indoc! { r#" + .foo { + clip-path: circle(50px); + } + "#}, + Browsers { + chrome: Some(80 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { clip-path: circle(50px); }", + indoc! { r#" + .foo { + -webkit-clip-path: circle(50px); + clip-path: circle(50px); + } + "#}, + Browsers { + safari: Some(8 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { clip-path: circle(50px); }", + indoc! { r#" + .foo { + clip-path: circle(50px); + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { fill: lch(50.998% 135.363 338) }", + indoc! { r#" + .foo { + fill: #ee00be; + fill: color(display-p3 .972962 -.362078 .804206); + fill: lch(50.998% 135.363 338); + } + "#}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { stroke: lch(50.998% 135.363 338) }", + indoc! { r#" + .foo { + stroke: #ee00be; + stroke: color(display-p3 .972962 -.362078 .804206); + stroke: lch(50.998% 135.363 338); + } + "#}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { fill: url(#foo) lch(50.998% 135.363 338) }", + indoc! { r##" + .foo { + fill: url("#foo") #ee00be; + fill: url("#foo") color(display-p3 .972962 -.362078 .804206); + fill: url("#foo") lch(50.998% 135.363 338); + } + "##}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { fill: var(--url) lch(50.998% 135.363 338) }", + indoc! { r#" + .foo { + fill: var(--url) #ee00be; + } + + @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( + 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(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)); + mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + } + "#}, + Browsers { + chrome: Some(8 << 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: 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)); + mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + } + "#}, + Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { mask-image: linear-gradient(red, green) }", + indoc! { r#" + .foo { + -webkit-mask-image: linear-gradient(red, green); + mask-image: linear-gradient(red, green); + } + "#}, + Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { -webkit-mask-image: url(x.svg); mask-image: url(x.svg); }", + indoc! { r#" + .foo { + -webkit-mask-image: url("x.svg"); + mask-image: url("x.svg"); + } + "#}, + Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px }", + indoc! { r#" + .foo { + -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#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; + mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px; + } + "#}, + Browsers { + chrome: Some(8 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { mask: -webkit-linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px }", + 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; + } + "#}, + Browsers { + chrome: Some(8 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px var(--foo) }", + indoc! { r#" + .foo { + -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 40px var(--foo); + mask: linear-gradient(#ff0f0e, #7773ff) 40px var(--foo); + } + + @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( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + -webkit-mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { mask: url(masks.svg#star) luminance }", + indoc! { r#" + .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() + }, + ); + + prefix_test( + ".foo { mask-image: url(masks.svg#star) }", + indoc! { r#" + .foo { + -webkit-mask-image: url("masks.svg#star"); + mask-image: url("masks.svg#star"); + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + mask-image: url(masks.svg#star); + mask-position: 25% 75%; + mask-size: cover; + mask-repeat: no-repeat; + mask-clip: padding-box; + mask-origin: content-box; + mask-composite: subtract; + mask-mode: luminance; + } + "#, + indoc! { r#" + .foo { + -webkit-mask: url("masks.svg#star") 25% 75% / cover no-repeat content-box padding-box; + -webkit-mask-composite: source-out; + -webkit-mask-source-type: luminance; + mask: url("masks.svg#star") 25% 75% / cover no-repeat content-box padding-box subtract luminance; + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + mask-position: 25% 75%; + mask-size: cover; + mask-repeat: no-repeat; + mask-clip: padding-box; + mask-origin: content-box; + mask-composite: subtract; + mask-mode: luminance; + } + "#, + indoc! { r#" + .foo { + -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 25% 75% / cover no-repeat content-box padding-box; + -webkit-mask-composite: source-out; + -webkit-mask-source-type: luminance; + mask: linear-gradient(#ff0f0e, #7773ff) 25% 75% / cover no-repeat content-box padding-box subtract luminance; + -webkit-mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25% 75% / cover no-repeat content-box padding-box; + -webkit-mask-composite: source-out; + -webkit-mask-source-type: luminance; + mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25% 75% / cover no-repeat content-box padding-box subtract luminance; + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + test( + r#" + .foo { + mask: none center / 100% no-repeat; + mask-image: var(--svg); + } + "#, + indoc! { r#" + .foo { + mask: none center / 100% no-repeat; + mask-image: var(--svg); + } + "#}, + ); + + prefix_test( + r#" + .foo { + mask-composite: subtract; + } + "#, + indoc! { r#" + .foo { + -webkit-mask-composite: source-out; + mask-composite: subtract; + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + mask-mode: luminance; + } + "#, + indoc! { r#" + .foo { + -webkit-mask-source-type: luminance; + mask-mode: luminance; + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + mask-border: url('border-mask.png') 25 / 35px / 12px space luminance; + } + "#, + indoc! { r#" + .foo { + -webkit-mask-box-image: url("border-mask.png") 25 / 35px / 12px space; + mask-border: url("border-mask.png") 25 / 35px / 12px space luminance; + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space luminance; + } + "#, + indoc! { r#" + .foo { + -webkit-mask-box-image: linear-gradient(#ff0f0e, #7773ff) 25 / 35px / 12px space; + mask-border: linear-gradient(#ff0f0e, #7773ff) 25 / 35px / 12px space luminance; + -webkit-mask-box-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space; + mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space luminance; + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + mask-border-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-box-image-source: linear-gradient(#ff0f0e, #7773ff); + mask-border-source: linear-gradient(#ff0f0e, #7773ff); + -webkit-mask-box-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + mask-border-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, ); prefix_test( - ".foo { clip-path: circle(50px); }", + r#" + .foo { + mask-border-source: url(foo.png); + mask-border-slice: 10 40 10 40; + mask-border-width: 10px; + mask-border-outset: 0; + mask-border-repeat: round round; + mask-border-mode: luminance; + } + "#, indoc! { r#" .foo { - -webkit-clip-path: circle(50px); - clip-path: circle(50px); + -webkit-mask-box-image: url("foo.png") 10 40 / 10px round; + mask-border: url("foo.png") 10 40 / 10px round luminance; } - "#}, + "#}, Browsers { - chrome: Some(30 << 16), + chrome: Some(90 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { clip-path: circle(50px); }", + r#" + .foo { + -webkit-mask-box-image-source: url(foo.png); + -webkit-mask-box-image-slice: 10 40 10 40; + -webkit-mask-box-image-width: 10px; + -webkit-mask-box-image-outset: 0; + -webkit-mask-box-image-repeat: round round; + } + "#, indoc! { r#" .foo { - clip-path: circle(50px); + -webkit-mask-box-image: url("foo.png") 10 40 / 10px round; } - "#}, + "#}, Browsers { - chrome: Some(80 << 16), + chrome: Some(90 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { clip-path: circle(50px); }", + r#" + .foo { + mask-border-slice: 10 40 10 40; + } + "#, indoc! { r#" .foo { - -webkit-clip-path: circle(50px); - clip-path: circle(50px); + -webkit-mask-box-image-slice: 10 40; + mask-border-slice: 10 40; } - "#}, + "#}, Browsers { - safari: Some(8 << 16), + chrome: Some(90 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { clip-path: circle(50px); }", + r#" + .foo { + mask-border-slice: var(--foo); + } + "#, indoc! { r#" .foo { - clip-path: circle(50px); + -webkit-mask-box-image-slice: var(--foo); + mask-border-slice: var(--foo); } - "#}, + "#}, Browsers { - safari: Some(14 << 16), + chrome: Some(90 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { fill: lch(50.998% 135.363 338) }", + r#" + .foo { + mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) var(--foo); + } + "#, indoc! { r#" .foo { - fill: #ee00be; - fill: color(display-p3 .972962 -.362078 .804206); - fill: lch(50.998% 135.363 338); + -webkit-mask-box-image: linear-gradient(#ff0f0e, #7773ff) var(--foo); + mask-border: linear-gradient(#ff0f0e, #7773ff) var(--foo); } - "#}, + + @supports (color: lab(0% 0 0)) { + .foo { + -webkit-mask-box-image: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo); + mask-border: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo); + } + } + "#}, Browsers { chrome: Some(90 << 16), - safari: Some(14 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { stroke: lch(50.998% 135.363 338) }", + r#" + .foo { + transition: mask 200ms; + } + "#, indoc! { r#" .foo { - stroke: #ee00be; - stroke: color(display-p3 .972962 -.362078 .804206); - stroke: lch(50.998% 135.363 338); + transition: -webkit-mask .2s, mask .2s; } - "#}, + "#}, Browsers { chrome: Some(90 << 16), - safari: Some(14 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { fill: url(#foo) lch(50.998% 135.363 338) }", - indoc! { r##" + r#" .foo { - fill: url("#foo") #ee00be; - fill: url("#foo") color(display-p3 .972962 -.362078 .804206); - fill: url("#foo") lch(50.998% 135.363 338); + transition: mask-border 200ms; } - "##}, + "#, + indoc! { r#" + .foo { + transition: -webkit-mask-box-image .2s, mask-border .2s; + } + "#}, Browsers { chrome: Some(90 << 16), - safari: Some(14 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { fill: var(--url) lch(50.998% 135.363 338) }", + r#" + .foo { + transition-property: mask; + } + "#, indoc! { r#" .foo { - fill: var(--url) #ee00be; + transition-property: -webkit-mask, mask; } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); - @supports (color: lab(0% 0 0)) { - .foo { - fill: var(--url) lab(50.998% 125.506 -50.7078); - } + prefix_test( + r#" + .foo { + transition-property: mask-border; } - "#}, + "#, + indoc! { r#" + .foo { + transition-property: -webkit-mask-box-image, mask-border; + } + "#}, Browsers { chrome: Some(90 << 16), ..Browsers::default() @@ -24026,2912 +27720,3283 @@ mod tests { ); prefix_test( - ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", + r#" + .foo { + transition-property: mask-composite, mask-mode; + } + "#, 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: 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)); - mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + transition-property: -webkit-mask-composite, mask-composite, -webkit-mask-source-type, mask-mode; } - "#}, + "#}, Browsers { - chrome: Some(8 << 16), + chrome: Some(90 << 16), ..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] + fn test_filter() { + minify_test( + ".foo { filter: url('filters.svg#filter-id'); }", + ".foo{filter:url(filters.svg#filter-id)}", + ); + minify_test(".foo { filter: blur(5px); }", ".foo{filter:blur(5px)}"); + minify_test(".foo { filter: blur(0px); }", ".foo{filter:blur()}"); + minify_test(".foo { filter: brightness(10%); }", ".foo{filter:brightness(10%)}"); + minify_test(".foo { filter: brightness(100%); }", ".foo{filter:brightness()}"); + minify_test( + ".foo { filter: drop-shadow(16px 16px 20px yellow); }", + ".foo{filter:drop-shadow(16px 16px 20px #ff0)}", + ); + minify_test( + ".foo { filter: contrast(175%) brightness(3%); }", + ".foo{filter:contrast(175%)brightness(3%)}", + ); + minify_test(".foo { filter: hue-rotate(0) }", ".foo{filter:hue-rotate()}"); + prefix_test( - ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", + ".foo { filter: blur(5px) }", indoc! { r#" .foo { - -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)); - mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + -webkit-filter: blur(5px); + filter: blur(5px); } "#}, Browsers { - chrome: Some(95 << 16), + chrome: Some(20 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { mask-image: linear-gradient(red, green) }", + ".foo { filter: blur(5px) }", indoc! { r#" .foo { - -webkit-mask-image: linear-gradient(red, green); - mask-image: linear-gradient(red, green); + filter: blur(5px); } "#}, Browsers { - chrome: Some(95 << 16), + chrome: Some(80 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { -webkit-mask-image: url(x.svg); mask-image: url(x.svg); }", + ".foo { backdrop-filter: blur(5px) }", indoc! { r#" .foo { - -webkit-mask-image: url("x.svg"); - mask-image: url("x.svg"); + backdrop-filter: blur(5px); } "#}, Browsers { - chrome: Some(95 << 16), + chrome: Some(80 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px }", + ".foo { backdrop-filter: blur(5px) }", 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: 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; - mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); } "#}, Browsers { - chrome: Some(8 << 16), + safari: Some(15 << 16), + ..Browsers::default() + }, + ); + prefix_test( + r#" + .foo { + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + } + "#, + indoc! {r#" + .foo { + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + } + "#}, + Browsers { + safari: Some(16 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { mask: -webkit-linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px }", + ".foo { filter: var(--foo) }", 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-filter: var(--foo); + filter: var(--foo); } "#}, Browsers { - chrome: Some(8 << 16), + chrome: Some(20 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px var(--foo) }", + ".foo { filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) }", indoc! { r#" .foo { - -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 40px var(--foo); - mask: linear-gradient(#ff0f0e, #7773ff) 40px var(--foo); - } - - @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); - } + -webkit-filter: drop-shadow(16px 16px 20px #b32323); + filter: drop-shadow(16px 16px 20px #b32323); + filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)); } "#}, Browsers { - chrome: Some(90 << 16), + chrome: Some(20 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { mask: url(masks.svg#star) luminance }", + ".foo { filter: contrast(175%) drop-shadow(16px 16px 20px lab(40% 56.6 39)) }", indoc! { r#" .foo { - -webkit-mask: url("masks.svg#star"); - -webkit-mask-source-type: luminance; - mask: url("masks.svg#star") luminance; + filter: contrast(175%) drop-shadow(16px 16px 20px #b32323); + filter: contrast(175%) drop-shadow(16px 16px 20px lab(40% 56.6 39)); } - "#}, + "#}, Browsers { - chrome: Some(90 << 16), + chrome: Some(4 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { mask-image: url(masks.svg#star) }", + ".foo { filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) drop-shadow(16px 16px 20px yellow) }", indoc! { r#" .foo { - -webkit-mask-image: url("masks.svg#star"); - mask-image: url("masks.svg#star"); + filter: drop-shadow(16px 16px 20px #b32323) drop-shadow(16px 16px 20px #ff0); + filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) drop-shadow(16px 16px 20px #ff0); } - "#}, + "#}, Browsers { - chrome: Some(90 << 16), + chrome: Some(4 << 16), ..Browsers::default() }, ); prefix_test( - r#" - .foo { - mask-image: url(masks.svg#star); - mask-position: 25% 75%; - mask-size: cover; - mask-repeat: no-repeat; - mask-clip: padding-box; - mask-origin: content-box; - mask-composite: subtract; - mask-mode: luminance; - } - "#, + ".foo { filter: var(--foo) drop-shadow(16px 16px 20px lab(40% 56.6 39)) }", indoc! { r#" .foo { - -webkit-mask: url("masks.svg#star") 25% 75% / cover no-repeat content-box padding-box; - -webkit-mask-composite: source-out; - -webkit-mask-source-type: luminance; - mask: url("masks.svg#star") 25% 75% / cover no-repeat content-box padding-box subtract luminance; + filter: var(--foo) drop-shadow(16px 16px 20px #b32323); } - "#}, + + @supports (color: lab(0% 0 0)) { + .foo { + filter: var(--foo) drop-shadow(16px 16px 20px lab(40% 56.6 39)); + } + } + "#}, Browsers { - chrome: Some(90 << 16), + chrome: Some(4 << 16), ..Browsers::default() }, ); + } - prefix_test( + #[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( r#" - .foo { - mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); - mask-position: 25% 75%; - mask-size: cover; - mask-repeat: no-repeat; - mask-clip: padding-box; - mask-origin: content-box; - mask-composite: subtract; - mask-mode: luminance; - } - "#, - indoc! { r#" - .foo { - -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 25% 75% / cover no-repeat content-box padding-box; - -webkit-mask-composite: source-out; - -webkit-mask-source-type: luminance; - mask: linear-gradient(#ff0f0e, #7773ff) 25% 75% / cover no-repeat content-box padding-box subtract luminance; - -webkit-mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25% 75% / cover no-repeat content-box padding-box; - -webkit-mask-composite: source-out; - -webkit-mask-source-type: luminance; - mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25% 75% / cover no-repeat content-box padding-box subtract luminance; - } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + @viewport { + width: 100vw; + }"#, + "@viewport{width:100vw}", + ); + minify_test( + r#" + @-ms-viewport { + width: device-width; + }"#, + "@-ms-viewport{width:device-width}", ); + } - test( + #[test] + fn test_at_scope() { + minify_test( r#" + @scope { .foo { - mask: none center / 100% no-repeat; - mask-image: var(--svg); + display: flex; } + } "#, - indoc! { r#" - .foo { - mask: none center / 100% no-repeat; - mask-image: var(--svg); - } - "#}, + "@scope{.foo{display:flex}}", ); - - prefix_test( + minify_test( r#" - .foo { - mask-composite: subtract; + @scope { + :scope { + display: flex; + color: lightblue; } + }"#, + "@scope{:scope{color:#add8e6;display:flex}}", + ); + minify_test( + r#" + @scope (.light-scheme) { + a { color: yellow; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-composite: source-out; - mask-composite: subtract; - } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + "@scope(.light-scheme){a{color:#ff0}}", ); - - prefix_test( + minify_test( r#" - .foo { - mask-mode: luminance; - } + @scope (.media-object) to (.content > *) { + a { color: yellow; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-source-type: luminance; - mask-mode: luminance; + "@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; } } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#, + "@scope(.parent-scope){@scope(:scope>.child-scope) to (:scope .limit){.content{color:#ff0}}}", ); - - prefix_test( + minify_test( r#" - .foo { - mask-border: url('border-mask.png') 25 / 35px / 12px space luminance; + .foo { + @scope (.bar) { + color: yellow; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image: url("border-mask.png") 25 / 35px / 12px space; - mask-border: url("border-mask.png") 25 / 35px / 12px space luminance; - } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + ".foo{@scope(.bar){color:#ff0}}", ); - - prefix_test( + nesting_test( r#" - .foo { - mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space luminance; + .foo { + @scope (.bar) { + color: yellow; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image: linear-gradient(#ff0f0e, #7773ff) 25 / 35px / 12px space; - mask-border: linear-gradient(#ff0f0e, #7773ff) 25 / 35px / 12px space luminance; - -webkit-mask-box-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space; - mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space luminance; + indoc! {r#" + @scope (.bar) { + color: #ff0; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + "#}, ); - - prefix_test( + nesting_test( r#" - .foo { - mask-border-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + .parent { + color: blue; + + @scope (& > .scope) to (& .limit) { + & .content { + color: yellow; + } } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image-source: linear-gradient(#ff0f0e, #7773ff); - mask-border-source: linear-gradient(#ff0f0e, #7773ff); - -webkit-mask-box-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); - mask-border-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); + indoc! {r#" + .parent { + color: #00f; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + + @scope (.parent > .scope) to (.parent > .scope .limit) { + :scope .content { + color: #ff0; + } + } + "#}, ); + } - prefix_test( + #[test] + fn test_custom_media() { + custom_media_test( r#" - .foo { - mask-border-source: url(foo.png); - mask-border-slice: 10 40 10 40; - mask-border-width: 10px; - mask-border-outset: 0; - mask-border-repeat: round round; - mask-border-mode: luminance; + @custom-media --modern (color), (hover); + + @media (--modern) and (width > 1024px) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image: url("foo.png") 10 40 / 10px round; - mask-border: url("foo.png") 10 40 / 10px round luminance; + indoc! {r#" + @media ((color) or (hover)) and (width > 1024px) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - -webkit-mask-box-image-source: url(foo.png); - -webkit-mask-box-image-slice: 10 40 10 40; - -webkit-mask-box-image-width: 10px; - -webkit-mask-box-image-outset: 0; - -webkit-mask-box-image-repeat: round round; + @custom-media --color (color); + + @media (--color) and (width > 1024px) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image: url("foo.png") 10 40 / 10px round; + indoc! {r#" + @media (color) and (width > 1024px) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - mask-border-slice: 10 40 10 40; + @custom-media --a (color); + @custom-media --b (--a); + + @media (--b) and (width > 1024px) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image-slice: 10 40; - mask-border-slice: 10 40; + indoc! {r#" + @media (color) and (width > 1024px) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - mask-border-slice: var(--foo); + @custom-media --not-color not (color); + + @media not (--not-color) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image-slice: var(--foo); - mask-border-slice: var(--foo); + indoc! {r#" + @media (color) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) var(--foo); + @custom-media --color-print print and (color); + + @media (--color-print) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - -webkit-mask-box-image: linear-gradient(#ff0f0e, #7773ff) var(--foo); - mask-border: linear-gradient(#ff0f0e, #7773ff) var(--foo); - } - - @supports (color: lab(0% 0 0)) { - .foo { - -webkit-mask-box-image: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo); - mask-border: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo); - } + indoc! {r#" + @media print and (color) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - transition: mask 200ms; + @custom-media --color-print print and (color); + + @media print and (--color-print) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - transition: -webkit-mask .2s, mask .2s; + indoc! {r#" + @media print and (color) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - transition: mask-border 200ms; + @custom-media --not-color-print not print and (color); + + @media not print and (--not-color-print) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - transition: -webkit-mask-box-image .2s, mask-border .2s; + indoc! {r#" + @media not print and (color) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - transition-property: mask; + @custom-media --print print; + + @media (--print) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - transition-property: -webkit-mask, mask; + indoc! {r#" + @media print { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - transition-property: mask-border; + @custom-media --print print; + + @media not (--print) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - transition-property: -webkit-mask-box-image, mask-border; + indoc! {r#" + @media not print { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - prefix_test( + custom_media_test( r#" - .foo { - transition-property: mask-composite, mask-mode; + @custom-media --print not print; + + @media not (--print) { + .a { + color: green; } + } "#, - indoc! { r#" - .foo { - transition-property: -webkit-mask-composite, mask-composite, -webkit-mask-source-type, mask-mode; + indoc! {r#" + @media print { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(90 << 16), - ..Browsers::default() - }, + } + "#}, ); - } - #[test] - fn test_filter() { - minify_test( - ".foo { filter: url('filters.svg#filter-id'); }", - ".foo{filter:url(filters.svg#filter-id)}", - ); - minify_test(".foo { filter: blur(5px); }", ".foo{filter:blur(5px)}"); - minify_test(".foo { filter: blur(0px); }", ".foo{filter:blur()}"); - minify_test(".foo { filter: brightness(10%); }", ".foo{filter:brightness(10%)}"); - minify_test(".foo { filter: brightness(100%); }", ".foo{filter:brightness()}"); - minify_test( - ".foo { filter: drop-shadow(16px 16px 20px yellow); }", - ".foo{filter:drop-shadow(16px 16px 20px #ff0)}", - ); - minify_test( - ".foo { filter: contrast(175%) brightness(3%); }", - ".foo{filter:contrast(175%)brightness(3%)}", - ); - minify_test(".foo { filter: hue-rotate(0) }", ".foo{filter:hue-rotate()}"); + custom_media_test( + r#" + @custom-media --print print; - prefix_test( - ".foo { filter: blur(5px) }", - indoc! { r#" - .foo { - -webkit-filter: blur(5px); - filter: blur(5px); + @media ((--print)) { + .a { + color: green; } - "#}, - Browsers { - chrome: Some(20 << 16), - ..Browsers::default() - }, - ); - - prefix_test( - ".foo { filter: blur(5px) }", - indoc! { r#" - .foo { - filter: blur(5px); + } + "#, + indoc! {r#" + @media print { + .a { + color: green; } + } "#}, - Browsers { - chrome: Some(80 << 16), - ..Browsers::default() - }, ); - prefix_test( - ".foo { backdrop-filter: blur(5px) }", - indoc! { r#" - .foo { - backdrop-filter: blur(5px); - } - "#}, - Browsers { - chrome: Some(80 << 16), - ..Browsers::default() - }, - ); + custom_media_test( + r#" + @custom-media --color (color); + @custom-media --print print; - prefix_test( - ".foo { backdrop-filter: blur(5px) }", - indoc! { r#" - .foo { - -webkit-backdrop-filter: blur(5px); - backdrop-filter: blur(5px); + @media (--print) and (--color) { + .a { + color: green; } - "#}, - Browsers { - safari: Some(15 << 16), - ..Browsers::default() - }, - ); - prefix_test( - r#" - .foo { - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); } "#, indoc! {r#" - .foo { - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); + @media print and (color) { + .a { + color: green; + } } "#}, - Browsers { - safari: Some(16 << 16), - ..Browsers::default() - }, ); - prefix_test( - ".foo { filter: var(--foo) }", - indoc! { r#" - .foo { - -webkit-filter: var(--foo); - filter: var(--foo); + custom_media_test( + r#" + @custom-media --color (color); + @custom-media --not-print not print; + + @media (--not-print) and (--color) { + .a { + color: green; + } + } + "#, + indoc! {r#" + @media not print and (color) { + .a { + color: green; } + } "#}, - Browsers { - chrome: Some(20 << 16), - ..Browsers::default() - }, ); - prefix_test( - ".foo { filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) }", - indoc! { r#" - .foo { - -webkit-filter: drop-shadow(16px 16px 20px #b32323); - filter: drop-shadow(16px 16px 20px #b32323); - filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)); + custom_media_test( + r#" + @custom-media --color (color); + @custom-media --screen screen; + @custom-media --print print; + + @media (--print) and (--color), (--screen) and (--color) { + .a { + color: green; } + } + "#, + indoc! {r#" + @media print and (color), screen and (color) { + .a { + color: green; + } + } "#}, - Browsers { - chrome: Some(20 << 16), - ..Browsers::default() - }, ); - prefix_test( - ".foo { filter: contrast(175%) drop-shadow(16px 16px 20px lab(40% 56.6 39)) }", - indoc! { r#" - .foo { - filter: contrast(175%) drop-shadow(16px 16px 20px #b32323); - filter: contrast(175%) drop-shadow(16px 16px 20px lab(40% 56.6 39)); + custom_media_test( + r#" + @custom-media --color print and (color), print and (script); + + @media (--color) { + .a { + color: green; + } + } + "#, + indoc! {r#" + @media print and ((color) or (script)) { + .a { + color: green; } + } "#}, - Browsers { - chrome: Some(4 << 16), - ..Browsers::default() - }, ); - prefix_test( - ".foo { filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) drop-shadow(16px 16px 20px yellow) }", - indoc! { r#" - .foo { - filter: drop-shadow(16px 16px 20px #b32323) drop-shadow(16px 16px 20px #ff0); - filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) drop-shadow(16px 16px 20px #ff0); - } - "#}, - Browsers { - chrome: Some(4 << 16), - ..Browsers::default() - }, - ); + custom_media_test( + r#" + @custom-media --color (color); + @custom-media --not-color not all and (--color); - prefix_test( - ".foo { filter: var(--foo) drop-shadow(16px 16px 20px lab(40% 56.6 39)) }", - indoc! { r#" - .foo { - filter: var(--foo) drop-shadow(16px 16px 20px #b32323); + @media (--not-color) { + .a { + color: green; } - - @supports (color: lab(0% 0 0)) { - .foo { - filter: var(--foo) drop-shadow(16px 16px 20px lab(40% 56.6 39)); + } + "#, + indoc! {r#" + @media not all and (color) { + .a { + color: green; } } "#}, - Browsers { - chrome: Some(4 << 16), - ..Browsers::default() - }, ); - } - #[test] - fn test_viewport() { - minify_test( - r#" - @viewport { - width: 100vw; - }"#, - "@viewport{width:100vw}", - ); - minify_test( + custom_media_test( r#" - @-ms-viewport { - width: device-width; - }"#, - "@-ms-viewport{width:device-width}", - ); - } + @custom-media --color (color); - #[test] - fn test_at_scope() { - minify_test( - r#" - @scope { - .foo { - display: flex; + @media not all and (--color) { + .a { + color: green; } } "#, - "@scope{.foo{display:flex}}", - ); - minify_test( - r#" - @scope { - :scope { - display: flex; - color: lightblue; + indoc! {r#" + @media not all and (color) { + .a { + color: green; + } } - }"#, - "@scope{:scope{color:#add8e6;display:flex}}", + "#}, ); - minify_test( + + custom_media_test( r#" - @scope (.light-scheme) { - a { color: yellow; } + @media (--print) { + .a { + color: green; + } } + + @custom-media --print print; "#, - "@scope(.light-scheme){a{color:#ff0}}", - ); - minify_test( - r#" - @scope (.media-object) to (.content > *) { - a { color: yellow; } + indoc! {r#" + @media print { + .a { + color: green; + } } - "#, - "@scope(.media-object) to (.content>*){a{color:#ff0}}", + "#}, ); - minify_test( + + custom_media_test( r#" - @scope to (.content > *) { - a { color: yellow; } + @custom-media --not-width not (min-width: 300px); + @media screen and ((prefers-color-scheme: dark) or (--not-width)) { + .foo { + order: 6; + } } "#, - "@scope to (.content>*){a{color:#ff0}}", + indoc! {r#" + @media screen and ((prefers-color-scheme: dark) or ((width < 300px))) { + .foo { + order: 6; + } + } + "#}, ); - minify_test( + + fn custom_media_error_test(source: &str, err: Error) { + let mut stylesheet = StyleSheet::parse( + &source, + ParserOptions { + filename: "test.css".into(), + flags: ParserFlags::CUSTOM_MEDIA, + ..ParserOptions::default() + }, + ) + .unwrap(); + let res = stylesheet.minify(MinifyOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..MinifyOptions::default() + }); + assert_eq!(res, Err(err)) + } + + custom_media_error_test( r#" - @scope (#my-component) { - & { color: yellow; } + @custom-media --color-print print and (color); + + @media screen and (--color-print) { + .a { + color: green; + } } "#, - "@scope(#my-component){&{color:#ff0}}", + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 1, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 3, + column: 7, + }), + }, ); - minify_test( + + custom_media_error_test( r#" - @scope (.parent-scope) { - @scope (:scope > .child-scope) to (:scope .limit) { - .content { color: yellow; } + @custom-media --color-print print and (color); + + @media not print and (--color-print) { + .a { + color: green; } } "#, - "@scope(.parent-scope){@scope(:scope>.child-scope) to (:scope .limit){.content{color:#ff0}}}", + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 1, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 3, + column: 7, + }), + }, ); - minify_test( + + custom_media_error_test( r#" - .foo { - @scope (.bar) { - color: yellow; - } - } + @custom-media --color-print print and (color); + @custom-media --color-screen screen and (color); + + @media (--color-print) or (--color-screen) {} "#, - ".foo{@scope(.bar){&{color:#ff0}}}", + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 2, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 4, + column: 7, + }), + }, ); - nesting_test( + + custom_media_error_test( r#" - .foo { - @scope (.bar) { - color: yellow; - } - } + @custom-media --color-print print and (color); + @custom-media --color-screen screen and (color); + + @media (--color-print) and (--color-screen) {} "#, - indoc! {r#" - @scope (.bar) { - :scope { - color: #ff0; - } - } - "#}, + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 2, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 4, + column: 7, + }), + }, ); - nesting_test( + + custom_media_error_test( r#" - .parent { - color: blue; + @custom-media --screen screen; + @custom-media --print print; - @scope (& > .scope) to (& .limit) { - & .content { - color: yellow; - } - } - } + @media (--print) and (--screen) {} "#, - indoc! {r#" - .parent { - color: #00f; - } - - @scope (.parent > .scope) to (.parent > .scope .limit) { - :scope .content { - color: #ff0; - } - } - "#}, + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 1, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 4, + column: 7, + }), + }, ); - } - #[test] - fn test_custom_media() { - custom_media_test( + custom_media_error_test( r#" - @custom-media --modern (color), (hover); + @custom-media --not-print not print and (color); + @custom-media --not-screen not screen and (color); - @media (--modern) and (width > 1024px) { + @media ((script) or ((--not-print) and (--not-screen))) { .a { color: green; } } "#, - indoc! {r#" - @media ((color) or (hover)) and (width > 1024px) { - .a { - color: green; - } - } - "#}, + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 2, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 4, + column: 7, + }), + }, ); - custom_media_test( + custom_media_error_test( r#" - @custom-media --color (color); + @custom-media --color screen and (color), print and (color); - @media (--color) and (width > 1024px) { + @media (--color) { .a { color: green; } } "#, - indoc! {r#" - @media (color) and (width > 1024px) { - .a { - color: green; - } - } - "#}, + Error { + kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { + custom_media_loc: Location { + source_index: 0, + line: 1, + column: 7, + }, + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 3, + column: 7, + }), + }, ); - custom_media_test( + custom_media_error_test( r#" - @custom-media --a (color); - @custom-media --b (--a); - - @media (--b) and (width > 1024px) { + @media (--not-defined) { .a { color: green; } } "#, - indoc! {r#" - @media (color) and (width > 1024px) { - .a { - color: green; - } - } - "#}, + Error { + kind: MinifyErrorKind::CustomMediaNotDefined { + name: "--not-defined".into(), + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 1, + column: 7, + }), + }, ); - custom_media_test( + custom_media_error_test( r#" - @custom-media --not-color not (color); + @custom-media --circular-mq-a (--circular-mq-b); + @custom-media --circular-mq-b (--circular-mq-a); - @media not (--not-color) { - .a { - color: green; + @media (--circular-mq-a) { + body { + order: 3; } } "#, - indoc! {r#" - @media (color) { - .a { - color: green; + Error { + kind: MinifyErrorKind::CircularCustomMedia { + name: "--circular-mq-a".into(), + }, + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 4, + column: 7, + }), + }, + ); + } + + #[test] + fn test_dependencies() { + fn dep_test(source: &str, expected: &str, deps: Vec<(&str, &str)>) { + let mut stylesheet = StyleSheet::parse( + &source, + ParserOptions { + filename: "test.css".into(), + ..ParserOptions::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + analyze_dependencies: Some(Default::default()), + minify: true, + ..PrinterOptions::default() + }) + .unwrap(); + assert_eq!(res.code, expected); + let dependencies = res.dependencies.unwrap(); + assert_eq!(dependencies.len(), deps.len()); + for (i, (url, placeholder)) in deps.into_iter().enumerate() { + match &dependencies[i] { + Dependency::Url(dep) => { + assert_eq!(dep.url, url); + assert_eq!(dep.placeholder, placeholder); + } + Dependency::Import(dep) => { + assert_eq!(dep.url, url); + assert_eq!(dep.placeholder, placeholder); + } } } - "#}, + } + + fn dep_error_test(source: &str, error: PrinterErrorKind) { + let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); + let res = stylesheet.to_css(PrinterOptions { + analyze_dependencies: Some(Default::default()), + ..PrinterOptions::default() + }); + match res { + Err(e) => assert_eq!(e.kind, error), + _ => unreachable!(), + } + } + + dep_test( + ".foo { background: image-set('./img12x.png', './img21x.png' 2x)}", + ".foo{background:image-set(\"hXFI8W\" 1x,\"5TkpBa\" 2x)}", + vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")], + ); + + dep_test( + ".foo { background: image-set(url(./img12x.png), url('./img21x.png') 2x)}", + ".foo{background:image-set(\"hXFI8W\" 1x,\"5TkpBa\" 2x)}", + vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")], + ); + + dep_test( + ".foo { --test: url(/foo.png) }", + ".foo{--test:url(\"lDnnrG\")}", + vec![("/foo.png", "lDnnrG")], + ); + + dep_test( + ".foo { --test: url(\"/foo.png\") }", + ".foo{--test:url(\"lDnnrG\")}", + 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(\"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(), + }, ); - custom_media_test( - r#" - @custom-media --color-print print and (color); + dep_test( + ".foo { behavior: url(#foo) }", + ".foo{behavior:url(\"Zn9-2q\")}", + vec![("#foo", "Zn9-2q")], + ); - @media (--color-print) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print and (color) { - .a { - color: green; - } - } - "#}, + dep_test( + ".foo { --foo: url(#foo) }", + ".foo{--foo:url(\"Zn9-2q\")}", + vec![("#foo", "Zn9-2q")], ); - custom_media_test( - r#" - @custom-media --color-print print and (color); + dep_test( + "@import \"test.css\"; .foo { color: red }", + "@import \"hHsogW\";.foo{color:red}", + vec![("test.css", "hHsogW")], + ); + } - @media print and (--color-print) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print and (color) { - .a { - color: green; - } + #[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!(), + } - custom_media_test( - r#" - @custom-media --not-color-print not print and (color); + let color = CssColor::parse_string("#f0f").unwrap(); + assert_eq!(color.to_css_string(PrinterOptions::default()).unwrap(), "#f0f"); - @media not print and (--not-color-print) { - .a { - color: green; - } - } - "#, + let rule = CssRule::parse_string(".foo { color: red }", ParserOptions::default()).unwrap(); + assert_eq!( + rule.to_css_string(PrinterOptions::default()).unwrap(), indoc! {r#" - @media not print and (color) { - .a { - color: green; - } - } - "#}, + .foo { + color: red; + }"#} ); - custom_media_test( - r#" - @custom-media --print print; + 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" + ); - @media (--print) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print { - .a { - color: green; - } + let code = indoc! { r#" + .foo { + color: green; } - "#}, - ); - custom_media_test( - r#" - @custom-media --print print; + .bar { + color: red; + background: pink; + } - @media not (--print) { - .a { + @media print { + .baz { color: green; } } - "#, - indoc! {r#" - @media not print { - .a { - 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) ); - custom_media_test( - r#" - @custom-media --print not print; + 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) + ); - @media not (--print) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print { - .a { - color: green; - } + 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)", ); + } - custom_media_test( - r#" - @custom-media --print print; + #[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;"); - @media ((--print)) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print { - .a { - color: green; + minify_test( + r#" + @layer foo { + .bar { + color: red; } } - "#}, + "#, + "@layer foo{.bar{color:red}}", ); - - custom_media_test( + minify_test( r#" - @custom-media --color (color); - @custom-media --print print; - - @media (--print) and (--color) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print and (color) { - .a { - color: green; + @layer foo.bar { + .bar { + color: red; } } - "#}, + "#, + "@layer foo.bar{.bar{color:red}}", ); + minify_test(r#" + @layer base { + p { max-width: 70ch; } + } - custom_media_test( - r#" - @custom-media --color (color); - @custom-media --not-print not print; + @layer framework { + @layer base { + p { margin-block: 0.75em; } + } - @media (--not-print) and (--color) { - .a { - color: green; + @layer theme { + p { color: #222; } } } - "#, - indoc! {r#" - @media not print and (color) { - .a { - color: green; + "#, "@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}}", ); - - custom_media_test( + minify_test( r#" - @custom-media --color (color); - @custom-media --screen screen; - @custom-media --print print; - - @media (--print) and (--color), (--screen) and (--color) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print and (color), screen and (color) { - .a { - color: green; + @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}}", ); - custom_media_test( + 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#" - @custom-media --color print and (color), print and (script); - - @media (--color) { - .a { - color: green; - } - } - "#, - indoc! {r#" - @media print and ((color) or (script)) { - .a { - color: green; + @layer one { + body { + background: red; } } - "#}, - ); - custom_media_test( - r#" - @custom-media --color (color); - @custom-media --not-color not all and (--color); + body { + background: red; + } - @media (--not-color) { - .a { - color: green; + @layer two { + body { + background: green; } } - "#, - indoc! {r#" - @media not all and (color) { - .a { - color: green; - } - } - "#}, - ); - - custom_media_test( - r#" - @custom-media --color (color); - @media not all and (--color) { - .a { - color: green; + @layer one { + body { + background: yellow; } } "#, - indoc! {r#" - @media not all and (color) { - .a { - color: green; - } - } - "#}, + "@layer one{body{background:#ff0}}body{background:red}@layer two{body{background:green}}", ); + } - custom_media_test( + #[test] + fn test_property() { + minify_test( r#" - @media (--print) { - .a { - color: green; - } - } - - @custom-media --print print; - "#, - indoc! {r#" - @media print { - .a { - color: green; - } + @property --property-name { + syntax: ''; + inherits: false; + initial-value: yellow; } - "#}, + "#, + "@property --property-name{syntax:\"\";inherits:false;initial-value:#ff0}", ); - custom_media_test( + test( r#" - @custom-media --not-width not (min-width: 300px); - @media screen and ((prefers-color-scheme: dark) or (--not-width)) { - .foo { - order: 6; - } + @property --property-name { + syntax: '*'; + inherits: false; + initial-value: ; } - "#, + "#, indoc! {r#" - @media screen and ((prefers-color-scheme: dark) or (not (width >= 300px))) { - .foo { - order: 6; - } - } - "#}, - ); - - fn custom_media_error_test(source: &str, err: Error) { - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - filename: "test.css".into(), - flags: ParserFlags::CUSTOM_MEDIA, - ..ParserOptions::default() - }, - ) - .unwrap(); - let res = stylesheet.minify(MinifyOptions { - targets: Browsers { - chrome: Some(95 << 16), - ..Browsers::default() - } - .into(), - ..MinifyOptions::default() - }); - assert_eq!(res, Err(err)) - } + @property --property-name { + syntax: "*"; + inherits: false; + initial-value: ; + } + "#}, + ); - custom_media_error_test( + minify_test( r#" - @custom-media --color-print print and (color); - - @media screen and (--color-print) { - .a { - color: green; - } + @property --property-name { + syntax: '*'; + inherits: false; + initial-value: ; } - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 1, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 3, - column: 7, - }), - }, + "#, + "@property --property-name{syntax:\"*\";inherits:false;initial-value:}", ); - custom_media_error_test( + test( r#" - @custom-media --color-print print and (color); - - @media not print and (--color-print) { - .a { - color: green; - } + @property --property-name { + syntax: '*'; + inherits: false; + initial-value:; } - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 1, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 3, - column: 7, - }), - }, + "#, + indoc! {r#" + @property --property-name { + syntax: "*"; + inherits: false; + initial-value: ; + } + "#}, ); - custom_media_error_test( + minify_test( r#" - @custom-media --color-print print and (color); - @custom-media --color-screen screen and (color); - - @media (--color-print) or (--color-screen) {} - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 2, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 4, - column: 7, - }), - }, + @property --property-name { + syntax: '*'; + inherits: false; + initial-value:; + } + "#, + "@property --property-name{syntax:\"*\";inherits:false;initial-value:}", ); - - custom_media_error_test( + minify_test( r#" - @custom-media --color-print print and (color); - @custom-media --color-screen screen and (color); - - @media (--color-print) and (--color-screen) {} - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 2, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 4, - column: 7, - }), - }, + @property --property-name { + syntax: '*'; + inherits: false; + initial-value: foo bar; + } + "#, + "@property --property-name{syntax:\"*\";inherits:false;initial-value:foo bar}", ); - custom_media_error_test( + minify_test( r#" - @custom-media --screen screen; - @custom-media --print print; + @property --property-name { + syntax: ''; + inherits: true; + initial-value: 25px; + } + "#, + "@property --property-name{syntax:\"\";inherits:true;initial-value:25px}", + ); - @media (--print) and (--screen) {} - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 1, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 4, - column: 7, - }), - }, + minify_test( + r#" + @property --property-name { + syntax: ''; + inherits: true; + initial-value: "hi"; + } + "#, + "@property --property-name{syntax:\"\";inherits:true;initial-value:\"hi\"}", ); - custom_media_error_test( + error_test( r#" - @custom-media --not-print not print and (color); - @custom-media --not-screen not screen and (color); + @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(), + }), + ); - @media ((script) or ((--not-print) and (--not-screen))) { - .a { - color: green; - } + error_test( + r#" + @property --property-name { + syntax: ''; + inherits: false; + initial-value: var(--some-value); } - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 2, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 4, - column: 7, - }), - }, + "#, + ParserError::UnexpectedToken(crate::properties::custom::Token::Function("var".into())), ); - custom_media_error_test( + error_test( r#" - @custom-media --color screen and (color), print and (color); + @property --property-name { + syntax: ''; + inherits: false; + } + "#, + ParserError::AtRuleBodyInvalid, + ); - @media (--color) { - .a { - color: green; - } + minify_test( + r#" + @property --property-name { + syntax: '*'; + inherits: false; } - "#, - Error { - kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { - custom_media_loc: Location { - source_index: 0, - line: 1, - column: 7, - }, - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 3, - column: 7, - }), - }, + "#, + "@property --property-name{syntax:\"*\";inherits:false}", ); - custom_media_error_test( + error_test( r#" - @media (--not-defined) { - .a { - color: green; - } + @property --property-name { + syntax: '*'; } - "#, - Error { - kind: MinifyErrorKind::CustomMediaNotDefined { - name: "--not-defined".into(), - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 1, - column: 7, - }), - }, + "#, + ParserError::AtRuleBodyInvalid, ); - custom_media_error_test( + error_test( r#" - @custom-media --circular-mq-a (--circular-mq-b); - @custom-media --circular-mq-b (--circular-mq-a); - - @media (--circular-mq-a) { - body { - order: 3; - } + @property --property-name { + inherits: false; } - "#, - Error { - kind: MinifyErrorKind::CircularCustomMedia { - name: "--circular-mq-a".into(), - }, - loc: Some(ErrorLocation { - filename: "test.css".into(), - line: 4, - column: 7, - }), - }, + "#, + ParserError::AtRuleBodyInvalid, ); - } - #[test] - fn test_dependencies() { - fn dep_test(source: &str, expected: &str, deps: Vec<(&str, &str)>) { - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - filename: "test.css".into(), - ..ParserOptions::default() - }, - ) - .unwrap(); - stylesheet.minify(MinifyOptions::default()).unwrap(); - let res = stylesheet - .to_css(PrinterOptions { - analyze_dependencies: Some(Default::default()), - minify: true, - ..PrinterOptions::default() - }) - .unwrap(); - assert_eq!(res.code, expected); - let dependencies = res.dependencies.unwrap(); - assert_eq!(dependencies.len(), deps.len()); - for (i, (url, placeholder)) in deps.into_iter().enumerate() { - match &dependencies[i] { - Dependency::Url(dep) => { - assert_eq!(dep.url, url); - assert_eq!(dep.placeholder, placeholder); - } - Dependency::Import(dep) => { - assert_eq!(dep.url, url); - assert_eq!(dep.placeholder, placeholder); - } - } + error_test( + r#" + @property property-name { + syntax: '*'; + inherits: false; } - } + "#, + ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("property-name".into())), + ); - fn dep_error_test(source: &str, error: PrinterErrorKind) { - let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); - let res = stylesheet.to_css(PrinterOptions { - analyze_dependencies: Some(Default::default()), - ..PrinterOptions::default() - }); - match res { - Err(e) => assert_eq!(e.kind, error), - _ => unreachable!(), + minify_test( + r#" + @property --property-name { + syntax: 'custom | '; + inherits: false; + initial-value: yellow; } - } - - dep_test( - ".foo { background: image-set('./img12x.png', './img21x.png' 2x)}", - ".foo{background:image-set(\"hXFI8W\" 1x,\"5TkpBa\" 2x)}", - vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")], + "#, + "@property --property-name{syntax:\"custom|\";inherits:false;initial-value:#ff0}", ); - dep_test( - ".foo { background: image-set(url(./img12x.png), url('./img21x.png') 2x)}", - ".foo{background:image-set(\"hXFI8W\" 1x,\"5TkpBa\" 2x)}", - vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")], + // 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: '