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 0a0a9618a..95d304311 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,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: | @@ -90,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: | @@ -109,6 +109,9 @@ jobs: - target: aarch64-unknown-linux-gnu strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + - target: aarch64-linux-android + strip: llvm-strip + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - target: armv7-unknown-linux-gnueabihf strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57 @@ -133,6 +136,14 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Setup Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} + run: | + sudo apt update && sudo apt install unzip -y + cd /tmp + wget -q https://dl.google.com/android/repository/android-ndk-r28-linux.zip -O /tmp/ndk.zip + unzip ndk.zip + - name: Setup cross compile toolchain if: ${{ matrix.setup }} run: ${{ matrix.setup }} @@ -144,8 +155,11 @@ jobs: - name: Build release run: yarn build-release env: + ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 RUST_TARGET: ${{ matrix.target }} - name: Build CLI + env: + ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 run: | yarn napi build --bin lightningcss --release --features cli --target ${{ matrix.target }} mv target/${{ matrix.target }}/release/lightningcss lightningcss @@ -153,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: | @@ -201,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: | @@ -232,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 @@ -250,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 671f2e017..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.7" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +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,20 +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" +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", @@ -74,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", @@ -100,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" @@ -121,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" @@ -137,19 +141,28 @@ 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.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf0ca73de70c3da94e4194e4a01fe732378f55d47cf4c0588caab22a0dbfa14" +checksum = "8dd48a6ca358df4f7000e3fb5f08738b1b91a0e5d5f862e2f77b2b14647547f5" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.12", + "browserslist-data", "chrono", "either", - "indexmap 2.2.6", "itertools 0.13.0", "nom", - "once_cell", "serde", "serde_json", "thiserror", @@ -157,9 +170,9 @@ dependencies = [ [[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", @@ -168,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", @@ -185,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", @@ -195,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,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]] @@ -236,14 +249,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -316,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" @@ -372,7 +376,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf", "serde", "smallvec", ] @@ -393,17 +397,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] name = "ctor" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] @@ -413,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", @@ -421,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" @@ -460,18 +464,18 @@ 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" @@ -487,9 +491,9 @@ 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", @@ -497,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" @@ -510,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" @@ -529,45 +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", "libc", - "wasi", + "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", ] @@ -578,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 = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -604,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", @@ -627,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", ] @@ -654,12 +660,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.15.2", "serde", ] @@ -678,15 +684,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -698,9 +695,9 @@ dependencies = [ [[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" @@ -725,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.58" +version = "1.0.0-alpha.70" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.12", "assert_cmd", "assert_fs", "atty", - "bitflags 2.4.1", + "bitflags 2.6.0", "browserslist-rs", "clap", "const-str", @@ -770,7 +768,8 @@ dependencies = [ "cssparser-color", "dashmap", "data-encoding", - "getrandom", + "getrandom 0.3.3", + "indexmap 2.7.0", "indoc", "itertools 0.10.5", "jemallocator", @@ -778,13 +777,14 @@ dependencies = [ "lightningcss-derive", "parcel_selectors", "parcel_sourcemap", - "paste", + "pastey", "pathdiff", "predicates 2.1.5", "pretty_assertions", "rayon", "schemars", "serde", + "serde-content", "serde_json", "smallvec", "static-self", @@ -802,7 +802,7 @@ dependencies = [ [[package]] name = "lightningcss-napi" -version = "0.2.1" +version = "0.4.7" dependencies = [ "crossbeam-channel", "cssparser", @@ -811,6 +811,7 @@ dependencies = [ "parcel_sourcemap", "rayon", "serde", + "serde-content", "serde-detach", "serde_bytes", "smallvec", @@ -839,15 +840,15 @@ dependencies = [ [[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", @@ -855,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" @@ -867,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" @@ -888,11 +880,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "napi" -version = "2.15.4" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e0dc78e0524286630914db66e31bad70160e379705a9ce92e0161ce2389d89" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "ctor", "napi-derive", "napi-sys", @@ -909,23 +901,23 @@ checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b" [[package]] name = "napi-derive" -version = "2.15.3" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56bd9f0bd84c1f138c5cb22bbf394f75d796b24dad689599ca94cf94e61cc21" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] name = "napi-derive-backend" -version = "1.0.61" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b8f403a37007cad225039fc0323b961bb40d697eea744140920ebb689ff1d" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" dependencies = [ "convert_case", "once_cell", @@ -933,14 +925,14 @@ dependencies = [ "quote", "regex", "semver", - "syn 2.0.63", + "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", ] @@ -963,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.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "os_str_bytes" @@ -990,15 +982,15 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "parcel_selectors" -version = "0.26.6" +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", @@ -1021,37 +1013,28 @@ dependencies = [ [[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", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets", ] [[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" @@ -1060,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]] @@ -1089,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", ] @@ -1099,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.63", -] - -[[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]] @@ -1124,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" @@ -1152,27 +1110,26 @@ 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", @@ -1180,9 +1137,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -1214,9 +1171,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1243,13 +1200,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +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" @@ -1262,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", ] @@ -1282,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", @@ -1298,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", @@ -1308,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", @@ -1329,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", @@ -1340,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", @@ -1372,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", @@ -1396,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" @@ -1411,11 +1366,12 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", + "indexmap 2.7.0", "schemars_derive", "serde", "serde_json", @@ -1424,14 +1380,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] @@ -1448,19 +1404,29 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.201" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +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" @@ -1473,46 +1439,62 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.12" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] name = "serde_derive_internals" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "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" @@ -1524,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" @@ -1536,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", ] @@ -1579,9 +1562,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1596,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.63", -] - -[[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", ] @@ -1684,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.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +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" @@ -1723,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", @@ -1737,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.63", + "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", @@ -1774,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.63", + "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" @@ -1809,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]] @@ -1824,35 +1806,20 @@ 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 0.48.5", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -1861,46 +1828,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1913,36 +1862,18 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1951,15 +1882,18 @@ 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 = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] [[package]] name = "wyz" @@ -1978,26 +1912,26 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index dd55452aa..c113a3921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,17 +6,17 @@ members = [ "c", "derive", "static-self", - "static-self-derive" + "static-self-derive", ] [package] authors = ["Devon Govett "] name = "lightningcss" -version = "1.0.0-alpha.58" +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] @@ -34,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 = [] -into_owned = ["static-self", "static-self/smallvec", "parcel_selectors/into_owned"] +into_owned = [ + "static-self", + "static-self/smallvec", + "static-self/indexmap", + "parcel_selectors/into_owned", +] substitute_variables = ["visitor", "into_owned"] [dependencies] -serde = { version = "1.0.201", features = ["derive"], optional = true } +serde = { version = "1.0.228", features = ["derive"], optional = true } +serde-content = { version = "0.1.2", features = ["serde"], optional = true } cssparser = "0.33.0" cssparser-color = "0.1.0" -parcel_selectors = { version = "0.26.6", 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" @@ -61,23 +74,26 @@ lazy_static = "1.4.0" const-str = "0.3.1" pathdiff = "0.2.1" ahash = "0.8.7" -paste = "1.0.12" +pastey = "0.1.0" +indexmap = { version = "2.2.6", features = ["serde"] } # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } -browserslist-rs = { version = "0.16.0", 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.43", path = "./derive" } -schemars = { version = "0.8.19", features = ["smallvec"], optional = true } -static-self = { version = "0.1.0", path = "static-self", optional = true } +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.2", features = ["custom"], default-features = false } +getrandom = { version = "0.3", default-features = false } [dev-dependencies] indoc = "1.0.3" diff --git a/c/Cargo.toml b/c/Cargo.toml index 96641dc75..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.16.0" } +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 ba689d967..759a18dbe 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -281,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/napi/Cargo.toml b/napi/Cargo.toml index 477c1898c..789062ea6 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Devon Govett "] name = "lightningcss-napi" -version = "0.2.1" +version = "0.4.7" description = "Node-API bindings for Lightning CSS" license = "MPL-2.0" repository = "https://github.com/parcel-bundler/lightningcss" @@ -14,12 +14,20 @@ 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.56", path = "../", features = ["nodejs", "serde"] } +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"]} +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/napi/src/lib.rs b/napi/src/lib.rs index 9edfb1ce6..f43a8d46e 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -121,8 +121,8 @@ pub fn transform_style_attribute(ctx: CallContext) -> napi::Result { mod bundle { use super::*; use crossbeam_channel::{self, Receiver, Sender}; - use lightningcss::bundler::FileProvider; - use napi::{Env, JsFunction, JsString, NapiRaw}; + use lightningcss::bundler::{FileProvider, ResolveResult}; + use napi::{Env, JsBoolean, JsFunction, JsString, NapiRaw}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Mutex; @@ -169,6 +169,7 @@ mod bundle { // Allocate a single channel per thread to communicate with the JS thread. thread_local! { static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); + static RESOLVER_CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); } impl SourceProvider for JsSourceProvider { @@ -203,9 +204,9 @@ mod bundle { } } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { - return CHANNEL.with(|channel| { + return RESOLVER_CHANNEL.with(|channel| { let message = ResolveMessage { specifier: specifier.to_owned(), originating_file: originating_file.to_str().unwrap().to_owned(), @@ -213,22 +214,18 @@ mod bundle { }; resolve.call(message, ThreadsafeFunctionCallMode::Blocking); - let result = channel.1.recv().unwrap(); - match result { - Ok(result) => Ok(PathBuf::from_str(&result).unwrap()), - Err(e) => Err(e), - } + channel.1.recv().unwrap() }); } - Ok(originating_file.with_file_name(specifier)) + Ok(originating_file.with_file_name(specifier).into()) } } struct ResolveMessage { specifier: String, originating_file: String, - tx: Sender>, + tx: Sender>, } struct ReadMessage { @@ -241,7 +238,11 @@ mod bundle { tx: Sender>, } - fn await_promise(env: Env, result: JsUnknown, tx: Sender>) -> napi::Result<()> { + fn await_promise(env: Env, result: JsUnknown, tx: Sender>, parse: Cb) -> napi::Result<()> + where + T: 'static, + Cb: 'static + Fn(JsUnknown) -> Result, + { // If the result is a promise, wait for it to resolve, and send the result to the channel. // Otherwise, send the result immediately. if result.is_promise()? { @@ -249,9 +250,8 @@ mod bundle { let then: JsFunction = get_named_property(&result, "then")?; let tx2 = tx.clone(); let cb = env.create_function_from_closure("callback", move |ctx| { - let res = ctx.get::(0)?.into_utf8()?; - let s = res.into_owned()?; - tx.send(Ok(s)).unwrap(); + let res = parse(ctx.get::(0)?)?; + tx.send(Ok(res)).unwrap(); ctx.env.get_undefined() })?; let eb = env.create_function_from_closure("error_callback", move |ctx| { @@ -261,10 +261,8 @@ mod bundle { })?; then.call(Some(&result), &[cb, eb])?; } else { - let result: JsString = result.try_into()?; - let utf8 = result.into_utf8()?; - let s = utf8.into_owned()?; - tx.send(Ok(s)).unwrap(); + let result = parse(result)?; + tx.send(Ok(result)).unwrap(); } Ok(()) @@ -274,10 +272,12 @@ mod bundle { let specifier = ctx.env.create_string(&ctx.value.specifier)?; let originating_file = ctx.env.create_string(&ctx.value.originating_file)?; let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?; - await_promise(ctx.env, result, ctx.value.tx) + await_promise(ctx.env, result, ctx.value.tx, move |unknown| { + ctx.env.from_js_value(unknown) + }) } - fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { + fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { match res { Ok(_) => Ok(()), Err(e) => { @@ -295,7 +295,9 @@ mod bundle { fn read_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { let file = ctx.env.create_string(&ctx.value.file)?; let result = ctx.callback.unwrap().call(None, &[file])?; - await_promise(ctx.env, result, ctx.value.tx) + await_promise(ctx.env, result, ctx.value.tx, |unknown| { + JsString::try_from(unknown)?.into_utf8()?.into_owned() + }) } fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { @@ -421,10 +423,10 @@ mod bundle { #[cfg(target_arch = "wasm32")] mod bundle { use super::*; + use lightningcss::bundler::ResolveResult; use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; use std::cell::UnsafeCell; - use std::path::{Path, PathBuf}; - use std::str::FromStr; + use std::path::Path; pub fn bundle(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; @@ -497,7 +499,7 @@ mod bundle { ); } - fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { + fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { if value.is_promise()? { let mut result = std::ptr::null_mut(); let mut error = std::ptr::null_mut(); @@ -513,7 +515,7 @@ mod bundle { value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; } - value.try_into() + Ok(value) } impl SourceProvider for JsSourceProvider { @@ -523,7 +525,9 @@ mod bundle { let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?; let file = self.env.create_string(file.to_str().unwrap())?; let source: JsUnknown = read.call(None, &[file])?; - let source = get_result(self.env, source)?.into_utf8()?.into_owned()?; + let source = get_result(self.env, source)?; + let source: JsString = source.try_into()?; + let source = source.into_utf8()?.into_owned()?; // cache the result let ptr = Box::into_raw(Box::new(source)); @@ -535,16 +539,17 @@ mod bundle { Ok(unsafe { &*ptr }) } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?; let specifier = self.env.create_string(specifier)?; let originating_file = self.env.create_string(originating_file.to_str().unwrap())?; let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?; - let result = get_result(self.env, result)?.into_utf8()?; - Ok(PathBuf::from_str(result.as_str()?).unwrap()) + let result = get_result(self.env, result)?; + let result = self.env.from_js_value(result)?; + Ok(result) } else { - Ok(originating_file.with_file_name(specifier)) + Ok(ResolveResult::File(originating_file.with_file_name(specifier))) } } } @@ -606,8 +611,10 @@ struct CssModulesConfig { pattern: Option, dashed_idents: Option, animation: Option, + container: Option, grid: Option, custom_idents: Option, + pure: Option, } #[cfg(feature = "bundler")] @@ -717,8 +724,10 @@ fn compile<'i>( }, 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 { @@ -847,8 +856,10 @@ fn compile_bundle< }, 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 { diff --git a/napi/src/transformer.rs b/napi/src/transformer.rs index ad20c611d..bab931f2d 100644 --- a/napi/src/transformer.rs +++ b/napi/src/transformer.rs @@ -298,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", @@ -310,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) { @@ -752,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; @@ -766,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, @@ -808,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()) @@ -825,8 +827,10 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { return Ok(TokensOrRaw(ValueOrVec::Vec(res.into_owned().0))); } - let de = ContentRefDeserializer::new(&content); - Ok(TokensOrRaw(ValueOrVec::deserialize(de)?)) + let de = serde_content::Deserializer::new(content).coerce_numbers(); + Ok(TokensOrRaw( + ValueOrVec::deserialize(de).map_err(|e| serde::de::Error::custom(e.to_string()))?, + )) } } diff --git a/node/Cargo.toml b/node/Cargo.toml index 6cf2aa214..b5c7505c3 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -9,8 +9,13 @@ publish = false crate-type = ["cdylib"] [dependencies] -lightningcss-napi = { version = "0.2.1", path = "../napi", features = ["bundler", "visitor"] } -napi = {version = "2.15.4", default-features = false, features = ["compat-mode"]} +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" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/node/ast.d.ts b/node/ast.d.ts index 19ab2c1ff..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. @@ -2342,9 +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"; } @@ -3819,12 +3845,25 @@ 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; @@ -5192,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 = | { @@ -6394,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. */ @@ -6406,6 +6445,25 @@ 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). */ @@ -6733,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"; /** @@ -6813,6 +6888,12 @@ export type PseudoElement = | { kind: "first-letter"; } + | { + kind: "details-content"; + } + | { + kind: "target-text"; + } | { kind: "selection"; vendorPrefix: VendorPrefix; @@ -6864,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"; @@ -7116,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). */ @@ -7162,6 +7273,10 @@ export type ParsedComponent = type: "length-percentage"; value: DimensionPercentageFor_LengthValue; } + | { + type: "string"; + value: String; + } | { type: "color"; value: CssColor; @@ -7263,6 +7378,9 @@ export type SyntaxComponentKind = | { type: "length-percentage"; } + | { + type: "string"; + } | { type: "color"; } @@ -7322,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. @@ -7395,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; @@ -7413,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; /** @@ -8264,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 { @@ -8283,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 { @@ -8312,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 { /** @@ -8325,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 { /** @@ -8338,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 { /** @@ -9126,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. */ @@ -9250,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. */ @@ -9382,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. */ @@ -9495,7 +9806,7 @@ export interface ContainerRule { /** * The container condition. */ - condition: ContainerCondition; + condition?: ContainerCondition | null; /** * The location of the rule in the source file. */ @@ -9545,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 e429b0f2e..822944124 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -3,7 +3,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; use napi::{CallContext, JsObject, JsUnknown}; -use napi_derive::{js_function, module_exports}; +use napi_derive::js_function; #[js_function(1)] fn transform(ctx: CallContext) -> napi::Result { @@ -26,7 +26,7 @@ pub fn bundle_async(ctx: CallContext) -> napi::Result { lightningcss_napi::bundle_async(ctx) } -#[cfg_attr(not(target_arch = "wasm32"), module_exports)] +#[cfg_attr(not(target_arch = "wasm32"), napi_derive::module_exports)] fn init(mut exports: JsObject) -> napi::Result<()> { exports.create_named_method("transform", transform)?; exports.create_named_method("transformStyleAttribute", transform_style_attribute)?; @@ -45,7 +45,6 @@ pub fn register_module() { unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> { use napi::{Env, JsObject, NapiValue}; - let env = Env::from_raw(raw_env); let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); init(exports) } diff --git a/node/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 50d113b57..4279e51c8 100644 --- a/node/test/bundle.test.mjs +++ b/node/test/bundle.test.mjs @@ -365,7 +365,7 @@ test('resolve return non-string', async () => { } if (!error) throw new Error(`\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`); - assert.equal(error.message, 'expect String, got: Number'); + assert.equal(error.message, 'data did not match any variant of untagged enum ResolveResult'); assert.equal(error.fileName, 'tests/testdata/foo.css'); assert.equal(error.loc, { line: 1, @@ -414,4 +414,29 @@ test('should support throwing in visitors', async () => { assert.equal(error.message, 'Some error'); }); +test('external import', async () => { + const { code: buffer } = await bundleAsync(/** @type {import('../index').BundleAsyncOptions} */ ({ + filename: 'tests/testdata/has_external.css', + resolver: { + resolve(specifier, originatingFile) { + if (specifier === './does_not_exist.css' || specifier.startsWith('https:')) { + return {external: specifier}; + } + return path.resolve(path.dirname(originatingFile), specifier); + } + } + })); + const code = buffer.toString('utf-8').trim(); + + const expected = ` +@import "https://fonts.googleapis.com/css2?family=Roboto&display=swap"; +@import "./does_not_exist.css"; + +.b { + height: calc(100vh - 64px); +} + `.trim(); + if (code !== expected) throw new Error(`\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`); +}); + test.run(); diff --git a/node/test/composeVisitors.test.mjs b/node/test/composeVisitors.test.mjs index bb95d9123..4379cf481 100644 --- a/node/test/composeVisitors.test.mjs +++ b/node/test/composeVisitors.test.mjs @@ -513,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({ @@ -686,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/visitor.test.mjs b/node/test/visitor.test.mjs index 3a42a696b..149825b7d 100644 --- a/node/test/visitor.test.mjs +++ b/node/test/visitor.test.mjs @@ -249,6 +249,68 @@ test('specific environment variables', () => { assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}'); }); +test('spacing with env substitution', () => { + // Test spacing for different cases when `env()` functions are replaced with actual values. + /** @type {Record} */ + let tokens = { + '--var1': 'var(--foo)', + '--var2': 'var(--bar)', + '--function': 'scale(1.5)', + '--length1': '10px', + '--length2': '20px', + '--x': '4', + '--y': '12', + '--num1': '5', + '--num2': '10', + '--num3': '15', + '--counter': '2', + '--ident1': 'solid', + '--ident2': 'auto', + '--rotate': '45deg', + '--percentage1': '25%', + '--percentage2': '75%', + '--color': 'red', + '--color1': '#ff1234', + '--string1': '"hello"', + '--string2': '" world"' + }; + + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + .test { + /* Asymmetric spacing - no space after var(). */ + background: env(--var1) env(--var2); + border: env(--var1)env(--ident1); + transform: env(--function) env(--function); + /* Normal spacing between values. */ + padding: env(--length1) env(--length2); + margin: env(--length1) env(--ident2); + outline: env(--color) env(--ident1); + /* Raw numbers that need spacing. */ + cursor: url(cursor.png) env(--x) env(--y), auto; + stroke-dasharray: env(--num1) env(--num2) env(--num3); + counter-increment: myCounter env(--counter); + /* Mixed token types. */ + background: linear-gradient(red env(--percentage1), blue env(--percentage2)); + content: env(--string1) env(--string2); + /* Inside calc expressions. */ + width: calc(env(--length1) - env(--length2)); + } + `), + visitor: { + EnvironmentVariable(env) { + if (env.name.type === 'custom' && tokens[env.name.ident]) { + return { raw: tokens[env.name.ident] }; + } + } + } + }); + + assert.equal(res.code.toString(), '.test{background:var(--foo) var(--bar);border:var(--foo)solid;transform:scale(1.5) scale(1.5);padding:10px 20px;margin:10px auto;outline:red solid;cursor:url(cursor.png) 4 12, auto;stroke-dasharray:5 10 15;counter-increment:myCounter 2;background:linear-gradient(red 25%, blue 75%);content:"hello" " world";width:calc(10px - 20px)}'); +}); + test('url', () => { // https://www.npmjs.com/package/postcss-url let res = transform({ @@ -1108,4 +1170,119 @@ test('visit stylesheet', () => { assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}'); }); +test('visitor function', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @dep "foo.js"; + + .foo { + width: 32px; + } + `), + visitor: ({addDependency}) => ({ + Rule: { + unknown: { + dep(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }) + }); + + assert.equal(res.code.toString(), '.foo{width:32px}'); + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'foo.js' + }]); +}); + +test('visitor function works with style attributes', () => { + let res = transformStyleAttribute({ + filename: 'test.css', + minify: true, + code: Buffer.from('height: 12px'), + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'test.json' + }]); +}); + +test('visitor function works with bundler', () => { + let res = bundle({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + +test('works with async bundler', async () => { + let res = await bundleAsync({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + test.run(); diff --git a/package.json b/package.json index 68e6701db..af17c0a58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.26.0", + "version": "1.31.1", "license": "MPL-2.0", "description": "A CSS parser, transformer, and minifier written in Rust", "main": "node/index.js", @@ -39,21 +39,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.5.44", + "@mdn/browser-compat-data": "~7.2.4", "@napi-rs/cli": "^2.14.0", - "autoprefixer": "^10.4.20", - "caniuse-lite": "^1.0.30001649", + "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", @@ -73,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 a436857e5..1a2165581 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.76.0" +channel = "1.92.0" components = ["rustfmt", "clippy"] diff --git a/scripts/build-ast.js b/scripts/build-ast.js index de0e7f20a..883aa442d 100644 --- a/scripts/build-ast.js +++ b/scripts/build-ast.js @@ -75,6 +75,27 @@ compileFromFile('node/ast.json', { 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 ca89d669e..ef447a7ea 100644 --- a/scripts/build-npm.js +++ b/scripts/build-npm.js @@ -38,6 +38,9 @@ const triples = [ }, { name: 'x86_64-unknown-freebsd' + }, + { + name: 'aarch64-linux-android' } ]; const cpuToNodeArch = { @@ -51,6 +54,7 @@ const sysToNodePlatform = { freebsd: 'freebsd', darwin: 'darwin', windows: 'win32', + android: 'android' }; let optionalDependencies = {}; diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js index 8fc83ca23..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, @@ -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, @@ -330,6 +335,28 @@ let mdnFeatures = { 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) { @@ -344,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([ @@ -377,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; } @@ -464,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()) @@ -637,7 +665,10 @@ impl Feature { if self.is_compatible(browsers) { return true } - browsers.${browser} = None; + #[allow(unused_assignments)] + { + browsers.${browser} = None; + } }\n`).join(' ')} false } diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index 4c2129705..824b2f2bd 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parcel_selectors" -version = "0.26.6" +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.201", features = ["derive"], optional = true } schemars = { version = "0.8.19", features = ["smallvec"], optional = true } -static-self = { version = "0.1.0", path = "../static-self", 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 c0e27a9ab..19563d5f9 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -197,7 +197,8 @@ 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>), @@ -206,6 +207,7 @@ pub enum SelectorParseErrorKind<'i> { InvalidQualNameInAttr(Token<'i>), ExplicitNamespaceUnexpectedToken(Token<'i>), ClassNeedsIdent(Token<'i>), + UnexpectedSelectorAfterPseudoElement(Token<'i>), } macro_rules! with_all_bounds { @@ -311,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>( @@ -319,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( @@ -327,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>( @@ -335,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> { @@ -878,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() } @@ -901,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() } @@ -1007,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, } @@ -1090,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. @@ -1300,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", @@ -1320,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'), @@ -2141,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; } @@ -2956,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(..) => { @@ -2967,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()? { @@ -2989,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) @@ -3337,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>( @@ -3352,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( @@ -3365,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 { @@ -3377,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()) } @@ -3912,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 d73545b8d..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) } } @@ -806,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()) + } } } @@ -826,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, @@ -866,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, @@ -873,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() @@ -1521,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! { @@ -1978,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 6c86d3988..f0c828d75 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -28,6 +28,7 @@ pub enum Feature { CapUnit, CaseInsensitive, ChUnit, + Checkmark, CircleListStyleType, CjkDecimalListStyleType, CjkEarthlyBranchListStyleType, @@ -42,6 +43,7 @@ pub enum Feature { DecimalLeadingZeroListStyleType, DecimalListStyleType, DefaultPseudo, + DetailsContent, DevanagariListStyleType, Dialog, DirSelector, @@ -85,6 +87,7 @@ pub enum Feature { Gencontent, GeorgianListStyleType, GradientInterpolationHints, + GrammarError, GujaratiListStyleType, GurmukhiListStyleType, HasSelector, @@ -97,7 +100,6 @@ pub enum Feature { ImageSet, InOutOfRange, IndeterminatePseudo, - IsAnimatableSize, IsSelector, JapaneseFormalListStyleType, JapaneseInformalListStyleType, @@ -141,7 +143,6 @@ pub enum Feature { MinFunction, ModFunction, MongolianListStyleType, - MozAvailableSize, MyanmarListStyleType, Namespaces, Nesting, @@ -157,6 +158,8 @@ pub enum Feature { P3Colors, PartPseudo, PersianListStyleType, + Picker, + PickerIcon, PlaceContent, PlaceItems, PlaceSelf, @@ -186,11 +189,14 @@ pub enum Feature { SimpChineseInformalListStyleType, SomaliListStyleType, SpaceSeparatedColorNotation, + SpellingError, SquareListStyleType, + StatePseudoClass, StretchSize, StringListStyleType, SymbolsListStyleType, TamilListStyleType, + TargetText, TeluguListStyleType, TextDecorationThicknessPercent, TextDecorationThicknessShorthand, @@ -212,6 +218,7 @@ pub enum Feature { VbUnit, VhUnit, ViUnit, + ViewTransition, ViewportPercentageUnitsDynamic, ViewportPercentageUnitsLarge, ViewportPercentageUnitsSmall, @@ -445,7 +452,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -537,7 +544,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -582,7 +589,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -627,7 +634,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -672,7 +679,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -717,7 +724,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -762,7 +769,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -807,7 +814,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -899,7 +906,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -944,7 +951,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1008,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 < 8323072 { + if version < 9371648 { return false; } } @@ -1033,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; } } @@ -1069,7 +1066,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1159,7 +1156,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1204,7 +1201,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1254,7 +1251,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1341,7 +1338,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1386,7 +1383,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1431,11 +1428,16 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } - if browsers.ie.is_some() || browsers.samsung.is_some() { + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -1471,7 +1473,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1516,7 +1518,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1561,7 +1563,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1628,7 +1630,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 8323072 { + if version < 9371648 { return false; } } @@ -1641,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 { @@ -2214,7 +2216,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 458752 { return false; } } @@ -2518,6 +2520,16 @@ impl Feature { return false; } } + if let Some(version) = browsers.safari { + if version < 1704448 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1704448 { + return false; + } + } if let Some(version) = browsers.samsung { if version < 917504 { return false; @@ -2528,7 +2540,7 @@ impl Feature { return false; } } - if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() { + if browsers.ie.is_some() { return false; } } @@ -2654,7 +2666,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -2773,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; @@ -2783,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; } } @@ -2908,21 +2930,41 @@ impl Feature { 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() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } Feature::AbsFunction | Feature::SignFunction => { + if let Some(version) = browsers.chrome { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 9043968 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 7733248 { return false; } } + if let Some(version) = browsers.opera { + if version < 5963776 { + return false; + } + } if let Some(version) = browsers.safari { if version < 984064 { return false; @@ -2933,13 +2975,12 @@ impl Feature { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.android { + if version < 9043968 { + return false; + } + } + if browsers.ie.is_some() || browsers.samsung.is_some() { return false; } } @@ -3385,12 +3426,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1769472 { + return false; + } + } if let Some(version) = browsers.android { if version < 8060928 { return false; } } - if browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -3454,6 +3500,294 @@ impl Feature { 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 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 { @@ -3835,6 +4169,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 9633792 { + return false; + } + } if let Some(version) = browsers.opera { if version < 5177344 { return false; @@ -3860,7 +4199,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -3875,6 +4214,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.firefox { + if version < 9633792 { + return false; + } + } if let Some(version) = browsers.opera { if version < 4915200 { return false; @@ -3900,7 +4244,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -4271,7 +4615,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4318,7 +4662,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4822,6 +5166,7 @@ impl Feature { | Feature::HiraganaIrohaListStyleType | Feature::KatakanaListStyleType | Feature::KatakanaIrohaListStyleType + | Feature::NoneListStyleType | Feature::AutoSize => { if let Some(version) = browsers.chrome { if version < 1179648 { @@ -5008,53 +5353,6 @@ impl Feature { } } } - Feature::NoneListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } Feature::SimpChineseFormalListStyleType | Feature::SimpChineseInformalListStyleType | Feature::TradChineseFormalListStyleType @@ -5177,63 +5475,33 @@ impl Feature { return false; } } - if let Some(version) = browsers.opera { - if version < 5439488 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8192000 { - return false; - } - } - if browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::FitContentSize => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } if let Some(version) = browsers.firefox { - if version < 262144 { + if version < 9633792 { return false; } } if let Some(version) = browsers.opera { - if version < 917504 { + if version < 5439488 { return false; } } if let Some(version) = browsers.safari { - if version < 458752 { + if version < 1703936 { return false; } } if let Some(version) = browsers.ios_saf { - if version < 458752 { + if version < 1703936 { return false; } } if let Some(version) = browsers.samsung { - if version < 66816 { + if version < 1769472 { return false; } } if let Some(version) = browsers.android { - if version < 263168 { + if version < 8192000 { return false; } } @@ -5241,24 +5509,19 @@ impl Feature { return false; } } - Feature::IsAnimatableSize => { + Feature::FitContentSize => { if let Some(version) = browsers.chrome { - if version < 1703936 { + if version < 1638400 { return false; } } if let Some(version) = browsers.edge { - if version < 786432 { + if version < 5177344 { return false; } } if let Some(version) = browsers.firefox { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { + if version < 262144 { return false; } } @@ -5287,6 +5550,9 @@ impl Feature { return false; } } + if browsers.ie.is_some() { + return false; + } } Feature::MaxContentSize => { if let Some(version) = browsers.chrome { @@ -5378,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; } } @@ -5428,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; } @@ -5469,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/css_modules.rs b/src/css_modules.rs index cfe33d71a..ce7008df2 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -41,6 +41,11 @@ pub struct Config<'i> { /// 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> { @@ -50,7 +55,9 @@ impl<'i> Default for Config<'i> { dashed_idents: Default::default(), animation: true, grid: true, + container: true, custom_idents: true, + pure: false, } } } @@ -107,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); @@ -126,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>, { @@ -150,6 +170,9 @@ impl<'i> Pattern<'i> { Segment::Hash => { write(hash)?; } + Segment::ContentHash => { + write(content_hash)?; + } } } Ok(()) @@ -162,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) } } @@ -181,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. @@ -245,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, } @@ -255,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(); @@ -279,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, } } @@ -295,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![], @@ -314,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![], @@ -336,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![], @@ -365,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(), ) @@ -385,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![], @@ -427,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 da97e37dc..0dd3da619 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -1,11 +1,10 @@ //! CSS declarations. use std::borrow::Cow; -use std::collections::HashMap; use std::ops::Range; use crate::context::{DeclarationContext, PropertyHandlerContext}; -use crate::error::{ParserError, PrinterError}; +use crate::error::{ParserError, PrinterError, PrinterErrorKind}; use crate::parser::ParserOptions; use crate::printer::Printer; use crate::properties::box_shadow::BoxShadowHandler; @@ -35,12 +34,15 @@ use crate::properties::{ ui::ColorSchemeHandler, }; use crate::properties::{Property, PropertyId}; +use crate::selector::SelectorList; use crate::traits::{PropertyHandler, ToCss}; use crate::values::ident::DashedIdent; use crate::values::string::CowArcStr; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; +use indexmap::IndexMap; +use smallvec::SmallVec; /// A CSS declaration block. /// @@ -157,18 +159,70 @@ impl<'i> DeclarationBlock<'i> { dest.whitespace()?; dest.write_char('{')?; dest.indent(); + dest.newline()?; + + self.to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0)?; + + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } + pub(crate) fn has_printable_declarations(&self) -> bool { + if self.len() > 1 { + return true; + } + + if self.declarations.len() == 1 { + !matches!(self.declarations[0], crate::properties::Property::Composes(_)) + } else if self.important_declarations.len() == 1 { + !matches!(self.important_declarations[0], crate::properties::Property::Composes(_)) + } else { + false + } + } + + /// Writes the declarations to a CSS declaration block. + pub fn to_css_declarations( + &self, + dest: &mut Printer, + has_nested_rules: bool, + selectors: &SelectorList, + source_index: u32, + ) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { let mut i = 0; let len = self.len(); macro_rules! write { ($decls: expr, $important: literal) => { for decl in &$decls { - dest.newline()?; + // The CSS modules `composes` property is handled specially, and omitted during printing. + // We need to add the classes it references to the list for the selectors in this rule. + if let crate::properties::Property::Composes(composes) = &decl { + if dest.is_nested() && dest.css_module.is_some() { + return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc)); + } + + if let Some(css_module) = &mut dest.css_module { + css_module + .handle_composes(&selectors, &composes, source_index) + .map_err(|e| dest.error(e, composes.loc))?; + continue; + } + } + + if i > 0 { + dest.newline()?; + } + decl.to_css(dest, $important)?; - if i != len - 1 || !dest.minify { + if i != len - 1 || !dest.minify || has_nested_rules { dest.write_char(';')?; } + i += 1; } }; @@ -176,10 +230,7 @@ impl<'i> DeclarationBlock<'i> { write!(self.declarations, false); write!(self.important_declarations, true); - - dest.dedent(); - dest.newline()?; - dest.write_char('}') + Ok(()) } } @@ -518,7 +569,7 @@ pub(crate) struct DeclarationHandler<'i> { prefix: PrefixHandler, direction: Option, unicode_bidi: Option, - custom_properties: HashMap<'i>, usize>, + custom_properties: IndexMap<'i>, usize>, decls: DeclarationList<'i>, } @@ -609,7 +660,7 @@ impl<'i> DeclarationHandler<'i> { direction: self.direction.clone(), ..Default::default() }; - for (key, index) in self.custom_properties.drain() { + for (key, index) in self.custom_properties.drain(..) { handler.custom_properties.insert(key, handler.decls.len()); handler.decls.push(self.decls[index].clone()); } diff --git a/src/error.rs b/src/error.rs index c7f62e4f7..b4b1b0ab8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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,11 +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> { @@ -259,8 +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:?}'" + ) + }, } } } @@ -289,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()), @@ -302,6 +323,9 @@ impl<'i> From<'i>> for SelectorError<'i> { } SelectorParseErrorKind::ClassNeedsIdent(t) => SelectorError::ClassNeedsIdent(t.into()), SelectorParseErrorKind::AmbiguousCssModuleClass(name) => SelectorError::AmbiguousCssModuleClass(name.into()), + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(t) => { + SelectorError::UnexpectedSelectorAfterPseudoElement(t.into()) + } } } } @@ -342,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 { @@ -354,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" + ), } } } @@ -383,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 02bf9fcf3..2a41655ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,7 @@ mod tests { use indoc::indoc; use pretty_assertions::assert_eq; use std::collections::HashMap; + use std::sync::{Arc, RwLock}; fn test(source: &str, expected: &str) { test_with_options(source, expected, ParserOptions::default()) @@ -78,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(); @@ -94,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 @@ -169,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, @@ -180,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); @@ -217,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)?)*),* } => { { @@ -335,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( @@ -1443,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( @@ -2118,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; } @@ -2139,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; } @@ -2161,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; } @@ -2183,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)); } @@ -3953,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] @@ -4102,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}", @@ -4128,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 }", @@ -6820,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", @@ -6830,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), @@ -6846,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}"); @@ -6900,32 +7262,185 @@ 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(), ); - - // named animation range percentages + 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 { @@ -7539,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) }", @@ -7550,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)) }", @@ -7682,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] @@ -7722,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}"); @@ -7741,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] @@ -7773,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] @@ -8194,7 +8759,7 @@ mod tests { } "#, indoc! { r#" - @media (min-color: 3) { + @media not (max-color: 2) { .foo { color: #7fff00; } @@ -8215,7 +8780,7 @@ mod tests { } "#, indoc! { r#" - @media (max-color: 1) { + @media not (min-color: 2) { .foo { color: #7fff00; } @@ -8236,7 +8801,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: 240.001px) { + @media not (max-width: 240px) { .foo { color: #7fff00; } @@ -8299,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; } @@ -8404,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; } @@ -8455,7 +9100,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: calc(1.001px + 1rem)) { + @media not (max-width: calc(1px + 1rem)) { .foo { color: #ff0; } @@ -8473,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; } @@ -8491,7 +9136,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: .001px) { + @media not (max-width: 0) { .foo { color: #ff0; } @@ -8545,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; } @@ -8651,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, @@ -8695,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] @@ -8896,6 +9576,29 @@ mod tests { @layer b, c; "#}, ); + + test( + r#" + @layer a; + @import "foo.css"; + + @layer a { + foo { + color: red; + } + } + "#, + indoc! {r#" + @layer a; + @import "foo.css"; + + @layer a { + foo { + color: red; + } + } + "#}, + ); } #[test] @@ -11094,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] @@ -11859,15 +12633,44 @@ mod tests { #[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)}", + 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( + test( + ".foo { transform: translate3d(12px,50%,3em)scale(2,.5) }", + indoc! {r#" + .foo { + transform: translate3d(12px, 50%, 3em) scale(2, .5); + } + "#}, + ); + test( + ".foo { transform:matrix(1,2,-1,1,80,80) }", + indoc! {r#" + .foo { + transform: matrix(1, 2, -1, 1, 80, 80); + } + "#}, + ); + + minify_test( + ".foo { transform: scale( 0.5 )translateX(10px ) }", + ".foo{transform:scale(.5)translate(10px)}", + ); + minify_test( + ".foo { transform: translate(2px, 3px)", + ".foo{transform:translate(2px,3px)}", + ); + minify_test( + ".foo { transform: translate(2px, 0px)", + ".foo{transform:translate(2px)}", + ); + minify_test( ".foo { transform: translate(0px, 2px)", ".foo{transform:translateY(2px)}", ); @@ -12061,16 +12864,31 @@ mod tests { minify_test(".foo { translate: 1px 2px 0px }", ".foo{translate:1px 2px}"); minify_test(".foo { translate: 1px 0px 2px }", ".foo{translate:1px 0 2px}"); minify_test(".foo { translate: none }", ".foo{translate:none}"); + minify_test(".foo { rotate: none }", ".foo{rotate:none}"); + minify_test(".foo { rotate: 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: -0deg }", ".foo{rotate:0deg}"); minify_test(".foo { rotate: 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: z 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: 0 0 1 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: x 10deg }", ".foo{rotate:x 10deg}"); minify_test(".foo { rotate: 1 0 0 10deg }", ".foo{rotate:x 10deg}"); - minify_test(".foo { rotate: y 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: 2 0 0 10deg }", ".foo{rotate:x 10deg}"); + minify_test(".foo { rotate: 0 2 0 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: 0 0 2 10deg }", ".foo{rotate:10deg}"); + minify_test(".foo { rotate: 0 0 5.3 10deg }", ".foo{rotate:10deg}"); + minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 10deg 0 0 -1 }", ".foo{rotate:-10deg}"); + minify_test(".foo { rotate: 10deg 0 0 -233 }", ".foo{rotate:-10deg}"); + minify_test(".foo { rotate: -1 0 0 0deg }", ".foo{rotate:x 0deg}"); + minify_test(".foo { rotate: 0deg 0 0 1 }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 0deg 0 0 -1 }", ".foo{rotate:0deg}"); minify_test(".foo { rotate: 0 1 0 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: x 0rad }", ".foo{rotate:x 0deg}"); + // TODO: In minify mode, convert units to the shortest form. + // minify_test(".foo { rotate: y 0turn }", ".foo{rotate:y 0deg}"); + minify_test(".foo { rotate: z 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 10deg y }", ".foo{rotate:y 10deg}"); minify_test(".foo { rotate: 1 1 1 10deg }", ".foo{rotate:1 1 1 10deg}"); - minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:none}"); - minify_test(".foo { rotate: none }", ".foo{rotate:none}"); minify_test(".foo { scale: 1 }", ".foo{scale:1}"); minify_test(".foo { scale: 1 1 }", ".foo{scale:1}"); minify_test(".foo { scale: 1 1 1 }", ".foo{scale:1}"); @@ -12494,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); } "#}, @@ -12512,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); } "#}, @@ -12530,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); } "#}, @@ -12548,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); } "#}, @@ -12566,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); } "#}, @@ -12584,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); } "#}, @@ -12602,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); } "#}, @@ -12636,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); } "#}, @@ -12760,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"); } "#}, @@ -12842,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)); } @@ -12858,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)); } @@ -12929,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)); } @@ -12945,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)); } @@ -12981,6 +13799,357 @@ mod tests { ..Browsers::default() }, ); + + // Test cases from https://github.com/postcss/autoprefixer/blob/541295c0e6dd348db2d3f52772b59cd403c59d29/test/cases/gradient.css + prefix_test( + r#" + a { + background: linear-gradient(350.5deg, white, black), linear-gradient(-130deg, black, white), linear-gradient(45deg, black, white); + } + b { + background-image: linear-gradient(rgba(0,0,0,1), white), linear-gradient(white, black); + } + strong { + background: linear-gradient(to top, transparent, rgba(0, 0, 0, 0.8) 20px, #000 30px, #000) no-repeat; + } + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + .simple1 { + background: linear-gradient(black, white); + } + .simple2 { + background: linear-gradient(to left, black 0%, rgba(0, 0, 0, 0.5)50%, white 100%); + } + .simple3 { + background: linear-gradient(to left, black 50%, white 100%); + } + .simple4 { + background: linear-gradient(to right top, black, white); + } + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, 0.5), white); + } + .silent { + background: -webkit-linear-gradient(top left, black, white); + } + .radial { + background: radial-gradient(farthest-side at 0 50%, white, black); + } + .second { + background: red linear-gradient(red, blue); + background: url('logo.png'), linear-gradient(#fff, #000); + } + .px { + background: linear-gradient(black 0, white 100px); + } + .list { + list-style-image: linear-gradient(white, black); + } + .mask { + mask: linear-gradient(white, black); + } + .newline { + background-image: + linear-gradient( white, black ), + linear-gradient( black, white ); + } + .convert { + background: linear-gradient(0deg, white, black); + background: linear-gradient(90deg, white, black); + background: linear-gradient(180deg, white, black); + background: linear-gradient(270deg, white, black); + } + .grad { + background: linear-gradient(1grad, white, black); + } + .rad { + background: linear-gradient(1rad, white, black); + } + .turn { + background: linear-gradient(0.3turn, white, black); + } + .norm { + background: linear-gradient(-90deg, white, black); + } + .mask { + mask-image: radial-gradient(circle at 86% 86%, transparent 8px, black 8px); + } + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + .contain { + background: radial-gradient(contain at center, white, black); + } + .no-div { + background: linear-gradient(black); + } + .background-shorthand { + background: radial-gradient(#FFF, transparent) 0 0 / cover no-repeat #F0F; + } + .background-advanced { + background: radial-gradient(ellipse farthest-corner at 5px 15px, rgba(214, 168, 18, 0.7) 0%, rgba(255, 21, 177, 0.7) 50%, rgba(210, 7, 148, 0.7) 95%), + radial-gradient(#FFF, transparent), + url(path/to/image.jpg) 50%/cover; + } + .multiradial { + mask-image: radial-gradient(circle closest-corner at 100% 50%, #000, transparent); + } + .broken { + mask-image: radial-gradient(white, black); + } + .loop { + background-image: url("https://test.com/lol(test.png"), radial-gradient(yellow, black, yellow); + } + .unitless-zero { + background-image: linear-gradient(0, green, blue); + background: repeating-linear-gradient(0, blue, red 33.3%) + } + .zero-grad { + background: linear-gradient(0grad, green, blue); + background-image: repeating-linear-gradient(0grad, blue, red 33.3%) + } + .zero-rad { + background: linear-gradient(0rad, green, blue); + } + .zero-turn { + background: linear-gradient(0turn, green, blue); + } + "#, + indoc! { r#" + a { + background: -webkit-linear-gradient(99.5deg, #fff, #000), -webkit-linear-gradient(220deg, #000, #fff), -webkit-linear-gradient(45deg, #000, #fff); + background: -o-linear-gradient(99.5deg, #fff, #000), -o-linear-gradient(220deg, #000, #fff), -o-linear-gradient(45deg, #000, #fff); + background: linear-gradient(350.5deg, #fff, #000), linear-gradient(-130deg, #000, #fff), linear-gradient(45deg, #000, #fff); + } + + b { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)), -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + background-image: -webkit-linear-gradient(top, #000, #fff), -webkit-linear-gradient(top, #fff, #000); + background-image: -o-linear-gradient(top, #000, #fff), -o-linear-gradient(top, #fff, #000); + background-image: linear-gradient(#000, #fff), linear-gradient(#fff, #000); + } + + strong { + background: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: -o-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + } + + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + + .simple1 { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background: -webkit-linear-gradient(top, #000, #fff); + background: -o-linear-gradient(top, #000, #fff); + background: linear-gradient(#000, #fff); + } + + .simple2 { + background: -webkit-gradient(linear, 100% 0, 0 0, from(#000), color-stop(.5, rgba(0, 0, 0, .5)), to(#fff)); + background: -webkit-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: -o-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: linear-gradient(to left, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + } + + .simple3 { + background: -webkit-gradient(linear, 100% 0, 0 0, color-stop(.5, #000), to(#fff)); + background: -webkit-linear-gradient(right, #000 50%, #fff 100%); + background: -o-linear-gradient(right, #000 50%, #fff 100%); + background: linear-gradient(to left, #000 50%, #fff 100%); + } + + .simple4 { + background: -webkit-gradient(linear, 0 100%, 100% 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(bottom left, #000, #fff); + background: -o-linear-gradient(bottom left, #000, #fff); + background: linear-gradient(to top right, #000, #fff); + } + + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, .5), white); + } + + .silent { + background: -webkit-gradient(linear, 100% 100%, 0 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(top left, #000, #fff); + } + + .radial { + background: -webkit-radial-gradient(farthest-side at 0, #fff, #000); + background: -o-radial-gradient(farthest-side at 0, #fff, #000); + background: radial-gradient(farthest-side at 0, #fff, #000); + } + + .second { + background: red -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f)); + background: red -webkit-linear-gradient(top, red, #00f); + background: red -o-linear-gradient(top, red, #00f); + background: red linear-gradient(red, #00f); + background: url("logo.png"), linear-gradient(#fff, #000); + } + + .px { + background: -webkit-linear-gradient(top, #000 0, #fff 100px); + background: -o-linear-gradient(top, #000 0, #fff 100px); + background: linear-gradient(#000 0, #fff 100px); + } + + .list { + list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + list-style-image: -webkit-linear-gradient(top, #fff, #000); + list-style-image: -o-linear-gradient(top, #fff, #000); + list-style-image: linear-gradient(#fff, #000); + } + + .mask { + -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + -webkit-mask: -webkit-linear-gradient(top, #fff, #000); + -webkit-mask: -o-linear-gradient(top, #fff, #000); + mask: -o-linear-gradient(top, #fff, #000); + -webkit-mask: linear-gradient(#fff, #000); + mask: linear-gradient(#fff, #000); + } + + .newline { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)), -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background-image: -webkit-linear-gradient(top, #fff, #000), -webkit-linear-gradient(top, #000, #fff); + background-image: -o-linear-gradient(top, #fff, #000), -o-linear-gradient(top, #000, #fff); + background-image: linear-gradient(#fff, #000), linear-gradient(#000, #fff); + } + + .convert { + background: -webkit-gradient(linear, 0 100%, 0 0, from(#fff), to(#000)); + background: -webkit-linear-gradient(90deg, #fff, #000); + background: -o-linear-gradient(90deg, #fff, #000); + background: linear-gradient(0deg, #fff, #000); + background: linear-gradient(90deg, #fff, #000); + background: linear-gradient(#fff, #000); + background: linear-gradient(270deg, #fff, #000); + } + + .grad { + background: -webkit-linear-gradient(89.1deg, #fff, #000); + background: -o-linear-gradient(89.1deg, #fff, #000); + background: linear-gradient(1grad, #fff, #000); + } + + .rad { + background: -webkit-linear-gradient(32.704deg, #fff, #000); + background: -o-linear-gradient(32.704deg, #fff, #000); + background: linear-gradient(57.2958deg, #fff, #000); + } + + .turn { + background: -webkit-linear-gradient(342deg, #fff, #000); + background: -o-linear-gradient(342deg, #fff, #000); + background: linear-gradient(.3turn, #fff, #000); + } + + .norm { + background: -webkit-linear-gradient(#fff, #000); + background: -o-linear-gradient(#fff, #000); + background: linear-gradient(-90deg, #fff, #000); + } + + .mask { + -webkit-mask-image: -webkit-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + } + + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + + .contain { + background: radial-gradient(contain at center, white, black); + } + + .no-div { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000)); + background: -webkit-linear-gradient(top, #000); + background: -o-linear-gradient(top, #000); + background: linear-gradient(#000); + } + + .background-shorthand { + background: #f0f -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f -o-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + } + + .background-advanced { + background: url("path/to/image.jpg") 50% / cover; + background: -webkit-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: -o-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -o-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + } + + .multiradial { + -webkit-mask-image: -webkit-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + } + + .broken { + -webkit-mask-image: -webkit-radial-gradient(#fff, #000); + -webkit-mask-image: -o-radial-gradient(#fff, #000); + mask-image: -o-radial-gradient(#fff, #000); + -webkit-mask-image: radial-gradient(#fff, #000); + mask-image: radial-gradient(#fff, #000); + } + + .loop { + background-image: url("https://test.com/lol(test.png"); + background-image: url("https://test.com/lol(test.png"), -webkit-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), -o-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), radial-gradient(#ff0, #000, #ff0); + } + + .unitless-zero { + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background-image: -webkit-linear-gradient(90deg, green, #00f); + background-image: -o-linear-gradient(90deg, green, #00f); + background-image: linear-gradient(0deg, green, #00f); + background: repeating-linear-gradient(0deg, #00f, red 33.3%); + } + + .zero-grad { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0grad, green, #00f); + background-image: repeating-linear-gradient(0grad, #00f, red 33.3%); + } + + .zero-rad, .zero-turn { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0deg, green, #00f); + } + "#}, + Browsers { + chrome: Some(25 << 16), + opera: Some(12 << 16), + android: Some(2 << 16 | 3 << 8), + ..Browsers::default() + }, + ); } #[test] @@ -13051,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 @@ -13073,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}"); @@ -13171,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; @@ -13215,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}"); @@ -13803,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] @@ -14905,6 +16216,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -15272,26 +16604,47 @@ mod tests { ..Browsers::default() }, ); - } - #[test] - fn test_text_shadow() { - minify_test( - ".foo { text-shadow: 1px 1px 2px yellow; }", - ".foo{text-shadow:1px 1px 2px #ff0}", - ); - minify_test( - ".foo { text-shadow: 1px 1px 2px 3px yellow; }", - ".foo{text-shadow:1px 1px 2px 3px #ff0}", - ); - minify_test( - ".foo { text-shadow: 1px 1px 0 yellow; }", - ".foo{text-shadow:1px 1px #ff0}", - ); - minify_test( - ".foo { text-shadow: 1px 1px yellow; }", - ".foo{text-shadow:1px 1px #ff0}", - ); + 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] + fn test_text_shadow() { + minify_test( + ".foo { text-shadow: 1px 1px 2px yellow; }", + ".foo{text-shadow:1px 1px 2px #ff0}", + ); + minify_test( + ".foo { text-shadow: 1px 1px 2px 3px yellow; }", + ".foo{text-shadow:1px 1px 2px 3px #ff0}", + ); + minify_test( + ".foo { text-shadow: 1px 1px 0 yellow; }", + ".foo{text-shadow:1px 1px #ff0}", + ); + minify_test( + ".foo { text-shadow: 1px 1px yellow; }", + ".foo{text-shadow:1px 1px #ff0}", + ); minify_test( ".foo { text-shadow: 1px 1px yellow, 2px 3px red; }", ".foo{text-shadow:1px 1px #ff0,2px 3px red}", @@ -15359,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] @@ -16069,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] @@ -16101,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( @@ -16142,7 +17548,7 @@ mod tests { "#, indoc! {r#" .foo { - list-style: \"★\" url("ellipse.png"); + list-style: url("ellipse.png") \"★\"; list-style-image: var(--img); } "#}, @@ -16153,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)); } @@ -16168,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 { @@ -16196,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] @@ -16396,6 +17850,7 @@ mod tests { minify_test(".foo { color: hsl(100deg, 100%, 50%) }", ".foo{color:#5f0}"); minify_test(".foo { color: hsl(100, 100%, 50%) }", ".foo{color:#5f0}"); minify_test(".foo { color: hsl(100 100% 50%) }", ".foo{color:#5f0}"); + minify_test(".foo { color: hsl(100 100 50) }", ".foo{color:#5f0}"); minify_test(".foo { color: hsl(100, 100%, 50%, .8) }", ".foo{color:#5f0c}"); minify_test(".foo { color: hsl(100 100% 50% / .8) }", ".foo{color:#5f0c}"); minify_test(".foo { color: hsla(100, 100%, 50%, .8) }", ".foo{color:#5f0c}"); @@ -16407,12 +17862,21 @@ mod tests { minify_test(".foo { color: hwb(194 0% 0% / 50%) }", ".foo{color:#00c4ff80}"); minify_test(".foo { color: hwb(194 0% 50%) }", ".foo{color:#006280}"); minify_test(".foo { color: hwb(194 50% 0%) }", ".foo{color:#80e1ff}"); + minify_test(".foo { color: hwb(194 50 0) }", ".foo{color:#80e1ff}"); minify_test(".foo { color: hwb(194 50% 50%) }", ".foo{color:gray}"); // minify_test(".foo { color: ActiveText }", ".foo{color:ActiveTet}"); minify_test( ".foo { color: lab(29.2345% 39.3825 20.0664); }", ".foo{color:lab(29.2345% 39.3825 20.0664)}", ); + minify_test( + ".foo { color: lab(29.2345 39.3825 20.0664); }", + ".foo{color:lab(29.2345% 39.3825 20.0664)}", + ); + minify_test( + ".foo { color: lab(29.2345% 39.3825% 20.0664%); }", + ".foo{color:lab(29.2345% 49.2281 25.083)}", + ); minify_test( ".foo { color: lab(29.2345% 39.3825 20.0664 / 100%); }", ".foo{color:lab(29.2345% 39.3825 20.0664)}", @@ -16425,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)}", @@ -16445,10 +17917,26 @@ mod tests { ".foo { color: oklab(40.101% 0.1147 0.0453); }", ".foo{color:oklab(40.101% .1147 .0453)}", ); + minify_test( + ".foo { color: oklab(.40101 0.1147 0.0453); }", + ".foo{color:oklab(40.101% .1147 .0453)}", + ); + minify_test( + ".foo { color: oklab(40.101% 0.1147% 0.0453%); }", + ".foo{color:oklab(40.101% .0004588 .0001812)}", + ); minify_test( ".foo { color: oklch(40.101% 0.12332 21.555); }", ".foo{color:oklch(40.101% .12332 21.555)}", ); + minify_test( + ".foo { color: oklch(.40101 0.12332 21.555); }", + ".foo{color:oklch(40.101% .12332 21.555)}", + ); + minify_test( + ".foo { color: oklch(40.101% 0.12332% 21.555); }", + ".foo{color:oklch(40.101% .00049328 21.555)}", + ); minify_test( ".foo { color: oklch(40.101% 0.12332 .5turn); }", ".foo{color:oklch(40.101% .12332 180)}", @@ -16991,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 { @@ -17093,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 { @@ -17270,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))", @@ -17286,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))}", @@ -17405,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). @@ -17543,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)", ); @@ -17681,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)", ); @@ -18144,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), @@ -18152,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), @@ -18171,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(). @@ -19040,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}", ); } } @@ -19149,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: @@ -20521,7 +22094,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -20669,40 +22241,248 @@ 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( + + test( r#" - .foo { - grid-template: "head head" - "nav main" 1fr - "foot ...."; + .test-miss-areas-4 { + grid: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}", - ); + 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 + "foot ...."; + } + "#, + ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}", + ); minify_test( r#" .foo { @@ -21348,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)); }", @@ -21368,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#" @@ -21397,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 { @@ -21420,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 { @@ -21450,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 { @@ -21618,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 { @@ -21784,11 +23723,44 @@ mod tests { prefix_test( r#" - @keyframes foo { - from { - --custom: lab(40% 56.6 39); - } - + @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); } @@ -21836,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 { @@ -21974,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), @@ -22195,7 +24225,7 @@ mod tests { grid-auto-flow: column; } - @media (min-width: 1024px) { + @media not (max-width: 1024px) { .foo { max-inline-size: 1024px; } @@ -23062,13 +25092,13 @@ mod tests { } "#, indoc! {r#" - .foo { - color: red; - } - .foo .bar { color: #00f; } + + .foo { + color: red; + } "#}, ); @@ -23082,12 +25112,16 @@ mod tests { "#, indoc! {r#" article { - color: red; + color: green; } article { color: #00f; } + + article { + color: red; + } "#}, ); @@ -23196,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; } + } "#}, ); @@ -23372,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] @@ -23466,6 +25575,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23512,6 +25622,7 @@ mod tests { // custom_idents: false, ..Default::default() }, + false, ); css_modules_test( @@ -23557,9 +25668,9 @@ mod tests { custom_idents: false, ..Default::default() }, + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" body { @@ -23601,9 +25712,9 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -23639,9 +25750,9 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -23679,6 +25790,7 @@ mod tests { grid: false, ..Default::default() }, + false, ); css_modules_test( @@ -23695,6 +25807,7 @@ mod tests { map! {}, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23729,6 +25842,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); // :global(:local(.hi)) { @@ -23761,6 +25875,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23790,6 +25905,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23827,6 +25943,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23846,6 +25963,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23865,6 +25983,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23884,6 +26003,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23903,6 +26023,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23933,6 +26054,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23954,6 +26076,7 @@ mod tests { pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), ..Default::default() }, + false, ); let stylesheet = StyleSheet::parse( @@ -24015,6 +26138,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24084,6 +26208,7 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); css_modules_test( @@ -24103,6 +26228,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" @@ -24120,6 +26246,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" @@ -24137,6 +26264,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" @@ -24157,6 +26285,7 @@ mod tests { animation: false, ..Default::default() }, + false, ); css_modules_test( r#" @@ -24175,69 +26304,347 @@ mod tests { }, HashMap::new(), crate::css_modules::Config { ..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; - } - - .foo:active { - color: yellow; + css_modules_test( + r#" + .test { + composes: foo bar from "foo.css"; + background: white; } - - .foo:focus-visible { - color: purple; + "#, + 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, + ); - let expected = indoc! { r#" - .foo.is-hovered { - color: red; + css_modules_test( + r#" + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } - + "#, + 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, + ); + + css_modules_test( + r#" + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } + } + "#, + 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, + ); + + 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, + ); + + 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, + ); + + 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, + ); + + 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, + ); + + 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, + ); + + 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, + ); + + css_modules_test( + ":nth-child(1 of .foo) {width: 20px}", + ":nth-child(1 of .EgL3uq_foo){width:20px}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ":nth-last-child(1 of .foo) {width: 20px}", + ":nth-last-child(1 of .EgL3uq_foo){width:20px}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + } + + // Stable hashes between project roots. + fn test_project_root(project_root: &str, filename: &str, hash: &str) { + let stylesheet = StyleSheet::parse( + r#" + .foo { + background: red; + } + "#, + ParserOptions { + filename: filename.into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) + .unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + 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"); + + let mut stylesheet = StyleSheet::parse( + r#" + .foo { + color: red; + .bar { + color: green; + } + composes: test from "foo.css"; + } + "#, + ParserOptions { + filename: "test.css".into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..Default::default() + }) + .unwrap(); + assert_eq!( + res.code, + indoc! {r#" + .EgL3uq_foo { + color: red; + } + + .EgL3uq_foo .EgL3uq_bar { + color: green; + } + + + "#} + ); + assert_eq!( + res.exports.unwrap(), + map! { + "foo" => "EgL3uq_foo" "test" from "foo.css", + "bar" => "EgL3uq_bar" + } + ); + } + + #[test] + 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; } @@ -24498,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}"); @@ -24783,12 +27192,33 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }", indoc! { r#" .foo { -webkit-mask-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)); - -webkit-mask-image: -webkit-linear-gradient(#ff0f0e, #7773ff); + -webkit-mask-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff); -webkit-mask-image: linear-gradient(#ff0f0e, #7773ff); mask-image: linear-gradient(#ff0f0e, #7773ff); -webkit-mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); @@ -24850,7 +27280,7 @@ mod tests { indoc! { r#" .foo { -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 40px 20px; - -webkit-mask: -webkit-linear-gradient(#ff0f0e, #7773ff) 40px 20px; + -webkit-mask: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 40px 20px; -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 40px 20px; mask: linear-gradient(#ff0f0e, #7773ff) 40px 20px; -webkit-mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px; @@ -24899,9 +27329,31 @@ mod tests { ); prefix_test( - ".foo { mask: url(masks.svg#star) luminance }", + r#" + @supports (color: lab(0% 0 0)) { + .foo { + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#, indoc! { r#" - .foo { + @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; @@ -25283,6 +27735,21 @@ mod tests { ..Browsers::default() }, ); + + let property = + Property::parse_string("text-rendering".into(), "geometricPrecision", ParserOptions::default()).unwrap(); + assert_eq!( + property, + Property::TextRendering(svg::TextRendering::GeometricPrecision) + ); + let property = + Property::parse_string("shape-rendering".into(), "geometricPrecision", ParserOptions::default()).unwrap(); + assert_eq!( + property, + Property::ShapeRendering(svg::ShapeRendering::GeometricPrecision) + ); + let property = Property::parse_string("color-interpolation".into(), "sRGB", ParserOptions::default()).unwrap(); + assert_eq!(property, Property::ColorInterpolation(svg::ColorInterpolation::SRGB)); } #[test] @@ -25454,6 +27921,82 @@ mod tests { ); } + #[test] + fn test_mix_blend_mode() { + minify_test( + ".foo { mix-blend-mode: normal }", + ".foo{mix-blend-mode:normal}", + ); + minify_test( + ".foo { mix-blend-mode: multiply }", + ".foo{mix-blend-mode:multiply}", + ); + minify_test( + ".foo { mix-blend-mode: screen }", + ".foo{mix-blend-mode:screen}", + ); + minify_test( + ".foo { mix-blend-mode: overlay }", + ".foo{mix-blend-mode:overlay}", + ); + minify_test( + ".foo { mix-blend-mode: darken }", + ".foo{mix-blend-mode:darken}", + ); + minify_test( + ".foo { mix-blend-mode: lighten }", + ".foo{mix-blend-mode:lighten}", + ); + minify_test( + ".foo { mix-blend-mode: color-dodge }", + ".foo{mix-blend-mode:color-dodge}", + ); + minify_test( + ".foo { mix-blend-mode: color-burn }", + ".foo{mix-blend-mode:color-burn}", + ); + minify_test( + ".foo { mix-blend-mode: hard-light }", + ".foo{mix-blend-mode:hard-light}", + ); + minify_test( + ".foo { mix-blend-mode: soft-light }", + ".foo{mix-blend-mode:soft-light}", + ); + minify_test( + ".foo { mix-blend-mode: difference }", + ".foo{mix-blend-mode:difference}", + ); + minify_test( + ".foo { mix-blend-mode: exclusion }", + ".foo{mix-blend-mode:exclusion}", + ); + minify_test( + ".foo { mix-blend-mode: hue }", + ".foo{mix-blend-mode:hue}", + ); + minify_test( + ".foo { mix-blend-mode: saturation }", + ".foo{mix-blend-mode:saturation}", + ); + minify_test( + ".foo { mix-blend-mode: color }", + ".foo{mix-blend-mode:color}", + ); + minify_test( + ".foo { mix-blend-mode: luminosity }", + ".foo{mix-blend-mode:luminosity}", + ); + minify_test( + ".foo { mix-blend-mode: plus-darker }", + ".foo{mix-blend-mode:plus-darker}", + ); + minify_test( + ".foo { mix-blend-mode: plus-lighter }", + ".foo{mix-blend-mode:plus-lighter}", + ); + } + #[test] fn test_viewport() { minify_test( @@ -25544,7 +28087,7 @@ mod tests { } } "#, - ".foo{@scope(.bar){&{color:#ff0}}}", + ".foo{@scope(.bar){color:#ff0}}", ); nesting_test( r#" @@ -25556,9 +28099,7 @@ mod tests { "#, indoc! {r#" @scope (.bar) { - :scope { - color: #ff0; - } + color: #ff0; } "#}, ); @@ -25948,7 +28489,7 @@ mod tests { } "#, indoc! {r#" - @media screen and ((prefers-color-scheme: dark) or (not (width >= 300px))) { + @media screen and ((prefers-color-scheme: dark) or ((width < 300px))) { .foo { order: 6; } @@ -26698,6 +29239,17 @@ mod tests { "@property --property-name{syntax:\"\";inherits:true;initial-value:25px}", ); + minify_test( + r#" + @property --property-name { + syntax: ''; + inherits: true; + initial-value: "hi"; + } + "#, + "@property --property-name{syntax:\"\";inherits:true;initial-value:\"hi\"}", + ); + error_test( r#" @property --property-name { @@ -26893,6 +29445,44 @@ mod tests { "#, "@property --property-name{syntax:\"\";inherits:true;initial-value:#00f}.foo{color:var(--property-name)}", ); + + test( + r#" + @media (width < 800px) { + @property --property-name { + syntax: '*'; + inherits: false; + } + } + "#, + indoc! {r#" + @media (width < 800px) { + @property --property-name { + syntax: "*"; + inherits: false + } + } + "#}, + ); + + test( + r#" + @layer foo { + @property --property-name { + syntax: '*'; + inherits: false; + } + } + "#, + indoc! {r#" + @layer foo { + @property --property-name { + syntax: "*"; + inherits: false + } + } + "#}, + ); } #[test] @@ -26980,6 +29570,52 @@ mod tests { ); } + #[test] + #[cfg(feature = "sourcemap")] + fn test_source_maps_with_license_comments() { + let source = r#"/*! a single line comment */ + /*! + a comment + containing + multiple + lines + */ + .a { + display: flex; + } + + .b { + display: hidden; + } + "#; + + let mut sm = parcel_sourcemap::SourceMap::new("/"); + let source_index = sm.add_source("input.css"); + sm.set_source_content(source_index as usize, source).unwrap(); + + let mut stylesheet = StyleSheet::parse( + &source, + ParserOptions { + source_index, + ..Default::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + stylesheet + .to_css(PrinterOptions { + source_map: Some(&mut sm), + minify: true, + ..PrinterOptions::default() + }) + .unwrap(); + let map = sm.to_json(None).unwrap(); + assert_eq!( + map, + r#"{"version":3,"sourceRoot":null,"mappings":";;;;;;;AAOI,gBAIA","sources":["input.css"],"sourcesContent":["/*! a single line comment */\n /*!\n a comment\n containing\n multiple\n lines\n */\n .a {\n display: flex;\n }\n\n .b {\n display: hidden;\n }\n "],"names":[]}"# + ); + } + #[test] fn test_error_recovery() { use std::sync::{Arc, RwLock}; @@ -27008,6 +29644,14 @@ mod tests { color: red; } } + + input:placeholder { + color: red; + } + + input::hover { + color: red; + } "#, indoc! { r#" .foo { @@ -27023,6 +29667,14 @@ mod tests { color: red; } } + + input:placeholder { + color: red; + } + + input::hover { + color: red; + } "#}, ParserOptions { filename: "test.css".into(), @@ -27060,6 +29712,22 @@ mod tests { column: 9 }) }, + Error { + kind: ParserError::SelectorError(SelectorError::UnsupportedPseudoClass("placeholder".into())), + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 24, + column: 13, + }), + }, + Error { + kind: ParserError::SelectorError(SelectorError::UnsupportedPseudoElement("hover".into())), + loc: Some(ErrorLocation { + filename: "test.css".into(), + line: 28, + column: 13, + }), + }, ] ) } @@ -27078,6 +29746,18 @@ mod tests { #[test] fn test_container_queries() { + // name only (no condition) - new syntax + minify_test( + r#" + @container foo { + .inner { + background: green; + } + } + "#, + "@container foo{.inner{background:green}}", + ); + // with name minify_test( r#" @@ -27388,6 +30068,76 @@ mod tests { "#, "@container style(--my-prop:foo - bar ()){.foo{color:red}}", ); + minify_test( + r#" + @container style(--test) { + .foo { + color: red; + } + } + "#, + "@container style(--test){.foo{color:red}}", + ); + minify_test( + r#" + @container style(width) { + .foo { + color: red; + } + } + "#, + "@container style(width){.foo{color:red}}", + ); + minify_test( + r#" + @container scroll-state(scrollable: top) { + .foo { + color: red; + } + } + "#, + "@container scroll-state(scrollable:top){.foo{color:red}}", + ); + minify_test( + r#" + @container scroll-state((stuck: top) and (stuck: left)) { + .foo { + color: red; + } + } + "#, + "@container scroll-state((stuck:top) and (stuck:left)){.foo{color:red}}", + ); + minify_test( + r#" + @container scroll-state(not ((scrollable: bottom) and (scrollable: right))) { + .foo { + color: red; + } + } + "#, + "@container scroll-state(not ((scrollable:bottom) and (scrollable:right))){.foo{color:red}}", + ); + minify_test( + r#" + @container (scroll-state(scrollable: inline-end)) { + .foo { + color: red; + } + } + "#, + "@container scroll-state(scrollable:inline-end){.foo{color:red}}", + ); + minify_test( + r#" + @container not scroll-state(scrollable: top) { + .foo { + color: red; + } + } + "#, + "@container not scroll-state(scrollable:top){.foo{color:red}}", + ); // Disallow 'none', 'not', 'and', 'or' as a `` // https://github.com/w3c/csswg-drafts/issues/7203#issuecomment-1144257312 @@ -27428,11 +30178,41 @@ mod tests { error_test("@container (inline-size <= foo) {}", ParserError::InvalidMediaQuery); error_test("@container (orientation <= 10px) {}", ParserError::InvalidMediaQuery); - error_test("@container style(width) {}", ParserError::EndOfInput); error_test( "@container style(style(--foo: bar)) {}", ParserError::UnexpectedToken(crate::properties::custom::Token::Function("style".into())), ); + error_test( + "@container scroll-state(scroll-state(scrollable: top)) {}", + ParserError::InvalidMediaQuery, + ); + error_test( + "@container unknown(foo) {}", + ParserError::UnexpectedToken(crate::properties::custom::Token::Function("unknown".into())), + ); + + // empty container (no name and no condition) should error + error_test("@container {}", ParserError::EndOfInput); + + // empty brackets should return a clearer error message + error_test("@container () {}", ParserError::EmptyBracketInCondition); + + // invalid condition after a name should error + error_test("@container foo () {}", ParserError::EmptyBracketInCondition); + error_test( + "@container foo bar {}", + ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("bar".into())), + ); + + error_recovery_test("@container unknown(foo) {}"); + } + + #[test] + fn test_css_modules_value_rule() { + css_modules_error_test( + "@value compact: (max-width: 37.4375em);", + ParserError::DeprecatedCssModulesValueRule, + ); } #[test] @@ -27446,11 +30226,12 @@ mod tests { color: red; } }"#, - indoc! {r#" - @foo test { - div { color: red; } - } - "#}, + indoc! { r#"@foo test { + div { + color: red; + } + } + "#}, ); minify_test( r#"@foo test { @@ -27610,6 +30391,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + color: env(--brand-color, color(display-p3 0 1 0)); + } + } + "#, + indoc! {r#" + @supports (color: color(display-p3 0 0 0)) { + .foo { + color: env(--brand-color, color(display-p3 0 1 0)); + } + } + "#}, + Browsers { + safari: Some(15 << 16), + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + css_modules_test( r#" @media (max-width: env(--branding-small)) { @@ -27635,6 +30438,7 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); } @@ -27714,14 +30518,20 @@ mod tests { minify_test(".foo { color-scheme: dark light; }", ".foo{color-scheme:light dark}"); minify_test(".foo { color-scheme: only light; }", ".foo{color-scheme:light only}"); minify_test(".foo { color-scheme: only dark; }", ".foo{color-scheme:dark only}"); + minify_test(".foo { color-scheme: inherit; }", ".foo{color-scheme:inherit}"); + minify_test(":root { color-scheme: unset; }", ":root{color-scheme:unset}"); + minify_test(".foo { color-scheme: unknow; }", ".foo{color-scheme:unknow}"); + minify_test(".foo { color-scheme: only; }", ".foo{color-scheme:only}"); + minify_test(".foo { color-scheme: dark foo; }", ".foo{color-scheme:dark foo}"); + minify_test(".foo { color-scheme: normal dark; }", ".foo{color-scheme:normal dark}"); minify_test( ".foo { color-scheme: dark light only; }", ".foo{color-scheme:light dark only}", ); - minify_test(".foo { color-scheme: foo bar light; }", ".foo{color-scheme:light}"); + minify_test(".foo { color-scheme: foo bar light; }", ".foo{color-scheme:foo bar light}"); minify_test( ".foo { color-scheme: only foo dark bar; }", - ".foo{color-scheme:dark only}", + ".foo{color-scheme:only foo dark bar}", ); prefix_test( ".foo { color-scheme: dark; }", @@ -27841,6 +30651,25 @@ mod tests { ..Browsers::default() }, ); + prefix_test( + r#" + .foo { + box-shadow: + oklch(100% 0 0deg / 50%) 0 0.63rem 0.94rem -0.19rem, + currentColor 0 0.44rem 0.8rem -0.58rem; + } + "#, + indoc! { r#" + .foo { + box-shadow: 0 .63rem .94rem -.19rem color(display-p3 1 1 1 / .5), 0 .44rem .8rem -.58rem; + box-shadow: 0 .63rem .94rem -.19rem lab(100% 0 0 / .5), 0 .44rem .8rem -.58rem; + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Browsers::default() + }, + ); prefix_test( ".foo { color: light-dark(var(--light), var(--dark)); }", @@ -27903,6 +30732,58 @@ mod tests { ..Browsers::default() }, ); + nesting_test_with_targets( + r#" + .foo { color-scheme: light; } + .bar { color: light-dark(red, green); } + "#, + indoc! {r#" + .foo { + color-scheme: light; + } + + .bar { + color: light-dark(red, green); + } + "#}, + Targets { + browsers: Some(Browsers { + safari: Some(13 << 16), + ..Browsers::default() + }), + include: Features::empty(), + exclude: Features::LightDark, + }, + ); + } + + #[test] + fn test_print_color_adjust() { + prefix_test( + ".foo { print-color-adjust: exact; }", + indoc! { r#" + .foo { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + "#}, + Browsers { + chrome: Some(135 << 16), + ..Browsers::default() + }, + ); + prefix_test( + ".foo { print-color-adjust: exact; }", + indoc! { r#" + .foo { + print-color-adjust: exact; + } + "#}, + Browsers { + chrome: Some(137 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -27935,5 +30816,187 @@ mod tests { ".foo { all: unset; background: var(--foo); }", ".foo{all:unset;background:var(--foo)}", ); + minify_test( + ".foo {--bar:currentcolor; --foo:1.1em; all:unset}", + ".foo{--bar:currentcolor;--foo:1.1em;all:unset}", + ); + } + + #[test] + fn test_view_transition() { + minify_test( + "@view-transition { navigation: auto }", + "@view-transition{navigation:auto}", + ); + minify_test( + "@view-transition { navigation: auto; types: none; }", + "@view-transition{navigation:auto;types:none}", + ); + minify_test( + "@view-transition { navigation: auto; types: foo bar; }", + "@view-transition{navigation:auto;types:foo bar}", + ); + minify_test( + "@layer { @view-transition { navigation: auto; types: foo bar; } }", + "@layer{@view-transition{navigation:auto;types:foo bar}}", + ); + } + + #[test] + fn test_skip_generating_unnecessary_fallbacks() { + prefix_test( + r#" + @supports (color: lab(0% 0 0)) and (color: color(display-p3 0 0 0)) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) and (color: color(display-p3 0 0 0)) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (color: lab(40% 56.6 39)) { + .foo { + color: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (color: lab(40% 56.6 39)) { + .foo { + color: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (background-color: lab(40% 56.6 39)) { + .foo { + background-color: lab(40% 56.6 39); + } + } + "#, + indoc! {r#" + @supports (background-color: lab(40% 56.6 39)) { + .foo { + background-color: lab(40% 56.6 39); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (color: light-dark(#f00, #00f)) { + .foo { + color: light-dark(#ff0, #0ff); + } + } + "#, + indoc! {r#" + @supports (color: light-dark(#f00, #00f)) { + .foo { + color: light-dark(#ff0, #0ff); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + // NOTE: fallback for lab is not necessary + prefix_test( + r#" + @supports (color: lab(0% 0 0)) and (not (color: color(display-p3 0 0 0))) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) and (not (color: color(display-p3 0 0 0))) { + .foo { + color: #b32323; + color: lab(40% 56.6 39); + } + + .bar { + color: #b32323; + color: color(display-p3 .643308 .192455 .167712); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) or (color: color(display-p3 0 0 0)) { + .foo { + color: lab(40% 56.6 39); + } + + .bar { + color: color(display-p3 .643308 .192455 .167712); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) or (color: color(display-p3 0 0 0)) { + .foo { + color: #b32323; + color: lab(40% 56.6 39); + } + + .bar { + color: #b32323; + color: color(display-p3 .643308 .192455 .167712); + } + } + "#}, + Browsers { + chrome: Some(4 << 16), + ..Browsers::default() + }, + ); } } diff --git a/src/macros.rs b/src/macros.rs index 8019c58cb..a50d54f5a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -189,7 +189,7 @@ macro_rules! shorthand_property_bitflags { crate::macros::shorthand_property_bitflags!($name, [$($all),*] $($rest),* ; $last_index + 1; $($var = $index)* $cur = $last_index + 1); }; ($name:ident, [$($all:ident),*] $cur:ident; $last_index:expr ; $($var:ident = $index:expr)+) => { - paste::paste! { + pastey::paste! { crate::macros::property_bitflags! { #[derive(Default, Debug)] struct [<$name Property>]: u8 { @@ -216,7 +216,7 @@ macro_rules! shorthand_handler { $( pub $key: Option<$type>, )* - flushed_properties: paste::paste!([<$shorthand Property>]), + flushed_properties: pastey::paste!([<$shorthand Property>]), has_any: bool } @@ -250,7 +250,7 @@ macro_rules! shorthand_handler { let mut unparsed = val.clone(); context.add_unparsed_fallbacks(&mut unparsed); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::try_from(&unparsed.property_id).unwrap()); }; dest.push(Property::Unparsed(unparsed)); @@ -263,7 +263,7 @@ macro_rules! shorthand_handler { fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { self.flush(dest, context); - self.flushed_properties = paste::paste!([<$shorthand Property>]::empty()); + self.flushed_properties = pastey::paste!([<$shorthand Property>]::empty()); } } @@ -289,7 +289,7 @@ macro_rules! shorthand_handler { }; $( - if $shorthand_fallback && !self.flushed_properties.intersects(paste::paste!([<$shorthand Property>]::$shorthand)) { + if $shorthand_fallback && !self.flushed_properties.intersects(pastey::paste!([<$shorthand Property>]::$shorthand)) { let fallbacks = shorthand.get_fallbacks(context.targets); for fallback in fallbacks { dest.push(Property::$shorthand(fallback)); @@ -298,7 +298,7 @@ macro_rules! shorthand_handler { )? dest.push(Property::$shorthand(shorthand)); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::$shorthand); }; } else { @@ -306,7 +306,7 @@ macro_rules! shorthand_handler { #[allow(unused_mut)] if let Some(mut val) = $key { $( - if $fallback && !self.flushed_properties.intersects(paste::paste!([<$shorthand Property>]::$prop)) { + if $fallback && !self.flushed_properties.intersects(pastey::paste!([<$shorthand Property>]::$prop)) { let fallbacks = val.get_fallbacks(context.targets); for fallback in fallbacks { dest.push(Property::$prop(fallback)); @@ -315,7 +315,7 @@ macro_rules! shorthand_handler { )? dest.push(Property::$prop(val)); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::$prop); }; } @@ -390,7 +390,7 @@ macro_rules! impl_shorthand { impl<'i> Shorthand<'i> for $t { #[allow(unused_variables)] fn from_longhands(decls: &DeclarationBlock<'i>, vendor_prefix: crate::vendor_prefix::VendorPrefix) -> Option<(Self, bool)> { - use paste::paste; + use pastey::paste; $( $( diff --git a/src/media_query.rs b/src/media_query.rs index 63c0b2364..2f0ee9de6 100644 --- a/src/media_query.rs +++ b/src/media_query.rs @@ -3,14 +3,14 @@ use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, use crate::macros::enum_property; use crate::parser::starts_with_ignore_ascii_case; use crate::printer::Printer; -use crate::properties::custom::EnvironmentVariable; +use crate::properties::custom::{EnvironmentVariable, TokenList}; #[cfg(feature = "visitor")] -use crate::rules::container::ContainerSizeFeatureId; +use crate::rules::container::{ContainerSizeFeatureId, ScrollStateFeatureId}; use crate::rules::custom_media::CustomMediaRule; use crate::rules::Location; use crate::stylesheet::ParserOptions; use crate::targets::{should_compile, Targets}; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::{DashedIdent, Ident}; use crate::values::number::{CSSInteger, CSSNumber}; use crate::values::string::CowArcStr; @@ -51,10 +51,17 @@ impl<'i> MediaList<'i> { } /// Parse a media query list from CSS. - pub fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + pub fn parse<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { let mut media_queries = vec![]; + if input.is_exhausted() { + return Ok(MediaList { media_queries }); + } + loop { - match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(i)) { + match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse_with_options(i, options)) { Ok(mq) => { media_queries.push(mq); } @@ -269,8 +276,11 @@ pub struct MediaQuery<'i> { pub condition: Option<'i>>, } -impl<'i> Parse<'i> for MediaQuery<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { +impl<'i> ParseWithOptions<'i> for MediaQuery<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { let (qualifier, explicit_media_type) = input .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> { let qualifier = input.try_parse(Qualifier::parse).ok(); @@ -280,9 +290,17 @@ impl<'i> Parse<'i> for MediaQuery<'i> { .unwrap_or_default(); let condition = if explicit_media_type.is_none() { - Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::ALLOW_OR)?) + Some(MediaCondition::parse_with_flags( + input, + QueryConditionFlags::ALLOW_OR, + options, + )?) } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { - Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::empty())?) + Some(MediaCondition::parse_with_flags( + input, + QueryConditionFlags::empty(), + options, + )?) } else { None }; @@ -476,8 +494,8 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> { condition, }), MediaQueryOrRaw::Raw { raw } => { - let res = - MediaQuery::parse_string(raw.as_ref()).map_err(|_| serde::de::Error::custom("Could not parse value"))?; + let res = MediaQuery::parse_string_with_options(raw.as_ref(), ParserOptions::default()) + .map_err(|_| serde::de::Error::custom("Could not parse value"))?; Ok(res.into_owned()) } } @@ -520,14 +538,30 @@ pub enum MediaCondition<'i> { /// The conditions for the operator. conditions: Vec<'i>>, }, + /// Unknown tokens. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + Unknown(TokenList<'i>), } /// A trait for conditions such as media queries and container queries. pub(crate) trait QueryCondition<'i>: Sized { - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>>; + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>>; fn create_negation(condition: Box) -> Self; fn create_operation(operator: Operator, conditions: Vec) -> Self; - fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + fn parse_style_query<'t>( + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + Err(input.new_error_for_next_token()) + } + + fn parse_scroll_state_query<'t>( + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { Err(input.new_error_for_next_token()) } @@ -536,8 +570,11 @@ pub(crate) trait QueryCondition<'i>: Sized { impl<'i> QueryCondition<'i> for MediaCondition<'i> { #[inline] - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - let feature = MediaFeature::parse(input)?; + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + let feature = MediaFeature::parse_with_options(input, options)?; Ok(Self::Feature(feature)) } @@ -556,6 +593,7 @@ impl<'i> QueryCondition<'i> for MediaCondition<'i> { MediaCondition::Not(_) => true, MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator, MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets), + MediaCondition::Unknown(_) => false, } } } @@ -568,6 +606,8 @@ bitflags! { const ALLOW_OR = 1 << 0; /// Whether to allow style container queries. const ALLOW_STYLE = 1 << 1; + /// Whether to allow scroll state container queries. + const ALLOW_SCROLL_STATE = 1 << 2; } } @@ -576,8 +616,18 @@ impl<'i> MediaCondition<'i> { fn parse_with_flags<'t>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result<'i, ParserError<'i>>> { - parse_query_condition(input, flags) + input + .try_parse(|input| parse_query_condition(input, flags, options)) + .or_else(|e| { + if options.error_recovery { + options.warn(e); + Ok(MediaCondition::Unknown(TokenList::parse(input, options, 0)?)) + } else { + Err(e) + } + }) } fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix { @@ -633,9 +683,12 @@ impl<'i> MediaCondition<'i> { } } -impl<'i> Parse<'i> for MediaCondition<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR) +impl<'i> ParseWithOptions<'i> for MediaCondition<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR, options) } } @@ -643,30 +696,47 @@ impl<'i> Parse<'i> for MediaCondition<'i> { pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result<'i, ParserError<'i>>> { let location = input.current_source_location(); - let (is_negation, is_style) = match *input.next()? { - Token::ParenthesisBlock => (false, false), - Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false), + enum QueryFunction { + None, + Style, + ScrollState, + } + + let (is_negation, function) = match *input.next()? { + Token::ParenthesisBlock => (false, QueryFunction::None), + Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, QueryFunction::None), Token::Function(ref f) if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") => { - (false, true) + (false, QueryFunction::Style) + } + Token::Function(ref f) + if flags.contains(QueryConditionFlags::ALLOW_SCROLL_STATE) && f.eq_ignore_ascii_case("scroll-state") => + { + (false, QueryFunction::ScrollState) } ref t => return Err(location.new_unexpected_token_error(t.clone())), }; - let first_condition = match (is_negation, is_style) { - (true, false) => { - let inner_condition = parse_parens_or_function(input, flags)?; + let first_condition = match (is_negation, function) { + (true, QueryFunction::None) => { + let inner_condition = parse_parens_or_function(input, flags, options)?; return Ok(P::create_negation(Box::new(inner_condition))); } - (true, true) => { - let inner_condition = P::parse_style_query(input)?; + (true, QueryFunction::Style) => { + let inner_condition = P::parse_style_query(input, options)?; return Ok(P::create_negation(Box::new(inner_condition))); } - (false, false) => parse_paren_block(input, flags)?, - (false, true) => P::parse_style_query(input)?, + (true, QueryFunction::ScrollState) => { + let inner_condition = P::parse_scroll_state_query(input, options)?; + return Ok(P::create_negation(Box::new(inner_condition))); + } + (false, QueryFunction::None) => parse_paren_block(input, flags, options)?, + (false, QueryFunction::Style) => P::parse_style_query(input, options)?, + (false, QueryFunction::ScrollState) => P::parse_scroll_state_query(input, options)?, }; let operator = match input.try_parse(Operator::parse) { @@ -680,7 +750,7 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( let mut conditions = vec![]; conditions.push(first_condition); - conditions.push(parse_parens_or_function(input, flags)?); + conditions.push(parse_parens_or_function(input, flags, options)?); let delim = match operator { Operator::And => "and", @@ -692,7 +762,7 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( return Ok(P::create_operation(operator, conditions)); } - conditions.push(parse_parens_or_function(input, flags)?); + conditions.push(parse_parens_or_function(input, flags, options)?); } } @@ -700,14 +770,20 @@ pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result<'i, ParserError<'i>>> { let location = input.current_source_location(); match *input.next()? { - Token::ParenthesisBlock => parse_paren_block(input, flags), + Token::ParenthesisBlock => parse_paren_block(input, flags, options), Token::Function(ref f) if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") => { - P::parse_style_query(input) + P::parse_style_query(input, options) + } + Token::Function(ref f) + if flags.contains(QueryConditionFlags::ALLOW_SCROLL_STATE) && f.eq_ignore_ascii_case("scroll-state") => + { + P::parse_scroll_state_query(input, options) } ref t => return Err(location.new_unexpected_token_error(t.clone())), } @@ -716,13 +792,21 @@ fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>( fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, + options: &ParserOptions<'_, 'i>, ) -> Result<'i, ParserError<'i>>> { input.parse_nested_block(|input| { - if let Ok(inner) = input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR)) { + // Detect empty brackets and provide a clearer error message. + if input.is_exhausted() { + return Err(input.new_custom_error(ParserError::EmptyBracketInCondition)); + } + + if let Ok(inner) = + input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR, options)) + { return Ok(inner); } - P::parse_feature(input) + P::parse_feature(input, options) }) } @@ -754,17 +838,27 @@ where { let mut iter = conditions.iter(); let first = iter.next().unwrap(); - to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets))?; + to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets.current))?; for item in iter { dest.write_char(' ')?; operator.to_css(dest)?; dest.write_char(' ')?; - to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets))?; + to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets.current))?; } Ok(()) } +impl<'i> MediaCondition<'i> { + fn negate(&self) -> Option<'i>> { + match self { + MediaCondition::Not(not) => Some((**not).clone()), + MediaCondition::Feature(f) => f.negate().map(MediaCondition::Feature), + _ => None, + } + } +} + impl<'i> ToCss for MediaCondition<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where @@ -773,13 +867,18 @@ impl<'i> ToCss for MediaCondition<'i> { match *self { MediaCondition::Feature(ref f) => f.to_css(dest), MediaCondition::Not(ref c) => { - dest.write_str("not ")?; - to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) + if let Some(negated) = c.negate() { + negated.to_css(dest) + } else { + dest.write_str("not ")?; + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) + } } MediaCondition::Operation { ref conditions, operator, } => operation_to_css(operator, conditions, dest), + MediaCondition::Unknown(ref tokens) => tokens.to_css(dest, false), } } } @@ -841,6 +940,16 @@ impl MediaFeatureComparison { MediaFeatureComparison::Equal => MediaFeatureComparison::Equal, } } + + fn negate(&self) -> MediaFeatureComparison { + match self { + MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThanEqual, + MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThan, + MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThanEqual, + MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThan, + MediaFeatureComparison::Equal => MediaFeatureComparison::Equal, + } + } } /// A generic media feature or container feature. @@ -849,7 +958,8 @@ impl MediaFeatureComparison { feature = "visitor", derive(Visit), visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>), - visit(<'i, ContainerSizeFeatureId>) + visit(<'i, ContainerSizeFeatureId>), + visit(<'i, ScrollStateFeatureId>) )] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -900,12 +1010,15 @@ pub enum QueryFeature<'i, FeatureId> { /// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature) pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>; -impl<'i, FeatureId> Parse<'i> for QueryFeature<'i, FeatureId> +impl<'i, FeatureId> ParseWithOptions<'i> for QueryFeature<'i, FeatureId> where - FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType, + FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone, { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - match input.try_parse(Self::parse_name_first) { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + match input.try_parse(|input| Self::parse_name_first(input, options)) { Ok(res) => Ok(res), Err( err @ ParseError { @@ -920,9 +1033,12 @@ where impl<'i, FeatureId> QueryFeature<'i, FeatureId> where - FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType, + FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone, { - fn parse_name_first<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + fn parse_name_first<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { let (name, legacy_op) = MediaFeatureName::parse(input)?; let operator = input.try_parse(|input| consume_operation_or_colon(input, true)); @@ -937,7 +1053,14 @@ where let value = MediaFeatureValue::parse(input, name.value_type())?; if !value.check_type(name.value_type()) { - return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); + if options.error_recovery { + options.warn(ParseError { + kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery), + location: input.current_source_location(), + }); + } else { + return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); + } } if let Some(operator) = operator.or(legacy_op) { @@ -1017,9 +1140,30 @@ where } pub(crate) fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { - parent_operator != Some(Operator::And) - && matches!(self, QueryFeature::Interval { .. }) - && should_compile!(targets, MediaIntervalSyntax) + match self { + QueryFeature::Interval { .. } => { + should_compile!(targets, MediaIntervalSyntax) && parent_operator != Some(Operator::And) + } + QueryFeature::Range { operator, .. } => { + should_compile!(targets, MediaRangeSyntax) + && matches!( + operator, + MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan + ) + } + _ => false, + } + } + + fn negate(&self) -> Option<'i, FeatureId>> { + match self { + QueryFeature::Range { name, operator, value } => Some(QueryFeature::Range { + name: (*name).clone(), + operator: operator.negate(), + value: value.clone(), + }), + _ => None, + } } } @@ -1028,23 +1172,24 @@ impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> { where W: std::fmt::Write, { - dest.write_char('(')?; - match self { QueryFeature::Boolean { name } => { + dest.write_char('(')?; name.to_css(dest)?; } QueryFeature::Plain { name, value } => { + dest.write_char('(')?; name.to_css(dest)?; dest.delim(':', false)?; value.to_css(dest)?; } QueryFeature::Range { name, operator, value } => { // If range syntax is unsupported, use min/max prefix if possible. - if should_compile!(dest.targets, MediaRangeSyntax) { - return write_min_max(operator, name, value, dest); + if should_compile!(dest.targets.current, MediaRangeSyntax) { + return write_min_max(operator, name, value, dest, false); } + dest.write_char('(')?; name.to_css(dest)?; operator.to_css(dest)?; value.to_css(dest)?; @@ -1056,12 +1201,13 @@ impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> { end, end_operator, } => { - if should_compile!(dest.targets, MediaIntervalSyntax) { - write_min_max(&start_operator.opposite(), name, start, dest)?; - dest.write_str(" and (")?; - return write_min_max(end_operator, name, end, dest); + if should_compile!(dest.targets.current, MediaIntervalSyntax) { + write_min_max(&start_operator.opposite(), name, start, dest, true)?; + dest.write_str(" and ")?; + return write_min_max(end_operator, name, end, dest, true); } + dest.write_char('(')?; start.to_css(dest)?; start_operator.to_css(dest)?; name.to_css(dest)?; @@ -1378,16 +1524,32 @@ fn write_min_max( name: &MediaFeatureName, value: &MediaFeatureValue, dest: &mut Printer, + is_range: bool, ) -> Result<(), PrinterError> where W: std::fmt::Write, { let prefix = match operator { - MediaFeatureComparison::GreaterThan | MediaFeatureComparison::GreaterThanEqual => Some("min-"), - MediaFeatureComparison::LessThan | MediaFeatureComparison::LessThanEqual => Some("max-"), + MediaFeatureComparison::GreaterThan => { + if is_range { + dest.write_char('(')?; + } + dest.write_str("not ")?; + Some("max-") + } + MediaFeatureComparison::GreaterThanEqual => Some("min-"), + MediaFeatureComparison::LessThan => { + if is_range { + dest.write_char('(')?; + } + dest.write_str("not ")?; + Some("min-") + } + MediaFeatureComparison::LessThanEqual => Some("max-"), MediaFeatureComparison::Equal => None, }; + dest.write_char('(')?; if let Some(prefix) = prefix { name.to_css_with_prefix(prefix, dest)?; } else { @@ -1395,17 +1557,15 @@ where } dest.delim(':', false)?; + value.to_css(dest)?; - let adjusted = match operator { - MediaFeatureComparison::GreaterThan => Some(value.clone() + 0.001), - MediaFeatureComparison::LessThan => Some(value.clone() + -0.001), - _ => None, - }; - - if let Some(value) = adjusted { - value.to_css(dest)?; - } else { - value.to_css(dest)?; + if is_range + && matches!( + operator, + MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan + ) + { + dest.write_char(')')?; } dest.write_char(')')?; @@ -1560,25 +1720,6 @@ impl<'i> ToCss for MediaFeatureValue<'i> { } } -impl<'i> std::ops::Add for MediaFeatureValue<'i> { - type Output = Self; - - fn add(self, other: f32) -> Self { - match self { - MediaFeatureValue::Length(len) => MediaFeatureValue::Length(len + Length::px(other)), - MediaFeatureValue::Number(num) => MediaFeatureValue::Number(num + other), - MediaFeatureValue::Integer(num) => { - MediaFeatureValue::Integer(num + if other.is_sign_positive() { 1 } else { -1 }) - } - MediaFeatureValue::Boolean(v) => MediaFeatureValue::Boolean(v), - MediaFeatureValue::Resolution(res) => MediaFeatureValue::Resolution(res + other), - MediaFeatureValue::Ratio(ratio) => MediaFeatureValue::Ratio(ratio + other), - MediaFeatureValue::Ident(id) => MediaFeatureValue::Ident(id), - MediaFeatureValue::Env(env) => MediaFeatureValue::Env(env), // TODO: calc support - } - } -} - /// Consumes an operation or a colon, or returns an error. fn consume_operation_or_colon<'i, 't>( input: &mut Parser<'i, 't>, @@ -1756,10 +1897,10 @@ mod tests { targets::{Browsers, Targets}, }; - fn parse(s: &str) -> MediaQuery { + fn parse(s: &str) -> MediaQuery<'_> { let mut input = ParserInput::new(&s); let mut parser = Parser::new(&mut input); - MediaQuery::parse(&mut parser).unwrap() + MediaQuery::parse_with_options(&mut parser, &ParserOptions::default()).unwrap() } fn and(a: &str, b: &str) -> String { @@ -1817,7 +1958,7 @@ mod tests { }; assert_eq!( media_query.to_css_string(printer_options).unwrap(), - "screen and not ((min-width: 200px) and (max-width: 499.999px))" + "screen and not ((min-width: 200px) and (not (min-width: 500px)))" ); } } diff --git a/src/parser.rs b/src/parser.rs index 2d4768444..0dd76041c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,13 +4,17 @@ use crate::media_query::*; use crate::printer::Printer; use crate::properties::custom::TokenList; use crate::rules::container::{ContainerCondition, ContainerName, ContainerRule}; +use crate::rules::font_feature_values::FontFeatureValuesRule; use crate::rules::font_palette_values::FontPaletteValuesRule; use crate::rules::layer::{LayerBlockRule, LayerStatementRule}; +use crate::rules::nesting::NestedDeclarationsRule; use crate::rules::property::PropertyRule; use crate::rules::scope::ScopeRule; use crate::rules::starting_style::StartingStyleRule; +use crate::rules::view_transition::ViewTransitionRule; use crate::rules::viewport::ViewportRule; +use crate::properties::font::FamilyName; use crate::rules::{ counter_style::CounterStyleRule, custom_media::CustomMediaRule, @@ -28,8 +32,8 @@ use crate::rules::{ unknown::UnknownAtRule, CssRule, CssRuleList, Location, }; -use crate::selector::{Component, SelectorList, SelectorParser}; -use crate::traits::Parse; +use crate::selector::{SelectorList, SelectorParser}; +use crate::traits::{Parse, ParseWithOptions}; use crate::values::ident::{CustomIdent, DashedIdent}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -152,7 +156,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> TopLevelRuleParser<'a, } } - pub fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'o, 'i, T> { + pub fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'x, 'o, 'i, T> { NestedRuleParser { options: &self.options, at_rule_parser: self.at_rule_parser, @@ -172,7 +176,7 @@ pub enum AtRulePrelude<'i, T> { /// A @font-face rule prelude. FontFace, /// A @font-feature-values rule prelude, with its FamilyName list. - FontFeatureValues, //(Vec), + FontFeatureValues(Vec<'i>>), /// A @font-palette-values rule prelude, with its name. FontPaletteValues(DashedIdent<'i>), /// A @counter-style rule prelude, with its counter style name. @@ -209,11 +213,15 @@ pub enum AtRulePrelude<'i, T> { /// An @property prelude. Property(DashedIdent<'i>), /// A @container prelude. - Container(Option<'i>>, ContainerCondition<'i>), + /// Spec: https://drafts.csswg.org/css-conditional-5/#container-rule + /// @container [ ? ? ]! + Container(Option<'i>>, Option<'i>>), /// A @starting-style prelude. StartingStyle, /// A @scope rule prelude. Scope(Option<'i>>, Option<'i>>), + /// A @view-transition rule prelude. + ViewTransition, /// An unknown prelude. Unknown(CowArcStr<'i>, TokenList<'i>), /// A custom prelude. @@ -240,7 +248,7 @@ impl<'i, T> AtRulePrelude<'i, T> { Self::Namespace(..) | Self::FontFace - | Self::FontFeatureValues + | Self::FontFeatureValues(..) | Self::FontPaletteValues(..) | Self::CounterStyle(..) | Self::Keyframes(..) @@ -249,7 +257,8 @@ impl<'i, T> AtRulePrelude<'i, T> { | Self::Import(..) | Self::CustomMedia(..) | Self::Viewport(..) - | Self::Charset => false, + | Self::Charset + | Self::ViewTransition => false, } } } @@ -288,7 +297,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLev } else { None }; - let media = MediaList::parse(input)?; + let media = MediaList::parse(input, &self.options)?; return Ok(AtRulePrelude::Import(url_string, media, supports, layer)); }, "namespace" => { @@ -310,13 +319,9 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLev }, "custom-media" if self.options.flags.contains(ParserFlags::CUSTOM_MEDIA) => { let name = DashedIdent::parse(input)?; - let media = MediaList::parse(input)?; + let media = MediaList::parse(input, &self.options)?; return Ok(AtRulePrelude::CustomMedia(name, media)) }, - "property" => { - let name = DashedIdent::parse(input)?; - return Ok(AtRulePrelude::Property(name)) - }, _ => {} } @@ -511,19 +516,13 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o }; // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. - // These act the same way as if they were nested within a `& { ... }` block. + // These are wrapped in an (invisible) NestedDeclarationsRule. let (declarations, mut rules) = self.parse_nested(input, false)?; if declarations.len() > 0 { rules.0.insert( 0, - CssRule::Style(StyleRule { - selectors: Component::Nesting.into(), - declarations, - vendor_prefix: VendorPrefix::empty(), - rules: CssRuleList(vec![]), - loc, - }), + CssRule::NestedDeclarations(NestedDeclarationsRule { declarations, loc }), ) } @@ -552,11 +551,11 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne ) -> Result<'i, Self::Error>> { let result = match_ignore_ascii_case! { &*name, "media" => { - let media = MediaList::parse(input)?; + let media = MediaList::parse(input, &self.options)?; AtRulePrelude::Media(media) }, "supports" => { - let cond = SupportsCondition::parse(input)?; + let cond = SupportsCondition::parse(input, )?; AtRulePrelude::Supports(cond) }, "font-face" => { @@ -570,6 +569,14 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne // let family_names = parse_family_name_list(self.context, input)?; // Ok(AtRuleType::WithBlock(AtRuleBlockPrelude::FontFeatureValues(family_names))) // }, + "font-feature-values" => { + let names = match Vec::::parse(input) { + Ok(names) => names, + Err(e) => return Err(e) + }; + + AtRulePrelude::FontFeatureValues(names) + }, "font-palette-values" => { let name = DashedIdent::parse(input)?; AtRulePrelude::FontPaletteValues(name) @@ -636,8 +643,18 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne }, "container" => { let name = input.try_parse(ContainerName::parse).ok(); - let condition = ContainerCondition::parse(input)?; - AtRulePrelude::Container(name, condition) + match input.try_parse(|input| ContainerCondition::parse_with_options(input, &self.options)) { + Ok(condition) => AtRulePrelude::Container(name, Some(condition)), + Err(e) => { + if name.is_some() && input.is_exhausted() { + // name only, no condition - allowed by new syntax + AtRulePrelude::Container(name, None) + } else { + // condition parsing failed (e.g., empty brackets or invalid tokens) + return Err(e); + } + } + } }, "starting-style" => { AtRulePrelude::StartingStyle @@ -669,6 +686,9 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne AtRulePrelude::Scope(scope_start, scope_end) }, + "view-transition" => { + AtRulePrelude::ViewTransition + }, "nest" if self.is_in_style_rule => { self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { @@ -678,6 +698,16 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne let selectors = SelectorList::parse(&selector_parser, input, ParseErrorRecovery::DiscardList, NestingRequirement::Contained)?; AtRulePrelude::Nest(selectors) }, + + "value" if self.options.css_modules.is_some() => { + return Err(input.new_custom_error(ParserError::DeprecatedCssModulesValueRule)); + }, + + "property" => { + let name = DashedIdent::parse(input)?; + return Ok(AtRulePrelude::Property(name)) + }, + _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser)? }; @@ -688,6 +718,45 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne Ok(result) } + #[inline] + fn rule_without_block( + &mut self, + prelude: AtRulePrelude<'i, T::Prelude>, + start: &ParserState, + ) -> Result { + let loc = self.loc(start); + match prelude { + AtRulePrelude::Layer(names) => { + if self.is_in_style_rule || names.is_empty() { + return Err(()); + } + + self.rules.0.push(CssRule::LayerStatement(LayerStatementRule { names, loc })); + Ok(()) + } + AtRulePrelude::Unknown(name, prelude) => { + self.rules.0.push(CssRule::Unknown(UnknownAtRule { + name, + prelude, + block: None, + loc, + })); + Ok(()) + } + AtRulePrelude::Custom(prelude) => { + self.rules.0.push(parse_custom_at_rule_without_block( + prelude, + start, + self.options, + self.at_rule_parser, + self.is_in_style_rule, + )?); + Ok(()) + } + _ => Err(()), + } + } + fn parse_block<'t>( &mut self, prelude: Self::Prelude, @@ -827,6 +896,13 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne self.rules.0.push(CssRule::StartingStyle(StartingStyleRule { rules, loc })); Ok(()) } + AtRulePrelude::ViewTransition => { + self + .rules + .0 + .push(CssRule::ViewTransition(ViewTransitionRule::parse(input, loc)?)); + Ok(()) + } AtRulePrelude::Nest(selectors) => { let (declarations, rules) = self.parse_nested(input, true)?; self.rules.0.push(CssRule::Nesting(NestingRule { @@ -841,7 +917,11 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne })); Ok(()) } - AtRulePrelude::FontFeatureValues => unreachable!(), + AtRulePrelude::FontFeatureValues(family_names) => { + let rule = FontFeatureValuesRule::parse(family_names, input, loc, self.options)?; + self.rules.0.push(CssRule::FontFeatureValues(rule)); + Ok(()) + } AtRulePrelude::Unknown(name, prelude) => { self.rules.0.push(CssRule::Unknown(UnknownAtRule { name, @@ -864,45 +944,6 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne } } } - - #[inline] - fn rule_without_block( - &mut self, - prelude: AtRulePrelude<'i, T::Prelude>, - start: &ParserState, - ) -> Result { - let loc = self.loc(start); - match prelude { - AtRulePrelude::Layer(names) => { - if self.is_in_style_rule || names.is_empty() { - return Err(()); - } - - self.rules.0.push(CssRule::LayerStatement(LayerStatementRule { names, loc })); - Ok(()) - } - AtRulePrelude::Unknown(name, prelude) => { - self.rules.0.push(CssRule::Unknown(UnknownAtRule { - name, - prelude, - block: None, - loc, - })); - Ok(()) - } - AtRulePrelude::Custom(prelude) => { - self.rules.0.push(parse_custom_at_rule_without_block( - prelude, - start, - self.options, - self.at_rule_parser, - self.is_in_style_rule, - )?); - Ok(()) - } - _ => Err(()), - } - } } impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> @@ -968,13 +1009,40 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> cssparser::DeclarationParse name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result<'i, Self::Error>> { - parse_declaration( - name, - input, - &mut self.declarations, - &mut self.important_declarations, - &self.options, - ) + if self.rules.0.is_empty() { + parse_declaration( + name, + input, + &mut self.declarations, + &mut self.important_declarations, + &self.options, + ) + } else if let Some(CssRule::NestedDeclarations(last)) = self.rules.0.last_mut() { + parse_declaration( + name, + input, + &mut last.declarations.declarations, + &mut last.declarations.important_declarations, + &self.options, + ) + } else { + let loc = self.loc(&input.state()); + let mut nested = NestedDeclarationsRule { + declarations: DeclarationBlock::new(), + loc, + }; + + parse_declaration( + name, + input, + &mut nested.declarations.declarations, + &mut nested.declarations.important_declarations, + &self.options, + )?; + + self.rules.0.push(CssRule::NestedDeclarations(nested)); + Ok(()) + } } } diff --git a/src/prefixes.rs b/src/prefixes.rs index 5cb213def..63792255e 100644 --- a/src/prefixes.rs +++ b/src/prefixes.rs @@ -800,18 +800,13 @@ impl Feature { prefixes |= VendorPrefix::WebKit; } } - if let Some(version) = browsers.ios_saf { - if version >= 393216 && version <= 852992 { - prefixes |= VendorPrefix::WebKit; - } - } if let Some(version) = browsers.opera { if version >= 983040 && version <= 6881280 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 262144 && version <= 852224 { + if version >= 197120 && version <= 851968 { prefixes |= VendorPrefix::WebKit; } } @@ -820,6 +815,11 @@ impl Feature { prefixes |= VendorPrefix::WebKit; } } + if let Some(version) = browsers.ios_saf { + if version >= 262144 && version <= 851968 { + prefixes |= VendorPrefix::WebKit; + } + } } Feature::FontFeatureSettings | Feature::FontVariantLigatures | Feature::FontLanguageOverride => { if let Some(version) = browsers.android { @@ -1268,23 +1268,23 @@ impl Feature { } } Feature::Stretch => { - if let Some(version) = browsers.chrome { - if version >= 1441792 { - prefixes |= VendorPrefix::WebKit; - } - } if let Some(version) = browsers.firefox { if version >= 196608 { prefixes |= VendorPrefix::Moz; } } if let Some(version) = browsers.android { - if version >= 263168 { + if version >= 263168 && version <= 263171 { + prefixes |= VendorPrefix::WebKit; + } + } + if let Some(version) = browsers.chrome { + if version >= 1441792 && version <= 8978432 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 { + if version >= 5177344 && version <= 8978432 { prefixes |= VendorPrefix::WebKit; } } @@ -1299,12 +1299,12 @@ impl Feature { } } if let Some(version) = browsers.safari { - if version >= 458752 { + if version >= 393472 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.samsung { - if version >= 327680 { + if version >= 262144 { prefixes |= VendorPrefix::WebKit; } } @@ -1386,12 +1386,12 @@ impl Feature { } Feature::TextDecoration => { if let Some(version) = browsers.ios_saf { - if version >= 524288 { + if version >= 524288 && version <= 1704192 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 524288 { + if version >= 524288 && version <= 1704192 { prefixes |= VendorPrefix::WebKit; } } @@ -1516,18 +1516,18 @@ impl Feature { } } Feature::BoxDecorationBreak => { - if let Some(version) = browsers.chrome { - if version >= 1441792 { + if let Some(version) = browsers.android { + if version >= 263168 && version <= 263171 { prefixes |= VendorPrefix::WebKit; } } - if let Some(version) = browsers.android { - if version >= 263168 { + if let Some(version) = browsers.chrome { + if version >= 1441792 && version <= 8454144 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 { + if version >= 5177344 && version <= 8454144 { prefixes |= VendorPrefix::WebKit; } } @@ -2153,18 +2153,18 @@ impl Feature { } } Feature::PrintColorAdjust | Feature::ColorAdjust => { - if let Some(version) = browsers.chrome { - if version >= 1114112 { + if let Some(version) = browsers.android { + if version >= 263168 && version <= 263171 { prefixes |= VendorPrefix::WebKit; } } - if let Some(version) = browsers.android { - if version >= 263168 { + if let Some(version) = browsers.chrome { + if version >= 1114112 && version <= 8847360 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.edge { - if version >= 5177344 { + if version >= 5177344 && version <= 8847360 { prefixes |= VendorPrefix::WebKit; } } @@ -2189,7 +2189,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 1835008 { prefixes |= VendorPrefix::WebKit; } } @@ -2231,7 +2231,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version >= 2424832 && version <= 5701632 { + if version >= 263168 && version <= 5701632 { prefixes |= VendorPrefix::WebKit; } } diff --git a/src/printer.rs b/src/printer.rs index d8c08e2db..b231fbecc 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -5,7 +5,7 @@ use crate::dependencies::{Dependency, DependencyOptions}; use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind}; use crate::rules::{Location, StyleContext}; use crate::selector::SelectorList; -use crate::targets::Targets; +use crate::targets::{Targets, TargetsWithSupportsScope}; use crate::vendor_prefix::VendorPrefix; use cssparser::{serialize_identifier, serialize_name}; #[cfg(feature = "sourcemap")] @@ -77,7 +77,7 @@ pub struct Printer<'a, 'b, 'c, W> { line: u32, col: u32, pub(crate) minify: bool, - pub(crate) targets: Targets, + pub(crate) targets: TargetsWithSupportsScope, /// Vendor prefix override. When non-empty, it overrides /// the vendor prefix of whatever is being printed. pub(crate) vendor_prefix: VendorPrefix, @@ -108,7 +108,7 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { line: 0, col: 0, minify: options.minify, - targets: options.targets, + targets: TargetsWithSupportsScope::new(options.targets), vendor_prefix: VendorPrefix::empty(), in_calc: false, css_module: None, @@ -146,6 +146,25 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { Ok(()) } + /// Writes a raw string which may contain newlines to the underlying destination. + pub fn write_str_with_newlines(&mut self, s: &str) -> Result<(), PrinterError> { + let mut last_line_start: usize = 0; + + for (idx, n) in s.char_indices() { + if n == '\n' { + self.line += 1; + self.col = 0; + + // Keep track of where the *next* line starts + last_line_start = idx + 1; + } + } + + self.col += (s.len() - last_line_start) as u32; + self.dest.write_str(s)?; + Ok(()) + } + /// Write a single character to the underlying destination. pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> { if c == '\n' { @@ -241,10 +260,11 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { if let Some(orig) = mapping.original { let sources_len = map.get_sources().len(); let source_index = map.add_source(sm.get_source(orig.source).unwrap()); + let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap())); original.original_line = orig.original_line; original.original_column = orig.original_column; original.source = source_index; - original.name = orig.name; + original.name = name; if map.get_sources().len() > sources_len { let content = sm.get_source_content(orig.source).unwrap().to_owned(); @@ -276,6 +296,11 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { &css_module.hashes[self.loc.source_index as usize], &css_module.sources[self.loc.source_index as usize], ident, + if let Some(content_hashes) = &css_module.content_hashes { + &content_hashes[self.loc.source_index as usize] + } else { + "" + }, |s| { self.col += s.len() as u32; if first { @@ -306,6 +331,11 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { &css_module.hashes[self.loc.source_index as usize], &css_module.sources[self.loc.source_index as usize], &ident[2..], + if let Some(content_hashes) = &css_module.content_hashes { + &content_hashes[self.loc.source_index as usize] + } else { + "" + }, |s| { self.col += s.len() as u32; serialize_name(s, dest) @@ -365,6 +395,19 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { res } + pub(crate) fn with_parent_context<'a, 'b, 'c, W>) -> Result>( + &mut self, + f: F, + ) -> Result { + let parent = std::mem::take(&mut self.context); + if let Some(parent) = parent { + self.context = parent.parent; + } + let res = f(self); + self.context = parent; + res + } + pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> { self.context.clone() } diff --git a/src/properties/animation.rs b/src/properties/animation.rs index f018f4af4..51bd1ea48 100644 --- a/src/properties/animation.rs +++ b/src/properties/animation.rs @@ -406,7 +406,11 @@ pub enum TimelineRangeName { /// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "lowercase"))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "lowercase") +)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum AnimationAttachmentRange { diff --git a/src/properties/box_shadow.rs b/src/properties/box_shadow.rs index de60f4d3d..663761654 100644 --- a/src/properties/box_shadow.rs +++ b/src/properties/box_shadow.rs @@ -225,7 +225,7 @@ impl BoxShadowHandler { let p3 = box_shadows .iter() .map(|shadow| BoxShadow { - color: shadow.color.to_p3().unwrap(), + color: shadow.color.to_p3().unwrap_or_else(|_| shadow.color.clone()), ..shadow.clone() }) .collect(); diff --git a/src/properties/contain.rs b/src/properties/contain.rs index 1c57bab49..27ad2bb95 100644 --- a/src/properties/contain.rs +++ b/src/properties/contain.rs @@ -30,6 +30,8 @@ enum_property! { InlineSize, /// Establishes a query container for container size queries on both the inline and block axis. Size, + /// Establishes a query container for container scroll-state queries + ScrollState, } } diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 40b522e89..26da05708 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -7,12 +7,12 @@ use crate::printer::Printer; use crate::properties::PropertyId; use crate::rules::supports::SupportsCondition; use crate::stylesheet::ParserOptions; -use crate::targets::{should_compile, Targets}; +use crate::targets::{should_compile, Features, Targets}; use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::angle::Angle; use crate::values::color::{ parse_hsl_hwb_components, parse_rgb_components, ColorFallbackKind, ComponentParser, CssColor, LightDarkColor, - HSL, RGBA, SRGB, + HSL, RGB, RGBA, }; use crate::values::ident::{CustomIdent, DashedIdent, DashedIdentReference, Ident}; use crate::values::length::{serialize_dimension, LengthValue}; @@ -370,59 +370,37 @@ impl<'i> TokenList<'i> { return Err(input.new_custom_error(ParserError::MaximumNestingDepth)); } - let mut last_is_delim = false; - let mut last_is_whitespace = false; loop { let state = input.state(); match input.next_including_whitespace_and_comments() { - Ok(&cssparser::Token::WhiteSpace(..)) | Ok(&cssparser::Token::Comment(..)) => { - // Skip whitespace if the last token was a delimiter. - // Otherwise, replace all whitespace and comments with a single space character. - if !last_is_delim { - tokens.push(Token::WhiteSpace(" ".into()).into()); - last_is_whitespace = true; - } - } Ok(&cssparser::Token::Function(ref f)) => { // Attempt to parse embedded color values into hex tokens. let f = f.into(); if let Some(color) = try_parse_color_token(&f, &state, input) { tokens.push(TokenOrValue::Color(color)); - last_is_delim = false; - last_is_whitespace = false; } else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) { tokens.push(TokenOrValue::UnresolvedColor(color)); - last_is_delim = true; - last_is_whitespace = false; } else if f == "url" { input.reset(&state); tokens.push(TokenOrValue::Url(Url::parse(input)?)); - last_is_delim = false; - last_is_whitespace = false; } else if f == "var" { let var = input.parse_nested_block(|input| { let var = Variable::parse(input, options, depth + 1)?; Ok(TokenOrValue::Var(var)) })?; tokens.push(var); - last_is_delim = true; - last_is_whitespace = false; } else if f == "env" { let env = input.parse_nested_block(|input| { let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?; Ok(TokenOrValue::Env(env)) })?; tokens.push(env); - last_is_delim = true; - last_is_whitespace = false; } else { let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?; tokens.push(TokenOrValue::Function(Function { name: Ident(f), arguments, })); - last_is_delim = true; // Whitespace is not required after any of these chars. - last_is_whitespace = false; } } Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => { @@ -431,19 +409,13 @@ impl<'i> TokenList<'i> { } else { tokens.push(Token::Hash(h.into()).into()); } - last_is_delim = false; - last_is_whitespace = false; } Ok(&cssparser::Token::UnquotedUrl(_)) => { input.reset(&state); tokens.push(TokenOrValue::Url(Url::parse(input)?)); - last_is_delim = false; - last_is_whitespace = false; } Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => { tokens.push(TokenOrValue::DashedIdent(name.into())); - last_is_delim = false; - last_is_whitespace = false; } Ok(token @ &cssparser::Token::ParenthesisBlock) | Ok(token @ &cssparser::Token::SquareBracketBlock) @@ -459,8 +431,6 @@ impl<'i> TokenList<'i> { input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?; tokens.push(closing_delimiter.into()); - last_is_delim = true; // Whitespace is not required after any of these chars. - last_is_whitespace = false; } Ok(token @ cssparser::Token::Dimension { .. }) => { let value = if let Ok(length) = LengthValue::try_from(token) { @@ -475,8 +445,6 @@ impl<'i> TokenList<'i> { TokenOrValue::Token(token.into()) }; tokens.push(value); - last_is_delim = false; - last_is_whitespace = false; } Ok(token) if token.is_parse_error() => { return Err(ParseError { @@ -485,18 +453,7 @@ impl<'i> TokenList<'i> { }) } Ok(token) => { - last_is_delim = matches!(token, cssparser::Token::Delim(_) | cssparser::Token::Comma); - - // If this is a delimiter, and the last token was whitespace, - // replace the whitespace with the delimiter since both are not required. - if last_is_delim && last_is_whitespace { - let last = tokens.last_mut().unwrap(); - *last = Token::from(token).into(); - } else { - tokens.push(Token::from(token).into()); - } - - last_is_whitespace = false; + tokens.push(Token::from(token).into()); } Err(_) => break, } @@ -532,20 +489,13 @@ impl<'i> TokenList<'i> { where W: std::fmt::Write, { - if !dest.minify && self.0.len() == 1 && matches!(self.0.first(), Some(token) if token.is_whitespace()) { - return Ok(()); - } - - let mut has_whitespace = false; - for (i, token_or_value) in self.0.iter().enumerate() { - has_whitespace = match token_or_value { + for token_or_value in self.0.iter() { + match token_or_value { TokenOrValue::Color(color) => { color.to_css(dest)?; - false } TokenOrValue::UnresolvedColor(color) => { color.to_css(dest, is_custom_property)?; - false } TokenOrValue::Url(url) => { if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() { @@ -557,77 +507,45 @@ impl<'i> TokenList<'i> { )); } url.to_css(dest)?; - false } TokenOrValue::Var(var) => { var.to_css(dest, is_custom_property)?; - self.write_whitespace_if_needed(i, dest)? } TokenOrValue::Env(env) => { env.to_css(dest, is_custom_property)?; - self.write_whitespace_if_needed(i, dest)? } TokenOrValue::Function(f) => { f.to_css(dest, is_custom_property)?; - self.write_whitespace_if_needed(i, dest)? } TokenOrValue::Length(v) => { // Do not serialize unitless zero lengths in custom properties as it may break calc(). let (value, unit) = v.to_unit_value(); serialize_dimension(value, unit, dest)?; - false } TokenOrValue::Angle(v) => { v.to_css(dest)?; - false } TokenOrValue::Time(v) => { v.to_css(dest)?; - false } TokenOrValue::Resolution(v) => { v.to_css(dest)?; - false } TokenOrValue::DashedIdent(v) => { v.to_css(dest)?; - false } TokenOrValue::AnimationName(v) => { v.to_css(dest)?; - false } TokenOrValue::Token(token) => match token { - Token::Delim(d) => { - if *d == '+' || *d == '-' { - dest.write_char(' ')?; - dest.write_char(*d)?; - dest.write_char(' ')?; - } else { - let ws_before = !has_whitespace && (*d == '/' || *d == '*'); - dest.delim(*d, ws_before)?; - } - true - } - Token::Comma => { - dest.delim(',', false)?; - true - } - Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => { - token.to_css(dest)?; - self.write_whitespace_if_needed(i, dest)? - } Token::Dimension { value, unit, .. } => { serialize_dimension(*value, unit, dest)?; - false } Token::Number { value, .. } => { value.to_css(dest)?; - false } _ => { token.to_css(dest)?; - matches!(token, Token::WhiteSpace(..)) } }, }; @@ -657,24 +575,8 @@ impl<'i> TokenList<'i> { Ok(()) } - #[inline] - fn write_whitespace_if_needed(&self, i: usize, dest: &mut Printer) -> Result - where - W: std::fmt::Write, - { - if !dest.minify - && i != self.0.len() - 1 - && !matches!( - self.0[i + 1], - TokenOrValue::Token(Token::Comma) | TokenOrValue::Token(Token::CloseParenthesis) - ) - { - // Whitespace is removed during parsing, so add it back if we aren't minifying. - dest.write_char(' ')?; - Ok(true) - } else { - Ok(false) - } + pub(crate) fn starts_with_whitespace(&self) -> bool { + matches!(self.0.get(0), Some(TokenOrValue::Token(Token::WhiteSpace(_)))) } } @@ -986,8 +888,18 @@ impl<'a> ToCss for Token<'a> { int_value: *int_value, } .to_css(dest)?, - Token::WhiteSpace(w) => cssparser::Token::WhiteSpace(w).to_css(dest)?, - Token::Comment(c) => cssparser::Token::Comment(c).to_css(dest)?, + Token::WhiteSpace(w) => { + if dest.minify { + dest.write_char(' ')?; + } else { + dest.write_str(&w)?; + } + } + Token::Comment(c) => { + if !dest.minify { + cssparser::Token::Comment(c).to_css(dest)?; + } + } Token::Colon => cssparser::Token::Colon.to_css(dest)?, Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?, Token::Comma => cssparser::Token::Comma.to_css(dest)?, @@ -1081,7 +993,7 @@ impl<'a> std::hash::Hash for Token<'a> { /// Converts a floating point value into its mantissa, exponent, /// and sign components so that it can be hashed. fn integer_decode(v: f32) -> (u32, i16, i8) { - let bits: u32 = unsafe { std::mem::transmute(v) }; + let bits: u32 = f32::to_bits(v); let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 }; let mut exponent: i16 = ((bits >> 23) & 0xff) as i16; let mantissa = if exponent == 0 { @@ -1176,6 +1088,44 @@ impl<'i> TokenList<'i> { res } + pub(crate) fn get_features(&self) -> Features { + let mut features = Features::empty(); + for token in &self.0 { + match token { + TokenOrValue::Color(color) => { + features |= color.get_features(); + } + TokenOrValue::UnresolvedColor(unresolved_color) => { + features |= Features::SpaceSeparatedColorNotation; + match unresolved_color { + UnresolvedColor::LightDark { light, dark } => { + features |= Features::LightDark; + features |= light.get_features(); + features |= dark.get_features(); + } + _ => {} + } + } + TokenOrValue::Function(f) => { + features |= f.arguments.get_features(); + } + TokenOrValue::Var(v) => { + if let Some(fallback) = &v.fallback { + features |= fallback.get_features(); + } + } + TokenOrValue::Env(v) => { + if let Some(fallback) = &v.fallback { + features |= fallback.get_features(); + } + } + _ => {} + } + } + + features + } + /// Substitutes variables with the provided values. #[cfg(feature = "substitute_variables")] #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))] @@ -1265,7 +1215,10 @@ impl<'i> Variable<'i> { dest.write_str("var(")?; self.name.to_css(dest)?; if let Some(fallback) = &self.fallback { - dest.delim(',', false)?; + dest.write_char(',')?; + if !fallback.starts_with_whitespace() { + dest.whitespace()?; + } fallback.to_css(dest, is_custom_property)?; } dest.write_char(')') @@ -1439,7 +1392,10 @@ impl<'i> EnvironmentVariable<'i> { } if let Some(fallback) = &self.fallback { - dest.delim(',', false)?; + dest.write_char(',')?; + if !fallback.starts_with_whitespace() { + dest.whitespace()?; + } fallback.to_css(dest, is_custom_property)?; } dest.write_char(')') @@ -1556,7 +1512,7 @@ impl<'i> UnresolvedColor<'i> { match_ignore_ascii_case! { &*f, "rgb" => { input.parse_nested_block(|input| { - parser.parse_relative::(input, |input, parser| { + parser.parse_relative::(input, |input, parser| { let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?; if is_legacy { return Err(input.new_custom_error(ParserError::InvalidValue)) @@ -1598,20 +1554,15 @@ impl<'i> UnresolvedColor<'i> { where W: std::fmt::Write, { - #[inline] - fn c(c: &f32) -> i32 { - (c * 255.0).round().clamp(0.0, 255.0) as i32 - } - match self { UnresolvedColor::RGB { r, g, b, alpha } => { - if should_compile!(dest.targets, SpaceSeparatedColorNotation) { + if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) { dest.write_str("rgba(")?; - c(r).to_css(dest)?; + r.to_css(dest)?; dest.delim(',', false)?; - c(g).to_css(dest)?; + g.to_css(dest)?; dest.delim(',', false)?; - c(b).to_css(dest)?; + b.to_css(dest)?; dest.delim(',', false)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')')?; @@ -1619,23 +1570,23 @@ impl<'i> UnresolvedColor<'i> { } dest.write_str("rgb(")?; - c(r).to_css(dest)?; + r.to_css(dest)?; dest.write_char(' ')?; - c(g).to_css(dest)?; + g.to_css(dest)?; dest.write_char(' ')?; - c(b).to_css(dest)?; + b.to_css(dest)?; dest.delim('/', true)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')') } UnresolvedColor::HSL { h, s, l, alpha } => { - if should_compile!(dest.targets, SpaceSeparatedColorNotation) { + if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) { dest.write_str("hsla(")?; h.to_css(dest)?; dest.delim(',', false)?; - Percentage(*s).to_css(dest)?; + Percentage(*s / 100.0).to_css(dest)?; dest.delim(',', false)?; - Percentage(*l).to_css(dest)?; + Percentage(*l / 100.0).to_css(dest)?; dest.delim(',', false)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')')?; @@ -1645,15 +1596,15 @@ impl<'i> UnresolvedColor<'i> { dest.write_str("hsl(")?; h.to_css(dest)?; dest.write_char(' ')?; - Percentage(*s).to_css(dest)?; + Percentage(*s / 100.0).to_css(dest)?; dest.write_char(' ')?; - Percentage(*l).to_css(dest)?; + Percentage(*l / 100.0).to_css(dest)?; dest.delim('/', true)?; alpha.to_css(dest, is_custom_property)?; dest.write_char(')') } UnresolvedColor::LightDark { light, dark } => { - if !dest.targets.is_compatible(crate::compat::Feature::LightDark) { + if should_compile!(dest.targets.current, LightDark) { dest.write_str("var(--lightningcss-light")?; dest.delim(',', false)?; light.to_css(dest, is_custom_property)?; diff --git a/src/properties/effects.rs b/src/properties/effects.rs index 1ba69f6a6..95d403d43 100644 --- a/src/properties/effects.rs +++ b/src/properties/effects.rs @@ -1,5 +1,6 @@ //! CSS properties related to filters and effects. +use crate::macros::enum_property; use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::targets::{Browsers, Targets}; @@ -410,3 +411,45 @@ impl IsCompatible for FilterList<'_> { true } } + +enum_property! { + /// A [``](https://www.w3.org/TR/compositing-1/#ltblendmodegt) value. + pub enum BlendMode { + /// The default blend mode; the top layer is drawn over the bottom layer. + Normal, + /// The source and destination are multiplied. + Multiply, + /// Multiplies the complements of the backdrop and source, then complements the result. + Screen, + /// Multiplies or screens, depending on the backdrop color. + Overlay, + /// Selects the darker of the backdrop and source. + Darken, + /// Selects the lighter of the backdrop and source. + Lighten, + /// Brightens the backdrop to reflect the source. + ColorDodge, + /// Darkens the backdrop to reflect the source. + ColorBurn, + /// Multiplies or screens, depending on the source color. + HardLight, + /// Darkens or lightens, depending on the source color. + SoftLight, + /// Subtracts the darker from the lighter. + Difference, + /// Similar to difference, but with lower contrast. + Exclusion, + /// The hue of the source with the saturation and luminosity of the backdrop. + Hue, + /// The saturation of the source with the hue and luminosity of the backdrop. + Saturation, + /// The hue and saturation of the source with the luminosity of the backdrop. + Color, + /// The luminosity of the source with the hue and saturation of the backdrop. + Luminosity, + /// Adds the source to the backdrop, producing a darker result. + PlusDarker, + /// Adds the source to the backdrop, producing a lighter result. + PlusLighter, + } +} diff --git a/src/properties/font.rs b/src/properties/font.rs index 377ea77c2..8b3031a9f 100644 --- a/src/properties/font.rs +++ b/src/properties/font.rs @@ -348,19 +348,46 @@ pub enum FontFamily<'i> { Generic(GenericFontFamily), /// A custom family name. #[cfg_attr(feature = "serde", serde(borrow))] - FamilyName(CowArcStr<'i>), + FamilyName(FamilyName<'i>), } impl<'i> Parse<'i> for FontFamily<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) { - return Ok(FontFamily::FamilyName(value.into())); - } - if let Ok(value) = input.try_parse(GenericFontFamily::parse) { return Ok(FontFamily::Generic(value)); } + let family = FamilyName::parse(input)?; + Ok(FontFamily::FamilyName(family)) + } +} + +impl<'i> ToCss for FontFamily<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match self { + FontFamily::Generic(val) => val.to_css(dest), + FontFamily::FamilyName(val) => val.to_css(dest), + } + } +} + +/// A font [family name](https://drafts.csswg.org/css-fonts/#family-name-syntax). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct FamilyName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'i>); + +impl<'i> Parse<'i> for FamilyName<'i> { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) { + return Ok(FamilyName(value.into())); + } + let value: CowArcStr<'i> = input.expect_ident()?.into(); let mut string = None; while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { @@ -380,40 +407,43 @@ impl<'i> Parse<'i> for FontFamily<'i> { value }; - Ok(FontFamily::FamilyName(value)) + Ok(FamilyName(value)) } } -impl<'i> ToCss for FontFamily<'i> { +impl<'i> ToCss for FamilyName<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { - match self { - FontFamily::Generic(val) => val.to_css(dest), - FontFamily::FamilyName(val) => { - // Generic family names such as sans-serif must be quoted if parsed as a string. - // CSS wide keywords, as well as "default", must also be quoted. - // https://www.w3.org/TR/css-fonts-4/#family-name-syntax - if !val.is_empty() && !GenericFontFamily::parse_string(val).is_ok() { - let mut id = String::new(); - let mut first = true; - for slice in val.split(' ') { - if first { - first = false; - } else { - id.push(' '); - } - serialize_identifier(slice, &mut id)?; - } - if id.len() < val.len() + 2 { - return dest.write_str(&id); - } - } + // Generic family names such as sans-serif must be quoted if parsed as a string. + // CSS wide keywords, as well as "default", must also be quoted. + // https://www.w3.org/TR/css-fonts-4/#family-name-syntax + let val = &self.0; + if !val.is_empty() && !GenericFontFamily::parse_string(val).is_ok() { + // Family names with two or more consecutive spaces must be quoted to preserve the spaces. + let needs_quotes = val.contains(" "); + if needs_quotes { serialize_string(&val, dest)?; - Ok(()) + return Ok(()); + } + + let mut id = String::new(); + let mut first = true; + for slice in val.split(' ') { + if first { + first = false; + } else { + id.push(' '); + } + serialize_identifier(slice, &mut id)?; + } + if id.len() < val.len() + 2 { + return dest.write_str(&id); } } + serialize_string(&val, dest)?; + Ok(()) } } @@ -994,7 +1024,7 @@ fn compatible_font_family(mut family: Option>, is_supported: boo (position + 1)..(position + 1), DEFAULT_SYSTEM_FONTS .iter() - .map(|name| FontFamily::FamilyName(CowArcStr::from(*name))), + .map(|name| FontFamily::FamilyName(FamilyName(CowArcStr::from(*name)))), ); } } diff --git a/src/properties/grid.rs b/src/properties/grid.rs index 6e7063197..553cdc542 100644 --- a/src/properties/grid.rs +++ b/src/properties/grid.rs @@ -533,6 +533,7 @@ impl ToCss for TrackSizeList { } /// A value for the [grid-template-areas](https://drafts.csswg.org/css-grid-2/#grid-template-areas-property) property. +/// none | + #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( @@ -704,6 +705,8 @@ impl GridTemplateAreas { /// A value for the [grid-template](https://drafts.csswg.org/css-grid-2/#explicit-grid-shorthand) shorthand property. /// +/// none | [ <'grid-template-rows'> / <'grid-template-columns'> ] | [ ? ? ? ]+ [ / ]? +/// /// If `areas` is not `None`, then `rows` must also not be `None`. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -935,6 +938,8 @@ impl_shorthand! { bitflags! { /// A value for the [grid-auto-flow](https://drafts.csswg.org/css-grid-2/#grid-auto-flow-property) property. /// + /// [ row | column ] || dense + /// /// The `Row` or `Column` flags may be combined with the `Dense` flag, but the `Row` and `Column` flags may /// not be combined. #[cfg_attr(feature = "visitor", derive(Visit))] @@ -1101,6 +1106,8 @@ impl ToCss for GridAutoFlow { /// A value for the [grid](https://drafts.csswg.org/css-grid-2/#grid-shorthand) shorthand property. /// +/// <'grid-template'> | <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? | [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'> +/// /// Explicit and implicit values may not be combined. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -1199,6 +1206,41 @@ impl ToCss for Grid<'_> { && self.auto_columns == TrackSizeList::default() && self.auto_flow == GridAutoFlow::default(); + // Handle the case where areas is set but rows is None (auto-flow syntax). + // In this case, output "auto-flow / columns" format. + if self.areas != GridTemplateAreas::None && self.rows == TrackSizing::None { + dest.write_str("auto-flow")?; + if self.auto_flow.contains(GridAutoFlow::Dense) { + dest.write_str(" dense")?; + } + if self.auto_rows != TrackSizeList::default() { + dest.write_char(' ')?; + self.auto_rows.to_css(dest)?; + } + dest.delim('/', true)?; + self.columns.to_css(dest)?; + return Ok(()); + } + + // Handle the case where areas is set but columns is None (auto-flow column syntax). + // In this case, output "rows / auto-flow" format. + if self.areas != GridTemplateAreas::None + && self.columns == TrackSizing::None + && self.auto_flow.direction() == GridAutoFlow::Column + { + self.rows.to_css(dest)?; + dest.delim('/', true)?; + dest.write_str("auto-flow")?; + if self.auto_flow.contains(GridAutoFlow::Dense) { + dest.write_str(" dense")?; + } + if self.auto_columns != TrackSizeList::default() { + dest.write_char(' ')?; + self.auto_columns.to_css(dest)?; + } + return Ok(()); + } + if self.areas != GridTemplateAreas::None || (self.rows != TrackSizing::None && self.columns != TrackSizing::None) || (self.areas == GridTemplateAreas::None && is_auto_initial) @@ -1256,16 +1298,28 @@ impl<'i> Grid<'i> { auto_columns: &TrackSizeList, auto_flow: &GridAutoFlow, ) -> bool { + let default_track_size_list = TrackSizeList::default(); + + // When areas is set but rows is None (auto-flow syntax like "grid: auto-flow / 1fr"), + // we can output the auto-flow shorthand along with "grid-template-areas" separately. + // ⚠️ The case of `grid: 1fr / auto-flow` does not require such handling. + if *areas != GridTemplateAreas::None && *rows == TrackSizing::None { + return auto_flow.direction() == GridAutoFlow::Row; + } + // The `grid` shorthand can either be fully explicit (e.g. same as `grid-template`), // or explicit along a single axis. If there are auto rows, then there cannot be explicit rows, for example. let is_template = GridTemplate::is_valid(rows, columns, areas); - let default_track_size_list = TrackSizeList::default(); let is_explicit = *auto_rows == default_track_size_list && *auto_columns == default_track_size_list && *auto_flow == GridAutoFlow::default(); + // grid-auto-flow: row shorthand syntax: + // [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'> let is_auto_rows = auto_flow.direction() == GridAutoFlow::Row && *rows == TrackSizing::None && *auto_columns == default_track_size_list; + // grid-auto-flow: column shorthand syntax: + // <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? let is_auto_columns = auto_flow.direction() == GridAutoFlow::Column && *columns == TrackSizing::None && *auto_rows == default_track_size_list; @@ -1274,6 +1328,7 @@ impl<'i> Grid<'i> { } } +// TODO: shorthand `grid: auto-flow 1fr / 100px` https://drafts.csswg.org/css-grid/#example-dec34e0f impl_shorthand! { Grid(Grid<'i>) { rows: [GridTemplateRows], @@ -1461,6 +1516,7 @@ macro_rules! impl_grid_placement { define_shorthand! { /// A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-row) shorthand property. + /// [ / ]? pub struct GridRow<'i> { /// The starting line. #[cfg_attr(feature = "serde", serde(borrow))] @@ -1471,7 +1527,8 @@ define_shorthand! { } define_shorthand! { - /// A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property. + /// A value for the [grid-column](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property. + /// [ / ]? pub struct GridColumn<'i> { /// The starting line. #[cfg_attr(feature = "serde", serde(borrow))] @@ -1486,6 +1543,7 @@ impl_grid_placement!(GridColumn); define_shorthand! { /// A value for the [grid-area](https://drafts.csswg.org/css-grid-2/#propdef-grid-area) shorthand property. + /// [ / ]{0,3} pub struct GridArea<'i> { /// The grid row start placement. #[cfg_attr(feature = "serde", serde(borrow))] @@ -1684,10 +1742,24 @@ impl<'i> PropertyHandler<'i> for GridHandler<'i> { auto_columns_val, auto_flow_val, ) { + let needs_separate_areas = *areas_val != GridTemplateAreas::None + && ((*rows_val == TrackSizing::None && auto_flow_val.direction() == GridAutoFlow::Row) + || (*columns_val == TrackSizing::None && auto_flow_val.direction() == GridAutoFlow::Column)); + + // Pad areas with "." for missing rows. But don't pad if we're using auto-flow syntax, + // because grid-template-areas should remain as-is in that case. + // Use tuple to avoid double cloning when needs_separate_areas is true. + let (areas_for_grid, areas_for_output) = if needs_separate_areas { + // Take the original areas directly to avoid cloning when needs_separate_areas is true + (areas_val.clone(), Some(areas_val.clone())) + } else { + (GridHandler::pad_grid_template_areas(rows_val, areas_val.clone()), None) + }; + dest.push(Property::Grid(Grid { rows: rows_val.clone(), columns: columns_val.clone(), - areas: areas_val.clone(), + areas: areas_for_grid, auto_rows: auto_rows_val.clone(), auto_columns: auto_columns_val.clone(), auto_flow: auto_flow_val.clone(), @@ -1697,16 +1769,25 @@ impl<'i> PropertyHandler<'i> for GridHandler<'i> { auto_rows = None; auto_columns = None; auto_flow = None; + + // When areas is set but rows/columns is None (auto-flow syntax), also output + // grid-template-areas separately since grid shorthand can't represent this combination. + if let Some(areas) = areas_for_output { + dest.push(Property::GridTemplateAreas(areas)); + } } } // The `grid-template` shorthand supports only explicit track values (i.e. no `repeat()`) // combined with grid-template-areas. If there are no areas, then any track values are allowed. if has_template && GridTemplate::is_valid(rows_val, columns_val, areas_val) { + // Pad areas with "." for missing rows + let padded_areas = GridHandler::pad_grid_template_areas(rows_val, areas_val.clone()); + dest.push(Property::GridTemplate(GridTemplate { rows: rows_val.clone(), columns: columns_val.clone(), - areas: areas_val.clone(), + areas: padded_areas, })); has_template = false; @@ -1763,6 +1844,39 @@ impl<'i> PropertyHandler<'i> for GridHandler<'i> { } } +/// Pads grid template areas with "." (None) for missing rows. +/// All the remaining unnamed areas in a grid can be referred using null cell +/// tokens. A null cell token is a sequence of one or more . (U+002E FULL STOP) +/// characters, e.g., ., ..., or ..... etc. A null cell token can be used to +/// create empty spaces in the grid. +/// Spec: https://drafts.csswg.org/css-grid/#ref-for-string-value① +impl GridHandler<'_> { + fn pad_grid_template_areas(rows: &TrackSizing, areas: GridTemplateAreas) -> GridTemplateAreas { + match (rows, areas) { + (TrackSizing::TrackList(rows_list), GridTemplateAreas::Areas { columns, areas }) => { + let rows_count = rows_list.items.len(); + let areas_rows_count = areas.len() / columns as usize; + if areas_rows_count < rows_count { + let mut padded_areas = areas; + // Fill each missing row with "." (represented as None) + for _ in areas_rows_count..rows_count { + for _ in 0..columns { + padded_areas.push(None); + } + } + GridTemplateAreas::Areas { + columns, + areas: padded_areas, + } + } else { + GridTemplateAreas::Areas { columns, areas } + } + } + (_, areas) => areas, + } + } +} + #[inline] fn is_grid_property(property_id: &PropertyId) -> bool { match property_id { diff --git a/src/properties/list.rs b/src/properties/list.rs index 54a0c0521..e04ed7477 100644 --- a/src/properties/list.rs +++ b/src/properties/list.rs @@ -4,7 +4,7 @@ use super::{Property, PropertyId}; use crate::context::PropertyHandlerContext; use crate::declaration::{DeclarationBlock, DeclarationList}; use crate::error::{ParserError, PrinterError}; -use crate::macros::{define_shorthand, enum_property, shorthand_handler, shorthand_property}; +use crate::macros::{define_shorthand, enum_property, shorthand_handler}; use crate::printer::Printer; use crate::targets::{Browsers, Targets}; use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss}; @@ -108,7 +108,7 @@ macro_rules! counter_styles { fn is_compatible(&self, browsers: Browsers) -> bool { match self { $( - PredefinedCounterStyle::$id => paste::paste! { + PredefinedCounterStyle::$id => pastey::paste! { crate::compat::Feature::[<$id ListStyleType>].is_compatible(browsers) }, )+ @@ -329,16 +329,120 @@ enum_property! { } } -shorthand_property! { +define_shorthand! { /// A value for the [list-style](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-property) shorthand property. pub struct ListStyle<'i> { - /// The list style type. - #[cfg_attr(feature = "serde", serde(borrow))] - list_style_type: ListStyleType(ListStyleType<'i>), - /// The list marker image. - image: ListStyleImage(Image<'i>), /// The position of the list marker. position: ListStylePosition(ListStylePosition), + /// The list marker image. + #[cfg_attr(feature = "serde", serde(borrow))] + image: ListStyleImage(Image<'i>), + /// The list style type. + list_style_type: ListStyleType(ListStyleType<'i>), + } +} + +impl<'i> Parse<'i> for ListStyle<'i> { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + let mut position = None; + let mut image = None; + let mut list_style_type = None; + let mut nones = 0; + + loop { + // `none` is ambiguous - both list-style-image and list-style-type support it. + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + nones += 1; + if nones > 2 { + return Err(input.new_custom_error(ParserError::InvalidValue)); + } + continue; + } + + if image.is_none() { + if let Ok(val) = input.try_parse(Image::parse) { + image = Some(val); + continue; + } + } + + if position.is_none() { + if let Ok(val) = input.try_parse(ListStylePosition::parse) { + position = Some(val); + continue; + } + } + + if list_style_type.is_none() { + if let Ok(val) = input.try_parse(ListStyleType::parse) { + list_style_type = Some(val); + continue; + } + } + + break; + } + + // Assign the `none` to the opposite property from the one we have a value for, + // or both in case neither list-style-image or list-style-type have a value. + match (nones, image, list_style_type) { + (2, None, None) | (1, None, None) => Ok(ListStyle { + position: position.unwrap_or_default(), + image: Image::None, + list_style_type: ListStyleType::None, + }), + (1, Some(image), None) => Ok(ListStyle { + position: position.unwrap_or_default(), + image, + list_style_type: ListStyleType::None, + }), + (1, None, Some(list_style_type)) => Ok(ListStyle { + position: position.unwrap_or_default(), + image: Image::None, + list_style_type, + }), + (0, image, list_style_type) => Ok(ListStyle { + position: position.unwrap_or_default(), + image: image.unwrap_or_default(), + list_style_type: list_style_type.unwrap_or_default(), + }), + _ => Err(input.new_custom_error(ParserError::InvalidValue)), + } + } +} + +impl<'i> ToCss for ListStyle<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + let mut needs_space = false; + if self.position != ListStylePosition::default() { + self.position.to_css(dest)?; + needs_space = true; + } + + if self.image != Image::default() { + if needs_space { + dest.write_char(' ')?; + } + self.image.to_css(dest)?; + needs_space = true; + } + + if self.list_style_type != ListStyleType::default() { + if needs_space { + dest.write_char(' ')?; + } + self.list_style_type.to_css(dest)?; + needs_space = true; + } + + if !needs_space { + self.position.to_css(dest)?; + } + + Ok(()) } } diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 7c5f40714..c39b618ba 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -104,7 +104,6 @@ pub mod display; pub mod effects; pub mod flex; pub mod font; -#[cfg(feature = "grid")] pub mod grid; pub mod list; pub(crate) mod margin_padding; @@ -133,7 +132,7 @@ use crate::traits::{Parse, ParseWithOptions, Shorthand, ToCss}; use crate::values::number::{CSSInteger, CSSNumber}; use crate::values::string::CowArcStr; use crate::values::{ - alpha::*, color::*, easing::EasingFunction, ident::CustomIdent, ident::DashedIdentReference, image::*, + alpha::*, color::*, easing::EasingFunction, ident::DashedIdentReference, ident::NoneOrCustomIdentList, image::*, length::*, position::*, rect::*, shape::FillRule, size::Size2D, time::Time, }; use crate::vendor_prefix::VendorPrefix; @@ -154,7 +153,6 @@ use display::*; use effects::*; use flex::*; use font::*; -#[cfg(feature = "grid")] use grid::*; use list::*; use margin_padding::*; @@ -854,7 +852,10 @@ macro_rules! define_properties { }, Custom(custom) => { custom.name.to_css(dest)?; - dest.delim(':', false)?; + dest.write_char(':')?; + if !custom.value.starts_with_whitespace() { + dest.whitespace()?; + } self.value_to_css(dest)?; write_important!(); return Ok(()) @@ -991,7 +992,7 @@ macro_rules! define_properties { D: serde::Deserializer<'de>, { enum ContentOrRaw<'de> { - Content(serde::__private::de::Content<'de>), + Content(serde_content::Value<'de>), Raw(CowArcStr<'de>) } @@ -1064,26 +1065,26 @@ macro_rules! define_properties { ContentOrRaw::Content(content) => content }; - let deserializer = serde::__private::de::ContentDeserializer::new(content); + let deserializer = serde_content::Deserializer::new(content).coerce_numbers(); match partial.property_id { $( $(#[$meta])* PropertyId::$property$((vp_name!($vp, prefix)))? => { - let value = <$type>::deserialize(deserializer)?; + let value = <$type>::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::$property(value $(, vp_name!($vp, prefix))?)) }, )+ PropertyId::Custom(name) => { if name.as_ref() == "unparsed" { - let value = UnparsedProperty::deserialize(deserializer)?; + let value = UnparsedProperty::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::Unparsed(value)) } else { - let value = CustomProperty::deserialize(deserializer)?; + let value = CustomProperty::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::Custom(value)) } } PropertyId::All => { - let value = CSSWideKeyword::deserialize(deserializer)?; + let value = CSSWideKeyword::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(Property::All(value)) } } @@ -1372,50 +1373,20 @@ define_properties! { "flex-negative": FlexNegative(CSSNumber, VendorPrefix) / Ms unprefixed: false, "flex-preferred-size": FlexPreferredSize(LengthPercentageOrAuto, VendorPrefix) / Ms unprefixed: false, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template-columns": GridTemplateColumns(TrackSizing<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template-rows": GridTemplateRows(TrackSizing<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-auto-columns": GridAutoColumns(TrackSizeList), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-auto-rows": GridAutoRows(TrackSizeList), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-auto-flow": GridAutoFlow(GridAutoFlow), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template-areas": GridTemplateAreas(GridTemplateAreas), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-template": GridTemplate(GridTemplate<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid": Grid(Grid<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-row-start": GridRowStart(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-row-end": GridRowEnd(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-column-start": GridColumnStart(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-column-end": GridColumnEnd(GridLine<'i>), - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-row": GridRow(GridRow<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-column": GridColumn(GridColumn<'i>) shorthand: true, - #[cfg(feature = "grid")] - #[cfg_attr(docsrs, doc(cfg(feature = "grid")))] "grid-area": GridArea(GridArea<'i>) shorthand: true, "margin-top": MarginTop(LengthPercentageOrAuto) [logical_group: Margin, category: Physical], @@ -1629,6 +1600,9 @@ define_properties! { "filter": Filter(FilterList<'i>, VendorPrefix) / WebKit, "backdrop-filter": BackdropFilter(FilterList<'i>, VendorPrefix) / WebKit, + // https://www.w3.org/TR/compositing-1/ + "mix-blend-mode": MixBlendMode(BlendMode), + // https://drafts.csswg.org/css2/ "z-index": ZIndex(position::ZIndex), @@ -1638,10 +1612,14 @@ define_properties! { "container": Container(Container<'i>) shorthand: true, // https://w3c.github.io/csswg-drafts/css-view-transitions-1/ - "view-transition-name": ViewTransitionName(CustomIdent<'i>), + "view-transition-name": ViewTransitionName(ViewTransitionName<'i>), + // https://drafts.csswg.org/css-view-transitions-2/ + "view-transition-class": ViewTransitionClass(NoneOrCustomIdentList<'i>), + "view-transition-group": ViewTransitionGroup(ViewTransitionGroup<'i>), // https://drafts.csswg.org/css-color-adjust/ "color-scheme": ColorScheme(ColorScheme), + "print-color-adjust": PrintColorAdjust(PrintColorAdjust, VendorPrefix) / WebKit, } impl<'i, T: smallvec::Array, V: Parse<'i>> Parse<'i> for SmallVec { diff --git a/src/properties/prefix_handler.rs b/src/properties/prefix_handler.rs index 7ad2dfe8e..b42234777 100644 --- a/src/properties/prefix_handler.rs +++ b/src/properties/prefix_handler.rs @@ -73,13 +73,14 @@ define_prefixes! { ClipPath, BoxDecorationBreak, TextSizeAdjust, + PrintColorAdjust, } macro_rules! define_fallbacks { ( $( $name: ident$(($p: ident))?, )+ ) => { - paste::paste! { + pastey::paste! { #[derive(Default)] pub(crate) struct FallbackHandler { $( @@ -97,7 +98,7 @@ macro_rules! define_fallbacks { $( $p = context.targets.prefixes($p, Feature::$name); )? - if paste::paste! { self.[<$name:snake>] }.is_none() { + if pastey::paste! { self.[<$name:snake>] }.is_none() { let fallbacks = val.get_fallbacks(context.targets); #[allow(unused_variables)] let has_fallbacks = !fallbacks.is_empty(); @@ -112,10 +113,10 @@ macro_rules! define_fallbacks { )? } - if paste::paste! { self.[<$name:snake>] }.is_none() || matches!(context.targets.browsers, Some(targets) if !val.is_compatible(targets)) { - paste::paste! { self.[<$name:snake>] = Some(dest.len()) }; + if pastey::paste! { self.[<$name:snake>] }.is_none() || matches!(context.targets.browsers, Some(targets) if !val.is_compatible(targets)) { + pastey::paste! { self.[<$name:snake>] = Some(dest.len()) }; dest.push(Property::$name(val $(, $p)?)); - } else if let Some(index) = paste::paste! { self.[<$name:snake>] } { + } else if let Some(index) = pastey::paste! { self.[<$name:snake>] } { dest[index] = Property::$name(val $(, $p)?); } } @@ -138,7 +139,7 @@ macro_rules! define_fallbacks { } let val = get_prefixed!($($p)?); - (val, paste::paste! { &mut self.[<$name:snake>] }) + (val, pastey::paste! { &mut self.[<$name:snake>] }) } )+ _ => return false @@ -161,7 +162,7 @@ macro_rules! define_fallbacks { fn finalize(&mut self, _: &mut DeclarationList, _: &mut PropertyHandlerContext) { $( - paste::paste! { self.[<$name:snake>] = None }; + pastey::paste! { self.[<$name:snake>] = None }; )+ } } diff --git a/src/properties/size.rs b/src/properties/size.rs index 2475773ba..c6a5eb2e6 100644 --- a/src/properties/size.rs +++ b/src/properties/size.rs @@ -144,8 +144,7 @@ impl IsCompatible for Size { } Stretch(vp) => match *vp { VendorPrefix::None => Feature::StretchSize, - VendorPrefix::WebKit => Feature::WebkitFillAvailableSize, - VendorPrefix::Moz => Feature::MozAvailableSize, + VendorPrefix::WebKit | VendorPrefix::Moz => Feature::WebkitFillAvailableSize, _ => return false, } .is_compatible(browsers), @@ -278,8 +277,7 @@ impl IsCompatible for MaxSize { } Stretch(vp) => match *vp { VendorPrefix::None => Feature::StretchSize, - VendorPrefix::WebKit => Feature::WebkitFillAvailableSize, - VendorPrefix::Moz => Feature::MozAvailableSize, + VendorPrefix::WebKit | VendorPrefix::Moz => Feature::WebkitFillAvailableSize, _ => return false, } .is_compatible(browsers), @@ -433,6 +431,7 @@ impl<'i> PropertyHandler<'i> for SizeHandler { Property::MinInlineSize(size) => property!(min_inline_size, size, Logical), Property::MaxInlineSize(size) => property!(max_inline_size, size, Logical), Property::Unparsed(unparsed) => { + self.flush(dest, context); macro_rules! logical_unparsed { ($physical: ident) => { if logical_supported { diff --git a/src/properties/svg.rs b/src/properties/svg.rs index 16b384568..552dd2d25 100644 --- a/src/properties/svg.rs +++ b/src/properties/svg.rs @@ -211,6 +211,7 @@ pub enum Marker<'i> { /// A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property. #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[css(case = lower)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -249,6 +250,7 @@ pub enum ColorRendering { /// A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property. #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[css(case = lower)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -270,6 +272,7 @@ pub enum ShapeRendering { /// A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property. #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[css(case = lower)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", diff --git a/src/properties/transform.rs b/src/properties/transform.rs index c8f5a7f72..cc00e5783 100644 --- a/src/properties/transform.rs +++ b/src/properties/transform.rs @@ -7,7 +7,6 @@ use crate::error::{ParserError, PrinterError}; use crate::macros::enum_property; use crate::prefixes::Feature; use crate::printer::Printer; -use crate::stylesheet::PrinterOptions; use crate::traits::{Parse, PropertyHandler, ToCss, Zero}; use crate::values::{ angle::Angle, @@ -59,20 +58,6 @@ impl ToCss for TransformList { // TODO: Re-enable with a better solution // See: https://github.com/parcel-bundler/lightningcss/issues/288 - if dest.minify { - let mut base = String::new(); - self.to_css_base(&mut Printer::new( - &mut base, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))?; - - dest.write_str(&base)?; - - return Ok(()); - } // if dest.minify { // // Combine transforms into a single matrix. // if let Some(matrix) = self.to_matrix() { @@ -141,7 +126,13 @@ impl TransformList { where W: std::fmt::Write, { + let mut first = true; for item in &self.0 { + if first { + first = false; + } else { + dest.whitespace()?; + } item.to_css(dest)?; } Ok(()) @@ -1518,29 +1509,39 @@ impl Translate { /// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "lowercase") +)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] -pub struct Rotate { - /// Rotation around the x axis. - pub x: f32, - /// Rotation around the y axis. - pub y: f32, - /// Rotation around the z axis. - pub z: f32, - /// The angle of rotation. - pub angle: Angle, +pub enum Rotate { + /// The `none` keyword. + None, + + /// Rotation on the x, y, and z axes. + #[cfg_attr(feature = "serde", serde(untagged))] + XYZ { + /// Rotation around the x axis. + x: f32, + /// Rotation around the y axis. + y: f32, + /// Rotation around the z axis. + z: f32, + /// The angle of rotation. + angle: Angle, + }, } impl<'i> Parse<'i> for Rotate { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + // CSS Transforms 2 §5.1: + // "It must serialize as the keyword none if and only if none was originally specified." + // Keep `none` explicit so identity rotations (e.g. `0deg`) do not round-trip to `none`. + // https://drafts.csswg.org/css-transforms-2/#individual-transforms if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { - return Ok(Rotate { - x: 0.0, - y: 0.0, - z: 1.0, - angle: Angle::Deg(0.0), - }); + return Ok(Rotate::None); } let angle = input.try_parse(Angle::parse); @@ -1564,7 +1565,7 @@ impl<'i> Parse<'i> for Rotate { ) .unwrap_or((0.0, 0.0, 1.0)); let angle = angle.or_else(|_| Angle::parse(input))?; - Ok(Rotate { x, y, z, angle }) + Ok(Rotate::XYZ { x, y, z, angle }) } } @@ -1573,32 +1574,46 @@ impl ToCss for Rotate { where W: std::fmt::Write, { - if self.x == 0.0 && self.y == 0.0 && self.z == 1.0 && self.angle.is_zero() { - dest.write_str("none")?; - return Ok(()); - } - - if self.x == 1.0 && self.y == 0.0 && self.z == 0.0 { - dest.write_str("x ")?; - } else if self.x == 0.0 && self.y == 1.0 && self.z == 0.0 { - dest.write_str("y ")?; - } else if !(self.x == 0.0 && self.y == 0.0 && self.z == 1.0) { - self.x.to_css(dest)?; - dest.write_char(' ')?; - self.y.to_css(dest)?; - dest.write_char(' ')?; - self.z.to_css(dest)?; - dest.write_char(' ')?; + match self { + Rotate::None => dest.write_str("none"), + Rotate::XYZ { x, y, z, angle } => { + // CSS Transforms 2 §5.1: + // "If the axis is parallel with the x or y axes, it must serialize as the appropriate keyword." + // "If a rotation about the z axis ... must serialize as just an ." + // Normalize parallel vectors (including non-unit vectors); flip the angle for negative axis directions. + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + if *y == 0.0 && *z == 0.0 && *x != 0.0 { + let angle = if *x < 0.0 { angle.clone() * -1.0 } else { angle.clone() }; + dest.write_str("x ")?; + angle.to_css(dest) + } else if *x == 0.0 && *z == 0.0 && *y != 0.0 { + let angle = if *y < 0.0 { angle.clone() * -1.0 } else { angle.clone() }; + dest.write_str("y ")?; + angle.to_css(dest) + } else if *x == 0.0 && *y == 0.0 && *z != 0.0 { + let angle = if *z < 0.0 { angle.clone() * -1.0 } else { angle.clone() }; + angle.to_css(dest) + } else { + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest)?; + dest.write_char(' ')?; + z.to_css(dest)?; + dest.write_char(' ')?; + angle.to_css(dest) + } + } } - - self.angle.to_css(dest) } } impl Rotate { /// Converts the rotation to a transform function. pub fn to_transform(&self) -> Transform { - Transform::Rotate3d(self.x, self.y, self.z, self.angle.clone()) + match self { + Rotate::None => Transform::Rotate3d(0.0, 0.0, 1.0, Angle::Deg(0.0)), + Rotate::XYZ { x, y, z, angle } => Transform::Rotate3d(*x, *y, *z, angle.clone()), + } } } diff --git a/src/properties/transition.rs b/src/properties/transition.rs index 5890cbb36..8d6a66298 100644 --- a/src/properties/transition.rs +++ b/src/properties/transition.rs @@ -10,6 +10,7 @@ use crate::prefixes::Feature; use crate::printer::Printer; use crate::properties::masking::get_webkit_mask_property; use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero}; +use crate::values::ident::CustomIdent; use crate::values::{easing::EasingFunction, time::Time}; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] @@ -106,6 +107,50 @@ impl<'i> ToCss for Transition<'i> { } } +/// A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub enum ViewTransitionName<'i> { + /// The element will not participate independently in a view transition. + #[default] + None, + /// The `auto` keyword. + Auto, + /// A custom name. + #[cfg_attr(feature = "serde", serde(borrow, untagged))] + Custom(CustomIdent<'i>), +} + +/// A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub enum ViewTransitionGroup<'i> { + /// The `normal` keyword. + #[default] + Normal, + /// The `contain` keyword. + Contain, + /// The `nearest` keyword. + Nearest, + /// A custom group. + #[cfg_attr(feature = "serde", serde(borrow, untagged))] + Custom(CustomIdent<'i>), +} + #[derive(Default)] pub(crate) struct TransitionHandler<'i> { properties: Option<(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix)>, @@ -154,12 +199,15 @@ impl<'i> PropertyHandler<'i> for TransitionHandler<'i> { } match property { - TransitionProperty(val, vp) => property!(TransitionProperty, properties, val, vp), + TransitionProperty(val, vp) => { + let merged_values = merge_properties(val.iter()); + property!(TransitionProperty, properties, &merged_values, vp); + } TransitionDuration(val, vp) => property!(TransitionDuration, durations, val, vp), TransitionDelay(val, vp) => property!(TransitionDelay, delays, val, vp), TransitionTimingFunction(val, vp) => property!(TransitionTimingFunction, timing_functions, val, vp), Transition(val, vp) => { - let properties: SmallVec<[PropertyId; 1]> = val.iter().map(|b| b.property.clone()).collect(); + let properties: SmallVec<[PropertyId; 1]> = merge_properties(val.iter().map(|b| &b.property)); maybe_flush!(properties, &properties, vp); let durations: SmallVec<[Time; 1]> = val.iter().map(|b| b.duration.clone()).collect(); @@ -328,6 +376,23 @@ fn is_transition_property(property_id: &PropertyId) -> bool { } } +fn merge_properties<'i: 'a, 'a>(val: impl Iterator<'i>>) -> SmallVec<[PropertyId<'i>; 1]> { + let mut merged_values = SmallVec::<[PropertyId<'_>; 1]>::with_capacity(val.size_hint().1.unwrap_or(1)); + for p in val { + let without_prefix = p.with_prefix(VendorPrefix::empty()); + if let Some(idx) = merged_values + .iter() + .position(|c| c.with_prefix(VendorPrefix::empty()) == without_prefix) + { + merged_values[idx].add_prefix(p.prefix()); + } else { + merged_values.push(p.clone()); + } + } + + merged_values +} + fn expand_properties<'i>( properties: &mut SmallVec<[PropertyId<'i>; 1]>, context: &mut PropertyHandlerContext, diff --git a/src/properties/ui.rs b/src/properties/ui.rs index 11e1eade4..e0802c500 100644 --- a/src/properties/ui.rs +++ b/src/properties/ui.rs @@ -1,13 +1,12 @@ //! CSS properties related to user interface. -use crate::compat::Feature; use crate::context::PropertyHandlerContext; use crate::declaration::{DeclarationBlock, DeclarationList}; use crate::error::{ParserError, PrinterError}; use crate::macros::{define_shorthand, enum_property, shorthand_property}; use crate::printer::Printer; use crate::properties::{Property, PropertyId}; -use crate::targets::{Browsers, Targets}; +use crate::targets::{should_compile, Browsers, Targets}; use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss}; use crate::values::color::CssColor; use crate::values::number::CSSNumber; @@ -434,33 +433,44 @@ bitflags! { impl<'i> Parse<'i> for ColorScheme { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { let mut res = ColorScheme::empty(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &ident, - "normal" => return Ok(res), - "only" => res |= ColorScheme::Only, - "light" => res |= ColorScheme::Light, - "dark" => res |= ColorScheme::Dark, - _ => {} - }; + let mut has_any = false; - while let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) { - match_ignore_ascii_case! { &ident, - "normal" => return Err(input.new_custom_error(ParserError::InvalidValue)), - "only" => { - // Only must be at the start or the end, not in the middle. - if res.contains(ColorScheme::Only) { - return Err(input.new_custom_error(ParserError::InvalidValue)); - } - res |= ColorScheme::Only; - return Ok(res); - }, - "light" => res |= ColorScheme::Light, - "dark" => res |= ColorScheme::Dark, - _ => {} - }; + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + return Ok(res); } - Ok(res) + if input.try_parse(|input| input.expect_ident_matching("only")).is_ok() { + res |= ColorScheme::Only; + has_any = true; + } + + loop { + if input.try_parse(|input| input.expect_ident_matching("light")).is_ok() { + res |= ColorScheme::Light; + has_any = true; + continue; + } + + if input.try_parse(|input| input.expect_ident_matching("dark")).is_ok() { + res |= ColorScheme::Dark; + has_any = true; + continue; + } + + break; + } + + // Only is allowed at the start or the end. + if !res.contains(ColorScheme::Only) && input.try_parse(|input| input.expect_ident_matching("only")).is_ok() { + res |= ColorScheme::Only; + has_any = true; + } + + if has_any { + return Ok(res); + } + + Err(input.new_custom_error(ParserError::InvalidValue)) } } @@ -485,6 +495,10 @@ impl ToCss for ColorScheme { } if self.contains(ColorScheme::Only) { + // Avoid parsing `color-scheme: only` as `color-scheme: only` + if !self.intersects(ColorScheme::Light | ColorScheme::Dark) { + return dest.write_str("only"); + } dest.write_str(" only")?; } @@ -548,7 +562,7 @@ impl<'i> PropertyHandler<'i> for ColorSchemeHandler { ) -> bool { match property { Property::ColorScheme(color_scheme) => { - if !context.targets.is_compatible(Feature::LightDark) { + if should_compile!(context.targets, LightDark) { if color_scheme.contains(ColorScheme::Light) { dest.push(define_var("--lightningcss-light", Token::Ident("initial".into()))); dest.push(define_var("--lightningcss-dark", Token::WhiteSpace(" ".into()))); @@ -572,6 +586,16 @@ impl<'i> PropertyHandler<'i> for ColorSchemeHandler { fn finalize(&mut self, _: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {} } +enum_property! { + /// A value for the [print-color-adjust](https://drafts.csswg.org/css-color-adjust/#propdef-print-color-adjust) property. + pub enum PrintColorAdjust { + /// The user agent is allowed to make adjustments to the element as it deems appropriate. + Economy, + /// The user agent is not allowed to make adjustments to the element. + Exact, + } +} + #[inline] fn define_var<'i>(name: &'static str, value: Token<'static>) -> Property<'i> { Property::Custom(CustomProperty { diff --git a/src/rules/container.rs b/src/rules/container.rs index e08a824e6..97bb1b3a4 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -9,13 +9,14 @@ use crate::media_query::{ define_query_features, operation_to_css, parse_query_condition, to_css_with_parens_if_needed, FeatureToCss, MediaFeatureType, Operator, QueryCondition, QueryConditionFlags, QueryFeature, ValueType, }; -use crate::parser::DefaultAtRule; +use crate::parser::{DefaultAtRule, ParserOptions}; use crate::printer::Printer; +use crate::properties::custom::TokenList; use crate::properties::{Property, PropertyId}; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; use crate::targets::{Features, Targets}; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::CustomIdent; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -31,7 +32,7 @@ pub struct ContainerRule<'i, R = DefaultAtRule> { #[cfg_attr(feature = "serde", serde(borrow))] pub name: Option<'i>>, /// The container condition. - pub condition: ContainerCondition<'i>, + pub condition: Option<'i>>, /// The rules within the `@container` rule. pub rules: CssRuleList<'i, R>, /// The location of the rule in the source file. @@ -68,6 +69,12 @@ pub enum ContainerCondition<'i> { /// A style query. #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] Style(StyleQuery<'i>), + /// A scroll state query. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + ScrollState(ScrollStateQuery<'i>), + /// Unknown tokens. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + Unknown(TokenList<'i>), } /// A container query size feature. @@ -112,9 +119,13 @@ impl FeatureToCss for ContainerSizeFeatureId { )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum StyleQuery<'i> { - /// A style feature, implicitly parenthesized. + /// A property declaration. #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] - Feature(Property<'i>), + Declaration(Property<'i>), + /// A property name, without a value. + /// This matches if the property value is different from the initial value. + #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::"))] + Property(PropertyId<'i>), /// A negation of a condition. #[cfg_attr(feature = "visitor", skip_type)] #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] @@ -129,10 +140,68 @@ pub enum StyleQuery<'i> { }, } +/// Represents a scroll state query within a container condition. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type", rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum ScrollStateQuery<'i> { + /// A size container feature, implicitly parenthesized. + #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] + Feature(ScrollStateFeature<'i>), + /// A negation of a condition. + #[cfg_attr(feature = "visitor", skip_type)] + #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] + Not(Box<'i>>), + /// A set of joint operations. + #[cfg_attr(feature = "visitor", skip_type)] + Operation { + /// The operator for the conditions. + operator: Operator, + /// The conditions for the operator. + conditions: Vec<'i>>, + }, +} + +/// A container query size feature. +pub type ScrollStateFeature<'i> = QueryFeature<'i, ScrollStateFeatureId>; + +define_query_features! { + /// A container query scroll state feature identifier. + pub enum ScrollStateFeatureId { + /// The [stuck](https://drafts.csswg.org/css-conditional-5/#stuck) scroll state feature. + "stuck": Stuck = Ident, + /// The [snapped](https://drafts.csswg.org/css-conditional-5/#snapped) scroll state feature. + "snapped": Snapped = Ident, + /// The [scrollable](https://drafts.csswg.org/css-conditional-5/#scrollable) scroll state feature. + "scrollable": Scrollable = Ident, + /// The [scrolled](https://drafts.csswg.org/css-conditional-5/#scrolled) scroll state feature. + "scrolled": Scrolled = Ident, + } +} + +impl FeatureToCss for ScrollStateFeatureId { + fn to_css_with_prefix(&self, prefix: &str, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + dest.write_str(prefix)?; + self.to_css(dest) + } +} + impl<'i> QueryCondition<'i> for ContainerCondition<'i> { #[inline] - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - let feature = QueryFeature::parse(input)?; + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + let feature = QueryFeature::parse_with_options(input, options)?; Ok(Self::Feature(feature)) } @@ -146,13 +215,33 @@ impl<'i> QueryCondition<'i> for ContainerCondition<'i> { Self::Operation { operator, conditions } } - fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + fn parse_style_query<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { input.parse_nested_block(|input| { - if let Ok(res) = input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR)) { + if let Ok(res) = + input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR, options)) + { return Ok(Self::Style(res)); } - Ok(Self::Style(StyleQuery::parse_feature(input)?)) + Ok(Self::Style(StyleQuery::parse_feature(input, options)?)) + }) + } + + fn parse_scroll_state_query<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + input.parse_nested_block(|input| { + if let Ok(res) = + input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR, options)) + { + return Ok(Self::ScrollState(res)); + } + + Ok(Self::ScrollState(ScrollStateQuery::parse_feature(input, options)?)) }) } @@ -162,19 +251,56 @@ impl<'i> QueryCondition<'i> for ContainerCondition<'i> { ContainerCondition::Operation { operator, .. } => Some(*operator) != parent_operator, ContainerCondition::Feature(f) => f.needs_parens(parent_operator, targets), ContainerCondition::Style(_) => false, + ContainerCondition::ScrollState(_) => false, + ContainerCondition::Unknown(_) => false, + } + } +} + +impl<'i> QueryCondition<'i> for ScrollStateQuery<'i> { + #[inline] + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + let feature = QueryFeature::parse_with_options(input, options)?; + Ok(Self::Feature(feature)) + } + + #[inline] + fn create_negation(condition: Box) -> Self { + Self::Not(condition) + } + + #[inline] + fn create_operation(operator: Operator, conditions: Vec) -> Self { + Self::Operation { operator, conditions } + } + + fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { + match self { + ScrollStateQuery::Not(_) => true, + ScrollStateQuery::Operation { operator, .. } => Some(*operator) != parent_operator, + ScrollStateQuery::Feature(f) => f.needs_parens(parent_operator, targets), } } } impl<'i> QueryCondition<'i> for StyleQuery<'i> { #[inline] - fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + fn parse_feature<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { let property_id = PropertyId::parse(input)?; - input.expect_colon()?; - input.skip_whitespace(); - let feature = Self::Feature(Property::parse(property_id, input, &Default::default())?); - let _ = input.try_parse(|input| parse_important(input)); - Ok(feature) + if input.try_parse(|input| input.expect_colon()).is_ok() { + input.skip_whitespace(); + let feature = Self::Declaration(Property::parse(property_id, input, options)?); + let _ = input.try_parse(|input| parse_important(input)); + Ok(feature) + } else { + Ok(Self::Property(property_id)) + } } #[inline] @@ -191,14 +317,34 @@ impl<'i> QueryCondition<'i> for StyleQuery<'i> { match self { StyleQuery::Not(_) => true, StyleQuery::Operation { operator, .. } => Some(*operator) != parent_operator, - StyleQuery::Feature(_) => true, + StyleQuery::Declaration(_) | StyleQuery::Property(_) => true, } } } -impl<'i> Parse<'i> for ContainerCondition<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - parse_query_condition(input, QueryConditionFlags::ALLOW_OR | QueryConditionFlags::ALLOW_STYLE) +impl<'i> ParseWithOptions<'i> for ContainerCondition<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result<'i, ParserError<'i>>> { + input + .try_parse(|input| { + parse_query_condition( + input, + QueryConditionFlags::ALLOW_OR + | QueryConditionFlags::ALLOW_STYLE + | QueryConditionFlags::ALLOW_SCROLL_STATE, + options, + ) + }) + .or_else(|e| { + if options.error_recovery { + options.warn(e); + Ok(ContainerCondition::Unknown(TokenList::parse(input, options, 0)?)) + } else { + Err(e) + } + }) } } @@ -211,7 +357,7 @@ impl<'i> ToCss for ContainerCondition<'i> { ContainerCondition::Feature(ref f) => f.to_css(dest), ContainerCondition::Not(ref c) => { dest.write_str("not ")?; - to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) } ContainerCondition::Operation { ref conditions, @@ -222,6 +368,19 @@ impl<'i> ToCss for ContainerCondition<'i> { query.to_css(dest)?; dest.write_char(')') } + ContainerCondition::ScrollState(ref query) => { + let needs_parens = !matches!(query, ScrollStateQuery::Feature(_)); + dest.write_str("scroll-state")?; + if needs_parens { + dest.write_char('(')?; + } + query.to_css(dest)?; + if needs_parens { + dest.write_char(')')?; + } + Ok(()) + } + ContainerCondition::Unknown(ref tokens) => tokens.to_css(dest, false), } } } @@ -232,10 +391,11 @@ impl<'i> ToCss for StyleQuery<'i> { W: std::fmt::Write, { match *self { - StyleQuery::Feature(ref f) => f.to_css(dest, false), + StyleQuery::Declaration(ref f) => f.to_css(dest, false), + StyleQuery::Property(ref f) => f.to_css(dest), StyleQuery::Not(ref c) => { dest.write_str("not ")?; - to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) } StyleQuery::Operation { ref conditions, @@ -245,6 +405,25 @@ impl<'i> ToCss for StyleQuery<'i> { } } +impl<'i> ToCss for ScrollStateQuery<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match *self { + ScrollStateQuery::Feature(ref f) => f.to_css(dest), + ScrollStateQuery::Not(ref c) => { + dest.write_str("not ")?; + to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current)) + } + ScrollStateQuery::Operation { + ref conditions, + operator, + } => operation_to_css(operator, conditions, dest), + } + } +} + /// A [``](https://drafts.csswg.org/css-contain-3/#typedef-container-name) in a `@container` rule. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -268,7 +447,15 @@ impl<'i> ToCss for ContainerName<'i> { where W: std::fmt::Write, { - self.0.to_css(dest) + // Container name should not be hashed + // https://github.com/vercel/next.js/issues/71233 + self.0.to_css_with_options( + dest, + match &dest.css_module { + Some(css_module) => css_module.config.container, + None => false, + }, + ) } } @@ -291,16 +478,22 @@ impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); dest.write_str("@container ")?; + let has_condition = self.condition.is_some(); + if let Some(name) = &self.name { name.to_css(dest)?; - dest.write_char(' ')?; + if has_condition { + dest.write_char(' ')?; + } } - // Don't downlevel range syntax in container queries. - let exclude = dest.targets.exclude; - dest.targets.exclude.insert(Features::MediaQueries); - self.condition.to_css(dest)?; - dest.targets.exclude = exclude; + if let Some(condition) = &self.condition { + // Don't downlevel range syntax in container queries. + let exclude = dest.targets.current.exclude; + dest.targets.current.exclude.insert(Features::MediaQueries); + condition.to_css(dest)?; + dest.targets.current.exclude = exclude; + } dest.whitespace()?; dest.write_char('{')?; diff --git a/src/rules/font_feature_values.rs b/src/rules/font_feature_values.rs new file mode 100644 index 000000000..9aeedbf17 --- /dev/null +++ b/src/rules/font_feature_values.rs @@ -0,0 +1,331 @@ +//! The `@font-feature-values` rule. + +use super::Location; +use crate::error::{ParserError, PrinterError}; +use crate::parser::ParserOptions; +use crate::printer::Printer; +use crate::properties::font::FamilyName; +use crate::traits::{Parse, ToCss}; +use crate::values::ident::Ident; +use crate::values::number::CSSInteger; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; +use cssparser::*; +use indexmap::IndexMap; +use smallvec::SmallVec; +use std::fmt::Write; + +/// A [@font-feature-values](https://drafts.csswg.org/css-fonts/#font-feature-values) rule. +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub struct FontFeatureValuesRule<'i> { + /// The name of the font feature values. + #[cfg_attr(feature = "serde", serde(borrow))] + pub name: Vec<'i>>, + /// The rules within the `@font-feature-values` rule. + pub rules: IndexMap<'i>>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +impl<'i> FontFeatureValuesRule<'i> { + pub(crate) fn parse<'t, 'o>( + family_names: Vec<'i>>, + input: &mut Parser<'i, 't>, + loc: Location, + options: &ParserOptions<'o, 'i>, + ) -> Result<'i, ParserError<'i>>> { + let mut rules = IndexMap::new(); + let mut rule_parser = FontFeatureValuesRuleParser { + rules: &mut rules, + options, + }; + let mut parser = RuleBodyParser::new(input, &mut rule_parser); + + while let Some(decl_or_rule) = parser.next() { + if let Err((err, _)) = decl_or_rule { + if parser.parser.options.error_recovery { + parser.parser.options.warn(err); + continue; + } + return Err(err); + } + } + + Ok(FontFeatureValuesRule { + name: family_names, + rules, + loc, + }) + } +} + +struct FontFeatureValuesRuleParser<'a, 'o, 'i> { + rules: &'a mut IndexMap<'i>>, + options: &'a ParserOptions<'o, 'i>, +} + +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for FontFeatureValuesRuleParser<'a, 'o, 'i> { + type Declaration = (); + type Error = ParserError<'i>; +} + +impl<'a, 'o, 'i> cssparser::AtRuleParser<'i> for FontFeatureValuesRuleParser<'a, 'o, 'i> { + type Prelude = FontFeatureSubruleType; + type AtRule = (); + type Error = ParserError<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<'i, Self::Error>> { + let loc = input.current_source_location(); + FontFeatureSubruleType::parse_string(&name) + .map_err(|_| loc.new_custom_error(ParserError::AtRuleInvalid(name.clone().into()))) + } + + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<'i, Self::Error>> { + let loc = start.source_location(); + let mut decls = IndexMap::new(); + let mut has_existing = false; + let declarations = if let Some(rule) = self.rules.get_mut(&prelude) { + has_existing = true; + &mut rule.declarations + } else { + &mut decls + }; + let mut decl_parser = FontFeatureDeclarationParser { declarations }; + let mut parser = RuleBodyParser::new(input, &mut decl_parser); + while let Some(decl) = parser.next() { + if let Err((err, _)) = decl { + if self.options.error_recovery { + self.options.warn(err); + continue; + } + return Err(err); + } + } + + if !has_existing { + self.rules.insert( + prelude, + FontFeatureSubrule { + name: prelude, + declarations: decls, + loc: Location { + source_index: self.options.source_index, + line: loc.line, + column: loc.column, + }, + }, + ); + } + + Ok(()) + } +} + +impl<'a, 'o, 'i> QualifiedRuleParser<'i> for FontFeatureValuesRuleParser<'a, 'o, 'i> { + type Prelude = (); + type QualifiedRule = (); + type Error = ParserError<'i>; +} + +impl<'a, 'o, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for FontFeatureValuesRuleParser<'a, 'o, 'i> { + fn parse_declarations(&self) -> bool { + false + } + + fn parse_qualified(&self) -> bool { + false + } +} + +impl<'i> ToCss for FontFeatureValuesRule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + dest.write_str("@font-feature-values ")?; + self.name.to_css(dest)?; + dest.whitespace()?; + dest.write_char('{')?; + if !self.rules.is_empty() { + dest.newline()?; + for rule in self.rules.values() { + rule.to_css(dest)?; + dest.newline()?; + } + } + dest.write_char('}') + } +} + +impl<'i> FontFeatureValuesRule<'i> { + pub(crate) fn merge(&mut self, other: &FontFeatureValuesRule<'i>) { + debug_assert_eq!(self.name, other.name); + for (prelude, rule) in &other.rules { + if let Some(existing) = self.rules.get_mut(prelude) { + existing + .declarations + .extend(rule.declarations.iter().map(|(k, v)| (k.clone(), v.clone()))); + } else { + self.rules.insert(*prelude, rule.clone()); + } + } + } +} + +/// The name of the `@font-feature-values` sub-rule. +/// font-feature-value-type = <@stylistic> | <@historical-forms> | <@styleset> | <@character-variant> +/// | <@swash> | <@ornaments> | <@annotation> +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub enum FontFeatureSubruleType { + /// @stylistic = @stylistic { } + Stylistic, + /// @historical-forms = @historical-forms { } + HistoricalForms, + /// @styleset = @styleset { } + Styleset, + /// @character-variant = @character-variant { } + CharacterVariant, + /// @swash = @swash { } + Swash, + /// @ornaments = @ornaments { } + Ornaments, + /// @annotation = @annotation { } + Annotation, +} + +/// A sub-rule of `@font-feature-values` +/// https://drafts.csswg.org/css-fonts/#font-feature-values-syntax +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct FontFeatureSubrule<'i> { + /// The name of the `@font-feature-values` sub-rule. + pub name: FontFeatureSubruleType, + /// The declarations within the `@font-feature-values` sub-rules. + #[cfg_attr(feature = "serde", serde(borrow))] + pub declarations: IndexMap<'i>, SmallVec<[CSSInteger; 1]>>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +impl<'i> ToCss for FontFeatureSubrule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + dest.write_char('@')?; + self.name.to_css(dest)?; + dest.write_char('{')?; + dest.indent(); + let len = self.declarations.len(); + for (i, (name, value)) in self.declarations.iter().enumerate() { + dest.newline()?; + name.to_css(dest)?; + dest.delim(':', false)?; + + let mut first = true; + for index in value { + if first { + first = false; + } else { + dest.write_char(' ')?; + } + index.to_css(dest)?; + } + + if i != len - 1 || !dest.minify { + dest.write_char(';')?; + } + } + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } +} + +struct FontFeatureDeclarationParser<'a, 'i> { + declarations: &'a mut IndexMap<'i>, SmallVec<[CSSInteger; 1]>>, +} + +impl<'a, 'i> cssparser::DeclarationParser<'i> for FontFeatureDeclarationParser<'a, 'i> { + type Declaration = (); + type Error = ParserError<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut cssparser::Parser<'i, 't>, + ) -> Result<'i, Self::Error>> { + let mut indices = SmallVec::new(); + loop { + if let Ok(value) = CSSInteger::parse(input) { + indices.push(value); + } else { + break; + } + } + + if indices.is_empty() { + return Err(input.new_custom_error(ParserError::InvalidValue)); + } + + self.declarations.insert(Ident(name.into()), indices); + Ok(()) + } +} + +/// Default methods reject all at rules. +impl<'a, 'i> AtRuleParser<'i> for FontFeatureDeclarationParser<'a, 'i> { + type Prelude = (); + type AtRule = (); + type Error = ParserError<'i>; +} + +impl<'a, 'i> QualifiedRuleParser<'i> for FontFeatureDeclarationParser<'a, 'i> { + type Prelude = (); + type QualifiedRule = (); + type Error = ParserError<'i>; +} + +impl<'a, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for FontFeatureDeclarationParser<'a, 'i> { + fn parse_qualified(&self) -> bool { + false + } + + fn parse_declarations(&self) -> bool { + true + } +} diff --git a/src/rules/font_palette_values.rs b/src/rules/font_palette_values.rs index 95ca014ca..af06c487f 100644 --- a/src/rules/font_palette_values.rs +++ b/src/rules/font_palette_values.rs @@ -261,7 +261,7 @@ impl<'i> FontPaletteValuesRule<'i> { // Generate color fallbacks. let mut fallbacks = ColorFallbackKind::empty(); for o in override_colors { - fallbacks |= o.color.get_necessary_fallbacks(*context.targets); + fallbacks |= o.color.get_necessary_fallbacks(context.targets.current); } if fallbacks.contains(ColorFallbackKind::RGB) { diff --git a/src/rules/media.rs b/src/rules/media.rs index 1340bea33..c398b9b01 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -39,7 +39,7 @@ impl<'i, T: Clone> MediaRule<'i, T> { self.query.transform_custom_media(self.loc, custom_media)?; } - self.query.transform_resolution(*context.targets); + self.query.transform_resolution(context.targets.current); Ok(self.rules.0.is_empty() || self.query.never_matches()) } } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 89ac30870..5598c96f5 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -41,6 +41,7 @@ pub mod counter_style; pub mod custom_media; pub mod document; pub mod font_face; +pub mod font_feature_values; pub mod font_palette_values; pub mod import; pub mod keyframes; @@ -55,8 +56,10 @@ pub mod starting_style; pub mod style; pub mod supports; pub mod unknown; +pub mod view_transition; pub mod viewport; +use self::font_feature_values::FontFeatureValuesRule; use self::font_palette_values::FontPaletteValuesRule; use self::layer::{LayerBlockRule, LayerStatementRule}; use self::property::PropertyRule; @@ -70,7 +73,7 @@ use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; use crate::selector::{is_compatible, is_equivalent, Component, Selector, SelectorList}; use crate::stylesheet::ParserOptions; -use crate::targets::Targets; +use crate::targets::{should_compile, TargetsWithSupportsScope}; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -87,7 +90,7 @@ use itertools::Itertools; use keyframes::KeyframesRule; use media::MediaRule; use namespace::NamespaceRule; -use nesting::NestingRule; +use nesting::{NestedDeclarationsRule, NestingRule}; use page::PageRule; use scope::ScopeRule; use smallvec::{smallvec, SmallVec}; @@ -97,6 +100,7 @@ use std::hash::{BuildHasherDefault, Hasher}; use style::StyleRule; use supports::SupportsRule; use unknown::UnknownAtRule; +use view_transition::ViewTransitionRule; use viewport::ViewportRule; #[derive(Clone)] @@ -146,6 +150,8 @@ pub enum CssRule<'i, R = DefaultAtRule> { FontFace(FontFaceRule<'i>), /// A `@font-palette-values` rule. FontPaletteValues(FontPaletteValuesRule<'i>), + /// A `@font-feature-values` rule. + FontFeatureValues(FontFeatureValuesRule<'i>), /// A `@page` rule. Page(PageRule<'i>), /// A `@supports` rule. @@ -158,6 +164,8 @@ pub enum CssRule<'i, R = DefaultAtRule> { MozDocument(MozDocumentRule<'i, R>), /// A `@nest` rule. Nesting(NestingRule<'i, R>), + /// A nested declarations rule. + NestedDeclarations(NestedDeclarationsRule<'i>), /// A `@viewport` rule. Viewport(ViewportRule<'i>), /// A `@custom-media` rule. @@ -174,6 +182,8 @@ pub enum CssRule<'i, R = DefaultAtRule> { Scope(ScopeRule<'i, R>), /// A `@starting-style` rule. StartingStyle(StartingStyleRule<'i, R>), + /// A `@view-transition` rule. + ViewTransition(ViewTransitionRule<'i>), /// A placeholder for a rule that was removed. Ignored, /// An unknown at-rule. @@ -199,7 +209,7 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul struct PartialRule<'de> { rule_type: CowArcStr<'de>, - content: serde::__private::de::Content<'de>, + content: serde_content::Value<'de>, } struct CssRuleVisitor; @@ -216,7 +226,7 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul A: serde::de::MapAccess<'de>, { let mut rule_type: Option<'de>> = None; - let mut value: Option = None; + let mut value: Option = None; while let Some(key) = map.next_key()? { match key { Field::Type => { @@ -235,96 +245,122 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul } let partial = deserializer.deserialize_map(CssRuleVisitor)?; - let deserializer = serde::__private::de::ContentDeserializer::new(partial.content); + let deserializer = serde_content::Deserializer::new(partial.content).coerce_numbers(); match partial.rule_type.as_ref() { "media" => { - let rule = MediaRule::deserialize(deserializer)?; + let rule = MediaRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Media(rule)) } "import" => { - let rule = ImportRule::deserialize(deserializer)?; + let rule = ImportRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Import(rule)) } "style" => { - let rule = StyleRule::deserialize(deserializer)?; + let rule = StyleRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Style(rule)) } "keyframes" => { - let rule = KeyframesRule::deserialize(deserializer)?; + let rule = + KeyframesRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Keyframes(rule)) } "font-face" => { - let rule = FontFaceRule::deserialize(deserializer)?; + let rule = FontFaceRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::FontFace(rule)) } "font-palette-values" => { - let rule = FontPaletteValuesRule::deserialize(deserializer)?; + let rule = + FontPaletteValuesRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::FontPaletteValues(rule)) } + "font-feature-values" => { + let rule = + FontFeatureValuesRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; + Ok(CssRule::FontFeatureValues(rule)) + } "page" => { - let rule = PageRule::deserialize(deserializer)?; + let rule = PageRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Page(rule)) } "supports" => { - let rule = SupportsRule::deserialize(deserializer)?; + let rule = SupportsRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Supports(rule)) } "counter-style" => { - let rule = CounterStyleRule::deserialize(deserializer)?; + let rule = + CounterStyleRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::CounterStyle(rule)) } "namespace" => { - let rule = NamespaceRule::deserialize(deserializer)?; + let rule = + NamespaceRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Namespace(rule)) } "moz-document" => { - let rule = MozDocumentRule::deserialize(deserializer)?; + let rule = + MozDocumentRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::MozDocument(rule)) } "nesting" => { - let rule = NestingRule::deserialize(deserializer)?; + let rule = NestingRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Nesting(rule)) } + "nested-declarations" => { + let rule = NestedDeclarationsRule::deserialize(deserializer) + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + Ok(CssRule::NestedDeclarations(rule)) + } "viewport" => { - let rule = ViewportRule::deserialize(deserializer)?; + let rule = ViewportRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Viewport(rule)) } "custom-media" => { - let rule = CustomMediaRule::deserialize(deserializer)?; + let rule = + CustomMediaRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::CustomMedia(rule)) } "layer-statement" => { - let rule = LayerStatementRule::deserialize(deserializer)?; + let rule = + LayerStatementRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::LayerStatement(rule)) } "layer-block" => { - let rule = LayerBlockRule::deserialize(deserializer)?; + let rule = + LayerBlockRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::LayerBlock(rule)) } "property" => { - let rule = PropertyRule::deserialize(deserializer)?; + let rule = PropertyRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Property(rule)) } "container" => { - let rule = ContainerRule::deserialize(deserializer)?; + let rule = + ContainerRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Container(rule)) } "scope" => { - let rule = ScopeRule::deserialize(deserializer)?; + let rule = ScopeRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Scope(rule)) } "starting-style" => { - let rule = StartingStyleRule::deserialize(deserializer)?; + let rule = + StartingStyleRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::StartingStyle(rule)) } + "view-transition" => { + let rule = + ViewTransitionRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; + Ok(CssRule::ViewTransition(rule)) + } "ignored" => Ok(CssRule::Ignored), "unknown" => { - let rule = UnknownAtRule::deserialize(deserializer)?; + let rule = + UnknownAtRule::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Unknown(rule)) } "custom" => { - let rule = R::deserialize(deserializer)?; + let rule = R::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(CssRule::Custom(rule)) } t => Err(serde::de::Error::unknown_variant(t, &[])), @@ -344,12 +380,14 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { CssRule::Keyframes(keyframes) => keyframes.to_css(dest), CssRule::FontFace(font_face) => font_face.to_css(dest), CssRule::FontPaletteValues(f) => f.to_css(dest), + CssRule::FontFeatureValues(font_feature_values) => font_feature_values.to_css(dest), CssRule::Page(font_face) => font_face.to_css(dest), CssRule::Supports(supports) => supports.to_css(dest), CssRule::CounterStyle(counter_style) => counter_style.to_css(dest), CssRule::Namespace(namespace) => namespace.to_css(dest), CssRule::MozDocument(document) => document.to_css(dest), CssRule::Nesting(nesting) => nesting.to_css(dest), + CssRule::NestedDeclarations(nested) => nested.to_css(dest), CssRule::Viewport(viewport) => viewport.to_css(dest), CssRule::CustomMedia(custom_media) => custom_media.to_css(dest), CssRule::LayerStatement(layer) => layer.to_css(dest), @@ -358,6 +396,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { CssRule::StartingStyle(rule) => rule.to_css(dest), CssRule::Container(container) => container.to_css(dest), CssRule::Scope(scope) => scope.to_css(dest), + CssRule::ViewTransition(rule) => rule.to_css(dest), CssRule::Unknown(unknown) => unknown.to_css(dest), CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError { kind: PrinterErrorKind::FmtError, @@ -481,13 +520,14 @@ impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for Css } pub(crate) struct MinifyContext<'a, 'i> { - pub targets: &'a Targets, + pub targets: TargetsWithSupportsScope, pub handler: &'a mut DeclarationHandler<'i>, pub important_handler: &'a mut DeclarationHandler<'i>, pub handler_context: PropertyHandlerContext<'i, 'a>, pub unused_symbols: &'a HashSet, pub custom_media: Option<'i>, CustomMediaRule<'i>>>, pub css_modules: bool, + pub pure_css_modules: bool, } impl<'i, T: Clone> CssRuleList<'i, T> { @@ -498,7 +538,9 @@ impl<'i, T: Clone> CssRuleList<'i, T> { ) -> Result<(), MinifyError> { let mut keyframe_rules = HashMap::new(); let mut layer_rules = HashMap::new(); + let mut has_layers = false; let mut property_rules = HashMap::new(); + let mut font_feature_values_rules = Vec::new(); let mut style_rules = HashMap::with_capacity_and_hasher(self.0.len(), BuildHasherDefault::::default()); let mut rules = Vec::new(); @@ -515,7 +557,8 @@ impl<'i, T: Clone> CssRuleList<'i, T> { macro_rules! set_prefix { ($keyframes: ident) => { - $keyframes.vendor_prefix = context.targets.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes); + $keyframes.vendor_prefix = + context.targets.current.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes); }; } @@ -539,7 +582,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { set_prefix!(keyframes); keyframe_rules.insert(keyframes.name.clone(), rules.len()); - let fallbacks = keyframes.get_fallbacks(context.targets); + let fallbacks = keyframes.get_fallbacks(&context.targets.current); rules.push(rule); rules.extend(fallbacks); continue; @@ -601,6 +644,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { } layer_rules.insert(name.clone(), rules.len()); + has_layers = true; } } CssRule::LayerStatement(layer) => { @@ -609,6 +653,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { for name in &layer.names { if !layer_rules.contains_key(name) { layer_rules.insert(name.clone(), rules.len()); + has_layers = true; rules.push(CssRule::LayerBlock(LayerBlockRule { name: Some(name.clone()), rules: CssRuleList(vec![]), @@ -627,14 +672,14 @@ impl<'i, T: Clone> CssRuleList<'i, T> { // If some of the selectors in this rule are not compatible with the targets, // we need to either wrap in :is() or split them into multiple rules. let incompatible = if style.selectors.0.len() > 1 - && context.targets.should_compile_selectors() - && !style.is_compatible(*context.targets) + && context.targets.current.should_compile_selectors() + && !style.is_compatible(context.targets.current) { // The :is() selector accepts a forgiving selector list, so use that if possible. // Note that :is() does not allow pseudo elements, so we need to check for that. // In addition, :is() takes the highest specificity of its arguments, so if the selectors // have different weights, we need to split them into separate rules as well. - if context.targets.is_compatible(crate::compat::Feature::IsSelector) + if context.targets.current.is_compatible(crate::compat::Feature::IsSelector) && !style.selectors.0.iter().any(|selector| selector.has_pseudo_element()) && style.selectors.0.iter().map(|selector| selector.specificity()).all_equal() { @@ -653,7 +698,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { .cloned() .partition::<[Selector; 1]>, _>(|selector| { let list = SelectorList::new(smallvec![selector.clone()]); - is_compatible(&list.0, *context.targets) + is_compatible(&list.0, context.targets.current) }); style.selectors = SelectorList::new(compatible); incompatible @@ -795,6 +840,11 @@ impl<'i, T: Clone> CssRuleList<'i, T> { continue; } } + CssRule::NestedDeclarations(nested) => { + if nested.minify(context, parent_is_unused) { + continue; + } + } CssRule::StartingStyle(rule) => { if rule.minify(context, parent_is_unused)? { continue; @@ -807,23 +857,40 @@ impl<'i, T: Clone> CssRuleList<'i, T> { f.minify(context, parent_is_unused); - let fallbacks = f.get_fallbacks(*context.targets); + let fallbacks = f.get_fallbacks(context.targets.current); rules.push(rule); rules.extend(fallbacks); continue; } + CssRule::FontFeatureValues(rule) => { + if let Some(index) = font_feature_values_rules + .iter() + .find(|index| matches!(&rules[**index], CssRule::FontFeatureValues(r) if r.name == rule.name)) + { + if let CssRule::FontFeatureValues(existing) = &mut rules[*index] { + existing.merge(rule); + } + continue; + } else { + font_feature_values_rules.push(rules.len()); + } + } CssRule::Property(property) => { if context.unused_symbols.contains(property.name.0.as_ref()) { continue; } - if let Some(index) = property_rules.get_mut(&property.name) { + if let Some(index) = property_rules.get(&property.name) { rules[*index] = rule; continue; } else { property_rules.insert(property.name.clone(), rules.len()); } } + CssRule::Import(_) => { + // @layer blocks can't be inlined into layers declared before imports. + layer_rules.clear(); + } _ => {} } @@ -832,7 +899,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> { // Optimize @layer rules. Combine subsequent empty layer blocks into a single @layer statement // so that layers are declared in the correct order. - if !layer_rules.is_empty() { + if has_layers { let mut declared_layers = HashSet::new(); let mut layer_statement = None; for index in 0..rules.len() { @@ -898,8 +965,8 @@ fn merge_style_rules<'i, T>( ) -> bool { // Merge declarations if the selectors are equivalent, and both are compatible with all targets. if style.selectors == last_style_rule.selectors - && style.is_compatible(*context.targets) - && last_style_rule.is_compatible(*context.targets) + && style.is_compatible(context.targets.current) + && last_style_rule.is_compatible(context.targets.current) && style.rules.0.is_empty() && last_style_rule.rules.0.is_empty() && (!context.css_modules || style.loc.source_index == last_style_rule.loc.source_index) @@ -928,7 +995,7 @@ fn merge_style_rules<'i, T>( { // If the new rule is unprefixed, replace the prefixes of the last rule. // Otherwise, add the new prefix. - if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() { + if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { last_style_rule.vendor_prefix = style.vendor_prefix; } else { last_style_rule.vendor_prefix |= style.vendor_prefix; @@ -937,9 +1004,9 @@ fn merge_style_rules<'i, T>( } // Append the selectors to the last rule if the declarations are the same, and all selectors are compatible. - if style.is_compatible(*context.targets) && last_style_rule.is_compatible(*context.targets) { + if style.is_compatible(context.targets.current) && last_style_rule.is_compatible(context.targets.current) { last_style_rule.selectors.0.extend(style.selectors.0.drain(..)); - if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() { + if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { last_style_rule.vendor_prefix = style.vendor_prefix; } else { last_style_rule.vendor_prefix |= style.vendor_prefix; @@ -958,7 +1025,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { let mut first = true; let mut last_without_block = false; - for rule in &self.0 { + for (i, rule) in self.0.iter().enumerate() { if let CssRule::Ignored = &rule { continue; } @@ -994,6 +1061,16 @@ impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { dest.newline()?; } rule.to_css(dest)?; + + // If this is an invisible nested declarations rule, and not the last rule in the block, add a semicolon. + if dest.minify + && !should_compile!(dest.targets.current, Nesting) + && matches!(rule, CssRule::NestedDeclarations(_)) + && i != self.0.len() - 1 + { + dest.write_char(';')?; + } + last_without_block = matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index 2473cc2a8..679e586aa 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -1,14 +1,20 @@ //! The `@nest` rule. +use smallvec::SmallVec; + use super::style::StyleRule; use super::Location; use super::MinifyContext; +use crate::context::DeclarationContext; +use crate::declaration::DeclarationBlock; use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; +use crate::targets::should_compile; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; + /// A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule. #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -47,3 +53,71 @@ impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> { self.style.to_css(dest) } } + +/// A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule. +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub struct NestedDeclarationsRule<'i> { + /// The style rule that defines the selector and declarations for the `@nest` rule. + #[cfg_attr(feature = "serde", serde(borrow))] + pub declarations: DeclarationBlock<'i>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +impl<'i> NestedDeclarationsRule<'i> { + pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool) -> bool { + if parent_is_unused { + return true; + } + + context.handler_context.context = DeclarationContext::StyleRule; + self + .declarations + .minify(context.handler, context.important_handler, &mut context.handler_context); + context.handler_context.context = DeclarationContext::None; + return false; + } +} + +impl<'i> ToCss for NestedDeclarationsRule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + + if should_compile!(dest.targets.current, Nesting) { + if let Some(context) = dest.context() { + let has_printable_declarations = self.declarations.has_printable_declarations(); + if has_printable_declarations { + dest.with_parent_context(|dest| context.selectors.to_css(dest))?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + dest.newline()?; + } + + self + .declarations + .to_css_declarations(dest, false, &context.selectors, self.loc.source_index)?; + + if has_printable_declarations { + dest.dedent(); + dest.newline()?; + dest.write_char('}')?; + } + return Ok(()); + } + } + + self + .declarations + .to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0) + } +} diff --git a/src/rules/scope.rs b/src/rules/scope.rs index e8a8d53bf..ab7d10048 100644 --- a/src/rules/scope.rs +++ b/src/rules/scope.rs @@ -5,7 +5,7 @@ use super::{CssRuleList, MinifyContext}; use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::selector::SelectorList; +use crate::selector::{is_pure_css_modules_selector, SelectorList}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -39,6 +39,26 @@ pub struct ScopeRule<'i, R = DefaultAtRule> { impl<'i, T: Clone> ScopeRule<'i, T> { pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> { + if context.pure_css_modules { + if let Some(scope_start) = &self.scope_start { + if !scope_start.0.iter().all(is_pure_css_modules_selector) { + return Err(MinifyError { + kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector, + loc: self.loc, + }); + } + } + + if let Some(scope_end) = &self.scope_end { + if !scope_end.0.iter().all(is_pure_css_modules_selector) { + return Err(MinifyError { + kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector, + loc: self.loc, + }); + } + } + } + self.rules.minify(context, false) } } diff --git a/src/rules/style.rs b/src/rules/style.rs index 63d8ed7dc..ce3cd4592 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -8,11 +8,13 @@ use super::MinifyContext; use crate::context::DeclarationContext; use crate::declaration::DeclarationBlock; use crate::error::ParserError; -use crate::error::{MinifyError, PrinterError, PrinterErrorKind}; +use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::rules::CssRuleList; -use crate::selector::{downlevel_selectors, get_prefix, is_compatible, is_unused, SelectorList}; +use crate::selector::{ + downlevel_selectors, get_prefix, is_compatible, is_pure_css_modules_selector, is_unused, SelectorList, +}; use crate::targets::{should_compile, Targets}; use crate::traits::ToCss; use crate::vendor_prefix::VendorPrefix; @@ -69,6 +71,19 @@ impl<'i, T: Clone> StyleRule<'i, T> { } } + let pure_css_modules = context.pure_css_modules; + if context.pure_css_modules { + if !self.selectors.0.iter().all(is_pure_css_modules_selector) { + return Err(MinifyError { + kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector, + loc: self.loc, + }); + } + + // Parent rule contained id or class, so child rules don't need to. + context.pure_css_modules = false; + } + context.handler_context.context = DeclarationContext::StyleRule; self .declarations @@ -85,6 +100,7 @@ impl<'i, T: Clone> StyleRule<'i, T> { } } + context.pure_css_modules = pure_css_modules; Ok(false) } } @@ -157,8 +173,8 @@ impl<'i, T> StyleRule<'i, T> { pub(crate) fn update_prefix(&mut self, context: &mut MinifyContext<'_, 'i>) { self.vendor_prefix = get_prefix(&self.selectors); - if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() { - self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), *context.targets); + if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { + self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), context.targets.current); } } } @@ -229,7 +245,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { W: std::fmt::Write, { // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. - let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets, Nesting); + let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets.current, Nesting); let len = self.declarations.declarations.len() + self.declarations.important_declarations.len(); let has_declarations = supports_nesting || len > 0 || self.rules.0.is_empty(); @@ -240,39 +256,16 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { dest.whitespace()?; dest.write_char('{')?; dest.indent(); - - let mut i = 0; - macro_rules! write { - ($decls: ident, $important: literal) => { - for decl in &self.declarations.$decls { - // The CSS modules `composes` property is handled specially, and omitted during printing. - // We need to add the classes it references to the list for the selectors in this rule. - if let crate::properties::Property::Composes(composes) = &decl { - if dest.is_nested() && dest.css_module.is_some() { - return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc)); - } - - if let Some(css_module) = &mut dest.css_module { - css_module - .handle_composes(&self.selectors, &composes, self.loc.source_index) - .map_err(|e| dest.error(e, composes.loc))?; - continue; - } - } - - dest.newline()?; - decl.to_css(dest, $important)?; - if i != len - 1 || !dest.minify || (supports_nesting && !self.rules.0.is_empty()) { - dest.write_char(';')?; - } - - i += 1; - } - }; + if len > 0 { + dest.newline()?; } - write!(declarations, false); - write!(important_declarations, true); + self.declarations.to_css_declarations( + dest, + supports_nesting && !self.rules.0.is_empty(), + &self.selectors, + self.loc.source_index, + )?; } macro_rules! newline { diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 04f1fdd33..a8deccd1b 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -7,8 +7,9 @@ use super::{CssRuleList, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; +use crate::properties::custom::TokenList; use crate::properties::PropertyId; -use crate::targets::Targets; +use crate::targets::{Features, FeaturesIterator, Targets}; use crate::traits::{Parse, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -42,8 +43,19 @@ impl<'i, T: Clone> SupportsRule<'i, T> { context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool, ) -> Result<(), MinifyError> { - self.condition.set_prefixes_for_targets(&context.targets); - self.rules.minify(context, parent_is_unused) + let inserted = context.targets.enter_supports(self.condition.get_supported_features()); + if inserted { + context.handler_context.targets = context.targets.current; + } + + self.condition.set_prefixes_for_targets(&context.targets.current); + let result = self.rules.minify(context, parent_is_unused); + + if inserted { + context.targets.exit_supports(); + context.handler_context.targets = context.targets.current; + } + result } } @@ -60,7 +72,13 @@ impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; + + let inserted = dest.targets.enter_supports(self.condition.get_supported_features()); self.rules.to_css(dest)?; + if inserted { + dest.targets.exit_supports(); + } + dest.dedent(); dest.newline()?; dest.write_char('}') @@ -149,6 +167,28 @@ impl<'i> SupportsCondition<'i> { _ => {} } } + + fn get_supported_features(&self) -> Features { + fn get_supported_features_internal(value: &SupportsCondition) -> Option { + match value { + SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(), + SupportsCondition::Declaration { value, .. } => { + let mut input = ParserInput::new(&value); + let mut parser = Parser::new(&mut input); + if let Ok(tokens) = TokenList::parse(&mut parser, &Default::default(), 0) { + Some(tokens.get_features()) + } else { + Some(Features::empty()) + } + } + // bail out if "not" or "or" exists for now + SupportsCondition::Not(_) | SupportsCondition::Or(_) => None, + SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => Some(Features::empty()), + } + } + + get_supported_features_internal(self).unwrap_or(Features::empty()) + } } impl<'i> Parse<'i> for SupportsCondition<'i> { diff --git a/src/rules/view_transition.rs b/src/rules/view_transition.rs new file mode 100644 index 000000000..fac6ec65a --- /dev/null +++ b/src/rules/view_transition.rs @@ -0,0 +1,196 @@ +//! The `@view-transition` rule. + +use super::Location; +use crate::error::{ParserError, PrinterError}; +use crate::printer::Printer; +use crate::properties::custom::CustomProperty; +use crate::stylesheet::ParserOptions; +use crate::traits::{Parse, ToCss}; +use crate::values::ident::NoneOrCustomIdentList; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; +use cssparser::*; + +/// A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule. +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct ViewTransitionRule<'i> { + /// Declarations in the `@view-transition` rule. + #[cfg_attr(feature = "serde", serde(borrow))] + pub properties: Vec<'i>>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +/// A property within a `@view-transition` rule. +/// +/// See [ViewTransitionRule](ViewTransitionRule). +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "property", content = "value", rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum ViewTransitionProperty<'i> { + /// The `navigation` property. + Navigation(Navigation), + /// The `types` property. + #[cfg_attr(feature = "serde", serde(borrow))] + Types(NoneOrCustomIdentList<'i>), + /// An unknown or unsupported property. + Custom(CustomProperty<'i>), +} + +/// A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) +/// property in a `@view-transition` rule. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum Navigation { + /// There will be no transition. + #[default] + None, + /// The transition will be enabled if the navigation is same-origin. + Auto, +} + +pub(crate) struct ViewTransitionDeclarationParser; + +impl<'i> cssparser::DeclarationParser<'i> for ViewTransitionDeclarationParser { + type Declaration = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut cssparser::Parser<'i, 't>, + ) -> Result<'i, Self::Error>> { + let state = input.state(); + match_ignore_ascii_case! { &name, + "navigation" => { + // https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor + if let Ok(navigation) = Navigation::parse(input) { + return Ok(ViewTransitionProperty::Navigation(navigation)); + } + }, + "types" => { + // https://drafts.csswg.org/css-view-transitions-2/#types-cross-doc + if let Ok(types) = NoneOrCustomIdentList::parse(input) { + return Ok(ViewTransitionProperty::Types(types)); + } + }, + _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration)) + } + + input.reset(&state); + return Ok(ViewTransitionProperty::Custom(CustomProperty::parse( + name.into(), + input, + &ParserOptions::default(), + )?)); + } +} + +/// Default methods reject all at rules. +impl<'i> AtRuleParser<'i> for ViewTransitionDeclarationParser { + type Prelude = (); + type AtRule = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; +} + +impl<'i> QualifiedRuleParser<'i> for ViewTransitionDeclarationParser { + type Prelude = (); + type QualifiedRule = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; +} + +impl<'i> RuleBodyItemParser<'i, ViewTransitionProperty<'i>, ParserError<'i>> for ViewTransitionDeclarationParser { + fn parse_qualified(&self) -> bool { + false + } + + fn parse_declarations(&self) -> bool { + true + } +} + +impl<'i> ViewTransitionRule<'i> { + pub(crate) fn parse<'t>( + input: &mut Parser<'i, 't>, + loc: Location, + ) -> Result<'i, ParserError<'i>>> { + let mut decl_parser = ViewTransitionDeclarationParser; + let mut parser = RuleBodyParser::new(input, &mut decl_parser); + let mut properties = vec![]; + while let Some(decl) = parser.next() { + if let Ok(decl) = decl { + properties.push(decl); + } + } + + Ok(ViewTransitionRule { properties, loc }) + } +} + +impl<'i> ToCss for ViewTransitionRule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + dest.write_str("@view-transition")?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + let len = self.properties.len(); + for (i, prop) in self.properties.iter().enumerate() { + dest.newline()?; + prop.to_css(dest)?; + if i != len - 1 || !dest.minify { + dest.write_char(';')?; + } + } + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } +} + +impl<'i> ToCss for ViewTransitionProperty<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + macro_rules! property { + ($prop: literal, $value: expr) => {{ + dest.write_str($prop)?; + dest.delim(':', false)?; + $value.to_css(dest) + }}; + } + + match self { + ViewTransitionProperty::Navigation(f) => property!("navigation", f), + ViewTransitionProperty::Types(t) => property!("types", t), + ViewTransitionProperty::Custom(custom) => { + dest.write_str(custom.name.as_ref())?; + dest.delim(':', false)?; + custom.value.to_css(dest, true) + } + } + } +} diff --git a/src/selector.rs b/src/selector.rs index 2c7d43403..355ab66e3 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1,7 +1,7 @@ //! CSS selectors. use crate::compat::Feature; -use crate::error::{ParserError, PrinterError}; +use crate::error::{ParserError, PrinterError, SelectorError}; use crate::parser::ParserFlags; use crate::printer::Printer; use crate::properties::custom::TokenList; @@ -21,6 +21,7 @@ use parcel_selectors::{ attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity}, parser::SelectorImpl, }; +use smallvec::SmallVec; use std::collections::HashSet; use std::fmt; @@ -157,8 +158,8 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "read-write" => ReadWrite(VendorPrefix::None), "-moz-read-write" => ReadWrite(VendorPrefix::Moz), "placeholder-shown" => PlaceholderShown(VendorPrefix::None), - "-moz-placeholder-shown" => PlaceholderShown(VendorPrefix::Moz), - "-ms-placeholder-shown" => PlaceholderShown(VendorPrefix::Ms), + "-moz-placeholder" => PlaceholderShown(VendorPrefix::Moz), + "-ms-input-placeholder" => PlaceholderShown(VendorPrefix::Ms), "default" => Default, "checked" => Checked, "indeterminate" => Indeterminate, @@ -177,6 +178,9 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "-webkit-autofill" => Autofill(VendorPrefix::WebKit), "-o-autofill" => Autofill(VendorPrefix::O), + // https://drafts.csswg.org/css-view-transitions-2/#pseudo-classes-for-selective-vt + "active-view-transition" => ActiveViewTransition, + // https://webkit.org/blog/363/styling-scrollbars/ "horizontal" => WebKitScrollbar(WebKitScrollbarPseudoClass::Horizontal), "vertical" => WebKitScrollbar(WebKitScrollbarPseudoClass::Vertical), @@ -196,7 +200,7 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, _ => { if !name.starts_with('-') { - self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()))); + self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone()))); } Custom { name: name.into() } } @@ -221,11 +225,20 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, Lang { languages } }, "dir" => Dir { direction: Direction::parse(parser)? }, + // https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-type-pseudo + "active-view-transition-type" => { + let kind = Parse::parse(parser)?; + ActiveViewTransitionType { kind } + }, + "state" => { + let state = CustomIdent::parse(parser)?; + State { state } + }, "local" if self.options.css_modules.is_some() => Local { selector: Box::new(Selector::parse(self, parser)?) }, "global" if self.options.css_modules.is_some() => Global { selector: Box::new(Selector::parse(self, parser)?) }, _ => { if !name.starts_with('-') { - self.options.warn(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()))); + self.options.warn(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone()))); } let mut args = Vec::new(); TokenList::parse_raw(parser, &mut args, &self.options, 0)?; @@ -258,6 +271,8 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "after" => After, "first-line" => FirstLine, "first-letter" => FirstLetter, + "details-content" => DetailsContent, + "target-text" => TargetText, "cue" => Cue, "cue-region" => CueRegion, "selection" => Selection(VendorPrefix::None), @@ -281,11 +296,17 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "-webkit-scrollbar-corner" => WebKitScrollbar(WebKitScrollbarPseudoElement::Corner), "-webkit-resizer" => WebKitScrollbar(WebKitScrollbarPseudoElement::Resizer), + "picker-icon" => PickerIcon, + "checkmark" => Checkmark, + "view-transition" => ViewTransition, + "grammar-error" => GrammarError, + "spelling-error" => SpellingError, + _ => { if !name.starts_with('-') { - self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()))); + self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone()))); } Custom { name: name.into() } } @@ -303,13 +324,14 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, let pseudo_element = match_ignore_ascii_case! { &name, "cue" => CueFunction { selector: Box::new(Selector::parse(self, arguments)?) }, "cue-region" => CueRegionFunction { selector: Box::new(Selector::parse(self, arguments)?) }, - "view-transition-group" => ViewTransitionGroup { part_name: ViewTransitionPartName::parse(arguments)? }, - "view-transition-image-pair" => ViewTransitionImagePair { part_name: ViewTransitionPartName::parse(arguments)? }, - "view-transition-old" => ViewTransitionOld { part_name: ViewTransitionPartName::parse(arguments)? }, - "view-transition-new" => ViewTransitionNew { part_name: ViewTransitionPartName::parse(arguments)? }, + "picker" => PickerFunction { identifier: Ident::parse(arguments)? }, + "view-transition-group" => ViewTransitionGroup { part: ViewTransitionPartSelector::parse(arguments)? }, + "view-transition-image-pair" => ViewTransitionImagePair { part: ViewTransitionPartSelector::parse(arguments)? }, + "view-transition-old" => ViewTransitionOld { part: ViewTransitionPartSelector::parse(arguments)? }, + "view-transition-new" => ViewTransitionNew { part: ViewTransitionPartSelector::parse(arguments)? }, _ => { if !name.starts_with('-') { - self.options.warn(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()))); + self.options.warn(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone()))); } let mut args = Vec::new(); TokenList::parse_raw(arguments, &mut args, &self.options, 0)?; @@ -507,6 +529,21 @@ pub enum PseudoClass<'i> { #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] Autofill(VendorPrefix), + /// The [:active-view-transition](https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-pseudo) pseudo class. + ActiveViewTransition, + /// The [:active-view-transition-type()](https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-type-pseudo) pseudo class. + ActiveViewTransitionType { + /// A view transition type. + #[cfg_attr(feature = "serde", serde(rename = "type"))] + kind: SmallVec<[CustomIdent<'i>; 1]>, + }, + + /// The [:state()](https://developer.mozilla.org/en-US/docs/Web/CSS/:state) pseudo class for custom element states. + State { + /// The custom state identifier. + state: CustomIdent<'i>, + }, + // CSS modules /// The CSS modules :local() pseudo class. Local { @@ -649,6 +686,11 @@ where dir.to_css(dest)?; return dest.write_str(")"); } + State { state } => { + dest.write_str(":state(")?; + state.to_css(dest)?; + return dest.write_str(")"); + } _ => {} } @@ -763,6 +805,13 @@ where // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill Autofill(prefix) => write_prefixed!(prefix, "autofill"), + ActiveViewTransition => dest.write_str(":active-view-transition"), + ActiveViewTransitionType { kind } => { + dest.write_str(":active-view-transition-type(")?; + kind.to_css(dest)?; + dest.write_char(')') + } + Local { selector } => serialize_selector(selector, dest, context, false), Global { selector } => { let css_module = std::mem::take(&mut dest.css_module); @@ -789,7 +838,7 @@ where }) } - Lang { languages: _ } | Dir { direction: _ } => unreachable!(), + Lang { languages: _ } | Dir { direction: _ } | State { .. } => unreachable!(), Custom { name } => { dest.write_char(':')?; return dest.write_str(&name); @@ -862,6 +911,10 @@ pub enum PseudoElement<'i> { FirstLine, /// The [::first-letter](https://drafts.csswg.org/css-pseudo-4/#first-letter-pseudo) pseudo element. FirstLetter, + /// The [::details-content](https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo) + DetailsContent, + /// The [::target-text](https://drafts.csswg.org/css-pseudo-4/#selectordef-target-text) + TargetText, /// The [::selection](https://drafts.csswg.org/css-pseudo-4/#selectordef-selection) pseudo element. #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] Selection(VendorPrefix), @@ -902,26 +955,39 @@ pub enum PseudoElement<'i> { #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] ViewTransitionGroup { /// A part name selector. - part_name: ViewTransitionPartName<'i>, + part: ViewTransitionPartSelector<'i>, }, /// The [::view-transition-image-pair()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-image-pair-pt-name-selector) functional pseudo element. #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] ViewTransitionImagePair { /// A part name selector. - part_name: ViewTransitionPartName<'i>, + part: ViewTransitionPartSelector<'i>, }, /// The [::view-transition-old()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-old-pt-name-selector) functional pseudo element. #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] ViewTransitionOld { /// A part name selector. - part_name: ViewTransitionPartName<'i>, + part: ViewTransitionPartSelector<'i>, }, /// The [::view-transition-new()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-new-pt-name-selector) functional pseudo element. #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] ViewTransitionNew { /// A part name selector. - part_name: ViewTransitionPartName<'i>, + part: ViewTransitionPartSelector<'i>, + }, + /// The [::picker()](https://drafts.csswg.org/css-forms-1/#the-picker-pseudo-element) functional pseudo element. + PickerFunction { + /// A form control identifier. + identifier: Ident<'i>, }, + /// The [::picker-icon](https://drafts.csswg.org/css-forms-1/#picker-opener-icon-the-picker-icon-pseudo-element) pseudo element. + PickerIcon, + /// The [::checkmark](https://drafts.csswg.org/css-forms-1/#styling-checkmarks-the-checkmark-pseudo-element) pseudo element. + Checkmark, + /// The [::grammar-error](https://drafts.csswg.org/css-pseudo/#selectordef-grammar-error) pseudo element. + GrammarError, + /// The [::spelling-error](https://drafts.csswg.org/css-pseudo/#selectordef-spelling-error) pseudo element. + SpellingError, /// An unknown pseudo element. Custom { /// The name of the pseudo element. @@ -965,44 +1031,17 @@ pub enum WebKitScrollbarPseudoElement { /// A [view transition part name](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). #[derive(PartialEq, Eq, Clone, Debug, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum ViewTransitionPartName<'i> { /// * + #[cfg_attr(feature = "serde", serde(rename = "*"))] All, /// + #[cfg_attr(feature = "serde", serde(borrow, untagged))] Name(CustomIdent<'i>), } -#[cfg(feature = "serde")] -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] -impl<'i> serde::Serialize for ViewTransitionPartName<'i> { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - ViewTransitionPartName::All => serializer.serialize_str("*"), - ViewTransitionPartName::Name(name) => serializer.serialize_str(&name.0), - } - } -} - -#[cfg(feature = "serde")] -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] -impl<'i, 'de: 'i> serde::Deserialize<'de> for ViewTransitionPartName<'i> { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = CowArcStr::deserialize(deserializer)?; - if s == "*" { - Ok(ViewTransitionPartName::All) - } else { - Ok(ViewTransitionPartName::Name(CustomIdent(s))) - } - } -} - #[cfg(feature = "jsonschema")] #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))] impl<'a> schemars::JsonSchema for ViewTransitionPartName<'a> { @@ -1041,6 +1080,59 @@ impl<'i> ToCss for ViewTransitionPartName<'i> { } } +/// A [view transition part selector](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub struct ViewTransitionPartSelector<'i> { + /// The view transition part name. + #[cfg_attr(feature = "serde", serde(borrow))] + name: Option<'i>>, + /// A list of view transition classes. + classes: Vec<'i>>, +} + +impl<'i> Parse<'i> for ViewTransitionPartSelector<'i> { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + input.skip_whitespace(); + let name = input.try_parse(ViewTransitionPartName::parse).ok(); + let mut classes = Vec::new(); + while let Ok(token) = input.next_including_whitespace() { + if matches!(token, Token::Delim('.')) { + match input.next_including_whitespace() { + Ok(Token::Ident(id)) => classes.push(CustomIdent(id.into())), + _ => return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))), + } + } else { + break; + } + } + + if !input.is_exhausted() || (name.is_none() && classes.is_empty()) { + return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))); + } + + Ok(ViewTransitionPartSelector { name, classes }) + } +} + +impl<'i> ToCss for ViewTransitionPartSelector<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + if let Some(name) = &self.name { + name.to_css(dest)?; + } + for class in &self.classes { + dest.write_char('.')?; + class.to_css(dest)?; + } + Ok(()) + } +} + impl<'i> cssparser::ToCss for PseudoElement<'i> { fn to_css(&self, dest: &mut W) -> std::fmt::Result where @@ -1092,6 +1184,8 @@ where Before => dest.write_str(":before"), FirstLine => dest.write_str(":first-line"), FirstLetter => dest.write_str(":first-letter"), + DetailsContent => dest.write_str("::details-content"), + TargetText => dest.write_str("::target-text"), Marker => dest.write_str("::marker"), Selection(prefix) => write_prefixed!(prefix, "selection"), Cue => dest.write_str("::cue"), @@ -1138,26 +1232,35 @@ where }) } ViewTransition => dest.write_str("::view-transition"), - ViewTransitionGroup { part_name } => { + ViewTransitionGroup { part } => { dest.write_str("::view-transition-group(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } - ViewTransitionImagePair { part_name } => { + ViewTransitionImagePair { part } => { dest.write_str("::view-transition-image-pair(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } - ViewTransitionOld { part_name } => { + ViewTransitionOld { part } => { dest.write_str("::view-transition-old(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } - ViewTransitionNew { part_name } => { + ViewTransitionNew { part } => { dest.write_str("::view-transition-new(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; + dest.write_char(')') + } + PickerFunction { identifier } => { + dest.write_str("::picker(")?; + identifier.to_css(dest)?; dest.write_char(')') } + PickerIcon => dest.write_str("::picker-icon"), + Checkmark => dest.write_str("::checkmark"), + GrammarError => dest.write_str("::grammar-error"), + SpellingError => dest.write_str("::spelling-error"), Custom { name: val } => { dest.write_str("::")?; return dest.write_str(val); @@ -1316,7 +1419,7 @@ where let mut combinators = selector.iter_raw_match_order().rev().filter_map(|x| x.as_combinator()); let compound_selectors = selector.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev(); - let should_compile_nesting = should_compile!(dest.targets, Nesting); + let should_compile_nesting = should_compile!(dest.targets.current, Nesting); let mut first = true; let mut combinators_exhausted = false; @@ -1575,6 +1678,14 @@ where selector.to_css(dest)?; dest.write_char(')') } + Component::NthOf(ref nth_of_data) => { + let nth_data = nth_of_data.nth_data(); + nth_data.write_start(dest, true)?; + nth_data.write_affine(dest)?; + dest.write_str(" of ")?; + serialize_selector_list(nth_of_data.selectors().iter(), dest, context, true)?; + dest.write_char(')') + } _ => { cssparser::ToCss::to_css(component, dest)?; Ok(()) @@ -1618,7 +1729,7 @@ where } else { // If there is no context, we are at the root if nesting is supported. This is equivalent to :scope. // Otherwise, if nesting is supported, serialize the nesting selector directly. - if should_compile!(dest.targets, Nesting) { + if should_compile!(dest.targets.current, Nesting) { dest.write_str(":scope") } else { dest.write_char('&') @@ -1820,6 +1931,8 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { PseudoClass::Autofill(prefix) if *prefix == VendorPrefix::None => Feature::Autofill, + PseudoClass::State { .. } => Feature::StatePseudoClass, + // Experimental, no browser support. PseudoClass::Current | PseudoClass::Past @@ -1836,7 +1949,9 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { | PseudoClass::Blank | PseudoClass::UserInvalid | PseudoClass::UserValid - | PseudoClass::Defined => return false, + | PseudoClass::Defined + | PseudoClass::ActiveViewTransition + | PseudoClass::ActiveViewTransitionType { .. } => return false, PseudoClass::Custom { .. } | _ => return false, } @@ -1846,12 +1961,24 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { PseudoElement::After | PseudoElement::Before => Feature::Gencontent, PseudoElement::FirstLine => Feature::FirstLine, PseudoElement::FirstLetter => Feature::FirstLetter, + PseudoElement::DetailsContent => Feature::DetailsContent, + PseudoElement::TargetText => Feature::TargetText, PseudoElement::Selection(prefix) if *prefix == VendorPrefix::None => Feature::Selection, PseudoElement::Placeholder(prefix) if *prefix == VendorPrefix::None => Feature::Placeholder, PseudoElement::Marker => Feature::MarkerPseudo, PseudoElement::Backdrop(prefix) if *prefix == VendorPrefix::None => Feature::Dialog, PseudoElement::Cue => Feature::Cue, PseudoElement::CueFunction { selector: _ } => Feature::CueFunction, + PseudoElement::ViewTransition + | PseudoElement::ViewTransitionNew { .. } + | PseudoElement::ViewTransitionOld { .. } + | PseudoElement::ViewTransitionGroup { .. } + | PseudoElement::ViewTransitionImagePair { .. } => Feature::ViewTransition, + PseudoElement::PickerFunction { identifier: _ } => Feature::Picker, + PseudoElement::PickerIcon => Feature::PickerIcon, + PseudoElement::Checkmark => Feature::Checkmark, + PseudoElement::GrammarError => Feature::GrammarError, + PseudoElement::SpellingError => Feature::SpellingError, PseudoElement::Custom { name: _ } | _ => return false, }, @@ -2093,6 +2220,25 @@ pub(crate) fn is_unused( }) } +/// Returns whether the selector has any class or id components. +pub(crate) fn is_pure_css_modules_selector(selector: &Selector) -> bool { + use parcel_selectors::parser::Component; + selector.iter_raw_match_order().any(|c| match c { + Component::Class(_) | Component::ID(_) => true, + Component::Is(s) | Component::Where(s) | Component::Has(s) | Component::Any(_, s) | Component::Negation(s) => { + s.iter().any(is_pure_css_modules_selector) + } + Component::NthOf(nth) => nth.selectors().iter().any(is_pure_css_modules_selector), + Component::Slotted(s) => is_pure_css_modules_selector(&s), + Component::Host(s) => s.as_ref().map(is_pure_css_modules_selector).unwrap_or(false), + Component::NonTSPseudoClass(pc) => match pc { + PseudoClass::Local { selector } => is_pure_css_modules_selector(&*selector), + _ => false, + }, + _ => false, + }) +} + #[cfg(feature = "visitor")] #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for SelectorList<'i> { diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 2848bf2bc..990a09be1 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -4,14 +4,14 @@ //! A [StyleAttribute](StyleAttribute) represents an inline `style` attribute in HTML. use crate::context::{DeclarationContext, PropertyHandlerContext}; -use crate::css_modules::{CssModule, CssModuleExports, CssModuleReferences}; +use crate::css_modules::{hash, CssModule, CssModuleExports, CssModuleReferences}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind}; use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; use crate::rules::{CssRule, CssRuleList, MinifyContext}; -use crate::targets::{should_compile, Targets}; +use crate::targets::{should_compile, Targets, TargetsWithSupportsScope}; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; #[cfg(feature = "visitor")] @@ -81,6 +81,10 @@ pub struct StyleSheet<'i, 'o, T = DefaultAtRule> { pub(crate) source_map_urls: Vec