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 ac107a13e..95d304311 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,9 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc binary: lightningcss.exe + - os: windows-latest + target: aarch64-pc-windows-msvc + binary: lightningcss.exe # Mac OS - os: macos-latest target: x86_64-apple-darwin @@ -45,7 +48,7 @@ jobs: if: ${{ matrix.strip }} run: ${{ matrix.strip }} *.node ${{ matrix.binary }} - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: | @@ -87,7 +90,7 @@ jobs: - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 run: strip -x *.node lightningcss - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-aarch64-apple-darwin path: | @@ -106,6 +109,9 @@ jobs: - target: aarch64-unknown-linux-gnu strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + - target: aarch64-linux-android + strip: llvm-strip + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - target: armv7-unknown-linux-gnueabihf strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57 @@ -130,6 +136,14 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Setup Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} + run: | + sudo apt update && sudo apt install unzip -y + cd /tmp + wget -q https://dl.google.com/android/repository/android-ndk-r28-linux.zip -O /tmp/ndk.zip + unzip ndk.zip + - name: Setup cross compile toolchain if: ${{ matrix.setup }} run: ${{ matrix.setup }} @@ -141,8 +155,11 @@ jobs: - name: Build release run: yarn build-release env: + ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 RUST_TARGET: ${{ matrix.target }} - name: Build CLI + env: + ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 run: | yarn napi build --bin lightningcss --release --features cli --target ${{ matrix.target }} mv target/${{ matrix.target }}/release/lightningcss lightningcss @@ -150,7 +167,7 @@ jobs: if: ${{ matrix.strip }} run: ${{ matrix.strip }} *.node lightningcss - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: | @@ -162,7 +179,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build FreeBSD - uses: cross-platform-actions/action@v0.23.0 + uses: cross-platform-actions/action@v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -170,7 +187,7 @@ jobs: RUSTUP_IO_THREADS: 1 with: operating_system: freebsd - version: '13.2' + version: '14.0' memory: 13G cpu_count: 3 environment_variables: 'DEBUG RUSTUP_IO_THREADS' @@ -198,7 +215,7 @@ jobs: rm -rf .yarn/cache - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-x86_64-unknown-freebsd path: | @@ -229,7 +246,7 @@ jobs: export PATH="$PATH:./binaryen-version_111/bin" yarn wasm:build-release - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: wasm path: wasm/lightningcss_node.wasm @@ -247,9 +264,11 @@ jobs: - uses: actions/checkout@v3 - uses: bahmutov/npm-install@v1.8.32 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts + - name: Show artifacts + run: ls -R artifacts - name: Build npm packages run: | node scripts/build-npm.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e81411540..10868f4a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,13 @@ yarn wasm:build yarn wasm:build-release ``` +Note: If you plan to build the WASM target, ensure that you have the required toolchain and binaries installed. + +```sh +rustup target add wasm32-unknown-unknown +cargo install wasm-opt +``` + ## Website The website is built using [Parcel](https://parceljs.org). You can start the development server by running: diff --git a/Cargo.lock b/Cargo.lock index 97cbc6b1b..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,26 +53,21 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anyhow" -version = "1.0.75" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "assert_cmd" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", - "predicates 3.0.4", + "libc", + "predicates 3.1.3", "predicates-core", "predicates-tree", "wait-timeout", @@ -80,14 +75,14 @@ dependencies = [ [[package]] name = "assert_fs" -version = "1.0.13" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" +checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" dependencies = [ "anstyle", "doc-comment", "globwalk", - "predicates 3.0.4", + "predicates 3.1.3", "predicates-core", "predicates-tree", "tempfile", @@ -106,9 +101,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64-simd" @@ -127,9 +122,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -143,33 +141,38 @@ dependencies = [ "wyz 0.5.1", ] +[[package]] +name = "browserslist-data" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c49471c5ae53cefe3ac4acc4d3c75cb4b68995b70b3bbb864f8e08fae282098c" +dependencies = [ + "ahash 0.8.12", + "chrono", +] + [[package]] name = "browserslist-rs" -version = "0.15.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "405bbd46590a441abe5db3e5c8af005aa42e640803fecb51912703e93e4ce8d3" +checksum = "8dd48a6ca358df4f7000e3fb5f08738b1b91a0e5d5f862e2f77b2b14647547f5" dependencies = [ - "ahash 0.8.7", - "anyhow", + "ahash 0.8.12", + "browserslist-data", "chrono", "either", - "indexmap 2.2.2", - "itertools 0.12.1", + "itertools 0.13.0", "nom", - "once_cell", - "quote", "serde", "serde_json", - "string_cache", - "string_cache_codegen", "thiserror", ] [[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", @@ -178,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", @@ -195,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", @@ -205,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" @@ -231,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]] @@ -246,9 +249,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -326,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" @@ -382,7 +376,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf", "serde", "smallvec", ] @@ -403,17 +397,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] name = "ctor" -version = "0.1.26" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] @@ -423,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", @@ -431,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" @@ -444,6 +438,12 @@ dependencies = [ "matches", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" @@ -464,24 +464,24 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" @@ -491,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", @@ -501,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" @@ -514,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" @@ -533,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", ] @@ -582,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" @@ -608,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", @@ -631,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", ] @@ -658,12 +660,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.15.2", "serde", ] @@ -684,27 +686,18 @@ dependencies = [ [[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.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jemalloc-sys" @@ -729,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.54" +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", @@ -774,7 +768,8 @@ dependencies = [ "cssparser-color", "dashmap", "data-encoding", - "getrandom", + "getrandom 0.3.3", + "indexmap 2.7.0", "indoc", "itertools 0.10.5", "jemallocator", @@ -782,12 +777,14 @@ dependencies = [ "lightningcss-derive", "parcel_selectors", "parcel_sourcemap", - "paste", + "pastey", "pathdiff", "predicates 2.1.5", + "pretty_assertions", "rayon", "schemars", "serde", + "serde-content", "serde_json", "smallvec", "static-self", @@ -795,8 +792,9 @@ dependencies = [ [[package]] name = "lightningcss-derive" -version = "1.0.0-alpha.42" +version = "1.0.0-alpha.43" dependencies = [ + "convert_case", "proc-macro2", "quote", "syn 1.0.109", @@ -804,7 +802,7 @@ dependencies = [ [[package]] name = "lightningcss-napi" -version = "0.1.0" +version = "0.4.7" dependencies = [ "crossbeam-channel", "cssparser", @@ -813,6 +811,7 @@ dependencies = [ "parcel_sourcemap", "rayon", "serde", + "serde-content", "serde-detach", "serde_bytes", "smallvec", @@ -841,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", @@ -857,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" @@ -869,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" @@ -890,17 +880,17 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "napi" -version = "2.10.3" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a967e17e9ba4e015a7bf9b92f90aa8dc321c6d913f6a6d2afd5b66a8ab36fc81" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "ctor", + "napi-derive", "napi-sys", "once_cell", "serde", "serde_json", - "thread_local", ] [[package]] @@ -911,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.39", + "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", @@ -935,24 +925,18 @@ dependencies = [ "quote", "regex", "semver", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] name = "napi-sys" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - [[package]] name = "nom" version = "7.1.3" @@ -971,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" @@ -998,15 +982,15 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "parcel_selectors" -version = "0.26.4" +version = "0.28.2" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "cssparser", - "fxhash", "log", - "phf 0.10.1", + "phf", "phf_codegen", "precomputed-hash", + "rustc-hash", "schemars", "serde", "smallvec", @@ -1027,21 +1011,11 @@ dependencies = [ "vlq", ] -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", @@ -1051,25 +1025,16 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.14" +name = "pastey" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "phf" @@ -1078,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]] @@ -1107,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", ] @@ -1117,20 +1072,11 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.39", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", + "syn 2.0.90", ] [[package]] @@ -1142,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" @@ -1170,32 +1110,41 @@ dependencies = [ [[package]] name = "predicates" -version = "3.0.4" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", - "itertools 0.11.0", "predicates-core", ] [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1222,9 +1171,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1251,13 +1200,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -1270,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", ] @@ -1290,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", @@ -1306,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", @@ -1316,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", @@ -1337,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", @@ -1348,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", @@ -1380,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", @@ -1404,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" @@ -1419,11 +1366,12 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.15" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", + "indexmap 2.7.0", "schemars_derive", "serde", "serde_json", @@ -1432,14 +1380,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.15" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] @@ -1456,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.192" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] +[[package]] +name = "serde-content" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" +dependencies = [ + "serde", +] + [[package]] name = "serde-detach" version = "0.0.1" @@ -1481,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.192" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simd-abstraction" version = "0.7.1" @@ -1532,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" @@ -1544,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", ] @@ -1568,32 +1543,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "string_cache" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" -dependencies = [ - "new_debug_unreachable", - "once_cell", - "parking_lot", - "phf_shared 0.10.0", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", -] - [[package]] name = "strsim" version = "0.10.0" @@ -1613,9 +1562,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1630,73 +1579,63 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "once_cell", "rustix", "windows-sys", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "textwrap" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", + "syn 2.0.90", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -1718,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" @@ -1757,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", @@ -1771,36 +1710,45 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1808,22 +1756,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "winapi" @@ -1843,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]] @@ -1858,31 +1806,32 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -1891,45 +1840,60 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] [[package]] name = "wyz" @@ -1946,22 +1910,28 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" -version = "0.7.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.39", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index 6806ca01d..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.54" +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 = ["lightningcss-derive"] -into_owned = ["static-self", "static-self/smallvec", "parcel_selectors/into_owned"] +visitor = [] +into_owned = [ + "static-self", + "static-self/smallvec", + "static-self/indexmap", + "parcel_selectors/into_owned", +] substitute_variables = ["visitor", "into_owned"] [dependencies] -serde = { version = "1.0.123", features = ["derive"], optional = true } +serde = { version = "1.0.228", features = ["derive"], optional = true } +serde-content = { version = "0.1.2", features = ["serde"], optional = true } cssparser = "0.33.0" cssparser-color = "0.1.0" -parcel_selectors = { version = "0.26.4", path = "./selectors" } +parcel_selectors = { version = "0.28.2", path = "./selectors" } itertools = "0.10.1" smallvec = { version = "1.7.0", features = ["union"] } bitflags = "2.2.1" @@ -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.15.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.42", path = "./derive", optional = true } -schemars = { version = "0.8.11", features = ["smallvec"], optional = true } -static-self = { version = "0.1.0", path = "static-self", optional = true } +lightningcss-derive = { version = "=1.0.0-alpha.43", path = "./derive" } +schemars = { version = "0.8.19", features = ["smallvec", "indexmap2"], optional = true } +static-self = { version = "0.1.2", path = "static-self", optional = true } [target.'cfg(target_os = "macos")'.dependencies] -jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"], optional = true } +jemallocator = { version = "0.3.2", features = [ + "disable_initial_exec_tls", +], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["custom"], default-features = false } +getrandom = { version = "0.3", default-features = false } [dev-dependencies] indoc = "1.0.3" @@ -85,6 +101,7 @@ assert_cmd = "2.0" assert_fs = "1.0" predicates = "2.1" serde_json = "1" +pretty_assertions = "1.4.0" [[test]] name = "cli_integration_tests" diff --git a/README.md b/README.md index 035544626..f44d7b3cd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An extremely fast CSS parser, transformer, and minifier written in Rust. Use it - **Extremely fast** – Parsing and minifying large files is completed in milliseconds, often with significantly smaller output than other tools. See [benchmarks](#benchmarks) below. - **Typed property values** – many other CSS parsers treat property values as an untyped series of tokens. This means that each transformer that wants to do something with these values must interpret them itself, leading to duplicate work and inconsistencies. Lightning CSS parses all values using the grammar from the CSS specification, and exposes a specific value type for each property. -- **Browser-grade parser** – Lightning CSS is built on the [cssparser](https://github.com/servo/rust-cssparser) and [selectors](https://github.com/servo/servo/tree/master/components/selectors) crates created by Mozilla and used by Firefox and Servo. These provide a solid general purpose CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties. +- **Browser-grade parser** – Lightning CSS is built on the [cssparser](https://github.com/servo/rust-cssparser) and [selectors](https://github.com/servo/stylo/tree/main/selectors) crates created by Mozilla and used by Firefox and Servo. These provide a solid general purpose CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties. - **Minification** – One of the main purposes of Lightning CSS is to minify CSS to make it smaller. This includes many optimizations including: - Combining longhand properties into shorthands where possible. - Merging adjacent rules with the same selectors or declarations when it is safe to do so. diff --git a/c/Cargo.toml b/c/Cargo.toml index 443d34ba8..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.15.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/derive/Cargo.toml b/derive/Cargo.toml index 1864748e9..ce55e5cca 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Devon Govett "] name = "lightningcss-derive" description = "Derive macros for lightningcss" -version = "1.0.0-alpha.42" +version = "1.0.0-alpha.43" license = "MPL-2.0" edition = "2021" repository = "https://github.com/parcel-bundler/lightningcss" @@ -14,3 +14,4 @@ proc-macro = true syn = { version = "1.0", features = ["extra-traits"] } quote = "1.0" proc-macro2 = "1.0" +convert_case = "0.6.0" diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 00b77a266..122414911 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,293 +1,20 @@ -use std::collections::HashSet; +use proc_macro::TokenStream; -use proc_macro::{self, TokenStream}; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; -use syn::{ - parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, - GenericParam, Generics, Ident, Member, Token, Type, Visibility, -}; +mod parse; +mod to_css; +mod visit; #[proc_macro_derive(Visit, attributes(visit, skip_visit, skip_type, visit_types))] pub fn derive_visit_children(input: TokenStream) -> TokenStream { - let DeriveInput { - ident, - data, - generics, - attrs, - .. - } = parse_macro_input!(input); - - let options: Vec = attrs - .iter() - .filter_map(|attr| { - if attr.path.is_ident("visit") { - let opts: VisitOptions = attr.parse_args().unwrap(); - Some(opts) - } else { - None - } - }) - .collect(); - - let visit_types = if let Some(attr) = attrs.iter().find(|attr| attr.path.is_ident("visit_types")) { - let types: VisitTypes = attr.parse_args().unwrap(); - let types = types.types; - Some(quote! { crate::visit_types!(#(#types)|*) }) - } else { - None - }; - - if options.is_empty() { - derive(&ident, &data, &generics, None, visit_types) - } else { - options - .into_iter() - .map(|options| derive(&ident, &data, &generics, Some(options), visit_types.clone())) - .collect() - } -} - -fn derive( - ident: &Ident, - data: &Data, - generics: &Generics, - options: Option, - visit_types: Option, -) -> TokenStream { - let mut impl_generics = generics.clone(); - let mut type_defs = quote! {}; - let generics = if let Some(VisitOptions { - generic: Some(generic), .. - }) = &options - { - let mappings = generics - .type_params() - .zip(generic.type_params()) - .map(|(a, b)| quote! { type #a = #b; }); - type_defs = quote! { #(#mappings)* }; - impl_generics.params.clear(); - generic - } else { - &generics - }; - - if impl_generics.lifetimes().next().is_none() { - impl_generics.params.insert(0, parse_quote! { 'i }) - } - - let lifetime = impl_generics.lifetimes().next().unwrap().clone(); - let t = impl_generics.type_params().find(|g| &g.ident.to_string() == "R"); - let v = quote! { __V }; - let t = if let Some(t) = t { - GenericParam::Type(t.ident.clone().into()) - } else { - let t: GenericParam = parse_quote! { __T }; - impl_generics - .params - .push(parse_quote! { #t: crate::visitor::Visit<#lifetime, __T, #v> }); - t - }; - - impl_generics - .params - .push(parse_quote! { #v: ?Sized + crate::visitor::Visitor<#lifetime, #t> }); - - for ty in generics.type_params() { - let name = &ty.ident; - impl_generics.make_where_clause().predicates.push(parse_quote! { - #name: Visit<#lifetime, #t, #v> - }) - } - - let mut seen_types = HashSet::new(); - let mut child_types = Vec::new(); - let mut visit = Vec::new(); - match data { - Data::Struct(s) => { - for ( - index, - Field { - vis, ty, ident, attrs, .. - }, - ) in s.fields.iter().enumerate() - { - if attrs.iter().any(|attr| attr.path.is_ident("skip_visit")) { - continue; - } - - if matches!(ty, Type::Reference(_)) || !matches!(vis, Visibility::Public(..)) { - continue; - } - - if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) { - seen_types.insert(ty.clone()); - child_types.push(quote! { - <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() - }); - } - - let name = ident - .as_ref() - .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); - visit.push(quote! { self.#name.visit(visitor)?; }) - } - } - Data::Enum(DataEnum { variants, .. }) => { - let variants = variants - .iter() - .map(|variant| { - let name = &variant.ident; - let mut field_names = Vec::new(); - let mut visit_fields = Vec::new(); - for (index, Field { ty, ident, attrs, .. }) in variant.fields.iter().enumerate() { - let name = ident.as_ref().map_or_else( - || Ident::new(&format!("_{}", index), Span::call_site()), - |ident| ident.clone(), - ); - field_names.push(name.clone()); - - if matches!(ty, Type::Reference(_)) { - continue; - } - - if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) && !skip_type(&variant.attrs) - { - seen_types.insert(ty.clone()); - child_types.push(quote! { - <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() - }); - } - - visit_fields.push(quote! { #name.visit(visitor)?; }) - } - - match variant.fields { - Fields::Unnamed(_) => { - quote! { - Self::#name(#(#field_names),*) => { - #(#visit_fields)* - } - } - } - Fields::Named(_) => { - quote! { - Self::#name { #(#field_names),* } => { - #(#visit_fields)* - } - } - } - Fields::Unit => quote! {}, - } - }) - .collect::(); - - visit.push(quote! { - match self { - #variants - _ => {} - } - }) - } - _ => {} - } - - if visit_types.is_none() && child_types.is_empty() { - child_types.push(quote! { crate::visitor::VisitTypes::empty().bits() }); - } - - let (_, ty_generics, _) = generics.split_for_impl(); - let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); - - let self_visit = if let Some(VisitOptions { - visit: Some(visit), - kind: Some(kind), - .. - }) = &options - { - child_types.push(quote! { crate::visitor::VisitTypes::#kind.bits() }); - - quote! { - fn visit(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { - if visitor.visit_types().contains(crate::visitor::VisitTypes::#kind) { - visitor.#visit(self) - } else { - self.visit_children(visitor) - } - } - } - } else { - quote! {} - }; - - let child_types = visit_types.unwrap_or_else(|| { - quote! { - { - #type_defs - crate::visitor::VisitTypes::from_bits_retain(#(#child_types)|*) - } - } - }); - - let output = quote! { - impl #impl_generics Visit<#lifetime, #t, #v> for #ident #ty_generics #where_clause { - const CHILD_TYPES: crate::visitor::VisitTypes = #child_types; - - #self_visit - - fn visit_children(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { - if !<#lifetime, #t, #v>>::CHILD_TYPES.intersects(visitor.visit_types()) { - return Ok(()) - } - - #(#visit)* - - Ok(()) - } - } - }; - - output.into() -} - -fn skip_type(attrs: &Vec) -> bool { - attrs.iter().any(|attr| attr.path.is_ident("skip_type")) -} - -struct VisitOptions { - visit: Option, - kind: Option, - generic: Option, -} - -impl Parse for VisitOptions { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let (visit, kind, comma) = if input.peek(Ident) { - let visit: Ident = input.parse()?; - let _: Token![,] = input.parse()?; - let kind: Ident = input.parse()?; - let comma: Result = input.parse(); - (Some(visit), Some(kind), comma.is_ok()) - } else { - (None, None, true) - }; - let generic: Option = if comma { Some(input.parse()?) } else { None }; - Ok(Self { visit, kind, generic }) - } + visit::derive_visit_children(input) } -struct VisitTypes { - types: Vec, +#[proc_macro_derive(Parse, attributes(css))] +pub fn derive_parse(input: TokenStream) -> TokenStream { + parse::derive_parse(input) } -impl Parse for VisitTypes { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let first: Ident = input.parse()?; - let mut types = vec![first]; - while input.parse::().is_ok() { - let id: Ident = input.parse()?; - types.push(id); - } - Ok(Self { types }) - } +#[proc_macro_derive(ToCss, attributes(css))] +pub fn derive_to_css(input: TokenStream) -> TokenStream { + to_css::derive_to_css(input) } diff --git a/derive/src/parse.rs b/derive/src/parse.rs new file mode 100644 index 000000000..995b344e3 --- /dev/null +++ b/derive/src/parse.rs @@ -0,0 +1,213 @@ +use convert_case::Casing; +use proc_macro::{self, TokenStream}; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{ + parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Fields, Ident, Token, +}; + +pub fn derive_parse(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + mut generics, + attrs, + .. + } = parse_macro_input!(input); + let opts = CssOptions::parse_attributes(&attrs).unwrap(); + let cloned_generics = generics.clone(); + let (_, ty_generics, _) = cloned_generics.split_for_impl(); + + if generics.lifetimes().next().is_none() { + generics.params.insert(0, parse_quote! { 'i }) + } + + let lifetime = generics.lifetimes().next().unwrap().clone(); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let imp = match &data { + Data::Enum(data) => derive_enum(&data, &ident, &opts), + _ => todo!(), + }; + + let output = quote! { + impl #impl_generics Parse<#lifetime> for #ident #ty_generics #where_clause { + fn parse<'t>(input: &mut Parser<#lifetime, 't>) -> Result<#lifetime, ParserError<#lifetime>>> { + #imp + } + } + }; + + output.into() +} + +fn derive_enum(data: &DataEnum, ident: &Ident, opts: &CssOptions) -> TokenStream2 { + let mut idents = Vec::new(); + let mut non_idents = Vec::new(); + for (index, variant) in data.variants.iter().enumerate() { + let name = &variant.ident; + let fields = variant + .fields + .iter() + .enumerate() + .map(|(index, field)| { + field.ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ) + }) + .collect::<_>>(); + + let mut expr = match &variant.fields { + Fields::Unit => { + idents.push(( + Literal::string(&variant.ident.to_string().to_case(opts.case)), + name.clone(), + )); + continue; + } + Fields::Named(_) => { + quote! { + return Ok(#ident::#name { #(#fields),* }) + } + } + Fields::Unnamed(_) => { + quote! { + return Ok(#ident::#name(#(#fields),*)) + } + } + }; + + // Group multiple ident branches together. + if !idents.is_empty() { + if idents.len() == 1 { + let (s, name) = idents.remove(0); + non_idents.push(quote! { + if input.try_parse(|input| input.expect_ident_matching(#s)).is_ok() { + return Ok(#ident::#name) + } + }); + } else { + let matches = idents + .iter() + .map(|(s, name)| { + quote! { + #s => return Ok(#ident::#name), + } + }) + .collect::<_>>(); + non_idents.push(quote! { + { + let state = input.state(); + if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) { + cssparser::match_ignore_ascii_case! { &*ident, + #(#matches)* + _ => {} + } + input.reset(&state); + } + } + }); + idents.clear(); + } + } + + let is_last = index == data.variants.len() - 1; + + for (index, field) in variant.fields.iter().enumerate().rev() { + let ty = &field.ty; + let field_name = field.ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ); + if is_last { + expr = quote! { + let #field_name = <#ty>::parse(input)?; + #expr + }; + } else { + expr = quote! { + if let Ok(#field_name) = input.try_parse(<#ty>::parse) { + #expr + } + }; + } + } + + non_idents.push(expr); + } + + let idents = if idents.is_empty() { + quote! {} + } else if idents.len() == 1 { + let (s, name) = idents.remove(0); + quote! { + input.expect_ident_matching(#s)?; + Ok(#ident::#name) + } + } else { + let idents = idents + .into_iter() + .map(|(s, name)| { + quote! { + #s => Ok(#ident::#name), + } + }) + .collect::<_>>(); + quote! { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + cssparser::match_ignore_ascii_case! { &*ident, + #(#idents)* + _ => Err(location.new_unexpected_token_error( + cssparser::Token::Ident(ident.clone()) + )) + } + } + }; + + let output = quote! { + #(#non_idents)* + #idents + }; + + output.into() +} + +pub struct CssOptions { + pub case: convert_case::Case, +} + +impl CssOptions { + pub fn parse_attributes(attrs: &Vec) -> syn::Result { + for attr in attrs { + if attr.path.is_ident("css") { + let opts: CssOptions = attr.parse_args()?; + return Ok(opts); + } + } + + Ok(CssOptions { + case: convert_case::Case::Kebab, + }) + } +} + +impl Parse for CssOptions { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut case = convert_case::Case::Kebab; + while !input.is_empty() { + let k: Ident = input.parse()?; + let _: Token![=] = input.parse()?; + let v: Ident = input.parse()?; + + if k == "case" { + if v == "lower" { + case = convert_case::Case::Flat; + } + } + } + + Ok(Self { case }) + } +} diff --git a/derive/src/to_css.rs b/derive/src/to_css.rs new file mode 100644 index 000000000..739a16d40 --- /dev/null +++ b/derive/src/to_css.rs @@ -0,0 +1,156 @@ +use convert_case::Casing; +use proc_macro::{self, TokenStream}; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Type}; + +use crate::parse::CssOptions; + +pub fn derive_to_css(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + attrs, + .. + } = parse_macro_input!(input); + + let opts = CssOptions::parse_attributes(&attrs).unwrap(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let imp = match &data { + Data::Enum(data) => derive_enum(&data, &opts), + _ => todo!(), + }; + + let output = quote! { + impl #impl_generics ToCss for #ident #ty_generics #where_clause { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #imp + } + } + }; + + output.into() +} + +fn derive_enum(data: &DataEnum, opts: &CssOptions) -> TokenStream2 { + let variants = data + .variants + .iter() + .map(|variant| { + let name = &variant.ident; + let fields = variant + .fields + .iter() + .enumerate() + .map(|(index, field)| { + field.ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ) + }) + .collect::<_>>(); + + #[derive(PartialEq)] + enum NeedsSpace { + Yes, + No, + Maybe, + } + + let mut needs_space = NeedsSpace::No; + let mut fields_iter = variant.fields.iter().zip(fields.iter()).peekable(); + let mut writes = Vec::new(); + let mut has_needs_space = false; + while let Some((field, name)) = fields_iter.next() { + writes.push(if fields.len() > 1 { + let space = match needs_space { + NeedsSpace::Yes => quote! { dest.write_char(' ')?; }, + NeedsSpace::No => quote! {}, + NeedsSpace::Maybe => { + has_needs_space = true; + quote! { + if needs_space { + dest.write_char(' ')?; + } + } + } + }; + + if is_option(&field.ty) { + needs_space = NeedsSpace::Maybe; + let after_space = if matches!(fields_iter.peek(), Some((field, _)) if !is_option(&field.ty)) { + // If the next field is non-optional, just insert the space here. + needs_space = NeedsSpace::No; + quote! { dest.write_char(' ')?; } + } else { + quote! {} + }; + quote! { + if let Some(v) = #name { + #space + v.to_css(dest)?; + #after_space + } + } + } else { + needs_space = NeedsSpace::Yes; + quote! { + #space + #name.to_css(dest)?; + } + } + } else { + quote! { #name.to_css(dest) } + }); + } + + if writes.len() > 1 { + writes.push(quote! { Ok(()) }); + } + + if has_needs_space { + writes.insert(0, quote! { let mut needs_space = false }); + } + + match variant.fields { + Fields::Unit => { + let s = Literal::string(&variant.ident.to_string().to_case(opts.case)); + quote! { + Self::#name => dest.write_str(#s) + } + } + Fields::Named(_) => { + quote! { + Self::#name { #(#fields),* } => { + #(#writes)* + } + } + } + Fields::Unnamed(_) => { + quote! { + Self::#name(#(#fields),*) => { + #(#writes)* + } + } + } + } + }) + .collect::<_>>(); + + let output = quote! { + match self { + #(#variants),* + } + }; + + output.into() +} + +fn is_option(ty: &Type) -> bool { + matches!(&ty, Type::Path(p) if p.qself.is_none() && p.path.segments.iter().next().unwrap().ident == "Option") +} diff --git a/derive/src/visit.rs b/derive/src/visit.rs new file mode 100644 index 000000000..020933644 --- /dev/null +++ b/derive/src/visit.rs @@ -0,0 +1,292 @@ +use std::collections::HashSet; + +use proc_macro::{self, TokenStream}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{ + parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, + GenericParam, Generics, Ident, Member, Token, Type, Visibility, +}; + +pub fn derive_visit_children(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + attrs, + .. + } = parse_macro_input!(input); + + let options: Vec = attrs + .iter() + .filter_map(|attr| { + if attr.path.is_ident("visit") { + let opts: VisitOptions = attr.parse_args().unwrap(); + Some(opts) + } else { + None + } + }) + .collect(); + + let visit_types = if let Some(attr) = attrs.iter().find(|attr| attr.path.is_ident("visit_types")) { + let types: VisitTypes = attr.parse_args().unwrap(); + let types = types.types; + Some(quote! { crate::visit_types!(#(#types)|*) }) + } else { + None + }; + + if options.is_empty() { + derive(&ident, &data, &generics, None, visit_types) + } else { + options + .into_iter() + .map(|options| derive(&ident, &data, &generics, Some(options), visit_types.clone())) + .collect() + } +} + +fn derive( + ident: &Ident, + data: &Data, + generics: &Generics, + options: Option, + visit_types: Option, +) -> TokenStream { + let mut impl_generics = generics.clone(); + let mut type_defs = quote! {}; + let generics = if let Some(VisitOptions { + generic: Some(generic), .. + }) = &options + { + let mappings = generics + .type_params() + .zip(generic.type_params()) + .map(|(a, b)| quote! { type #a = #b; }); + type_defs = quote! { #(#mappings)* }; + impl_generics.params.clear(); + generic + } else { + &generics + }; + + if impl_generics.lifetimes().next().is_none() { + impl_generics.params.insert(0, parse_quote! { 'i }) + } + + let lifetime = impl_generics.lifetimes().next().unwrap().clone(); + let t = impl_generics.type_params().find(|g| &g.ident.to_string() == "R"); + let v = quote! { __V }; + let t = if let Some(t) = t { + GenericParam::Type(t.ident.clone().into()) + } else { + let t: GenericParam = parse_quote! { __T }; + impl_generics + .params + .push(parse_quote! { #t: crate::visitor::Visit<#lifetime, __T, #v> }); + t + }; + + impl_generics + .params + .push(parse_quote! { #v: ?Sized + crate::visitor::Visitor<#lifetime, #t> }); + + for ty in generics.type_params() { + let name = &ty.ident; + impl_generics.make_where_clause().predicates.push(parse_quote! { + #name: Visit<#lifetime, #t, #v> + }) + } + + let mut seen_types = HashSet::new(); + let mut child_types = Vec::new(); + let mut visit = Vec::new(); + match data { + Data::Struct(s) => { + for ( + index, + Field { + vis, ty, ident, attrs, .. + }, + ) in s.fields.iter().enumerate() + { + if attrs.iter().any(|attr| attr.path.is_ident("skip_visit")) { + continue; + } + + if matches!(ty, Type::Reference(_)) || !matches!(vis, Visibility::Public(..)) { + continue; + } + + if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) { + seen_types.insert(ty.clone()); + child_types.push(quote! { + <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() + }); + } + + let name = ident + .as_ref() + .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); + visit.push(quote! { self.#name.visit(visitor)?; }) + } + } + Data::Enum(DataEnum { variants, .. }) => { + let variants = variants + .iter() + .map(|variant| { + let name = &variant.ident; + let mut field_names = Vec::new(); + let mut visit_fields = Vec::new(); + for (index, Field { ty, ident, attrs, .. }) in variant.fields.iter().enumerate() { + let name = ident.as_ref().map_or_else( + || Ident::new(&format!("_{}", index), Span::call_site()), + |ident| ident.clone(), + ); + field_names.push(name.clone()); + + if matches!(ty, Type::Reference(_)) { + continue; + } + + if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) && !skip_type(&variant.attrs) + { + seen_types.insert(ty.clone()); + child_types.push(quote! { + <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() + }); + } + + visit_fields.push(quote! { #name.visit(visitor)?; }) + } + + match variant.fields { + Fields::Unnamed(_) => { + quote! { + Self::#name(#(#field_names),*) => { + #(#visit_fields)* + } + } + } + Fields::Named(_) => { + quote! { + Self::#name { #(#field_names),* } => { + #(#visit_fields)* + } + } + } + Fields::Unit => quote! {}, + } + }) + .collect::(); + + visit.push(quote! { + match self { + #variants + _ => {} + } + }) + } + _ => {} + } + + if visit_types.is_none() && child_types.is_empty() { + child_types.push(quote! { crate::visitor::VisitTypes::empty().bits() }); + } + + let (_, ty_generics, _) = generics.split_for_impl(); + let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); + + let self_visit = if let Some(VisitOptions { + visit: Some(visit), + kind: Some(kind), + .. + }) = &options + { + child_types.push(quote! { crate::visitor::VisitTypes::#kind.bits() }); + + quote! { + fn visit(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { + if visitor.visit_types().contains(crate::visitor::VisitTypes::#kind) { + visitor.#visit(self) + } else { + self.visit_children(visitor) + } + } + } + } else { + quote! {} + }; + + let child_types = visit_types.unwrap_or_else(|| { + quote! { + { + #type_defs + crate::visitor::VisitTypes::from_bits_retain(#(#child_types)|*) + } + } + }); + + let output = quote! { + impl #impl_generics Visit<#lifetime, #t, #v> for #ident #ty_generics #where_clause { + const CHILD_TYPES: crate::visitor::VisitTypes = #child_types; + + #self_visit + + fn visit_children(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { + if !<#lifetime, #t, #v>>::CHILD_TYPES.intersects(visitor.visit_types()) { + return Ok(()) + } + + #(#visit)* + + Ok(()) + } + } + }; + + output.into() +} + +fn skip_type(attrs: &Vec) -> bool { + attrs.iter().any(|attr| attr.path.is_ident("skip_type")) +} + +struct VisitOptions { + visit: Option, + kind: Option, + generic: Option, +} + +impl Parse for VisitOptions { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let (visit, kind, comma) = if input.peek(Ident) { + let visit: Ident = input.parse()?; + let _: Token![,] = input.parse()?; + let kind: Ident = input.parse()?; + let comma: Result = input.parse(); + (Some(visit), Some(kind), comma.is_ok()) + } else { + (None, None, true) + }; + let generic: Option = if comma { Some(input.parse()?) } else { None }; + Ok(Self { visit, kind, generic }) + } +} + +struct VisitTypes { + types: Vec, +} + +impl Parse for VisitTypes { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let first: Ident = input.parse()?; + let mut types = vec![first]; + while input.parse::().is_ok() { + let id: Ident = input.parse()?; + types.push(id); + } + Ok(Self { types }) + } +} diff --git a/napi/Cargo.toml b/napi/Cargo.toml index 028a34505..789062ea6 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Devon Govett "] name = "lightningcss-napi" -version = "0.1.0" +version = "0.4.7" description = "Node-API bindings for Lightning CSS" license = "MPL-2.0" repository = "https://github.com/parcel-bundler/lightningcss" @@ -13,13 +13,21 @@ visitor = ["lightningcss/visitor"] bundler = ["dep:crossbeam-channel", "dep:rayon"] [dependencies] -serde = { version = "1.0.123", features = ["derive"] } +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 = { 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 8127b18e2..f43a8d46e 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -24,6 +24,7 @@ mod at_rule_parser; mod threadsafe_function; #[cfg(feature = "visitor")] mod transformer; +mod utils; #[cfg(feature = "visitor")] use transformer::JsVisitor; @@ -34,6 +35,8 @@ struct JsVisitor; #[cfg(feature = "visitor")] use lightningcss::visitor::Visit; +use utils::get_named_property; + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct TransformResult<'i> { @@ -73,7 +76,7 @@ impl<'i> TransformResult<'i> { #[cfg(feature = "visitor")] fn get_visitor(env: Env, opts: &JsObject) -> Option { - if let Ok(visitor) = opts.get_named_property::("visitor") { + if let Ok(visitor) = get_named_property::(opts, "visitor") { Some(JsVisitor::new(env, visitor)) } else { None @@ -118,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; @@ -166,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 { @@ -200,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(), @@ -210,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 { @@ -238,17 +238,20 @@ 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()? { let result: JsObject = result.try_into()?; - let then: JsFunction = result.get_named_property("then")?; + 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| { @@ -258,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(()) @@ -271,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) => { @@ -292,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<()> { @@ -306,9 +311,9 @@ mod bundle { let config: BundleConfig = ctx.env.from_js_value(&opts)?; - if let Ok(resolver) = opts.get_named_property::("resolver") { + if let Ok(resolver) = get_named_property::(&opts, "resolver") { let read = if resolver.has_named_property("read")? { - let read = resolver.get_named_property::("read")?; + let read = get_named_property::(&resolver, "read")?; Some(ThreadsafeFunction::create( ctx.env.raw(), unsafe { read.raw() }, @@ -320,7 +325,7 @@ mod bundle { }; let resolve = if resolver.has_named_property("resolve")? { - let resolve = resolver.get_named_property::("resolve")?; + let resolve = get_named_property::(&resolver, "resolve")?; Some(ThreadsafeFunction::create( ctx.env.raw(), unsafe { resolve.raw() }, @@ -418,19 +423,19 @@ 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)?; let mut visitor = get_visitor(*ctx.env, &opts); - let resolver = opts.get_named_property::("resolver")?; - let read = resolver.get_named_property::("read")?; + let resolver = get_named_property::(&opts, "resolver")?; + let read = get_named_property::(&resolver, "read")?; let resolve = if resolver.has_named_property("resolve")? { - let resolve = resolver.get_named_property::("resolve")?; + let resolve = get_named_property::(&resolver, "resolve")?; Some(ctx.env.create_reference(resolve)?) } else { None @@ -494,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(); @@ -504,13 +509,13 @@ mod bundle { return Err(napi::Error::from(error)); } if result.is_null() { - return Err(napi::Error::new(napi::Status::GenericFailure, "No result".into())); + return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string())); } value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; } - value.try_into() + Ok(value) } impl SourceProvider for JsSourceProvider { @@ -520,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)); @@ -532,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))) } } } @@ -602,6 +610,11 @@ enum CssModulesOption { struct CssModulesConfig { pattern: Option, dashed_idents: Option, + animation: Option, + container: Option, + grid: Option, + custom_idents: Option, + pure: Option, } #[cfg(feature = "bundler")] @@ -710,6 +723,11 @@ fn compile<'i>( Default::default() }, dashed_idents: c.dashed_idents.unwrap_or_default(), + animation: c.animation.unwrap_or(true), + container: c.container.unwrap_or(true), + grid: c.grid.unwrap_or(true), + custom_idents: c.custom_idents.unwrap_or(true), + pure: c.pure.unwrap_or_default(), }), } } else { @@ -837,6 +855,11 @@ fn compile_bundle< Default::default() }, dashed_idents: c.dashed_idents.unwrap_or_default(), + animation: c.animation.unwrap_or(true), + container: c.container.unwrap_or(true), + grid: c.grid.unwrap_or(true), + custom_idents: c.custom_idents.unwrap_or(true), + pure: c.pure.unwrap_or_default(), }), } } else { diff --git a/napi/src/transformer.rs b/napi/src/transformer.rs index 111ff2cd1..bab931f2d 100644 --- a/napi/src/transformer.rs +++ b/napi/src/transformer.rs @@ -24,7 +24,7 @@ use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; -use crate::at_rule_parser::AtRule; +use crate::{at_rule_parser::AtRule, utils::get_named_property}; pub struct JsVisitor { env: Env, @@ -99,7 +99,7 @@ impl Visitors { fn named(&self, stage: VisitStage, name: &str) -> Option { self .for_stage(stage) - .and_then(|m| m.get_named_property::(name).ok()) + .and_then(|m| get_named_property::(m, name).ok()) } fn custom(&self, stage: VisitStage, obj: &str, name: &str) -> Option { @@ -112,7 +112,7 @@ impl Visitors { Ok(ValueType::Object) => { let o: napi::Result = v.try_into(); if let Ok(o) = o { - return o.get_named_property::(name).ok(); + return get_named_property::(&o, name).ok(); } } _ => {} @@ -177,7 +177,8 @@ impl JsVisitor { let mut types = VisitTypes::empty(); macro_rules! get { ($name: literal, $( $t: ident )|+) => {{ - let res: Option = visitor.get_named_property($name).ok(); + let res: Option = get_named_property(&visitor, $name).ok(); + if res.is_some() { types |= $( VisitTypes::$t )|+; } @@ -190,12 +191,13 @@ impl JsVisitor { macro_rules! map { ($name: literal, $( $t: ident )|+) => {{ - if let Ok(obj) = visitor.get_named_property::($name) { + let obj: Option = get_named_property(&visitor, $name).ok(); + + if obj.is_some() { types |= $( VisitTypes::$t )|+; - env.create_reference(obj).ok() - } else { - None } + + obj.and_then(|obj| env.create_reference(obj).ok()) }}; } @@ -296,6 +298,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Keyframes(..) => "keyframes", CssRule::FontFace(..) => "font-face", CssRule::FontPaletteValues(..) => "font-palette-values", + CssRule::FontFeatureValues(..) => "font-feature-values", CssRule::Page(..) => "page", CssRule::Supports(..) => "supports", CssRule::CounterStyle(..) => "counter-style", @@ -308,8 +311,10 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Scope(..) => "scope", CssRule::MozDocument(..) => "moz-document", CssRule::Nesting(..) => "nesting", + CssRule::NestedDeclarations(..) => "nested-declarations", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", + CssRule::ViewTransition(..) => "view-transition", CssRule::Unknown(v) => { let name = v.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "unknown", name) { @@ -750,9 +755,8 @@ impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de D: serde::Deserializer<'de>, { use serde::Deserializer; - let content = serde::__private::de::Content::deserialize(deserializer)?; - let de: serde::__private::de::ContentRefDeserializer = - serde::__private::de::ContentRefDeserializer::new(&content); + let content = serde_content::Value::deserialize(deserializer)?; + let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); // Try to deserialize as a sequence first. let mut was_seq = false; @@ -764,13 +768,15 @@ impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de if was_seq { // Allow fallback if we know the value is also a list (e.g. selector). if res.is_ok() || !IS_VEC { - return res.map(ValueOrVec::Vec); + return res.map_err(|e| serde::de::Error::custom(e.to_string())).map(ValueOrVec::Vec); } } // If it wasn't a sequence, try a value. - let de = serde::__private::de::ContentRefDeserializer::new(&content); - return V::deserialize(de).map(ValueOrVec::Value); + let de = serde_content::Deserializer::new(content).coerce_numbers(); + return V::deserialize(de) + .map_err(|e| serde::de::Error::custom(e.to_string())) + .map(ValueOrVec::Value); struct SeqVisitor<'a, V> { was_seq: &'a mut bool, @@ -806,16 +812,14 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { where D: serde::Deserializer<'de>, { - use serde::__private::de::ContentRefDeserializer; - #[derive(serde::Deserialize)] struct Raw<'i> { #[serde(borrow)] raw: CowArcStr<'i>, } - let content = serde::__private::de::Content::deserialize(deserializer)?; - let de: ContentRefDeserializer = ContentRefDeserializer::new(&content); + let content = serde_content::Value::deserialize(deserializer)?; + let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); if let Ok(res) = Raw::deserialize(de) { let res = TokenList::parse_string_with_options(res.raw.as_ref(), ParserOptions::default()) @@ -823,8 +827,10 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { return Ok(TokensOrRaw(ValueOrVec::Vec(res.into_owned().0))); } - let de = ContentRefDeserializer::new(&content); - Ok(TokensOrRaw(ValueOrVec::deserialize(de)?)) + let de = serde_content::Deserializer::new(content).coerce_numbers(); + Ok(TokensOrRaw( + ValueOrVec::deserialize(de).map_err(|e| serde::de::Error::custom(e.to_string()))?, + )) } } diff --git a/napi/src/utils.rs b/napi/src/utils.rs new file mode 100644 index 000000000..90ac14957 --- /dev/null +++ b/napi/src/utils.rs @@ -0,0 +1,7 @@ +use napi::{Error, JsObject, JsUnknown, Result}; + +// Workaround for https://github.com/napi-rs/napi-rs/issues/1641 +pub fn get_named_property>(obj: &JsObject, property: &str) -> Result { + let unknown = obj.get_named_property::(property)?; + T::try_from(unknown) +} diff --git a/node/Cargo.toml b/node/Cargo.toml index b85a64a9b..b5c7505c3 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -9,8 +9,13 @@ publish = false crate-type = ["cdylib"] [dependencies] -lightningcss-napi = { version = "0.1.0", path = "../napi", features = ["bundler", "visitor"] } -napi = {version = "=2.10.3", 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 25c9dd7f5..28e9d0930 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -1,4 +1,4 @@ -/* tslint:disable */ +/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, @@ -33,6 +33,10 @@ export type Rule = | { type: "font-palette-values"; value: FontPaletteValuesRule; } +| { + type: "font-feature-values"; + value: FontFeatureValuesRule; + } | { type: "page"; value: PageRule; @@ -57,6 +61,10 @@ export type Rule = | { type: "nesting"; value: NestingRule; } +| { + type: "nested-declarations"; + value: NestedDeclarationsRule; + } | { type: "viewport"; value: ViewportRule; @@ -89,6 +97,10 @@ export type Rule = | { type: "starting-style"; value: StartingStyleRule; } +| { + type: "view-transition"; + value: ViewTransitionRule; + } | { type: "ignored"; } @@ -122,6 +134,10 @@ export type MediaCondition = */ operator: Operator; type: "operation"; + } + | { + type: "unknown"; + value: TokenOrValue[]; }; /** * A generic media feature or container feature. @@ -504,6 +520,10 @@ export type TokenOrValue = | { type: "dashed-ident"; value: String; + } + | { + type: "animation-name"; + value: AnimationName; }; /** * A raw CSS token. @@ -1114,6 +1134,21 @@ export type Time = type: "milliseconds"; value: number; }; +/** + * A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. + */ +export type AnimationName = + | { + type: "none"; + } + | { + type: "ident"; + value: String; + } + | { + type: "string"; + value: String; + }; /** * A CSS environment variable name. */ @@ -1935,6 +1970,21 @@ export type PropertyId = property: "animation-fill-mode"; vendorPrefix: VendorPrefix; } + | { + property: "animation-composition"; + } + | { + property: "animation-timeline"; + } + | { + property: "animation-range-start"; + } + | { + property: "animation-range-end"; + } + | { + property: "animation-range"; + } | { property: "animation"; vendorPrefix: VendorPrefix; @@ -2066,6 +2116,12 @@ export type PropertyId = property: "text-size-adjust"; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + } + | { + property: "unicode-bidi"; + } | { property: "box-decoration-break"; vendorPrefix: VendorPrefix; @@ -2302,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"; } @@ -3277,6 +3343,26 @@ export type Declaration = value: AnimationFillMode[]; vendorPrefix: VendorPrefix; } + | { + property: "animation-composition"; + value: AnimationComposition[]; + } + | { + property: "animation-timeline"; + value: AnimationTimeline[]; + } + | { + property: "animation-range-start"; + value: AnimationRangeStart[]; + } + | { + property: "animation-range-end"; + value: AnimationRangeEnd[]; + } + | { + property: "animation-range"; + value: AnimationRange[]; + } | { property: "animation"; value: Animation[]; @@ -3445,6 +3531,14 @@ export type Declaration = value: TextSizeAdjust; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + value: Direction2; + } + | { + property: "unicode-bidi"; + value: UnicodeBidi; + } | { property: "box-decoration-break"; value: BoxDecorationBreak; @@ -3751,12 +3845,29 @@ export type Declaration = } | { property: "view-transition-name"; - value: String; + value: ViewTransitionName; + } + | { + property: "view-transition-class"; + value: NoneOrCustomIdentList; + } + | { + property: "view-transition-group"; + value: ViewTransitionGroup; } | { property: "color-scheme"; value: ColorScheme; } + | { + property: "print-color-adjust"; + value: PrintColorAdjust; + vendorPrefix: VendorPrefix; + } + | { + property: "all"; + value: CSSWideKeyword; + } | { property: "unparsed"; value: UnparsedProperty; @@ -4158,23 +4269,30 @@ export type PositionComponentFor_VerticalPositionKeyword = * See [RadialGradient](RadialGradient). */ export type EndingShape = - | { - type: "circle"; - value: Circle; - } | { type: "ellipse"; value: Ellipse; + } + | { + type: "circle"; + value: Circle; }; /** - * A circle ending shape for a `radial-gradient()`. + * An ellipse ending shape for a `radial-gradient()`. * * See [RadialGradient](RadialGradient). */ -export type Circle = +export type Ellipse = | { - type: "radius"; - value: Length; + type: "size"; + /** + * The x-radius of the ellipse. + */ + x: DimensionPercentageFor_LengthValue; + /** + * The y-radius of the ellipse. + */ + y: DimensionPercentageFor_LengthValue; } | { type: "extent"; @@ -4187,21 +4305,14 @@ export type Circle = */ export type ShapeExtent = "closest-side" | "farthest-side" | "closest-corner" | "farthest-corner"; /** - * An ellipse ending shape for a `radial-gradient()`. + * A circle ending shape for a `radial-gradient()`. * * See [RadialGradient](RadialGradient). */ -export type Ellipse = +export type Circle = | { - type: "size"; - /** - * The x-radius of the ellipse. - */ - x: DimensionPercentageFor_LengthValue; - /** - * The y-radius of the ellipse. - */ - y: DimensionPercentageFor_LengthValue; + type: "radius"; + value: Length; } | { type: "extent"; @@ -4366,11 +4477,11 @@ export type WebKitGradientPointComponentFor_HorizontalPositionKeyword = */ export type NumberOrPercentage = | { - type: "percentage"; + type: "number"; value: number; } | { - type: "number"; + type: "percentage"; value: number; }; /** @@ -4681,13 +4792,13 @@ export type RectFor_LengthOrNumber = [LengthOrNumber, LengthOrNumber, LengthOrNu * Either a [``](https://www.w3.org/TR/css-values-4/#lengths) or a [``](https://www.w3.org/TR/css-values-4/#numbers). */ export type LengthOrNumber = - | { - type: "length"; - value: Length; - } | { type: "number"; value: number; + } + | { + type: "length"; + value: Length; }; /** * A single [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) keyword. @@ -5120,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 = | { @@ -5424,21 +5535,6 @@ export type StepPosition = | { type: "jump-both"; }; -/** - * A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. - */ -export type AnimationName = - | { - type: "none"; - } - | { - type: "ident"; - value: String; - } - | { - type: "string"; - value: String; - }; /** * A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property. */ @@ -5462,6 +5558,75 @@ export type AnimationPlayState = "running" | "paused"; * A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property. */ export type AnimationFillMode = "none" | "forwards" | "backwards" | "both"; +/** + * A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property. + */ +export type AnimationComposition = "replace" | "add" | "accumulate"; +/** + * A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property. + */ +export type AnimationTimeline = + | { + type: "auto"; + } + | { + type: "none"; + } + | { + type: "dashed-ident"; + value: String; + } + | { + type: "scroll"; + value: ScrollTimeline; + } + | { + type: "view"; + value: ViewTimeline; + }; +/** + * A scroll axis, used in the `scroll()` function. + */ +export type ScrollAxis = "block" | "inline" | "x" | "y"; +/** + * A scroller, used in the `scroll()` function. + */ +export type Scroller = "root" | "nearest" | "self"; +/** + * A generic value that represents a value with two components, e.g. a border radius. + * + * When serialized, only a single component will be written if both are equal. + * + * @minItems 2 + * @maxItems 2 + */ +export type Size2DFor_LengthPercentageOrAuto = [LengthPercentageOrAuto, LengthPercentageOrAuto]; +/** + * A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. + */ +export type AnimationRangeStart = AnimationAttachmentRange; +/** + * A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. + */ +export type AnimationAttachmentRange = + "normal" | DimensionPercentageFor_LengthValue | { + /** + * The name of the timeline range. + */ + name: TimelineRangeName; + /** + * The offset from the start of the named timeline range. + */ + offset: DimensionPercentageFor_LengthValue; + }; +/** + * A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges) + */ +export type TimelineRangeName = "cover" | "contain" | "entry" | "exit" | "entry-crossing" | "exit-crossing"; +/** + * A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. + */ +export type AnimationRangeEnd = AnimationAttachmentRange; /** * An individual [transform function](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions). */ @@ -5577,7 +5742,7 @@ export type Transform = /** * A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property. */ -export type TransformStyle = "flat" | "preserve-3d"; +export type TransformStyle = "flat" | "preserve3d"; /** * A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property. */ @@ -5597,6 +5762,44 @@ export type Perspective = type: "length"; value: Length; }; +/** + * A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. + */ +export type Translate = + | "none" + | { + /** + * The x translation. + */ + x: DimensionPercentageFor_LengthValue; + /** + * The y translation. + */ + y: DimensionPercentageFor_LengthValue; + /** + * The z translation. + */ + z: Length; + }; +/** + * A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. + */ +export type Scale = + | "none" + | { + /** + * Scale on the x axis. + */ + x: NumberOrPercentage; + /** + * Scale on the y axis. + */ + y: NumberOrPercentage; + /** + * Scale on the z axis. + */ + z: NumberOrPercentage; + }; /** * Defines how text case should be transformed in the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. */ @@ -5729,6 +5932,14 @@ export type TextSizeAdjust = type: "percentage"; value: number; }; +/** + * A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. + */ +export type Direction2 = "ltr" | "rtl"; +/** + * A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. + */ +export type UnicodeBidi = "normal" | "embed" | "isolate" | "bidi-override" | "isolate-override" | "plaintext"; /** * A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property. */ @@ -5927,9 +6138,6 @@ export type MarkerSide = "match-self" | "match-parent"; * An SVG [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value used in the `fill` and `stroke` properties. */ export type SVGPaint = - | { - type: "none"; - } | { /** * A fallback to be used used in case the paint server cannot be resolved. @@ -5950,6 +6158,9 @@ export type SVGPaint = } | { type: "context-stroke"; + } + | { + type: "none"; }; /** * A fallback for an SVG paint in case a paint server `url()` cannot be resolved. @@ -6222,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. */ @@ -6234,6 +6445,29 @@ export type ContainerNameList = type: "names"; value: String[]; }; +/** + * A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property. + */ +export type ViewTransitionName = + "none" | "auto" | String; +/** + * The `none` keyword, or a space-separated list of custom idents. + */ +export type NoneOrCustomIdentList = + "none" | String[]; +/** + * A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property. + */ +export type ViewTransitionGroup = + "normal" | "contain" | "nearest" | String; +/** + * A value for the [print-color-adjust](https://drafts.csswg.org/css-color-adjust/#propdef-print-color-adjust) property. + */ +export type PrintColorAdjust = "economy" | "exact"; +/** + * A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). + */ +export type CSSWideKeyword = "initial" | "inherit" | "unset" | "revert" | "revert-layer"; /** * A CSS custom property name. */ @@ -6557,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"; /** @@ -6637,6 +6888,12 @@ export type PseudoElement = | { kind: "first-letter"; } + | { + kind: "details-content"; + } + | { + kind: "target-text"; + } | { kind: "selection"; vendorPrefix: VendorPrefix; @@ -6688,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"; @@ -6756,6 +7032,10 @@ export type KeyframeSelector = } | { type: "to"; + } + | { + type: "timeline-range-percentage"; + value: TimelineRangePercentage; }; /** * KeyframesName @@ -6936,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). */ @@ -6982,6 +7273,10 @@ export type ParsedComponent = type: "length-percentage"; value: DimensionPercentageFor_LengthValue; } + | { + type: "string"; + value: String; + } | { type: "color"; value: CssColor; @@ -7040,8 +7335,8 @@ export type ParsedComponent = }; } | { - type: "token"; - value: Token; + type: "token-list"; + value: TokenOrValue[]; }; /** * A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated. @@ -7083,6 +7378,9 @@ export type SyntaxComponentKind = | { type: "length-percentage"; } + | { + type: "string"; + } | { type: "color"; } @@ -7142,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. @@ -7215,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; @@ -7233,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; /** @@ -8084,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 { @@ -8103,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 { @@ -8132,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 { /** @@ -8145,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 { /** @@ -8158,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 { /** @@ -8420,6 +8847,45 @@ export interface Transition { */ timingFunction: EasingFunction; } +/** + * The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function. + */ +export interface ScrollTimeline { + /** + * Specifies which axis of the scroll container to use as the progress for the timeline. + */ + axis: ScrollAxis; + /** + * Specifies which element to use as the scroll container. + */ + scroller: Scroller; +} +/** + * The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function. + */ +export interface ViewTimeline { + /** + * Specifies which axis of the scroll container to use as the progress for the timeline. + */ + axis: ScrollAxis; + /** + * Provides an adjustment of the view progress visibility range. + */ + inset: Size2DFor_LengthPercentageOrAuto; +} +/** + * A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property. + */ +export interface AnimationRange { + /** + * The end of the animation's attachment range. + */ + end: AnimationRangeEnd; + /** + * The start of the animation's attachment range. + */ + start: AnimationRangeStart; +} /** * A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property. */ @@ -8452,6 +8918,10 @@ export interface Animation { * The current play state of the animation. */ playState: AnimationPlayState; + /** + * The animation timeline. + */ + timeline: AnimationTimeline; /** * The easing function for the animation. */ @@ -8489,23 +8959,6 @@ export interface Matrix3DForFloat { m43: number; m44: number; } -/** - * A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. - */ -export interface Translate { - /** - * The x translation. - */ - x: DimensionPercentageFor_LengthValue; - /** - * The y translation. - */ - y: DimensionPercentageFor_LengthValue; - /** - * The z translation. - */ - z: Length; -} /** * A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. */ @@ -8527,23 +8980,6 @@ export interface Rotate { */ z: number; } -/** - * A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. - */ -export interface Scale { - /** - * Scale on the x axis. - */ - x: NumberOrPercentage; - /** - * Scale on the y axis. - */ - y: NumberOrPercentage; - /** - * Scale on the z axis. - */ - z: NumberOrPercentage; -} /** * A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. */ @@ -8937,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. */ @@ -8973,6 +9422,19 @@ export interface Keyframe { */ selectors: KeyframeSelector[]; } +/** + * A percentage of a given timeline range + */ +export interface TimelineRangePercentage { + /** + * The name of the timeline range. + */ + name: TimelineRangeName; + /** + * The percentage progress between the start and end of the range. + */ + percentage: number; +} /** * A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule. */ @@ -9048,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. */ @@ -9180,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. */ @@ -9293,7 +9806,7 @@ export interface ContainerRule { /** * The container condition. */ - condition: ContainerCondition; + condition?: ContainerCondition | null; /** * The location of the rule in the source file. */ @@ -9343,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 16030b51c..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)?; @@ -41,12 +41,15 @@ fn init(mut exports: JsObject) -> napi::Result<()> { #[cfg(target_arch = "wasm32")] #[no_mangle] -pub unsafe fn napi_register_wasm_v1(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) { - use napi::{Env, JsObject, NapiValue}; +pub fn register_module() { + unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> { + use napi::{Env, JsObject, NapiValue}; - let env = Env::from_raw(raw_env); - let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); - init(exports); + let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); + init(exports) + } + + napi::bindgen_prelude::register_module_exports(register) } #[cfg(target_arch = "wasm32")] 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 d8fcb832a..af17c0a58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.24.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.0", + "@mdn/browser-compat-data": "~7.2.4", "@napi-rs/cli": "^2.14.0", - "autoprefixer": "^10.4.17", - "caniuse-lite": "^1.0.30001585", + "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 4e6bbebf5..883aa442d 100644 --- a/scripts/build-ast.js +++ b/scripts/build-ast.js @@ -55,6 +55,48 @@ compileFromFile('node/ast.json', { if (path.node.name.startsWith('GenericBorderFor_LineStyleAnd_')) { path.node.name = 'GenericBorderFor_LineStyle'; } + }, + TSTypeAliasDeclaration(path) { + // Workaround for schemars not supporting untagged variants. + // https://github.com/GREsau/schemars/issues/222 + if ( + (path.node.id.name === 'Translate' || path.node.id.name === 'Scale') && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[1].members[0].key.name === 'xyz' + ) { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } else if (path.node.id.name === 'AnimationAttachmentRange' && path.node.typeAnnotation.type === 'TSUnionType') { + let types = path.node.typeAnnotation.types; + if (types[1].type === 'TSTypeLiteral' && types[1].members[0].key.name === 'lengthpercentage') { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } + + if (types[2].type === 'TSTypeLiteral' && types[2].members[0].key.name === 'timelinerange') { + path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); + } + } else if ( + path.node.id.name === 'NoneOrCustomIdentList' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[1].members[0].key.name === 'idents' + ) { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } else if ( + path.node.id.name === 'ViewTransitionGroup' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[3].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[3].members[0].key.name === 'custom' + ) { + path.get('typeAnnotation.types.3').replaceWith(path.node.typeAnnotation.types[3].members[0].typeAnnotation.typeAnnotation); + } else if ( + path.node.id.name === 'ViewTransitionName' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[2].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[2].members[0].key.name === 'custom' + ) { + path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); + } } }); diff --git a/scripts/build-npm.js b/scripts/build-npm.js index dd4c91bc3..ef447a7ea 100644 --- a/scripts/build-npm.js +++ b/scripts/build-npm.js @@ -15,6 +15,9 @@ const triples = [ { name: 'x86_64-pc-windows-msvc', }, + { + name: 'aarch64-pc-windows-msvc' + }, { name: 'aarch64-apple-darwin', }, @@ -35,6 +38,9 @@ const triples = [ }, { name: 'x86_64-unknown-freebsd' + }, + { + name: 'aarch64-linux-android' } ]; const cpuToNodeArch = { @@ -48,6 +54,7 @@ const sysToNodePlatform = { freebsd: 'freebsd', darwin: 'darwin', windows: 'win32', + android: 'android' }; let optionalDependencies = {}; diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js index 597e1dbdc..3a9f7945f 100644 --- a/scripts/build-prefixes.js +++ b/scripts/build-prefixes.js @@ -25,6 +25,7 @@ const MDN_BROWSER_MAPPING = { firefox_android: 'firefox', opera_android: 'opera', safari_ios: 'ios_saf', + webview_ios: 'ios_saf', samsunginternet_android: 'samsung', webview_android: 'android', oculus: null, @@ -70,6 +71,10 @@ prefixes['any-pseudo'] = { }) } +// Safari 4-13 supports background-clip: text with a prefix. +prefixes['background-clip'].browsers.push('safari 13'); +prefixes['background-clip'].browsers.push('ios_saf 4', 'ios_saf 13'); + let flexSpec = {}; let oldGradient = {}; let p = new Map(); @@ -253,7 +258,7 @@ for (let feature of cssFeatures) { addValue(compat, {}, 'custom-media-queries'); let mdnFeatures = { - doublePositionGradients: mdn.css.types.image.gradient['radial-gradient'].doubleposition.__compat.support, + doublePositionGradients: mdn.css.types.gradient['radial-gradient'].doubleposition.__compat.support, clampFunction: mdn.css.types.clamp.__compat.support, placeSelf: mdn.css.properties['place-self'].__compat.support, placeContent: mdn.css.properties['place-content'].__compat.support, @@ -282,7 +287,7 @@ let mdnFeatures = { logicalPaddingShorthand: mdn.css.properties['padding-inline'].__compat.support, logicalInset: mdn.css.properties['inset-inline-start'].__compat.support, logicalSize: mdn.css.properties['inline-size'].__compat.support, - logicalTextAlign: mdn.css.properties['text-align']['flow_relative_values_start_and_end'].__compat.support, + logicalTextAlign: mdn.css.properties['text-align'].start.__compat.support, labColors: mdn.css.types.color.lab.__compat.support, oklabColors: mdn.css.types.color.oklab.__compat.support, colorFunction: mdn.css.types.color.color.__compat.support, @@ -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, @@ -329,6 +334,29 @@ let mdnFeatures = { fontStretchPercentage: mdn.css.properties['font-stretch'].percentage.__compat.support, lightDark: mdn.css.types.color['light-dark'].__compat.support, accentSystemColor: mdn.css.types.color['system-color'].accentcolor_accentcolortext.__compat.support, + animationTimelineShorthand: mdn.css.properties.animation['animation-timeline_included'].__compat.support, + viewTransition: mdn.css.selectors['view-transition'].__compat.support, + detailsContent: mdn.css.selectors['details-content'].__compat.support, + targetText: mdn.css.selectors['target-text'].__compat.support, + picker: mdn.css.selectors.picker.__compat.support, + pickerIcon: mdn.css.selectors['picker-icon'].__compat.support, + checkmark: mdn.css.selectors.checkmark.__compat.support, + grammarError: mdn.css.selectors['grammar-error'].__compat.support, + spellingError: mdn.css.selectors['spelling-error'].__compat.support, + statePseudoClass: Object.fromEntries( + Object.entries(mdn.css.selectors.state.__compat.support) + .map(([browser, value]) => { + // Chrome/Edge 90-124 supported old :--foo syntax which was removed. + // Only include full :state(foo) support from 125+. + if (Array.isArray(value)) { + value = value.filter(v => !v.partial_implementation) + } else if (value.partial_implementation) { + value = undefined; + } + + return [browser, value]; + }) + ), }; for (let key in mdn.css.types.length) { @@ -343,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([ @@ -376,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; } @@ -463,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()) @@ -636,7 +665,10 @@ impl Feature { if self.is_compatible(browsers) { return true } - browsers.${browser} = None; + #[allow(unused_assignments)] + { + browsers.${browser} = None; + } }\n`).join(' ')} false } diff --git a/scripts/build-wasm.js b/scripts/build-wasm.js index 9f69a9af8..718821da9 100644 --- a/scripts/build-wasm.js +++ b/scripts/build-wasm.js @@ -54,15 +54,21 @@ wasmPkg.type = 'module'; wasmPkg.main = 'index.mjs'; wasmPkg.module = 'index.mjs'; wasmPkg.exports = { - types: './index.d.ts', - node: { - import: './wasm-node.mjs', - require: './wasm-node.cjs', + '.': { + types: './index.d.ts', + node: { + import: './wasm-node.mjs', + require: './wasm-node.cjs' + }, + default: { + import: './index.mjs', + require: './index.cjs' + } }, - default: { - import: './index.mjs', - require: './index.cjs', - } + // Allow esbuild to import the wasm file + // without copying it in the src directory. + // Simplifies loading it in the browser when used in a library. + './lightningcss_node.wasm': './lightningcss_node.wasm' }; wasmPkg.types = 'index.d.ts'; wasmPkg.sideEffects = false; diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index c3dd94026..824b2f2bd 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parcel_selectors" -version = "0.26.4" +version = "0.28.2" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/parcel_selectors/" description = "CSS Selectors matching for Rust - forked for lightningcss" @@ -9,6 +9,7 @@ readme = "README.md" keywords = ["css", "selectors"] license = "MPL-2.0" build = "build.rs" +edition = "2021" [lib] name = "parcel_selectors" @@ -24,14 +25,14 @@ serde = ["dep:serde", "smallvec/serde"] [dependencies] bitflags = "2.2.1" cssparser = "0.33.0" -fxhash = "0.2" +rustc-hash = "2" log = "0.4" -phf = "0.10" +phf = "0.11.2" precomputed-hash = "0.1" smallvec = "1.0" -serde = { version = "1.0.123", features = ["derive"], optional = true } -schemars = { version = "0.8.11", features = ["smallvec"], optional = true } -static-self = { version = "0.1.0", path = "../static-self", optional = true } +serde = { version = "1.0.201", features = ["derive"], optional = true } +schemars = { version = "0.8.19", features = ["smallvec"], optional = true } +static-self = { version = "0.1.2", path = "../static-self", optional = true } [build-dependencies] -phf_codegen = "0.10" +phf_codegen = "0.11" diff --git a/selectors/bloom.rs b/selectors/bloom.rs index bbfbee45b..e9d2f7307 100644 --- a/selectors/bloom.rs +++ b/selectors/bloom.rs @@ -283,7 +283,7 @@ fn hash2(hash: u32) -> u32 { #[test] fn create_and_insert_some_stuff() { - use fxhash::FxHasher; + use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; use std::mem::transmute; diff --git a/selectors/build.rs b/selectors/build.rs index 945bb9bb6..787e2d80d 100644 --- a/selectors/build.rs +++ b/selectors/build.rs @@ -2,8 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -extern crate phf_codegen; - use std::env; use std::fs::File; use std::io::{BufWriter, Write}; diff --git a/selectors/lib.rs b/selectors/lib.rs index 56217d284..2047b4e61 100644 --- a/selectors/lib.rs +++ b/selectors/lib.rs @@ -9,18 +9,8 @@ extern crate bitflags; #[macro_use] extern crate cssparser; -extern crate fxhash; #[macro_use] extern crate log; -extern crate phf; -extern crate precomputed_hash; -#[cfg(feature = "jsonschema")] -extern crate schemars; -#[cfg(feature = "serde")] -extern crate serde; -extern crate smallvec; -#[cfg(feature = "into_owned")] -extern crate static_self; pub mod attr; pub mod bloom; diff --git a/selectors/matching.rs b/selectors/matching.rs index ce3d7a59f..61f74a85c 100644 --- a/selectors/matching.rs +++ b/selectors/matching.rs @@ -60,7 +60,7 @@ impl ElementSelectorFlags { } /// Holds per-compound-selector data. -struct LocalMatchingContext<'a, 'b: 'a, 'i, Impl: SelectorImpl<'i>> { +struct LocalMatchingContext<'a, 'b, 'i, Impl: SelectorImpl<'i>> { shared: &'a mut MatchingContext<'b, 'i, Impl>, matches_hover_and_active_quirk: MatchesHoverAndActiveQuirk, } diff --git a/selectors/nth_index_cache.rs b/selectors/nth_index_cache.rs index 2ca33e7bb..c5bb8db0a 100644 --- a/selectors/nth_index_cache.rs +++ b/selectors/nth_index_cache.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::tree::OpaqueElement; -use fxhash::FxHashMap; +use rustc_hash::FxHashMap; /// A cache to speed up matching of nth-index-like selectors. /// diff --git a/selectors/parser.rs b/selectors/parser.rs index 25b1445b5..19563d5f9 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -197,7 +197,9 @@ pub enum SelectorParseErrorKind<'i> { MissingNestingPrefix, UnexpectedTokenInAttributeSelector(Token<'i>), PseudoElementExpectedIdent(Token<'i>), - UnsupportedPseudoClassOrElement(CowRcStr<'i>), + UnsupportedPseudoElement(CowRcStr<'i>), + UnsupportedPseudoClass(CowRcStr<'i>), + AmbiguousCssModuleClass(CowRcStr<'i>), UnexpectedIdent(CowRcStr<'i>), ExpectedNamespace(CowRcStr<'i>), ExpectedBarInAttr(Token<'i>), @@ -205,6 +207,7 @@ pub enum SelectorParseErrorKind<'i> { InvalidQualNameInAttr(Token<'i>), ExplicitNamespaceUnexpectedToken(Token<'i>), ClassNeedsIdent(Token<'i>), + UnexpectedSelectorAfterPseudoElement(Token<'i>), } macro_rules! with_all_bounds { @@ -310,7 +313,7 @@ pub trait Parser<'i> { location: SourceLocation, name: CowRcStr<'i>, ) -> Result<<'i>>::NonTSPseudoClass, ParseError<'i, Self::Error>> { - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_non_ts_functional_pseudo_class<'t>( @@ -318,7 +321,7 @@ pub trait Parser<'i> { name: CowRcStr<'i>, arguments: &mut CssParser<'i, 't>, ) -> Result<<'i>>::NonTSPseudoClass, ParseError<'i, Self::Error>> { - Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_pseudo_element( @@ -326,7 +329,7 @@ pub trait Parser<'i> { location: SourceLocation, name: CowRcStr<'i>, ) -> Result<<'i>>::PseudoElement, ParseError<'i, Self::Error>> { - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name))) } fn parse_functional_pseudo_element<'t>( @@ -334,7 +337,7 @@ pub trait Parser<'i> { name: CowRcStr<'i>, arguments: &mut CssParser<'i, 't>, ) -> Result<<'i>>::PseudoElement, ParseError<'i, Self::Error>> { - Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name))) } fn default_namespace(&self) -> Option<<'i>>::NamespaceUrl> { @@ -877,12 +880,12 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> { /// Returns an iterator over the entire sequence of simple selectors and /// combinators, in matching order (from right to left). #[inline] - pub fn iter_raw_match_order(&self) -> slice::Iter<'i, Impl>> { + pub fn iter_raw_match_order(&self) -> slice::Iter<'_, Component<'i, Impl>> { self.1.iter() } #[inline] - pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut<'i, Impl>> { + pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut<'_, Component<'i, Impl>> { self.1.iter_mut() } @@ -900,7 +903,7 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> { /// combinators, in parse order (from left to right), starting from /// `offset`. #[inline] - pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev<'i, Impl>>> { + pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev<'_, Component<'i, Impl>>> { self.1[..self.len() - offset].iter().rev() } @@ -1006,7 +1009,7 @@ impl<'i, Impl: SelectorImpl<'i>> From<'i, Impl>>> for Selector<'i, } #[derive(Clone)] -pub struct SelectorIter<'a, 'i, Impl: 'a + SelectorImpl<'i>> { +pub struct SelectorIter<'a, 'i, Impl: SelectorImpl<'i>> { iter: slice::Iter<'a, Component<'i, Impl>>, next_combinator: Option, } @@ -1089,7 +1092,7 @@ impl<'a, 'i, Impl: SelectorImpl<'i>> fmt::Debug for SelectorIter<'a, 'i, Impl> { } /// An iterator over all simple selectors belonging to ancestors. -struct AncestorIter<'a, 'i, Impl: 'a + SelectorImpl<'i>>(SelectorIter<'a, 'i, Impl>); +struct AncestorIter<'a, 'i, Impl: SelectorImpl<'i>>(SelectorIter<'a, 'i, Impl>); impl<'a, 'i, Impl: 'a + SelectorImpl<'i>> AncestorIter<'a, 'i, Impl> { /// Creates an AncestorIter. The passed-in iterator is assumed to point to /// the beginning of the child sequence, which will be skipped. @@ -1299,7 +1302,7 @@ impl NthSelectorData { /// Writes the beginning of the selector. #[inline] - fn write_start(&self, dest: &mut W, is_function: bool) -> fmt::Result { + pub fn write_start(&self, dest: &mut W, is_function: bool) -> fmt::Result { dest.write_str(match self.ty { NthType::Child if is_function => ":nth-child(", NthType::Child => ":first-child", @@ -1319,7 +1322,7 @@ impl NthSelectorData { /// Serialize (part of the CSS Syntax spec, but currently only used here). /// #[inline] - fn write_affine(&self, dest: &mut W) -> fmt::Result { + pub fn write_affine(&self, dest: &mut W) -> fmt::Result { match (self.a, self.b) { (0, 0) => dest.write_char('0'), @@ -2140,6 +2143,14 @@ where } if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + // Input should be exhausted here. + let source_location = input.current_source_location(); + if let Ok(next) = input.next() { + let next = next.clone(); + return Err( + source_location.new_custom_error(SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(next)), + ); + } break; } @@ -2955,6 +2966,7 @@ where Impl: SelectorImpl<'i>, { let start = input.state(); + let token_location = input.current_source_location(); let token = match input.next_including_whitespace().map(|t| t.clone()) { Ok(t) => t, Err(..) => { @@ -2966,14 +2978,18 @@ where Ok(Some(match token { Token::IDHash(id) => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { - return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + return Err(token_location.new_custom_error( + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::IDHash(id)), + )); } let id = Component::ID(id.into()); SimpleSelectorParseResult::SimpleSelector(id) } Token::Delim('.') => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { - return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + return Err(token_location.new_custom_error( + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::Delim('.')), + )); } let location = input.current_source_location(); let class = match *input.next_including_whitespace()? { @@ -2988,7 +3004,9 @@ where } Token::SquareBracketBlock => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { - return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + return Err(token_location.new_custom_error( + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::SquareBracketBlock), + )); } let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?; SimpleSelectorParseResult::SimpleSelector(attr) @@ -3336,7 +3354,7 @@ pub mod tests { "active" => return Ok(PseudoClass::Active), _ => {} } - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_non_ts_functional_pseudo_class<'t>( @@ -3351,7 +3369,7 @@ pub mod tests { }, _ => {} } - Err(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name))) } fn parse_pseudo_element( @@ -3364,7 +3382,7 @@ pub mod tests { "after" => return Ok(PseudoElement::After), _ => {} } - Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name))) + Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name))) } fn default_namespace(&self) -> Option { @@ -3376,7 +3394,7 @@ pub mod tests { } } - fn parse<'i>(input: &'i str) -> Result, SelectorParseError<'i>> { + fn parse<'i>(input: &'i str) -> Result<'i, DummySelectorImpl>, SelectorParseError<'i>> { parse_ns(input, &DummyParser::default()) } @@ -3911,6 +3929,20 @@ pub mod tests { assert!(parse("foo:where()").is_err()); assert!(parse("foo:where(div, foo, .bar baz)").is_ok()); assert!(parse("foo:where(::before)").is_err()); + + assert!(parse("foo::details-content").is_ok()); + assert!(parse("foo::target-text").is_ok()); + + assert!(parse("select::picker").is_err()); + assert!(parse("::picker()").is_err()); + assert!(parse("::picker(select)").is_ok()); + assert!(parse("select::picker-icon").is_ok()); + assert!(parse("option::checkmark").is_ok()); + + assert!(parse("::grammar-error").is_ok()); + assert!(parse("::spelling-error").is_ok()); + assert!(parse("::part(mypart)::grammar-error").is_ok()); + assert!(parse("::part(mypart)::spelling-error").is_ok()); } #[test] diff --git a/src/bundler.rs b/src/bundler.rs index 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 1a4bec654..f0c828d75 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -10,11 +10,14 @@ pub enum Feature { AfarListStyleType, AmharicAbegedeListStyleType, AmharicListStyleType, + AnchorSizeSize, + AnimationTimelineShorthand, AnyLink, AnyPseudo, ArabicIndicListStyleType, ArmenianListStyleType, AsterisksListStyleType, + AutoSize, Autofill, BengaliListStyleType, BinaryListStyleType, @@ -25,6 +28,7 @@ pub enum Feature { CapUnit, CaseInsensitive, ChUnit, + Checkmark, CircleListStyleType, CjkDecimalListStyleType, CjkEarthlyBranchListStyleType, @@ -39,6 +43,7 @@ pub enum Feature { DecimalLeadingZeroListStyleType, DecimalListStyleType, DefaultPseudo, + DetailsContent, DevanagariListStyleType, Dialog, DirSelector, @@ -82,6 +87,7 @@ pub enum Feature { Gencontent, GeorgianListStyleType, GradientInterpolationHints, + GrammarError, GujaratiListStyleType, GurmukhiListStyleType, HasSelector, @@ -94,7 +100,6 @@ pub enum Feature { ImageSet, InOutOfRange, IndeterminatePseudo, - IsAnimatableSize, IsSelector, JapaneseFormalListStyleType, JapaneseInformalListStyleType, @@ -138,10 +143,10 @@ pub enum Feature { MinFunction, ModFunction, MongolianListStyleType, - MozAvailableSize, MyanmarListStyleType, Namespaces, Nesting, + NoneListStyleType, NotSelectorList, NthChildOf, OctalListStyleType, @@ -153,6 +158,8 @@ pub enum Feature { P3Colors, PartPseudo, PersianListStyleType, + Picker, + PickerIcon, PlaceContent, PlaceItems, PlaceSelf, @@ -182,11 +189,14 @@ pub enum Feature { SimpChineseInformalListStyleType, SomaliListStyleType, SpaceSeparatedColorNotation, + SpellingError, SquareListStyleType, + StatePseudoClass, StretchSize, StringListStyleType, SymbolsListStyleType, TamilListStyleType, + TargetText, TeluguListStyleType, TextDecorationThicknessPercent, TextDecorationThicknessShorthand, @@ -208,6 +218,7 @@ pub enum Feature { VbUnit, VhUnit, ViUnit, + ViewTransition, ViewportPercentageUnitsDynamic, ViewportPercentageUnitsLarge, ViewportPercentageUnitsSmall, @@ -441,7 +452,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -533,7 +544,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -578,7 +589,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -623,11 +634,16 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + 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; } } @@ -663,7 +679,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -708,7 +724,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -753,7 +769,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -798,7 +814,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -890,7 +906,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -935,7 +951,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -999,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 < 7929856 { + if version < 9371648 { return false; } } @@ -1024,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; } } @@ -1060,7 +1066,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1150,7 +1156,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1195,7 +1201,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1245,7 +1251,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1332,7 +1338,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1377,7 +1383,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1422,11 +1428,16 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + 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; } } @@ -1462,7 +1473,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1507,7 +1518,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1552,7 +1563,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1619,7 +1630,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 7929856 { + if version < 9371648 { return false; } } @@ -1632,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 { @@ -2205,7 +2216,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 458752 { return false; } } @@ -2509,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; @@ -2519,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; } } @@ -2645,7 +2666,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -2764,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; @@ -2774,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; } } @@ -2868,17 +2899,27 @@ impl Feature { return false; } } - Feature::RoundFunction - | Feature::RemFunction - | Feature::ModFunction - | Feature::AbsFunction - | Feature::SignFunction - | Feature::HypotFunction => { + Feature::RoundFunction | Feature::RemFunction | Feature::ModFunction => { + if let Some(version) = browsers.chrome { + if version < 8192000 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 8192000 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 7733248 { return false; } } + if let Some(version) = browsers.opera { + if version < 5439488 { + return false; + } + } if let Some(version) = browsers.safari { if version < 984064 { return false; @@ -2889,13 +2930,102 @@ impl Feature { return false; } } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.samsung { + if version < 1769472 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8192000 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::AbsFunction | Feature::SignFunction => { + if let Some(version) = browsers.chrome { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 9043968 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5963776 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 9043968 { + return false; + } + } + if browsers.ie.is_some() || browsers.samsung.is_some() { + return false; + } + } + Feature::HypotFunction => { + if let Some(version) = browsers.chrome { + if version < 7864320 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7864320 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5242880 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 984064 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7864320 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -2986,7 +3116,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -3080,7 +3210,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 262144 { + if version < 2752512 { return false; } } @@ -3271,19 +3401,42 @@ impl Feature { return false; } } + if let Some(version) = browsers.edge { + if version < 8060928 { + return false; + } + } if let Some(version) = browsers.firefox { if version < 7864320 { return false; } } - if browsers.android.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.opera.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() - { + if let Some(version) = browsers.opera { + if version < 5373952 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1115392 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1115392 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1769472 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 8060928 { + return false; + } + } + if browsers.ie.is_some() { return false; } } @@ -3313,69 +3466,391 @@ impl Feature { return false; } } - Feature::QUnit => { + Feature::AnimationTimelineShorthand => { if let Some(version) = browsers.chrome { - if version < 4128768 { + if version < 7536640 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3211264 { + if version < 7536640 { return false; } } if let Some(version) = browsers.opera { - if version < 3014656 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 852224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 852992 { + if version < 5046272 { return false; } } if let Some(version) = browsers.samsung { - if version < 524288 { + if version < 1507328 { return false; } } if let Some(version) = browsers.android { - if version < 4128768 { + if version < 7536640 { return false; } } - if browsers.ie.is_some() { + if browsers.firefox.is_some() + || browsers.ie.is_some() + || browsers.ios_saf.is_some() + || browsers.safari.is_some() + { return false; } } - Feature::CapUnit => { + Feature::ViewTransition => { if let Some(version) = browsers.chrome { - if version < 7667712 { + if version < 7143424 { return false; } } if let Some(version) = browsers.edge { - if version < 7667712 { + if version < 7143424 { return false; } } if let Some(version) = browsers.firefox { - if version < 6356992 { + if version < 9437184 { return false; } } if let Some(version) = browsers.opera { - if version < 5111808 { + 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 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 5177344 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 3211264 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 3014656 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 852224 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 852992 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 524288 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 4128768 { + return false; + } + } + if browsers.ie.is_some() { + return false; + } + } + Feature::CapUnit => { + if let Some(version) = browsers.chrome { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 6356992 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 5177344 { return false; } } @@ -3389,12 +3864,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } if let Some(version) = browsers.android { - if version < 7667712 { + if version < 7733248 { return false; } } - if browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -3680,17 +4160,22 @@ impl Feature { } Feature::RcapUnit => { if let Some(version) = browsers.chrome { - if version < 7667712 { + if version < 7733248 { return false; } } if let Some(version) = browsers.edge { - if version < 7667712 { + if version < 7733248 { + return false; + } + } + if let Some(version) = browsers.firefox { + if version < 9633792 { return false; } } if let Some(version) = browsers.opera { - if version < 5111808 { + if version < 5177344 { return false; } } @@ -3704,12 +4189,17 @@ impl Feature { return false; } } + if let Some(version) = browsers.samsung { + if version < 1638400 { + return false; + } + } if let Some(version) = browsers.android { - if version < 7667712 { + if version < 7733248 { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() { + if browsers.ie.is_some() { return false; } } @@ -3724,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; @@ -3749,7 +4244,7 @@ impl Feature { return false; } } - if browsers.firefox.is_some() || browsers.ie.is_some() { + if browsers.ie.is_some() { return false; } } @@ -4120,7 +4615,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4167,7 +4662,7 @@ impl Feature { } } if let Some(version) = browsers.android { - if version < 2424832 { + if version < 263168 { return false; } } @@ -4670,14 +5165,16 @@ impl Feature { | Feature::HiraganaListStyleType | Feature::HiraganaIrohaListStyleType | Feature::KatakanaListStyleType - | Feature::KatakanaIrohaListStyleType => { + | Feature::KatakanaIrohaListStyleType + | Feature::NoneListStyleType + | Feature::AutoSize => { if let Some(version) = browsers.chrome { if version < 1179648 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 786432 { return false; } } @@ -4686,6 +5183,11 @@ impl Feature { return false; } } + if let Some(version) = browsers.ie { + if version < 720896 { + return false; + } + } if let Some(version) = browsers.opera { if version < 917504 { return false; @@ -4711,9 +5213,6 @@ impl Feature { return false; } } - if browsers.ie.is_some() { - return false; - } } Feature::KoreanHangulFormalListStyleType | Feature::KoreanHanjaFormalListStyleType @@ -4965,44 +5464,44 @@ impl Feature { return false; } } - Feature::FitContentSize => { + Feature::AnchorSizeSize => { if let Some(version) = browsers.chrome { - if version < 1638400 { + if version < 8192000 { return false; } } if let Some(version) = browsers.edge { - if version < 5177344 { + if version < 8192000 { 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; } } @@ -5010,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; } } @@ -5056,10 +5550,13 @@ impl Feature { return false; } } + if browsers.ie.is_some() { + return false; + } } Feature::MaxContentSize => { if let Some(version) = browsers.chrome { - if version < 3014656 { + if version < 1638400 { return false; } } @@ -5089,12 +5586,12 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version < 327680 { + if version < 66816 { return false; } } if let Some(version) = browsers.android { - if version < 3014656 { + if version < 263168 { return false; } } @@ -5147,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; } } @@ -5197,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; } @@ -5238,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 b794bb431..ce7008df2 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -25,13 +25,41 @@ use std::hash::{Hash, Hasher}; use std::path::Path; /// Configuration for CSS modules. -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct Config<'i> { /// The name pattern to use when renaming class names and other identifiers. /// Default is `[hash]_[local]`. pub pattern: Pattern<'i>, /// Whether to rename dashed identifiers, e.g. custom properties. pub dashed_idents: bool, + /// Whether to scope animation names. + /// Default is `true`. + pub animation: bool, + /// Whether to scope grid names. + /// Default is `true`. + pub grid: bool, + /// Whether to scope custom identifiers + /// Default is `true`. + pub custom_idents: bool, + /// Whether to scope container names. + /// Default is `true`. + pub container: bool, + /// Whether to check for pure CSS modules. + pub pure: bool, +} + +impl<'i> Default for Config<'i> { + fn default() -> Self { + Config { + pattern: Default::default(), + dashed_idents: Default::default(), + animation: true, + grid: true, + container: true, + custom_idents: true, + pure: false, + } + } } /// A CSS modules class name pattern. @@ -86,6 +114,7 @@ impl<'i> Pattern<'i> { "[name]" => Segment::Name, "[local]" => Segment::Local, "[hash]" => Segment::Hash, + "[content-hash]" => Segment::ContentHash, s => return Err(PatternParseError::UnknownPlaceholder(s.into(), start_idx)), }; segments.push(segment); @@ -105,8 +134,20 @@ impl<'i> Pattern<'i> { Ok(Pattern { segments }) } + /// Whether the pattern contains any `[content-hash]` segments. + pub fn has_content_hash(&self) -> bool { + self.segments.iter().any(|s| matches!(s, Segment::ContentHash)) + } + /// Write the substituted pattern to a destination. - pub fn write(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E> + pub fn write( + &self, + hash: &str, + path: &Path, + local: &str, + content_hash: &str, + mut write: W, + ) -> Result<(), E> where W: FnMut(&str) -> Result<(), E>, { @@ -129,6 +170,9 @@ impl<'i> Pattern<'i> { Segment::Hash => { write(hash)?; } + Segment::ContentHash => { + write(content_hash)?; + } } } Ok(()) @@ -141,8 +185,9 @@ impl<'i> Pattern<'i> { hash: &str, path: &Path, local: &str, + content_hash: &str, ) -> Result { - self.write(hash, path, local, |s| res.write_str(s))?; + self.write(hash, path, local, content_hash, |s| res.write_str(s))?; Ok(res) } } @@ -160,6 +205,8 @@ pub enum Segment<'i> { Local, /// A hash of the file name. Hash, + /// A hash of the file contents. + ContentHash, } /// A referenced name within a CSS module, e.g. via the `composes` property. @@ -224,6 +271,7 @@ pub(crate) struct CssModule<'a, 'b, 'c> { pub config: &'a Config<'b>, pub sources: Vec<&'c Path>, pub hashes: Vec, + pub content_hashes: &'a Option>, pub exports_by_source_index: Vec, pub references: &'a mut HashMap, } @@ -234,6 +282,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { sources: &'c Vec, project_root: Option<&'c str>, references: &'a mut HashMap, + content_hashes: &'a Option>, ) -> Self { let project_root = project_root.map(|p| Path::new(p)); let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect(); @@ -258,6 +307,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(), sources, hashes, + content_hashes, references, } } @@ -274,6 +324,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], local, + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -293,6 +348,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], &local[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -315,6 +375,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], name, + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -344,6 +409,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[*source_index as usize], &self.sources[*source_index as usize], &name[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[*source_index as usize] + } else { + "" + }, ) .unwrap(), ) @@ -364,6 +434,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], &name[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -406,6 +481,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], name.0.as_ref(), + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), }, diff --git a/src/declaration.rs b/src/declaration.rs index 556fd152f..0dd3da619 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -1,16 +1,16 @@ //! 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; use crate::properties::custom::{CustomProperty, CustomPropertyName}; use crate::properties::masking::MaskHandler; +use crate::properties::text::{Direction, UnicodeBidi}; use crate::properties::{ align::AlignHandler, animation::AnimationHandler, @@ -34,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. /// @@ -156,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; } }; @@ -175,10 +230,7 @@ impl<'i> DeclarationBlock<'i> { write!(self.declarations, false); write!(self.important_declarations, true); - - dest.dedent(); - dest.newline()?; - dest.write_char('}') + Ok(()) } } @@ -515,7 +567,9 @@ pub(crate) struct DeclarationHandler<'i> { color_scheme: ColorSchemeHandler, fallback: FallbackHandler, prefix: PrefixHandler, - custom_properties: HashMap<'i>, usize>, + direction: Option, + unicode_bidi: Option, + custom_properties: IndexMap<'i>, usize>, decls: DeclarationList<'i>, } @@ -552,6 +606,7 @@ impl<'i> DeclarationHandler<'i> { || self.color_scheme.handle_property(property, &mut self.decls, context) || self.fallback.handle_property(property, &mut self.decls, context) || self.prefix.handle_property(property, &mut self.decls, context) + || self.handle_all(property) || self.handle_custom_property(property, context) } @@ -587,6 +642,36 @@ impl<'i> DeclarationHandler<'i> { false } + fn handle_all(&mut self, property: &Property<'i>) -> bool { + // The `all` property resets all properies except `unicode-bidi`, `direction`, and custom properties. + // https://drafts.csswg.org/css-cascade-5/#all-shorthand + match property { + Property::UnicodeBidi(bidi) => { + self.unicode_bidi = Some(*bidi); + true + } + Property::Direction(direction) => { + self.direction = Some(*direction); + true + } + Property::All(keyword) => { + let mut handler = DeclarationHandler { + unicode_bidi: self.unicode_bidi.clone(), + direction: self.direction.clone(), + ..Default::default() + }; + for (key, index) in self.custom_properties.drain(..) { + handler.custom_properties.insert(key, handler.decls.len()); + handler.decls.push(self.decls[index].clone()); + } + handler.decls.push(Property::All(keyword.clone())); + *self = handler; + true + } + _ => false, + } + } + fn add_conditional_fallbacks( &self, custom: &mut CustomProperty<'i>, @@ -607,6 +692,13 @@ impl<'i> DeclarationHandler<'i> { } pub fn finalize(&mut self, context: &mut PropertyHandlerContext<'i, '_>) { + if let Some(direction) = std::mem::take(&mut self.direction) { + self.decls.push(Property::Direction(direction)); + } + if let Some(unicode_bidi) = std::mem::take(&mut self.unicode_bidi) { + self.decls.push(Property::UnicodeBidi(unicode_bidi)); + } + self.background.finalize(&mut self.decls, context); self.border.finalize(&mut self.decls, context); self.outline.finalize(&mut self.decls, context); diff --git a/src/error.rs b/src/error.rs index cac0f59c1..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,8 +236,20 @@ pub enum SelectorError<'i> { UnexpectedTokenInAttributeSelector( #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>, ), - /// An unsupported pseudo class or pseudo element was encountered. - UnsupportedPseudoClassOrElement(CowArcStr<'i>), + + /// An unsupported pseudo class was encountered. + UnsupportedPseudoClass(CowArcStr<'i>), + + /// An unsupported pseudo element was encountered. + UnsupportedPseudoElement(CowArcStr<'i>), + + /// Ambiguous CSS module class. + AmbiguousCssModuleClass(CowArcStr<'i>), + + /// An unexpected token was encountered after a pseudo element. + UnexpectedSelectorAfterPseudoElement( + #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>, + ), } impl<'i> fmt::Display for SelectorError<'i> { @@ -256,7 +274,15 @@ impl<'i> fmt::Display for SelectorError<'i> { PseudoElementExpectedIdent(token) => write!(f, "Invalid token in pseudo element: {:?}", token), UnexpectedIdent(name) => write!(f, "Unexpected identifier: {}", name), UnexpectedTokenInAttributeSelector(token) => write!(f, "Unexpected token in attribute selector: {:?}", token), - UnsupportedPseudoClassOrElement(name) => write!(f, "Unsupported pseudo class or element: {}", name), + UnsupportedPseudoClass(name) =>write!(f, "'{name}' is not recognized as a valid pseudo-class. Did you mean '::{name}' (pseudo-element) or is this a typo?"), + UnsupportedPseudoElement(name) => write!(f, "'{name}' is not recognized as a valid pseudo-element. Did you mean ':{name}' (pseudo-class) or is this a typo?"), + AmbiguousCssModuleClass(_) => write!(f, "Ambiguous CSS module class not supported"), + UnexpectedSelectorAfterPseudoElement(token) => { + write!( + f, + "Pseudo-elements like '::before' or '::after' can't be followed by selectors like '{token:?}'" + ) + }, } } } @@ -285,9 +311,8 @@ impl<'i> From<'i>> for SelectorError<'i> { SelectorError::UnexpectedTokenInAttributeSelector(t.into()) } SelectorParseErrorKind::PseudoElementExpectedIdent(t) => SelectorError::PseudoElementExpectedIdent(t.into()), - SelectorParseErrorKind::UnsupportedPseudoClassOrElement(t) => { - SelectorError::UnsupportedPseudoClassOrElement(t.into()) - } + SelectorParseErrorKind::UnsupportedPseudoClass(t) => SelectorError::UnsupportedPseudoClass(t.into()), + SelectorParseErrorKind::UnsupportedPseudoElement(t) => SelectorError::UnsupportedPseudoElement(t.into()), SelectorParseErrorKind::UnexpectedIdent(t) => SelectorError::UnexpectedIdent(t.into()), SelectorParseErrorKind::ExpectedNamespace(t) => SelectorError::ExpectedNamespace(t.into()), SelectorParseErrorKind::ExpectedBarInAttr(t) => SelectorError::ExpectedBarInAttr(t.into()), @@ -297,6 +322,10 @@ impl<'i> From<'i>> for SelectorError<'i> { SelectorError::ExplicitNamespaceUnexpectedToken(t.into()) } SelectorParseErrorKind::ClassNeedsIdent(t) => SelectorError::ClassNeedsIdent(t.into()), + SelectorParseErrorKind::AmbiguousCssModuleClass(name) => SelectorError::AmbiguousCssModuleClass(name.into()), + SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(t) => { + SelectorError::UnexpectedSelectorAfterPseudoElement(t.into()) + } } } } @@ -337,6 +366,8 @@ pub enum MinifyErrorKind { /// The source location of the `@custom-media` rule with unsupported boolean logic. custom_media_loc: Location, }, + /// A CSS module selector did not contain at least one class or id selector. + ImpureCSSModuleSelector, } impl fmt::Display for MinifyErrorKind { @@ -349,6 +380,10 @@ impl fmt::Display for MinifyErrorKind { f, "Boolean logic with media types in @custom-media rules is not supported by Lightning CSS" ), + ImpureCSSModuleSelector => write!( + f, + "A selector in CSS modules should contain at least one class or ID selector" + ), } } } @@ -378,7 +413,7 @@ pub enum PrinterErrorKind { FmtError, /// The CSS modules `composes` property cannot be used within nested rules. InvalidComposesNesting, - /// The CSS modules `composes` property cannot be used with a simple class selector. + /// The CSS modules `composes` property can only be used with a simple class selector. InvalidComposesSelector, /// The CSS modules pattern must end with `[local]` for use in CSS grid. InvalidCssModulesPatternInGrid, diff --git a/src/lib.rs b/src/lib.rs index efaf28c9e..2a41655ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,9 @@ mod tests { use crate::vendor_prefix::VendorPrefix; use cssparser::SourceLocation; use indoc::indoc; + use pretty_assertions::assert_eq; use std::collections::HashMap; + use std::sync::{Arc, RwLock}; fn test(source: &str, expected: &str) { test_with_options(source, expected, ParserOptions::default()) @@ -77,10 +79,18 @@ mod tests { assert_eq!(res.code, expected); } + fn test_with_printer_options<'i, 'o>(source: &'i str, expected: &'i str, options: PrinterOptions<'o>) { + let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet.to_css(options).unwrap(); + assert_eq!(res.code, expected); + } + fn minify_test(source: &str, expected: &str) { minify_test_with_options(source, expected, ParserOptions::default()) } + #[track_caller] fn minify_test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) { let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); @@ -93,6 +103,18 @@ mod tests { assert_eq!(res.code, expected); } + fn minify_error_test_with_options<'i, 'o>( + source: &'i str, + error: MinifyErrorKind, + options: ParserOptions<'o, 'i>, + ) { + let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap(); + match stylesheet.minify(MinifyOptions::default()) { + Err(e) => assert_eq!(e.kind, error), + _ => unreachable!(), + } + } + fn prefix_test(source: &str, expected: &str, targets: Browsers) { let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); stylesheet @@ -168,6 +190,7 @@ mod tests { expected_exports: CssModuleExports, expected_references: CssModuleReferences, config: crate::css_modules::Config<'i>, + minify: bool, ) { let mut stylesheet = StyleSheet::parse( &source, @@ -179,7 +202,12 @@ mod tests { ) .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + minify, + ..Default::default() + }) + .unwrap(); assert_eq!(res.code, expected); assert_eq!(res.exports.unwrap(), expected_exports); assert_eq!(res.references.unwrap(), expected_references); @@ -216,6 +244,39 @@ mod tests { } } + fn error_recovery_test(source: &str) -> Vec<'_>>> { + let warnings = Arc::new(RwLock::default()); + { + let res = StyleSheet::parse( + &source, + ParserOptions { + error_recovery: true, + warnings: Some(warnings.clone()), + ..Default::default() + }, + ); + match res { + Ok(..) => {} + Err(e) => unreachable!("parser error should be recovered, but got {e:?}"), + } + } + Arc::into_inner(warnings).unwrap().into_inner().unwrap() + } + + fn css_modules_error_test(source: &str, error: ParserError) { + let res = StyleSheet::parse( + &source, + ParserOptions { + css_modules: Some(Default::default()), + ..Default::default() + }, + ); + match res { + Ok(_) => unreachable!(), + Err(e) => assert_eq!(e.kind, error), + } + } + macro_rules! map( { $($key:expr => $name:literal $(referenced: $referenced: literal)? $($value:literal $(global: $global: literal)? $(from $from:literal)?)*),* } => { { @@ -334,6 +395,122 @@ mod tests { ); } + #[test] + pub fn test_math_fn() { + // max() + minify_test( + r#" + .foo { + color: rgb(max(255, 100), 0, 0); + } + "#, + indoc! {".foo{color:red}" + }, + ); + // min() + minify_test( + r#" + .foo { + color: rgb(min(255, 500), 0, 0); + } + "#, + indoc! {".foo{color:red}" + }, + ); + // abs() + minify_test( + r#" + .foo { + color: rgb(abs(-255), 0, 0); + } + "#, + indoc! {".foo{color:red}" + }, + ); + // clamp() + minify_test( + r#" + .foo { + flex: clamp(1, 5.20, 20); + color: rgb(clamp(0, 255, 300), 0, 0); + } + "#, + indoc! {".foo{color:red;flex:5.2}" + }, + ); + // round() + minify_test( + r#" + .round-color { + color: rgb(round(down, 255.6, 1), 0, 0); + } + "#, + indoc! {".round-color{color:red}" + }, + ); + // hypot() + minify_test( + r#" + .hypot-color { + color: rgb(hypot(255, 0), 0, 0); + } + "#, + indoc! {".hypot-color{color:red}" + }, + ); + // sign(), sign(50) = 1 + minify_test( + r#" + .sign-color { + color: rgb(sign(50), 0, 0); + } + "#, + indoc! {".sign-color{color:#010000}" + }, + ); + // rem(), rem(21, 2) = 1 + minify_test( + r#" + .rem-color { + color: rgb(rem(21, 2), 0, 0); + } + "#, + indoc! {".rem-color{color:#010000}" + }, + ); + // max() in width + minify_test( + r#" + .foo { + width: max(200px, 5px); + } + "#, + indoc! {".foo{width:200px}" + }, + ); + // max() in opacity + minify_test( + r#" + .foo { + opacity: max(1, 0.2); + filter: invert(min(1, 0.5)); + } + "#, + indoc! {".foo{opacity:1;filter:invert(.5)}" + }, + ); + // TODO: support calc in Integer + // minify_test( + // r#" + // .foo { + // z-index: max(100, 20); + // } + // "#, + // indoc! {".foo{z-index:100}" + // }, + // ); + } + #[test] pub fn test_border() { test( @@ -1442,6 +1619,33 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + &format!( + r#" + @supports (color: lab(0% 0 0)) {{ + .foo {{ + {}: var(--border-width) solid lab(40% 56.6 39); + }} + }} + "#, + prop + ), + &format!( + indoc! {r#" + @supports (color: lab(0% 0 0)) {{ + .foo {{ + {}: var(--border-width) solid lab(40% 56.6 39); + }} + }} + "#}, + prop, + ), + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); } prefix_test( @@ -2117,7 +2321,7 @@ mod tests { indoc! {r#" .foo { -webkit-border-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 60; - -webkit-border-image: -webkit-linear-gradient(#ff0f0e, #7773ff) 60; + -webkit-border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60; border-image: linear-gradient(#ff0f0e, #7773ff) 60; border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60; } @@ -2138,8 +2342,8 @@ mod tests { indoc! {r#" .foo { -webkit-border-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 60; - -webkit-border-image: -webkit-linear-gradient(#ff0f0e, #7773ff) 60; - -moz-border-image: -moz-linear-gradient(#ff0f0e, #7773ff) 60; + -webkit-border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60; + -moz-border-image: -moz-linear-gradient(top, #ff0f0e, #7773ff) 60; border-image: linear-gradient(#ff0f0e, #7773ff) 60; border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60; } @@ -2160,8 +2364,8 @@ mod tests { "#, indoc! {r#" .foo { - border-image: -webkit-linear-gradient(#ff0f0e, #7773ff) 60; - border-image: -moz-linear-gradient(#ff0f0e, #7773ff) 60; + border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60; + border-image: -moz-linear-gradient(top, #ff0f0e, #7773ff) 60; border-image: linear-gradient(#ff0f0e, #7773ff) 60; border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60; } @@ -2182,7 +2386,7 @@ mod tests { "#, indoc! {r#" .foo { - border-image-source: -webkit-linear-gradient(#ff0f0e, #7773ff); + border-image-source: -webkit-linear-gradient(top, #ff0f0e, #7773ff); border-image-source: linear-gradient(#ff0f0e, #7773ff); border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); } @@ -3952,6 +4156,15 @@ mod tests { minify_test(".foo { aspect-ratio: 2 / 3 }", ".foo{aspect-ratio:2/3}"); minify_test(".foo { aspect-ratio: auto 2 / 3 }", ".foo{aspect-ratio:auto 2/3}"); minify_test(".foo { aspect-ratio: 2 / 3 auto }", ".foo{aspect-ratio:auto 2/3}"); + + minify_test( + ".foo { width: 200px; width: var(--foo); }", + ".foo{width:200px;width:var(--foo)}", + ); + minify_test( + ".foo { width: var(--foo); width: 200px; }", + ".foo{width:var(--foo);width:200px}", + ); } #[test] @@ -4101,12 +4314,24 @@ mod tests { ); minify_test( ".foo { background-position: left 10px center }", - ".foo{background-position:10px 50%}", + ".foo{background-position:10px}", ); minify_test( ".foo { background-position: right 10px center }", ".foo{background-position:right 10px center}", ); + minify_test( + ".foo { background-position: center top 10px }", + ".foo{background-position:50% 10px}", + ); + minify_test( + ".foo { background-position: center bottom 10px }", + ".foo{background-position:center bottom 10px}", + ); + minify_test( + ".foo { background-position: center 10px }", + ".foo{background-position:50% 10px}", + ); minify_test( ".foo { background-position: right 10px top 20px }", ".foo{background-position:right 10px top 20px}", @@ -4127,6 +4352,26 @@ mod tests { ".foo { background-position: bottom right }", ".foo{background-position:100% 100%}", ); + minify_test( + ".foo { background-position: center top }", + ".foo{background-position:top}", + ); + minify_test( + ".foo { background-position: center bottom }", + ".foo{background-position:bottom}", + ); + minify_test( + ".foo { background-position: left center }", + ".foo{background-position:0}", + ); + minify_test( + ".foo { background-position: right center }", + ".foo{background-position:100%}", + ); + minify_test( + ".foo { background-position: 20px center }", + ".foo{background-position:20px}", + ); minify_test( ".foo { background: url('img-sprite.png') no-repeat bottom right }", @@ -6819,6 +7064,19 @@ mod tests { ":root::view-transition {position: fixed}", ":root::view-transition{position:fixed}", ); + minify_test( + ":root:active-view-transition {position: fixed}", + ":root:active-view-transition{position:fixed}", + ); + minify_test( + ":root:active-view-transition-type(slide-in) {position: fixed}", + ":root:active-view-transition-type(slide-in){position:fixed}", + ); + minify_test( + ":root:active-view-transition-type(slide-in, reverse) {position: fixed}", + ":root:active-view-transition-type(slide-in,reverse){position:fixed}", + ); + for name in &[ "view-transition-group", "view-transition-image-pair", @@ -6829,14 +7087,46 @@ mod tests { &format!(":root::{}(*) {{position: fixed}}", name), &format!(":root::{}(*){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(*.class) {{position: fixed}}", name), + &format!(":root::{}(*.class){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(*.class.class) {{position: fixed}}", name), + &format!(":root::{}(*.class.class){{position:fixed}}", name), + ); minify_test( &format!(":root::{}(foo) {{position: fixed}}", name), &format!(":root::{}(foo){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(foo.class) {{position: fixed}}", name), + &format!(":root::{}(foo.class){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name), + &format!(":root::{}(foo.bar.baz){{position:fixed}}", name), + ); minify_test( &format!(":root::{}(foo):only-child {{position: fixed}}", name), &format!(":root::{}(foo):only-child{{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(foo.bar.baz):only-child {{position: fixed}}", name), + &format!(":root::{}(foo.bar.baz):only-child{{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(.foo) {{position: fixed}}", name), + &format!(":root::{}(.foo){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(.foo.bar) {{position: fixed}}", name), + &format!(":root::{}(.foo.bar){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}( .foo.bar ) {{position: fixed}}", name), + &format!(":root::{}(.foo.bar){{position:fixed}}", name), + ); error_test( &format!(":root::{}(foo):first-child {{position: fixed}}", name), ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement), @@ -6845,8 +7135,81 @@ mod tests { &format!(":root::{}(foo)::before {{position: fixed}}", name), ParserError::SelectorError(SelectorError::InvalidState), ); + error_test( + &format!(":root::{}(*.*) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*. cls) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(foo .bar) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls. c) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls>cls) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls.foo.*) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); } + minify_test( + "wa-checkbox:state(disabled) {color:red}", + "wa-checkbox:state(disabled){color:red}", + ); + minify_test( + "button:state(checked) {background:blue}", + "button:state(checked){background:#00f}", + ); + minify_test( + "input:state(custom-state) {border:1px solid}", + "input:state(custom-state){border:1px solid}", + ); + minify_test( + "button:active:not(:state(disabled))::part(control) {border:1px solid}", + "button:active:not(:state(disabled))::part(control){border:1px solid}", + ); + // Test nested CSS with :state() selector + nesting_test( + r#" + custom-element { + color: blue; + &:state(loading) { + opacity: 0.5; + & .spinner { + display: block; + } + } + &:state(error) { + border: 2px solid red; + } + } + "#, + indoc! {r#" + custom-element { + color: #00f; + } + + custom-element:state(loading) { + opacity: .5; + } + + custom-element:state(loading) .spinner { + display: block; + } + custom-element:state(error) { + border: 2px solid red; + } + "#}, + ); minify_test(".foo ::deep .bar {width: 20px}", ".foo ::deep .bar{width:20px}"); minify_test(".foo::deep .bar {width: 20px}", ".foo::deep .bar{width:20px}"); minify_test(".foo ::deep.bar {width: 20px}", ".foo ::deep.bar{width:20px}"); @@ -6899,33 +7262,201 @@ mod tests { ".foo /deep/ .bar{width:20px}", deep_options.clone(), ); - } - #[test] - fn test_keyframes() { - minify_test( - r#" - @keyframes "test" { - 100% { - background: blue - } - } - "#, - "@keyframes test{to{background:#00f}}", + let pure_css_module_options = ParserOptions { + css_modules: Some(crate::css_modules::Config { + pure: true, + ..Default::default() + }), + ..ParserOptions::default() + }; + + minify_error_test_with_options( + "div {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), ); - minify_test( - r#" - @keyframes test { - 100% { - background: blue - } - } - "#, - "@keyframes test{to{background:#00f}}", + minify_error_test_with_options( + ":global(.foo) {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), ); - - // CSS-wide keywords and `none` cannot remove quotes. - minify_test( + minify_error_test_with_options( + "[foo=bar] {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "div, .foo {width: 20px}", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_test_with_options( + ":local(.foo) {width: 20px}", + "._8Z4fiW_foo{width:20px}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "div.my-class {color: red;}", + "div._8Z4fiW_my-class{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "#id {color: red;}", + "#_8Z4fiW_id{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "a .my-class{color: red;}", + "a ._8Z4fiW_my-class{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".my-class a {color: red;}", + "._8Z4fiW_my-class a{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".my-class:is(a) {color: red;}", + "._8Z4fiW_my-class:is(a){color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "div:has(.my-class) {color: red;}", + "div:has(._8Z4fiW_my-class){color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".foo { html &:hover { a_value: some-value; } }", + "._8Z4fiW_foo{html &:hover{a_value:some-value}}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".foo { span { color: red; } }", + "._8Z4fiW_foo{& span{color:red}}", + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "html { .foo { span { color: red; } } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".foo { div { span { color: red; } } }", + "._8Z4fiW_foo{& div{& span{color:red}}}", + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "@scope (div) { .foo { color: red } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "@scope (.a) to (div) { .foo { color: red } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_error_test_with_options( + "@scope (.a) to (.b) { div { color: red } }", + MinifyErrorKind::ImpureCSSModuleSelector, + pure_css_module_options.clone(), + ); + minify_test_with_options( + "@scope (.a) to (.b) { .foo { color: red } }", + "@scope(._8Z4fiW_a) to (._8Z4fiW_b){._8Z4fiW_foo{color:red}}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "/* cssmodules-pure-no-check */ :global(.foo) { color: red }", + ".foo{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "/*! some license */ /* cssmodules-pure-no-check */ :global(.foo) { color: red }", + "/*! some license */\n.foo{color:red}", + pure_css_module_options.clone(), + ); + + error_test( + "input.defaultCheckbox::before h1 {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Ident( + "h1".into(), + ))), + ); + error_test( + "input.defaultCheckbox::before .my-class {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.'))), + ); + error_test( + "input.defaultCheckbox::before.my-class {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.'))), + ); + error_test( + "input.defaultCheckbox::before #id {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash( + "id".into(), + ))), + ); + error_test( + "input.defaultCheckbox::before#id {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash( + "id".into(), + ))), + ); + error_test( + "input.defaultCheckbox::before [attr] {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement( + Token::SquareBracketBlock, + )), + ); + error_test( + "input.defaultCheckbox::before[attr] {width: 20px}", + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement( + Token::SquareBracketBlock, + )), + ); + } + + #[test] + fn test_keyframes() { + minify_test( + r#" + @keyframes "test" { + 100% { + background: blue + } + } + "#, + "@keyframes test{to{background:#00f}}", + ); + minify_test( + r#" + @keyframes test { + 100% { + background: blue + } + } + "#, + "@keyframes test{to{background:#00f}}", + ); + + // named animation range percentages + minify_test( + r#" + @keyframes test { + entry 0% { + background: blue + } + exit 100% { + background: green + } + } + "#, + "@keyframes test{entry 0%{background:#00f}exit 100%{background:green}}", + ); + + // CSS-wide keywords and `none` cannot remove quotes. + minify_test( r#" @keyframes "revert" { from { @@ -6947,6 +7478,18 @@ mod tests { "@keyframes \"none\"{0%{background:green}}", ); + // named animation ranges cannot be used with to or from + minify_test( + r#" + @keyframes test { + entry to { + background: blue + } + } + "#, + "@keyframes test{}", + ); + // CSS-wide keywords without quotes throws an error. error_test( r#" @@ -7511,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) }", @@ -7522,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)) }", @@ -7654,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] @@ -7694,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}"); @@ -7713,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] @@ -7745,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] @@ -8166,7 +8759,7 @@ mod tests { } "#, indoc! { r#" - @media (min-color: 3) { + @media not (max-color: 2) { .foo { color: #7fff00; } @@ -8187,7 +8780,7 @@ mod tests { } "#, indoc! { r#" - @media (max-color: 1) { + @media not (min-color: 2) { .foo { color: #7fff00; } @@ -8208,7 +8801,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: 240.001px) { + @media not (max-width: 240px) { .foo { color: #7fff00; } @@ -8271,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; } @@ -8376,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; } @@ -8427,7 +9100,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: calc(1.001px + 1rem)) { + @media not (max-width: calc(1px + 1rem)) { .foo { color: #ff0; } @@ -8445,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; } @@ -8463,7 +9136,7 @@ mod tests { } "#, indoc! { r#" - @media (min-width: .001px) { + @media not (max-width: 0) { .foo { color: #ff0; } @@ -8517,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; } @@ -8623,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, @@ -8667,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] @@ -8868,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] @@ -11066,22 +11797,93 @@ 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; + } - #[test] - fn test_animation() { - minify_test(".foo { animation-name: test }", ".foo{animation-name:test}"); - minify_test(".foo { animation-name: \"test\" }", ".foo{animation-name:test}"); - minify_test(".foo { animation-name: foo, bar }", ".foo{animation-name:foo,bar}"); - minify_test(".foo { animation-name: \"none\" }", ".foo{animation-name:\"none\"}"); - minify_test( - ".foo { animation-name: \"none\", foo }", - ".foo{animation-name:\"none\",foo}", + .baz { + transition-property: -webkit-backdrop-filter; + } + "# + }, + Browsers { + safari: Some(15 << 16), + ..Browsers::default() + }, ); - let name = crate::properties::animation::AnimationName::parse_string("default"); - assert!(matches!(name, Err(..))); - - minify_test(".foo { animation-name: none }", ".foo{animation-name:none}"); + 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] + fn test_animation() { + minify_test(".foo { animation-name: test }", ".foo{animation-name:test}"); + minify_test(".foo { animation-name: \"test\" }", ".foo{animation-name:test}"); + minify_test(".foo { animation-name: foo, bar }", ".foo{animation-name:foo,bar}"); + minify_test(".foo { animation-name: \"none\" }", ".foo{animation-name:\"none\"}"); + minify_test( + ".foo { animation-name: \"none\", foo }", + ".foo{animation-name:\"none\",foo}", + ); + let name = crate::properties::animation::AnimationName::parse_string("default"); + assert!(matches!(name, Err(..))); + + minify_test(".foo { animation-name: none }", ".foo{animation-name:none}"); minify_test(".foo { animation-name: none, none }", ".foo{animation-name:none,none}"); // Test CSS-wide keywords @@ -11242,6 +12044,46 @@ mod tests { ".foo { animation: foo 0s 3s infinite }", ".foo{animation:0s 3s infinite foo}", ); + minify_test(".foo { animation: foo 3s --test }", ".foo{animation:3s foo --test}"); + minify_test(".foo { animation: foo 3s scroll() }", ".foo{animation:3s foo scroll()}"); + minify_test( + ".foo { animation: foo 3s scroll(block) }", + ".foo{animation:3s foo scroll()}", + ); + minify_test( + ".foo { animation: foo 3s scroll(root inline) }", + ".foo{animation:3s foo scroll(root inline)}", + ); + minify_test( + ".foo { animation: foo 3s scroll(inline root) }", + ".foo{animation:3s foo scroll(root inline)}", + ); + minify_test( + ".foo { animation: foo 3s scroll(inline nearest) }", + ".foo{animation:3s foo scroll(inline)}", + ); + minify_test( + ".foo { animation: foo 3s view(block) }", + ".foo{animation:3s foo view()}", + ); + minify_test( + ".foo { animation: foo 3s view(inline) }", + ".foo{animation:3s foo view(inline)}", + ); + minify_test( + ".foo { animation: foo 3s view(inline 10px 10px) }", + ".foo{animation:3s foo view(inline 10px)}", + ); + minify_test( + ".foo { animation: foo 3s view(inline 10px 12px) }", + ".foo{animation:3s foo view(inline 10px 12px)}", + ); + minify_test( + ".foo { animation: foo 3s view(inline auto auto) }", + ".foo{animation:3s foo view(inline)}", + ); + minify_test(".foo { animation: foo 3s auto }", ".foo{animation:3s foo}"); + minify_test(".foo { animation-composition: add }", ".foo{animation-composition:add}"); test( r#" .foo { @@ -11253,6 +12095,7 @@ mod tests { animation-play-state: running; animation-delay: 100ms; animation-fill-mode: forwards; + animation-timeline: auto; } "#, indoc! {r#" @@ -11272,6 +12115,7 @@ mod tests { animation-play-state: running, paused; animation-delay: 100ms, 0s; animation-fill-mode: forwards, none; + animation-timeline: auto, auto; } "#, indoc! {r#" @@ -11318,6 +12162,7 @@ mod tests { animation-play-state: running; animation-delay: 100ms; animation-fill-mode: forwards; + animation-timeline: auto; } "#, indoc! {r#" @@ -11330,6 +12175,55 @@ mod tests { animation-play-state: running; animation-delay: .1s; animation-fill-mode: forwards; + animation-timeline: auto; + } + "#}, + ); + test( + r#" + .foo { + animation-name: foo; + animation-duration: 0.09s; + animation-timing-function: ease-in-out; + animation-iteration-count: 2; + animation-direction: alternate; + animation-play-state: running; + animation-delay: 100ms; + animation-fill-mode: forwards; + animation-timeline: scroll(); + } + "#, + indoc! {r#" + .foo { + animation: 90ms ease-in-out .1s 2 alternate forwards foo scroll(); + } + "#}, + ); + test( + r#" + .foo { + animation-name: foo; + animation-duration: 0.09s; + animation-timing-function: ease-in-out; + animation-iteration-count: 2; + animation-direction: alternate; + animation-play-state: running; + animation-delay: 100ms; + animation-fill-mode: forwards; + animation-timeline: scroll(), view(); + } + "#, + indoc! {r#" + .foo { + animation-name: foo; + animation-duration: 90ms; + animation-timing-function: ease-in-out; + animation-iteration-count: 2; + animation-direction: alternate; + animation-play-state: running; + animation-delay: .1s; + animation-fill-mode: forwards; + animation-timeline: scroll(), view(); } "#}, ); @@ -11440,162 +12334,490 @@ mod tests { ..Browsers::default() }, ); - } - #[test] - fn test_transform() { - minify_test( - ".foo { transform: translate(2px, 3px)", - ".foo{transform:translate(2px,3px)}", - ); - minify_test( - ".foo { transform: translate(2px, 0px)", - ".foo{transform:translate(2px)}", + prefix_test( + r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#, + indoc! {r#" + .foo { + animation: .2s ease-in-out bar; + animation-timeline: scroll(); + } + "#}, + Browsers { + safari: Some(16 << 16), + ..Browsers::default() + }, ); - minify_test( - ".foo { transform: translate(0px, 2px)", - ".foo{transform:translateY(2px)}", + prefix_test( + r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#, + indoc! {r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#}, + Browsers { + chrome: Some(120 << 16), + ..Browsers::default() + }, ); - minify_test(".foo { transform: translateX(2px)", ".foo{transform:translate(2px)}"); - minify_test(".foo { transform: translateY(2px)", ".foo{transform:translateY(2px)}"); - minify_test(".foo { transform: translateZ(2px)", ".foo{transform:translateZ(2px)}"); - minify_test( - ".foo { transform: translate3d(2px, 3px, 4px)", - ".foo{transform:translate3d(2px,3px,4px)}", + prefix_test( + r#" + .foo { + animation: .2s ease-in-out bar scroll(); + } + "#, + indoc! {r#" + .foo { + -webkit-animation: .2s ease-in-out bar; + animation: .2s ease-in-out bar; + animation-timeline: scroll(); + } + "#}, + Browsers { + safari: Some(6 << 16), + ..Browsers::default() + }, ); + minify_test( - ".foo { transform: translate3d(10%, 20%, 4px)", - ".foo{transform:translate3d(10%,20%,4px)}", + ".foo { animation-range-start: entry 10% }", + ".foo{animation-range-start:entry 10%}", ); minify_test( - ".foo { transform: translate3d(2px, 0px, 0px)", - ".foo{transform:translate(2px)}", + ".foo { animation-range-start: entry 0% }", + ".foo{animation-range-start:entry}", ); minify_test( - ".foo { transform: translate3d(0px, 2px, 0px)", - ".foo{transform:translateY(2px)}", + ".foo { animation-range-start: entry }", + ".foo{animation-range-start:entry}", ); + minify_test(".foo { animation-range-start: 50% }", ".foo{animation-range-start:50%}"); minify_test( - ".foo { transform: translate3d(0px, 0px, 2px)", - ".foo{transform:translateZ(2px)}", + ".foo { animation-range-end: exit 10% }", + ".foo{animation-range-end:exit 10%}", ); minify_test( - ".foo { transform: translate3d(2px, 3px, 0px)", - ".foo{transform:translate(2px,3px)}", + ".foo { animation-range-end: exit 100% }", + ".foo{animation-range-end:exit}", ); - minify_test(".foo { transform: scale(2, 3)", ".foo{transform:scale(2,3)}"); - minify_test(".foo { transform: scale(10%, 20%)", ".foo{transform:scale(.1,.2)}"); - minify_test(".foo { transform: scale(2, 2)", ".foo{transform:scale(2)}"); - minify_test(".foo { transform: scale(2, 1)", ".foo{transform:scaleX(2)}"); - minify_test(".foo { transform: scale(1, 2)", ".foo{transform:scaleY(2)}"); - minify_test(".foo { transform: scaleX(2)", ".foo{transform:scaleX(2)}"); - minify_test(".foo { transform: scaleY(2)", ".foo{transform:scaleY(2)}"); - minify_test(".foo { transform: scaleZ(2)", ".foo{transform:scaleZ(2)}"); - minify_test(".foo { transform: scale3d(2, 3, 4)", ".foo{transform:scale3d(2,3,4)}"); - minify_test(".foo { transform: scale3d(2, 1, 1)", ".foo{transform:scaleX(2)}"); - minify_test(".foo { transform: scale3d(1, 2, 1)", ".foo{transform:scaleY(2)}"); - minify_test(".foo { transform: scale3d(1, 1, 2)", ".foo{transform:scaleZ(2)}"); - minify_test(".foo { transform: scale3d(2, 2, 1)", ".foo{transform:scale(2)}"); - minify_test(".foo { transform: rotate(20deg)", ".foo{transform:rotate(20deg)}"); - minify_test(".foo { transform: rotateX(20deg)", ".foo{transform:rotateX(20deg)}"); - minify_test(".foo { transform: rotateY(20deg)", ".foo{transform:rotateY(20deg)}"); - minify_test(".foo { transform: rotateZ(20deg)", ".foo{transform:rotate(20deg)}"); - minify_test(".foo { transform: rotate(360deg)", ".foo{transform:rotate(360deg)}"); + minify_test(".foo { animation-range-end: exit }", ".foo{animation-range-end:exit}"); + minify_test(".foo { animation-range-end: 50% }", ".foo{animation-range-end:50%}"); minify_test( - ".foo { transform: rotate3d(2, 3, 4, 20deg)", - ".foo{transform:rotate3d(2,3,4,20deg)}", + ".foo { animation-range: entry 10% exit 90% }", + ".foo{animation-range:entry 10% exit 90%}", ); minify_test( - ".foo { transform: rotate3d(1, 0, 0, 20deg)", - ".foo{transform:rotateX(20deg)}", + ".foo { animation-range: entry 0% exit 100% }", + ".foo{animation-range:entry exit}", ); + minify_test(".foo { animation-range: entry }", ".foo{animation-range:entry}"); minify_test( - ".foo { transform: rotate3d(0, 1, 0, 20deg)", - ".foo{transform:rotateY(20deg)}", + ".foo { animation-range: entry 0% entry 100% }", + ".foo{animation-range:entry}", ); + minify_test(".foo { animation-range: 50% normal }", ".foo{animation-range:50%}"); minify_test( - ".foo { transform: rotate3d(0, 0, 1, 20deg)", - ".foo{transform:rotate(20deg)}", + ".foo { animation-range: normal normal }", + ".foo{animation-range:normal}", ); - minify_test(".foo { transform: rotate(405deg)}", ".foo{transform:rotate(405deg)}"); - minify_test(".foo { transform: rotateX(405deg)}", ".foo{transform:rotateX(405deg)}"); - minify_test(".foo { transform: rotateY(405deg)}", ".foo{transform:rotateY(405deg)}"); - minify_test(".foo { transform: rotate(-200deg)}", ".foo{transform:rotate(-200deg)}"); - minify_test(".foo { transform: rotate(0)", ".foo{transform:rotate(0)}"); - minify_test(".foo { transform: rotate(0deg)", ".foo{transform:rotate(0)}"); - minify_test( - ".foo { transform: rotateX(-200deg)}", - ".foo{transform:rotateX(-200deg)}", + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit 90%; + } + "#}, ); - minify_test( - ".foo { transform: rotateY(-200deg)}", - ".foo{transform:rotateY(-200deg)}", + test( + r#" + .foo { + animation-range-start: entry 0%; + animation-range-end: entry 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + } + "#}, ); - minify_test( - ".foo { transform: rotate3d(1, 1, 0, -200deg)", - ".foo{transform:rotate3d(1,1,0,-200deg)}", + test( + r#" + .foo { + animation-range-start: entry 0%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry exit; + } + "#}, ); - minify_test(".foo { transform: skew(20deg)", ".foo{transform:skew(20deg)}"); - minify_test(".foo { transform: skew(20deg, 0deg)", ".foo{transform:skew(20deg)}"); - minify_test(".foo { transform: skew(0deg, 20deg)", ".foo{transform:skewY(20deg)}"); - minify_test(".foo { transform: skewX(20deg)", ".foo{transform:skew(20deg)}"); - minify_test(".foo { transform: skewY(20deg)", ".foo{transform:skewY(20deg)}"); - minify_test( - ".foo { transform: perspective(10px)", - ".foo{transform:perspective(10px)}", + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: normal; + } + "#, + indoc! {r#" + .foo { + animation-range: 10%; + } + "#}, ); - minify_test( - ".foo { transform: matrix(1, 2, -1, 1, 80, 80)", - ".foo{transform:matrix(1,2,-1,1,80,80)}", + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%, exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit 90%, entry 50% exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + animation: spin 100ms; + } + "#, + indoc! {r#" + .foo { + animation: .1s spin; + } + "#}, + ); + test( + r#" + .foo { + animation: spin 100ms; + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation: .1s spin; + animation-range: entry 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + animation: var(--animation) 100ms; + } + "#, + indoc! {r#" + .foo { + animation: var(--animation) .1s; + } + "#}, + ); + } + + #[test] + fn test_transform() { + test( + ".foo { transform: perspective(500px)translate3d(10px, 0, 20px)rotateY(30deg) }", + indoc! {r#" + .foo { + transform: perspective(500px) translate3d(10px, 0, 20px) rotateY(30deg); + } + "#}, + ); + test( + ".foo { transform: translate3d(12px,50%,3em)scale(2,.5) }", + indoc! {r#" + .foo { + transform: translate3d(12px, 50%, 3em) scale(2, .5); + } + "#}, + ); + test( + ".foo { transform:matrix(1,2,-1,1,80,80) }", + indoc! {r#" + .foo { + transform: matrix(1, 2, -1, 1, 80, 80); + } + "#}, ); + minify_test( - ".foo { transform: matrix3d(1, 0, 0, 0, 0, 1, 6, 0, 0, 0, 1, 0, 50, 100, 0, 1.1)", - ".foo{transform:matrix3d(1,0,0,0,0,1,6,0,0,0,1,0,50,100,0,1.1)}", + ".foo { transform: scale( 0.5 )translateX(10px ) }", + ".foo{transform:scale(.5)translate(10px)}", ); minify_test( - ".foo{transform:translate(100px,200px) rotate(45deg) skew(10deg) scale(2)}", - ".foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)}", + ".foo { transform: translate(2px, 3px)", + ".foo{transform:translate(2px,3px)}", ); minify_test( - ".foo{transform:translate(200px,300px) translate(100px,200px) scale(2)}", - ".foo{transform:matrix(2,0,0,2,300,500)}", + ".foo { transform: translate(2px, 0px)", + ".foo{transform:translate(2px)}", ); minify_test( - ".foo{transform:translate(100px,200px) rotate(45deg)}", - ".foo{transform:translate(100px,200px)rotate(45deg)}", + ".foo { transform: translate(0px, 2px)", + ".foo{transform:translateY(2px)}", + ); + minify_test(".foo { transform: translateX(2px)", ".foo{transform:translate(2px)}"); + minify_test(".foo { transform: translateY(2px)", ".foo{transform:translateY(2px)}"); + minify_test(".foo { transform: translateZ(2px)", ".foo{transform:translateZ(2px)}"); + minify_test( + ".foo { transform: translate3d(2px, 3px, 4px)", + ".foo{transform:translate3d(2px,3px,4px)}", ); minify_test( - ".foo{transform:rotate3d(1, 1, 1, 45deg) translate3d(100px, 100px, 10px)}", - ".foo{transform:rotate3d(1,1,1,45deg)translate3d(100px,100px,10px)}", + ".foo { transform: translate3d(10%, 20%, 4px)", + ".foo{transform:translate3d(10%,20%,4px)}", + ); + minify_test( + ".foo { transform: translate3d(2px, 0px, 0px)", + ".foo{transform:translate(2px)}", ); minify_test( - ".foo{transform:translate3d(100px, 100px, 10px) skew(10deg) scale3d(2, 3, 4)}", - ".foo{transform:matrix3d(2,0,0,0,.528981,3,0,0,0,0,4,0,100,100,10,1)}", + ".foo { transform: translate3d(0px, 2px, 0px)", + ".foo{transform:translateY(2px)}", ); minify_test( - ".foo{transform:matrix3d(0.804737854124365, 0.5058793634016805, -0.31061721752604554, 0, -0.31061721752604554, 0.804737854124365, 0.5058793634016805, 0, 0.5058793634016805, -0.31061721752604554, 0.804737854124365, 0, 100, 100, 10, 1)}", - ".foo{transform:translate3d(100px,100px,10px)rotate3d(1,1,1,45deg)}" + ".foo { transform: translate3d(0px, 0px, 2px)", + ".foo{transform:translateZ(2px)}", ); minify_test( - ".foo{transform:matrix3d(1, 0, 0, 0, 0, 0.7071067811865476, 0.7071067811865475, 0, 0, -0.7071067811865475, 0.7071067811865476, 0, 100, 100, 10, 1)}", - ".foo{transform:translate3d(100px,100px,10px)rotateX(45deg)}" + ".foo { transform: translate3d(2px, 3px, 0px)", + ".foo{transform:translate(2px,3px)}", ); + minify_test(".foo { transform: scale(2, 3)", ".foo{transform:scale(2,3)}"); + minify_test(".foo { transform: scale(10%, 20%)", ".foo{transform:scale(.1,.2)}"); + minify_test(".foo { transform: scale(2, 2)", ".foo{transform:scale(2)}"); + minify_test(".foo { transform: scale(2, 1)", ".foo{transform:scaleX(2)}"); + minify_test(".foo { transform: scale(1, 2)", ".foo{transform:scaleY(2)}"); + minify_test(".foo { transform: scaleX(2)", ".foo{transform:scaleX(2)}"); + minify_test(".foo { transform: scaleY(2)", ".foo{transform:scaleY(2)}"); + minify_test(".foo { transform: scaleZ(2)", ".foo{transform:scaleZ(2)}"); + minify_test(".foo { transform: scale3d(2, 3, 4)", ".foo{transform:scale3d(2,3,4)}"); + minify_test(".foo { transform: scale3d(2, 1, 1)", ".foo{transform:scaleX(2)}"); + minify_test(".foo { transform: scale3d(1, 2, 1)", ".foo{transform:scaleY(2)}"); + minify_test(".foo { transform: scale3d(1, 1, 2)", ".foo{transform:scaleZ(2)}"); + minify_test(".foo { transform: scale3d(2, 2, 1)", ".foo{transform:scale(2)}"); + minify_test(".foo { transform: rotate(20deg)", ".foo{transform:rotate(20deg)}"); + minify_test(".foo { transform: rotateX(20deg)", ".foo{transform:rotateX(20deg)}"); + minify_test(".foo { transform: rotateY(20deg)", ".foo{transform:rotateY(20deg)}"); + minify_test(".foo { transform: rotateZ(20deg)", ".foo{transform:rotate(20deg)}"); + minify_test(".foo { transform: rotate(360deg)", ".foo{transform:rotate(360deg)}"); minify_test( - ".foo{transform:translate3d(100px, 200px, 10px) translate(100px, 100px)}", - ".foo{transform:translate3d(200px,300px,10px)}", + ".foo { transform: rotate3d(2, 3, 4, 20deg)", + ".foo{transform:rotate3d(2,3,4,20deg)}", ); minify_test( - ".foo{transform:rotate(45deg) rotate(45deg)}", - ".foo{transform:rotate(90deg)}", + ".foo { transform: rotate3d(1, 0, 0, 20deg)", + ".foo{transform:rotateX(20deg)}", ); minify_test( - ".foo{transform:matrix(0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 100, 100)}", - ".foo{transform:translate(100px,100px)rotate(45deg)}" + ".foo { transform: rotate3d(0, 1, 0, 20deg)", + ".foo{transform:rotateY(20deg)}", ); minify_test( - ".foo{transform:translateX(2in) translateX(50px)}", - ".foo{transform:translate(242px)}", + ".foo { transform: rotate3d(0, 0, 1, 20deg)", + ".foo{transform:rotate(20deg)}", + ); + minify_test(".foo { transform: rotate(405deg)}", ".foo{transform:rotate(405deg)}"); + minify_test(".foo { transform: rotateX(405deg)}", ".foo{transform:rotateX(405deg)}"); + minify_test(".foo { transform: rotateY(405deg)}", ".foo{transform:rotateY(405deg)}"); + minify_test(".foo { transform: rotate(-200deg)}", ".foo{transform:rotate(-200deg)}"); + minify_test(".foo { transform: rotate(0)", ".foo{transform:rotate(0)}"); + minify_test(".foo { transform: rotate(0deg)", ".foo{transform:rotate(0)}"); + minify_test( + ".foo { transform: rotateX(-200deg)}", + ".foo{transform:rotateX(-200deg)}", + ); + minify_test( + ".foo { transform: rotateY(-200deg)}", + ".foo{transform:rotateY(-200deg)}", + ); + minify_test( + ".foo { transform: rotate3d(1, 1, 0, -200deg)", + ".foo{transform:rotate3d(1,1,0,-200deg)}", + ); + minify_test(".foo { transform: skew(20deg)", ".foo{transform:skew(20deg)}"); + minify_test(".foo { transform: skew(20deg, 0deg)", ".foo{transform:skew(20deg)}"); + minify_test(".foo { transform: skew(0deg, 20deg)", ".foo{transform:skewY(20deg)}"); + minify_test(".foo { transform: skewX(20deg)", ".foo{transform:skew(20deg)}"); + minify_test(".foo { transform: skewY(20deg)", ".foo{transform:skewY(20deg)}"); + minify_test( + ".foo { transform: perspective(10px)", + ".foo{transform:perspective(10px)}", + ); + minify_test( + ".foo { transform: matrix(1, 2, -1, 1, 80, 80)", + ".foo{transform:matrix(1,2,-1,1,80,80)}", + ); + minify_test( + ".foo { transform: matrix3d(1, 0, 0, 0, 0, 1, 6, 0, 0, 0, 1, 0, 50, 100, 0, 1.1)", + ".foo{transform:matrix3d(1,0,0,0,0,1,6,0,0,0,1,0,50,100,0,1.1)}", + ); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test( + // ".foo{transform:translate(100px,200px) rotate(45deg) skew(10deg) scale(2)}", + // ".foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)}", + // ); + // minify_test( + // ".foo{transform:translate(200px,300px) translate(100px,200px) scale(2)}", + // ".foo{transform:matrix(2,0,0,2,300,500)}", + // ); + minify_test( + ".foo{transform:translate(100px,200px) rotate(45deg)}", + ".foo{transform:translate(100px,200px)rotate(45deg)}", + ); + minify_test( + ".foo{transform:rotate3d(1, 1, 1, 45deg) translate3d(100px, 100px, 10px)}", + ".foo{transform:rotate3d(1,1,1,45deg)translate3d(100px,100px,10px)}", ); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test( + // ".foo{transform:translate3d(100px, 100px, 10px) skew(10deg) scale3d(2, 3, 4)}", + // ".foo{transform:matrix3d(2,0,0,0,.528981,3,0,0,0,0,4,0,100,100,10,1)}", + // ); + // minify_test( + // ".foo{transform:matrix3d(0.804737854124365, 0.5058793634016805, -0.31061721752604554, 0, -0.31061721752604554, 0.804737854124365, 0.5058793634016805, 0, 0.5058793634016805, -0.31061721752604554, 0.804737854124365, 0, 100, 100, 10, 1)}", + // ".foo{transform:translate3d(100px,100px,10px)rotate3d(1,1,1,45deg)}" + // ); + // minify_test( + // ".foo{transform:matrix3d(1, 0, 0, 0, 0, 0.7071067811865476, 0.7071067811865475, 0, 0, -0.7071067811865475, 0.7071067811865476, 0, 100, 100, 10, 1)}", + // ".foo{transform:translate3d(100px,100px,10px)rotateX(45deg)}" + // ); + // minify_test( + // ".foo{transform:translate3d(100px, 200px, 10px) translate(100px, 100px)}", + // ".foo{transform:translate3d(200px,300px,10px)}", + // ); + // minify_test( + // ".foo{transform:rotate(45deg) rotate(45deg)}", + // ".foo{transform:rotate(90deg)}", + // ); + // minify_test( + // ".foo{transform:matrix(0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 100, 100)}", + // ".foo{transform:translate(100px,100px)rotate(45deg)}" + // ); + // minify_test( + // ".foo{transform:translateX(2in) translateX(50px)}", + // ".foo{transform:translate(242px)}", + // ); minify_test( ".foo{transform:translateX(calc(2in + 50px))}", ".foo{transform:translate(242px)}", @@ -11641,26 +12863,43 @@ mod tests { minify_test(".foo { translate: 1px 0px 0px }", ".foo{translate:1px}"); minify_test(".foo { translate: 1px 2px 0px }", ".foo{translate:1px 2px}"); minify_test(".foo { translate: 1px 0px 2px }", ".foo{translate:1px 0 2px}"); - minify_test(".foo { translate: none }", ".foo{translate:0}"); + minify_test(".foo { translate: none }", ".foo{translate:none}"); + minify_test(".foo { rotate: none }", ".foo{rotate:none}"); + minify_test(".foo { rotate: 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: -0deg }", ".foo{rotate:0deg}"); minify_test(".foo { rotate: 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: z 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: 0 0 1 10deg }", ".foo{rotate:10deg}"); minify_test(".foo { rotate: x 10deg }", ".foo{rotate:x 10deg}"); minify_test(".foo { rotate: 1 0 0 10deg }", ".foo{rotate:x 10deg}"); - minify_test(".foo { rotate: y 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: 2 0 0 10deg }", ".foo{rotate:x 10deg}"); + minify_test(".foo { rotate: 0 2 0 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: 0 0 2 10deg }", ".foo{rotate:10deg}"); + minify_test(".foo { rotate: 0 0 5.3 10deg }", ".foo{rotate:10deg}"); + minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 10deg 0 0 -1 }", ".foo{rotate:-10deg}"); + minify_test(".foo { rotate: 10deg 0 0 -233 }", ".foo{rotate:-10deg}"); + minify_test(".foo { rotate: -1 0 0 0deg }", ".foo{rotate:x 0deg}"); + minify_test(".foo { rotate: 0deg 0 0 1 }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 0deg 0 0 -1 }", ".foo{rotate:0deg}"); minify_test(".foo { rotate: 0 1 0 10deg }", ".foo{rotate:y 10deg}"); + minify_test(".foo { rotate: x 0rad }", ".foo{rotate:x 0deg}"); + // TODO: In minify mode, convert units to the shortest form. + // minify_test(".foo { rotate: y 0turn }", ".foo{rotate:y 0deg}"); + minify_test(".foo { rotate: z 0deg }", ".foo{rotate:0deg}"); + minify_test(".foo { rotate: 10deg y }", ".foo{rotate:y 10deg}"); minify_test(".foo { rotate: 1 1 1 10deg }", ".foo{rotate:1 1 1 10deg}"); - minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:none}"); - minify_test(".foo { rotate: none }", ".foo{rotate:none}"); minify_test(".foo { scale: 1 }", ".foo{scale:1}"); minify_test(".foo { scale: 1 1 }", ".foo{scale:1}"); minify_test(".foo { scale: 1 1 1 }", ".foo{scale:1}"); - minify_test(".foo { scale: none }", ".foo{scale:1}"); + minify_test(".foo { scale: none }", ".foo{scale:none}"); minify_test(".foo { scale: 1 0 }", ".foo{scale:1 0}"); minify_test(".foo { scale: 1 0 1 }", ".foo{scale:1 0}"); minify_test(".foo { scale: 1 0 0 }", ".foo{scale:1 0 0}"); - minify_test(".foo { transform: scale(3); scale: 0.5 }", ".foo{transform:scale(1.5)}"); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test(".foo { transform: scale(3); scale: 0.5 }", ".foo{transform:scale(1.5)}"); minify_test(".foo { scale: 0.5; transform: scale(3); }", ".foo{transform:scale(3)}"); prefix_test( @@ -12073,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); } "#}, @@ -12091,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); } "#}, @@ -12109,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); } "#}, @@ -12127,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); } "#}, @@ -12145,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); } "#}, @@ -12163,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); } "#}, @@ -12181,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); } "#}, @@ -12215,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); } "#}, @@ -12339,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"); } "#}, @@ -12421,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)); } @@ -12437,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)); } @@ -12508,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)); } @@ -12524,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)); } @@ -12560,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] @@ -12630,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 @@ -12652,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}"); @@ -12750,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; @@ -12794,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}"); @@ -13117,6 +14847,68 @@ mod tests { ..Default::default() }, ); + prefix_test( + r#" + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } + } + "#, + indoc! { r#" + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Default::default() + }, + ); + prefix_test( + r#" + @supports ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } + } + "#, + indoc! { r#" + @supports ((-webkit-backdrop-filter: blur(20px))) or ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + } + } + "#}, + Browsers { + safari: Some(14 << 16), + ..Default::default() + }, + ); + prefix_test( + r#" + @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) { + div { + backdrop-filter: blur(10px); + } + } + "#, + indoc! { r#" + @supports (backdrop-filter: blur(10px)) { + div { + backdrop-filter: blur(10px); + } + } + "#}, + Browsers { + chrome: Some(80 << 16), + ..Default::default() + }, + ); minify_test( r#" @supports (width: calc(10px * 2)) { @@ -13320,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] @@ -14424,14 +16218,35 @@ mod tests { prefix_test( r#" - .foo { - text-decoration: underline 10px; + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } } "#, indoc! {r#" - .foo { - text-decoration: underline; - text-decoration-thickness: 10px; + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + .foo { + text-decoration: underline 10px; + } + "#, + indoc! {r#" + .foo { + text-decoration: underline; + text-decoration-thickness: 10px; } "#}, Browsers { @@ -14789,6 +16604,27 @@ mod tests { ..Browsers::default() }, ); + + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-emphasis: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-emphasis: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + safari: Some(8 << 16), + ..Browsers::default() + }, + ); } #[test] @@ -14876,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] @@ -15586,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] @@ -15618,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( @@ -15659,7 +17548,7 @@ mod tests { "#, indoc! {r#" .foo { - list-style: \"★\" url("ellipse.png"); + list-style: url("ellipse.png") \"★\"; list-style-image: var(--img); } "#}, @@ -15670,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)); } @@ -15685,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 { @@ -15713,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] @@ -15913,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}"); @@ -15924,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)}", @@ -15942,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)}", @@ -15962,21 +17917,37 @@ mod tests { ".foo { color: oklab(40.101% 0.1147 0.0453); }", ".foo{color:oklab(40.101% .1147 .0453)}", ); + minify_test( + ".foo { color: oklab(.40101 0.1147 0.0453); }", + ".foo{color:oklab(40.101% .1147 .0453)}", + ); + minify_test( + ".foo { color: oklab(40.101% 0.1147% 0.0453%); }", + ".foo{color:oklab(40.101% .0004588 .0001812)}", + ); minify_test( ".foo { color: oklch(40.101% 0.12332 21.555); }", ".foo{color:oklch(40.101% .12332 21.555)}", ); + minify_test( + ".foo { color: oklch(.40101 0.12332 21.555); }", + ".foo{color:oklch(40.101% .12332 21.555)}", + ); + minify_test( + ".foo { color: oklch(40.101% 0.12332% 21.555); }", + ".foo{color:oklch(40.101% .00049328 21.555)}", + ); minify_test( ".foo { color: oklch(40.101% 0.12332 .5turn); }", ".foo{color:oklch(40.101% .12332 180)}", ); minify_test( ".foo { color: color(display-p3 1 0.5 0); }", - ".foo{color:color(display-p3 1 .5)}", + ".foo{color:color(display-p3 1 .5 0)}", ); minify_test( ".foo { color: color(display-p3 100% 50% 0%); }", - ".foo{color:color(display-p3 1 .5)}", + ".foo{color:color(display-p3 1 .5 0)}", ); minify_test( ".foo { color: color(xyz-d50 0.2005 0.14089 0.4472); }", @@ -16002,24 +17973,27 @@ mod tests { ".foo { color: color(xyz 20.05% 14.089% 44.72%); }", ".foo{color:color(xyz .2005 .14089 .4472)}", ); - minify_test(".foo { color: color(xyz 0.2005 0 0); }", ".foo{color:color(xyz .2005)}"); - minify_test(".foo { color: color(xyz 0 0 0); }", ".foo{color:color(xyz)}"); - minify_test(".foo { color: color(xyz 0 1 0); }", ".foo{color:color(xyz 0 1)}"); - minify_test(".foo { color: color(xyz 0 1); }", ".foo{color:color(xyz 0 1)}"); - minify_test(".foo { color: color(xyz 1); }", ".foo{color:color(xyz 1)}"); - minify_test(".foo { color: color(xyz); }", ".foo{color:color(xyz)}"); + minify_test( + ".foo { color: color(xyz 0.2005 0 0); }", + ".foo{color:color(xyz .2005 0 0)}", + ); + minify_test(".foo { color: color(xyz 0 0 0); }", ".foo{color:color(xyz 0 0 0)}"); + minify_test(".foo { color: color(xyz 0 1 0); }", ".foo{color:color(xyz 0 1 0)}"); minify_test( ".foo { color: color(xyz 0 1 0 / 20%); }", - ".foo{color:color(xyz 0 1/.2)}", + ".foo{color:color(xyz 0 1 0/.2)}", + ); + minify_test( + ".foo { color: color(xyz 0 0 0 / 20%); }", + ".foo{color:color(xyz 0 0 0/.2)}", ); - minify_test(".foo { color: color(xyz / 20%); }", ".foo{color:color(xyz/.2)}"); minify_test( ".foo { color: color(display-p3 100% 50% 0 / 20%); }", - ".foo{color:color(display-p3 1 .5/.2)}", + ".foo{color:color(display-p3 1 .5 0/.2)}", ); minify_test( - ".foo { color: color(display-p3 100% / 20%); }", - ".foo{color:color(display-p3 1/.2)}", + ".foo { color: color(display-p3 100% 0 0 / 20%); }", + ".foo{color:color(display-p3 1 0 0/.2)}", ); minify_test(".foo { color: hsl(none none none) }", ".foo{color:#000}"); minify_test(".foo { color: hwb(none none none) }", ".foo{color:red}"); @@ -16505,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 { @@ -16607,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 { @@ -16784,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))", @@ -16800,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))}", @@ -16919,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). @@ -17057,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)", ); @@ -17195,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)", ); @@ -17658,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), @@ -17666,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), @@ -17685,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(). @@ -18554,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}", ); } } @@ -18663,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: @@ -19966,7 +22025,7 @@ mod tests { ".foo {{ color: color-mix(in {0}, color({0} -2 -3 -4 / -5), color({0} -4 -6 -8 / -10)) }}", color_space ), - &format!(".foo{{color:color({}/0)}}", result_color_space), + &format!(".foo{{color:color({} 0 0 0/0)}}", result_color_space), ); minify_test( @@ -20035,7 +22094,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -20183,32 +22241,240 @@ mod tests { ".foo{grid-template-areas:\"head head\"\"nav main\"\". .\"}", ); + // to grid-* shorthand + minify_test( + r#" + .test-miss-areas { + grid-template-columns: 1fr 90px; + grid-template-rows: auto 80px; + grid-template-areas: "one"; + } + "#, + ".test-miss-areas{grid-template:\"one\"\".\"80px/1fr 90px}", + ); test( r#" - .foo { - grid-template-areas: "head head" "nav main" "foot ...."; + .test-miss-areas-2 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 30px 60px 100px; + grid-template-areas: "a a a" "b c c"; } "#, indoc! { r#" - .foo { - grid-template-areas: "head head" - "nav main" - "foot ."; + .test-miss-areas-2 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; } "#}, ); - minify_test( + test( r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom]; + .test-miss-areas-3 { + grid-template: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", + indoc! { r#" + .test-miss-areas-3 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, ); - minify_test( - r#" + + test( + r#" + .test-miss-areas-4 { + grid: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; + } + "#, + indoc! { r#" + .test-miss-areas-4 { + grid: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, + ); + + // test no unreachable error + minify_test( + r#" + .grid-shorthand-areas { + grid: auto / 1fr 3fr; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas{grid:\".content.\"/1fr 3fr}", + ); + minify_test( + r#" + .grid-shorthand-areas-rows { + grid: auto / 1fr 3fr; + grid-template-rows: 20px; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas-rows{grid:\".content.\"20px/1fr 3fr}", + ); + + // test grid-auto-flow: row in grid shorthand + test( + r#" + .test-auto-flow-row-1 { + grid: auto-flow / 1fr 2fr 1fr; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-row-1 { + grid: auto-flow / 1fr 2fr 1fr; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-row-2 { + grid: auto-flow auto / 100px 100px; + grid-template-areas: " one two "; + } + "#, + indoc! { r#" + .test-auto-flow-row-2 { + grid: auto-flow / 100px 100px; + grid-template-areas: "one two"; + } + "#}, + ); + test( + r#" + .test-auto-flow-dense { + grid: dense auto-flow / 1fr 2fr; + grid-template-areas: " . content . "; + } + "#, + indoc! { r#" + .test-auto-flow-dense { + grid: auto-flow dense / 1fr 2fr; + grid-template-areas: ". content ."; + } + "#}, + ); + minify_test( + r#" + .grid-auto-flow-row-auto-rows { + grid: auto-flow 40px / 1fr 90px; + grid-template-areas: "a"; + } + "#, + ".grid-auto-flow-row-auto-rows{grid:auto-flow 40px/1fr 90px;grid-template-areas:\"a\"}", + ); + minify_test( + r#" + .grid-auto-flow-row-auto-rows-multiple { + grid: auto-flow 40px max-content / 1fr; + grid-template-areas: ". a"; + } + "#, + ".grid-auto-flow-row-auto-rows-multiple{grid:auto-flow 40px max-content/1fr;grid-template-areas:\".a\"}", + ); + + // test grid-auto-flow: column in grid shorthand + test( + r#" + .test-auto-flow-column-1 { + grid: 300px / auto-flow; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-1 { + grid: 300px / auto-flow; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-column-2 { + grid: 200px 1fr / auto-flow auto; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-2 { + grid: 200px 1fr / auto-flow; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-column-dense { + grid: 1fr 2fr / dense auto-flow; + grid-template-areas: " . content . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-dense { + grid: 1fr 2fr / auto-flow dense; + grid-template-areas: ". content ."; + } + "#}, + ); + minify_test( + r#" + .grid-auto-flow-column-auto-rows { + grid: 1fr 3fr / auto-flow 40px; + grid-template-areas: "a"; + } + "#, + ".grid-auto-flow-column-auto-rows{grid:1fr 3fr/auto-flow 40px;grid-template-areas:\"a\"}", + ); + minify_test( + r#" + .grid-auto-flow-column-auto-rows-multiple { + grid: 1fr / auto-flow 40px max-content ; + grid-template-areas: ". a"; + } + "#, + ".grid-auto-flow-column-auto-rows-multiple{grid:1fr/auto-flow 40px max-content;grid-template-areas:\".a\"}", + ); + + test( + r#" + .foo { + grid-template-areas: "head head" "nav main" "foot ...."; + } + "#, + indoc! { r#" + .foo { + grid-template-areas: "head head" + "nav main" + "foot ."; + } + "#}, + ); + + minify_test( + r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom]; + } + "#, + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", + ); + minify_test( + r#" .foo { grid-template: "head head" "nav main" 1fr @@ -20862,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)); }", @@ -20882,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#" @@ -20911,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 { @@ -20934,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 { @@ -20964,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 { @@ -21132,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 { @@ -21298,14 +23723,47 @@ mod tests { prefix_test( r#" - @keyframes foo { - from { - --custom: lab(40% 56.6 39); - } - - to { - --custom: lch(50.998% 135.363 338); - } + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + r#" + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lch(50.998% 135.363 338); + } } "#, indoc! {r#" @@ -21350,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 { @@ -21488,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), @@ -21709,7 +24225,7 @@ mod tests { grid-auto-flow: column; } - @media (min-width: 1024px) { + @media not (max-width: 1024px) { .foo { max-inline-size: 1024px; } @@ -22576,13 +25092,13 @@ mod tests { } "#, indoc! {r#" - .foo { - color: red; - } - .foo .bar { color: #00f; } + + .foo { + color: red; + } "#}, ); @@ -22596,12 +25112,16 @@ mod tests { "#, indoc! {r#" article { - color: red; + color: green; } article { color: #00f; } + + article { + color: red; + } "#}, ); @@ -22710,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; } + } "#}, ); @@ -22886,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] @@ -22980,9 +25575,102 @@ mod tests { }, HashMap::new(), Default::default(), + false, + ); + + css_modules_test( + r#" + .foo { + color: red; + } + + #id { + animation: 2s test; + } + + @keyframes test { + from { color: red } + to { color: yellow } + } + "#, + indoc! {r#" + .EgL3uq_foo { + color: red; + } + + #EgL3uq_id { + animation: 2s test; + } + + @keyframes test { + from { + color: red; + } + + to { + color: #ff0; + } + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "id" => "EgL3uq_id" + }, + HashMap::new(), + crate::css_modules::Config { + animation: false, + // custom_idents: false, + ..Default::default() + }, + false, + ); + + css_modules_test( + r#" + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#, + indoc! {r#" + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#}, + map! { + "circles" => "EgL3uq_circles" referenced: true + }, + HashMap::new(), + crate::css_modules::Config { + custom_idents: false, + ..Default::default() + }, + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" body { @@ -23024,9 +25712,9 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" .grid { @@ -23062,22 +25750,64 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" - test { - transition-property: opacity; - } - "#, - indoc! {r#" - test { - transition-property: opacity; + .grid { + grid-template-areas: "foo"; + } + + .foo { + grid-area: foo; + } + + .bar { + grid-column-start: foo-start; + } + "#, + indoc! {r#" + .EgL3uq_grid { + grid-template-areas: "foo"; + } + + .EgL3uq_foo { + grid-area: foo; + } + + .EgL3uq_bar { + grid-column-start: foo-start; + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "grid" => "EgL3uq_grid", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + crate::css_modules::Config { + grid: false, + ..Default::default() + }, + false, + ); + + css_modules_test( + r#" + test { + transition-property: opacity; + } + "#, + indoc! {r#" + test { + transition-property: opacity; } "#}, map! {}, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23112,6 +25842,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); // :global(:local(.hi)) { @@ -23144,6 +25875,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23173,6 +25905,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23210,6 +25943,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23229,6 +25963,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23248,6 +25983,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23267,6 +26003,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23286,6 +26023,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23315,160 +26053,487 @@ mod tests { "foo" => "EgL3uq_foo" }, HashMap::new(), - Default::default(), + Default::default(), + false, + ); + + css_modules_test( + r#" + .foo { + color: red; + } + "#, + indoc! {r#" + .test-EgL3uq-foo { + color: red; + } + "#}, + map! { + "foo" => "test-EgL3uq-foo" + }, + HashMap::new(), + crate::css_modules::Config { + pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), + ..Default::default() + }, + false, + ); + + let stylesheet = StyleSheet::parse( + r#" + .grid { + grid-template-areas: "foo"; + } + + .foo { + grid-area: foo; + } + + .bar { + grid-column-start: foo-start; + } + "#, + ParserOptions { + css_modules: Some(crate::css_modules::Config { + pattern: crate::css_modules::Pattern::parse("test-[local]-[hash]").unwrap(), + ..Default::default() + }), + ..ParserOptions::default() + }, + ) + .unwrap(); + if let Err(err) = stylesheet.to_css(PrinterOptions::default()) { + assert_eq!(err.kind, PrinterErrorKind::InvalidCssModulesPatternInGrid); + } else { + unreachable!() + } + + css_modules_test( + r#" + @property --foo { + syntax: ''; + inherits: false; + initial-value: yellow; + } + + .foo { + --foo: red; + color: var(--foo); + } + "#, + indoc! {r#" + @property --foo { + syntax: ""; + inherits: false; + initial-value: #ff0; + } + + .EgL3uq_foo { + --foo: red; + color: var(--foo); + } + "#}, + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + false, + ); + + css_modules_test( + r#" + @property --foo { + syntax: ''; + inherits: false; + initial-value: yellow; + } + + @font-palette-values --Cooler { + font-family: Bixa; + base-palette: 1; + override-colors: 1 #7EB7E4; + } + + .foo { + --foo: red; + --bar: green; + color: var(--foo); + font-palette: --Cooler; + } + + .bar { + color: var(--color from "./b.css"); + } + "#, + indoc! {r#" + @property --EgL3uq_foo { + syntax: ""; + inherits: false; + initial-value: #ff0; + } + + @font-palette-values --EgL3uq_Cooler { + font-family: Bixa; + base-palette: 1; + override-colors: 1 #7eb7e4; + } + + .EgL3uq_foo { + --EgL3uq_foo: red; + --EgL3uq_bar: green; + color: var(--EgL3uq_foo); + font-palette: --EgL3uq_Cooler; + } + + .EgL3uq_bar { + color: var(--ma1CsG); + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "--foo" => "--EgL3uq_foo" referenced: true, + "--bar" => "--EgL3uq_bar", + "bar" => "EgL3uq_bar", + "--Cooler" => "--EgL3uq_Cooler" referenced: true + }, + HashMap::from([( + "--ma1CsG".into(), + CssModuleReference::Dependency { + name: "--color".into(), + specifier: "./b.css".into(), + }, + )]), + crate::css_modules::Config { + dashed_idents: true, + ..Default::default() + }, + false, + ); + + css_modules_test( + r#" + .test { + animation: rotate var(--duration) linear infinite; + } + "#, + indoc! {r#" + .EgL3uq_test { + animation: EgL3uq_rotate var(--duration) linear infinite; + } + "#}, + map! { + "test" => "EgL3uq_test", + "rotate" => "EgL3uq_rotate" referenced: true + }, + HashMap::new(), + Default::default(), + false, + ); + css_modules_test( + r#" + .test { + animation: none var(--duration); + } + "#, + indoc! {r#" + .EgL3uq_test { + animation: none var(--duration); + } + "#}, + map! { + "test" => "EgL3uq_test" + }, + HashMap::new(), + Default::default(), + false, + ); + css_modules_test( + r#" + .test { + animation: var(--animation); + } + "#, + indoc! {r#" + .EgL3uq_test { + animation: var(--animation); + } + "#}, + map! { + "test" => "EgL3uq_test" + }, + HashMap::new(), + Default::default(), + false, + ); + css_modules_test( + r#" + .test { + animation: rotate var(--duration); + } + "#, + indoc! {r#" + .EgL3uq_test { + animation: rotate var(--duration); + } + "#}, + map! { + "test" => "EgL3uq_test" + }, + HashMap::new(), + crate::css_modules::Config { + animation: false, + ..Default::default() + }, + false, + ); + css_modules_test( + r#" + .test { + animation: "rotate" var(--duration); + } + "#, + indoc! {r#" + .EgL3uq_test { + animation: EgL3uq_rotate var(--duration); + } + "#}, + map! { + "test" => "EgL3uq_test", + "rotate" => "EgL3uq_rotate" referenced: true + }, + HashMap::new(), + crate::css_modules::Config { ..Default::default() }, + false, + ); + + css_modules_test( + r#" + .test { + composes: foo bar from "foo.css"; + background: white; + } + "#, + 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, + ); + + 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#" - .foo { - color: red; + .box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } "#, indoc! {r#" - .test-EgL3uq-foo { - color: red; + .EgL3uq_box2 { + @container main (width >= 0) { + background-color: #90ee90; + } } "#}, map! { - "foo" => "test-EgL3uq-foo" + "box2" => "EgL3uq_box2" }, HashMap::new(), crate::css_modules::Config { - pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), + container: false, ..Default::default() }, + false, ); - let stylesheet = StyleSheet::parse( - r#" - .grid { - grid-template-areas: "foo"; - } - - .foo { - grid-area: foo; - } - - .bar { - grid-column-start: foo-start; - } - "#, - ParserOptions { - css_modules: Some(crate::css_modules::Config { - pattern: crate::css_modules::Pattern::parse("test-[local]-[hash]").unwrap(), - ..Default::default() - }), - ..ParserOptions::default() + css_modules_test( + ".foo { view-transition-name: bar }", + ".EgL3uq_foo{view-transition-name:EgL3uq_bar}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" }, - ) - .unwrap(); - if let Err(err) = stylesheet.to_css(PrinterOptions::default()) { - assert_eq!(err.kind, PrinterErrorKind::InvalidCssModulesPatternInGrid); - } else { - unreachable!() - } - + HashMap::new(), + Default::default(), + true, + ); css_modules_test( - r#" - @property --foo { - syntax: ''; - inherits: false; - initial-value: yellow; - } - - .foo { - --foo: red; - color: var(--foo); - } - "#, - indoc! {r#" - @property --foo { - syntax: ""; - inherits: false; - initial-value: #ff0; - } - - .EgL3uq_foo { - --foo: red; - color: var(--foo); - } - "#}, + ".foo { view-transition-name: none }", + ".EgL3uq_foo{view-transition-name:none}", map! { "foo" => "EgL3uq_foo" }, HashMap::new(), Default::default(), + true, ); - css_modules_test( - r#" - @property --foo { - syntax: ''; - inherits: false; - initial-value: yellow; - } - - @font-palette-values --Cooler { - font-family: Bixa; - base-palette: 1; - override-colors: 1 #7EB7E4; - } - - .foo { - --foo: red; - --bar: green; - color: var(--foo); - font-palette: --Cooler; - } - - .bar { - color: var(--color from "./b.css"); - } - "#, - indoc! {r#" - @property --EgL3uq_foo { - syntax: ""; - inherits: false; - initial-value: #ff0; - } + ".foo { view-transition-name: auto }", + ".EgL3uq_foo{view-transition-name:auto}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); - @font-palette-values --EgL3uq_Cooler { - font-family: Bixa; - base-palette: 1; - override-colors: 1 #7eb7e4; - } + 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, + ); - .EgL3uq_foo { - --EgL3uq_foo: red; - --EgL3uq_bar: green; - color: var(--EgL3uq_foo); - font-palette: --EgL3uq_Cooler; - } + 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, + ); - .EgL3uq_bar { - color: var(--ma1CsG); - } - "#}, + css_modules_test( + "@view-transition { types: foo bar baz }", + "@view-transition{types:EgL3uq_foo EgL3uq_bar EgL3uq_baz}", map! { "foo" => "EgL3uq_foo", - "--foo" => "--EgL3uq_foo" referenced: true, - "--bar" => "--EgL3uq_bar", "bar" => "EgL3uq_bar", - "--Cooler" => "--EgL3uq_Cooler" referenced: true + "baz" => "EgL3uq_baz" }, - HashMap::from([( - "--ma1CsG".into(), - CssModuleReference::Dependency { - name: "--color".into(), - specifier: "./b.css".into(), - }, - )]), - crate::css_modules::Config { - dashed_idents: true, - ..Default::default() + 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( @@ -23507,6 +26572,56 @@ mod tests { test_project_root("/foo", "/foo/test.css", "EgL3uq"); test_project_root("/foo/bar", "/foo/bar/baz/test.css", "xLEkNW"); test_project_root("/foo", "/foo/baz/test.css", "xLEkNW"); + + let mut stylesheet = StyleSheet::parse( + r#" + .foo { + color: red; + .bar { + color: green; + } + composes: test from "foo.css"; + } + "#, + ParserOptions { + filename: "test.css".into(), + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..Default::default() + }) + .unwrap(); + assert_eq!( + res.code, + indoc! {r#" + .EgL3uq_foo { + color: red; + } + + .EgL3uq_foo .EgL3uq_bar { + color: green; + } + + + "#} + ); + assert_eq!( + res.exports.unwrap(), + map! { + "foo" => "EgL3uq_foo" "test" from "foo.css", + "bar" => "EgL3uq_bar" + } + ); } #[test] @@ -23790,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}"); @@ -24048,21 +27165,42 @@ mod tests { fill: url("#foo") color(display-p3 .972962 -.362078 .804206); fill: url("#foo") lch(50.998% 135.363 338); } - "##}, + "##}, + Browsers { + chrome: Some(90 << 16), + safari: Some(14 << 16), + ..Browsers::default() + }, + ); + + prefix_test( + ".foo { fill: var(--url) lch(50.998% 135.363 338) }", + indoc! { r#" + .foo { + fill: var(--url) #ee00be; + } + + @supports (color: lab(0% 0 0)) { + .foo { + fill: var(--url) lab(50.998% 125.506 -50.7078); + } + } + "#}, Browsers { chrome: Some(90 << 16), - safari: Some(14 << 16), ..Browsers::default() }, ); prefix_test( - ".foo { fill: var(--url) lch(50.998% 135.363 338) }", - indoc! { r#" - .foo { - fill: var(--url) #ee00be; + 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); @@ -24080,7 +27218,7 @@ mod tests { indoc! { r#" .foo { -webkit-mask-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)); - -webkit-mask-image: -webkit-linear-gradient(#ff0f0e, #7773ff); + -webkit-mask-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff); -webkit-mask-image: linear-gradient(#ff0f0e, #7773ff); mask-image: linear-gradient(#ff0f0e, #7773ff); -webkit-mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)); @@ -24142,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; @@ -24190,6 +27328,28 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#, + indoc! { r#" + @supports (color: lab(0% 0 0)) { + .foo { + -webkit-mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( ".foo { mask: url(masks.svg#star) luminance }", indoc! { r#" @@ -24575,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] @@ -24746,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( @@ -24836,7 +28087,7 @@ mod tests { } } "#, - ".foo{@scope(.bar){&{color:#ff0}}}", + ".foo{@scope(.bar){color:#ff0}}", ); nesting_test( r#" @@ -24848,9 +28099,7 @@ mod tests { "#, indoc! {r#" @scope (.bar) { - :scope { - color: #ff0; - } + color: #ff0; } "#}, ); @@ -25240,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; } @@ -25990,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 { @@ -26076,13 +29336,15 @@ mod tests { "@property --property-name{syntax:\"custom|\";inherits:false;initial-value:#ff0}", ); - minify_test(r#" - @property --property-name { - syntax: ''; - inherits: false; - initial-value: translate(200px,300px) translate(100px,200px) scale(2); - } - "#, "@property --property-name{syntax:\"\";inherits:false;initial-value:matrix(2,0,0,2,300,500)}"); + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // minify_test(r#" + // @property --property-name { + // syntax: ''; + // inherits: false; + // initial-value: translate(200px,300px) translate(100px,200px) scale(2); + // } + // "#, "@property --property-name{syntax:\"\";inherits:false;initial-value:matrix(2,0,0,2,300,500)}"); minify_test( r#" @@ -26183,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] @@ -26270,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}; @@ -26298,6 +29644,14 @@ mod tests { color: red; } } + + input:placeholder { + color: red; + } + + input::hover { + color: red; + } "#, indoc! { r#" .foo { @@ -26313,6 +29667,14 @@ mod tests { color: red; } } + + input:placeholder { + color: red; + } + + input::hover { + color: red; + } "#}, ParserOptions { filename: "test.css".into(), @@ -26350,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, + }), + }, ] ) } @@ -26368,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#" @@ -26678,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 @@ -26718,10 +30178,40 @@ 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 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, ); } @@ -26736,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 { @@ -26900,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)) { @@ -26925,6 +30438,7 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); } @@ -27004,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; }", @@ -27112,6 +30632,45 @@ 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 #ffffff80, 0 .44rem .8rem -.58rem; + box-shadow: 0 .63rem .94rem -.19rem lab(100% 0 0 / .5), 0 .44rem .8rem -.58rem; + } + "#}, + Browsers { + chrome: Some(95 << 16), + ..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)); }", indoc! { r#" @@ -27173,5 +30732,271 @@ 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] + fn test_all() { + minify_test(".foo { all: initial; all: initial }", ".foo{all:initial}"); + minify_test(".foo { all: initial; all: revert }", ".foo{all:revert}"); + minify_test(".foo { background: red; all: revert-layer }", ".foo{all:revert-layer}"); + minify_test( + ".foo { background: red; all: revert-layer; background: green }", + ".foo{all:revert-layer;background:green}", + ); + minify_test( + ".foo { --test: red; all: revert-layer }", + ".foo{--test:red;all:revert-layer}", + ); + minify_test( + ".foo { unicode-bidi: embed; all: revert-layer }", + ".foo{all:revert-layer;unicode-bidi:embed}", + ); + minify_test( + ".foo { direction: rtl; all: revert-layer }", + ".foo{all:revert-layer;direction:rtl}", + ); + minify_test( + ".foo { direction: rtl; all: revert-layer; direction: ltr }", + ".foo{all:revert-layer;direction:ltr}", + ); + minify_test(".foo { background: var(--foo); all: unset; }", ".foo{all:unset}"); + minify_test( + ".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 e102676f9..a50d54f5a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -8,12 +8,12 @@ macro_rules! enum_property { )+ } ) => { - $(#[$outer])* - #[derive(Debug, Clone, Copy, PartialEq)] + #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] #[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 = "kebab-case"))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] + $(#[$outer])* $vis enum $name { $( $(#[$meta])* @@ -21,50 +21,17 @@ macro_rules! enum_property { )+ } - impl<'i> Parse<'i> for $name { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - let location = input.current_source_location(); - let ident = input.expect_ident()?; - match &ident[..] { - $( - s if s.eq_ignore_ascii_case(stringify!($x)) => Ok($name::$x), - )+ - _ => Err(location.new_unexpected_token_error( - cssparser::Token::Ident(ident.clone()) - )) - } - } - - fn parse_string(input: &'i str) -> Result<'i, ParserError<'i>>> { - match input { - $( - s if s.eq_ignore_ascii_case(stringify!($x)) => Ok($name::$x), - )+ - _ => return Err(ParseError { - kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(cssparser::Token::Ident(input.into()))), - location: cssparser::SourceLocation { line: 0, column: 1 } - }) - } - } - } - impl $name { /// Returns a string representation of the value. pub fn as_str(&self) -> &str { use $name::*; match self { $( - $x => const_str::convert_ascii_case!(lower, stringify!($x)), + $x => const_str::convert_ascii_case!(kebab, stringify!($x)), )+ } } } - - impl ToCss for $name { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write { - dest.write_str(self.as_str()) - } - } }; ( $(#[$outer:meta])* @@ -92,25 +59,13 @@ macro_rules! enum_property { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { let location = input.current_source_location(); let ident = input.expect_ident()?; - match &ident[..] { + cssparser::match_ignore_ascii_case! { &*ident, $( - s if s.eq_ignore_ascii_case($str) => Ok($name::$id), + $str => Ok($name::$id), )+ _ => Err(location.new_unexpected_token_error( cssparser::Token::Ident(ident.clone()) - )) - } - } - - fn parse_string(input: &'i str) -> Result<'i, ParserError<'i>>> { - match input { - $( - s if s.eq_ignore_ascii_case($str) => Ok($name::$id), - )+ - _ => return Err(ParseError { - kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(cssparser::Token::Ident(input.into()))), - location: cssparser::SourceLocation { line: 0, column: 1 } - }) + )), } } } @@ -234,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 { @@ -261,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 } @@ -295,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)); @@ -308,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()); } } @@ -334,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)); @@ -343,7 +298,7 @@ macro_rules! shorthand_handler { )? dest.push(Property::$shorthand(shorthand)); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::$shorthand); }; } else { @@ -351,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)); @@ -360,7 +315,7 @@ macro_rules! shorthand_handler { )? dest.push(Property::$prop(val)); - paste::paste! { + pastey::paste! { self.flushed_properties.insert([<$shorthand Property>]::$prop); }; } @@ -435,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 fe06123d2..63792255e 100644 --- a/src/prefixes.rs +++ b/src/prefixes.rs @@ -550,12 +550,12 @@ impl Feature { } } if let Some(version) = browsers.ios_saf { - if version >= 589824 { + if version >= 589824 && version <= 1115648 { prefixes |= VendorPrefix::WebKit; } } if let Some(version) = browsers.safari { - if version >= 589824 { + if version >= 589824 && version <= 1115648 { prefixes |= VendorPrefix::WebKit; } } @@ -800,23 +800,23 @@ 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 { + 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; } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 1572864 { + prefixes |= VendorPrefix::WebKit; + } + } + if let Some(version) = browsers.ios_saf { + if version >= 262144 && version <= 851968 { prefixes |= VendorPrefix::WebKit; } } @@ -1124,7 +1124,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version >= 983040 { + if version >= 983040 && version <= 6225920 { prefixes |= VendorPrefix::WebKit; } } @@ -1231,11 +1231,6 @@ impl Feature { } } Feature::FitContent => { - if let Some(version) = browsers.firefox { - if version >= 196608 { - prefixes |= VendorPrefix::Moz; - } - } if let Some(version) = browsers.android { if version >= 263168 && version <= 263171 { prefixes |= VendorPrefix::WebKit; @@ -1246,6 +1241,11 @@ impl Feature { prefixes |= VendorPrefix::WebKit; } } + if let Some(version) = browsers.firefox { + if version >= 196608 && version <= 6094848 { + prefixes |= VendorPrefix::Moz; + } + } if let Some(version) = browsers.ios_saf { if version >= 458752 && version <= 852992 { prefixes |= VendorPrefix::WebKit; @@ -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; } } @@ -1468,7 +1468,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version >= 983040 { + if version >= 983040 && version <= 6881280 { prefixes |= VendorPrefix::WebKit; } } @@ -1478,7 +1478,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 1572864 { prefixes |= VendorPrefix::WebKit; } } @@ -1510,24 +1510,24 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 327680 { prefixes |= VendorPrefix::WebKit; } } } 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; } } @@ -1785,7 +1785,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 851968 { prefixes |= VendorPrefix::WebKit; } } @@ -1865,7 +1865,7 @@ impl Feature { } } if let Some(version) = browsers.opera { - if version >= 983040 { + if version >= 983040 && version <= 6422528 { prefixes |= VendorPrefix::WebKit; } } @@ -1875,7 +1875,7 @@ impl Feature { } } if let Some(version) = browsers.samsung { - if version >= 262144 { + if version >= 262144 && version <= 1441792 { 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 12fe09e32..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(); @@ -267,30 +287,37 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { /// Writes a CSS identifier to the underlying destination, escaping it /// as appropriate. If the `css_modules` option was enabled, then a hash /// is added, and the mapping is added to the CSS module. - pub fn write_ident(&mut self, ident: &str) -> Result<(), PrinterError> { - if let Some(css_module) = &mut self.css_module { - let dest = &mut self.dest; - let mut first = true; - css_module.config.pattern.write( - &css_module.hashes[self.loc.source_index as usize], - &css_module.sources[self.loc.source_index as usize], - ident, - |s| { - self.col += s.len() as u32; - if first { - first = false; - serialize_identifier(s, dest) + pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> { + if handle_css_module { + if let Some(css_module) = &mut self.css_module { + let dest = &mut self.dest; + let mut first = true; + css_module.config.pattern.write( + &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 { - serialize_name(s, dest) - } - }, - )?; + "" + }, + |s| { + self.col += s.len() as u32; + if first { + first = false; + serialize_identifier(s, dest) + } else { + serialize_name(s, dest) + } + }, + )?; - css_module.add_local(&ident, &ident, self.loc.source_index); - } else { - serialize_identifier(ident, self)?; + css_module.add_local(&ident, &ident, self.loc.source_index); + return Ok(()); + } } + serialize_identifier(ident, self)?; Ok(()) } @@ -304,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) @@ -363,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/align.rs b/src/properties/align.rs index 12917c189..819e9ac4c 100644 --- a/src/properties/align.rs +++ b/src/properties/align.rs @@ -73,13 +73,13 @@ enum_property! { /// A [``](https://www.w3.org/TR/css-align-3/#typedef-content-distribution) value. pub enum ContentDistribution { /// Items are spaced evenly, with the first and last items against the edge of the container. - "space-between": SpaceBetween, + SpaceBetween, /// Items are spaced evenly, with half-size spaces at the start and end. - "space-around": SpaceAround, + SpaceAround, /// Items are spaced evenly, with full-size spaces at the start and end. - "space-evenly": SpaceEvenly, + SpaceEvenly, /// Items are stretched evenly to fill free space. - "stretch": Stretch, + Stretch, } } @@ -99,20 +99,20 @@ enum_property! { /// A [``](https://www.w3.org/TR/css-align-3/#typedef-content-position) value. pub enum ContentPosition { /// Content is centered within the container. - "center": Center, + Center, /// Content is aligned to the start of the container. - "start": Start, + Start, /// Content is aligned to the end of the container. - "end": End, + End, /// Same as `start` when within a flexbox container. - "flex-start": FlexStart, + FlexStart, /// Same as `end` when within a flexbox container. - "flex-end": FlexEnd, + FlexEnd, } } /// A value for the [align-content](https://www.w3.org/TR/css-align-3/#propdef-align-content) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -132,54 +132,13 @@ pub enum AlignContent { ContentDistribution(ContentDistribution), /// A content position keyword. ContentPosition { - /// A content position keyword. - value: ContentPosition, /// An overflow alignment mode. overflow: Option, + /// A content position keyword. + value: ContentPosition, }, } -impl<'i> Parse<'i> for AlignContent { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { - return Ok(AlignContent::Normal); - } - - if let Ok(val) = input.try_parse(BaselinePosition::parse) { - return Ok(AlignContent::BaselinePosition(val)); - } - - if let Ok(val) = input.try_parse(ContentDistribution::parse) { - return Ok(AlignContent::ContentDistribution(val)); - } - - let overflow = input.try_parse(OverflowPosition::parse).ok(); - let value = ContentPosition::parse(input)?; - Ok(AlignContent::ContentPosition { overflow, value }) - } -} - -impl ToCss for AlignContent { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - AlignContent::Normal => dest.write_str("normal"), - AlignContent::BaselinePosition(val) => val.to_css(dest), - AlignContent::ContentDistribution(val) => val.to_css(dest), - AlignContent::ContentPosition { overflow, value } => { - if let Some(overflow) = overflow { - overflow.to_css(dest)?; - dest.write_str(" ")?; - } - - value.to_css(dest) - } - } - } -} - /// A value for the [justify-content](https://www.w3.org/TR/css-align-3/#propdef-justify-content) property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -349,24 +308,24 @@ enum_property! { /// A [``](https://www.w3.org/TR/css-align-3/#typedef-self-position) value. pub enum SelfPosition { /// Item is centered within the container. - "center": Center, + Center, /// Item is aligned to the start of the container. - "start": Start, + Start, /// Item is aligned to the end of the container. - "end": End, + End, /// Item is aligned to the edge of the container corresponding to the start side of the item. - "self-start": SelfStart, + SelfStart, /// Item is aligned to the edge of the container corresponding to the end side of the item. - "self-end": SelfEnd, + SelfEnd, /// Item is aligned to the start of the container, within flexbox layouts. - "flex-start": FlexStart, + FlexStart, /// Item is aligned to the end of the container, within flexbox layouts. - "flex-end": FlexEnd, + FlexEnd, } } /// A value for the [align-self](https://www.w3.org/TR/css-align-3/#align-self-property) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -387,59 +346,13 @@ pub enum AlignSelf { BaselinePosition(BaselinePosition), /// A self position keyword. SelfPosition { - /// A self position keyword. - value: SelfPosition, /// An overflow alignment mode. overflow: Option, + /// A self position keyword. + value: SelfPosition, }, } -impl<'i> Parse<'i> for AlignSelf { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() { - return Ok(AlignSelf::Auto); - } - - if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { - return Ok(AlignSelf::Normal); - } - - if input.try_parse(|input| input.expect_ident_matching("stretch")).is_ok() { - return Ok(AlignSelf::Stretch); - } - - if let Ok(val) = input.try_parse(BaselinePosition::parse) { - return Ok(AlignSelf::BaselinePosition(val)); - } - - let overflow = input.try_parse(OverflowPosition::parse).ok(); - let value = SelfPosition::parse(input)?; - Ok(AlignSelf::SelfPosition { overflow, value }) - } -} - -impl ToCss for AlignSelf { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - AlignSelf::Auto => dest.write_str("auto"), - AlignSelf::Normal => dest.write_str("normal"), - AlignSelf::Stretch => dest.write_str("stretch"), - AlignSelf::BaselinePosition(val) => val.to_css(dest), - AlignSelf::SelfPosition { overflow, value } => { - if let Some(overflow) = overflow { - overflow.to_css(dest)?; - dest.write_str(" ")?; - } - - value.to_css(dest) - } - } - } -} - /// A value for the [justify-self](https://www.w3.org/TR/css-align-3/#justify-self-property) property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -615,7 +528,7 @@ impl ToCss for PlaceSelf { } /// A value for the [align-items](https://www.w3.org/TR/css-align-3/#align-items-property) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -634,54 +547,13 @@ pub enum AlignItems { BaselinePosition(BaselinePosition), /// A self position keyword. SelfPosition { - /// A self position keyword. - value: SelfPosition, /// An overflow alignment mode. overflow: Option, + /// A self position keyword. + value: SelfPosition, }, } -impl<'i> Parse<'i> for AlignItems { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { - return Ok(AlignItems::Normal); - } - - if input.try_parse(|input| input.expect_ident_matching("stretch")).is_ok() { - return Ok(AlignItems::Stretch); - } - - if let Ok(val) = input.try_parse(BaselinePosition::parse) { - return Ok(AlignItems::BaselinePosition(val)); - } - - let overflow = input.try_parse(OverflowPosition::parse).ok(); - let value = SelfPosition::parse(input)?; - Ok(AlignItems::SelfPosition { overflow, value }) - } -} - -impl ToCss for AlignItems { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - AlignItems::Normal => dest.write_str("normal"), - AlignItems::Stretch => dest.write_str("stretch"), - AlignItems::BaselinePosition(val) => val.to_css(dest), - AlignItems::SelfPosition { overflow, value } => { - if let Some(overflow) = overflow { - overflow.to_css(dest)?; - dest.write_str(" ")?; - } - - value.to_css(dest) - } - } - } -} - /// A legacy justification keyword, as used in the `justify-items` property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -925,7 +797,7 @@ impl ToCss for PlaceItems { /// A [gap](https://www.w3.org/TR/css-align-3/#column-row-gap) value, as used in the /// `column-gap` and `row-gap` properties. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -941,29 +813,6 @@ pub enum GapValue { LengthPercentage(LengthPercentage), } -impl<'i> Parse<'i> for GapValue { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { - return Ok(GapValue::Normal); - } - - let val = LengthPercentage::parse(input)?; - Ok(GapValue::LengthPercentage(val)) - } -} - -impl ToCss for GapValue { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - GapValue::Normal => dest.write_str("normal"), - GapValue::LengthPercentage(lp) => lp.to_css(dest), - } - } -} - define_shorthand! { /// A value for the [gap](https://www.w3.org/TR/css-align-3/#gap-shorthand) shorthand property. pub struct Gap { diff --git a/src/properties/animation.rs b/src/properties/animation.rs index c995c2221..51bd1ea48 100644 --- a/src/properties/animation.rs +++ b/src/properties/animation.rs @@ -1,15 +1,20 @@ //! CSS properties related to keyframe animations. +use std::borrow::Cow; + use crate::context::PropertyHandlerContext; use crate::declaration::{DeclarationBlock, DeclarationList}; use crate::error::{ParserError, PrinterError}; use crate::macros::*; use crate::prefixes::Feature; use crate::printer::Printer; -use crate::properties::{Property, PropertyId, VendorPrefix}; +use crate::properties::{Property, PropertyId, TokenOrValue, VendorPrefix}; use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero}; +use crate::values::ident::DashedIdent; use crate::values::number::CSSNumber; -use crate::values::string::CowArcStr; +use crate::values::percentage::Percentage; +use crate::values::size::Size2D; +use crate::values::string::CSSString; use crate::values::{easing::EasingFunction, ident::CustomIdent, time::Time}; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -17,8 +22,10 @@ use cssparser::*; use itertools::izip; use smallvec::SmallVec; +use super::{LengthPercentage, LengthPercentageOrAuto}; + /// A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -35,22 +42,7 @@ pub enum AnimationName<'i> { Ident(CustomIdent<'i>), /// A `` name of a `@keyframes` rule. #[cfg_attr(feature = "serde", serde(borrow))] - String(CowArcStr<'i>), -} - -impl<'i> Parse<'i> for AnimationName<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { - return Ok(AnimationName::None); - } - - if let Ok(s) = input.try_parse(|input| input.expect_string_cloned()) { - return Ok(AnimationName::String(s.into())); - } - - let ident = CustomIdent::parse(input)?; - Ok(AnimationName::Ident(ident)) - } + String(CSSString<'i>), } impl<'i> ToCss for AnimationName<'i> { @@ -58,17 +50,24 @@ impl<'i> ToCss for AnimationName<'i> { where W: std::fmt::Write, { + let css_module_animation_enabled = + dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation); + match self { AnimationName::None => dest.write_str("none"), AnimationName::Ident(s) => { - if let Some(css_module) = &mut dest.css_module { - css_module.reference(&s.0, dest.loc.source_index) + if css_module_animation_enabled { + if let Some(css_module) = &mut dest.css_module { + css_module.reference(&s.0, dest.loc.source_index) + } } - s.to_css(dest) + s.to_css_with_options(dest, css_module_animation_enabled) } AnimationName::String(s) => { - if let Some(css_module) = &mut dest.css_module { - css_module.reference(&s, dest.loc.source_index) + if css_module_animation_enabled { + if let Some(css_module) = &mut dest.css_module { + css_module.reference(&s, dest.loc.source_index) + } } // CSS-wide keywords and `none` cannot remove quotes. @@ -78,7 +77,7 @@ impl<'i> ToCss for AnimationName<'i> { Ok(()) }, _ => { - dest.write_ident(s.as_ref()) + dest.write_ident(s.as_ref(), css_module_animation_enabled) } } } @@ -90,7 +89,7 @@ impl<'i> ToCss for AnimationName<'i> { pub type AnimationNameList<'i> = SmallVec<[AnimationName<'i>; 1]>; /// A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -112,40 +111,17 @@ impl Default for AnimationIterationCount { } } -impl<'i> Parse<'i> for AnimationIterationCount { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("infinite")).is_ok() { - return Ok(AnimationIterationCount::Infinite); - } - - let number = CSSNumber::parse(input)?; - return Ok(AnimationIterationCount::Number(number)); - } -} - -impl ToCss for AnimationIterationCount { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - AnimationIterationCount::Number(val) => val.to_css(dest), - AnimationIterationCount::Infinite => dest.write_str("infinite"), - } - } -} - enum_property! { /// A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property. pub enum AnimationDirection { /// The animation is played as specified - "normal": Normal, + Normal, /// The animation is played in reverse. - "reverse": Reverse, + Reverse, /// The animation iterations alternate between forward and reverse. - "alternate": Alternate, + Alternate, /// The animation iterations alternate between forward and reverse, with reverse occurring first. - "alternate-reverse": AlternateReverse, + AlternateReverse, } } @@ -191,6 +167,420 @@ impl Default for AnimationFillMode { } } +enum_property! { + /// A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property. + pub enum AnimationComposition { + /// The result of compositing the effect value with the underlying value is simply the effect value. + Replace, + /// The effect value is added to the underlying value. + Add, + /// The effect value is accumulated onto the underlying value. + Accumulate, + } +} + +/// A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property. +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type", content = "value", rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub enum AnimationTimeline<'i> { + /// The animation’s timeline is a DocumentTimeline, more specifically the default document timeline. + Auto, + /// The animation is not associated with a timeline. + None, + /// A timeline referenced by name. + #[cfg_attr(feature = "serde", serde(borrow))] + DashedIdent(DashedIdent<'i>), + /// The scroll() function. + Scroll(ScrollTimeline), + /// The view() function. + View(ViewTimeline), +} + +impl<'i> Default for AnimationTimeline<'i> { + fn default() -> Self { + AnimationTimeline::Auto + } +} + +/// The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function. +#[derive(Debug, Clone, PartialEq)] +#[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 ScrollTimeline { + /// Specifies which element to use as the scroll container. + pub scroller: Scroller, + /// Specifies which axis of the scroll container to use as the progress for the timeline. + pub axis: ScrollAxis, +} + +impl<'i> Parse<'i> for ScrollTimeline { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + input.expect_function_matching("scroll")?; + input.parse_nested_block(|input| { + let mut scroller = None; + let mut axis = None; + loop { + if scroller.is_none() { + scroller = input.try_parse(Scroller::parse).ok(); + } + + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + if axis.is_some() { + continue; + } + } + break; + } + + Ok(ScrollTimeline { + scroller: scroller.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + }) + } +} + +impl ToCss for ScrollTimeline { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + dest.write_str("scroll(")?; + + let mut needs_space = false; + if self.scroller != Scroller::default() { + self.scroller.to_css(dest)?; + needs_space = true; + } + + if self.axis != ScrollAxis::default() { + if needs_space { + dest.write_char(' ')?; + } + self.axis.to_css(dest)?; + } + + dest.write_char(')') + } +} + +enum_property! { + /// A scroller, used in the `scroll()` function. + pub enum Scroller { + /// Specifies to use the document viewport as the scroll container. + "root": Root, + /// Specifies to use the nearest ancestor scroll container. + "nearest": Nearest, + /// Specifies to use the element’s own principal box as the scroll container. + "self": SelfElement, + } +} + +impl Default for Scroller { + fn default() -> Self { + Scroller::Nearest + } +} + +enum_property! { + /// A scroll axis, used in the `scroll()` function. + pub enum ScrollAxis { + /// Specifies to use the measure of progress along the block axis of the scroll container. + Block, + /// Specifies to use the measure of progress along the inline axis of the scroll container. + Inline, + /// Specifies to use the measure of progress along the horizontal axis of the scroll container. + X, + /// Specifies to use the measure of progress along the vertical axis of the scroll container. + Y, + } +} + +impl Default for ScrollAxis { + fn default() -> Self { + ScrollAxis::Block + } +} + +/// The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function. +#[derive(Debug, Clone, PartialEq)] +#[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 ViewTimeline { + /// Specifies which axis of the scroll container to use as the progress for the timeline. + pub axis: ScrollAxis, + /// Provides an adjustment of the view progress visibility range. + pub inset: Size2D, +} + +impl<'i> Parse<'i> for ViewTimeline { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + input.expect_function_matching("view")?; + input.parse_nested_block(|input| { + let mut axis = None; + let mut inset = None; + loop { + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + } + + if inset.is_none() { + inset = input.try_parse(Size2D::parse).ok(); + if inset.is_some() { + continue; + } + } + break; + } + + Ok(ViewTimeline { + axis: axis.unwrap_or_default(), + inset: inset.unwrap_or(Size2D(LengthPercentageOrAuto::Auto, LengthPercentageOrAuto::Auto)), + }) + }) + } +} + +impl ToCss for ViewTimeline { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + dest.write_str("view(")?; + let mut needs_space = false; + if self.axis != ScrollAxis::default() { + self.axis.to_css(dest)?; + needs_space = true; + } + + if self.inset.0 != LengthPercentageOrAuto::Auto || self.inset.1 != LengthPercentageOrAuto::Auto { + if needs_space { + dest.write_char(' ')?; + } + self.inset.to_css(dest)?; + } + + dest.write_char(')') + } +} + +/// A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges) +#[derive(Debug, Clone, PartialEq, 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 TimelineRangeName { + /// Represents the full range of the view progress timeline. + Cover, + /// Represents the range during which the principal box is either fully contained by, + /// or fully covers, its view progress visibility range within the scrollport. + Contain, + /// Represents the range during which the principal box is entering the view progress visibility range. + Entry, + /// Represents the range during which the principal box is exiting the view progress visibility range. + Exit, + /// Represents the range during which the principal box crosses the end border edge. + EntryCrossing, + /// Represents the range during which the principal box crosses the start border edge. + ExitCrossing, +} + +/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) +/// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. +#[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 = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub enum AnimationAttachmentRange { + /// The start of the animation’s attachment range is the start of its associated timeline. + Normal, + /// The animation attachment range starts at the specified point on the timeline measuring from the start of the timeline. + #[cfg_attr(feature = "serde", serde(untagged))] + LengthPercentage(LengthPercentage), + /// The animation attachment range starts at the specified point on the timeline measuring from the start of the specified named timeline range. + #[cfg_attr(feature = "serde", serde(untagged))] + TimelineRange { + /// The name of the timeline range. + name: TimelineRangeName, + /// The offset from the start of the named timeline range. + offset: LengthPercentage, + }, +} + +impl<'i> AnimationAttachmentRange { + fn parse<'t>(input: &mut Parser<'i, 't>, default: f32) -> Result<'i, ParserError<'i>>> { + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + return Ok(AnimationAttachmentRange::Normal); + } + + if let Ok(val) = input.try_parse(LengthPercentage::parse) { + return Ok(AnimationAttachmentRange::LengthPercentage(val)); + } + + let name = TimelineRangeName::parse(input)?; + let offset = input + .try_parse(LengthPercentage::parse) + .unwrap_or(LengthPercentage::Percentage(Percentage(default))); + Ok(AnimationAttachmentRange::TimelineRange { name, offset }) + } + + fn to_css(&self, dest: &mut Printer, default: f32) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match self { + Self::Normal => dest.write_str("normal"), + Self::LengthPercentage(val) => val.to_css(dest), + Self::TimelineRange { name, offset } => { + name.to_css(dest)?; + if *offset != LengthPercentage::Percentage(Percentage(default)) { + dest.write_char(' ')?; + offset.to_css(dest)?; + } + Ok(()) + } + } + } +} + +impl Default for AnimationAttachmentRange { + fn default() -> Self { + AnimationAttachmentRange::Normal + } +} + +/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. +#[derive(Debug, Clone, PartialEq)] +#[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 AnimationRangeStart(pub AnimationAttachmentRange); + +impl<'i> Parse<'i> for AnimationRangeStart { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + let range = AnimationAttachmentRange::parse(input, 0.0)?; + Ok(Self(range)) + } +} + +impl ToCss for AnimationRangeStart { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + self.0.to_css(dest, 0.0) + } +} + +/// A value for the [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))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub struct AnimationRangeEnd(pub AnimationAttachmentRange); + +impl<'i> Parse<'i> for AnimationRangeEnd { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + let range = AnimationAttachmentRange::parse(input, 1.0)?; + Ok(Self(range)) + } +} + +impl ToCss for AnimationRangeEnd { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + self.0.to_css(dest, 1.0) + } +} + +/// A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property. +#[derive(Debug, Clone, PartialEq)] +#[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 AnimationRange { + /// The start of the animation's attachment range. + pub start: AnimationRangeStart, + /// The end of the animation's attachment range. + pub end: AnimationRangeEnd, +} + +impl<'i> Parse<'i> for AnimationRange { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + let start = AnimationRangeStart::parse(input)?; + let end = input + .try_parse(AnimationRangeStart::parse) + .map(|r| AnimationRangeEnd(r.0)) + .unwrap_or_else(|_| { + // If <'animation-range-end'> is omitted and <'animation-range-start'> includes a component, then + // animation-range-end is set to that same and 100%. Otherwise, any omitted longhand is set to its initial value. + match &start.0 { + AnimationAttachmentRange::TimelineRange { name, .. } => { + AnimationRangeEnd(AnimationAttachmentRange::TimelineRange { + name: name.clone(), + offset: LengthPercentage::Percentage(Percentage(1.0)), + }) + } + _ => AnimationRangeEnd(AnimationAttachmentRange::default()), + } + }); + Ok(AnimationRange { start, end }) + } +} + +impl ToCss for AnimationRange { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + self.start.to_css(dest)?; + + let omit_end = match (&self.start.0, &self.end.0) { + ( + AnimationAttachmentRange::TimelineRange { name: start_name, .. }, + AnimationAttachmentRange::TimelineRange { + name: end_name, + offset: end_offset, + }, + ) => start_name == end_name && *end_offset == LengthPercentage::Percentage(Percentage(1.0)), + (_, end) => *end == AnimationAttachmentRange::default(), + }; + + if !omit_end { + dest.write_char(' ')?; + self.end.to_css(dest)?; + } + Ok(()) + } +} + define_list_shorthand! { /// A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property. pub struct Animation<'i>(VendorPrefix) { @@ -211,6 +601,8 @@ define_list_shorthand! { delay: AnimationDelay(Time, VendorPrefix), /// The animation fill mode. fill_mode: AnimationFillMode(AnimationFillMode, VendorPrefix), + /// The animation timeline. + timeline: AnimationTimeline(AnimationTimeline<'i>), } } @@ -224,6 +616,7 @@ impl<'i> Parse<'i> for Animation<'i> { let mut play_state = None; let mut delay = None; let mut fill_mode = None; + let mut timeline = None; macro_rules! parse_prop { ($var: ident, $type: ident) => { @@ -245,6 +638,7 @@ impl<'i> Parse<'i> for Animation<'i> { parse_prop!(fill_mode, AnimationFillMode); parse_prop!(play_state, AnimationPlayState); parse_prop!(name, AnimationName); + parse_prop!(timeline, AnimationTimeline); break; } @@ -257,6 +651,7 @@ impl<'i> Parse<'i> for Animation<'i> { play_state: play_state.unwrap_or(AnimationPlayState::Running), delay: delay.unwrap_or(Time::Seconds(0.0)), fill_mode: fill_mode.unwrap_or(AnimationFillMode::None), + timeline: timeline.unwrap_or(AnimationTimeline::Auto), }) } } @@ -268,7 +663,7 @@ impl<'i> ToCss for Animation<'i> { { match &self.name { AnimationName::None => {} - AnimationName::Ident(CustomIdent(name)) | AnimationName::String(name) => { + AnimationName::Ident(CustomIdent(name)) | AnimationName::String(CSSString(name)) => { if !self.duration.is_zero() || !self.delay.is_zero() { self.duration.to_css(dest)?; dest.write_char(' ')?; @@ -312,6 +707,11 @@ impl<'i> ToCss for Animation<'i> { // Chrome does not yet support strings, however. self.name.to_css(dest)?; + if self.name != AnimationName::None && self.timeline != AnimationTimeline::default() { + dest.write_char(' ')?; + self.timeline.to_css(dest)?; + } + Ok(()) } } @@ -329,6 +729,9 @@ pub(crate) struct AnimationHandler<'i> { play_states: Option<(SmallVec<[AnimationPlayState; 1]>, VendorPrefix)>, delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>, fill_modes: Option<(SmallVec<[AnimationFillMode; 1]>, VendorPrefix)>, + timelines: Option<[AnimationTimeline<'i>; 1]>>, + range_starts: Option<[AnimationRangeStart; 1]>>, + range_ends: Option<[AnimationRangeEnd; 1]>>, has_any: bool, } @@ -339,8 +742,6 @@ impl<'i> PropertyHandler<'i> for AnimationHandler<'i> { dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>, ) -> bool { - use Property::*; - macro_rules! maybe_flush { ($prop: ident, $val: expr, $vp: ident) => {{ // If two vendor prefixes for the same property have different @@ -369,15 +770,32 @@ impl<'i> PropertyHandler<'i> for AnimationHandler<'i> { } match property { - AnimationName(val, vp) => property!(names, val, vp), - AnimationDuration(val, vp) => property!(durations, val, vp), - AnimationTimingFunction(val, vp) => property!(timing_functions, val, vp), - AnimationIterationCount(val, vp) => property!(iteration_counts, val, vp), - AnimationDirection(val, vp) => property!(directions, val, vp), - AnimationPlayState(val, vp) => property!(play_states, val, vp), - AnimationDelay(val, vp) => property!(delays, val, vp), - AnimationFillMode(val, vp) => property!(fill_modes, val, vp), - Animation(val, vp) => { + Property::AnimationName(val, vp) => property!(names, val, vp), + Property::AnimationDuration(val, vp) => property!(durations, val, vp), + Property::AnimationTimingFunction(val, vp) => property!(timing_functions, val, vp), + Property::AnimationIterationCount(val, vp) => property!(iteration_counts, val, vp), + Property::AnimationDirection(val, vp) => property!(directions, val, vp), + Property::AnimationPlayState(val, vp) => property!(play_states, val, vp), + Property::AnimationDelay(val, vp) => property!(delays, val, vp), + Property::AnimationFillMode(val, vp) => property!(fill_modes, val, vp), + Property::AnimationTimeline(val) => { + self.timelines = Some(val.clone()); + self.has_any = true; + } + Property::AnimationRangeStart(val) => { + self.range_starts = Some(val.clone()); + self.has_any = true; + } + Property::AnimationRangeEnd(val) => { + self.range_ends = Some(val.clone()); + self.has_any = true; + } + Property::AnimationRange(val) => { + self.range_starts = Some(val.iter().map(|v| v.start.clone()).collect()); + self.range_ends = Some(val.iter().map(|v| v.end.clone()).collect()); + self.has_any = true; + } + Property::Animation(val, vp) => { let names = val.iter().map(|b| b.name.clone()).collect(); maybe_flush!(names, &names, vp); @@ -402,6 +820,8 @@ impl<'i> PropertyHandler<'i> for AnimationHandler<'i> { let fill_modes = val.iter().map(|b| b.fill_mode.clone()).collect(); maybe_flush!(fill_modes, &fill_modes, vp); + self.timelines = Some(val.iter().map(|b| b.timeline.clone()).collect()); + property!(names, &names, vp); property!(durations, &durations, vp); property!(timing_functions, &timing_functions, vp); @@ -410,8 +830,43 @@ impl<'i> PropertyHandler<'i> for AnimationHandler<'i> { property!(play_states, &play_states, vp); property!(delays, &delays, vp); property!(fill_modes, &fill_modes, vp); + + // The animation shorthand resets animation-range + // https://drafts.csswg.org/scroll-animations/#named-range-animation-declaration + self.range_starts = None; + self.range_ends = None; } - Unparsed(val) if is_animation_property(&val.property_id) => { + Property::Unparsed(val) if is_animation_property(&val.property_id) => { + let mut val = Cow::Borrowed(val); + if matches!(val.property_id, PropertyId::Animation(_)) { + use crate::properties::custom::Token; + + // Find an identifier that isn't a keyword and replace it with an + // AnimationName token so it is scoped in CSS modules. + for token in &mut val.to_mut().value.0 { + match token { + TokenOrValue::Token(Token::Ident(id)) => { + if AnimationDirection::parse_string(&id).is_err() + && AnimationPlayState::parse_string(&id).is_err() + && AnimationFillMode::parse_string(&id).is_err() + && !EasingFunction::is_ident(&id) + && id.as_ref() != "infinite" + && id.as_ref() != "auto" + { + *token = TokenOrValue::AnimationName(AnimationName::Ident(CustomIdent(id.clone()))); + } + } + TokenOrValue::Token(Token::String(s)) => { + *token = TokenOrValue::AnimationName(AnimationName::String(CSSString(s.clone()))); + } + _ => {} + } + } + + self.range_starts = None; + self.range_ends = None; + } + self.flush(dest, context); dest.push(Property::Unparsed( val.get_prefixed(context.targets, Feature::Animation), @@ -444,6 +899,9 @@ impl<'i> AnimationHandler<'i> { let mut play_states = std::mem::take(&mut self.play_states); let mut delays = std::mem::take(&mut self.delays); let mut fill_modes = std::mem::take(&mut self.fill_modes); + let mut timelines_value = std::mem::take(&mut self.timelines); + let range_starts = std::mem::take(&mut self.range_starts); + let range_ends = std::mem::take(&mut self.range_ends); if let ( Some((names, names_vp)), @@ -474,6 +932,15 @@ impl<'i> AnimationHandler<'i> { & *play_states_vp & *delays_vp & *fill_modes_vp; + let mut timelines = if let Some(timelines) = &mut timelines_value { + Cow::Borrowed(timelines) + } else if !intersection.contains(VendorPrefix::None) { + // Prefixed animation shorthand does not support animation-timeline + Cow::Owned(std::iter::repeat(AnimationTimeline::Auto).take(len).collect()) + } else { + Cow::Owned(SmallVec::new()) + }; + if !intersection.is_empty() && durations.len() == len && timing_functions.len() == len @@ -482,7 +949,19 @@ impl<'i> AnimationHandler<'i> { && play_states.len() == len && delays.len() == len && fill_modes.len() == len + && timelines.len() == len { + let timeline_property = if timelines.iter().any(|t| *t != AnimationTimeline::Auto) + && (intersection != VendorPrefix::None + || !context + .targets + .is_compatible(crate::compat::Feature::AnimationTimelineShorthand)) + { + Some(Property::AnimationTimeline(timelines.clone().into_owned())) + } else { + None + }; + let animations = izip!( names.drain(..), durations.drain(..), @@ -491,10 +970,21 @@ impl<'i> AnimationHandler<'i> { directions.drain(..), play_states.drain(..), delays.drain(..), - fill_modes.drain(..) + fill_modes.drain(..), + timelines.to_mut().drain(..) ) .map( - |(name, duration, timing_function, iteration_count, direction, play_state, delay, fill_mode)| { + |( + name, + duration, + timing_function, + iteration_count, + direction, + play_state, + delay, + fill_mode, + timeline, + )| { Animation { name, duration, @@ -504,6 +994,11 @@ impl<'i> AnimationHandler<'i> { play_state, delay, fill_mode, + timeline: if timeline_property.is_some() { + AnimationTimeline::Auto + } else { + timeline + }, } }, ) @@ -518,6 +1013,11 @@ impl<'i> AnimationHandler<'i> { play_states_vp.remove(intersection); delays_vp.remove(intersection); fill_modes_vp.remove(intersection); + + if let Some(p) = timeline_property { + dest.push(p); + } + timelines_value = None; } } @@ -540,6 +1040,36 @@ impl<'i> AnimationHandler<'i> { prop!(play_states, AnimationPlayState); prop!(delays, AnimationDelay); prop!(fill_modes, AnimationFillMode); + + if let Some(val) = timelines_value { + dest.push(Property::AnimationTimeline(val)); + } + + match (range_starts, range_ends) { + (Some(range_starts), Some(range_ends)) => { + if range_starts.len() == range_ends.len() { + dest.push(Property::AnimationRange( + range_starts + .into_iter() + .zip(range_ends.into_iter()) + .map(|(start, end)| AnimationRange { start, end }) + .collect(), + )); + } else { + dest.push(Property::AnimationRangeStart(range_starts)); + dest.push(Property::AnimationRangeEnd(range_ends)); + } + } + (range_starts, range_ends) => { + if let Some(range_starts) = range_starts { + dest.push(Property::AnimationRangeStart(range_starts)); + } + + if let Some(range_ends) = range_ends { + dest.push(Property::AnimationRangeEnd(range_ends)); + } + } + } } } @@ -554,6 +1084,11 @@ fn is_animation_property(property_id: &PropertyId) -> bool { | PropertyId::AnimationPlayState(_) | PropertyId::AnimationDelay(_) | PropertyId::AnimationFillMode(_) + | PropertyId::AnimationComposition + | PropertyId::AnimationTimeline + | PropertyId::AnimationRange + | PropertyId::AnimationRangeStart + | PropertyId::AnimationRangeEnd | PropertyId::Animation(_) => true, _ => false, } diff --git a/src/properties/background.rs b/src/properties/background.rs index 1dc1aeb39..f47821db7 100644 --- a/src/properties/background.rs +++ b/src/properties/background.rs @@ -113,13 +113,13 @@ enum_property! { /// See [BackgroundRepeat](BackgroundRepeat). pub enum BackgroundRepeatKeyword { /// The image is repeated in this direction. - "repeat": Repeat, + Repeat, /// The image is repeated so that it fits, and then spaced apart evenly. - "space": Space, + Space, /// The image is scaled so that it repeats an even number of times. - "round": Round, + Round, /// The image is placed once and not repeated in this direction. - "no-repeat": NoRepeat, + NoRepeat, } } @@ -214,11 +214,11 @@ enum_property! { /// A value for the [background-origin](https://www.w3.org/TR/css-backgrounds-3/#background-origin) property. pub enum BackgroundOrigin { /// The position is relative to the border box. - "border-box": BorderBox, + BorderBox, /// The position is relative to the padding box. - "padding-box": PaddingBox, + PaddingBox, /// The position is relative to the content box. - "content-box": ContentBox, + ContentBox, } } @@ -226,15 +226,15 @@ enum_property! { /// A value for the [background-clip](https://drafts.csswg.org/css-backgrounds-4/#background-clip) property. pub enum BackgroundClip { /// The background is clipped to the border box. - "border-box": BorderBox, + BorderBox, /// The background is clipped to the padding box. - "padding-box": PaddingBox, + PaddingBox, /// The background is clipped to the content box. - "content-box": ContentBox, + ContentBox, /// The background is clipped to the area painted by the border. - "border": Border, + Border, /// The background is clipped to the text content of the element. - "text": Text, + Text, } } diff --git a/src/properties/border.rs b/src/properties/border.rs index ee2c41027..9308eebb7 100644 --- a/src/properties/border.rs +++ b/src/properties/border.rs @@ -23,7 +23,7 @@ use crate::visitor::Visit; use cssparser::*; /// A value for the [border-width](https://www.w3.org/TR/css-backgrounds-3/#border-width) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -58,37 +58,6 @@ impl IsCompatible for BorderSideWidth { } } -impl<'i> Parse<'i> for BorderSideWidth { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(length) = input.try_parse(|i| Length::parse(i)) { - return Ok(BorderSideWidth::Length(length)); - } - let location = input.current_source_location(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &ident, - "thin" => Ok(BorderSideWidth::Thin), - "medium" => Ok(BorderSideWidth::Medium), - "thick" => Ok(BorderSideWidth::Thick), - _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))) - } - } -} - -impl ToCss for BorderSideWidth { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - use BorderSideWidth::*; - match self { - Thin => dest.write_str("thin"), - Medium => dest.write_str("medium"), - Thick => dest.write_str("thick"), - Length(length) => length.to_css(dest), - } - } -} - enum_property! { /// A [``](https://drafts.csswg.org/css-backgrounds/#typedef-line-style) value, used in the `border-style` property. pub enum LineStyle { diff --git a/src/properties/border_image.rs b/src/properties/border_image.rs index 8f444efe4..300d458e0 100644 --- a/src/properties/border_image.rs +++ b/src/properties/border_image.rs @@ -102,7 +102,7 @@ impl IsCompatible for BorderImageRepeat { } /// A value for the [border-image-width](https://www.w3.org/TR/css-backgrounds-3/#border-image-width) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -126,38 +126,6 @@ impl Default for BorderImageSideWidth { } } -impl<'i> Parse<'i> for BorderImageSideWidth { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { - return Ok(BorderImageSideWidth::Auto); - } - - if let Ok(number) = input.try_parse(CSSNumber::parse) { - return Ok(BorderImageSideWidth::Number(number)); - } - - if let Ok(percent) = input.try_parse(|input| LengthPercentage::parse(input)) { - return Ok(BorderImageSideWidth::LengthPercentage(percent)); - } - - Err(input.new_error_for_next_token()) - } -} - -impl ToCss for BorderImageSideWidth { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - use BorderImageSideWidth::*; - match self { - Auto => dest.write_str("auto"), - LengthPercentage(l) => l.to_css(dest), - Number(n) => n.to_css(dest), - } - } -} - impl IsCompatible for BorderImageSideWidth { fn is_compatible(&self, browsers: Browsers) -> bool { match self { diff --git a/src/properties/box_shadow.rs b/src/properties/box_shadow.rs index 0d757cafb..663761654 100644 --- a/src/properties/box_shadow.rs +++ b/src/properties/box_shadow.rs @@ -208,7 +208,7 @@ impl BoxShadowHandler { let rgb = box_shadows .iter() .map(|shadow| BoxShadow { - color: shadow.color.to_rgb().unwrap(), + color: shadow.color.to_rgb().unwrap_or_else(|_| shadow.color.clone()), ..shadow.clone() }) .collect(); @@ -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(); @@ -236,7 +236,7 @@ impl BoxShadowHandler { let lab = box_shadows .iter() .map(|shadow| BoxShadow { - color: shadow.color.to_lab().unwrap(), + color: shadow.color.to_lab().unwrap_or_else(|_| shadow.color.clone()), ..shadow.clone() }) .collect(); diff --git a/src/properties/contain.rs b/src/properties/contain.rs index 761e2d925..27ad2bb95 100644 --- a/src/properties/contain.rs +++ b/src/properties/contain.rs @@ -25,11 +25,13 @@ enum_property! { pub enum ContainerType { /// The element is not a query container for any container size queries, /// but remains a query container for container style queries. - "normal": Normal, + Normal, /// Establishes a query container for container size queries on the container’s own inline axis. - "inline-size": InlineSize, + InlineSize, /// Establishes a query container for container size queries on both the inline and block axis. - "size": Size, + Size, + /// Establishes a query container for container scroll-state queries + ScrollState, } } diff --git a/src/properties/custom.rs b/src/properties/custom.rs index f93b4005a..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}; @@ -27,6 +27,7 @@ use crate::visitor::Visit; use cssparser::color::parse_hash_color; use cssparser::*; +use super::AnimationName; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; @@ -241,6 +242,8 @@ pub enum TokenOrValue<'i> { Resolution(Resolution), /// A dashed ident. DashedIdent(DashedIdent<'i>), + /// An animation name. + AnimationName(AnimationName<'i>), } impl<'i> From<'i>> for TokenOrValue<'i> { @@ -367,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)) => { @@ -428,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) @@ -456,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) { @@ -472,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 { @@ -482,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, } @@ -529,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() { @@ -554,73 +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)?; } 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(..)) } }, }; @@ -650,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(_)))) } } @@ -979,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)?, @@ -1074,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 { @@ -1169,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")))] @@ -1258,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(')') @@ -1322,25 +1282,25 @@ enum_property! { /// A UA-defined environment variable name. pub enum UAEnvironmentVariable { /// The safe area inset from the top of the viewport. - "safe-area-inset-top": SafeAreaInsetTop, + SafeAreaInsetTop, /// The safe area inset from the right of the viewport. - "safe-area-inset-right": SafeAreaInsetRight, + SafeAreaInsetRight, /// The safe area inset from the bottom of the viewport. - "safe-area-inset-bottom": SafeAreaInsetBottom, + SafeAreaInsetBottom, /// The safe area inset from the left of the viewport. - "safe-area-inset-left": SafeAreaInsetLeft, + SafeAreaInsetLeft, /// The viewport segment width. - "viewport-segment-width": ViewportSegmentWidth, + ViewportSegmentWidth, /// The viewport segment height. - "viewport-segment-height": ViewportSegmentHeight, + ViewportSegmentHeight, /// The viewport segment top position. - "viewport-segment-top": ViewportSegmentTop, + ViewportSegmentTop, /// The viewport segment left position. - "viewport-segment-left": ViewportSegmentLeft, + ViewportSegmentLeft, /// The viewport segment bottom position. - "viewport-segment-bottom": ViewportSegmentBottom, + ViewportSegmentBottom, /// The viewport segment right position. - "viewport-segment-right": ViewportSegmentRight, + ViewportSegmentRight, } } @@ -1432,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(')') @@ -1549,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)) @@ -1591,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(')')?; @@ -1612,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(')')?; @@ -1638,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/display.rs b/src/properties/display.rs index d5a53d137..3df7c8193 100644 --- a/src/properties/display.rs +++ b/src/properties/display.rs @@ -18,9 +18,9 @@ enum_property! { /// A [``](https://drafts.csswg.org/css-display-3/#typedef-display-outside) value. #[allow(missing_docs)] pub enum DisplayOutside { - "block": Block, - "inline": Inline, - "run-in": RunIn, + Block, + Inline, + RunIn, } } @@ -309,25 +309,25 @@ enum_property! { /// See [Display](Display). #[allow(missing_docs)] pub enum DisplayKeyword { - "none": None, - "contents": Contents, - "table-row-group": TableRowGroup, - "table-header-group": TableHeaderGroup, - "table-footer-group": TableFooterGroup, - "table-row": TableRow, - "table-cell": TableCell, - "table-column-group": TableColumnGroup, - "table-column": TableColumn, - "table-caption": TableCaption, - "ruby-base": RubyBase, - "ruby-text": RubyText, - "ruby-base-container": RubyBaseContainer, - "ruby-text-container": RubyTextContainer, + None, + Contents, + TableRowGroup, + TableHeaderGroup, + TableFooterGroup, + TableRow, + TableCell, + TableColumnGroup, + TableColumn, + TableCaption, + RubyBase, + RubyText, + RubyBaseContainer, + RubyTextContainer, } } /// A value for the [display](https://drafts.csswg.org/css-display-3/#the-display-properties) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -347,29 +347,6 @@ pub enum Display { Pair(DisplayPair), } -impl<'i> Parse<'i> for Display { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(pair) = input.try_parse(DisplayPair::parse) { - return Ok(Display::Pair(pair)); - } - - let keyword = DisplayKeyword::parse(input)?; - Ok(Display::Keyword(keyword)) - } -} - -impl ToCss for Display { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - Display::Keyword(keyword) => keyword.to_css(dest), - Display::Pair(pair) => pair.to_css(dest), - } - } -} - enum_property! { /// A value for the [visibility](https://drafts.csswg.org/css-display-3/#visibility) property. pub enum Visibility { 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/flex.rs b/src/properties/flex.rs index cf631993f..e885c5975 100644 --- a/src/properties/flex.rs +++ b/src/properties/flex.rs @@ -25,13 +25,13 @@ enum_property! { /// A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property. pub enum FlexDirection { /// Flex items are laid out in a row. - "row": Row, + Row, /// Flex items are laid out in a row, and reversed. - "row-reverse": RowReverse, + RowReverse, /// Flex items are laid out in a column. - "column": Column, + Column, /// Flex items are laid out in a column, and reversed. - "column-reverse": ColumnReverse, + ColumnReverse, } } @@ -233,13 +233,13 @@ enum_property! { /// Partially equivalent to `flex-direction` in the standard syntax. pub enum BoxOrient { /// Items are laid out horizontally. - "horizontal": Horizontal, + Horizontal, /// Items are laid out vertically. - "vertical": Vertical, + Vertical, /// Items are laid out along the inline axis, according to the writing direction. - "inline-axis": InlineAxis, + InlineAxis, /// Items are laid out along the block axis, according to the writing direction. - "block-axis": BlockAxis, + BlockAxis, } } diff --git a/src/properties/font.rs b/src/properties/font.rs index 84396cdb7..8b3031a9f 100644 --- a/src/properties/font.rs +++ b/src/properties/font.rs @@ -20,7 +20,7 @@ use crate::visitor::Visit; use cssparser::*; /// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -44,38 +44,6 @@ impl Default for FontWeight { } } -impl<'i> Parse<'i> for FontWeight { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(val) = input.try_parse(AbsoluteFontWeight::parse) { - return Ok(FontWeight::Absolute(val)); - } - - let location = input.current_source_location(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &*ident, - "bolder" => Ok(FontWeight::Bolder), - "lighter" => Ok(FontWeight::Lighter), - _ => Err(location.new_unexpected_token_error( - cssparser::Token::Ident(ident.clone()) - )) - } - } -} - -impl ToCss for FontWeight { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - use FontWeight::*; - match self { - Absolute(val) => val.to_css(dest), - Bolder => dest.write_str("bolder"), - Lighter => dest.write_str("lighter"), - } - } -} - impl IsCompatible for FontWeight { fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { match self { @@ -89,7 +57,7 @@ impl IsCompatible for FontWeight { /// as used in the `font-weight` property. /// /// See [FontWeight](FontWeight). -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -112,24 +80,6 @@ impl Default for AbsoluteFontWeight { } } -impl<'i> Parse<'i> for AbsoluteFontWeight { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(val) = input.try_parse(CSSNumber::parse) { - return Ok(AbsoluteFontWeight::Weight(val)); - } - - let location = input.current_source_location(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &*ident, - "normal" => Ok(AbsoluteFontWeight::Normal), - "bold" => Ok(AbsoluteFontWeight::Bold), - _ => Err(location.new_unexpected_token_error( - cssparser::Token::Ident(ident.clone()) - )) - } - } -} - impl ToCss for AbsoluteFontWeight { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where @@ -197,7 +147,7 @@ enum_property! { } /// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -215,35 +165,6 @@ pub enum FontSize { Relative(RelativeFontSize), } -impl<'i> Parse<'i> for FontSize { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(val) = input.try_parse(LengthPercentage::parse) { - return Ok(FontSize::Length(val)); - } - - if let Ok(val) = input.try_parse(AbsoluteFontSize::parse) { - return Ok(FontSize::Absolute(val)); - } - - let val = RelativeFontSize::parse(input)?; - Ok(FontSize::Relative(val)) - } -} - -impl ToCss for FontSize { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - use FontSize::*; - match self { - Absolute(val) => val.to_css(dest), - Length(val) => val.to_css(dest), - Relative(val) => val.to_css(dest), - } - } -} - impl IsCompatible for FontSize { fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { match self { @@ -309,7 +230,7 @@ impl Into for &FontStretchKeyword { } /// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -331,17 +252,6 @@ impl Default for FontStretch { } } -impl<'i> Parse<'i> for FontStretch { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(val) = input.try_parse(Percentage::parse) { - return Ok(FontStretch::Percentage(val)); - } - - let keyword = FontStretchKeyword::parse(input)?; - Ok(FontStretch::Keyword(keyword)) - } -} - impl Into for &FontStretch { fn into(self) -> Percentage { match self { @@ -438,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()) { @@ -470,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(()) } } @@ -601,19 +541,19 @@ enum_property! { /// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property. pub enum FontVariantCaps { /// No special capitalization features are applied. - "normal": Normal, + Normal, /// The small capitals feature is used for lower case letters. - "small-caps": SmallCaps, + SmallCaps, /// Small capitals are used for both upper and lower case letters. - "all-small-caps": AllSmallCaps, + AllSmallCaps, /// Petite capitals are used. - "petite-caps": PetiteCaps, + PetiteCaps, /// Petite capitals are used for both upper and lower case letters. - "all-petite-caps": AllPetiteCaps, + AllPetiteCaps, /// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters. - "unicase": Unicase, + Unicase, /// Uses titling capitals. - "titling-caps": TitlingCaps, + TitlingCaps, } } @@ -644,7 +584,7 @@ impl IsCompatible for FontVariantCaps { } /// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -668,33 +608,6 @@ impl Default for LineHeight { } } -impl<'i> Parse<'i> for LineHeight { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { - return Ok(LineHeight::Normal); - } - - if let Ok(val) = input.try_parse(CSSNumber::parse) { - return Ok(LineHeight::Number(val)); - } - - Ok(LineHeight::Length(LengthPercentage::parse(input)?)) - } -} - -impl ToCss for LineHeight { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - LineHeight::Normal => dest.write_str("normal"), - LineHeight::Number(val) => val.to_css(dest), - LineHeight::Length(val) => val.to_css(dest), - } - } -} - impl IsCompatible for LineHeight { fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { match self { @@ -708,27 +621,27 @@ enum_property! { /// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. pub enum VerticalAlignKeyword { /// Align the baseline of the box with the baseline of the parent box. - "baseline": Baseline, + Baseline, /// Lower the baseline of the box to the proper position for subscripts of the parent’s box. - "sub": Sub, + Sub, /// Raise the baseline of the box to the proper position for superscripts of the parent’s box. - "super": Super, + Super, /// Align the top of the aligned subtree with the top of the line box. - "top": Top, + Top, /// Align the top of the box with the top of the parent’s content area. - "text-top": TextTop, + TextTop, /// Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. - "middle": Middle, + Middle, /// Align the bottom of the aligned subtree with the bottom of the line box. - "bottom": Bottom, + Bottom, /// Align the bottom of the box with the bottom of the parent’s content area. - "text-bottom": TextBottom, + TextBottom, } } /// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. // TODO: there is a more extensive spec in CSS3 but it doesn't seem any browser implements it? https://www.w3.org/TR/css-inline-3/#transverse-alignment -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -744,29 +657,6 @@ pub enum VerticalAlign { Length(LengthPercentage), } -impl<'i> Parse<'i> for VerticalAlign { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(len) = input.try_parse(LengthPercentage::parse) { - return Ok(VerticalAlign::Length(len)); - } - - let kw = VerticalAlignKeyword::parse(input)?; - Ok(VerticalAlign::Keyword(kw)) - } -} - -impl ToCss for VerticalAlign { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - VerticalAlign::Keyword(kw) => kw.to_css(dest), - VerticalAlign::Length(len) => len.to_css(dest), - } - } -} - define_shorthand! { /// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property. pub struct Font<'i> { @@ -1134,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 fa0e5a646..553cdc542 100644 --- a/src/properties/grid.rs +++ b/src/properties/grid.rs @@ -24,7 +24,7 @@ use crate::serialization::ValueWrapper; /// A [track sizing](https://drafts.csswg.org/css-grid-2/#track-sizing) value /// for the `grid-template-rows` and `grid-template-columns` properties. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -178,7 +178,7 @@ pub struct TrackRepeat<'i> { /// used in the `repeat()` function. /// /// See [TrackRepeat](TrackRepeat). -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -365,37 +365,6 @@ impl<'i> ToCss for TrackRepeat<'i> { } } -impl<'i> Parse<'i> for RepeatCount { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(num) = input.try_parse(CSSInteger::parse) { - return Ok(RepeatCount::Number(num)); - } - - let location = input.current_source_location(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &*ident, - "auto-fill" => Ok(RepeatCount::AutoFill), - "auto-fit" => Ok(RepeatCount::AutoFit), - _ => Err(location.new_unexpected_token_error( - cssparser::Token::Ident(ident.clone()) - )) - } - } -} - -impl ToCss for RepeatCount { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - RepeatCount::AutoFill => dest.write_str("auto-fill"), - RepeatCount::AutoFit => dest.write_str("auto-fit"), - RepeatCount::Number(num) => num.to_css(dest), - } - } -} - fn parse_line_names<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result<'i>, ParseError<'i, ParserError<'i>>> { @@ -430,21 +399,24 @@ fn write_ident(name: &str, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { - if let Some(css_module) = &mut dest.css_module { - if let Some(last) = css_module.config.pattern.segments.last() { - if !matches!(last, crate::css_modules::Segment::Local) { - return Err(Error { - kind: PrinterErrorKind::InvalidCssModulesPatternInGrid, - loc: Some(ErrorLocation { - filename: dest.filename().into(), - line: dest.loc.line, - column: dest.loc.column, - }), - }); + let css_module_grid_enabled = dest.css_module.as_ref().map_or(false, |css_module| css_module.config.grid); + if css_module_grid_enabled { + if let Some(css_module) = &mut dest.css_module { + if let Some(last) = css_module.config.pattern.segments.last() { + if !matches!(last, crate::css_modules::Segment::Local) { + return Err(Error { + kind: PrinterErrorKind::InvalidCssModulesPatternInGrid, + loc: Some(ErrorLocation { + filename: dest.filename().into(), + line: dest.loc.line, + column: dest.loc.column, + }), + }); + } } } } - dest.write_ident(name)?; + dest.write_ident(name, css_module_grid_enabled)?; Ok(()) } @@ -516,29 +488,6 @@ impl<'i> TrackList<'i> { } } -impl<'i> Parse<'i> for TrackSizing<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { - return Ok(TrackSizing::None); - } - - let track_list = TrackList::parse(input)?; - Ok(TrackSizing::TrackList(track_list)) - } -} - -impl<'i> ToCss for TrackSizing<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - TrackSizing::None => dest.write_str("none"), - TrackSizing::TrackList(list) => list.to_css(dest), - } - } -} - impl<'i> TrackSizing<'i> { fn is_explicit(&self) -> bool { match self { @@ -584,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( @@ -755,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))] @@ -986,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))] @@ -1152,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))] @@ -1250,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) @@ -1307,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; @@ -1325,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], @@ -1512,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))] @@ -1522,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))] @@ -1537,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))] @@ -1735,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(), @@ -1748,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; @@ -1814,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 4112ddfe4..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}; @@ -15,7 +15,7 @@ use crate::visitor::Visit; use cssparser::*; /// A value for the [list-style-type](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#text-markers) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -40,34 +40,6 @@ impl Default for ListStyleType<'_> { } } -impl<'i> Parse<'i> for ListStyleType<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { - return Ok(ListStyleType::None); - } - - if let Ok(val) = input.try_parse(CounterStyle::parse) { - return Ok(ListStyleType::CounterStyle(val)); - } - - let s = CSSString::parse(input)?; - Ok(ListStyleType::String(s)) - } -} - -impl ToCss for ListStyleType<'_> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - ListStyleType::None => dest.write_str("none"), - ListStyleType::CounterStyle(style) => style.to_css(dest), - ListStyleType::String(s) => s.to_css(dest), - } - } -} - impl IsCompatible for ListStyleType<'_> { fn is_compatible(&self, browsers: Browsers) -> bool { match self { @@ -117,7 +89,7 @@ macro_rules! counter_styles { $vis:vis enum $name:ident { $( $(#[$meta: meta])* - $str: literal: $id: ident, + $id: ident, )+ } ) => { @@ -127,7 +99,7 @@ macro_rules! counter_styles { pub enum PredefinedCounterStyle { $( $(#[$meta])* - $str: $id, + $id, )+ } } @@ -136,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) }, )+ @@ -151,68 +123,68 @@ counter_styles! { #[allow(missing_docs)] pub enum PredefinedCounterStyle { // https://www.w3.org/TR/css-counter-styles-3/#simple-numeric - "decimal": Decimal, - "decimal-leading-zero": DecimalLeadingZero, - "arabic-indic": ArabicIndic, - "armenian": Armenian, - "upper-armenian": UpperArmenian, - "lower-armenian": LowerArmenian, - "bengali": Bengali, - "cambodian": Cambodian, - "khmer": Khmer, - "cjk-decimal": CjkDecimal, - "devanagari": Devanagari, - "georgian": Georgian, - "gujarati": Gujarati, - "gurmukhi": Gurmukhi, - "hebrew": Hebrew, - "kannada": Kannada, - "lao": Lao, - "malayalam": Malayalam, - "mongolian": Mongolian, - "myanmar": Myanmar, - "oriya": Oriya, - "persian": Persian, - "lower-roman": LowerRoman, - "upper-roman": UpperRoman, - "tamil": Tamil, - "telugu": Telugu, - "thai": Thai, - "tibetan": Tibetan, + Decimal, + DecimalLeadingZero, + ArabicIndic, + Armenian, + UpperArmenian, + LowerArmenian, + Bengali, + Cambodian, + Khmer, + CjkDecimal, + Devanagari, + Georgian, + Gujarati, + Gurmukhi, + Hebrew, + Kannada, + Lao, + Malayalam, + Mongolian, + Myanmar, + Oriya, + Persian, + LowerRoman, + UpperRoman, + Tamil, + Telugu, + Thai, + Tibetan, // https://www.w3.org/TR/css-counter-styles-3/#simple-alphabetic - "lower-alpha": LowerAlpha, - "lower-latin": LowerLatin, - "upper-alpha": UpperAlpha, - "upper-latin": UpperLatin, - "lower-greek": LowerGreek, - "hiragana": Hiragana, - "hiragana-iroha": HiraganaIroha, - "katakana": Katakana, - "katakana-iroha": KatakanaIroha, + LowerAlpha, + LowerLatin, + UpperAlpha, + UpperLatin, + LowerGreek, + Hiragana, + HiraganaIroha, + Katakana, + KatakanaIroha, // https://www.w3.org/TR/css-counter-styles-3/#simple-symbolic - "disc": Disc, - "circle": Circle, - "square": Square, - "disclosure-open": DisclosureOpen, - "disclosure-closed": DisclosureClosed, + Disc, + Circle, + Square, + DisclosureOpen, + DisclosureClosed, // https://www.w3.org/TR/css-counter-styles-3/#simple-fixed - "cjk-earthly-branch": CjkEarthlyBranch, - "cjk-heavenly-stem": CjkHeavenlyStem, + CjkEarthlyBranch, + CjkHeavenlyStem, // https://www.w3.org/TR/css-counter-styles-3/#complex-cjk - "japanese-informal": JapaneseInformal, - "japanese-formal": JapaneseFormal, - "korean-hangul-formal": KoreanHangulFormal, - "korean-hanja-informal": KoreanHanjaInformal, - "korean-hanja-formal": KoreanHanjaFormal, - "simp-chinese-informal": SimpChineseInformal, - "simp-chinese-formal": SimpChineseFormal, - "trad-chinese-informal": TradChineseInformal, - "trad-chinese-formal": TradChineseFormal, - "ethiopic-numeric": EthiopicNumeric, + JapaneseInformal, + JapaneseFormal, + KoreanHangulFormal, + KoreanHanjaInformal, + KoreanHanjaFormal, + SimpChineseInformal, + SimpChineseFormal, + TradChineseInformal, + TradChineseFormal, + EthiopicNumeric, } } @@ -309,7 +281,7 @@ impl Default for SymbolsType { /// `symbols()` function. /// /// See [CounterStyle](CounterStyle). -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -326,29 +298,6 @@ pub enum Symbol<'i> { Image(Image<'i>), } -impl<'i> Parse<'i> for Symbol<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(img) = input.try_parse(Image::parse) { - return Ok(Symbol::Image(img)); - } - - let s = CSSString::parse(input)?; - Ok(Symbol::String(s.into())) - } -} - -impl<'i> ToCss for Symbol<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - Symbol::String(s) => s.to_css(dest), - Symbol::Image(img) => img.to_css(dest), - } - } -} - enum_property! { /// A value for the [list-style-position](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-position-property) property. pub enum ListStylePosition { @@ -375,21 +324,125 @@ enum_property! { /// A value for the [marker-side](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#marker-side) property. #[allow(missing_docs)] pub enum MarkerSide { - "match-self": MatchSelf, - "match-parent": MatchParent, + MatchSelf, + MatchParent, } } -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/masking.rs b/src/properties/masking.rs index 35560c6e8..ff05e1f63 100644 --- a/src/properties/masking.rs +++ b/src/properties/masking.rs @@ -37,11 +37,11 @@ enum_property! { /// A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property. pub enum MaskMode { /// The luminance values of the mask image is used. - "luminance": Luminance, + Luminance, /// The alpha values of the mask image is used. - "alpha": Alpha, + Alpha, /// If an SVG source is used, the value matches the `mask-type` property. Otherwise, the alpha values are used. - "match-source": MatchSource, + MatchSource, } } @@ -58,11 +58,11 @@ enum_property! { /// See also [MaskMode](MaskMode). pub enum WebKitMaskSourceType { /// Equivalent to `match-source` in the standard `mask-mode` syntax. - "auto": Auto, + Auto, /// The luminance values of the mask image is used. - "luminance": Luminance, + Luminance, /// The alpha values of the mask image is used. - "alpha": Alpha, + Alpha, } } @@ -81,19 +81,19 @@ enum_property! { /// as used in the `mask-clip` and `clip-path` properties. pub enum GeometryBox { /// The painted content is clipped to the content box. - "border-box": BorderBox, + BorderBox, /// The painted content is clipped to the padding box. - "padding-box": PaddingBox, + PaddingBox, /// The painted content is clipped to the border box. - "content-box": ContentBox, + ContentBox, /// The painted content is clipped to the margin box. - "margin-box": MarginBox, + MarginBox, /// The painted content is clipped to the object bounding box. - "fill-box": FillBox, + FillBox, /// The painted content is clipped to the stroke bounding box. - "stroke-box": StrokeBox, + StrokeBox, /// Uses the nearest SVG viewport as reference box. - "view-box": ViewBox, + ViewBox, } } @@ -104,7 +104,7 @@ impl Default for GeometryBox { } /// A value for the [mask-clip](https://www.w3.org/TR/css-masking-1/#the-mask-clip) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -120,29 +120,6 @@ pub enum MaskClip { NoClip, } -impl<'i> Parse<'i> for MaskClip { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(b) = input.try_parse(GeometryBox::parse) { - return Ok(MaskClip::GeometryBox(b)); - } - - input.expect_ident_matching("no-clip")?; - Ok(MaskClip::NoClip) - } -} - -impl ToCss for MaskClip { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - MaskClip::GeometryBox(b) => b.to_css(dest), - MaskClip::NoClip => dest.write_str("no-clip"), - } - } -} - impl IsCompatible for MaskClip { fn is_compatible(&self, browsers: Browsers) -> bool { match self { @@ -191,21 +168,21 @@ enum_property! { /// See also [MaskComposite](MaskComposite). #[allow(missing_docs)] pub enum WebKitMaskComposite { - "clear": Clear, - "copy": Copy, + Clear, + Copy, /// Equivalent to `add` in the standard `mask-composite` syntax. - "source-over": SourceOver, + SourceOver, /// Equivalent to `intersect` in the standard `mask-composite` syntax. - "source-in": SourceIn, + SourceIn, /// Equivalent to `subtract` in the standard `mask-composite` syntax. - "source-out": SourceOut, - "source-atop": SourceAtop, - "destination-over": DestinationOver, - "destination-in": DestinationIn, - "destination-out": DestinationOut, - "destination-atop": DestinationAtop, + SourceOut, + SourceAtop, + DestinationOver, + DestinationIn, + DestinationOut, + DestinationAtop, /// Equivalent to `exclude` in the standard `mask-composite` syntax. - "xor": Xor, + Xor, } } @@ -477,9 +454,9 @@ enum_property! { /// A value for the [mask-border-mode](https://www.w3.org/TR/css-masking-1/#the-mask-border-mode) property. pub enum MaskBorderMode { /// The luminance values of the mask image is used. - "luminance": Luminance, + Luminance, /// The alpha values of the mask image is used. - "alpha": Alpha, + Alpha, } } diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 6e39fc5dc..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; @@ -123,6 +122,7 @@ pub mod ui; use crate::declaration::DeclarationBlock; use crate::error::{ParserError, PrinterError}; use crate::logical::{LogicalGroup, PropertyCategory}; +use crate::macros::enum_property; use crate::parser::starts_with_ignore_ascii_case; use crate::parser::ParserOptions; use crate::prefixes::Feature; @@ -132,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; @@ -153,7 +153,6 @@ use display::*; use effects::*; use flex::*; use font::*; -#[cfg(feature = "grid")] use grid::*; use list::*; use margin_padding::*; @@ -322,7 +321,7 @@ macro_rules! define_properties { } } - fn with_prefix(&self, prefix: VendorPrefix) -> PropertyId<'i> { + pub(crate) fn with_prefix(&self, prefix: VendorPrefix) -> PropertyId<'i> { use PropertyId::*; match self { $( @@ -344,6 +343,26 @@ macro_rules! define_properties { } } + pub(crate) fn add_prefix(&mut self, prefix: VendorPrefix) { + use PropertyId::*; + match self { + $( + $(#[$meta])* + $property$((vp_name!($vp, p)))? => { + macro_rules! get_prefixed { + ($v: ty) => {{ + *p |= prefix; + }}; + () => {{}}; + } + + get_prefixed!($($vp)?) + }, + )+ + _ => {} + } + } + pub(crate) fn set_prefixes_for_targets(&mut self, targets: Targets) { match self { $( @@ -668,6 +687,8 @@ macro_rules! define_properties { $(#[$meta])* $property($type, $($vp)?), )+ + /// The [all](https://drafts.csswg.org/css-cascade-5/#all-shorthand) shorthand property. + All(CSSWideKeyword), /// An unparsed property. Unparsed(UnparsedProperty<'i>), /// A custom or unknown property. @@ -690,6 +711,7 @@ macro_rules! define_properties { } }, )+ + PropertyId::All => return Ok(Property::All(CSSWideKeyword::parse(input)?)), PropertyId::Custom(name) => return Ok(Property::Custom(CustomProperty::parse(name, input, options)?)), _ => {} }; @@ -711,6 +733,7 @@ macro_rules! define_properties { $(#[$meta])* $property(_, $(vp_name!($vp, p))?) => PropertyId::$property$((*vp_name!($vp, p)))?, )+ + All(_) => PropertyId::All, Unparsed(unparsed) => unparsed.property_id.clone(), Custom(custom) => PropertyId::Custom(custom.name.clone()) } @@ -759,6 +782,7 @@ macro_rules! define_properties { val.to_css(dest) } )+ + All(keyword) => keyword.to_css(dest), Unparsed(unparsed) => { unparsed.value.to_css(dest, false) } @@ -818,6 +842,7 @@ macro_rules! define_properties { ($name, get_prefix!($($vp)?)) }, )+ + All(_) => ("all", VendorPrefix::None), Unparsed(unparsed) => { let mut prefix = unparsed.property_id.prefix(); if prefix.is_empty() { @@ -827,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(()) @@ -946,7 +974,10 @@ macro_rules! define_properties { s.serialize_field("value", value)?; } )+ - _ => unreachable!() + All(value) => { + s.serialize_field("value", value)?; + } + Unparsed(_) | Custom(_) => unreachable!() } s.end() @@ -961,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>) } @@ -1034,25 +1065,28 @@ 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 => unreachable!() + PropertyId::All => { + let value = CSSWideKeyword::deserialize(deserializer).map_err(|e| serde::de::Error::custom(e.to_string()))?; + Ok(Property::All(value)) + } } } } @@ -1114,6 +1148,17 @@ macro_rules! define_properties { with_prefix!($($vp)?) }, )+ + { + property!("all"); + #[derive(schemars::JsonSchema)] + struct T { + #[schemars(rename = "property", schema_with = "property")] + _property: u8, + #[schemars(rename = "value")] + _value: CSSWideKeyword + } + T::json_schema(gen) + }, { property!("unparsed"); @@ -1328,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], @@ -1447,6 +1462,11 @@ define_properties! { "animation-play-state": AnimationPlayState(SmallVec<[AnimationPlayState; 1]>, VendorPrefix) / WebKit / Moz / O, "animation-delay": AnimationDelay(SmallVec<[Time; 1]>, VendorPrefix) / WebKit / Moz / O, "animation-fill-mode": AnimationFillMode(SmallVec<[AnimationFillMode; 1]>, VendorPrefix) / WebKit / Moz / O, + "animation-composition": AnimationComposition(SmallVec<[AnimationComposition; 1]>), + "animation-timeline": AnimationTimeline(SmallVec<[AnimationTimeline<'i>; 1]>), + "animation-range-start": AnimationRangeStart(SmallVec<[AnimationRangeStart; 1]>), + "animation-range-end": AnimationRangeEnd(SmallVec<[AnimationRangeEnd; 1]>), + "animation-range": AnimationRange(SmallVec<[AnimationRange; 1]>), "animation": Animation(AnimationList<'i>, VendorPrefix) / WebKit / Moz / O shorthand: true, // https://drafts.csswg.org/css-transforms-2/ @@ -1493,6 +1513,10 @@ define_properties! { // https://w3c.github.io/csswg-drafts/css-size-adjust/ "text-size-adjust": TextSizeAdjust(TextSizeAdjust, VendorPrefix) / WebKit / Moz / Ms, + // https://drafts.csswg.org/css-writing-modes-3/ + "direction": Direction(Direction), + "unicode-bidi": UnicodeBidi(UnicodeBidi), + // https://www.w3.org/TR/css-break-3/ "box-decoration-break": BoxDecorationBreak(BoxDecorationBreak, VendorPrefix) / WebKit, @@ -1576,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), @@ -1585,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 { @@ -1647,3 +1678,19 @@ impl ToCss for Vec { Ok(()) } } + +enum_property! { + /// A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). + pub enum CSSWideKeyword { + /// The property's initial value. + "initial": Initial, + /// The property's computed value on the parent element. + "inherit": Inherit, + /// Either inherit or initial depending on whether the property is inherited. + "unset": Unset, + /// Rolls back the cascade to the cascaded value of the earlier origin. + "revert": Revert, + /// Rolls back the cascade to the value of the previous cascade layer. + "revert-layer": RevertLayer, + } +} diff --git a/src/properties/outline.rs b/src/properties/outline.rs index e0a710e27..2ecdb2024 100644 --- a/src/properties/outline.rs +++ b/src/properties/outline.rs @@ -15,7 +15,7 @@ use crate::visitor::Visit; use cssparser::*; /// A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -31,29 +31,6 @@ pub enum OutlineStyle { LineStyle(LineStyle), } -impl<'i> Parse<'i> for OutlineStyle { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(border_style) = input.try_parse(LineStyle::parse) { - return Ok(OutlineStyle::LineStyle(border_style)); - } - - input.expect_ident_matching("auto")?; - Ok(OutlineStyle::Auto) - } -} - -impl ToCss for OutlineStyle { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - OutlineStyle::Auto => dest.write_str("auto"), - OutlineStyle::LineStyle(border_style) => border_style.to_css(dest), - } - } -} - impl Default for OutlineStyle { fn default() -> OutlineStyle { OutlineStyle::LineStyle(LineStyle::None) diff --git a/src/properties/position.rs b/src/properties/position.rs index ace9a40d5..34a0cf3bf 100644 --- a/src/properties/position.rs +++ b/src/properties/position.rs @@ -73,7 +73,7 @@ impl ToCss for Position { } /// A value for the [z-index](https://drafts.csswg.org/css2/#z-index) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -89,29 +89,6 @@ pub enum ZIndex { Integer(CSSInteger), } -impl<'i> Parse<'i> for ZIndex { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(value) = input.expect_integer() { - return Ok(ZIndex::Integer(value)); - } - - input.expect_ident_matching("auto")?; - Ok(ZIndex::Auto) - } -} - -impl ToCss for ZIndex { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - ZIndex::Auto => dest.write_str("auto"), - ZIndex::Integer(value) => value.to_css(dest), - } - } -} - #[derive(Default)] pub(crate) struct PositionHandler { position: Option, diff --git a/src/properties/prefix_handler.rs b/src/properties/prefix_handler.rs index 78dd7d099..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,15 +139,9 @@ macro_rules! define_fallbacks { } let val = get_prefixed!($($p)?); - (val, paste::paste! { &mut self.[<$name:snake>] }) + (val, pastey::paste! { &mut self.[<$name:snake>] }) } )+ - PropertyId::All => { - let mut unparsed = val.clone(); - context.add_unparsed_fallbacks(&mut unparsed); - dest.push(Property::Unparsed(unparsed)); - return true - }, _ => return false }; @@ -167,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 54cc134da..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), @@ -300,9 +298,9 @@ enum_property! { /// A value for the [box-sizing](https://drafts.csswg.org/css-sizing-3/#box-sizing) property. pub enum BoxSizing { /// Exclude the margin/border/padding from the width and height. - "content-box": ContentBox, + ContentBox, /// Include the padding and border (but not the margin) in the width and height. - "border-box": BorderBox, + BorderBox, } } @@ -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 26109941e..552dd2d25 100644 --- a/src/properties/svg.rs +++ b/src/properties/svg.rs @@ -13,7 +13,7 @@ use cssparser::*; /// An SVG [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value /// used in the `fill` and `stroke` properties. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -23,8 +23,6 @@ use cssparser::*; )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum SVGPaint<'i> { - /// No paint. - None, /// A URL reference to a paint server element, e.g. `linearGradient`, `radialGradient`, and `pattern`. Url { #[cfg_attr(feature = "serde", serde(borrow))] @@ -40,12 +38,14 @@ pub enum SVGPaint<'i> { ContextFill, /// Use the paint value of stroke from a context element. ContextStroke, + /// No paint. + None, } /// A fallback for an SVG paint in case a paint server `url()` cannot be resolved. /// /// See [SVGPaint](SVGPaint). -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -61,74 +61,6 @@ pub enum SVGPaintFallback { Color(CssColor), } -impl<'i> Parse<'i> for SVGPaint<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(url) = input.try_parse(Url::parse) { - let fallback = input.try_parse(SVGPaintFallback::parse).ok(); - return Ok(SVGPaint::Url { url, fallback }); - } - - if let Ok(color) = input.try_parse(CssColor::parse) { - return Ok(SVGPaint::Color(color)); - } - - let location = input.current_source_location(); - let keyword = input.expect_ident()?; - match_ignore_ascii_case! { &keyword, - "none" => Ok(SVGPaint::None), - "context-fill" => Ok(SVGPaint::ContextFill), - "context-stroke" => Ok(SVGPaint::ContextStroke), - _ => Err(location.new_unexpected_token_error( - cssparser::Token::Ident(keyword.clone()) - )) - } - } -} - -impl<'i> ToCss for SVGPaint<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - SVGPaint::None => dest.write_str("none"), - SVGPaint::Url { url, fallback } => { - url.to_css(dest)?; - if let Some(fallback) = fallback { - dest.write_char(' ')?; - fallback.to_css(dest)?; - } - Ok(()) - } - SVGPaint::Color(color) => color.to_css(dest), - SVGPaint::ContextFill => dest.write_str("context-fill"), - SVGPaint::ContextStroke => dest.write_str("context-stroke"), - } - } -} - -impl<'i> Parse<'i> for SVGPaintFallback { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { - return Ok(SVGPaintFallback::None); - } - - Ok(SVGPaintFallback::Color(CssColor::parse(input)?)) - } -} - -impl ToCss for SVGPaintFallback { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - SVGPaintFallback::None => dest.write_str("none"), - SVGPaintFallback::Color(color) => color.to_css(dest), - } - } -} - impl<'i> FallbackValues for SVGPaint<'i> { fn get_fallbacks(&mut self, targets: Targets) -> Vec { match self { @@ -182,15 +114,15 @@ enum_property! { /// A value for the [stroke-linejoin](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property. pub enum StrokeLinejoin { /// A sharp corner is to be used to join path segments. - "miter": Miter, + Miter, /// Same as `miter` but clipped beyond `stroke-miterlimit`. - "miter-clip": MiterClip, + MiterClip, /// A round corner is to be used to join path segments. - "round": Round, + Round, /// A bevelled corner is to be used to join path segments. - "bevel": Bevel, + Bevel, /// An arcs corner is to be used to join path segments. - "arcs": Arcs, + Arcs, } } @@ -260,7 +192,7 @@ impl ToCss for StrokeDasharray { } /// A value for the [marker](https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties) properties. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( @@ -277,89 +209,104 @@ pub enum Marker<'i> { Url(Url<'i>), } -impl<'i> Parse<'i> for Marker<'i> { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(url) = input.try_parse(Url::parse) { - return Ok(Marker::Url(url)); - } - - input.expect_ident_matching("none")?; - Ok(Marker::None) - } -} - -impl<'i> ToCss for Marker<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - Marker::None => dest.write_str("none"), - Marker::Url(url) => url.to_css(dest), - } - } -} - -enum_property! { - /// A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property. - pub enum ColorInterpolation { - /// The UA can choose between sRGB or linearRGB. - Auto, - /// Color interpolation occurs in the sRGB color space. - SRGB, - /// Color interpolation occurs in the linearized RGB color space - LinearRGB, - } +/// 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", + 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 ColorInterpolation { + /// The UA can choose between sRGB or linearRGB. + Auto, + /// Color interpolation occurs in the sRGB color space. + SRGB, + /// Color interpolation occurs in the linearized RGB color space + LinearRGB, } -enum_property! { - /// A value for the [color-rendering](https://www.w3.org/TR/SVG2/painting.html#ColorRendering) property. - pub enum ColorRendering { - /// The UA can choose a tradeoff between speed and quality. - Auto, - /// The UA shall optimize speed over quality. - OptimizeSpeed, - /// The UA shall optimize quality over speed. - OptimizeQuality, - } +/// A value for the [color-rendering](https://www.w3.org/TR/SVG2/painting.html#ColorRendering) property. +#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[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 ColorRendering { + /// The UA can choose a tradeoff between speed and quality. + Auto, + /// The UA shall optimize speed over quality. + OptimizeSpeed, + /// The UA shall optimize quality over speed. + OptimizeQuality, } -enum_property! { - /// A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property. - pub enum ShapeRendering { - /// The UA can choose an appropriate tradeoff. - Auto, - /// The UA shall optimize speed. - OptimizeSpeed, - /// The UA shall optimize crisp edges. - CrispEdges, - /// The UA shall optimize geometric precision. - GeometricPrecision, - } +/// 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", + 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 ShapeRendering { + /// The UA can choose an appropriate tradeoff. + Auto, + /// The UA shall optimize speed. + OptimizeSpeed, + /// The UA shall optimize crisp edges. + CrispEdges, + /// The UA shall optimize geometric precision. + GeometricPrecision, } -enum_property! { - /// A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property. - pub enum TextRendering { - /// The UA can choose an appropriate tradeoff. - Auto, - /// The UA shall optimize speed. - OptimizeSpeed, - /// The UA shall optimize legibility. - OptimizeLegibility, - /// The UA shall optimize geometric precision. - GeometricPrecision, - } +/// 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", + 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 TextRendering { + /// The UA can choose an appropriate tradeoff. + Auto, + /// The UA shall optimize speed. + OptimizeSpeed, + /// The UA shall optimize legibility. + OptimizeLegibility, + /// The UA shall optimize geometric precision. + GeometricPrecision, } -enum_property! { - /// A value for the [image-rendering](https://www.w3.org/TR/SVG2/painting.html#ImageRendering) property. - pub enum ImageRendering { - /// The UA can choose a tradeoff between speed and quality. - Auto, - /// The UA shall optimize speed over quality. - OptimizeSpeed, - /// The UA shall optimize quality over speed. - OptimizeQuality, - } +/// A value for the [image-rendering](https://www.w3.org/TR/SVG2/painting.html#ImageRendering) property. +#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[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 ImageRendering { + /// The UA can choose a tradeoff between speed and quality. + Auto, + /// The UA shall optimize speed over quality. + OptimizeSpeed, + /// The UA shall optimize quality over speed. + OptimizeQuality, } diff --git a/src/properties/text.rs b/src/properties/text.rs index 4c7b6fde3..622b870fa 100644 --- a/src/properties/text.rs +++ b/src/properties/text.rs @@ -238,13 +238,13 @@ enum_property! { /// A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property. pub enum WordBreak { /// Words break according to their customary rules. - "normal": Normal, + Normal, /// Breaking is forbidden within “words”. - "keep-all": KeepAll, + KeepAll, /// Breaking is allowed within “words”. - "break-all": BreakAll, + BreakAll, /// Breaking is allowed if there is no otherwise acceptable break points in a line. - "break-word": BreakWord, + BreakWord, } } @@ -279,12 +279,12 @@ enum_property! { /// A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property. pub enum OverflowWrap { /// Lines may break only at allowed break points. - "normal": Normal, + Normal, /// Breaking is allowed if there is no otherwise acceptable break points in a line. - "anywhere": Anywhere, + Anywhere, /// As for anywhere except that soft wrap opportunities introduced by break-word are /// not considered when calculating min-content intrinsic sizes. - "break-word": BreakWord, + BreakWord, } } @@ -292,21 +292,21 @@ enum_property! { /// A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property. pub enum TextAlign { /// Inline-level content is aligned to the start edge of the line box. - "start": Start, + Start, /// Inline-level content is aligned to the end edge of the line box. - "end": End, + End, /// Inline-level content is aligned to the line-left edge of the line box. - "left": Left, + Left, /// Inline-level content is aligned to the line-right edge of the line box. - "right": Right, + Right, /// Inline-level content is centered within the line box. - "center": Center, + Center, /// Text is justified according to the method specified by the text-justify property. - "justify": Justify, + Justify, /// Matches the parent element. - "match-parent": MatchParent, + MatchParent, /// Same as justify, but also justifies the last line. - "justify-all": JustifyAll, + JustifyAll, } } @@ -314,21 +314,21 @@ enum_property! { /// A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property. pub enum TextAlignLast { /// Content on the affected line is aligned per `text-align-all` unless set to `justify`, in which case it is start-aligned. - "auto": Auto, + Auto, /// Inline-level content is aligned to the start edge of the line box. - "start": Start, + Start, /// Inline-level content is aligned to the end edge of the line box. - "end": End, + End, /// Inline-level content is aligned to the line-left edge of the line box. - "left": Left, + Left, /// Inline-level content is aligned to the line-right edge of the line box. - "right": Right, + Right, /// Inline-level content is centered within the line box. - "center": Center, + Center, /// Text is justified according to the method specified by the text-justify property. - "justify": Justify, + Justify, /// Matches the parent element. - "match-parent": MatchParent, + MatchParent, } } @@ -336,19 +336,19 @@ enum_property! { /// A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property. pub enum TextJustify { /// The UA determines the justification algorithm to follow. - "auto": Auto, + Auto, /// Justification is disabled. - "none": None, + None, /// Justification adjusts spacing at word separators only. - "inter-word": InterWord, + InterWord, /// Justification adjusts spacing between each character. - "inter-character": InterCharacter, + InterCharacter, } } /// A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property) /// and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -364,29 +364,6 @@ pub enum Spacing { Length(Length), } -impl<'i> Parse<'i> for Spacing { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { - return Ok(Spacing::Normal); - } - - let length = Length::parse(input)?; - Ok(Spacing::Length(length)) - } -} - -impl ToCss for Spacing { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - Spacing::Normal => dest.write_str("normal"), - Spacing::Length(len) => len.to_css(dest), - } - } -} - /// A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] @@ -466,7 +443,7 @@ impl ToCss for TextIndent { } /// A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -484,34 +461,6 @@ pub enum TextSizeAdjust { Percentage(Percentage), } -impl<'i> Parse<'i> for TextSizeAdjust { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(p) = input.try_parse(Percentage::parse) { - return Ok(TextSizeAdjust::Percentage(p)); - } - - let ident = input.expect_ident_cloned()?; - match_ignore_ascii_case! {&*ident, - "auto" => Ok(TextSizeAdjust::Auto), - "none" => Ok(TextSizeAdjust::None), - _ => Err(input.new_unexpected_token_error(Token::Ident(ident.clone()))) - } - } -} - -impl ToCss for TextSizeAdjust { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - TextSizeAdjust::Auto => dest.write_str("auto"), - TextSizeAdjust::None => dest.write_str("none"), - TextSizeAdjust::Percentage(p) => p.to_css(dest), - } - } -} - bitflags! { /// A value for the [text-decoration-line](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-line-property) property. /// @@ -749,7 +698,7 @@ impl Default for TextDecorationStyle { } /// A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -773,34 +722,6 @@ impl Default for TextDecorationThickness { } } -impl<'i> Parse<'i> for TextDecorationThickness { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() { - return Ok(TextDecorationThickness::Auto); - } - - if input.try_parse(|input| input.expect_ident_matching("from-font")).is_ok() { - return Ok(TextDecorationThickness::FromFont); - } - - let lp = LengthPercentage::parse(input)?; - Ok(TextDecorationThickness::LengthPercentage(lp)) - } -} - -impl ToCss for TextDecorationThickness { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - TextDecorationThickness::Auto => dest.write_str("auto"), - TextDecorationThickness::FromFont => dest.write_str("from-font"), - TextDecorationThickness::LengthPercentage(lp) => lp.to_css(dest), - } - } -} - define_shorthand! { /// A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property. pub struct TextDecoration(VendorPrefix) { @@ -927,15 +848,15 @@ enum_property! { /// See [TextEmphasisStyle](TextEmphasisStyle). pub enum TextEmphasisShape { /// Display small circles as marks. - "dot": Dot, + Dot, /// Display large circles as marks. - "circle": Circle, + Circle, /// Display double circles as marks. - "double-circle": DoubleCircle, + DoubleCircle, /// Display triangles as marks. - "triangle": Triangle, + Triangle, /// Display sesames as marks. - "sesame": Sesame, + Sesame, } } @@ -1599,3 +1520,31 @@ impl FallbackValues for SmallVec<[TextShadow; 1]> { res } } + +enum_property! { + /// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. + pub enum Direction { + /// This value sets inline base direction (bidi directionality) to line-left-to-line-right. + Ltr, + /// This value sets inline base direction (bidi directionality) to line-right-to-line-left. + Rtl, + } +} + +enum_property! { + /// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. + pub enum UnicodeBidi { + /// The box does not open an additional level of embedding. + Normal, + /// If the box is inline, this value creates a directional embedding by opening an additional level of embedding. + Embed, + /// On an inline box, this bidi-isolates its contents. + Isolate, + /// This value puts the box’s immediate inline content in a directional override. + BidiOverride, + /// This combines the isolation behavior of isolate with the directional override behavior of bidi-override. + IsolateOverride, + /// This value behaves as isolate except that the base directionality is determined using a heuristic rather than the direction property. + Plaintext, + } +} diff --git a/src/properties/transform.rs b/src/properties/transform.rs index 87699837d..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, @@ -57,64 +56,66 @@ impl ToCss for TransformList { return Ok(()); } - if dest.minify { - // Combine transforms into a single matrix. - if let Some(matrix) = self.to_matrix() { - // Generate based on the original transforms. - let mut base = String::new(); - self.to_css_base(&mut Printer::new( - &mut base, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))?; - - // Decompose the matrix into transform functions if possible. - // If the resulting length is shorter than the original, use it. - if let Some(d) = matrix.decompose() { - let mut decomposed = String::new(); - d.to_css_base(&mut Printer::new( - &mut decomposed, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))?; - if decomposed.len() < base.len() { - base = decomposed; - } - } - - // Also generate a matrix() or matrix3d() representation and compare that. - let mut mat = String::new(); - if let Some(matrix) = matrix.to_matrix2d() { - Transform::Matrix(matrix).to_css(&mut Printer::new( - &mut mat, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))? - } else { - Transform::Matrix3d(matrix).to_css(&mut Printer::new( - &mut mat, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))? - } - - if mat.len() < base.len() { - dest.write_str(&mat)?; - } else { - dest.write_str(&base)?; - } - - return Ok(()); - } - } + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + // if dest.minify { + // // Combine transforms into a single matrix. + // if let Some(matrix) = self.to_matrix() { + // // Generate based on the original transforms. + // let mut base = String::new(); + // self.to_css_base(&mut Printer::new( + // &mut base, + // PrinterOptions { + // minify: true, + // ..PrinterOptions::default() + // }, + // ))?; + // + // // Decompose the matrix into transform functions if possible. + // // If the resulting length is shorter than the original, use it. + // if let Some(d) = matrix.decompose() { + // let mut decomposed = String::new(); + // d.to_css_base(&mut Printer::new( + // &mut decomposed, + // PrinterOptions { + // minify: true, + // ..PrinterOptions::default() + // }, + // ))?; + // if decomposed.len() < base.len() { + // base = decomposed; + // } + // } + // + // // Also generate a matrix() or matrix3d() representation and compare that. + // let mut mat = String::new(); + // if let Some(matrix) = matrix.to_matrix2d() { + // Transform::Matrix(matrix).to_css(&mut Printer::new( + // &mut mat, + // PrinterOptions { + // minify: true, + // ..PrinterOptions::default() + // }, + // ))? + // } else { + // Transform::Matrix3d(matrix).to_css(&mut Printer::new( + // &mut mat, + // PrinterOptions { + // minify: true, + // ..PrinterOptions::default() + // }, + // ))? + // } + // + // if mat.len() < base.len() { + // dest.write_str(&mat)?; + // } else { + // dest.write_str(&base)?; + // } + // + // return Ok(()); + // } + // } self.to_css_base(dest) } @@ -125,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(()) @@ -1366,8 +1373,8 @@ enum_property! { /// A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property. #[allow(missing_docs)] pub enum TransformStyle { - "flat": Flat, - "preserve-3d": Preserve3d, + Flat, + Preserve3d, } } @@ -1375,15 +1382,15 @@ enum_property! { /// A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property. pub enum TransformBox { /// Uses the content box as reference box. - "content-box": ContentBox, + ContentBox, /// Uses the border box as reference box. - "border-box": BorderBox, + BorderBox, /// Uses the object bounding box as reference box. - "fill-box": FillBox, + FillBox, /// Uses the stroke bounding box as reference box. - "stroke-box": StrokeBox, + StrokeBox, /// Uses the nearest SVG viewport as reference box. - "view-box": ViewBox, + ViewBox, } } @@ -1397,7 +1404,7 @@ enum_property! { } /// A value for the [perspective](https://drafts.csswg.org/css-transforms-2/#perspective-property) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -1413,51 +1420,36 @@ pub enum Perspective { Length(Length), } -impl<'i> Parse<'i> for Perspective { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { - return Ok(Perspective::None); - } - - Ok(Perspective::Length(Length::parse(input)?)) - } -} - -impl ToCss for Perspective { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - Perspective::None => dest.write_str("none"), - Perspective::Length(len) => len.to_css(dest), - } - } -} - /// A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) 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 Translate { - /// The x translation. - pub x: LengthPercentage, - /// The y translation. - pub y: LengthPercentage, - /// The z translation. - pub z: Length, +pub enum Translate { + /// The "none" keyword. + None, + + /// The x, y, and z translations. + #[cfg_attr(feature = "serde", serde(untagged))] + XYZ { + /// The x translation. + x: LengthPercentage, + /// The y translation. + y: LengthPercentage, + /// The z translation. + z: Length, + }, } impl<'i> Parse<'i> for Translate { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { - return Ok(Translate { - x: LengthPercentage::zero(), - y: LengthPercentage::zero(), - z: Length::zero(), - }); + return Ok(Translate::None); } let x = LengthPercentage::parse(input)?; @@ -1468,7 +1460,7 @@ impl<'i> Parse<'i> for Translate { None }; - Ok(Translate { + Ok(Translate::XYZ { x, y: y.unwrap_or(LengthPercentage::zero()), z: z.unwrap_or(Length::zero()), @@ -1481,15 +1473,23 @@ impl ToCss for Translate { where W: std::fmt::Write, { - self.x.to_css(dest)?; - if !self.y.is_zero() || !self.z.is_zero() { - dest.write_char(' ')?; - self.y.to_css(dest)?; - if !self.z.is_zero() { - dest.write_char(' ')?; - self.z.to_css(dest)?; + match self { + Translate::None => { + dest.write_str("none")?; } - } + Translate::XYZ { x, y, z } => { + x.to_css(dest)?; + if !y.is_zero() || !z.is_zero() { + dest.write_char(' ')?; + y.to_css(dest)?; + if !z.is_zero() { + dest.write_char(' ')?; + z.to_css(dest)?; + } + } + } + }; + Ok(()) } } @@ -1497,36 +1497,51 @@ impl ToCss for Translate { impl Translate { /// Converts the translation to a transform function. pub fn to_transform(&self) -> Transform { - Transform::Translate3d(self.x.clone(), self.y.clone(), self.z.clone()) + match self { + Translate::None => { + Transform::Translate3d(LengthPercentage::zero(), LengthPercentage::zero(), Length::zero()) + } + Translate::XYZ { x, y, z } => Transform::Translate3d(x.clone(), y.clone(), z.clone()), + } } } /// 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); @@ -1550,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 }) } } @@ -1559,58 +1574,79 @@ 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()), + } } } /// A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) 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 Scale { - /// Scale on the x axis. - pub x: NumberOrPercentage, - /// Scale on the y axis. - pub y: NumberOrPercentage, - /// Scale on the z axis. - pub z: NumberOrPercentage, +pub enum Scale { + /// The "none" keyword. + None, + + /// Scale on the x, y, and z axis. + #[cfg_attr(feature = "serde", serde(untagged))] + XYZ { + /// Scale on the x axis. + x: NumberOrPercentage, + /// Scale on the y axis. + y: NumberOrPercentage, + /// Scale on the z axis. + z: NumberOrPercentage, + }, } impl<'i> Parse<'i> for Scale { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { - return Ok(Scale { - x: NumberOrPercentage::Number(1.0), - y: NumberOrPercentage::Number(1.0), - z: NumberOrPercentage::Number(1.0), - }); + return Ok(Scale::None); } let x = NumberOrPercentage::parse(input)?; @@ -1621,7 +1657,7 @@ impl<'i> Parse<'i> for Scale { None }; - Ok(Scale { + Ok(Scale::XYZ { x: x.clone(), y: y.unwrap_or(x), z: z.unwrap_or(NumberOrPercentage::Number(1.0)), @@ -1634,14 +1670,21 @@ impl ToCss for Scale { where W: std::fmt::Write, { - self.x.to_css(dest)?; - let zv: f32 = (&self.z).into(); - if self.y != self.x || zv != 1.0 { - dest.write_char(' ')?; - self.y.to_css(dest)?; - if zv != 1.0 { - dest.write_char(' ')?; - self.z.to_css(dest)?; + match self { + Scale::None => { + dest.write_str("none")?; + } + Scale::XYZ { x, y, z } => { + x.to_css(dest)?; + let zv: f32 = z.into(); + if y != x || zv != 1.0 { + dest.write_char(' ')?; + y.to_css(dest)?; + if zv != 1.0 { + dest.write_char(' ')?; + z.to_css(dest)?; + } + } } } @@ -1652,7 +1695,14 @@ impl ToCss for Scale { impl Scale { /// Converts the scale to a transform function. pub fn to_transform(&self) -> Transform { - Transform::Scale3d(self.x.clone(), self.y.clone(), self.z.clone()) + match self { + Scale::None => Transform::Scale3d( + NumberOrPercentage::Number(1.0), + NumberOrPercentage::Number(1.0), + NumberOrPercentage::Number(1.0), + ), + Scale::XYZ { x, y, z } => Transform::Scale3d(x.clone(), y.clone(), z.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 4952082e4..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; @@ -94,42 +93,42 @@ enum_property! { /// See [Cursor](Cursor). #[allow(missing_docs)] pub enum CursorKeyword { - "auto": Auto, - "default": Default, - "none": None, - "context-menu": ContextMenu, - "help": Help, - "pointer": Pointer, - "progress": Progress, - "wait": Wait, - "cell": Cell, - "crosshair": Crosshair, - "text": Text, - "vertical-text": VerticalText, - "alias": Alias, - "copy": Copy, - "move": Move, - "no-drop": NoDrop, - "not-allowed": NotAllowed, - "grab": Grab, - "grabbing": Grabbing, - "e-resize": EResize, - "n-resize": NResize, - "ne-resize": NeResize, - "nw-resize": NwResize, - "s-resize": SResize, - "se-resize": SeResize, - "sw-resize": SwResize, - "w-resize": WResize, - "ew-resize": EwResize, - "ns-resize": NsResize, - "nesw-resize": NeswResize, - "nwse-resize": NwseResize, - "col-resize": ColResize, - "row-resize": RowResize, - "all-scroll": AllScroll, - "zoom-in": ZoomIn, - "zoom-out": ZoomOut, + Auto, + Default, + None, + ContextMenu, + Help, + Pointer, + Progress, + Wait, + Cell, + Crosshair, + Text, + VerticalText, + Alias, + Copy, + Move, + NoDrop, + NotAllowed, + Grab, + Grabbing, + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, + AllScroll, + ZoomIn, + ZoomOut, } } @@ -179,7 +178,7 @@ impl<'i> ToCss for Cursor<'i> { } /// A value for the [caret-color](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-color) property. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Parse, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -201,29 +200,6 @@ impl Default for ColorOrAuto { } } -impl<'i> Parse<'i> for ColorOrAuto { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() { - return Ok(ColorOrAuto::Auto); - } - - let color = CssColor::parse(input)?; - Ok(ColorOrAuto::Color(color)) - } -} - -impl ToCss for ColorOrAuto { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - match self { - ColorOrAuto::Auto => dest.write_str("auto"), - ColorOrAuto::Color(color) => color.to_css(dest), - } - } -} - impl FallbackValues for ColorOrAuto { fn get_fallbacks(&mut self, targets: Targets) -> Vec { match self { @@ -457,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); + } + + 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); } - Ok(res) + Err(input.new_custom_error(ParserError::InvalidValue)) } } @@ -508,6 +495,10 @@ impl ToCss for ColorScheme { } if self.contains(ColorScheme::Only) { + // Avoid parsing `color-scheme: only` as `color-scheme: only` + if !self.intersects(ColorScheme::Light | ColorScheme::Dark) { + return dest.write_str("only"); + } dest.write_str(" only")?; } @@ -571,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()))); @@ -595,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/keyframes.rs b/src/rules/keyframes.rs index a9489f304..02d0943b6 100644 --- a/src/rules/keyframes.rs +++ b/src/rules/keyframes.rs @@ -8,6 +8,7 @@ use crate::declaration::DeclarationBlock; use crate::error::{ParserError, PrinterError}; use crate::parser::ParserOptions; use crate::printer::Printer; +use crate::properties::animation::TimelineRangeName; use crate::properties::custom::{CustomProperty, UnparsedProperty}; use crate::properties::Property; use crate::targets::Targets; @@ -92,9 +93,12 @@ impl<'i> ToCss for KeyframesName<'i> { where W: std::fmt::Write, { + let css_module_animation_enabled = + dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation); + match self { KeyframesName::Ident(ident) => { - dest.write_ident(ident.0.as_ref())?; + dest.write_ident(ident.0.as_ref(), css_module_animation_enabled)?; } KeyframesName::Custom(s) => { // CSS-wide keywords and `none` cannot remove quotes. @@ -103,7 +107,7 @@ impl<'i> ToCss for KeyframesName<'i> { serialize_string(&s, dest)?; }, _ => { - dest.write_ident(s.as_ref())?; + dest.write_ident(s.as_ref(), css_module_animation_enabled)?; } } } @@ -262,9 +266,34 @@ impl<'i> ToCss for KeyframesRule<'i> { } } +/// A percentage of a given timeline range +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type", rename_all = "camelCase") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +pub struct TimelineRangePercentage { + /// The name of the timeline range. + name: TimelineRangeName, + /// The percentage progress between the start and end of the range. + percentage: Percentage, +} + +impl<'i> Parse<'i> for TimelineRangePercentage { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { + let name = TimelineRangeName::parse(input)?; + let percentage = Percentage::parse(input)?; + Ok(TimelineRangePercentage { name, percentage }) + } +} + /// A [keyframe selector](https://drafts.csswg.org/css-animations/#typedef-keyframe-selector) /// within an `@keyframes` rule. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Parse)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", @@ -280,24 +309,8 @@ pub enum KeyframeSelector { From, /// The `to` keyword. Equivalent to 100%. To, -} - -impl<'i> Parse<'i> for KeyframeSelector { - fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<'i, ParserError<'i>>> { - if let Ok(val) = input.try_parse(Percentage::parse) { - return Ok(KeyframeSelector::Percentage(val)); - } - - let location = input.current_source_location(); - let ident = input.expect_ident()?; - match_ignore_ascii_case! { &*ident, - "from" => Ok(KeyframeSelector::From), - "to" => Ok(KeyframeSelector::To), - _ => Err(location.new_unexpected_token_error( - cssparser::Token::Ident(ident.clone()) - )) - } - } + /// A [named timeline range selector](https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes) + TimelineRangePercentage(TimelineRangePercentage), } impl ToCss for KeyframeSelector { @@ -321,6 +334,14 @@ impl ToCss for KeyframeSelector { } } KeyframeSelector::To => dest.write_str("to"), + KeyframeSelector::TimelineRangePercentage(TimelineRangePercentage { + name: timeline_range_name, + percentage, + }) => { + timeline_range_name.to_css(dest)?; + dest.write_char(' ')?; + percentage.to_css(dest) + } } } } 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/page.rs b/src/rules/page.rs index e4897cd34..d95c94c26 100644 --- a/src/rules/page.rs +++ b/src/rules/page.rs @@ -81,41 +81,41 @@ enum_property! { /// A [page margin box](https://www.w3.org/TR/css-page-3/#margin-boxes). pub enum PageMarginBox { /// A fixed-size box defined by the intersection of the top and left margins of the page box. - "top-left-corner": TopLeftCorner, + TopLeftCorner, /// A variable-width box filling the top page margin between the top-left-corner and top-center page-margin boxes. - "top-left": TopLeft, + TopLeft, /// A variable-width box centered horizontally between the page’s left and right border edges and filling the /// page top margin between the top-left and top-right page-margin boxes. - "top-center": TopCenter, + TopCenter, /// A variable-width box filling the top page margin between the top-center and top-right-corner page-margin boxes. - "top-right": TopRight, + TopRight, /// A fixed-size box defined by the intersection of the top and right margins of the page box. - "top-right-corner": TopRightCorner, + TopRightCorner, /// A variable-height box filling the left page margin between the top-left-corner and left-middle page-margin boxes. - "left-top": LeftTop, + LeftTop, /// A variable-height box centered vertically between the page’s top and bottom border edges and filling the /// left page margin between the left-top and left-bottom page-margin boxes. - "left-middle": LeftMiddle, + LeftMiddle, /// A variable-height box filling the left page margin between the left-middle and bottom-left-corner page-margin boxes. - "left-bottom": LeftBottom, + LeftBottom, /// A variable-height box filling the right page margin between the top-right-corner and right-middle page-margin boxes. - "right-top": RightTop, + RightTop, /// A variable-height box centered vertically between the page’s top and bottom border edges and filling the right /// page margin between the right-top and right-bottom page-margin boxes. - "right-middle": RightMiddle, + RightMiddle, /// A variable-height box filling the right page margin between the right-middle and bottom-right-corner page-margin boxes. - "right-bottom": RightBottom, + RightBottom, /// A fixed-size box defined by the intersection of the bottom and left margins of the page box. - "bottom-left-corner": BottomLeftCorner, + BottomLeftCorner, /// A variable-width box filling the bottom page margin between the bottom-left-corner and bottom-center page-margin boxes. - "bottom-left": BottomLeft, + BottomLeft, /// A variable-width box centered horizontally between the page’s left and right border edges and filling the bottom /// page margin between the bottom-left and bottom-right page-margin boxes. - "bottom-center": BottomCenter, + BottomCenter, /// A variable-width box filling the bottom page margin between the bottom-center and bottom-right-corner page-margin boxes. - "bottom-right": BottomRight, + BottomRight, /// A fixed-size box defined by the intersection of the bottom and right margins of the page box. - "bottom-right-corner": BottomRightCorner, + BottomRightCorner, } } 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 68c13e6a6..a8deccd1b 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -1,12 +1,15 @@ //! The `@supports` rule. +use std::collections::HashMap; + use super::Location; 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; @@ -40,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 } } @@ -58,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('}') @@ -147,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> { @@ -159,6 +201,7 @@ impl<'i> Parse<'i> for SupportsCondition<'i> { let in_parens = Self::parse_in_parens(input)?; let mut expected_type = None; let mut conditions = Vec::new(); + let mut seen_declarations = HashMap::new(); loop { let condition = input.try_parse(|input| { @@ -185,14 +228,40 @@ impl<'i> Parse<'i> for SupportsCondition<'i> { if let Ok(condition) = condition { if conditions.is_empty() { - conditions.push(in_parens.clone()) + conditions.push(in_parens.clone()); + if let SupportsCondition::Declaration { property_id, value } = &in_parens { + seen_declarations.insert((property_id.with_prefix(VendorPrefix::None), value.clone()), 0); + } + } + + if let SupportsCondition::Declaration { property_id, value } = condition { + // Merge multiple declarations with the same property id (minus prefix) and value together. + let property_id = property_id.with_prefix(VendorPrefix::None); + let key = (property_id.clone(), value.clone()); + if let Some(index) = seen_declarations.get(&key) { + if let SupportsCondition::Declaration { + property_id: cur_property, + .. + } = &mut conditions[*index] + { + cur_property.add_prefix(property_id.prefix()); + } + } else { + seen_declarations.insert(key, conditions.len()); + conditions.push(SupportsCondition::Declaration { property_id, value }); + } + } else { + conditions.push(condition); } - conditions.push(condition) } else { break; } } + if conditions.len() == 1 { + return Ok(conditions.pop().unwrap()); + } + match expected_type { Some(1) => Ok(SupportsCondition::And(conditions)), Some(2) => Ok(SupportsCondition::Or(conditions)), 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 9cc048486..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), @@ -190,9 +194,13 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "corner-present" => WebKitScrollbar(WebKitScrollbarPseudoClass::CornerPresent), "window-inactive" => WebKitScrollbar(WebKitScrollbarPseudoClass::WindowInactive), + "local" | "global" if self.options.css_modules.is_some() => { + return Err(loc.new_custom_error(SelectorParseErrorKind::AmbiguousCssModuleClass(name.clone()))) + }, + _ => { 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() } } @@ -217,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)?; @@ -254,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), @@ -277,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() } } @@ -299,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)?; @@ -503,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 { @@ -645,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(")"); + } _ => {} } @@ -672,7 +718,7 @@ where if let Some(class) = class { dest.write_char('.')?; - dest.write_ident(class) + dest.write_ident(class, true) } else { dest.write_str($s) } @@ -759,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); @@ -785,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); @@ -858,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), @@ -898,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. @@ -961,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> { @@ -1037,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 @@ -1088,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"), @@ -1134,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); @@ -1312,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; @@ -1551,11 +1658,11 @@ where Component::Nesting => serialize_nesting(dest, context, false), Component::Class(ref class) => { dest.write_char('.')?; - dest.write_ident(&class.0) + dest.write_ident(&class.0, true) } Component::ID(ref id) => { dest.write_char('#')?; - dest.write_ident(&id.0) + dest.write_ident(&id.0, true) } Component::Host(selector) => { dest.write_str(":host")?; @@ -1571,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(()) @@ -1614,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('&') @@ -1624,8 +1739,13 @@ where #[inline] fn has_type_selector(selector: &Selector) -> bool { - let mut iter = selector.iter_raw_parse_order_from(0); + // For input:checked the component vector is + // [input, :checked] so we have to check it using matching order. + // + // This both happens for input:checked and is(input:checked) + let mut iter = selector.iter_raw_match_order(); let first = iter.next(); + if is_namespace(first) { is_type_selector(iter.next()) } else { @@ -1811,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 @@ -1827,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, } @@ -1837,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, }, @@ -2084,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