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 db6c59aed..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.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.39", + "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.55" +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,11 +880,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "napi" -version = "2.15.4" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e0dc78e0524286630914db66e31bad70160e379705a9ce92e0161ce2389d89" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "ctor", "napi-derive", "napi-sys", @@ -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - -[[package]] -name = "phf" -version = "0.10.1" +version = "0.2.3" 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 4e27dbf59..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.55" +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/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 3360a9cdf..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 = { version = "1.0.0-alpha.54", 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 cc3ce78ed..f43a8d46e 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -121,8 +121,8 @@ pub fn transform_style_attribute(ctx: CallContext) -> napi::Result { mod bundle { use super::*; use crossbeam_channel::{self, Receiver, Sender}; - use lightningcss::bundler::FileProvider; - use napi::{Env, JsFunction, JsString, NapiRaw}; + use lightningcss::bundler::{FileProvider, ResolveResult}; + use napi::{Env, JsBoolean, JsFunction, JsString, NapiRaw}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Mutex; @@ -169,6 +169,7 @@ mod bundle { // Allocate a single channel per thread to communicate with the JS thread. thread_local! { static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); + static RESOLVER_CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); } impl SourceProvider for JsSourceProvider { @@ -203,9 +204,9 @@ mod bundle { } } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { - return CHANNEL.with(|channel| { + return RESOLVER_CHANNEL.with(|channel| { let message = ResolveMessage { specifier: specifier.to_owned(), originating_file: originating_file.to_str().unwrap().to_owned(), @@ -213,22 +214,18 @@ mod bundle { }; resolve.call(message, ThreadsafeFunctionCallMode::Blocking); - let result = channel.1.recv().unwrap(); - match result { - Ok(result) => Ok(PathBuf::from_str(&result).unwrap()), - Err(e) => Err(e), - } + channel.1.recv().unwrap() }); } - Ok(originating_file.with_file_name(specifier)) + Ok(originating_file.with_file_name(specifier).into()) } } struct ResolveMessage { specifier: String, originating_file: String, - tx: Sender>, + tx: Sender>, } struct ReadMessage { @@ -241,7 +238,11 @@ mod bundle { tx: Sender>, } - fn await_promise(env: Env, result: JsUnknown, tx: Sender>) -> napi::Result<()> { + fn await_promise(env: Env, result: JsUnknown, tx: Sender>, parse: Cb) -> napi::Result<()> + where + T: 'static, + Cb: 'static + Fn(JsUnknown) -> Result, + { // If the result is a promise, wait for it to resolve, and send the result to the channel. // Otherwise, send the result immediately. if result.is_promise()? { @@ -249,9 +250,8 @@ mod bundle { let then: JsFunction = get_named_property(&result, "then")?; let tx2 = tx.clone(); let cb = env.create_function_from_closure("callback", move |ctx| { - let res = ctx.get::(0)?.into_utf8()?; - let s = res.into_owned()?; - tx.send(Ok(s)).unwrap(); + let res = parse(ctx.get::(0)?)?; + tx.send(Ok(res)).unwrap(); ctx.env.get_undefined() })?; let eb = env.create_function_from_closure("error_callback", move |ctx| { @@ -261,10 +261,8 @@ mod bundle { })?; then.call(Some(&result), &[cb, eb])?; } else { - let result: JsString = result.try_into()?; - let utf8 = result.into_utf8()?; - let s = utf8.into_owned()?; - tx.send(Ok(s)).unwrap(); + let result = parse(result)?; + tx.send(Ok(result)).unwrap(); } Ok(()) @@ -274,10 +272,12 @@ mod bundle { let specifier = ctx.env.create_string(&ctx.value.specifier)?; let originating_file = ctx.env.create_string(&ctx.value.originating_file)?; let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?; - await_promise(ctx.env, result, ctx.value.tx) + await_promise(ctx.env, result, ctx.value.tx, move |unknown| { + ctx.env.from_js_value(unknown) + }) } - fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { + fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { match res { Ok(_) => Ok(()), Err(e) => { @@ -295,7 +295,9 @@ mod bundle { fn read_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { let file = ctx.env.create_string(&ctx.value.file)?; let result = ctx.callback.unwrap().call(None, &[file])?; - await_promise(ctx.env, result, ctx.value.tx) + await_promise(ctx.env, result, ctx.value.tx, |unknown| { + JsString::try_from(unknown)?.into_utf8()?.into_owned() + }) } fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { @@ -421,10 +423,10 @@ mod bundle { #[cfg(target_arch = "wasm32")] mod bundle { use super::*; + use lightningcss::bundler::ResolveResult; use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; use std::cell::UnsafeCell; - use std::path::{Path, PathBuf}; - use std::str::FromStr; + use std::path::Path; pub fn bundle(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; @@ -497,7 +499,7 @@ mod bundle { ); } - fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { + fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { if value.is_promise()? { let mut result = std::ptr::null_mut(); let mut error = std::ptr::null_mut(); @@ -513,7 +515,7 @@ mod bundle { value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; } - value.try_into() + Ok(value) } impl SourceProvider for JsSourceProvider { @@ -523,7 +525,9 @@ mod bundle { let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?; let file = self.env.create_string(file.to_str().unwrap())?; let source: JsUnknown = read.call(None, &[file])?; - let source = get_result(self.env, source)?.into_utf8()?.into_owned()?; + let source = get_result(self.env, source)?; + let source: JsString = source.try_into()?; + let source = source.into_utf8()?.into_owned()?; // cache the result let ptr = Box::into_raw(Box::new(source)); @@ -535,16 +539,17 @@ mod bundle { Ok(unsafe { &*ptr }) } - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?; let specifier = self.env.create_string(specifier)?; let originating_file = self.env.create_string(originating_file.to_str().unwrap())?; let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?; - let result = get_result(self.env, result)?.into_utf8()?; - Ok(PathBuf::from_str(result.as_str()?).unwrap()) + let result = get_result(self.env, result)?; + let result = self.env.from_js_value(result)?; + Ok(result) } else { - Ok(originating_file.with_file_name(specifier)) + Ok(ResolveResult::File(originating_file.with_file_name(specifier))) } } } @@ -605,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")] @@ -713,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 { @@ -840,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 ad20c611d..bab931f2d 100644 --- a/napi/src/transformer.rs +++ b/napi/src/transformer.rs @@ -298,6 +298,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Keyframes(..) => "keyframes", CssRule::FontFace(..) => "font-face", CssRule::FontPaletteValues(..) => "font-palette-values", + CssRule::FontFeatureValues(..) => "font-feature-values", CssRule::Page(..) => "page", CssRule::Supports(..) => "supports", CssRule::CounterStyle(..) => "counter-style", @@ -310,8 +311,10 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Scope(..) => "scope", CssRule::MozDocument(..) => "moz-document", CssRule::Nesting(..) => "nesting", + CssRule::NestedDeclarations(..) => "nested-declarations", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", + CssRule::ViewTransition(..) => "view-transition", CssRule::Unknown(v) => { let name = v.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "unknown", name) { @@ -752,9 +755,8 @@ impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de D: serde::Deserializer<'de>, { use serde::Deserializer; - let content = serde::__private::de::Content::deserialize(deserializer)?; - let de: serde::__private::de::ContentRefDeserializer = - serde::__private::de::ContentRefDeserializer::new(&content); + let content = serde_content::Value::deserialize(deserializer)?; + let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); // Try to deserialize as a sequence first. let mut was_seq = false; @@ -766,13 +768,15 @@ impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de if was_seq { // Allow fallback if we know the value is also a list (e.g. selector). if res.is_ok() || !IS_VEC { - return res.map(ValueOrVec::Vec); + return res.map_err(|e| serde::de::Error::custom(e.to_string())).map(ValueOrVec::Vec); } } // If it wasn't a sequence, try a value. - let de = serde::__private::de::ContentRefDeserializer::new(&content); - return V::deserialize(de).map(ValueOrVec::Value); + let de = serde_content::Deserializer::new(content).coerce_numbers(); + return V::deserialize(de) + .map_err(|e| serde::de::Error::custom(e.to_string())) + .map(ValueOrVec::Value); struct SeqVisitor<'a, V> { was_seq: &'a mut bool, @@ -808,16 +812,14 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { where D: serde::Deserializer<'de>, { - use serde::__private::de::ContentRefDeserializer; - #[derive(serde::Deserialize)] struct Raw<'i> { #[serde(borrow)] raw: CowArcStr<'i>, } - let content = serde::__private::de::Content::deserialize(deserializer)?; - let de: ContentRefDeserializer = ContentRefDeserializer::new(&content); + let content = serde_content::Value::deserialize(deserializer)?; + let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); if let Ok(res) = Raw::deserialize(de) { let res = TokenList::parse_string_with_options(res.raw.as_ref(), ParserOptions::default()) @@ -825,8 +827,10 @@ impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { return Ok(TokensOrRaw(ValueOrVec::Vec(res.into_owned().0))); } - let de = ContentRefDeserializer::new(&content); - Ok(TokensOrRaw(ValueOrVec::deserialize(de)?)) + let de = serde_content::Deserializer::new(content).coerce_numbers(); + Ok(TokensOrRaw( + ValueOrVec::deserialize(de).map_err(|e| serde::de::Error::custom(e.to_string()))?, + )) } } diff --git a/node/Cargo.toml b/node/Cargo.toml index 4f9e8b758..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.15.4", default-features = false, features = ["compat-mode"]} +lightningcss-napi = { version = "0.4.7", path = "../napi", features = [ + "bundler", + "visitor", +] } +napi = { version = "2.15.4", default-features = false, features = [ + "compat-mode", +] } napi-derive = "2" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/node/ast.d.ts b/node/ast.d.ts index 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 e429b0f2e..822944124 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -3,7 +3,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; use napi::{CallContext, JsObject, JsUnknown}; -use napi_derive::{js_function, module_exports}; +use napi_derive::js_function; #[js_function(1)] fn transform(ctx: CallContext) -> napi::Result { @@ -26,7 +26,7 @@ pub fn bundle_async(ctx: CallContext) -> napi::Result { lightningcss_napi::bundle_async(ctx) } -#[cfg_attr(not(target_arch = "wasm32"), module_exports)] +#[cfg_attr(not(target_arch = "wasm32"), napi_derive::module_exports)] fn init(mut exports: JsObject) -> napi::Result<()> { exports.create_named_method("transform", transform)?; exports.create_named_method("transformStyleAttribute", transform_style_attribute)?; @@ -45,7 +45,6 @@ pub fn register_module() { unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> { use napi::{Env, JsObject, NapiValue}; - let env = Env::from_raw(raw_env); let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); init(exports) } diff --git a/node/targets.d.ts b/node/targets.d.ts index c962f229f..ccc7c95f0 100644 --- a/node/targets.d.ts +++ b/node/targets.d.ts @@ -33,7 +33,8 @@ export const Features: { DoublePositionGradients: 131072, VendorPrefixes: 262144, LogicalProperties: 524288, + LightDark: 1048576, Selectors: 31, MediaQueries: 448, - Colors: 64512, + Colors: 1113088, }; diff --git a/node/test/bundle.test.mjs b/node/test/bundle.test.mjs index 50d113b57..4279e51c8 100644 --- a/node/test/bundle.test.mjs +++ b/node/test/bundle.test.mjs @@ -365,7 +365,7 @@ test('resolve return non-string', async () => { } if (!error) throw new Error(`\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`); - assert.equal(error.message, 'expect String, got: Number'); + assert.equal(error.message, 'data did not match any variant of untagged enum ResolveResult'); assert.equal(error.fileName, 'tests/testdata/foo.css'); assert.equal(error.loc, { line: 1, @@ -414,4 +414,29 @@ test('should support throwing in visitors', async () => { assert.equal(error.message, 'Some error'); }); +test('external import', async () => { + const { code: buffer } = await bundleAsync(/** @type {import('../index').BundleAsyncOptions} */ ({ + filename: 'tests/testdata/has_external.css', + resolver: { + resolve(specifier, originatingFile) { + if (specifier === './does_not_exist.css' || specifier.startsWith('https:')) { + return {external: specifier}; + } + return path.resolve(path.dirname(originatingFile), specifier); + } + } + })); + const code = buffer.toString('utf-8').trim(); + + const expected = ` +@import "https://fonts.googleapis.com/css2?family=Roboto&display=swap"; +@import "./does_not_exist.css"; + +.b { + height: calc(100vh - 64px); +} + `.trim(); + if (code !== expected) throw new Error(`\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`); +}); + test.run(); diff --git a/node/test/composeVisitors.test.mjs b/node/test/composeVisitors.test.mjs index bb95d9123..4379cf481 100644 --- a/node/test/composeVisitors.test.mjs +++ b/node/test/composeVisitors.test.mjs @@ -513,6 +513,87 @@ test('unknown rules', () => { assert.equal(res.code.toString(), '.menu_link{background:#056ef0}'); }); +test('custom at rules', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @testA; + @testB; + `), + customAtRules: { + testA: {}, + testB: {} + }, + visitor: composeVisitors([ + { + Rule: { + custom: { + testA(rule) { + return { + type: 'style', + value: { + loc: rule.loc, + selectors: [ + [{ type: 'class', name: 'testA' }] + ], + declarations: { + declarations: [ + { + property: 'color', + value: { + type: 'rgb', + r: 0xff, + g: 0x00, + b: 0x00, + alpha: 1, + } + } + ] + } + } + }; + } + } + } + }, + { + Rule: { + custom: { + testB(rule) { + return { + type: 'style', + value: { + loc: rule.loc, + selectors: [ + [{ type: 'class', name: 'testB' }] + ], + declarations: { + declarations: [ + { + property: 'color', + value: { + type: 'rgb', + r: 0x00, + g: 0xff, + b: 0x00, + alpha: 1, + } + } + ] + } + } + }; + } + } + } + } + ]) + }); + + assert.equal(res.code.toString(), '.testA{color:red}.testB{color:#0f0}'); +}); + test('known rules', () => { let declared = new Map(); let res = transform({ @@ -686,4 +767,94 @@ test('variables', () => { assert.equal(res.code.toString(), 'body{padding:20px;width:600px}'); }); +test('StyleSheet', () => { + let styleSheetCalledCount = 0; + let styleSheetExitCalledCount = 0; + transform({ + filename: 'test.css', + code: Buffer.from(` + body { + color: blue; + } + `), + visitor: composeVisitors([ + { + StyleSheet() { + styleSheetCalledCount++ + }, + StyleSheetExit() { + styleSheetExitCalledCount++ + } + }, + { + StyleSheet() { + styleSheetCalledCount++ + }, + StyleSheetExit() { + styleSheetExitCalledCount++ + } + } + ]) + }); + assert.equal(styleSheetCalledCount, 2); + assert.equal(styleSheetExitCalledCount, 2); +}); + +test('visitor function', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @dep "foo.js"; + @dep2 "bar.js"; + + .foo { + width: 32px; + } + `), + visitor: composeVisitors([ + ({addDependency}) => ({ + Rule: { + unknown: { + dep(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }), + ({addDependency}) => ({ + Rule: { + unknown: { + dep2(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }) + ]) + }); + + assert.equal(res.code.toString(), '.foo{width:32px}'); + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'foo.js' + }, + { + type: 'file', + filePath: 'bar.js' + } + ]); +}); + test.run(); diff --git a/node/test/visitor.test.mjs b/node/test/visitor.test.mjs index 3a42a696b..149825b7d 100644 --- a/node/test/visitor.test.mjs +++ b/node/test/visitor.test.mjs @@ -249,6 +249,68 @@ test('specific environment variables', () => { assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}'); }); +test('spacing with env substitution', () => { + // Test spacing for different cases when `env()` functions are replaced with actual values. + /** @type {Record} */ + let tokens = { + '--var1': 'var(--foo)', + '--var2': 'var(--bar)', + '--function': 'scale(1.5)', + '--length1': '10px', + '--length2': '20px', + '--x': '4', + '--y': '12', + '--num1': '5', + '--num2': '10', + '--num3': '15', + '--counter': '2', + '--ident1': 'solid', + '--ident2': 'auto', + '--rotate': '45deg', + '--percentage1': '25%', + '--percentage2': '75%', + '--color': 'red', + '--color1': '#ff1234', + '--string1': '"hello"', + '--string2': '" world"' + }; + + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + .test { + /* Asymmetric spacing - no space after var(). */ + background: env(--var1) env(--var2); + border: env(--var1)env(--ident1); + transform: env(--function) env(--function); + /* Normal spacing between values. */ + padding: env(--length1) env(--length2); + margin: env(--length1) env(--ident2); + outline: env(--color) env(--ident1); + /* Raw numbers that need spacing. */ + cursor: url(cursor.png) env(--x) env(--y), auto; + stroke-dasharray: env(--num1) env(--num2) env(--num3); + counter-increment: myCounter env(--counter); + /* Mixed token types. */ + background: linear-gradient(red env(--percentage1), blue env(--percentage2)); + content: env(--string1) env(--string2); + /* Inside calc expressions. */ + width: calc(env(--length1) - env(--length2)); + } + `), + visitor: { + EnvironmentVariable(env) { + if (env.name.type === 'custom' && tokens[env.name.ident]) { + return { raw: tokens[env.name.ident] }; + } + } + } + }); + + assert.equal(res.code.toString(), '.test{background:var(--foo) var(--bar);border:var(--foo)solid;transform:scale(1.5) scale(1.5);padding:10px 20px;margin:10px auto;outline:red solid;cursor:url(cursor.png) 4 12, auto;stroke-dasharray:5 10 15;counter-increment:myCounter 2;background:linear-gradient(red 25%, blue 75%);content:"hello" " world";width:calc(10px - 20px)}'); +}); + test('url', () => { // https://www.npmjs.com/package/postcss-url let res = transform({ @@ -1108,4 +1170,119 @@ test('visit stylesheet', () => { assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}'); }); +test('visitor function', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @dep "foo.js"; + + .foo { + width: 32px; + } + `), + visitor: ({addDependency}) => ({ + Rule: { + unknown: { + dep(rule) { + let file = rule.prelude[0].value.value; + addDependency({ + type: 'file', + filePath: file + }); + return []; + } + } + } + }) + }); + + assert.equal(res.code.toString(), '.foo{width:32px}'); + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'foo.js' + }]); +}); + +test('visitor function works with style attributes', () => { + let res = transformStyleAttribute({ + filename: 'test.css', + minify: true, + code: Buffer.from('height: 12px'), + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [{ + type: 'file', + filePath: 'test.json' + }]); +}); + +test('visitor function works with bundler', () => { + let res = bundle({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + +test('works with async bundler', async () => { + let res = await bundleAsync({ + filename: 'tests/testdata/a.css', + minify: true, + visitor: ({addDependency}) => ({ + Length() { + addDependency({ + type: 'file', + filePath: 'test.json' + }); + } + }) + }); + + assert.equal(res.dependencies, [ + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + }, + { + type: 'file', + filePath: 'test.json' + } + ]); +}); + test.run(); diff --git a/package.json b/package.json index c9fe3d723..af17c0a58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightningcss", - "version": "1.24.1", + "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 f13b7c485..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,32 +7262,200 @@ 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_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" { @@ -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,23 +11797,94 @@ 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}"); - minify_test(".foo { animation-name: none, none }", ".foo{animation-name:none,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 minify_test(".foo { animation-name: unset }", ".foo{animation-name:unset}"); @@ -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,112 +12334,436 @@ 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)", + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: 10%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: 10% exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#, + indoc! {r#" + .foo { + animation-range: entry; + animation-range-end: var(--end); + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%; + } + "#, + indoc! {r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range-start: entry 10%, entry 50%; + animation-range-end: exit 90%, exit 100%; + } + "#, + indoc! {r#" + .foo { + animation-range: entry 10% exit 90%, entry 50% exit; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + animation: spin 100ms; + } + "#, + indoc! {r#" + .foo { + animation: .1s spin; + } + "#}, + ); + test( + r#" + .foo { + animation: spin 100ms; + animation-range: entry; + animation-range-end: 90%; + } + "#, + indoc! {r#" + .foo { + animation: .1s spin; + animation-range: entry 90%; + } + "#}, + ); + test( + r#" + .foo { + animation-range: entry; + animation-range-end: 90%; + animation: var(--animation) 100ms; + } + "#, + indoc! {r#" + .foo { + animation: var(--animation) .1s; + } + "#}, + ); + } + + #[test] + fn test_transform() { + test( + ".foo { transform: perspective(500px)translate3d(10px, 0, 20px)rotateY(30deg) }", + indoc! {r#" + .foo { + transform: perspective(500px) translate3d(10px, 0, 20px) rotateY(30deg); + } + "#}, + ); + test( + ".foo { transform: translate3d(12px,50%,3em)scale(2,.5) }", + indoc! {r#" + .foo { + transform: translate3d(12px, 50%, 3em) scale(2, .5); + } + "#}, + ); + test( + ".foo { transform:matrix(1,2,-1,1,80,80) }", + indoc! {r#" + .foo { + transform: matrix(1, 2, -1, 1, 80, 80); + } + "#}, + ); + + minify_test( + ".foo { transform: scale( 0.5 )translateX(10px ) }", + ".foo{transform:scale(.5)translate(10px)}", + ); + minify_test( + ".foo { transform: translate(2px, 3px)", + ".foo{transform:translate(2px,3px)}", + ); + minify_test( + ".foo { transform: translate(2px, 0px)", + ".foo{transform:translate(2px)}", + ); + minify_test( + ".foo { transform: translate(0px, 2px)", + ".foo{transform:translateY(2px)}", + ); + minify_test(".foo { transform: translateX(2px)", ".foo{transform:translate(2px)}"); + minify_test(".foo { transform: translateY(2px)", ".foo{transform:translateY(2px)}"); + minify_test(".foo { transform: translateZ(2px)", ".foo{transform:translateZ(2px)}"); + minify_test( + ".foo { transform: translate3d(2px, 3px, 4px)", + ".foo{transform:translate3d(2px,3px,4px)}", + ); + minify_test( + ".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(0px, 2px, 0px)", + ".foo{transform:translateY(2px)}", + ); + minify_test( + ".foo { transform: translate3d(0px, 0px, 2px)", + ".foo{transform:translateZ(2px)}", + ); + minify_test( + ".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: rotate3d(2, 3, 4, 20deg)", + ".foo{transform:rotate3d(2,3,4,20deg)}", + ); + minify_test( + ".foo { transform: rotate3d(1, 0, 0, 20deg)", + ".foo{transform:rotateX(20deg)}", + ); + minify_test( + ".foo { transform: rotate3d(0, 1, 0, 20deg)", + ".foo{transform:rotateY(20deg)}", + ); + minify_test( + ".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( @@ -11645,21 +12863,36 @@ 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}"); @@ -12079,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); } "#}, @@ -12097,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); } "#}, @@ -12115,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); } "#}, @@ -12133,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); } "#}, @@ -12151,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); } "#}, @@ -12169,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); } "#}, @@ -12187,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); } "#}, @@ -12221,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); } "#}, @@ -12345,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"); } "#}, @@ -12427,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)); } @@ -12443,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)); } @@ -12514,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)); } @@ -12530,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)); } @@ -12566,10 +13799,361 @@ mod tests { ..Browsers::default() }, ); - } - #[test] - fn test_font_face() { + // Test cases from https://github.com/postcss/autoprefixer/blob/541295c0e6dd348db2d3f52772b59cd403c59d29/test/cases/gradient.css + prefix_test( + r#" + a { + background: linear-gradient(350.5deg, white, black), linear-gradient(-130deg, black, white), linear-gradient(45deg, black, white); + } + b { + background-image: linear-gradient(rgba(0,0,0,1), white), linear-gradient(white, black); + } + strong { + background: linear-gradient(to top, transparent, rgba(0, 0, 0, 0.8) 20px, #000 30px, #000) no-repeat; + } + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + .simple1 { + background: linear-gradient(black, white); + } + .simple2 { + background: linear-gradient(to left, black 0%, rgba(0, 0, 0, 0.5)50%, white 100%); + } + .simple3 { + background: linear-gradient(to left, black 50%, white 100%); + } + .simple4 { + background: linear-gradient(to right top, black, white); + } + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, 0.5), white); + } + .silent { + background: -webkit-linear-gradient(top left, black, white); + } + .radial { + background: radial-gradient(farthest-side at 0 50%, white, black); + } + .second { + background: red linear-gradient(red, blue); + background: url('logo.png'), linear-gradient(#fff, #000); + } + .px { + background: linear-gradient(black 0, white 100px); + } + .list { + list-style-image: linear-gradient(white, black); + } + .mask { + mask: linear-gradient(white, black); + } + .newline { + background-image: + linear-gradient( white, black ), + linear-gradient( black, white ); + } + .convert { + background: linear-gradient(0deg, white, black); + background: linear-gradient(90deg, white, black); + background: linear-gradient(180deg, white, black); + background: linear-gradient(270deg, white, black); + } + .grad { + background: linear-gradient(1grad, white, black); + } + .rad { + background: linear-gradient(1rad, white, black); + } + .turn { + background: linear-gradient(0.3turn, white, black); + } + .norm { + background: linear-gradient(-90deg, white, black); + } + .mask { + mask-image: radial-gradient(circle at 86% 86%, transparent 8px, black 8px); + } + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + .contain { + background: radial-gradient(contain at center, white, black); + } + .no-div { + background: linear-gradient(black); + } + .background-shorthand { + background: radial-gradient(#FFF, transparent) 0 0 / cover no-repeat #F0F; + } + .background-advanced { + background: radial-gradient(ellipse farthest-corner at 5px 15px, rgba(214, 168, 18, 0.7) 0%, rgba(255, 21, 177, 0.7) 50%, rgba(210, 7, 148, 0.7) 95%), + radial-gradient(#FFF, transparent), + url(path/to/image.jpg) 50%/cover; + } + .multiradial { + mask-image: radial-gradient(circle closest-corner at 100% 50%, #000, transparent); + } + .broken { + mask-image: radial-gradient(white, black); + } + .loop { + background-image: url("https://test.com/lol(test.png"), radial-gradient(yellow, black, yellow); + } + .unitless-zero { + background-image: linear-gradient(0, green, blue); + background: repeating-linear-gradient(0, blue, red 33.3%) + } + .zero-grad { + background: linear-gradient(0grad, green, blue); + background-image: repeating-linear-gradient(0grad, blue, red 33.3%) + } + .zero-rad { + background: linear-gradient(0rad, green, blue); + } + .zero-turn { + background: linear-gradient(0turn, green, blue); + } + "#, + indoc! { r#" + a { + background: -webkit-linear-gradient(99.5deg, #fff, #000), -webkit-linear-gradient(220deg, #000, #fff), -webkit-linear-gradient(45deg, #000, #fff); + background: -o-linear-gradient(99.5deg, #fff, #000), -o-linear-gradient(220deg, #000, #fff), -o-linear-gradient(45deg, #000, #fff); + background: linear-gradient(350.5deg, #fff, #000), linear-gradient(-130deg, #000, #fff), linear-gradient(45deg, #000, #fff); + } + + b { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)), -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + background-image: -webkit-linear-gradient(top, #000, #fff), -webkit-linear-gradient(top, #fff, #000); + background-image: -o-linear-gradient(top, #000, #fff), -o-linear-gradient(top, #fff, #000); + background-image: linear-gradient(#000, #fff), linear-gradient(#fff, #000); + } + + strong { + background: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: -o-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + background: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat; + } + + div { + background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red); + } + + .old-radial { + background: radial-gradient(0 50%, ellipse farthest-corner, black, white); + } + + .simple1 { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background: -webkit-linear-gradient(top, #000, #fff); + background: -o-linear-gradient(top, #000, #fff); + background: linear-gradient(#000, #fff); + } + + .simple2 { + background: -webkit-gradient(linear, 100% 0, 0 0, from(#000), color-stop(.5, rgba(0, 0, 0, .5)), to(#fff)); + background: -webkit-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: -o-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + background: linear-gradient(to left, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%); + } + + .simple3 { + background: -webkit-gradient(linear, 100% 0, 0 0, color-stop(.5, #000), to(#fff)); + background: -webkit-linear-gradient(right, #000 50%, #fff 100%); + background: -o-linear-gradient(right, #000 50%, #fff 100%); + background: linear-gradient(to left, #000 50%, #fff 100%); + } + + .simple4 { + background: -webkit-gradient(linear, 0 100%, 100% 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(bottom left, #000, #fff); + background: -o-linear-gradient(bottom left, #000, #fff); + background: linear-gradient(to top right, #000, #fff); + } + + .direction { + background: linear-gradient(top left, black, rgba(0, 0, 0, .5), white); + } + + .silent { + background: -webkit-gradient(linear, 100% 100%, 0 0, from(#000), to(#fff)); + background: -webkit-linear-gradient(top left, #000, #fff); + } + + .radial { + background: -webkit-radial-gradient(farthest-side at 0, #fff, #000); + background: -o-radial-gradient(farthest-side at 0, #fff, #000); + background: radial-gradient(farthest-side at 0, #fff, #000); + } + + .second { + background: red -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f)); + background: red -webkit-linear-gradient(top, red, #00f); + background: red -o-linear-gradient(top, red, #00f); + background: red linear-gradient(red, #00f); + background: url("logo.png"), linear-gradient(#fff, #000); + } + + .px { + background: -webkit-linear-gradient(top, #000 0, #fff 100px); + background: -o-linear-gradient(top, #000 0, #fff 100px); + background: linear-gradient(#000 0, #fff 100px); + } + + .list { + list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + list-style-image: -webkit-linear-gradient(top, #fff, #000); + list-style-image: -o-linear-gradient(top, #fff, #000); + list-style-image: linear-gradient(#fff, #000); + } + + .mask { + -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)); + -webkit-mask: -webkit-linear-gradient(top, #fff, #000); + -webkit-mask: -o-linear-gradient(top, #fff, #000); + mask: -o-linear-gradient(top, #fff, #000); + -webkit-mask: linear-gradient(#fff, #000); + mask: linear-gradient(#fff, #000); + } + + .newline { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)), -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)); + background-image: -webkit-linear-gradient(top, #fff, #000), -webkit-linear-gradient(top, #000, #fff); + background-image: -o-linear-gradient(top, #fff, #000), -o-linear-gradient(top, #000, #fff); + background-image: linear-gradient(#fff, #000), linear-gradient(#000, #fff); + } + + .convert { + background: -webkit-gradient(linear, 0 100%, 0 0, from(#fff), to(#000)); + background: -webkit-linear-gradient(90deg, #fff, #000); + background: -o-linear-gradient(90deg, #fff, #000); + background: linear-gradient(0deg, #fff, #000); + background: linear-gradient(90deg, #fff, #000); + background: linear-gradient(#fff, #000); + background: linear-gradient(270deg, #fff, #000); + } + + .grad { + background: -webkit-linear-gradient(89.1deg, #fff, #000); + background: -o-linear-gradient(89.1deg, #fff, #000); + background: linear-gradient(1grad, #fff, #000); + } + + .rad { + background: -webkit-linear-gradient(32.704deg, #fff, #000); + background: -o-linear-gradient(32.704deg, #fff, #000); + background: linear-gradient(57.2958deg, #fff, #000); + } + + .turn { + background: -webkit-linear-gradient(342deg, #fff, #000); + background: -o-linear-gradient(342deg, #fff, #000); + background: linear-gradient(.3turn, #fff, #000); + } + + .norm { + background: -webkit-linear-gradient(#fff, #000); + background: -o-linear-gradient(#fff, #000); + background: linear-gradient(-90deg, #fff, #000); + } + + .mask { + -webkit-mask-image: -webkit-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + -webkit-mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px); + } + + .cover { + background: radial-gradient(ellipse cover at center, white, black); + } + + .contain { + background: radial-gradient(contain at center, white, black); + } + + .no-div { + background: -webkit-gradient(linear, 0 0, 0 100%, from(#000)); + background: -webkit-linear-gradient(top, #000); + background: -o-linear-gradient(top, #000); + background: linear-gradient(#000); + } + + .background-shorthand { + background: #f0f -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f -o-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + background: #f0f radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat; + } + + .background-advanced { + background: url("path/to/image.jpg") 50% / cover; + background: -webkit-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: -o-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -o-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + background: radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover; + } + + .multiradial { + -webkit-mask-image: -webkit-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + -webkit-mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0)); + } + + .broken { + -webkit-mask-image: -webkit-radial-gradient(#fff, #000); + -webkit-mask-image: -o-radial-gradient(#fff, #000); + mask-image: -o-radial-gradient(#fff, #000); + -webkit-mask-image: radial-gradient(#fff, #000); + mask-image: radial-gradient(#fff, #000); + } + + .loop { + background-image: url("https://test.com/lol(test.png"); + background-image: url("https://test.com/lol(test.png"), -webkit-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), -o-radial-gradient(#ff0, #000, #ff0); + background-image: url("https://test.com/lol(test.png"), radial-gradient(#ff0, #000, #ff0); + } + + .unitless-zero { + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background-image: -webkit-linear-gradient(90deg, green, #00f); + background-image: -o-linear-gradient(90deg, green, #00f); + background-image: linear-gradient(0deg, green, #00f); + background: repeating-linear-gradient(0deg, #00f, red 33.3%); + } + + .zero-grad { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0grad, green, #00f); + background-image: repeating-linear-gradient(0grad, #00f, red 33.3%); + } + + .zero-rad, .zero-turn { + background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f)); + background: -webkit-linear-gradient(90deg, green, #00f); + background: -o-linear-gradient(90deg, green, #00f); + background: linear-gradient(0deg, green, #00f); + } + "#}, + Browsers { + chrome: Some(25 << 16), + opera: Some(12 << 16), + android: Some(2 << 16 | 3 << 8), + ..Browsers::default() + }, + ); + } + + #[test] + fn test_font_face() { minify_test( r#"@font-face { src: url("test.woff"); @@ -12636,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 @@ -12658,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}"); @@ -12756,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; @@ -12800,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}"); @@ -13388,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] @@ -14490,6 +16216,27 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + .foo { + text-decoration: lab(50.998% 125.506 -50.7078) var(--style); + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" .foo { @@ -14857,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] @@ -14944,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] @@ -15654,21 +17443,42 @@ mod tests { ..Browsers::default() }, ); - } - #[test] - fn test_list() { - minify_test(".foo { list-style-type: disc; }", ".foo{list-style-type:disc}"); - minify_test(".foo { list-style-type: \"★\"; }", ".foo{list-style-type:\"★\"}"); - minify_test( - ".foo { list-style-type: symbols(cyclic '○' '●'); }", - ".foo{list-style-type:symbols(cyclic \"○\" \"●\")}", - ); - minify_test( - ".foo { list-style-type: symbols('○' '●'); }", - ".foo{list-style-type:symbols(\"○\" \"●\")}", - ); - minify_test( + 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] + fn test_list() { + minify_test(".foo { list-style-type: disc; }", ".foo{list-style-type:disc}"); + minify_test(".foo { list-style-type: \"★\"; }", ".foo{list-style-type:\"★\"}"); + minify_test( + ".foo { list-style-type: symbols(cyclic '○' '●'); }", + ".foo{list-style-type:symbols(cyclic \"○\" \"●\")}", + ); + minify_test( + ".foo { list-style-type: symbols('○' '●'); }", + ".foo{list-style-type:symbols(\"○\" \"●\")}", + ); + minify_test( ".foo { list-style-type: symbols(symbolic '○' '●'); }", ".foo{list-style-type:symbols(\"○\" \"●\")}", ); @@ -15686,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( @@ -15727,7 +17548,7 @@ mod tests { "#, indoc! {r#" .foo { - list-style: \"★\" url("ellipse.png"); + list-style: url("ellipse.png") \"★\"; list-style-image: var(--img); } "#}, @@ -15738,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)); } @@ -15753,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 { @@ -15781,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] @@ -15981,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}"); @@ -15992,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)}", @@ -16010,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)}", @@ -16030,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); }", @@ -16070,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}"); @@ -16573,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 { @@ -16675,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 { @@ -16852,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))", @@ -16868,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))}", @@ -16987,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). @@ -17125,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)", ); @@ -17263,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)", ); @@ -17726,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), @@ -17734,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), @@ -17753,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(). @@ -18622,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}", ); } } @@ -18731,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: @@ -20034,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( @@ -20103,7 +22094,6 @@ mod tests { } } - #[cfg(feature = "grid")] #[test] fn test_grid() { minify_test( @@ -20251,82 +22241,290 @@ mod tests { ".foo{grid-template-areas:\"head head\"\"nav main\"\". .\"}", ); + // to grid-* shorthand + minify_test( + r#" + .test-miss-areas { + grid-template-columns: 1fr 90px; + grid-template-rows: auto 80px; + grid-template-areas: "one"; + } + "#, + ".test-miss-areas{grid-template:\"one\"\".\"80px/1fr 90px}", + ); test( r#" - .foo { - grid-template-areas: "head head" "nav main" "foot ...."; + .test-miss-areas-2 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 30px 60px 100px; + grid-template-areas: "a a a" "b c c"; } "#, indoc! { r#" - .foo { - grid-template-areas: "head head" - "nav main" - "foot ."; + .test-miss-areas-2 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; } "#}, ); - minify_test( + test( r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom]; + .test-miss-areas-3 { + grid-template: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", - ); - minify_test( - r#" - .foo { - grid-template: "head head" - "nav main" 1fr - "foot ...."; + indoc! { r#" + .test-miss-areas-3 { + grid-template: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; } - "#, - ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}", + "#}, ); - minify_test( + + test( r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom] - / auto 1fr auto; + .test-miss-areas-4 { + grid: 30px 60px 100px / 1fr 1fr 1fr; + grid-template-areas: "a a a" "b c c"; } "#, - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", + indoc! { r#" + .test-miss-areas-4 { + grid: "a a a" 30px + "b c c" 60px + ". . ." 100px + / 1fr 1fr 1fr; + } + "#}, ); + // test no unreachable error minify_test( - ".foo { grid-template: auto 1fr / auto 1fr auto; }", - ".foo{grid-template:auto 1fr/auto 1fr auto}", + r#" + .grid-shorthand-areas { + grid: auto / 1fr 3fr; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas{grid:\".content.\"/1fr 3fr}", ); minify_test( - ".foo { grid-template: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3] / [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }", - ".foo{grid-template:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]/[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}" + r#" + .grid-shorthand-areas-rows { + grid: auto / 1fr 3fr; + grid-template-rows: 20px; + grid-template-areas: ". content ."; + } + "#, + ".grid-shorthand-areas-rows{grid:\".content.\"20px/1fr 3fr}", ); + // test grid-auto-flow: row in grid shorthand test( - ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", - indoc! {r#" - .foo { - grid-template: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom] - / auto 1fr auto; - } - "#}, + 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( - ".foo{grid-template:[header-top]\"a a a\"[main-top]\"b b b\"1fr/auto 1fr auto}", - indoc! {r#" - .foo { - grid-template: [header-top] "a a a" - [main-top] "b b b" 1fr - / auto 1fr auto; - } - "#}, - ); - - minify_test(".foo { grid-auto-flow: row }", ".foo{grid-auto-flow:row}"); + r#" + .test-auto-flow-row-2 { + grid: auto-flow auto / 100px 100px; + grid-template-areas: " one two "; + } + "#, + indoc! { r#" + .test-auto-flow-row-2 { + grid: auto-flow / 100px 100px; + grid-template-areas: "one two"; + } + "#}, + ); + test( + r#" + .test-auto-flow-dense { + grid: dense auto-flow / 1fr 2fr; + grid-template-areas: " . content . "; + } + "#, + indoc! { r#" + .test-auto-flow-dense { + grid: auto-flow dense / 1fr 2fr; + grid-template-areas: ". content ."; + } + "#}, + ); + minify_test( + r#" + .grid-auto-flow-row-auto-rows { + grid: auto-flow 40px / 1fr 90px; + grid-template-areas: "a"; + } + "#, + ".grid-auto-flow-row-auto-rows{grid:auto-flow 40px/1fr 90px;grid-template-areas:\"a\"}", + ); + minify_test( + r#" + .grid-auto-flow-row-auto-rows-multiple { + grid: auto-flow 40px max-content / 1fr; + grid-template-areas: ". a"; + } + "#, + ".grid-auto-flow-row-auto-rows-multiple{grid:auto-flow 40px max-content/1fr;grid-template-areas:\".a\"}", + ); + + // test grid-auto-flow: column in grid shorthand + test( + r#" + .test-auto-flow-column-1 { + grid: 300px / auto-flow; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-1 { + grid: 300px / auto-flow; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-column-2 { + grid: 200px 1fr / auto-flow auto; + grid-template-areas: " . one . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-2 { + grid: 200px 1fr / auto-flow; + grid-template-areas: ". one ."; + } + "#}, + ); + test( + r#" + .test-auto-flow-column-dense { + grid: 1fr 2fr / dense auto-flow; + grid-template-areas: " . content . "; + } + "#, + indoc! { r#" + .test-auto-flow-column-dense { + grid: 1fr 2fr / auto-flow dense; + grid-template-areas: ". content ."; + } + "#}, + ); + minify_test( + r#" + .grid-auto-flow-column-auto-rows { + grid: 1fr 3fr / auto-flow 40px; + grid-template-areas: "a"; + } + "#, + ".grid-auto-flow-column-auto-rows{grid:1fr 3fr/auto-flow 40px;grid-template-areas:\"a\"}", + ); + minify_test( + r#" + .grid-auto-flow-column-auto-rows-multiple { + grid: 1fr / auto-flow 40px max-content ; + grid-template-areas: ". a"; + } + "#, + ".grid-auto-flow-column-auto-rows-multiple{grid:1fr/auto-flow 40px max-content;grid-template-areas:\".a\"}", + ); + + test( + r#" + .foo { + grid-template-areas: "head head" "nav main" "foot ...."; + } + "#, + indoc! { r#" + .foo { + grid-template-areas: "head head" + "nav main" + "foot ."; + } + "#}, + ); + + minify_test( + r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom]; + } + "#, + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}", + ); + minify_test( + r#" + .foo { + grid-template: "head head" + "nav main" 1fr + "foot ...."; + } + "#, + ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}", + ); + minify_test( + r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom] + / auto 1fr auto; + } + "#, + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", + ); + + minify_test( + ".foo { grid-template: auto 1fr / auto 1fr auto; }", + ".foo{grid-template:auto 1fr/auto 1fr auto}", + ); + minify_test( + ".foo { grid-template: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3] / [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }", + ".foo{grid-template:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]/[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}" + ); + + test( + ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}", + indoc! {r#" + .foo { + grid-template: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom] + / auto 1fr auto; + } + "#}, + ); + test( + ".foo{grid-template:[header-top]\"a a a\"[main-top]\"b b b\"1fr/auto 1fr auto}", + indoc! {r#" + .foo { + grid-template: [header-top] "a a a" + [main-top] "b b b" 1fr + / auto 1fr auto; + } + "#}, + ); + + minify_test(".foo { grid-auto-flow: row }", ".foo{grid-auto-flow:row}"); minify_test(".foo { grid-auto-flow: column }", ".foo{grid-auto-flow:column}"); minify_test(".foo { grid-auto-flow: row dense }", ".foo{grid-auto-flow:dense}"); minify_test(".foo { grid-auto-flow: dense row }", ".foo{grid-auto-flow:dense}"); @@ -20930,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)); }", @@ -20950,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#" @@ -20979,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 { @@ -21002,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 { @@ -21032,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 { @@ -21202,13 +23537,35 @@ mod tests { prefix_test( r#" - .foo { - --foo: color(display-p3 0 1 0); + @supports (color: color(display-p3 0 0 0)) { + .foo { + --foo: color(display-p3 0 1 0); + } } "#, indoc! {r#" - .foo { - --foo: color(display-p3 0 1 0); + @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 { + --foo: color(display-p3 0 1 0); + } + "#, + indoc! {r#" + .foo { + --foo: color(display-p3 0 1 0); } "#}, Browsers { @@ -21364,6 +23721,39 @@ mod tests { }, ); + prefix_test( + r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#, + indoc! {r#" + @supports (color: lab(0% 0 0)) { + @keyframes foo { + from { + --custom: lab(40% 56.6 39); + } + + to { + --custom: lab(50.998% 125.506 -50.7078); + } + } + } + "#}, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + prefix_test( r#" @keyframes foo { @@ -21418,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 { @@ -21556,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), @@ -21777,7 +24225,7 @@ mod tests { grid-auto-flow: column; } - @media (min-width: 1024px) { + @media not (max-width: 1024px) { .foo { max-inline-size: 1024px; } @@ -22644,13 +25092,13 @@ mod tests { } "#, indoc! {r#" - .foo { - color: red; - } - .foo .bar { color: #00f; } + + .foo { + color: red; + } "#}, ); @@ -22664,12 +25112,16 @@ mod tests { "#, indoc! {r#" article { - color: red; + color: green; } article { color: #00f; } + + article { + color: red; + } "#}, ); @@ -22778,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; } + } "#}, ); @@ -22954,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] @@ -23048,75 +25575,168 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" - body { - grid: [header-top] "a a a" [header-bottom] - [main-top] "b b b" 1fr [main-bottom] - / auto 1fr auto; + .foo { + color: red; } - header { - grid-area: a; + #id { + animation: 2s test; } - main { - grid-row: main-top / main-bottom; + @keyframes test { + from { color: red } + to { color: yellow } } "#, indoc! {r#" - body { - grid: [EgL3uq_header-top] "EgL3uq_a EgL3uq_a EgL3uq_a" [EgL3uq_header-bottom] - [EgL3uq_main-top] "EgL3uq_b EgL3uq_b EgL3uq_b" 1fr [EgL3uq_main-bottom] - / auto 1fr auto; + .EgL3uq_foo { + color: red; } - header { - grid-area: EgL3uq_a; + #EgL3uq_id { + animation: 2s test; } - main { - grid-row: EgL3uq_main-top / EgL3uq_main-bottom; + @keyframes test { + from { + color: red; + } + + to { + color: #ff0; + } } "#}, map! { - "header-top" => "EgL3uq_header-top", - "header-bottom" => "EgL3uq_header-bottom", - "main-top" => "EgL3uq_main-top", - "main-bottom" => "EgL3uq_main-bottom", - "a" => "EgL3uq_a", - "b" => "EgL3uq_b" + "foo" => "EgL3uq_foo", + "id" => "EgL3uq_id" }, HashMap::new(), - Default::default(), + crate::css_modules::Config { + animation: false, + // custom_idents: false, + ..Default::default() + }, + false, ); - #[cfg(feature = "grid")] css_modules_test( r#" - .grid { - grid-template-areas: "foo"; - } + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } - .foo { - grid-area: foo; - } + ul { + list-style: circles; + } - .bar { - grid-column-start: foo-start; - } - "#, + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#, indoc! {r#" - .EgL3uq_grid { - grid-template-areas: "EgL3uq_foo"; - } + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } - .EgL3uq_foo { - grid-area: EgL3uq_foo; - } + 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, + ); + + css_modules_test( + r#" + body { + grid: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom] + / auto 1fr auto; + } + + header { + grid-area: a; + } + + main { + grid-row: main-top / main-bottom; + } + "#, + indoc! {r#" + body { + grid: [EgL3uq_header-top] "EgL3uq_a EgL3uq_a EgL3uq_a" [EgL3uq_header-bottom] + [EgL3uq_main-top] "EgL3uq_b EgL3uq_b EgL3uq_b" 1fr [EgL3uq_main-bottom] + / auto 1fr auto; + } + + header { + grid-area: EgL3uq_a; + } + + main { + grid-row: EgL3uq_main-top / EgL3uq_main-bottom; + } + "#}, + map! { + "header-top" => "EgL3uq_header-top", + "header-bottom" => "EgL3uq_header-bottom", + "main-top" => "EgL3uq_main-top", + "main-bottom" => "EgL3uq_main-bottom", + "a" => "EgL3uq_a", + "b" => "EgL3uq_b" + }, + HashMap::new(), + Default::default(), + false, + ); + + css_modules_test( + r#" + .grid { + grid-template-areas: "foo"; + } + + .foo { + grid-area: foo; + } + + .bar { + grid-column-start: foo-start; + } + "#, + indoc! {r#" + .EgL3uq_grid { + grid-template-areas: "EgL3uq_foo"; + } + + .EgL3uq_foo { + grid-area: EgL3uq_foo; + } .EgL3uq_bar { grid-column-start: EgL3uq_foo-start; @@ -23130,6 +25750,47 @@ mod tests { }, HashMap::new(), Default::default(), + false, + ); + + css_modules_test( + r#" + .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( @@ -23146,6 +25807,7 @@ mod tests { map! {}, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23180,6 +25842,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); // :global(:local(.hi)) { @@ -23212,6 +25875,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23241,6 +25905,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23278,6 +25943,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23297,6 +25963,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23316,6 +25983,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23335,6 +26003,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23354,6 +26023,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23379,164 +26049,491 @@ mod tests { } "#}, map! { - "test" => "EgL3uq_test" "EgL3uq_foo" "foo" from "foo.css" "bar" from "bar.css", - "foo" => "EgL3uq_foo" + "test" => "EgL3uq_test" "EgL3uq_foo" "foo" from "foo.css" "bar" from "bar.css", + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + 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(), - Default::default(), + 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( @@ -23575,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] @@ -23858,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}"); @@ -24116,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); @@ -24148,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)); @@ -24210,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; @@ -24258,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#" @@ -24643,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] @@ -24814,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( @@ -24904,7 +28087,7 @@ mod tests { } } "#, - ".foo{@scope(.bar){&{color:#ff0}}}", + ".foo{@scope(.bar){color:#ff0}}", ); nesting_test( r#" @@ -24916,9 +28099,7 @@ mod tests { "#, indoc! {r#" @scope (.bar) { - :scope { - color: #ff0; - } + color: #ff0; } "#}, ); @@ -25308,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; } @@ -26058,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 { @@ -26253,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] @@ -26340,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}; @@ -26368,6 +29644,14 @@ mod tests { color: red; } } + + input:placeholder { + color: red; + } + + input::hover { + color: red; + } "#, indoc! { r#" .foo { @@ -26383,6 +29667,14 @@ mod tests { color: red; } } + + input:placeholder { + color: red; + } + + input::hover { + color: red; + } "#}, ParserOptions { filename: "test.css".into(), @@ -26420,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, + }), + }, ] ) } @@ -26438,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#" @@ -26748,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 @@ -26788,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, ); } @@ -26806,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 { @@ -26970,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)) { @@ -26995,6 +30438,7 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); } @@ -27074,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; }", @@ -27182,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#" @@ -27243,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 845140f98..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::*; @@ -688,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. @@ -710,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)?)), _ => {} }; @@ -731,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()) } @@ -779,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) } @@ -838,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() { @@ -847,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(()) @@ -966,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() @@ -981,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>) } @@ -1054,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)) + } } } } @@ -1134,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"); @@ -1348,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], @@ -1467,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/ @@ -1513,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, @@ -1596,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), @@ -1605,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 { @@ -1667,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 75cd909a6..cc00e5783 100644 --- a/src/properties/transform.rs +++ b/src/properties/transform.rs @@ -7,7 +7,6 @@ use crate::error::{ParserError, PrinterError}; use crate::macros::enum_property; use crate::prefixes::Feature; use crate::printer::Printer; -use crate::stylesheet::PrinterOptions; use crate::traits::{Parse, PropertyHandler, ToCss, Zero}; use crate::values::{ angle::Angle, @@ -59,20 +58,6 @@ impl ToCss for TransformList { // TODO: Re-enable with a better solution // See: https://github.com/parcel-bundler/lightningcss/issues/288 - if dest.minify { - let mut base = String::new(); - self.to_css_base(&mut Printer::new( - &mut base, - PrinterOptions { - minify: true, - ..PrinterOptions::default() - }, - ))?; - - dest.write_str(&base)?; - - return Ok(()); - } // if dest.minify { // // Combine transforms into a single matrix. // if let Some(matrix) = self.to_matrix() { @@ -141,7 +126,13 @@ impl TransformList { where W: std::fmt::Write, { + let mut first = true; for item in &self.0 { + if first { + first = false; + } else { + dest.whitespace()?; + } item.to_css(dest)?; } Ok(()) @@ -1382,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, } } @@ -1391,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, } } @@ -1413,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", @@ -1429,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)?; @@ -1484,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()), @@ -1497,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(()) } } @@ -1513,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); @@ -1566,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 }) } } @@ -1575,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)?; @@ -1637,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)), @@ -1650,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)?; + } + } } } @@ -1668,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 04f1fdd33..a8deccd1b 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -7,8 +7,9 @@ use super::{CssRuleList, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; +use crate::properties::custom::TokenList; use crate::properties::PropertyId; -use crate::targets::Targets; +use crate::targets::{Features, FeaturesIterator, Targets}; use crate::traits::{Parse, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -42,8 +43,19 @@ impl<'i, T: Clone> SupportsRule<'i, T> { context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool, ) -> Result<(), MinifyError> { - self.condition.set_prefixes_for_targets(&context.targets); - self.rules.minify(context, parent_is_unused) + let inserted = context.targets.enter_supports(self.condition.get_supported_features()); + if inserted { + context.handler_context.targets = context.targets.current; + } + + self.condition.set_prefixes_for_targets(&context.targets.current); + let result = self.rules.minify(context, parent_is_unused); + + if inserted { + context.targets.exit_supports(); + context.handler_context.targets = context.targets.current; + } + result } } @@ -60,7 +72,13 @@ impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; + + let inserted = dest.targets.enter_supports(self.condition.get_supported_features()); self.rules.to_css(dest)?; + if inserted { + dest.targets.exit_supports(); + } + dest.dedent(); dest.newline()?; dest.write_char('}') @@ -149,6 +167,28 @@ impl<'i> SupportsCondition<'i> { _ => {} } } + + fn get_supported_features(&self) -> Features { + fn get_supported_features_internal(value: &SupportsCondition) -> Option { + match value { + SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(), + SupportsCondition::Declaration { value, .. } => { + let mut input = ParserInput::new(&value); + let mut parser = Parser::new(&mut input); + if let Ok(tokens) = TokenList::parse(&mut parser, &Default::default(), 0) { + Some(tokens.get_features()) + } else { + Some(Features::empty()) + } + } + // bail out if "not" or "or" exists for now + SupportsCondition::Not(_) | SupportsCondition::Or(_) => None, + SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => Some(Features::empty()), + } + } + + get_supported_features_internal(self).unwrap_or(Features::empty()) + } } impl<'i> Parse<'i> for SupportsCondition<'i> { diff --git a/src/rules/view_transition.rs b/src/rules/view_transition.rs new file mode 100644 index 000000000..fac6ec65a --- /dev/null +++ b/src/rules/view_transition.rs @@ -0,0 +1,196 @@ +//! The `@view-transition` rule. + +use super::Location; +use crate::error::{ParserError, PrinterError}; +use crate::printer::Printer; +use crate::properties::custom::CustomProperty; +use crate::stylesheet::ParserOptions; +use crate::traits::{Parse, ToCss}; +use crate::values::ident::NoneOrCustomIdentList; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; +use cssparser::*; + +/// A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule. +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct ViewTransitionRule<'i> { + /// Declarations in the `@view-transition` rule. + #[cfg_attr(feature = "serde", serde(borrow))] + pub properties: Vec<'i>>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +/// A property within a `@view-transition` rule. +/// +/// See [ViewTransitionRule](ViewTransitionRule). +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "property", content = "value", rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum ViewTransitionProperty<'i> { + /// The `navigation` property. + Navigation(Navigation), + /// The `types` property. + #[cfg_attr(feature = "serde", serde(borrow))] + Types(NoneOrCustomIdentList<'i>), + /// An unknown or unsupported property. + Custom(CustomProperty<'i>), +} + +/// A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) +/// property in a `@view-transition` rule. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum Navigation { + /// There will be no transition. + #[default] + None, + /// The transition will be enabled if the navigation is same-origin. + Auto, +} + +pub(crate) struct ViewTransitionDeclarationParser; + +impl<'i> cssparser::DeclarationParser<'i> for ViewTransitionDeclarationParser { + type Declaration = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut cssparser::Parser<'i, 't>, + ) -> Result<'i, Self::Error>> { + let state = input.state(); + match_ignore_ascii_case! { &name, + "navigation" => { + // https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor + if let Ok(navigation) = Navigation::parse(input) { + return Ok(ViewTransitionProperty::Navigation(navigation)); + } + }, + "types" => { + // https://drafts.csswg.org/css-view-transitions-2/#types-cross-doc + if let Ok(types) = NoneOrCustomIdentList::parse(input) { + return Ok(ViewTransitionProperty::Types(types)); + } + }, + _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration)) + } + + input.reset(&state); + return Ok(ViewTransitionProperty::Custom(CustomProperty::parse( + name.into(), + input, + &ParserOptions::default(), + )?)); + } +} + +/// Default methods reject all at rules. +impl<'i> AtRuleParser<'i> for ViewTransitionDeclarationParser { + type Prelude = (); + type AtRule = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; +} + +impl<'i> QualifiedRuleParser<'i> for ViewTransitionDeclarationParser { + type Prelude = (); + type QualifiedRule = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; +} + +impl<'i> RuleBodyItemParser<'i, ViewTransitionProperty<'i>, ParserError<'i>> for ViewTransitionDeclarationParser { + fn parse_qualified(&self) -> bool { + false + } + + fn parse_declarations(&self) -> bool { + true + } +} + +impl<'i> ViewTransitionRule<'i> { + pub(crate) fn parse<'t>( + input: &mut Parser<'i, 't>, + loc: Location, + ) -> Result<'i, ParserError<'i>>> { + let mut decl_parser = ViewTransitionDeclarationParser; + let mut parser = RuleBodyParser::new(input, &mut decl_parser); + let mut properties = vec![]; + while let Some(decl) = parser.next() { + if let Ok(decl) = decl { + properties.push(decl); + } + } + + Ok(ViewTransitionRule { properties, loc }) + } +} + +impl<'i> ToCss for ViewTransitionRule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + dest.write_str("@view-transition")?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + let len = self.properties.len(); + for (i, prop) in self.properties.iter().enumerate() { + dest.newline()?; + prop.to_css(dest)?; + if i != len - 1 || !dest.minify { + dest.write_char(';')?; + } + } + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } +} + +impl<'i> ToCss for ViewTransitionProperty<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + macro_rules! property { + ($prop: literal, $value: expr) => {{ + dest.write_str($prop)?; + dest.delim(':', false)?; + $value.to_css(dest) + }}; + } + + match self { + ViewTransitionProperty::Navigation(f) => property!("navigation", f), + ViewTransitionProperty::Types(t) => property!("types", t), + ViewTransitionProperty::Custom(custom) => { + dest.write_str(custom.name.as_ref())?; + dest.delim(':', false)?; + custom.value.to_css(dest, true) + } + } + } +} diff --git a/src/selector.rs b/src/selector.rs index 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