diff --git a/.eslintrc.js b/.eslintrc.js index 0ba078fed..be8f75e44 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,8 @@ module.exports = { '**/*.d.ts', '**/pages.gen.ts', '**/devtools-extension/extension/**', + // Test fixtures of the vendored StyleX SWC crates + 'packages/@stylexjs/rust-plugin/crates/**/fixtures/**', ], overrides: [ { diff --git a/.github/workflows/rust-compiler.yml b/.github/workflows/rust-compiler.yml new file mode 100644 index 000000000..f637625be --- /dev/null +++ b/.github/workflows/rust-compiler.yml @@ -0,0 +1,144 @@ +name: rust-compiler + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'packages/@stylexjs/rust-plugin/**' + - 'packages/@stylexjs/babel-plugin/**' + - 'packages/@stylexjs/shared/**' + - 'packages/style-value-parser/**' + - '.github/workflows/rust-compiler.yml' + push: + branches: ['main', 'feat/stylex-rust-port'] + paths: + - 'packages/@stylexjs/rust-plugin/**' + - 'packages/@stylexjs/babel-plugin/**' + - 'packages/@stylexjs/shared/**' + - 'packages/style-value-parser/**' + - '.github/workflows/rust-compiler.yml' + +permissions: + contents: read + +jobs: + native: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node: ['22.x', '24.x'] + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + defaults: + run: + shell: bash + env: + CARGO_BUILD_JOBS: '2' + steps: + # Keep sources byte-identical to the repository (rustfmt checks Unix + # newlines) and allow the deepest vendored paths. + - name: Configure Git for Windows checkouts + if: runner.os == 'Windows' + run: | + git config --global core.autocrlf false + git config --system core.longpaths true + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + cache: yarn + cache-dependency-path: yarn.lock + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: '1.96.1' + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/@stylexjs/rust-plugin + # Formatting and lint fail fast, before the release build. + - name: Rust formatting + run: + cargo fmt --manifest-path packages/@stylexjs/rust-plugin/Cargo.toml -- + --check + - name: Rust lint + run: | + cargo clippy --locked --manifest-path packages/@stylexjs/rust-plugin/Cargo.toml --all-targets -- -D warnings + cargo clippy --locked --manifest-path packages/@stylexjs/rust-plugin/Cargo.toml --features node --lib -- -D warnings + - run: corepack prepare yarn@1.22.22 --activate + - run: yarn install --frozen-lockfile --ignore-scripts + - name: Build the JavaScript packages the suites import + run: + npm run build --workspace=style-value-parser + --workspace=@stylexjs/shared --workspace=@stylexjs/stylex + --workspace=@stylexjs/babel-plugin + - name: Build native compiler + env: + STYLEX_NATIVE_TEST_HOOKS: '1' + run: npm run build:native --workspace=@stylexjs/rust-plugin + - name: Check the addon for build-machine paths + run: node packages/@stylexjs/rust-plugin/scripts/check-binary-paths.cjs + - name: Rust dependency license inventory is current + if: runner.os == 'Linux' && matrix.node == '24.x' + working-directory: packages/@stylexjs/rust-plugin + # The build fetched only Linux crates; the inventory covers every target. + run: | + cargo fetch --locked + node scripts/licenses.cjs --check + # The unchanged JS suite hardcodes POSIX paths in snapshots and mocks, so + # it does not pass on Windows even with the JS compiler; there the native + # run is compared test for test against the JS compiler instead. + - name: Current Babel reference suite + if: runner.os != 'Windows' + run: node packages/@stylexjs/rust-plugin/scripts/parity.cjs babel + - name: + Rust, native API, bundler, and unchanged Babel suites on the native + compiler + env: + STYLEX_PARITY_MODE: + ${{ runner.os == 'Windows' && 'compare' || 'rust' }} + run: npm run test:native --workspace=@stylexjs/rust-plugin + + node18: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '18.x' + cache: yarn + cache-dependency-path: yarn.lock + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: '1.96.1' + - uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/@stylexjs/rust-plugin + - run: corepack prepare yarn@1.22.22 --activate + - run: yarn install --frozen-lockfile --ignore-scripts --ignore-engines + - name: Build the JavaScript packages the suites import + run: + npm run build --workspace=style-value-parser + --workspace=@stylexjs/shared --workspace=@stylexjs/stylex + --workspace=@stylexjs/babel-plugin + - run: npm run build:native --workspace=@stylexjs/rust-plugin + env: + STYLEX_NATIVE_TEST_HOOKS: '1' + - name: + Native API, Babel adapter, and bundler plugin on the oldest supported + Node + run: + node --test + packages/@stylexjs/rust-plugin/tests/babel_facade_current.cjs + packages/@stylexjs/rust-plugin/tests/native_api_current.cjs + packages/@stylexjs/rust-plugin/tests/host_boundary_current.cjs + packages/@stylexjs/rust-plugin/tests/babel_validation_current.cjs + packages/@stylexjs/rust-plugin/tests/creation_data_boundary_current.cjs + packages/@stylexjs/rust-plugin/tests/compiled_consumer_current.cjs + packages/@stylexjs/rust-plugin/tests/named_env_callee_site_current.cjs + packages/@stylexjs/rust-plugin/tests/host_memo_current.cjs + packages/@stylexjs/rust-plugin/tests/creation_callback_effects_current.cjs + packages/@stylexjs/rust-plugin/tests/theme_projection_current.cjs + packages/@stylexjs/rust-plugin/tests/count_refusal_current.cjs + packages/@stylexjs/rust-plugin/tests/memo_parameter_identity_current.cjs diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index aa9828c3c..1b50d10c5 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -6,9 +6,22 @@ extend-exclude = [ "packages/**/__tests__/", # Fixture data has lots of typos "packages/benchmarks/**/fixtures/", + # Vendored third-party crate + "packages/@stylexjs/rust-plugin/vendor/", + # Vendored StyleX SWC crates: upstream identifiers and test data + "packages/@stylexjs/rust-plugin/crates/", + # Third-party crate names and verbatim license texts + "packages/@stylexjs/rust-plugin/LICENSE_PROVENANCE.json", + "packages/@stylexjs/rust-plugin/RUST_DEPENDENCY_LICENSES.md", + "packages/@stylexjs/rust-plugin/scripts/licenses-supplement.json", + "packages/@stylexjs/rust-plugin/THIRD_PARTY_NOTICES.md", ] [default.extend-words] # Ignore false-positives stylex = "stylex" nd = "nd" + +[default.extend-identifiers] +# A generated StyleX variable hash used in test fixtures +kLKAdn = "kLKAdn" diff --git a/packages/@stylexjs/rust-plugin/.gitattributes b/packages/@stylexjs/rust-plugin/.gitattributes new file mode 100644 index 000000000..284feeeae --- /dev/null +++ b/packages/@stylexjs/rust-plugin/.gitattributes @@ -0,0 +1,7 @@ +# Every text file in this package is LF in the working tree, on Windows as well, +# matching the vendored stylex-swc-plugin. Git decides what is text by content. +* text=auto eol=lf +vendor/** -text +tests/fixtures/** -text +# Verbatim third-party license texts, some with CRLF line endings. +RUST_DEPENDENCY_LICENSES.md -text diff --git a/packages/@stylexjs/rust-plugin/.gitignore b/packages/@stylexjs/rust-plugin/.gitignore new file mode 100644 index 000000000..863a0ef93 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/.gitignore @@ -0,0 +1,5 @@ +/target/ +/native/ +*.tgz +!crates/stylex-path-resolver/fixtures/**/node_modules +!crates/stylex-path-resolver/fixtures/**/node_modules/** diff --git a/packages/@stylexjs/rust-plugin/Cargo.lock b/packages/@stylexjs/rust-plugin/Cargo.lock new file mode 100644 index 000000000..c13d43575 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/Cargo.lock @@ -0,0 +1,4164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "ast_node" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf54a7a1bf98e127c22e9e2b9d19619092c74283fdc14e4d67da69780f90db6" +dependencies = [ + "quote", + "swc_macros_common", + "syn 2.0.119", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "better_scoped_tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd228125315b132eed175bf47619ac79b945b26e56b848ba203ae4ea8603609" +dependencies = [ + "scoped-tls", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-str" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dde6d05e75a31ec9610eb6446a6f0a10dd30ff5100d720fee4c7c7a9008b5ba" +dependencies = [ + "bytes", + "serde", +] + +[[package]] +name = "camino" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cbor4ii" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faed1a83001dc2c9201451030cc317e35bef36c84d3781d7c5bb9f343c397da8" + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "compact_str" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "static_assertions", + "zmij", +] + +[[package]] +name = "concurrent_lru" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feb5cb312f774e8a24540e27206db4e890f7d488563671d24a16389cf4c2e4e" +dependencies = [ + "once_cell", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1af709f1f33454bf52eadfc8c78b3b9ef9cb26fb54d16dc9cd9a7299f899fd1b" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11119743ad110e8c1bdccd930d7f5c30c99e5fc76a7b63ec9807e84eef0c5f59" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.14.0", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dragonbox_ecma" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "endian-type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fancy-regex" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301f5bf187b3c295fce6468d3875037a0bccc5f6b151c63cac2f85babf21912" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fast-glob" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d26eec0ae9682c457cb0f85de67ad417b716ae852736a5d94c2ad6e92a997c9" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "from_variant" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9" +dependencies = [ + "swc_macros_common", + "syn 2.0.119", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "halfbrown" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ed2f2edad8a14c8186b847909a41fbb9c3eafa44f88bd891114ed5019da09" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hstr" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e2be4baa20c69a32de3b3b8ee67d7a7376591cc61f81ecc08fa43a1e36b897" +dependencies = [ + "hashbrown 0.14.5", + "new_debug_unreachable", + "once_cell", + "rustc-hash", + "serde", + "triomphe", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "icu_collator" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08984ed58ac439ebf3e13d2cf26b0c46a60afcd21721c1d14087c0b240344dda" +dependencies = [ + "icu_collator_data", + "icu_collections", + "icu_locale_core", + "icu_locale_fallback", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d7e54efdddeb1208c08dd5d32b53a879ac00d2d3d051b2255fd26821d16368" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if_chain" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "is-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8267aa6001e25494f3015f9663bbd88a18240c74483afa5f0934a1b3e4c388e9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "is-terminal" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-strip-comments" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25376d12b2f6ae53f986f86e2a808a56af03d72284ae24fc35a2e290d09ee3c3" +dependencies = [ + "memchr", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "owo-colors", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "murmur2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb585ade2549a017db2e35978b77c319214fa4b37cede841e27954dd6e8f3ca8" + +[[package]] +name = "napi" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907ff7366fb7182e28edd0f08ef8ac5d6b13b8d65a3c0b3a10bf3ac092437d6b" +dependencies = [ + "bitflags", + "ctor", + "futures", + "libc", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", + "serde", + "serde_json", +] + +[[package]] +name = "napi-build" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b899b545d3aa6dca985939059f258c5488d34e4ecf39c274e20009748f4b846d" + +[[package]] +name = "napi-derive" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32eb00f92f84cf462fc328f49082b29c2a7d2eebeb1d447d19c1f9fdd9f89ee" +dependencies = [ + "convert_case 0.12.0", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "napi-derive-backend" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1fe28c64d8fc12e69f9b817d0febdbe7914555760f1e353106281b8ff99cc90" +dependencies = [ + "convert_case 0.12.0", + "proc-macro2", + "quote", + "semver", + "syn 3.0.9", +] + +[[package]] +name = "napi-sys" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d153beb7d5c973f4b9395be8a83054a28968b61188f1dd1a2cc2321af22daea2" +dependencies = [ + "libloading", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nodejs-built-in-modules" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5eb86a92577833b75522336f210c49d9ebd7dd55a44d80a92e68c668a75f27c" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.3", + "libc", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + +[[package]] +name = "oxc_resolver" +version = "11.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7fd0839a5ecee17c0d2c88c8006e8ab4dff25d4a79e3a0bf7a9f0cce17f4e3" +dependencies = [ + "compact_str 0.10.0", + "dashmap", + "fast-glob", + "indexmap", + "json-strip-comments", + "memchr", + "nodejs-built-in-modules", + "once_cell", + "percent-encoding", + "pnp", + "rustc-hash", + "rustix", + "self_cell", + "serde", + "serde_json", + "simd-json", + "simdutf8", + "thiserror 2.0.21", + "tracing", + "windows", +] + +[[package]] +name = "package-json" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6bbc867371379f20d5d80c84d9b9fe09eb3ce035cbb3abbef958dbd806dc1e" +dependencies = [ + "anyhow", + "derive_builder", + "serde", + "serde_json", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "par-core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96cbd21255b7fb29a5d51ef38a779b517a91abd59e2756c039583f43ef4c90f" +dependencies = [ + "once_cell", +] + +[[package]] +name = "par-iter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eae0176a010bb94b9a67f0eb9da0fd31410817d58850649c54f485124c9a71a" +dependencies = [ + "either", + "par-core", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros 0.14.0", + "phf_shared 0.14.0", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared 0.14.0", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator 0.14.0", + "phf_shared 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.4", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher 1.0.4", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "pnp" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0e591607db15df65036bd424bd57782296da9963d823da5a139ed451f615b5" +dependencies = [ + "byteorder", + "concurrent_lru", + "flate2", + "indexmap", + "nodejs-built-in-modules", + "pathdiff", + "radix_trie", + "regress 0.12.0", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "postcss_value_parser" +version = "0.19.2" +dependencies = [ + "criterion", + "swc_malloc", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[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 = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "radix_fmt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426" + +[[package]] +name = "radix_trie" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "regress" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158a764437582235e3501f683b93a0a6f8d825d04a789dbe5ed30b8799b8908a" +dependencies = [ + "hashbrown 0.16.1", + "memchr", +] + +[[package]] +name = "regress" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eef8b209c3c1c15dbad02c1f30f9539f00dc7253e0cbcdaae442a50a09d7c1" +dependencies = [ + "memchr", +] + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "ryu-js" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serial_test" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6df5ed973ad8d834e09f824f9e9f449af6b9a3745f78dec7cc752770bd3bf11" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd-json" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebe141eb4a23ecc4d5de93fa5b139ac65dc07c38037a2fe9a8fb7c9d36bd832" +dependencies = [ + "halfbrown", + "ref-cast", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_enum" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e" +dependencies = [ + "quote", + "swc_macros_common", + "syn 2.0.119", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stylex_ast" +version = "0.19.2" +dependencies = [ + "anyhow", + "ctor", + "indexmap", + "pretty_env_logger", + "rustc-hash", + "stylex_constants", + "stylex_macros", + "stylex_utils", + "swc_core", +] + +[[package]] +name = "stylex_atoms" +version = "0.19.2" +dependencies = [ + "ctor", + "pretty_env_logger", + "rustc-hash", + "stylex_ast", + "stylex_constants", + "swc_core", +] + +[[package]] +name = "stylex_constants" +version = "0.19.2" +dependencies = [ + "ctor", + "once_cell", + "phf 0.14.0", + "pretty_env_logger", + "rustc-hash", +] + +[[package]] +name = "stylex_css" +version = "0.19.2" +dependencies = [ + "criterion", + "ctor", + "icu_collator", + "log", + "postcss_value_parser", + "pretty_env_logger", + "stylex_constants", + "stylex_css_parser", + "stylex_enums", + "stylex_macros", + "stylex_regex", + "stylex_structures", + "stylex_types", + "stylex_utils", + "swc_malloc", +] + +[[package]] +name = "stylex_css_parser" +version = "0.19.2" +dependencies = [ + "criterion", + "cssparser", + "log", + "rustc-hash", + "serde_json", + "stylex_macros", + "stylex_utils", + "swc_core", + "swc_malloc", + "thiserror 2.0.21", +] + +[[package]] +name = "stylex_diagnostics" +version = "0.19.2" +dependencies = [ + "anyhow", + "ctor", + "log", + "rustc-hash", + "smallvec", + "stacker", + "stylex_ast", + "stylex_macros", + "stylex_regex", + "stylex_state_index", + "stylex_utils", + "swc_compiler_base", + "swc_config", + "swc_core", + "url", +] + +[[package]] +name = "stylex_enums" +version = "0.19.2" +dependencies = [ + "indexmap", + "rustc-hash", + "serde", + "stylex_macros", + "swc_core", +] + +[[package]] +name = "stylex_evaluator" +version = "0.19.2" +dependencies = [ + "anyhow", + "criterion", + "ctor", + "hstr", + "indexmap", + "log", + "pretty_env_logger", + "rustc-hash", + "stacker", + "stylex_ast", + "stylex_constants", + "stylex_diagnostics", + "stylex_enums", + "stylex_js", + "stylex_macros", + "stylex_regex", + "stylex_state", + "stylex_structures", + "stylex_utils", + "swc_core", + "swc_ecma_parser", + "swc_malloc", +] + +[[package]] +name = "stylex_js" +version = "0.19.2" +dependencies = [ + "icu_normalizer", + "indexmap", + "ryu-js", + "stylex_constants", + "stylex_macros", + "stylex_utils", + "swc_core", +] + +[[package]] +name = "stylex_macros" +version = "0.19.2" +dependencies = [ + "colored", + "log", + "stylex_constants", +] + +[[package]] +name = "stylex_nested_config" +version = "0.19.2" +dependencies = [ + "ctor", + "indexmap", + "pretty_env_logger", + "stylex_ast", + "stylex_enums", + "stylex_macros", + "stylex_structures", + "swc_core", +] + +[[package]] +name = "stylex_path_resolver" +version = "0.19.2" +dependencies = [ + "ctor", + "fancy-regex", + "indexmap", + "log", + "once_cell", + "oxc_resolver", + "package-json", + "path-clean", + "pathdiff", + "pretty_env_logger", + "rustc-hash", + "serde", + "serde_json", + "serial_test", + "stylex_macros", +] + +[[package]] +name = "stylex_regex" +version = "0.19.2" +dependencies = [ + "fancy-regex", + "once_cell", +] + +[[package]] +name = "stylex_rust_compiler" +version = "0.19.1" +dependencies = [ + "anyhow", + "icu_collator", + "icu_locale_core", + "indexmap", + "log", + "napi", + "napi-build", + "napi-derive", + "pretty_assertions", + "regex", + "rustc-hash", + "ryu-js", + "serde", + "serde_json", + "stacker", + "stylex_constants", + "stylex_css", + "stylex_diagnostics", + "stylex_enums", + "stylex_evaluator", + "stylex_macros", + "stylex_state", + "stylex_structures", + "stylex_transform", + "stylex_types", + "swc_core", + "swc_ecma_parser", + "swc_malloc", +] + +[[package]] +name = "stylex_state" +version = "0.19.2" +dependencies = [ + "anyhow", + "criterion", + "ctor", + "indexmap", + "log", + "path-clean", + "pretty_env_logger", + "rustc-hash", + "serde", + "serde_json", + "stacker", + "stylex_ast", + "stylex_constants", + "stylex_diagnostics", + "stylex_enums", + "stylex_js", + "stylex_macros", + "stylex_path_resolver", + "stylex_state_index", + "stylex_structures", + "stylex_styleq", + "stylex_types", + "stylex_utils", + "swc_core", + "swc_malloc", +] + +[[package]] +name = "stylex_state_index" +version = "0.19.2" +dependencies = [ + "ctor", + "pretty_env_logger", + "rustc-hash", + "stylex_ast", + "stylex_utils", + "swc_core", +] + +[[package]] +name = "stylex_structures" +version = "0.19.2" +dependencies = [ + "indexmap", + "once_cell", + "rustc-hash", + "serde", + "stylex_ast", + "stylex_constants", + "stylex_enums", + "stylex_macros", + "stylex_utils", + "swc_core", +] + +[[package]] +name = "stylex_styleq" +version = "0.19.2" +dependencies = [ + "criterion", + "indexmap", + "log", + "rustc-hash", + "serial_test", + "stylex_constants", + "swc_malloc", + "xxhash-rust", +] + +[[package]] +name = "stylex_transform" +version = "0.19.2" +dependencies = [ + "criterion", + "ctor", + "indexmap", + "insta", + "log", + "path-clean", + "phf 0.14.0", + "pretty_env_logger", + "rustc-hash", + "serde", + "serde_json", + "stylex_ast", + "stylex_atoms", + "stylex_constants", + "stylex_css", + "stylex_css_parser", + "stylex_diagnostics", + "stylex_enums", + "stylex_evaluator", + "stylex_js", + "stylex_macros", + "stylex_nested_config", + "stylex_path_resolver", + "stylex_regex", + "stylex_state", + "stylex_state_index", + "stylex_structures", + "stylex_styleq", + "stylex_types", + "stylex_utils", + "swc_compiler_base", + "swc_config", + "swc_core", + "swc_ecma_parser", + "swc_malloc", + "testing", +] + +[[package]] +name = "stylex_types" +version = "0.19.2" +dependencies = [ + "ctor", + "derive_more", + "indexmap", + "pretty_env_logger", + "serde", + "serde_json", + "stylex_macros", + "stylex_regex", + "stylex_structures", + "stylex_utils", +] + +[[package]] +name = "stylex_utils" +version = "0.19.2" +dependencies = [ + "indexmap", + "murmur2", + "radix_fmt", + "rustc-hash", + "serde_json", + "stacker", + "swc_core", + "swc_ecma_parser", + "xxhash-rust", +] + +[[package]] +name = "swc_allocator" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb41c2f41afa7357a86109f7e058f6b140ab415b8cd196c1a7b54f2703c85417" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.14.5", + "rustc-hash", +] + +[[package]] +name = "swc_atoms" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70a493080ceb12dabddb96905a3ddac679fbe9c71ddc81a2a769126e8cde7c4" +dependencies = [ + "cbor4ii", + "hstr", + "once_cell", + "serde", +] + +[[package]] +name = "swc_common" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142d2a06cafce623859ca799f5d1935bb8a886f18b66a2585ddeadff210121d0" +dependencies = [ + "anyhow", + "ast_node", + "better_scoped_tls", + "bytes-str", + "cbor4ii", + "either", + "from_variant", + "num-bigint", + "once_cell", + "parking_lot", + "rustc-hash", + "serde", + "siphasher 0.3.11", + "swc_atoms", + "swc_eq_ignore_macros", + "swc_sourcemap", + "swc_visit", + "termcolor", + "tracing", + "unicode-width 0.2.3", + "url", +] + +[[package]] +name = "swc_compiler_base" +version = "65.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1513da5e8483741dee002668d6cedd07da1cefa6517868888f30a9348c65486" +dependencies = [ + "anyhow", + "base64", + "bytes-str", + "once_cell", + "pathdiff", + "rustc-hash", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "swc_config", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_minifier", + "swc_ecma_parser", + "swc_ecma_visit", + "swc_sourcemap", +] + +[[package]] +name = "swc_config" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733985ad1340ea469d0d4366bae9664aa8f246006fa822a7d9365b41e48d7986" +dependencies = [ + "anyhow", + "bytes-str", + "dashmap", + "globset", + "indexmap", + "once_cell", + "regex", + "regress 0.11.1", + "rustc-hash", + "serde", + "serde_json", + "swc_config_macro", + "swc_sourcemap", +] + +[[package]] +name = "swc_config_macro" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b416e8ce6de17dc5ea496e10c7012b35bbc0e3fef38d2e065eed936490db0b3" +dependencies = [ + "proc-macro2", + "quote", + "swc_macros_common", + "syn 2.0.119", +] + +[[package]] +name = "swc_core" +version = "81.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454124107875a9a7633b41e95215125e8c5bae43bf185521a58abb6f3ffa8bb8" +dependencies = [ + "swc_allocator", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_testing", + "swc_ecma_transforms_typescript", + "swc_ecma_utils", + "swc_ecma_visit", + "swc_plugin", + "swc_plugin_macro", + "swc_plugin_proxy", + "swc_transform_common", +] + +[[package]] +name = "swc_ecma_ast" +version = "29.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839d2cf4e611b944438c304ec59396221b3db4580acd172fcd70522bb450ee9" +dependencies = [ + "bitflags", + "cbor4ii", + "is-macro", + "num-bigint", + "once_cell", + "phf 0.11.3", + "rustc-hash", + "serde", + "string_enum", + "swc_atoms", + "swc_common", + "swc_visit", + "unicode-id-start", +] + +[[package]] +name = "swc_ecma_codegen" +version = "32.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190b35a7417966c5702df3bd69a6fd13c4c4dd2a9984a3029a78bcac0b01abc7" +dependencies = [ + "ascii", + "compact_str 0.7.1", + "dragonbox_ecma", + "memchr", + "num-bigint", + "once_cell", + "regex", + "rustc-hash", + "serde", + "swc_allocator", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen_macros", + "swc_ecma_utils", + "tracing", +] + +[[package]] +name = "swc_ecma_codegen_macros" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e276dc62c0a2625a560397827989c82a93fd545fcf6f7faec0935a82cc4ddbb8" +dependencies = [ + "proc-macro2", + "swc_macros_common", + "syn 2.0.119", +] + +[[package]] +name = "swc_ecma_hooks" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3abc7b19ffc01d358fe6dfce904cc0a9c30c4db6deed11011b67c320f71682c" +dependencies = [ + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_minifier" +version = "62.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98ecce04823366c5239f9b023a79ca0ef493bcbea5dfdf5be54e5742e75a6f6" +dependencies = [ + "arrayvec", + "bitflags", + "indexmap", + "num-bigint", + "num_cpus", + "once_cell", + "par-core", + "par-iter", + "parking_lot", + "phf 0.11.3", + "radix_fmt", + "rustc-hash", + "ryu-js", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "swc_config", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_hooks", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_optimization", + "swc_ecma_utils", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_parser" +version = "45.1.6" +dependencies = [ + "bitflags", + "compact_str 0.7.1", + "either", + "num-bigint", + "phf 0.11.3", + "rustc-hash", + "seq-macro", + "serde", + "stacker", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_testing" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9eca985397245f45f146e13bb4f4aec6c9238fe8a05124ab9e626662f0d813" +dependencies = [ + "anyhow", + "hex", + "sha2", + "testing", + "tracing", +] + +[[package]] +name = "swc_ecma_transforms_base" +version = "49.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5b5b6a31a512d23854cb6fabc9347596bde4106cd3a65dc4f9072bbb0514b4" +dependencies = [ + "better_scoped_tls", + "indexmap", + "once_cell", + "par-core", + "phf 0.11.3", + "rustc-hash", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_utils", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_transforms_optimization" +version = "52.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81534b978cee37ec7e8055fe04f32c195c82450eac2e3751f9629df7cbe7d69a" +dependencies = [ + "bytes-str", + "dashmap", + "indexmap", + "once_cell", + "par-core", + "petgraph", + "rustc-hash", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_transforms_react" +version = "55.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d051cd9043cd7ca24eb154606e6a6914ed13d0dca686bd681a5277d99a8e4c7" +dependencies = [ + "base64", + "bytes-str", + "indexmap", + "once_cell", + "rustc-hash", + "serde", + "sha1", + "string_enum", + "swc_atoms", + "swc_common", + "swc_config", + "swc_ecma_ast", + "swc_ecma_hooks", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_testing" +version = "53.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76aec842cdbe4ae1c8a953f8b126c9e26a42ee388c9b3c9b68a0e4e5e9747c9" +dependencies = [ + "ansi_term", + "anyhow", + "base64", + "hex", + "serde", + "serde_json", + "sha2", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_testing", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", + "swc_sourcemap", + "tempfile", + "testing", +] + +[[package]] +name = "swc_ecma_transforms_typescript" +version = "55.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9794ce264ba60fed86141043d8846fc522149d2144df3d56dadcd379f64adc84" +dependencies = [ + "bytes-str", + "rustc-hash", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms_base", + "swc_ecma_transforms_react", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_utils" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef03eeed69158884baa19bf5e02d6bf21a8cc2e728346dec3cc913f69cf190c" +dependencies = [ + "dragonbox_ecma", + "indexmap", + "num_cpus", + "once_cell", + "par-core", + "rustc-hash", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_visit" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a830a070fea84a3a8a8c556463f0551e3bfc6cf2f8bdeecd62fa753e22a72289" +dependencies = [ + "new_debug_unreachable", + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", + "tracing", +] + +[[package]] +name = "swc_eq_ignore_macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "swc_error_reporters" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997aa1a85b9ebdab127d8819ea5ebfa5b90e5a958d969e25cce4c853c36d26de" +dependencies = [ + "anyhow", + "miette", + "once_cell", + "serde", + "swc_common", +] + +[[package]] +name = "swc_macros_common" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "swc_malloc" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d222be9e55e86fd6ee32e97c7b12829692329ac8bbed652dcd6c2e1d488af935" +dependencies = [ + "mimalloc", + "tikv-jemallocator", +] + +[[package]] +name = "swc_plugin" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b27449420554de6ad8d49004ad3d36e6ac64ecb51d1b0fe1002afcd7a45d85" +dependencies = [ + "once_cell", +] + +[[package]] +name = "swc_plugin_macro" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584f7e74ca311c843d67e143331d9fb17bd5468f6216f3feac0b012d7cc268c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "swc_plugin_proxy" +version = "30.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13ffe2fc1fb9b1936f6ceaf21d8e2e68f23ac9f0022db414e5f30c5fc52a966" +dependencies = [ + "better_scoped_tls", + "cbor4ii", + "rustc-hash", + "swc_common", + "swc_ecma_ast", + "tracing", +] + +[[package]] +name = "swc_sourcemap" +version = "10.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9ce7c46dc9730284761c88b34d15a8cfbaf40884705f3deec65b9493765ac8" +dependencies = [ + "base64-simd", + "bitvec", + "bytes-str", + "data-encoding", + "debugid", + "if_chain", + "rustc-hash", + "serde", + "serde_json", + "unicode-id-start", + "url", +] + +[[package]] +name = "swc_transform_common" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693ff873dd31f50c44f5c05182631715bc03f7e1123e7805279b25d1db2bed20" +dependencies = [ + "better_scoped_tls", + "rustc-hash", + "serde", + "swc_common", +] + +[[package]] +name = "swc_visit" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7df72a1be54e379864cebf04f26abb4d7ee637977f62f8ec8387233f7a27e88" +dependencies = [ + "either", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "testing" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab499434ba981fb7aada35505f2035e1bae4517c18bafebf8954dd2dc7e979af" +dependencies = [ + "cargo_metadata", + "difference", + "once_cell", + "pretty_assertions", + "regex", + "rustc-hash", + "serde", + "serde_json", + "swc_common", + "swc_error_reporters", + "testing_macros", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "testing_macros" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c21fb13b703fb0bf466ec891704055ec18e2a8c8d2d47c31b65d7006044e146" +dependencies = [ + "anyhow", + "glob", + "once_cell", + "proc-macro2", + "quote", + "regex", + "relative-path", + "syn 2.0.119", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.3", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "triomphe" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d15f4f5898bcfc5d8a8e45ed30b2f108d710c1a2e5489dbf80e078f707181bc" +dependencies = [ + "serde", + "stable_deref_trait", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-id-start" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0825d2e09dcae814a4273c1e1435979d414ede25b2ee235b1545da6682483765" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf16_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7de35d9d8757dbc1f5125dc8b51438c68b4430546a735c3a9b91aaaaf9a18a" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "uuid" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-trait" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e80f0c733af0720a501b3905d22e2f97662d8eacfe082a75ed7ffb5ab08cb59" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +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 = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/@stylexjs/rust-plugin/Cargo.toml b/packages/@stylexjs/rust-plugin/Cargo.toml new file mode 100644 index 000000000..40c36ea1e --- /dev/null +++ b/packages/@stylexjs/rust-plugin/Cargo.toml @@ -0,0 +1,176 @@ +[package] +name = "stylex_rust_compiler" +edition = "2021" +license = "MIT" +version = "0.19.1" +rust-version.workspace = true + +[lib] +name = "stylex_swc_plugin" +crate-type = ["cdylib", "rlib"] + + +[features] +default = [] +node = ["dep:napi", "dep:napi-derive", "dep:napi-build", "dep:swc_malloc"] +# Test-only N-API diagnostics (compilerFunctionCount), for the Node suites. +# Never enabled by npm builds; CI builds with STYLEX_NATIVE_TEST_HOOKS=1. +test-hooks = ["node"] + +[dependencies] +anyhow = "1.0.98" +indexmap = { version = "2.10.0", features = ["serde"] } +regex = "1.11.1" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = { version = "1.0.140", features = ["preserve_order"] } +ryu-js = "1.0.2" +stacker = "0.1.23" +icu_collator = { workspace = true } +icu_locale_core = "2.3.0" +# The native frontend and the vendored StyleX core share one SWC version. +swc_core = { workspace = true, features = [ + "common", + "common_sourcemap", + "ecma_ast", + "ecma_ast_serde", + "ecma_codegen", + "ecma_parser", + "ecma_parser_typescript", + "ecma_parser_unstable", + "ecma_visit", + "ecma_transforms", +] } +swc_ecma_parser = { workspace = true, features = ["flow", "unstable", "typescript"] } +stylex_structures = { path = "crates/stylex-structures" } +stylex_state = { path = "crates/stylex-state" } +stylex_transform = { path = "crates/stylex-transform" } +stylex_types = { path = "crates/stylex-types" } +stylex_enums = { path = "crates/stylex-enums" } +stylex_macros = { path = "crates/stylex-macros" } +stylex_constants = { path = "crates/stylex-constants" } +stylex_css = { path = "crates/stylex-css" } +stylex_evaluator = { path = "crates/stylex-evaluator" } +stylex_diagnostics = { path = "crates/stylex-diagnostics" } +rustc-hash = { workspace = true } +log = { workspace = true } +napi = { version = "3.9.0", default-features = false, features = ["napi8", "serde-json"], optional = true } +napi-derive = { version = "3.5.0", optional = true } +# The Node addon's global allocator, as SWC's own Node bindings choose it per +# target (mimalloc on Linux glibc x86_64/aarch64, macOS and Windows; the system +# allocator elsewhere). Only the addon links it: Rust API users keep theirs. +# The addon tunes it at load (src/node.rs, `allocator_tuning`). +swc_malloc = { workspace = true, optional = true } + +[build-dependencies] +napi-build = { version = "2.3.0", optional = true } + +[dev-dependencies] +pretty_assertions = "1.4.1" + +[patch.crates-io] +swc_ecma_parser = { path = "vendor/swc_ecma_parser" } + +# The Node addon is built as a cdylib-only final artifact (scripts/build.cjs: +# `cargo rustc --crate-type cdylib`), which is what lets fat LTO run: rustc +# performs LTO only for a final artifact, and the package's `rlib` crate type +# (kept for the Rust API and tests) would otherwise make it skip LTO entirely. +[profile.release] +lto = "fat" +codegen-units = 1 +opt-level = 3 +strip = "symbols" + +[workspace] +resolver = "2" +members = [".", "crates/*"] + +# The vendored StyleX core crates (crates/*) are the public stylex-swc-plugin +# workspace at tag 0.19.2 (6e7d3983); see crates/PROVENANCE.md. Their +# manifests inherit these settings. Upstream declares rust-version 1.97; the +# selected crates have been checked to build on 1.96, which this repository pins. +[workspace.package] +edition = "2024" +license = "MIT" +repository = "https://github.com/facebook/stylex" +rust-version = "1.96" +version = "0.19.2" + +[workspace.dependencies] +anyhow = "1.0.102" +colored = { version = "3.1.1" } +criterion = { version = "0.8", features = ["html_reports"] } +cssparser = { version = "0.38.0" } +ctor = "1.0.7" +derive_more = { version = "2.1.1", features = ["display"] } +fancy-regex = "0.19.0" +# Root collation for the pseudo-key comparator. Six of its crates are new to +# this workspace; the other 26 in its tree already arrive through SWC's +# `idna_adapter`. `crates/stylex-css/src/utils/pre_rule.rs` costs the choice +# against the alternative it was measured against. +# `unstable` is an empty marker feature -- it pulls no extra crates -- and it +# gates `CollatorBorrowed::new_root`, the only constructor for root collation +# that cannot fail. `Collator::try_new` returns a `Result` whose `Err` means the +# compiled CLDR data this crate carries is absent, which is a build-time +# dependency rather than a runtime state: unreachable, untestable, and therefore +# an uncoverable region in a workspace that excludes nothing from coverage. +# `new_root` has no such arm. See `crates/stylex-css/src/utils/pre_rule.rs`. +# +# The cost is an API the crate reserves the right to change in a minor release. +# `Cargo.lock` is committed, so that can only surface on a deliberate +# `cargo update`, and the failure is a compile error naming this line rather than +# anything silent. +icu_collator = { version = "2.3.1", features = ["unstable"] } +# Already in the graph through `icu_collator`, with these features; named here +# for `String.prototype.normalize`, so no package is added. +icu_normalizer = { version = "2.3.0", default-features = false, features = ["compiled_data", "utf16_iter"] } +indexmap = { version = "2.14.0", features = ["serde"] } +insta = { version = "1.47.2" } +log = "0.4.32" +murmur2 = { version = "0.1.0" } +once_cell = { version = "1.21.4" } +path-clean = { version = "1.0.1" } +phf = { version = "0.14.0" } +pretty_env_logger = "0.5.0" +radix_fmt = { version = "1.0.0" } +rustc-hash = "2.1.2" +serde = "1.0.228" +serde_json = "1.0.150" +serial_test = { version = "4.0.1" } +# Already in the graph through SWC, which uses it for the same reason: a list +# that is a handful of items in every real case does not need the heap. Named +# here so the span cache key can sort its names on the stack. +smallvec = { version = "1.15.1" } +# Already in the graph through swc_ecma_parser, which grows the stack for the same +# reason: a recursive walk over a nested expression must not be bounded by the +# thread's stack. Named here so the evaluator can depend on it directly. +stacker = "0.1.24" +swc_compiler_base = { version = "65.0.0" } +swc_config = { version = "6.0.0" } +swc_core = { version = "81.0.0" } +swc_ecma_parser = { version = "45.1.1" } +url = "2.5.8" +# The Node addon's global allocator (root `node` feature): mimalloc where SWC's +# own Node bindings use it, the system allocator elsewhere. +swc_malloc = { version = "1.2.5" } +swc_sourcemap = { version = "10.0.2" } +testing = "27.0.0" +thiserror = "2.0.18" +# The structural key's hasher. 128-bit output in one pass, where two salted +# `DefaultHasher` states cost two -- see `stylex-utils/src/hash.rs`. +xxhash-rust = { version = "0.8.15", features = ["xxh3"] } + +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + +# Workspace-wide clippy gates. Rules enforced here: +# - performance: avoid hidden allocations / clones in hot paths +# - correctness: prefer explicit conversion paths and idiomatic iterators +# Test code can opt out via `#[cfg_attr(test, allow())]` on the offender. + +[workspace.lints.clippy] +cloned_instead_of_copied = "warn" +format_in_format_args = "warn" +inefficient_to_string = "warn" +manual_string_new = "warn" +needless_collect = "warn" +redundant_clone = "warn" diff --git a/packages/@stylexjs/rust-plugin/LICENSE_PROVENANCE.json b/packages/@stylexjs/rust-plugin/LICENSE_PROVENANCE.json new file mode 100644 index 000000000..352e90542 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/LICENSE_PROVENANCE.json @@ -0,0 +1,363 @@ +{ + "scope": "cargo tree -p stylex_rust_compiler --features node -e normal --target all", + "packages": [ + {"name":"adler2","version":"2.0.0","license":"0BSD OR MIT OR Apache-2.0","files":[{"path":"LICENSE-0BSD","sha256":"861399f8c21c042b110517e76dc6b63a2b334276c8cf17412fc3c8908ca8dc17"},{"path":"LICENSE-APACHE","sha256":"8ada45cd9f843acf64e4722ae262c622a2b3b3007c7310ef36ac1061a30f6adb"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"ahash","version":"0.8.12","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"0444c6991eead6822f7b9102e654448d51624431119546492e8b231db42c48bb"}]}, + {"name":"aho-corasick","version":"1.1.5","license":"Unlicense OR MIT","files":[{"path":"COPYING","sha256":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f"},{"path":"LICENSE-MIT","sha256":"0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f"},{"path":"UNLICENSE","sha256":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c"}]}, + {"name":"allocator-api2","version":"0.2.21","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"20fe7b00e904ed690e3b9fd6073784d3fc428141dbd10b81c01fd143d0797f58"},{"path":"LICENSE-MIT","sha256":"36516aefdc84c5d5a1e7485425913a22dbda69eb1930c5e84d6ae4972b5194b9"}]}, + {"name":"ansi_term","version":"0.12.1","license":"MIT","files":[{"path":"LICENCE","sha256":"2762990c7fbba9d550802a2593c1d857dcd52596bb0f9f192a97e9a7ac5f4f9e"}]}, + {"name":"anyhow","version":"1.0.104","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"arrayvec","version":"0.7.8","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"4da95ec4ecb65b738d470b7d762894ad9c97da93e6cbfb18b570fc2c96f4b871"}]}, + {"name":"ascii","version":"1.1.0","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"fabba0cb7d00a4b3211fdc13699223ab2d22f88678ccd608494cf2b332b903e9"},{"path":"LICENSE-MIT","sha256":"7e4b8a17b118d3d7fd7a362a4b6563e0bd98b098a24c4c7f07dd22042091a847"}]}, + {"name":"ast_node","version":"7.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"base64","version":"0.22.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"0dd882e53de11566d50f8e8e2d5a651bcf3fabee4987d70f306233cf39094ba7"}]}, + {"name":"base64-simd","version":"0.8.0","license":"MIT","files":[{"path":"https://github.com/Nugine/simd","sha256":"71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d"}]}, + {"name":"better_scoped_tls","version":"1.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"bit-set","version":"0.8.0","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90"},{"path":"LICENSE-MIT","sha256":"f51ac2c59a222f7476ce507ca879960e2b64ea64bb2786eefdbeb7b0b538d1b7"}]}, + {"name":"bit-vec","version":"0.8.0","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90"},{"path":"LICENSE-MIT","sha256":"f51ac2c59a222f7476ce507ca879960e2b64ea64bb2786eefdbeb7b0b538d1b7"}]}, + {"name":"bitflags","version":"2.13.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"bitvec","version":"1.1.1","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"411781fd38700f2357a14126d0ab048164ab881f1dcb335c1bb932e232c9a2f5"}]}, + {"name":"block-buffer","version":"0.10.4","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5"},{"path":"LICENSE-MIT","sha256":"d5c22aa3118d240e877ad41c5d9fa232f9c77d757d4aac0c2f943afc0a95e0ef"}]}, + {"name":"bstr","version":"1.12.1","license":"MIT OR Apache-2.0","files":[{"path":"COPYING","sha256":"68653aaa727a2bfa31b7a751e31701ce33c49d695c12dd291a07d1c54da4c14b"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6b7374c39a57e57fc2c38eb529c4c88340152b10f51dd5ae2d819dfa67f61715"}]}, + {"name":"bumpalo","version":"3.20.3","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"65f94e99ddaf4f5d1782a6dae23f35d4293a9a01444a13135a6887017d353cee"}]}, + {"name":"byteorder","version":"1.5.0","license":"Unlicense OR MIT","files":[{"path":"COPYING","sha256":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f"},{"path":"LICENSE-MIT","sha256":"0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f"},{"path":"UNLICENSE","sha256":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c"}]}, + {"name":"bytes","version":"1.12.1","license":"MIT","files":[{"path":"LICENSE","sha256":"45f522cacecb1023856e46df79ca625dfc550c94910078bd8aec6e02880b3d42"}]}, + {"name":"bytes-str","version":"0.2.9","license":"Apache-2.0","files":[{"path":"https://github.com/dudykr/ddbase.git","sha256":"58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd"}]}, + {"name":"camino","version":"1.1.11","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"cargo_metadata","version":"0.18.1","license":"MIT","files":[{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"cargo-platform","version":"0.1.9","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"8ada45cd9f843acf64e4722ae262c622a2b3b3007c7310ef36ac1061a30f6adb"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"castaway","version":"0.2.4","license":"MIT","files":[{"path":"LICENSE","sha256":"f98e09091d5ae02b2f2ec1ead4f7f28c4c44d1cb98b078739ba67091637e170c"}]}, + {"name":"cbor4ii","version":"1.2.2","license":"MIT","files":[{"path":"LICENSE","sha256":"3f09ca77e208780d4b2c1b6e8e3404cb747c540b766a5292d7cbe8a196d3f844"}]}, + {"name":"cfg-if","version":"1.0.5","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397"}]}, + {"name":"colored","version":"3.1.1","license":"MPL-2.0","files":[{"path":"LICENSE","sha256":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85"}]}, + {"name":"compact_str","version":"0.10.0","license":"MIT","files":[{"path":"LICENSE","sha256":"0d52feec79589df30138dad3800323fe5686b764b73347330a2dfef8ac83efe8"}]}, + {"name":"compact_str","version":"0.7.1","license":"MIT","files":[{"path":"LICENSE","sha256":"0d52feec79589df30138dad3800323fe5686b764b73347330a2dfef8ac83efe8"}]}, + {"name":"concurrent_lru","version":"0.2.0","license":"MIT","files":[{"path":"LICENSE","sha256":"95a8b499fd09a26945eb97a46d53405ec35442f60e77f354a5c598c63f439e1d"}]}, + {"name":"convert_case","version":"0.10.0","license":"MIT","files":[{"path":"LICENSE","sha256":"aed7b1758e35afa0cd0fde059d61950747ca11cd0e5e169cb21c11608daed772"}]}, + {"name":"convert_case","version":"0.12.0","license":"MIT","files":[{"path":"LICENSE","sha256":"aed7b1758e35afa0cd0fde059d61950747ca11cd0e5e169cb21c11608daed772"}]}, + {"name":"cpufeatures","version":"0.2.17","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5"},{"path":"LICENSE-MIT","sha256":"ae9baa7beea910273c2f384c2a6b721fb7bd02bda3436074a1072e4ee689f985"}]}, + {"name":"crc32fast","version":"1.5.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"61d383b05b87d78f94d2937e2580cce47226d17823c0430fbcad09596537efcf"}]}, + {"name":"crossbeam-utils","version":"0.8.21","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"5734ed989dfca1f625b40281ee9f4530f91b2411ec01cb748223e7eb87e201ab"}]}, + {"name":"crypto-common","version":"0.1.6","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5"},{"path":"LICENSE-MIT","sha256":"3521672491a3479422d5fe1aca6645dd2984090f85da6e5205abfb18fb7a6897"}]}, + {"name":"cssparser","version":"0.38.0","license":"MPL-2.0","files":[{"path":"LICENSE","sha256":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85"}]}, + {"name":"cssparser-macros","version":"0.7.0","license":"MPL-2.0","files":[{"path":"LICENSE","sha256":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85"}]}, + {"name":"ctor","version":"1.0.13","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a8ad31b1c3f40dca5a84119351b8fa8ddc868edd77fad8a8ebf6d8f2d16fa4ae"},{"path":"LICENSE-MIT","sha256":"bccaa8b6c09f94e81f06696e179dbe058464bbdfbc823b6d49cada1d71e84ac3"}]}, + {"name":"darling","version":"0.20.11","license":"MIT","files":[{"path":"LICENSE","sha256":"8ea93490d74a5a1b1af3ff71d786271b3f1e5f0bea79ac16e02ec533cef040d6"}]}, + {"name":"darling_core","version":"0.20.11","license":"MIT","files":[{"path":"LICENSE","sha256":"8ea93490d74a5a1b1af3ff71d786271b3f1e5f0bea79ac16e02ec533cef040d6"}]}, + {"name":"darling_macro","version":"0.20.11","license":"MIT","files":[{"path":"LICENSE","sha256":"8ea93490d74a5a1b1af3ff71d786271b3f1e5f0bea79ac16e02ec533cef040d6"}]}, + {"name":"dashmap","version":"6.2.1","license":"MIT","files":[{"path":"LICENSE","sha256":"16692e8cee4aa06e3913787497eba2d47c42002014136f5da67be6ee640e28a3"}]}, + {"name":"data-encoding","version":"2.11.1","license":"MIT","files":[{"path":"LICENSE","sha256":"b68ad1a3367b825447089e1f8d6829b97f47a89eb78d2f4ebaef4672f5606186"}]}, + {"name":"debugid","version":"0.8.0","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"}]}, + {"name":"derive_builder","version":"0.20.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"8c9612877aacfa1b42f5e80b679e6c3c93a7ba2bb99dfaf6e87e200dca4b4e6c"}]}, + {"name":"derive_builder_core","version":"0.20.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"8c9612877aacfa1b42f5e80b679e6c3c93a7ba2bb99dfaf6e87e200dca4b4e6c"}]}, + {"name":"derive_builder_macro","version":"0.20.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"8c9612877aacfa1b42f5e80b679e6c3c93a7ba2bb99dfaf6e87e200dca4b4e6c"}]}, + {"name":"derive_more","version":"2.1.1","license":"MIT","files":[{"path":"LICENSE","sha256":"8a35369f3ca263b3c62fbb5032947e53b6bfebc6c8a4d1bb982de1c069f6fba5"}]}, + {"name":"derive_more-impl","version":"2.1.1","license":"MIT","files":[{"path":"LICENSE","sha256":"8a35369f3ca263b3c62fbb5032947e53b6bfebc6c8a4d1bb982de1c069f6fba5"}]}, + {"name":"diff","version":"0.1.13","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1"},{"path":"LICENSE-MIT","sha256":"c5de694d2f7a6084593c7280222149b683f084103f9627be3a22ed5eee4ce86c"}]}, + {"name":"difference","version":"2.0.0","license":"MIT","files":[{"path":"LICENSE","sha256":"1792fda8a7468d32b8b24322c0cf1c083ba3053afcd315f10b46668ba01b77a3"}]}, + {"name":"digest","version":"0.10.7","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5"},{"path":"LICENSE-MIT","sha256":"9e0dfd2dd4173a530e238cb6adb37aa78c34c6bc7444e0e10c1ab5d8881f63ba"}]}, + {"name":"displaydoc","version":"0.2.7","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"dragonbox_ecma","version":"0.1.12","license":"Apache-2.0 WITH LLVM-exception OR BSL-1.0","files":[{"path":"LICENSE-Apache2-LLVM","sha256":"624d724f544656ff2f965b3b41e3050133fa042d80292a05330c786a12808cb9"},{"path":"LICENSE-Boost","sha256":"c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566"}]}, + {"name":"dtoa","version":"1.0.10","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"dtoa-short","version":"0.3.5","license":"MPL-2.0","files":[{"path":"LICENSE","sha256":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5"}]}, + {"name":"either","version":"1.19.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7576269ea71f767b99297934c0b2367532690f8c4badc695edf8e04ab6a1e545"}]}, + {"name":"endian-type","version":"0.2.0","license":"MIT","files":[{"path":"LICENSE","sha256":"2a50d5b85df79a0ad844670b1c93fbbbfa2d243e23109ab04c3b273950efa329"}]}, + {"name":"env_logger","version":"0.10.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"6efb0476a1cc085077ed49357026d8c173bf33017278ef440f222fb9cbcb66e6"}]}, + {"name":"equivalent","version":"1.0.3","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7365cc8878a1d7ce155a58c4ca09c3d7a6be413efa5334a80ea842912b669349"}]}, + {"name":"errno","version":"0.3.14","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"8764a597675778ddfd4e25f81b08a05dbcf089ac05662df7613fe67f150e3aa2"}]}, + {"name":"fancy-regex","version":"0.19.2","license":"MIT","files":[{"path":"LICENSE","sha256":"3bc70e239e91272782006c638fc0452a714d384224f11f0923036b7be07cf9b5"}]}, + {"name":"fast-glob","version":"1.0.0","license":"MIT","files":[{"path":"https://github.com/oxc-project/fast-glob.git","sha256":"6820b5c369fa4c0cda088630d17a635f1dcdb4b5501f6077996934fab1690e2b"}]}, + {"name":"fastrand","version":"2.3.0","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"fixedbitset","version":"0.5.7","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"ce592787ff2321feab698a4c612237f4378cc658ebb1d472913e5802cc47afb4"}]}, + {"name":"flate2","version":"1.1.5","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"75d3bf11a47fed60aee0a6cc56a97b896b470ee8fb52d6cd553bd8348337f422"}]}, + {"name":"float-cmp","version":"0.10.0","license":"MIT","files":[{"path":"LICENSE","sha256":"40be1e77825d7e49485a2e43d89bed29dfff29f8f529e71d3c683656021f0d08"}]}, + {"name":"fnv","version":"1.0.7","license":"Apache-2.0 / MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"65fdb6c76cd61612070c066eec9ecdb30ee74fb27859d0d9af58b9f499fd0c3e"}]}, + {"name":"foldhash","version":"0.2.0","license":"Zlib","files":[{"path":"LICENSE","sha256":"b1181a40b2a7b25cf66fd01481713bc1005df082c53ef73e851e55071b102744"}]}, + {"name":"form_urlencoded","version":"1.2.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"20c7855c364d57ea4c97889a5e8d98470a9952dade37bd9248b9a54431670e5e"}]}, + {"name":"from_variant","version":"3.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"funty","version":"2.0.0","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"f790cc576999f5998c766d3d26d7d64dc368e805a98461484f65e8d961ec6d9f"}]}, + {"name":"futures","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-channel","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-core","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-executor","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-io","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-macro","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-sink","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-task","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"futures-util","version":"0.3.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"275c491d6d1160553c32fd6127061d7f9606c3ea25abfad6ca3f6ed088785427"},{"path":"LICENSE-MIT","sha256":"6652c868f35dfe5e8ef636810a4e576b9d663f3a17fb0f5613ad73583e1b88fd"}]}, + {"name":"generic-array","version":"0.14.9","license":"MIT","files":[{"path":"LICENSE","sha256":"c09aae9d3c77b531f56351a9947bc7446511d6b025b3255312d3e3442a9a7583"}]}, + {"name":"getrandom","version":"0.3.4","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"aaff376532ea30a0cd5330b9502ad4a4c8bf769c539c87ffe78819d188a18ebf"},{"path":"LICENSE-MIT","sha256":"29e9fe5074bd27e0e5d5d110394fbbcd841baee2651a3c4b4560a632702cede4"}]}, + {"name":"glob","version":"0.3.4","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"globset","version":"0.4.18","license":"Unlicense OR MIT","files":[{"path":"COPYING","sha256":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f"},{"path":"LICENSE-MIT","sha256":"0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f"},{"path":"UNLICENSE","sha256":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c"}]}, + {"name":"halfbrown","version":"0.4.0","license":"Apache-2.0/MIT","files":[{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"002c2696d92b5c8cf956c11072baa58eaf9f6ade995c031ea635c6a1ee342ad1"}]}, + {"name":"hashbrown","version":"0.14.5","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"ff8f68cb076caf8cefe7a6430d4ac086ce6af2ca8ce2c4e5a2004d4552ef52a2"}]}, + {"name":"hashbrown","version":"0.16.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"ff8f68cb076caf8cefe7a6430d4ac086ce6af2ca8ce2c4e5a2004d4552ef52a2"}]}, + {"name":"hashbrown","version":"0.17.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"ff8f68cb076caf8cefe7a6430d4ac086ce6af2ca8ce2c4e5a2004d4552ef52a2"}]}, + {"name":"heck","version":"0.5.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0"}]}, + {"name":"hermit-abi","version":"0.4.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"hermit-abi","version":"0.5.3","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"hex","version":"0.4.3","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"f7bdb3426d045cd50efd4953026e3eb5a83d0199f458a075602611b9344da5b9"}]}, + {"name":"hstr","version":"4.0.0","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd"}]}, + {"name":"humantime","version":"2.1.0","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"f6deca8261a8f4a3403dc74c725c46051157fd36c27cd4b100277eb1f303ad11"}]}, + {"name":"icu_collator","version":"2.3.1","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_collator_data","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_collections","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_locale_core","version":"2.4.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_locale_fallback","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_locale_fallback_data","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_normalizer","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_normalizer_data","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_properties","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_properties_data","version":"2.3.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"icu_provider","version":"2.4.0","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"ident_case","version":"1.0.1","license":"MIT/Apache-2.0","files":[{"path":"LICENSE","sha256":"508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1"}]}, + {"name":"idna","version":"1.1.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"b38f11f6096706e6de553dabe2a7ed142d59b6fa8c97e290c67496154745cdd5"}]}, + {"name":"idna_adapter","version":"1.2.2","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"8b43ce8accd61e9d370b5ca9e9c4f953279b5c239926c62315b40e24df51b726"}]}, + {"name":"if_chain","version":"1.0.3","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"5de1878b4dbad85dc60131e58e6bca4503f7b141c2f7b2b295151568859c964a"}]}, + {"name":"indexmap","version":"2.14.2","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"ecc269ef87fd38a1d98e30bfac9ba964a9dbd9315c3770fed98d4d7cb5882055"}]}, + {"name":"is-macro","version":"0.3.8","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd"}]}, + {"name":"is-terminal","version":"0.4.15","license":"MIT","files":[{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"},{"path":"LICENSE-MIT-atty","sha256":"bab426a663ce3d5bbbcea9cdc300da74e94d76c3c79e46699a953f967a08e533"}]}, + {"name":"itoa","version":"1.0.18","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"json-strip-comments","version":"3.1.0","license":"MIT","files":[{"path":"LICENSE","sha256":"95ac278b1fd5fdbbb5d48f8318862ff48b49b96e94ef927f61399968996bf868"}]}, + {"name":"lazy_static","version":"1.5.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"0621878e61f0d0fda054bcbe02df75192c28bde1ecc8289cbd86aeba2dd72720"}]}, + {"name":"libc","version":"0.2.191","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"123a331b5dbf04c30097fa43b8f858bc85df671fe776de498d01f3d6b7c1f69e"}]}, + {"name":"libloading","version":"0.9.0","license":"ISC","files":[{"path":"LICENSE","sha256":"b29f8b01452350c20dd1af16ef83b598fea3053578ccc1c7a0ef40e57be2620f"}]}, + {"name":"libmimalloc-sys","version":"0.1.49","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"a08554fab028af3af047e8588d32d019662c1b1bf07959a195a2f75fe340726b"},{"path":"c_src/mimalloc/v2/LICENSE","sha256":"82ade5d7d9b029044b5fbbc326207fc47c2d44ee4dd71e8a559c36d728217de9"},{"path":"c_src/mimalloc/v3/LICENSE","sha256":"82ade5d7d9b029044b5fbbc326207fc47c2d44ee4dd71e8a559c36d728217de9"}]}, + {"name":"libz-rs-sys","version":"0.5.4","license":"Zlib","files":[{"path":"LICENSE","sha256":"e72111c52b7d96ebe25348dee19f0744f444d3c95ae6b1ecb6ccaecc5bce05ba"}]}, + {"name":"link-section","version":"0.19.3","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a8ad31b1c3f40dca5a84119351b8fa8ddc868edd77fad8a8ebf6d8f2d16fa4ae"},{"path":"LICENSE-MIT","sha256":"bccaa8b6c09f94e81f06696e179dbe058464bbdfbc823b6d49cada1d71e84ac3"}]}, + {"name":"linktime-proc-macro","version":"0.2.3","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a8ad31b1c3f40dca5a84119351b8fa8ddc868edd77fad8a8ebf6d8f2d16fa4ae"},{"path":"LICENSE-MIT","sha256":"bccaa8b6c09f94e81f06696e179dbe058464bbdfbc823b6d49cada1d71e84ac3"}]}, + {"name":"linux-raw-sys","version":"0.12.1","license":"Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT","files":[{"path":"COPYRIGHT","sha256":"3290ae0fbc9ddb77d2239121d710f0bb9d31b3b4744e6d97fe01e652b4c1870b"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-Apache-2.0_WITH_LLVM-exception","sha256":"268872b9816f90fd8e85db5a28d33f8150ebb8dd016653fb39ef1f94f2686bc5"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"litemap","version":"0.8.3","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"lock_api","version":"0.4.14","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"c9a75f18b9ab2927829a208fc6aa2cf4e63b8420887ba29cdb265d6619ae82d5"}]}, + {"name":"log","version":"0.4.34","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"matchers","version":"0.2.0","license":"MIT","files":[{"path":"LICENSE","sha256":"a47129d738752a6ae52247fea645b42cb320d19e9327f1eb2d1a7f99d9455ad3"}]}, + {"name":"memchr","version":"2.8.3","license":"Unlicense OR MIT","files":[{"path":"COPYING","sha256":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f"},{"path":"LICENSE-MIT","sha256":"0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f"},{"path":"UNLICENSE","sha256":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c"}]}, + {"name":"miette","version":"7.6.0","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"}]}, + {"name":"miette-derive","version":"7.6.0","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"02872c3ff23e11da5a9b3289df11b581be36b4956bfb7cfdc3d8043f92d64794"}]}, + {"name":"mimalloc","version":"0.1.52","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"a08554fab028af3af047e8588d32d019662c1b1bf07959a195a2f75fe340726b"}]}, + {"name":"miniz_oxide","version":"0.8.5","license":"MIT OR Zlib OR Apache-2.0","files":[{"path":"LICENSE","sha256":"4108245a1f2df9d4e94df8abed5b4ba0759bb2f9b40a6b939f1be141077ae50b"},{"path":"LICENSE-APACHE.md","sha256":"0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"},{"path":"LICENSE-MIT.md","sha256":"799e9ca9d179295ef372f25d3769cdda7d25bb2668add6a6a1e22d1e4c678b8d"},{"path":"LICENSE-ZLIB.md","sha256":"0a54e647fe54104658b5e563c04c6f9edf251710e47bce692e0bd990a4ddaa39"}]}, + {"name":"murmur2","version":"0.1.0","license":"Apache-2.0","files":[{"path":"https://github.com/jcaesar/murmur2rs","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"napi","version":"3.16.1","license":"MIT","files":[{"path":"https://github.com/napi-rs/napi-rs","sha256":"3f1ce66533302df3a32edbfdfc0b78f0dd34659e4c1f5817162e5ea3c2297215"}]}, + {"name":"napi-derive","version":"3.7.0","license":"MIT","files":[{"path":"https://github.com/napi-rs/napi-rs","sha256":"3f1ce66533302df3a32edbfdfc0b78f0dd34659e4c1f5817162e5ea3c2297215"}]}, + {"name":"napi-derive-backend","version":"7.0.0","license":"MIT","files":[{"path":"https://github.com/napi-rs/napi-rs","sha256":"3f1ce66533302df3a32edbfdfc0b78f0dd34659e4c1f5817162e5ea3c2297215"}]}, + {"name":"napi-sys","version":"3.4.1","license":"MIT","files":[{"path":"https://github.com/napi-rs/napi-rs","sha256":"3f1ce66533302df3a32edbfdfc0b78f0dd34659e4c1f5817162e5ea3c2297215"}]}, + {"name":"new_debug_unreachable","version":"1.0.6","license":"MIT","files":[{"path":"LICENSE-MIT","sha256":"f7715d38a3fa1b4ac97c5729740752505a39cb92ee83ab5b102aeb5eaa7cdea4"}]}, + {"name":"nibble_vec","version":"0.1.0","license":"MIT","files":[{"path":"LICENSE","sha256":"e4e06f4bee6b18fbe6ab33b26331739a0b2bda0c1037cfa869e7d97512e9fcf7"}]}, + {"name":"nodejs-built-in-modules","version":"1.0.0","license":"MIT","files":[{"path":"https://github.com/oxc-project/nodejs-built-in-modules","sha256":"20538257cfb1812d48164a7d11c0008a66b33c01b4debdfa0ec2a104fe00f028"}]}, + {"name":"nohash-hasher","version":"0.2.0","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"ec353d4fecf7963b4c054384557e5dbc3c7a717997eb4a3815b315721a6aa75a"}]}, + {"name":"nu-ansi-term","version":"0.50.1","license":"MIT","files":[{"path":"LICENSE","sha256":"cfdb7491dfa8800a1d561957d6a072ad63a6305ffa48386d04eae8eac8d65e8a"}]}, + {"name":"num_cpus","version":"1.17.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"391a5396cec6230bfabd4ef4eb2350eb895bc5efce377a2218f5702ed020d3e3"}]}, + {"name":"num-bigint","version":"0.4.8","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"num-integer","version":"0.1.47","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"num-traits","version":"0.2.19","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"once_cell","version":"1.21.4","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"outref","version":"0.5.2","license":"MIT","files":[{"path":"LICENSE","sha256":"a00511478c83ebb464ef19d840f061c7444adc4636bb561900e75c5808cfc6cc"}]}, + {"name":"owo-colors","version":"4.2.2","license":"MIT","files":[{"path":"LICENSE","sha256":"267afe8db5697b4b668d36a5a0d481b67d16ae751465e9ddf03d1e77a79c140a"}]}, + {"name":"oxc_resolver","version":"11.24.3","license":"MIT","files":[{"path":"https://github.com/oxc-project/oxc-resolver","sha256":"95ced5ecf1133fbf41d409b5555c86c344f83f3b019926057ddbc07cfdcc27b3"}]}, + {"name":"package-json","version":"0.5.0","license":"MIT","files":[{"path":"LICENSE","sha256":"ef26a2093e23241cc6d65784be70e6d03491f215b7302ed893fb203581a501e7"}]}, + {"name":"par-core","version":"2.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/dudykr/ddbase.git","sha256":"58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd"}]}, + {"name":"par-iter","version":"2.0.0","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd"}]}, + {"name":"parking_lot","version":"0.12.5","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"c9a75f18b9ab2927829a208fc6aa2cf4e63b8420887ba29cdb265d6619ae82d5"}]}, + {"name":"parking_lot_core","version":"0.9.12","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"c9a75f18b9ab2927829a208fc6aa2cf4e63b8420887ba29cdb265d6619ae82d5"}]}, + {"name":"path-clean","version":"1.0.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"b9fe21454aa22842385c21361374c688c593151ad34630523ec13d5408355060"},{"path":"LICENSE-MIT","sha256":"6b2b798531073d6beeac459787f4c56b0db8e66ae176028d3d11c222a7023b00"}]}, + {"name":"pathdiff","version":"0.2.3","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"dd7818e575815db1fc6ba5ae894cd1674dfbff78562407b1f9f44da0fb857fe4"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"percent-encoding","version":"2.3.2","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"b38f11f6096706e6de553dabe2a7ed142d59b6fa8c97e290c67496154745cdd5"}]}, + {"name":"petgraph","version":"0.7.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7576269ea71f767b99297934c0b2367532690f8c4badc695edf8e04ab6a1e545"},{"path":"assets/LICENSE.md","sha256":"608fdcbb7daea2541c57228f21fc97deec310b19dafde70b2f4b84a96bf779f8"}]}, + {"name":"phf","version":"0.11.3","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf","version":"0.14.0","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf_generator","version":"0.11.3","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf_generator","version":"0.14.0","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf_macros","version":"0.11.3","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf_macros","version":"0.14.0","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf_shared","version":"0.11.3","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"phf_shared","version":"0.14.0","license":"MIT","files":[{"path":"LICENSE","sha256":"0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38"}]}, + {"name":"pin-project-lite","version":"0.2.17","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"pnp","version":"0.12.12","license":"BSD-2-Clause","files":[{"path":"LICENSE.md","sha256":"238d933f5c226cc197bd1dae2ad0c468e157b4cba8ed844f81549ba6db777dc4"}]}, + {"name":"postcss_value_parser","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/postcss-value-parser/LICENSE","sha256":"3687447039151857a6ba378db062172c7f33d4aa70a615c87a43a9c50e990485"}]}, + {"name":"potential_utf","version":"0.1.6","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"pretty_assertions","version":"1.4.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08"},{"path":"LICENSE-MIT","sha256":"8c9612877aacfa1b42f5e80b679e6c3c93a7ba2bb99dfaf6e87e200dca4b4e6c"}]}, + {"name":"pretty_env_logger","version":"0.5.0","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7b033f57f23fffca5bee9406cf453ce31e919c1b867df54bfa7487e73d8c9c1f"}]}, + {"name":"proc-macro2","version":"1.0.107","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"psm","version":"0.1.32","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"965a63a81d9a2fbeb5f9096954dabb49690f9dffcdac9825f675b25c807252a2"},{"path":"LICENSE-MIT","sha256":"3e3714aa69bd874601741fd7d7ad5298740cece37778e279fc1ab4451c5a11af"}]}, + {"name":"quote","version":"1.0.47","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"r-efi","version":"5.3.0","license":"MIT OR Apache-2.0 OR LGPL-2.1-or-later","files":[{"path":"https://github.com/r-efi/r-efi","sha256":"ff92bed461f50338dd703a9ba9aee496a425957873df1d91192776e4bdf5dda7"},{"path":"https://github.com/r-efi/r-efi","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"radium","version":"0.7.0","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"13f4cc9fbc8d4a447b28aa84019c10ad4abf4b5f6919db061bf6690ccc23bc02"}]}, + {"name":"radix_fmt","version":"1.0.0","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"430dfad25bf0492d2ac16e6a0c17897afdab04e45bf5e06626ec998d78e8af3e"}]}, + {"name":"radix_trie","version":"0.3.0","license":"MIT","files":[{"path":"LICENSE","sha256":"e4e06f4bee6b18fbe6ab33b26331739a0b2bda0c1037cfa869e7d97512e9fcf7"}]}, + {"name":"rand","version":"0.8.8","license":"MIT OR Apache-2.0","files":[{"path":"COPYRIGHT","sha256":"90eb64f0279b0d9432accfa6023ff803bc4965212383697eee27a0f426d5f8d5"},{"path":"LICENSE-APACHE","sha256":"35242e7a83f69875e6edeff02291e688c97caafe2f8902e4e19b49d3e78b4cab"},{"path":"LICENSE-MIT","sha256":"209fbbe0ad52d9235e37badf9cadfe4dbdc87203179c0899e738b39ade42177b"}]}, + {"name":"rand_core","version":"0.6.4","license":"MIT OR Apache-2.0","files":[{"path":"COPYRIGHT","sha256":"90eb64f0279b0d9432accfa6023ff803bc4965212383697eee27a0f426d5f8d5"},{"path":"LICENSE-APACHE","sha256":"6df43f6f4b5d4587f3d8d71e45532c688fd168afa5fe89d571cb32fa09c4ef51"},{"path":"LICENSE-MIT","sha256":"209fbbe0ad52d9235e37badf9cadfe4dbdc87203179c0899e738b39ade42177b"}]}, + {"name":"redox_syscall","version":"0.5.18","license":"MIT","files":[{"path":"LICENSE","sha256":"efcfee7981ff72431fffb06925cad00a23dce079ed4354f61030ad5abdb78829"}]}, + {"name":"ref-cast","version":"1.0.25","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"ref-cast-impl","version":"1.0.25","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"regex","version":"1.13.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"regex-automata","version":"0.4.18","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"}]}, + {"name":"regex-syntax","version":"0.8.11","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb"},{"path":"src/unicode_tables/LICENSE-UNICODE","sha256":"74db5baf44a41b1000312c673544b3374e4198af5605c7f9080a402cec42cfa3"}]}, + {"name":"regress","version":"0.11.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"e36d55837170195cdf934aa6a48c0ad6efba1bde3f5b4a1b9259c171d5aeb647"},{"path":"LICENSE-MIT","sha256":"3f8a05fc8617c5ce569fa5a4f2301cd847fe00fa011bdaa8f353fd55a2691a54"}]}, + {"name":"regress","version":"0.12.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"e36d55837170195cdf934aa6a48c0ad6efba1bde3f5b4a1b9259c171d5aeb647"},{"path":"LICENSE-MIT","sha256":"3f8a05fc8617c5ce569fa5a4f2301cd847fe00fa011bdaa8f353fd55a2691a54"}]}, + {"name":"relative-path","version":"1.9.3","license":"MIT OR Apache-2.0","files":[{"path":"https://github.com/udoprog/relative-path","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"}]}, + {"name":"rustc-hash","version":"2.1.3","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"95bd3988beee069fa2848f648dab43cc6e0b2add2ad6bcb17360caf749802bcc"},{"path":"LICENSE-MIT","sha256":"30fefc3a7d6a0041541858293bcbea2dde4caa4c0a5802f996a7f7e8c0085652"}]}, + {"name":"rustix","version":"1.1.4","license":"Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT","files":[{"path":"COPYRIGHT","sha256":"377c2e7c53250cc5905c0b0532d35973392af16ffb9596a41d99d202cf3617c9"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-Apache-2.0_WITH_LLVM-exception","sha256":"268872b9816f90fd8e85db5a28d33f8150ebb8dd016653fb39ef1f94f2686bc5"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"rustversion","version":"1.0.23","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"ryu","version":"1.0.23","license":"Apache-2.0 OR BSL-1.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-BOOST","sha256":"c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566"}]}, + {"name":"ryu-js","version":"1.0.3","license":"Apache-2.0 OR BSL-1.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-BOOST","sha256":"c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566"}]}, + {"name":"scoped-tls","version":"1.0.1","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397"}]}, + {"name":"scopeguard","version":"1.2.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"fb77f0a9c53e473abe5103c8632ef9f0f2874d4fb3f17cb2d8c661aab9cee9d7"}]}, + {"name":"self_cell","version":"1.2.1","license":"Apache-2.0 OR GPL-2.0-only","files":[{"path":"https://github.com/Voultapher/self_cell","sha256":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"}]}, + {"name":"semver","version":"1.0.28","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"seq-macro","version":"0.3.6","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"serde","version":"1.0.229","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"serde_core","version":"1.0.229","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"serde_derive","version":"1.0.229","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"serde_json","version":"1.0.154","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"sha1","version":"0.10.6","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5"},{"path":"LICENSE-MIT","sha256":"b4eb00df6e2a4d22518fcaa6a2b4646f249b3a3c9814509b22bd2091f1392ff1"}]}, + {"name":"sha2","version":"0.10.9","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5"},{"path":"LICENSE-MIT","sha256":"b4eb00df6e2a4d22518fcaa6a2b4646f249b3a3c9814509b22bd2091f1392ff1"}]}, + {"name":"sharded-slab","version":"0.1.7","license":"MIT","files":[{"path":"LICENSE","sha256":"eafbfa606bc005ed7fd2f623d65af17ffe4ef7c017221ac14405bf4140771ea9"}]}, + {"name":"simd-adler32","version":"0.3.8","license":"MIT","files":[{"path":"LICENSE.md","sha256":"42a35170233e83e18856792e748de4c1ce4a63b2afce9a370c89ef3fe23f9f2d"}]}, + {"name":"simd-json","version":"0.18.1","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"002c2696d92b5c8cf956c11072baa58eaf9f6ade995c031ea635c6a1ee342ad1"}]}, + {"name":"simdutf8","version":"0.1.5","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-Apache","sha256":"0cec06e0e55fbc3dc5cee4fca9b607f66cb8f4e4dbcf3b3c013594dd156732e9"},{"path":"LICENSE-MIT","sha256":"1847e0e0698142ed4347c1441a9fa81c8fbddd44b1d8bbcd5e3647f991759d7f"}]}, + {"name":"siphasher","version":"0.3.11","license":"MIT/Apache-2.0","files":[{"path":"COPYING","sha256":"306b56d1a86d1ff32cffdf3bd26d809354f7f06bcc8c4c1697fcb5e28aeedb7a"}]}, + {"name":"siphasher","version":"1.0.4","license":"MIT OR Apache-2.0","files":[{"path":"COPYING","sha256":"c962ee4d1d05ddc138b202b2540219ebc57893fcf97b364852094a9a94ce1365"},{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"f9c4c77baa3828004ee54b8a4f2db2e88ed44a6237a493965bf551fac0fcb62d"}]}, + {"name":"slab","version":"0.4.12","license":"MIT","files":[{"path":"LICENSE","sha256":"8ce0830173fdac609dfb4ea603fdc002c2f4af0dc9b1a005653f5da9cf534b18"}]}, + {"name":"smallvec","version":"1.16.4","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"0b28172679e0009b655da42797c03fd163a3379d5cfa67ba1f1655e974a2a1a9"}]}, + {"name":"stable_deref_trait","version":"1.2.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"5e05b024f653a5ce199e77cbbbd42fb5553562ec714b819421ed0c3e552a75d7"}]}, + {"name":"stacker","version":"0.1.25","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397"}]}, + {"name":"static_assertions","version":"1.1.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"ea084a2373ebc1f0902c09266e7bf25a05ab3814c1805bb017ffa7308f90c061"}]}, + {"name":"string_enum","version":"1.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"strsim","version":"0.11.1","license":"MIT","files":[{"path":"LICENSE","sha256":"1e697ce8d21401fbf1bddd9b5c3fd4c4c79ae1e3bdf51f81761c85e11d5a89cd"}]}, + {"name":"stylex_ast","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_atoms","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_constants","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_css","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_css_parser","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_diagnostics","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_enums","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_evaluator","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_js","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_macros","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_nested_config","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_path_resolver","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_regex","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_rust_compiler","version":"0.19.1","license":"MIT","files":[{"path":"LICENSE","sha256":"da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93"}]}, + {"name":"stylex_state","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_state_index","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_structures","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_styleq","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/stylex-styleq/LICENSE","sha256":"b00770e3c5d6a029cb104f7ac0d328c88ec684d31e8188c61ca91fcc17743cdc"}]}, + {"name":"stylex_transform","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_types","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"stylex_utils","version":"0.19.2","license":"MIT","files":[{"path":"packages/@stylexjs/rust-plugin/crates/LICENSE","sha256":"2f9d346f4283465d016816c7fe4a2c8b634c73a35bd2a23b7ba26ec9f0135834"},{"path":"packages/@stylexjs/rust-plugin/crates/NOTICE.md","sha256":"1ed3ad818f92ce8fd1a33b749f72607aa409d2cb60cd5534a7ec7026b753c657"}]}, + {"name":"swc_allocator","version":"5.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_atoms","version":"10.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_common","version":"26.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_compiler_base","version":"65.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_config","version":"6.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_config_macro","version":"1.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_core","version":"81.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_ast","version":"29.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_codegen","version":"32.0.3","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_codegen_macros","version":"2.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_hooks","version":"1.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_minifier","version":"62.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_parser","version":"45.1.6","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_testing","version":"27.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_transforms_base","version":"49.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_transforms_optimization","version":"52.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_transforms_react","version":"55.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_transforms_testing","version":"53.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_transforms_typescript","version":"55.0.4","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_utils","version":"35.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_ecma_visit","version":"29.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_eq_ignore_macros","version":"1.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_error_reporters","version":"28.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_macros_common","version":"1.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_malloc","version":"1.2.5","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_plugin","version":"1.0.1","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_plugin_macro","version":"2.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_plugin_proxy","version":"30.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_sourcemap","version":"10.0.3","license":"BSD-3-Clause","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"7516e1cf340213f60d96bca77bb012882dbf80e7cca5922c914174f605d9ef71"}]}, + {"name":"swc_transform_common","version":"20.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"swc_visit","version":"2.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"syn","version":"2.0.119","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"syn","version":"3.0.9","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"synstructure","version":"0.14.0","license":"MIT","files":[{"path":"LICENSE","sha256":"219920e865eee70b7dcfc948a86b099e7f4fe2de01bcca2ca9a20c0a033f2b59"}]}, + {"name":"tap","version":"1.0.1","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"8b4e95f5cf0dc40269c99f7b787203ffe04ded245ca2427422c196efa2b2f42a"}]}, + {"name":"tempfile","version":"3.21.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"8b427f5bc501764575e52ba4f9d95673cf8f6d80a86d0d06599852e1a9a20a36"}]}, + {"name":"termcolor","version":"1.4.1","license":"Unlicense OR MIT","files":[{"path":"COPYING","sha256":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f"},{"path":"LICENSE-MIT","sha256":"0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f"},{"path":"UNLICENSE","sha256":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c"}]}, + {"name":"testing","version":"27.0.0","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"testing_macros","version":"1.0.2","license":"Apache-2.0","files":[{"path":"https://github.com/swc-project/swc.git","sha256":"d96eba1f1881334d50ffc144276dfa98e147a415e7c591ecdfcb81680361dc91"}]}, + {"name":"textwrap","version":"0.16.2","license":"MIT","files":[{"path":"LICENSE","sha256":"ce93600c49fbb3e14df32efe752264644f6a2f8e08a735ba981725799e5309ef"}]}, + {"name":"thiserror","version":"1.0.69","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"thiserror","version":"2.0.21","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"thiserror-impl","version":"1.0.69","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"thiserror-impl","version":"2.0.21","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"thread_local","version":"1.1.9","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"c9a75f18b9ab2927829a208fc6aa2cf4e63b8420887ba29cdb265d6619ae82d5"}]}, + {"name":"tikv-jemalloc-sys","version":"0.5.4+5.3.0-patched","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397"},{"path":"jemalloc/COPYING","sha256":"94aa2caa98c25d942f58b956c71dba6a99ff98fc3a31cbc669fe2a4cd0268b53"}]}, + {"name":"tikv-jemallocator","version":"0.5.4","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397"}]}, + {"name":"tinystr","version":"0.8.5","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"tracing","version":"0.1.44","license":"MIT","files":[{"path":"LICENSE","sha256":"898b1ae9821e98daf8964c8d6c7f61641f5f5aa78ad500020771c0939ee0dea1"}]}, + {"name":"tracing-attributes","version":"0.1.31","license":"MIT","files":[{"path":"LICENSE","sha256":"898b1ae9821e98daf8964c8d6c7f61641f5f5aa78ad500020771c0939ee0dea1"}]}, + {"name":"tracing-core","version":"0.1.36","license":"MIT","files":[{"path":"LICENSE","sha256":"898b1ae9821e98daf8964c8d6c7f61641f5f5aa78ad500020771c0939ee0dea1"},{"path":"src/spin/LICENSE","sha256":"58545fed1565e42d687aecec6897d35c6d37ccb71479a137c0deb2203e125c79"}]}, + {"name":"tracing-log","version":"0.2.0","license":"MIT","files":[{"path":"LICENSE","sha256":"898b1ae9821e98daf8964c8d6c7f61641f5f5aa78ad500020771c0939ee0dea1"}]}, + {"name":"tracing-subscriber","version":"0.3.20","license":"MIT","files":[{"path":"LICENSE","sha256":"898b1ae9821e98daf8964c8d6c7f61641f5f5aa78ad500020771c0939ee0dea1"}]}, + {"name":"triomphe","version":"0.1.17","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a6b7260485462860838b76ad1846d21f1605e6d239141c3aa949a601d992676c"},{"path":"LICENSE-MIT","sha256":"15656cc11a8331f28c0986b8ab97220d3e76f98e60ed388b5ffad37dfac4710c"}]}, + {"name":"typenum","version":"1.19.0","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE","sha256":"db11fec9946737df39ca3898d9cd8c10ec6f6c3a884a6802b0ad0b81b4e8f23a"},{"path":"LICENSE-APACHE","sha256":"516b24e051bf5630880ebbd55c40a25ce9552ebaf8970a53e8976eb70e522406"},{"path":"LICENSE-MIT","sha256":"a825bd853ab71619a4923d7b4311221427848070ff44d990da39b0b274c1683f"}]}, + {"name":"unicode-id-start","version":"1.5.0","license":"(MIT OR Apache-2.0) AND Unicode-3.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"},{"path":"LICENSE-UNICODE","sha256":"f7db81051789b729fea528a63ec4c938fdcb93d9d61d97dc8cc2e9df6d47f2a1"}]}, + {"name":"unicode-ident","version":"1.0.27","license":"(MIT OR Apache-2.0) AND Unicode-3.0","files":[{"path":"LICENSE-APACHE","sha256":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"},{"path":"LICENSE-UNICODE","sha256":"f7db81051789b729fea528a63ec4c938fdcb93d9d61d97dc8cc2e9df6d47f2a1"}]}, + {"name":"unicode-linebreak","version":"0.1.5","license":"Apache-2.0","files":[{"path":"LICENSE","sha256":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"}]}, + {"name":"unicode-segmentation","version":"1.13.3","license":"MIT OR Apache-2.0","files":[{"path":"COPYRIGHT","sha256":"23860c2a7b5d96b21569afedf033469bab9fe14a1b24a35068b8641c578ce24d"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0"}]}, + {"name":"unicode-width","version":"0.1.14","license":"MIT OR Apache-2.0","files":[{"path":"COPYRIGHT","sha256":"23860c2a7b5d96b21569afedf033469bab9fe14a1b24a35068b8641c578ce24d"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0"}]}, + {"name":"unicode-width","version":"0.2.3","license":"MIT OR Apache-2.0","files":[{"path":"COPYRIGHT","sha256":"23860c2a7b5d96b21569afedf033469bab9fe14a1b24a35068b8641c578ce24d"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0"}]}, + {"name":"unicode-xid","version":"0.2.6","license":"MIT OR Apache-2.0","files":[{"path":"COPYRIGHT","sha256":"23860c2a7b5d96b21569afedf033469bab9fe14a1b24a35068b8641c578ce24d"},{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0"}]}, + {"name":"url","version":"2.5.8","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"b38f11f6096706e6de553dabe2a7ed142d59b6fa8c97e290c67496154745cdd5"}]}, + {"name":"utf_types","version":"1.0.0","license":"Apache-2.0 OR MIT","files":[{"path":"COPYRIGHT","sha256":"3210be7332b5bdf48eb24a945258b9f38616a2cceb0dfc06e3c3c7e9740475a0"},{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c"}]}, + {"name":"utf16_iter","version":"1.1.0","license":"Apache-2.0 OR MIT","files":[{"path":"COPYRIGHT","sha256":"b84efe109a420fa3ca98be33f4227327af7ffa426195812c270feb1268bc2426"},{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c"}]}, + {"name":"utf8_iter","version":"1.1.0","license":"Apache-2.0 OR MIT","files":[{"path":"COPYRIGHT","sha256":"c30152c94a6d75e021adbc52b3a52470366a46edb917e17deae3259251af244c"},{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c"}]}, + {"name":"uuid","version":"1.29.0","license":"Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-MIT","sha256":"436bc5a105d8e57dcd8778730f3754f7bf39c14d2f530e4cde4bd2d17a83ec3d"}]}, + {"name":"value-trait","version":"0.12.1","license":"Apache-2.0/MIT","files":[{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"002c2696d92b5c8cf956c11072baa58eaf9f6ade995c031ea635c6a1ee342ad1"}]}, + {"name":"vsimd","version":"0.8.0","license":"MIT","files":[{"path":"https://github.com/Nugine/simd","sha256":"71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d"}]}, + {"name":"wasip2","version":"1.0.1+wasi-0.2.4","license":"Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT","files":[{"path":"https://github.com/bytecodealliance/wasi-rs","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"}]}, + {"name":"winapi","version":"0.3.9","license":"MIT/Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1"},{"path":"LICENSE-MIT","sha256":"ce7bc3499fee93d5022ef430d5e4201e79a6d9154f3974e42f41349f0569e09b"}]}, + {"name":"winapi-i686-pc-windows-gnu","version":"0.4.0","license":"MIT/Apache-2.0","files":[{"path":"https://github.com/retep998/winapi-rs","sha256":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1"}]}, + {"name":"winapi-util","version":"0.1.11","license":"Unlicense OR MIT","files":[{"path":"COPYING","sha256":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f"},{"path":"LICENSE-MIT","sha256":"cb3c929a05e6cbc9de9ab06a4c57eeb60ca8c724bef6c138c87d3a577e27aa14"},{"path":"UNLICENSE","sha256":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c"}]}, + {"name":"winapi-x86_64-pc-windows-gnu","version":"0.4.0","license":"MIT/Apache-2.0","files":[{"path":"https://github.com/retep998/winapi-rs","sha256":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1"}]}, + {"name":"windows","version":"0.62.2","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_aarch64_gnullvm","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_aarch64_msvc","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_i686_gnu","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_i686_gnullvm","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_i686_msvc","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_x86_64_gnu","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_x86_64_gnullvm","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows_x86_64_msvc","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-collections","version":"0.3.2","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-core","version":"0.62.2","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-future","version":"0.3.2","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-implement","version":"0.60.2","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-interface","version":"0.59.3","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-link","version":"0.2.1","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-numerics","version":"0.3.1","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-result","version":"0.4.1","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-strings","version":"0.5.1","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-sys","version":"0.52.0","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-sys","version":"0.59.0","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-sys","version":"0.61.2","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-targets","version":"0.52.6","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"windows-threading","version":"0.2.1","license":"MIT OR Apache-2.0","files":[{"path":"license-apache-2.0","sha256":"c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b"},{"path":"license-mit","sha256":"c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"}]}, + {"name":"wit-bindgen","version":"0.46.0","license":"Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"},{"path":"LICENSE-Apache-2.0_WITH_LLVM-exception","sha256":"268872b9816f90fd8e85db5a28d33f8150ebb8dd016653fb39ef1f94f2686bc5"},{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]}, + {"name":"write16","version":"1.0.0","license":"Apache-2.0 OR MIT","files":[{"path":"COPYRIGHT","sha256":"3210be7332b5bdf48eb24a945258b9f38616a2cceb0dfc06e3c3c7e9740475a0"},{"path":"LICENSE-APACHE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"LICENSE-MIT","sha256":"3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c"}]}, + {"name":"writeable","version":"0.6.4","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"wyz","version":"0.5.1","license":"MIT","files":[{"path":"LICENSE.txt","sha256":"411781fd38700f2357a14126d0ab048164ab881f1dcb335c1bb932e232c9a2f5"}]}, + {"name":"xxhash-rust","version":"0.8.18","license":"BSL-1.0","files":[{"path":"LICENSE","sha256":"c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566"}]}, + {"name":"yansi","version":"1.0.1","license":"MIT OR Apache-2.0","files":[{"path":"LICENSE-APACHE","sha256":"a8410ec5312922e0edae227ae056c7ea78093092503717ae377a2be4f0d295f1"},{"path":"LICENSE-MIT","sha256":"ce1f970284c4ab06e90671a984086692dc44da3e2b8ee86444d79b6f77bcf9e7"}]}, + {"name":"yoke","version":"0.8.4","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"yoke-derive","version":"0.8.4","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"zerocopy","version":"0.8.63","license":"BSD-2-Clause OR Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"9d185ac6703c4b0453974c0d85e9eee43e6941009296bb1f5eb0b54e2329e9f3"},{"path":"LICENSE-BSD","sha256":"83c1763356e822adde0a2cae748d938a73fdc263849ccff6b27776dff213bd32"},{"path":"LICENSE-MIT","sha256":"1a2f5c12ddc934d58956aa5dbdd3255fe55fd957633ab7d0d39e4f0daa73f7df"}]}, + {"name":"zerocopy-derive","version":"0.8.63","license":"BSD-2-Clause OR Apache-2.0 OR MIT","files":[{"path":"LICENSE-APACHE","sha256":"9d185ac6703c4b0453974c0d85e9eee43e6941009296bb1f5eb0b54e2329e9f3"},{"path":"LICENSE-BSD","sha256":"83c1763356e822adde0a2cae748d938a73fdc263849ccff6b27776dff213bd32"},{"path":"LICENSE-MIT","sha256":"1a2f5c12ddc934d58956aa5dbdd3255fe55fd957633ab7d0d39e4f0daa73f7df"}]}, + {"name":"zerofrom","version":"0.1.8","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"zerofrom-derive","version":"0.1.8","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"zerotrie","version":"0.2.5","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"zerovec","version":"0.11.9","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"zerovec-derive","version":"0.11.7","license":"Unicode-3.0","files":[{"path":"LICENSE","sha256":"f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2"}]}, + {"name":"zlib-rs","version":"0.5.4","license":"Zlib","files":[{"path":"LICENSE","sha256":"e72111c52b7d96ebe25348dee19f0744f444d3c95ae6b1ecb6ccaecc5bce05ba"}]}, + {"name":"zmij","version":"1.0.23","license":"MIT","files":[{"path":"LICENSE-MIT","sha256":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"}]} + ] +} diff --git a/packages/@stylexjs/rust-plugin/README.md b/packages/@stylexjs/rust-plugin/README.md new file mode 100644 index 000000000..5af6b6292 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/README.md @@ -0,0 +1,63 @@ +# @stylexjs/rust-plugin + +A native Rust implementation of the StyleX compiler. It takes the same options as +`@stylexjs/babel-plugin` and produces the same output; the Babel plugin's +unchanged test suite runs against it. The compiler core is based on +[stylex-swc-plugin](https://github.com/Dwlad90/stylex-swc-plugin) by Vladislav +Buinovski (MIT); see `THIRD_PARTY_NOTICES.md`. It is vendored in `crates/` at +0.19.2 with the changes listed in `crates/PROVENANCE.md`. + +The package is private: the native addon is built from source +(`npm run build:native`); no prebuilt binaries are published. + +## Usage + +```js +const { transformSync, processStylexRules } = require('@stylexjs/rust-plugin'); + +const { code, map, metadata } = transformSync(source, { + filename: '/src/Button.js', + sourceMaps: true, + unstable_moduleResolution: { type: 'commonJS', rootDir: process.cwd() }, +}); +const css = processStylexRules(metadata.stylex, { useLayers: true }); +``` + +Function-valued options (`env` functions, custom module resolution, +`debugFilePath`) are called on the calling JavaScript thread. + +When other Babel plugins must run first, use the Babel adapter in place of +`@stylexjs/babel-plugin`: + +```js +plugins: [myPlugin, [require('@stylexjs/rust-plugin/babel'), stylexOptions]]; +``` + +## Differences from the Babel plugin + +- A shadowed `stylex` import or namespace parameter is left as authored. +- Styles that read function parameters, local types or `this` stay in their scope. +- Spread in array literals and in arguments to pure builtins evaluates. +- Optional member reads (`obj?.color`) evaluate. +- `==` and `!=` use JavaScript semantics. +- Functions defined in the file and called later evaluate; a read before its declaration is refused in every order. +- Property reads on arrow functions are refused. +- `stylex.when.ancestor(1)` reports an error instead of crashing. +- Theme variable groups joined or converted to text through array helpers (`[colors].map(g => g).join('')`) are refused; token reads through them work. +- `stylex.props`/`attrs` calls with nested conditional dynamic styles stay runtime calls. +- An inline object passed as `sx` compiles to its styles (the Babel plugin emits `style="[object Object]"`). +- `experimental_crossFileParsing` imports compile as `commonJS` ones do; the Babel plugin rejects every imported file in this mode. +- StyleX code another plugin adds during the traversal is compiled; the Babel plugin can leave it uncompiled. +- In the Babel adapter, a later plugin that rewrites a definition another depends on fails the build (copied, rather than moved, compiled nodes are not detected). + +## Development + +```bash +npm run build:native --workspace @stylexjs/rust-plugin # release build, copies native/*.node +npm run test:native --workspace @stylexjs/rust-plugin # Rust tests, native API, Babel suite +npm run test:parity --workspace @stylexjs/rust-plugin -- babel # reference run of the Babel suite +npm run bench --workspace @stylexjs/rust-plugin # whole-file timings against Babel +``` + +- `vendor/README.md`: the patched `swc_ecma_parser`. +- `THIRD_PARTY_NOTICES.md`: licenses of the native addon's dependencies. diff --git a/packages/@stylexjs/rust-plugin/RUST_DEPENDENCY_LICENSES.md b/packages/@stylexjs/rust-plugin/RUST_DEPENDENCY_LICENSES.md new file mode 100644 index 000000000..fc6abf635 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/RUST_DEPENDENCY_LICENSES.md @@ -0,0 +1,2329 @@ +# Rust dependency license texts + +License and notice texts of every Cargo package in the Node addon's dependency graph +(`cargo tree -p stylex_rust_compiler --features node -e normal --target all`), which includes procedural macros and +target-specific crates. Generated by `scripts/licenses.cjs`; `LICENSE_PROVENANCE.json` lists +the source file of each text. Texts that differ only in whitespace, URL scheme, a license title +or the Apache-2.0 appendix template appear once; each package's copyright lines are kept verbatim. + +| Package | Declared license | Texts | +|---|---|---| +| adler2 2.0.0 | 0BSD OR MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 1fc59585 | +| ahash 0.8.12 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| aho-corasick 1.1.5 | Unlicense OR MIT | fe2a9817, 2069c208, 7e7a2c78 | +| allocator-api2 0.2.21 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| ansi_term 0.12.1 | MIT | fe2a9817 | +| anyhow 1.0.104 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| arrayvec 0.7.8 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| ascii 1.1.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| ast_node 7.0.0 | Apache-2.0 | 59d8f0ba | +| base64 0.22.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| base64-simd 0.8.0 | MIT | fe2a9817 | +| better_scoped_tls 1.0.1 | Apache-2.0 | 59d8f0ba | +| bit-set 0.8.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| bit-vec 0.8.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| bitflags 2.13.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| bitvec 1.1.1 | MIT | fe2a9817 | +| block-buffer 0.10.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| bstr 1.12.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 80cb89b5 | +| bumpalo 3.20.3 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| byteorder 1.5.0 | Unlicense OR MIT | fe2a9817, 2069c208, 7e7a2c78 | +| bytes 1.12.1 | MIT | fe2a9817 | +| bytes-str 0.2.9 | Apache-2.0 | 59d8f0ba | +| camino 1.1.11 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| cargo_metadata 0.18.1 | MIT | fe2a9817 | +| cargo-platform 0.1.9 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| castaway 0.2.4 | MIT | fe2a9817 | +| cbor4ii 1.2.2 | MIT | fe2a9817 | +| cfg-if 1.0.5 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| colored 3.1.1 | MPL-2.0 | a7fae6cb | +| compact_str 0.10.0 | MIT | fe2a9817 | +| compact_str 0.7.1 | MIT | fe2a9817 | +| concurrent_lru 0.2.0 | MIT | fe2a9817 | +| convert_case 0.10.0 | MIT | fe2a9817 | +| convert_case 0.12.0 | MIT | fe2a9817 | +| cpufeatures 0.2.17 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| crc32fast 1.5.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| crossbeam-utils 0.8.21 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| crypto-common 0.1.6 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| cssparser 0.38.0 | MPL-2.0 | a7fae6cb | +| cssparser-macros 0.7.0 | MPL-2.0 | a7fae6cb | +| ctor 1.0.13 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| darling 0.20.11 | MIT | fe2a9817 | +| darling_core 0.20.11 | MIT | fe2a9817 | +| darling_macro 0.20.11 | MIT | fe2a9817 | +| dashmap 6.2.1 | MIT | fe2a9817 | +| data-encoding 2.11.1 | MIT | fe2a9817 | +| debugid 0.8.0 | Apache-2.0 | 59d8f0ba | +| derive_builder 0.20.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| derive_builder_core 0.20.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| derive_builder_macro 0.20.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| derive_more 2.1.1 | MIT | fe2a9817 | +| derive_more-impl 2.1.1 | MIT | fe2a9817 | +| diff 0.1.13 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| difference 2.0.0 | MIT | fe2a9817 | +| digest 0.10.7 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| displaydoc 0.2.7 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| dragonbox_ecma 0.1.12 | Apache-2.0 WITH LLVM-exception OR BSL-1.0 | 59d8f0ba, 68440691, a796d573 | +| dtoa 1.0.10 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| dtoa-short 0.3.5 | MPL-2.0 | a7fae6cb | +| either 1.19.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| endian-type 0.2.0 | MIT | fe2a9817 | +| env_logger 0.10.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| equivalent 1.0.3 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| errno 0.3.14 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| fancy-regex 0.19.2 | MIT | fe2a9817 | +| fast-glob 1.0.0 | MIT | fe2a9817 | +| fastrand 2.3.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| fixedbitset 0.5.7 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| flate2 1.1.5 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| float-cmp 0.10.0 | MIT | fe2a9817 | +| fnv 1.0.7 | Apache-2.0 / MIT | fe2a9817, 59d8f0ba | +| foldhash 0.2.0 | Zlib | 444399c3 | +| form_urlencoded 1.2.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| from_variant 3.0.0 | Apache-2.0 | 59d8f0ba | +| funty 2.0.0 | MIT | fe2a9817 | +| futures 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-channel 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-core 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-executor 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-io 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-macro 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-sink 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-task 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| futures-util 0.3.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| generic-array 0.14.9 | MIT | fe2a9817 | +| getrandom 0.3.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| glob 0.3.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| globset 0.4.18 | Unlicense OR MIT | fe2a9817, 2069c208, 7e7a2c78 | +| halfbrown 0.4.0 | Apache-2.0/MIT | fe2a9817, 59d8f0ba | +| hashbrown 0.14.5 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| hashbrown 0.16.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| hashbrown 0.17.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| heck 0.5.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| hermit-abi 0.4.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| hermit-abi 0.5.3 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| hex 0.4.3 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| hstr 4.0.0 | Apache-2.0 | 59d8f0ba | +| humantime 2.1.0 | MIT/Apache-2.0 | 59d8f0ba, fa58f7ee | +| icu_collator 2.3.1 | Unicode-3.0 | 4bea89d5 | +| icu_collator_data 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_collections 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_locale_core 2.4.0 | Unicode-3.0 | 4bea89d5 | +| icu_locale_fallback 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_locale_fallback_data 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_normalizer 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_normalizer_data 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_properties 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_properties_data 2.3.0 | Unicode-3.0 | 4bea89d5 | +| icu_provider 2.4.0 | Unicode-3.0 | 4bea89d5 | +| ident_case 1.0.1 | MIT/Apache-2.0 | fe2a9817 | +| idna 1.1.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| idna_adapter 1.2.2 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| if_chain 1.0.3 | MIT/Apache-2.0 | fe2a9817, 59d8f0ba | +| indexmap 2.14.2 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| is-macro 0.3.8 | Apache-2.0 | 59d8f0ba | +| is-terminal 0.4.15 | MIT | fe2a9817, dc63b631 | +| itoa 1.0.18 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| json-strip-comments 3.1.0 | MIT | fe2a9817 | +| lazy_static 1.5.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| libc 0.2.191 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| libloading 0.9.0 | ISC | 4829cc8e | +| libmimalloc-sys 0.1.49 | MIT | fe2a9817 | +| libz-rs-sys 0.5.4 | Zlib | 444399c3 | +| link-section 0.19.3 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| linktime-proc-macro 0.2.3 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| linux-raw-sys 0.12.1 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 68440691, 27c18106 | +| litemap 0.8.3 | Unicode-3.0 | 4bea89d5 | +| lock_api 0.4.14 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| log 0.4.34 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| matchers 0.2.0 | MIT | fe2a9817 | +| memchr 2.8.3 | Unlicense OR MIT | fe2a9817, 2069c208, 7e7a2c78 | +| miette 7.6.0 | Apache-2.0 | 59d8f0ba | +| miette-derive 7.6.0 | Apache-2.0 | 59d8f0ba | +| mimalloc 0.1.52 | MIT | fe2a9817 | +| miniz_oxide 0.8.5 | MIT OR Zlib OR Apache-2.0 | fe2a9817, 59d8f0ba, 444399c3 | +| murmur2 0.1.0 | Apache-2.0 | 59d8f0ba | +| napi 3.16.1 | MIT | 24528692 | +| napi-derive 3.7.0 | MIT | 24528692 | +| napi-derive-backend 7.0.0 | MIT | 24528692 | +| napi-sys 3.4.1 | MIT | 24528692 | +| new_debug_unreachable 1.0.6 | MIT | fe2a9817 | +| nibble_vec 0.1.0 | MIT | fe2a9817 | +| nodejs-built-in-modules 1.0.0 | MIT | fe2a9817 | +| nohash-hasher 0.2.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| nu-ansi-term 0.50.1 | MIT | fe2a9817 | +| num_cpus 1.17.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| num-bigint 0.4.8 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| num-integer 0.1.47 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| num-traits 0.2.19 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| once_cell 1.21.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| outref 0.5.2 | MIT | fe2a9817 | +| owo-colors 4.2.2 | MIT | fe2a9817 | +| oxc_resolver 11.24.3 | MIT | fe2a9817 | +| package-json 0.5.0 | MIT | fe2a9817 | +| par-core 2.0.0 | Apache-2.0 | 59d8f0ba | +| par-iter 2.0.0 | Apache-2.0 | 59d8f0ba | +| parking_lot 0.12.5 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| parking_lot_core 0.9.12 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| path-clean 1.0.1 | MIT OR Apache-2.0 | 6037ec11, a47b5281 | +| pathdiff 0.2.3 | MIT/Apache-2.0 | fe2a9817, 59d8f0ba | +| percent-encoding 2.3.2 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| petgraph 0.7.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 63851e6f | +| phf 0.11.3 | MIT | fe2a9817 | +| phf 0.14.0 | MIT | fe2a9817 | +| phf_generator 0.11.3 | MIT | fe2a9817 | +| phf_generator 0.14.0 | MIT | fe2a9817 | +| phf_macros 0.11.3 | MIT | fe2a9817 | +| phf_macros 0.14.0 | MIT | fe2a9817 | +| phf_shared 0.11.3 | MIT | fe2a9817 | +| phf_shared 0.14.0 | MIT | fe2a9817 | +| pin-project-lite 0.2.17 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| pnp 0.12.12 | BSD-2-Clause | a97d8f85 | +| postcss_value_parser 0.19.2 | MIT | fe2a9817 | +| potential_utf 0.1.6 | Unicode-3.0 | 4bea89d5 | +| pretty_assertions 1.4.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| pretty_env_logger 0.5.0 | MIT/Apache-2.0 | fe2a9817, 59d8f0ba | +| proc-macro2 1.0.107 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| psm 0.1.32 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| quote 1.0.47 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| r-efi 5.3.0 | MIT OR Apache-2.0 OR LGPL-2.1-or-later | 59d8f0ba, 091b05f0 | +| radium 0.7.0 | MIT | fe2a9817 | +| radix_fmt 1.0.0 | Apache-2.0 | 59d8f0ba | +| radix_trie 0.3.0 | MIT | fe2a9817 | +| rand 0.8.8 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, fbe36b2f | +| rand_core 0.6.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, fbe36b2f | +| redox_syscall 0.5.18 | MIT | fe2a9817 | +| ref-cast 1.0.25 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| ref-cast-impl 1.0.25 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| regex 1.13.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| regex-automata 0.4.18 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| regex-syntax 0.8.11 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 71f2cc72 | +| regress 0.11.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| regress 0.12.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| relative-path 1.9.3 | MIT OR Apache-2.0 | 59d8f0ba | +| rustc-hash 2.1.3 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| rustix 1.1.4 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 68440691, 175bdea6 | +| rustversion 1.0.23 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| ryu 1.0.23 | Apache-2.0 OR BSL-1.0 | 59d8f0ba, a796d573 | +| ryu-js 1.0.3 | Apache-2.0 OR BSL-1.0 | 59d8f0ba, a796d573 | +| scoped-tls 1.0.1 | MIT/Apache-2.0 | fe2a9817, 59d8f0ba | +| scopeguard 1.2.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| self_cell 1.2.1 | Apache-2.0 OR GPL-2.0-only | 59d8f0ba | +| semver 1.0.28 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| seq-macro 0.3.6 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| serde 1.0.229 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| serde_core 1.0.229 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| serde_derive 1.0.229 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| serde_json 1.0.154 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| sha1 0.10.6 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| sha2 0.10.9 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| sharded-slab 0.1.7 | MIT | fe2a9817 | +| simd-adler32 0.3.8 | MIT | fe2a9817 | +| simd-json 0.18.1 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| simdutf8 0.1.5 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| siphasher 0.3.11 | MIT/Apache-2.0 | e1c73bd2 | +| siphasher 1.0.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, e1c73bd2 | +| slab 0.4.12 | MIT | fe2a9817 | +| smallvec 1.16.4 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| stable_deref_trait 1.2.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| stacker 0.1.25 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| static_assertions 1.1.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| string_enum 1.0.2 | Apache-2.0 | 59d8f0ba | +| strsim 0.11.1 | MIT | fe2a9817 | +| stylex_ast 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_atoms 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_constants 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_css 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_css_parser 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_diagnostics 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_enums 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_evaluator 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_js 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_macros 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_nested_config 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_path_resolver 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_regex 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_rust_compiler 0.19.1 | MIT | fe2a9817 | +| stylex_state 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_state_index 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_structures 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_styleq 0.19.2 | MIT | fe2a9817 | +| stylex_transform 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_types 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| stylex_utils 0.19.2 | MIT | fe2a9817, d89f3ef2 | +| swc_allocator 5.0.0 | Apache-2.0 | 59d8f0ba | +| swc_atoms 10.0.0 | Apache-2.0 | 59d8f0ba | +| swc_common 26.0.0 | Apache-2.0 | 59d8f0ba | +| swc_compiler_base 65.0.0 | Apache-2.0 | 59d8f0ba | +| swc_config 6.0.0 | Apache-2.0 | 59d8f0ba | +| swc_config_macro 1.0.1 | Apache-2.0 | 59d8f0ba | +| swc_core 81.0.2 | Apache-2.0 | 59d8f0ba | +| swc_ecma_ast 29.0.2 | Apache-2.0 | 59d8f0ba | +| swc_ecma_codegen 32.0.3 | Apache-2.0 | 59d8f0ba | +| swc_ecma_codegen_macros 2.0.2 | Apache-2.0 | 59d8f0ba | +| swc_ecma_hooks 1.0.0 | Apache-2.0 | 59d8f0ba | +| swc_ecma_minifier 62.0.1 | Apache-2.0 | 59d8f0ba | +| swc_ecma_parser 45.1.6 | Apache-2.0 | 59d8f0ba | +| swc_ecma_testing 27.0.0 | Apache-2.0 | 59d8f0ba | +| swc_ecma_transforms_base 49.0.1 | Apache-2.0 | 59d8f0ba | +| swc_ecma_transforms_optimization 52.0.2 | Apache-2.0 | 59d8f0ba | +| swc_ecma_transforms_react 55.0.1 | Apache-2.0 | 59d8f0ba | +| swc_ecma_transforms_testing 53.0.0 | Apache-2.0 | 59d8f0ba | +| swc_ecma_transforms_typescript 55.0.4 | Apache-2.0 | 59d8f0ba | +| swc_ecma_utils 35.0.2 | Apache-2.0 | 59d8f0ba | +| swc_ecma_visit 29.0.0 | Apache-2.0 | 59d8f0ba | +| swc_eq_ignore_macros 1.0.1 | Apache-2.0 | 59d8f0ba | +| swc_error_reporters 28.0.0 | Apache-2.0 | 59d8f0ba | +| swc_macros_common 1.0.1 | Apache-2.0 | 59d8f0ba | +| swc_malloc 1.2.5 | Apache-2.0 | 59d8f0ba | +| swc_plugin 1.0.1 | Apache-2.0 | 59d8f0ba | +| swc_plugin_macro 2.0.0 | Apache-2.0 | 59d8f0ba | +| swc_plugin_proxy 30.0.0 | Apache-2.0 | 59d8f0ba | +| swc_sourcemap 10.0.3 | BSD-3-Clause | 8a355c16 | +| swc_transform_common 20.0.0 | Apache-2.0 | 59d8f0ba | +| swc_visit 2.0.2 | Apache-2.0 | 59d8f0ba | +| syn 2.0.119 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| syn 3.0.9 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| synstructure 0.14.0 | MIT | fe2a9817 | +| tap 1.0.1 | MIT | fe2a9817 | +| tempfile 3.21.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| termcolor 1.4.1 | Unlicense OR MIT | fe2a9817, 2069c208, 7e7a2c78 | +| testing 27.0.0 | Apache-2.0 | 59d8f0ba | +| testing_macros 1.0.2 | Apache-2.0 | 59d8f0ba | +| textwrap 0.16.2 | MIT | fe2a9817 | +| thiserror 1.0.69 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| thiserror 2.0.21 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| thiserror-impl 1.0.69 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| thiserror-impl 2.0.21 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| thread_local 1.1.9 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| tikv-jemalloc-sys 0.5.4+5.3.0-patched | MIT/Apache-2.0 | fe2a9817, 59d8f0ba, 23f0eb5c | +| tikv-jemallocator 0.5.4 | MIT/Apache-2.0 | fe2a9817, 59d8f0ba | +| tinystr 0.8.5 | Unicode-3.0 | 4bea89d5 | +| tracing 0.1.44 | MIT | fe2a9817 | +| tracing-attributes 0.1.31 | MIT | fe2a9817 | +| tracing-core 0.1.36 | MIT | fe2a9817 | +| tracing-log 0.2.0 | MIT | fe2a9817 | +| tracing-subscriber 0.3.20 | MIT | fe2a9817 | +| triomphe 0.1.17 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| typenum 1.19.0 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, db11fec9 | +| unicode-id-start 1.5.0 | (MIT OR Apache-2.0) AND Unicode-3.0 | fe2a9817, 59d8f0ba, 5c5512ba | +| unicode-ident 1.0.27 | (MIT OR Apache-2.0) AND Unicode-3.0 | fe2a9817, 59d8f0ba, 5c5512ba | +| unicode-linebreak 0.1.5 | Apache-2.0 | 59d8f0ba | +| unicode-segmentation 1.13.3 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 9fba0587 | +| unicode-width 0.1.14 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 9fba0587 | +| unicode-width 0.2.3 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 9fba0587 | +| unicode-xid 0.2.6 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba, 9fba0587 | +| url 2.5.8 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| utf_types 1.0.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 3d0edf29 | +| utf16_iter 1.1.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 6405f47d | +| utf8_iter 1.1.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 8d6ca7f4 | +| uuid 1.29.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba | +| value-trait 0.12.1 | Apache-2.0/MIT | fe2a9817, 59d8f0ba | +| vsimd 0.8.0 | MIT | fe2a9817 | +| wasip2 1.0.1+wasi-0.2.4 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT | 59d8f0ba | +| winapi 0.3.9 | MIT/Apache-2.0 | fe2a9817, 59d8f0ba | +| winapi-i686-pc-windows-gnu 0.4.0 | MIT/Apache-2.0 | 59d8f0ba | +| winapi-util 0.1.11 | Unlicense OR MIT | fe2a9817, 2069c208, 7e7a2c78 | +| winapi-x86_64-pc-windows-gnu 0.4.0 | MIT/Apache-2.0 | 59d8f0ba | +| windows 0.62.2 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_aarch64_gnullvm 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_aarch64_msvc 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_i686_gnu 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_i686_gnullvm 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_i686_msvc 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_x86_64_gnu 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_x86_64_gnullvm 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows_x86_64_msvc 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-collections 0.3.2 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-core 0.62.2 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-future 0.3.2 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-implement 0.60.2 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-interface 0.59.3 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-link 0.2.1 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-numerics 0.3.1 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-result 0.4.1 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-strings 0.5.1 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-sys 0.52.0 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-sys 0.59.0 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-sys 0.61.2 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-targets 0.52.6 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| windows-threading 0.2.1 | MIT OR Apache-2.0 | 59d8f0ba, 380f9bad | +| wit-bindgen 0.46.0 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 68440691 | +| write16 1.0.0 | Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 3d0edf29 | +| writeable 0.6.4 | Unicode-3.0 | 4bea89d5 | +| wyz 0.5.1 | MIT | fe2a9817 | +| xxhash-rust 0.8.18 | BSL-1.0 | a796d573 | +| yansi 1.0.1 | MIT OR Apache-2.0 | fe2a9817, 59d8f0ba | +| yoke 0.8.4 | Unicode-3.0 | 4bea89d5 | +| yoke-derive 0.8.4 | Unicode-3.0 | 4bea89d5 | +| zerocopy 0.8.63 | BSD-2-Clause OR Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 278abc70 | +| zerocopy-derive 0.8.63 | BSD-2-Clause OR Apache-2.0 OR MIT | fe2a9817, 59d8f0ba, 278abc70 | +| zerofrom 0.1.8 | Unicode-3.0 | 4bea89d5 | +| zerofrom-derive 0.1.8 | Unicode-3.0 | 4bea89d5 | +| zerotrie 0.2.5 | Unicode-3.0 | 4bea89d5 | +| zerovec 0.11.9 | Unicode-3.0 | 4bea89d5 | +| zerovec-derive 0.11.7 | Unicode-3.0 | 4bea89d5 | +| zlib-rs 0.5.4 | Zlib | 444399c3 | +| zmij 1.0.23 | MIT | fe2a9817 | + +## Text fe2a9817 + +Packages: adler2 2.0.0, ahash 0.8.12, aho-corasick 1.1.5, allocator-api2 0.2.21, ansi_term 0.12.1, anyhow 1.0.104, arrayvec 0.7.8, ascii 1.1.0, base64 0.22.1, bit-set 0.8.0, bit-vec 0.8.0, bitflags 2.13.2, bitvec 1.1.1, block-buffer 0.10.4, bstr 1.12.1, bumpalo 3.20.3, byteorder 1.5.0, bytes 1.12.1, camino 1.1.11, cargo_metadata 0.18.1, cargo-platform 0.1.9, castaway 0.2.4, cbor4ii 1.2.2, cfg-if 1.0.5, compact_str 0.10.0, compact_str 0.7.1, concurrent_lru 0.2.0, convert_case 0.10.0, convert_case 0.12.0, cpufeatures 0.2.17, crc32fast 1.5.0, crossbeam-utils 0.8.21, crypto-common 0.1.6, ctor 1.0.13, darling 0.20.11, darling_core 0.20.11, darling_macro 0.20.11, dashmap 6.2.1, data-encoding 2.11.1, derive_builder 0.20.2, derive_builder_core 0.20.2, derive_builder_macro 0.20.2, derive_more 2.1.1, derive_more-impl 2.1.1, diff 0.1.13, difference 2.0.0, digest 0.10.7, displaydoc 0.2.7, dtoa 1.0.10, either 1.19.0, endian-type 0.2.0, env_logger 0.10.2, equivalent 1.0.3, errno 0.3.14, fancy-regex 0.19.2, fastrand 2.3.0, fixedbitset 0.5.7, flate2 1.1.5, float-cmp 0.10.0, fnv 1.0.7, form_urlencoded 1.2.2, funty 2.0.0, futures 0.3.34, futures-channel 0.3.34, futures-core 0.3.34, futures-executor 0.3.34, futures-io 0.3.34, futures-macro 0.3.34, futures-sink 0.3.34, futures-task 0.3.34, futures-util 0.3.34, generic-array 0.14.9, getrandom 0.3.4, glob 0.3.4, globset 0.4.18, halfbrown 0.4.0, hashbrown 0.14.5, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.4.0, hermit-abi 0.5.3, hex 0.4.3, ident_case 1.0.1, idna 1.1.0, idna_adapter 1.2.2, if_chain 1.0.3, indexmap 2.14.2, is-terminal 0.4.15, itoa 1.0.18, json-strip-comments 3.1.0, lazy_static 1.5.0, libc 0.2.191, libmimalloc-sys 0.1.49, link-section 0.19.3, linktime-proc-macro 0.2.3, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.34, matchers 0.2.0, memchr 2.8.3, mimalloc 0.1.52, miniz_oxide 0.8.5, new_debug_unreachable 1.0.6, nibble_vec 0.1.0, nohash-hasher 0.2.0, nu-ansi-term 0.50.1, num_cpus 1.17.0, num-bigint 0.4.8, num-integer 0.1.47, num-traits 0.2.19, once_cell 1.21.4, outref 0.5.2, owo-colors 4.2.2, package-json 0.5.0, parking_lot 0.12.5, parking_lot_core 0.9.12, pathdiff 0.2.3, percent-encoding 2.3.2, petgraph 0.7.1, phf 0.11.3, phf 0.14.0, phf_generator 0.11.3, phf_generator 0.14.0, phf_macros 0.11.3, phf_macros 0.14.0, phf_shared 0.11.3, phf_shared 0.14.0, pin-project-lite 0.2.17, postcss_value_parser 0.19.2, pretty_assertions 1.4.1, pretty_env_logger 0.5.0, proc-macro2 1.0.107, psm 0.1.32, quote 1.0.47, radium 0.7.0, radix_trie 0.3.0, rand 0.8.8, rand_core 0.6.4, redox_syscall 0.5.18, ref-cast 1.0.25, ref-cast-impl 1.0.25, regex 1.13.1, regex-automata 0.4.18, regex-syntax 0.8.11, regress 0.11.1, regress 0.12.0, rustc-hash 2.1.3, rustix 1.1.4, rustversion 1.0.23, scoped-tls 1.0.1, scopeguard 1.2.0, semver 1.0.28, seq-macro 0.3.6, serde 1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_json 1.0.154, sha1 0.10.6, sha2 0.10.9, sharded-slab 0.1.7, simd-adler32 0.3.8, simd-json 0.18.1, simdutf8 0.1.5, siphasher 1.0.4, slab 0.4.12, smallvec 1.16.4, stable_deref_trait 1.2.1, stacker 0.1.25, static_assertions 1.1.0, strsim 0.11.1, stylex_ast 0.19.2, stylex_atoms 0.19.2, stylex_constants 0.19.2, stylex_css 0.19.2, stylex_css_parser 0.19.2, stylex_diagnostics 0.19.2, stylex_enums 0.19.2, stylex_evaluator 0.19.2, stylex_js 0.19.2, stylex_macros 0.19.2, stylex_nested_config 0.19.2, stylex_path_resolver 0.19.2, stylex_regex 0.19.2, stylex_rust_compiler 0.19.1, stylex_state 0.19.2, stylex_state_index 0.19.2, stylex_structures 0.19.2, stylex_styleq 0.19.2, stylex_transform 0.19.2, stylex_types 0.19.2, stylex_utils 0.19.2, syn 2.0.119, syn 3.0.9, synstructure 0.14.0, tap 1.0.1, tempfile 3.21.0, termcolor 1.4.1, textwrap 0.16.2, thiserror 1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, thread_local 1.1.9, tikv-jemalloc-sys 0.5.4+5.3.0-patched, tikv-jemallocator 0.5.4, tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36, tracing-log 0.2.0, tracing-subscriber 0.3.20, triomphe 0.1.17, typenum 1.19.0, unicode-id-start 1.5.0, unicode-ident 1.0.27, unicode-segmentation 1.13.3, unicode-width 0.1.14, unicode-width 0.2.3, unicode-xid 0.2.6, url 2.5.8, utf_types 1.0.0, utf16_iter 1.1.0, utf8_iter 1.1.0, uuid 1.29.0, value-trait 0.12.1, winapi 0.3.9, winapi-util 0.1.11, wit-bindgen 0.46.0, write16 1.0.0, wyz 0.5.1, yansi 1.0.1, zerocopy 0.8.63, zerocopy-derive 0.8.63, zmij 1.0.23, base64-simd 0.8.0, fast-glob 1.0.0, nodejs-built-in-modules 1.0.0, oxc_resolver 11.24.3, vsimd 0.8.0 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +Copyright notices: + +```text +ahash 0.8.12: + Copyright (c) 2018 Tom Kaitchuck +aho-corasick 1.1.5, byteorder 1.5.0, globset 0.4.18, memchr 2.8.3, termcolor 1.4.1: + Copyright (c) 2015 Andrew Gallant +ansi_term 0.12.1: + Copyright (c) 2014 Benjamin Sago +arrayvec 0.7.8: + Copyright (c) Ulrik Sverdrup "bluss" 2015-2023 +ascii 1.1.0: + Copyright (c) 2017 Thomas Bahn and contributors + Copyright (c) 2014 The Rust Project Developers +base64 0.22.1: + Copyright (c) 2015 Alice Maz +base64-simd 0.8.0, vsimd 0.8.0: + Copyright (c) 2021 Nugine +bit-set 0.8.0, bit-vec 0.8.0: + Copyright (c) 2023 The Rust Project Developers +bitflags 2.13.2, glob 0.3.4, log 0.4.34, num-bigint 0.4.8, num-integer 0.1.47, num-traits 0.2.19, regex 1.13.1, regex-automata 0.4.18, regex-syntax 0.8.11: + Copyright (c) 2014 The Rust Project Developers +bitvec 1.1.1, wyz 0.5.1: + Copyright (c) 2018 myrrlyn (Alexander Payne) +block-buffer 0.10.4: + Copyright (c) 2018-2019 The RustCrypto Project Developers +bstr 1.12.1: + Copyright (c) 2018-2019 Andrew Gallant +bumpalo 3.20.3: + Copyright (c) 2019 Nick Fitzgerald +bytes 1.12.1: + Copyright (c) 2018 Carl Lerche +castaway 0.2.4: + Copyright (c) 2021 Stephen M. Coakley +cbor4ii 1.2.2: + Copyright (c) 2021 quininer +cfg-if 1.0.5, scoped-tls 1.0.1, stacker 0.1.25, tikv-jemalloc-sys 0.5.4+5.3.0-patched, tikv-jemallocator 0.5.4: + Copyright (c) 2014 Alex Crichton +compact_str 0.10.0, compact_str 0.7.1: + Copyright (c) 2021 Parker Timmerman +concurrent_lru 0.2.0: + Copyright (c) 2020 Jianong Zhong +convert_case 0.10.0, convert_case 0.12.0: + Copyright (c) 2025 rutrum +cpufeatures 0.2.17: + Copyright (c) 2020-2025 The RustCrypto Project Developers +crc32fast 1.5.0: + Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors +crossbeam-utils 0.8.21: + Copyright (c) 2019 The Crossbeam Project Developers +crypto-common 0.1.6: + Copyright (c) 2021 RustCrypto Developers +darling 0.20.11, darling_core 0.20.11, darling_macro 0.20.11: + Copyright (c) 2017 Ted Driggs +dashmap 6.2.1: + Copyright (c) 2019 Acrimon +data-encoding 2.11.1: + Copyright (c) 2015-2020 Julien Cretin + Copyright (c) 2017-2020 Google Inc. +derive_builder 0.20.2, derive_builder_core 0.20.2, derive_builder_macro 0.20.2, pretty_assertions 1.4.1: + Copyright (c) 2016 rust-derive-builder contributors +derive_more 2.1.1, derive_more-impl 2.1.1: + Copyright (c) 2016 Jelte Fennema +diff 0.1.13: + Copyright (c) 2015 Utkarsh Kukreti +difference 2.0.0: + Copyright (c) 2015 Johann Hofmann +digest 0.10.7: + Copyright (c) 2017 Artyom Pavlov +either 1.19.0, endian-type 0.2.0, petgraph 0.7.1: + Copyright (c) 2015 +env_logger 0.10.2: + Copyright (c) Individual contributors +equivalent 1.0.3: + Copyright (c) 2016--2023 +errno 0.3.14: + Copyright (c) 2014 Chris Wong +fancy-regex 0.19.2: + Copyright 2015 The Fancy Regex Authors. +fast-glob 1.0.0: + Copyright (c) 2025-present Contributors + Copyright (c) 2024 shulaoda + Copyright (c) 2023 Devon Govett +fixedbitset 0.5.7: + Copyright (c) 2015-2017 +flate2 1.1.5: + Copyright (c) 2014-2025 Alex Crichton +float-cmp 0.10.0: + Copyright (c) 2014-2020 Optimal Computing (NZ) Ltd +fnv 1.0.7: + Copyright (c) 2017 Contributors +form_urlencoded 1.2.2: + Copyright (c) 2013-2016 The rust-url developers +funty 2.0.0: + Copyright (c) 2020 myrrlyn (Alexander Payne) +futures 0.3.34, futures-channel 0.3.34, futures-core 0.3.34, futures-executor 0.3.34, futures-io 0.3.34, futures-macro 0.3.34, futures-sink 0.3.34, futures-task 0.3.34, futures-util 0.3.34: + Copyright (c) 2016 Alex Crichton + Copyright (c) 2017 The Tokio Authors +generic-array 0.14.9: + Copyright (c) 2015 Bartłomiej Kamiński +getrandom 0.3.4: + Copyright (c) 2018-2025 The rust-random Project Developers + Copyright (c) 2014 The Rust Project Developers +halfbrown 0.4.0, simd-json 0.18.1, value-trait 0.12.1: + Copyright (c) [year] [fullname] +hashbrown 0.14.5, hashbrown 0.16.1, hashbrown 0.17.1: + Copyright (c) 2016 Amanieu d'Antras +heck 0.5.0, unicode-segmentation 1.13.3, unicode-width 0.1.14, unicode-width 0.2.3, unicode-xid 0.2.6: + Copyright (c) 2015 The Rust Project Developers +hex 0.4.3: + Copyright (c) 2013-2014 The Rust Project Developers. + Copyright (c) 2015-2020 The rust-hex Developers +idna 1.1.0, percent-encoding 2.3.2, url 2.5.8: + Copyright (c) 2013-2025 The rust-url developers +idna_adapter 1.2.2: + Copyright (c) The rust-url developers +if_chain 1.0.3: + Copyright (c) 2018 Chris Wong +indexmap 2.14.2: + Copyright (c) 2016--2017 +json-strip-comments 3.1.0: + Copyright (c) 2024 oxc +lazy_static 1.5.0: + Copyright (c) 2010 The Rust Project Developers +libc 0.2.191: + Copyright (c) The Rust Project Developers +libmimalloc-sys 0.1.49, mimalloc 0.1.52: + Copyright 2019 Octavian Oncescu +libmimalloc-sys 0.1.49: + Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen +lock_api 0.4.14, parking_lot 0.12.5, parking_lot_core 0.9.12, thread_local 1.1.9: + Copyright (c) 2016 The Rust Project Developers +matchers 0.2.0, sharded-slab 0.1.7: + Copyright (c) 2019 Eliza Weisman +miniz_oxide 0.8.5: + Copyright 2013-2014 RAD Game Tools and Valve Software + Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + Copyright (c) 2017 Frommi + Copyright (c) 2017-2024 oyvindln +new_debug_unreachable 1.0.6: + Copyright (c) 2015 Jonathan Reem +nibble_vec 0.1.0, radix_trie 0.3.0: + Copyright (c) 2015 Michael Sproul +nodejs-built-in-modules 1.0.0: + Copyright (c) 2024-present VoidZero Inc. & Contributors +nohash-hasher 0.2.0: + Copyright 2018 Parity Technologies (UK) Ltd. +nu-ansi-term 0.50.1: + Copyright (c) 2014 Benjamin Sago + Copyright (c) 2021-2022 The Nushell Project Developers +num_cpus 1.17.0: + Copyright (c) 2015-2025 Sean McArthur +outref 0.5.2: + Copyright (c) 2022 Nugine +owo-colors 4.2.2: + Copyright (c) 2020 - present The owo-colors Developers +oxc_resolver 11.24.3: + Copyright (c) 2024-present VoidZero Inc. & Contributors + Copyright (c) 2023 Boshen +package-json 0.5.0: + Copyright (c) 2022 Liu Bowen +phf 0.11.3, phf 0.14.0, phf_generator 0.11.3, phf_generator 0.14.0, phf_macros 0.11.3, phf_macros 0.14.0, phf_shared 0.11.3, phf_shared 0.14.0: + Copyright (c) 2014-2022 Steven Fackler, Yuki Okushi +postcss_value_parser 0.19.2: + Copyright (c) Bogdan Chadkin +pretty_env_logger 0.5.0: + Copyright (c) 2017 Sean McArthur +psm 0.1.32: + Copyright © 2018, Simonas Kazlauskas +radium 0.7.0: + Copyright (c) 2019 kneecaw (Nika Layzell) +rand 0.8.8, rand_core 0.6.4: + Copyright 2018 Developers of the Rand project + Copyright (c) 2014 The Rust Project Developers +redox_syscall 0.5.18: + Copyright (c) 2017 Redox OS Developers +regress 0.11.1, regress 0.12.0: + Copyright (c) 2020 ridiculous_fish +scopeguard 1.2.0: + Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers +sha1 0.10.6, sha2 0.10.9: + Copyright (c) 2006-2009 Graydon Hoare + Copyright (c) 2009-2013 Mozilla Foundation + Copyright (c) 2016 Artyom Pavlov +simd-adler32 0.3.8: + Copyright (c) [2021] [Marvin Countryman] +slab 0.4.12: + Copyright (c) 2019 Carl Lerche +smallvec 1.16.4: + Copyright (c) 2018 The Servo Project Developers +stable_deref_trait 1.2.1: + Copyright (c) 2017 Robert Grosse +static_assertions 1.1.0: + Copyright (c) 2017 Nikolai Vazquez +strsim 0.11.1: + Copyright (c) 2015 Danny Guo + Copyright (c) 2016 Titus Wormer + Copyright (c) 2018 Akash Kurdekar +stylex_ast 0.19.2, stylex_atoms 0.19.2, stylex_constants 0.19.2, stylex_css 0.19.2, stylex_css_parser 0.19.2, stylex_diagnostics 0.19.2, stylex_enums 0.19.2, stylex_evaluator 0.19.2, stylex_js 0.19.2, stylex_macros 0.19.2, stylex_nested_config 0.19.2, stylex_path_resolver 0.19.2, stylex_regex 0.19.2, stylex_state 0.19.2, stylex_state_index 0.19.2, stylex_structures 0.19.2, stylex_transform 0.19.2, stylex_types 0.19.2, stylex_utils 0.19.2: + Copyright (c) 2024 Vladislav Buinovski +stylex_rust_compiler 0.19.1: + Copyright (c) Meta Platforms, Inc. and affiliates. +stylex_styleq 0.19.2: + Copyright (c) Nicolas Gallagher +synstructure 0.14.0: + Copyright 2016 Nika Layzell +tap 1.0.1: + Copyright (c) 2017 Elliot Linder +tempfile 3.21.0: + Copyright (c) 2015 Steven Allen +textwrap 0.16.2: + Copyright (c) 2016 Martin Geisler +tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36, tracing-log 0.2.0, tracing-subscriber 0.3.20: + Copyright (c) 2019 Tokio Contributors +tracing-core 0.1.36: + Copyright (c) 2014 Mathijs van de Nes +triomphe 0.1.17: + Copyright (c) 2019 Manish Goregaokar +typenum 1.19.0: + Copyright (c) 2014 Paho Lurie-Gregg +utf_types 1.0.0, utf16_iter 1.1.0, utf8_iter 1.1.0, write16 1.0.0: + Copyright Mozilla Foundation +uuid 1.29.0: + Copyright (c) 2014 The Rust Project Developers + Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon +winapi 0.3.9: + Copyright (c) 2015-2018 The winapi-rs Developers +winapi-util 0.1.11: + Copyright (c) 2017 Andrew Gallant +yansi 1.0.1: + Copyright (c) 2017 Sergio Benitez +zerocopy 0.8.63, zerocopy-derive 0.8.63: + Copyright 2023 The Fuchsia Authors +``` + +## Text 59d8f0ba + +Packages: adler2 2.0.0, ahash 0.8.12, allocator-api2 0.2.21, anyhow 1.0.104, arrayvec 0.7.8, ascii 1.1.0, base64 0.22.1, bit-set 0.8.0, bit-vec 0.8.0, bitflags 2.13.2, block-buffer 0.10.4, bstr 1.12.1, bumpalo 3.20.3, camino 1.1.11, cargo-platform 0.1.9, cfg-if 1.0.5, cpufeatures 0.2.17, crc32fast 1.5.0, crossbeam-utils 0.8.21, crypto-common 0.1.6, ctor 1.0.13, debugid 0.8.0, derive_builder 0.20.2, derive_builder_core 0.20.2, derive_builder_macro 0.20.2, diff 0.1.13, digest 0.10.7, displaydoc 0.2.7, dragonbox_ecma 0.1.12, dtoa 1.0.10, either 1.19.0, env_logger 0.10.2, equivalent 1.0.3, errno 0.3.14, fastrand 2.3.0, fixedbitset 0.5.7, flate2 1.1.5, fnv 1.0.7, form_urlencoded 1.2.2, futures 0.3.34, futures-channel 0.3.34, futures-core 0.3.34, futures-executor 0.3.34, futures-io 0.3.34, futures-macro 0.3.34, futures-sink 0.3.34, futures-task 0.3.34, futures-util 0.3.34, getrandom 0.3.4, glob 0.3.4, halfbrown 0.4.0, hashbrown 0.14.5, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.4.0, hermit-abi 0.5.3, hex 0.4.3, hstr 4.0.0, humantime 2.1.0, idna 1.1.0, idna_adapter 1.2.2, if_chain 1.0.3, indexmap 2.14.2, is-macro 0.3.8, itoa 1.0.18, lazy_static 1.5.0, libc 0.2.191, link-section 0.19.3, linktime-proc-macro 0.2.3, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.34, miette 7.6.0, miette-derive 7.6.0, miniz_oxide 0.8.5, nohash-hasher 0.2.0, num_cpus 1.17.0, num-bigint 0.4.8, num-integer 0.1.47, num-traits 0.2.19, once_cell 1.21.4, par-iter 2.0.0, parking_lot 0.12.5, parking_lot_core 0.9.12, pathdiff 0.2.3, percent-encoding 2.3.2, petgraph 0.7.1, pin-project-lite 0.2.17, pretty_assertions 1.4.1, pretty_env_logger 0.5.0, proc-macro2 1.0.107, psm 0.1.32, quote 1.0.47, radix_fmt 1.0.0, rand 0.8.8, rand_core 0.6.4, ref-cast 1.0.25, ref-cast-impl 1.0.25, regex 1.13.1, regex-automata 0.4.18, regex-syntax 0.8.11, regress 0.11.1, regress 0.12.0, rustc-hash 2.1.3, rustix 1.1.4, rustversion 1.0.23, ryu 1.0.23, ryu-js 1.0.3, scoped-tls 1.0.1, scopeguard 1.2.0, semver 1.0.28, seq-macro 0.3.6, serde 1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_json 1.0.154, sha1 0.10.6, sha2 0.10.9, simd-json 0.18.1, simdutf8 0.1.5, siphasher 1.0.4, smallvec 1.16.4, stable_deref_trait 1.2.1, stacker 0.1.25, static_assertions 1.1.0, syn 2.0.119, syn 3.0.9, tempfile 3.21.0, thiserror 1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, thread_local 1.1.9, tikv-jemalloc-sys 0.5.4+5.3.0-patched, tikv-jemallocator 0.5.4, triomphe 0.1.17, typenum 1.19.0, unicode-id-start 1.5.0, unicode-ident 1.0.27, unicode-linebreak 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.1.14, unicode-width 0.2.3, unicode-xid 0.2.6, url 2.5.8, utf_types 1.0.0, utf16_iter 1.1.0, utf8_iter 1.1.0, uuid 1.29.0, value-trait 0.12.1, winapi 0.3.9, windows 0.62.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_msvc 0.52.6, windows_i686_gnu 0.52.6, windows_i686_gnullvm 0.52.6, windows_i686_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_gnullvm 0.52.6, windows_x86_64_msvc 0.52.6, windows-collections 0.3.2, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.61.2, windows-targets 0.52.6, windows-threading 0.2.1, wit-bindgen 0.46.0, write16 1.0.0, yansi 1.0.1, zerocopy 0.8.63, zerocopy-derive 0.8.63, ast_node 7.0.0, better_scoped_tls 1.0.1, bytes-str 0.2.9, from_variant 3.0.0, murmur2 0.1.0, par-core 2.0.0, r-efi 5.3.0, relative-path 1.9.3, self_cell 1.2.1, string_enum 1.0.2, swc_allocator 5.0.0, swc_atoms 10.0.0, swc_common 26.0.0, swc_compiler_base 65.0.0, swc_config 6.0.0, swc_config_macro 1.0.1, swc_core 81.0.2, swc_ecma_ast 29.0.2, swc_ecma_codegen 32.0.3, swc_ecma_codegen_macros 2.0.2, swc_ecma_hooks 1.0.0, swc_ecma_minifier 62.0.1, swc_ecma_parser 45.1.6, swc_ecma_testing 27.0.0, swc_ecma_transforms_base 49.0.1, swc_ecma_transforms_optimization 52.0.2, swc_ecma_transforms_react 55.0.1, swc_ecma_transforms_testing 53.0.0, swc_ecma_transforms_typescript 55.0.4, swc_ecma_utils 35.0.2, swc_ecma_visit 29.0.0, swc_eq_ignore_macros 1.0.1, swc_error_reporters 28.0.0, swc_macros_common 1.0.1, swc_malloc 1.2.5, swc_plugin 1.0.1, swc_plugin_macro 2.0.0, swc_plugin_proxy 30.0.0, swc_transform_common 20.0.0, swc_visit 2.0.2, testing 27.0.0, testing_macros 1.0.2, wasip2 1.0.1+wasi-0.2.4, winapi-i686-pc-windows-gnu 0.4.0, winapi-x86_64-pc-windows-gnu 0.4.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Copyright notices: + +```text +ast_node 7.0.0, better_scoped_tls 1.0.1, from_variant 3.0.0, murmur2 0.1.0, r-efi 5.3.0, string_enum 1.0.2, swc_allocator 5.0.0, swc_atoms 10.0.0, swc_common 26.0.0, swc_compiler_base 65.0.0, swc_config 6.0.0, swc_config_macro 1.0.1, swc_core 81.0.2, swc_ecma_ast 29.0.2, swc_ecma_codegen 32.0.3, swc_ecma_codegen_macros 2.0.2, swc_ecma_hooks 1.0.0, swc_ecma_minifier 62.0.1, swc_ecma_parser 45.1.6, swc_ecma_testing 27.0.0, swc_ecma_transforms_base 49.0.1, swc_ecma_transforms_optimization 52.0.2, swc_ecma_transforms_react 55.0.1, swc_ecma_transforms_testing 53.0.0, swc_ecma_transforms_typescript 55.0.4, swc_ecma_utils 35.0.2, swc_ecma_visit 29.0.0, swc_eq_ignore_macros 1.0.1, swc_error_reporters 28.0.0, swc_macros_common 1.0.1, swc_malloc 1.2.5, swc_plugin 1.0.1, swc_plugin_macro 2.0.0, swc_plugin_proxy 30.0.0, swc_transform_common 20.0.0, swc_visit 2.0.2, testing 27.0.0, testing_macros 1.0.2: + Copyright 2024 SWC contributors. +futures 0.3.34, futures-channel 0.3.34, futures-core 0.3.34, futures-executor 0.3.34, futures-io 0.3.34, futures-macro 0.3.34, futures-sink 0.3.34, futures-task 0.3.34, futures-util 0.3.34: + Copyright (c) 2016 Alex Crichton + Copyright (c) 2017 The Tokio Authors +miette-derive 7.6.0: + Copyright [yyyy] Kat + Marchán +pathdiff 0.2.3: + Copyright 2017 The Rust Project Developers +radix_fmt 1.0.0: + Copyright 2018 Boiethios +regress 0.11.1, regress 0.12.0: + Copyright 2020 ridiculous_fish +typenum 1.19.0: + Copyright 2014 Paho Lurie-Gregg +windows 0.62.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_msvc 0.52.6, windows_i686_gnu 0.52.6, windows_i686_gnullvm 0.52.6, windows_i686_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_gnullvm 0.52.6, windows_x86_64_msvc 0.52.6, windows-collections 0.3.2, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.61.2, windows-targets 0.52.6, windows-threading 0.2.1: + Copyright (c) Microsoft Corporation. +yansi 1.0.1: + Copyright 2017 Sergio Benitez +zerocopy 0.8.63, zerocopy-derive 0.8.63: + Copyright 2023 The Fuchsia Authors +``` + +## Text 380f9bad + +Packages: windows 0.62.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_msvc 0.52.6, windows_i686_gnu 0.52.6, windows_i686_gnullvm 0.52.6, windows_i686_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_gnullvm 0.52.6, windows_x86_64_msvc 0.52.6, windows-collections 0.3.2, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.61.2, windows-targets 0.52.6, windows-threading 0.2.1 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +Copyright notices: + +```text +windows 0.62.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_msvc 0.52.6, windows_i686_gnu 0.52.6, windows_i686_gnullvm 0.52.6, windows_i686_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_gnullvm 0.52.6, windows_x86_64_msvc 0.52.6, windows-collections 0.3.2, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.61.2, windows-targets 0.52.6, windows-threading 0.2.1: + Copyright (c) Microsoft Corporation. +``` + +## Text 4bea89d5 + +Packages: icu_collator 2.3.1, icu_collator_data 2.3.0, icu_collections 2.3.0, icu_locale_core 2.4.0, icu_locale_fallback 2.3.0, icu_locale_fallback_data 2.3.0, icu_normalizer 2.3.0, icu_normalizer_data 2.3.0, icu_properties 2.3.0, icu_properties_data 2.3.0, icu_provider 2.4.0, litemap 0.8.3, potential_utf 0.1.6, tinystr 0.8.5, writeable 0.6.4, yoke 0.8.4, yoke-derive 0.8.4, zerofrom 0.1.8, zerofrom-derive 0.1.8, zerotrie 0.2.5, zerovec 0.11.9, zerovec-derive 0.11.7 + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + +Copyright notices: + +```text +icu_collator 2.3.1, icu_collator_data 2.3.0, icu_collections 2.3.0, icu_locale_core 2.4.0, icu_locale_fallback 2.3.0, icu_locale_fallback_data 2.3.0, icu_normalizer 2.3.0, icu_normalizer_data 2.3.0, icu_properties 2.3.0, icu_properties_data 2.3.0, icu_provider 2.4.0, litemap 0.8.3, potential_utf 0.1.6, tinystr 0.8.5, writeable 0.6.4, yoke 0.8.4, yoke-derive 0.8.4, zerofrom 0.1.8, zerofrom-derive 0.1.8, zerotrie 0.2.5, zerovec 0.11.9, zerovec-derive 0.11.7: + Copyright © 2020-2024 Unicode, Inc. +``` + +## Text d89f3ef2 + +Packages: stylex_ast 0.19.2, stylex_atoms 0.19.2, stylex_constants 0.19.2, stylex_css 0.19.2, stylex_css_parser 0.19.2, stylex_diagnostics 0.19.2, stylex_enums 0.19.2, stylex_evaluator 0.19.2, stylex_js 0.19.2, stylex_macros 0.19.2, stylex_nested_config 0.19.2, stylex_path_resolver 0.19.2, stylex_regex 0.19.2, stylex_state 0.19.2, stylex_state_index 0.19.2, stylex_structures 0.19.2, stylex_transform 0.19.2, stylex_types 0.19.2, stylex_utils 0.19.2 + +# Notice + +This project is MIT licensed; the terms are in [LICENSE](./LICENSE) and the +copyright there is the project's own. It contains third-party work, listed +below with its copyright holder and licence. + +## StyleX + +- **Upstream:** +- **Copyright (c) Meta Platforms, Inc. and affiliates.** +- **Licence:** MIT + +The compiler's observable behaviour (class-name hashing, property expansion, +value canonicalization, rule priorities) is StyleX's, and parts of the +implementation follow Meta's structure to match it exactly. + +## styleq + +- **Upstream:** +- **Copyright (c) Nicolas Gallagher** +- **Licence:** MIT, text at + [crates/stylex-styleq/LICENSE](./crates/stylex-styleq/LICENSE) + +[`crates/stylex-styleq`](./crates/stylex-styleq) is a Rust port of the class-name +merger the StyleX runtime uses. + +## postcss-value-parser + +- **Upstream:** +- **Copyright (c) Bogdan Chadkin** +- **Licence:** MIT, text at + [crates/postcss-value-parser/LICENSE](./crates/postcss-value-parser/LICENSE) + +[`crates/postcss-value-parser`](./crates/postcss-value-parser) carries this CSS +value scanner as its own dependency-free crate. + +## Everything else + +Registry dependencies carry their own licence files; `Cargo.lock` records their +versions. + +## Text 2069c208 + +Packages: aho-corasick 1.1.5, byteorder 1.5.0, globset 0.4.18, memchr 2.8.3, termcolor 1.4.1, winapi-util 0.1.11 + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +## Text 7e7a2c78 + +Packages: aho-corasick 1.1.5, byteorder 1.5.0, globset 0.4.18, memchr 2.8.3, termcolor 1.4.1, winapi-util 0.1.11 + +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + +## Text 24528692 + +Packages: napi 3.16.1, napi-derive 3.7.0, napi-derive-backend 7.0.0, napi-sys 3.4.1 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Copyright notices: + +```text +napi 3.16.1, napi-derive 3.7.0, napi-derive-backend 7.0.0, napi-sys 3.4.1: + Copyright (c) 2018 GitHub +napi 3.16.1, napi-derive 3.7.0, napi-derive-backend 7.0.0, napi-sys 3.4.1: + Copyright (c) 2020-present LongYinan +``` + +## Text 444399c3 + +Packages: foldhash 0.2.0, libz-rs-sys 0.5.4, miniz_oxide 0.8.5, zlib-rs 0.5.4 + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +Copyright notices: + +```text +foldhash 0.2.0: + Copyright (c) 2024 Orson Peters +libz-rs-sys 0.5.4, zlib-rs 0.5.4: + (C) 2024 Trifecta Tech Foundation +miniz_oxide 0.8.5: + Copyright 2013-2014 RAD Game Tools and Valve Software + Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + Copyright (c) 2020 Frommi + Copyright (c) 2017-2024 oyvindln +``` + +## Text 68440691 + +Packages: dragonbox_ecma 0.1.12, linux-raw-sys 0.12.1, rustix 1.1.4, wit-bindgen 0.46.0 + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +## Text 9fba0587 + +Packages: unicode-segmentation 1.13.3, unicode-width 0.1.14, unicode-width 0.2.3, unicode-xid 0.2.6 + +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + +## Text a796d573 + +Packages: dragonbox_ecma 0.1.12, ryu 1.0.23, ryu-js 1.0.3, xxhash-rust 0.8.18 + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +## Text a7fae6cb + +Packages: colored 3.1.1, cssparser 0.38.0, cssparser-macros 0.7.0, dtoa-short 0.3.5 + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +## Text 278abc70 + +Packages: zerocopy 0.8.63, zerocopy-derive 0.8.63 + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright notices: + +```text +zerocopy 0.8.63, zerocopy-derive 0.8.63: + Copyright 2019 The Fuchsia Authors. +``` + +## Text 3d0edf29 + +Packages: utf_types 1.0.0, write16 1.0.0 + +Licensed under the Apache License (Version 2.0), or the MIT license, +(the "Licenses") at your option. You may not use this file except in +compliance with one of the Licenses. You may obtain copies of the +Licenses at: + + https://www.apache.org/licenses/LICENSE-2.0 + https://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software +distributed under the Licenses is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Licenses for the specific language governing permissions and +limitations under the Licenses. + +-- + +Test code is dedicated to the Public Domain when so designated (see +the individual files for PD/CC0-dedicated sections). + +Copyright notices: + +```text +utf_types 1.0.0, write16 1.0.0: + Copyright Mozilla Foundation +``` + +## Text 5c5512ba + +Packages: unicode-id-start 1.5.0, unicode-ident 1.0.27 + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +Copyright notices: + +```text +unicode-id-start 1.5.0, unicode-ident 1.0.27: + Copyright © 1991-2023 Unicode, Inc. +``` + +## Text e1c73bd2 + +Packages: siphasher 0.3.11, siphasher 1.0.4 + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. + +Copyright notices: + +```text +siphasher 0.3.11: + Copyright 2012-2016 The Rust Project Developers. + Copyright 2016-2023 Frank Denis. +siphasher 1.0.4: + Copyright 2012-2016 The Rust Project Developers. + Copyright 2016-2026 Frank Denis. +``` + +## Text fbe36b2f + +Packages: rand 0.8.8, rand_core 0.6.4 + +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. + +## Text 091b05f0 + +Packages: r-efi 5.3.0 + +LICENSE: + This project is triple-licensed under the MIT License, the Apache + License, Version 2.0, and the GNU Lesser General Public License, + Version 2.1+. + +AUTHORS-MIT: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +AUTHORS-ASL: + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +AUTHORS-LGPL: + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; If not, see . + +AUTHORS: (ordered alphabetically) + Alex James + Ayush Singh + Boris-Chengbiao Zhou + Bret Barkelew + Christopher Zurcher + David Rheinsberg + Dmitry Mostovenko + Hiroki Tokunaga + Joe Richey + John Schock + Michael Kubacki + Oliver Smith-Denny + Richard Wiedenhöft + Rob Bradford , + Tom Gundersen + Trevor Gross + +Copyright notices: + +```text +r-efi 5.3.0: + COPYRIGHT: (ordered alphabetically) + Copyright (C) 2017-2023 Red Hat, Inc. + Copyright (C) 2019-2023 Microsoft Corporation + Copyright (C) 2022-2023 David Rheinsberg +``` + +## Text 175bdea6 + +Packages: rustix 1.1.4 + +Short version for non-lawyers: + +`rustix` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + +Longer version: + +Copyrights in the `rustix` project are retained by their contributors. +No copyright assignment is required to contribute to the `rustix` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `rustix` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + +## Text 1fc59585 + +Packages: adler2 2.0.0 + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Copyright notices: + +```text +adler2 2.0.0: + Copyright (C) Jonas Schievink +``` + +## Text 23f0eb5c + +Packages: tikv-jemalloc-sys 0.5.4+5.3.0-patched + +Unless otherwise specified, files in the jemalloc source distribution are +subject to the following license: +-------------------------------------------------------------------------------- +Copyright (C) 2002-present Jason Evans . +All rights reserved. +Copyright (C) 2007-2012 Mozilla Foundation. All rights reserved. +Copyright (C) 2009-present Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice(s), + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice(s), + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- + +## Text 27c18106 + +Packages: linux-raw-sys 0.12.1 + +Short version for non-lawyers: + +`linux-raw-sys` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + +Longer version: + +Copyrights in the `linux-raw-sys` project are retained by their contributors. +No copyright assignment is required to contribute to the `linux-raw-sys` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `linux-raw-sys` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + +## Text 4829cc8e + +Packages: libloading 0.9.0 + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +Copyright notices: + +```text +libloading 0.9.0: + Copyright © 2015, Simonas Kazlauskas +``` + +## Text 6037ec11 + +Packages: path-clean 1.0.1 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN + +Copyright notices: + +```text +path-clean 1.0.1: + Copyright (c) 2018 Dan Reeves +``` + +## Text 63851e6f + +Packages: petgraph 0.7.1 + +Graphosaurus (c) by the petgraph project. + +Graphosaurus is licensed under a +Creative Commons Attribution-ShareAlike 4.0 International License. + +You should have received a copy of the license along with this +work. If not, see . + +## Text 6405f47d + +Packages: utf16_iter 1.1.0 + +Licensed under the Apache License (Version 2.0), or the MIT license, +(the "Licenses") at your option. You may not use this file except in +compliance with one of the Licenses. You may obtain copies of the +Licenses at: + + https://www.apache.org/licenses/LICENSE-2.0 + https://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software +distributed under the Licenses is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Licenses for the specific language governing permissions and +limitations under the Licenses. + +-- + +Test code is dedicated to the Public Domain when so designated (see +the individual files for PD/CC0-dedicated sections). + +-- + +The implementation for Utf16CharIndices was adapted from the +CharIndices implementation of the Rust standard library at revision +ab32548539ec38a939c1b58599249f3b54130026 +(https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/library/core/src/str/iter.rs). + +Excerpt from https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/COPYRIGHT , +which refers to +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-APACHE +and +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-MIT +: + +For full authorship information, see the version control history or +https://thanks.rust-lang.org + +Except as otherwise noted (below and/or in individual files), Rust is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +Copyright notices: + +```text +utf16_iter 1.1.0: + Copyright Mozilla Foundation +``` + +## Text 71f2cc72 + +Packages: regex-syntax 0.8.11 + +UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE + +Unicode Data Files include all data files under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +Unicode Data Files do not include PDF online code charts under the +directory http://www.unicode.org/Public/. + +Software includes any source code published in the Unicode Standard +or under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +NOTICE TO USER: Carefully read the following legal agreement. +BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S +DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), +YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. +IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE +THE DATA FILES OR SOFTWARE. + +COPYRIGHT AND PERMISSION NOTICE + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +Copyright notices: + +```text +regex-syntax 0.8.11: + Copyright © 1991-2018 Unicode, Inc. All rights reserved. + Distributed under the Terms of Use in http://www.unicode.org/copyright.html. +``` + +## Text 80cb89b5 + +Packages: bstr 1.12.1 + +This project is licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or + https://opensource.org/licenses/MIT) + +at your option. + +## Text 8a355c16 + +Packages: swc_sourcemap 10.0.3 + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright notices: + +```text +swc_sourcemap 10.0.3: + Copyright (c) 2016 by Armin Ronacher. +``` + +## Text 8d6ca7f4 + +Packages: utf8_iter 1.1.0 + +Licensed under the Apache License (Version 2.0), or the MIT license, +(the "Licenses") at your option. You may not use this file except in +compliance with one of the Licenses. You may obtain copies of the +Licenses at: + + https://www.apache.org/licenses/LICENSE-2.0 + https://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software +distributed under the Licenses is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Licenses for the specific language governing permissions and +limitations under the Licenses. + +-- + +Test code is dedicated to the Public Domain when so designated (see +the individual files for PD/CC0-dedicated sections). + +-- + +The implementation for Utf8CharIndices was adapted from the +CharIndices implementation of the Rust standard library at revision +ab32548539ec38a939c1b58599249f3b54130026 +(https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/library/core/src/str/iter.rs). + +Excerpt from https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/COPYRIGHT , +which refers to +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-APACHE +and +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-MIT +: + +For full authorship information, see the version control history or +https://thanks.rust-lang.org + +Except as otherwise noted (below and/or in individual files), Rust is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +Copyright notices: + +```text +utf8_iter 1.1.0: + Copyright Mozilla Foundation +``` + +## Text a47b5281 + +Packages: path-clean 1.0.1 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Copyright notices: + +```text +path-clean 1.0.1: + Copyright 2018 Dan Reeves +``` + +## Text a97d8f85 + +Packages: pnp 0.12.12 + +BSD 2-Clause License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright notices: + +```text +pnp 0.12.12: + Copyright (c) 2016-present, Yarn Contributors. + All rights reserved. +``` + +## Text db11fec9 + +Packages: typenum 1.19.0 + +MIT OR Apache-2.0 + +## Text dc63b631 + +Packages: is-terminal 0.4.15 + +Portions of this project are derived from atty, which bears the following +copyright notice and permission notice: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Copyright notices: + +```text +is-terminal 0.4.15: + Copyright (c) 2015-2019 Doug Tangren +``` + +## Text fa58f7ee + +Packages: humantime 2.1.0 + +Includes parts of http date with the following copyright: +Copyright (c) 2016 Pyfisch + +Includes portions of musl libc with the following copyright: +Copyright © 2005-2013 Rich Felker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Copyright notices: + +```text +humantime 2.1.0: + Copyright (c) 2016 The humantime Developers +``` diff --git a/packages/@stylexjs/rust-plugin/THIRD_PARTY_NOTICES.md b/packages/@stylexjs/rust-plugin/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..636d11b63 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/THIRD_PARTY_NOTICES.md @@ -0,0 +1,314 @@ +# Third-party notices + +The native addon (`native/*.node`) is built from the Rust crates below. The +full license and notice texts for every crate in its normal dependency graph +(`cargo tree -p stylex_rust_compiler --features node -e normal --target all`, +which also includes proc-macro and target-specific crates) are in +`RUST_DEPENDENCY_LICENSES.md`; `LICENSE_PROVENANCE.json` records where each +text came from. + +- The StyleX compiler core in `crates/` is vendored from + [stylex-swc-plugin](https://github.com/Dwlad90/stylex-swc-plugin) 0.19.2 under + the MIT license: see `crates/LICENSE` (license text and copyright), + `crates/NOTICE.md` (attribution) and `crates/PROVENANCE.md` + (exact source and every local change). +- `vendor/swc_ecma_parser` is the published `swc_ecma_parser` 45.1.6 crate with + the patches described in `vendor/README.md` (Apache-2.0). +- On Windows, macOS and 64-bit Linux (GNU) the addon uses mimalloc as its memory + allocator through `swc_malloc`; `libmimalloc-sys` compiles Microsoft's mimalloc + C sources (MIT). On 32-bit ARM Linux (GNU), `swc_malloc` selects jemalloc + through `tikv-jemallocator` (jemalloc's BSD-2-Clause license). + +Every crate's license is its own. The table lists the crates of the +host-target graph (`cargo tree -e normal --features node`) with their declared +licenses. + +| Crate | Version | License | Source | +|---|---|---|---| +| ahash | 0.8.12 | MIT OR Apache-2.0 | crates.io | +| aho-corasick | 1.1.5 | Unlicense OR MIT | crates.io | +| allocator-api2 | 0.2.21 | MIT OR Apache-2.0 | crates.io | +| ansi_term | 0.12.1 | MIT | crates.io | +| anyhow | 1.0.104 | MIT OR Apache-2.0 | crates.io | +| arrayvec | 0.7.8 | MIT OR Apache-2.0 | crates.io | +| ascii | 1.1.0 | Apache-2.0 OR MIT | crates.io | +| base64 | 0.22.1 | MIT OR Apache-2.0 | crates.io | +| base64-simd | 0.8.0 | MIT | crates.io | +| better_scoped_tls | 1.0.1 | Apache-2.0 | crates.io | +| bit-set | 0.8.0 | Apache-2.0 OR MIT | crates.io | +| bit-vec | 0.8.0 | Apache-2.0 OR MIT | crates.io | +| bitflags | 2.13.2 | MIT OR Apache-2.0 | crates.io | +| bitvec | 1.1.1 | MIT | crates.io | +| block-buffer | 0.10.4 | MIT OR Apache-2.0 | crates.io | +| bstr | 1.12.1 | MIT OR Apache-2.0 | crates.io | +| bumpalo | 3.20.3 | MIT OR Apache-2.0 | crates.io | +| byteorder | 1.5.0 | Unlicense OR MIT | crates.io | +| bytes | 1.12.1 | MIT | crates.io | +| bytes-str | 0.2.9 | Apache-2.0 | crates.io | +| camino | 1.1.11 | MIT OR Apache-2.0 | crates.io | +| cargo-platform | 0.1.9 | MIT OR Apache-2.0 | crates.io | +| cargo_metadata | 0.18.1 | MIT | crates.io | +| castaway | 0.2.4 | MIT | crates.io | +| cbor4ii | 1.2.2 | MIT | crates.io | +| cfg-if | 1.0.5 | MIT OR Apache-2.0 | crates.io | +| colored | 3.1.1 | MPL-2.0 | crates.io | +| compact_str | 0.10.0 | MIT | crates.io | +| compact_str | 0.7.1 | MIT | crates.io | +| concurrent_lru | 0.2.0 | MIT | crates.io | +| convert_case | 0.10.0 | MIT | crates.io | +| convert_case | 0.12.0 | MIT | crates.io | +| cpufeatures | 0.2.17 | MIT OR Apache-2.0 | crates.io | +| crc32fast | 1.5.0 | MIT OR Apache-2.0 | crates.io | +| crossbeam-utils | 0.8.21 | MIT OR Apache-2.0 | crates.io | +| crypto-common | 0.1.6 | MIT OR Apache-2.0 | crates.io | +| cssparser | 0.38.0 | MPL-2.0 | crates.io | +| ctor | 1.0.13 | Apache-2.0 OR MIT | crates.io | +| darling | 0.20.11 | MIT | crates.io | +| darling_core | 0.20.11 | MIT | crates.io | +| dashmap | 6.2.1 | MIT | crates.io | +| data-encoding | 2.11.1 | MIT | crates.io | +| debugid | 0.8.0 | Apache-2.0 | crates.io | +| derive_builder | 0.20.2 | MIT OR Apache-2.0 | crates.io | +| derive_builder_core | 0.20.2 | MIT OR Apache-2.0 | crates.io | +| derive_more | 2.1.1 | MIT | crates.io | +| diff | 0.1.13 | MIT OR Apache-2.0 | crates.io | +| difference | 2.0.0 | MIT | crates.io | +| digest | 0.10.7 | MIT OR Apache-2.0 | crates.io | +| dragonbox_ecma | 0.1.12 | Apache-2.0 WITH LLVM-exception OR BSL-1.0 | crates.io | +| dtoa | 1.0.10 | MIT OR Apache-2.0 | crates.io | +| dtoa-short | 0.3.5 | MPL-2.0 | crates.io | +| either | 1.19.0 | MIT OR Apache-2.0 | crates.io | +| endian-type | 0.2.0 | MIT | crates.io | +| env_logger | 0.10.2 | MIT OR Apache-2.0 | crates.io | +| equivalent | 1.0.3 | Apache-2.0 OR MIT | crates.io | +| fancy-regex | 0.19.2 | MIT | crates.io | +| fast-glob | 1.0.0 | MIT | crates.io | +| fastrand | 2.3.0 | Apache-2.0 OR MIT | crates.io | +| fixedbitset | 0.5.7 | MIT OR Apache-2.0 | crates.io | +| flate2 | 1.1.5 | MIT OR Apache-2.0 | crates.io | +| float-cmp | 0.10.0 | MIT | crates.io | +| fnv | 1.0.7 | Apache-2.0 / MIT | crates.io | +| foldhash | 0.2.0 | Zlib | crates.io | +| form_urlencoded | 1.2.2 | MIT OR Apache-2.0 | crates.io | +| funty | 2.0.0 | MIT | crates.io | +| futures | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-channel | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-core | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-executor | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-io | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-sink | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-task | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| futures-util | 0.3.34 | MIT OR Apache-2.0 | crates.io | +| generic-array | 0.14.9 | MIT | crates.io | +| getrandom | 0.3.4 | MIT OR Apache-2.0 | crates.io | +| glob | 0.3.4 | MIT OR Apache-2.0 | crates.io | +| globset | 0.4.18 | Unlicense OR MIT | crates.io | +| halfbrown | 0.4.0 | Apache-2.0/MIT | crates.io | +| hashbrown | 0.14.5 | MIT OR Apache-2.0 | crates.io | +| hashbrown | 0.16.1 | MIT OR Apache-2.0 | crates.io | +| hashbrown | 0.17.1 | MIT OR Apache-2.0 | crates.io | +| heck | 0.5.0 | MIT OR Apache-2.0 | crates.io | +| hex | 0.4.3 | MIT OR Apache-2.0 | crates.io | +| hstr | 4.0.0 | Apache-2.0 | crates.io | +| humantime | 2.1.0 | MIT/Apache-2.0 | crates.io | +| icu_collator | 2.3.1 | Unicode-3.0 | crates.io | +| icu_collator_data | 2.3.0 | Unicode-3.0 | crates.io | +| icu_collections | 2.3.0 | Unicode-3.0 | crates.io | +| icu_locale_core | 2.4.0 | Unicode-3.0 | crates.io | +| icu_locale_fallback | 2.3.0 | Unicode-3.0 | crates.io | +| icu_locale_fallback_data | 2.3.0 | Unicode-3.0 | crates.io | +| icu_normalizer | 2.3.0 | Unicode-3.0 | crates.io | +| icu_normalizer_data | 2.3.0 | Unicode-3.0 | crates.io | +| icu_properties | 2.3.0 | Unicode-3.0 | crates.io | +| icu_properties_data | 2.3.0 | Unicode-3.0 | crates.io | +| icu_provider | 2.4.0 | Unicode-3.0 | crates.io | +| ident_case | 1.0.1 | MIT/Apache-2.0 | crates.io | +| idna | 1.1.0 | MIT OR Apache-2.0 | crates.io | +| idna_adapter | 1.2.2 | Apache-2.0 OR MIT | crates.io | +| if_chain | 1.0.3 | MIT/Apache-2.0 | crates.io | +| indexmap | 2.14.2 | Apache-2.0 OR MIT | crates.io | +| is-terminal | 0.4.15 | MIT | crates.io | +| itoa | 1.0.18 | MIT OR Apache-2.0 | crates.io | +| json-strip-comments | 3.1.0 | MIT | crates.io | +| lazy_static | 1.5.0 | MIT OR Apache-2.0 | crates.io | +| libc | 0.2.191 | MIT OR Apache-2.0 | crates.io | +| libloading | 0.9.0 | ISC | crates.io | +| libmimalloc-sys | 0.1.49 | MIT | crates.io | +| libz-rs-sys | 0.5.4 | Zlib | crates.io | +| link-section | 0.19.3 | Apache-2.0 OR MIT | crates.io | +| linux-raw-sys | 0.12.1 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT | crates.io | +| litemap | 0.8.3 | Unicode-3.0 | crates.io | +| lock_api | 0.4.14 | MIT OR Apache-2.0 | crates.io | +| log | 0.4.34 | MIT OR Apache-2.0 | crates.io | +| matchers | 0.2.0 | MIT | crates.io | +| memchr | 2.8.3 | Unlicense OR MIT | crates.io | +| miette | 7.6.0 | Apache-2.0 | crates.io | +| mimalloc | 0.1.52 | MIT | crates.io | +| murmur2 | 0.1.0 | Apache-2.0 | crates.io | +| napi | 3.16.1 | MIT | crates.io | +| napi-derive-backend | 7.0.0 | MIT | crates.io | +| napi-sys | 3.4.1 | MIT | crates.io | +| new_debug_unreachable | 1.0.6 | MIT | crates.io | +| nibble_vec | 0.1.0 | MIT | crates.io | +| nodejs-built-in-modules | 1.0.0 | MIT | crates.io | +| nohash-hasher | 0.2.0 | Apache-2.0 OR MIT | crates.io | +| nu-ansi-term | 0.50.1 | MIT | crates.io | +| num-bigint | 0.4.8 | MIT OR Apache-2.0 | crates.io | +| num-integer | 0.1.47 | MIT OR Apache-2.0 | crates.io | +| num-traits | 0.2.19 | MIT OR Apache-2.0 | crates.io | +| num_cpus | 1.17.0 | MIT OR Apache-2.0 | crates.io | +| once_cell | 1.21.4 | MIT OR Apache-2.0 | crates.io | +| outref | 0.5.2 | MIT | crates.io | +| owo-colors | 4.2.2 | MIT | crates.io | +| oxc_resolver | 11.24.3 | MIT | crates.io | +| package-json | 0.5.0 | MIT | crates.io | +| par-core | 2.0.0 | Apache-2.0 | crates.io | +| par-iter | 2.0.0 | Apache-2.0 | crates.io | +| parking_lot | 0.12.5 | MIT OR Apache-2.0 | crates.io | +| parking_lot_core | 0.9.12 | MIT OR Apache-2.0 | crates.io | +| path-clean | 1.0.1 | MIT OR Apache-2.0 | crates.io | +| pathdiff | 0.2.3 | MIT/Apache-2.0 | crates.io | +| percent-encoding | 2.3.2 | MIT OR Apache-2.0 | crates.io | +| petgraph | 0.7.1 | MIT OR Apache-2.0 | crates.io | +| phf | 0.11.3 | MIT | crates.io | +| phf | 0.14.0 | MIT | crates.io | +| phf_generator | 0.11.3 | MIT | crates.io | +| phf_generator | 0.14.0 | MIT | crates.io | +| phf_shared | 0.11.3 | MIT | crates.io | +| phf_shared | 0.14.0 | MIT | crates.io | +| pin-project-lite | 0.2.17 | Apache-2.0 OR MIT | crates.io | +| pnp | 0.12.12 | BSD-2-Clause | crates.io | +| postcss_value_parser | 0.19.2 | MIT | vendored (crates/) | +| potential_utf | 0.1.6 | Unicode-3.0 | crates.io | +| pretty_assertions | 1.4.1 | MIT OR Apache-2.0 | crates.io | +| pretty_env_logger | 0.5.0 | MIT/Apache-2.0 | crates.io | +| proc-macro2 | 1.0.107 | MIT OR Apache-2.0 | crates.io | +| psm | 0.1.32 | MIT OR Apache-2.0 | crates.io | +| quote | 1.0.47 | MIT OR Apache-2.0 | crates.io | +| radium | 0.7.0 | MIT | crates.io | +| radix_fmt | 1.0.0 | Apache-2.0 | crates.io | +| radix_trie | 0.3.0 | MIT | crates.io | +| rand | 0.8.8 | MIT OR Apache-2.0 | crates.io | +| rand_core | 0.6.4 | MIT OR Apache-2.0 | crates.io | +| ref-cast | 1.0.25 | MIT OR Apache-2.0 | crates.io | +| regex | 1.13.1 | MIT OR Apache-2.0 | crates.io | +| regex-automata | 0.4.18 | MIT OR Apache-2.0 | crates.io | +| regex-syntax | 0.8.11 | MIT OR Apache-2.0 | crates.io | +| regress | 0.11.1 | MIT OR Apache-2.0 | crates.io | +| regress | 0.12.0 | MIT OR Apache-2.0 | crates.io | +| relative-path | 1.9.3 | MIT OR Apache-2.0 | crates.io | +| rustc-hash | 2.1.3 | Apache-2.0 OR MIT | crates.io | +| rustix | 1.1.4 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT | crates.io | +| ryu | 1.0.23 | Apache-2.0 OR BSL-1.0 | crates.io | +| ryu-js | 1.0.3 | Apache-2.0 OR BSL-1.0 | crates.io | +| scoped-tls | 1.0.1 | MIT/Apache-2.0 | crates.io | +| scopeguard | 1.2.0 | MIT OR Apache-2.0 | crates.io | +| self_cell | 1.2.1 | Apache-2.0 OR GPL-2.0-only | crates.io | +| semver | 1.0.28 | MIT OR Apache-2.0 | crates.io | +| serde | 1.0.229 | MIT OR Apache-2.0 | crates.io | +| serde_core | 1.0.229 | MIT OR Apache-2.0 | crates.io | +| serde_json | 1.0.154 | MIT OR Apache-2.0 | crates.io | +| sha1 | 0.10.6 | MIT OR Apache-2.0 | crates.io | +| sha2 | 0.10.9 | MIT OR Apache-2.0 | crates.io | +| sharded-slab | 0.1.7 | MIT | crates.io | +| simd-json | 0.18.1 | Apache-2.0 OR MIT | crates.io | +| simdutf8 | 0.1.5 | MIT OR Apache-2.0 | crates.io | +| siphasher | 0.3.11 | MIT/Apache-2.0 | crates.io | +| siphasher | 1.0.4 | MIT OR Apache-2.0 | crates.io | +| slab | 0.4.12 | MIT | crates.io | +| smallvec | 1.16.4 | MIT OR Apache-2.0 | crates.io | +| stable_deref_trait | 1.2.1 | MIT OR Apache-2.0 | crates.io | +| stacker | 0.1.25 | MIT OR Apache-2.0 | crates.io | +| static_assertions | 1.1.0 | MIT OR Apache-2.0 | crates.io | +| strsim | 0.11.1 | MIT | crates.io | +| stylex_ast | 0.19.2 | MIT | vendored (crates/) | +| stylex_atoms | 0.19.2 | MIT | vendored (crates/) | +| stylex_constants | 0.19.2 | MIT | vendored (crates/) | +| stylex_css | 0.19.2 | MIT | vendored (crates/) | +| stylex_css_parser | 0.19.2 | MIT | vendored (crates/) | +| stylex_diagnostics | 0.19.2 | MIT | vendored (crates/) | +| stylex_enums | 0.19.2 | MIT | vendored (crates/) | +| stylex_evaluator | 0.19.2 | MIT | vendored (crates/) | +| stylex_js | 0.19.2 | MIT | vendored (crates/) | +| stylex_macros | 0.19.2 | MIT | vendored (crates/) | +| stylex_nested_config | 0.19.2 | MIT | vendored (crates/) | +| stylex_path_resolver | 0.19.2 | MIT | vendored (crates/) | +| stylex_regex | 0.19.2 | MIT | vendored (crates/) | +| stylex_state | 0.19.2 | MIT | vendored (crates/) | +| stylex_state_index | 0.19.2 | MIT | vendored (crates/) | +| stylex_structures | 0.19.2 | MIT | vendored (crates/) | +| stylex_styleq | 0.19.2 | MIT | vendored (crates/) | +| stylex_transform | 0.19.2 | MIT | vendored (crates/) | +| stylex_types | 0.19.2 | MIT | vendored (crates/) | +| stylex_utils | 0.19.2 | MIT | vendored (crates/) | +| swc_allocator | 5.0.0 | Apache-2.0 | crates.io | +| swc_atoms | 10.0.0 | Apache-2.0 | crates.io | +| swc_common | 26.0.0 | Apache-2.0 | crates.io | +| swc_compiler_base | 65.0.0 | Apache-2.0 | crates.io | +| swc_config | 6.0.0 | Apache-2.0 | crates.io | +| swc_core | 81.0.2 | Apache-2.0 | crates.io | +| swc_ecma_ast | 29.0.2 | Apache-2.0 | crates.io | +| swc_ecma_codegen | 32.0.3 | Apache-2.0 | crates.io | +| swc_ecma_hooks | 1.0.0 | Apache-2.0 | crates.io | +| swc_ecma_minifier | 62.0.1 | Apache-2.0 | crates.io | +| swc_ecma_parser | 45.1.6 | Apache-2.0 | vendored (vendor/) | +| swc_ecma_testing | 27.0.0 | Apache-2.0 | crates.io | +| swc_ecma_transforms_base | 49.0.1 | Apache-2.0 | crates.io | +| swc_ecma_transforms_optimization | 52.0.2 | Apache-2.0 | crates.io | +| swc_ecma_transforms_react | 55.0.1 | Apache-2.0 | crates.io | +| swc_ecma_transforms_testing | 53.0.0 | Apache-2.0 | crates.io | +| swc_ecma_transforms_typescript | 55.0.4 | Apache-2.0 | crates.io | +| swc_ecma_utils | 35.0.2 | Apache-2.0 | crates.io | +| swc_ecma_visit | 29.0.0 | Apache-2.0 | crates.io | +| swc_error_reporters | 28.0.0 | Apache-2.0 | crates.io | +| swc_macros_common | 1.0.1 | Apache-2.0 | crates.io | +| swc_malloc | 1.2.5 | Apache-2.0 | crates.io | +| swc_plugin | 1.0.1 | Apache-2.0 | crates.io | +| swc_plugin_proxy | 30.0.0 | Apache-2.0 | crates.io | +| swc_sourcemap | 10.0.3 | BSD-3-Clause | crates.io | +| swc_transform_common | 20.0.0 | Apache-2.0 | crates.io | +| swc_visit | 2.0.2 | Apache-2.0 | crates.io | +| syn | 2.0.119 | MIT OR Apache-2.0 | crates.io | +| syn | 3.0.9 | MIT OR Apache-2.0 | crates.io | +| synstructure | 0.14.0 | MIT | crates.io | +| tap | 1.0.1 | MIT | crates.io | +| tempfile | 3.21.0 | MIT OR Apache-2.0 | crates.io | +| termcolor | 1.4.1 | Unlicense OR MIT | crates.io | +| testing | 27.0.0 | Apache-2.0 | crates.io | +| textwrap | 0.16.2 | MIT | crates.io | +| thiserror | 1.0.69 | MIT OR Apache-2.0 | crates.io | +| thiserror | 2.0.21 | MIT OR Apache-2.0 | crates.io | +| thread_local | 1.1.9 | MIT OR Apache-2.0 | crates.io | +| tinystr | 0.8.5 | Unicode-3.0 | crates.io | +| tracing | 0.1.44 | MIT | crates.io | +| tracing-core | 0.1.36 | MIT | crates.io | +| tracing-log | 0.2.0 | MIT | crates.io | +| tracing-subscriber | 0.3.20 | MIT | crates.io | +| triomphe | 0.1.17 | MIT OR Apache-2.0 | crates.io | +| typenum | 1.19.0 | MIT OR Apache-2.0 | crates.io | +| unicode-id-start | 1.5.0 | AND Unicode-3.0 | crates.io | +| unicode-ident | 1.0.27 | AND Unicode-3.0 | crates.io | +| unicode-linebreak | 0.1.5 | Apache-2.0 | crates.io | +| unicode-segmentation | 1.13.3 | MIT OR Apache-2.0 | crates.io | +| unicode-width | 0.1.14 | MIT OR Apache-2.0 | crates.io | +| unicode-width | 0.2.3 | MIT OR Apache-2.0 | crates.io | +| unicode-xid | 0.2.6 | MIT OR Apache-2.0 | crates.io | +| url | 2.5.8 | MIT OR Apache-2.0 | crates.io | +| utf16_iter | 1.1.0 | Apache-2.0 OR MIT | crates.io | +| utf8_iter | 1.1.0 | Apache-2.0 OR MIT | crates.io | +| utf_types | 1.0.0 | Apache-2.0 OR MIT | crates.io | +| uuid | 1.29.0 | Apache-2.0 OR MIT | crates.io | +| value-trait | 0.12.1 | Apache-2.0/MIT | crates.io | +| vsimd | 0.8.0 | MIT | crates.io | +| write16 | 1.0.0 | Apache-2.0 OR MIT | crates.io | +| writeable | 0.6.4 | Unicode-3.0 | crates.io | +| wyz | 0.5.1 | MIT | crates.io | +| xxhash-rust | 0.8.18 | BSL-1.0 | crates.io | +| yansi | 1.0.1 | MIT OR Apache-2.0 | crates.io | +| yoke | 0.8.4 | Unicode-3.0 | crates.io | +| zerocopy | 0.8.63 | BSD-2-Clause OR Apache-2.0 OR MIT | crates.io | +| zerofrom | 0.1.8 | Unicode-3.0 | crates.io | +| zerotrie | 0.2.5 | Unicode-3.0 | crates.io | +| zerovec | 0.11.9 | Unicode-3.0 | crates.io | +| zlib-rs | 0.5.4 | Zlib | crates.io | +| zmij | 1.0.23 | MIT | crates.io | diff --git a/packages/@stylexjs/rust-plugin/babel.d.ts b/packages/@stylexjs/rust-plugin/babel.d.ts new file mode 100644 index 000000000..0760bae39 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/babel.d.ts @@ -0,0 +1,14 @@ +import type { PluginObj } from '@babel/core'; +import type { CompilerOptions, ProcessOptions, Rule } from './index'; + +declare function styleXTransform(): PluginObj; +declare namespace styleXTransform { + function withOptions( + options: CompilerOptions, + ): [typeof styleXTransform, CompilerOptions]; + function processStylexRules( + rules: readonly Rule[], + options?: ProcessOptions | boolean, + ): string; +} +export = styleXTransform; diff --git a/packages/@stylexjs/rust-plugin/babel.js b/packages/@stylexjs/rust-plugin/babel.js new file mode 100644 index 000000000..068f87861 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/babel.js @@ -0,0 +1,2629 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const compiler = require('./index'); +const { createRequire } = require('node:module'); +const babelCore = require('@babel/core'); +const babelRequire = createRequire(require.resolve('@babel/core')); +const generate = babelRequire('@babel/generator').default; +const { NodePath } = babelRequire('@babel/traverse'); +const babelParser = babelRequire('@babel/parser'); +const { TraceMap, originalPositionFor } = babelRequire( + '@jridgewell/trace-mapping', +); +const { codeFrameColumns } = require('@babel/code-frame'); +const DIAGNOSTIC_PREFIX = 'STYLEX_DIAGNOSTIC:'; +const t = babelCore.types; +const BUILT_IN_SOURCES = ['@stylexjs/stylex', 'stylex', '@stylexjs/atoms']; +// Calls the original plugin compiles on Program.exit rather than on visit. +const EXIT_CALLS = new Set(['props', 'attrs', 'merge', 'default']); + +// --------------------------------------------------------------------------- +// Parsing with the file's own grammar and parser. + +// Parser overrides (notably Hermes) produce the file's AST. +function parserOverrides(state) { + return (state.file.opts.plugins || []).filter( + (plugin) => typeof plugin.parserOverride === 'function', + ); +} + +// Node types that need the flow or typescript parser plugin to reparse. +const FLOW_TYPES = new Set(t.FLIPPED_ALIAS_KEYS.Flow); +const TYPESCRIPT_TYPES = new Set(t.FLIPPED_ALIAS_KEYS.TypeScript); + +function syntaxOf(node, syntax) { + const type = node.type; + if (type.startsWith('JSX')) syntax.jsx = true; + if (FLOW_TYPES.has(type)) syntax.flow = true; + if (TYPESCRIPT_TYPES.has(type)) syntax.typescript = true; +} + +// `known` is the syntax of the current program when the caller collected it. +function grammarOptions(state, known = null) { + const parserOpts = { ...state.file.opts.parserOpts }; + delete parserOpts.sourceFileName; + const plugins = [...(parserOpts.plugins || [])]; + const syntax = known ?? { jsx: false, flow: false, typescript: false }; + if (known == null) + t.traverseFast(state.file.ast.program, (node) => syntaxOf(node, syntax)); + if (parserOverrides(state).length > 0 && !syntax.typescript) { + syntax.jsx = true; + syntax.flow = true; + } + const has = (name) => + plugins.some( + (plugin) => (Array.isArray(plugin) ? plugin[0] : plugin) === name, + ); + if (syntax.typescript && !has('typescript')) plugins.push('typescript'); + if (syntax.flow && !syntax.typescript && !has('flow')) + plugins.push(['flow', { all: true }]); + if (syntax.jsx && !has('jsx')) plugins.push('jsx'); + parserOpts.plugins = plugins; + return parserOpts; +} + +// --------------------------------------------------------------------------- +// Diagnostics. + +function sourcePosition(source, offset) { + // SWC reports UTF-8 offsets. Babel's columns count UTF-16 code units. + const prefix = Buffer.from(source, 'utf8') + .subarray(0, offset) + .toString('utf8'); + const lines = prefix.split(/\r\n|\n|\r|\u2028|\u2029/); + return { line: lines.length, column: lines[lines.length - 1].length + 1 }; +} + +function diagnosticFromError(error) { + if (error != null && error.stylexDiagnostic != null) + return error.stylexDiagnostic; + if (error != null && error.diagnostic != null) return error.diagnostic; + const message = error != null ? error.message : null; + if (typeof message !== 'string' || !message.startsWith(DIAGNOSTIC_PREFIX)) + return null; + try { + const diagnostic = JSON.parse(message.slice(DIAGNOSTIC_PREFIX.length)); + if ( + typeof diagnostic.message === 'string' && + ['Error', 'SyntaxError'].includes(diagnostic.name) + ) + return diagnostic; + } catch (_) { + // A malformed native envelope is a native error, not a StyleX diagnostic. + } + return null; +} + +// A position in Babel-generated text, traced back to the input file. +function tracedPosition(map, position) { + const traced = originalPositionFor(map, { + line: position.line, + column: position.column - 1, + }); + return traced.line == null + ? null + : { line: traced.line, column: traced.column + 1 }; +} + +function diagnosticError(diagnostic, input, state) { + let message = diagnostic.message; + const range = diagnostic.range; + if ( + range != null && + Number.isInteger(range.start) && + Number.isInteger(range.end) + ) { + let source = input.code; + let start = sourcePosition(source, range.start); + let end = sourcePosition(source, range.end); + if (input.map != null) { + // The compiler read Babel-generated text; frame the user's file. + const map = new TraceMap(input.map); + const originalStart = tracedPosition(map, start); + if (originalStart != null) { + const covered = Buffer.from(source, 'utf8') + .subarray(range.start, range.end) + .toString('utf8'); + const startNode = originalNodeAt(state, originalStart, covered); + source = state.file.code; + start = originalStart; + end = + startNode != null && startNode.loc != null + ? { + line: startNode.loc.end.line, + column: startNode.loc.end.column + 1, + } + : start; + } + } + // Like Babel's buildCodeFrameError, a multiline node frames only its start. + message += + '\n' + + codeFrameColumns( + source, + { + start, + end: start.line === end.line ? end : undefined, + }, + { highlightCode: state.file.opts.highlightCode !== false }, + ); + } + const Constructor = diagnostic.name === 'SyntaxError' ? SyntaxError : Error; + const error = new Constructor(message); + error.code = + diagnostic.kind === 'parse' ? 'BABEL_PARSE_ERROR' : 'BABEL_TRANSFORM_ERROR'; + return error; +} + +// The outermost input node at `position` whose Babel printing spans `covered`. +function originalNodeAt(state, position, covered) { + const candidates = []; + t.traverseFast(state.file.ast.program, (node) => { + const loc = node.loc; + if ( + loc != null && + loc.start.line === position.line && + loc.start.column === position.column - 1 + ) + candidates.push(node); + }); + const text = covered.replace(/\s+/g, ''); + return ( + candidates.find( + (node) => generate(node).code.replace(/\s+/g, '') === text, + ) ?? + candidates[0] ?? + null + ); +} + +function metadataRules(rules) { + return rules.map(([name, fields, priority]) => { + const output = + fields.constKey != null && fields.constVal != null + ? { + constKey: fields.constKey, + constVal: fields.constVal, + ltr: fields.ltr, + rtl: fields.rtl ?? null, + } + : { ltr: fields.ltr, rtl: fields.rtl ?? null }; + return [name, output, priority]; + }); +} + +function logDiagnostic(method, message) { + if (method === 'warn') { + console.warn(message); + return; + } + // Current Babel uses two console arguments for its package-prefixed errors. + const match = /^\[([^\]]+)\] ([\s\S]*)$/.exec(message); + if (match != null) console[method](`[${match[1]}]`, match[2]); + else console[method](message); +} + +// Items of `next` that are not accounted for by `previous` (as a multiset). +function multisetDifference(next, previous, key = JSON.stringify) { + const counts = new Map(); + for (const item of previous) { + const k = key(item); + counts.set(k, (counts.get(k) || 0) + 1); + } + return next.filter((item) => { + const k = key(item); + const count = counts.get(k) || 0; + if (count === 0) return true; + counts.set(k, count - 1); + return false; + }); +} + +// --------------------------------------------------------------------------- +// Which files and calls belong to StyleX. + +function importSources(options) { + const sources = new Map(BUILT_IN_SOURCES.map((source) => [source, null])); + for (const entry of Array.isArray(options.importSources) + ? options.importSources + : []) { + if (typeof entry === 'string') sources.set(entry, null); + else if (entry != null && typeof entry.from === 'string') + sources.set(entry.from, typeof entry.as === 'string' ? entry.as : null); + } + return sources; +} + +function sxPropName(options) { + return options.sxPropName === false + ? null + : typeof options.sxPropName === 'string' + ? options.sxPropName + : 'sx'; +} + +function isRequireOf(node, sources) { + return ( + node != null && + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node.arguments.length === 1 && + node.arguments[0].type === 'StringLiteral' && + sources.has(node.arguments[0].value) + ); +} + +// Whether source text provably spells no StyleX import source or sx attribute. +// An escape that could stand for a character of one makes it give up. +const CONTROL_ESCAPES = 'nrtbfv'; +function cannotSpellStyleX(code, s) { + if (s.sx != null && code.includes(s.sx)) return false; + const needles = [...s.sources.keys()]; + if (needles.some((needle) => code.includes(needle))) return false; + const controls = needles.some((needle) => /[\n\r\t\b\f\v]/.test(needle)); + for (let i = code.indexOf('\\'); i !== -1; i = code.indexOf('\\', i + 2)) { + const next = code[i + 1]; + if (next === undefined || /[ux0-9\r\n\u2028\u2029]/.test(next)) + return false; + if (CONTROL_ESCAPES.includes(next)) { + if (controls) return false; + } else if (needles.some((needle) => needle.includes(next))) return false; + } + return true; +} + +// Also collects the program's syntax and, optionally, its local declarations. +function mayUseStyleX( + program, + options, + syntax, + declarations = null, + visit = null, +) { + const sources = importSources(options); + const sx = sxPropName(options); + let relevant = false; + t.traverseFast(program, (node) => { + syntaxOf(node, syntax); + if (declarations != null) indexDeclaration(node, declarations); + if (visit != null) visit(node); + if (relevant) return; + if ( + node.type === 'ImportDeclaration' && + node.importKind !== 'type' && + node.importKind !== 'typeof' && + sources.has(node.source.value) + ) + relevant = true; + if (isRequireOf(node, sources)) relevant = true; + if ( + sx != null && + node.type === 'JSXOpeningElement' && + node.name.type === 'JSXIdentifier' && + node.name.name[0] === node.name.name[0].toLowerCase() && + node.attributes.some( + (attr) => + attr.type === 'JSXAttribute' && + attr.name.type === 'JSXIdentifier' && + attr.name.name === sx, + ) + ) + relevant = true; + }); + return relevant; +} + +function indexDeclaration(node, declarations) { + let names = null; + if (node.type === 'VariableDeclarator') + names = + node.id.type === 'Identifier' + ? [node.id.name] + : Object.keys(t.getBindingIdentifiers(node.id)); + else if ( + (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') && + node.id != null + ) + names = [node.id.name]; + if (names == null) return; + for (const name of names) { + const list = declarations.get(name); + if (list == null) declarations.set(name, [node]); + else list.push(node); + } +} + +// Local declarations a subtree may transitively read, ignoring shadowing. +function dependencies(root, declarations) { + const found = new Set(); + const seen = new Set(); + const pending = [root]; + while (pending.length > 0) { + t.traverseFast(pending.pop(), (node) => { + if (node.type !== 'Identifier' || seen.has(node.name)) return; + seen.add(node.name); + for (const declaration of declarations.get(node.name) || []) + if (!found.has(declaration)) { + found.add(declaration); + pending.push(declaration); + } + }); + } + found.delete(root); + return [...found]; +} + +// Local names through which top-level imports and requires reach StyleX. +// `namespaces` hold the module object, `named` map a local to its export. +function stylexNames(program, sources) { + const namespaces = new Set(); + const named = new Map(); + for (const statement of program.body) { + if ( + statement.type === 'ImportDeclaration' && + statement.importKind !== 'type' && + statement.importKind !== 'typeof' && + sources.has(statement.source.value) + ) { + const as = sources.get(statement.source.value); + for (const specifier of statement.specifiers) { + if (specifier.type === 'ImportSpecifier') { + if (specifier.importKind === 'type') continue; + const imported = + specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value; + if (as != null && imported === as) + namespaces.add(specifier.local.name); + else named.set(specifier.local.name, imported); + } else namespaces.add(specifier.local.name); + } + } + if (statement.type === 'VariableDeclaration') { + for (const declarator of statement.declarations) { + let init = declarator.init; + // `_interopRequireWildcard(require(...))` and similar wrappers. + while ( + init != null && + init.type === 'CallExpression' && + !isRequireOf(init, sources) && + init.arguments.length > 0 + ) + init = init.arguments[0]; + if (!isRequireOf(init, sources)) continue; + if (declarator.id.type === 'Identifier') + namespaces.add(declarator.id.name); + else if (declarator.id.type === 'ObjectPattern') { + for (const property of declarator.id.properties) { + if ( + property.type === 'ObjectProperty' && + property.key.type === 'Identifier' && + property.value.type === 'Identifier' + ) + named.set(property.value.name, property.key.name); + } + } + } + } + } + return { + namespaces, + named, + empty: namespaces.size === 0 && named.size === 0, + }; +} + +// The StyleX API a call uses (`create`, `props`, `default` for `stylex(...)`), +// or null for any other call. +function stylexCall(node, names) { + let callee = node.callee; + if (callee.type === 'SequenceExpression') + callee = callee.expressions[callee.expressions.length - 1]; + if (callee.type === 'Identifier') { + if (names.named.has(callee.name)) return names.named.get(callee.name); + return names.namespaces.has(callee.name) ? 'default' : null; + } + let property = null; + while (callee.type === 'MemberExpression') { + property = callee.property; + callee = callee.object; + } + if (callee.type !== 'Identifier' || property == null) return null; + const name = + property.type === 'Identifier' + ? property.name + : property.type === 'StringLiteral' + ? property.value + : ''; + if (names.namespaces.has(callee.name)) return name; + return names.named.has(callee.name) ? names.named.get(callee.name) : null; +} + +function sxAttribute(node, sx) { + return ( + sx != null && + node.type === 'JSXOpeningElement' && + node.name.type === 'JSXIdentifier' && + node.name.name[0] === node.name.name[0].toLowerCase() && + node.attributes.some( + (attr) => + attr.type === 'JSXAttribute' && + attr.name.type === 'JSXIdentifier' && + attr.name.name === sx, + ) + ); +} + +// --------------------------------------------------------------------------- +// Structural equality of ASTs, ignoring positions, comments and parser extras. +// A hash match is always confirmed structurally. + +const IGNORED_FIELDS = new Set([ + 'type', + 'loc', + 'start', + 'end', + 'range', + 'extra', + 'leadingComments', + 'trailingComments', + 'innerComments', + 'comments', + 'tokens', + 'errors', + 'sourceFile', + 'parent', +]); + +function isNode(value) { + return ( + value != null && + typeof value === 'object' && + typeof value.type === 'string' && + !Array.isArray(value) + ); +} + +const typeFields = new Map(); +function fieldsOf(node) { + let names = typeFields.get(node.type); + if (names !== undefined) return names; + const fields = t.NODE_FIELDS[node.type]; + if (fields == null) + // A node type Babel does not define: compare its own data fields. + return Object.keys(node).filter( + (key) => key[0] !== '_' && !IGNORED_FIELDS.has(key), + ); + names = Object.keys(fields).filter((key) => !IGNORED_FIELDS.has(key)); + typeFields.set(node.type, names); + return names; +} + +// Scalars that describe the same program compare equal. +function absent(value) { + return ( + value == null || + value === false || + (Array.isArray(value) && value.length === 0) + ); +} + +function sameScalar(a, b) { + if (absent(a) && absent(b)) return true; + if (a === b) return true; + if (typeof a === 'object' && typeof b === 'object' && a && b) + return JSON.stringify(a) === JSON.stringify(b); + return Number.isNaN(a) && Number.isNaN(b); +} + +function mix(hash, value) { + return Math.imul(hash ^ value, 0x01000193) >>> 0; +} + +// Strings hash by an interned id, which V8 handles far faster than mixing +// characters. Ids are only stable within one `Shapes` table. +function hashString(hash, text, strings) { + let id = strings.get(text); + if (id === undefined) { + id = strings.size + 1; + strings.set(text, id); + } + return mix(hash, id); +} + +const SCALAR_TAGS = { string: 1, number: 2, boolean: 3, bigint: 4 }; + +function hashScalar(hash, value, strings) { + if (absent(value)) return hash; + if (typeof value === 'object') + return hashString(mix(hash, 6), JSON.stringify(value), strings); + return hashString( + mix(hash, SCALAR_TAGS[typeof value] || 5), + String(value), + strings, + ); +} + +function commentsEqual(a, b) { + for (const field of COMMENT_FIELDS) { + const left = a[field] || []; + const right = b[field] || []; + if (left.length !== right.length) return false; + for (let index = 0; index < left.length; index++) + if ( + left[index].type !== right[index].type || + left[index].value !== right[index].value + ) + return false; + } + return true; +} + +// Exact structural equality; with `comments`, attached comment text too. +// Nodes of `b` in `opaque` only need a counterpart of the same type. +function equalTrees(a, b, comments, opaque = null) { + if (a === b) return true; + if (opaque != null && isNode(b) && opaque.has(b)) + return isNode(a) && a.type === b.type; + if (!isNode(a) || !isNode(b)) return sameScalar(a, b); + if (a.type !== b.type) return false; + if (comments && !commentsEqual(a, b)) return false; + for (const field of fieldsOf(a)) { + const left = a[field]; + const right = b[field]; + if (Array.isArray(left) || Array.isArray(right)) { + const l = left || []; + const r = right || []; + if (l.length !== r.length) return false; + for (let index = 0; index < l.length; index++) + if (!equalTrees(l[index], r[index], comments, opaque)) return false; + } else if (isNode(left) || isNode(right)) { + if (!equalTrees(left, right, comments, opaque)) return false; + } else if (!sameScalar(left, right)) return false; + } + return true; +} + +// Compact exact records of a subtree's structure, as `equalTrees` compares it. +// Scalars are a numeric tag then the value, so they never align with a type. +const ABSENT = 0; +const SCALAR = 1; +const OBJECT = 2; +// Matches any subtree: used for definitions StyleX replaces at their visit. +const OPAQUE = 3; + +function shapeRecord(node, record = [], opaque = null) { + if (!isNode(node)) { + recordScalar(node, record); + return record; + } + if (opaque != null && opaque.has(node)) { + record.push(OPAQUE); + return record; + } + record.push(node.type); + for (const field of fieldsOf(node)) { + const value = node[field]; + if (Array.isArray(value)) { + record.push(value.length); + for (const item of value) shapeRecord(item, record, opaque); + } else if (isNode(value)) shapeRecord(value, record, opaque); + else recordScalar(value, record); + } + return record; +} + +// Scalars that describe the same program record the same (see `sameScalar`). +function recordScalar(value, record) { + if (absent(value)) record.push(ABSENT); + else if (typeof value === 'object') + record.push(OBJECT, JSON.stringify(value)); + else record.push(SCALAR, value); +} + +// Whether another plugin may edit a definition before StyleX visits it. +function othersMayEditDefinitions(state) { + const opts = state.file.opts; + const passes = [ + opts.plugins || [], + ...(opts.presets || []).map((p) => p.plugins), + ]; + const pass = passes.find((plugins) => + plugins.some((plugin) => plugin.key === state.key), + ); + if (pass == null) return true; + return pass.some((plugin) => { + if (plugin.key === state.key) return false; + const visitor = plugin.visitor || {}; + return Object.keys(visitor).some( + (key) => + key !== '_exploded' && + key !== '_verified' && + (key !== 'Program' || visitor.Program.enter != null), + ); + }); +} + +// The plugins of the pass StyleX runs in, and StyleX's place in it. +function passOf(state) { + const opts = state.file.opts; + const passes = [ + opts.plugins || [], + ...(opts.presets || []).map((p) => p.plugins), + ]; + const index = passes.findIndex((plugins) => + plugins.some((plugin) => plugin.key === state.key), + ); + return index < 0 ? null : { first: index === 0, plugins: passes[index] }; +} + +const visitorKeys = (plugin) => + Object.keys(plugin.visitor || {}).filter( + (key) => key !== '_exploded' && key !== '_verified', + ); + +// Whether no other plugin of the pass has a visitor or a `pre` hook. +function aloneInPass(state) { + // A visitor wrapper runs code around every visitor, StyleX's included. + if (state.file.opts.wrapPluginVisitorMethod != null) return false; + const pass = passOf(state); + return ( + pass != null && + pass.plugins.every( + (plugin) => + plugin.key === state.key || + (plugin.pre == null && visitorKeys(plugin).length === 0), + ) + ); +} + +// Prototype of Babel-parsed nodes (Babel clones input ASTs to plain objects). +const PARSED = Object.getPrototypeOf(babelParser.parse('')); + +// Whether the program is still Babel's untouched first-pass parse of the file. +function parsedAndUntouched(state, program) { + const opts = state.file.opts; + // With `cloneInputAst` off, plugins receive the caller's own nodes. + if (opts.cloneInputAst !== true) return false; + // A visitor wrapper can change the program before StyleX's Program.enter. + if (opts.wrapPluginVisitorMethod != null) return false; + if ( + state.file.ast.program !== program.node || + Object.getPrototypeOf(state.file.ast) !== PARSED || + Object.getPrototypeOf(program.node) !== PARSED + ) + return false; + // A parser override in any pass parses instead of Babel. + const passes = [ + opts.plugins || [], + ...(opts.presets || []).map((preset) => preset.plugins), + ]; + if ( + passes.some((plugins) => + plugins.some((plugin) => typeof plugin.parserOverride === 'function'), + ) + ) + return false; + const pass = passOf(state); + if (pass == null || !pass.first) return false; + let before = true; + for (const plugin of pass.plugins) { + if (plugin.key === state.key) { + before = false; + continue; + } + if (plugin.pre != null) return false; + const visitor = plugin.visitor || {}; + if (before && (visitor.enter != null || visitor.Program?.enter != null)) + return false; + } + return true; +} + +// Parser options under which JSX parses without the `jsx` plugin. +function impliesJsx(parserOpts) { + return (parserOpts?.plugins || []).some( + (plugin) => + Array.isArray(plugin) && + plugin[0] === 'typescript' && + plugin[1]?.isTSX === true, + ); +} + +function matchesShapeRecord(node, record) { + let index = 0; + const scalar = (value) => { + const tag = record[index++]; + if (absent(value)) return tag === ABSENT; + if (typeof value === 'object') + return tag === OBJECT && record[index++] === JSON.stringify(value); + if (tag !== SCALAR) return false; + const recorded = record[index++]; + return ( + recorded === value || + (typeof value === 'number' && + Number.isNaN(value) && + Number.isNaN(recorded)) + ); + }; + const walk = (current) => { + if (record[index] === OPAQUE) { + index++; + return true; + } + if (!isNode(current)) return scalar(current); + if (record[index++] !== current.type) return false; + for (const field of fieldsOf(current)) { + const value = current[field]; + if (Array.isArray(value)) { + if (record[index++] !== value.length) return false; + for (const item of value) if (!walk(item)) return false; + } else if (isNode(value)) { + if (!walk(value)) return false; + } else if (!scalar(value)) return false; + } + return true; + }; + return walk(node) && index === record.length; +} + +class Shapes { + constructor() { + this.hashes = new WeakMap(); + this.strings = new Map(); + } + + hash(node) { + let hash = this.hashes.get(node); + if (hash !== undefined) return hash; + const strings = this.strings; + hash = hashString(0x811c9dc5, node.type, strings); + const fields = fieldsOf(node); + for (let position = 0; position < fields.length; position++) { + const value = node[fields[position]]; + if (absent(value)) continue; + hash = mix(hash, position + 1); + if (Array.isArray(value)) { + hash = mix(hash, value.length); + for (const item of value) + hash = isNode(item) + ? mix(hash, this.hash(item)) + : hashScalar(hash, item, strings); + } else if (isNode(value)) hash = mix(hash, this.hash(value)); + else hash = hashScalar(hash, value, strings); + } + this.hashes.set(node, hash); + return hash; + } + + itemKey(item) { + return isNode(item) ? this.hash(item) : hashScalar(1, item, this.strings); + } + + // A key from the node's type and own scalars only; equal subtrees share it. + shallowKey(item) { + if (!isNode(item)) return hashScalar(1, item, this.strings); + const strings = this.strings; + let hash = hashString(0x811c9dc5, item.type, strings); + const fields = fieldsOf(item); + for (let position = 0; position < fields.length; position++) { + const value = item[fields[position]]; + if (absent(value) || Array.isArray(value) || isNode(value)) continue; + hash = hashScalar(mix(hash, position + 1), value, strings); + } + return hash; + } + + // A key equal for equal subtrees from the first `depth` levels only. + boundedKey(item, depth) { + if (!isNode(item)) return hashScalar(1, item, this.strings); + const strings = this.strings; + let hash = hashString(0x811c9dc5, item.type, strings); + const fields = fieldsOf(item); + for (let position = 0; position < fields.length; position++) { + const value = item[fields[position]]; + if (absent(value)) continue; + hash = mix(hash, position + 1); + if (Array.isArray(value)) { + hash = mix(hash, value.length); + if (depth > 1) + for (const child of value) + hash = mix(hash, this.boundedKey(child, depth - 1)); + } else if (isNode(value)) { + if (depth > 1) hash = mix(hash, this.boundedKey(value, depth - 1)); + } else hash = hashScalar(hash, value, strings); + } + return hash; + } + + // Full keys only where the other list may hold an equal item. + listKeys(before, after) { + const shallowBefore = before.map((item) => this.boundedKey(item, 4)); + const shallowAfter = after.map((item) => this.boundedKey(item, 4)); + const inAfter = new Set(shallowAfter); + const inBefore = new Set(shallowBefore); + return [ + before.map((item, index) => + inAfter.has(shallowBefore[index]) ? this.itemKey(item) : `b${index}`, + ), + after.map((item, index) => + inBefore.has(shallowAfter[index]) ? this.itemKey(item) : `a${index}`, + ), + ]; + } + + // Exact equality, confirmed structurally after a hash match. + same(a, b) { + if (a === b) return true; + if (!isNode(a) || !isNode(b)) return sameScalar(a, b); + return this.hash(a) === this.hash(b) && equalTrees(a, b, false); + } + + // Only the nodes' own non-node fields. + sameScalars(a, b) { + for (const field of new Set([...fieldsOf(a), ...fieldsOf(b)])) { + const left = a[field]; + const right = b[field]; + if ( + Array.isArray(left) || + isNode(left) || + Array.isArray(right) || + isNode(right) + ) + continue; + if (!sameScalar(left, right)) return false; + } + return true; + } +} + +// --------------------------------------------------------------------------- +// Grafting a compiler result onto the live AST. + +// The outer bindings a declaration statement declares, as one key, or null. +const DECLARATIONS = new Set([ + 'VariableDeclaration', + 'FunctionDeclaration', + 'ClassDeclaration', + 'ExportNamedDeclaration', + 'ExportDefaultDeclaration', + 'VariableDeclarator', +]); +function declaredKey(node) { + if (!isNode(node) || !DECLARATIONS.has(node.type)) return null; + // Common shapes: a single named declarator, function or class, or its export. + const declaration = + node.type === 'ExportNamedDeclaration' ? node.declaration : node; + if (declaration != null) { + if ( + declaration.type === 'VariableDeclaration' && + declaration.declarations.length === 1 && + declaration.declarations[0].id.type === 'Identifier' + ) + return declaration.declarations[0].id.name; + if ( + declaration.type === 'VariableDeclarator' && + declaration.id.type === 'Identifier' + ) + return declaration.id.name; + if ( + (declaration.type === 'FunctionDeclaration' || + declaration.type === 'ClassDeclaration') && + declaration.id != null + ) + return declaration.id.name; + } + const names = Object.keys(t.getBindingIdentifiers(node, false, true)); + return names.length === 0 ? null : names.join(','); +} + +class Graft { + constructor(shapes, record, defer = null, hold = null) { + this.shapes = shapes; + this.record = record; + // Items the compile dropped that must stay until the traversal ends. + this.defer = defer; + this.deferred = []; + // Nodes kept in place until visited; their replacements wait in `held`. + this.hold = hold; + this.held = []; + this.edits = []; + // Whether the live tree changed at all (held replacements wait outside). + this.changed = false; + this.adopted = new Set(); + // [replaced node, replacement] pairs. + this.replaced = []; + // Unmatched list runs, as { removed, added } (with `record`). + this.regions = []; + // The list each deferred item stays in. + this.deferredIn = new Map(); + // Commented items removed outright, with their neighbours in the result. + this.removed = []; + } + + adopt(node, previous = null) { + this.adopted.add(node); + if (isNode(previous)) this.replaced.push([previous, node]); + return node; + } + + node(previous, next) { + if (previous === next) return previous; + if (!isNode(previous) || !isNode(next) || previous.type !== next.type) + return isNode(next) ? this.replace(previous, next) : next; + if (!this.shapes.sameScalars(previous, next)) + return this.replace(previous, next); + // Compare children directly, so an edited subtree is never hashed whole. + this.children(previous, next); + return previous; + } + + replace(previous, next) { + this.adopt(next, previous); + if (isNode(previous) && this.hold != null && this.hold(previous)) { + this.held.push([previous, next]); + return previous; + } + return next; + } + + children(previous, next) { + const fields = new Set([...fieldsOf(previous), ...fieldsOf(next)]); + for (const field of fields) { + const before = previous[field]; + const after = next[field]; + if (Array.isArray(before) || Array.isArray(after)) { + this.list(previous, field, before || [], after || []); + } else if (isNode(before) && isNode(after)) { + const result = this.node(before, after); + if (result !== before) this.set(previous, field, result); + } else if (isNode(before) || isNode(after)) { + this.set( + previous, + field, + isNode(after) ? this.adopt(after, before) : after, + ); + } + } + } + + set(parent, field, value) { + this.changed = true; + if (this.record) + this.edits.push({ parent, field, before: parent[field], after: value }); + parent[field] = value; + } + + list(parent, field, before, after) { + const shapes = this.shapes; + const pairs = matchItems(before, after, shapes); + const result = []; + let declared = null; + const removed = []; + const drop = (item) => { + if (this.defer == null || !isNode(item) || !this.defer(item)) { + if (isNode(item) && (item.leadingComments || item.trailingComments)) + removed.push({ item, at: result.length }); + return; + } + // A declaration re-emitted elsewhere in this list was rewritten, not removed. + if (declared == null) { + declared = new Map(); + // Outer bindings only: `function C(s)` does not declare the program's `s`. + for (const other of after) + if (isNode(other)) + for (const name of Object.keys( + t.getBindingIdentifiers(other, false, true), + )) + if (!declared.has(name)) declared.set(name, other); + } + const rewritten = Object.keys(t.getBindingIdentifiers(item, false, true)) + .map((name) => declared.get(name)) + .find((other) => other != null); + if (rewritten != null) { + this.replaced.push([item, rewritten]); + return; + } + this.deferred.push(item); + this.deferredIn.set(item, { parent, field }); + result.push(item); + }; + let i = 0; + let j = 0; + const gap = (iEnd, jEnd) => { + // With `record`, a gap's dropped and adopted items form one region. + const region = this.record ? { removed: [], added: [] } : null; + const adopted = (next) => { + if (region != null && isNode(next)) region.added.push(next); + return isNode(next) ? this.adopt(next) : next; + }; + const dropped = (item) => { + if (region != null && isNode(item)) region.removed.push(item); + drop(item); + }; + // Unmatched runs: pair same-typed items in order and graft within them. + const ahead = new Map(); + for (let k = j; k < jEnd; k++) { + const type = isNode(after[k]) ? after[k].type : null; + ahead.set(type, (ahead.get(type) || 0) + 1); + } + // Declared names of the items ahead in the run, counted on first need. + let aheadKeys = null; + const aheadOf = (key) => { + if (aheadKeys == null) { + aheadKeys = new Map(); + for (let k = j; k < jEnd; k++) { + const other = declaredKey(after[k]); + if (other != null) + aheadKeys.set(other, (aheadKeys.get(other) || 0) + 1); + } + } + return aheadKeys.get(key) || 0; + }; + const passed = (next) => { + const type = isNode(next) ? next.type : null; + ahead.set(type, ahead.get(type) - 1); + if (aheadKeys != null) { + const key = declaredKey(next); + if (key != null) aheadKeys.set(key, aheadKeys.get(key) - 1); + } + }; + // Declarations of different bindings never pair. + let remaining = null; + const remainingOf = (key) => { + if (remaining == null) { + remaining = new Map(); + for (let k = i; k < iEnd; k++) { + const other = declaredKey(before[k]); + if (other != null) + remaining.set(other, (remaining.get(other) || 0) + 1); + } + } + return remaining.get(key) || 0; + }; + // An item leaving the run: it no longer counts as remaining. + const left = (item) => { + if (remaining != null) { + const key = declaredKey(item); + if (key != null) remaining.set(key, remaining.get(key) - 1); + } + return item; + }; + while (i < iEnd && j < jEnd) { + const previous = before[i]; + const next = after[j]; + const sameType = + isNode(previous) && isNode(next) && previous.type === next.type; + const previousKey = declaredKey(previous); + const nextKey = sameType ? declaredKey(next) : null; + if (previousKey != null && aheadOf(previousKey) === 0) { + // No compiled declaration of these bindings ahead: removed. + dropped(left(before[i++])); + } else if (sameType && previousKey !== nextKey) { + // Gone if the compiled declaration's source is later in the run; else new. + if ( + nextKey != null && + previousKey == null && + remainingOf(nextKey) > 0 + ) + dropped(left(before[i++])); + else { + passed(after[j++]); + result.push(adopted(next)); + } + } else if (sameType) { + passed(after[j++]); + result.push(this.node(left(previous), next)); + i++; + } else if (isNode(previous) && ahead.get(previous.type) > 0) { + passed(after[j++]); + result.push(adopted(next)); + } else if ( + isNode(previous) && + isNode(next) && + this.hold != null && + this.hold(previous) + ) { + // A definition replaced in place stays until visited, like any other. + passed(after[j++]); + result.push(this.replace(left(previous), next)); + i++; + } else dropped(left(before[i++])); + } + for (; j < jEnd; j++) result.push(adopted(after[j])); + while (i < iEnd) dropped(left(before[i++])); + if ( + region != null && + region.removed.length > 0 && + region.added.length > 0 + ) + this.regions.push(region); + }; + for (const [bi, ai] of pairs) { + gap(bi, ai); + result.push(before[bi]); + i = bi + 1; + j = ai + 1; + } + gap(before.length, after.length); + for (const { item, at } of removed) + this.removed.push({ + item, + previous: isNode(result[at - 1]) ? result[at - 1] : null, + next: isNode(result[at]) ? result[at] : null, + }); + const unchanged = + result.length === before.length && + result.every((item, index) => item === before[index]); + if (unchanged) return; + this.changed = true; + if (this.record) + this.edits.push({ + parent, + field, + list: true, + before: before.slice(), + after: result.slice(), + }); + if (Array.isArray(parent[field])) + parent[field].splice(0, parent[field].length, ...result); + else parent[field] = result; + } +} + +// Grafts a compiled program onto a live Babel program in place, replacing only +// changed subtrees. `adopted` gets the replacements; `edits` feed `revert`. +function graftProgram( + program, + compiled, + { record = false, defer = null, hold = null } = {}, +) { + const graft = new Graft(new Shapes(), record, defer, hold); + graft.children(program, compiled); + return { + adopted: graft.adopted, + edits: graft.edits, + replaced: graft.replaced, + deferred: graft.deferred, + held: graft.held, + regions: graft.regions, + removed: graft.removed, + deferredIn: graft.deferredIn, + changed: graft.changed, + }; +} + +// Index pairs of equal items in two lists, in order. +function matchItems(before, after, shapes) { + const pairs = []; + let start = 0; + while ( + start < before.length && + start < after.length && + equalTrees(before[start], after[start], false) + ) { + pairs.push([start, start]); + start++; + } + let endBefore = before.length; + let endAfter = after.length; + const suffix = []; + while ( + endBefore > start && + endAfter > start && + equalTrees(before[endBefore - 1], after[endAfter - 1], false) + ) { + endBefore--; + endAfter--; + suffix.push([endBefore, endAfter]); + } + const n = endBefore - start; + const m = endAfter - start; + let aligned; + if (n > 0 && m > 0 && n * m <= 64) { + // A short changed run: compare items directly instead of hashing subtrees. + const shallow = (item) => shapes.shallowKey(item); + const keysAfter = after.slice(start, endAfter).map(shallow); + const equal = []; + for (let x = 0; x < n; x++) { + const key = shallow(before[start + x]); + equal.push( + keysAfter.map( + (other, y) => + other === key && + equalTrees(before[start + x], after[start + y], false), + ), + ); + } + const table = new Uint32Array((n + 1) * (m + 1)); + for (let x = n - 1; x >= 0; x--) + for (let y = m - 1; y >= 0; y--) + table[x * (m + 1) + y] = equal[x][y] + ? table[(x + 1) * (m + 1) + y + 1] + 1 + : Math.max(table[(x + 1) * (m + 1) + y], table[x * (m + 1) + y + 1]); + let x = 0; + let y = 0; + while (x < n && y < m) { + if (equal[x][y]) { + pairs.push([start + x, start + y]); + x++; + y++; + } else if (table[(x + 1) * (m + 1) + y] >= table[x * (m + 1) + y + 1]) + x++; + else y++; + } + } else if ( + n > 0 && + n === m && + (aligned = positional(before, after, start, n, shapes)) != null + ) { + // Replaced in place: the aligned pairs are the whole common subsequence. + for (const x of aligned) pairs.push([x, x]); + } else if (n > 0 && m > 0) { + const middleBefore = before.slice(start, endBefore); + const middleAfter = after.slice(start, endAfter); + const [keysBefore, keysAfter] = shapes.listKeys(middleBefore, middleAfter); + for (const [x, y] of matchLists(keysBefore, keysAfter)) + if (shapes.same(middleBefore[x], middleAfter[y])) + pairs.push([start + x, start + y]); + } + return pairs.concat(suffix.reverse()); +} + +function equalItem(a, b, shapes) { + return ( + shapes.shallowKey(a) === shapes.shallowKey(b) && equalTrees(a, b, false) + ); +} + +// Equal positions of two same-length runs if that matching is optimal, or null. +function positional(before, after, start, length, shapes) { + const equal = []; + const unmatchedBefore = new Set(); + const unmatchedAfter = []; + for (let x = start; x < start + length; x++) { + if (equalItem(before[x], after[x], shapes)) equal.push(x); + else { + unmatchedBefore.add(shapes.shallowKey(before[x])); + unmatchedAfter.push(shapes.shallowKey(after[x])); + } + } + return unmatchedAfter.some((key) => unmatchedBefore.has(key)) ? null : equal; +} + +// Longest common subsequence of two key lists, as index pairs. +function matchLists(a, b) { + const pairs = []; + let start = 0; + while (start < a.length && start < b.length && a[start] === b[start]) { + pairs.push([start, start]); + start++; + } + let endA = a.length; + let endB = b.length; + const suffix = []; + while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { + endA--; + endB--; + suffix.push([endA, endB]); + } + const n = endA - start; + const m = endB - start; + if (n > 0 && m > 0) { + if (n * m <= 1 << 20) { + const table = new Uint32Array((n + 1) * (m + 1)); + for (let x = n - 1; x >= 0; x--) + for (let y = m - 1; y >= 0; y--) + table[x * (m + 1) + y] = + a[start + x] === b[start + y] + ? table[(x + 1) * (m + 1) + y + 1] + 1 + : Math.max( + table[(x + 1) * (m + 1) + y], + table[x * (m + 1) + y + 1], + ); + let x = 0; + let y = 0; + while (x < n && y < m) { + if (a[start + x] === b[start + y]) { + pairs.push([start + x, start + y]); + x++; + y++; + } else if (table[(x + 1) * (m + 1) + y] >= table[x * (m + 1) + y + 1]) + x++; + else y++; + } + } else { + // Very long changed runs: match each key to its next occurrence. + const positions = new Map(); + for (let y = start; y < endB; y++) { + if (!positions.has(b[y])) positions.set(b[y], []); + positions.get(b[y]).push(y); + } + const next = new Map(); + let last = start - 1; + for (let x = start; x < endA; x++) { + const candidates = positions.get(a[x]); + if (candidates == null) continue; + let cursor = next.get(a[x]) || 0; + while (cursor < candidates.length && candidates[cursor] <= last) + cursor++; + if (cursor < candidates.length) { + last = candidates[cursor++]; + pairs.push([x, last]); + } + next.set(a[x], cursor); + } + } + } + return pairs.concat(suffix.reverse()); +} + +// Undoes recorded edits that are still in place, newest first. +// Items other plugins added since keep their relative positions. +function revert(edits) { + const restored = new Set(); + for (let index = edits.length - 1; index >= 0; index--) { + const edit = edits[index]; + const { parent, field } = edit; + if (!edit.list) { + if (parent[field] === edit.after) { + parent[field] = edit.before; + if (isNode(edit.before)) restored.add(edit.before); + } + continue; + } + const current = parent[field]; + if (!Array.isArray(current)) continue; + const kept = new Set(edit.before); + const ours = new Set(edit.after); + const inserted = new Set(edit.after.filter((item) => !kept.has(item))); + const present = new Set(current); + // Only items this graft removed come back; later plugins' removals stand. + const removed = edit.before.filter( + (item) => !present.has(item) && !ours.has(item), + ); + const order = new Map( + edit.before.map((item, position) => [item, position]), + ); + const result = []; + let next = 0; + const flush = (limit) => { + while (next < removed.length && order.get(removed[next]) < limit) { + result.push(removed[next]); + if (isNode(removed[next])) restored.add(removed[next]); + next++; + } + }; + for (const item of current) { + if (inserted.has(item)) continue; + if (order.has(item)) flush(order.get(item)); + result.push(item); + } + flush(Infinity); + current.splice(0, current.length, ...result); + } + return restored; +} + +// --------------------------------------------------------------------------- +// Locations of grafted nodes, traced to the Babel input file. + +function lineStarts(source) { + const starts = [0]; + const breaks = /\r\n|\n|\r|\u2028|\u2029/g; + for (let match; (match = breaks.exec(source)); ) + starts.push(match.index + match[0].length); + return starts; +} + +function locator(nativeMap, priorMap, source) { + const native = new TraceMap(nativeMap); + const prior = priorMap == null ? null : new TraceMap(priorMap); + const starts = lineStarts(source); + function trace(position) { + let result = originalPositionFor(native, { + line: position.line, + column: position.column, + }); + if (result.line == null) return null; + if (prior != null) + result = originalPositionFor(prior, { + line: result.line, + column: result.column, + }); + if (result.line == null || starts[result.line - 1] == null) return null; + return { + line: result.line, + column: result.column, + index: starts[result.line - 1] + result.column, + }; + } + return function remap(node) { + if (node.loc == null) return; + const start = trace(node.loc.start); + const end = trace(node.loc.end); + if (start == null || end == null || end.index < start.index) { + node.loc = null; + delete node.start; + delete node.end; + return; + } + // No filename: the generator then uses its sourceFileName as the source. + node.loc = { start, end, identifierName: node.loc.identifierName }; + node.start = start.index; + node.end = end.index; + }; +} + +const COMMENT_FIELDS = ['leadingComments', 'innerComments', 'trailingComments']; + +// Remaps grafted subtrees and drops comment copies unchanged nodes still carry. +// Returns every attached comment, for `file.ast.comments`. +function settle(program, adopted, remap, visit, previousOf) { + const kept = new Set(); + const keptStarts = new Set(); + const walk = (node) => { + if (adopted.has(node)) return; + if (visit != null) visit(node); + for (const field of COMMENT_FIELDS) { + const comments = node[field]; + if (comments != null) + for (const comment of comments) { + kept.add(comment); + if (comment.start != null) keptStarts.add(comment.start); + } + } + for (const field of fieldsOf(node)) { + const value = node[field]; + if (Array.isArray(value)) { + for (const item of value) if (isNode(item)) walk(item); + } else if (isNode(value)) walk(value); + } + }; + walk(program); + const all = new Set(kept); + for (const root of adopted) { + // Span the replaced node, since a traced end can collapse onto the start. + const previous = previousOf.get(root); + const spans = previous?.loc != null && previous.start != null; + if (spans) { + root.loc = { + start: previous.loc.start, + end: previous.loc.end, + identifierName: root.loc?.identifierName, + }; + root.start = previous.start; + root.end = previous.end; + } + t.traverseFast(root, (node) => { + if (visit != null) visit(node); + if (!spans || node !== root) remap(node); + for (const field of COMMENT_FIELDS) { + if (node[field] == null) continue; + const comments = []; + for (const comment of node[field]) { + if (!all.has(comment) && !kept.has(comment)) { + remap(comment); + if (comment.start != null && keptStarts.has(comment.start)) + continue; + all.add(comment); + } + if (all.has(comment)) comments.push(comment); + } + if (comments.length > 0) node[field] = comments; + else delete node[field]; + } + }); + } + return [...all].sort((x, y) => (x.start ?? Infinity) - (y.start ?? Infinity)); +} + +// --------------------------------------------------------------------------- +// Compilation. + +// Diagnostics `compile` raised, so callers needn't inspect user-thrown values. +const compileDiagnostics = new WeakSet(); + +function compile(input, state, filename) { + // Babel wraps an escaping exception once, so user exceptions stay unwrapped. + const errors = { userThrew: false }; + try { + return compiler.transformSync(input.code, { + ...state.opts, + filename, + __sourceFileName: state.file.opts.filename || '', + ...(state.file.opts.sourceType == null + ? {} + : { __sourceType: state.file.opts.sourceType }), + sourceMaps: true, + __errorsHandledByBabel: errors, + }); + } catch (error) { + // A user exception reaches Babel untouched: the same value, uninspected. + if (errors.userThrew) throw error; + const diagnostic = diagnosticFromError(error); + if (diagnostic != null) { + const wrapped = diagnosticError(diagnostic, input, state); + wrapped.stylexDiagnostic = diagnostic; + compileDiagnostics.add(wrapped); + throw wrapped; + } + throw error; + } +} + +function generated(program, state, filename) { + // `retainLines` keeps input lines for the debug data's `file:line`. + const output = generate( + program, + { + comments: true, + retainLines: true, + sourceMaps: true, + sourceFileName: filename, + }, + state.file.code, + ); + return { code: output.code, map: output.map }; +} + +// Applies a compiler result to the program. Returns the grafted nodes. +function apply( + program, + state, + input, + result, + record, + defer = null, + hold = null, +) { + if (result.map == null) + throw new Error( + 'The native StyleX compiler did not provide a source map for changed code', + ); + // The output keeps its input's syntax; the entry compile knows it from its walk. + const parsed = babelParser.parse( + result.code, + grammarOptions(state, record ? state.stylexFacade.syntax : null), + ); + if (parsed == null) + throw new Error( + 'The native StyleX compiler produced no JavaScript program', + ); + const graft = graftProgram(program.node, parsed.program, { + record, + defer, + hold, + }); + const remap = locator(result.map, input.map, state.file.code); + const s = state.stylexFacade; + s.names = stylexNames(program.node, s.sources); + // With StyleX alone in its pass, no StyleX node reaches the traversal late. + const everything = record && s.alone; + const known = everything ? EVERY_NODE : new WeakSet(); + state.file.ast.comments = settle( + program.node, + graft.adopted, + remap, + everything + ? null + : (node) => { + if (styleXNode(node, s)) known.add(node); + }, + new Map(graft.replaced.map(([previous, node]) => [node, previous])), + ); + dropCommentCopies(program.node.body, graft.adopted); + // Removals share comments with siblings, like Babel's `path.remove()`. + if (!record && graft.removed.length > 0) { + const all = new Set(state.file.ast.comments); + for (const { item, previous, next } of graft.removed) { + shareComments(item, previous, next); + for (const field of COMMENT_FIELDS) + for (const comment of item[field] || []) all.add(comment); + } + state.file.ast.comments = [...all].sort( + (x, y) => (x.start ?? Infinity) - (y.start ?? Infinity), + ); + } + s.known = known; + if (graft.changed) program.scope.crawl(); + return graft; +} + +function styleXNode(node, s) { + return ( + (node.type === 'CallExpression' && stylexCall(node, s.names) != null) || + sxAttribute(node, s.sx) + ); +} + +// The known set when no other plugin takes part in the traversal. +const EVERY_NODE = Object.freeze({ has: () => true, add() {} }); + +// StyleX already in the program; calls left for the runtime are not new work. +function knownStyleX(program, s) { + const known = new WeakSet(); + if (s.names.empty && s.sx == null) return known; + t.traverseFast(program, (node) => { + if (styleXNode(node, s)) known.add(node); + }); + return known; +} + +function enter(program, state) { + state.file.metadata.stylex = []; + const s = { + sources: importSources(state.opts), + sx: sxPropName(state.opts), + names: null, + known: new WeakSet(), + edits: [], + pending: [], + pendingNodes: new WeakSet(), + seenSx: new WeakSet(), + syntax: null, + sites: [], + holds: new Map(), + // Unused definitions' calls, with what they read as compiled. + unusedForms: new Map(), + // Every definition's recorded input, with shared records of what they read. + inputs: [], + dependencyShapes: new Map(), + // Compiled replacements of other StyleX (props, attrs), and the calls. + consumers: [], + // Those compiled from a definition found stale, with that definition. + staleConsumers: null, + // Program-level references, to their declarations (with records). + references: null, + sxElements: null, + // Consumers as written, entry-adopted nodes, and replacements by source. + consumerCalls: null, + adopted: null, + outputs: null, + // Entry output undone when the compile became stale. + restored: new Set(), + checkedProgram: false, + program: null, + // A provisionally compiled definition changed before its visit. + stale: false, + // Whether definitions are recorded to detect such changes. + recordShapes: false, + // StyleX is the only plugin of its pass that sees the program. + alone: false, + // The list each unused definition stays in until exit. + unusedLists: null, + // The program is Babel's untouched parse of the file's text. + pristine: false, + hoisted: [], + unused: [], + untouched: new WeakSet(), + deferred: null, + dirty: false, + rules: [], + warnings: [], + errors: [], + }; + state.stylexFacade = s; + s.names = stylexNames(program.node, s.sources); + // If others may edit definitions first, the entry compile stays provisional. + s.recordShapes = othersMayEditDefinitions(state); + s.alone = aloneInPass(state); + const declarations = s.recordShapes ? new Map() : null; + const syntax = { jsx: false, flow: false, typescript: false }; + // Consumers as written, to find output that read a definition found stale. + const consumerCalls = s.recordShapes ? [] : null; + // sx elements as written; the entry graft keeps them and renames the attribute. + const sxElements = []; + s.sxElements = sxElements; + const overrides = parserOverrides(state); + s.pristine = overrides.length === 0 && parsedAndUntouched(state, program); + // An untouched parse holds an sx attribute only where the text spells its name. + const sxPossible = + s.sx != null && !(s.pristine && !state.file.code.includes(s.sx)); + const visit = + consumerCalls == null + ? !sxPossible + ? null + : (node) => { + if (sxAttribute(node, s.sx)) sxElements.push(node); + } + : (node) => { + if (sxAttribute(node, s.sx)) sxElements.push(node); + const consumer = (call) => + call?.type === 'CallExpression' && + EXIT_CALLS.has(stylexCall(call, s.names)); + // A spread of props is compiled to attributes replacing the spread. + if ( + consumer(node) || + (node.type === 'JSXSpreadAttribute' && consumer(node.argument)) + ) + consumerCalls.push(node); + else if (sxAttribute(node, s.sx)) + for (const attribute of node.attributes) + if ( + attribute.type === 'JSXAttribute' && + attribute.name.type === 'JSXIdentifier' && + attribute.name.name === s.sx + ) + consumerCalls.push(attribute); + }; + // An untouched parse cannot hold StyleX the text cannot spell: skip the walk. + if (s.pristine && s.names.empty && cannotSpellStyleX(state.file.code, s)) + return; + // An untouched parse needs no walk if nothing beyond its imports is collected. + const walkFree = + s.pristine && + !s.names.empty && + visit == null && + declarations == null && + !impliesJsx(state.file.opts.parserOpts); + if ( + !walkFree && + !mayUseStyleX(program.node, state.opts, syntax, declarations, visit) + ) + return; + s.consumerCalls = consumerCalls; + // The program-level binding of each reference, by Babel's scope at entry. + if (s.recordShapes) { + s.references = new Map(); + for (const binding of Object.values(program.scope.bindings)) + for (const reference of binding.referencePaths) + s.references.set(reference.node, binding.path.node); + // Each consumer's nodes and the program-level declarations it references. + s.consumerCalls = consumerCalls.map((call) => { + const nodes = []; + const declarations = new Set(); + t.traverseFast(call, (node) => { + nodes.push(node); + const declaration = s.references.get(node); + if (declaration != null) declarations.add(declaration); + }); + return { call, nodes, declarations }; + }); + } + s.syntax = syntax; + s.program = program; + const filename = state.file.opts.filename || 'unknown.js'; + // Compile the file's text if the AST still matches it, else Babel's printing. + let unchanged = false; + if (overrides.length === 0) { + if (s.pristine) unchanged = true; + else { + let original = null; + try { + original = babelParser.parse( + state.file.code, + state.file.opts.parserOpts, + ).program; + } catch (_) { + original = null; + } + unchanged = original != null && equalTrees(original, program.node, true); + // Free the reference tree before the compile and the output parse. + original = null; + } + } + const input = unchanged + ? { code: state.file.code, map: null } + : generated(program.node, state, filename); + let result; + try { + result = compile(input, state, filename); + } catch (error) { + if ( + !compileDiagnostics.has(error) || + error.stylexDiagnostic.kind === 'parse' + ) + throw error; + // Report at exit unless visitors that run first change what this compile saw. + s.deferred = { error, program: t.cloneNode(program.node, true, true) }; + return; + } + s.rules = metadataRules(result.metadata?.stylex || []); + state.file.metadata.stylex = s.rules; + s.warnings = result.warnings || []; + s.errors = result.errors || []; + for (const warning of s.warnings) logDiagnostic('warn', warning); + for (const error of s.errors) logDiagnostic('error', error); + // Never touch a no-op AST just to refresh its printed source. + if (result.code !== input.code) { + // Provisional: a plugin may still detach a definition this compile consumed. + const names = s.names; + // Whether a subtree contains a definition call (stops at the first one). + const defines = (node) => { + if (node == null || typeof node.type !== 'string') return false; + if (node.type === 'CallExpression') { + const api = stylexCall(node, names); + if (api != null && !EXIT_CALLS.has(api)) return true; + } + for (const key of t.VISITOR_KEYS[node.type] || []) { + const value = node[key]; + if (Array.isArray(value)) { + for (const child of value) if (defines(child)) return true; + } else if (defines(value)) return true; + } + return false; + }; + // Definitions keep their source form until visited, and unused ones until exit. + const definition = (node) => { + if (node.type !== 'CallExpression') return false; + const api = stylexCall(node, names); + return api != null && !EXIT_CALLS.has(api); + }; + const graft = apply( + program, + state, + input, + result, + true, + defines, + definition, + ); + s.edits = graft.edits; + if (s.recordShapes) { + s.adopted = graft.adopted; + s.outputs = [ + ...graft.replaced.map(([previous, node]) => ({ + removed: [previous], + added: [node], + })), + ...graft.regions, + ]; + } + const held = new Map(graft.held); + // Hoists for a held definition arrive with it, as the original plugin does. + const hoistsFor = new Map(); + const owners = new Map(); + for (const [previous, node] of graft.held) + t.traverseFast(node, (inner) => { + if (inner.type === 'Identifier') owners.set(inner.name, previous); + }); + const replacements = new Set(graft.replaced.map(([, node]) => node)); + const ownerOf = (statement) => { + if ( + statement.type !== 'VariableDeclaration' || + !graft.adopted.has(statement) || + replacements.has(statement) + ) + return null; + const names = Object.keys(t.getBindingIdentifiers(statement)); + const owner = names.length > 0 ? owners.get(names[0]) : null; + return owner != null && names.every((name) => owners.get(name) === owner) + ? owner + : null; + }; + const body = program.node.body; + const owned = body.map(ownerOf); + for (const [index, owner] of owned.entries()) { + if (owner == null) continue; + // It returns where the compiler put it: before the next statement. + let next = index + 1; + while (next < body.length && owned[next] != null) next++; + if (!hoistsFor.has(owner)) hoistsFor.set(owner, []); + hoistsFor.get(owner).push({ node: body[index], before: body[next] }); + } + body.splice( + 0, + body.length, + ...body.filter((_, index) => owned[index] == null), + ); + + // What each definition read, recorded so its visit can confirm it. + const definitionCalls = new Set(); + if (s.recordShapes) { + for (const [previous] of graft.held) definitionCalls.add(previous); + for (const node of graft.deferred) + t.traverseFast(node, (inner) => { + if (definition(inner)) definitionCalls.add(inner); + }); + } + const inputs = (call) => { + if (!s.recordShapes) return null; + const record = { call, shape: shapeRecord(call), dependencies: [] }; + for (const declaration of dependencies(call, declarations)) { + let shape = s.dependencyShapes.get(declaration); + if (shape == null) { + shape = shapeRecord(declaration, [], definitionCalls); + s.dependencyShapes.set(declaration, shape); + } + record.dependencies.push(declaration); + } + s.inputs.push(record); + return record; + }; + for (const [previous, node] of graft.replaced) + if (held.has(previous)) { + s.sites.push({ node, previous, held: previous }); + s.holds.set(previous, { + replacement: node, + hoists: hoistsFor.get(previous) || [], + callee: previous.callee, + args: previous.arguments.slice(), + inputs: inputs(previous), + }); + } else if (defines(previous)) s.sites.push({ node, previous }); + else if (s.recordShapes) s.consumers.push({ node, previous }); + s.unusedLists = graft.deferredIn; + for (const node of graft.deferred) { + s.sites.push({ node }); + // Declarations are checked by initializer identity, others by structure. + const declaration = node.type === 'VariableDeclaration'; + s.unused.push({ + node, + copy: declaration ? null : t.cloneNode(node, true, true), + inits: declaration + ? node.declarations.map((declarator) => declarator.init) + : null, + }); + t.traverseFast(node, (inner) => { + if (inner.type !== 'CallExpression') return; + s.untouched.add(inner); + if (s.recordShapes && definition(inner)) + s.unusedForms.set(inner, inputs(inner)); + }); + } + } else { + s.names = stylexNames(program.node, s.sources); + if (s.alone) { + s.known = EVERY_NODE; + s.unusedLists = new Map(); + } else s.known = knownStyleX(program.node, s); + } +} + +function unchangedInputs(record, s) { + return ( + matchesShapeRecord(record.call, record.shape) && + record.dependencies.every((declaration) => + matchesShapeRecord(declaration, s.dependencyShapes.get(declaration)), + ) + ); +} + +// Undoes the stale entry output before other plugins visit it; exit recompiles. +function markStale(s) { + if (s.stale) return; + s.stale = true; + s.staleConsumers = staleConsumers(s); + for (const node of revert(s.edits)) s.restored.add(node); + s.edits = []; +} + +// Compiled consumers (props, attrs) that read a definition whose input changed. +function staleConsumers(s) { + if ((s.consumerCalls || []).length === 0 || s.references == null) return []; + // The declarator each recorded definition initializes. + const calls = new Map(s.inputs.map((record) => [record.call, record])); + const declaratorOf = new Map(); + t.traverseFast(s.program.node, (node) => { + if (node.type !== 'VariableDeclarator' || node.init == null) return; + t.traverseFast(node.init, (inner) => { + if (calls.has(inner)) declaratorOf.set(calls.get(inner), node); + }); + }); + // The program-level declarations a subtree references, by Babel's scope. + const referenced = (root) => { + const found = new Set(); + t.traverseFast(root, (node) => { + const declaration = s.references.get(node); + if (declaration != null) found.add(declaration); + }); + return found; + }; + // Changed definitions and declarations, then every definition referencing one. + const affected = new Map(); + for (const record of s.inputs) { + if (unchangedInputs(record, s)) continue; + const declarator = declaratorOf.get(record); + if (!matchesShapeRecord(record.call, record.shape) && declarator != null) + affected.set(declarator, record); + for (const dependency of record.dependencies) + if (!matchesShapeRecord(dependency, s.dependencyShapes.get(dependency))) + affected.set(dependency, record); + } + for (let grown = true; grown; ) { + grown = false; + for (const record of s.inputs) { + const declarator = declaratorOf.get(record); + if (declarator == null || affected.has(declarator)) continue; + for (const declaration of referenced(record.call)) + if (affected.has(declaration)) { + affected.set(declarator, affected.get(declaration)); + grown = true; + break; + } + } + } + const result = []; + for (const { call, nodes, declarations } of s.consumerCalls) { + if (call.start == null) continue; + for (const declaration of declarations) + if (affected.has(declaration)) { + result.push({ call, nodes, definition: affected.get(declaration) }); + break; + } + } + return result; +} + +function locationOf(node) { + const start = node?.loc?.start; + return start == null + ? 'an unknown location' + : `${start.line}:${start.column + 1}`; +} + +// Throws if other plugins already transformed a stale consumer's output. +function assertNoStaleConsumers(program, state, s) { + const stale = s.staleConsumers || []; + if (stale.length === 0 || s.outputs == null) return; + // Every node of a stale consumer as written; its output lies at or inside them. + const consumerOf = new Map(); + for (const entry of stale) + for (const node of entry.nodes) + if (!consumerOf.has(node)) consumerOf.set(node, entry); + const ownerIn = (items) => { + for (const item of items) { + if (!isNode(item)) continue; + let found = null; + t.traverseFast(item, (node) => { + found ??= consumerOf.get(node) ?? null; + }); + if (found != null) return found; + } + return null; + }; + const owners = new Map(); + const own = (items, owner) => { + for (const item of items) + if (isNode(item)) + t.traverseFast(item, (node) => { + owners.set(node, owner); + }); + }; + for (const { removed, added } of s.outputs) { + const owner = ownerIn(removed); + if (owner != null) own(added, owner); + } + if (owners.size === 0) return; + let leaked = null; + t.traverseFast(program.node, (node) => { + if (leaked == null && owners.has(node)) leaked = owners.get(node); + }); + if (leaked == null) return; + const { call, definition } = leaked; + throw state.file.buildCodeFrameError( + call, + `A plugin rewrote the StyleX definition at ${locationOf( + definition.call, + )} (or a value it reads) during the traversal, after its consumer at ${locationOf( + call, + )} was compiled and transformed by other plugins, so the consumer would keep class names without CSS. Move the definition above its consumers, or run that plugin in an earlier pass.`, + Error, + ); +} + +// Checks every recorded input at the first visit, after all Program.enter hooks. +function checkProgram(s) { + if (s.checkedProgram || !s.recordShapes) return; + s.checkedProgram = true; + if (s.inputs.some((record) => !unchangedInputs(record, s))) markStale(s); +} + +// Drops copies of kept statements' comments from adopted program statements. +function dropCommentCopies(body, adopted) { + for (let index = 0; index < body.length; index++) { + const statement = body[index]; + if (!adopted.has(statement)) continue; + let previous = index - 1; + while (previous >= 0 && adopted.has(body[previous])) previous--; + let next = index + 1; + while (next < body.length && adopted.has(body[next])) next++; + const owned = []; + for (const neighbour of [body[previous], body[next]]) + if (neighbour != null) + for (const field of COMMENT_FIELDS) + for (const comment of neighbour[field] || []) owned.push(comment); + if (owned.length === 0) continue; + for (const field of ['leadingComments', 'trailingComments']) { + const comments = statement[field]; + if (comments == null) continue; + const left = comments.filter( + (comment) => + owned.includes(comment) || + !owned.some( + (other) => + other.type === comment.type && other.value === comment.value, + ), + ); + if (left.length > 0) statement[field] = left; + else delete statement[field]; + } + } +} + +// A definition earlier plugins created after entry: left for the exit compile. +function created(path, state) { + const s = state.stylexFacade; + if (s == null || s.names.empty) return; + const node = path.node; + const hold = s.holds.get(node); + if (hold != null) { + s.holds.delete(node); + // A call rewritten here or edited in place by an earlier visitor is stale. + checkProgram(s); + const same = + !s.stale && + node.callee === hold.callee && + node.arguments.length === hold.args.length && + node.arguments.every( + (argument, index) => argument === hold.args[index], + ) && + (hold.inputs == null || unchangedInputs(hold.inputs, s)); + if (!same) { + // Compiled at exit, as visited here; later plugins see the compiled form. + markStale(s); + s.dirty = true; + s.pending.push({ path, node, copy: t.cloneNode(node, true) }); + s.pendingNodes.add(node); + path.skip(); + return; + } + const inserted = []; + for (const { node: hoist, before } of hold.hoists) { + const program = path.scope.getProgramParent().path; + const index = before == null ? -1 : program.node.body.indexOf(before); + const anchor = + index >= 0 + ? NodePath.get({ + parentPath: program, + parent: program.node, + container: program.node.body, + listKey: 'body', + key: index, + }).setContext(program.context) + : path.find((ancestor) => ancestor.parentPath?.isProgram()); + if (anchor == null) { + s.dirty = true; + return; + } + inserted.push(...anchor.insertBefore(hoist)); + s.hoisted.push(hoist); + } + const [replaced] = path.replaceWith(hold.replacement); + if (inserted.length > 0) registerHoists(inserted, replaced); + return; + } + if (s.untouched.has(node)) { + checkProgram(s); + const recorded = s.unusedForms.get(node); + if (recorded != null) { + s.unusedForms.delete(node); + if (!unchangedInputs(recorded, s)) markStale(s); + } + path.skip(); + return; + } + if (s.known.has(node)) return; + const api = stylexCall(node, s.names); + if (api == null || EXIT_CALLS.has(api)) return; + if (path.findParent((parent) => s.pendingNodes.has(parent.node)) != null) + return; + s.pending.push({ path, node, copy: t.cloneNode(node, true) }); + s.pendingNodes.add(node); + path.skip(); +} + +// Counts hoist references that Babel's insertion misses, so they are not removed. +function registerHoists(hoists, replaced) { + const scope = replaced.scope.getProgramParent(); + const names = new Set(); + for (const hoist of hoists) { + const declared = Object.keys(hoist.getBindingIdentifiers()); + if (!declared.some((name) => scope.hasOwnBinding(name))) + scope.registerDeclaration(hoist); + for (const name of declared) names.add(name); + } + const reference = (path) => { + const name = path.node.name; + if (!names.has(name)) return; + const binding = scope.getOwnBinding(name); + if (binding != null && path.scope.getBinding(name) === binding) + binding.reference(path); + }; + const visitor = { ReferencedIdentifier: reference }; + for (const root of [replaced, ...hoists]) { + if (root.isReferencedIdentifier()) reference(root); + root.traverse(visitor); + } +} + +// StyleX the program gained after entry, classified as the original plugin's +// exit would: visited consumers compile; late definitions and sx elements stay. +function lateStyleX(program, s, skip) { + const found = { compile: false, hidden: [], positions: new Map() }; + const sites = new Set(s.sites.map((site) => site.node)); + for (const consumer of s.consumers) sites.add(consumer.node); + for (const site of s.sites) if (site.held != null) sites.add(site.held); + const walk = (node, parent, field, index) => { + if (sites.has(node)) found.positions.set(node, { parent, field, index }); + if (skip.has(node) || s.known.has(node)) return; + if (node.type === 'CallExpression') { + const api = stylexCall(node, s.names); + if (api != null) { + if (!EXIT_CALLS.has(api)) { + found.hidden.push({ node, parent, field, index }); + return; + } + found.compile = true; + } + } else if (sxAttribute(node, s.sx)) { + if (s.seenSx.has(node)) found.compile = true; + else found.hidden.push({ node, sx: true }); + } + for (const key of fieldsOf(node)) { + const value = node[key]; + if (Array.isArray(value)) { + value.forEach((item, position) => { + if (isNode(item)) walk(item, node, key, position); + }); + } else if (isNode(value)) walk(value, node, key, null); + } + }; + walk(program, null, null, null); + // Rebuild detached or unvisited definition sites from the surviving definitions. + for (const site of s.sites) { + const applied = found.positions.has(site.node); + const waiting = site.held != null && found.positions.has(site.held); + if (!applied && !waiting) found.compile = true; + if (waiting && s.holds.has(site.held)) found.compile = true; + } + return found; +} + +// Hides constructs from the exit compile behind placeholders, until `restore`. +function hide(hidden) { + const placeholders = new Map(); + for (const [position, entry] of hidden.entries()) { + const name = `__stylexExitPlaceholder${position}`; + placeholders.set(name, entry); + const placeholder = t.identifier(name); + if (entry.index == null) entry.parent[entry.field] = placeholder; + else entry.parent[entry.field][entry.index] = placeholder; + } + return placeholders; +} + +function restore(program, placeholders) { + if (placeholders.size === 0) return; + const walk = (node) => { + for (const key of fieldsOf(node)) { + const value = node[key]; + if (Array.isArray(value)) { + value.forEach((item, position) => { + if (item?.type === 'Identifier' && placeholders.has(item.name)) + value[position] = placeholders.get(item.name).node; + else if (isNode(item)) walk(item); + }); + } else if (value?.type === 'Identifier' && placeholders.has(value.name)) + node[key] = placeholders.get(value.name).node; + else if (isNode(value)) walk(value); + } + }; + walk(program); +} + +// Removes unused definitions the entry compile dropped, wherever they are now. +function removeUnused(program, unused, positions) { + const byList = new Map(); + let nested = false; + for (const { node } of unused) { + const position = positions.get(node); + if (position == null || position.index == null) continue; + const list = position.parent[position.field]; + if (!byList.has(list)) byList.set(list, new Set()); + byList.get(list).add(node); + if (position.parent === program.node) { + for (const name of Object.keys(t.getBindingIdentifiers(node))) + program.scope.removeBinding(name); + } else nested = true; + } + for (const [list, nodes] of byList) { + shareRemovedComments(list, nodes); + list.splice(0, list.length, ...list.filter((item) => !nodes.has(item))); + } + if (nested) program.scope.crawl(); +} + +// Removed statements share comments with siblings like Babel's `path.remove()`. +function shareRemovedComments(list, nodes) { + for (let index = 0; index < list.length; index++) { + const node = list[index]; + if (!nodes.has(node)) continue; + const leading = node.leadingComments; + const trailing = node.trailingComments; + if (leading == null && trailing == null) continue; + let back = index - 1; + while (back >= 0 && nodes.has(list[back])) back--; + shareComments( + node, + isNode(list[back]) ? list[back] : null, + isNode(list[index + 1]) ? list[index + 1] : null, + ); + } +} + +// Babel's `shareCommentsWithSiblings`, for a node leaving between two others. +function shareComments(node, previous, next) { + const without = (comments, existing) => + existing == null + ? comments + : comments.filter((comment) => !existing.includes(comment)); + const leading = node.leadingComments; + const trailing = node.trailingComments; + if (previous != null) { + if (leading != null) + t.addComments( + previous, + 'trailing', + without(leading, previous.trailingComments), + ); + if (trailing != null && next == null) + t.addComments(previous, 'trailing', trailing); + } + if (next != null) { + if (trailing != null) + t.addComments(next, 'leading', without(trailing, next.leadingComments)); + if (leading != null && previous == null) + t.addComments(next, 'leading', leading); + } +} + +function exit(program, state) { + const s = state.stylexFacade; + if (s == null) return; + s.names = stylexNames(program.node, s.sources); + // Unrelated files pay only this scan of top-level statements. + if ( + s.names.empty && + s.pending.length === 0 && + !s.dirty && + s.deferred == null && + s.sites.length === 0 + ) + return; + // With StyleX alone in its pass, unused definitions are where it left them. + if ( + s.alone && + s.pending.length === 0 && + !s.dirty && + !s.stale && + s.deferred == null && + s.unusedLists != null + ) { + const positions = new Map(); + for (const { node } of s.unused) { + const list = s.unusedLists.get(node); + if (list != null) positions.set(node, { ...list, index: 0 }); + } + removeUnused(program, s.unused, positions); + return; + } + const late = lateStyleX(program.node, s, new Set()); + // A replaced initializer (coverage instrumentation) is a different definition. + const changed = s.unused.some( + ({ node, copy, inits }) => + late.positions.has(node) && + (inits != null + ? node.declarations.length !== inits.length || + node.declarations.some( + (declarator, index) => declarator.init !== inits[index], + ) + : !equalTrees(copy, node, false, s.untouched)), + ); + if (s.pending.length === 0 && !changed && !s.stale && !late.compile) { + if (s.deferred != null) throw s.deferred.error; + removeUnused(program, s.unused, late.positions); + return; + } + // Earlier plugins added StyleX the original plugin compiles: recompile once. + const restored = revert(s.edits); + for (const node of s.restored) restored.add(node); + // The revert restores sx elements as written: they are source, not late. + for (const element of s.sxElements || []) restored.add(element); + s.restored = new Set(); + s.edits = []; + // Definitions other plugins moved, and stale consumers, return to their places. + const moved = lateStyleX(program.node, s, new Set()).positions; + // A consumer's output can return only where an expression stood. + const returning = s.stale + ? [ + ...s.sites, + ...s.consumers.filter( + ({ node, previous }) => + t.isExpression(node) && t.isExpression(previous), + ), + ] + : s.sites; + for (const site of returning) { + const position = site.previous != null ? moved.get(site.node) : null; + if (position == null || position.parent == null) continue; + if (position.index == null) position.parent[position.field] = site.previous; + else position.parent[position.field][position.index] = site.previous; + restored.add(site.previous); + } + if (s.stale) assertNoStaleConsumers(program, state, s); + s.consumers = []; + s.sites = []; + s.unused = []; + // Hoists that arrived with applied definitions are compiler output too. + if (s.hoisted.length > 0) { + const hoisted = new Set(s.hoisted); + const body = program.node.body; + body.splice( + 0, + body.length, + ...body.filter((statement) => !hoisted.has(statement)), + ); + s.hoisted = []; + } + for (const { path, node, copy } of s.pending) { + if (path.removed || path.node !== node || path.container == null) continue; + path.container[path.key] = copy; + restored.add(copy); + } + s.pending = []; + if (s.deferred != null) { + if (equalTrees(s.deferred.program, program.node, false)) + throw s.deferred.error; + } + const { hidden } = lateStyleX(program.node, s, restored); + // sx attributes are renamed rather than replaced. + const sxNames = []; + for (const entry of hidden) + if (entry.sx) { + const attribute = entry.node.attributes.find( + (attr) => + attr.type === 'JSXAttribute' && + attr.name.type === 'JSXIdentifier' && + attr.name.name === s.sx, + ); + sxNames.push(attribute.name); + attribute.name.name = `data-stylex-exit-placeholder-${sxNames.length}`; + } + const placeholders = hide(hidden.filter((entry) => !entry.sx)); + const filename = state.file.opts.filename || 'unknown.js'; + const input = generated(program.node, state, filename); + let result; + try { + result = compile(input, state, filename); + } finally { + for (const name of sxNames) name.name = s.sx; + } + // The entry compile was provisional: rules come from the surviving definitions. + s.rules = metadataRules(result.metadata?.stylex || []); + state.file.metadata.stylex = s.rules; + for (const warning of multisetDifference(result.warnings || [], s.warnings)) + logDiagnostic('warn', warning); + for (const error of multisetDifference(result.errors || [], s.errors)) + logDiagnostic('error', error); + let fresh = restored; + if (result.code !== input.code) { + const { adopted } = apply(program, state, input, result, false); + fresh = new Set([...restored, ...adopted]); + } + restore(program.node, placeholders); + for (const name of sxNames) name.name = s.sx; + program.scope.crawl(); + s.names = stylexNames(program.node, s.sources); + s.known = knownStyleX(program.node, s); + requeue(program, fresh); +} + +// Queues nodes placed on exit on the file's traversal, as Babel does for inserts. +function requeue(program, nodes) { + if (nodes.size === 0) return; + // Create paths while walking, rather than resolving each from the program. + const walk = (path) => { + if (nodes.has(path.node)) { + program.requeue(path); + return; + } + for (const field of fieldsOf(path.node)) { + const value = path.node[field]; + if (Array.isArray(value)) { + value.forEach((item, index) => { + if (isNode(item)) + walk( + NodePath.get({ + parentPath: path, + parent: path.node, + container: value, + listKey: field, + key: index, + }).setContext(path.context), + ); + }); + } else if (isNode(value)) + walk( + NodePath.get({ + parentPath: path, + parent: path.node, + container: path.node, + key: field, + }).setContext(path.context), + ); + } + }; + walk(program); +} + +// Rewrites an sx element created after entry into a `props(...)` spread, as +// the original plugin does; without a StyleX namespace in scope it waits for exit. +function sxElement(path, state) { + const s = state.stylexFacade; + if (s == null || s.known.has(path.node) || !sxAttribute(path.node, s.sx)) + return; + s.seenSx.add(path.node); + s.dirty = true; + const program = path.scope.getProgramParent(); + const namespace = [...s.names.namespaces].find((name) => { + const binding = program.getBinding(name); + return binding != null && path.scope.getBinding(name) === binding; + }); + if (namespace == null) return; + for (const attribute of path.get('attributes')) { + if ( + !attribute.isJSXAttribute() || + !attribute.get('name').isJSXIdentifier({ name: s.sx }) + ) + continue; + const value = attribute.get('value'); + if ( + !value.isJSXExpressionContainer() || + value.get('expression').isJSXEmptyExpression() + ) + continue; + attribute.replaceWith( + t.jsxSpreadAttribute( + t.callExpression( + t.memberExpression(t.identifier(namespace), t.identifier('props')), + [value.node.expression], + ), + ), + ); + break; + } +} + +function styleXTransform() { + return { + name: 'stylex-rust', + visitor: { + Program: { enter, exit }, + 'Statement|Directive'(path, state) { + const s = state.stylexFacade; + if (s != null) checkProgram(s); + }, + CallExpression: created, + ImportDeclaration(path, state) { + const s = state.stylexFacade; + if (s == null || !s.sources.has(path.node.source.value)) return; + // An earlier plugin imported StyleX during the traversal. + s.names = stylexNames(path.parentPath.node, s.sources); + }, + JSXOpeningElement: sxElement, + }, + }; +} + +styleXTransform.withOptions = (options) => [styleXTransform, options]; +styleXTransform.processStylexRules = compiler.processStylexRules; +module.exports = styleXTransform; diff --git a/packages/@stylexjs/rust-plugin/build.rs b/packages/@stylexjs/rust-plugin/build.rs new file mode 100644 index 000000000..23cb14cef --- /dev/null +++ b/packages/@stylexjs/rust-plugin/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(feature = "node")] + napi_build::setup(); +} diff --git a/packages/@stylexjs/rust-plugin/crates/LICENSE b/packages/@stylexjs/rust-plugin/crates/LICENSE new file mode 100644 index 000000000..afae8f0e7 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vladislav Buinovski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@stylexjs/rust-plugin/crates/NOTICE.md b/packages/@stylexjs/rust-plugin/crates/NOTICE.md new file mode 100644 index 000000000..ae7b7137a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/NOTICE.md @@ -0,0 +1,40 @@ +# Notice + +This project is MIT licensed; the terms are in [LICENSE](./LICENSE) and the +copyright there is the project's own. It contains third-party work, listed +below with its copyright holder and licence. + +## StyleX + +- **Upstream:** +- **Copyright (c) Meta Platforms, Inc. and affiliates.** +- **Licence:** MIT + +The compiler's observable behaviour (class-name hashing, property expansion, +value canonicalization, rule priorities) is StyleX's, and parts of the +implementation follow Meta's structure to match it exactly. + +## styleq + +- **Upstream:** +- **Copyright (c) Nicolas Gallagher** +- **Licence:** MIT, text at + [crates/stylex-styleq/LICENSE](./crates/stylex-styleq/LICENSE) + +[`crates/stylex-styleq`](./crates/stylex-styleq) is a Rust port of the class-name +merger the StyleX runtime uses. + +## postcss-value-parser + +- **Upstream:** +- **Copyright (c) Bogdan Chadkin** +- **Licence:** MIT, text at + [crates/postcss-value-parser/LICENSE](./crates/postcss-value-parser/LICENSE) + +[`crates/postcss-value-parser`](./crates/postcss-value-parser) carries this CSS +value scanner as its own dependency-free crate. + +## Everything else + +Registry dependencies carry their own licence files; `Cargo.lock` records their +versions. diff --git a/packages/@stylexjs/rust-plugin/crates/PROVENANCE.md b/packages/@stylexjs/rust-plugin/crates/PROVENANCE.md new file mode 100644 index 000000000..8d71fe13a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/PROVENANCE.md @@ -0,0 +1,141 @@ +# Vendored StyleX core crates + +The StyleX compiler core from +[stylex-swc-plugin](https://github.com/Dwlad90/stylex-swc-plugin) (MIT; see +`LICENSE` and `NOTICE.md`), vendored at tag `0.19.2`, commit `6e7d3983`: + +- repository archive (`git archive 6e7d3983`) sha256 + `8836aea57dfd4cee147df404753883a760422e628dc15df243fe89bb0dc5e1a5` +- 21-crate core subset archive sha256 + `f6d2d895a6360dff661926bc558a1d9035710c095386331725de164646d6a8e3` + +Per-crate upstream git tree ids: + +``` +aa40421ccfb22fc832656110fb3d16bdaa6fbcf9 crates/postcss-value-parser +0193f86f7703fa8e7b0d0a24410034aa8add38a1 crates/stylex-ast +c719579e6281eaead18392dcac364c014392a6b2 crates/stylex-atoms +90d22a248e84d817874e9c319fd744e31850ad99 crates/stylex-constants +f8ff06c6579341dd8243b6c4e0d73aadeabf5556 crates/stylex-css-parser +da98d690ee160b4297286f2662f8a00e15b3ff00 crates/stylex-css +159239cec918c0d64336143f31159026771cf7bb crates/stylex-diagnostics +674005e71b6a7964c6ccc6d9a66e43ac93d60ae5 crates/stylex-enums +e15b924b54733f6f87e50e430a93bee340e8d22f crates/stylex-evaluator +3b16cb4df9f8794d7aab204c72028a6384a5b38a crates/stylex-js +48b9c59d722e81753f9cb6bb5e1babf3f581cc35 crates/stylex-macros +0d779b8461cc9ec24f130e74d105f6b43242349d crates/stylex-nested-config +6fd9b84cfb11de10af7c8e3562badd545903f192 crates/stylex-path-resolver +787c23cc19d566921080fe012e577dc5751832c2 crates/stylex-regex +6c33d2a9cdfc7bf81c53a92f7d71930cce45dbd9 crates/stylex-state-index +3f653b6ce199ecdf8dbcc28430af8bcf0101f42a crates/stylex-state +a722f63724ca83ada81c288a5c780d845ca98647 crates/stylex-structures +fb7cf2d616969dd5d1f4f49fc89af090c4ffffb5 crates/stylex-styleq +83fcfeb07d0f1aaaf01c6054594719a629878ef1 crates/stylex-transform +67fd65603219b1c419170752838c4aea241c4bb0 crates/stylex-types +4db9a5ed34b82852736c8a123a1ac23f8026dc93 crates/stylex-utils +``` + +## Vendoring + +- Copied: each crate's `src/`, `Cargo.toml` and license files, and + `stylex-path-resolver/fixtures` byte-identical to `0.19.2` (codeload archive + sha256 `cf4f84b6a32cfcdf4657cc0d9b831c7577cf401f15f05db9624822e3be02affa`; + `../.gitignore` re-includes its `node_modules`, `dist` and `lib`). +- Left out: upstream benches, integration-test directories, docs and JS tooling, + and the `[[bench]]`/`[[test]]` tables naming them. The `tests/` directories in + `stylex-css`, `stylex-css-parser` and `stylex-evaluator` are ours. +- Workspace package, dependency and lint tables are copied from upstream into + `../Cargo.toml`; `rust-version` is 1.96 (upstream 1.97). +- `rustfmt.toml` carries upstream's stable formatting options; + `stylex-state/package.json` keeps the package name upstream's tests read. +- `scripts/test.cjs` runs the unit tests under `cargo test` with a 16 MiB + `RUST_MIN_STACK` (upstream uses nextest); CI runs them at the default stack. +- Comments in the vendored crates were condensed; code is unchanged (verified + token-for-token). + +## Behaviour changes + +Each keeps the Babel plugin's output and diagnostics where upstream differed. + +Evaluator (`stylex-evaluator`, `stylex-js`) +- The Boa engine is removed; a native evaluator folds what the Babel plugin + folds, with JavaScript semantics for host-provided sparse arrays, `==`/`!=`, + spread in hole-free arrays and pure-builtin arguments, optional member reads, + later-called local functions and reference equality. +- Source-literal holes refuse at their first offending element with Babel's + unlocated fault (member calls keep Babel's CallExpression refusal); a hole + Babel never reaches, inside an approved spread or call operand, may be named + instead of Babel's spread or call refusal. +- Reads before declaration, builtin method values, arrow-function property + reads and optional calls are refused. +- Allocation, length and recursion budgets refuse with typed reasons. +- Results reach the transform through one fallible boundary + (`materialize_result`). +- Results are memoized per evaluation, keyed on the base context only; + host reads and calls are never cached. +- Known limitation: a Rust caller that writes bindings through `StateManager` + or `FunctionMap` directly, or shares a budget across state owners, can read a + stale memoized value; `transformSync` and the Babel adapter cannot. + +Host boundary (`stylex-structures`, `stylex-state`) +- `env` values and callbacks are live host values, read and called in Babel's + order; their failures keep their cause. +- Host-run callback effects are journaled per evaluation scope and replayed in + order. +- `ImportResolver` resolves theme imports, names files and rewrites aliased + imports; `DebugFileNamer` serves `debugFilePath`. + +Module resolution and paths (`stylex-state`, `stylex-path-resolver`) +- All options default as in the Babel plugin + (`src/utils/transform/core_options.rs`); no `unstable_moduleResolution` + means no resolution. +- `commonJS` resolves relative imports natively and asks the host otherwise; a + manifest without `name` gives `undefined:`. +- `experimental_crossFileParsing` resolves as `commonJS` (upstream refused it; + `cross_file_parsing_is_refused` became `cross_file_parsing_resolves_as_common_js`). +- A file outside any package resolves against its own directory; package + lookups start at the resolution root. +- Missing imports are left unresolved instead of panicking. + +Transform and output (`stylex-transform`, `stylex-state`, `stylex-css`) +- Rule metadata is reported per `add_style` call, in order, duplicates kept. +- Injected imports and helper names follow Babel's merge and naming order. +- Call and JSX-spread sites are distinguished by span. +- Writes through known aliases and callbacks mark an object as mutated. +- Compiled objects keep required `null` keys and Babel's key order; unused reset + keys are pruned in dev as in prod. +- `defineVars`, `createTheme` and the nested forms read their arguments as data. +- With `test: true`, `createTheme` emits only the test label class. +- Debug labels (`$$css`, `data-style-src`) are `path.relative` from the root or + package with the platform separator, and the compiled file's line; upstream's + `/`-joined labels, last-two-segment fallback and input-source-map path (and + their unit tests) are replaced. + +Diagnostics (`stylex-diagnostics`, `stylex-macros`) +- Errors carry Babel's message and the span of the node Babel reports; the host + renders the frame. + +Manifests +- SWC core 81 / parser 45; `boa_engine` removed; added `icu_collator`, + `smallvec`, `stacker`, `xxhash-rust`. + +## Performance-only changes + +These change no output. + +- `stylex-state`: rule registration builds `_inject2(...)` statements and hashes + placement keys only under runtime injection; rule metadata is derived on first + read. +- `stylex-state`, `stylex-transform`: the producer dispatch copies declarators + only where needed and computes each call's key once for all lookups. +- `stylex-state`: top-level expression recording skips array literals and + object literals that cannot equal a compiled `create`. +- `stylex-css`: relational-selector and compound-pseudo priority scans run only + on keys that can match; default priorities are one map lookup; lowercase CSS + keywords skip the value parser. +- `stylex-css-parser`: `validate_media_query` keeps a bounded per-thread memo. +- `stylex-ast`, `stylex-utils`, `stylex-transform`: string and number formatting + (`convert_string_to_prop_name`, `write_js_number`, `dashify`, `to_radix`, + class names, `transform_value`, `createTheme` variables) avoids the formatter + and re-encoding. +- `stylex-transform`: `stylex_create_set` sizes its maps up front. diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/Cargo.toml b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/Cargo.toml new file mode 100644 index 000000000..3c39fb8b7 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/Cargo.toml @@ -0,0 +1,29 @@ +[package] +description = "Loose CSS declaration-value scanner that preserves the author's own characters" +edition.workspace = true +license.workspace = true +name = "postcss_value_parser" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + + +# No `cdylib`: there is no `extern "C"` surface here and nothing links this as a +# dynamic library, so building one cost every workspace build for nothing. +[lib] +crate-type = ["rlib"] +doctest = false + +# No dependencies, and that is the point: the scanner is a self-contained port +# of one library, and anything it reached for would be a chance to behave +# differently from it. Dev dependencies are a separate question -- nothing under +# `[dev-dependencies]` reaches the shipped scanner or can change what it emits. +[dependencies] + +[dev-dependencies] +criterion.workspace = true +swc_malloc.workspace = true + + +[lints] +workspace = true diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/LICENSE b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/LICENSE new file mode 100644 index 000000000..6dcaefcbe --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) Bogdan Chadkin + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/lib.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/lib.rs new file mode 100644 index 000000000..46c9f777a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/lib.rs @@ -0,0 +1,133 @@ +//! A port of `postcss-value-parser` v4.2.0, revision `43ae6d3`. +//! Copyright (c) Bogdan Chadkin . MIT licensed; see `LICENSE` and +//! [`NOTICE.md`](../../NOTICE.md). +//! +//! A loose, never-failing CSS declaration-value scanner whose serialized output must not change: +//! StyleX hashes that text into class names, so upstream's quirks are preserved byte for byte. +//! +//! Callers must bound nesting depth first: serializing, walking and dropping recurse per level +//! (`stylex-css` limits it to 64). + +mod parse; +mod stringify; +mod unit; +mod walk; + +#[cfg(test)] +mod tests; + +pub use parse::parse; +pub use stringify::{Custom, stringify, stringify_node, stringify_node_with, stringify_with}; +pub use unit::{Dimension, unit}; +pub use walk::walk; + +use std::fmt; + +/// Which of the parser's seven token shapes a [`Node`] is. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NodeKind { + /// Anything not covered below: identifiers, numbers, dimensions, operators. + Word, + /// A quoted string; `value` is the text between the quotes, escapes included. + String, + /// A `,`, `:` or `/` separator, with surrounding whitespace in `before`/`after`. + Div, + /// Whitespace no other node claimed. + Space, + /// A `/* ... */` comment. `value` holds the text between the delimiters. + Comment, + /// A function call. `value` is the name, `nodes` the arguments. + Function, + /// A `U+xxxx` style range. + UnicodeRange, +} + +impl NodeKind { + /// The `type` string for this kind. + pub const fn as_str(self) -> &'static str { + match self { + NodeKind::Word => "word", + NodeKind::String => "string", + NodeKind::Div => "div", + NodeKind::Space => "space", + NodeKind::Comment => "comment", + NodeKind::Function => "function", + NodeKind::UnicodeRange => "unicode-range", + } + } +} + +impl fmt::Display for NodeKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One parsed token. A `None` field is the JavaScript's `undefined`, distinct from `""`. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Node { + pub kind: NodeKind, + /// Source text; the inner text for strings and comments; the name for functions. + pub value: String, + /// Leading whitespace (divs; just inside `(` for functions). + pub before: Option, + /// Trailing whitespace (divs; just inside `)` for functions). + pub after: Option, + /// The quote character used (strings only). + pub quote: Option, + /// The string, comment or function ran off the end of the input. + pub unclosed: bool, + /// Function arguments; `url()` holds a single word node. + pub nodes: Option>, + /// Start byte offset; normalizers compare offsets, so it must stay accurate. + pub source_index: usize, + /// End byte offset (exclusive); the end of input for an unclosed token. + pub source_end_index: usize, +} + +impl Node { + /// A node with every optional field absent. + pub fn new(kind: NodeKind, value: String, source_index: usize, source_end_index: usize) -> Self { + Node { + kind, + value, + before: None, + after: None, + quote: None, + unclosed: false, + nodes: None, + source_index, + source_end_index, + } + } +} + +/// A parsed value. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValueParser { + pub nodes: Vec, +} + +impl ValueParser { + /// Parses `value`; never fails. + pub fn new(value: &str) -> Self { + ValueParser { + nodes: parse(value), + } + } + + /// Visits every node; `cb` returns whether to descend (ignored when `bubble` is set). + pub fn walk(&mut self, mut cb: F, bubble: bool) -> &mut Self + where + F: FnMut(&mut Node, usize) -> bool, + { + walk(&mut self.nodes, &mut cb, bubble); + self + } +} + +impl fmt::Display for ValueParser { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&stringify(&self.nodes)) + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/parse.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/parse.rs new file mode 100644 index 000000000..a2785a74f --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/parse.rs @@ -0,0 +1,459 @@ +//! The scanner. + +use std::borrow::Cow; + +use crate::{Node, NodeKind}; + +const OPEN_PARENTHESES: u32 = b'(' as u32; +const CLOSE_PARENTHESES: u32 = b')' as u32; +const SINGLE_QUOTE: u32 = b'\'' as u32; +const DOUBLE_QUOTE: u32 = b'"' as u32; +const BACKSLASH: u32 = b'\\' as u32; +const SLASH: u32 = b'/' as u32; +const COMMA: u32 = b',' as u32; +const COLON: u32 = b':' as u32; +const STAR: u32 = b'*' as u32; + +/// What `charCodeAt` returns past the end (JS `NaN`): equal to no byte and greater than 32. +const OUT_OF_RANGE: u32 = u32::MAX; + +/// Scans bytes, not UTF-16 units: every tested character is ASCII, so offsets are byte offsets. +fn char_code_at(value: &str, pos: usize) -> u32 { + match value.as_bytes().get(pos) { + Some(byte) => u32::from(*byte), + None => OUT_OF_RANGE, + } +} + +/// `String.prototype.slice`: out-of-range and inverted bounds clamp to `""`. +/// A cut inside a character falls back to a lossy conversion instead of panicking. +fn slice(value: &str, start: usize, end: usize) -> String { + let start = start.min(value.len()); + let end = end.clamp(start, value.len()); + + match value.get(start..end) { + Some(text) => text.to_owned(), + None => String::from_utf8_lossy(&value.as_bytes()[start..end]).into_owned(), + } +} + +/// `String.prototype.indexOf` for a single character, searching from `from`. +fn index_of(value: &str, needle: char, from: usize) -> Option { + value + .get(from..) + .and_then(|tail| tail.find(needle)) + .map(|at| at + from) +} + +/// `String.prototype.indexOf("*/")`, searching from `from`. +fn index_of_comment_end(value: &str, from: usize) -> Option { + value + .get(from..) + .and_then(|tail| tail.find("*/")) + .map(|at| at + from) +} + +/// `/^[a-f0-9?-]+$/i` on the tail after the caller has checked the leading `u+`. +fn is_unicode_range_tail(tail: &[u8]) -> bool { + !tail.is_empty() + && tail + .iter() + .all(|byte| byte.is_ascii_hexdigit() || *byte == b'?' || *byte == b'-') +} + +/// Whether a word is a `U+0-7F` style range rather than an ordinary word. +fn is_unicode_range(token: &str) -> bool { + let bytes = token.as_bytes(); + + match (bytes.first(), bytes.get(1)) { + (Some(b'u' | b'U'), Some(b'+')) => is_unicode_range_tail(&bytes[2..]), + _ => false, + } +} + +/// A function whose `)` has not been seen yet; its children move into the node on close. +struct OpenFunction { + node: Node, + children: Vec, +} + +impl OpenFunction { + fn close(self) -> Node { + Node { + nodes: Some(self.children), + ..self.node + } + } +} + +/// Where the scanner is writing — the JavaScript's `tokens`. +fn tokens<'value>( + root: &'value mut Vec, + stack: &'value mut [OpenFunction], +) -> &'value mut Vec { + match stack.last_mut() { + Some(open) => &mut open.children, + None => root, + } +} + +/// The last node written, if it is a div — the JavaScript's `prev`. +fn trailing_div<'value>( + root: &'value mut Vec, + stack: &'value mut [OpenFunction], +) -> Option<&'value mut Node> { + tokens(root, stack) + .last_mut() + .filter(|node| node.kind == NodeKind::Div) +} + +/// Whether the innermost open function is `calc()`, where `*` and `/` are operators. +fn parent_is_calc(stack: &[OpenFunction]) -> bool { + matches!(stack.last(), Some(open) if open.node.value == "calc") +} + +/// Whether the innermost open function is one other than `calc()`. +fn parent_is_non_calc_function(stack: &[OpenFunction]) -> bool { + matches!(stack.last(), Some(open) if open.node.value != "calc") +} + +/// Capacity hint for the top-level node list (a quarter of the byte length); only a hint. +fn estimated_nodes(bytes: usize) -> usize { + (bytes / 4).clamp(1, 32) +} + +/// Turns a declaration value into a loose token list. Never fails. +pub fn parse(input: &str) -> Vec { + // Copied only when a closing delimiter has to be invented. + let mut value: Cow<'_, str> = Cow::Borrowed(input); + + let mut pos: usize = 0; + let mut code = char_code_at(&value, pos); + // Captured before invented delimiters extend the buffer. + let max = value.len(); + let mut root: Vec = Vec::with_capacity(estimated_nodes(input.len())); + let mut stack: Vec = Vec::new(); + // JS `parent` is `undefined` until the first function opens; the `/` whitespace rule + // tests `!parent`, so this is observable. + let mut parent_seen = false; + + let mut name = String::new(); + let mut before = String::new(); + let mut after = String::new(); + + while pos < max { + if code <= 32 { + // Whitespace. + let mut next = pos; + loop { + next += 1; + code = char_code_at(&value, next); + if code > 32 { + break; + } + } + let token = slice(&value, pos, next); + + if code == CLOSE_PARENTHESES && !stack.is_empty() { + after = token; + } else if let Some(prev) = trailing_div(&mut root, &mut stack) { + prev.source_end_index += token.len(); + prev.after = Some(token); + } else if code == COMMA + || code == COLON + || (code == SLASH + && char_code_at(&value, next + 1) != STAR + && (!parent_seen || parent_is_non_calc_function(&stack))) + { + before = token; + } else { + tokens(&mut root, &mut stack).push(Node::new(NodeKind::Space, token, pos, next)); + } + + pos = next; + } else if code == SINGLE_QUOTE || code == DOUBLE_QUOTE { + // Quotes. + let mut next = pos; + let quote = match code == SINGLE_QUOTE { + true => b'\'', + false => b'"', + }; + let mut token = Node::new(NodeKind::String, String::new(), pos, 0); + token.quote = Some(char::from(quote)); + + loop { + let mut escape = false; + + match index_of(&value, char::from(quote), next + 1) { + Some(found) => { + next = found; + let mut escape_pos = found; + while char_code_at(&value, escape_pos.wrapping_sub(1)) == BACKSLASH { + escape_pos -= 1; + escape = !escape; + } + }, + None => { + value.to_mut().push(char::from(quote)); + next = value.len() - 1; + token.unclosed = true; + }, + } + + if !escape { + break; + } + } + + token.value = slice(&value, pos + 1, next); + token.source_end_index = match token.unclosed { + true => next, + false => next + 1, + }; + tokens(&mut root, &mut stack).push(token); + pos = next + 1; + code = char_code_at(&value, pos); + } else if code == SLASH && char_code_at(&value, pos + 1) == STAR { + // Comments. + // Matches upstream: the search starts at the opening `/`, so `/*/` closes itself and does not + // round-trip. + let mut token = Node::new(NodeKind::Comment, String::new(), pos, 0); + + let next = match index_of_comment_end(&value, pos) { + Some(found) => { + token.source_end_index = found + 2; + found + }, + None => { + token.unclosed = true; + token.source_end_index = value.len(); + value.len() + }, + }; + + token.value = slice(&value, pos + 2, next); + tokens(&mut root, &mut stack).push(token); + + pos = next + 2; + code = char_code_at(&value, pos); + } else if (code == SLASH || code == STAR) && parent_is_calc(&stack) { + // Operation within calc. + let token = slice(&value, pos, pos + 1); + let length = token.len(); + tokens(&mut root, &mut stack).push(Node::new( + NodeKind::Word, + token, + // `before` is always empty here; saturating to be safe. + pos.saturating_sub(before.len()), + pos + length, + )); + pos += 1; + code = char_code_at(&value, pos); + } else if code == SLASH || code == COMMA || code == COLON { + // Dividers. + let token = slice(&value, pos, pos + 1); + let length = token.len(); + let mut node = Node::new( + NodeKind::Div, + token, + pos.saturating_sub(before.len()), + pos + length, + ); + node.before = Some(std::mem::take(&mut before)); + node.after = Some(String::new()); + tokens(&mut root, &mut stack).push(node); + + pos += 1; + code = char_code_at(&value, pos); + } else if code == OPEN_PARENTHESES { + // Whitespace after the open parenthesis. + let mut next = pos; + loop { + next += 1; + code = char_code_at(&value, next); + if code > 32 { + break; + } + } + let parentheses_open_pos = pos; + let name_start = pos.saturating_sub(name.len()); + let mut token = Node::new(NodeKind::Function, std::mem::take(&mut name), name_start, 0); + token.before = Some(slice(&value, parentheses_open_pos + 1, next)); + pos = next; + + if token.value == "url" && code != SINGLE_QUOTE && code != DOUBLE_QUOTE { + // A url body is taken whole. + next -= 1; + loop { + let mut escape = false; + + match index_of(&value, ')', next + 1) { + Some(found) => { + next = found; + let mut escape_pos = found; + while char_code_at(&value, escape_pos.wrapping_sub(1)) == BACKSLASH { + escape_pos -= 1; + escape = !escape; + } + }, + None => { + value.to_mut().push(')'); + next = value.len() - 1; + token.unclosed = true; + }, + } + + if !escape { + break; + } + } + + // Whitespace before the close, scanned backwards. Index 0 is unreachable (a `(` precedes); + // the guard prevents an infinite loop if that ever changes. + let mut whitespace_pos = next; + while whitespace_pos > 0 { + whitespace_pos -= 1; + code = char_code_at(&value, whitespace_pos); + + if code > 32 { + break; + } + } + let body_end = whitespace_pos + 1; + + if parentheses_open_pos < whitespace_pos { + // JS's `pos <= whitespace_pos` guard is dead here: the body is non-empty. + let mut nodes = vec![Node::new( + NodeKind::Word, + slice(&value, pos, body_end), + pos, + body_end, + )]; + if token.unclosed && body_end != next { + token.after = Some(String::new()); + nodes.push(Node::new( + NodeKind::Space, + slice(&value, body_end, next), + body_end, + next, + )); + } else { + token.after = Some(slice(&value, body_end, next)); + } + token.nodes = Some(nodes); + } else { + token.after = Some(String::new()); + token.nodes = Some(Vec::new()); + } + + pos = next + 1; + token.source_end_index = match token.unclosed { + true => next, + false => pos, + }; + code = char_code_at(&value, pos); + tokens(&mut root, &mut stack).push(token); + } else { + token.after = Some(String::new()); + token.source_end_index = pos + 1; + // JS appends to the parent on open; attaching on close lands in the same position. + stack.push(OpenFunction { + node: token, + children: Vec::new(), + }); + parent_seen = true; + } + } else if code == CLOSE_PARENTHESES + && let Some(mut finished) = stack.pop() + { + // Close parentheses. An unmatched `)` falls through to the word scanner, as in JS. + pos += 1; + code = char_code_at(&value, pos); + + finished.node.after = Some(std::mem::take(&mut after)); + // Matches upstream: this sum is overwritten on the next line. + finished.node.source_end_index += finished.node.after.as_ref().map_or(0, String::len); + finished.node.source_end_index = pos; + + let closed = finished.close(); + tokens(&mut root, &mut stack).push(closed); + parent_seen = true; + } else { + // Words. + let mut next = pos; + loop { + if code == BACKSLASH { + next += 1; + } + next += 1; + code = char_code_at(&value, next); + + let terminates = code <= 32 + || code == SINGLE_QUOTE + || code == DOUBLE_QUOTE + || code == COMMA + || code == COLON + || code == SLASH + || code == OPEN_PARENTHESES + || (code == STAR && parent_is_calc(&stack)) + || (code == CLOSE_PARENTHESES && !stack.is_empty()); + + if next >= max || terminates { + break; + } + } + let token = slice(&value, pos, next); + + if code == OPEN_PARENTHESES { + name = token; + } else if is_unicode_range(&token) { + tokens(&mut root, &mut stack).push(Node::new(NodeKind::UnicodeRange, token, pos, next)); + } else { + tokens(&mut root, &mut stack).push(Node::new(NodeKind::Word, token, pos, next)); + } + + pos = next; + } + } + + // Anything still open ran off the end of the input. + while let Some(mut open) = stack.pop() { + open.node.unclosed = true; + open.node.source_end_index = value.len(); + let closed = open.close(); + tokens(&mut root, &mut stack).push(closed); + } + + root +} + +#[cfg(test)] +mod tests { + use super::slice; + + /// Every cut the scanner makes lands on a character boundary. + #[test] + fn a_cut_on_a_character_boundary_takes_the_cheap_path() { + assert_eq!(slice("hello", 1, 4), "ell"); + assert_eq!(slice("日本語", 0, 3), "日"); + assert_eq!(slice("a→b", 1, 4), "→"); + } + + /// Out-of-range and inverted bounds clamp to `""`, like `String.prototype.slice`. + #[test] + fn bounds_outside_the_buffer_clamp_instead_of_panicking() { + assert_eq!(slice("abc", 0, 99), "abc"); + assert_eq!(slice("abc", 99, 99), ""); + assert_eq!(slice("abc", 2, 1), ""); + assert_eq!(slice("", 0, 5), ""); + } + + /// A cut inside a character (unreachable from [`parse`]) yields replacement characters. + #[test] + fn a_cut_inside_a_character_is_replaced_rather_than_fatal() { + // One replacement character per broken sequence, not per byte. + assert_eq!(slice("日", 0, 1), "\u{fffd}"); + assert_eq!(slice("a日b", 1, 3), "\u{fffd}"); + assert_eq!(slice("a日b", 0, 3), "a\u{fffd}"); + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/stringify.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/stringify.rs new file mode 100644 index 000000000..37f70e77f --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/stringify.rs @@ -0,0 +1,108 @@ +//! Serializing a node list back to text. + +use crate::{Node, NodeKind}; + +/// A per-node override: `Some` replaces the node's text (children included), `None` declines. +pub type Custom<'call> = &'call mut dyn FnMut(&Node) -> Option; + +type MaybeCustom<'outer, 'call> = &'outer mut Option>; + +/// Spells one node into `out`. +/// Siblings reach the override left to right; upstream visits them right to left. +fn write_node(node: &Node, custom: MaybeCustom<'_, '_>, out: &mut String) { + if let Some(over) = custom.as_mut() + && let Some(replacement) = over(node) + { + out.push_str(&replacement); + return; + } + + match node.kind { + NodeKind::Word | NodeKind::Space => out.push_str(&node.value), + NodeKind::String => { + if let Some(quote) = node.quote { + out.push(quote); + } + out.push_str(&node.value); + if let Some(quote) = node.quote + && !node.unclosed + { + out.push(quote); + } + }, + NodeKind::Comment => { + out.push_str("/*"); + out.push_str(&node.value); + if !node.unclosed { + out.push_str("*/"); + } + }, + NodeKind::Div => { + out.push_str(node.before.as_deref().unwrap_or_default()); + out.push_str(&node.value); + out.push_str(node.after.as_deref().unwrap_or_default()); + }, + // Matches upstream: a non-function node with children spells out as the children alone. + NodeKind::UnicodeRange => match node.nodes.as_deref() { + None => out.push_str(&node.value), + Some(children) => write_list(children, custom, out), + }, + // Matches upstream: a function without a child list spells out as its bare name. + NodeKind::Function => match node.nodes.as_deref() { + None => out.push_str(&node.value), + Some(children) => { + out.push_str(&node.value); + out.push('('); + out.push_str(node.before.as_deref().unwrap_or_default()); + write_list(children, custom, out); + out.push_str(node.after.as_deref().unwrap_or_default()); + if !node.unclosed { + out.push(')'); + } + }, + }, + } +} + +fn write_list(nodes: &[Node], custom: MaybeCustom<'_, '_>, out: &mut String) { + for node in nodes { + write_node(node, custom, out); + } +} + +/// Capacity hint from the span the nodes were parsed from; only a hint. +fn estimated_len(nodes: &[Node]) -> usize { + match (nodes.first(), nodes.last()) { + (Some(first), Some(last)) => last.source_end_index.saturating_sub(first.source_index), + _ => 0, + } +} + +/// Spells a node list back out, in order. +pub fn stringify(nodes: &[Node]) -> String { + let mut out = String::with_capacity(estimated_len(nodes)); + write_list(nodes, &mut None, &mut out); + out +} + +/// Spells one node back out; unclosed nodes stay unclosed. +/// The kind decides the spelling: a re-kinded function spells as its new kind. +pub fn stringify_node(node: &Node) -> String { + let mut out = String::with_capacity(estimated_len(std::slice::from_ref(node))); + write_node(node, &mut None, &mut out); + out +} + +/// [`stringify`], with an override consulted for every node it reaches. +pub fn stringify_with(nodes: &[Node], custom: Custom<'_>) -> String { + let mut out = String::with_capacity(estimated_len(nodes)); + write_list(nodes, &mut Some(custom), &mut out); + out +} + +/// [`stringify_node`], with an override consulted for that node and its descendants. +pub fn stringify_node_with(node: &Node, custom: Custom<'_>) -> String { + let mut out = String::with_capacity(estimated_len(std::slice::from_ref(node))); + write_node(node, &mut Some(custom), &mut out); + out +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests.rs new file mode 100644 index 000000000..525431b27 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests.rs @@ -0,0 +1,7 @@ +//! Tests for the value scanner: [`parity`] against a JavaScript-generated table, and +//! [`properties`] for invariants a table cannot cover. + +mod cases; +mod dump; +mod parity; +mod properties; diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/cases.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/cases.rs new file mode 100644 index 000000000..cca5e9be5 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/cases.rs @@ -0,0 +1,4873 @@ +// @generated by scripts/generate-value-parser-cases.mjs -- do not edit by hand. +// +// Every expectation is a literal answer from the JavaScript being stood in for. +// Regenerate after adding an input to the generator, never by eye: +// +// pnpm run --filter=@stylexswc/postcss-value-parser generate:value-parser-cases + +/// One value, and what the JavaScript does with it. +pub(super) struct ParserCase { + /// The declaration value, as an author would write it. + pub input: &'static str, + /// What serialising the parsed value back out produces. Equal to `input` + /// for all but the `/*/` comment quirk documented on the module. + pub output: &'static str, + /// The node tree, one line per node, nested nodes indented by two. + pub ast: &'static str, +} + +/// 961 values: the differential harness's whole corpus, plus +/// malformed, truncated and degenerate inputs no author would write. +pub(super) const PARSER_CASES: &[ParserCase] = &[ + ParserCase { + input: "opacity 0.2s ease-in-out", + output: "opacity 0.2s ease-in-out", + ast: "word \"opacity\" 0..7\nspace \" \" 7..8\nword \"0.2s\" 8..12\nspace \" \" 12..13\nword \"ease-in-out\" 13..24", + }, + ParserCase { + input: "50% bottom", + output: "50% bottom", + ast: "word \"50%\" 0..3\nspace \" \" 3..4\nword \"bottom\" 4..10", + }, + ParserCase { + input: "-50% -120%", + output: "-50% -120%", + ast: "word \"-50%\" 0..4\nspace \" \" 4..5\nword \"-120%\" 5..10", + }, + ParserCase { + input: "top 0.75rem left 0.625rem", + output: "top 0.75rem left 0.625rem", + ast: "word \"top\" 0..3\nspace \" \" 3..4\nword \"0.75rem\" 4..11\nspace \" \" 11..12\nword \"left\" 12..16\nspace \" \" 16..17\nword \"0.625rem\" 17..25", + }, + ParserCase { + input: "transparent dotted 0.125rem", + output: "transparent dotted 0.125rem", + ast: "word \"transparent\" 0..11\nspace \" \" 11..12\nword \"dotted\" 12..18\nspace \" \" 18..19\nword \"0.125rem\" 19..27", + }, + ParserCase { + input: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%)", + output: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%)", + ast: "function \"linear-gradient\" 0..72 before=\"\" after=\"\" nodes=11\n word \"to\" 16..18\n space \" \" 18..19\n word \"bottom\" 19..25\n div \",\" 25..27 before=\"\" after=\" \"\n function \"rgba\" 27..43 before=\"\" after=\"\" nodes=7\n word \"0\" 32..33\n div \",\" 33..35 before=\"\" after=\" \"\n word \"0\" 35..36\n div \",\" 36..38 before=\"\" after=\" \"\n word \"0\" 38..39\n div \",\" 39..41 before=\"\" after=\" \"\n word \"0\" 41..42\n space \" \" 43..44\n word \"0%\" 44..46\n div \",\" 46..48 before=\"\" after=\" \"\n function \"rgba\" 48..66 before=\"\" after=\"\" nodes=7\n word \"0\" 53..54\n div \",\" 54..56 before=\"\" after=\" \"\n word \"0\" 56..57\n div \",\" 57..59 before=\"\" after=\" \"\n word \"0\" 59..60\n div \",\" 60..62 before=\"\" after=\" \"\n word \"0.6\" 62..65\n space \" \" 66..67\n word \"100%\" 67..71", + }, + ParserCase { + input: "calc(-1 * var(--spacing))", + output: "calc(-1 * var(--spacing))", + ast: "function \"calc\" 0..25 before=\"\" after=\"\" nodes=5\n word \"-1\" 5..7\n space \" \" 7..8\n word \"*\" 8..9\n space \" \" 9..10\n function \"var\" 10..24 before=\"\" after=\"\" nodes=1\n word \"--spacing\" 14..23", + }, + ParserCase { + input: "max(4.8125rem, 100vw * 0.12)", + output: "max(4.8125rem, 100vw * 0.12)", + ast: "function \"max\" 0..28 before=\"\" after=\"\" nodes=7\n word \"4.8125rem\" 4..13\n div \",\" 13..15 before=\"\" after=\" \"\n word \"100vw\" 15..20\n space \" \" 20..21\n word \"*\" 21..22\n space \" \" 22..23\n word \"0.12\" 23..27", + }, + ParserCase { + input: "calc(var(--b) * var(--c)) 0", + output: "calc(var(--b) * var(--c)) 0", + ast: "function \"calc\" 0..25 before=\"\" after=\"\" nodes=5\n function \"var\" 5..13 before=\"\" after=\"\" nodes=1\n word \"--b\" 9..12\n space \" \" 13..14\n word \"*\" 14..15\n space \" \" 15..16\n function \"var\" 16..24 before=\"\" after=\"\" nodes=1\n word \"--c\" 20..23\nspace \" \" 25..26\nword \"0\" 26..27", + }, + ParserCase { + input: "#ffffff", + output: "#ffffff", + ast: "word \"#ffffff\" 0..7", + }, + ParserCase { + input: "linear-gradient(#000000, #ffffff)", + output: "linear-gradient(#000000, #ffffff)", + ast: "function \"linear-gradient\" 0..33 before=\"\" after=\"\" nodes=3\n word \"#000000\" 16..23\n div \",\" 23..25 before=\"\" after=\" \"\n word \"#ffffff\" 25..32", + }, + ParserCase { + input: "'sidebar content'", + output: "'sidebar content'", + ast: "string \"sidebar content\" 0..17 quote=\"'\"", + }, + ParserCase { + input: "translateX(-50%) translateY(-50%)", + output: "translateX(-50%) translateY(-50%)", + ast: "function \"translateX\" 0..16 before=\"\" after=\"\" nodes=1\n word \"-50%\" 11..15\nspace \" \" 16..17\nfunction \"translateY\" 17..33 before=\"\" after=\"\" nodes=1\n word \"-50%\" 28..32", + }, + ParserCase { + input: "-10000px", + output: "-10000px", + ast: "word \"-10000px\" 0..8", + }, + ParserCase { + input: "\"→ Привет 日本語 🙂\"", + output: "\"→ Привет 日本語 🙂\"", + ast: "string \"→ Привет 日本語 🙂\" 0..33 quote=\"\\\"\"", + }, + ParserCase { + input: "\"\\201C\"", + output: "\"\\201C\"", + ast: "string \"\\\\201C\" 0..7 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a \\\" b\"", + output: "\"a \\\" b\"", + ast: "string \"a \\\\\\\" b\" 0..8 quote=\"\\\"\"", + }, + ParserCase { + input: "'a \\' b'", + output: "'a \\' b'", + ast: "string \"a \\\\' b\" 0..8 quote=\"'\"", + }, + ParserCase { + input: "\"\"", + output: "\"\"", + ast: "string \"\" 0..2 quote=\"\\\"\"", + }, + ParserCase { + input: "'Helvetica Neue', \"Segoe UI\", sans-serif", + output: "'Helvetica Neue', \"Segoe UI\", sans-serif", + ast: "string \"Helvetica Neue\" 0..16 quote=\"'\"\ndiv \",\" 16..18 before=\"\" after=\" \"\nstring \"Segoe UI\" 18..28 quote=\"\\\"\"\ndiv \",\" 28..30 before=\"\" after=\" \"\nword \"sans-serif\" 30..40", + }, + ParserCase { + input: "url(\"a;b{c}d: e /* f */\")", + output: "url(\"a;b{c}d: e /* f */\")", + ast: "function \"url\" 0..25 before=\"\" after=\"\" nodes=1\n string \"a;b{c}d: e /* f */\" 4..24 quote=\"\\\"\"", + }, + ParserCase { + input: "url(image.png?a=1&b=2)", + output: "url(image.png?a=1&b=2)", + ast: "function \"url\" 0..22 before=\"\" after=\"\" nodes=1\n word \"image.png?a=1&b=2\" 4..21", + }, + ParserCase { + input: "url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\")", + output: "url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\")", + ast: "function \"url\" 0..93 before=\"\" after=\"\" nodes=1\n string \"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\" 4..92 quote=\"\\\"\"", + }, + ParserCase { + input: "url(\"asset.png\"), linear-gradient(red, blue)", + output: "url(\"asset.png\"), linear-gradient(red, blue)", + ast: "function \"url\" 0..16 before=\"\" after=\"\" nodes=1\n string \"asset.png\" 4..15 quote=\"\\\"\"\ndiv \",\" 16..18 before=\"\" after=\" \"\nfunction \"linear-gradient\" 18..44 before=\"\" after=\"\" nodes=3\n word \"red\" 34..37\n div \",\" 37..39 before=\"\" after=\" \"\n word \"blue\" 39..43", + }, + ParserCase { + input: "calc(100% /* half */ - 20px)", + output: "calc(100% /* half */ - 20px)", + ast: "function \"calc\" 0..28 before=\"\" after=\"\" nodes=7\n word \"100%\" 5..9\n space \" \" 9..10\n comment \" half \" 10..20\n space \" \" 20..21\n word \"-\" 21..22\n space \" \" 22..23\n word \"20px\" 23..27", + }, + ParserCase { + input: "/* a */ red", + output: "/* a */ red", + ast: "comment \" a \" 0..7\nspace \" \" 7..8\nword \"red\" 8..11", + }, + ParserCase { + input: "red !important", + output: "red !important", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"!important\" 4..14", + }, + ParserCase { + input: "red ! important", + output: "red ! important", + ast: "word \"red\" 0..3\nspace \" \" 3..6\nword \"!\" 6..7\nspace \" \" 7..10\nword \"important\" 10..19", + }, + ParserCase { + input: "", + output: "", + ast: "", + }, + ParserCase { + input: " ", + output: " ", + ast: "space \" \" 0..3", + }, + ParserCase { + input: "calc(100% - 20px", + output: "calc(100% - 20px", + ast: "function \"calc\" 0..16 before=\"\" after=\"\" unclosed nodes=5\n word \"100%\" 5..9\n space \" \" 9..10\n word \"-\" 10..11\n space \" \" 11..12\n word \"20px\" 12..16", + }, + ParserCase { + input: "\"unterminated", + output: "\"unterminated", + ast: "string \"unterminated\" 0..13 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "red /* unterminated", + output: "red /* unterminated", + ast: "word \"red\" 0..3\nspace \" \" 3..4\ncomment \" unterminated\" 4..19 unclosed", + }, + ParserCase { + input: "1px solid } color: red", + output: "1px solid } color: red", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"}\" 10..11\nspace \" \" 11..12\nword \"color\" 12..17\ndiv \":\" 17..19 before=\"\" after=\" \"\nword \"red\" 19..22", + }, + ParserCase { + input: "var(x)", + output: "var(x)", + ast: "function \"var\" 0..6 before=\"\" after=\"\" nodes=1\n word \"x\" 4..5", + }, + ParserCase { + input: "var(--x, var(--y, #ABCDEF))", + output: "var(--x, var(--y, #ABCDEF))", + ast: "function \"var\" 0..27 before=\"\" after=\"\" nodes=3\n word \"--x\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"var\" 9..26 before=\"\" after=\"\" nodes=3\n word \"--y\" 13..16\n div \",\" 16..18 before=\"\" after=\" \"\n word \"#ABCDEF\" 18..25", + }, + ParserCase { + input: "var(--x)px", + output: "var(--x)px", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"px\" 8..10", + }, + ParserCase { + input: "oklch(from var(--brand) l c h / 50%)", + output: "oklch(from var(--brand) l c h / 50%)", + ast: "function \"oklch\" 0..36 before=\"\" after=\"\" nodes=11\n word \"from\" 6..10\n space \" \" 10..11\n function \"var\" 11..23 before=\"\" after=\"\" nodes=1\n word \"--brand\" 15..22\n space \" \" 23..24\n word \"l\" 24..25\n space \" \" 25..26\n word \"c\" 26..27\n space \" \" 27..28\n word \"h\" 28..29\n div \"/\" 29..32 before=\" \" after=\" \"\n word \"50%\" 32..35", + }, + ParserCase { + input: "calc-size(fit-content, size / 2)", + output: "calc-size(fit-content, size / 2)", + ast: "function \"calc-size\" 0..32 before=\"\" after=\"\" nodes=5\n word \"fit-content\" 10..21\n div \",\" 21..23 before=\"\" after=\" \"\n word \"size\" 23..27\n div \"/\" 27..30 before=\" \" after=\" \"\n word \"2\" 30..31", + }, + ParserCase { + input: "500ms", + output: "500ms", + ast: "word \"500ms\" 0..5", + }, + ParserCase { + input: "5ms", + output: "5ms", + ast: "word \"5ms\" 0..3", + }, + ParserCase { + input: "0px", + output: "0px", + ast: "word \"0px\" 0..3", + }, + ParserCase { + input: "rotate(0deg)", + output: "rotate(0deg)", + ast: "function \"rotate\" 0..12 before=\"\" after=\"\" nodes=1\n word \"0deg\" 7..11", + }, + ParserCase { + input: "0%", + output: "0%", + ast: "word \"0%\" 0..2", + }, + ParserCase { + input: "0fr 1fr", + output: "0fr 1fr", + ast: "word \"0fr\" 0..3\nspace \" \" 3..4\nword \"1fr\" 4..7", + }, + ParserCase { + input: "0.5", + output: "0.5", + ast: "word \"0.5\" 0..3", + }, + ParserCase { + input: "-0.24px", + output: "-0.24px", + ast: "word \"-0.24px\" 0..7", + }, + ParserCase { + input: "backgroundColor", + output: "backgroundColor", + ast: "word \"backgroundColor\" 0..15", + }, + ParserCase { + input: "10000000000000000000000", + output: "10000000000000000000000", + ast: "word \"10000000000000000000000\" 0..23", + }, + ParserCase { + input: "0.30000000000000004px", + output: "0.30000000000000004px", + ast: "word \"0.30000000000000004px\" 0..21", + }, + ParserCase { + input: "UPPERCASE", + output: "UPPERCASE", + ast: "word \"UPPERCASE\" 0..9", + }, + ParserCase { + input: "CALC(100% - 20PX)", + output: "CALC(100% - 20PX)", + ast: "function \"CALC\" 0..17 before=\"\" after=\"\" nodes=5\n word \"100%\" 5..9\n space \" \" 9..10\n word \"-\" 10..11\n space \" \" 11..12\n word \"20PX\" 12..16", + }, + ParserCase { + input: "'a a' 'b c'", + output: "'a a' 'b c'", + ast: "string \"a a\" 0..5 quote=\"'\"\nspace \" \" 5..6\nstring \"b c\" 6..11 quote=\"'\"", + }, + ParserCase { + input: "1px\t2px\n3px 4px", + output: "1px\t2px\n3px 4px", + ast: "word \"1px\" 0..3\nspace \"\\t\" 3..4\nword \"2px\" 4..7\nspace \"\\n\" 7..8\nword \"3px\" 8..11\nspace \" \" 11..13\nword \"4px\" 13..16", + }, + ParserCase { + input: " rotate(10deg) translate3d( 0 , 0 , 0 ) ", + output: " rotate(10deg) translate3d( 0 , 0 , 0 ) ", + ast: "space \" \" 0..2\nfunction \"rotate\" 2..15 before=\"\" after=\"\" nodes=1\n word \"10deg\" 9..14\nspace \" \" 15..17\nfunction \"translate3d\" 17..41 before=\" \" after=\" \" nodes=5\n word \"0\" 30..31\n div \",\" 31..34 before=\" \" after=\" \"\n word \"0\" 34..35\n div \",\" 35..38 before=\" \" after=\" \"\n word \"0\" 38..39\nspace \" \" 41..43", + }, + ParserCase { + input: "calc(calc(calc(calc(calc(calc(calc(calc(1px + 2px) + 3px) + 4px) + 5px) + 6px) + 7px) + 8px) + 9px)", + output: "calc(calc(calc(calc(calc(calc(calc(calc(1px + 2px) + 3px) + 4px) + 5px) + 6px) + 7px) + 8px) + 9px)", + ast: "function \"calc\" 0..99 before=\"\" after=\"\" nodes=5\n function \"calc\" 5..92 before=\"\" after=\"\" nodes=5\n function \"calc\" 10..85 before=\"\" after=\"\" nodes=5\n function \"calc\" 15..78 before=\"\" after=\"\" nodes=5\n function \"calc\" 20..71 before=\"\" after=\"\" nodes=5\n function \"calc\" 25..64 before=\"\" after=\"\" nodes=5\n function \"calc\" 30..57 before=\"\" after=\"\" nodes=5\n function \"calc\" 35..50 before=\"\" after=\"\" nodes=5\n word \"1px\" 40..43\n space \" \" 43..44\n word \"+\" 44..45\n space \" \" 45..46\n word \"2px\" 46..49\n space \" \" 50..51\n word \"+\" 51..52\n space \" \" 52..53\n word \"3px\" 53..56\n space \" \" 57..58\n word \"+\" 58..59\n space \" \" 59..60\n word \"4px\" 60..63\n space \" \" 64..65\n word \"+\" 65..66\n space \" \" 66..67\n word \"5px\" 67..70\n space \" \" 71..72\n word \"+\" 72..73\n space \" \" 73..74\n word \"6px\" 74..77\n space \" \" 78..79\n word \"+\" 79..80\n space \" \" 80..81\n word \"7px\" 81..84\n space \" \" 85..86\n word \"+\" 86..87\n space \" \" 87..88\n word \"8px\" 88..91\n space \" \" 92..93\n word \"+\" 93..94\n space \" \" 94..95\n word \"9px\" 95..98", + }, + ParserCase { + input: "calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(1px)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))", + output: "calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(calc(1px)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))", + ast: "function \"calc\" 0..393 before=\"\" after=\"\" nodes=1\n function \"calc\" 5..392 before=\"\" after=\"\" nodes=1\n function \"calc\" 10..391 before=\"\" after=\"\" nodes=1\n function \"calc\" 15..390 before=\"\" after=\"\" nodes=1\n function \"calc\" 20..389 before=\"\" after=\"\" nodes=1\n function \"calc\" 25..388 before=\"\" after=\"\" nodes=1\n function \"calc\" 30..387 before=\"\" after=\"\" nodes=1\n function \"calc\" 35..386 before=\"\" after=\"\" nodes=1\n function \"calc\" 40..385 before=\"\" after=\"\" nodes=1\n function \"calc\" 45..384 before=\"\" after=\"\" nodes=1\n function \"calc\" 50..383 before=\"\" after=\"\" nodes=1\n function \"calc\" 55..382 before=\"\" after=\"\" nodes=1\n function \"calc\" 60..381 before=\"\" after=\"\" nodes=1\n function \"calc\" 65..380 before=\"\" after=\"\" nodes=1\n function \"calc\" 70..379 before=\"\" after=\"\" nodes=1\n function \"calc\" 75..378 before=\"\" after=\"\" nodes=1\n function \"calc\" 80..377 before=\"\" after=\"\" nodes=1\n function \"calc\" 85..376 before=\"\" after=\"\" nodes=1\n function \"calc\" 90..375 before=\"\" after=\"\" nodes=1\n function \"calc\" 95..374 before=\"\" after=\"\" nodes=1\n function \"calc\" 100..373 before=\"\" after=\"\" nodes=1\n function \"calc\" 105..372 before=\"\" after=\"\" nodes=1\n function \"calc\" 110..371 before=\"\" after=\"\" nodes=1\n function \"calc\" 115..370 before=\"\" after=\"\" nodes=1\n function \"calc\" 120..369 before=\"\" after=\"\" nodes=1\n function \"calc\" 125..368 before=\"\" after=\"\" nodes=1\n function \"calc\" 130..367 before=\"\" after=\"\" nodes=1\n function \"calc\" 135..366 before=\"\" after=\"\" nodes=1\n function \"calc\" 140..365 before=\"\" after=\"\" nodes=1\n function \"calc\" 145..364 before=\"\" after=\"\" nodes=1\n function \"calc\" 150..363 before=\"\" after=\"\" nodes=1\n function \"calc\" 155..362 before=\"\" after=\"\" nodes=1\n function \"calc\" 160..361 before=\"\" after=\"\" nodes=1\n function \"calc\" 165..360 before=\"\" after=\"\" nodes=1\n function \"calc\" 170..359 before=\"\" after=\"\" nodes=1\n function \"calc\" 175..358 before=\"\" after=\"\" nodes=1\n function \"calc\" 180..357 before=\"\" after=\"\" nodes=1\n function \"calc\" 185..356 before=\"\" after=\"\" nodes=1\n function \"calc\" 190..355 before=\"\" after=\"\" nodes=1\n function \"calc\" 195..354 before=\"\" after=\"\" nodes=1\n function \"calc\" 200..353 before=\"\" after=\"\" nodes=1\n function \"calc\" 205..352 before=\"\" after=\"\" nodes=1\n function \"calc\" 210..351 before=\"\" after=\"\" nodes=1\n function \"calc\" 215..350 before=\"\" after=\"\" nodes=1\n function \"calc\" 220..349 before=\"\" after=\"\" nodes=1\n function \"calc\" 225..348 before=\"\" after=\"\" nodes=1\n function \"calc\" 230..347 before=\"\" after=\"\" nodes=1\n function \"calc\" 235..346 before=\"\" after=\"\" nodes=1\n function \"calc\" 240..345 before=\"\" after=\"\" nodes=1\n function \"calc\" 245..344 before=\"\" after=\"\" nodes=1\n function \"calc\" 250..343 before=\"\" after=\"\" nodes=1\n function \"calc\" 255..342 before=\"\" after=\"\" nodes=1\n function \"calc\" 260..341 before=\"\" after=\"\" nodes=1\n function \"calc\" 265..340 before=\"\" after=\"\" nodes=1\n function \"calc\" 270..339 before=\"\" after=\"\" nodes=1\n function \"calc\" 275..338 before=\"\" after=\"\" nodes=1\n function \"calc\" 280..337 before=\"\" after=\"\" nodes=1\n function \"calc\" 285..336 before=\"\" after=\"\" nodes=1\n function \"calc\" 290..335 before=\"\" after=\"\" nodes=1\n function \"calc\" 295..334 before=\"\" after=\"\" nodes=1\n function \"calc\" 300..333 before=\"\" after=\"\" nodes=1\n function \"calc\" 305..332 before=\"\" after=\"\" nodes=1\n function \"calc\" 310..331 before=\"\" after=\"\" nodes=1\n function \"calc\" 315..330 before=\"\" after=\"\" nodes=1\n function \"calc\" 320..329 before=\"\" after=\"\" nodes=1\n word \"1px\" 325..328", + }, + ParserCase { + input: "var(--a, var(--b, var(--c, var(--d, var(--e, var(--f, red))))))", + output: "var(--a, var(--b, var(--c, var(--d, var(--e, var(--f, red))))))", + ast: "function \"var\" 0..63 before=\"\" after=\"\" nodes=3\n word \"--a\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"var\" 9..62 before=\"\" after=\"\" nodes=3\n word \"--b\" 13..16\n div \",\" 16..18 before=\"\" after=\" \"\n function \"var\" 18..61 before=\"\" after=\"\" nodes=3\n word \"--c\" 22..25\n div \",\" 25..27 before=\"\" after=\" \"\n function \"var\" 27..60 before=\"\" after=\"\" nodes=3\n word \"--d\" 31..34\n div \",\" 34..36 before=\"\" after=\" \"\n function \"var\" 36..59 before=\"\" after=\"\" nodes=3\n word \"--e\" 40..43\n div \",\" 43..45 before=\"\" after=\" \"\n function \"var\" 45..58 before=\"\" after=\"\" nodes=3\n word \"--f\" 49..52\n div \",\" 52..54 before=\"\" after=\" \"\n word \"red\" 54..57", + }, + ParserCase { + input: "-webkit-box", + output: "-webkit-box", + ast: "word \"-webkit-box\" 0..11", + }, + ParserCase { + input: "-webkit-linear-gradient(top, #FFFFFF, #000000)", + output: "-webkit-linear-gradient(top, #FFFFFF, #000000)", + ast: "function \"-webkit-linear-gradient\" 0..46 before=\"\" after=\"\" nodes=5\n word \"top\" 24..27\n div \",\" 27..29 before=\"\" after=\" \"\n word \"#FFFFFF\" 29..36\n div \",\" 36..38 before=\"\" after=\" \"\n word \"#000000\" 38..45", + }, + ParserCase { + input: "WebkitTransform", + output: "WebkitTransform", + ast: "word \"WebkitTransform\" 0..15", + }, + ParserCase { + input: "-webkit-transform", + output: "-webkit-transform", + ast: "word \"-webkit-transform\" 0..17", + }, + ParserCase { + input: "\"\\2014 A\"", + output: "\"\\2014 A\"", + ast: "string \"\\\\2014 A\" 0..9 quote=\"\\\"\"", + }, + ParserCase { + input: "\"\\1F600\"", + output: "\"\\1F600\"", + ast: "string \"\\\\1F600\" 0..8 quote=\"\\\"\"", + }, + ParserCase { + input: "\"\\2014 A\", sans-serif", + output: "\"\\2014 A\", sans-serif", + ast: "string \"\\\\2014 A\" 0..9 quote=\"\\\"\"\ndiv \",\" 9..11 before=\"\" after=\" \"\nword \"sans-serif\" 11..21", + }, + ParserCase { + input: "\"\\1F600\", sans-serif", + output: "\"\\1F600\", sans-serif", + ast: "string \"\\\\1F600\" 0..8 quote=\"\\\"\"\ndiv \",\" 8..10 before=\"\" after=\" \"\nword \"sans-serif\" 10..20", + }, + ParserCase { + input: "\"→ Привет 日本語 🙂\", sans-serif", + output: "\"→ Привет 日本語 🙂\", sans-serif", + ast: "string \"→ Привет 日本語 🙂\" 0..33 quote=\"\\\"\"\ndiv \",\" 33..35 before=\"\" after=\" \"\nword \"sans-serif\" 35..45", + }, + ParserCase { + input: "My\\ Font, sans-serif", + output: "My\\ Font, sans-serif", + ast: "word \"My\\\\ Font\" 0..8\ndiv \",\" 8..10 before=\"\" after=\" \"\nword \"sans-serif\" 10..20", + }, + ParserCase { + input: "日本語フォント, sans-serif", + output: "日本語フォント, sans-serif", + ast: "word \"日本語フォント\" 0..21\ndiv \",\" 21..23 before=\"\" after=\" \"\nword \"sans-serif\" 23..33", + }, + ParserCase { + input: "[full-start 1fr [content-start", + output: "[full-start 1fr [content-start", + ast: "word \"[full-start\" 0..11\nspace \" \" 11..12\nword \"1fr\" 12..15\nspace \" \" 15..16\nword \"[content-start\" 16..30", + }, + ParserCase { + input: "red {", + output: "red {", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"{\" 4..5", + }, + ParserCase { + input: "10px ++ 20px", + output: "10px ++ 20px", + ast: "word \"10px\" 0..4\nspace \" \" 4..5\nword \"++\" 5..7\nspace \" \" 7..8\nword \"20px\" 8..12", + }, + ParserCase { + input: "0 0 1px #000, 0 0 2px #000, 0 0 3px #000, 0 0 4px #000, 0 0 5px #000, 0 0 6px #000, 0 0 7px #000, 0 0 8px #000, 0 0 9px #000, 0 0 10px #000", + output: "0 0 1px #000, 0 0 2px #000, 0 0 3px #000, 0 0 4px #000, 0 0 5px #000, 0 0 6px #000, 0 0 7px #000, 0 0 8px #000, 0 0 9px #000, 0 0 10px #000", + ast: "word \"0\" 0..1\nspace \" \" 1..2\nword \"0\" 2..3\nspace \" \" 3..4\nword \"1px\" 4..7\nspace \" \" 7..8\nword \"#000\" 8..12\ndiv \",\" 12..14 before=\"\" after=\" \"\nword \"0\" 14..15\nspace \" \" 15..16\nword \"0\" 16..17\nspace \" \" 17..18\nword \"2px\" 18..21\nspace \" \" 21..22\nword \"#000\" 22..26\ndiv \",\" 26..28 before=\"\" after=\" \"\nword \"0\" 28..29\nspace \" \" 29..30\nword \"0\" 30..31\nspace \" \" 31..32\nword \"3px\" 32..35\nspace \" \" 35..36\nword \"#000\" 36..40\ndiv \",\" 40..42 before=\"\" after=\" \"\nword \"0\" 42..43\nspace \" \" 43..44\nword \"0\" 44..45\nspace \" \" 45..46\nword \"4px\" 46..49\nspace \" \" 49..50\nword \"#000\" 50..54\ndiv \",\" 54..56 before=\"\" after=\" \"\nword \"0\" 56..57\nspace \" \" 57..58\nword \"0\" 58..59\nspace \" \" 59..60\nword \"5px\" 60..63\nspace \" \" 63..64\nword \"#000\" 64..68\ndiv \",\" 68..70 before=\"\" after=\" \"\nword \"0\" 70..71\nspace \" \" 71..72\nword \"0\" 72..73\nspace \" \" 73..74\nword \"6px\" 74..77\nspace \" \" 77..78\nword \"#000\" 78..82\ndiv \",\" 82..84 before=\"\" after=\" \"\nword \"0\" 84..85\nspace \" \" 85..86\nword \"0\" 86..87\nspace \" \" 87..88\nword \"7px\" 88..91\nspace \" \" 91..92\nword \"#000\" 92..96\ndiv \",\" 96..98 before=\"\" after=\" \"\nword \"0\" 98..99\nspace \" \" 99..100\nword \"0\" 100..101\nspace \" \" 101..102\nword \"8px\" 102..105\nspace \" \" 105..106\nword \"#000\" 106..110\ndiv \",\" 110..112 before=\"\" after=\" \"\nword \"0\" 112..113\nspace \" \" 113..114\nword \"0\" 114..115\nspace \" \" 115..116\nword \"9px\" 116..119\nspace \" \" 119..120\nword \"#000\" 120..124\ndiv \",\" 124..126 before=\"\" after=\" \"\nword \"0\" 126..127\nspace \" \" 127..128\nword \"0\" 128..129\nspace \" \" 129..130\nword \"10px\" 130..134\nspace \" \" 134..135\nword \"#000\" 135..139", + }, + ParserCase { + input: "var(--web-wash);", + output: "var(--web-wash);", + ast: "function \"var\" 0..15 before=\"\" after=\"\" nodes=1\n word \"--web-wash\" 4..14\nword \";\" 15..16", + }, + ParserCase { + input: "red ; ", + output: "red ; ", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \";\" 4..5\nspace \" \" 5..6", + }, + ParserCase { + input: "red;;", + output: "red;;", + ast: "word \"red;;\" 0..5", + }, + ParserCase { + input: "red; margin: 10px", + output: "red; margin: 10px", + ast: "word \"red;\" 0..4\nspace \" \" 4..5\nword \"margin\" 5..11\ndiv \":\" 11..13 before=\"\" after=\" \"\nword \"10px\" 13..17", + }, + ParserCase { + input: "opacity,margin-top", + output: "opacity,margin-top", + ast: "word \"opacity\" 0..7\ndiv \",\" 7..8 before=\"\" after=\"\"\nword \"margin-top\" 8..18", + }, + ParserCase { + input: "0 2px 4px var(--shadow-1)", + output: "0 2px 4px var(--shadow-1)", + ast: "word \"0\" 0..1\nspace \" \" 1..2\nword \"2px\" 2..5\nspace \" \" 5..6\nword \"4px\" 6..9\nspace \" \" 9..10\nfunction \"var\" 10..25 before=\"\" after=\"\" nodes=1\n word \"--shadow-1\" 14..24", + }, + ParserCase { + input: "1px 1px#000", + output: "1px 1px#000", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"1px#000\" 4..11", + }, + ParserCase { + input: ".5", + output: ".5", + ast: "word \".5\" 0..2", + }, + ParserCase { + input: ".5s", + output: ".5s", + ast: "word \".5s\" 0..3", + }, + ParserCase { + input: "var(--x)rem", + output: "var(--x)rem", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"rem\" 8..11", + }, + ParserCase { + input: "var(--x)vh", + output: "var(--x)vh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"vh\" 8..10", + }, + ParserCase { + input: "var(--x)turn", + output: "var(--x)turn", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"turn\" 8..12", + }, + ParserCase { + input: "var(--a)deg", + output: "var(--a)deg", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--a\" 4..7\nword \"deg\" 8..11", + }, + ParserCase { + input: "'a\\'b'", + output: "'a\\'b'", + ast: "string \"a\\\\'b\" 0..6 quote=\"'\"", + }, + ParserCase { + input: "'hello'", + output: "'hello'", + ast: "string \"hello\" 0..7 quote=\"'\"", + }, + ParserCase { + input: "*{{{color:green}}}", + output: "*{{{color:green}}}", + ast: "word \"*{{{color\" 0..9\ndiv \":\" 9..10 before=\"\" after=\"\"\nword \"green}}}\" 10..18", + }, + ParserCase { + input: "color:red", + output: "color:red", + ast: "word \"color\" 0..5\ndiv \":\" 5..6 before=\"\" after=\"\"\nword \"red\" 6..9", + }, + ParserCase { + input: "just-text", + output: "just-text", + ast: "word \"just-text\" 0..9", + }, + ParserCase { + input: "red; margin:0", + output: "red; margin:0", + ast: "word \"red;\" 0..4\nspace \" \" 4..5\nword \"margin\" 5..11\ndiv \":\" 11..12 before=\"\" after=\"\"\nword \"0\" 12..13", + }, + ParserCase { + input: "var(--a); background: var(--b)", + output: "var(--a); background: var(--b)", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--a\" 4..7\nword \";\" 8..9\nspace \" \" 9..10\nword \"background\" 10..20\ndiv \":\" 20..22 before=\"\" after=\" \"\nfunction \"var\" 22..30 before=\"\" after=\"\" nodes=1\n word \"--b\" 26..29", + }, + ParserCase { + input: "rgb(var(--r), 0, 0); background: var(--bg)", + output: "rgb(var(--r), 0, 0); background: var(--bg)", + ast: "function \"rgb\" 0..19 before=\"\" after=\"\" nodes=5\n function \"var\" 4..12 before=\"\" after=\"\" nodes=1\n word \"--r\" 8..11\n div \",\" 12..14 before=\"\" after=\" \"\n word \"0\" 14..15\n div \",\" 15..17 before=\"\" after=\" \"\n word \"0\" 17..18\nword \";\" 19..20\nspace \" \" 20..21\nword \"background\" 21..31\ndiv \":\" 31..33 before=\"\" after=\" \"\nfunction \"var\" 33..42 before=\"\" after=\"\" nodes=1\n word \"--bg\" 37..41", + }, + ParserCase { + input: "var(--xColor); background: var(--xBg); border-color: var(--xBorder)", + output: "var(--xColor); background: var(--xBg); border-color: var(--xBorder)", + ast: "function \"var\" 0..13 before=\"\" after=\"\" nodes=1\n word \"--xColor\" 4..12\nword \";\" 13..14\nspace \" \" 14..15\nword \"background\" 15..25\ndiv \":\" 25..27 before=\"\" after=\" \"\nfunction \"var\" 27..37 before=\"\" after=\"\" nodes=1\n word \"--xBg\" 31..36\nword \";\" 37..38\nspace \" \" 38..39\nword \"border-color\" 39..51\ndiv \":\" 51..53 before=\"\" after=\" \"\nfunction \"var\" 53..67 before=\"\" after=\"\" nodes=1\n word \"--xBorder\" 57..66", + }, + ParserCase { + input: "block; color: blue", + output: "block; color: blue", + ast: "word \"block;\" 0..6\nspace \" \" 6..7\nword \"color\" 7..12\ndiv \":\" 12..14 before=\"\" after=\" \"\nword \"blue\" 14..18", + }, + ParserCase { + input: "10px; padding: 0", + output: "10px; padding: 0", + ast: "word \"10px;\" 0..5\nspace \" \" 5..6\nword \"padding\" 6..13\ndiv \":\" 13..15 before=\"\" after=\" \"\nword \"0\" 15..16", + }, + ParserCase { + input: "calc(var(--x) + 10px)", + output: "calc(var(--x) + 10px)", + ast: "function \"calc\" 0..21 before=\"\" after=\"\" nodes=5\n function \"var\" 5..13 before=\"\" after=\"\" nodes=1\n word \"--x\" 9..12\n space \" \" 13..14\n word \"+\" 14..15\n space \" \" 15..16\n word \"10px\" 16..20", + }, + ParserCase { + input: "()/", + output: "()/", + ast: "function \"\" 0..2 before=\"\" after=\"\" nodes=0\ndiv \"/\" 2..3 before=\"\" after=\"\"", + }, + ParserCase { + input: "00)", + output: "00)", + ast: "word \"00)\" 0..3", + }, + ParserCase { + input: "00\\", + output: "00\\", + ast: "word \"00\\\\\" 0..4", + }, + ParserCase { + input: "00*", + output: "00*", + ast: "word \"00*\" 0..3", + }, + ParserCase { + input: "1e309px", + output: "1e309px", + ast: "word \"1e309px\" 0..7", + }, + ParserCase { + input: "1e-324px", + output: "1e-324px", + ast: "word \"1e-324px\" 0..8", + }, + ParserCase { + input: "1.7976931348623157e308px", + output: "1.7976931348623157e308px", + ast: "word \"1.7976931348623157e308px\" 0..24", + }, + ParserCase { + input: "5e-324px", + output: "5e-324px", + ast: "word \"5e-324px\" 0..8", + }, + ParserCase { + input: "9007199254740993px", + output: "9007199254740993px", + ast: "word \"9007199254740993px\" 0..18", + }, + ParserCase { + input: "123456789012345678901234567890px", + output: "123456789012345678901234567890px", + ast: "word \"123456789012345678901234567890px\" 0..32", + }, + ParserCase { + input: "1epx", + output: "1epx", + ast: "word \"1epx\" 0..4", + }, + ParserCase { + input: ".", + output: ".", + ast: "word \".\" 0..1", + }, + ParserCase { + input: "0.px", + output: "0.px", + ast: "word \"0.px\" 0..4", + }, + ParserCase { + input: "-0px", + output: "-0px", + ast: "word \"-0px\" 0..4", + }, + ParserCase { + input: "0.12345678901234567890123456789", + output: "0.12345678901234567890123456789", + ast: "word \"0.12345678901234567890123456789\" 0..31", + }, + ParserCase { + input: "\"é\"", + output: "\"é\"", + ast: "string \"é\" 0..5 quote=\"\\\"\"", + }, + ParserCase { + input: "é", + output: "é", + ast: "word \"é\" 0..3", + }, + ParserCase { + input: "\"a‏b\"", + output: "\"a‏b\"", + ast: "string \"a‏b\" 0..7 quote=\"\\\"\"", + }, + ParserCase { + input: "\"ab\"", + output: "\"ab\"", + ast: "string \"ab\" 0..7 quote=\"\\\"\"", + }, + ParserCase { + input: "a b", + output: "a b", + ast: "word \"a b\" 0..4", + }, + ParserCase { + input: "\\😀", + output: "\\😀", + ast: "word \"\\\\😀\" 0..5", + }, + ParserCase { + input: "1😀px", + output: "1😀px", + ast: "word \"1😀px\" 0..7", + }, + ParserCase { + input: "a() 1px /2", + output: "a() 1px /2", + ast: "function \"a\" 0..3 before=\"\" after=\"\" nodes=0\nspace \" \" 3..4\nword \"1px\" 4..7\nspace \" \" 7..8\ndiv \"/\" 8..9 before=\"\" after=\"\"\nword \"2\" 9..10", + }, + ParserCase { + input: "url(x) 1px /2", + output: "url(x) 1px /2", + ast: "function \"url\" 0..6 before=\"\" after=\"\" nodes=1\n word \"x\" 4..5\nspace \" \" 6..7\nword \"1px\" 7..10\ndiv \"/\" 10..12 before=\" \" after=\"\"\nword \"2\" 12..13", + }, + ParserCase { + input: "calc(1) / 2", + output: "calc(1) / 2", + ast: "function \"calc\" 0..7 before=\"\" after=\"\" nodes=1\n word \"1\" 5..6\nspace \" \" 7..8\ndiv \"/\" 8..10 before=\"\" after=\" \"\nword \"2\" 10..11", + }, + ParserCase { + input: "URL(a b)", + output: "URL(a b)", + ast: "function \"URL\" 0..8 before=\"\" after=\"\" nodes=3\n word \"a\" 4..5\n space \" \" 5..6\n word \"b\" 6..7", + }, + ParserCase { + input: "url ( a )", + output: "url ( a )", + ast: "word \"url\" 0..3\nspace \" \" 3..4\nfunction \"\" 4..9 before=\" \" after=\" \" nodes=1\n word \"a\" 6..7", + }, + ParserCase { + input: " ", + output: " ", + ast: "word \" \" 0..3", + }, + ParserCase { + input: " ", + output: " ", + ast: "word \" \" 0..2", + }, + ParserCase { + input: "1px 2px", + output: "1px 2px", + ast: "word \"1px 2px\" 0..9", + }, + ParserCase { + input: "\u{1}", + output: "\u{1}", + ast: "space \"\\u0001\" 0..1", + }, + ParserCase { + input: "red;calc(1px", + output: "red;calc(1px", + ast: "function \"red;calc\" 0..12 before=\"\" after=\"\" unclosed nodes=1\n word \"1px\" 9..12", + }, + ParserCase { + input: "red;\"a", + output: "red;\"a", + ast: "word \"red;\" 0..4\nstring \"a\" 4..6 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "calc(1px;)", + output: "calc(1px;)", + ast: "function \"calc\" 0..10 before=\"\" after=\"\" nodes=1\n word \"1px;\" 5..9", + }, + ParserCase { + input: "notfn", + output: "notfn", + ast: "word \"notfn\" 0..5", + }, + ParserCase { + input: "red", + output: "red", + ast: "word \"red\" 0..3", + }, + ParserCase { + input: "#abcdef", + output: "#abcdef", + ast: "word \"#abcdef\" 0..7", + }, + ParserCase { + input: "foo", + output: "foo", + ast: "word \"foo\" 0..3", + }, + ParserCase { + input: "var(--foo)", + output: "var(--foo)", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=1\n word \"--foo\" 4..9", + }, + ParserCase { + input: "var(--token", + output: "var(--token", + ast: "function \"var\" 0..11 before=\"\" after=\"\" unclosed nodes=1\n word \"--token\" 4..11", + }, + ParserCase { + input: "blue", + output: "blue", + ast: "word \"blue\" 0..4", + }, + ParserCase { + input: "green", + output: "green", + ast: "word \"green\" 0..5", + }, + ParserCase { + input: "red;a", + output: "red;a", + ast: "word \"red;a\" 0..5", + }, + ParserCase { + input: "#ff0000", + output: "#ff0000", + ast: "word \"#ff0000\" 0..7", + }, + ParserCase { + input: "solid", + output: "solid", + ast: "word \"solid\" 0..5", + }, + ParserCase { + input: "10px", + output: "10px", + ast: "word \"10px\" 0..4", + }, + ParserCase { + input: "1px solid #000", + output: "1px solid #000", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"#000\" 10..14", + }, + ParserCase { + input: "1px", + output: "1px", + ast: "word \"1px\" 0..3", + }, + ParserCase { + input: "unset", + output: "unset", + ast: "word \"unset\" 0..5", + }, + ParserCase { + input: "anim 1s", + output: "anim 1s", + ast: "word \"anim\" 0..4\nspace \" \" 4..5\nword \"1s\" 5..7", + }, + ParserCase { + input: "spin 1s", + output: "spin 1s", + ast: "word \"spin\" 0..4\nspace \" \" 4..5\nword \"1s\" 5..7", + }, + ParserCase { + input: "ignore", + output: "ignore", + ast: "word \"ignore\" 0..6", + }, + ParserCase { + input: "radial-gradient(circle, red, blue)", + output: "radial-gradient(circle, red, blue)", + ast: "function \"radial-gradient\" 0..34 before=\"\" after=\"\" nodes=5\n word \"circle\" 16..22\n div \",\" 22..24 before=\"\" after=\" \"\n word \"red\" 24..27\n div \",\" 27..29 before=\"\" after=\" \"\n word \"blue\" 29..33", + }, + ParserCase { + input: "#F7F5F6", + output: "#F7F5F6", + ast: "word \"#F7F5F6\" 0..7", + }, + ParserCase { + input: "black", + output: "black", + ast: "word \"black\" 0..5", + }, + ParserCase { + input: "purple", + output: "purple", + ast: "word \"purple\" 0..6", + }, + ParserCase { + input: "rgb(0, {channel}, 0)", + output: "rgb(0, {channel}, 0)", + ast: "function \"rgb\" 0..20 before=\"\" after=\"\" nodes=5\n word \"0\" 4..5\n div \",\" 5..7 before=\"\" after=\" \"\n word \"{channel}\" 7..16\n div \",\" 16..18 before=\"\" after=\" \"\n word \"0\" 18..19", + }, + ParserCase { + input: "rgb(0,0,", + output: "rgb(0,0,", + ast: "function \"rgb\" 0..8 before=\"\" after=\"\" unclosed nodes=4\n word \"0\" 4..5\n div \",\" 5..6 before=\"\" after=\"\"\n word \"0\" 6..7\n div \",\" 7..8 before=\"\" after=\"\"", + }, + ParserCase { + input: "rgb(from red r g b)", + output: "rgb(from red r g b)", + ast: "function \"rgb\" 0..19 before=\"\" after=\"\" nodes=9\n word \"from\" 4..8\n space \" \" 8..9\n word \"red\" 9..12\n space \" \" 12..13\n word \"r\" 13..14\n space \" \" 14..15\n word \"g\" 15..16\n space \" \" 16..17\n word \"b\" 17..18", + }, + ParserCase { + input: "rgba(from red r g b / 0.5)", + output: "rgba(from red r g b / 0.5)", + ast: "function \"rgba\" 0..26 before=\"\" after=\"\" nodes=11\n word \"from\" 5..9\n space \" \" 9..10\n word \"red\" 10..13\n space \" \" 13..14\n word \"r\" 14..15\n space \" \" 15..16\n word \"g\" 16..17\n space \" \" 17..18\n word \"b\" 18..19\n div \"/\" 19..22 before=\" \" after=\" \"\n word \"0.5\" 22..25", + }, + ParserCase { + input: "var(----__hashed_var__1jqb1tb)", + output: "var(----__hashed_var__1jqb1tb)", + ast: "function \"var\" 0..30 before=\"\" after=\"\" nodes=1\n word \"----__hashed_var__1jqb1tb\" 4..29", + }, + ParserCase { + input: "var(--background-color)", + output: "var(--background-color)", + ast: "function \"var\" 0..23 before=\"\" after=\"\" nodes=1\n word \"--background-color\" 4..22", + }, + ParserCase { + input: "white", + output: "white", + ast: "word \"white\" 0..5", + }, + ParserCase { + input: "({body})", + output: "({body})", + ast: "function \"\" 0..8 before=\"\" after=\"\" nodes=1\n word \"{body}\" 1..7", + }, + ParserCase { + input: "URL(a}b)", + output: "URL(a}b)", + ast: "function \"URL\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a}b\" 4..7", + }, + ParserCase { + input: "Url(a}b)", + output: "Url(a}b)", + ast: "function \"Url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a}b\" 4..7", + }, + ParserCase { + input: "\\url(a}b)", + output: "\\url(a}b)", + ast: "function \"\\\\url\" 0..9 before=\"\" after=\"\" nodes=1\n word \"a}b\" 5..8", + }, + ParserCase { + input: "a", + output: "a", + ast: "word \"a\" 0..1", + }, + ParserCase { + input: "blurl(a}b)", + output: "blurl(a}b)", + ast: "function \"blurl\" 0..10 before=\"\" after=\"\" nodes=1\n word \"a}b\" 6..9", + }, + ParserCase { + input: "linear-gradient(red,blue),url(http://example.com/a.png)", + output: "linear-gradient(red,blue),url(http://example.com/a.png)", + ast: "function \"linear-gradient\" 0..25 before=\"\" after=\"\" nodes=3\n word \"red\" 16..19\n div \",\" 19..20 before=\"\" after=\"\"\n word \"blue\" 20..24\ndiv \",\" 25..26 before=\"\" after=\"\"\nfunction \"url\" 26..55 before=\"\" after=\"\" nodes=1\n word \"http://example.com/a.png\" 30..54", + }, + ParserCase { + input: "linear-gradient(to right, rgb(from red r g b), blue)", + output: "linear-gradient(to right, rgb(from red r g b), blue)", + ast: "function \"linear-gradient\" 0..52 before=\"\" after=\"\" nodes=7\n word \"to\" 16..18\n space \" \" 18..19\n word \"right\" 19..24\n div \",\" 24..26 before=\"\" after=\" \"\n function \"rgb\" 26..45 before=\"\" after=\"\" nodes=9\n word \"from\" 30..34\n space \" \" 34..35\n word \"red\" 35..38\n space \" \" 38..39\n word \"r\" 39..40\n space \" \" 40..41\n word \"g\" 41..42\n space \" \" 42..43\n word \"b\" 43..44\n div \",\" 45..47 before=\"\" after=\" \"\n word \"blue\" 47..51", + }, + ParserCase { + input: "noturl(a}b)", + output: "noturl(a}b)", + ast: "function \"noturl\" 0..11 before=\"\" after=\"\" nodes=1\n word \"a}b\" 7..10", + }, + ParserCase { + input: "url(", + output: "url(", + ast: "function \"url\" 0..4 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "url(\"/icons/•#hash.svg\")calc(1px)rgb(0,0,0)", + output: "url(\"/icons/•#hash.svg\")calc(1px)rgb(0,0,0)", + ast: "function \"url\" 0..26 before=\"\" after=\"\" nodes=1\n string \"/icons/•#hash.svg\" 4..25 quote=\"\\\"\"\nfunction \"calc\" 26..35 before=\"\" after=\"\" nodes=1\n word \"1px\" 31..34\nfunction \"rgb\" 35..45 before=\"\" after=\"\" nodes=5\n word \"0\" 39..40\n div \",\" 40..41 before=\"\" after=\"\"\n word \"0\" 41..42\n div \",\" 42..43 before=\"\" after=\"\"\n word \"0\" 43..44", + }, + ParserCase { + input: "url(\"a)b.png\") 10px", + output: "url(\"a)b.png\") 10px", + ast: "function \"url\" 0..14 before=\"\" after=\"\" nodes=1\n string \"a)b.png\" 4..13 quote=\"\\\"\"\nspace \" \" 14..15\nword \"10px\" 15..19", + }, + ParserCase { + input: "url(\"a/*b.png\")", + output: "url(\"a/*b.png\")", + ast: "function \"url\" 0..15 before=\"\" after=\"\" nodes=1\n string \"a/*b.png\" 4..14 quote=\"\\\"\"", + }, + ParserCase { + input: "url(\"a\\\")b.png\") 10px", + output: "url(\"a\\\")b.png\") 10px", + ast: "function \"url\" 0..16 before=\"\" after=\"\" nodes=1\n string \"a\\\\\\\")b.png\" 4..15 quote=\"\\\"\"\nspace \" \" 16..17\nword \"10px\" 17..21", + }, + ParserCase { + input: "url(\"asset.png\") no-repeat center/cover", + output: "url(\"asset.png\") no-repeat center/cover", + ast: "function \"url\" 0..16 before=\"\" after=\"\" nodes=1\n string \"asset.png\" 4..15 quote=\"\\\"\"\nspace \" \" 16..17\nword \"no-repeat\" 17..26\nspace \" \" 26..27\nword \"center\" 27..33\ndiv \"/\" 33..34 before=\"\" after=\"\"\nword \"cover\" 34..39", + }, + ParserCase { + input: "url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==\")", + output: "url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==\")", + ast: "function \"url\" 0..173 before=\"\" after=\"\" nodes=1\n string \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==\" 4..172 quote=\"\\\"\"", + }, + ParserCase { + input: "url(\"foo)", + output: "url(\"foo)", + ast: "function \"url\" 0..10 before=\"\" after=\"\" unclosed nodes=1\n string \"foo)\" 4..9 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "url(\"unterminated", + output: "url(\"unterminated", + ast: "function \"url\" 0..18 before=\"\" after=\"\" unclosed nodes=1\n string \"unterminated\" 4..17 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "url(\"var(foo)\")", + output: "url(\"var(foo)\")", + ast: "function \"url\" 0..15 before=\"\" after=\"\" nodes=1\n string \"var(foo)\" 4..14 quote=\"\\\"\"", + }, + ParserCase { + input: "url('a)b.png')calc(1px)", + output: "url('a)b.png')calc(1px)", + ast: "function \"url\" 0..14 before=\"\" after=\"\" nodes=1\n string \"a)b.png\" 4..13 quote=\"'\"\nfunction \"calc\" 14..23 before=\"\" after=\"\" nodes=1\n word \"1px\" 19..22", + }, + ParserCase { + input: "url(a(b(c)).png) 10px", + output: "url(a(b(c)).png) 10px", + ast: "function \"url\" 0..10 before=\"\" after=\"\" nodes=1\n word \"a(b(c\" 4..9\nword \").png)\" 10..16\nspace \" \" 16..17\nword \"10px\" 17..21", + }, + ParserCase { + input: "url(a(b).png)", + output: "url(a(b).png)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a(b\" 4..7\nword \".png)\" 8..13", + }, + ParserCase { + input: "url(a(b).png)calc(1px)", + output: "url(a(b).png)calc(1px)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a(b\" 4..7\nfunction \".png)calc\" 8..22 before=\"\" after=\"\" nodes=1\n word \"1px\" 18..21", + }, + ParserCase { + input: "url(a.png) no-repeat center/cover", + output: "url(a.png) no-repeat center/cover", + ast: "function \"url\" 0..10 before=\"\" after=\"\" nodes=1\n word \"a.png\" 4..9\nspace \" \" 10..11\nword \"no-repeat\" 11..20\nspace \" \" 20..21\nword \"center\" 21..27\ndiv \"/\" 27..28 before=\"\" after=\"\"\nword \"cover\" 28..33", + }, + ParserCase { + input: "url(a.png)calc(1px)", + output: "url(a.png)calc(1px)", + ast: "function \"url\" 0..10 before=\"\" after=\"\" nodes=1\n word \"a.png\" 4..9\nfunction \"calc\" 10..19 before=\"\" after=\"\" nodes=1\n word \"1px\" 15..18", + }, + ParserCase { + input: "url(a/*b)", + output: "url(a/*b)", + ast: "function \"url\" 0..9 before=\"\" after=\"\" nodes=1\n word \"a/*b\" 4..8", + }, + ParserCase { + input: "url(a/*b.png)", + output: "url(a/*b.png)", + ast: "function \"url\" 0..13 before=\"\" after=\"\" nodes=1\n word \"a/*b.png\" 4..12", + }, + ParserCase { + input: "url(a;b)", + output: "url(a;b)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a;b\" 4..7", + }, + ParserCase { + input: "url(a\\)}b)", + output: "url(a\\)}b)", + ast: "function \"url\" 0..10 before=\"\" after=\"\" nodes=1\n word \"a\\\\)}b\" 4..9", + }, + ParserCase { + input: "url(a\\\\)}b)", + output: "url(a\\\\)}b)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a\\\\\\\\\" 4..7\nword \"}b)\" 8..11", + }, + ParserCase { + input: "url(a{b", + output: "url(a{b", + ast: "function \"url\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \"a{b\" 4..7", + }, + ParserCase { + input: "url(a{b)", + output: "url(a{b)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a{b\" 4..7", + }, + ParserCase { + input: "url(a}b", + output: "url(a}b", + ast: "function \"url\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \"a}b\" 4..7", + }, + ParserCase { + input: "url(a}b)", + output: "url(a}b)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a}b\" 4..7", + }, + ParserCase { + input: "url(data:image/png;base64,AAA", + output: "url(data:image/png;base64,AAA", + ast: "function \"url\" 0..29 before=\"\" after=\"\" unclosed nodes=1\n word \"data:image/png;base64,AAA\" 4..29", + }, + ParserCase { + input: "url(data:image/svg+xml;base64,AA)", + output: "url(data:image/svg+xml;base64,AA)", + ast: "function \"url\" 0..33 before=\"\" after=\"\" nodes=1\n word \"data:image/svg+xml;base64,AA\" 4..32", + }, + ParserCase { + input: "url(data:image/svg+xml;utf8,)", + output: "url(data:image/svg+xml;utf8,)", + ast: "function \"url\" 0..35 before=\"\" after=\"\" nodes=1\n word \"data:image/svg+xml;utf8,\" 4..34", + }, + ParserCase { + input: "url(http://example.com/img.png)", + output: "url(http://example.com/img.png)", + ast: "function \"url\" 0..31 before=\"\" after=\"\" nodes=1\n word \"http://example.com/img.png\" 4..30", + }, + ParserCase { + input: "url(https://fonts.googleapis.com/css2?family=Roboto)", + output: "url(https://fonts.googleapis.com/css2?family=Roboto)", + ast: "function \"url\" 0..52 before=\"\" after=\"\" nodes=1\n word \"https://fonts.googleapis.com/css2?family=Roboto\" 4..51", + }, + ParserCase { + input: "url(image.png)", + output: "url(image.png)", + ast: "function \"url\" 0..14 before=\"\" after=\"\" nodes=1\n word \"image.png\" 4..13", + }, + ParserCase { + input: "url(it's-fine.png)", + output: "url(it's-fine.png)", + ast: "function \"url\" 0..18 before=\"\" after=\"\" nodes=1\n word \"it's-fine.png\" 4..17", + }, + ParserCase { + input: "url(var(foo))", + output: "url(var(foo))", + ast: "function \"url\" 0..12 before=\"\" after=\"\" nodes=1\n word \"var(foo\" 4..11\nword \")\" 12..13", + }, + ParserCase { + input: "url(x{body})", + output: "url(x{body})", + ast: "function \"url\" 0..12 before=\"\" after=\"\" nodes=1\n word \"x{body}\" 4..11", + }, + ParserCase { + input: "100%.5", + output: "100%.5", + ast: "word \"100%.5\" 0..6", + }, + ParserCase { + input: "50%#fff", + output: "50%#fff", + ast: "word \"50%#fff\" 0..7", + }, + ParserCase { + input: "50%10", + output: "50%10", + ast: "word \"50%10\" 0..5", + }, + ParserCase { + input: "50%10 40%.5", + output: "50%10 40%.5", + ast: "word \"50%10\" 0..5\nspace \" \" 5..6\nword \"40%.5\" 6..11", + }, + ParserCase { + input: "center", + output: "center", + ast: "word \"center\" 0..6", + }, + ParserCase { + input: "top end", + output: "top end", + ast: "word \"top\" 0..3\nspace \" \" 3..4\nword \"end\" 4..7", + }, + ParserCase { + input: "top left", + output: "top left", + ast: "word \"top\" 0..3\nspace \" \" 3..4\nword \"left\" 4..8", + }, + ParserCase { + input: "top start", + output: "top start", + ast: "word \"top\" 0..3\nspace \" \" 3..4\nword \"start\" 4..9", + }, + ParserCase { + input: "1px solid red", + output: "1px solid red", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"red\" 10..13", + }, + ParserCase { + input: "5px solid blue", + output: "5px solid blue", + ast: "word \"5px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"blue\" 10..14", + }, + ParserCase { + input: "7px", + output: "7px", + ast: "word \"7px\" 0..3", + }, + ParserCase { + input: "6px", + output: "6px", + ast: "word \"6px\" 0..3", + }, + ParserCase { + input: "5px", + output: "5px", + ast: "word \"5px\" 0..3", + }, + ParserCase { + input: "red blue", + output: "red blue", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"blue\" 4..8", + }, + ParserCase { + input: "red blue green yellow", + output: "red blue green yellow", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"blue\" 4..8\nspace \" \" 8..9\nword \"green\" 9..14\nspace \" \" 14..15\nword \"yellow\" 15..21", + }, + ParserCase { + input: "var(--divider)", + output: "var(--divider)", + ast: "function \"var\" 0..14 before=\"\" after=\"\" nodes=1\n word \"--divider\" 4..13", + }, + ParserCase { + input: "url(./img.jpg) 30 space", + output: "url(./img.jpg) 30 space", + ast: "function \"url\" 0..14 before=\"\" after=\"\" nodes=1\n word \"./img.jpg\" 4..13\nspace \" \" 14..15\nword \"30\" 15..17\nspace \" \" 17..18\nword \"space\" 18..23", + }, + ParserCase { + input: "2px dashed blue", + output: "2px dashed blue", + ast: "word \"2px\" 0..3\nspace \" \" 3..4\nword \"dashed\" 4..10\nspace \" \" 10..11\nword \"blue\" 11..15", + }, + ParserCase { + input: "1px 2px", + output: "1px 2px", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"2px\" 4..7", + }, + ParserCase { + input: "8px", + output: "8px", + ast: "word \"8px\" 0..3", + }, + ParserCase { + input: "dashed solid", + output: "dashed solid", + ast: "word \"dashed\" 0..6\nspace \" \" 6..7\nword \"solid\" 7..12", + }, + ParserCase { + input: "solid dashed", + output: "solid dashed", + ast: "word \"solid\" 0..5\nspace \" \" 5..6\nword \"dashed\" 6..12", + }, + ParserCase { + input: "1px solid red !important", + output: "1px solid red !important", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"red\" 10..13\nspace \" \" 13..14\nword \"!important\" 14..24", + }, + ParserCase { + input: "1px solid red /* ✓ */", + output: "1px solid red /* ✓ */", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"red\" 10..13\nspace \" \" 13..14\ncomment \" ✓ \" 14..23", + }, + ParserCase { + input: "calc((1px + 2px)", + output: "calc((1px + 2px)", + ast: "function \"calc\" 0..16 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 5..16 before=\"\" after=\"\" nodes=5\n word \"1px\" 6..9\n space \" \" 9..10\n word \"+\" 10..11\n space \" \" 11..12\n word \"2px\" 12..15", + }, + ParserCase { + input: "none", + output: "none", + ast: "word \"none\" 0..4", + }, + ParserCase { + input: "4px", + output: "4px", + ast: "word \"4px\" 0..3", + }, + ParserCase { + input: "0 0 2px 0", + output: "0 0 2px 0", + ast: "word \"0\" 0..1\nspace \" \" 1..2\nword \"0\" 2..3\nspace \" \" 3..4\nword \"2px\" 4..7\nspace \" \" 7..8\nword \"0\" 8..9", + }, + ParserCase { + input: "1px 2px 3px 4px", + output: "1px 2px 3px 4px", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"2px\" 4..7\nspace \" \" 7..8\nword \"3px\" 8..11\nspace \" \" 11..12\nword \"4px\" 12..15", + }, + ParserCase { + input: "-1px -1px #000", + output: "-1px -1px #000", + ast: "word \"-1px\" 0..4\nspace \" \" 4..5\nword \"-1px\" 5..9\nspace \" \" 9..10\nword \"#000\" 10..14", + }, + ParserCase { + input: "0px 0px {n}px #000", + output: "0px 0px {n}px #000", + ast: "word \"0px\" 0..3\nspace \" \" 3..4\nword \"0px\" 4..7\nspace \" \" 7..8\nword \"{n}px\" 8..13\nspace \" \" 13..14\nword \"#000\" 14..18", + }, + ParserCase { + input: "0px 2px 4px var(--shadow-1)", + output: "0px 2px 4px var(--shadow-1)", + ast: "word \"0px\" 0..3\nspace \" \" 3..4\nword \"2px\" 4..7\nspace \" \" 7..8\nword \"4px\" 8..11\nspace \" \" 11..12\nfunction \"var\" 12..27 before=\"\" after=\"\" nodes=1\n word \"--shadow-1\" 16..26", + }, + ParserCase { + input: "10px 20px 30px 40px green", + output: "10px 20px 30px 40px green", + ast: "word \"10px\" 0..4\nspace \" \" 4..5\nword \"20px\" 5..9\nspace \" \" 9..10\nword \"30px\" 10..14\nspace \" \" 14..15\nword \"40px\" 15..19\nspace \" \" 19..20\nword \"green\" 20..25", + }, + ParserCase { + input: "1px 1px #000", + output: "1px 1px #000", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"1px\" 4..7\nspace \" \" 7..8\nword \"#000\" 8..12", + }, + ParserCase { + input: "1px 1px 1px 1px #000", + output: "1px 1px 1px 1px #000", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"1px\" 4..7\nspace \" \" 7..8\nword \"1px\" 8..11\nspace \" \" 11..12\nword \"1px\" 12..15\nspace \" \" 15..16\nword \"#000\" 16..20", + }, + ParserCase { + input: "1px 2px 3px 4px red", + output: "1px 2px 3px 4px red", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"2px\" 4..7\nspace \" \" 7..8\nword \"3px\" 8..11\nspace \" \" 11..12\nword \"4px\" 12..15\nspace \" \" 15..16\nword \"red\" 16..19", + }, + ParserCase { + input: "1px#000", + output: "1px#000", + ast: "word \"1px#000\" 0..7", + }, + ParserCase { + input: "2px 2px 2px 2px red, inset 1px 1px 1px 1px #000", + output: "2px 2px 2px 2px red, inset 1px 1px 1px 1px #000", + ast: "word \"2px\" 0..3\nspace \" \" 3..4\nword \"2px\" 4..7\nspace \" \" 7..8\nword \"2px\" 8..11\nspace \" \" 11..12\nword \"2px\" 12..15\nspace \" \" 15..16\nword \"red\" 16..19\ndiv \",\" 19..21 before=\"\" after=\" \"\nword \"inset\" 21..26\nspace \" \" 26..27\nword \"1px\" 27..30\nspace \" \" 30..31\nword \"1px\" 31..34\nspace \" \" 34..35\nword \"1px\" 35..38\nspace \" \" 38..39\nword \"1px\" 39..42\nspace \" \" 42..43\nword \"#000\" 43..47", + }, + ParserCase { + input: "inset 1px 1px #000", + output: "inset 1px 1px #000", + ast: "word \"inset\" 0..5\nspace \" \" 5..6\nword \"1px\" 6..9\nspace \" \" 9..10\nword \"1px\" 10..13\nspace \" \" 13..14\nword \"#000\" 14..18", + }, + ParserCase { + input: "inset 1px 1px 1px 1px #000", + output: "inset 1px 1px 1px 1px #000", + ast: "word \"inset\" 0..5\nspace \" \" 5..6\nword \"1px\" 6..9\nspace \" \" 9..10\nword \"1px\" 10..13\nspace \" \" 13..14\nword \"1px\" 14..17\nspace \" \" 17..18\nword \"1px\" 18..21\nspace \" \" 21..22\nword \"#000\" 22..26", + }, + ParserCase { + input: "border-box", + output: "border-box", + ast: "word \"border-box\" 0..10", + }, + ParserCase { + input: "end", + output: "end", + ast: "word \"end\" 0..3", + }, + ParserCase { + input: "inline-end", + output: "inline-end", + ast: "word \"inline-end\" 0..10", + }, + ParserCase { + input: "inline-start", + output: "inline-start", + ast: "word \"inline-start\" 0..12", + }, + ParserCase { + input: "start", + output: "start", + ast: "word \"start\" 0..5", + }, + ParserCase { + input: "\"abc var(foo)", + output: "\"abc var(foo)", + ast: "string \"abc var(foo)\" 0..13 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "#", + output: "#", + ast: "word \"#\" 0..1", + }, + ParserCase { + input: "%#fff", + output: "%#fff", + ast: "word \"%#fff\" 0..5", + }, + ParserCase { + input: ")#fff", + output: ")#fff", + ast: "word \")#fff\" 0..5", + }, + ParserCase { + input: ")(", + output: ")(", + ast: "function \")\" 0..2 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: ")A", + output: ")A", + ast: "word \")A\" 0..2", + }, + ParserCase { + input: ")a )Z )0 )# )(", + output: ")a )Z )0 )# )(", + ast: "word \")a\" 0..2\nspace \" \" 2..3\nword \")Z\" 3..5\nspace \" \" 5..6\nword \")0\" 6..8\nspace \" \" 8..9\nword \")#\" 9..11\nspace \" \" 11..12\nfunction \")\" 12..14 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "--Foo(1px)", + output: "--Foo(1px)", + ast: "function \"--Foo\" 0..10 before=\"\" after=\"\" nodes=1\n word \"1px\" 6..9", + }, + ParserCase { + input: "--custom-fn(foo)", + output: "--custom-fn(foo)", + ast: "function \"--custom-fn\" 0..16 before=\"\" after=\"\" nodes=1\n word \"foo\" 12..15", + }, + ParserCase { + input: "--custom-fn(red)", + output: "--custom-fn(red)", + ast: "function \"--custom-fn\" 0..16 before=\"\" after=\"\" nodes=1\n word \"red\" 12..15", + }, + ParserCase { + input: "0Hz", + output: "0Hz", + ast: "word \"0Hz\" 0..3", + }, + ParserCase { + input: "0unknown", + output: "0unknown", + ast: "word \"0unknown\" 0..8", + }, + ParserCase { + input: "40%.1147", + output: "40%.1147", + ast: "word \"40%.1147\" 0..8", + }, + ParserCase { + input: "9#fff", + output: "9#fff", + ast: "word \"9#fff\" 0..5", + }, + ParserCase { + input: "@", + output: "@", + ast: "word \"@\" 0..1", + }, + ParserCase { + input: "A#fff", + output: "A#fff", + ast: "word \"A#fff\" 0..5", + }, + ParserCase { + input: "VAR(--foo)", + output: "VAR(--foo)", + ast: "function \"VAR\" 0..10 before=\"\" after=\"\" nodes=1\n word \"--foo\" 4..9", + }, + ParserCase { + input: "VAR(foo)", + output: "VAR(foo)", + ast: "function \"VAR\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7", + }, + ParserCase { + input: "Var(foo)", + output: "Var(foo)", + ast: "function \"Var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7", + }, + ParserCase { + input: "\\u00a0", + output: "\\u00a0", + ast: "word \"\\\\u00a0\" 0..6", + }, + ParserCase { + input: "\\u2028", + output: "\\u2028", + ast: "word \"\\\\u2028\" 0..6", + }, + ParserCase { + input: "\\u3000", + output: "\\u3000", + ast: "word \"\\\\u3000\" 0..6", + }, + ParserCase { + input: "a#fff Z#fff 1#fff %#fff", + output: "a#fff Z#fff 1#fff %#fff", + ast: "word \"a#fff\" 0..5\nspace \" \" 5..6\nword \"Z#fff\" 6..11\nspace \" \" 11..12\nword \"1#fff\" 12..17\nspace \" \" 17..18\nword \"%#fff\" 18..23", + }, + ParserCase { + input: "calc(0 - var(--someVar))", + output: "calc(0 - var(--someVar))", + ast: "function \"calc\" 0..24 before=\"\" after=\"\" nodes=5\n word \"0\" 5..6\n space \" \" 6..7\n word \"-\" 7..8\n space \" \" 8..9\n function \"var\" 9..23 before=\"\" after=\"\" nodes=1\n word \"--someVar\" 13..22", + }, + ParserCase { + input: "calc(0px - var(--someVar) + 10px)", + output: "calc(0px - var(--someVar) + 10px)", + ast: "function \"calc\" 0..33 before=\"\" after=\"\" nodes=9\n word \"0px\" 5..8\n space \" \" 8..9\n word \"-\" 9..10\n space \" \" 10..11\n function \"var\" 11..25 before=\"\" after=\"\" nodes=1\n word \"--someVar\" 15..24\n space \" \" 25..26\n word \"+\" 26..27\n space \" \" 27..28\n word \"10px\" 28..32", + }, + ParserCase { + input: "calc(;a", + output: "calc(;a", + ast: "function \"calc\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \";a\" 5..7", + }, + ParserCase { + input: "clamp(0, (var(--l-threshold, 0.623) / l - 1) * infinity, 1)", + output: "clamp(0, (var(--l-threshold, 0.623) / l - 1) * infinity, 1)", + ast: "function \"clamp\" 0..70 before=\"\" after=\"\" nodes=9\n word \"0\" 6..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"\" 9..47 before=\"\" after=\"\" nodes=7\n function \"var\" 10..35 before=\"\" after=\"\" nodes=3\n word \"--l-threshold\" 14..27\n div \",\" 27..29 before=\"\" after=\" \"\n word \"0.623\" 29..34\n div \"/\" 35..41 before=\" \" after=\" \"\n word \"l\" 41..42\n space \" \" 42..43\n word \"-\" 43..44\n space \" \" 44..45\n word \"1\" 45..46\n space \" \" 47..50\n word \"*\" 50..51\n space \" \" 51..55\n word \"infinity\" 55..63\n div \",\" 63..68 before=\"\" after=\" \"\n word \"1\" 68..69", + }, + ParserCase { + input: "clamp(200px, 40%, 400px)", + output: "clamp(200px, 40%, 400px)", + ast: "function \"clamp\" 0..29 before=\"\" after=\"\" nodes=5\n word \"200px\" 6..11\n div \",\" 11..14 before=\"\" after=\" \"\n word \"40%\" 14..17\n div \",\" 17..23 before=\"\" after=\" \"\n word \"400px\" 23..28", + }, + ParserCase { + input: "clamp(min(10vw, 20rem), 300px, max(90vw, 55rem))", + output: "clamp(min(10vw, 20rem), 300px, max(90vw, 55rem))", + ast: "function \"clamp\" 0..65 before=\"\" after=\"\" nodes=5\n function \"min\" 6..27 before=\"\" after=\"\" nodes=3\n word \"10vw\" 10..14\n div \",\" 14..21 before=\"\" after=\" \"\n word \"20rem\" 21..26\n div \",\" 27..33 before=\"\" after=\" \"\n word \"300px\" 33..38\n div \",\" 38..44 before=\"\" after=\" \"\n function \"max\" 44..64 before=\"\" after=\"\" nodes=3\n word \"90vw\" 48..52\n div \",\" 52..58 before=\"\" after=\" \"\n word \"55rem\" 58..63", + }, + ParserCase { + input: "clamp(min(10vw,20rem),300px,max(90vw,55rem))", + output: "clamp(min(10vw,20rem),300px,max(90vw,55rem))", + ast: "function \"clamp\" 0..44 before=\"\" after=\"\" nodes=5\n function \"min\" 6..21 before=\"\" after=\"\" nodes=3\n word \"10vw\" 10..14\n div \",\" 14..15 before=\"\" after=\"\"\n word \"20rem\" 15..20\n div \",\" 21..22 before=\"\" after=\"\"\n word \"300px\" 22..27\n div \",\" 27..28 before=\"\" after=\"\"\n function \"max\" 28..43 before=\"\" after=\"\" nodes=3\n word \"90vw\" 32..36\n div \",\" 36..37 before=\"\" after=\"\"\n word \"55rem\" 37..42", + }, + ParserCase { + input: "color(from green srgb r g b)", + output: "color(from green srgb r g b)", + ast: "function \"color\" 0..28 before=\"\" after=\"\" nodes=11\n word \"from\" 6..10\n space \" \" 10..11\n word \"green\" 11..16\n space \" \" 16..17\n word \"srgb\" 17..21\n space \" \" 21..22\n word \"r\" 22..23\n space \" \" 23..24\n word \"g\" 24..25\n space \" \" 25..26\n word \"b\" 26..27", + }, + ParserCase { + input: "gray", + output: "gray", + ast: "word \"gray\" 0..4", + }, + ParserCase { + input: "hotpink", + output: "hotpink", + ast: "word \"hotpink\" 0..7", + }, + ParserCase { + input: "hsl(0 0% 0%", + output: "hsl(0 0% 0%", + ast: "function \"hsl\" 0..11 before=\"\" after=\"\" unclosed nodes=5\n word \"0\" 4..5\n space \" \" 5..6\n word \"0%\" 6..8\n space \" \" 8..9\n word \"0%\" 9..11", + }, + ParserCase { + input: "hsl(120, 100%, 50%)", + output: "hsl(120, 100%, 50%)", + ast: "function \"hsl\" 0..19 before=\"\" after=\"\" nodes=5\n word \"120\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"100%\" 9..13\n div \",\" 13..15 before=\"\" after=\" \"\n word \"50%\" 15..18", + }, + ParserCase { + input: "hsla(120, 100%, 50%, 0.5)", + output: "hsla(120, 100%, 50%, 0.5)", + ast: "function \"hsla\" 0..25 before=\"\" after=\"\" nodes=7\n word \"120\" 5..8\n div \",\" 8..10 before=\"\" after=\" \"\n word \"100%\" 10..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"50%\" 16..19\n div \",\" 19..21 before=\"\" after=\" \"\n word \"0.5\" 21..24", + }, + ParserCase { + input: "hwb(194 0% 0%)", + output: "hwb(194 0% 0%)", + ast: "function \"hwb\" 0..14 before=\"\" after=\"\" nodes=5\n word \"194\" 4..7\n space \" \" 7..8\n word \"0%\" 8..10\n space \" \" 10..11\n word \"0%\" 11..13", + }, + ParserCase { + input: "inherit", + output: "inherit", + ast: "word \"inherit\" 0..7", + }, + ParserCase { + input: "lab(50% 40 59.5)", + output: "lab(50% 40 59.5)", + ast: "function \"lab\" 0..16 before=\"\" after=\"\" nodes=5\n word \"50%\" 4..7\n space \" \" 7..8\n word \"40\" 8..10\n space \" \" 10..11\n word \"59.5\" 11..15", + }, + ParserCase { + input: "lch(52.2% 72.2 50)", + output: "lch(52.2% 72.2 50)", + ast: "function \"lch\" 0..18 before=\"\" after=\"\" nodes=5\n word \"52.2%\" 4..9\n space \" \" 9..10\n word \"72.2\" 10..14\n space \" \" 14..15\n word \"50\" 15..17", + }, + ParserCase { + input: "oklab(40.101% 0.1147 0.0453)", + output: "oklab(40.101% 0.1147 0.0453)", + ast: "function \"oklab\" 0..28 before=\"\" after=\"\" nodes=5\n word \"40.101%\" 6..13\n space \" \" 13..14\n word \"0.1147\" 14..20\n space \" \" 20..21\n word \"0.0453\" 21..27", + }, + ParserCase { + input: "oklab(40.101%.1147 .0453)", + output: "oklab(40.101%.1147 .0453)", + ast: "function \"oklab\" 0..25 before=\"\" after=\"\" nodes=3\n word \"40.101%.1147\" 6..18\n space \" \" 18..19\n word \".0453\" 19..24", + }, + ParserCase { + input: "oklab(from #0000FF calc(l + 0.1) a b / calc(alpha * 0.9))", + output: "oklab(from #0000FF calc(l + 0.1) a b / calc(alpha * 0.9))", + ast: "function \"oklab\" 0..57 before=\"\" after=\"\" nodes=11\n word \"from\" 6..10\n space \" \" 10..11\n word \"#0000FF\" 11..18\n space \" \" 18..19\n function \"calc\" 19..32 before=\"\" after=\"\" nodes=5\n word \"l\" 24..25\n space \" \" 25..26\n word \"+\" 26..27\n space \" \" 27..28\n word \"0.1\" 28..31\n space \" \" 32..33\n word \"a\" 33..34\n space \" \" 34..35\n word \"b\" 35..36\n div \"/\" 36..39 before=\" \" after=\" \"\n function \"calc\" 39..56 before=\"\" after=\"\" nodes=5\n word \"alpha\" 44..49\n space \" \" 49..50\n word \"*\" 50..51\n space \" \" 51..52\n word \"0.9\" 52..55", + }, + ParserCase { + input: "oklab(from green l a b / 0.5)", + output: "oklab(from green l a b / 0.5)", + ast: "function \"oklab\" 0..29 before=\"\" after=\"\" nodes=11\n word \"from\" 6..10\n space \" \" 10..11\n word \"green\" 11..16\n space \" \" 16..17\n word \"l\" 17..18\n space \" \" 18..19\n word \"a\" 19..20\n space \" \" 20..21\n word \"b\" 21..22\n div \"/\" 22..25 before=\" \" after=\" \"\n word \"0.5\" 25..28", + }, + ParserCase { + input: "oklab(from hsl(180 100% 50%) calc(l - 0.1) a b)", + output: "oklab(from hsl(180 100% 50%) calc(l - 0.1) a b)", + ast: "function \"oklab\" 0..47 before=\"\" after=\"\" nodes=9\n word \"from\" 6..10\n space \" \" 10..11\n function \"hsl\" 11..28 before=\"\" after=\"\" nodes=5\n word \"180\" 15..18\n space \" \" 18..19\n word \"100%\" 19..23\n space \" \" 23..24\n word \"50%\" 24..27\n space \" \" 28..29\n function \"calc\" 29..42 before=\"\" after=\"\" nodes=5\n word \"l\" 34..35\n space \" \" 35..36\n word \"-\" 36..37\n space \" \" 37..38\n word \"0.1\" 38..41\n space \" \" 42..43\n word \"a\" 43..44\n space \" \" 44..45\n word \"b\" 45..46", + }, + ParserCase { + input: "oklch(0.7 0.15 180)", + output: "oklch(0.7 0.15 180)", + ast: "function \"oklch\" 0..23 before=\"\" after=\"\" nodes=5\n word \"0.7\" 6..9\n space \" \" 9..12\n word \"0.15\" 12..16\n space \" \" 16..19\n word \"180\" 19..22", + }, + ParserCase { + input: "oklch(0.7 0.15 180)", + output: "oklch(0.7 0.15 180)", + ast: "function \"oklch\" 0..19 before=\"\" after=\"\" nodes=5\n word \"0.7\" 6..9\n space \" \" 9..10\n word \"0.15\" 10..14\n space \" \" 14..15\n word \"180\" 15..18", + }, + ParserCase { + input: "oklch(from var(--xs74gcj) l c h / 0.5)", + output: "oklch(from var(--xs74gcj) l c h / 0.5)", + ast: "function \"oklch\" 0..38 before=\"\" after=\"\" nodes=11\n word \"from\" 6..10\n space \" \" 10..11\n function \"var\" 11..25 before=\"\" after=\"\" nodes=1\n word \"--xs74gcj\" 15..24\n space \" \" 25..26\n word \"l\" 26..27\n space \" \" 27..28\n word \"c\" 28..29\n space \" \" 29..30\n word \"h\" 30..31\n div \"/\" 31..34 before=\" \" after=\" \"\n word \"0.5\" 34..37", + }, + ParserCase { + input: "oklch(from {value} l c h)", + output: "oklch(from {value} l c h)", + ast: "function \"oklch\" 0..25 before=\"\" after=\"\" nodes=9\n word \"from\" 6..10\n space \" \" 10..11\n word \"{value}\" 11..18\n space \" \" 18..19\n word \"l\" 19..20\n space \" \" 20..21\n word \"c\" 21..22\n space \" \" 22..23\n word \"h\" 23..24", + }, + ParserCase { + input: "red /* \" */", + output: "red /* \" */", + ast: "word \"red\" 0..3\nspace \" \" 3..4\ncomment \" \\\" \" 4..11", + }, + ParserCase { + input: "red /* ; */", + output: "red /* ; */", + ast: "word \"red\" 0..3\nspace \" \" 3..4\ncomment \" ; \" 4..11", + }, + ParserCase { + input: "red /* x", + output: "red /* x", + ast: "word \"red\" 0..3\nspace \" \" 3..4\ncomment \" x\" 4..8 unclosed", + }, + ParserCase { + input: "red {\"a", + output: "red {\"a", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"{\" 4..5\nstring \"a\" 5..7 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "red {calc(1px", + output: "red {calc(1px", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nfunction \"{calc\" 4..13 before=\"\" after=\"\" unclosed nodes=1\n word \"1px\" 10..13", + }, + ParserCase { + input: "red }", + output: "red }", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"}\" 4..5", + }, + ParserCase { + input: "red }calc(1px", + output: "red }calc(1px", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nfunction \"}calc\" 4..13 before=\"\" after=\"\" unclosed nodes=1\n word \"1px\" 10..13", + }, + ParserCase { + input: "red; /* x */", + output: "red; /* x */", + ast: "word \"red;\" 0..4\nspace \" \" 4..5\ncomment \" x \" 5..12", + }, + ParserCase { + input: "red;'a", + output: "red;'a", + ast: "word \"red;\" 0..4\nstring \"a\" 4..6 quote=\"'\" unclosed", + }, + ParserCase { + input: "red;/* x", + output: "red;/* x", + ast: "word \"red;\" 0..4\ncomment \" x\" 4..8 unclosed", + }, + ParserCase { + input: "red;background:blue", + output: "red;background:blue", + ast: "word \"red;background\" 0..14\ndiv \":\" 14..15 before=\"\" after=\"\"\nword \"blue\" 15..19", + }, + ParserCase { + input: "red;background:blue;calc(", + output: "red;background:blue;calc(", + ast: "word \"red;background\" 0..14\ndiv \":\" 14..15 before=\"\" after=\"\"\nfunction \"blue;calc\" 15..25 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "red\\;blue", + output: "red\\;blue", + ast: "word \"red\\\\;blue\" 0..9", + }, + ParserCase { + input: "rgb( from red r g b )", + output: "rgb( from red r g b )", + ast: "function \"rgb\" 0..25 before=\" \" after=\" \" nodes=9\n word \"from\" 7..11\n space \" \" 11..12\n word \"red\" 12..15\n space \" \" 15..16\n word \"r\" 16..17\n space \" \" 17..18\n word \"g\" 18..19\n space \" \" 19..20\n word \"b\" 20..21", + }, + ParserCase { + input: "rgb(0,0,0)rgba(255,255,255,.5)", + output: "rgb(0,0,0)rgba(255,255,255,.5)", + ast: "function \"rgb\" 0..10 before=\"\" after=\"\" nodes=5\n word \"0\" 4..5\n div \",\" 5..6 before=\"\" after=\"\"\n word \"0\" 6..7\n div \",\" 7..8 before=\"\" after=\"\"\n word \"0\" 8..9\nfunction \"rgba\" 10..30 before=\"\" after=\"\" nodes=7\n word \"255\" 15..18\n div \",\" 18..19 before=\"\" after=\"\"\n word \"255\" 19..22\n div \",\" 22..23 before=\"\" after=\"\"\n word \"255\" 23..26\n div \",\" 26..27 before=\"\" after=\"\"\n word \".5\" 27..29", + }, + ParserCase { + input: "rgb(255, 0, 0", + output: "rgb(255, 0, 0", + ast: "function \"rgb\" 0..13 before=\"\" after=\"\" unclosed nodes=5\n word \"255\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"0\" 9..10\n div \",\" 10..12 before=\"\" after=\" \"\n word \"0\" 12..13", + }, + ParserCase { + input: "rgb(255, 0, 0)", + output: "rgb(255, 0, 0)", + ast: "function \"rgb\" 0..14 before=\"\" after=\"\" nodes=5\n word \"255\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"0\" 9..10\n div \",\" 10..12 before=\"\" after=\" \"\n word \"0\" 12..13", + }, + ParserCase { + input: "rgb(from red r g b)", + output: "rgb(from red r g b)", + ast: "function \"rgb\" 0..23 before=\"\" after=\"\" nodes=9\n word \"from\" 4..8\n space \" \" 8..11\n word \"red\" 11..14\n space \" \" 14..17\n word \"r\" 17..18\n space \" \" 18..19\n word \"g\" 19..20\n space \" \" 20..21\n word \"b\" 21..22", + }, + ParserCase { + input: "rgb(from red r g b) /* unclosed", + output: "rgb(from red r g b) /* unclosed", + ast: "function \"rgb\" 0..19 before=\"\" after=\"\" nodes=9\n word \"from\" 4..8\n space \" \" 8..9\n word \"red\" 9..12\n space \" \" 12..13\n word \"r\" 13..14\n space \" \" 14..15\n word \"g\" 15..16\n space \" \" 16..17\n word \"b\" 17..18\nspace \" \" 19..20\ncomment \" unclosed\" 20..31 unclosed", + }, + ParserCase { + input: "rgb(from red r g b) } .evil{color:blue", + output: "rgb(from red r g b) } .evil{color:blue", + ast: "function \"rgb\" 0..19 before=\"\" after=\"\" nodes=9\n word \"from\" 4..8\n space \" \" 8..9\n word \"red\" 9..12\n space \" \" 12..13\n word \"r\" 13..14\n space \" \" 14..15\n word \"g\" 15..16\n space \" \" 16..17\n word \"b\" 17..18\nspace \" \" 19..20\nword \"}\" 20..21\nspace \" \" 21..22\nword \".evil{color\" 22..33\ndiv \":\" 33..34 before=\"\" after=\"\"\nword \"blue\" 34..38", + }, + ParserCase { + input: "rgb(var(r), 0, 0)", + output: "rgb(var(r), 0, 0)", + ast: "function \"rgb\" 0..17 before=\"\" after=\"\" nodes=5\n function \"var\" 4..10 before=\"\" after=\"\" nodes=1\n word \"r\" 8..9\n div \",\" 10..12 before=\"\" after=\" \"\n word \"0\" 12..13\n div \",\" 13..15 before=\"\" after=\" \"\n word \"0\" 15..16", + }, + ParserCase { + input: "rgb({channel}, 0, 0)", + output: "rgb({channel}, 0, 0)", + ast: "function \"rgb\" 0..20 before=\"\" after=\"\" nodes=5\n word \"{channel}\" 4..13\n div \",\" 13..15 before=\"\" after=\" \"\n word \"0\" 15..16\n div \",\" 16..18 before=\"\" after=\" \"\n word \"0\" 18..19", + }, + ParserCase { + input: "rgba( 1, 222, 33 , 0.5)", + output: "rgba( 1, 222, 33 , 0.5)", + ast: "function \"rgba\" 0..24 before=\" \" after=\"\" nodes=7\n word \"1\" 6..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"222\" 9..12\n div \",\" 12..15 before=\"\" after=\" \"\n word \"33\" 15..17\n div \",\" 17..20 before=\" \" after=\" \"\n word \"0.5\" 20..23", + }, + ParserCase { + input: "rgba(0, 0, 0, 0.5)", + output: "rgba(0, 0, 0, 0.5)", + ast: "function \"rgba\" 0..18 before=\"\" after=\"\" nodes=7\n word \"0\" 5..6\n div \",\" 6..8 before=\"\" after=\" \"\n word \"0\" 8..9\n div \",\" 9..11 before=\"\" after=\" \"\n word \"0\" 11..12\n div \",\" 12..14 before=\"\" after=\" \"\n word \"0.5\" 14..17", + }, + ParserCase { + input: "solid#abc", + output: "solid#abc", + ast: "word \"solid#abc\" 0..9", + }, + ParserCase { + input: "transparent", + output: "transparent", + ast: "word \"transparent\" 0..11", + }, + ParserCase { + input: "vAr(foo)", + output: "vAr(foo)", + ast: "function \"vAr\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7", + }, + ParserCase { + input: "var(\"foo\")", + output: "var(\"foo\")", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=1\n string \"foo\" 4..9 quote=\"\\\"\"", + }, + ParserCase { + input: "var('foo')", + output: "var('foo')", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=1\n string \"foo\" 4..9 quote=\"'\"", + }, + ParserCase { + input: "var()", + output: "var()", + ast: "function \"var\" 0..5 before=\"\" after=\"\" nodes=0", + }, + ParserCase { + input: "var(, foo)", + output: "var(, foo)", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=2\n div \",\" 4..6 before=\"\" after=\" \"\n word \"foo\" 6..9", + }, + ParserCase { + input: "var(--)", + output: "var(--)", + ast: "function \"var\" 0..7 before=\"\" after=\"\" nodes=1\n word \"--\" 4..6", + }, + ParserCase { + input: "var(----x)", + output: "var(----x)", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=1\n word \"----x\" 4..9", + }, + ParserCase { + input: "var(--a) var(--b) var(--c)", + output: "var(--a) var(--b) var(--c)", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--a\" 4..7\nspace \" \" 8..9\nfunction \"var\" 9..17 before=\"\" after=\"\" nodes=1\n word \"--b\" 13..16\nspace \" \" 17..18\nfunction \"var\" 18..26 before=\"\" after=\"\" nodes=1\n word \"--c\" 22..25", + }, + ParserCase { + input: "var(--a)var(--b)var(--c)", + output: "var(--a)var(--b)var(--c)", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--a\" 4..7\nfunction \"var\" 8..16 before=\"\" after=\"\" nodes=1\n word \"--b\" 12..15\nfunction \"var\" 16..24 before=\"\" after=\"\" nodes=1\n word \"--c\" 20..23", + }, + ParserCase { + input: "var(--a, var(--b))", + output: "var(--a, var(--b))", + ast: "function \"var\" 0..18 before=\"\" after=\"\" nodes=3\n word \"--a\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"var\" 9..17 before=\"\" after=\"\" nodes=1\n word \"--b\" 13..16", + }, + ParserCase { + input: "var(--a, var(b))", + output: "var(--a, var(b))", + ast: "function \"var\" 0..16 before=\"\" after=\"\" nodes=3\n word \"--a\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"var\" 9..15 before=\"\" after=\"\" nodes=1\n word \"b\" 13..14", + }, + ParserCase { + input: "var(--bar)", + output: "var(--bar)", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=1\n word \"--bar\" 4..9", + }, + ParserCase { + input: "var(--color) var(--bg)", + output: "var(--color) var(--bg)", + ast: "function \"var\" 0..12 before=\"\" after=\"\" nodes=1\n word \"--color\" 4..11\nspace \" \" 12..13\nfunction \"var\" 13..22 before=\"\" after=\"\" nodes=1\n word \"--bg\" 17..21", + }, + ParserCase { + input: "var(--emoji-🎨)", + output: "var(--emoji-🎨)", + ast: "function \"var\" 0..17 before=\"\" after=\"\" nodes=1\n word \"--emoji-🎨\" 4..16", + }, + ParserCase { + input: "var(--foo", + output: "var(--foo", + ast: "function \"var\" 0..9 before=\"\" after=\"\" unclosed nodes=1\n word \"--foo\" 4..9", + }, + ParserCase { + input: "var(--foo, red)", + output: "var(--foo, red)", + ast: "function \"var\" 0..15 before=\"\" after=\"\" nodes=3\n word \"--foo\" 4..9\n div \",\" 9..11 before=\"\" after=\" \"\n word \"red\" 11..14", + }, + ParserCase { + input: "var(--fóo)", + output: "var(--fóo)", + ast: "function \"var\" 0..11 before=\"\" after=\"\" nodes=1\n word \"--fóo\" 4..10", + }, + ParserCase { + input: "var(--my-color)", + output: "var(--my-color)", + ast: "function \"var\" 0..15 before=\"\" after=\"\" nodes=1\n word \"--my-color\" 4..14", + }, + ParserCase { + input: "var(--someVar)", + output: "var(--someVar)", + ast: "function \"var\" 0..14 before=\"\" after=\"\" nodes=1\n word \"--someVar\" 4..13", + }, + ParserCase { + input: "var(--xAbCdEf)", + output: "var(--xAbCdEf)", + ast: "function \"var\" 0..14 before=\"\" after=\"\" nodes=1\n word \"--xAbCdEf\" 4..13", + }, + ParserCase { + input: "var(--xColor)", + output: "var(--xColor)", + ast: "function \"var\" 0..13 before=\"\" after=\"\" nodes=1\n word \"--xColor\" 4..12", + }, + ParserCase { + input: "var(--xColor, red)", + output: "var(--xColor, red)", + ast: "function \"var\" 0..18 before=\"\" after=\"\" nodes=3\n word \"--xColor\" 4..12\n div \",\" 12..14 before=\"\" after=\" \"\n word \"red\" 14..17", + }, + ParserCase { + input: "var(--日本語)", + output: "var(--日本語)", + ast: "function \"var\" 0..16 before=\"\" after=\"\" nodes=1\n word \"--日本語\" 4..15", + }, + ParserCase { + input: "var(/* foo */ bar)", + output: "var(/* foo */ bar)", + ast: "function \"var\" 0..18 before=\"\" after=\"\" nodes=3\n comment \" foo \" 4..13\n space \" \" 13..14\n word \"bar\" 14..17", + }, + ParserCase { + input: "var(foo", + output: "var(foo", + ast: "function \"var\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \"foo\" 4..7", + }, + ParserCase { + input: "var(foo)", + output: "var(foo)", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7", + }, + ParserCase { + input: "var(foo) \"abc", + output: "var(foo) \"abc", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7\nspace \" \" 8..9\nstring \"abc\" 9..13 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "var(foo) /* unterminated", + output: "var(foo) /* unterminated", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7\nspace \" \" 8..9\ncomment \" unterminated\" 9..24 unclosed", + }, + ParserCase { + input: "var(foo) } color: red", + output: "var(foo) } color: red", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7\nspace \" \" 8..9\nword \"}\" 9..10\nspace \" \" 10..11\nword \"color\" 11..16\ndiv \":\" 16..18 before=\"\" after=\" \"\nword \"red\" 18..21", + }, + ParserCase { + input: "var(foo); margin: 10px", + output: "var(foo); margin: 10px", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"foo\" 4..7\nword \";\" 8..9\nspace \" \" 9..10\nword \"margin\" 10..16\ndiv \":\" 16..18 before=\"\" after=\" \"\nword \"10px\" 18..22", + }, + ParserCase { + input: "var(foo, red", + output: "var(foo, red", + ast: "function \"var\" 0..12 before=\"\" after=\"\" unclosed nodes=3\n word \"foo\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"red\" 9..12", + }, + ParserCase { + input: "var(someVariableName)", + output: "var(someVariableName)", + ast: "function \"var\" 0..21 before=\"\" after=\"\" nodes=1\n word \"someVariableName\" 4..20", + }, + ParserCase { + input: "var(x);a", + output: "var(x);a", + ast: "function \"var\" 0..6 before=\"\" after=\"\" nodes=1\n word \"x\" 4..5\nword \";a\" 6..8", + }, + ParserCase { + input: "yellow", + output: "yellow", + ast: "word \"yellow\" 0..6", + }, + ParserCase { + input: "•", + output: "•", + ast: "word \"•\" 0..3", + }, + ParserCase { + input: "日本語#fff", + output: "日本語#fff", + ast: "word \"日本語#fff\" 0..13", + }, + ParserCase { + input: "\";\"", + output: "\";\"", + ast: "string \";\" 0..3 quote=\"\\\"\"", + }, + ParserCase { + input: "\"prev\"", + output: "\"prev\"", + ast: "string \"prev\" 0..6 quote=\"\\\"\"", + }, + ParserCase { + input: "\"rgb(from red r g b)\"", + output: "\"rgb(from red r g b)\"", + ast: "string \"rgb(from red r g b)\" 0..25 quote=\"\\\"\"", + }, + ParserCase { + input: "\"var(foo)\"", + output: "\"var(foo)\"", + ast: "string \"var(foo)\" 0..10 quote=\"\\\"\"", + }, + ParserCase { + input: "\"{}\"", + output: "\"{}\"", + ast: "string \"{}\" 0..4 quote=\"\\\"\"", + }, + ParserCase { + input: "'var(foo)'", + output: "'var(foo)'", + ast: "string \"var(foo)\" 0..10 quote=\"'\"", + }, + ParserCase { + input: "'•'", + output: "'•'", + ast: "string \"•\" 0..5 quote=\"'\"", + }, + ParserCase { + input: "50% off \\\\", + output: "50% off \\\\", + ast: "word \"50%\" 0..3\nspace \" \" 3..4\nword \"off\" 4..7\nspace \" \" 7..8\nword \"\\\\\\\\\" 8..10", + }, + ParserCase { + input: "Bob's and Jim's", + output: "Bob's and Jim's", + ast: "word \"Bob\" 0..3\nstring \"s and Jim\" 3..14 quote=\"'\"\nword \"s\" 14..15", + }, + ParserCase { + input: "He said \"hello\"", + output: "He said \"hello\"", + ast: "word \"He\" 0..2\nspace \" \" 2..3\nword \"said\" 3..7\nspace \" \" 7..8\nstring \"hello\" 8..15 quote=\"\\\"\"", + }, + ParserCase { + input: "\\\\2014", + output: "\\\\2014", + ast: "word \"\\\\\\\\2014\" 0..6", + }, + ParserCase { + input: "\\\\201C hello \\\\201D", + output: "\\\\201C hello \\\\201D", + ast: "word \"\\\\\\\\201C\" 0..6\nspace \" \" 6..7\nword \"hello\" 7..12\nspace \" \" 12..13\nword \"\\\\\\\\201D\" 13..19", + }, + ParserCase { + input: "\\u{1F600}a", + output: "\\u{1F600}a", + ast: "word \"\\\\u{1F600}a\" 0..10", + }, + ParserCase { + input: "abc", + output: "abc", + ast: "word \"abc\" 0..3", + }, + ParserCase { + input: "attr(data-value)", + output: "attr(data-value)", + ast: "function \"attr\" 0..16 before=\"\" after=\"\" nodes=1\n word \"data-value\" 5..15", + }, + ParserCase { + input: "attr(some-attribute)", + output: "attr(some-attribute)", + ast: "function \"attr\" 0..20 before=\"\" after=\"\" nodes=1\n word \"some-attribute\" 5..19", + }, + ParserCase { + input: "next", + output: "next", + ast: "word \"next\" 0..4", + }, + ParserCase { + input: "open-quote \"hello\" close-quote", + output: "open-quote \"hello\" close-quote", + ast: "word \"open-quote\" 0..10\nspace \" \" 10..11\nstring \"hello\" 11..18 quote=\"\\\"\"\nspace \" \" 18..19\nword \"close-quote\" 19..30", + }, + ParserCase { + input: "x", + output: "x", + ast: "word \"x\" 0..1", + }, + ParserCase { + input: "🎉", + output: "🎉", + ast: "word \"🎉\" 0..4", + }, + ParserCase { + input: "squircle", + output: "squircle", + ast: "word \"squircle\" 0..8", + }, + ParserCase { + input: "bevel", + output: "bevel", + ast: "word \"bevel\" 0..5", + }, + ParserCase { + input: "notch", + output: "notch", + ast: "word \"notch\" 0..5", + }, + ParserCase { + input: "e-resize", + output: "e-resize", + ast: "word \"e-resize\" 0..8", + }, + ParserCase { + input: "ne-resize", + output: "ne-resize", + ast: "word \"ne-resize\" 0..9", + }, + ParserCase { + input: "nw-resize", + output: "nw-resize", + ast: "word \"nw-resize\" 0..9", + }, + ParserCase { + input: "se-resize", + output: "se-resize", + ast: "word \"se-resize\" 0..9", + }, + ParserCase { + input: "sw-resize", + output: "sw-resize", + ast: "word \"sw-resize\" 0..9", + }, + ParserCase { + input: "w-resize", + output: "w-resize", + ast: "word \"w-resize\" 0..8", + }, + ParserCase { + input: "block", + output: "block", + ast: "word \"block\" 0..5", + }, + ParserCase { + input: "block !important", + output: "block !important", + ast: "word \"block\" 0..5\nspace \" \" 5..6\nword \"!important\" 6..16", + }, + ParserCase { + input: "flex", + output: "flex", + ast: "word \"flex\" 0..4", + }, + ParserCase { + input: "grid", + output: "grid", + ast: "word \"grid\" 0..4", + }, + ParserCase { + input: "initial", + output: "initial", + ast: "word \"initial\" 0..7", + }, + ParserCase { + input: "1 1 0", + output: "1 1 0", + ast: "word \"1\" 0..1\nspace \" \" 1..2\nword \"1\" 2..3\nspace \" \" 3..4\nword \"0\" 4..5", + }, + ParserCase { + input: "row wrap", + output: "row wrap", + ast: "word \"row\" 0..3\nspace \" \" 3..4\nword \"wrap\" 4..8", + }, + ParserCase { + input: "1", + output: "1", + ast: "word \"1\" 0..1", + }, + ParserCase { + input: "16px/16 Arial", + output: "16px/16 Arial", + ast: "word \"16px\" 0..4\ndiv \"/\" 4..5 before=\"\" after=\"\"\nword \"16\" 5..7\nspace \" \" 7..8\nword \"Arial\" 8..13", + }, + ParserCase { + input: "\"-.5\"", + output: "\"-.5\"", + ast: "string \"-.5\" 0..5 quote=\"\\\"\"", + }, + ParserCase { + input: "\"Helvetica Neue", + output: "\"Helvetica Neue", + ast: "string \"Helvetica Neue\" 0..15 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "\"Helvetica Neue\", sans-serif", + output: "\"Helvetica Neue\", sans-serif", + ast: "string \"Helvetica Neue\" 0..16 quote=\"\\\"\"\ndiv \",\" 16..18 before=\"\" after=\" \"\nword \"sans-serif\" 18..28", + }, + ParserCase { + input: "\"Helvetica Neue, sans-serif", + output: "\"Helvetica Neue, sans-serif", + ast: "string \"Helvetica Neue, sans-serif\" 0..27 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "\"Helvetica \\\"Neue\", sans-serif", + output: "\"Helvetica \\\"Neue\", sans-serif", + ast: "string \"Helvetica \\\\\\\"Neue\" 0..18 quote=\"\\\"\"\ndiv \",\" 18..20 before=\"\" after=\" \"\nword \"sans-serif\" 20..30", + }, + ParserCase { + input: "\"My\\\\ Font\"", + output: "\"My\\\\ Font\"", + ast: "string \"My\\\\\\\\ Font\" 0..11 quote=\"\\\"\"", + }, + ParserCase { + input: "\"\\\\1F600\", sans-serif", + output: "\"\\\\1F600\", sans-serif", + ast: "string \"\\\\\\\\1F600\" 0..9 quote=\"\\\"\"\ndiv \",\" 9..11 before=\"\" after=\" \"\nword \"sans-serif\" 11..21", + }, + ParserCase { + input: "\"\\\\2014 A\", sans-serif", + output: "\"\\\\2014 A\", sans-serif", + ast: "string \"\\\\\\\\2014 A\" 0..10 quote=\"\\\"\"\ndiv \",\" 10..12 before=\"\" after=\" \"\nword \"sans-serif\" 12..22", + }, + ParserCase { + input: "\"a\" 1px", + output: "\"a\" 1px", + ast: "string \"a\" 0..3 quote=\"\\\"\"\nspace \" \" 3..4\nword \"1px\" 4..7", + }, + ParserCase { + input: "\"a)b#c%d.5/1*2(\"", + output: "\"a)b#c%d.5/1*2(\"", + ast: "string \"a)b#c%d.5/1*2(\" 0..16 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\\\"-.5\"", + output: "\"a\\\"-.5\"", + ast: "string \"a\\\\\\\"-.5\" 0..8 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\\\"b#c\"", + output: "\"a\\\"b#c\"", + ast: "string \"a\\\\\\\"b#c\" 0..8 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\\\\b\"", + output: "\"a\\\\b\"", + ast: "string \"a\\\\\\\\b\" 0..6 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\\u200fb\"", + output: "\"a\\u200fb\"", + ast: "string \"a\\\\u200fb\" 0..10 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\\ufeffb\"", + output: "\"a\\ufeffb\"", + ast: "string \"a\\\\ufeffb\" 0..10 quote=\"\\\"\"", + }, + ParserCase { + input: "\"e\\u0301\"", + output: "\"e\\u0301\"", + ast: "string \"e\\\\u0301\" 0..9 quote=\"\\\"\"", + }, + ParserCase { + input: "\"hello)world\"", + output: "\"hello)world\"", + ast: "string \"hello)world\" 0..13 quote=\"\\\"\"", + }, + ParserCase { + input: "\"var(foo)\", sans-serif", + output: "\"var(foo)\", sans-serif", + ast: "string \"var(foo)\" 0..10 quote=\"\\\"\"\ndiv \",\" 10..12 before=\"\" after=\" \"\nword \"sans-serif\" 12..22", + }, + ParserCase { + input: "\"•\"", + output: "\"•\"", + ast: "string \"•\" 0..5 quote=\"\\\"\"", + }, + ParserCase { + input: "\"•✓日本語😀\"", + output: "\"•✓日本語😀\"", + ast: "string \"•✓日本語😀\" 0..21 quote=\"\\\"\"", + }, + ParserCase { + input: "\"🎉 -.5\"", + output: "\"🎉 -.5\"", + ast: "string \"🎉 -.5\" 0..10 quote=\"\\\"\"", + }, + ParserCase { + input: "'-.5'", + output: "'-.5'", + ast: "string \"-.5\" 0..5 quote=\"'\"", + }, + ParserCase { + input: "'Helvetica Neue", + output: "'Helvetica Neue", + ast: "string \"Helvetica Neue\" 0..15 quote=\"'\" unclosed", + }, + ParserCase { + input: "'Helvetica Neue', sans-serif", + output: "'Helvetica Neue', sans-serif", + ast: "string \"Helvetica Neue\" 0..16 quote=\"'\"\ndiv \",\" 16..18 before=\"\" after=\" \"\nword \"sans-serif\" 18..28", + }, + ParserCase { + input: "'SF Pro Text', 'SF Pro Icons', Helvetica Neue', 'Helvetica', sans-serif", + output: "'SF Pro Text', 'SF Pro Icons', Helvetica Neue', 'Helvetica', sans-serif", + ast: "string \"SF Pro Text\" 0..13 quote=\"'\"\ndiv \",\" 13..15 before=\"\" after=\" \"\nstring \"SF Pro Icons\" 15..29 quote=\"'\"\ndiv \",\" 29..31 before=\"\" after=\" \"\nword \"Helvetica\" 31..40\nspace \" \" 40..41\nword \"Neue\" 41..45\nstring \", \" 45..49 quote=\"'\"\nword \"Helvetica\" 49..58\nstring \", sans-serif\" 58..71 quote=\"'\" unclosed", + }, + ParserCase { + input: "'a)b#c%d.5/1*2('", + output: "'a)b#c%d.5/1*2('", + ast: "string \"a)b#c%d.5/1*2(\" 0..16 quote=\"'\"", + }, + ParserCase { + input: "'a\\'b#c'", + output: "'a\\'b#c'", + ast: "string \"a\\\\'b#c\" 0..8 quote=\"'\"", + }, + ParserCase { + input: "'•✓日本語😀'", + output: "'•✓日本語😀'", + ast: "string \"•✓日本語😀\" 0..21 quote=\"'\"", + }, + ParserCase { + input: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif", + output: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif", + ast: "word \"-apple-system\" 0..13\ndiv \",\" 13..15 before=\"\" after=\" \"\nword \"BlinkMacSystemFont\" 15..33\ndiv \",\" 33..35 before=\"\" after=\" \"\nstring \"Segoe UI\" 35..45 quote=\"\\\"\"\ndiv \",\" 45..47 before=\"\" after=\" \"\nword \"Roboto\" 47..53\ndiv \",\" 53..55 before=\"\" after=\" \"\nword \"sans-serif\" 55..65", + }, + ParserCase { + input: "A\\;B", + output: "A\\;B", + ast: "word \"A\\\\;B\" 0..4", + }, + ParserCase { + input: "My\\\\ Font, sans-serif", + output: "My\\\\ Font, sans-serif", + ast: "word \"My\\\\\\\\\" 0..4\nspace \" \" 4..5\nword \"Font\" 5..9\ndiv \",\" 9..11 before=\"\" after=\" \"\nword \"sans-serif\" 11..21", + }, + ParserCase { + input: "\\2014;calc(", + output: "\\2014;calc(", + ast: "function \"\\\\2014;calc\" 0..11 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "\\\\😀", + output: "\\\\😀", + ast: "word \"\\\\\\\\😀\" 0..6", + }, + ParserCase { + input: "a\\u00a0b", + output: "a\\u00a0b", + ast: "word \"a\\\\u00a0b\" 0..8", + }, + ParserCase { + input: "a\\{b", + output: "a\\{b", + ast: "word \"a\\\\{b\" 0..4", + }, + ParserCase { + input: "a\\}b", + output: "a\\}b", + ast: "word \"a\\\\}b\" 0..4", + }, + ParserCase { + input: "e\\u0301", + output: "e\\u0301", + ast: "word \"e\\\\u0301\" 0..7", + }, + ParserCase { + input: "привет", + output: "привет", + ast: "word \"привет\" 0..12", + }, + ParserCase { + input: "שלום", + output: "שלום", + ast: "word \"שלום\" 0..8", + }, + ParserCase { + input: "مرحبا", + output: "مرحبا", + ast: "word \"مرحبا\" 0..10", + }, + ParserCase { + input: "✓", + output: "✓", + ast: "word \"✓\" 0..3", + }, + ParserCase { + input: "日本語", + output: "日本語", + ast: "word \"日本語\" 0..9", + }, + ParserCase { + input: "日本語;calc(1px", + output: "日本語;calc(1px", + ast: "function \"日本語;calc\" 0..18 before=\"\" after=\"\" unclosed nodes=1\n word \"1px\" 15..18", + }, + ParserCase { + input: "😀", + output: "😀", + ast: "word \"😀\" 0..4", + }, + ParserCase { + input: "🙂;sans-serif", + output: "🙂;sans-serif", + ast: "word \"🙂;sans-serif\" 0..15", + }, + ParserCase { + input: "1.25rem", + output: "1.25rem", + ast: "word \"1.25rem\" 0..7", + }, + ParserCase { + input: "14px", + output: "14px", + ast: "word \"14px\" 0..4", + }, + ParserCase { + input: "16px", + output: "16px", + ast: "word \"16px\" 0..4", + }, + ParserCase { + input: "1em", + output: "1em", + ast: "word \"1em\" 0..3", + }, + ParserCase { + input: "1rem", + output: "1rem", + ast: "word \"1rem\" 0..4", + }, + ParserCase { + input: "24px", + output: "24px", + ast: "word \"24px\" 0..4", + }, + ParserCase { + input: "2em", + output: "2em", + ast: "word \"2em\" 0..3", + }, + ParserCase { + input: "32px", + output: "32px", + ast: "word \"32px\" 0..4", + }, + ParserCase { + input: "calc(100% - 24px)", + output: "calc(100% - 24px)", + ast: "function \"calc\" 0..17 before=\"\" after=\"\" nodes=5\n word \"100%\" 5..9\n space \" \" 9..10\n word \"-\" 10..11\n space \" \" 11..12\n word \"24px\" 12..16", + }, + ParserCase { + input: "clamp(1rem, 2vw, 3rem)", + output: "clamp(1rem, 2vw, 3rem)", + ast: "function \"clamp\" 0..22 before=\"\" after=\"\" nodes=5\n word \"1rem\" 6..10\n div \",\" 10..12 before=\"\" after=\" \"\n word \"2vw\" 12..15\n div \",\" 15..17 before=\"\" after=\" \"\n word \"3rem\" 17..21", + }, + ParserCase { + input: "2px", + output: "2px", + ast: "word \"2px\" 0..3", + }, + ParserCase { + input: "var(--gap)rem", + output: "var(--gap)rem", + ast: "function \"var\" 0..10 before=\"\" after=\"\" nodes=1\n word \"--gap\" 4..9\nword \"rem\" 10..13", + }, + ParserCase { + input: "-1", + output: "-1", + ast: "word \"-1\" 0..2", + }, + ParserCase { + input: "content", + output: "content", + ast: "word \"content\" 0..7", + }, + ParserCase { + input: "sidebar", + output: "sidebar", + ast: "word \"sidebar\" 0..7", + }, + ParserCase { + input: "column", + output: "column", + ast: "word \"column\" 0..6", + }, + ParserCase { + input: "\"content\"", + output: "\"content\"", + ast: "string \"content\" 0..9 quote=\"\\\"\"", + }, + ParserCase { + input: "\"content\" \"sidebar\"", + output: "\"content\" \"sidebar\"", + ast: "string \"content\" 0..9 quote=\"\\\"\"\nspace \" \" 9..10\nstring \"sidebar\" 10..19 quote=\"\\\"\"", + }, + ParserCase { + input: "\"content\"\"sidebar\"", + output: "\"content\"\"sidebar\"", + ast: "string \"content\" 0..9 quote=\"\\\"\"\nstring \"sidebar\" 9..18 quote=\"\\\"\"", + }, + ParserCase { + input: "\"sidebar content\"", + output: "\"sidebar content\"", + ast: "string \"sidebar content\" 0..17 quote=\"\\\"\"", + }, + ParserCase { + input: "'\"content\" \"sidebar\"'", + output: "'\"content\" \"sidebar\"'", + ast: "string \"\\\"content\\\" \\\"sidebar\\\"\" 0..21 quote=\"'\"", + }, + ParserCase { + input: "'\"content\"'", + output: "'\"content\"'", + ast: "string \"\\\"content\\\"\" 0..11 quote=\"'\"", + }, + ParserCase { + input: "0fr", + output: "0fr", + ast: "word \"0fr\" 0..3", + }, + ParserCase { + input: "100%", + output: "100%", + ast: "word \"100%\" 0..4", + }, + ParserCase { + input: "auto minmax(0, 1fr)", + output: "auto minmax(0, 1fr)", + ast: "word \"auto\" 0..4\nspace \" \" 4..5\nfunction \"minmax\" 5..19 before=\"\" after=\"\" nodes=3\n word \"0\" 12..13\n div \",\" 13..15 before=\"\" after=\" \"\n word \"1fr\" 15..18", + }, + ParserCase { + input: "minmax(0, 1fr)", + output: "minmax(0, 1fr)", + ast: "function \"minmax\" 0..14 before=\"\" after=\"\" nodes=3\n word \"0\" 7..8\n div \",\" 8..10 before=\"\" after=\" \"\n word \"1fr\" 10..13", + }, + ParserCase { + input: "var(--x)fr", + output: "var(--x)fr", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"fr\" 8..10", + }, + ParserCase { + input: "minmax(0, 1fr) auto", + output: "minmax(0, 1fr) auto", + ast: "function \"minmax\" 0..14 before=\"\" after=\"\" nodes=3\n word \"0\" 7..8\n div \",\" 8..10 before=\"\" after=\" \"\n word \"1fr\" 10..13\nspace \" \" 14..15\nword \"auto\" 15..19", + }, + ParserCase { + input: "/* c */ calc-size(auto, size * 0)", + output: "/* c */ calc-size(auto, size * 0)", + ast: "comment \" c \" 0..7\nspace \" \" 7..8\nfunction \"calc-size\" 8..33 before=\"\" after=\"\" nodes=7\n word \"auto\" 18..22\n div \",\" 22..24 before=\"\" after=\" \"\n word \"size\" 24..28\n space \" \" 28..29\n word \"*\" 29..30\n space \" \" 30..31\n word \"0\" 31..32", + }, + ParserCase { + input: "/* unclosed", + output: "/* unclosed", + ast: "comment \" unclosed\" 0..11 unclosed", + }, + ParserCase { + input: "0", + output: "0", + ast: "word \"0\" 0..1", + }, + ParserCase { + input: "100px", + output: "100px", + ast: "word \"100px\" 0..5", + }, + ParserCase { + input: "1px /* unclosed", + output: "1px /* unclosed", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\ncomment \" unclosed\" 4..15 unclosed", + }, + ParserCase { + input: "@ /* unclosed", + output: "@ /* unclosed", + ast: "word \"@\" 0..1\nspace \" \" 1..2\ncomment \" unclosed\" 2..13 unclosed", + }, + ParserCase { + input: "@ { color: red", + output: "@ { color: red", + ast: "word \"@\" 0..1\nspace \" \" 1..2\nword \"{\" 2..3\nspace \" \" 3..4\nword \"color\" 4..9\ndiv \":\" 9..11 before=\"\" after=\" \"\nword \"red\" 11..14", + }, + ParserCase { + input: "@ } .evil{color:red", + output: "@ } .evil{color:red", + ast: "word \"@\" 0..1\nspace \" \" 1..2\nword \"}\" 2..3\nspace \" \" 3..4\nword \".evil{color\" 4..15\ndiv \":\" 15..16 before=\"\" after=\"\"\nword \"red\" 16..19", + }, + ParserCase { + input: "CALC-SIZE(auto, round(up, size, 50px))", + output: "CALC-SIZE(auto, round(up, size, 50px))", + ast: "function \"CALC-SIZE\" 0..38 before=\"\" after=\"\" nodes=3\n word \"auto\" 10..14\n div \",\" 14..16 before=\"\" after=\" \"\n function \"round\" 16..37 before=\"\" after=\"\" nodes=5\n word \"up\" 22..24\n div \",\" 24..26 before=\"\" after=\" \"\n word \"size\" 26..30\n div \",\" 30..32 before=\"\" after=\" \"\n word \"50px\" 32..36", + }, + ParserCase { + input: "[foo]", + output: "[foo]", + ast: "word \"[foo]\" 0..5", + }, + ParserCase { + input: "calc-size( auto , size * 0 )", + output: "calc-size( auto , size * 0 )", + ast: "function \"calc-size\" 0..32 before=\" \" after=\" \" nodes=7\n word \"auto\" 11..15\n div \",\" 15..18 before=\" \" after=\" \"\n word \"size\" 18..22\n space \" \" 22..25\n word \"*\" 25..26\n space \" \" 26..29\n word \"0\" 29..30", + }, + ParserCase { + input: "calc-size(300px + 2rem, size / 2)", + output: "calc-size(300px + 2rem, size / 2)", + ast: "function \"calc-size\" 0..33 before=\"\" after=\"\" nodes=9\n word \"300px\" 10..15\n space \" \" 15..16\n word \"+\" 16..17\n space \" \" 17..18\n word \"2rem\" 18..22\n div \",\" 22..24 before=\"\" after=\" \"\n word \"size\" 24..28\n div \"/\" 28..31 before=\" \" after=\" \"\n word \"2\" 31..32", + }, + ParserCase { + input: "calc-size(any, 300px * 1.5)", + output: "calc-size(any, 300px * 1.5)", + ast: "function \"calc-size\" 0..27 before=\"\" after=\"\" nodes=7\n word \"any\" 10..13\n div \",\" 13..15 before=\"\" after=\" \"\n word \"300px\" 15..20\n space \" \" 20..21\n word \"*\" 21..22\n space \" \" 22..23\n word \"1.5\" 23..26", + }, + ParserCase { + input: "calc-size(auto, size * 0)", + output: "calc-size(auto, size * 0)", + ast: "function \"calc-size\" 0..25 before=\"\" after=\"\" nodes=7\n word \"auto\" 10..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"size\" 16..20\n space \" \" 20..21\n word \"*\" 21..22\n space \" \" 22..23\n word \"0\" 23..24", + }, + ParserCase { + input: "calc-size(auto, size * 0) /* c */", + output: "calc-size(auto, size * 0) /* c */", + ast: "function \"calc-size\" 0..25 before=\"\" after=\"\" nodes=7\n word \"auto\" 10..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"size\" 16..20\n space \" \" 20..21\n word \"*\" 21..22\n space \" \" 22..23\n word \"0\" 23..24\nspace \" \" 25..26\ncomment \" c \" 26..33", + }, + ParserCase { + input: "calc-size(calc-size(max-content, size), size + 2rem)", + output: "calc-size(calc-size(max-content, size), size + 2rem)", + ast: "function \"calc-size\" 0..52 before=\"\" after=\"\" nodes=7\n function \"calc-size\" 10..38 before=\"\" after=\"\" nodes=3\n word \"max-content\" 20..31\n div \",\" 31..33 before=\"\" after=\" \"\n word \"size\" 33..37\n div \",\" 38..40 before=\"\" after=\" \"\n word \"size\" 40..44\n space \" \" 44..45\n word \"+\" 45..46\n space \" \" 46..47\n word \"2rem\" 47..51", + }, + ParserCase { + input: "calc-size(var(--intrinsic-size), max(100px, size + 20px))", + output: "calc-size(var(--intrinsic-size), max(100px, size + 20px))", + ast: "function \"calc-size\" 0..57 before=\"\" after=\"\" nodes=3\n function \"var\" 10..31 before=\"\" after=\"\" nodes=1\n word \"--intrinsic-size\" 14..30\n div \",\" 31..33 before=\"\" after=\" \"\n function \"max\" 33..56 before=\"\" after=\"\" nodes=7\n word \"100px\" 37..42\n div \",\" 42..44 before=\"\" after=\" \"\n word \"size\" 44..48\n space \" \" 48..49\n word \"+\" 49..50\n space \" \" 50..51\n word \"20px\" 51..55", + }, + ParserCase { + input: "false", + output: "false", + ast: "word \"false\" 0..5", + }, + ParserCase { + input: "foo * bar", + output: "foo * bar", + ast: "word \"foo\" 0..3\nspace \" \" 3..4\nword \"*\" 4..5\nspace \" \" 5..6\nword \"bar\" 6..9", + }, + ParserCase { + input: "future-fn(\"a, b\" * 2)", + output: "future-fn(\"a, b\" * 2)", + ast: "function \"future-fn\" 0..23 before=\"\" after=\"\" nodes=5\n string \"a, b\" 10..18 quote=\"\\\"\"\n space \" \" 18..19\n word \"*\" 19..20\n space \" \" 20..21\n word \"2\" 21..22", + }, + ParserCase { + input: "future-fn(\"a\\\", b\" * 2)", + output: "future-fn(\"a\\\", b\" * 2)", + ast: "function \"future-fn\" 0..25 before=\"\" after=\"\" nodes=5\n string \"a\\\\\\\", b\" 10..20 quote=\"\\\"\"\n space \" \" 20..21\n word \"*\" 21..22\n space \" \" 22..23\n word \"2\" 23..24", + }, + ParserCase { + input: "future-fn(\"a\\\\\" * 2)", + output: "future-fn(\"a\\\\\" * 2)", + ast: "function \"future-fn\" 0..20 before=\"\" after=\"\" nodes=5\n string \"a\\\\\\\\\" 10..15 quote=\"\\\"\"\n space \" \" 15..16\n word \"*\" 16..17\n space \" \" 17..18\n word \"2\" 18..19", + }, + ParserCase { + input: "future-fn(foo * 2)", + output: "future-fn(foo * 2)", + ast: "function \"future-fn\" 0..18 before=\"\" after=\"\" nodes=5\n word \"foo\" 10..13\n space \" \" 13..14\n word \"*\" 14..15\n space \" \" 15..16\n word \"2\" 16..17", + }, + ParserCase { + input: "future-fn(foo /* a, b */ * 2)", + output: "future-fn(foo /* a, b */ * 2)", + ast: "function \"future-fn\" 0..31 before=\"\" after=\"\" nodes=7\n word \"foo\" 10..13\n space \" \" 13..14\n comment \" a, b \" 14..26\n space \" \" 26..27\n word \"*\" 27..28\n space \" \" 28..29\n word \"2\" 29..30", + }, + ParserCase { + input: "future-fn(foo/2 * @)", + output: "future-fn(foo/2 * @)", + ast: "function \"future-fn\" 0..20 before=\"\" after=\"\" nodes=7\n word \"foo\" 10..13\n div \"/\" 13..14 before=\"\" after=\"\"\n word \"2\" 14..15\n space \" \" 15..16\n word \"*\" 16..17\n space \" \" 17..18\n word \"@\" 18..19", + }, + ParserCase { + input: "var(--h)vh", + output: "var(--h)vh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--h\" 4..7\nword \"vh\" 8..10", + }, + ParserCase { + input: "}", + output: "}", + ast: "word \"}\" 0..1", + }, + ParserCase { + input: "0rad", + output: "0rad", + ast: "word \"0rad\" 0..4", + }, + ParserCase { + input: "90deg", + output: "90deg", + ast: "word \"90deg\" 0..5", + }, + ParserCase { + input: "0dpi", + output: "0dpi", + ast: "word \"0dpi\" 0..4", + }, + ParserCase { + input: "10px 20px 30px 40px", + output: "10px 20px 30px 40px", + ast: "word \"10px\" 0..4\nspace \" \" 4..5\nword \"20px\" 5..9\nspace \" \" 9..10\nword \"30px\" 10..14\nspace \" \" 14..15\nword \"40px\" 15..19", + }, + ParserCase { + input: "10px 20px", + output: "10px 20px", + ast: "word \"10px\" 0..4\nspace \" \" 4..5\nword \"20px\" 5..9", + }, + ParserCase { + input: "flex-start", + output: "flex-start", + ast: "word \"flex-start\" 0..10", + }, + ParserCase { + input: "\"--\"", + output: "\"--\"", + ast: "string \"--\" 0..4 quote=\"\\\"\"", + }, + ParserCase { + input: "'=='", + output: "'=='", + ast: "string \"==\" 0..4 quote=\"'\"", + }, + ParserCase { + input: "inside", + output: "inside", + ast: "word \"inside\" 0..6", + }, + ParserCase { + input: "inside \"--\" linear-gradient(90deg, white 100%)", + output: "inside \"--\" linear-gradient(90deg, white 100%)", + ast: "word \"inside\" 0..6\nspace \" \" 6..7\nstring \"--\" 7..11 quote=\"\\\"\"\nspace \" \" 11..12\nfunction \"linear-gradient\" 12..46 before=\"\" after=\"\" nodes=5\n word \"90deg\" 28..33\n div \",\" 33..35 before=\"\" after=\" \"\n word \"white\" 35..40\n space \" \" 40..41\n word \"100%\" 41..45", + }, + ParserCase { + input: "none georgian", + output: "none georgian", + ast: "word \"none\" 0..4\nspace \" \" 4..5\nword \"georgian\" 5..13", + }, + ParserCase { + input: "none inherit", + output: "none inherit", + ast: "word \"none\" 0..4\nspace \" \" 4..5\nword \"inherit\" 5..12", + }, + ParserCase { + input: "none inside", + output: "none inside", + ast: "word \"none\" 0..4\nspace \" \" 4..5\nword \"inside\" 5..11", + }, + ParserCase { + input: "none square", + output: "none square", + ast: "word \"none\" 0..4\nspace \" \" 4..5\nword \"square\" 5..11", + }, + ParserCase { + input: "none var(--image)", + output: "none var(--image)", + ast: "word \"none\" 0..4\nspace \" \" 4..5\nfunction \"var\" 5..17 before=\"\" after=\"\" nodes=1\n word \"--image\" 9..16", + }, + ParserCase { + input: "outside \"+\" linear-gradient(90deg, white 100%)", + output: "outside \"+\" linear-gradient(90deg, white 100%)", + ast: "word \"outside\" 0..7\nspace \" \" 7..8\nstring \"+\" 8..11 quote=\"\\\"\"\nspace \" \" 11..12\nfunction \"linear-gradient\" 12..46 before=\"\" after=\"\" nodes=5\n word \"90deg\" 28..33\n div \",\" 33..35 before=\"\" after=\" \"\n word \"white\" 35..40\n space \" \" 40..41\n word \"100%\" 41..45", + }, + ParserCase { + input: "simp-chinese-informal linear-gradient(90deg, white 100%)", + output: "simp-chinese-informal linear-gradient(90deg, white 100%)", + ast: "word \"simp-chinese-informal\" 0..21\nspace \" \" 21..22\nfunction \"linear-gradient\" 22..56 before=\"\" after=\"\" nodes=5\n word \"90deg\" 38..43\n div \",\" 43..45 before=\"\" after=\" \"\n word \"white\" 45..50\n space \" \" 50..51\n word \"100%\" 51..55", + }, + ParserCase { + input: "square", + output: "square", + ast: "word \"square\" 0..6", + }, + ParserCase { + input: "square inside", + output: "square inside", + ast: "word \"square\" 0..6\nspace \" \" 6..7\nword \"inside\" 7..13", + }, + ParserCase { + input: "outside", + output: "outside", + ast: "word \"outside\" 0..7", + }, + ParserCase { + input: "-5px -.5px", + output: "-5px -.5px", + ast: "word \"-5px\" 0..4\nspace \" \" 4..5\nword \"-.5px\" 5..10", + }, + ParserCase { + input: "1 2 3 4", + output: "1 2 3 4", + ast: "word \"1\" 0..1\nspace \" \" 1..2\nword \"2\" 2..3\nspace \" \" 3..4\nword \"3\" 4..5\nspace \" \" 5..6\nword \"4\" 6..7", + }, + ParserCase { + input: "1.2345678901234567px 7%", + output: "1.2345678901234567px 7%", + ast: "word \"1.2345678901234567px\" 0..20\nspace \" \" 20..21\nword \"7%\" 21..23", + }, + ParserCase { + input: "10px 20px 30px", + output: "10px 20px 30px", + ast: "word \"10px\" 0..4\nspace \" \" 4..7\nword \"20px\" 7..11\nspace \" \" 11..14\nword \"30px\" 14..18", + }, + ParserCase { + input: "1E2px 1.50px", + output: "1E2px 1.50px", + ast: "word \"1E2px\" 0..5\nspace \" \" 5..6\nword \"1.50px\" 6..12", + }, + ParserCase { + input: "1px\\t2px\\n3px 4px", + output: "1px\\t2px\\n3px 4px", + ast: "word \"1px\\\\t2px\\\\n3px\" 0..13\nspace \" \" 13..15\nword \"4px\" 15..18", + }, + ParserCase { + input: "4px ", + output: "4px ", + ast: "word \"4px\" 0..3\nspace \" \" 3..4", + }, + ParserCase { + input: "auto", + output: "auto", + ast: "word \"auto\" 0..4", + }, + ParserCase { + input: "calc((100% - 50px) * 0.5) 20px 0", + output: "calc((100% - 50px) * 0.5) 20px 0", + ast: "function \"calc\" 0..25 before=\"\" after=\"\" nodes=5\n function \"\" 5..18 before=\"\" after=\"\" nodes=5\n word \"100%\" 6..10\n space \" \" 10..11\n word \"-\" 11..12\n space \" \" 12..13\n word \"50px\" 13..17\n space \" \" 18..19\n word \"*\" 19..20\n space \" \" 20..21\n word \"0.5\" 21..24\nspace \" \" 25..26\nword \"20px\" 26..30\nspace \" \" 30..31\nword \"0\" 31..32", + }, + ParserCase { + input: "max(0px, (48px - var(--x16dnrjz)) / 2)", + output: "max(0px, (48px - var(--x16dnrjz)) / 2)", + ast: "function \"max\" 0..38 before=\"\" after=\"\" nodes=5\n word \"0px\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"\" 9..33 before=\"\" after=\"\" nodes=5\n word \"48px\" 10..14\n space \" \" 14..15\n word \"-\" 15..16\n space \" \" 16..17\n function \"var\" 17..32 before=\"\" after=\"\" nodes=1\n word \"--x16dnrjz\" 21..31\n div \"/\" 33..36 before=\" \" after=\" \"\n word \"2\" 36..37", + }, + ParserCase { + input: "5px 10px", + output: "5px 10px", + ast: "word \"5px\" 0..3\nspace \" \" 3..4\nword \"10px\" 4..8", + }, + ParserCase { + input: "-.24px", + output: "-.24px", + ast: "word \"-.24px\" 0..6", + }, + ParserCase { + input: "-0.5px", + output: "-0.5px", + ast: "word \"-0.5px\" 0..6", + }, + ParserCase { + input: "calc((100% - 50px) * 0.5)", + output: "calc((100% - 50px) * 0.5)", + ast: "function \"calc\" 0..25 before=\"\" after=\"\" nodes=5\n function \"\" 5..18 before=\"\" after=\"\" nodes=5\n word \"100%\" 6..10\n space \" \" 10..11\n word \"-\" 11..12\n space \" \" 12..13\n word \"50px\" 13..17\n space \" \" 18..19\n word \"*\" 19..20\n space \" \" 20..21\n word \"0.5\" 21..24", + }, + ParserCase { + input: "calc(100dvh - var(--header-height, 0px))", + output: "calc(100dvh - var(--header-height, 0px))", + ast: "function \"calc\" 0..40 before=\"\" after=\"\" nodes=5\n word \"100dvh\" 5..11\n space \" \" 11..12\n word \"-\" 12..13\n space \" \" 13..14\n function \"var\" 14..39 before=\"\" after=\"\" nodes=3\n word \"--header-height\" 18..33\n div \",\" 33..35 before=\"\" after=\" \"\n word \"0px\" 35..38", + }, + ParserCase { + input: "-.5", + output: "-.5", + ast: "word \"-.5\" 0..3", + }, + ParserCase { + input: ".5px", + output: ".5px", + ast: "word \".5px\" 0..4", + }, + ParserCase { + input: "hidden visible", + output: "hidden visible", + ast: "word \"hidden\" 0..6\nspace \" \" 6..7\nword \"visible\" 7..14", + }, + ParserCase { + input: "+1px +2% 0.5px 000.5px", + output: "+1px +2% 0.5px 000.5px", + ast: "word \"+1px\" 0..4\nspace \" \" 4..5\nword \"+2%\" 5..8\nspace \" \" 8..9\nword \"0.5px\" 9..14\nspace \" \" 14..15\nword \"000.5px\" 15..22", + }, + ParserCase { + input: "-0px 1e21px", + output: "-0px 1e21px", + ast: "word \"-0px\" 0..4\nspace \" \" 4..5\nword \"1e21px\" 5..11", + }, + ParserCase { + input: "2", + output: "2", + ast: "word \"2\" 0..1", + }, + ParserCase { + input: "calc((100% - 50px) * 0.5) var(--rightpadding, 20px)", + output: "calc((100% - 50px) * 0.5) var(--rightpadding, 20px)", + ast: "function \"calc\" 0..25 before=\"\" after=\"\" nodes=5\n function \"\" 5..18 before=\"\" after=\"\" nodes=5\n word \"100%\" 6..10\n space \" \" 10..11\n word \"-\" 11..12\n space \" \" 12..13\n word \"50px\" 13..17\n space \" \" 18..19\n word \"*\" 19..20\n space \" \" 20..21\n word \"0.5\" 21..24\nspace \" \" 25..26\nfunction \"var\" 26..51 before=\"\" after=\"\" nodes=3\n word \"--rightpadding\" 30..44\n div \",\" 44..46 before=\"\" after=\" \"\n word \"20px\" 46..50", + }, + ParserCase { + input: "var(--rightpadding, 20px)", + output: "var(--rightpadding, 20px)", + ast: "function \"var\" 0..25 before=\"\" after=\"\" nodes=3\n word \"--rightpadding\" 4..18\n div \",\" 18..20 before=\"\" after=\" \"\n word \"20px\" 20..24", + }, + ParserCase { + input: "box-none", + output: "box-none", + ast: "word \"box-none\" 0..8", + }, + ParserCase { + input: "box-only", + output: "box-only", + ast: "word \"box-only\" 0..8", + }, + ParserCase { + input: "--anchor", + output: "--anchor", + ast: "word \"--anchor\" 0..8", + }, + ParserCase { + input: "\"\"\"\"", + output: "\"\"\"\"", + ast: "string \"\" 0..2 quote=\"\\\"\"\nstring \"\" 2..4 quote=\"\\\"\"", + }, + ParserCase { + input: "\"\"\"\"\"\"", + output: "\"\"\"\"\"\"", + ast: "string \"\" 0..2 quote=\"\\\"\"\nstring \"\" 2..4 quote=\"\\\"\"\nstring \"\" 4..6 quote=\"\\\"\"", + }, + ParserCase { + input: "\"123\"", + output: "\"123\"", + ast: "string \"123\" 0..5 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\" x \"b\"", + output: "\"a\" x \"b\"", + ast: "string \"a\" 0..3 quote=\"\\\"\"\nspace \" \" 3..4\nword \"x\" 4..5\nspace \" \" 5..6\nstring \"b\" 6..9 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\"\"b\"", + output: "\"a\"\"b\"", + ast: "string \"a\" 0..3 quote=\"\\\"\"\nstring \"b\" 3..6 quote=\"\\\"\"", + }, + ParserCase { + input: "\"a\"'b'", + output: "\"a\"'b'", + ast: "string \"a\" 0..3 quote=\"\\\"\"\nstring \"b\" 3..6 quote=\"'\"", + }, + ParserCase { + input: "'\"\"'", + output: "'\"\"'", + ast: "string \"\\\"\\\"\" 0..4 quote=\"'\"", + }, + ParserCase { + input: "'\"123\"'", + output: "'\"123\"'", + ast: "string \"\\\"123\\\"\" 0..7 quote=\"'\"", + }, + ParserCase { + input: "''", + output: "''", + ast: "string \"\" 0..2 quote=\"'\"", + }, + ParserCase { + input: "'a''b'", + output: "'a''b'", + ast: "string \"a\" 0..3 quote=\"'\"\nstring \"b\" 3..6 quote=\"'\"", + }, + ParserCase { + input: "1px solid underline", + output: "1px solid underline", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"solid\" 4..9\nspace \" \" 9..10\nword \"underline\" 10..19", + }, + ParserCase { + input: "1px 1px 1px #000", + output: "1px 1px 1px #000", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"1px\" 4..7\nspace \" \" 7..8\nword \"1px\" 8..11\nspace \" \" 11..12\nword \"#000\" 12..16", + }, + ParserCase { + input: "0grad", + output: "0grad", + ast: "word \"0grad\" 0..5", + }, + ParserCase { + input: "0turn", + output: "0turn", + ast: "word \"0turn\" 0..5", + }, + ParserCase { + input: "rotate(0rad)", + output: "rotate(0rad)", + ast: "function \"rotate\" 0..12 before=\"\" after=\"\" nodes=1\n word \"0rad\" 7..11", + }, + ParserCase { + input: "rotate(10deg)translate3d(0,0,0)", + output: "rotate(10deg)translate3d(0,0,0)", + ast: "function \"rotate\" 0..13 before=\"\" after=\"\" nodes=1\n word \"10deg\" 7..12\nfunction \"translate3d\" 13..31 before=\"\" after=\"\" nodes=5\n word \"0\" 25..26\n div \",\" 26..27 before=\"\" after=\"\"\n word \"0\" 27..28\n div \",\" 28..29 before=\"\" after=\"\"\n word \"0\" 29..30", + }, + ParserCase { + input: "rotate(45deg)", + output: "rotate(45deg)", + ast: "function \"rotate\" 0..13 before=\"\" after=\"\" nodes=1\n word \"45deg\" 7..12", + }, + ParserCase { + input: "rotate(var(--a)deg)", + output: "rotate(var(--a)deg)", + ast: "function \"rotate\" 0..19 before=\"\" after=\"\" nodes=2\n function \"var\" 7..15 before=\"\" after=\"\" nodes=1\n word \"--a\" 11..14\n word \"deg\" 15..18", + }, + ParserCase { + input: "rotate(var(--x)grad)", + output: "rotate(var(--x)grad)", + ast: "function \"rotate\" 0..20 before=\"\" after=\"\" nodes=2\n function \"var\" 7..15 before=\"\" after=\"\" nodes=1\n word \"--x\" 11..14\n word \"grad\" 15..19", + }, + ParserCase { + input: "rotate(var(--x)rad)", + output: "rotate(var(--x)rad)", + ast: "function \"rotate\" 0..19 before=\"\" after=\"\" nodes=2\n function \"var\" 7..15 before=\"\" after=\"\" nodes=1\n word \"--x\" 11..14\n word \"rad\" 15..18", + }, + ParserCase { + input: "rotate(var(--x)turn)", + output: "rotate(var(--x)turn)", + ast: "function \"rotate\" 0..20 before=\"\" after=\"\" nodes=2\n function \"var\" 7..15 before=\"\" after=\"\" nodes=1\n word \"--x\" 11..14\n word \"turn\" 15..19", + }, + ParserCase { + input: "rotateX(45deg)", + output: "rotateX(45deg)", + ast: "function \"rotateX\" 0..14 before=\"\" after=\"\" nodes=1\n word \"45deg\" 8..13", + }, + ParserCase { + input: "scaleX(2)", + output: "scaleX(2)", + ast: "function \"scaleX\" 0..9 before=\"\" after=\"\" nodes=1\n word \"2\" 7..8", + }, + ParserCase { + input: "scaleY(0.5)", + output: "scaleY(0.5)", + ast: "function \"scaleY\" 0..11 before=\"\" after=\"\" nodes=1\n word \"0.5\" 7..10", + }, + ParserCase { + input: "skewX(10deg)", + output: "skewX(10deg)", + ast: "function \"skewX\" 0..12 before=\"\" after=\"\" nodes=1\n word \"10deg\" 6..11", + }, + ParserCase { + input: "skewY(5deg)", + output: "skewY(5deg)", + ast: "function \"skewY\" 0..11 before=\"\" after=\"\" nodes=1\n word \"5deg\" 6..10", + }, + ParserCase { + input: "translate(-.5px,\"-.25px\")", + output: "translate(-.5px,\"-.25px\")", + ast: "function \"translate\" 0..25 before=\"\" after=\"\" nodes=3\n word \"-.5px\" 10..15\n div \",\" 15..16 before=\"\" after=\"\"\n string \"-.25px\" 16..24 quote=\"\\\"\"", + }, + ParserCase { + input: "translate(-.5px,-.25px)", + output: "translate(-.5px,-.25px)", + ast: "function \"translate\" 0..23 before=\"\" after=\"\" nodes=3\n word \"-.5px\" 10..15\n div \",\" 15..16 before=\"\" after=\"\"\n word \"-.25px\" 16..22", + }, + ParserCase { + input: "translate(🎉,-.5px)", + output: "translate(🎉,-.5px)", + ast: "function \"translate\" 0..21 before=\"\" after=\"\" nodes=3\n word \"🎉\" 10..14\n div \",\" 14..15 before=\"\" after=\"\"\n word \"-.5px\" 15..20", + }, + ParserCase { + input: "translateX(0px) translateY(0) scale(1) rotate(30deg)", + output: "translateX(0px) translateY(0) scale(1) rotate(30deg)", + ast: "function \"translateX\" 0..15 before=\"\" after=\"\" nodes=1\n word \"0px\" 11..14\nspace \" \" 15..16\nfunction \"translateY\" 16..29 before=\"\" after=\"\" nodes=1\n word \"0\" 27..28\nspace \" \" 29..30\nfunction \"scale\" 30..38 before=\"\" after=\"\" nodes=1\n word \"1\" 36..37\nspace \" \" 38..39\nfunction \"rotate\" 39..52 before=\"\" after=\"\" nodes=1\n word \"30deg\" 46..51", + }, + ParserCase { + input: "translateX(100px) translateY(-300px) scale(0.7)", + output: "translateX(100px) translateY(-300px) scale(0.7)", + ast: "function \"translateX\" 0..17 before=\"\" after=\"\" nodes=1\n word \"100px\" 11..16\nspace \" \" 17..18\nfunction \"translateY\" 18..36 before=\"\" after=\"\" nodes=1\n word \"-300px\" 29..35\nspace \" \" 36..37\nfunction \"scale\" 37..47 before=\"\" after=\"\" nodes=1\n word \"0.7\" 43..46", + }, + ParserCase { + input: "translateX(10px)", + output: "translateX(10px)", + ast: "function \"translateX\" 0..16 before=\"\" after=\"\" nodes=1\n word \"10px\" 11..15", + }, + ParserCase { + input: "translateY(20px)", + output: "translateY(20px)", + ast: "function \"translateY\" 0..16 before=\"\" after=\"\" nodes=1\n word \"20px\" 11..15", + }, + ParserCase { + input: "opacity 1s", + output: "opacity 1s", + ast: "word \"opacity\" 0..7\nspace \" \" 7..8\nword \"1s\" 8..10", + }, + ParserCase { + input: "-.9s", + output: "-.9s", + ast: "word \"-.9s\" 0..4", + }, + ParserCase { + input: "0.01s", + output: "0.01s", + ast: "word \"0.01s\" 0..5", + }, + ParserCase { + input: "0ms", + output: "0ms", + ast: "word \"0ms\" 0..3", + }, + ParserCase { + input: "1000ms", + output: "1000ms", + ast: "word \"1000ms\" 0..6", + }, + ParserCase { + input: "10ms", + output: "10ms", + ast: "word \"10ms\" 0..4", + }, + ParserCase { + input: "1234ms", + output: "1234ms", + ast: "word \"1234ms\" 0..6", + }, + ParserCase { + input: "1ms", + output: "1ms", + ast: "word \"1ms\" 0..3", + }, + ParserCase { + input: "2s", + output: "2s", + ast: "word \"2s\" 0..2", + }, + ParserCase { + input: "var(--d)ms", + output: "var(--d)ms", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--d\" 4..7\nword \"ms\" 8..10", + }, + ParserCase { + input: "var(--x)s", + output: "var(--x)s", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"s\" 8..9", + }, + ParserCase { + input: "--foo", + output: "--foo", + ast: "word \"--foo\" 0..5", + }, + ParserCase { + input: "--myVar", + output: "--myVar", + ast: "word \"--myVar\" 0..7", + }, + ParserCase { + input: "all", + output: "all", + ast: "word \"all\" 0..3", + }, + ParserCase { + input: "bottom", + output: "bottom", + ast: "word \"bottom\" 0..6", + }, + ParserCase { + input: "height", + output: "height", + ast: "word \"height\" 0..6", + }, + ParserCase { + input: "margin-top", + output: "margin-top", + ast: "word \"margin-top\" 0..10", + }, + ParserCase { + input: "marginTop", + output: "marginTop", + ast: "word \"marginTop\" 0..9", + }, + ParserCase { + input: "opacity, inset-inline-start", + output: "opacity, inset-inline-start", + ast: "word \"opacity\" 0..7\ndiv \",\" 7..9 before=\"\" after=\" \"\nword \"inset-inline-start\" 9..27", + }, + ParserCase { + input: "opacity, insetInlineStart", + output: "opacity, insetInlineStart", + ast: "word \"opacity\" 0..7\ndiv \",\" 7..9 before=\"\" after=\" \"\nword \"insetInlineStart\" 9..25", + }, + ParserCase { + input: "opacity, margin-top", + output: "opacity, margin-top", + ast: "word \"opacity\" 0..7\ndiv \",\" 7..9 before=\"\" after=\" \"\nword \"margin-top\" 9..19", + }, + ParserCase { + input: "width", + output: "width", + ast: "word \"width\" 0..5", + }, + ParserCase { + input: "cubic-bezier(.08,.52,.52,1)", + output: "cubic-bezier(.08,.52,.52,1)", + ast: "function \"cubic-bezier\" 0..27 before=\"\" after=\"\" nodes=7\n word \".08\" 13..16\n div \",\" 16..17 before=\"\" after=\"\"\n word \".52\" 17..20\n div \",\" 20..21 before=\"\" after=\"\"\n word \".52\" 21..24\n div \",\" 24..25 before=\"\" after=\"\"\n word \"1\" 25..26", + }, + ParserCase { + input: ")", + output: ")", + ast: "word \")\" 0..1", + }, + ParserCase { + input: ")*3", + output: ")*3", + ast: "word \")*3\" 0..3", + }, + ParserCase { + input: ")-1", + output: ")-1", + ast: "word \")-1\" 0..3", + }, + ParserCase { + input: ")/1 )*.5", + output: ")/1 )*.5", + ast: "word \")\" 0..1\ndiv \"/\" 1..2 before=\"\" after=\"\"\nword \"1\" 2..3\nspace \" \" 3..4\nword \")*.5\" 4..8", + }, + ParserCase { + input: ")/7", + output: ")/7", + ast: "word \")\" 0..1\ndiv \"/\" 1..2 before=\"\" after=\"\"\nword \"7\" 2..3", + }, + ParserCase { + input: ")3", + output: ")3", + ast: "word \")3\" 0..2", + }, + ParserCase { + input: ")Q", + output: ")Q", + ast: "word \")Q\" 0..2", + }, + ParserCase { + input: "* 3", + output: "* 3", + ast: "word \"*\" 0..1\nspace \" \" 1..2\nword \"3\" 2..3", + }, + ParserCase { + input: "*(", + output: "*(", + ast: "function \"*\" 0..2 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "*(100%)", + output: "*(100%)", + ast: "function \"*\" 0..7 before=\"\" after=\"\" nodes=1\n word \"100%\" 2..6", + }, + ParserCase { + input: "/ 7", + output: "/ 7", + ast: "div \"/\" 0..2 before=\"\" after=\" \"\nword \"7\" 2..3", + }, + ParserCase { + input: "/* a */", + output: "/* a */", + ast: "comment \" a \" 0..7", + }, + ParserCase { + input: "/* a */1px", + output: "/* a */1px", + ast: "comment \" a \" 0..7\nword \"1px\" 7..10", + }, + ParserCase { + input: "/* a }b */ 1px", + output: "/* a }b */ 1px", + ast: "comment \" a }b \" 0..10\nspace \" \" 10..11\nword \"1px\" 11..14", + }, + ParserCase { + input: "/**/", + output: "/**/", + ast: "comment \"\" 0..4", + }, + ParserCase { + input: "/*/ x */ 1px", + output: "/**/ x */ 1px", + ast: "comment \"\" 0..3\nspace \" \" 3..4\nword \"x\" 4..5\nspace \" \" 5..6\nword \"*\" 6..7\ndiv \"/\" 7..9 before=\"\" after=\" \"\nword \"1px\" 9..12", + }, + ParserCase { + input: "/-1", + output: "/-1", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\nword \"-1\" 1..3", + }, + ParserCase { + input: "/-2", + output: "/-2", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\nword \"-2\" 1..3", + }, + ParserCase { + input: "/.5", + output: "/.5", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\nword \".5\" 1..3", + }, + ParserCase { + input: "/.5 /-1 *( *3", + output: "/.5 /-1 *( *3", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\nword \".5\" 1..3\ndiv \"/\" 3..5 before=\" \" after=\"\"\nword \"-1\" 5..7\nspace \" \" 7..8\nfunction \"*\" 8..13 before=\" \" after=\"\" unclosed nodes=1\n word \"*3\" 11..13", + }, + ParserCase { + input: "00\\\\", + output: "00\\\\", + ast: "word \"00\\\\\\\\\" 0..4", + }, + ParserCase { + input: "10%-.5px", + output: "10%-.5px", + ast: "word \"10%-.5px\" 0..8", + }, + ParserCase { + input: "1px /*/ y */", + output: "1px /**/ y */", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\ncomment \"\" 4..7\nspace \" \" 7..8\nword \"y\" 8..9\nspace \" \" 9..10\nword \"*\" 10..11\ndiv \"/\" 11..12 before=\"\" after=\"\"", + }, + ParserCase { + input: "1px\\u30002px", + output: "1px\\u30002px", + ast: "word \"1px\\\\u30002px\" 0..12", + }, + ParserCase { + input: "50%", + output: "50%", + ast: "word \"50%\" 0..3", + }, + ParserCase { + input: "calc((100% + 3% - 100px) / 7)", + output: "calc((100% + 3% - 100px) / 7)", + ast: "function \"calc\" 0..31 before=\"\" after=\"\" nodes=5\n function \"\" 5..26 before=\"\" after=\"\" nodes=9\n word \"100%\" 6..10\n space \" \" 10..11\n word \"+\" 11..12\n space \" \" 12..13\n word \"3%\" 13..15\n space \" \" 15..16\n word \"-\" 16..17\n space \" \" 17..20\n word \"100px\" 20..25\n space \" \" 26..27\n word \"/\" 27..28\n space \" \" 28..29\n word \"7\" 29..30", + }, + ParserCase { + input: "calc(-.5px + 1px)", + output: "calc(-.5px + 1px)", + ast: "function \"calc\" 0..17 before=\"\" after=\"\" nodes=5\n word \"-.5px\" 5..10\n space \" \" 10..11\n word \"+\" 11..12\n space \" \" 12..13\n word \"1px\" 13..16", + }, + ParserCase { + input: "calc(0px + 10px)", + output: "calc(0px + 10px)", + ast: "function \"calc\" 0..16 before=\"\" after=\"\" nodes=5\n word \"0px\" 5..8\n space \" \" 8..9\n word \"+\" 9..10\n space \" \" 10..11\n word \"10px\" 11..15", + }, + ParserCase { + input: "calc(0px - var(--someVar))", + output: "calc(0px - var(--someVar))", + ast: "function \"calc\" 0..26 before=\"\" after=\"\" nodes=5\n word \"0px\" 5..8\n space \" \" 8..9\n word \"-\" 9..10\n space \" \" 10..11\n function \"var\" 11..25 before=\"\" after=\"\" nodes=1\n word \"--someVar\" 15..24", + }, + ParserCase { + input: "calc(1+2)em", + output: "calc(1+2)em", + ast: "function \"calc\" 0..9 before=\"\" after=\"\" nodes=1\n word \"1+2\" 5..8\nword \"em\" 9..11", + }, + ParserCase { + input: "calc(100% - 20px)", + output: "calc(100% - 20px)", + ast: "function \"calc\" 0..17 before=\"\" after=\"\" nodes=5\n word \"100%\" 5..9\n space \" \" 9..10\n word \"-\" 10..11\n space \" \" 11..12\n word \"20px\" 12..16", + }, + ParserCase { + input: "calc(100% - 2px)", + output: "calc(100% - 2px)", + ast: "function \"calc\" 0..16 before=\"\" after=\"\" nodes=5\n word \"100%\" 5..9\n space \" \" 9..10\n word \"-\" 10..11\n space \" \" 11..12\n word \"2px\" 12..15", + }, + ParserCase { + input: "calc(100% - calc(20px + 10px))", + output: "calc(100% - calc(20px + 10px))", + ast: "function \"calc\" 0..30 before=\"\" after=\"\" nodes=5\n word \"100%\" 5..9\n space \" \" 9..10\n word \"-\" 10..11\n space \" \" 11..12\n function \"calc\" 12..29 before=\"\" after=\"\" nodes=5\n word \"20px\" 17..21\n space \" \" 21..22\n word \"+\" 22..23\n space \" \" 23..24\n word \"10px\" 24..28", + }, + ParserCase { + input: "calc(1px /*", + output: "calc(1px /*", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" unclosed nodes=3\n word \"1px\" 5..8\n space \" \" 8..9\n comment \"\" 9..11 unclosed", + }, + ParserCase { + input: "calc(1px);height:2px", + output: "calc(1px);height:2px", + ast: "function \"calc\" 0..9 before=\"\" after=\"\" nodes=1\n word \"1px\" 5..8\nword \";height\" 9..16\ndiv \":\" 16..17 before=\"\" after=\"\"\nword \"2px\" 17..20", + }, + ParserCase { + input: "calc(1px)calc(2px)calc(3px)", + output: "calc(1px)calc(2px)calc(3px)", + ast: "function \"calc\" 0..9 before=\"\" after=\"\" nodes=1\n word \"1px\" 5..8\nfunction \"calc\" 9..18 before=\"\" after=\"\" nodes=1\n word \"2px\" 14..17\nfunction \"calc\" 18..27 before=\"\" after=\"\" nodes=1\n word \"3px\" 23..26", + }, + ParserCase { + input: "calc(1px-.5px)", + output: "calc(1px-.5px)", + ast: "function \"calc\" 0..14 before=\"\" after=\"\" nodes=1\n word \"1px-.5px\" 5..13", + }, + ParserCase { + input: "calc(a)42", + output: "calc(a)42", + ast: "function \"calc\" 0..7 before=\"\" after=\"\" nodes=1\n word \"a\" 5..6\nword \"42\" 7..9", + }, + ParserCase { + input: "calc(var(--x)-.5px)", + output: "calc(var(--x)-.5px)", + ast: "function \"calc\" 0..19 before=\"\" after=\"\" nodes=2\n function \"var\" 5..13 before=\"\" after=\"\" nodes=1\n word \"--x\" 9..12\n word \"-.5px\" 13..18", + }, + ParserCase { + input: "calc(var(foo) + 10px)", + output: "calc(var(foo) + 10px)", + ast: "function \"calc\" 0..21 before=\"\" after=\"\" nodes=5\n function \"var\" 5..13 before=\"\" after=\"\" nodes=1\n word \"foo\" 9..12\n space \" \" 13..14\n word \"+\" 14..15\n space \" \" 15..16\n word \"10px\" 16..20", + }, + ParserCase { + input: "calc-size(fit-content,size/2)", + output: "calc-size(fit-content,size/2)", + ast: "function \"calc-size\" 0..29 before=\"\" after=\"\" nodes=5\n word \"fit-content\" 10..21\n div \",\" 21..22 before=\"\" after=\"\"\n word \"size\" 22..26\n div \"/\" 26..27 before=\"\" after=\"\"\n word \"2\" 27..28", + }, + ParserCase { + input: "max(100px, 200px)", + output: "max(100px, 200px)", + ast: "function \"max\" 0..17 before=\"\" after=\"\" nodes=3\n word \"100px\" 4..9\n div \",\" 9..11 before=\"\" after=\" \"\n word \"200px\" 11..16", + }, + ParserCase { + input: "min(100%, 500px)", + output: "min(100%, 500px)", + ast: "function \"min\" 0..16 before=\"\" after=\"\" nodes=3\n word \"100%\" 4..8\n div \",\" 8..10 before=\"\" after=\" \"\n word \"500px\" 10..15", + }, + ParserCase { + input: "size/2", + output: "size/2", + ast: "word \"size\" 0..4\ndiv \"/\" 4..5 before=\"\" after=\"\"\nword \"2\" 5..6", + }, + ParserCase { + input: "var(--x)%", + output: "var(--x)%", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"%\" 8..9", + }, + ParserCase { + input: "var(--x)ABC", + output: "var(--x)ABC", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"ABC\" 8..11", + }, + ParserCase { + input: "var(--x)EM", + output: "var(--x)EM", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"EM\" 8..10", + }, + ParserCase { + input: "var(--x)Hz", + output: "var(--x)Hz", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"Hz\" 8..10", + }, + ParserCase { + input: "var(--x)PX", + output: "var(--x)PX", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"PX\" 8..10", + }, + ParserCase { + input: "var(--x)Px", + output: "var(--x)Px", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"Px\" 8..10", + }, + ParserCase { + input: "var(--x)Q", + output: "var(--x)Q", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"Q\" 8..9", + }, + ParserCase { + input: "var(--x)a", + output: "var(--x)a", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"a\" 8..9", + }, + ParserCase { + input: "var(--x)abc", + output: "var(--x)abc", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"abc\" 8..11", + }, + ParserCase { + input: "var(--x)auto", + output: "var(--x)auto", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"auto\" 8..12", + }, + ParserCase { + input: "var(--x)calc", + output: "var(--x)calc", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"calc\" 8..12", + }, + ParserCase { + input: "var(--x)cap", + output: "var(--x)cap", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cap\" 8..11", + }, + ParserCase { + input: "var(--x)ch", + output: "var(--x)ch", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"ch\" 8..10", + }, + ParserCase { + input: "var(--x)cm", + output: "var(--x)cm", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cm\" 8..10", + }, + ParserCase { + input: "var(--x)cqb", + output: "var(--x)cqb", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cqb\" 8..11", + }, + ParserCase { + input: "var(--x)cqh", + output: "var(--x)cqh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cqh\" 8..11", + }, + ParserCase { + input: "var(--x)cqi", + output: "var(--x)cqi", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cqi\" 8..11", + }, + ParserCase { + input: "var(--x)cqmax", + output: "var(--x)cqmax", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cqmax\" 8..13", + }, + ParserCase { + input: "var(--x)cqmin", + output: "var(--x)cqmin", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cqmin\" 8..13", + }, + ParserCase { + input: "var(--x)cqw", + output: "var(--x)cqw", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"cqw\" 8..11", + }, + ParserCase { + input: "var(--x)div", + output: "var(--x)div", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"div\" 8..11", + }, + ParserCase { + input: "var(--x)dpcm", + output: "var(--x)dpcm", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"dpcm\" 8..12", + }, + ParserCase { + input: "var(--x)dpi", + output: "var(--x)dpi", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"dpi\" 8..11", + }, + ParserCase { + input: "var(--x)dppx", + output: "var(--x)dppx", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"dppx\" 8..12", + }, + ParserCase { + input: "var(--x)dvh", + output: "var(--x)dvh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"dvh\" 8..11", + }, + ParserCase { + input: "var(--x)dvw", + output: "var(--x)dvw", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"dvw\" 8..11", + }, + ParserCase { + input: "var(--x)em", + output: "var(--x)em", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"em\" 8..10", + }, + ParserCase { + input: "var(--x)ex", + output: "var(--x)ex", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"ex\" 8..10", + }, + ParserCase { + input: "var(--x)hello", + output: "var(--x)hello", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"hello\" 8..13", + }, + ParserCase { + input: "var(--x)ic", + output: "var(--x)ic", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"ic\" 8..10", + }, + ParserCase { + input: "var(--x)in", + output: "var(--x)in", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"in\" 8..10", + }, + ParserCase { + input: "var(--x)kHz", + output: "var(--x)kHz", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"kHz\" 8..11", + }, + ParserCase { + input: "var(--x)lh", + output: "var(--x)lh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"lh\" 8..10", + }, + ParserCase { + input: "var(--x)lvh", + output: "var(--x)lvh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"lvh\" 8..11", + }, + ParserCase { + input: "var(--x)lvw", + output: "var(--x)lvw", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"lvw\" 8..11", + }, + ParserCase { + input: "var(--x)mm", + output: "var(--x)mm", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"mm\" 8..10", + }, + ParserCase { + input: "var(--x)p", + output: "var(--x)p", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"p\" 8..9", + }, + ParserCase { + input: "var(--x)pc", + output: "var(--x)pc", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"pc\" 8..10", + }, + ParserCase { + input: "var(--x)pt", + output: "var(--x)pt", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"pt\" 8..10", + }, + ParserCase { + input: "var(--x)rgb", + output: "var(--x)rgb", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"rgb\" 8..11", + }, + ParserCase { + input: "var(--x)rlh", + output: "var(--x)rlh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"rlh\" 8..11", + }, + ParserCase { + input: "var(--x)rotate", + output: "var(--x)rotate", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"rotate\" 8..14", + }, + ParserCase { + input: "var(--x)solid", + output: "var(--x)solid", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"solid\" 8..13", + }, + ParserCase { + input: "var(--x)span", + output: "var(--x)span", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"span\" 8..12", + }, + ParserCase { + input: "var(--x)svh", + output: "var(--x)svh", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"svh\" 8..11", + }, + ParserCase { + input: "var(--x)svw", + output: "var(--x)svw", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"svw\" 8..11", + }, + ParserCase { + input: "var(--x)translate3d", + output: "var(--x)translate3d", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"translate3d\" 8..19", + }, + ParserCase { + input: "var(--x)var", + output: "var(--x)var", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"var\" 8..11", + }, + ParserCase { + input: "var(--x)vb", + output: "var(--x)vb", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"vb\" 8..10", + }, + ParserCase { + input: "var(--x)vi", + output: "var(--x)vi", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"vi\" 8..10", + }, + ParserCase { + input: "var(--x)vmax", + output: "var(--x)vmax", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"vmax\" 8..12", + }, + ParserCase { + input: "var(--x)vmin", + output: "var(--x)vmin", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"vmin\" 8..12", + }, + ParserCase { + input: "var(--x)vw", + output: "var(--x)vw", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"vw\" 8..10", + }, + ParserCase { + input: "var(--x)日本語", + output: "var(--x)日本語", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--x\" 4..7\nword \"日本語\" 8..17", + }, + ParserCase { + input: "é(1)", + output: "é(1)", + ast: "function \"é\" 0..5 before=\"\" after=\"\" nodes=1\n word \"1\" 3..4", + }, + ParserCase { + input: "éab(1)", + output: "éab(1)", + ast: "function \"éab\" 0..7 before=\"\" after=\"\" nodes=1\n word \"1\" 5..6", + }, + ParserCase { + input: "éurl(foo/2)", + output: "éurl(foo/2)", + ast: "function \"éurl\" 0..12 before=\"\" after=\"\" nodes=3\n word \"foo\" 6..9\n div \"/\" 9..10 before=\"\" after=\"\"\n word \"2\" 10..11", + }, + ParserCase { + input: "日本(1/2)", + output: "日本(1/2)", + ast: "function \"日本\" 0..11 before=\"\" after=\"\" nodes=3\n word \"1\" 7..8\n div \"/\" 8..9 before=\"\" after=\"\"\n word \"2\" 9..10", + }, + ParserCase { + input: "inset-inline-start", + output: "inset-inline-start", + ast: "word \"inset-inline-start\" 0..18", + }, + ParserCase { + input: "insetInlineStart", + output: "insetInlineStart", + ast: "word \"insetInlineStart\" 0..16", + }, + ParserCase { + input: "scroll-position", + output: "scroll-position", + ast: "word \"scroll-position\" 0..15", + }, + ParserCase { + input: " ", + output: " ", + ast: "space \" \" 0..1", + }, + ParserCase { + input: "\t\n\r\u{c}\u{b}", + output: "\t\n\r\u{c}\u{b}", + ast: "space \"\\t\\n\\r\\u000c\\u000b\" 0..5", + }, + ParserCase { + input: "calc(", + output: "calc(", + ast: "function \"calc\" 0..5 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "calc(1px", + output: "calc(1px", + ast: "function \"calc\" 0..8 before=\"\" after=\"\" unclosed nodes=1\n word \"1px\" 5..8", + }, + ParserCase { + input: "calc((1px)", + output: "calc((1px)", + ast: "function \"calc\" 0..10 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 5..10 before=\"\" after=\"\" nodes=1\n word \"1px\" 6..9", + }, + ParserCase { + input: "calc(1px))", + output: "calc(1px))", + ast: "function \"calc\" 0..9 before=\"\" after=\"\" nodes=1\n word \"1px\" 5..8\nword \")\" 9..10", + }, + ParserCase { + input: "))((", + output: "))((", + ast: "function \"))\" 0..4 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 3..4 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "(", + output: "(", + ast: "function \"\" 0..1 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "((", + output: "((", + ast: "function \"\" 0..2 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 1..2 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "(((", + output: "(((", + ast: "function \"\" 0..3 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 1..3 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 2..3 before=\"\" after=\"\" unclosed nodes=0", + }, + ParserCase { + input: "(('", + output: "(('", + ast: "function \"\" 0..4 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 1..4 before=\"\" after=\"\" unclosed nodes=1\n string \"\" 2..3 quote=\"'\" unclosed", + }, + ParserCase { + input: "(((\\", + output: "(((\\", + ast: "function \"\" 0..4 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 1..4 before=\"\" after=\"\" unclosed nodes=1\n function \"\" 2..4 before=\"\" after=\"\" unclosed nodes=1\n word \"\\\\\" 3..5", + }, + ParserCase { + input: "translate(1px, ", + output: "translate(1px, ", + ast: "function \"translate\" 0..15 before=\"\" after=\"\" unclosed nodes=2\n word \"1px\" 10..13\n div \",\" 13..15 before=\"\" after=\" \"", + }, + ParserCase { + input: "var(--a", + output: "var(--a", + ast: "function \"var\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \"--a\" 4..7", + }, + ParserCase { + input: "var(--a,", + output: "var(--a,", + ast: "function \"var\" 0..8 before=\"\" after=\"\" unclosed nodes=2\n word \"--a\" 4..7\n div \",\" 7..8 before=\"\" after=\"\"", + }, + ParserCase { + input: "url(a", + output: "url(a", + ast: "function \"url\" 0..5 before=\"\" after=\"\" unclosed nodes=1\n word \"a\" 4..5", + }, + ParserCase { + input: "url( a ", + output: "url( a ", + ast: "function \"url\" 0..7 before=\" \" after=\"\" unclosed nodes=2\n word \"a\" 5..6\n space \" \" 6..7", + }, + ParserCase { + input: "url(a b", + output: "url(a b", + ast: "function \"url\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \"a b\" 4..7", + }, + ParserCase { + input: "url('a", + output: "url('a", + ast: "function \"url\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n string \"a\" 4..6 quote=\"'\" unclosed", + }, + ParserCase { + input: "url(\"a", + output: "url(\"a", + ast: "function \"url\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n string \"a\" 4..6 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "\"", + output: "\"", + ast: "string \"\" 0..1 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "'", + output: "'", + ast: "string \"\" 0..1 quote=\"'\" unclosed", + }, + ParserCase { + input: "\"a", + output: "\"a", + ast: "string \"a\" 0..2 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "'a", + output: "'a", + ast: "string \"a\" 0..2 quote=\"'\" unclosed", + }, + ParserCase { + input: "\"a'", + output: "\"a'", + ast: "string \"a'\" 0..3 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "'a\"", + output: "'a\"", + ast: "string \"a\\\"\" 0..3 quote=\"'\" unclosed", + }, + ParserCase { + input: "\"a\" \"b\"", + output: "\"a\" \"b\"", + ast: "string \"a\" 0..3 quote=\"\\\"\"\nspace \" \" 3..4\nstring \"b\" 4..7 quote=\"\\\"\"", + }, + ParserCase { + input: "'a'\"b\"", + output: "'a'\"b\"", + ast: "string \"a\" 0..3 quote=\"'\"\nstring \"b\" 3..6 quote=\"\\\"\"", + }, + ParserCase { + input: "\\", + output: "\\", + ast: "word \"\\\\\" 0..2", + }, + ParserCase { + input: "a\\", + output: "a\\", + ast: "word \"a\\\\\" 0..3", + }, + ParserCase { + input: "\"a\\\"", + output: "\"a\\\"", + ast: "string \"a\\\\\\\"\" 0..4 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "'a\\'", + output: "'a\\'", + ast: "string \"a\\\\'\" 0..4 quote=\"'\" unclosed", + }, + ParserCase { + input: "\\\\", + output: "\\\\", + ast: "word \"\\\\\\\\\" 0..2", + }, + ParserCase { + input: "a\\\\", + output: "a\\\\", + ast: "word \"a\\\\\\\\\" 0..3", + }, + ParserCase { + input: "\"a\\\\\"", + output: "\"a\\\\\"", + ast: "string \"a\\\\\\\\\" 0..5 quote=\"\\\"\"", + }, + ParserCase { + input: "url(a\\)", + output: "url(a\\)", + ast: "function \"url\" 0..7 before=\"\" after=\"\" unclosed nodes=1\n word \"a\\\\)\" 4..7", + }, + ParserCase { + input: "url(a\\\\)", + output: "url(a\\\\)", + ast: "function \"url\" 0..8 before=\"\" after=\"\" nodes=1\n word \"a\\\\\\\\\" 4..7", + }, + ParserCase { + input: "/*", + output: "/*", + ast: "comment \"\" 0..2 unclosed", + }, + ParserCase { + input: "/**", + output: "/**", + ast: "comment \"*\" 0..3 unclosed", + }, + ParserCase { + input: "/*a", + output: "/*a", + ast: "comment \"a\" 0..3 unclosed", + }, + ParserCase { + input: "/*/", + output: "/**/", + ast: "comment \"\" 0..3", + }, + ParserCase { + input: "/*/ x */", + output: "/**/ x */", + ast: "comment \"\" 0..3\nspace \" \" 3..4\nword \"x\" 4..5\nspace \" \" 5..6\nword \"*\" 6..7\ndiv \"/\" 7..8 before=\"\" after=\"\"", + }, + ParserCase { + input: "/*a*/b/*c*/", + output: "/*a*/b/*c*/", + ast: "comment \"a\" 0..5\nword \"b\" 5..6\ncomment \"c\" 6..11", + }, + ParserCase { + input: "a/*b*/c", + output: "a/*b*/c", + ast: "word \"a\" 0..1\ncomment \"b\" 1..6\nword \"c\" 6..7", + }, + ParserCase { + input: "/* nested /* comment */", + output: "/* nested /* comment */", + ast: "comment \" nested /* comment \" 0..23", + }, + ParserCase { + input: ",", + output: ",", + ast: "div \",\" 0..1 before=\"\" after=\"\"", + }, + ParserCase { + input: ",,,", + output: ",,,", + ast: "div \",\" 0..1 before=\"\" after=\"\"\ndiv \",\" 1..2 before=\"\" after=\"\"\ndiv \",\" 2..3 before=\"\" after=\"\"", + }, + ParserCase { + input: ":", + output: ":", + ast: "div \":\" 0..1 before=\"\" after=\"\"", + }, + ParserCase { + input: "/", + output: "/", + ast: "div \"/\" 0..1 before=\"\" after=\"\"", + }, + ParserCase { + input: "//", + output: "//", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\ndiv \"/\" 1..2 before=\"\" after=\"\"", + }, + ParserCase { + input: "/ /", + output: "/ /", + ast: "div \"/\" 0..2 before=\"\" after=\" \"\ndiv \"/\" 2..3 before=\"\" after=\"\"", + }, + ParserCase { + input: "a,,b", + output: "a,,b", + ast: "word \"a\" 0..1\ndiv \",\" 1..2 before=\"\" after=\"\"\ndiv \",\" 2..3 before=\"\" after=\"\"\nword \"b\" 3..4", + }, + ParserCase { + input: "a , , b", + output: "a , , b", + ast: "word \"a\" 0..1\ndiv \",\" 1..4 before=\" \" after=\" \"\ndiv \",\" 4..6 before=\"\" after=\" \"\nword \"b\" 6..7", + }, + ParserCase { + input: " , ", + output: " , ", + ast: "div \",\" 0..3 before=\" \" after=\" \"", + }, + ParserCase { + input: "a:b:c", + output: "a:b:c", + ast: "word \"a\" 0..1\ndiv \":\" 1..2 before=\"\" after=\"\"\nword \"b\" 2..3\ndiv \":\" 3..4 before=\"\" after=\"\"\nword \"c\" 4..5", + }, + ParserCase { + input: " a ", + output: " a ", + ast: "space \" \" 0..2\nword \"a\" 2..3\nspace \" \" 3..5", + }, + ParserCase { + input: "a b", + output: "a b", + ast: "word \"a\" 0..1\nspace \" \" 1..3\nword \"b\" 3..4", + }, + ParserCase { + input: "calc( 1px )", + output: "calc( 1px )", + ast: "function \"calc\" 0..11 before=\" \" after=\" \" nodes=1\n word \"1px\" 6..9", + }, + ParserCase { + input: "calc( )", + output: "calc( )", + ast: "function \"calc\" 0..8 before=\" \" after=\"\" nodes=0", + }, + ParserCase { + input: "f( a , b )", + output: "f( a , b )", + ast: "function \"f\" 0..10 before=\" \" after=\" \" nodes=3\n word \"a\" 3..4\n div \",\" 4..7 before=\" \" after=\" \"\n word \"b\" 7..8", + }, + ParserCase { + input: "f(\n a\n)", + output: "f(\n a\n)", + ast: "function \"f\" 0..8 before=\"\\n \" after=\"\\n\" nodes=1\n word \"a\" 5..6", + }, + ParserCase { + input: "U+26", + output: "U+26", + ast: "unicode-range \"U+26\" 0..4", + }, + ParserCase { + input: "u+26", + output: "u+26", + ast: "unicode-range \"u+26\" 0..4", + }, + ParserCase { + input: "U+0-7F", + output: "U+0-7F", + ast: "unicode-range \"U+0-7F\" 0..6", + }, + ParserCase { + input: "U+4??", + output: "U+4??", + ast: "unicode-range \"U+4??\" 0..5", + }, + ParserCase { + input: "U+", + output: "U+", + ast: "word \"U+\" 0..2", + }, + ParserCase { + input: "U+zz", + output: "U+zz", + ast: "word \"U+zz\" 0..4", + }, + ParserCase { + input: "U+26,U+27", + output: "U+26,U+27", + ast: "unicode-range \"U+26\" 0..4\ndiv \",\" 4..5 before=\"\" after=\"\"\nunicode-range \"U+27\" 5..9", + }, + ParserCase { + input: "Ux26", + output: "Ux26", + ast: "word \"Ux26\" 0..4", + }, + ParserCase { + input: "var(--a)", + output: "var(--a)", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--a\" 4..7", + }, + ParserCase { + input: "var(--a, 1px)", + output: "var(--a, 1px)", + ast: "function \"var\" 0..13 before=\"\" after=\"\" nodes=3\n word \"--a\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"1px\" 9..12", + }, + ParserCase { + input: "var(--a, var(--b, 1px))", + output: "var(--a, var(--b, 1px))", + ast: "function \"var\" 0..23 before=\"\" after=\"\" nodes=3\n word \"--a\" 4..7\n div \",\" 7..9 before=\"\" after=\" \"\n function \"var\" 9..22 before=\"\" after=\"\" nodes=3\n word \"--b\" 13..16\n div \",\" 16..18 before=\"\" after=\" \"\n word \"1px\" 18..21", + }, + ParserCase { + input: "var(--a,var(--b,var(--c,0)))", + output: "var(--a,var(--b,var(--c,0)))", + ast: "function \"var\" 0..28 before=\"\" after=\"\" nodes=3\n word \"--a\" 4..7\n div \",\" 7..8 before=\"\" after=\"\"\n function \"var\" 8..27 before=\"\" after=\"\" nodes=3\n word \"--b\" 12..15\n div \",\" 15..16 before=\"\" after=\"\"\n function \"var\" 16..26 before=\"\" after=\"\" nodes=3\n word \"--c\" 20..23\n div \",\" 23..24 before=\"\" after=\"\"\n word \"0\" 24..25", + }, + ParserCase { + input: "var(--not-prefixed)", + output: "var(--not-prefixed)", + ast: "function \"var\" 0..19 before=\"\" after=\"\" nodes=1\n word \"--not-prefixed\" 4..18", + }, + ParserCase { + input: "var(missing-dashes)", + output: "var(missing-dashes)", + ast: "function \"var\" 0..19 before=\"\" after=\"\" nodes=1\n word \"missing-dashes\" 4..18", + }, + ParserCase { + input: "var(--a) var(--b)", + output: "var(--a) var(--b)", + ast: "function \"var\" 0..8 before=\"\" after=\"\" nodes=1\n word \"--a\" 4..7\nspace \" \" 8..9\nfunction \"var\" 9..17 before=\"\" after=\"\" nodes=1\n word \"--b\" 13..16", + }, + ParserCase { + input: "env(safe-area-inset-top, 0px)", + output: "env(safe-area-inset-top, 0px)", + ast: "function \"env\" 0..29 before=\"\" after=\"\" nodes=3\n word \"safe-area-inset-top\" 4..23\n div \",\" 23..25 before=\"\" after=\" \"\n word \"0px\" 25..28", + }, + ParserCase { + input: "Привет", + output: "Привет", + ast: "word \"Привет\" 0..12", + }, + ParserCase { + input: "url(日本語.png)", + output: "url(日本語.png)", + ast: "function \"url\" 0..18 before=\"\" after=\"\" nodes=1\n word \"日本語.png\" 4..17", + }, + ParserCase { + input: "f(→)", + output: "f(→)", + ast: "function \"f\" 0..6 before=\"\" after=\"\" nodes=1\n word \"→\" 2..5", + }, + ParserCase { + input: "/* 🙂 */", + output: "/* 🙂 */", + ast: "comment \" 🙂 \" 0..10", + }, + ParserCase { + input: "--Привет", + output: "--Привет", + ast: "word \"--Привет\" 0..14", + }, + ParserCase { + input: "a→b", + output: "a→b", + ast: "word \"a→b\" 0..5", + }, + ParserCase { + input: "🙂🙂🙂", + output: "🙂🙂🙂", + ast: "word \"🙂🙂🙂\" 0..12", + }, + ParserCase { + input: "\"\\1F642\"", + output: "\"\\1F642\"", + ast: "string \"\\\\1F642\" 0..8 quote=\"\\\"\"", + }, + ParserCase { + input: "content value", + output: "content value", + ast: "word \"content value\" 0..14", + }, + ParserCase { + input: "-WEBKIT-BOX", + output: "-WEBKIT-BOX", + ast: "word \"-WEBKIT-BOX\" 0..11", + }, + ParserCase { + input: "-moz-calc(1px + 2px)", + output: "-moz-calc(1px + 2px)", + ast: "function \"-moz-calc\" 0..20 before=\"\" after=\"\" nodes=5\n word \"1px\" 10..13\n space \" \" 13..14\n word \"+\" 14..15\n space \" \" 15..16\n word \"2px\" 16..19", + }, + ParserCase { + input: "-webkit-gradient(linear, left top, left bottom)", + output: "-webkit-gradient(linear, left top, left bottom)", + ast: "function \"-webkit-gradient\" 0..47 before=\"\" after=\"\" nodes=9\n word \"linear\" 17..23\n div \",\" 23..25 before=\"\" after=\" \"\n word \"left\" 25..29\n space \" \" 29..30\n word \"top\" 30..33\n div \",\" 33..35 before=\"\" after=\" \"\n word \"left\" 35..39\n space \" \" 39..40\n word \"bottom\" 40..46", + }, + ParserCase { + input: "--webkit-not-a-prefix", + output: "--webkit-not-a-prefix", + ast: "word \"--webkit-not-a-prefix\" 0..21", + }, + ParserCase { + input: "-webkit-image-set(url(a.png) 1x, url(b.png) 2x)", + output: "-webkit-image-set(url(a.png) 1x, url(b.png) 2x)", + ast: "function \"-webkit-image-set\" 0..47 before=\"\" after=\"\" nodes=7\n function \"url\" 18..28 before=\"\" after=\"\" nodes=1\n word \"a.png\" 22..27\n space \" \" 28..29\n word \"1x\" 29..31\n div \",\" 31..33 before=\"\" after=\" \"\n function \"url\" 33..43 before=\"\" after=\"\" nodes=1\n word \"b.png\" 37..42\n space \" \" 43..44\n word \"2x\" 44..46", + }, + ParserCase { + input: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#FF000000)", + output: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#FF000000)", + ast: "word \"progid\" 0..6\ndiv \":\" 6..7 before=\"\" after=\"\"\nfunction \"DXImageTransform.Microsoft.gradient\" 7..67 before=\"\" after=\"\" nodes=1\n word \"startColorstr=#FF000000\" 43..66", + }, + ParserCase { + input: "1px !important", + output: "1px !important", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"!important\" 4..14", + }, + ParserCase { + input: "1px!important", + output: "1px!important", + ast: "word \"1px!important\" 0..13", + }, + ParserCase { + input: "1px !important", + output: "1px !important", + ast: "word \"1px\" 0..3\nspace \" \" 3..5\nword \"!important\" 5..15", + }, + ParserCase { + input: "1px ! important", + output: "1px ! important", + ast: "word \"1px\" 0..3\nspace \" \" 3..4\nword \"!\" 4..5\nspace \" \" 5..6\nword \"important\" 6..15", + }, + ParserCase { + input: "!important", + output: "!important", + ast: "word \"!important\" 0..10", + }, + ParserCase { + input: "red !IMPORTANT", + output: "red !IMPORTANT", + ast: "word \"red\" 0..3\nspace \" \" 3..4\nword \"!IMPORTANT\" 4..14", + }, + ParserCase { + input: "+.5", + output: "+.5", + ast: "word \"+.5\" 0..3", + }, + ParserCase { + input: "1e3", + output: "1e3", + ast: "word \"1e3\" 0..3", + }, + ParserCase { + input: "1e", + output: "1e", + ast: "word \"1e\" 0..2", + }, + ParserCase { + input: "1e+", + output: "1e+", + ast: "word \"1e+\" 0..3", + }, + ParserCase { + input: "1E-3px", + output: "1E-3px", + ast: "word \"1E-3px\" 0..6", + }, + ParserCase { + input: "1.2.3", + output: "1.2.3", + ast: "word \"1.2.3\" 0..5", + }, + ParserCase { + input: "10PX", + output: "10PX", + ast: "word \"10PX\" 0..4", + }, + ParserCase { + input: "10 px", + output: "10 px", + ast: "word \"10\" 0..2\nspace \" \" 2..3\nword \"px\" 3..5", + }, + ParserCase { + input: "calc(100vw * 0.12)", + output: "calc(100vw * 0.12)", + ast: "function \"calc\" 0..18 before=\"\" after=\"\" nodes=5\n word \"100vw\" 5..10\n space \" \" 10..11\n word \"*\" 11..12\n space \" \" 12..13\n word \"0.12\" 13..17", + }, + ParserCase { + input: "calc(1px+2px)", + output: "calc(1px+2px)", + ast: "function \"calc\" 0..13 before=\"\" after=\"\" nodes=1\n word \"1px+2px\" 5..12", + }, + ParserCase { + input: "calc(1px + 2px)", + output: "calc(1px + 2px)", + ast: "function \"calc\" 0..15 before=\"\" after=\"\" nodes=5\n word \"1px\" 5..8\n space \" \" 8..9\n word \"+\" 9..10\n space \" \" 10..11\n word \"2px\" 11..14", + }, + ParserCase { + input: "calc(1px/2)", + output: "calc(1px/2)", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" nodes=3\n word \"1px\" 5..8\n word \"/\" 8..9\n word \"2\" 9..10", + }, + ParserCase { + input: "calc(1px / 2)", + output: "calc(1px / 2)", + ast: "function \"calc\" 0..13 before=\"\" after=\"\" nodes=5\n word \"1px\" 5..8\n space \" \" 8..9\n word \"/\" 9..10\n space \" \" 10..11\n word \"2\" 11..12", + }, + ParserCase { + input: "calc(1px*2)", + output: "calc(1px*2)", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" nodes=3\n word \"1px\" 5..8\n word \"*\" 8..9\n word \"2\" 9..10", + }, + ParserCase { + input: "1px/2", + output: "1px/2", + ast: "word \"1px\" 0..3\ndiv \"/\" 3..4 before=\"\" after=\"\"\nword \"2\" 4..5", + }, + ParserCase { + input: "1px / 2", + output: "1px / 2", + ast: "word \"1px\" 0..3\ndiv \"/\" 3..6 before=\" \" after=\" \"\nword \"2\" 6..7", + }, + ParserCase { + input: "min(1px,2px)", + output: "min(1px,2px)", + ast: "function \"min\" 0..12 before=\"\" after=\"\" nodes=3\n word \"1px\" 4..7\n div \",\" 7..8 before=\"\" after=\"\"\n word \"2px\" 8..11", + }, + ParserCase { + input: "clamp(1px, 2vw, 3px)", + output: "clamp(1px, 2vw, 3px)", + ast: "function \"clamp\" 0..20 before=\"\" after=\"\" nodes=5\n word \"1px\" 6..9\n div \",\" 9..11 before=\"\" after=\" \"\n word \"2vw\" 11..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"3px\" 16..19", + }, + ParserCase { + input: "calc(calc(1px * 2) / 3)", + output: "calc(calc(1px * 2) / 3)", + ast: "function \"calc\" 0..23 before=\"\" after=\"\" nodes=5\n function \"calc\" 5..18 before=\"\" after=\"\" nodes=5\n word \"1px\" 10..13\n space \" \" 13..14\n word \"*\" 14..15\n space \" \" 15..16\n word \"2\" 16..17\n space \" \" 18..19\n word \"/\" 19..20\n space \" \" 20..21\n word \"3\" 21..22", + }, + ParserCase { + input: "a:b", + output: "a:b", + ast: "word \"a\" 0..1\ndiv \":\" 1..2 before=\"\" after=\"\"\nword \"b\" 2..3", + }, + ParserCase { + input: "a: b", + output: "a: b", + ast: "word \"a\" 0..1\ndiv \":\" 1..3 before=\"\" after=\" \"\nword \"b\" 3..4", + }, + ParserCase { + input: "a :b", + output: "a :b", + ast: "word \"a\" 0..1\ndiv \":\" 1..3 before=\" \" after=\"\"\nword \"b\" 3..4", + }, + ParserCase { + input: "url(https://example.com/a.png)", + output: "url(https://example.com/a.png)", + ast: "function \"url\" 0..30 before=\"\" after=\"\" nodes=1\n word \"https://example.com/a.png\" 4..29", + }, + ParserCase { + input: "url(\"https://example.com/a,b.png\")", + output: "url(\"https://example.com/a,b.png\")", + ast: "function \"url\" 0..34 before=\"\" after=\"\" nodes=1\n string \"https://example.com/a,b.png\" 4..33 quote=\"\\\"\"", + }, + ParserCase { + input: "url(data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=)", + output: "url(data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=)", + ast: "function \"url\" 0..47 before=\"\" after=\"\" nodes=1\n word \"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=\" 4..46", + }, + ParserCase { + input: "image-set(\"a.png\" 1x, \"b.png\" 2x)", + output: "image-set(\"a.png\" 1x, \"b.png\" 2x)", + ast: "function \"image-set\" 0..33 before=\"\" after=\"\" nodes=7\n string \"a.png\" 10..17 quote=\"\\\"\"\n space \" \" 17..18\n word \"1x\" 18..20\n div \",\" 20..22 before=\"\" after=\" \"\n string \"b.png\" 22..29 quote=\"\\\"\"\n space \" \" 29..30\n word \"2x\" 30..32", + }, + ParserCase { + input: "\\(", + output: "\\(", + ast: "word \"\\\\(\" 0..2", + }, + ParserCase { + input: "\\)", + output: "\\)", + ast: "word \"\\\\)\" 0..2", + }, + ParserCase { + input: "\\(\\)", + output: "\\(\\)", + ast: "word \"\\\\(\\\\)\" 0..4", + }, + ParserCase { + input: "\\( \\)", + output: "\\( \\)", + ast: "word \"\\\\(\" 0..2\nspace \" \" 2..3\nword \"\\\\)\" 3..5", + }, + ParserCase { + input: "() )wo)rd)", + output: "() )wo)rd)", + ast: "function \"\" 0..2 before=\"\" after=\"\" nodes=0\nspace \" \" 2..3\nword \")wo)rd)\" 3..10", + }, + ParserCase { + input: "( )", + output: "( )", + ast: "function \"\" 0..3 before=\" \" after=\"\" nodes=0", + }, + ParserCase { + input: "( | )", + output: "( | )", + ast: "function \"\" 0..5 before=\" \" after=\" \" nodes=1\n word \"|\" 2..3", + }, + ParserCase { + input: "name()", + output: "name()", + ast: "function \"name\" 0..6 before=\"\" after=\"\" nodes=0", + }, + ParserCase { + input: "((()))", + output: "((()))", + ast: "function \"\" 0..6 before=\"\" after=\"\" nodes=1\n function \"\" 1..5 before=\"\" after=\"\" nodes=1\n function \"\" 2..4 before=\"\" after=\"\" nodes=0", + }, + ParserCase { + input: "( calc(( ) ))word", + output: "( calc(( ) ))word", + ast: "function \"\" 0..13 before=\" \" after=\"\" nodes=1\n function \"calc\" 2..12 before=\"\" after=\" \" nodes=1\n function \"\" 7..10 before=\" \" after=\"\" nodes=0\nword \"word\" 13..17", + }, + ParserCase { + input: "( , )", + output: "( , )", + ast: "function \"\" 0..5 before=\" \" after=\" \" nodes=1\n div \",\" 2..3 before=\"\" after=\"\"", + }, + ParserCase { + input: " , : ", + output: " , : ", + ast: "div \",\" 0..3 before=\" \" after=\" \"\ndiv \":\" 3..5 before=\"\" after=\" \"", + }, + ParserCase { + input: "'word\\'word'", + output: "'word\\'word'", + ast: "string \"word\\\\'word\" 0..12 quote=\"'\"", + }, + ParserCase { + input: "\"word\\\"word\"", + output: "\"word\\\"word\"", + ast: "string \"word\\\\\\\"word\" 0..12 quote=\"\\\"\"", + }, + ParserCase { + input: "\"word'word\"", + output: "\"word'word\"", + ast: "string \"word'word\" 0..11 quote=\"\\\"\"", + }, + ParserCase { + input: "'word\"word'", + output: "'word\"word'", + ast: "string \"word\\\"word\" 0..11 quote=\"'\"", + }, + ParserCase { + input: "\"word", + output: "\"word", + ast: "string \"word\" 0..5 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "\"word\\", + output: "\"word\\", + ast: "string \"word\\\\\" 0..6 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: "\"string\"", + output: "\"string\"", + ast: "string \"string\" 0..8 quote=\"\\\"\"", + }, + ParserCase { + input: "word1\"string\"word2", + output: "word1\"string\"word2", + ast: "word \"word1\" 0..5\nstring \"string\" 5..13 quote=\"\\\"\"\nword \"word2\" 13..18", + }, + ParserCase { + input: " \"string\" ", + output: " \"string\" ", + ast: "space \" \" 0..1\nstring \"string\" 1..9 quote=\"\\\"\"\nspace \" \" 9..10", + }, + ParserCase { + input: " \\\"word\\'\\ \\\t ", + output: " \\\"word\\'\\ \\\t ", + ast: "space \" \" 0..1\nword \"\\\\\\\"word\\\\'\\\\ \\\\\\t\" 1..13\nspace \" \" 13..14", + }, + ParserCase { + input: "bold italic 12px \t /3 'Open Sans', Arial, \"Helvetica Neue\", sans-serif", + output: "bold italic 12px \t /3 'Open Sans', Arial, \"Helvetica Neue\", sans-serif", + ast: "word \"bold\" 0..4\nspace \" \" 4..5\nword \"italic\" 5..11\nspace \" \" 11..12\nword \"12px\" 12..16\ndiv \"/\" 16..20 before=\" \\t \" after=\"\"\nword \"3\" 20..21\nspace \" \" 21..22\nstring \"Open Sans\" 22..33 quote=\"'\"\ndiv \",\" 33..35 before=\"\" after=\" \"\nword \"Arial\" 35..40\ndiv \",\" 40..42 before=\"\" after=\" \"\nstring \"Helvetica Neue\" 42..58 quote=\"\\\"\"\ndiv \",\" 58..60 before=\"\" after=\" \"\nword \"sans-serif\" 60..70", + }, + ParserCase { + input: "rgba( 29, 439 , 29 )", + output: "rgba( 29, 439 , 29 )", + ast: "function \"rgba\" 0..20 before=\" \" after=\" \" nodes=5\n word \"29\" 6..8\n div \",\" 8..10 before=\"\" after=\" \"\n word \"439\" 10..13\n div \",\" 13..16 before=\" \" after=\" \"\n word \"29\" 16..18", + }, + ParserCase { + input: "url( /gfx/img/bg.jpg )", + output: "url( /gfx/img/bg.jpg )", + ast: "function \"url\" 0..22 before=\" \" after=\" \" nodes=1\n word \"/gfx/img/bg.jpg\" 5..20", + }, + ParserCase { + input: "url( /gfx/img/bg.jpg ", + output: "url( /gfx/img/bg.jpg ", + ast: "function \"url\" 0..21 before=\" \" after=\"\" unclosed nodes=2\n word \"/gfx/img/bg.jpg\" 5..20\n space \" \" 20..21", + }, + ParserCase { + input: "url( \"/gfx/img/bg.jpg\" hello )", + output: "url( \"/gfx/img/bg.jpg\" hello )", + ast: "function \"url\" 0..30 before=\" \" after=\" \" nodes=3\n string \"/gfx/img/bg.jpg\" 5..22 quote=\"\\\"\"\n space \" \" 22..23\n word \"hello\" 23..28", + }, + ParserCase { + input: "calc(1 + 2)", + output: "calc(1 + 2)", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" nodes=5\n word \"1\" 5..6\n space \" \" 6..7\n word \"+\" 7..8\n space \" \" 8..9\n word \"2\" 9..10", + }, + ParserCase { + input: "calc(1 - 2)", + output: "calc(1 - 2)", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" nodes=5\n word \"1\" 5..6\n space \" \" 6..7\n word \"-\" 7..8\n space \" \" 8..9\n word \"2\" 9..10", + }, + ParserCase { + input: "calc(1 * 2)", + output: "calc(1 * 2)", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" nodes=5\n word \"1\" 5..6\n space \" \" 6..7\n word \"*\" 7..8\n space \" \" 8..9\n word \"2\" 9..10", + }, + ParserCase { + input: "calc(1 / 2)", + output: "calc(1 / 2)", + ast: "function \"calc\" 0..11 before=\"\" after=\"\" nodes=5\n word \"1\" 5..6\n space \" \" 6..7\n word \"/\" 7..8\n space \" \" 8..9\n word \"2\" 9..10", + }, + ParserCase { + input: "calc(1*2)", + output: "calc(1*2)", + ast: "function \"calc\" 0..9 before=\"\" after=\"\" nodes=3\n word \"1\" 5..6\n word \"*\" 6..7\n word \"2\" 7..8", + }, + ParserCase { + input: "calc(1/2)", + output: "calc(1/2)", + ast: "function \"calc\" 0..9 before=\"\" after=\"\" nodes=3\n word \"1\" 5..6\n word \"/\" 6..7\n word \"2\" 7..8", + }, + ParserCase { + input: "calc(((768px - 100vw) / 2) - 15px)", + output: "calc(((768px - 100vw) / 2) - 15px)", + ast: "function \"calc\" 0..34 before=\"\" after=\"\" nodes=5\n function \"\" 5..26 before=\"\" after=\"\" nodes=3\n function \"\" 6..21 before=\"\" after=\"\" nodes=5\n word \"768px\" 7..12\n space \" \" 12..13\n word \"-\" 13..14\n space \" \" 14..15\n word \"100vw\" 15..20\n div \"/\" 21..24 before=\" \" after=\" \"\n word \"2\" 24..25\n space \" \" 26..27\n word \"-\" 27..28\n space \" \" 28..29\n word \"15px\" 29..33", + }, + ParserCase { + input: "(min-width: 700px) and (orientation: \\$landscape)", + output: "(min-width: 700px) and (orientation: \\$landscape)", + ast: "function \"\" 0..18 before=\"\" after=\"\" nodes=3\n word \"min-width\" 1..10\n div \":\" 10..12 before=\"\" after=\" \"\n word \"700px\" 12..17\nspace \" \" 18..19\nword \"and\" 19..22\nspace \" \" 22..23\nfunction \"\" 23..49 before=\"\" after=\"\" nodes=3\n word \"orientation\" 24..35\n div \":\" 35..37 before=\"\" after=\" \"\n word \"\\\\$landscape\" 37..48", + }, + ParserCase { + input: "url( http://website.com/assets\\)_test )", + output: "url( http://website.com/assets\\)_test )", + ast: "function \"url\" 0..39 before=\" \" after=\" \" nodes=1\n word \"http://website.com/assets\\\\)_test\" 5..37", + }, + ParserCase { + input: "fn1(fn2(255), fn3(.2)), fn4(fn5(255,.2), fn6)", + output: "fn1(fn2(255), fn3(.2)), fn4(fn5(255,.2), fn6)", + ast: "function \"fn1\" 0..22 before=\"\" after=\"\" nodes=3\n function \"fn2\" 4..12 before=\"\" after=\"\" nodes=1\n word \"255\" 8..11\n div \",\" 12..14 before=\"\" after=\" \"\n function \"fn3\" 14..21 before=\"\" after=\"\" nodes=1\n word \".2\" 18..20\ndiv \",\" 22..24 before=\"\" after=\" \"\nfunction \"fn4\" 24..45 before=\"\" after=\"\" nodes=3\n function \"fn5\" 28..39 before=\"\" after=\"\" nodes=3\n word \"255\" 32..35\n div \",\" 35..36 before=\"\" after=\"\"\n word \".2\" 36..38\n div \",\" 39..41 before=\"\" after=\" \"\n word \"fn6\" 41..44", + }, + ParserCase { + input: "(0 32 word ", + output: "(0 32 word ", + ast: "function \"\" 0..11 before=\"\" after=\"\" unclosed nodes=6\n word \"0\" 1..2\n space \" \" 2..3\n word \"32\" 3..5\n space \" \" 5..6\n word \"word\" 6..10\n space \" \" 10..11", + }, + ParserCase { + input: "( ( ( ) ", + output: "( ( ( ) ", + ast: "function \"\" 0..8 before=\" \" after=\"\" unclosed nodes=1\n function \"\" 2..8 before=\" \" after=\"\" unclosed nodes=2\n function \"\" 4..7 before=\" \" after=\"\" nodes=0\n space \" \" 7..8", + }, + ParserCase { + input: "0 32 word ) ", + output: "0 32 word ) ", + ast: "word \"0\" 0..1\nspace \" \" 1..2\nword \"32\" 2..4\nspace \" \" 4..5\nword \"word\" 5..9\nspace \" \" 9..10\nword \")\" 10..11\nspace \" \" 11..12", + }, + ParserCase { + input: "Bond\\ 007", + output: "Bond\\ 007", + ast: "word \"Bond\\\\ 007\" 0..9", + }, + ParserCase { + input: "url(foo/bar.jpg), url(http://website.com/img.jpg)", + output: "url(foo/bar.jpg), url(http://website.com/img.jpg)", + ast: "function \"url\" 0..16 before=\"\" after=\"\" nodes=1\n word \"foo/bar.jpg\" 4..15\ndiv \",\" 16..18 before=\"\" after=\" \"\nfunction \"url\" 18..49 before=\"\" after=\"\" nodes=1\n word \"http://website.com/img.jpg\" 22..48", + }, + ParserCase { + input: "url()", + output: "url()", + ast: "function \"url\" 0..5 before=\"\" after=\"\" nodes=0", + }, + ParserCase { + input: "url( )", + output: "url( )", + ast: "function \"url\" 0..6 before=\" \" after=\"\" nodes=0", + }, + ParserCase { + input: "url( )", + output: "url( )", + ast: "function \"url\" 0..8 before=\" \" after=\"\" nodes=0", + }, + ParserCase { + input: "url(\n)", + output: "url(\n)", + ast: "function \"url\" 0..6 before=\"\\n\" after=\"\" nodes=0", + }, + ParserCase { + input: "url(\r\n)", + output: "url(\r\n)", + ast: "function \"url\" 0..7 before=\"\\r\\n\" after=\"\" nodes=0", + }, + ParserCase { + input: "url(\n\n\n)", + output: "url(\n\n\n)", + ast: "function \"url\" 0..8 before=\"\\n\\n\\n\" after=\"\" nodes=0", + }, + ParserCase { + input: "url(\r\n\r\n\r\n)", + output: "url(\r\n\r\n\r\n)", + ast: "function \"url\" 0..11 before=\"\\r\\n\\r\\n\\r\\n\" after=\"\" nodes=0", + }, + ParserCase { + input: "url( \n \t \r\n )", + output: "url( \n \t \r\n )", + ast: "function \"url\" 0..16 before=\" \\n \\t \\r\\n \" after=\"\" nodes=0", + }, + ParserCase { + input: "/*before*/ 1px /*between*/ 1px /*after*/", + output: "/*before*/ 1px /*between*/ 1px /*after*/", + ast: "comment \"before\" 0..10\nspace \" \" 10..11\nword \"1px\" 11..14\nspace \" \" 14..15\ncomment \"between\" 15..26\nspace \" \" 26..27\nword \"1px\" 27..30\nspace \" \" 30..31\ncomment \"after\" 31..40", + }, + ParserCase { + input: "rgba( 0, 55/55, 0/*,.5*/ )", + output: "rgba( 0, 55/55, 0/*,.5*/ )", + ast: "function \"rgba\" 0..26 before=\" \" after=\" \" nodes=8\n word \"0\" 6..7\n div \",\" 7..9 before=\"\" after=\" \"\n word \"55\" 9..11\n div \"/\" 11..12 before=\"\" after=\"\"\n word \"55\" 12..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"0\" 16..17\n comment \",.5\" 17..24", + }, + ParserCase { + input: "url( \"/demo/bg.png\" /*comment*/ )", + output: "url( \"/demo/bg.png\" /*comment*/ )", + ast: "function \"url\" 0..33 before=\" \" after=\" \" nodes=3\n string \"/demo/bg.png\" 5..19 quote=\"\\\"\"\n space \" \" 19..20\n comment \"comment\" 20..31", + }, + ParserCase { + input: "url( /*comment*/ /demo/bg.png )", + output: "url( /*comment*/ /demo/bg.png )", + ast: "function \"url\" 0..31 before=\" \" after=\" \" nodes=1\n word \"/*comment*/ /demo/bg.png\" 5..29", + }, + ParserCase { + input: "url( /demo/bg.png /*comment*/ )", + output: "url( /demo/bg.png /*comment*/ )", + ast: "function \"url\" 0..31 before=\" \" after=\" \" nodes=1\n word \"/demo/bg.png /*comment*/\" 5..29", + }, + ParserCase { + input: "/*comment*/ 1px /* unclosed ", + output: "/*comment*/ 1px /* unclosed ", + ast: "comment \"comment\" 0..11\nspace \" \" 11..12\nword \"1px\" 12..15\nspace \" \" 15..16\ncomment \" unclosed \" 16..28 unclosed", + }, + ParserCase { + input: "Hawaii \\35 -0", + output: "Hawaii \\35 -0", + ast: "word \"Hawaii\" 0..6\nspace \" \" 6..7\nword \"\\\\35\" 7..10\nspace \" \" 10..11\nword \"-0\" 11..13", + }, + ParserCase { + input: "U+0-7f", + output: "U+0-7f", + ast: "unicode-range \"U+0-7f\" 0..6", + }, + ParserCase { + input: "U+0025-00FF", + output: "U+0025-00FF", + ast: "unicode-range \"U+0025-00FF\" 0..11", + }, + ParserCase { + input: "U+0025-00FF, U+4??", + output: "U+0025-00FF, U+4??", + ast: "unicode-range \"U+0025-00FF\" 0..11\ndiv \",\" 11..13 before=\"\" after=\" \"\nunicode-range \"U+4??\" 13..18", + }, + ParserCase { + input: "U+4??Z", + output: "U+4??Z", + ast: "word \"U+4??Z\" 0..6", + }, + ParserCase { + input: "U+Z", + output: "U+Z", + ast: "word \"U+Z\" 0..3", + }, + ParserCase { + input: "bold italic 12px/3 'Open Sans', Arial, \"Helvetica Neue\", sans-serif", + output: "bold italic 12px/3 'Open Sans', Arial, \"Helvetica Neue\", sans-serif", + ast: "word \"bold\" 0..4\nspace \" \" 4..5\nword \"italic\" 5..11\nspace \" \" 11..12\nword \"12px\" 12..16\ndiv \"/\" 16..17 before=\"\" after=\"\"\nword \"3\" 17..18\nspace \" \" 18..19\nstring \"Open Sans\" 19..30 quote=\"'\"\ndiv \",\" 30..32 before=\"\" after=\" \"\nword \"Arial\" 32..37\ndiv \",\" 37..39 before=\"\" after=\" \"\nstring \"Helvetica Neue\" 39..55 quote=\"\\\"\"\ndiv \",\" 55..57 before=\"\" after=\" \"\nword \"sans-serif\" 57..67", + }, + ParserCase { + input: "\" 12, 54, 65 ", + output: "\" 12, 54, 65 ", + ast: "string \" 12, 54, 65 \" 0..14 quote=\"\\\"\" unclosed", + }, + ParserCase { + input: " rgba( 12, 54, 65) ", + output: " rgba( 12, 54, 65) ", + ast: "space \" \" 0..1\nfunction \"rgba\" 1..19 before=\" \" after=\"\" nodes=5\n word \"12\" 7..9\n div \",\" 9..12 before=\"\" after=\" \"\n word \"54\" 12..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"65\" 16..18\nspace \" \" 19..20", + }, + ParserCase { + input: " rgba( 12, 54, 65 ", + output: " rgba( 12, 54, 65 ", + ast: "space \" \" 0..1\nfunction \"rgba\" 1..19 before=\" \" after=\"\" unclosed nodes=6\n word \"12\" 7..9\n div \",\" 9..12 before=\"\" after=\" \"\n word \"54\" 12..14\n div \",\" 14..16 before=\"\" after=\" \"\n word \"65\" 16..18\n space \" \" 18..19", + }, + ParserCase { + input: "background-image:linear-gradient(45deg,transparent 25%,hsla(0,0%,100%,.2) 25%,hsla(0,0%,100%,.2) 75%,transparent 75%,transparent 25%,hsla(0,0%,100%,.2) 75%,transparent 75%,transparent),linear-gradient(45deg,transparent 25%,hsla(0,0%,100%,.2))", + output: "background-image:linear-gradient(45deg,transparent 25%,hsla(0,0%,100%,.2) 25%,hsla(0,0%,100%,.2) 75%,transparent 75%,transparent 25%,hsla(0,0%,100%,.2) 75%,transparent 75%,transparent),linear-gradient(45deg,transparent 25%,hsla(0,0%,100%,.2))", + ast: "word \"background-image\" 0..16\ndiv \":\" 16..17 before=\"\" after=\"\"\nfunction \"linear-gradient\" 17..184 before=\"\" after=\"\" nodes=31\n word \"45deg\" 33..38\n div \",\" 38..39 before=\"\" after=\"\"\n word \"transparent\" 39..50\n space \" \" 50..51\n word \"25%\" 51..54\n div \",\" 54..55 before=\"\" after=\"\"\n function \"hsla\" 55..73 before=\"\" after=\"\" nodes=7\n word \"0\" 60..61\n div \",\" 61..62 before=\"\" after=\"\"\n word \"0%\" 62..64\n div \",\" 64..65 before=\"\" after=\"\"\n word \"100%\" 65..69\n div \",\" 69..70 before=\"\" after=\"\"\n word \".2\" 70..72\n space \" \" 73..74\n word \"25%\" 74..77\n div \",\" 77..78 before=\"\" after=\"\"\n function \"hsla\" 78..96 before=\"\" after=\"\" nodes=7\n word \"0\" 83..84\n div \",\" 84..85 before=\"\" after=\"\"\n word \"0%\" 85..87\n div \",\" 87..88 before=\"\" after=\"\"\n word \"100%\" 88..92\n div \",\" 92..93 before=\"\" after=\"\"\n word \".2\" 93..95\n space \" \" 96..97\n word \"75%\" 97..100\n div \",\" 100..101 before=\"\" after=\"\"\n word \"transparent\" 101..112\n space \" \" 112..113\n word \"75%\" 113..116\n div \",\" 116..117 before=\"\" after=\"\"\n word \"transparent\" 117..128\n space \" \" 128..129\n word \"25%\" 129..132\n div \",\" 132..133 before=\"\" after=\"\"\n function \"hsla\" 133..151 before=\"\" after=\"\" nodes=7\n word \"0\" 138..139\n div \",\" 139..140 before=\"\" after=\"\"\n word \"0%\" 140..142\n div \",\" 142..143 before=\"\" after=\"\"\n word \"100%\" 143..147\n div \",\" 147..148 before=\"\" after=\"\"\n word \".2\" 148..150\n space \" \" 151..152\n word \"75%\" 152..155\n div \",\" 155..156 before=\"\" after=\"\"\n word \"transparent\" 156..167\n space \" \" 167..168\n word \"75%\" 168..171\n div \",\" 171..172 before=\"\" after=\"\"\n word \"transparent\" 172..183\ndiv \",\" 184..185 before=\"\" after=\"\"\nfunction \"linear-gradient\" 185..242 before=\"\" after=\"\" nodes=7\n word \"45deg\" 201..206\n div \",\" 206..207 before=\"\" after=\"\"\n word \"transparent\" 207..218\n space \" \" 218..219\n word \"25%\" 219..222\n div \",\" 222..223 before=\"\" after=\"\"\n function \"hsla\" 223..241 before=\"\" after=\"\" nodes=7\n word \"0\" 228..229\n div \",\" 229..230 before=\"\" after=\"\"\n word \"0%\" 230..232\n div \",\" 232..233 before=\"\" after=\"\"\n word \"100%\" 233..237\n div \",\" 237..238 before=\"\" after=\"\"\n word \".2\" 238..240", + }, + ParserCase { + input: "/!*comment*!/ 1px /!* unclosed ", + output: "/!*comment*!/ 1px /!* unclosed ", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\nword \"!*comment*!\" 1..12\ndiv \"/\" 12..14 before=\"\" after=\" \"\nword \"1px\" 14..17\ndiv \"/\" 17..19 before=\" \" after=\"\"\nword \"!*\" 19..21\nspace \" \" 21..22\nword \"unclosed\" 22..30\nspace \" \" 30..31", + }, + ParserCase { + input: "/!*before*!/ rgb( /!*red component*!/ 12, 54 /!*green component*!/, /!* blue *!/ 65)/!* after *!/ ", + output: "/!*before*!/ rgb( /!*red component*!/ 12, 54 /!*green component*!/, /!* blue *!/ 65)/!* after *!/ ", + ast: "div \"/\" 0..1 before=\"\" after=\"\"\nword \"!*before*!\" 1..11\ndiv \"/\" 11..13 before=\"\" after=\" \"\nfunction \"rgb\" 13..85 before=\" \" after=\"\" nodes=22\n div \"/\" 18..19 before=\"\" after=\"\"\n word \"!*red\" 19..24\n space \" \" 24..25\n word \"component*!\" 25..36\n div \"/\" 36..38 before=\"\" after=\" \"\n word \"12\" 38..40\n div \",\" 40..43 before=\"\" after=\" \"\n word \"54\" 43..45\n div \"/\" 45..47 before=\" \" after=\"\"\n word \"!*green\" 47..54\n space \" \" 54..55\n word \"component*!\" 55..66\n div \"/\" 66..67 before=\"\" after=\"\"\n div \",\" 67..69 before=\"\" after=\" \"\n div \"/\" 69..70 before=\"\" after=\"\"\n word \"!*\" 70..72\n space \" \" 72..73\n word \"blue\" 73..77\n space \" \" 77..78\n word \"*!\" 78..80\n div \"/\" 80..82 before=\"\" after=\" \"\n word \"65\" 82..84\ndiv \"/\" 85..86 before=\"\" after=\"\"\nword \"!*\" 86..88\nspace \" \" 88..89\nword \"after\" 89..94\nspace \" \" 94..95\nword \"*!\" 95..97\ndiv \"/\" 97..99 before=\"\" after=\" \"", + }, + ParserCase { + input: "url(\t)", + output: "url(\t)", + ast: "function \"url\" 0..6 before=\"\\t\" after=\"\" nodes=0", + }, + ParserCase { + input: "url( \n \t \n )", + output: "url( \n \t \n )", + ast: "function \"url\" 0..15 before=\" \\n \\t \\n \" after=\"\" nodes=0", + }, + ParserCase { + input: ".23rem", + output: ".23rem", + ast: "word \".23rem\" 0..6", + }, + ParserCase { + input: ".2.3rem", + output: ".2.3rem", + ast: "word \".2.3rem\" 0..7", + }, + ParserCase { + input: "2.", + output: "2.", + ast: "word \"2.\" 0..2", + }, + ParserCase { + input: "+2.", + output: "+2.", + ast: "word \"+2.\" 0..3", + }, + ParserCase { + input: "-2.", + output: "-2.", + ast: "word \"-2.\" 0..3", + }, + ParserCase { + input: "+-2.", + output: "+-2.", + ast: "word \"+-2.\" 0..4", + }, + ParserCase { + input: ".rem", + output: ".rem", + ast: "word \".rem\" 0..4", + }, + ParserCase { + input: "1e4px", + output: "1e4px", + ast: "word \"1e4px\" 0..5", + }, + ParserCase { + input: "1e10", + output: "1e10", + ast: "word \"1e10\" 0..4", + }, + ParserCase { + input: "e", + output: "e", + ast: "word \"e\" 0..1", + }, + ParserCase { + input: "e1", + output: "e1", + ast: "word \"e1\" 0..2", + }, + ParserCase { + input: "2rem", + output: "2rem", + ast: "word \"2rem\" 0..4", + }, + ParserCase { + input: "2.000rem", + output: "2.000rem", + ast: "word \"2.000rem\" 0..8", + }, + ParserCase { + input: "+2rem", + output: "+2rem", + ast: "word \"+2rem\" 0..5", + }, + ParserCase { + input: "-2rem", + output: "-2rem", + ast: "word \"-2rem\" 0..5", + }, + ParserCase { + input: "1.1rem", + output: "1.1rem", + ast: "word \"1.1rem\" 0..6", + }, + ParserCase { + input: "+1.1rem", + output: "+1.1rem", + ast: "word \"+1.1rem\" 0..7", + }, + ParserCase { + input: "-1.1rem", + output: "-1.1rem", + ast: "word \"-1.1rem\" 0..7", + }, + ParserCase { + input: "1.1e1rem", + output: "1.1e1rem", + ast: "word \"1.1e1rem\" 0..8", + }, + ParserCase { + input: "+1.1e1rem", + output: "+1.1e1rem", + ast: "word \"+1.1e1rem\" 0..9", + }, + ParserCase { + input: "-1.1e1rem", + output: "-1.1e1rem", + ast: "word \"-1.1e1rem\" 0..9", + }, + ParserCase { + input: "1.1e+1rem", + output: "1.1e+1rem", + ast: "word \"1.1e+1rem\" 0..9", + }, + ParserCase { + input: "1.1e-1rem", + output: "1.1e-1rem", + ast: "word \"1.1e-1rem\" 0..9", + }, + ParserCase { + input: "1.1e1e1rem", + output: "1.1e1e1rem", + ast: "word \"1.1e1e1rem\" 0..10", + }, + ParserCase { + input: "1.1e-1e", + output: "1.1e-1e", + ast: "word \"1.1e-1e\" 0..7", + }, + ParserCase { + input: "1.1e--++1e", + output: "1.1e--++1e", + ast: "word \"1.1e--++1e\" 0..10", + }, + ParserCase { + input: "1.1e--++1rem", + output: "1.1e--++1rem", + ast: "word \"1.1e--++1rem\" 0..12", + }, + ParserCase { + input: "100+px", + output: "100+px", + ast: "word \"100+px\" 0..6", + }, + ParserCase { + input: "100.0.0px", + output: "100.0.0px", + ast: "word \"100.0.0px\" 0..9", + }, + ParserCase { + input: "100e1epx", + output: "100e1epx", + ast: "word \"100e1epx\" 0..8", + }, + ParserCase { + input: "100e1e1px", + output: "100e1e1px", + ast: "word \"100e1e1px\" 0..9", + }, + ParserCase { + input: "+100.1e+1e+1px", + output: "+100.1e+1e+1px", + ast: "word \"+100.1e+1e+1px\" 0..14", + }, + ParserCase { + input: "-100.1e-1e-1px", + output: "-100.1e-1e-1px", + ast: "word \"-100.1e-1e-1px\" 0..14", + }, + ParserCase { + input: "+.5px", + output: "+.5px", + ast: "word \"+.5px\" 0..5", + }, + ParserCase { + input: "-.5px", + output: "-.5px", + ast: "word \"-.5px\" 0..5", + }, + ParserCase { + input: ".5e1px", + output: ".5e1px", + ast: "word \".5e1px\" 0..6", + }, + ParserCase { + input: "-.5e1px", + output: "-.5e1px", + ast: "word \"-.5e1px\" 0..7", + }, + ParserCase { + input: "+.5e1px", + output: "+.5e1px", + ast: "word \"+.5e1px\" 0..7", + }, + ParserCase { + input: ".5e1e1px", + output: ".5e1e1px", + ast: "word \".5e1e1px\" 0..8", + }, + ParserCase { + input: ".5.5px", + output: ".5.5px", + ast: "word \".5.5px\" 0..6", + }, + ParserCase { + input: "1e1", + output: "1e1", + ast: "word \"1e1\" 0..3", + }, + ParserCase { + input: "1ee", + output: "1ee", + ast: "word \"1ee\" 0..3", + }, + ParserCase { + input: "1e-", + output: "1e-", + ast: "word \"1e-\" 0..3", + }, + ParserCase { + input: "1e+1", + output: "1e+1", + ast: "word \"1e+1\" 0..4", + }, + ParserCase { + input: "1e++1", + output: "1e++1", + ast: "word \"1e++1\" 0..5", + }, + ParserCase { + input: "1e--1", + output: "1e--1", + ast: "word \"1e--1\" 0..5", + }, + ParserCase { + input: "+10", + output: "+10", + ast: "word \"+10\" 0..3", + }, + ParserCase { + input: "-10", + output: "-10", + ast: "word \"-10\" 0..3", + }, + ParserCase { + input: ".2px", + output: ".2px", + ast: "word \".2px\" 0..4", + }, + ParserCase { + input: "-.2px", + output: "-.2px", + ast: "word \"-.2px\" 0..5", + }, + ParserCase { + input: "+.2px", + output: "+.2px", + ast: "word \"+.2px\" 0..5", + }, + ParserCase { + input: ".a", + output: ".a", + ast: "word \".a\" 0..2", + }, + ParserCase { + input: "+", + output: "+", + ast: "word \"+\" 0..1", + }, + ParserCase { + input: "-", + output: "-", + ast: "word \"-\" 0..1", + }, + ParserCase { + input: "-a", + output: "-a", + ast: "word \"-a\" 0..2", + }, + ParserCase { + input: "+a", + output: "+a", + ast: "word \"+a\" 0..2", + }, + ParserCase { + input: "+.a", + output: "+.a", + ast: "word \"+.a\" 0..3", + }, + ParserCase { + input: "-.a", + output: "-.a", + ast: "word \"-.a\" 0..3", + }, + ParserCase { + input: " rgba( 34 , 45 , 54, .5 ) ", + output: " rgba( 34 , 45 , 54, .5 ) ", + ast: "space \" \" 0..1\nfunction \"rgba\" 1..25 before=\" \" after=\" \" nodes=7\n word \"34\" 7..9\n div \",\" 9..12 before=\" \" after=\" \"\n word \"45\" 12..14\n div \",\" 14..17 before=\" \" after=\" \"\n word \"54\" 17..19\n div \",\" 19..21 before=\"\" after=\" \"\n word \".5\" 21..23\nspace \" \" 25..26", + }, + ParserCase { + input: "w1 w2 w6 \n f(4) ( ) () \t \"s't\" 'st\\\"2'", + output: "w1 w2 w6 \n f(4) ( ) () \t \"s't\" 'st\\\"2'", + ast: "word \"w1\" 0..2\nspace \" \" 2..3\nword \"w2\" 3..5\nspace \" \" 5..6\nword \"w6\" 6..8\nspace \" \\n \" 8..11\nfunction \"f\" 11..15 before=\"\" after=\"\" nodes=1\n word \"4\" 13..14\nspace \" \" 15..16\nfunction \"\" 16..19 before=\" \" after=\"\" nodes=0\nspace \" \" 19..20\nfunction \"\" 20..22 before=\"\" after=\"\" nodes=0\nspace \" \\t \" 22..25\nstring \"s't\" 25..30 quote=\"\\\"\"\nspace \" \" 30..31\nstring \"st\\\\\\\"2\" 31..38 quote=\"'\"", + }, + ParserCase { + input: "fn( ) fn2( fn3())", + output: "fn( ) fn2( fn3())", + ast: "function \"fn\" 0..5 before=\" \" after=\"\" nodes=0\nspace \" \" 5..6\nfunction \"fn2\" 6..17 before=\" \" after=\"\" nodes=1\n function \"fn3\" 11..16 before=\"\" after=\"\" nodes=0", + }, +]; + +/// One serialisation that does not spell the tree out plainly — either +/// because an override replaced a node, or because a node was re-kinded +/// after parsing. Paired with the Rust that reproduces it by label. +pub(super) struct OverrideCase { + /// Names the override the Rust side has to write to match. + pub label: &'static str, + /// The value parsed before the override runs. + pub input: &'static str, + /// What came out. + pub output: &'static str, +} + +/// 6 serialisations that an override or a re-kinded node changed. +pub(super) const OVERRIDE_CASES: &[OverrideCase] = &[ + OverrideCase { + label: "function-to-bracket-list", + input: " rgba(12, 54, 65 ) ", + output: " rgba[12,54,65] ", + }, + OverrideCase { + label: "function-to-bracket-list-one-node", + input: " rgba(12, 54, 65 ) ", + output: "rgba[12,54,65]", + }, + OverrideCase { + label: "replace-nested-function", + input: "calc(1px + var(--bar))", + output: "calc(1px + 10px)", + }, + OverrideCase { + label: "override-declines-every-node", + input: " rgba(12, 54, 65 ) ", + output: " rgba(12, 54, 65 ) ", + }, + OverrideCase { + label: "function-retyped-as-word", + input: " rgba(12, 54, 65 ) ", + output: " rgba ", + }, + OverrideCase { + label: "function-retyped-as-unicode-range", + input: " rgba(12, 54, 65 ) ", + output: " 12, 54, 65 ", + }, +]; diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/dump.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/dump.rs new file mode 100644 index 000000000..5e336e535 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/dump.rs @@ -0,0 +1,69 @@ +//! A node tree flattened to text for single-assertion comparison. +//! +//! Must match `scripts/generate-value-parser-cases.mjs` character for character. + +use crate::Node; + +/// The canonical quoted form of a string field. +fn dump_string(text: &str) -> String { + let mut out = String::from("\""); + + for ch in text.chars() { + match ch { + '\\' => out.push_str("\\\\"), + '"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ if (ch as u32) < 0x20 || ch as u32 == 0x7f => { + out.push_str(&format!("\\u{:04x}", ch as u32)); + }, + _ => out.push(ch), + } + } + + out.push('"'); + out +} + +fn dump_into(nodes: &[Node], indent: usize, out: &mut Vec) { + for node in nodes { + let mut line = format!( + "{}{} {} {}..{}", + " ".repeat(indent), + node.kind, + dump_string(&node.value), + node.source_index, + node.source_end_index + ); + + if let Some(before) = node.before.as_deref() { + line.push_str(&format!(" before={}", dump_string(before))); + } + if let Some(after) = node.after.as_deref() { + line.push_str(&format!(" after={}", dump_string(after))); + } + if let Some(quote) = node.quote { + line.push_str(&format!(" quote={}", dump_string("e.to_string()))); + } + if node.unclosed { + line.push_str(" unclosed"); + } + if let Some(children) = node.nodes.as_deref() { + line.push_str(&format!(" nodes={}", children.len())); + } + + out.push(line); + + if let Some(children) = node.nodes.as_deref() { + dump_into(children, indent + 2, out); + } + } +} + +/// One line per node, nested nodes indented by two. +pub(super) fn dump(nodes: &[Node]) -> String { + let mut lines = Vec::new(); + dump_into(nodes, 0, &mut lines); + lines.join("\n") +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/parity.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/parity.rs new file mode 100644 index 000000000..4077f0f4f --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/parity.rs @@ -0,0 +1,92 @@ +//! Parity with the JavaScript: every expectation comes from +//! `scripts/generate-value-parser-cases.mjs` via `cases.rs`; never edit them by hand. +//! +//! Regenerate the table after adding an input to the generator: +//! +//! ```sh +//! pnpm run --filter=@stylexswc/postcss-value-parser generate:value-parser-cases +//! ``` + +use crate::{Node, NodeKind, ValueParser, parse, stringify, stringify_node_with, stringify_with}; + +use super::cases::{OVERRIDE_CASES, PARSER_CASES}; +use super::dump::dump; + +#[test] +fn the_entry_point_parses_and_serializes_like_its_parts() { + for case in PARSER_CASES { + let parsed = ValueParser::new(case.input); + + assert_eq!( + dump(&parsed.nodes), + case.ast, + "tree differs for {:?}", + case.input + ); + assert_eq!( + parsed.to_string(), + case.output, + "serialized text differs for {:?}", + case.input + ); + } +} + +/// Collapses a function to `name[arg,arg,arg]`; twin of `bracketFunctions` in the generator. +fn bracket_functions(node: &Node) -> Option { + if node.kind != NodeKind::Function { + return None; + } + + let children = node.nodes.as_deref()?; + let args: Vec<&str> = [0, 2, 4] + .iter() + .filter_map(|at| children.get(*at)) + .map(|child| child.value.as_str()) + .collect(); + + Some(format!("{}[{}]", node.value, args.join(","))) +} + +/// Serializing with a per-node override; scenarios mirror the generator's. +#[test] +fn an_override_replaces_the_same_nodes() { + for case in OVERRIDE_CASES { + let mut nodes = parse(case.input); + + let produced = match case.label { + "function-to-bracket-list" => stringify_with(&nodes, &mut bracket_functions), + "function-to-bracket-list-one-node" => match nodes.get(1) { + Some(node) => stringify_node_with(node, &mut bracket_functions), + None => panic!("{:?} has no second node", case.input), + }, + "replace-nested-function" => stringify_with(&nodes, &mut |node: &Node| match node.kind + == NodeKind::Function + && node.value == "var" + { + true => Some(String::from("10px")), + false => None, + }), + "override-declines-every-node" => stringify_with(&nodes, &mut |_: &Node| None), + // A re-kinded node spells out as its new kind, dropping children and parentheses. + "function-retyped-as-word" => { + match nodes.get_mut(1) { + Some(node) => node.kind = NodeKind::Word, + None => panic!("{:?} has no second node", case.input), + } + stringify(&nodes) + }, + // Unicode-range keeps the children and drops the name and parentheses. + "function-retyped-as-unicode-range" => { + match nodes.get_mut(1) { + Some(node) => node.kind = NodeKind::UnicodeRange, + None => panic!("{:?} has no second node", case.input), + } + stringify(&nodes) + }, + other => panic!("no Rust side for the {other:?} scenario"), + }; + + assert_eq!(produced, case.output, "the {} scenario differs", case.label); + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/properties.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/properties.rs new file mode 100644 index 000000000..c3f9b5a49 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/tests/properties.rs @@ -0,0 +1,73 @@ +//! Invariants that must hold for every value: no crashes, well-formed offsets, walk order. + +use crate::{Node, NodeKind, ValueParser, stringify, stringify_node}; + +#[test] +fn deeply_nested_functions_parse_serialize_and_walk() { + let depth = 512; + let input = format!("{}1px{}", "calc(".repeat(depth), ")".repeat(depth)); + + let mut parsed = ValueParser::new(&input); + assert_eq!(parsed.to_string(), input); + + let mut visited = 0; + parsed.walk( + |_, _| { + visited += 1; + true + }, + false, + ); + // One function per level, plus the `1px` at the bottom. + assert_eq!(visited, depth + 1); +} + +#[test] +fn deeply_nested_unclosed_functions_are_all_flagged() { + let depth = 512; + let input = format!("{}1px", "calc(".repeat(depth)); + + let mut parsed = ValueParser::new(&input); + assert_eq!(parsed.to_string(), input); + + let mut functions = 0; + parsed.walk( + |node, _| { + if node.kind == NodeKind::Function { + functions += 1; + assert!(node.unclosed, "level {functions} is not flagged unclosed"); + assert_eq!(node.source_end_index, input.len()); + } + true + }, + false, + ); + assert_eq!(functions, depth); +} + +#[test] +fn a_bubbling_walk_visits_children_first_and_ignores_the_answer() { + let mut parsed = ValueParser::new("fn2( fn3())"); + let mut seen = Vec::new(); + + parsed.walk( + |node, _| { + if node.kind == NodeKind::Function { + seen.push(node.value.clone()); + } + false + }, + true, + ); + + assert_eq!(seen, ["fn3", "fn2"]); +} + +/// A function without a child list (only buildable by hand) spells as its bare name. +#[test] +fn a_function_with_no_child_list_serializes_as_its_name() { + let node = Node::new(NodeKind::Function, String::from("calc"), 0, 4); + + assert_eq!(stringify_node(&node), "calc"); + assert_eq!(stringify(std::slice::from_ref(&node)), "calc"); +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/unit.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/unit.rs new file mode 100644 index 000000000..6ee5ea0e1 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/unit.rs @@ -0,0 +1,107 @@ +//! Splitting a word into a number and a unit. + +const MINUS: u32 = b'-' as u32; +const PLUS: u32 = b'+' as u32; +const DOT: u32 = b'.' as u32; +const EXP: u32 = b'e' as u32; +const EXP_UPPER: u32 = b'E' as u32; + +/// Stand-in for JS `NaN` past the end of the string; see `parse::char_code_at`. +const OUT_OF_RANGE: u32 = u32::MAX; + +fn char_code_at(value: &[u8], pos: usize) -> u32 { + match value.get(pos) { + Some(byte) => u32::from(*byte), + None => OUT_OF_RANGE, + } +} + +fn is_digit(code: u32) -> bool { + (48..=57).contains(&code) +} + +/// A word split into its leading number and whatever follows (`10zz` → `("10", "zz")`). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Dimension<'value> { + /// The leading number, as written. + pub number: &'value str, + /// The rest; empty for a bare number. + pub unit: &'value str, +} + +/// Whether three code points would start a number, per +/// . +fn like_number(value: &[u8]) -> bool { + let code = char_code_at(value, 0); + + if code == PLUS || code == MINUS { + let next_code = char_code_at(value, 1); + + if is_digit(next_code) { + return true; + } + + return next_code == DOT && is_digit(char_code_at(value, 2)); + } + + if code == DOT { + return is_digit(char_code_at(value, 1)); + } + + is_digit(code) +} + +/// Splits a word into its leading number and unit, per +/// ; `None` if it doesn't start with one. +pub fn unit(value: &str) -> Option> { + let bytes = value.as_bytes(); + let length = bytes.len(); + let mut pos = 0; + + if length == 0 || !like_number(bytes) { + return None; + } + + let code = char_code_at(bytes, pos); + + if code == PLUS || code == MINUS { + pos += 1; + } + + while pos < length && is_digit(char_code_at(bytes, pos)) { + pos += 1; + } + + if char_code_at(bytes, pos) == DOT && is_digit(char_code_at(bytes, pos + 1)) { + pos += 2; + + while pos < length && is_digit(char_code_at(bytes, pos)) { + pos += 1; + } + } + + let code = char_code_at(bytes, pos); + let next_code = char_code_at(bytes, pos + 1); + let next_next_code = char_code_at(bytes, pos + 2); + + // An exponent joins only when complete: `1e+` splits as `("1", "e+")`. + if (code == EXP || code == EXP_UPPER) + && (is_digit(next_code) + || ((next_code == PLUS || next_code == MINUS) && is_digit(next_next_code))) + { + pos += match next_code == PLUS || next_code == MINUS { + true => 3, + false => 2, + }; + + while pos < length && is_digit(char_code_at(bytes, pos)) { + pos += 1; + } + } + + // Cuts are always on character boundaries; the fallbacks mirror JS `slice`. + Some(Dimension { + number: value.get(..pos).unwrap_or(value), + unit: value.get(pos..).unwrap_or(""), + }) +} diff --git a/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/walk.rs b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/walk.rs new file mode 100644 index 000000000..17d47d269 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/postcss-value-parser/src/walk.rs @@ -0,0 +1,28 @@ +//! Visiting every node. + +use crate::{Node, NodeKind}; + +/// Visits every node, descending into functions; `cb` gets the node and its sibling index and +/// returns whether to descend. With `bubble`, children come first and the answer is ignored. +/// +/// Unlike upstream, `cb` cannot see the sibling list, so structural edits must happen outside +/// the walk. +pub fn walk(nodes: &mut [Node], cb: &mut dyn FnMut(&mut Node, usize) -> bool, bubble: bool) { + for (index, node) in nodes.iter_mut().enumerate() { + let descend = match bubble { + true => true, + false => cb(node, index), + }; + + if descend + && node.kind == NodeKind::Function + && let Some(children) = node.nodes.as_mut() + { + walk(children, cb, bubble); + } + + if bubble { + cb(node, index); + } + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/rustfmt.toml b/packages/@stylexjs/rust-plugin/crates/rustfmt.toml new file mode 100644 index 000000000..472fff745 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/rustfmt.toml @@ -0,0 +1,12 @@ +# The formatting of the vendored core (upstream rustfmt.toml, stable options). +edition = "2024" +max_width = 100 +newline_style = "Unix" +tab_spaces = 2 +merge_derives = true +reorder_imports = true +use_small_heuristics = "Default" +fn_params_layout = "Tall" +match_block_trailing_comma = true +use_field_init_shorthand = true +use_try_shorthand = true diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/Cargo.toml b/packages/@stylexjs/rust-plugin/crates/stylex-ast/Cargo.toml new file mode 100644 index 000000000..084bcd0b7 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/Cargo.toml @@ -0,0 +1,36 @@ +[package] +description = "AST factories, convertors and helpers for StyleX SWC plugin" +edition.workspace = true +license.workspace = true +name = "stylex_ast" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + + +[lib] +crate-type = ["rlib"] +doctest = false + +[dependencies] +anyhow.workspace = true +indexmap.workspace = true +rustc-hash.workspace = true +swc_core = { workspace = true, features = [ + "common", + "ecma_codegen", + "ecma_parser", + "ecma_plugin_transform", + "ecma_utils", +] } + +stylex_constants = { path = "../stylex-constants" } +stylex_macros = { path = "../stylex-macros" } +stylex_utils = { path = "../stylex-utils" } + +[dev-dependencies] +ctor.workspace = true +pretty_env_logger.workspace = true + +[lints] +workspace = true diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/convertors.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/convertors.rs new file mode 100644 index 000000000..b07cb34d8 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/convertors.rs @@ -0,0 +1,402 @@ +use std::borrow::Cow; + +use anyhow::anyhow; +use stylex_macros::{stylex_panic, stylex_unimplemented}; +use stylex_utils::{number::to_js_string, string::utf16_length, swc::get_expr_node_kind}; +use swc_core::{ + atoms::{Atom, Wtf8Atom}, + common::DUMMY_SP, + ecma::{ + ast::{ + BigInt, Bool, CallExpr, Expr, Ident, KeyValueProp, Lit, MemberProp, Number, ObjectLit, Prop, + PropName, PropOrSpread, Str, Tpl, TplElement, VarDeclarator, + }, + parser::Context, + utils::{quote_ident, quote_str}, + }, +}; + +use stylex_constants::constants::messages::{ + ILLEGAL_PROP_VALUE, INVALID_UTF8, SPREAD_NOT_SUPPORTED, +}; + +use super::factories::{ + create_big_int_lit, create_boolean_lit, create_ident, create_null_lit, create_number_lit, + create_string_lit, +}; + +/// Reads a member property's static key: ident name, string value, or JS `String(Number)`. +/// `None` for private names, non-literal computed keys and invalid UTF-8. +pub fn convert_member_prop_to_string(prop: &MemberProp) -> Option { + match prop { + MemberProp::Ident(ident) => Some(ident.sym.to_string()), + MemberProp::Computed(computed) => { + convert_static_member_key_expr_to_string(normalize_expr(computed.expr.as_ref())) + }, + MemberProp::PrivateName(_) => None, + } +} + +/// Unwraps parentheses (iteratively, spans preserved); see [`normalize_expr_mut`]. +pub fn normalize_expr(mut expr: &Expr) -> &Expr { + while let Expr::Paren(paren) = expr { + expr = paren.expr.as_ref(); + } + expr +} + +/// The call a declarator is initialized by, read through parentheses; `None` otherwise. +pub fn init_call(var_declarator: &VarDeclarator) -> Option<&CallExpr> { + var_declarator + .init + .as_deref() + .map(normalize_expr) + .and_then(Expr::as_call) +} + +/// Mutable counterpart to [`normalize_expr`]; spans are preserved. +pub fn normalize_expr_mut(mut expr: &mut Expr) -> &mut Expr { + while let Expr::Paren(paren) = expr { + expr = paren.expr.as_mut(); + } + expr +} + +fn convert_static_member_key_expr_to_string(expr: &Expr) -> Option { + match expr { + Expr::Lit(Lit::Str(s)) => s.value.as_str().map(|value| value.to_string()), + Expr::Lit(Lit::Num(n)) => Some(to_js_string(n.value)), + Expr::Lit(Lit::BigInt(big_int)) => Some(big_int.value.to_string()), + Expr::Tpl(tpl) => convert_tpl_to_string_lit(tpl).and_then(|lit| convert_lit_to_string(&lit)), + _ => None, + } +} + +pub fn convert_lit_to_number(lit_num: &Lit) -> Result { + match lit_num { + Lit::Bool(Bool { value, .. }) => Ok(if *value { 1.0 } else { 0.0 }), + Lit::Num(num) => Ok(num.value), + Lit::Str(strng) => { + let string_value = convert_atom_to_string(&strng.value); + match string_value.parse::() { + Ok(num) => Ok(num), + Err(_) => Err(anyhow!("Value is not a number: {}", string_value)), + } + }, + _ => Err(anyhow!( + "Value is not a number: {}", + get_expr_node_kind(&Expr::from(lit_num.clone())) + )), + } +} + +pub fn convert_tpl_to_string_lit(tpl: &Tpl) -> Option { + if !tpl.exprs.is_empty() || tpl.quasis.len() != 1 { + return None; + } + + let quasi = &tpl.quasis[0]; + let value = match quasi.cooked.as_ref() { + Some(cooked) => match cooked.as_str() { + Some(value) => value, + None => stylex_panic!("Failed to extract a string value from the expression."), + }, + None => stylex_panic!("Failed to extract cooked value from template literal element."), + }; + + Some(create_string_lit(value)) +} + +pub fn convert_simple_tpl_to_str_expr(expr: Expr) -> Expr { + match expr { + Expr::Tpl(ref tpl) => convert_tpl_to_string_lit(tpl).map_or(expr, Expr::Lit), + _ => expr, + } +} + +pub fn convert_concat_to_tpl_expr(expr: Expr) -> Expr { + match expr { + Expr::Call(ref call_expr) => concat_call_to_template_literal(call_expr).unwrap_or(expr), + _ => expr, + } +} + +pub(crate) fn concat_call_to_template_literal(call_expr: &CallExpr) -> Option { + use swc_core::common::DUMMY_SP; + + let callee = call_expr.callee.as_expr()?; + let member_expr = callee.as_member()?; + let prop_ident = member_expr.prop.as_ident()?; + + if prop_ident.sym.as_ref() != "concat" { + return None; + } + + let object_lit = member_expr.obj.as_lit()?; + let base_string = { + let base_string = extract_str_lit_ref(object_lit)?; + base_string.to_string() + }; + + let mut exprs = Vec::new(); + let mut quasis = Vec::new(); + + quasis.push(TplElement { + span: DUMMY_SP, + tail: false, + cooked: Some(base_string.clone().into()), + raw: base_string.into(), + }); + + for (i, arg) in call_expr.args.iter().enumerate() { + if arg.spread.is_some() { + continue; + } + + exprs.push(arg.expr.clone()); + + let is_last = i == call_expr.args.len() - 1; + quasis.push(TplElement { + span: DUMMY_SP, + tail: is_last, + cooked: Some("".into()), + raw: "".into(), + }); + } + + Some(Expr::Tpl(Tpl { + span: DUMMY_SP, + exprs, + quasis, + })) +} + +pub fn create_number_expr(value: f64) -> Expr { + Expr::from(create_number_lit(value)) +} + +/// A numeric literal spelled the way JavaScript spells it (`0` for `-0`, `NaN`, `Infinity`), +/// unlike [`create_number_expr`], which leaves the emitter to invent `0 / 0` and `1 / 0`. +pub fn create_js_number_expr(value: f64) -> Expr { + Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value, + raw: Some(to_js_string(value).into()), + })) +} + +pub fn create_big_int_expr(value: BigInt) -> Expr { + Expr::from(create_big_int_lit(value)) +} + +pub fn create_string_expr(value: &str) -> Expr { + Expr::Lit(create_string_lit(value)) +} + +pub fn create_bool_expr(value: bool) -> Expr { + Expr::Lit(create_boolean_lit(value)) +} + +/// Whether an identifier is the language's `undefined` (shadowing is refused upstream). +pub fn is_js_undefined(ident: &Ident) -> bool { + ident.sym.as_ref() == "undefined" +} + +pub fn create_ident_expr(value: &str) -> Expr { + Expr::Ident(create_ident(value)) +} + +pub fn create_null_expr() -> Expr { + Expr::Lit(create_null_lit()) +} + +fn should_wrap_prop_name_key_with_quotes(key: &str) -> bool { + // Reserved words are lowercase ASCII, so any other ASCII character skips the lookups. + if let Some((&first, rest)) = key.as_bytes().split_first() + && first.is_ascii() + && Ident::is_valid_ascii_start(first) + && rest + .iter() + .all(|byte| byte.is_ascii() && Ident::is_valid_ascii_continue(*byte)) + && key.bytes().any(|byte| !byte.is_ascii_lowercase()) + { + return false; + } + + if Ident::verify_symbol(key).is_ok() { + return false; + } + + !Context::default().is_reserved_word(&key.into()) +} + +pub fn convert_string_to_prop_name(value: &str) -> PropName { + if should_wrap_prop_name_key_with_quotes(value) { + PropName::Str(quote_str!(value)) + } else { + PropName::Ident(quote_ident!(value)) + } +} + +/// The key-value pair a property stands for, copying only a shorthand name. +pub fn expanded_shorthand_prop(prop: &Prop) -> Cow<'_, Prop> { + match prop.as_shorthand() { + Some(ident) => Cow::Owned(Prop::from(KeyValueProp { + key: convert_string_to_prop_name(ident.sym.as_ref()), + value: Box::new(Expr::Ident(ident.clone())), + })), + None => Cow::Borrowed(prop), + } +} + +/// Rewrites a shorthand name in place into its key-value pair. +pub fn expand_shorthand_prop(prop: &mut Box) { + if let Cow::Owned(expanded) = expanded_shorthand_prop(prop) { + **prop = expanded; + } +} + +/// A string literal's length in UTF-16 code units; handles unpaired surrogates without panicking. +pub fn atom_utf16_length(atom: &Wtf8Atom) -> usize { + match atom.as_str() { + Some(value) => utf16_length(value), + None => atom.to_ill_formed_utf16().count(), + } +} + +/// The character at a UTF-16 index of `atom`; `None` past the end. +/// Half of an astral character answers U+FFFD, matching the engine fold and `charAt` (a decided +/// exemption from `guidelines/stack/RUST.md`). +pub fn atom_utf16_char_at(atom: &Wtf8Atom, index: usize) -> Option { + let unit = match atom.as_str() { + Some(text) => text.encode_utf16().nth(index), + None => atom.to_ill_formed_utf16().nth(index), + }?; + + Some(char::from_u32(u32::from(unit)).unwrap_or(char::REPLACEMENT_CHARACTER)) +} + +pub fn convert_atom_to_string(atom: &Wtf8Atom) -> String { + match atom.as_str() { + Some(value) => value.to_string(), + None => stylex_panic!("{}", INVALID_UTF8), + } +} + +pub fn convert_wtf8_to_atom(atom: &Wtf8Atom) -> Atom { + match atom.as_atom() { + Some(value) => value.clone(), + None => stylex_panic!("{}", INVALID_UTF8), + } +} + +pub fn convert_str_lit_to_string(str_lit: &Str) -> String { + convert_str_lit_to_str_ref(str_lit).to_string() +} + +/// A string literal's text, borrowed; refuses lone surrogates like the owned form. +/// Kept apart from [`extract_str_lit_ref`] because each has a different user-facing message. +pub fn convert_str_lit_to_str_ref(str_lit: &Str) -> &str { + match str_lit.value.as_str() { + Some(value) => value, + None => stylex_panic!("{}", INVALID_UTF8), + } +} + +pub fn convert_str_lit_to_atom(str_lit: &Str) -> Atom { + match str_lit.value.as_atom() { + Some(value) => value.clone(), + None => stylex_panic!("{}", INVALID_UTF8), + } +} + +pub fn extract_tpl_cooked_value(elem: &TplElement) -> &str { + match elem.cooked.as_ref() { + Some(cooked) => match cooked.as_str() { + Some(value) => value, + None => stylex_panic!("{}", INVALID_UTF8), + }, + None => stylex_panic!( + "Template literal element has no cooked value (contains an invalid escape sequence)." + ), + } +} + +pub fn convert_atom_to_str_ref(atom: &swc_core::atoms::Wtf8Atom) -> &str { + match atom.as_str() { + Some(value) => value, + None => stylex_panic!("Failed to convert SWC Atom to string (invalid WTF-8 encoding)."), + } +} + +pub fn convert_lit_to_string(value: &Lit) -> Option { + match value { + Lit::Str(strng) => Some(convert_str_lit_to_string(strng)), + Lit::Num(num) => Some(to_js_string(num.value)), + Lit::BigInt(big_int) => Some(format!("{}", big_int.value)), + _ => None, + } +} + +pub fn extract_str_lit_ref(lit: &Lit) -> Option<&str> { + match lit { + Lit::Str(strng) => Some(convert_atom_to_str_ref(&strng.value)), + _ => None, + } +} + +/// A key's authored name, borrowed for identifiers and strings; numbers, big integers and +/// computed keys are spelled out. [`convert_key_value_to_str`] is the owned form. +pub fn key_value_name(key_value: &KeyValueProp) -> Cow<'_, str> { + match &key_value.key { + PropName::Ident(ident) => Cow::Borrowed(ident.sym.as_str()), + PropName::Str(strng) => Cow::Borrowed(convert_str_lit_to_str_ref(strng)), + PropName::Num(num) => Cow::Owned(to_js_string(num.value)), + PropName::BigInt(big_int) => Cow::Owned(big_int.value.to_string()), + PropName::Computed(computed) => Cow::Owned(match computed.expr.as_ref() { + Expr::Lit(lit) => match convert_lit_to_string(lit) { + Some(s) => s, + None => stylex_panic!("Computed property key must be a string or number literal."), + }, + Expr::Tpl(tpl) => { + match convert_tpl_to_string_lit(tpl).and_then(|lit| convert_lit_to_string(&lit)) { + Some(s) => s, + None => stylex_unimplemented!("Computed key is not a literal"), + } + }, + _ => stylex_unimplemented!("Computed key is not a literal"), + }), + } +} + +/// A key's authored name, owned and unquoted. +#[inline] +pub fn convert_key_value_to_str(key_value: &KeyValueProp) -> String { + key_value_name(key_value).into_owned() +} + +pub fn get_key_values_from_object(object: &ObjectLit) -> Vec { + object + .props + .iter() + .map(|prop| match prop { + PropOrSpread::Spread(_) => stylex_unimplemented!("{}", SPREAD_NOT_SUPPORTED), + // Copies only the answered pair, not the whole value subtree. + PropOrSpread::Prop(prop) => match expanded_shorthand_prop(prop).as_ref() { + Prop::KeyValue(key_value) => key_value.clone(), + _ => stylex_panic!("{}", ILLEGAL_PROP_VALUE), + }, + }) + .collect() +} + +/// Extracts the initializer expression from a variable declarator. +/// +/// # Panics +/// Via `stylex_panic!` when the declarator has no initializer (e.g. `let x;`). +pub fn get_expr_from_var_decl(var_decl: &VarDeclarator) -> &Expr { + match &var_decl.init { + Some(var_decl_init) => var_decl_init, + None => stylex_panic!("Variable declaration must be initialized with an expression."), + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/factories.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/factories.rs new file mode 100644 index 000000000..c1df6e38e --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/factories.rs @@ -0,0 +1,608 @@ +use stylex_macros::stylex_panic; +use swc_core::{ + common::{DUMMY_SP, Span, SyntaxContext}, + ecma::ast::{ + ArrayLit, ArrowExpr, ArrowFunctionBody, BigInt, BinExpr, BinaryOp, BindingIdent, CallExpr, + Callee, ComputedPropName, CondExpr, Expr, ExprOrSpread, Ident, IdentName, ImportDecl, + ImportPhase, ImportSpecifier, ImportStarAsSpecifier, JSXAttr, JSXAttrName, JSXAttrOrSpread, + JSXAttrValue, KeyValueProp, Lit, MemberExpr, MemberProp, ModuleDecl, ModuleItem, Null, + ObjectLit, ParenExpr, Pat, Prop, PropName, PropOrSpread, SpreadElement, Str, UnaryExpr, + VarDeclarator, + }, +}; + +use super::convertors::{ + convert_string_to_prop_name, create_bool_expr, create_null_expr, create_number_expr, + create_string_expr, +}; + +/// Wraps an owned expression in a `ParenExpr` with `DUMMY_SP`. +/// +/// # Example +/// ```ignore +/// let wrapped = wrap_in_paren(some_expr); +/// ``` +#[inline] +pub fn wrap_in_paren(expr: Expr) -> Expr { + Expr::Paren(ParenExpr { + span: DUMMY_SP, + expr: Box::new(expr), + }) +} + +/// Wraps a clone of an expression in a `ParenExpr` with `DUMMY_SP`. +/// +/// # Example +/// ```ignore +/// let wrapped = wrap_in_paren_ref(&path); +/// ``` +#[inline] +pub fn wrap_in_paren_ref(expr: &Expr) -> Expr { + wrap_in_paren(expr.clone()) +} + +/// Creates an `ObjectLit` from a vector of `PropOrSpread`. +/// +/// # Example +/// ```ignore +/// let object_lit = create_object_lit(vec![prop_or_spread_factory(key, value)]); +/// ``` +#[inline] +pub fn create_object_lit(props: Vec) -> ObjectLit { + ObjectLit { + span: DUMMY_SP, + props, + } +} + +/// Creates an `ArrayLit` from a vector of `ExprOrSpread`. +/// +/// # Example +/// ```ignore +/// let array_lit = create_array_lit(vec![create_expr_or_spread(value)]); +/// ``` +#[inline] +pub fn create_array_lit(elems: Vec>) -> ArrayLit { + ArrayLit { + span: DUMMY_SP, + elems, + } +} + +/// Creates an `Expr::Object` from a vector of `PropOrSpread`. +/// +/// # Example +/// ```ignore +/// let object_expression = create_object_expression(vec![prop_or_spread_factory(key, value)]); +/// ``` +#[inline] +pub fn create_object_expression(props: Vec) -> Expr { + Expr::from(create_object_lit(props)) +} + +#[inline] +pub fn create_array_expression(elems: Vec>) -> Expr { + Expr::from(create_array_lit(elems)) +} + +#[inline] +pub fn create_bin_expr(op: BinaryOp, left: Expr, right: Expr) -> Expr { + Expr::from(BinExpr { + span: DUMMY_SP, + op, + left: Box::new(left), + right: Box::new(right), + }) +} + +#[inline] +pub fn create_cond_expr(test: Expr, cons: Expr, alt: Expr) -> Expr { + Expr::from(CondExpr { + span: DUMMY_SP, + test: Box::new(test), + cons: Box::new(cons), + alt: Box::new(alt), + }) +} + +/// Creates a `PropOrSpread` from a key and value. +/// +/// # Example +/// ```ignore +/// let prop_or_spread = create_key_value_prop("key", value); +#[inline] +pub fn create_key_value_prop(key: &str, value: Expr) -> PropOrSpread { + PropOrSpread::from(Prop::from(KeyValueProp { + key: convert_string_to_prop_name(key), + value: Box::new(value), + })) +} + +/// Creates a `BindingIdent` from an `Ident`. +/// +/// # Example +/// ```ignore +/// let binding_ident = create_binding_ident(ident); +/// ``` +#[inline] +pub fn create_binding_ident(ident: Ident) -> BindingIdent { + BindingIdent::from(ident) +} + +/// Creates a `Lit::Str` from a string. +/// +/// # Example +/// ```ignore +/// let lit_str = create_string_lit("value"); +/// ``` +#[inline] +pub fn create_string_lit(value: &str) -> Lit { + Lit::from(value) +} + +/// Creates a `Lit::Number` from a number. +#[inline] +pub fn create_number_lit(value: f64) -> Lit { + Lit::from(value) +} + +/// Creates a `Lit::BigInt` from a `BigInt`. +/// +/// # Example +/// ```ignore +/// let lit_big_int = create_big_int_lit(BigInt::from(123)); +/// ``` +#[inline] +pub fn create_big_int_lit(value: BigInt) -> Lit { + Lit::from(value) +} + +/// Creates a `Lit::Boolean` from a boolean. +/// +/// # Example +/// ```ignore +/// let lit_boolean = create_boolean_lit(true); +/// ``` +#[inline] +pub fn create_boolean_lit(value: bool) -> Lit { + Lit::from(value) +} + +/// Creates a `Lit::Null` from a `Null`. +/// +/// # Example +/// ```ignore +/// let lit_null = create_null_lit(); +/// ``` +#[inline] +pub fn create_null_lit() -> Lit { + Lit::Null(Null { span: DUMMY_SP }) +} + +/// Creates an `Ident` from a string. +/// +/// # Example +/// ```ignore +/// let ident = create_ident("props"); +/// ``` +#[inline] +pub fn create_ident(name: &str) -> Ident { + Ident::from(name) +} + +#[inline] +pub fn create_nested_object_prop(key: &str, values: Vec) -> PropOrSpread { + let object = ObjectLit { + span: DUMMY_SP, + props: values, + }; + + create_key_value_prop(key, Expr::Object(object)) +} + +/// Creates a `KeyValueProp` with an `IdentName` key. +/// +/// # Example +/// ```ignore +/// let key_value = create_key_value_prop_ident("props", value); +#[inline] +pub fn create_key_value_prop_ident(key: &str, value: Expr) -> KeyValueProp { + KeyValueProp { + key: PropName::Ident(IdentName::new(key.into(), DUMMY_SP)), + value: Box::new(value), + } +} + +/// Creates a `PropOrSpread` with an unconditional `PropName::Ident` key, skipping identifier +/// validation (for keys like `@media …` that callers read back with `.as_ident()`). +#[inline] +pub fn create_ident_key_value_prop(key: &str, value: Expr) -> PropOrSpread { + PropOrSpread::from(Prop::from(KeyValueProp { + key: PropName::Ident(IdentName::new(key.into(), DUMMY_SP)), + value: Box::new(value), + })) +} + +/// Creates a `PropOrSpread` with an unconditional `PropName::Str` key (e.g. `$$css`). +/// +/// # Example +/// ```ignore +/// let prop_or_spread = create_str_key_value_prop("$$css", create_bool_expr(true)); +/// ``` +#[inline] +pub fn create_str_key_value_prop(key: &str, value: Expr) -> PropOrSpread { + PropOrSpread::from(Prop::from(KeyValueProp { + key: PropName::Str(Str { + span: DUMMY_SP, + value: key.into(), + raw: None, + }), + value: Box::new(value), + })) +} + +/// Creates a `PropOrSpread` with a string key and string value. +/// +/// # Example +/// ```ignore +/// let prop_or_spread = create_string_key_value_prop("key", "value"); +#[inline] +pub fn create_string_key_value_prop(key: &str, value: &str) -> PropOrSpread { + let value = create_string_expr(value); + + create_key_value_prop(key, value) +} + +// NOTE: Tests only using this function +#[allow(dead_code)] +pub fn create_string_array_prop(key: &str, value: &[&str]) -> PropOrSpread { + let array = ArrayLit { + span: DUMMY_SP, + elems: value + .iter() + .map(|v| Some(create_string_expr_or_spread(v))) + .collect::>>(), + }; + + create_key_value_prop(key, Expr::from(array)) +} + +/// Creates a `PropOrSpread` with a boolean key and value. +/// +/// # Example +/// ```ignore +/// let prop_or_spread = prop_or_spread_boolean_factory("key", true); +#[inline] +pub fn create_boolean_prop(key: &str, value: Option) -> PropOrSpread { + match value { + Some(value) => create_key_value_prop(key, create_bool_expr(value)), + None => stylex_panic!("Value is not a boolean"), + } +} + +/// Wraps an expression in `ExprOrSpread` with no spread. +#[inline] +pub fn create_expr_or_spread(expr: Expr) -> ExprOrSpread { + ExprOrSpread { + expr: Box::new(expr), + spread: None, + } +} + +/// Creates an `ExprOrSpread` with a string value. +/// +/// # Example +/// ```ignore +/// let expr_or_spread = create_string_expr_or_spread("value"); +/// ``` +#[inline] +pub fn create_string_expr_or_spread(value: &str) -> ExprOrSpread { + create_expr_or_spread(create_string_expr(value)) +} + +#[inline] +pub fn create_number_expr_or_spread(value: f64) -> ExprOrSpread { + create_expr_or_spread(create_number_expr(value)) +} + +// NOTE: Tests only using this function +#[allow(dead_code)] +pub fn create_array(values: &[Expr]) -> ArrayLit { + array_fabric(values, None) +} + +/// Creates an `ArrayLit` with a spread. +/// +/// # Example +/// ```ignore +/// let array_lit = _create_spreaded_array(vec![expr1, expr2, expr3]); +/// ``` +#[inline] +pub fn create_spreaded_array(values: &[Expr]) -> ArrayLit { + array_fabric(values, Some(DUMMY_SP)) +} + +/// Creates an `ArrayLit` with a spread. +/// +/// # Example +/// ```ignore +/// let array_lit = array_fabric(vec![expr1, expr2, expr3], Some(DUMMY_SP)); +// NOTE: Tests only using this function +#[allow(dead_code)] +fn array_fabric(values: &[Expr], spread: Option) -> ArrayLit { + ArrayLit { + span: DUMMY_SP, + elems: values + .iter() + .map(|value| { + Some(ExprOrSpread { + spread, + expr: Box::new(value.clone()), + }) + }) + .collect(), + } +} + +/// Creates an `IdentName` from a string. +/// +/// # Example +/// ```ignore +/// let ident_name = create_ident_name("props"); +/// ``` +#[inline] +pub fn create_ident_name(name: &str) -> IdentName { + IdentName::new(name.into(), DUMMY_SP) +} + +/// Creates a `SpreadElement` for spreading an expression. +/// +/// # Example +/// ```ignore +/// let spread = create_spread_element(obj_expr); +/// ``` +#[inline] +pub fn create_spread_element(expr: Expr) -> SpreadElement { + SpreadElement { + dot3_token: DUMMY_SP, + expr: Box::new(expr), + } +} + +/// Creates a `PropOrSpread::Spread` for an object literal. +/// +/// # Example +/// ```ignore +/// let spread = create_spread_prop(call_expr); +/// ``` +#[inline] +pub fn create_spread_prop(expr: Expr) -> PropOrSpread { + PropOrSpread::Spread(create_spread_element(expr)) +} + +/// Creates a `MemberExpr` (`obj.prop` or `obj[expr]`). +/// +/// # Example +/// ```ignore +/// let member = create_member_expr(obj_expr, create_computed_member_prop(key_expr)); +/// ``` +#[inline] +pub fn create_member_expr(obj: Expr, prop: MemberProp) -> MemberExpr { + MemberExpr { + span: DUMMY_SP, + obj: Box::new(obj), + prop, + } +} + +/// A `MemberProp` for `obj.`: an ident when `key` is a valid identifier (same rule as +/// [`convert_string_to_prop_name`]), else a computed string key. +/// +/// # Example +/// ```ignore +/// let prop = create_member_prop_from_key("color"); // -> obj.color +/// let prop = create_member_prop_from_key("--c"); // -> obj["--c"] +/// ``` +#[inline] +pub fn create_member_prop_from_key(key: &str) -> MemberProp { + match convert_string_to_prop_name(key) { + PropName::Ident(ident) => MemberProp::Ident(ident), + _ => create_computed_member_prop(create_string_expr(key)), + } +} + +/// Creates a computed `MemberProp` (the `[expr]` in `obj[expr]`). +/// +/// # Example +/// ```ignore +/// let prop = create_computed_member_prop(create_string_expr("--custom-prop")); +/// ``` +#[inline] +pub fn create_computed_member_prop(expr: Expr) -> MemberProp { + MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(expr), + }) +} + +/// Creates a `CallExpr` with a member callee (`obj.method(...args)`). +/// +/// # Example +/// ```ignore +/// let member = member_expr_factory("stylex", "props"); +/// let call = create_member_call_expr(member, vec![arg1, arg2]); +/// ``` +#[inline] +pub fn create_member_call_expr(callee_member: MemberExpr, args: Vec) -> CallExpr { + create_call_expr(Expr::Member(callee_member), args) +} + +/// Creates a `CallExpr` from an arbitrary callee expression (`()(...args)`). +#[inline] +pub fn create_call_expr(callee: Expr, args: Vec) -> CallExpr { + CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::new(callee)), + args, + type_args: None, + ctxt: SyntaxContext::empty(), + } +} + +/// Creates a `CallExpr` with an identifier callee (e.g., `func(...args)`). +/// +/// # Example +/// ```ignore +/// let call = create_ident_call_expr("merge", vec![arg1]); +/// ``` +#[inline] +pub fn create_ident_call_expr(callee_ident: &str, args: Vec) -> CallExpr { + create_call_expr(Expr::Ident(create_ident(callee_ident)), args) +} + +/// Creates an arrow expression `() => expr`. +/// +/// # Example +/// ```ignore +/// let arrow = create_arrow_expression(call_expr); +/// ``` +#[inline] +pub fn create_arrow_expression(body_expr: Expr) -> Expr { + create_arrow_expression_with_params(vec![], body_expr) +} + +/// Creates an arrow expression with parameters `(params) => expr`. +/// +/// # Example +/// ```ignore +/// let arrow = create_arrow_expression_with_params( +/// vec![Pat::Ident(create_binding_ident(create_ident("_v")))], +/// body_expr, +/// ); +/// ``` +#[inline] +pub fn create_arrow_expression_with_params(params: Vec, body_expr: Expr) -> Expr { + Expr::Arrow(ArrowExpr { + span: DUMMY_SP, + params, + body: Box::new(ArrowFunctionBody::Expr(Box::new(body_expr))), + is_async: false, + is_generator: false, + type_params: None, + return_type: None, + ctxt: SyntaxContext::empty(), + }) +} + +/// Creates a `JSXAttrOrSpread::SpreadElement` for JSX attributes. +/// +/// # Example +/// ```ignore +/// let jsx_spread = create_jsx_spread_attr(props_call); +/// ``` +#[inline] +pub fn create_jsx_spread_attr(expr: Expr) -> JSXAttrOrSpread { + JSXAttrOrSpread::SpreadElement(create_spread_element(expr)) +} + +/// Creates a `JSXAttrOrSpread::JSXAttr` wrapper. +/// +/// # Example +/// ```ignore +/// let jsx_attr = create_jsx_attr_or_spread(attr); +/// ``` +#[inline] +pub fn create_jsx_attr_or_spread(attr: JSXAttr) -> JSXAttrOrSpread { + use JSXAttrOrSpread; + JSXAttrOrSpread::JSXAttr(attr) +} + +/// Creates a `JSXAttr` from a name and value. +/// +/// # Example +/// ```ignore +/// let jsx_attr = create_jsx_attr("name", value); +/// ``` +#[allow(dead_code)] +#[inline] +pub fn create_jsx_attr(name: &str, value: JSXAttrValue) -> JSXAttr { + JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(IdentName::from(name)), + value: Some(value), + } +} + +/// Creates a `VarDeclarator` with an expression initializer. +/// +/// # Example +/// ```ignore +/// let decl = create_var_declarator(my_ident, some_expr); +/// ``` +#[inline] +pub fn create_var_declarator(ident: Ident, init: Expr) -> VarDeclarator { + VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(create_binding_ident(ident)), + init: Some(Box::new(init)), + definite: false, + } +} + +/// Creates a `VarDeclarator` initialized to `null`. +/// +/// # Example +/// ```ignore +/// let decl = var_declarator_null_init_factory(my_ident); +/// ``` +#[inline] +pub fn create_null_var_declarator(ident: Ident) -> VarDeclarator { + create_var_declarator(ident, create_null_expr()) +} + +/// Creates a `VarDeclarator` initialized to a string. +/// +/// # Example +/// ```ignore +/// let decl = create_string_var_declarator(my_ident, "value"); +/// ``` +#[inline] +pub fn create_string_var_declarator(ident: Ident, value: &str) -> VarDeclarator { + create_var_declarator(ident, create_string_expr(value)) +} + +/// Creates a namespace import `import * as from ''` as a `ModuleItem`. +/// +/// # Example +/// ```ignore +/// let item = create_import_namespace_decl("stylex", "@stylexjs/stylex"); +/// ``` +#[inline] +pub fn create_import_namespace_decl(local: &str, source: &str) -> ModuleItem { + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Namespace(ImportStarAsSpecifier { + span: DUMMY_SP, + local: create_ident(local), + })], + src: Box::new(Str { + span: DUMMY_SP, + value: source.into(), + raw: None, + }), + type_only: false, + with: None, + phase: ImportPhase::Evaluation, + })) +} + +/// Rebuilds a borrowed unary expression as an owned `Expr` (deep-copies the operand). +/// +/// # Example +/// ```ignore +/// let path = create_unary_expr(unary); +/// ``` +#[inline] +pub fn create_unary_expr(unary: &UnaryExpr) -> Expr { + Expr::Unary(unary.clone()) +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/imports.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/imports.rs new file mode 100644 index 000000000..47a74df00 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/imports.rs @@ -0,0 +1,12 @@ +//! Reading what an import specifier brings into scope. + +use swc_core::ecma::ast::{Ident, ImportSpecifier}; + +/// The name a specifier binds locally, whichever import form it takes. +pub fn local_binding_of(specifier: &ImportSpecifier) -> &Ident { + match specifier { + ImportSpecifier::Named(named) => &named.local, + ImportSpecifier::Default(default) => &default.local, + ImportSpecifier::Namespace(namespace) => &namespace.local, + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/keys.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/keys.rs new file mode 100644 index 000000000..d21df0f66 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/keys.rs @@ -0,0 +1,120 @@ +//! Reading the static name of an object property or member key. + +use stylex_constants::constants::messages::{ + ILLEGAL_PROP_VALUE, KEY_HAS_NO_NAME, SPREAD_NOT_SUPPORTED, +}; +use stylex_macros::stylex_panic; +use stylex_utils::number::to_js_string; +use swc_core::{ + atoms::Atom, + ecma::ast::{Expr, KeyValueProp, Lit, MemberProp, ObjectLit, Prop, PropName, PropOrSpread}, +}; + +use super::convertors::{convert_str_lit_to_atom, convert_tpl_to_string_lit, normalize_expr}; + +pub fn namespace_name_from_prop_key(key: &PropName) -> Option { + match key { + PropName::Ident(ident) => Some(ident.sym.clone()), + PropName::Str(strng) => Some(convert_str_lit_to_atom(strng)), + // Spelled the way JavaScript spells the number (`1e21` names `1e+21`). + PropName::Num(num) => Some(Atom::from(to_js_string(num.value))), + PropName::BigInt(big_int) => Some(Atom::from(big_int.value.to_string())), + PropName::Computed(computed) => namespace_name_from_expr(computed.expr.as_ref()), + } +} + +/// The authored name of a key, or `None` (including for invalid UTF-8; never panics). +/// [`namespace_name_from_prop_key`] refuses invalid UTF-8 instead. +pub fn try_namespace_name_from_prop_key(key: &PropName) -> Option { + match key { + PropName::Ident(ident) => Some(ident.sym.clone()), + PropName::Str(strng) => strng.value.as_atom().cloned(), + PropName::Num(num) => Some(Atom::from(to_js_string(num.value))), + PropName::BigInt(big_int) => Some(Atom::from(big_int.value.to_string())), + PropName::Computed(computed) => try_namespace_name_from_expr(computed.expr.as_ref()), + } +} + +fn try_namespace_name_from_lit(lit: &Lit) -> Option { + match lit { + Lit::Str(strng) => strng.value.as_atom().cloned(), + Lit::Num(num) => Some(Atom::from(to_js_string(num.value))), + Lit::BigInt(big_int) => Some(Atom::from(big_int.value.to_string())), + _ => None, + } +} + +fn try_namespace_name_from_expr(expr: &Expr) -> Option { + // Read through parentheses, matching `convert_member_prop_to_string`. + match normalize_expr(expr) { + Expr::Lit(lit) => try_namespace_name_from_lit(lit), + // A template with one quasi and no interpolation names that quasi's text. + Expr::Tpl(tpl) if tpl.exprs.is_empty() && tpl.quasis.len() == 1 => tpl.quasis[0] + .cooked + .as_ref() + .and_then(|cooked| cooked.as_atom().cloned()), + _ => None, + } +} + +pub fn namespace_name_from_member_prop(prop: &MemberProp) -> Option { + match prop { + MemberProp::Ident(ident) => Some(ident.sym.clone()), + MemberProp::Computed(computed) => namespace_name_from_expr(computed.expr.as_ref()), + MemberProp::PrivateName(_) => None, + } +} + +fn namespace_name_from_lit(lit: &Lit) -> Option { + match lit { + Lit::Str(strng) => Some(convert_str_lit_to_atom(strng)), + Lit::Num(num) => Some(Atom::from(to_js_string(num.value))), + Lit::BigInt(big_int) => Some(Atom::from(big_int.value.to_string())), + _ => None, + } +} + +fn namespace_name_from_expr(expr: &Expr) -> Option { + // Read through parentheses, as in [`try_namespace_name_from_expr`]. + match normalize_expr(expr) { + Expr::Lit(lit) => namespace_name_from_lit(lit), + Expr::Tpl(tpl) => convert_tpl_to_string_lit(tpl) + .as_ref() + .and_then(namespace_name_from_lit), + _ => None, + } +} + +/// The literal keys of an object literal, in source order, duplicates included. +pub fn collect_object_lit_keys(object: &ObjectLit) -> impl Iterator + '_ { + object.props.iter().filter_map(|prop| { + prop_as_key_value(prop).and_then(|key_value| namespace_name_from_prop_key(&key_value.key)) + }) +} + +/// A property's name and key-value; panics on any other shape (only reachable by hand). +pub fn named_key_value(prop: &PropOrSpread) -> (Atom, &KeyValueProp) { + let key_value = match prop { + PropOrSpread::Spread(_) => stylex_panic!("{}", SPREAD_NOT_SUPPORTED), + PropOrSpread::Prop(prop) => match prop.as_ref() { + Prop::KeyValue(key_value) => key_value, + _ => stylex_panic!("{}", ILLEGAL_PROP_VALUE), + }, + }; + + match namespace_name_from_prop_key(&key_value.key) { + Some(name) => (name, key_value), + None => stylex_panic!("{}", KEY_HAS_NO_NAME), + } +} + +/// Returns the key-value of a `Prop::KeyValue`; `None` for any other shape. +pub fn prop_as_key_value(prop: &PropOrSpread) -> Option<&KeyValueProp> { + match prop { + PropOrSpread::Prop(p) => match p.as_ref() { + Prop::KeyValue(kv) => Some(kv), + _ => None, + }, + _ => None, + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/mod.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/mod.rs new file mode 100644 index 000000000..9ef5c2727 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/mod.rs @@ -0,0 +1,9 @@ +pub mod convertors; +pub mod factories; +pub mod imports; +pub mod keys; +pub mod objects; +pub mod source_file; + +#[cfg(test)] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/objects.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/objects.rs new file mode 100644 index 000000000..9c341f76f --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/objects.rs @@ -0,0 +1,184 @@ +//! Operations on an object literal's own properties: enumeration order, duplicate keys and +//! spread merging, following JavaScript's rules. + +use std::hash::BuildHasher; + +use indexmap::IndexMap; +use rustc_hash::FxHashMap; +use stylex_utils::number::to_js_string; +use swc_core::atoms::Atom; +use swc_core::ecma::ast::{Prop, PropName, PropOrSpread}; + +use super::convertors::convert_wtf8_to_atom; + +/// The name a property declares; `None` for spreads, accessors, methods and computed keys. +fn prop_key(prop: &PropOrSpread) -> Option { + match prop { + PropOrSpread::Prop(prop) => match prop.as_ref() { + Prop::Shorthand(ident) => Some(ident.sym.clone()), + Prop::KeyValue(key_val) => match &key_val.key { + PropName::Ident(ident) => Some(ident.sym.clone()), + PropName::Str(strng) => Some(convert_wtf8_to_atom(&strng.value)), + // `{ 42: x }` and `{ '42': x }` name one property; spelled via `to_js_string` (`1e+21`). + PropName::Num(number) => Some(Atom::from(to_js_string(number.value))), + // `{ 1n: x }` names the property `"1"`. + PropName::BigInt(big_int) => Some(Atom::from(big_int.value.to_string())), + _ => None, + }, + _ => None, + }, + _ => None, + } +} + +/// The array index a key spells: the canonical decimal form of an integer below 2^32 - 1 +/// (`"0"` but not `"00"`, `"+0"` or `"01"`). +fn array_index_of(key: &str) -> Option { + // Digits only, so `+0` stays a string key (Rust would accept the sign). + if key.is_empty() || !key.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + + if key.len() > 1 && key.starts_with('0') { + return None; + } + + let index = key.parse::().ok()?; + + match index == u32::MAX { + true => None, + false => Some(index), + } +} + +/// Stable-sort key for JavaScript enumeration order: array indices first by value, then the +/// rest in insertion order. +fn own_key_rank(name: Option<&str>) -> (bool, u32) { + match name.and_then(array_index_of) { + Some(index) => (false, index), + None => (true, 0), + } +} + +/// Whether a name is an array index; checked first to skip sorting in the common case. +fn is_index_name(name: Option<&str>) -> bool { + name.and_then(array_index_of).is_some() +} + +/// The array index a number key spells, without allocating its string form. +fn number_array_index(value: f64) -> Option { + let is_index = value >= 0.0 && value < f64::from(u32::MAX) && value.fract() == 0.0; + + is_index.then_some(value as u32) +} + +/// The array index a property's key spells; must agree with `array_index_of(prop_key(..))`. +fn prop_array_index(prop: &PropOrSpread) -> Option { + let PropOrSpread::Prop(prop) = prop else { + return None; + }; + + match prop.as_ref() { + Prop::Shorthand(ident) => array_index_of(&ident.sym), + Prop::KeyValue(key_val) => match &key_val.key { + PropName::Ident(ident) => array_index_of(&ident.sym), + // Same convertor as `prop_key`, so unspellable text is refused the same way. + PropName::Str(strng) => array_index_of(&convert_wtf8_to_atom(&strng.value)), + PropName::Num(number) => number_array_index(number.value), + PropName::BigInt(big_int) => array_index_of(&big_int.value.to_string()), + _ => None, + }, + _ => None, + } +} + +/// Reorders properties into JavaScript enumeration order: array-index keys ascending, then the +/// rest in insertion order. This decides stylesheet order, so it must match upstream. +pub fn order_own_keys(props: Vec) -> Vec { + let mut indexed: Vec<(u32, PropOrSpread)> = Vec::new(); + let mut named: Vec = Vec::with_capacity(props.len()); + + for prop in props { + match prop_array_index(&prop) { + Some(index) => indexed.push((index, prop)), + None => named.push(prop), + } + } + + if indexed.is_empty() { + return named; + } + + indexed.sort_by_key(|(index, _)| *index); + + let mut ordered = Vec::with_capacity(indexed.len() + named.len()); + ordered.extend(indexed.into_iter().map(|(_, prop)| prop)); + ordered.extend(named); + + ordered +} + +/// [`order_own_keys`] for an ordered map; `name_of` returning `None` ranks as a string key. +pub fn order_own_map_keys( + map: &mut IndexMap, + name_of: impl Fn(&K) -> Option<&str>, +) where + S: BuildHasher, +{ + if !map.keys().any(|key| is_index_name(name_of(key))) { + return; + } + + map.sort_by_cached_key(|key, _| own_key_rank(name_of(key))); +} + +/// Keeps the last property under each name, in the position the name first took, and drops +/// unnamed properties (like `Object.entries({ color: 1, opacity: 2, color: 3 })`). +pub fn remove_duplicates(props: Vec) -> Vec { + let mut positions: FxHashMap = FxHashMap::default(); + let mut result: Vec = Vec::with_capacity(props.len()); + + for prop in props { + let Some(key) = prop_key(&prop) else { + continue; + }; + + match positions.get(&key) { + Some(&position) => result[position] = prop, + None => { + positions.insert(key, result.len()); + result.push(prop); + }, + } + } + + result +} + +/// The properties of `{ ...old_props, ...new_props }`: a shallow `Object.assign`, where a +/// repeated key takes the later value at the position it first took. +pub fn assign_props( + old_props: Vec, + new_props: Vec, +) -> Vec { + let mut merged: Vec = Vec::with_capacity(old_props.len() + new_props.len()); + let mut positions: FxHashMap = FxHashMap::default(); + + for prop in old_props.into_iter().chain(new_props) { + let Some(key) = prop_key(&prop) else { + // Unnamed properties keep their place and collide with nothing. + merged.push(prop); + continue; + }; + + match positions.get(&key) { + Some(&position) => merged[position] = prop, + None => { + positions.insert(key, merged.len()); + merged.push(prop); + }, + } + } + + merged +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/source_file.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/source_file.rs new file mode 100644 index 000000000..62f1e4e74 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/source_file.rs @@ -0,0 +1,50 @@ +//! Reading parts of a source file name; virtual files answer with an empty value. + +use stylex_constants::constants::messages::INVALID_UTF8; +use stylex_macros::stylex_panic; +use swc_core::common::FileName; + +/// The file name without its extension, or an empty string for a virtual file. +pub fn extract_filename_from_path(path: &FileName) -> String { + match path { + FileName::Real(path_buf) => { + let stem = match path_buf.file_stem() { + Some(s) => s, + None => stylex_panic!("File path has no file stem component."), + }; + match stem.to_str() { + Some(s) => s.to_string(), + None => stylex_panic!("{}", INVALID_UTF8), + } + }, + _ => String::new(), + } +} + +/// The whole path, or an empty string for a virtual file. +pub fn extract_path(path: &FileName) -> &str { + match path { + FileName::Real(path_buf) => match path_buf.to_str() { + Some(s) => s, + None => stylex_panic!("{}", INVALID_UTF8), + }, + _ => "", + } +} + +/// The file name with its extension, or `None` for a virtual file. +pub fn extract_filename_with_ext_from_path(path: &FileName) -> Option<&str> { + match path { + FileName::Real(path_buf) => { + let name = match path_buf.file_name() { + Some(n) => n, + None => stylex_panic!("File path has no file name component."), + }; + Some(match name.to_str() { + Some(s) => s, + None => stylex_panic!("{}", INVALID_UTF8), + }) + }, + _ => None, + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_invalid_utf8_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_invalid_utf8_test.rs new file mode 100644 index 000000000..63be61dff --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_invalid_utf8_test.rs @@ -0,0 +1,99 @@ +//! Panic-path coverage for convertor helpers that require valid UTF-8. + +use crate::ast::convertors::*; +use swc_core::{atoms::Wtf8Atom, common::DUMMY_SP, ecma::ast::*}; + +fn invalid_wtf8_atom() -> Wtf8Atom { + // Unpaired surrogate: valid WTF-8 storage, invalid UTF-8 decoding. + unsafe { Wtf8Atom::from_bytes_unchecked(&[0xed, 0xa0, 0x80]) } +} + +/// `convert_tpl_to_string_lit` should panic when cooked text is missing. +#[test] +fn convert_tpl_to_string_lit_with_missing_cooked_panics() { + let tpl = Tpl { + span: DUMMY_SP, + exprs: vec![], + quasis: vec![TplElement { + span: DUMMY_SP, + tail: true, + cooked: None, + raw: "".into(), + }], + }; + + let result = std::panic::catch_unwind(|| convert_tpl_to_string_lit(&tpl)); + assert!(result.is_err()); +} + +/// `convert_tpl_to_string_lit` should panic when cooked text is invalid UTF-8. +#[test] +fn convert_tpl_to_string_lit_with_invalid_utf8_panics() { + let tpl = Tpl { + span: DUMMY_SP, + exprs: vec![], + quasis: vec![TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(invalid_wtf8_atom()), + raw: "".into(), + }], + }; + + let result = std::panic::catch_unwind(|| convert_tpl_to_string_lit(&tpl)); + assert!(result.is_err()); +} + +/// WTF-8 conversion should panic when UTF-8 decoding fails. +#[test] +fn convert_wtf8_to_atom_invalid_utf8_panics() { + let atom = invalid_wtf8_atom(); + let result = std::panic::catch_unwind(|| convert_wtf8_to_atom(&atom)); + assert!(result.is_err()); +} + +/// Atom extraction should panic on invalid UTF-8 payloads. +#[test] +fn convert_str_lit_to_atom_invalid_utf8_panics() { + let str_lit = Str { + span: DUMMY_SP, + value: invalid_wtf8_atom(), + raw: None, + }; + let result = std::panic::catch_unwind(|| convert_str_lit_to_atom(&str_lit)); + assert!(result.is_err()); +} + +/// Cooked template extraction should panic when bytes are invalid UTF-8. +#[test] +fn extract_tpl_cooked_value_invalid_utf8_panics() { + let elem = TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(invalid_wtf8_atom()), + raw: "".into(), + }; + let result = std::panic::catch_unwind(|| extract_tpl_cooked_value(&elem)); + assert!(result.is_err()); +} + +/// Cooked template extraction should panic when no cooked value exists. +#[test] +fn extract_tpl_cooked_value_missing_cooked_panics() { + let elem = TplElement { + span: DUMMY_SP, + tail: true, + cooked: None, + raw: "".into(), + }; + let result = std::panic::catch_unwind(|| extract_tpl_cooked_value(&elem)); + assert!(result.is_err()); +} + +/// Borrowed string extraction should panic on invalid UTF-8 payloads. +#[test] +fn convert_atom_to_str_ref_invalid_utf8_panics() { + let atom = invalid_wtf8_atom(); + let result = std::panic::catch_unwind(|| convert_atom_to_str_ref(&atom)); + assert!(result.is_err()); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_key_value_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_key_value_test.rs new file mode 100644 index 000000000..5683fbded --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_key_value_test.rs @@ -0,0 +1,142 @@ +//! Tests for `key_value_name`, `convert_key_value_to_str` and +//! `get_key_values_from_object`. + +use crate::ast::{ + convertors::{convert_key_value_to_str, get_key_values_from_object, key_value_name}, + factories::{create_ident, create_object_lit, create_string_lit}, +}; +use std::borrow::Cow; + +use swc_core::{ + atoms::Atom, + common::DUMMY_SP, + ecma::ast::{ + BigInt, BigIntValue, ComputedPropName, Expr, Function, IdentName, KeyValueProp, Number, Prop, + PropName, PropOrSpread, SpreadElement, Tpl, TplElement, + }, +}; + +fn key_value_with_name(key: PropName) -> KeyValueProp { + KeyValueProp { + key, + value: Box::new(Expr::Lit(create_string_lit("ignored"))), + } +} + +// ---------- convert_key_value_to_str ---------- + +#[test] +fn key_computed_simple_template_literal() { + let tpl = Tpl { + span: DUMMY_SP, + exprs: vec![], + quasis: vec![TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(Atom::new("tpl-key").into()), + raw: Atom::new("tpl-key"), + }], + }; + let kv = key_value_with_name(PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Tpl(tpl)), + })); + assert_eq!(convert_key_value_to_str(&kv), "tpl-key"); +} + +#[test] +#[should_panic(expected = "Computed property key must be a string or number literal")] +fn key_computed_non_string_literal_panics() { + let kv = key_value_with_name(PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(swc_core::ecma::ast::Lit::Null( + swc_core::ecma::ast::Null { span: DUMMY_SP }, + ))), + })); + let _ = convert_key_value_to_str(&kv); +} + +#[test] +#[should_panic(expected = "Computed key is not a literal")] +fn key_computed_complex_template_panics() { + let tpl = Tpl { + span: DUMMY_SP, + exprs: vec![Box::new(Expr::Ident(create_ident("x")))], + quasis: vec![ + TplElement { + span: DUMMY_SP, + tail: false, + cooked: Some(Atom::new("a").into()), + raw: Atom::new("a"), + }, + TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(Atom::new("b").into()), + raw: Atom::new("b"), + }, + ], + }; + let kv = key_value_with_name(PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Tpl(tpl)), + })); + let _ = convert_key_value_to_str(&kv); +} + +// ---------- get_key_values_from_object ---------- + +#[test] +#[should_panic(expected = "spread operator")] +fn spread_prop_panics() { + let obj = create_object_lit(vec![PropOrSpread::Spread(SpreadElement { + dot3_token: DUMMY_SP, + expr: Box::new(Expr::Ident(create_ident("rest"))), + })]); + let _ = get_key_values_from_object(&obj); +} + +#[test] +#[should_panic(expected = "style value")] +fn non_key_value_prop_panics() { + // A getter is neither key-value nor shorthand. + let getter = Prop::Getter(swc_core::ecma::ast::GetterProp { + span: DUMMY_SP, + key: PropName::Ident(IdentName { + span: DUMMY_SP, + sym: Atom::new("g"), + }), + function: Box::new(Function::default()), + }); + let obj = create_object_lit(vec![PropOrSpread::Prop(Box::new(getter))]); + let _ = get_key_values_from_object(&obj); +} + +// ---------- key_value_name ---------- + +/// Names the key doesn't hold are owned. +#[test] +fn a_key_that_spells_its_name_is_owned() { + let shapes = [ + PropName::Num(Number { + span: DUMMY_SP, + value: 42.0, + raw: None, + }), + PropName::BigInt(BigInt { + span: DUMMY_SP, + value: Box::new(BigIntValue::from(100u32)), + raw: None, + }), + PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(create_string_lit("computed"))), + }), + ]; + + for shape in shapes { + let kv = key_value_with_name(shape); + + assert!(matches!(key_value_name(&kv), Cow::Owned(_))); + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_test.rs new file mode 100644 index 000000000..ee06029ad --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/convertors_test.rs @@ -0,0 +1,169 @@ +//! Tests for AST convertor functions that transform between node types. + +use crate::ast::{convertors::*, factories::*}; +use swc_core::{ + atoms::{Wtf8Atom, wtf8::Wtf8Buf}, + common::DUMMY_SP, + ecma::ast::*, +}; + +/// The error names the literal kind it could not read. +#[test] +fn convert_lit_to_number_names_the_literal_kind_it_refused() { + let cases = [ + ( + Lit::Null(swc_core::ecma::ast::Null { span: DUMMY_SP }), + "NullLiteral", + ), + ( + Lit::Regex(Regex { + span: DUMMY_SP, + exp: "a".into(), + flags: "".into(), + }), + "RegExpLiteral", + ), + ( + Lit::JSXText(JSXText { + span: DUMMY_SP, + value: "t".into(), + raw: "t".into(), + }), + "JSXText", + ), + ]; + + for (lit, kind) in cases { + match convert_lit_to_number(&lit) { + Ok(number) => panic!("expected {:?} to refuse, got {}", lit, number), + Err(error) => assert_eq!( + error.to_string(), + format!("Value is not a number: {}", kind), + "wrong label for {:?}", + lit + ), + } + } +} + +/// Half of an astral character answers U+FFFD, from both well-formed and lone-surrogate text; +/// `None` means only past the end. +#[test] +fn atom_utf16_char_at_replaces_half_of_a_character() { + let paired = Wtf8Atom::from("\u{1F600}a"); + + assert_eq!( + atom_utf16_char_at(&paired, 0), + Some(char::REPLACEMENT_CHARACTER) + ); + assert_eq!( + atom_utf16_char_at(&paired, 1), + Some(char::REPLACEMENT_CHARACTER) + ); + assert_eq!(atom_utf16_char_at(&paired, 2), Some('a')); + + let lone = Wtf8Atom::from(Wtf8Buf::from_ill_formed_utf16(&[0x0061, 0xD83D, 0x0062])); + + assert!( + lone.as_str().is_none(), + "expected an atom with no UTF-8 form to test against" + ); + assert_eq!(atom_utf16_char_at(&lone, 0), Some('a')); + assert_eq!( + atom_utf16_char_at(&lone, 1), + Some(char::REPLACEMENT_CHARACTER) + ); + assert_eq!(atom_utf16_char_at(&lone, 2), Some('b')); + assert_eq!(atom_utf16_char_at(&lone, 3), None); +} + +#[test] +fn extract_tpl_cooked_value_valid() { + let elem = TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some("cooked".into()), + raw: "cooked".into(), + }; + assert_eq!(extract_tpl_cooked_value(&elem), "cooked"); +} + +#[test] +fn convert_lit_to_string_bigint() { + let big = BigInt { + span: DUMMY_SP, + value: Box::new(99i64.into()), + raw: None, + }; + let lit = Lit::BigInt(big); + assert_eq!(convert_lit_to_string(&lit), Some("99".to_string())); +} + +#[test] +fn extract_str_lit_ref_num_returns_none() { + let lit = create_number_lit(1.0); + assert!(extract_str_lit_ref(&lit).is_none()); +} + +#[test] +fn expand_shorthand_prop_converts() { + let ident = Ident::from("x"); + let mut prop = Box::new(Prop::Shorthand(ident)); + expand_shorthand_prop(&mut prop); + assert!(matches!(*prop, Prop::KeyValue(_))); +} + +fn computed_bigint_member_prop(value: i64) -> MemberProp { + create_computed_member_prop(Expr::Lit(Lit::BigInt(BigInt { + span: DUMMY_SP, + value: Box::new(value.into()), + raw: None, + }))) +} + +#[test] +fn convert_member_prop_to_string_bigint() { + // A computed BigInt key (`obj[123n]`) renders as its decimal digits. + assert_eq!( + convert_member_prop_to_string(&computed_bigint_member_prop(123)).as_deref(), + Some("123") + ); + assert_eq!( + convert_member_prop_to_string(&computed_bigint_member_prop(-45)).as_deref(), + Some("-45") + ); +} + +#[test] +fn normalize_expr_handles_deeply_nested_parens_without_recursion() { + let mut expr = create_string_expr("before"); + for _ in 0..16_384 { + expr = Expr::Paren(ParenExpr { + span: DUMMY_SP, + expr: Box::new(expr), + }); + } + + assert!(matches!(normalize_expr(&expr), Expr::Lit(Lit::Str(_)))); + *normalize_expr_mut(&mut expr) = create_string_expr("after"); + + // Drop iteratively so cleanup does not recurse. + while let Expr::Paren(paren) = expr { + expr = *paren.expr; + } + assert!(matches!(expr, Expr::Lit(Lit::Str(_)))); +} + +#[test] +#[should_panic(expected = "Variable declaration must be initialized")] +fn get_expr_from_var_decl_panics_without_initializer() { + let decl = VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(create_binding_ident(create_ident("a"))), + init: None, + definite: false, + }; + get_expr_from_var_decl(&decl); +} + +// is_js_undefined diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/factories_error_paths_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/factories_error_paths_test.rs new file mode 100644 index 000000000..6ca1b644b --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/factories_error_paths_test.rs @@ -0,0 +1,10 @@ +//! Error-path tests for AST factory helpers. + +use crate::ast::factories::create_boolean_prop; + +/// `create_boolean_prop` should panic when required boolean value is missing. +#[test] +fn create_boolean_prop_none_panics() { + let result = std::panic::catch_unwind(|| create_boolean_prop("enabled", None)); + assert!(result.is_err()); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/factories_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/factories_test.rs new file mode 100644 index 000000000..3e5f4f09a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/factories_test.rs @@ -0,0 +1,61 @@ +//! Tests for AST factory functions that create SWC expression nodes. + +use crate::ast::{ + convertors::{create_number_expr, create_string_expr}, + factories::*, +}; +use swc_core::ecma::ast::*; + +#[test] +fn wrap_in_paren_ref_clones_and_wraps() { + let expr = create_number_expr(5.0); + let wrapped = wrap_in_paren_ref(&expr); + assert!(matches!(wrapped, Expr::Paren(_))); +} + +#[test] +fn create_string_array_prop_creates_array() { + let prop = create_string_array_prop("values", &["a", "b"]); + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = *p { + assert!(matches!(*kv.value, Expr::Array(_))); + } else { + panic!("Expected KeyValue"); + } + } else { + panic!("Expected Prop"); + } +} + +#[test] +fn create_boolean_prop_some() { + let prop = create_boolean_prop("enabled", Some(true)); + assert!(matches!(prop, PropOrSpread::Prop(_))); +} + +#[test] +fn create_number_expr_or_spread_creates() { + let eos = create_number_expr_or_spread(3.0); + assert!(eos.spread.is_none()); +} + +#[test] +fn create_array_and_spreaded_array() { + let exprs = vec![create_string_expr("a"), create_string_expr("b")]; + let arr = create_array(&exprs); + assert_eq!(arr.elems.len(), 2); + assert!(arr.elems[0].as_ref().unwrap().spread.is_none()); + + let spreaded = create_spreaded_array(&exprs); + assert_eq!(spreaded.elems.len(), 2); + assert!(spreaded.elems[0].as_ref().unwrap().spread.is_some()); +} + +#[test] +fn create_null_var_declarator_works() { + let id = create_ident("y"); + let decl = create_null_var_declarator(id); + if let Some(init) = decl.init.as_ref() { + assert!(matches!(init.as_ref(), Expr::Lit(Lit::Null(_)))); + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/keys_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/keys_test.rs new file mode 100644 index 000000000..1b73dfcc3 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/keys_test.rs @@ -0,0 +1,202 @@ +//! Tests for the object and member key readers. + +use crate::ast::keys::{ + named_key_value, namespace_name_from_member_prop, namespace_name_from_prop_key, + prop_as_key_value, try_namespace_name_from_prop_key, +}; +use swc_core::{ + atoms::Atom, + common::DUMMY_SP, + ecma::ast::{ + BigInt, BigIntValue, ComputedPropName, Expr, Function, GetterProp, Ident, IdentName, + KeyValueProp, Lit, MemberProp, MethodProp, Number, PrivateName, Prop, PropName, PropOrSpread, + SpreadElement, Str, Tpl, TplElement, + }, +}; + +fn ident_name(name: &str) -> IdentName { + IdentName { + span: DUMMY_SP, + sym: Atom::new(name), + } +} + +fn string_lit(value: &str) -> Str { + Str { + span: DUMMY_SP, + value: Atom::new(value).into(), + raw: None, + } +} + +fn number_lit(value: f64) -> Number { + Number { + span: DUMMY_SP, + value, + raw: None, + } +} + +fn big_int_lit(value: i64) -> BigInt { + BigInt { + span: DUMMY_SP, + value: Box::new(BigIntValue::from(value)), + raw: None, + } +} + +fn computed(expr: Expr) -> ComputedPropName { + ComputedPropName { + span: DUMMY_SP, + expr: Box::new(expr), + } +} + +/// A template literal with no expressions. +fn static_tpl(text: &str) -> Tpl { + Tpl { + span: DUMMY_SP, + exprs: vec![], + quasis: vec![TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(Atom::new(text).into()), + raw: Atom::new(text), + }], + } +} + +/// A template literal with an interpolation. +fn dynamic_tpl() -> Tpl { + Tpl { + span: DUMMY_SP, + exprs: vec![Box::new(Expr::Ident(Ident::new_no_ctxt( + Atom::new("value"), + DUMMY_SP, + )))], + quasis: vec![ + TplElement { + span: DUMMY_SP, + tail: false, + cooked: Some(Atom::new("a").into()), + raw: Atom::new("a"), + }, + TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(Atom::new("b").into()), + raw: Atom::new("b"), + }, + ], + } +} + +fn key_value_prop(key: PropName, value: Expr) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key, + value: Box::new(value), + }))) +} + +// ---------- namespace_name_from_prop_key ---------- + +// ---------- try_namespace_name_from_prop_key ---------- + +/// Both readers agree on every shape with a readable name. +#[test] +fn both_readers_agree_on_every_readable_shape() { + let keys = [ + PropName::Ident(ident_name("root")), + PropName::Str(string_lit("a key")), + PropName::Num(number_lit(1e21)), + PropName::BigInt(big_int_lit(12)), + PropName::Computed(computed(Expr::Lit(Lit::Str(string_lit("root"))))), + PropName::Computed(computed(Expr::Lit(Lit::Num(number_lit(7.0))))), + PropName::Computed(computed(Expr::Lit(Lit::BigInt(big_int_lit(12))))), + PropName::Computed(computed(Expr::Tpl(static_tpl("root")))), + PropName::Computed(computed(Expr::Tpl(dynamic_tpl()))), + PropName::Computed(computed(Expr::Lit(Lit::Null(swc_core::ecma::ast::Null { + span: DUMMY_SP, + })))), + PropName::Computed(computed(Expr::Ident(Ident::new_no_ctxt( + Atom::new("name"), + DUMMY_SP, + )))), + ]; + + for key in keys { + assert_eq!( + try_namespace_name_from_prop_key(&key), + namespace_name_from_prop_key(&key), + "the two readers disagree on {key:?}" + ); + } +} + +// ---------- namespace_name_from_member_prop ---------- + +#[test] +fn refuses_a_private_member() { + let prop = MemberProp::PrivateName(PrivateName { + span: DUMMY_SP, + name: Atom::new("secret"), + }); + + assert_eq!(namespace_name_from_member_prop(&prop), None); +} + +// ---------- prop_as_key_value ---------- + +#[test] +fn refuses_a_getter() { + let prop = PropOrSpread::Prop(Box::new(Prop::Getter(GetterProp { + span: DUMMY_SP, + key: PropName::Ident(ident_name("root")), + function: Box::new(Function::default()), + }))); + + assert!(prop_as_key_value(&prop).is_none()); +} + +// ---------- named_key_value ---------- + +#[test] +#[should_panic(expected = "The spread operator (...) is not supported in this context.")] +fn refuses_a_spread_it_cannot_name() { + let prop = PropOrSpread::Spread(SpreadElement { + dot3_token: DUMMY_SP, + expr: Box::new(Expr::Ident(Ident::new_no_ctxt(Atom::new("rest"), DUMMY_SP))), + }); + + named_key_value(&prop); +} + +#[test] +#[should_panic(expected = "A style value can only contain an array, string or number.")] +fn refuses_a_property_that_carries_no_value() { + let prop = PropOrSpread::Prop(Box::new(Prop::Method(MethodProp { + key: PropName::Ident(ident_name("root")), + function: Box::default(), + }))); + + named_key_value(&prop); +} + +#[test] +#[should_panic(expected = "The key has no name at compile time.")] +fn refuses_a_key_with_no_name() { + let prop = key_value_prop( + PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Ident(Ident::new_no_ctxt( + Atom::new("runtime"), + DUMMY_SP, + ))), + }), + Expr::Lit(Lit::Num(number_lit(1.0))), + ); + + named_key_value(&prop); +} + +// ---------- collect_object_lit_keys ---------- diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/mod.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/mod.rs new file mode 100644 index 000000000..e5be4020f --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/mod.rs @@ -0,0 +1,8 @@ +mod convertors_invalid_utf8_test; +mod convertors_key_value_test; +mod convertors_test; +mod factories_error_paths_test; +mod factories_test; +mod keys_test; +mod objects_test; +mod source_file_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/objects_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/objects_test.rs new file mode 100644 index 000000000..b42792cc3 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/objects_test.rs @@ -0,0 +1,236 @@ +use swc_core::{ + common::DUMMY_SP, + ecma::ast::{ + BigInt, ComputedPropName, Expr, Function, GetterProp, IdentName, KeyValueProp, Number, + ObjectLit, Prop, PropName, PropOrSpread, SpreadElement, Str, + }, +}; + +use crate::ast::convertors::{convert_wtf8_to_atom, create_number_expr}; +use crate::ast::factories::create_ident; +use crate::ast::objects::{assign_props, order_own_keys, remove_duplicates}; + +fn make_kv_prop(key: &str, val: f64) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName { + span: DUMMY_SP, + sym: key.into(), + }), + value: Box::new(create_number_expr(val)), + }))) +} + +fn make_kv_str_key_prop(key: &str, val: f64) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Str(Str { + span: DUMMY_SP, + value: key.into(), + raw: None, + }), + value: Box::new(create_number_expr(val)), + }))) +} + +fn make_shorthand_prop(name: &str) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::Shorthand(create_ident(name)))) +} + +fn make_spread_prop() -> PropOrSpread { + PropOrSpread::Spread(SpreadElement { + dot3_token: DUMMY_SP, + expr: Box::new(create_number_expr(1.0)), + }) +} + +fn make_kv_num_key_obj_prop(key: f64, inner: Vec) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Num(Number { + span: DUMMY_SP, + value: key, + raw: None, + }), + value: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: inner, + })), + }))) +} + +fn make_bigint_obj_prop(val: u32, inner: Vec) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::BigInt(BigInt { + span: DUMMY_SP, + value: Box::new(val.into()), + raw: None, + }), + value: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: inner, + })), + }))) +} + +fn make_kv_num_key_prop(key: f64, val: f64) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Num(Number { + span: DUMMY_SP, + value: key, + raw: None, + }), + value: Box::new(create_number_expr(val)), + }))) +} + +fn make_getter_prop(key: &str) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::Getter(GetterProp { + span: DUMMY_SP, + key: PropName::Ident(IdentName { + span: DUMMY_SP, + sym: key.into(), + }), + function: Box::new(Function::default()), + }))) +} + +/// A computed-key property `{ [key]: val }`; readers never name it. +fn make_computed_key_prop(key: f64, val: f64) -> PropOrSpread { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(create_number_expr(key)), + }), + value: Box::new(create_number_expr(val)), + }))) +} + +fn keys_of(props: &[PropOrSpread]) -> Vec { + props + .iter() + .map(|prop| match prop { + PropOrSpread::Prop(prop) => match prop.as_ref() { + Prop::KeyValue(kv) => match &kv.key { + PropName::Ident(ident) => ident.sym.to_string(), + PropName::Str(strng) => convert_wtf8_to_atom(&strng.value).to_string(), + PropName::Num(num) => num.value.to_string(), + PropName::BigInt(big_int) => big_int.value.to_string(), + PropName::Computed(_) => String::from(""), + }, + Prop::Shorthand(ident) => ident.sym.to_string(), + _ => String::from(""), + }, + PropOrSpread::Spread(_) => String::from(""), + }) + .collect() +} + +mod remove_duplicates_tests { + use super::*; + + #[test] + fn a_shorthand_and_a_key_value_under_one_name_are_one_property() { + let props = vec![make_shorthand_prop("x"), make_kv_prop("x", 5.0)]; + + assert_eq!(keys_of(&remove_duplicates(props)), vec!["x"]); + } + + // `['a']` would name `a` in JS, but unevaluated it must not collide with `a`. + #[test] + fn a_computed_key_does_not_collide_with_the_name_it_would_spell() { + let props = vec![ + make_kv_num_key_prop(0.0, 1.0), + make_computed_key_prop(0.0, 2.0), + ]; + + assert_eq!(keys_of(&remove_duplicates(props)), vec!["0"]); + } +} + +mod assign_props_tests { + use super::*; + + #[test] + fn a_repeated_big_integer_key_is_one_key() { + let old = vec![make_bigint_obj_prop(42, vec![make_kv_prop("x", 1.0)])]; + let new = vec![make_bigint_obj_prop(42, vec![make_kv_prop("y", 2.0)])]; + + // `{ 42n: x }` names `"42"`. + assert_eq!(keys_of(&assign_props(old, new)), vec!["42"]); + } + + // Unnamed properties collide with nothing and keep their place. + + #[test] + fn a_merge_of_nothing_readable_keeps_every_property() { + let old = vec![make_computed_key_prop(0.0, 1.0), make_spread_prop()]; + let new = vec![make_getter_prop("val"), make_computed_key_prop(1.0, 2.0)]; + + assert_eq!( + keys_of(&assign_props(old, new)), + vec!["", "", "", ""] + ); + } +} + +mod order_own_keys_tests { + use super::*; + + #[test] + fn reads_a_numeric_key_as_the_index_it_spells() { + let props = vec![ + make_kv_prop("color", 1.0), + make_kv_num_key_obj_prop(3.0, vec![]), + make_kv_str_key_prop("1", 2.0), + ]; + + assert_eq!(keys_of(&order_own_keys(props)), vec!["1", "3", "color"]); + } + + #[test] + fn a_property_with_no_readable_key_keeps_its_place_among_the_string_keys() { + let props = vec![ + make_kv_prop("a", 1.0), + make_spread_prop(), + make_kv_str_key_prop("0", 2.0), + ]; + + assert_eq!(keys_of(&order_own_keys(props)), vec!["0", "a", ""]); + } + + #[test] + fn a_computed_key_reads_as_no_key_at_all() { + let props = vec![ + make_computed_key_prop(0.0, 1.0), + make_kv_str_key_prop("0", 2.0), + ]; + + assert_eq!(keys_of(&order_own_keys(props)), vec!["0", ""]); + } + + // Even when named `0`. + #[test] + fn a_getter_named_by_digits_is_not_an_index() { + let props = vec![make_getter_prop("0"), make_kv_str_key_prop("1", 1.0)]; + + assert_eq!(keys_of(&order_own_keys(props)), vec!["1", ""]); + } + + #[test] + fn a_big_integer_key_orders_as_the_index_it_spells() { + let props = vec![ + make_kv_prop("color", 1.0), + make_bigint_obj_prop(3, vec![]), + make_kv_str_key_prop("1", 3.0), + ]; + + assert_eq!(keys_of(&order_own_keys(props)), vec!["1", "3", "color"]); + } + + #[test] + fn a_shorthand_property_named_by_digits_is_an_index() { + let props = vec![make_kv_prop("a", 1.0), make_shorthand_prop("0")]; + + assert_eq!(keys_of(&order_own_keys(props)), vec!["0", "a"]); + } +} + +mod order_own_map_keys_tests {} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/source_file_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/source_file_test.rs new file mode 100644 index 000000000..7fb56740c --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/ast/tests/source_file_test.rs @@ -0,0 +1,352 @@ +//! Tests for reading the parts of a source file's name. + +use std::{any::Any, path::PathBuf}; + +use stylex_constants::constants::messages::INVALID_UTF8; +use swc_core::common::FileName; + +use crate::ast::source_file::{ + extract_filename_from_path, extract_filename_with_ext_from_path, extract_path, +}; + +/// A real path on disk, which is the only kind that has a name to read. +fn real(path: &str) -> FileName { + FileName::Real(PathBuf::from(path)) +} + +/// Fails unless the call panicked with the given message. +fn assert_panic_message(result: Result>, expected: &str) { + let payload = match result { + Ok(_) => panic!("expected a panic carrying {expected:?}, but the call returned"), + Err(payload) => payload, + }; + let message = match payload.downcast_ref::() { + Some(message) => message.clone(), + None => panic!("panic payload was not a string"), + }; + assert!( + message.contains(expected), + "panic message {message:?} does not contain {expected:?}" + ); +} + +// extract_filename_from_path + +mod extract_filename_from_path_tests { + use super::*; + + #[test] + fn returns_stem_for_simple_js_file() { + let path = FileName::Real(PathBuf::from("/path/to/file.js")); + assert_eq!(extract_filename_from_path(&path), "file"); + } + + #[test] + fn returns_stem_for_dotted_extension() { + let path = FileName::Real(PathBuf::from("/path/to/file.stylex.ts")); + assert_eq!(extract_filename_from_path(&path), "file.stylex"); + } + + #[test] + fn returns_stem_for_flat_path() { + let path = FileName::Real(PathBuf::from("simple.js")); + assert_eq!(extract_filename_from_path(&path), "simple"); + } + + #[test] + fn returns_empty_string_for_anon() { + let path = FileName::Anon; + assert_eq!(extract_filename_from_path(&path), ""); + } + + #[test] + fn returns_stem_for_tsx_file() { + let path = FileName::Real(PathBuf::from("/src/components/Button.tsx")); + assert_eq!(extract_filename_from_path(&path), "Button"); + } + + #[test] + fn returns_stem_for_no_extension() { + let path = FileName::Real(PathBuf::from("/path/to/Makefile")); + assert_eq!(extract_filename_from_path(&path), "Makefile"); + } +} + +// extract_path + +mod extract_path_tests { + use super::*; + + #[test] + fn returns_full_path_for_real_file() { + let path = FileName::Real(PathBuf::from("/path/to/file.js")); + assert_eq!(extract_path(&path), "/path/to/file.js"); + } + + #[test] + fn returns_empty_for_anon() { + let path = FileName::Anon; + assert_eq!(extract_path(&path), ""); + } + + #[test] + fn returns_relative_path() { + let path = FileName::Real(PathBuf::from("relative/file.ts")); + assert_eq!(extract_path(&path), "relative/file.ts"); + } +} + +// extract_filename_with_ext_from_path + +mod extract_filename_with_ext_from_path_tests { + use super::*; + + #[test] + fn returns_filename_with_ext_for_js() { + let path = FileName::Real(PathBuf::from("/path/to/file.js")); + assert_eq!(extract_filename_with_ext_from_path(&path), Some("file.js")); + } + + #[test] + fn returns_filename_with_double_ext() { + let path = FileName::Real(PathBuf::from("/path/to/file.stylex.ts")); + assert_eq!( + extract_filename_with_ext_from_path(&path), + Some("file.stylex.ts") + ); + } + + #[test] + fn returns_none_for_anon() { + let path = FileName::Anon; + assert_eq!(extract_filename_with_ext_from_path(&path), None); + } + + #[test] + fn returns_filename_without_directory() { + let path = FileName::Real(PathBuf::from("standalone.css")); + assert_eq!( + extract_filename_with_ext_from_path(&path), + Some("standalone.css") + ); + } + + #[test] + fn returns_filename_without_extension() { + let path = FileName::Real(PathBuf::from("/path/Makefile")); + assert_eq!(extract_filename_with_ext_from_path(&path), Some("Makefile")); + } +} + +// Virtual files + +/// Virtual files give back an empty value rather than an error. +mod virtual_file_tests { + use super::*; + + fn virtual_names() -> Vec { + vec![ + FileName::Anon, + FileName::Macros("stylex".to_string()), + FileName::QuoteExpansion, + FileName::MacroExpansion, + FileName::ProcMacroSourceCode, + FileName::Internal("generated".to_string()), + FileName::Custom("".to_string()), + FileName::Custom(String::new()), + ] + } + + #[test] + fn every_virtual_file_has_no_stem() { + for name in virtual_names() { + assert_eq!(extract_filename_from_path(&name), "", "{name:?}"); + } + } + + #[test] + fn every_virtual_file_has_no_path() { + for name in virtual_names() { + assert_eq!(extract_path(&name), "", "{name:?}"); + } + } + + #[test] + fn every_virtual_file_has_no_file_name() { + for name in virtual_names() { + assert_eq!(extract_filename_with_ext_from_path(&name), None, "{name:?}"); + } + } +} + +// Edge-case real paths + +mod unusual_path_tests { + use super::*; + + /// A dot-file has no extension to strip, so the whole name is the stem. + #[test] + fn reads_a_dot_file_as_its_own_stem() { + assert_eq!( + extract_filename_from_path(&real("/src/.babelrc")), + ".babelrc" + ); + assert_eq!( + extract_filename_with_ext_from_path(&real("/src/.babelrc")), + Some(".babelrc") + ); + } + + /// A trailing slash still reads the last component as the file name. + #[test] + fn reads_through_a_trailing_slash() { + assert_eq!( + extract_filename_from_path(&real("/src/components/")), + "components" + ); + assert_eq!( + extract_filename_with_ext_from_path(&real("/src/components/")), + Some("components") + ); + } + + #[test] + fn reads_names_that_are_not_plain_ascii() { + for (path, stem, name) in [ + ("/src/Ünïcödé.js", "Ünïcödé", "Ünïcödé.js"), + ("/src/🎨.tsx", "🎨", "🎨.tsx"), + ("/src/日本語.stylex.ts", "日本語.stylex", "日本語.stylex.ts"), + ("/src/with space.js", "with space", "with space.js"), + ("/src/tab\tname.js", "tab\tname", "tab\tname.js"), + ] { + assert_eq!(extract_filename_from_path(&real(path)), stem, "{path}"); + assert_eq!( + extract_filename_with_ext_from_path(&real(path)), + Some(name), + "{path}" + ); + assert_eq!(extract_path(&real(path)), path, "{path}"); + } + } + + #[test] + fn reads_an_extremely_long_and_deep_path() { + let deep = "/".to_string() + &"segment/".repeat(10_000) + "file.stylex.js"; + let path = real(&deep); + + assert_eq!(extract_filename_from_path(&path), "file.stylex"); + assert_eq!( + extract_filename_with_ext_from_path(&path), + Some("file.stylex.js") + ); + assert_eq!(extract_path(&path), deep); + } + + #[test] + fn reads_a_single_component_with_no_directory() { + assert_eq!(extract_filename_from_path(&real("file.js")), "file"); + assert_eq!(extract_path(&real("file.js")), "file.js"); + } +} + +// Paths with no name to read + +/// Real paths naming no file panic rather than produce a wrong class name. +mod nameless_path_tests { + use super::*; + + fn nameless_paths() -> Vec<&'static str> { + vec!["/", "..", "", "/..", "../..", "./.."] + } + + #[test] + fn stem_of_a_nameless_path_panics() { + for path in nameless_paths() { + let name = real(path); + let result = std::panic::catch_unwind(|| extract_filename_from_path(&name)); + assert_panic_message(result, "File path has no file stem component."); + } + } + + #[test] + fn file_name_of_a_nameless_path_panics() { + for path in nameless_paths() { + let name = real(path); + let result = + std::panic::catch_unwind(|| extract_filename_with_ext_from_path(&name).map(str::to_string)); + assert_panic_message(result, "File path has no file name component."); + } + } + + /// Reading the whole path still works. + #[test] + fn whole_path_of_a_nameless_path_is_readable() { + for path in nameless_paths() { + assert_eq!(extract_path(&real(path)), path); + } + } +} + +// Paths that are not valid UTF-8 + +/// Non-UTF-8 names panic, since everything downstream is UTF-8. The invalid name is built per +/// platform so the tests run everywhere. +mod invalid_utf8_path_tests { + use super::*; + + /// One unit no UTF-8 decoder accepts: `0xff` on Unix, an unpaired surrogate on Windows. + #[cfg(unix)] + fn invalid_utf8_path(prefix: &str, suffix: &str) -> PathBuf { + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; + + let mut bytes = prefix.as_bytes().to_vec(); + bytes.push(0xff); + bytes.extend_from_slice(suffix.as_bytes()); + PathBuf::from(OsString::from_vec(bytes)) + } + + #[cfg(windows)] + fn invalid_utf8_path(prefix: &str, suffix: &str) -> PathBuf { + use std::{ffi::OsString, os::windows::ffi::OsStringExt}; + + let mut units: Vec = prefix.encode_utf16().collect(); + units.push(0xd800); + units.extend(suffix.encode_utf16()); + PathBuf::from(OsString::from_wide(&units)) + } + + #[test] + fn stem_of_an_invalid_utf8_name_panics() { + let name = FileName::Real(invalid_utf8_path("/src/", ".js")); + let result = std::panic::catch_unwind(|| extract_filename_from_path(&name)); + assert_panic_message(result, INVALID_UTF8); + } + + #[test] + fn whole_path_of_an_invalid_utf8_name_panics() { + let name = FileName::Real(invalid_utf8_path("/src/file", ".js")); + let result = std::panic::catch_unwind(|| extract_path(&name).to_string()); + assert_panic_message(result, INVALID_UTF8); + } + + #[test] + fn file_name_of_an_invalid_utf8_name_panics() { + let name = FileName::Real(invalid_utf8_path("/src/file", ".js")); + let result = + std::panic::catch_unwind(|| extract_filename_with_ext_from_path(&name).map(str::to_string)); + assert_panic_message(result, INVALID_UTF8); + } + + /// Only the whole-path reader fails on an invalid directory name. + #[test] + fn invalid_bytes_in_a_directory_leave_the_file_name_readable() { + let path = FileName::Real(invalid_utf8_path("/src/", "/file.js")); + + assert_eq!(extract_filename_from_path(&path), "file"); + assert_eq!(extract_filename_with_ext_from_path(&path), Some("file.js")); + assert_panic_message( + std::panic::catch_unwind(|| extract_path(&path).to_string()), + INVALID_UTF8, + ); + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/lib.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/lib.rs new file mode 100644 index 000000000..80e7753dd --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/lib.rs @@ -0,0 +1,4 @@ +pub mod ast; + +#[cfg(test)] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/tests.rs b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/tests.rs new file mode 100644 index 000000000..857c3cfee --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-ast/src/tests.rs @@ -0,0 +1,6 @@ +use ctor::ctor; + +#[ctor(unsafe)] +fn init_logger() { + pretty_env_logger::formatted_builder().try_init().ok(); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-atoms/Cargo.toml b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/Cargo.toml new file mode 100644 index 000000000..307140528 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/Cargo.toml @@ -0,0 +1,32 @@ +[package] +description = "StyleX atoms for the StyleX SWC plugin" +edition.workspace = true +license.workspace = true +name = "stylex_atoms" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + + +[lib] +crate-type = ["rlib"] +doctest = false + +[dependencies] +rustc-hash.workspace = true +swc_core = { workspace = true, features = [ + "common", + "ecma_ast", + "ecma_utils", + "ecma_visit", +] } + +stylex_ast = { path = "../stylex-ast" } +stylex_constants = { path = "../stylex-constants" } + +[dev-dependencies] +ctor.workspace = true +pretty_env_logger.workspace = true + +[lints] +workspace = true diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/lib.rs b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/lib.rs new file mode 100644 index 000000000..122cf60f8 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/lib.rs @@ -0,0 +1,9 @@ +//! StyleX atoms: compiles the inline `x.display.flex` / `x.color(value)` syntax inside +//! `stylex.props(...)`. Style compilation is injected by the consumer through [`Compile`]. +//! +//! [`Compile`]: crate::transform::Compile + +pub mod transform; + +#[cfg(test)] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/tests.rs b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/tests.rs new file mode 100644 index 000000000..9cd8aec9a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/tests.rs @@ -0,0 +1,9 @@ +use ctor::ctor; + +#[ctor(unsafe)] +fn init_logger() { + pretty_env_logger::formatted_builder().try_init().ok(); +} + +#[path = "tests/transform_test.rs"] +mod transform_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/tests/transform_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/tests/transform_test.rs new file mode 100644 index 000000000..ceb44461e --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/tests/transform_test.rs @@ -0,0 +1,441 @@ +use rustc_hash::FxHashMap; +use swc_core::{ + common::{DUMMY_SP, SyntaxContext}, + ecma::{ + ast::{ + CallExpr, Callee, ComputedPropName, Expr, ExprOrSpread, Id, Ident, IdentName, Lit, + MemberExpr, MemberProp, ObjectLit, Str, Super, + }, + visit::VisitMutWith, + }, +}; + +use crate::transform::{ + AtomCompileResult, AtomFlatValue, Compile, DynamicStyle, InjectedAtomStyle, + compile_dynamic_style, create_utility_styles_visitor, get_dynamic_style_from_path, + get_static_style_from_path, +}; + +fn ident(name: &str) -> Ident { + ident_with_ctxt(name, SyntaxContext::empty()) +} + +fn ident_with_ctxt(name: &str, ctxt: SyntaxContext) -> Ident { + Ident { + span: DUMMY_SP, + ctxt, + sym: name.into(), + optional: false, + } +} + +fn atom_id(name: &str) -> Id { + ident(name).to_id() +} + +fn ident_prop(name: &str) -> MemberProp { + MemberProp::Ident(IdentName { + span: DUMMY_SP, + sym: name.into(), + }) +} + +fn computed_str_prop(value: &str) -> MemberProp { + MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: value.into(), + raw: None, + }))), + }) +} + +/// Builds `base.first.second` where `base` is an identifier. +fn two_level_member(base: &str, first: MemberProp, second: MemberProp) -> MemberExpr { + MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(ident(base))), + prop: first, + })), + prop: second, + } +} + +/// Builds `base.prop`. +fn one_level_member(base: &str, prop: MemberProp) -> MemberExpr { + MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(ident(base))), + prop, + } +} + +fn default_imports() -> FxHashMap { + let mut map = FxHashMap::default(); + map.insert(atom_id("css"), "*".to_string()); + map +} + +/// The flat namespace shape the mock returns, covering each branch of the prop-hash search. +#[derive(Clone, Copy)] +enum FlatShape { + /// `[prop-hash, $$css]` — a normal compiled namespace. + Normal, + /// `[$$css, devClass(key == value), prop-hash]` — both must be skipped. + DevClassLead, + /// The style is not statically evaluable; `style_x_create_set` returns `None`. + NonEvaluable, +} + +struct MockCompiler { + imports: FxHashMap, + registered: Vec>, + hoist_count: usize, + shape: FlatShape, +} + +impl MockCompiler { + fn new(imports: FxHashMap) -> Self { + Self::with_shape(imports, FlatShape::Normal) + } + + fn with_shape(imports: FxHashMap, shape: FlatShape) -> Self { + Self { + imports, + registered: vec![], + hoist_count: 0, + shape, + } + } +} + +impl Compile for MockCompiler { + fn atom_imports(&self) -> &FxHashMap { + &self.imports + } + + fn style_x_create_set(&mut self, property: &str, _value: &str) -> Option { + if matches!(self.shape, FlatShape::NonEvaluable) { + return None; + } + + let class_name = format!("x{}", property); + let prop_hash = ( + format!("k{}", property), + AtomFlatValue::String(class_name.clone()), + ); + let marker = ("$$css".to_string(), AtomFlatValue::Bool(true)); + let dev_class = ( + "devClass".to_string(), + AtomFlatValue::String("devClass".to_string()), + ); + let compiled_flat = match self.shape { + FlatShape::Normal => vec![prop_hash, marker], + FlatShape::DevClassLead => vec![marker, dev_class, prop_hash], + FlatShape::NonEvaluable => unreachable!("handled above"), + }; + Some(AtomCompileResult { + compiled_ast: Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![], + }), + compiled_flat, + injected: vec![InjectedAtomStyle { + class_name, + priority: 3000.0, + ltr: format!(".x{}{{{}:value}}", property, property), + rtl: None, + }], + }) + } + + fn register_styles(&mut self, injected: &[InjectedAtomStyle]) { + self.registered.push(injected.to_vec()); + } + + fn hoist_expression(&mut self, _expr: Expr) -> Expr { + self.hoist_count += 1; + Expr::Ident(ident("_temp")) + } +} + +#[test] +fn get_static_style_ignores_non_atom_identifier() { + let imports = default_imports(); + let member = two_level_member("styles", ident_prop("display"), ident_prop("flex")); + assert_eq!(get_static_style_from_path(&member, &imports), None); +} + +fn call_member(base: &str, method: MemberProp, arg: Expr) -> CallExpr { + CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(one_level_member(base, method)))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(arg), + }], + type_args: None, + } +} + +fn str_expr(value: &str) -> Expr { + Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: value.into(), + raw: None, + })) +} + +/// Builds `base.first.second(arg)`. +fn call_two_level(base: &str, first: MemberProp, second: MemberProp, arg: Expr) -> CallExpr { + CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(two_level_member( + base, first, second, + )))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(arg), + }], + type_args: None, + } +} + +#[test] +fn get_static_style_non_member_non_ident_object_returns_none() { + // `"literal".flex` — the object is neither a member nor an identifier. + let imports = default_imports(); + let member = MemberExpr { + span: DUMMY_SP, + obj: Box::new(str_expr("literal")), + prop: ident_prop("flex"), + }; + assert_eq!(get_static_style_from_path(&member, &imports), None); +} + +#[test] +fn get_dynamic_style_returns_none_for_spread_arg() { + // `css.color(...value)` — a spread argument is not a single value. + let imports = default_imports(); + let mut call = call_member("css", ident_prop("color"), Expr::Ident(ident("value"))); + call.args[0].spread = Some(DUMMY_SP); + assert!(get_dynamic_style_from_path(&call, &imports).is_none()); +} + +#[test] +fn get_dynamic_style_two_level_member() { + // `css.padding.top(value)` — the property is the inner member key. + let imports = default_imports(); + let call = call_two_level( + "css", + ident_prop("padding"), + ident_prop("top"), + Expr::Ident(ident("value")), + ); + let dynamic = get_dynamic_style_from_path(&call, &imports); + assert!(matches!(dynamic, Some(DynamicStyle { ref property, .. }) if property == "padding")); +} + +#[test] +fn get_dynamic_style_two_level_non_atom_returns_none() { + let imports = default_imports(); + let call = call_two_level( + "styles", + ident_prop("padding"), + ident_prop("top"), + Expr::Ident(ident("value")), + ); + assert!(get_dynamic_style_from_path(&call, &imports).is_none()); +} + +#[test] +fn compile_dynamic_style_skips_marker_and_dev_class_keys() { + // `$$css` and a dev class lead the namespace and must be skipped. + let mut compiler = MockCompiler::with_shape(default_imports(), FlatShape::DevClassLead); + let call = call_member("css", ident_prop("color"), Expr::Ident(ident("value"))); + let result = compile_dynamic_style(&mut compiler, &call); + assert!(matches!(result, Some(Expr::Call(_)))); + assert_eq!(compiler.hoist_count, 1); +} + +#[test] +fn visitor_leaves_non_expression_callee_untouched() { + let mut compiler = MockCompiler::new(default_imports()); + let mut expr = Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Super(Super { span: DUMMY_SP }), + args: vec![], + type_args: None, + }); + + let mut visitor = create_utility_styles_visitor(&mut compiler); + expr.visit_mut_with(&mut visitor); + + let call = match &expr { + Expr::Call(call) => call, + other => panic!("expected call expression, got {:?}", other), + }; + assert!(matches!(&call.callee, Callee::Super(_))); + assert_eq!(compiler.registered.len(), 0); + assert_eq!(compiler.hoist_count, 0); +} + +#[test] +fn visitor_leaves_bad_arity_two_level_atom_callee_untouched() { + let mut compiler = MockCompiler::new(default_imports()); + let mut call = call_two_level( + "css", + ident_prop("display"), + ident_prop("flex"), + Expr::Ident(ident("first")), + ); + call.args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(ident("second"))), + }); + let mut expr = Expr::Call(call); + + let mut visitor = create_utility_styles_visitor(&mut compiler); + expr.visit_mut_with(&mut visitor); + + let call = match &expr { + Expr::Call(call) => call, + other => panic!("expected call expression, got {:?}", other), + }; + let callee = match &call.callee { + Callee::Expr(callee) => callee, + other => panic!("expected expression callee, got {:?}", other), + }; + + match callee.as_ref() { + Expr::Member(member) => match member.obj.as_ref() { + Expr::Member(parent) => { + assert!(matches!(parent.obj.as_ref(), Expr::Ident(id) if id.sym == *"css")); + assert!(matches!(&parent.prop, MemberProp::Ident(id) if id.sym == *"display")); + assert!(matches!(&member.prop, MemberProp::Ident(id) if id.sym == *"flex")); + }, + other => panic!("expected nested member object, got {:?}", other), + }, + other => panic!("expected member callee, got {:?}", other), + } + assert_eq!(compiler.registered.len(), 0); + assert_eq!(compiler.hoist_count, 0); +} + +#[test] +fn visitor_compiles_atoms_nested_in_computed_callee_keys() { + let mut compiler = MockCompiler::new(default_imports()); + let computed_key = Expr::Member(two_level_member( + "css", + ident_prop("display"), + ident_prop("flex"), + )); + let mut expr = Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(ident("handlers"))), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(computed_key), + }), + }))), + args: vec![], + type_args: None, + }); + + let mut visitor = create_utility_styles_visitor(&mut compiler); + expr.visit_mut_with(&mut visitor); + + let call = match &expr { + Expr::Call(call) => call, + other => panic!("expected call expression, got {:?}", other), + }; + let callee = match &call.callee { + Callee::Expr(callee) => callee, + other => panic!("expected expression callee, got {:?}", other), + }; + + match callee.as_ref() { + Expr::Member(member) => match &member.prop { + MemberProp::Computed(computed) => assert!(matches!(computed.expr.as_ref(), Expr::Object(_))), + other => panic!("expected computed property, got {:?}", other), + }, + other => panic!("expected member callee, got {:?}", other), + } + assert_eq!(compiler.registered.len(), 1); +} + +#[test] +fn visitor_leaves_non_evaluable_atom_untouched() { + // A non-evaluable static atom is left as written for the runtime. + let mut compiler = MockCompiler::with_shape(default_imports(), FlatShape::NonEvaluable); + let mut expr = Expr::Member(two_level_member( + "css", + ident_prop("display"), + ident_prop("flex"), + )); + let mut visitor = create_utility_styles_visitor(&mut compiler); + expr.visit_mut_with(&mut visitor); + + assert!(matches!(&expr, Expr::Member(_))); + assert_eq!(compiler.registered.len(), 0); +} + +#[test] +fn compile_dynamic_style_uses_verbatim_custom_property_name() { + let mut compiler = MockCompiler::new(default_imports()); + let call = call_member( + "css", + computed_str_prop("accent-color"), + Expr::Ident(ident("value")), + ); + let result = compile_dynamic_style(&mut compiler, &call); + assert!(matches!(result, Some(Expr::Call(_)))); + + let injected = &compiler.registered[0]; + let custom_property_name = "--x-accent-color"; + let property_rule = injected + .iter() + .find(|style| style.class_name == custom_property_name) + .expect("missing @property rule"); + assert!( + property_rule + .ltr + .contains(&format!("@property {custom_property_name}")) + ); + + let call_expr = match result { + Some(Expr::Call(call)) => call, + other => panic!("expected call expression, got {:?}", other), + }; + let callee = match call_expr.callee { + Callee::Expr(expr) => expr, + _ => panic!("expected expression callee"), + }; + match callee.as_ref() { + Expr::Member(member) => assert!(matches!(&member.prop, MemberProp::Computed(_))), + other => panic!("expected member callee, got {:?}", other), + } +} + +#[test] +fn compile_dynamic_style_rejects_unsafe_computed_property_name() { + let mut compiler = MockCompiler::new(default_imports()); + let call = call_member( + "css", + computed_str_prop("color; } body {"), + Expr::Ident(ident("value")), + ); + + assert!(compile_dynamic_style(&mut compiler, &call).is_none()); + assert!(compiler.registered.is_empty()); + assert_eq!(compiler.hoist_count, 0); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/transform.rs b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/transform.rs new file mode 100644 index 000000000..109367632 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-atoms/src/transform.rs @@ -0,0 +1,330 @@ +//! Inline atomic-style transform for `@stylexjs/atoms`: +//! +//! - `css.display.flex` → `{ $$css: true, display: "x..." }` (static) +//! - `css.color(value)` → a hoisted dynamic-style function call (dynamic) + +use rustc_hash::FxHashMap; +use stylex_ast::ast::{ + convertors::{ + convert_member_prop_to_string, create_bool_expr, create_ident_expr, create_null_expr, + create_string_expr, + }, + factories::{ + create_array_expression, create_arrow_expression_with_params, create_bin_expr, + create_binding_ident, create_cond_expr, create_expr_or_spread, create_ident, + create_key_value_prop, create_member_call_expr, create_member_expr, + create_member_prop_from_key, create_object_expression, create_str_key_value_prop, + }, +}; +use stylex_constants::constants::common::COMPILED_KEY; +use swc_core::ecma::{ + ast::{BinaryOp, CallExpr, Callee, Expr, Id, Ident, MemberExpr, MemberProp, Pat}, + visit::{VisitMut, VisitMutWith}, +}; + +/// The npm package name that marks an import as an atoms source. +pub const ATOMS_SOURCE: &str = "@stylexjs/atoms"; + +/// A flat compiled value: a class name string, the `$$css` marker, or `null`. +#[derive(Debug, Clone, PartialEq)] +pub enum AtomFlatValue { + String(String), + Bool(bool), + Null, +} + +/// A single injected CSS rule produced while compiling an atom. +#[derive(Debug, Clone, PartialEq)] +pub struct InjectedAtomStyle { + pub class_name: String, + pub priority: f64, + pub ltr: String, + pub rtl: Option, +} + +/// The result of compiling one `{ [property]: value }` atom via [`Compile::style_x_create_set`]. +#[derive(Debug, Clone)] +pub struct AtomCompileResult { + /// The compiled `__inline__` namespace, e.g. `{ display: "x78zum5", $$css: true }`. + pub compiled_ast: Expr, + /// Flat view of the same namespace, used to find the property hash and class name. + pub compiled_flat: Vec<(String, AtomFlatValue)>, + pub injected: Vec, +} + +/// A detected static style, e.g. `{ property: "display", value: "flex" }`. +#[derive(Debug, Clone, PartialEq)] +pub struct StaticStyle { + pub property: String, + pub value: String, +} + +/// A detected dynamic style, e.g. `css.color(value)` → `{ property: "color", value }`. +#[derive(Debug, Clone)] +pub struct DynamicStyle { + pub property: String, + pub value: Box, +} + +/// Compilation utilities injected by the consumer (JS `compile` plus `StateManager` access). +pub trait Compile { + /// Local identifier → imported name; namespace/default imports map to `"*"`. + fn atom_imports(&self) -> &FxHashMap; + + /// `compile.styleXCreateSet` (plus dev class names) on `{ __inline__: { [property]: value } }`. + /// `None` leaves the authored expression for the runtime. + fn style_x_create_set(&mut self, property: &str, value: &str) -> Option; + + /// Registers injected CSS rules (`state.registerStyles`). + fn register_styles(&mut self, injected: &[InjectedAtomStyle]); + + /// Hoists an expression to module scope, returning a reference (`compile.hoistExpression`). + fn hoist_expression(&mut self, expr: Expr) -> Expr; +} + +/// Strips one leading underscore, for values that clash with JS reserved words or start with a +/// digit (`css.display._flex`, `css.zIndex._1`). +pub fn normalize_value(value: &str) -> String { + match value.strip_prefix('_') { + Some(stripped) => stripped.to_string(), + None => value.to_string(), + } +} + +/// Whether `ident` refers to an atoms import (keyed by full `Id`, so shadowed locals don't match). +pub fn is_utility_styles_identifier(ident: &Ident, atom_imports: &FxHashMap) -> bool { + atom_imports.contains_key(&ident.to_id()) +} + +/// Detects a static atom: `css.display.flex`, `css.width['calc(...)']`, or default-import +/// `css.flex`. +pub fn get_static_style_from_path( + member: &MemberExpr, + atom_imports: &FxHashMap, +) -> Option { + let value_key = convert_member_prop_to_string(&member.prop)?; + + match member.obj.as_ref() { + Expr::Member(parent) => { + let prop_name = convert_member_prop_to_string(&parent.prop)?; + if let Expr::Ident(base) = parent.obj.as_ref() + && is_utility_styles_identifier(base, atom_imports) + { + return Some(StaticStyle { + property: prop_name, + value: normalize_value(&value_key), + }); + } + None + }, + Expr::Ident(base) => match atom_imports.get(&base.to_id()) { + Some(imported_name) => { + let property = if imported_name == "*" { + value_key.clone() + } else { + imported_name.clone() + }; + Some(StaticStyle { + property, + value: normalize_value(&value_key), + }) + }, + None => None, + }, + _ => None, + } +} + +/// Detects a dynamic atom: `css.color(value)` or `css.a.b(value)` (single argument). +pub fn get_dynamic_style_from_path( + call: &CallExpr, + atom_imports: &FxHashMap, +) -> Option { + let callee = match &call.callee { + Callee::Expr(expr) => expr.as_ref(), + _ => return None, + }; + + let callee_member = match callee { + Expr::Member(member) => member, + _ => return None, + }; + + let value_key = convert_member_prop_to_string(&callee_member.prop)?; + + if call.args.len() != 1 { + return None; + } + + let arg = &call.args[0]; + if arg.spread.is_some() { + return None; + } + let value = arg.expr.clone(); + + match callee_member.obj.as_ref() { + Expr::Ident(base) if is_utility_styles_identifier(base, atom_imports) => Some(DynamicStyle { + property: value_key, + value, + }), + Expr::Member(parent) => { + let prop_name = convert_member_prop_to_string(&parent.prop)?; + if let Expr::Ident(base) = parent.obj.as_ref() + && is_utility_styles_identifier(base, atom_imports) + { + return Some(DynamicStyle { + property: prop_name, + value, + }); + } + None + }, + _ => None, + } +} + +/// Compiles `css.display.flex` → `{ $$css: true, display: "x78zum5" }`; `None` leaves the +/// original expression. +pub fn compile_static_style(compiler: &mut T, member: &MemberExpr) -> Option { + let style = get_static_style_from_path(member, compiler.atom_imports())?; + + let result = compiler.style_x_create_set(&style.property, &style.value)?; + + compiler.register_styles(&result.injected); + + Some(result.compiled_ast) +} + +/// Compiles `css.color(value)` → `_temp.color(value)`; `None` leaves the original call. +pub fn compile_dynamic_style(compiler: &mut T, call: &CallExpr) -> Option { + let style = get_dynamic_style_from_path(call, compiler.atom_imports())?; + let property = style.property; + + if !is_safe_css_property_fragment(&property) { + return None; + } + + let var_name = format!("--x-{property}"); + let css_value = format!("var({var_name})"); + + let mut result = compiler.style_x_create_set(&property, &css_value)?; + + // Find the prop-hash key: not `$$css`, not the dev class name (where key == value). + let (prop_key, class_name) = result.compiled_flat.iter().find_map(|(key, value)| { + if key == COMPILED_KEY { + return None; + } + match value { + AtomFlatValue::String(class_name) if class_name != key => { + Some((key.clone(), class_name.clone())) + }, + _ => None, + } + })?; + + result.injected.push(InjectedAtomStyle { + class_name: var_name.clone(), + priority: 0.0, + ltr: format!("@property {var_name} {{ syntax: \"*\"; inherits: false;}}"), + rtl: None, + }); + + compiler.register_styles(&result.injected); + + let param = create_ident("_v"); + let param_expr = || Expr::Ident(param.clone()); + let null_check = || create_bin_expr(BinaryOp::NotEq, param_expr(), create_null_expr()); + + let compiled_obj = create_object_expression(vec![ + create_str_key_value_prop( + &prop_key, + create_cond_expr(null_check(), create_string_expr(&class_name), param_expr()), + ), + create_str_key_value_prop(COMPILED_KEY, create_bool_expr(true)), + ]); + + let inline_vars_obj = create_object_expression(vec![create_str_key_value_prop( + &var_name, + create_cond_expr(null_check(), param_expr(), create_ident_expr("undefined")), + )]); + + let fn_body = create_array_expression(vec![ + Some(create_expr_or_spread(compiled_obj)), + Some(create_expr_or_spread(inline_vars_obj)), + ]); + + let arrow_fn = + create_arrow_expression_with_params(vec![Pat::Ident(create_binding_ident(param))], fn_body); + + let styles_obj = create_object_expression(vec![create_key_value_prop(&property, arrow_fn)]); + + let hoisted_id = compiler.hoist_expression(styles_obj); + + let callee_member = create_member_expr(hoisted_id, create_member_prop_from_key(&property)); + + Some(Expr::Call(create_member_call_expr( + callee_member, + vec![create_expr_or_spread(*style.value)], + ))) +} + +/// Rewrites atom member expressions to static objects and atom calls to hoisted calls. +pub struct UtilityStylesVisitor<'a, T: Compile> { + compiler: &'a mut T, +} + +/// Creates a [`UtilityStylesVisitor`] borrowing the injected compiler. +pub fn create_utility_styles_visitor(compiler: &mut T) -> UtilityStylesVisitor<'_, T> { + UtilityStylesVisitor { compiler } +} + +impl VisitMut for UtilityStylesVisitor<'_, T> { + fn visit_mut_expr(&mut self, expr: &mut Expr) { + match expr { + Expr::Call(call) => { + if let Some(replacement) = compile_dynamic_style(self.compiler, call) { + *expr = replacement; + return; + } + + // Not a dynamic atom: visit computed callee keys, but never compile the callee chain + // itself as a static atom. + call.args.visit_mut_with(self); + if let Callee::Expr(callee_expr) = &mut call.callee { + match callee_expr.as_mut() { + Expr::Member(member) => visit_member_callee(member, self), + other => other.visit_mut_with(self), + } + } + return; + }, + Expr::Member(member) => { + if let Some(replacement) = compile_static_style(self.compiler, member) { + *expr = replacement; + return; + } + }, + _ => {}, + } + + expr.visit_mut_children_with(self); + } +} + +fn visit_member_callee(member: &mut MemberExpr, visitor: &mut UtilityStylesVisitor) { + match member.obj.as_mut() { + Expr::Member(parent) => visit_member_callee(parent, visitor), + other => other.visit_mut_with(visitor), + } + + if let MemberProp::Computed(computed) = &mut member.prop { + computed.expr.visit_mut_with(visitor); + } +} + +fn is_safe_css_property_fragment(value: &str) -> bool { + !value.is_empty() + && value + .chars() + .all(|c| !c.is_whitespace() && !matches!(c, ';' | '{' | '}')) +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/Cargo.toml b/packages/@stylexjs/rust-plugin/crates/stylex-constants/Cargo.toml new file mode 100644 index 000000000..17c2f4af2 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/Cargo.toml @@ -0,0 +1,25 @@ +[package] +description = "Constants, regex patterns and static data for StyleX SWC plugin" +edition.workspace = true +license.workspace = true +name = "stylex_constants" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + + +[lib] +crate-type = ["rlib"] +doctest = false + +[dependencies] +once_cell.workspace = true +phf = { workspace = true, features = ["macros"] } +rustc-hash.workspace = true + +[dev-dependencies] +ctor.workspace = true +pretty_env_logger.workspace = true + +[lints] +workspace = true diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/api_names.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/api_names.rs new file mode 100644 index 000000000..540fc8a47 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/api_names.rs @@ -0,0 +1,23 @@ +pub const STYLEX_ATTRS: &str = "attrs"; +pub const STYLEX_CREATE: &str = "create"; +pub const STYLEX_CREATE_THEME: &str = "createTheme"; +pub const STYLEX_DEFAULT_MARKER: &str = "defaultMarker"; +pub const STYLEX_UNSTABLE_CREATE_THEME_NESTED: &str = "unstable_createThemeNested"; +pub const STYLEX_UNSTABLE_CONDITIONAL: &str = "unstable_conditional"; +pub const STYLEX_UNSTABLE_DEFINE_CONSTS_NESTED: &str = "unstable_defineConstsNested"; +pub const STYLEX_UNSTABLE_DEFINE_VARS_NESTED: &str = "unstable_defineVarsNested"; +pub const STYLEX_DEFINE_CONSTS: &str = "defineConsts"; +pub const STYLEX_DEFINE_MARKER: &str = "defineMarker"; +pub const STYLEX_DEFINE_VARS: &str = "defineVars"; +pub const STYLEX_ENV: &str = "env"; +pub const STYLEX_FIRST_THAT_WORKS: &str = "firstThatWorks"; +pub const STYLEX_KEYFRAMES: &str = "keyframes"; +pub const STYLEX_POSITION_TRY: &str = "positionTry"; +pub const STYLEX_PROPS: &str = "props"; +pub const STYLEX_SX: &str = "sx"; +pub const STYLEX_TYPES: &str = "types"; +pub const STYLEX_VIEW_TRANSITION_CLASS: &str = "viewTransitionClass"; +pub const STYLEX_WHEN: &str = "when"; + +/// The only key of a function config (`{ fn }` in Babel). +pub const FUNCTION_CONFIG_FN_KEY: &str = "fn"; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/common.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/common.rs new file mode 100644 index 000000000..e4f5488a1 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/common.rs @@ -0,0 +1,251 @@ +pub static DEFAULT_INJECT_PATH: &str = "@stylexjs/stylex/lib/stylex-inject"; +use once_cell::sync::Lazy; +use phf::phf_set; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static VALID_CALLEES: phf::Set<&'static str> = phf_set! { + + "String", "Number", "Math", "Object", "Array" +}; + +/// Globals recognised only as a value, e.g. `[…].filter(Boolean)`; Babel never folds `Boolean(x)`. +pub static VALUE_ONLY_GLOBALS: phf::Set<&'static str> = phf_set! { + "Boolean" +}; + +/// The `String` statics a fold may call. Per-global allowlists, not denylists: reflective +/// statics lead from plain data to `Function`. +pub static VALID_STRING_METHODS: phf::Set<&'static str> = phf_set! { + "fromCharCode", + "fromCodePoint", + "raw", +}; + +/// The `Number` statics a fold may call. See [`VALID_STRING_METHODS`]. +pub static VALID_NUMBER_METHODS: phf::Set<&'static str> = phf_set! { + "isFinite", + "isInteger", + "isNaN", + "isSafeInteger", + "parseFloat", + "parseInt", +}; + +/// The `Math` statics a fold may call; `random` is excluded so builds are deterministic. +pub static VALID_MATH_METHODS: phf::Set<&'static str> = phf_set! { + "abs", + "acos", + "acosh", + "asin", + "asinh", + "atan", + "atan2", + "atanh", + "cbrt", + "ceil", + "clz32", + "cos", + "cosh", + "exp", + "expm1", + "f16round", + "floor", + "fround", + "hypot", + "imul", + "log", + "log10", + "log1p", + "log2", + "max", + "min", + "pow", + "round", + "sign", + "sin", + "sinh", + "sqrt", + "tan", + "tanh", + "trunc", +}; + +/// The `Object` statics a fold may call; mutating and reflective statics are excluded. +pub static VALID_OBJECT_METHODS: phf::Set<&'static str> = phf_set! { + "entries", + "fromEntries", + "getOwnPropertyNames", + "getOwnPropertySymbols", + "groupBy", + "hasOwn", + "is", + "isExtensible", + "isFrozen", + "isSealed", + "keys", + "values", +}; + +/// The `Array` statics a fold may call. See [`VALID_STRING_METHODS`]. +pub static VALID_ARRAY_METHODS: phf::Set<&'static str> = phf_set! { + "from", + "isArray", + "of", +}; + +/// Array methods that mutate their receiver, disqualifying the binding as a constant. +pub static MUTATING_ARRAY_METHODS: phf::Set<&'static str> = phf_set! { + "push", + "pop", + "shift", + "unshift", + "splice", + "sort", + "reverse", + "fill", + "copyWithin", +}; + +/// `Object` statics that mutate their argument; used like [`MUTATING_ARRAY_METHODS`]. +pub static MUTATING_OBJECT_METHODS: phf::Set<&'static str> = phf_set! { + "assign", + "defineProperty", + "defineProperties", + "setPrototypeOf", +}; + +pub static COMPILED_KEY: &str = "$$css"; + +pub static SPLIT_TOKEN: &str = "__$$__"; + +pub static ROOT_FONT_SIZE: i8 = 16; + +pub static VAR_GROUP_HASH_KEY: &str = "__varGroupHash__"; + +pub static COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES: Lazy<[&str; 9]> = Lazy::new(|| { + [ + "oklch", + "lch", + "oklab", + "hsla", + "radial-gradient", + "hwb", + "lab", + "clamp", + "hsl", + ] +}); + +/// Color functions supporting relative syntax (`rgb(from red r g b)`); SWC cannot parse it, +/// so these values are only re-spaced. +pub static COLOR_RELATIVE_VALUE_FUNCTIONS: Lazy<[&str; 10]> = Lazy::new(|| { + [ + "rgb", "rgba", "hsl", "hsla", "hwb", "lab", "lch", "oklab", "oklch", "color", + ] +}); + +pub static CSS_CONTENT_FUNCTIONS: Lazy<[&str; 7]> = Lazy::new(|| { + [ + "attr(", + "counter(", + "counters(", + "url(", + "linear-gradient(", + "image-set(", + "var(--", + ] +}); + +pub static CSS_CONTENT_KEYWORDS: Lazy<[&str; 11]> = Lazy::new(|| { + [ + "normal", + "none", + "open-quote", + "close-quote", + "no-open-quote", + "no-close-quote", + "inherit", + "initial", + "revert", + "revert-layer", + "unset", + ] +}); + +// TODO: Once we have a reliable validator, these property checks should be +// replaced with validators that can also validate the values. +pub static VALID_POSITION_TRY_PROPERTIES: Lazy<[&str; 40]> = Lazy::new(|| { + [ + // anchor Properties + "anchorName", + // position Properties + "positionAnchor", + "positionArea", + // inset Properties + "top", + "right", + "bottom", + "left", + "inset", + "insetBlock", + "insetBlockEnd", + "insetBlockStart", + "insetInline", + "insetInlineEnd", + "insetInlineStart", + // margin Properties + "margin", + "marginBlock", + "marginBlockEnd", + "marginBlockStart", + "marginInline", + "marginInlineEnd", + "marginInlineStart", + "marginTop", + "marginBottom", + "marginLeft", + "marginRight", + // size properties + "width", + "height", + "minWidth", + "minHeight", + "maxWidth", + "maxHeight", + "blockSize", + "inlineSize", + "minBlockSize", + "minInlineSize", + "maxBlockSize", + "maxInlineSize", + // self alignment properties + "alignSelf", + "justifySelf", + "placeSelf", + ] +}); + +// Validation of `stylex.viewTransitionClass` function call +pub static VALID_VIEW_TRANSITION_CLASS_PROPERTIES: Lazy<[&str; 4]> = + Lazy::new(|| ["group", "imagePair", "old", "new"]); + +pub static CONSTS_FILE_EXTENSION: &str = ".const"; + +/// Custom property for the logical float/clear polyfill's start direction (`left` in LTR). +pub static LOGICAL_FLOAT_START_VAR: &str = "--stylex-logical-start"; + +/// Custom property for the logical float/clear polyfill's end direction (`right` in LTR). +pub static LOGICAL_FLOAT_END_VAR: &str = "--stylex-logical-end"; + +/// JSX runtime call names (React classic and automatic runtime, Vue); `DEV` variants are +/// matched by suffix. +pub static RUNTIME_JSX_CALL_NAMES: &[&str] = &[ + "jsx", + "jsxs", + "createElement", + "createElementBlock", + "createElementVNode", + "createVNode", +]; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/cursor_flip.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/cursor_flip.rs new file mode 100644 index 000000000..f36f8eff3 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/cursor_flip.rs @@ -0,0 +1,12 @@ +use phf::phf_map; + +pub static CURSOR_FLIP: phf::Map<&'static str, &'static str> = phf_map! { + "e-resize" => "w-resize", + "w-resize" => "e-resize", + "ne-resize" => "nw-resize", + "nesw-resize" => "nwse-resize", + "nw-resize" => "ne-resize", + "nwse-resize" => "nesw-resize", + "se-resize" => "sw-resize", + "sw-resize" => "se-resize", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/evaluation_errors.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/evaluation_errors.rs new file mode 100644 index 000000000..89d4dc117 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/evaluation_errors.rs @@ -0,0 +1,398 @@ +// pub static IMPORT_FILE_PARSING_ERROR: &str = r#"There was error when +// attempting to parse the imported file. Please ensure that the 'babelrc' file +// is configured to be able to parse this file."#; + +/// A reference resolving to a default import specifier, which theme files never export. +/// Matches Babel's text for this input (not its unreached `DEFAULT_IMPORT`). +pub static IMPORT_FILE_EVAL_ERROR: &str = concat!( + "There was an error when attempting to evaluate the imported file.\n", + "Please ensure that the imported file is self-contained and does not rely ", + "on dynamic behavior.\n" +); + +// pub static DEFAULT_IMPORT: &str = r#"Error: Cannot use default imports. + +// Please define your styles without depending on values imported from other +// files. + +// You *may* use named imports to use variables defined with `defineVars` in a +// file with `.stylex.js` or `.stylex.ts` file. See: https://stylexjs.com/docs/learn/theming/defining-variables/#rules-when-defining-variables for more information."#; + +pub static PATH_WITHOUT_NODE: &str = r#"Unexpected error: +Could not resolve the code being evaluated."#; + +pub static UNEXPECTED_MEMBER_LOOKUP: &str = r#"Unexpected error: +Could not determine the property being accessed."#; + +pub static IMPORT_PATH_RESOLUTION_ERROR: &str = r#"Could not resolve the path to the imported file. +Please ensure that the theme file has a .stylex.js or .stylex.ts extension and follows the +rules for defining variables: + +https://stylexjs.com/docs/learn/theming/defining-variables/#rules-when-defining-variables +"#; + +pub static NON_CONSTANT: &str = "Referenced value is not a constant.\n\n"; + +/// A reference whose binding is declared later in the module. +pub static USED_BEFORE_DECLARATION: &str = "Referenced value is used before declaration.\n\n"; + +/// A reference to `undefined`/`Infinity`/`NaN` that names a shadowing binding, not the global. +/// Matches Babel's text. +pub static UNINITIALIZED_CONST: &str = "Referenced constant is not initialized.\n\n"; + +pub static UNDEFINED_CONST: &str = "Referenced constant is not defined."; + +pub static OBJECT_METHOD: &str = "Unsupported object method.\n\n"; + +/// An author's arrow, with every argument bound, whose body produced no compile-time value. +pub static FUNCTION_BODY_WITHOUT_VALUE: &str = "The function's body has no compile-time value.\n\n"; + +/// A call to a global or StyleX function with an argument the engine bridge cannot carry. +pub fn uncoercible_value(callee: &str) -> String { + format!( + "Cannot coerce this value at compile time.\nOnly static values can be passed to {}().\n\n", + callee + ) +} + +/// A folded array longer than the fold will materialise. +pub fn array_length_too_large(limit: u64) -> String { + format!( + concat!( + "Array length is too large to evaluate at compile time.\n", + "At most {} elements are supported.\n\n" + ), + limit + ) +} + +/// A methods-only global called as a function, e.g. `Math(x)`. +pub fn not_a_function(callee: &str) -> String { + format!( + "{} is not a function.\nOnly its methods can be called.\n\n", + callee + ) +} + +/// Every spread in a value position; matches Babel's `UNSUPPORTED_EXPRESSION(path.node.type)`. +pub const SPREAD_ELEMENT: &str = "Unsupported expression: SpreadElement\n\n"; + +pub fn unsupported_operator(op: &str) -> String { + format!("Unsupported operator: {}\n\n", op) +} + +pub fn unsupported_expression(type_: &str) -> String { + format!("Unsupported expression: {}\n\n", type_) +} + +/// An expression nested past the fold's recursion budget (counts fold steps, not source levels). +pub fn expression_too_deep(limit: usize) -> String { + format!( + "Expression is too deeply nested to evaluate at compile time.\n\ + At most {} levels of nested evaluation are supported.\n\n", + limit + ) +} + +/// Source nested deeper than printing and parsing can carry (counts source levels). +pub fn nesting_too_deep_to_carry(limit: usize) -> String { + format!( + "Expression is nested too deeply to fold at compile time.\n\ + At most {} levels of nested expressions can be carried.\n\n", + limit + ) +} + +/// Shared shape for call refusals: the call, then the reason. +fn cannot_fold(call: &str, reason: &str) -> String { + format!("Cannot fold '{}' at compile time.\n{}\n\n", call, reason) +} + +/// A method whose answer depends on locale data the compiler does not carry. +pub fn locale_sensitive_method(method: &str) -> String { + cannot_fold( + method, + "Its answer depends on locale data the compiler does not carry.", + ) +} + +/// A refused static on an engine-owned global (`Math.random`, `Object.freeze`, …); names +/// `Receiver.method`. +pub fn unfoldable_static(callee: &str, method: &str) -> String { + cannot_fold( + &format!("{}.{}", callee, method), + "A fold has to answer from the source alone, and this call does not.", + ) +} + +/// A method called on a numeric literal; matches Babel, which applies it without a receiver. +pub fn numeric_literal_receiver(method: &str) -> String { + format!( + concat!( + "Cannot call '{}' on a number literal.\n", + "Only a number a fold produced can be a method receiver.\n\n" + ), + method + ) +} + +/// Shared shape for refusals of length-amplifying calls. +fn cannot_bound(built: &str, call: &str, reason: &str) -> String { + format!( + "Cannot bound the {} '{}' would build.\n{}\n\n", + built, call, reason + ) +} + +/// A length-amplifying call whose result length could not be read. +pub fn unbounded_amplified_length(method: &str, limit: u64) -> String { + cannot_bound( + "string", + method, + &format!( + concat!( + "Its length must resolve to a number of at most {}, ", + "on a receiver whose own length can be read." + ), + limit + ), + ) +} + +/// An `Array(n)` whose length reached the engine without being bounded. +pub fn unbounded_declared_length(call: &str) -> String { + cannot_bound( + "array", + call, + concat!( + "Write the elements out, or keep the rest of the expression foldable ", + "so its length can be checked." + ), + ) +} + +/// A length-amplifying call whose result is too large; `repeats` is how many times it is +/// evaluated (named only when more than one). +pub fn amplified_length_too_large( + method: &str, + count: u64, + built: u64, + repeats: u64, + limit: u64, +) -> String { + let asked = match built == count { + true => format!("{} characters", built), + false => format!( + "{} copies of the value it is called on, which is {} characters", + count, built + ), + }; + + cannot_bound( + "string", + method, + &format!( + "It asks for {}{}, and at most {} are supported.", + asked, + per_element(built, repeats, "characters"), + limit + ), + ) +} + +/// The total across `repeats` evaluations, or empty when there is only one. +fn per_element(built: u64, repeats: u64, unit: &str) -> String { + match repeats > 1 { + true => format!( + concat!( + " once per element of the receiver it is written inside, ", + "which is {} evaluations and {} {} in all" + ), + repeats, + built.saturating_mul(repeats), + unit + ), + false => String::new(), + } +} + +/// A call declaring more array elements than the fold's entry budget. +pub fn amplified_entries_too_large(call: &str, declared: u64, repeats: u64, limit: u64) -> String { + cannot_bound( + "array", + call, + &format!( + "It declares a length of {} elements{}, and at most {} are supported.", + declared, + per_element(declared, repeats, "elements"), + limit + ), + ) +} + +/// A folded object with more properties than the fold's budget. +pub fn object_size_too_large(limit: u64) -> String { + format!( + "Object is too large to evaluate at compile time.\nAt most {} properties are supported.\n\n", + limit + ) +} + +/// A folded value with no carried form; `kind` is the JavaScript `typeof`. +pub fn unfoldable_fold_result(kind: &str) -> String { + format!( + concat!( + "Cannot carry a folded {} back from the engine.\n", + "Only strings, numbers, booleans, null, undefined, arrays and plain objects ", + "can be folded.\n\n" + ), + kind + ) +} + +/// The compile-time engine could not be prepared (unreachable in practice). +pub fn engine_did_not_start(message: &str) -> String { + format!( + "The compile-time JavaScript engine could not start.\n{}\n\n", + message + ) +} + +/// A call the engine threw on, in the engine's words; names the method because the engine's +/// message may not. +pub fn engine_threw(method: &str, message: &str) -> String { + cannot_fold(method, message) +} + +/// A read of `__proto__` or `prototype` (literal keys are fine). +/// Text and line breaks match Babel's; do not re-wrap. +pub static BLOCKED_PROPERTY_ACCESS: &str = concat!( + "Access to this property is not allowed during compilation.\n", + "Accessing prototype-chain properties such as 'constructor', '__proto__', or 'prototype'\n", + "is blocked to prevent arbitrary code execution.\n" +); + +/// A call of a string-compiling function (`Function` and kin, `eval`); backstop behind +/// [`BLOCKED_PROPERTY_ACCESS`]. Text and line breaks match Babel's. +pub static BLOCKED_FUNCTION_CALL: &str = concat!( + "Calling this function is not allowed during compilation.\n", + "Functions that compile strings into code, such as 'Function' and 'eval', are blocked to\n", + "prevent arbitrary code execution.\n" +); + +/// A binding that would shadow a name the printed fold reserves for its own checks. +pub fn reserved_compiler_name(name: &str) -> String { + format!( + "Cannot fold an expression that binds '{}' at compile time.\n\ + The compiler reserves that name for its own use in the source it evaluates.\n\n", + name + ) +} + +/// A read of `constructor`, `call`, `apply` or `bind`, which leads to the function graph. +pub fn escaping_property(property: &str) -> String { + format!( + "Cannot fold a read of '{}' at compile time.\n\ + It leads off the value that was written and onto the language's own function graph.\n\n", + property + ) +} + +/// A length-amplifying call inside a callback whose receiver's element count cannot be read. +pub fn amplification_inside_a_callback(built: &str, call: &str) -> String { + format!( + concat!( + "Cannot bound the {} '{}' would build inside a callback.\n", + "The callback's receiver holds an element count that cannot be read here, ", + "so a length written into the source bounds one evaluation rather than the ", + "call. Write the receiver's elements out, or write the value out instead.\n\n" + ), + built, call + ) +} + +/// A statement kind (e.g. a loop) the fold does not read inside a callback body. +pub fn unfoldable_statement(kind: &str) -> String { + format!( + "Cannot fold a callback whose body uses a {}.\n\ + Only a declaration, a branch, a block and a return are read inside a callback body.\n\n", + kind + ) +} + +/// A folded string longer than the fold will carry back. +pub fn folded_string_too_large(limit: u64) -> String { + format!( + "Folded string is too large to evaluate at compile time.\n\ + At most {} characters are supported.\n\n", + limit + ) +} + +/// Expression names used by [`grown_string_too_large`]. +pub const CONCATENATION: &str = "concatenation"; +pub const TEMPLATE_LITERAL: &str = "template literal"; +pub const NUMERIC_CONVERSION: &str = "numeric conversion"; +pub const STRING_CONVERSION: &str = "string conversion"; + +/// A string the evaluator itself (`+`, templates, conversions) grew past the folded-string limit. +pub fn grown_string_too_large(kind: &str, limit: u64) -> String { + format!( + "This {} builds a string too large to evaluate at compile time.\n\ + At most {} characters are supported.\n\n", + kind, limit + ) +} + +/// A binding whose resolved value has too many characters to copy into a fold. +pub fn bound_value_too_large(name: &str, limit: u64) -> String { + format!( + "Cannot carry the value of '{}' into a fold.\nAt most {} characters are supported.\n\n", + name, limit + ) +} + +/// A binding whose resolved value has too many elements and properties to copy into a fold. +pub fn bound_value_has_too_many_entries(name: &str, limit: u64) -> String { + format!( + concat!( + "Cannot carry the value of '{}' into a fold.\n", + "At most {} elements and properties are supported.\n\n" + ), + name, limit + ) +} + +/// A function binding not declared as a carryable expression-bodied arrow. +pub fn unfoldable_function(name: &str) -> String { + format!( + "Cannot carry the function '{}' into a fold.\n\ + Its declaration is not one the compiler can evaluate.\n\n", + name + ) +} + +/// A global's name used as a value, e.g. `[…].filter(Boolean)`. +pub fn global_as_a_value(name: &str) -> String { + format!( + "Cannot carry the global '{}' into a fold.\n\ + The compiler folds what a global answers, not the global itself.\n\n", + name + ) +} + +/// A printed fold that did not evaluate to a function (an invariant; refused, not asserted). +pub fn uncallable_printed_fold(method: &str) -> String { + cannot_fold( + method, + "The printed expression did not compile to a function.", + ) +} + +/// A declined method call whose receiver was fine. +pub fn unfoldable_call(method: &str) -> String { + cannot_fold( + method, + "Its receiver or one of its arguments is not in a form the compiler can evaluate.", + ) +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/length_units.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/length_units.rs new file mode 100644 index 000000000..570c853d9 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/length_units.rs @@ -0,0 +1,150 @@ +use phf::phf_set; + +pub static LENGTH_UNITS: phf::Set<&'static str> = phf_set! { + "backgroundPositionX", + "backgroundPositionY", + "blockSize", + "borderBlockEndWidth", + "borderBlockStartWidth", + "borderBlockWidth", + "borderVerticalWidth", + "borderBottomLeftRadius", + "borderBottomRightRadius", + "borderBottomWidth", + "borderEndEndRadius", + "borderEndStartRadius", + "borderInlineEndWidth", + "borderEndWidth", + "borderInlineStartWidth", + "borderStartWidth", + "borderInlineWidth", + "borderHorizontalWidth", + "borderLeftWidth", + "borderRightWidth", + "borderSpacing", + "borderStartEndRadius", + "borderStartStartRadius", + "borderTopLeftRadius", + "borderTopRightRadius", + "borderTopWidth", + "bottom", + "columnGap", + "columnRuleWidth", + "columnWidth", + "containIntrinsicBlockSize", + "containIntrinsicHeight", + "containIntrinsicInlineSize", + "containIntrinsicWidth", + "flexBasis", + "fontSize", + "fontSmooth", + "height", + "inlineSize", + "insetBlockEnd", + "insetBlockStart", + "insetInlineEnd", + "insetInlineStart", + "left", + "letterSpacing", + "marginBlockEnd", + "marginBlockStart", + "marginBottom", + "marginInlineEnd", + "marginEnd", + "marginInlineStart", + "marginStart", + "marginLeft", + "marginRight", + "marginTop", + "maxBlockSize", + "maxHeight", + "maxInlineSize", + "maxWidth", + "minBlockSize", + "minHeight", + "minInlineSize", + "minWidth", + "offsetDistance", + "outlineOffset", + "outlineWidth", + "overflowClipMargin", + "paddingBlockEnd", + "paddingBlockStart", + "paddingBottom", + "paddingInlineEnd", + "paddingEnd", + "paddingInlineStart", + "paddingStart", + "paddingLeft", + "paddingRight", + "paddingTop", + "perspective", + "right", + "rowGap", + "scrollMarginBlockEnd", + "scrollMarginBlockStart", + "scrollMarginBottom", + "scrollMarginInlineEnd", + "scrollMarginInlineStart", + "scrollMarginLeft", + "scrollMarginRight", + "scrollMarginTop", + "scrollPaddingBlockEnd", + "scrollPaddingBlockStart", + "scrollPaddingBottom", + "scrollPaddingInlineEnd", + "scrollPaddingInlineStart", + "scrollPaddingLeft", + "scrollPaddingRight", + "scrollPaddingTop", + "scrollSnapMarginBottom", + "scrollSnapMarginLeft", + "scrollSnapMarginRight", + "scrollSnapMarginTop", + "shapeMargin", + "tabSize", + "textDecorationThickness", + "textIndent", + "textUnderlineOffset", + "top", + "transformOrigin", + "translate", + "verticalAlign", + "width", + "wordSpacing", + "border", + "borderBlock", + "borderBlockEnd", + "borderBlockStart", + "borderBottom", + "borderLeft", + "borderRadius", + "borderRight", + "borderTop", + "borderWidth", + "columnRule", + "containIntrinsicSize", + "gap", + "inset", + "insetBlock", + "insetInline", + "margin", + "marginBlock", + "marginVertical", + "marginInline", + "marginHorizontal", + "offset", + "outline", + "padding", + "paddingBlock", + "paddingVertical", + "paddingInline", + "paddingHorizontal", + "scrollMargin", + "scrollMarginBlock", + "scrollMarginInline", + "scrollPadding", + "scrollPaddingBlock", + "scrollPaddingInline", + "scrollSnapMargin", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/logical_to_ltr.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/logical_to_ltr.rs new file mode 100644 index 000000000..b35fbf228 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/logical_to_ltr.rs @@ -0,0 +1,46 @@ +use phf::phf_map; + +// These properties are kept for a polyfill that is only used with +// `legacy-expand-shorthands` +pub static PROPERTY_TO_LTR: phf::Map<&'static str, &'static str> = phf_map! { + "margin-start" => "margin-left", + "margin-end" => "margin-right", + "padding-start" => "padding-left", + "padding-end" => "padding-right", + "border-start" => "border-left", + "border-end" => "border-right", + "border-start-width" => "border-left-width", + "border-end-width" => "border-right-width", + "border-start-color" => "border-left-color", + "border-end-color" => "border-right-color", + "border-start-style" => "border-left-style", + "border-end-style" => "border-right-style", + "border-top-start-radius" => "border-top-left-radius", + "border-bottom-start-radius" => "border-bottom-left-radius", + "border-top-end-radius" => "border-top-right-radius", + "border-bottom-end-radius" => "border-bottom-right-radius", + "start" => "left", + "end" => "right", + +}; + +pub static INLINE_PROPERTY_TO_LTR: phf::Map<&'static str, &'static str> = phf_map! { + "margin-inline-start" => "margin-left", + "margin-inline-end" => "margin-right", + "padding-inline-start" => "padding-left", + "padding-inline-end" => "padding-right", + "border-inline-start" => "border-left", + "border-inline-end" => "border-right", + "border-inline-start-width" => "border-left-width", + "border-inline-end-width" => "border-right-width", + "border-inline-start-color" => "border-left-color", + "border-inline-end-color" => "border-right-color", + "border-inline-start-style" => "border-left-style", + "border-inline-end-style" => "border-right-style", + "border-start-start-radius" => "border-top-left-radius", + "border-end-start-radius" => "border-bottom-left-radius", + "border-start-end-radius" => "border-top-right-radius", + "border-end-end-radius" => "border-bottom-right-radius", + "inset-inline-start" => "left", + "inset-inline-end" => "right", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/logical_to_rtl.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/logical_to_rtl.rs new file mode 100644 index 000000000..c5f58c4dc --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/logical_to_rtl.rs @@ -0,0 +1,52 @@ +use phf::phf_map; + +// These properties are kept for a polyfill that is only used with +// `legacy-expand-shorthands` +pub static LOGICAL_TO_RTL: phf::Map<&'static str, &'static str> = phf_map! { + "margin-start" => "margin-right", + "margin-end" => "margin-left", + "padding-start" => "padding-right", + "padding-end" => "padding-left", + "border-start" => "border-right", + "border-end" => "border-left", + "border-start-width" => "border-right-width", + "border-end-width" => "border-left-width", + "border-start-color" => "border-right-color", + "border-end-color" => "border-left-color", + "border-start-style" => "border-right-style", + "border-end-style" => "border-left-style", + "border-top-start-radius" => "border-top-right-radius", + "border-bottom-start-radius" => "border-bottom-right-radius", + "border-top-end-radius" => "border-top-left-radius", + "border-bottom-end-radius" => "border-bottom-left-radius", + "start" => "right", + "end" => "left", +}; + +pub static INLINE_TO_RTL: phf::Map<&'static str, &'static str> = phf_map! { + "margin-inline-start" => "margin-right", + "margin-inline-end" => "margin-left", + "padding-inline-start" => "padding-right", + "padding-inline-end" => "padding-left", + "border-inline-start" => "border-right", + "border-inline-end" => "border-left", + "border-inline-start-width" => "border-right-width", + "border-inline-end-width" => "border-left-width", + "border-inline-start-color" => "border-right-color", + "border-inline-end-color" => "border-left-color", + "border-inline-start-style" => "border-right-style", + "border-inline-end-style" => "border-left-style", + "border-start-start-radius" => "border-top-right-radius", + "border-end-start-radius" => "border-bottom-right-radius", + "border-start-end-radius" => "border-top-left-radius", + "border-end-end-radius" => "border-bottom-left-radius", + "inset-inline-start" => "right", + "inset-inline-end" => "left", +}; + +pub static LOGICAL_VALUE_TO_RTL: phf::Map<&'static str, &'static str> = phf_map! { + "start" => "right", + "inline-start" => "right", + "end" => "left", + "inline-end" => "left", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/long_hand_logical.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/long_hand_logical.rs new file mode 100644 index 000000000..ae3992aeb --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/long_hand_logical.rs @@ -0,0 +1,461 @@ +use phf::phf_set; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static LONG_HAND_LOGICAL: phf::Set<&'static str> = phf_set! { + // Composition and Blending + "background-blend-mode", + "isolation", + "mix-blend-mode", + + // CSS Animations + "animation-composition", + "animation-delay", + "animation-direction", + "animation-duration", + "animation-fill-mode", + "animation-iteration-count", + "animation-name", + "animation-play-state", + "animation-range-end", + "animation-range-start", + "animation-timing-function", + "animation-timeline", + + "scroll-timeline-axis", + "scroll-timeline-name", + + "timeline-scope", + + "view-timeline-axis", + "view-timeline-inset", + "view-timeline-name", + + // CSS Backgrounds and Borders + "background-attachment", + "background-clip", + "background-color", + "background-image", + "background-origin", + "background-repeat", + "background-size", + "background-position-x", + "background-position-y", + + "border-block-color", // Logical Properties + "border-block-stylex", // Logical Properties + "border-block-width", // Logical Properties + "border-block-start-color", // Logical Properties + "border-block-start-style", // Logical Properties + "border-block-start-width", // Logical Properties + "border-block-end-color", // Logical Properties + "border-block-end-style", // Logical Properties + "border-block-end-width", // Logical Properties + "border-inline-start-color", // Logical Properties + "border-inline-start-style", // Logical Properties + "border-inline-start-width", // Logical Properties + "border-inline-end-color", // Logical Properties + "border-inline-end-style", // Logical Properties + "border-inline-end-width", // Logical Properties + + "border-image-outset", + "border-image-repeat", + "border-image-slice", + "border-image-source", + "border-image-width", + + "border-start-end-radius", // Logical Properties + "border-start-start-radius", // Logical Properties + "border-end-end-radius", // Logical Properties + "border-end-start-radius", // Logical Properties + + "corner-start-start-shape", // Logical Properties + "corner-start-end-shape", // Logical Properties + "corner-end-start-shape", // Logical Properties + "corner-end-end-shape", // Logical Properties + + "box-shadow", + + // CSS Basic User Interface + "accent-color", + "appearance", + "aspect-ratio", + + "caret-color", + "caret-shape", + + "cursor", + "ime-mode", + "input-security", + + "outline-color", + "outline-offset", + "outline-style", + "outline-width", + + "pointer-events", + "resize", // horizontal, vertical, block, inline, both + "text-overflow", + "user-select", + + // CSS Box Alignment + "grid-row-gap", // alias for `row-gap` + "row-gap", + "grid-column-gap", // alias for `column-gap` + "column-gap", + + "align-content", + "justify-content", + + "align-items", + "justify-items", + + "align-self", + "justify-self", + + // CSS Box Model + "box-sizing", + + "block-size", // Logical Properties + "inline-size", // Logical Properties + + "max-block-size", // Logical Properties + "max-inline-size", // Logical Properties + "min-block-size", // Logical Properties + "min-inline-size", // Logical Properties + + "margin-block-start", // Logical Properties + "margin-block-end", // Logical Properties + "margin-inline-start", // Logical Properties + "margin-inline-end", // Logical Properties + + "margin-trim", + + "overscroll-behavior-block", + "overscroll-behavior-inline", + + "padding-block-start", // Logical Properties + "padding-block-end", // Logical Properties + "padding-inline-start", // Logical Properties + "padding-inline-end", // Logical Properties + + "visibility", + + // CSS Color + "color", + "color-scheme", + "forced-color-adjust", + "opacity", + "print-color-adjust", + + // CSS Columns + "column-count", + "column-width", + + "column-fill", + "column-span", + + "column-rule-color", + "column-rule-style", + "column-rule-width", + + // CSS Containment + "contain", + + "contain-intrinsic-block-size", + "contain-intrinsic-width", + "contain-intrinsic-height", + "contain-intrinsic-inline-size", + + "container-name", + "container-type", + + "content-visibility", + + // CSS Counter Styles + "counter-increment", + "counter-reset", + "counter-set", + + // CSS Display + "display", + + // CSS Flexible Box Layout + "flex-basis", + "flex-grow", + "flex-shrink", + + "flex-direction", + "flex-wrap", + + "order", + + // CSS Fonts + "font-family", + "font-size", + "font-stretch", + "font-style", + "font-weight", + "line-height", + "font-variant-alternates", + "font-variant-caps", + "font-variant-east-asian", + "font-variant-emoji", + "font-variant-ligatures", + "font-variant-numeric", + "font-variant-position", + + "font-feature-settings", + "font-kerning", + "font-language-override", + "font-optical-sizing", + "font-palette", + "font-variation-settings", + "font-size-adjust", + "font-smooth", // Non-standard + "font-synthesis-position", + "font-synthesis-small-caps", + "font-synthesis-style", + "font-synthesis-weight", + + "line-height-step", + + // CSS Fragmentation + "box-decoration-break", + "break-after", + "break-before", + "break-inside", + "orphans", + "widows", + + // CSS Generated Content + "content", + "quotes", + + // CSS Grid Layout + "grid-auto-flow", + "grid-auto-rows", + "grid-auto-columns", + "grid-template-columns", + "grid-template-rows", + + "grid-row-start", + "grid-row-end", + "grid-column-start", + "grid-column-end", + + "align-tracks", + "justify-tracks", + "masonry-auto-flow", + + // CSS Images + "image-orientation", + "image-rendering", + "image-resolution", + "object-fit", + "object-position", + + // CSS Inline + "initial-letter", + "initial-letter-align", + + // CSS Lists and Counters + "list-style-image", + "list-style-position", + "list-style-type", + + // CSS Masking + "clip", // @deprecated + "clip-path", + + "mask-clip", + "mask-composite", + "mask-image", + "mask-mode", + "mask-origin", + "mask-position", + "mask-repeat", + "mask-size", + + "mask-type", + + "mask-border-mode", + "mask-border-outset", + "mask-border-repeat", + "mask-border-slice", + "mask-border-source", + "mask-border-width", + + // CSS Miscellaneous + "text-rendering", + + // CSS Motion Path + "offset-anchor", + "offset-distance", + "offset-path", + "offset-position", + "offset-rotate", + + // CSS Overflow + "-webkit-box-orient", + "-webkit-line-clamp", + // "block-overflow", + + "overflow-block", + "overflow-inline", + + "overflow-clip-margin", // partial support + + "scroll-gutter", + "scroll-behavior", + + // CSS Pages + "page", + "page-break-after", + "page-break-before", + "page-break-inside", + + // CSS Positioning + "inset-block-start", // Logical Properties + "inset-block-end", // Logical Properties + "inset-inline-start", // Logical Properties + "inset-inline-end", // Logical Properties + + "clear", + "float", + // "overlay", + "position", + "z-index", + + // CSS Ruby + "ruby-align", + "ruby-merge", + "ruby-position", + + // CSS Scroll Anchoring + "overflow-anchor", + + // CSS Scroll Snap + "scroll-margin-block-start", + "scroll-margin-block-end", + "scroll-margin-inline-start", + "scroll-margin-inline-end", + + "scroll-padding-block-start", + "scroll-padding-block-end", + "scroll-padding-inline-start", + "scroll-padding-inline-end", + + "scroll-snap-align", + // "scroll-snap-coordinate", + // "scroll-snap-destination", + // "scroll-snap-points-x", + // "scroll-snap-points-y", + "scroll-snap-stop", + // "scroll-snap-type-x", + // "scroll-snap-type-y", + + // CSS Scrollbars + "scrollbar-color", + "scrollbar-width", + + // CSS Shapes + "shape-image-threshold", + "shape-margin", + "shape-outside", + + // CSS Speech + "azimuth", + + // CSS Table + "border-collapse", + "border-spacing", + "caption-side", + "empty-cells", + "table-layout", + "vertical-align", + + // CSS Text Decoration + "text-decoration-color", + "text-decoration-line", + "text-decoration-skip", + "text-decoration-skip-ink", + "text-decoration-style", + "text-decoration-thickness", + + "text-emphasis-color", + "text-emphasis-position", + "text-emphasis-style", + "text-shadow", + "text-underline-offset", + "text-underline-position", + + // CSS Text + "hanging-punctuation", + "hyphenate-character", + "hyphenate-limit-chars", + "hyphens", + "letter-spacing", + "line-break", + "overflow-wrap", + "paint-order", + "tab-size", + "text-align", + "text-align-last", + "text-indent", + "text-justify", + "text-size-adjust", + "text-transform", + "text-wrap", + "white-space", + "white-space-collapse", + // "white-space-trim", + "word-break", + "word-spacing", + "word-wrap", + + // CSS Transforms + "backface-visibility", + "perspective", + "perspective-origin", + "rotate", + "scale", + "transform", + "transform-box", + "transform-origin", + "transform-style", + "translate", + + // CSS Transitions + // "transition-behavior", + "transition-delay", + "transition-duration", + "transition-property", + "transition-timing-function", + + // CSS View Transitions + "view-transition-name", + + // CSS Will Change + "will-change", + + // CSS Writing Modes + "direction", + "text-combine-upright", + "text-orientation", + "unicode-bidi", + "writing-mode", + + // CSS Filter Effects + "backdrop-filter", + "filter", + + // MathML + "math-depth", + "math-shift", + "math-style", + + // CSS Pointer Events + "touch-action" +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/long_hand_physical.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/long_hand_physical.rs new file mode 100644 index 000000000..674a084dd --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/long_hand_physical.rs @@ -0,0 +1,75 @@ +use phf::phf_set; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static LONG_HAND_PHYSICAL: phf::Set<&'static str> = phf_set! { + "border-bottom-color", + "border-bottom-style", + "border-bottom-width", + "border-left-color", + "border-left-style", + "border-left-width", + "border-right-color", + "border-right-style", + "border-right-width", + + "border-top-color", + "border-top-style", + "border-top-width", + + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-left-radius", + "border-bottom-right-radius", + + "corner-top-left-shape", + "corner-top-right-shape", + "corner-bottom-left-shape", + "corner-bottom-right-shape", + + "height", + "width", + + "max-height", + "max-width", + "min-height", + "min-width", + + "margin-top", + "margin-bottom", + "margin-left", + "margin-right", + + "overscroll-behavior-y", + "overscroll-behavior-x", + + "padding-top", + "padding-bottom", + "padding-left", + "padding-right", + + // Current Babel's priority table leaves line-clamp/max-lines unclassified + // (commented entries), so they take the normal 3000 fallback. + + "overflow-y", + "overflow-x", + + + // CSS Positioning + "top", + "bottom", + "left", + "right", + + // CSS Scroll Snap + "scroll-margin-top", + "scroll-margin-bottom", + "scroll-margin-left", + "scroll-margin-right", + + "scroll-padding-top", + "scroll-padding-bottom", + "scroll-padding-left", + "scroll-padding-right", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/messages.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/messages.rs new file mode 100644 index 000000000..7d1eb6482 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/messages.rs @@ -0,0 +1,204 @@ +// Functions that generate messages with parameters +pub fn illegal_argument_length(fn_name: &str, arg_length: usize) -> String { + let plural = if arg_length == 1 { "" } else { "s" }; + format!( + "{}() should have {} argument{}.", + fn_name, arg_length, plural + ) +} + +pub fn non_static_value(fn_name: &str) -> String { + format!( + "Only static values are allowed inside of {} {}() call.", + indefinite_article(fn_name), + fn_name + ) +} + +/// `"a"` or `"an"` by the first spelled letter of `name` (fine for StyleX API names). +fn indefinite_article(name: &str) -> &'static str { + match name.chars().next() { + Some(c) => match c.to_ascii_lowercase() { + 'a' | 'e' | 'i' | 'o' | 'u' => "an", + _ => "a", + }, + None => "a", + } +} + +pub fn non_style_object(fn_name: &str) -> String { + format!("{}() can only accept an object.", fn_name) +} + +pub fn non_export_named_declaration(fn_name: &str) -> String { + format!( + "The return value of {}() must be bound to a named export.", + fn_name + ) +} + +pub fn unbound_call_value(fn_name: &str) -> String { + format!("{}() calls must be bound to a bare variable.", fn_name) +} + +/// Rust-only: the printer drops parentheses around TS assertions, so +/// `(create({…}) as Styles).root` would print as `create() as Styles.root`. +pub fn type_asserted_call_value(fn_name: &str) -> String { + format!( + "{}() cannot be written inside a type assertion. Bind the call to a variable and assert the variable.", + fn_name + ) +} + +pub fn cannot_generate_hash(fn_name: &str) -> String { + format!( + "Unable to generate hash for {}(). Check that the file has a valid extension and that unstable_moduleResolution is configured.", + fn_name + ) +} + +// Static constants +pub static DUPLICATE_CONDITIONAL: &str = + "The same pseudo selector or at-rule cannot be used more than once."; + +pub static ILLEGAL_PROP_VALUE: &str = "A style value can only contain an array, string or number."; + +pub static ILLEGAL_PROP_ARRAY_VALUE: &str = + "A style array value can only contain strings or numbers."; + +pub static ILLEGAL_NAMESPACE_VALUE: &str = "A StyleX namespace must be an object."; + +/// Matches the runtime's `TypeError` word for word, which Babel surfaces via `Object.keys`. +pub static NULLISH_TO_OBJECT: &str = "Cannot convert undefined or null to object"; + +pub static INVALID_PSEUDO_OR_AT_RULE: &str = "Invalid pseudo or at-rule."; + +pub static LINT_UNCLOSED_FUNCTION: &str = "Rule contains an unclosed function"; + +pub static LINT_UNCLOSED_STRING: &str = "Rule contains an unclosed string"; + +pub static LINT_UNCLOSED_COMMENT: &str = "Rule contains an unclosed comment"; + +pub static LINT_RULE_BREAKING_TOKEN: &str = + "Rule contains a `{`, `}` or `;` outside of a string or comment"; + +pub static LINT_VALUE_NESTED_TOO_DEEPLY: &str = + "Rule contains a value nested more deeply than the compiler supports"; + +pub static LINT_VALUE_HAS_NO_TOKENS: &str = "Rule contains a value with nothing to normalize"; + +/// An `!important` Babel's handler cannot remove cleanly (it may be nested, not misplaced). +pub static LINT_IMPORTANT_NOT_LAST: &str = + "Rule contains an importance annotation the compiler cannot remove from the value"; + +pub static NON_OBJECT_KEYFRAME: &str = "Every frame within a keyframes() call must be an object."; + +pub static NON_CONTIGUOUS_VARS: &str = + "All variables passed to firstThatWorks() must be contiguous."; + +pub static NO_OBJECT_SPREADS: &str = "Object spreads are not allowed in create() calls."; + +pub static ONLY_NAMED_PARAMETERS_IN_DYNAMIC_STYLE_FUNCTIONS: &str = "Only named parameters are allowed in Dynamic Style functions. Destructuring, spreading or default values are not allowed."; + +pub static UNPREFIXED_CUSTOM_PROPERTIES: &str = "Unprefixed custom properties"; + +pub static NON_STATIC_SECOND_ARG_CREATE_THEME_VALUE: &str = + "createTheme() can only accept an object as the second argument."; + +pub static THEME_IMPORT_KEY_AS_OBJECT_KEY: &str = + "Theme import keys cannot be used as object keys. Please use a valid object key."; + +pub static POSITION_TRY_INVALID_PROPERTY: &str = "Invalid property in `positionTry()` call. It may only contain, positionAnchor, positionArea, inset properties (top, left, insetInline etc.), margin properties, size properties (height, inlineSize, etc.), and self-alignment properties (alignSelf, justifySelf, placeSelf)"; + +pub static VIEW_TRANSITION_CLASS_INVALID_PROPERTY: &str = "Invalid property in `viewTransitionClass()` call. It may only contain group, imagePair, old, and new properties"; + +pub static INVALID_MEDIA_QUERY_SYNTAX: &str = "Invalid media query syntax."; + +pub static SPREAD_NOT_SUPPORTED: &str = + "The spread operator (...) is not supported in this context. Declare each property explicitly."; + +/// A spread whose operand's own properties cannot be read at compile time (e.g. an astral +/// string). +pub static SPREAD_PROPERTIES_UNREADABLE: &str = + "The spread argument's properties could not be read at compile time."; + +pub static EXPRESSION_IS_NOT_A_STRING: &str = + "Expected a string value but received a non-string expression."; + +/// A computed key with no string form at compile time (a function, a lone surrogate, a +/// compiler-internal value). +pub static KEY_HAS_NO_NAME: &str = "The key has no name at compile time."; + +pub static VALUES_MUST_BE_OBJECT: &str = "The values argument must be a plain object."; + +pub static ONLY_OVERRIDE_DEFINE_VARS: &str = + "Can only override variables theme created with defineVars()."; +pub static ONLY_OVERRIDE_DEFINE_VARS_NESTED: &str = + "Can only override variables theme created with unstable_defineVarsNested()."; + +pub static MEMBER_NOT_RESOLVED: &str = + "Could not resolve the member expression. Ensure the object and property are statically known."; + +pub static PROPERTY_NOT_FOUND: &str = + "Property not found on the object. Ensure the key exists and is spelled correctly."; + +pub static EXPECTED_OBJECT_EXPRESSION: &str = "Expected an object expression."; + +pub static VALUE_MUST_BE_STRING: &str = "Expected a string value but received a different type."; + +pub static VALUE_MUST_BE_LITERAL: &str = + "Expected a static literal value (string, number, or boolean)."; + +pub static ARGUMENT_NOT_EXPRESSION: &str = "Function argument must be a static expression."; + +pub static EXPORT_ID_NOT_SET: &str = + "Export identifier is not set. The variable must be bound to a named export."; + +pub static INVALID_UTF8: &str = "String value contains invalid UTF-8 encoding."; + +pub static EXPECTED_CSS_VAR: &str = "Expected a CSS custom property (variable) reference."; + +pub static KEY_MUST_EVAL_TO_STRING: &str = "Style property key must evaluate to a string."; + +pub static VALUE_NOT_EXPRESSION: &str = "Style value must evaluate to a static expression."; + +pub static EVAL_RESULT_EXPECTED: &str = "Expected a value from evaluation result."; + +pub static VAR_DECL_INIT_REQUIRED: &str = "Variable declaration must have an initializer."; + +pub static OBJECT_KEY_MUST_BE_IDENT: &str = "Object key must be a static identifier."; + +pub static THEME_VARS_MUST_BE_OBJECT: &str = "Theme variables must be defined as a plain object."; + +pub static RULE_SET_EMPTY: &str = "The style rule set is empty. At least one rule is required."; + +pub fn expected_call_expression(fn_name: &str) -> String { + format!( + "{}(): Expected a call expression. Ensure the value is a direct function call.", + fn_name + ) +} + +/// An object variable without a `default` key; matches Babel's text, naming the top-level +/// variable. +pub fn missing_default_value(key: &str) -> String { + format!("Default value is not defined for {} variable.", key) +} + +pub fn invalid_define_vars_function_value() -> String { + "Function values in defineVars() must be zero-argument and return a static value supported by defineVars().".to_string() +} + +pub fn cyclic_define_vars_reference(cycle: &str) -> String { + format!( + "Cyclic same-group references in defineVars() are not allowed: {}.", + cycle + ) +} + +pub fn unknown_define_vars_reference(key: &str, dependency: &str) -> String { + format!( + "Unknown same-group reference \"{}\" found while resolving \"{}\" in defineVars().", + dependency, key + ) +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/mod.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/mod.rs new file mode 100644 index 000000000..820b013c8 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/mod.rs @@ -0,0 +1,19 @@ +pub mod api_names; +pub mod common; +pub mod cursor_flip; +pub mod evaluation_errors; +pub mod length_units; +pub mod logical_to_ltr; +pub mod logical_to_rtl; +pub mod long_hand_logical; +pub mod long_hand_physical; +pub mod messages; +pub mod number_properties; +pub mod priorities; +pub mod shorthands_of_longhands; +pub mod shorthands_of_shorthands; +pub mod time_units; +pub mod unitless_number_properties; + +#[cfg(test)] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/number_properties.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/number_properties.rs new file mode 100644 index 000000000..10f270da9 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/number_properties.rs @@ -0,0 +1,12 @@ +use phf::phf_map; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static NUMBER_PROPERTY_SUFFIXIES: phf::Map<&'static str, &'static str> = phf_map! { + "animationDelay"=> "ms", + "animationDuration"=> "ms", + "transitionDelay"=> "ms", + "transitionDuration"=> "ms", + "voiceDuration"=> "ms", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/priorities.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/priorities.rs new file mode 100644 index 000000000..8426b195a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/priorities.rs @@ -0,0 +1,72 @@ +use phf::phf_map; + +pub static PSEUDO_CLASS_PRIORITIES: phf::Map<&'static str, &'static f64> = phf_map! { + ":is" => &40.0, + ":where" => &40.0, + ":not" => &40.0, + ":has" => &45.0, + ":dir" => &50.0, + ":lang" => &51.0, + ":first-child" => &52.0, + ":first-of-type" => &53.0, + ":last-child" => &54.0, + ":last-of-type" => &55.0, + ":only-child" => &56.0, + ":only-of-type" => &57.0, + + ":nth-child" => &60.0, + ":nth-last-child" => &61.0, + ":nth-of-type" => &62.0, + ":nth-last-of-type" => &63.0, // "nth-last-of-type" is the same priority as "nth-of-typ.0e + ":empty" => &70.0, + + ":link" => &80.0, + ":any-link" => &81.0, + ":local-link" => &82.0, + ":target-within" => &83.0, + ":target" => &84.0, + ":visited" => &85.0, + + ":enabled" => &91.0, + ":disabled" => &92.0, + ":required" => &93.0, + ":optional" => &94.0, + ":read-only" => &95.0, + ":read-write" => &96.0, + ":placeholder-shown" => &97.0, + ":in-range" => &98.0, + ":out-of-range" => &99.0, + + ":default" => &100.0, + ":checked" => &101.0, + ":indeterminate" => &101.0, + ":blank" => &102.0, + ":valid" => &103.0, + ":invalid" => &104.0, + ":user-invalid" => &105.0, + + ":autofill" => &110.0, + + ":picture-in-picture" => &120.0, + ":modal" => &121.0, + ":fullscreen" => &122.0, + ":paused" => &123.0, + ":playing" => &124.0, + ":current" => &125.0, + ":past" => &126.0, + ":future" => &127.0, + + ":hover" => &130.0, + ":focusWithin" => &140.0, + ":focus" => &150.0, + ":focusVisible" => &160.0, + ":active" => &170.0, +}; + +pub static AT_RULE_PRIORITIES: phf::Map<&'static str, &'static f64> = phf_map! { + "@supports" => &30.0, + "@media" => &200.0, + "@container" => &300.0, +}; + +pub static PSEUDO_ELEMENT_PRIORITY: f64 = 5000.0; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/shorthands_of_longhands.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/shorthands_of_longhands.rs new file mode 100644 index 000000000..2b14a0335 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/shorthands_of_longhands.rs @@ -0,0 +1,125 @@ +use phf::phf_set; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static SHORTHANDS_OF_LONGHANDS: phf::Set<&'static str> = phf_set! { + // CSS Animations + "animation-range", + + "scroll-timeline", + + + "view-timeline", + + // CSS Backgrounds and Borders + "background-position", + + "border-color", + "border-style", + "border-width", + "border-block-start", // Logical Properties + "border-top", + "border-block-end", // Logical Properties + "border-bottom", + "border-inline-color", // Logical Properties + "border-inline-style", // Logical Properties + "border-inline-width", // Logical Properties + "border-inline-start", // Logical Properties + "border-left", + "border-inline-end", // Logical Properties + "border-right", + + "border-image", + + "border-radius", + + "corner-shape", + + // CSS Basic User Interface + "caret", + + + "outline", + + + // CSS Box Alignment + "grid-gap", // alias for `gap` + "gap", + + "place-content", + "place-items", + "place-self", + + // CSS Box Model + "margin-block", // Logical Properties + "margin-inline", // Logical Properties + + + "overscroll-behavior", + + "padding-block", // Logical Properties + "padding-inline", // Logical Properties + + // CSS Columns + "columns", + + "column-rule", + + // CSS Containment + + "contain-intrinsic-size", + + "container", + + // CSS Flexible Box Layout + "flex", + "flex-flow", + + // CSS Fonts + "font-variant", + + // CSS Grid Layout + "grid-template-areas", + "grid-row", + "grid-column", + + + // CSS Images + + // CSS Inline + + // CSS Lists and Counters + "list-style", + + // CSS Masking + "mask", + "mask-border", + + // CSS Motion Path + "offset", + + // CSS Overflow + "overflow", + + // CSS Positioning + "inset-block", // Logical Properties + "inset-inline", // Logical Properties + + // CSS Scroll Snap + "scroll-margin-block", + "scroll-margin-inline", + + "scroll-padding-block", + "scroll-padding-inline", + + "scroll-snap-type", + + // CSS Text Decoration + "text-decoration", + + "text-emphasis", + + // CSS Transitions + "transition", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/shorthands_of_shorthands.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/shorthands_of_shorthands.rs new file mode 100644 index 000000000..e95f0ca88 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/shorthands_of_shorthands.rs @@ -0,0 +1,40 @@ +use phf::phf_set; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static SHORTHANDS_OF_SHORTHANDS: phf::Set<&'static str> = phf_set! { +// CSS Animations +"animation", + +// CSS Backgrounds and Borders +"background", + +"border", // OF SHORTHANDS! +"border-block", // Logical Properties +"border-inline", // Logical Properties + +// CSS Box Model +"margin", + +"padding", + +// CSS Fonts +"font", + +// CSS Grid Layout +"grid", +"grid-template", +"grid-area", + +// CSS Miscellaneous +"all", // avoid! + +// CSS Positioning +"inset", // Logical Properties + +// CSS Scroll Snap +"scroll-margin", + +"scroll-padding", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/common_constants_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/common_constants_test.rs new file mode 100644 index 000000000..dc4b878f9 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/common_constants_test.rs @@ -0,0 +1,26 @@ +//! Coverage and behavioral tests for shared constants used by the transform +//! pipeline. + +use crate::constants::common::{ + COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES, COLOR_RELATIVE_VALUE_FUNCTIONS, + CSS_CONTENT_FUNCTIONS, CSS_CONTENT_KEYWORDS, +}; + +#[test] +fn lazy_constant_lists_have_expected_shape() { + assert_eq!(COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES.len(), 9); + assert!(COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES.contains(&"oklch")); + assert!(COLOR_FUNCTION_LISTED_NORMALIZED_PROPERTY_VALUES.contains(&"radial-gradient")); + + assert_eq!(COLOR_RELATIVE_VALUE_FUNCTIONS.len(), 10); + assert!(COLOR_RELATIVE_VALUE_FUNCTIONS.contains(&"rgb")); + assert!(COLOR_RELATIVE_VALUE_FUNCTIONS.contains(&"color")); + + assert_eq!(CSS_CONTENT_FUNCTIONS.len(), 7); + assert!(CSS_CONTENT_FUNCTIONS.contains(&"url(")); + assert!(CSS_CONTENT_FUNCTIONS.contains(&"var(--")); + + assert_eq!(CSS_CONTENT_KEYWORDS.len(), 11); + assert!(CSS_CONTENT_KEYWORDS.contains(&"normal")); + assert!(CSS_CONTENT_KEYWORDS.contains(&"revert-layer")); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/evaluation_errors_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/evaluation_errors_test.rs new file mode 100644 index 000000000..7bfd51882 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/evaluation_errors_test.rs @@ -0,0 +1,199 @@ +//! Tests for evaluation error message functions and static constants. + +use crate::constants::{ + common::{VALID_CALLEES, VALUE_ONLY_GLOBALS}, + evaluation_errors::*, +}; + +#[test] +fn test_expression_too_deep() { + assert_eq!( + expression_too_deep(32), + "Expression is too deeply nested to evaluate at compile time.\nAt most 32 levels of nested evaluation are supported.\n\n" + ); + assert!(expression_too_deep(320).contains("At most 320 levels")); +} + +#[test] +fn test_nesting_too_deep_to_carry() { + assert_eq!( + nesting_too_deep_to_carry(8192), + "Expression is nested too deeply to fold at compile time.\nAt most 8192 levels of nested expressions can be carried.\n\n" + ); + + assert_ne!(nesting_too_deep_to_carry(8192), expression_too_deep(8192)); +} + +#[test] +fn test_locale_sensitive_method() { + assert_eq!( + locale_sensitive_method("toLocaleUpperCase"), + "Cannot fold 'toLocaleUpperCase' at compile time.\nIts answer depends on locale data the compiler does not carry.\n\n" + ); + assert!(locale_sensitive_method("localeCompare").contains("'localeCompare'")); +} + +#[test] +fn test_numeric_literal_receiver() { + assert_eq!( + numeric_literal_receiver("toFixed"), + "Cannot call 'toFixed' on a number literal.\nOnly a number a fold produced can be a method receiver.\n\n" + ); +} + +#[test] +fn test_unbounded_amplified_length() { + assert_eq!( + unbounded_amplified_length("repeat", 1_000_000), + "Cannot bound the string 'repeat' would build.\nIts length must resolve to a number of at most 1000000, on a receiver whose own length can be read.\n\n" + ); + assert!(unbounded_amplified_length("padStart", 32).contains("at most 32")); +} + +#[test] +fn test_unbounded_declared_length() { + assert_eq!( + unbounded_declared_length("Array"), + "Cannot bound the array 'Array' would build.\nWrite the elements out, or keep the rest of the expression foldable so its length can be checked.\n\n" + ); +} + +#[test] +fn test_amplified_length_too_large() { + assert_eq!( + amplified_length_too_large("padStart", 1_200_000, 1_200_000, 1, 1_000_000), + "Cannot bound the string 'padStart' would build.\nIt asks for 1200000 characters, and at most 1000000 are supported.\n\n" + ); + assert_eq!( + amplified_length_too_large("repeat", 600_000, 1_200_000, 1, 1_000_000), + "Cannot bound the string 'repeat' would build.\nIt asks for 600000 copies of the value it is called on, which is 1200000 characters, and at most 1000000 are supported.\n\n" + ); + assert_eq!( + amplified_length_too_large("repeat", 3, 3, 4, 10), + "Cannot bound the string 'repeat' would build.\nIt asks for 3 characters once per element of the receiver it is written inside, which is 4 evaluations and 12 characters in all, and at most 10 are supported.\n\n" + ); + assert!(!amplified_length_too_large("repeat", 3, 3, 1, 2).contains("once per element")); + assert!( + amplified_length_too_large("padEnd", 9, 9, 1, 8) + .starts_with("Cannot bound the string 'padEnd' would build.") + ); +} + +#[test] +fn test_unfoldable_fold_result() { + assert_eq!( + unfoldable_fold_result("function"), + "Cannot carry a folded function back from the engine.\nOnly strings, numbers, booleans, null, undefined, arrays and plain objects can be folded.\n\n" + ); + assert!(unfoldable_fold_result("symbol").starts_with("Cannot carry a folded symbol back")); + assert!(unfoldable_fold_result("bigint").starts_with("Cannot carry a folded bigint back")); +} + +#[test] +fn test_engine_threw() { + assert_eq!( + engine_threw( + "reduce", + "TypeError: Reduce of empty array with no initial value" + ), + "Cannot fold 'reduce' at compile time.\nTypeError: Reduce of empty array with no initial value\n\n" + ); + assert!( + engine_threw("unsupported", "TypeError: not a callable function").contains("'unsupported'") + ); +} + +#[test] +fn test_amplification_inside_a_callback() { + assert_eq!( + amplification_inside_a_callback("string", "repeat"), + "Cannot bound the string 'repeat' would build inside a callback.\nThe callback's receiver holds an element count that cannot be read here, so a length written into the source bounds one evaluation rather than the call. Write the receiver's elements out, or write the value out instead.\n\n" + ); + assert!(amplification_inside_a_callback("string", "padStart").contains("'padStart'")); + assert!( + amplification_inside_a_callback("array", "Array").contains("Cannot bound the array 'Array'") + ); +} + +#[test] +fn test_uncallable_printed_fold() { + assert_eq!( + uncallable_printed_fold("trim"), + "Cannot fold 'trim' at compile time.\nThe printed expression did not compile to a function.\n\n" + ); + assert!(uncallable_printed_fold("join").contains("'join'")); +} + +#[test] +fn test_unfoldable_function() { + assert_eq!( + unfoldable_function("upper"), + "Cannot carry the function 'upper' into a fold.\n\ + Its declaration is not one the compiler can evaluate.\n\n" + ); + assert!(unfoldable_function("byLength").contains("'byLength'")); + assert!(!unfoldable_function("byLength").contains("Cannot fold")); +} + +#[test] +fn test_global_as_a_value() { + assert_eq!( + global_as_a_value("Boolean"), + "Cannot carry the global 'Boolean' into a fold.\n\ + The compiler folds what a global answers, not the global itself.\n\n" + ); + for name in VALID_CALLEES.iter().chain(VALUE_ONLY_GLOBALS.iter()) { + assert!(global_as_a_value(name).contains(&format!("'{}'", name))); + assert!(!global_as_a_value(name).contains("constant")); + } +} + +#[test] +fn test_unfoldable_static() { + assert_eq!( + unfoldable_static("Math", "random"), + "Cannot fold 'Math.random' at compile time.\nA fold has to answer from the source alone, and this call does not.\n\n" + ); + assert!(unfoldable_static("Object", "freeze").contains("'Object.freeze'")); + assert_ne!( + unfoldable_static("Object", "assign"), + unfoldable_static("Math", "assign") + ); + for method in [ + "assign", + "defineProperty", + "freeze", + "seal", + "preventExtensions", + ] { + assert!(unfoldable_static("Object", method).contains(&format!("'Object.{}'", method))); + } + assert!(unfoldable_static("Math", "random").starts_with("Cannot fold '")); +} + +#[test] +fn test_engine_did_not_start() { + assert_eq!( + engine_did_not_start("out of memory"), + "The compile-time JavaScript engine could not start.\nout of memory\n\n" + ); + assert!(engine_did_not_start("Realm::create failed").contains("Realm::create failed")); + assert_eq!( + engine_did_not_start(""), + "The compile-time JavaScript engine could not start.\n\n\n" + ); +} + +#[test] +fn test_unfoldable_statement() { + assert_eq!( + unfoldable_statement("for loop"), + "Cannot fold a callback whose body uses a for loop.\n\ + Only a declaration, a branch, a block and a return are read inside a callback body.\n\n" + ); + assert_ne!( + unfoldable_statement("while loop"), + unfoldable_statement("for loop") + ); + assert!(unfoldable_statement("throw").contains("Only a declaration, a branch")); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/messages_test.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/messages_test.rs new file mode 100644 index 000000000..7dfef7677 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/messages_test.rs @@ -0,0 +1,26 @@ +//! Tests for user-facing message generation functions (argument length, static +//! values, etc). + +use crate::constants::messages::*; + +#[test] +fn test_non_static_value_with_empty_api_name_falls_back_to_a() { + assert_eq!( + non_static_value(""), + "Only static values are allowed inside of a () call." + ); +} + +#[test] +fn test_type_asserted_call_value() { + assert_eq!( + type_asserted_call_value("create"), + "create() cannot be written inside a type assertion. Bind the call to a variable and assert the variable." + ); +} + +#[test] +fn test_expected_call_expression() { + let result = expected_call_expression("defineVars"); + assert!(result.starts_with("defineVars(): Expected a call expression")); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/mod.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/mod.rs new file mode 100644 index 000000000..924550974 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/tests/mod.rs @@ -0,0 +1,3 @@ +mod common_constants_test; +mod evaluation_errors_test; +mod messages_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/time_units.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/time_units.rs new file mode 100644 index 000000000..49db8fb7b --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/time_units.rs @@ -0,0 +1,10 @@ +use rustc_hash::FxHashSet; + +use super::number_properties::NUMBER_PROPERTY_SUFFIXIES; + +pub fn get_time_units() -> FxHashSet { + NUMBER_PROPERTY_SUFFIXIES + .keys() + .map(|key| key.to_string()) + .collect() +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/unitless_number_properties.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/unitless_number_properties.rs new file mode 100644 index 000000000..7e1163262 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/constants/unitless_number_properties.rs @@ -0,0 +1,54 @@ +use phf::phf_set; + +// Using MDN data as a source of truth to populate the above sets +// by group in alphabetical order: + +pub static UNITLESS_NUMBER_PROPERTIES: phf::Set<&'static str> = phf_set! { + "WebkitLineClamp", + "animationIterationCount", + "aspectRatio", + "borderImageOutset", + "borderImageSlice", + "borderImageWidth", + "counterSet", + "counterReset", + "columnCount", + "flex", + "flexGrow", + "flexShrink", + "flexOrder", + "gridRow", + "gridRowStart", + "gridRowEnd", + "gridColumn", + "gridColumnStart", + "gridColumnEnd", + "gridArea", + "fontSizeAdjust", + "fontWeight", + "hyphenateLimitChars", + "lineClamp", + "lineHeight", + "maskBorderOutset", + "maskBorderSlice", + "maskBorderWidth", + "opacity", + "order", + "orphans", + "tabSize", + "widows", + "zIndex", + "fillOpacity", + "floodOpacity", + "rotate", + "scale", + "shapeImageThreshold", + "stopOpacity", + "strokeDasharray", + "strokeDashoffset", + "strokeMiterlimit", + "strokeOpacity", + "strokeWidth", + "mathDepth", + "zoom", +}; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/lib.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/lib.rs new file mode 100644 index 000000000..107affd76 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/lib.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod logger; + +#[cfg(test)] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/logger.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/logger.rs new file mode 100644 index 000000000..245cd00b2 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/logger.rs @@ -0,0 +1,2 @@ +/// The branded prefix for all StyleX log messages and diagnostics. +pub const STYLEX_LOG_PREFIX: &str = "[StyleX]"; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/tests.rs b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/tests.rs new file mode 100644 index 000000000..857c3cfee --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-constants/src/tests.rs @@ -0,0 +1,6 @@ +use ctor::ctor; + +#[ctor(unsafe)] +fn init_logger() { + pretty_env_logger::formatted_builder().try_init().ok(); +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/Cargo.toml b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/Cargo.toml new file mode 100644 index 000000000..4fde810da --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/Cargo.toml @@ -0,0 +1,40 @@ +[package] +description = "CSS value parser" +edition.workspace = true +license.workspace = true +name = "stylex_css_parser" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + + +[lib] +crate-type = ["rlib"] +doctest = false + +[dependencies] +cssparser.workspace = true +log.workspace = true +rustc-hash.workspace = true +serde_json = { workspace = true, features = ["preserve_order"] } +swc_core = { workspace = true, features = [ + "common", + "ecma_codegen", + "ecma_parser", + "ecma_plugin_transform", + "ecma_utils", +] } +thiserror.workspace = true + +stylex_macros = { path = "../stylex-macros" } +stylex_utils = { path = "../stylex-utils" } + +[dev-dependencies] +criterion.workspace = true +swc_malloc.workspace = true + + + + +[lints] +workspace = true diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_query.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_query.rs new file mode 100644 index 000000000..c758ff50b --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_query.rs @@ -0,0 +1,1988 @@ +/*! +Media query parsing and representation. +*/ + +use stylex_utils::number::{to_js_string, write_js_number}; + +use crate::{ + CssParseError, + css_types::{Length, calc::Calc}, + token_parser::{TokenParser, tokens}, + token_types::{SimpleToken, TokenList}, +}; +use rustc_hash::FxHashMap; +use std::cell::RefCell; +use std::fmt::{self, Display}; + +/// Fraction for media query values like `(aspect-ratio: 16/9)`. +/// Halves are `f64`: CSS admits `16.5/9`, as does upstream `mediaRuleValueParser`. +#[derive(Debug, Clone, PartialEq)] +pub struct Fraction { + pub numerator: f64, + pub denominator: f64, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Fraction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Through `to_js_string` so the halves are spelled the way JavaScript spells them. + write!( + f, + "{} / {}", + to_js_string(self.numerator), + to_js_string(self.denominator) + ) + } +} + +/// Word rule types for media queries +#[derive(Debug, Clone, PartialEq)] +pub enum WordRule { + Color, + Monochrome, + Grid, + ColorIndex, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for WordRule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WordRule::Color => write!(f, "color"), + WordRule::Monochrome => write!(f, "monochrome"), + WordRule::Grid => write!(f, "grid"), + WordRule::ColorIndex => write!(f, "color-index"), + } + } +} + +/// Media rule values that can appear in media queries +#[derive(Debug, Clone, PartialEq)] +pub enum MediaRuleValue { + Number(f64), + Length(Length), + String(String), + Fraction(Fraction), +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaRuleValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MediaRuleValue::Number(n) => write_js_number(f, *n), + MediaRuleValue::Length(l) => write!(f, "{}", l), + MediaRuleValue::String(s) => write!(f, "{}", s), + MediaRuleValue::Fraction(frac) => write!(f, "{}", frac), + } + } +} + +/// Media keyword for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaKeyword { + pub r#type: String, // Always "media-keyword" + pub key: String, // 'screen' | 'print' | 'all' + pub not: bool, + pub only: bool, // Boolean field for CSS media queries +} + +impl MediaKeyword { + pub fn new(key: impl Into, not: bool, only: bool) -> Self { + Self { + r#type: "media-keyword".to_string(), + key: key.into(), + not, + only, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaKeyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut parts = Vec::new(); + + if self.not { + parts.push("not".to_string()); + } + + if self.only { + parts.push("only".to_string()); + } + + parts.push(self.key.clone()); + write!(f, "{}", parts.join(" ")) + } +} + +/// Media word rule for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaWordRule { + pub r#type: String, // Always "word-rule" + pub key_value: String, // The word rule value +} + +impl MediaWordRule { + pub fn new(key_value: impl Into) -> Self { + Self { + r#type: "word-rule".to_string(), + key_value: key_value.into(), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaWordRule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({})", self.key_value) + } +} + +/// Media rule pair for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaRulePair { + #[allow(dead_code)] + pub r#type: String, // Always "pair" + pub key: String, // Property name + pub value: MediaRuleValue, // Property value +} + +impl MediaRulePair { + pub fn new(key: impl Into, value: MediaRuleValue) -> Self { + Self { + r#type: "pair".to_string(), + key: key.into(), + value, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaRulePair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}: {})", self.key, self.value) + } +} + +/// Media NOT rule for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaNotRule { + #[allow(dead_code)] + pub r#type: String, // Always "not" + pub rule: Box, // Nested rule +} + +impl MediaNotRule { + pub fn new(rule: MediaQueryRule) -> Self { + Self { + r#type: "not".to_string(), + rule: Box::new(rule), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaNotRule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.rule.as_ref() { + MediaQueryRule::And(_) | MediaQueryRule::Or(_) => { + write!(f, "(not ({}))", self.rule) + }, + _ => { + write!(f, "(not {})", self.rule) + }, + } + } +} + +/// Media AND rules for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaAndRules { + pub r#type: String, // Always "and" + pub rules: Vec, // Array of rules +} + +impl MediaAndRules { + pub fn new(rules: Vec) -> Self { + Self { + r#type: "and".to_string(), + rules, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaAndRules { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let rule_strings: Vec = self.rules.iter().map(|rule| rule.to_string()).collect(); + write!(f, "{}", rule_strings.join(" and ")) + } +} + +/// Media OR rules for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaOrRules { + pub r#type: String, // Always "or" + pub rules: Vec, // Array of rules +} + +impl MediaOrRules { + pub fn new(rules: Vec) -> Self { + Self { + r#type: "or".to_string(), + rules, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaOrRules { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let rule_strings: Vec = self.rules.iter().map(|rule| rule.to_string()).collect(); + write!(f, "{}", rule_strings.join(" or ")) + } +} + +/// All media query rules for CSS media queries +#[derive(Debug, Clone, PartialEq)] +#[allow(dead_code)] +pub enum MediaQueryRule { + MediaKeyword(MediaKeyword), + WordRule(MediaWordRule), + Pair(MediaRulePair), + Not(MediaNotRule), + And(MediaAndRules), + Or(MediaOrRules), +} + +/// Main MediaQuery struct for CSS media queries +#[derive(Debug, Clone, PartialEq)] +pub struct MediaQuery { + pub queries: MediaQueryRule, +} + +impl MediaQuery { + pub fn new(queries: MediaQueryRule) -> Self { + Self { + queries: super::media_types::resolve(Self::normalize(queries)), + } + } + + /// Normalize `rule`, consuming each combinator's children instead of deep-cloning them. + pub fn normalize(rule: MediaQueryRule) -> MediaQueryRule { + match rule { + MediaQueryRule::And(and_rules) => { + let mut flattened: Vec = Vec::with_capacity(and_rules.rules.len()); + for r in and_rules.rules { + match Self::normalize(r) { + MediaQueryRule::And(inner_and) => { + flattened.extend(inner_and.rules); + }, + norm => { + flattened.push(norm); + }, + } + } + + let merged = merge_and_simplify_ranges(flattened); + if merged.is_empty() { + return MediaQueryRule::MediaKeyword(MediaKeyword::new("all".to_string(), true, false)); + } + MediaQueryRule::And(MediaAndRules::new(merged)) + }, + MediaQueryRule::Or(or_rules) => { + let normalized_rules: Vec = + or_rules.rules.into_iter().map(Self::normalize).collect(); + MediaQueryRule::Or(MediaOrRules::new(normalized_rules)) + }, + MediaQueryRule::Not(not_rule) => { + let normalized_operand = Self::normalize(*not_rule.rule); + + match normalized_operand { + MediaQueryRule::MediaKeyword(ref keyword) if keyword.key == "all" && keyword.not => { + return MediaQueryRule::MediaKeyword(MediaKeyword::new( + "all".to_string(), + false, + false, + )); + }, + MediaQueryRule::Not(inner_not) => { + return Self::normalize(*inner_not.rule); + }, + _ => {}, + } + + MediaQueryRule::Not(MediaNotRule::new(normalized_operand)) + }, + other => other, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaQueryRule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", MediaQuery::format_queries(self, false)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for MediaQuery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "@media {}", + MediaQuery::format_queries(&self.queries, true) + ) + } +} + +impl MediaQuery { + fn format_queries(queries: &MediaQueryRule, is_top_level: bool) -> String { + match queries { + MediaQueryRule::MediaKeyword(keyword) => { + let prefix = if keyword.not { + "not " + } else if keyword.only { + "only " + } else { + "" + }; + // A media type is never a boolean feature, including under an AND. + format!("{}{}", prefix, keyword.key) + }, + MediaQueryRule::WordRule(word_rule) => { + format!("({})", word_rule.key_value) + }, + MediaQueryRule::Pair(pair) => match &pair.value { + MediaRuleValue::Fraction(frac) => { + // Through the shared formatter: upstream interpolates a JS `number`, which differs + // from Rust's `{}` outside `[1e-6, 1e21)` and at `-0`. + format!( + "({}: {} / {})", + pair.key, + to_js_string(frac.numerator), + to_js_string(frac.denominator) + ) + }, + MediaRuleValue::Length(len) => { + format!("({}: {})", pair.key, len) + }, + MediaRuleValue::String(s) => { + format!("({}: {})", pair.key, s) + }, + MediaRuleValue::Number(n) => { + format!("({}: {})", pair.key, to_js_string(*n)) + }, + }, + MediaQueryRule::Not(not_rule) => match not_rule.rule.as_ref() { + MediaQueryRule::And(_) | MediaQueryRule::Or(_) | MediaQueryRule::Not(_) => { + format!( + "(not ({}))", + MediaQuery::format_queries(not_rule.rule.as_ref(), false) + ) + }, + _ => { + format!( + "(not {})", + MediaQuery::format_queries(not_rule.rule.as_ref(), false) + ) + }, + }, + MediaQueryRule::And(and_rules) => { + let rule_strings: Vec = and_rules + .rules + .iter() + .map(|rule| MediaQuery::format_queries(rule, false)) + .collect(); + rule_strings.join(" and ") + }, + MediaQueryRule::Or(or_rules) => { + // Filter out invalid rules (like empty or rules) + let valid_rules: Vec<&MediaQueryRule> = or_rules + .rules + .iter() + .filter(|r| !matches!(r, MediaQueryRule::Or(or) if or.rules.is_empty())) + .collect(); + + if valid_rules.is_empty() { + return "not all".to_string(); + } + + if valid_rules.len() == 1 { + return MediaQuery::format_queries(valid_rules[0], is_top_level); + } + + let formatted_rules: Vec = valid_rules + .iter() + .map(|rule| match rule { + MediaQueryRule::And(_) | MediaQueryRule::Or(_) => { + let rule_string = MediaQuery::format_queries(rule, false); + if !is_top_level { + format!("({})", rule_string) + } else { + rule_string + } + }, + _ => MediaQuery::format_queries(rule, false), + }) + .collect(); + + if is_top_level { + formatted_rules.join(", ") + } else { + formatted_rules.join(" or ") + } + }, + } + } +} + +impl MediaQuery { + pub fn parser() -> TokenParser { + TokenParser::new( + |tokens| { + if let Ok(Some(SimpleToken::AtKeyword(keyword))) = tokens.peek() { + if keyword == "media" { + let _ = tokens.consume_next_token(); // consume "@media" + + if let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } else { + // "@media" without space or content should be a syntax error + return Err(CssParseError::ParseError { + message: "Expected whitespace or content after @media".to_string(), + }); + } + } else { + return Err(CssParseError::ParseError { + message: "Expected @media at-keyword".to_string(), + }); + } + } else { + // No `@media` prefix: parse just the query part. + } + + let rule = (media_query_rule_parser().run)(tokens)?; + Ok(MediaQuery::new(rule)) + }, + "media_query_parser", + ) + } + + /// Whether every syntactic parenthesis in `input` closes (upstream `_hasBalancedParens`). + /// Parens inside strings or escaped as `\(` are skipped, unlike upstream. + pub fn has_balanced_parens(input: &str) -> bool { + scan_query_structure(input).parens_balanced + } +} + +/// Maximum media query nesting depth; see [`stylex_utils::nesting::MAX_NESTING_DEPTH`]. +const MAX_QUERY_NESTING_DEPTH: usize = stylex_utils::nesting::MAX_NESTING_DEPTH; + +/// Validate a media query string. +/// The text scan bounds tokenizer recursion; [`TokenList::with_depth`] bounds parser recursion. +pub fn validate_media_query(input: &str) -> Result { + // Cached by query text; bounded so a long-lived host does not keep every query. + const CAPACITY: usize = 1024; + thread_local! { + static VALIDATED: RefCell>> = + RefCell::new(FxHashMap::default()); + } + + if let Some(known) = VALIDATED.with(|validated| validated.borrow().get(input).cloned()) { + return known; + } + + let result = validate_media_query_uncached(input); + + VALIDATED.with(|validated| { + let mut validated = validated.borrow_mut(); + if validated.len() >= CAPACITY { + validated.clear(); + } + validated.insert(input.to_owned(), result.clone()); + }); + + result +} + +fn validate_media_query_uncached(input: &str) -> Result { + let structure = scan_query_structure(input); + + if !structure.parens_balanced { + return Err(crate::at_queries::messages::MediaQueryErrors::UNBALANCED_PARENS.to_string()); + } + + // Before the parse rather than during it: tokenizing is what would abort. + if structure.max_paren_depth > MAX_QUERY_NESTING_DEPTH { + return Err(crate::at_queries::messages::MediaQueryErrors::SYNTAX_ERROR.to_string()); + } + + match MediaQuery::parser().parse_to_end(input) { + Ok(media_query) => Ok(media_query), + Err(_) => Err(crate::at_queries::messages::MediaQueryErrors::SYNTAX_ERROR.to_string()), + } +} + +/// Paren balance and depth, from one walk over the raw text before tokenizing. +struct QueryStructure { + /// Every parenthesis that is syntax closes, and none closes too early. + parens_balanced: bool, + /// The deepest parenthesis nesting, outside strings and escapes. + max_paren_depth: usize, +} + +fn scan_query_structure(input: &str) -> QueryStructure { + let mut depth: usize = 0; + let mut max_paren_depth: usize = 0; + let mut chars = input.chars(); + + // Every unbalanced case breaks out here with the depth seen so far. + let parens_balanced = loop { + let Some(ch) = chars.next() else { + break depth == 0; + }; + + match ch { + // A backslash escapes whatever follows, including a quote or a paren. + '\\' => { + if chars.next().is_none() { + break false; + } + }, + '"' | '\'' => { + if !skip_string(&mut chars, ch) { + break false; + } + }, + '(' => { + depth += 1; + max_paren_depth = max_paren_depth.max(depth); + }, + // A close with nothing open is unbalanced. + ')' => match depth.checked_sub(1) { + Some(remaining) => depth = remaining, + None => break false, + }, + _ => {}, + } + }; + + QueryStructure { + parens_balanced, + max_paren_depth, + } +} + +/// Consume up to and including the `quote` that ends a string, reporting +/// whether one was found before the input ran out. +fn skip_string(chars: &mut std::str::Chars<'_>, quote: char) -> bool { + while let Some(ch) = chars.next() { + match ch { + '\\' => { + if chars.next().is_none() { + return false; + } + }, + c if c == quote => return true, + _ => {}, + } + } + + false +} + +/// The dimensions whose `min-`/`max-` bounds merge into a single interval. +const DIMENSIONS: [&str; 2] = ["width", "height"]; + +/// A fresh, empty interval accumulator per entry of [`DIMENSIONS`]. +fn new_dimension_intervals() -> [(&'static str, DimensionIntervals); DIMENSIONS.len()] { + DIMENSIONS.map(|dim| (dim, DimensionIntervals::default())) +} + +/// Which side of a dimension a rule bounds: `min-` from below, `max-` from above. +#[derive(Clone, Copy)] +enum Bound { + Min, + Max, +} + +/// The numeric `min-`/`max-` constraint a rule places on `dim`, and whether it is negated. +/// `None` means the rule is not interval-mergeable for `dim`. +fn dimension_constraint<'a>( + rule: &'a MediaQueryRule, + dim: &str, +) -> Option<(Bound, &'a Length, bool)> { + let (inner, negated) = match rule { + MediaQueryRule::Not(not_rule) => (not_rule.rule.as_ref(), true), + other => (other, false), + }; + + let MediaQueryRule::Pair(pair) = inner else { + return None; + }; + + let bound = match (pair.key.strip_prefix("min-"), pair.key.strip_prefix("max-")) { + (Some(rest), _) if rest == dim => Bound::Min, + (_, Some(rest)) if rest == dim => Bound::Max, + _ => return None, + }; + + match &pair.value { + MediaRuleValue::Length(length) => Some((bound, length, negated)), + _ => None, + } +} + +/// Nudge that makes a negated or strict bound exclusive: `not (min-height: 600px)` becomes +/// `max-height: 599.99px`. Negated pixel `min-width` uses [`MAX_WIDTH_EPSILON`] instead. +const EPSILON: f64 = 0.01; + +/// Nudge for a negated pixel `min-width` (matches upstream): `99.98px`, clearing browser +/// device-pixel rounding that a hundredth does not. +const MAX_WIDTH_EPSILON: f64 = 0.02; + +/// The interval a single constraint imposes on its dimension. +/// Bounds are emitted as the query's `min-`/`max-` text, so they must be `f64` (matches upstream). +fn constraint_interval( + bound: Bound, + dimension: &str, + length: &Length, + negated: bool, +) -> (f64, f64) { + let value = length.value; + + // A negated pixel `min-width` steps by the wider nudge. + let negated_min_step = if dimension == "width" && length.unit.eq_ignore_ascii_case("px") { + MAX_WIDTH_EPSILON + } else { + EPSILON + }; + + // A negated `min-` bound is a `max-` bound just below it, and vice versa. + match (bound, negated) { + (Bound::Min, false) => (value, f64::INFINITY), + (Bound::Max, false) => (f64::NEG_INFINITY, value), + (Bound::Min, true) => (f64::NEG_INFINITY, value - negated_min_step), + (Bound::Max, true) => (value + EPSILON, f64::INFINITY), + } +} + +/// Accumulated numeric constraints on one dimension; a unit differing from the first interval's +/// sets `unit_conflict`, which makes the whole merge bail out. +#[derive(Default)] +struct DimensionIntervals { + intervals: Vec<(f64, f64)>, + unit: String, + unit_conflict: bool, +} + +impl DimensionIntervals { + /// Add `interval`, flagging a unit that disagrees with the first interval's. + fn push(&mut self, interval: (f64, f64), unit: &str) { + match self.intervals.is_empty() { + true => self.unit = unit.to_string(), + false => self.unit_conflict |= self.unit != unit, + } + + self.intervals.push(interval); + } + + /// Intersect every recorded interval; `None` means the constraints contradict. + fn intersect(&self) -> Option<(f64, f64)> { + let mut lower = f64::NEG_INFINITY; + let mut upper = f64::INFINITY; + + for (l, u) in &self.intervals { + if *l > lower { + lower = *l; + } + if *u < upper { + upper = *u; + } + } + + match lower > upper { + true => None, + false => Some((lower, upper)), + } + } +} + +/// Distribution budget in branch nodes (`2^depth * rules.len()`); past it the merge gives up +/// and emits the rules as written. Upstream has no cap (it exhausts heap or string length). +const MAX_DISTRIBUTION_NODES: u64 = 1 << 18; + +/// How many times distributing `rules` can split: the sum over the list of each negation's depth. +fn distribution_depth(rules: &[MediaQueryRule]) -> u32 { + rules + .iter() + .map(|rule| match rule { + MediaQueryRule::Not(not_rule) => negation_depth(not_rule.rule.as_ref()), + _ => 0, + }) + .sum() +} + +/// How many splits `not operand` costs: one per binary `and`, recursively. +fn negation_depth(operand: &MediaQueryRule) -> u32 { + match as_binary_and(operand) { + Some((left, right)) => 1 + negation_depth(left).max(negation_depth(right)), + None => 0, + } +} + +/// The two operands of an `and` of exactly two, or `None` for anything else. +fn as_binary_and(rule: &MediaQueryRule) -> Option<(&MediaQueryRule, &MediaQueryRule)> { + match rule { + MediaQueryRule::And(and_rules) => match and_rules.rules.as_slice() { + [left, right] => Some((left, right)), + _ => None, + }, + _ => None, + } +} + +/// Simplify an `and` list's ranges (upstream `mergeAndSimplifyRanges`); a list too large to +/// expand is returned unchanged rather than erroring. +fn merge_and_simplify_ranges(rules: Vec) -> Vec { + // Checked up front so the result is all-merged or all-as-written; saturating so an absurd + // input lands on "too big" rather than overflowing. + let branches = 1u64 + .checked_shl(distribution_depth(&rules)) + .unwrap_or(u64::MAX); + + if branches.saturating_mul(rules.len() as u64) > MAX_DISTRIBUTION_NODES { + return rules; + } + + merge_intervals_for_and(rules) +} + +/// Merge an `and` list's width/height constraints into one interval per dimension. +/// Returns empty on contradiction (`not all`), `rules` unchanged if not mergeable, else the merge. +fn merge_intervals_for_and(rules: Vec) -> Vec { + let mut dimensions = new_dimension_intervals(); + + // Handle DeMorgan's law: not (A and B) = (not A) or (not B) + for (index, rule) in rules.iter().enumerate() { + if let MediaQueryRule::Not(not_rule) = rule + && let Some((left, right)) = as_binary_and(not_rule.rule.as_ref()) + { + // Every rule but this one, plus one negated operand: built at `rules.len()` capacity. + let branch_without_current = |negated: &MediaQueryRule| { + let mut branch = Vec::with_capacity(rules.len()); + branch.extend_from_slice(&rules[..index]); + branch.extend_from_slice(&rules[index + 1..]); + branch.push(MediaQueryRule::Not(MediaNotRule::new(negated.clone()))); + branch + }; + + let left_branch_rules = branch_without_current(left); + let right_branch_rules = branch_without_current(right); + + let left_branch = merge_intervals_for_and(left_branch_rules); + let right_branch = merge_intervals_for_and(right_branch_rules); + + // Empty branches are dropped; a contradictory one survives (an empty `or`, printed as + // `not all`). A multi-rule branch is re-wrapped in `and`. + let or_rules: Vec = [left_branch, right_branch] + .into_iter() + .filter(|branch| !branch.is_empty()) + .map(|mut branch| match branch.len() { + 1 => branch.remove(0), + _ => MediaQueryRule::And(MediaAndRules::new(branch)), + }) + .collect(); + + return vec![MediaQueryRule::Or(MediaOrRules::new(or_rules))]; + } + } + + for rule in &rules { + let mut mergeable = false; + + for (dim, state) in dimensions.iter_mut() { + let Some((bound, length, negated)) = dimension_constraint(rule, dim) else { + continue; + }; + + state.push( + constraint_interval(bound, dim, length, negated), + &length.unit, + ); + + mergeable = true; + break; + } + + // Anything that is not a numeric width/height constraint blocks the merge. + if !mergeable { + return rules; + } + } + + // Mixed units cannot be intersected numerically; leave the rules untouched. + if dimensions.iter().any(|(_, state)| state.unit_conflict) { + return rules; + } + + let mut result = Vec::new(); + + for (dim, state) in &dimensions { + if state.intervals.is_empty() { + continue; + } + + let Some((lower, upper)) = state.intersect() else { + return Vec::new(); + }; + + // Matches upstream `!= ±Infinity` (not `is_finite`): an overflowed bound is kept, printing + // invalid CSS the browser drops, rather than dropped to leave a rule that applies. + if lower != f64::NEG_INFINITY { + result.push(MediaQueryRule::Pair(MediaRulePair::new( + format!("min-{dim}"), + MediaRuleValue::Length(Length::new(lower, state.unit.clone())), + ))); + } + + if upper != f64::INFINITY { + result.push(MediaQueryRule::Pair(MediaRulePair::new( + format!("max-{dim}"), + MediaRuleValue::Length(Length::new(upper, state.unit.clone())), + ))); + } + } + + if result.is_empty() { rules } else { result } +} + +/// Extract the ident from a token the parser guarantees is an `Ident`. +fn extract_ident_as_media_type(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + "all".to_string() + } +} + +/// Extract the ident from a token the parser guarantees is an `Ident` (word rule context). +fn extract_ident_as_word_rule(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + "color".to_string() + } +} + +/// Convert a Dimension token to a MediaRuleValue::Length. +fn dimension_to_media_rule_value(token: SimpleToken) -> MediaRuleValue { + if let SimpleToken::Dimension { value, unit } = token { + MediaRuleValue::Length(Length::new(value, unit)) + } else { + MediaRuleValue::Number(0.0) + } +} + +/// Convert an Ident token to a MediaRuleValue::String. +fn ident_to_media_rule_value(token: SimpleToken) -> MediaRuleValue { + if let SimpleToken::Ident(value) = token { + MediaRuleValue::String(value) + } else { + MediaRuleValue::String(String::new()) + } +} + +/// Convert a Number token to a MediaRuleValue::Number. +fn number_to_media_rule_value(token: SimpleToken) -> MediaRuleValue { + if let SimpleToken::Number(value) = token { + MediaRuleValue::Number(value) + } else { + MediaRuleValue::Number(0.0) + } +} + +/// Which end of a range a bound is, and so which way its epsilon moves. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum BoundEnd { + /// A lower bound: a strict `>` becomes an inclusive `>=` one epsilon up. + Min, + /// An upper bound: a strict `<` becomes an inclusive `<=` one epsilon down. + Max, +} + +/// Shift a length bound by one epsilon so a strict inequality becomes inclusive (upstream +/// `adjustDimension`); non-`Length` values are left alone. +fn adjust_dimension(value: &mut MediaRuleValue, end: BoundEnd, epsilon: f64) { + if let MediaRuleValue::Length(length) = value { + match end { + BoundEnd::Min => length.value += epsilon, + BoundEnd::Max => length.value -= epsilon, + } + } +} + +/// Basic media type parser: screen | print | all +fn basic_media_type_parser() -> TokenParser { + tokens::ident() + .map(extract_ident_as_media_type, Some("extract_media_type")) + .where_fn( + |value| matches!(value.as_str(), "screen" | "print" | "all"), + Some("valid_media_type"), + ) +} + +/// Media keyword parser with optional not/only modifiers +fn media_keyword_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let mut not_value = false; + let mut only_value = false; // Default to false instead of None + + if let Ok(Some(SimpleToken::Ident(val))) = tokens.peek() + && val == "not" + { + let _ = tokens.consume_next_token(); // consume "not" + not_value = true; + + skip_whitespace(tokens); + } + + if let Ok(Some(SimpleToken::Ident(val))) = tokens.peek() + && val == "only" + { + let _ = tokens.consume_next_token(); // consume "only" + only_value = true; + + skip_whitespace(tokens); + } + + // Matches upstream: `not only screen` parses and prints as `not screen`; + // `(not only screen)` and `only not screen` stay refused. + if not_value { + only_value = false; + } + + let media_type = (basic_media_type_parser().run)(tokens)?; + + Ok(MediaQueryRule::MediaKeyword(MediaKeyword::new( + media_type, not_value, only_value, + ))) + }, + "media_keyword_parser", + ) +} + +/// Media word rule parser for (color), (monochrome), etc. +fn media_word_rule_parser() -> TokenParser { + tokens::ident() + .map(extract_ident_as_word_rule, Some("extract_word_rule")) + .where_fn( + |value| { + matches!( + value.as_str(), + "color" | "monochrome" | "grid" | "color-index" + ) + }, + Some("valid_word_rule"), + ) + .surrounded_by( + TokenParser::::token(SimpleToken::LeftParen, Some("OpenParen")), + Some(TokenParser::::token( + SimpleToken::RightParen, + Some("CloseParen"), + )), + ) + .map( + |keyword| MediaQueryRule::WordRule(MediaWordRule::new(keyword)), + Some("create_word_rule"), + ) +} + +fn media_rule_value_parser() -> TokenParser { + TokenParser::one_of(vec![ + Calc::parser().map( + |calc| MediaRuleValue::String(calc.to_string()), + Some("calc_to_string"), + ), + // Dimensions (e.g., 768px) + tokens::dimension().map(dimension_to_media_rule_value, Some("dimension_to_length")), + tokens::ident().map(ident_to_media_rule_value, Some("ident_to_string")), + // Fraction parsing (number / number) like aspect-ratio: 16/9 + TokenParser::new( + |tokens| { + let first_num = if let Ok(Some(SimpleToken::Number(value))) = tokens.consume_next_token() { + value + } else { + return Err(CssParseError::ParseError { + message: "Expected first number in fraction".to_string(), + }); + }; + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::Delim(ch))) = tokens.consume_next_token() { + if ch != '/' { + return Err(CssParseError::ParseError { + message: "Expected '/' in fraction".to_string(), + }); + } + } else { + return Err(CssParseError::ParseError { + message: "Expected '/' delimiter".to_string(), + }); + } + + skip_whitespace(tokens); + + let second_num = if let Ok(Some(SimpleToken::Number(value))) = tokens.consume_next_token() { + value + } else { + return Err(CssParseError::ParseError { + message: "Expected second number in fraction".to_string(), + }); + }; + + Ok(MediaRuleValue::Fraction(Fraction { + numerator: first_num, + denominator: second_num, + })) + }, + "fraction_parser", + ), + // Numbers (must be last to avoid consuming numbers that are part of fractions) + tokens::number().map(number_to_media_rule_value, Some("number_to_value")), + ]) +} + +/// Simple pair parser for (key: value) media features +fn simple_pair_parser(value_parser: TokenParser) -> TokenParser { + let value_parser_rc = value_parser.run.clone(); // Clone the Rc + + TokenParser::new( + move |tokens| { + if let Ok(Some(SimpleToken::LeftParen)) = tokens.consume_next_token() { + // Good, we have opening paren + } else { + return Err(CssParseError::ParseError { + message: "Expected opening parenthesis".to_string(), + }); + } + + skip_whitespace(tokens); + + let key = if let Ok(Some(SimpleToken::Ident(key_name))) = tokens.consume_next_token() { + key_name + } else { + return Err(CssParseError::ParseError { + message: "Expected media feature name".to_string(), + }); + }; + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::Colon)) = tokens.consume_next_token() { + // Good, we have colon + } else { + return Err(CssParseError::ParseError { + message: "Expected colon after media feature name".to_string(), + }); + } + + skip_whitespace(tokens); + + let value = (value_parser_rc)(tokens)?; + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::RightParen)) = tokens.consume_next_token() { + // Good, we have closing paren + } else { + return Err(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + }); + } + + Ok(MediaQueryRule::Pair(MediaRulePair::new(key, value))) + }, + "simple_pair_parser", + ) +} + +/// Combined inequality parser - handles both forward and reversed inequalities +fn combined_inequality_parser() -> TokenParser { + TokenParser::one_of(vec![ + media_inequality_rule_parser(), // Forward: (width <= 1250px) + media_inequality_rule_parser_reversed(), // Reversed: (1250px >= width) + ]) +} + +/// Forward inequality parser: (width <= 1250px) or (width < 1250px) +fn media_inequality_rule_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let open_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected opening parenthesis".to_string(), + })?; + if !matches!(open_token, SimpleToken::LeftParen) { + return Err(CssParseError::ParseError { + message: format!("Expected '(' token, got {:?}", open_token), + }); + } + + skip_whitespace(tokens); + + let key_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected property name".to_string(), + })?; + let key = if let SimpleToken::Ident(name) = key_token { + if name == "width" || name == "height" { + name + } else { + return Err(CssParseError::ParseError { + message: format!("Expected 'width' or 'height', got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected identifier, got {:?}", key_token), + }); + }; + + skip_whitespace(tokens); + + let op_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected comparison operator".to_string(), + })?; + let op = if let SimpleToken::Delim(op_char) = op_token { + if op_char == '<' || op_char == '>' { + op_char + } else { + return Err(CssParseError::ParseError { + message: format!("Expected '<' or '>', got '{}'", op_char), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected delimiter, got {:?}", op_token), + }); + }; + + skip_whitespace(tokens); + + let has_equals = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() { + let _ = tokens.consume_next_token(); + true + } else { + false + }; + + skip_whitespace(tokens); + + let dim_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected dimension value".to_string(), + })?; + let mut dimension = if let SimpleToken::Dimension { value, unit } = dim_token { + Length::new(value, unit) + } else { + return Err(CssParseError::ParseError { + message: format!("Expected dimension, got {:?}", dim_token), + }); + }; + + skip_whitespace(tokens); + + let close_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + if !has_equals { + if op == '>' { + // (width > 400px) -> min-width: 400.01px + dimension.value += EPSILON; + } else { + // (width < 400px) -> max-width: 399.99px + dimension.value -= EPSILON; + } + } + + // Convert to final key: (width < 1250px) becomes max-width + let final_key = if op == '>' { + format!("min-{}", key) + } else { + format!("max-{}", key) + }; + + Ok(MediaQueryRule::Pair(MediaRulePair::new( + final_key, + MediaRuleValue::Length(dimension), + ))) + }, + "media_inequality_rule_parser", + ) +} + +/// Reversed inequality parser: (1250px >= width) or (1250px > width) +fn media_inequality_rule_parser_reversed() -> TokenParser { + TokenParser::new( + |tokens| { + let open_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected opening parenthesis".to_string(), + })?; + if !matches!(open_token, SimpleToken::LeftParen) { + return Err(CssParseError::ParseError { + message: format!("Expected '(' token, got {:?}", open_token), + }); + } + + skip_whitespace(tokens); + + let dim_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected dimension value".to_string(), + })?; + let dimension = if let SimpleToken::Dimension { value, unit } = dim_token { + MediaRuleValue::Length(Length::new(value, unit)) + } else { + return Err(CssParseError::ParseError { + message: format!("Expected dimension, got {:?}", dim_token), + }); + }; + + skip_whitespace(tokens); + + let op_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected comparison operator".to_string(), + })?; + let op = if let SimpleToken::Delim(op_char) = op_token { + if op_char == '<' || op_char == '>' { + op_char + } else { + return Err(CssParseError::ParseError { + message: format!("Expected '<' or '>', got '{}'", op_char), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected delimiter, got {:?}", op_token), + }); + }; + + skip_whitespace(tokens); + + let has_equals = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() { + let _ = tokens.consume_next_token(); + true + } else { + false + }; + + skip_whitespace(tokens); + + let key_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected property name".to_string(), + })?; + let key = if let SimpleToken::Ident(name) = key_token { + if name == "width" || name == "height" { + name + } else { + return Err(CssParseError::ParseError { + message: format!("Expected 'width' or 'height', got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected identifier, got {:?}", key_token), + }); + }; + + skip_whitespace(tokens); + + let close_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + let mut adjusted_dimension = dimension; + if !has_equals { + // `>` in a reversed inequality reads as an upper bound: `100px > width`. + let end = if op == '>' { + BoundEnd::Max + } else { + BoundEnd::Min + }; + adjust_dimension(&mut adjusted_dimension, end, EPSILON); + } + + // Convert to final key: (1250px > width) becomes max-width + let final_key = if op == '>' { + format!("max-{}", key) + } else { + format!("min-{}", key) + }; + + Ok(MediaQueryRule::Pair(MediaRulePair::new( + final_key, + adjusted_dimension, + ))) + }, + "media_inequality_rule_parser_reversed", + ) +} + +/// Double inequality parser: (500px <= width <= 1000px) +fn double_inequality_rule_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let open_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected opening parenthesis".to_string(), + })?; + if !matches!(open_token, SimpleToken::LeftParen) { + return Err(CssParseError::ParseError { + message: format!("Expected '(' token, got {:?}", open_token), + }); + } + + skip_whitespace(tokens); + + let lower_dim_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected lower bound dimension".to_string(), + })?; + let lower_dimension = if let SimpleToken::Dimension { value, unit } = lower_dim_token { + MediaRuleValue::Length(Length::new(value, unit)) + } else { + return Err(CssParseError::ParseError { + message: format!("Expected dimension, got {:?}", lower_dim_token), + }); + }; + + skip_whitespace(tokens); + + let op1_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected first comparison operator".to_string(), + })?; + let _op1 = if let SimpleToken::Delim(op_char) = op1_token { + if op_char == '<' || op_char == '>' { + op_char + } else { + return Err(CssParseError::ParseError { + message: format!("Expected '<' or '>', got '{}'", op_char), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected delimiter, got {:?}", op1_token), + }); + }; + + skip_whitespace(tokens); + + let _eq1 = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() { + let _ = tokens.consume_next_token(); + true + } else { + false + }; + + skip_whitespace(tokens); + + let key_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected property name".to_string(), + })?; + let key = if let SimpleToken::Ident(name) = key_token { + if name == "width" || name == "height" { + name + } else { + return Err(CssParseError::ParseError { + message: format!("Expected 'width' or 'height', got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected identifier, got {:?}", key_token), + }); + }; + + skip_whitespace(tokens); + + let op2_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected second comparison operator".to_string(), + })?; + let _op2 = if let SimpleToken::Delim(op_char) = op2_token { + if op_char == '<' || op_char == '>' { + op_char + } else { + return Err(CssParseError::ParseError { + message: format!("Expected '<' or '>', got '{}'", op_char), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected delimiter, got {:?}", op2_token), + }); + }; + + skip_whitespace(tokens); + + let _eq2 = if let Ok(Some(SimpleToken::Delim('='))) = tokens.peek() { + let _ = tokens.consume_next_token(); + true + } else { + false + }; + + skip_whitespace(tokens); + + let upper_dim_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected upper bound dimension".to_string(), + })?; + let upper_dimension = if let SimpleToken::Dimension { value, unit } = upper_dim_token { + MediaRuleValue::Length(Length::new(value, unit)) + } else { + return Err(CssParseError::ParseError { + message: format!("Expected dimension, got {:?}", upper_dim_token), + }); + }; + + skip_whitespace(tokens); + + let close_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + // Matches upstream `doubleInequalityRuleParser`: each bound's key follows its own operator, + // so a mixed-direction range puts both bounds on one side; the epsilon follows the key. + let end_for = |op: char| match op { + '>' => BoundEnd::Max, + _ => BoundEnd::Min, + }; + + let lower_end = end_for(_op1); + // The upper operator reads the other way: `width > B` is a lower bound. + let upper_end = match _op2 { + '>' => BoundEnd::Min, + _ => BoundEnd::Max, + }; + + let key_for = |end: BoundEnd| match end { + BoundEnd::Min => format!("min-{}", key), + BoundEnd::Max => format!("max-{}", key), + }; + + let mut lower_value = lower_dimension; + let mut upper_value = upper_dimension; + + if !_eq1 { + adjust_dimension(&mut lower_value, lower_end, EPSILON); + } + + if !_eq2 { + adjust_dimension(&mut upper_value, upper_end, EPSILON); + } + + Ok(MediaQueryRule::And(MediaAndRules::new(vec![ + MediaQueryRule::Pair(MediaRulePair::new(key_for(lower_end), lower_value)), + MediaQueryRule::Pair(MediaRulePair::new(key_for(upper_end), upper_value)), + ]))) + }, + "double_inequality_rule_parser", + ) +} + +/// Enhanced NOT parser that handles complex nested expressions +fn leading_not_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let not_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected 'not' keyword".to_string(), + })?; + if let SimpleToken::Ident(keyword) = not_token { + if keyword != "not" { + return Err(CssParseError::ParseError { + message: format!("Expected 'not', got '{}'", keyword), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected identifier, got {:?}", not_token), + }); + } + + let whitespace_token = + tokens + .consume_next_token() + .ok() + .flatten() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace after 'not'".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace, got {:?}", whitespace_token), + }); + } + + if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() + && keyword == "only" + { + return Err(CssParseError::ParseError { + message: "Media query modifiers 'not' and 'only' cannot be combined".to_string(), + }); + } + + // Through the depth budget: a chain of bare `not`s recurses with no parentheses to count. + let inner_rule = tokens.with_depth(|tokens| (normal_rule_parser().run)(tokens))?; + Ok(MediaQueryRule::Not(MediaNotRule::new(inner_rule))) + }, + "leading_not_parser", + ) +} + +/// This parser specifically handles "(not ...)" patterns +fn parenthesized_not_parser() -> TokenParser { + TokenParser::new( + |tokens| { + if let Ok(Some(SimpleToken::LeftParen)) = tokens.peek() { + let _ = tokens.consume_next_token(); // consume '(' + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() { + if keyword == "not" { + let _ = tokens.consume_next_token(); // consume "not" + + if let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } else { + return Err(CssParseError::ParseError { + message: "Expected whitespace after 'not' in parenthesized expression".to_string(), + }); + } + + if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() + && keyword == "only" + { + return Err(CssParseError::ParseError { + message: "Media query modifiers 'not' and 'only' cannot be combined".to_string(), + }); + } + + let inner_rule = tokens.with_depth(|tokens| (normal_rule_parser().run)(tokens))?; + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() { + let _ = tokens.consume_next_token(); // consume ')' + Ok(MediaQueryRule::Not(MediaNotRule::new(inner_rule))) + } else { + Err(CssParseError::ParseError { + message: "Expected closing parenthesis after parenthesized NOT expression" + .to_string(), + }) + } + } else { + Err(CssParseError::ParseError { + message: "Expected 'not' keyword in parenthesized NOT expression".to_string(), + }) + } + } else { + Err(CssParseError::ParseError { + message: "Expected 'not' keyword in parenthesized NOT expression".to_string(), + }) + } + } else { + Err(CssParseError::ParseError { + message: "Expected opening parenthesis for parenthesized NOT expression".to_string(), + }) + } + }, + "parenthesized_not_parser", + ) +} + +/// Parse a whole media query: a comma-separated list of conditions. +/// `or` groups inside one comma segment and segments group above it; flattening changes output. +fn media_query_rule_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let mut segments = vec![(or_combinator_parser().run)(tokens)?]; + + loop { + let checkpoint = tokens.save_position(); + skip_whitespace(tokens); + + if !matches!(tokens.peek(), Ok(Some(SimpleToken::Comma))) { + let _ = tokens.restore_position(checkpoint); + break; + } + + let _ = tokens.consume_next_token(); + skip_whitespace(tokens); + segments.push((or_combinator_parser().run)(tokens)?); + } + + Ok(collapse_single_rule(segments, |segments| { + MediaQueryRule::Or(MediaOrRules::new(segments)) + })) + }, + "media_query_rule_parser", + ) +} + +/// Consume any run of whitespace tokens, leaving the next real token in place. +fn skip_whitespace(tokens: &mut TokenList) { + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } +} + +/// The sole rule when `rules` holds exactly one, otherwise `combine(rules)`. +fn collapse_single_rule( + rules: Vec, + combine: impl FnOnce(Vec) -> MediaQueryRule, +) -> MediaQueryRule { + match <[MediaQueryRule; 1]>::try_from(rules) { + Ok([only]) => only, + Err(rules) => combine(rules), + } +} + +/// Parse OR-separated media query rules (comma-separated OR "or" keyword) +fn or_combinator_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let mut rules = Vec::new(); + + // Past the guards below, both flags are false for every operand. + let first = parse_and_list(tokens)?; + let first_combinability = first.combinability; + rules.push(first.rule); + + // A comma also disjoins, but binds more loosely and is handled a level up. + loop { + let checkpoint = tokens.save_position(); + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() + && keyword == "or" + { + // The left operand is only judged once an `or` follows it. + first_combinability.refuse_as_or_operand()?; + + let _ = tokens.consume_next_token(); // consume "or" + skip_whitespace(tokens); + + let operand = parse_and_list(tokens)?; + operand.combinability.refuse_as_or_operand()?; + + rules.push(operand.rule); + continue; + } + + // No more `or`: restore position and stop. + let _ = tokens.restore_position(checkpoint); + break; + } + + Ok(collapse_single_rule(rules, |rules| { + MediaQueryRule::Or(MediaOrRules::new(rules)) + })) + }, + "or_combinator_parser", + ) +} + +/// What one `and` list at a level turned out to be. +/// The flags record unparenthesized spellings, which `rule` alone cannot distinguish. +struct AndList { + rule: MediaQueryRule, + combinability: Combinability, +} + +/// The two unparenthesized spellings CSS restricts. +#[derive(Clone, Copy)] +struct Combinability { + /// An `and` keyword joined this list, with no parentheses around it. + joined_by_and: bool, + /// The list is a bare `not `, which CSS makes the whole condition. + is_bare_negation: bool, +} + +impl Combinability { + /// Refuse a list CSS does not let stand as an `or` operand (mixed `and`/`or`, or bare `not`). + fn refuse_as_or_operand(self) -> Result<(), CssParseError> { + if self.joined_by_and { + return Err(uncombinable(MIXED_COMBINATORS)); + } + + if self.is_bare_negation { + return Err(uncombinable(COMBINED_NEGATION)); + } + + Ok(()) + } +} + +/// Whether the next token is a bare `not`; only one that yields a `Not` rule is restricted +/// (`not screen and ...` is a media type query). +fn peeks_bare_not(tokens: &mut TokenList) -> bool { + let checkpoint = tokens.save_position(); + skip_whitespace(tokens); + + let found = matches!(tokens.peek(), Ok(Some(SimpleToken::Ident(keyword))) if keyword == "not"); + + let _ = tokens.restore_position(checkpoint); + found +} + +/// One condition takes `and`s or `or`s, never both. +const MIXED_COMBINATORS: &str = "`and` and `or` cannot be mixed without parentheses"; + +/// A bare `not` is the whole condition, not an operand in one. +const COMBINED_NEGATION: &str = "a `not` condition cannot be combined without parentheses"; + +/// The error a spelling CSS does not define earns. +fn uncombinable(reason: &'static str) -> CssParseError { + CssParseError::ParseError { + message: reason.to_string(), + } +} + +/// Parse an `and`-separated list of media query rules. +fn parse_and_list(tokens: &mut TokenList) -> Result { + let leading_not = peeks_bare_not(tokens); + let first_rule = (normal_rule_parser().run)(tokens)?; + let is_bare_negation = leading_not && matches!(first_rule, MediaQueryRule::Not(_)); + + let mut rules = vec![first_rule]; + let mut joined_by_and = false; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + // Check whether the next non-whitespace token is `and`. + let checkpoint = tokens.save_position(); + skip_whitespace(tokens); + + let is_and = matches!(tokens.peek(), Ok(Some(SimpleToken::Ident(keyword))) if keyword == "and"); + if !is_and { + let _ = tokens.restore_position(checkpoint); + break; + } + + if is_bare_negation { + return Err(uncombinable(COMBINED_NEGATION)); + } + + let _ = tokens.consume_next_token(); // consume "and" + skip_whitespace(tokens); + + // A bare negation is allowed only straight after a media type's `and` + // (``); `screen and (a) and not (b)` is still refused. + let follows_a_media_type = matches!(rules.as_slice(), [MediaQueryRule::MediaKeyword(_)]); + + let operand_leading_not = peeks_bare_not(tokens); + let rule = (normal_rule_parser().run)(tokens)?; + if operand_leading_not && !follows_a_media_type && matches!(rule, MediaQueryRule::Not(_)) { + return Err(uncombinable(COMBINED_NEGATION)); + } + + rules.push(rule); + joined_by_and = true; + } + + Ok(AndList { + rule: collapse_single_rule(rules, |rules| { + MediaQueryRule::And(MediaAndRules::new(rules)) + }), + combinability: Combinability { + joined_by_and, + is_bare_negation, + }, + }) +} + +/// Normal rule parser that combines all rule types +fn normal_rule_parser() -> TokenParser { + TokenParser::one_of(vec![ + // Media keywords first, so `not screen` / `only print` are keyword rules, not NOT rules. + media_keyword_parser(), + parenthesized_not_parser(), + leading_not_parser(), + parenthesized_expression_parser(), + // (500px <= width <= 1000px) + double_inequality_rule_parser(), + // (width <= 1250px) and (1250px >= width) + combined_inequality_parser(), + // (color), (monochrome), (grid), (color-index) + media_word_rule_parser(), + // (min-width: 768px) + simple_pair_parser(media_rule_value_parser()), + ]) +} + +/// Parse parenthesized expressions, e.g. `(not (max-width: 1024px))`, `((a) and (b))`. +fn parenthesized_expression_parser() -> TokenParser { + TokenParser::new( + |tokens| { + if let Ok(Some(SimpleToken::LeftParen)) = tokens.peek() { + let _ = tokens.consume_next_token(); // consume '(' + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() + && keyword == "not" + { + let not_rule = (leading_not_parser().run)(tokens)?; + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() { + let _ = tokens.consume_next_token(); // consume ')' + return Ok(not_rule); + } else { + return Err(CssParseError::ParseError { + message: "Expected closing parenthesis after parenthesized NOT expression" + .to_string(), + }); + } + } + + // `or` parser, not `and`: `((a) or (b)) and (c)` is valid CSS and accepted upstream. + let inner_expression = tokens.with_depth(|tokens| (or_combinator_parser().run)(tokens))?; + + skip_whitespace(tokens); + + if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() { + let _ = tokens.consume_next_token(); // consume ')' + Ok(inner_expression) + } else { + Err(CssParseError::ParseError { + message: "Expected closing parenthesis after parenthesized expression".to_string(), + }) + } + } else { + Err(CssParseError::ParseError { + message: "Expected opening parenthesis for parenthesized expression".to_string(), + }) + } + }, + "parenthesized_expression_parser", + ) +} + +#[cfg(test)] +#[path = "../tests/at_queries/media_query_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/at_queries/parse_media_query_test.rs"] +mod parse_media_query_test; + +#[cfg(test)] +#[path = "../tests/at_queries/media_query_coverage_test.rs"] +mod media_query_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_query_transform.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_query_transform.rs new file mode 100644 index 000000000..3c2953700 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_query_transform.rs @@ -0,0 +1,259 @@ +/*! +"Last media query wins" transformation: at depth >= 1 of the style object, earlier media +queries are rewritten with negations of later ones so the last matching query wins. +*/ + +use super::media_query::{ + MediaAndRules, MediaNotRule, MediaOrRules, MediaQuery, MediaQueryRule, validate_media_query, +}; +use stylex_macros::stylex_panic; +use stylex_utils::collections::{FxBuildHasher, FxIndexMap, IndexMapEntry}; +use swc_core::{ + atoms::Wtf8Atom, + common::DUMMY_SP, + ecma::ast::{Expr, KeyValueProp, ObjectLit, Prop, PropName, PropOrSpread, Str}, +}; + +/// The property's key as text; `None` for numeric, bigint or computed keys. +fn key_value_str(key_value: &KeyValueProp) -> Option<&str> { + match &key_value.key { + PropName::Str(s) => s.value.as_str(), + PropName::Ident(id) => Some(id.sym.as_str()), + _ => None, + } +} + +/// Entry point for the last-media-query-wins transform. +pub fn last_media_query_wins_transform(styles: &[KeyValueProp]) -> Vec { + dfs_process_queries(styles, 0) +} + +/// Create an `ObjectLit` from key-value pairs. +fn create_object_from_key_values(key_values: Vec) -> ObjectLit { + let props = key_values + .into_iter() + .map(|kv| PropOrSpread::Prop(Box::new(Prop::KeyValue(kv)))) + .collect(); + + ObjectLit { + span: DUMMY_SP, + props, + } +} + +/// DFS traversal with depth tracking (upstream `dfsProcessQueries`). +fn dfs_process_queries(obj: &[KeyValueProp], depth: u32) -> Vec { + let mut result = Vec::with_capacity(obj.len()); + + for prop in obj { + match &*prop.value { + Expr::Object(obj_lit) => { + // An object with spreads/shorthands/methods is preserved unchanged so StyleX validation + // can still report the non-static value. + let mut key_values = Vec::with_capacity(obj_lit.props.len()); + let mut only_key_values = true; + for obj_prop in &obj_lit.props { + if let PropOrSpread::Prop(p) = obj_prop + && let Prop::KeyValue(kv) = &**p + { + key_values.push(kv.clone()); + } else { + only_key_values = false; + break; + } + } + + if !only_key_values { + result.push(prop.clone()); + continue; + } + + let processed_values = dfs_process_queries(&key_values, depth + 1); + let transformed_obj = create_object_from_key_values(processed_values); + + result.push(KeyValueProp { + key: prop.key.clone(), + value: Box::new(Expr::Object(transformed_obj)), + }); + }, + _ => { + // Non-object values pass through, including `firstThatWorks` arrays. + result.push(prop.clone()); + }, + } + } + + if depth >= 1 { + transform_media_queries_in_result(result) + } else { + result + } +} + +/// What identifies a property inside one object level: its name, or its position when the +/// name is unreadable. Unlike a JS object, integer-like keys keep source order. +#[derive(PartialEq, Eq, Hash)] +enum PropertyKey { + /// A name this pass can read, which is what an author wrote. + Named(String), + /// A property whose name this pass cannot read, identified by its position. + Positional(usize), +} + +impl PropertyKey { + /// The property's name, when it has one this pass can read. + fn name(&self) -> Option<&str> { + match self { + PropertyKey::Named(name) => Some(name), + PropertyKey::Positional(_) => None, + } + } +} + +fn property_key(key_value: &KeyValueProp, index: usize) -> PropertyKey { + match key_value_str(key_value) { + Some(name) if !name.is_empty() => PropertyKey::Named(name.to_owned()), + _ => PropertyKey::Positional(index), + } +} + +/// One `@media` key at this level, with everything its rewrite needs. +struct MediaEntry { + /// The authored key, exactly as it appears in the map. + key: String, + /// That key, parsed. + query: MediaQuery, +} + +/// Rewrite the `@media` keys of one object level so that a later query wins. +/// +/// Matches upstream `dfsProcessQueries` (`delete result[old]; result[new] = value` on a JS +/// object): rewritten keys move to the end in declaration order; a key canonicalizing to an +/// existing one keeps the earlier position with the later value (a ported upstream defect); the +/// value is read at rewrite time; duplicate authored keys collapse first-position, last-value. +/// +/// TODO(upstream-report): record the facebook/stylex issue number here and in +/// the ADR once the report drafted for it is filed. +fn transform_media_queries_in_result(result: Vec) -> Vec { + let is_media_key = |key: &str| key.starts_with("@media "); + + // Bail out before building the map so a level with no media key is returned as is. + if !result + .iter() + .any(|kv| key_value_str(kv).is_some_and(is_media_key)) + { + return result; + } + + let mut entries: FxIndexMap = + FxIndexMap::with_capacity_and_hasher(result.len(), FxBuildHasher); + for (index, kv) in result.into_iter().enumerate() { + entries.insert(property_key(&kv, index), kv); + } + + let media_keys = entries + .keys() + .filter_map(PropertyKey::name) + .filter(|name| is_media_key(name)) + .map(str::to_owned) + .collect::>(); + + let mut media_entries: Vec = Vec::with_capacity(media_keys.len()); + for key in media_keys { + // Validated, not just parsed: the tokenizer synthesizes a closing paren at end of input. + match validate_media_query(&key) { + Ok(query) => media_entries.push(MediaEntry { key, query }), + Err(_) => { + // Hard error: no later phase rejects it. The caller reports invalid media query syntax. + stylex_panic!("Invalid media query: {}", key); + }, + } + } + + // Each key negates the queries declared after it: suffixes of one list, sliced when consumed. + let declared_queries: Vec = media_entries + .iter() + .map(|entry| entry.query.clone()) + .collect(); + + // `shift_remove` keeps order at a quadratic cost; fine at style-object sizes. + for (index, entry) in media_entries.into_iter().enumerate() { + // The lookup cannot miss. The value is read now, so an earlier rewrite that landed on a + // later key is what that key carries. + let rewritten = entries + .shift_remove(&PropertyKey::Named(entry.key)) + .map(|current| { + let later_queries = declared_queries[index + 1..].to_vec(); + let combined_query = combine_media_query_with_negations(entry.query, later_queries); + (combined_query.to_string(), current.value) + }); + + // Iterated rather than branched on, so the impossible `None` needs no arm. + for (new_media_key, value) in rewritten.into_iter() { + // Atom taken before the key moves into the map, avoiding a second copy of a long name. + let key_atom = Wtf8Atom::from(new_media_key.as_str()); + + match entries.entry(PropertyKey::Named(new_media_key)) { + // Already present: keeps its position and takes this value. + IndexMapEntry::Occupied(mut occupied) => occupied.get_mut().value = value, + IndexMapEntry::Vacant(vacant) => { + vacant.insert(KeyValueProp { + key: PropName::Str(Str { + span: DUMMY_SP, + value: key_atom, + raw: None, + }), + value, + }); + }, + } + } + } + + entries.into_values().collect() +} + +/// Combine a media query with the negations of every query that follows it. +fn combine_media_query_with_negations( + current: MediaQuery, + negations: Vec, +) -> MediaQuery { + if negations.is_empty() { + return current; + } + + let not_rules: Vec = negations + .into_iter() + .map(|mq| MediaQueryRule::Not(MediaNotRule::new(mq.queries))) + .collect(); + + let combined_ast = match current.queries { + MediaQueryRule::Or(or_rules) => { + let new_rules = or_rules + .rules + .into_iter() + .map(|rule| { + let mut and_rules = vec![rule]; + and_rules.extend(not_rules.clone()); + MediaQueryRule::And(MediaAndRules::new(and_rules)) + }) + .collect(); + MediaQueryRule::Or(MediaOrRules::new(new_rules)) + }, + other => { + let mut rules = vec![other]; + rules.extend(not_rules); + MediaQueryRule::And(MediaAndRules::new(rules)) + }, + }; + + MediaQuery::new(combined_ast) +} + +#[cfg(test)] +#[path = "../tests/at_queries/media_query_transform_test.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/at_queries/media_query_transform_coverage_test.rs"] +mod media_query_transform_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_types.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_types.rs new file mode 100644 index 000000000..41766e333 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/media_types.rs @@ -0,0 +1,160 @@ +//! Current StyleX media-type resolution (#1888). Runs after interval normalization. +//! Source: meta/stylex style-value-parser/at-queries/media-query.js (3e33a25). +use super::media_query::{MediaAndRules, MediaKeyword, MediaNotRule, MediaOrRules, MediaQueryRule}; + +fn keyword(key: &str, not: bool) -> MediaQueryRule { + MediaQueryRule::MediaKeyword(MediaKeyword::new(key, not, false)) +} +fn never(rule: &MediaQueryRule) -> bool { + matches!(rule, MediaQueryRule::MediaKeyword(value) if value.key == "all" && value.not) +} +fn always(rule: &MediaQueryRule) -> bool { + matches!(rule, MediaQueryRule::MediaKeyword(value) if value.key == "all" && !value.not && !value.only) +} +fn contains_type(rule: &MediaQueryRule) -> bool { + match rule { + MediaQueryRule::MediaKeyword(_) => true, + MediaQueryRule::Not(value) => contains_type(&value.rule), + MediaQueryRule::And(value) => value.rules.iter().any(contains_type), + MediaQueryRule::Or(value) => value.rules.iter().any(contains_type), + _ => false, + } +} +fn complement(key: &str) -> &'static str { + match key { + "screen" => "print", + "print" => "screen", + _ => "never", + } +} +fn requirement(rule: &MediaQueryRule) -> Option<&str> { + match rule { + MediaQueryRule::MediaKeyword(value) => Some(if value.not { + complement(&value.key) + } else { + &value.key + }), + MediaQueryRule::Not(value) => match value.rule.as_ref() { + MediaQueryRule::MediaKeyword(value) if !value.not => Some(complement(&value.key)), + _ => None, + }, + _ => None, + } +} +fn negate(rule: MediaQueryRule) -> MediaQueryRule { + match rule { + MediaQueryRule::MediaKeyword(mut value) => { + value.not = !value.not; + MediaQueryRule::MediaKeyword(value) + }, + MediaQueryRule::Not(value) => *value.rule, + MediaQueryRule::And(value) => MediaQueryRule::Or(MediaOrRules::new( + value.rules.into_iter().map(negate).collect(), + )), + MediaQueryRule::Or(value) => MediaQueryRule::And(MediaAndRules::new( + value.rules.into_iter().map(negate).collect(), + )), + value => MediaQueryRule::Not(MediaNotRule::new(value)), + } +} + +pub(super) fn resolve(rule: MediaQueryRule) -> MediaQueryRule { + // Media-type-free ASTs retain their exact normalized shape and ordering. + if !contains_type(&rule) { + return rule; + } + match rule { + MediaQueryRule::Or(value) => { + let mut rules = Vec::new(); + for child in value.rules { + match resolve(child) { + MediaQueryRule::Or(value) => { + rules.extend(value.rules.into_iter().filter(|rule| !never(rule))) + }, + value if !never(&value) => rules.push(value), + _ => {}, + } + } + if rules.iter().any(always) { + return keyword("all", false); + } + match rules.len() { + 0 => keyword("all", true), + 1 => rules.remove(0), + _ => MediaQueryRule::Or(MediaOrRules::new(rules)), + } + }, + MediaQueryRule::And(value) => { + let mut resolved: Vec<_> = value.rules.into_iter().map(resolve).collect(); + if let Some(index) = resolved + .iter() + .position(|rule| matches!(rule, MediaQueryRule::Or(_))) + { + let MediaQueryRule::Or(branches) = resolved.remove(index) else { + unreachable!() + }; + return resolve(MediaQueryRule::Or(MediaOrRules::new( + branches + .rules + .into_iter() + .map(|branch| { + MediaQueryRule::And(MediaAndRules::new( + std::iter::once(branch) + .chain(resolved.iter().cloned()) + .collect(), + )) + }) + .collect(), + ))); + } + let mut media_type = "all".to_owned(); + let mut anchor = None; + let mut others = Vec::new(); + for rule in resolved { + if let MediaQueryRule::MediaKeyword(value) = &rule + && !value.not + { + if value.key != "all" { + if media_type == "all" { + media_type = value.key.clone(); + anchor = Some(rule.clone()); + } else if media_type != value.key { + return keyword("all", true); + } else if anchor.is_none() { + anchor = Some(rule.clone()); + } + } + continue; + } + match requirement(&rule) { + Some("never") => return keyword("all", true), + Some("all") => {}, + Some(required) if media_type == "all" => media_type = required.to_owned(), + Some(required) if media_type != required => return keyword("all", true), + Some(_) => {}, + None => others.push(rule), + } + } + if media_type != "all" { + others.insert(0, anchor.unwrap_or_else(|| keyword(&media_type, false))); + } + match others.len() { + 0 => keyword("all", false), + 1 => others.remove(0), + _ => MediaQueryRule::And(MediaAndRules::new(others)), + } + }, + MediaQueryRule::Not(value) => { + let inner = resolve(*value.rule); + match inner { + MediaQueryRule::MediaKeyword(mut value) => { + value.not = !value.not; + resolve(MediaQueryRule::MediaKeyword(value)) + }, + MediaQueryRule::And(_) | MediaQueryRule::Or(_) => resolve(negate(inner)), + value => MediaQueryRule::Not(MediaNotRule::new(value)), + } + }, + value => value, + } +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/messages.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/messages.rs new file mode 100644 index 000000000..9a81a64fb --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/messages.rs @@ -0,0 +1,10 @@ +/*! +Error messages for media query parsing and validation. +*/ + +pub struct MediaQueryErrors; + +impl MediaQueryErrors { + pub const SYNTAX_ERROR: &'static str = "Invalid media query syntax."; + pub const UNBALANCED_PARENS: &'static str = "Unbalanced parentheses in media query."; +} diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/mod.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/mod.rs new file mode 100644 index 000000000..78f757835 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/at_queries/mod.rs @@ -0,0 +1,12 @@ +/*! +At-rule parsing: media query parsing, validation and transformation. +*/ + +pub mod media_query; +pub mod media_query_transform; +mod media_types; +pub mod messages; + +pub use media_query::{MediaQuery, MediaQueryRule, validate_media_query}; +pub use media_query_transform::last_media_query_wins_transform; +pub use messages::MediaQueryErrors; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/base_types.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/base_types.rs new file mode 100644 index 000000000..bd47a3a6b --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/base_types.rs @@ -0,0 +1,94 @@ +/*! +Base string utilities used throughout the CSS parser. +*/ + +use std::fmt::{self, Display}; + +/// A string slice utility that provides efficient substring operations. +#[derive(Debug, Clone)] +pub struct SubString { + pub string: String, + pub start_index: usize, + pub end_index: usize, +} + +impl SubString { + /// Create a new SubString from a string + pub fn new(s: &str) -> Self { + Self { + string: s.to_string(), + start_index: 0, + end_index: if s.is_empty() { 0 } else { s.len() - 1 }, + } + } + + /// Check if the substring starts with the given string + pub fn starts_with(&self, s: &str) -> bool { + let chars: Vec = self.string.chars().collect(); + let search_chars: Vec = s.chars().collect(); + + for i in 0..search_chars.len() { + if self.start_index + i > self.end_index + || self.start_index + i >= chars.len() + || chars[self.start_index + i] != search_chars[i] + { + return false; + } + } + true + } + + /// Get the first character of the substring + pub fn first(&self) -> Option { + if self.start_index > self.end_index { + return None; + } + self.string.chars().nth(self.start_index) + } + + /// Get a character at a relative index from the start + pub fn get(&self, relative_index: usize) -> Option { + let absolute_index = self.start_index + relative_index; + if absolute_index > self.end_index { + return None; + } + self.string.chars().nth(absolute_index) + } + + /// Convert the substring to a String + pub fn into_string(&self) -> String { + if self.start_index > self.end_index { + return String::new(); + } + + let chars: Vec = self.string.chars().collect(); + let start = self.start_index.min(chars.len()); + let end = (self.end_index + 1).min(chars.len()); + + if start >= end { + String::new() + } else { + chars[start..end].iter().collect() + } + } + + /// Check if the substring is empty + pub fn is_empty(&self) -> bool { + self.start_index > self.end_index + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for SubString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.into_string()) + } +} + +#[cfg(test)] +#[path = "tests/base_types_test.rs"] +mod base_types_test; + +#[cfg(test)] +#[path = "tests/base_types_coverage_test.rs"] +mod base_types_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/alpha_value.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/alpha_value.rs new file mode 100644 index 000000000..20073a733 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/alpha_value.rs @@ -0,0 +1,100 @@ +/*! +CSS alpha value: a number, or a percentage divided down to one. + +Not bounded to `0..=1` (matches the reference compiler), so `rgba(0, 0, 0, 2)` parses. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::write_js_number; + +use crate::{ + CssParseError, + token_parser::TokenParser, + token_types::{SimpleToken, TokenList}, +}; +use std::fmt::{self, Display}; + +/// Alpha value for CSS colors +#[derive(Debug, Clone, PartialEq)] +pub struct AlphaValue { + pub value: f64, // the authored alpha, unbounded -- see the module header +} + +impl AlphaValue { + /// Create a new AlphaValue + pub fn new(value: f64) -> Self { + Self { value } + } + + /// Extract an `AlphaValue` from a `SimpleToken::Percentage`; other variants are unreachable. + pub(crate) fn extract_percentage_token(token: SimpleToken) -> AlphaValue { + if let SimpleToken::Percentage(value) = token { + // The token carries the authored percent: `50%` becomes `0.5`. + AlphaValue::new(value / 100.0) + } else { + stylex_unreachable!() + } + } + + /// Extract an `AlphaValue` from a `SimpleToken::Number`; other variants are unreachable. + pub(crate) fn extract_number_token(token: SimpleToken) -> AlphaValue { + if let SimpleToken::Number(value) = token { + AlphaValue::new(value) + } else { + stylex_unreachable!() + } + } + + /// Parser for alpha values + pub fn parser() -> TokenParser { + TokenParser::one_of(vec![ + // Percentage: v[4].signCharacter === '-' ? -1 : 1) * v[4].value) / 100 + TokenParser::::token(SimpleToken::Percentage(0.0), Some("Percentage")) + .map(Self::extract_percentage_token, Some("percentage_to_alpha")), + // Number: (v[4].signCharacter === '-' ? -1 : 1) * v[4].value + TokenParser::::token(SimpleToken::Number(0.0), Some("Number")) + .map(Self::extract_number_token, Some("number_to_alpha")), + ]) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for AlphaValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write_js_number(f, self.value) + } +} + +/// Alpha as a number, for color parsing. +pub fn alpha_as_number() -> TokenParser { + AlphaValue::parser().map(|alpha| alpha.value, Some("alpha_to_number")) +} + +/// Read a legacy colour's alpha from the token list, unbounded like upstream `alphaAsNumber`; +/// a percentage divides down to a fraction. +/// +/// Divergence: upstream negates a signed value twice (`-0.5` reads as `+0.5`); this keeps it +/// negative. No plugin path reaches it (see *Unwitnessed parser* in `CONTEXT.md`); +/// [`crate::css_types::common_types::NumberOrPercentage`] shares the gap. +pub(crate) fn parse_alpha_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected alpha value token".to_string(), + })?; + + match token { + SimpleToken::Number(value) => Ok(value), + SimpleToken::Percentage(value) => Ok(value / 100.0), + _ => Err(CssParseError::ParseError { + message: format!( + "Expected Number or Percentage token for alpha, got {:?}", + token + ), + }), + } +} + +#[cfg(test)] +#[path = "../tests/css_types/alpha_value_coverage_test.rs"] +mod alpha_value_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/angle.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/angle.rs new file mode 100644 index 000000000..5964c1390 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/angle.rs @@ -0,0 +1,101 @@ +/*! +CSS ``: `deg`, `grad`, `rad` and `turn` units. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::to_js_string; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// Valid angle units +pub const ANGLE_UNITS: &[&str] = &["deg", "grad", "rad", "turn"]; + +/// CSS Angle value with unit +#[derive(Debug, Clone, PartialEq)] +pub struct Angle { + pub value: f64, + pub unit: String, // "deg", "grad", "rad", or "turn" +} + +impl Angle { + /// Create a new Angle value + pub fn new(value: f64, unit: impl Into) -> Self { + Self { + value, + unit: unit.into(), + } + } + + /// All valid angle units + pub fn units() -> &'static [&'static str] { + ANGLE_UNITS + } + + /// Check if a unit is a valid angle unit + pub fn is_valid_unit(unit: &str) -> bool { + ANGLE_UNITS.contains(&unit) + } + + /// Whether `token` is a `Dimension` with a valid angle unit. + pub(crate) fn is_valid_angle_dimension(token: &SimpleToken) -> bool { + if let SimpleToken::Dimension { unit, .. } = token { + Self::is_valid_unit(unit) + } else { + false + } + } + + /// Extract an `Angle` from a `SimpleToken::Dimension`; other variants are unreachable. + pub(crate) fn extract_dimension_token(token: SimpleToken) -> Angle { + if let SimpleToken::Dimension { value, unit } = token { + Angle::new(value, unit) + } else { + stylex_unreachable!() + } + } + + /// Whether `token` is a `Number` with value `0.0`. + pub(crate) fn is_zero_number(token: &SimpleToken) -> bool { + if let SimpleToken::Number(value) = token { + *value == 0.0 + } else { + false + } + } + + /// Parser for CSS angle values + pub fn parser() -> TokenParser { + let dimension_parser = TokenParser::::token( + SimpleToken::Dimension { + value: 0.0, + unit: String::new(), + }, + Some("Dimension"), + ) + .where_fn(Self::is_valid_angle_dimension, Some("valid_angle_unit")) + .map(Self::extract_dimension_token, Some("to_angle")); + + // Unitless zero, defaulting to `deg` + let zero_parser = TokenParser::::token(SimpleToken::Number(0.0), Some("Number")) + .where_fn(Self::is_zero_number, Some("zero_value")) + .map(|_| Angle::new(0.0, "deg"), Some("zero_angle")); + + TokenParser::one_of(vec![dimension_parser, zero_parser]) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Angle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", to_js_string(self.value), self.unit) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/angle_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/angle_coverage_test.rs"] +mod angle_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/angle_percentage.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/angle_percentage.rs new file mode 100644 index 000000000..26ae44586 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/angle_percentage.rs @@ -0,0 +1,71 @@ +/*! +CSS ``: an angle or a percentage. +*/ + +use crate::{ + css_types::{Angle, Percentage}, + token_parser::TokenParser, +}; +use std::fmt::{self, Display}; + +/// Union type for angle or percentage values +#[derive(Debug, Clone, PartialEq)] +pub enum AnglePercentage { + Angle(Angle), + Percentage(Percentage), +} + +impl AnglePercentage { + /// Parser for angle or percentage values + pub fn parser() -> TokenParser { + TokenParser::one_of(vec![ + Angle::parser().map(AnglePercentage::Angle, Some("angle")), + Percentage::parser().map(AnglePercentage::Percentage, Some("percentage")), + ]) + } + + /// Check if this is an angle value + pub fn is_angle(&self) -> bool { + matches!(self, AnglePercentage::Angle(_)) + } + + /// Check if this is a percentage value + pub fn is_percentage(&self) -> bool { + matches!(self, AnglePercentage::Percentage(_)) + } + + /// Get the angle value if this is an angle, None otherwise + pub fn as_angle(&self) -> Option<&Angle> { + match self { + AnglePercentage::Angle(angle) => Some(angle), + _ => None, + } + } + + /// Get the percentage value if this is a percentage, None otherwise + pub fn as_percentage(&self) -> Option<&Percentage> { + match self { + AnglePercentage::Percentage(percentage) => Some(percentage), + _ => None, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for AnglePercentage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnglePercentage::Angle(angle) => angle.fmt(f), + AnglePercentage::Percentage(percentage) => percentage.fmt(f), + } + } +} + +/// Convenience function for creating the parser +pub fn angle_percentage_parser() -> TokenParser { + AnglePercentage::parser() +} + +#[cfg(test)] +#[path = "../tests/css_types/angle_percentage_tests.rs"] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/basic_shape.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/basic_shape.rs new file mode 100644 index 000000000..db37e71d2 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/basic_shape.rs @@ -0,0 +1,623 @@ +/** + * CSS basic shape parsers for `clip-path` and `shape-outside`. + */ +use crate::token_parser::TokenParser; +use crate::{ + CssParseError, + css_types::{ + length_percentage::{LengthPercentage, length_percentage_parser}, + position::Position, + }, + token_types::{SimpleToken, TokenList}, +}; +use std::fmt; + +#[derive(Debug, Clone, PartialEq)] +pub enum BasicShape { + Inset { + top: LengthPercentage, + right: LengthPercentage, + bottom: LengthPercentage, + left: LengthPercentage, + round: Option, + }, + Circle { + radius: CircleRadius, + position: Option, + }, + Ellipse { + radius_x: CircleRadius, + radius_y: CircleRadius, + position: Option, + }, + Polygon { + fill_rule: Option, + points: Vec<(LengthPercentage, LengthPercentage)>, + }, + Path { + fill_rule: Option, + path: String, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CircleRadius { + Length(LengthPercentage), + ClosestSide, + FarthestSide, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for CircleRadius { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CircleRadius::Length(lp) => write!(f, "{}", lp), + CircleRadius::ClosestSide => write!(f, "closest-side"), + CircleRadius::FarthestSide => write!(f, "farthest-side"), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for BasicShape { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BasicShape::Inset { + top, + right, + bottom, + left, + round, + } => { + let round_str = round + .as_ref() + .map(|r| format!(" round {}", r)) + .unwrap_or_default(); + + if top == right + && right == bottom + && bottom == left + && left == round.as_ref().unwrap_or(left) + { + write!(f, "inset({}{})", top, round_str) + } else if top == bottom && left == right { + write!(f, "inset({} {}{})", top, right, round_str) + } else if top == bottom { + write!(f, "inset({} {} {}{})", top, right, bottom, round_str) + } else { + write!( + f, + "inset({} {} {} {}{})", + top, right, bottom, left, round_str + ) + } + }, + BasicShape::Circle { radius, position } => { + let pos_str = position + .as_ref() + .map(|p| format!(" at {}", p)) + .unwrap_or_default(); + write!(f, "circle({}{})", radius, pos_str) + }, + BasicShape::Ellipse { + radius_x, + radius_y, + position, + } => { + let pos_str = position + .as_ref() + .map(|p| format!(" at {}", p)) + .unwrap_or_default(); + write!(f, "ellipse({} {}{})", radius_x, radius_y, pos_str) + }, + BasicShape::Polygon { fill_rule, points } => { + let fill_rule_str = fill_rule + .as_ref() + .map(|fr| format!("{}, ", fr)) + .unwrap_or_default(); + let points_str = points + .iter() + .map(|(x, y)| format!("{} {}", x, y)) + .collect::>() + .join(", "); + write!(f, "polygon({}{})", fill_rule_str, points_str) + }, + BasicShape::Path { fill_rule, path } => { + let fill_rule_str = fill_rule + .as_ref() + .map(|fr| format!("{}, ", fr)) + .unwrap_or_default(); + write!(f, "path({}\"{}\")", fill_rule_str, path) + }, + } + } +} + +impl BasicShape { + fn inset_parser() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "inset" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected inset() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected inset() function but reached end of input".to_string(), + }); + }, + } + + let top = (length_percentage_parser().run)(tokens)?; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let right = match (length_percentage_parser().run)(tokens) { + Ok(right_val) => { + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + right_val + }, + _ => top.clone(), + }; + + let bottom = match (length_percentage_parser().run)(tokens) { + Ok(bottom_val) => { + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + bottom_val + }, + _ => top.clone(), + }; + + let left = match (length_percentage_parser().run)(tokens) { + Ok(left_val) => { + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + left_val + }, + _ => right.clone(), + }; + + // Optional `round`; preceding whitespace was consumed by the value loops. + let round = if let Ok(Some(SimpleToken::Ident(keyword))) = tokens.peek() { + if keyword == "round" { + let _ = tokens.consume_next_token(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let radius_value = (length_percentage_parser().run)(tokens)?; + Some(radius_value) + } else { + None + } + } else { + None + }; + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(BasicShape::Inset { + top, + right, + bottom, + left, + round, + }) + }, + "inset_parser", + ) + } + + fn circle_parser() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "circle" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected circle() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected circle() function but reached end of input".to_string(), + }); + }, + } + + // Parse radius: length-percentage | closest-side | farthest-side + let radius = if let Some(token) = tokens.peek_infallible() { + match token { + SimpleToken::Ident(value) if value == "closest-side" => { + let _ = tokens.consume_next_token(); // consume the ident + CircleRadius::ClosestSide + }, + SimpleToken::Ident(value) if value == "farthest-side" => { + let _ = tokens.consume_next_token(); // consume the ident + CircleRadius::FarthestSide + }, + _ => { + let lp = (length_percentage_parser().run)(tokens)?; + CircleRadius::Length(lp) + }, + } + } else { + return Err(CssParseError::ParseError { + message: "Expected radius for circle".to_string(), + }); + }; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let position = Self::parse_optional_position(tokens)?; + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(BasicShape::Circle { radius, position }) + }, + "circle_parser", + ) + } + + fn ellipse_parser() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "ellipse" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected ellipse() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected ellipse() function but reached end of input".to_string(), + }); + }, + } + + let parse_radius = |tokens: &mut TokenList| -> Result { + if let Some(token) = tokens.peek_infallible() { + match token { + SimpleToken::Ident(value) if value == "closest-side" => { + let _ = tokens.consume_next_token(); + Ok(CircleRadius::ClosestSide) + }, + SimpleToken::Ident(value) if value == "farthest-side" => { + let _ = tokens.consume_next_token(); + Ok(CircleRadius::FarthestSide) + }, + _ => { + let lp = (length_percentage_parser().run)(tokens)?; + Ok(CircleRadius::Length(lp)) + }, + } + } else { + Err(CssParseError::ParseError { + message: "Expected radius".to_string(), + }) + } + }; + + let radius_x = parse_radius(tokens)?; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let radius_y = parse_radius(tokens)?; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let position = Self::parse_optional_position(tokens)?; + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(BasicShape::Ellipse { + radius_x, + radius_y, + position, + }) + }, + "ellipse_parser", + ) + } + + fn polygon_parser() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "polygon" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected polygon() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected polygon() function but reached end of input".to_string(), + }); + }, + } + + // Parse optional fillRule (nonzero | evenodd) + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let fill_rule = if let Ok(Some(SimpleToken::Ident(rule))) = tokens.peek() { + if rule == "nonzero" || rule == "evenodd" { + // Consume the fill-rule ident matched above. + let _ = tokens.consume_next_token(); + // Skip optional comma and whitespace + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + if let Ok(Some(SimpleToken::Comma)) = tokens.peek() { + let _ = tokens.consume_next_token(); + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + } + Some(rule) + } else { + Some("nonzero".to_string()) // default + } + } else { + Some("nonzero".to_string()) // default + }; + + // At least one point (x y, x y, ...) + let mut points = Vec::new(); + + loop { + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() { + break; + } + + let x = (length_percentage_parser().run)(tokens)?; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let y = (length_percentage_parser().run)(tokens)?; + + points.push((x, y)); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + // Check for comma (more points) or closing paren (end) + if let Ok(Some(token)) = tokens.peek() { + match token { + SimpleToken::Comma => { + let _ = tokens.consume_next_token(); // consume the comma + continue; // parse next point + }, + SimpleToken::RightParen => { + break; // done parsing points + }, + _ => { + return Err(CssParseError::ParseError { + message: format!( + "Expected comma or closing paren in polygon, got {:?}", + token + ), + }); + }, + } + } else { + return Err(CssParseError::ParseError { + message: "Unexpected end of input in polygon".to_string(), + }); + } + } + + // The loop only breaks on a peeked RightParen; consume it. + let _ = tokens.consume_next_token(); + + if points.is_empty() { + return Err(CssParseError::ParseError { + message: "Polygon must have at least one point".to_string(), + }); + } + + Ok(BasicShape::Polygon { fill_rule, points }) + }, + "polygon_parser", + ) + } + + fn path_parser() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "path" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected path() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected path() function but reached end of input".to_string(), + }); + }, + } + + // Parse optional fillRule (nonzero | evenodd) + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let fill_rule = if let Ok(Some(SimpleToken::Ident(rule))) = tokens.peek() { + if rule == "nonzero" || rule == "evenodd" { + // Consume the fill-rule ident matched above. + let _ = tokens.consume_next_token(); + // Skip optional comma and whitespace + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + if let Ok(Some(SimpleToken::Comma)) = tokens.peek() { + let _ = tokens.consume_next_token(); + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + } + Some(rule) + } else { + Some("nonzero".to_string()) // default + } + } else { + Some("nonzero".to_string()) // default + }; + + // Whitespace before the path string is already consumed. + let path = match tokens.consume_next_token_infallible() { + Some(SimpleToken::String(s)) => s, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected string literal for path, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected path string but reached end of input".to_string(), + }); + }, + }; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(BasicShape::Path { fill_rule, path }) + }, + "path_parser", + ) + } + + /// Parse optional "at Position" for circle and ellipse + fn parse_optional_position(tokens: &mut TokenList) -> Result, CssParseError> { + let checkpoint = tokens.current_index; + + // Callers already drain whitespace; tests exercise this guard directly. + if let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.peek_infallible() { + Some(SimpleToken::Ident(keyword)) if keyword == "at" => { + let _ = tokens.consume_next_token(); // consume "at" + + if let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let position_parser = Position::parser(); + match position_parser.run.as_ref()(tokens) { + Ok(position) => Ok(Some(position)), + Err(e) => Err(e), + } + }, + _ => { + // No `at`: rewind + tokens.set_current_index(checkpoint); + Ok(None) + }, + } + } + + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![ + Self::inset_parser(), + Self::circle_parser(), + Self::ellipse_parser(), + Self::polygon_parser(), + Self::path_parser(), + ]) + } +} + +pub fn basic_shape_parser() -> TokenParser { + BasicShape::parse() +} + +#[cfg(test)] +#[path = "../tests/css_types/basic_shape_test.rs"] +mod basic_shape_test; + +#[cfg(test)] +#[path = "../tests/css_types/basic_shape_coverage_test.rs"] +mod basic_shape_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/blend_mode.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/blend_mode.rs new file mode 100644 index 000000000..0765864e4 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/blend_mode.rs @@ -0,0 +1,144 @@ +/*! +CSS `` values (`mix-blend-mode`, `background-blend-mode`). +*/ + +use stylex_macros::stylex_unreachable; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// CSS blend mode values +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, +} + +impl BlendMode { + /// All valid blend mode values + pub fn all_values() -> &'static [&'static str] { + &[ + "normal", + "multiply", + "screen", + "overlay", + "darken", + "lighten", + "color-dodge", + "color-burn", + "hard-light", + "soft-light", + "difference", + "exclusion", + "hue", + "saturation", + "color", + "luminosity", + ] + } + + /// Convert from string representation + pub fn parse(s: &str) -> Option { + match s { + "normal" => Some(BlendMode::Normal), + "multiply" => Some(BlendMode::Multiply), + "screen" => Some(BlendMode::Screen), + "overlay" => Some(BlendMode::Overlay), + "darken" => Some(BlendMode::Darken), + "lighten" => Some(BlendMode::Lighten), + "color-dodge" => Some(BlendMode::ColorDodge), + "color-burn" => Some(BlendMode::ColorBurn), + "hard-light" => Some(BlendMode::HardLight), + "soft-light" => Some(BlendMode::SoftLight), + "difference" => Some(BlendMode::Difference), + "exclusion" => Some(BlendMode::Exclusion), + "hue" => Some(BlendMode::Hue), + "saturation" => Some(BlendMode::Saturation), + "color" => Some(BlendMode::Color), + "luminosity" => Some(BlendMode::Luminosity), + _ => None, + } + } + + /// Convert to string representation + pub fn as_str(&self) -> &'static str { + match self { + BlendMode::Normal => "normal", + BlendMode::Multiply => "multiply", + BlendMode::Screen => "screen", + BlendMode::Overlay => "overlay", + BlendMode::Darken => "darken", + BlendMode::Lighten => "lighten", + BlendMode::ColorDodge => "color-dodge", + BlendMode::ColorBurn => "color-burn", + BlendMode::HardLight => "hard-light", + BlendMode::SoftLight => "soft-light", + BlendMode::Difference => "difference", + BlendMode::Exclusion => "exclusion", + BlendMode::Hue => "hue", + BlendMode::Saturation => "saturation", + BlendMode::Color => "color", + BlendMode::Luminosity => "luminosity", + } + } + + /// Check if a string is a valid blend mode + pub fn is_valid_blend_mode(s: &str) -> bool { + Self::all_values().contains(&s) + } + + /// Extract the identifier string from a `SimpleToken::Ident`; other variants are unreachable. + pub(crate) fn extract_ident_token(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + stylex_unreachable!() + } + } + + /// Parser for blend mode values + pub fn parser() -> TokenParser { + use crate::token_parser::tokens; + + tokens::ident() + .map(Self::extract_ident_token, Some("extract_ident_value")) + .where_fn( + |value: &String| Self::is_valid_blend_mode(value), + Some("valid_blend_mode"), + ) + .map(|value| Self::parse(&value).unwrap(), Some("to_blend_mode")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for BlendMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/blend_mode_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/blend_mode_test.rs"] +mod blend_mode_test; + +#[cfg(test)] +#[path = "../tests/css_types/blend_mode_coverage_test.rs"] +mod blend_mode_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/calc.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/calc.rs new file mode 100644 index 000000000..b4b051bb8 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/calc.rs @@ -0,0 +1,562 @@ +/*! +CSS `calc()` expression parsing with operator precedence. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::{to_js_string, write_js_number}; + +use crate::{ + CssParseError, + css_types::{calc_constant::CalcConstant, common_types::Percentage}, + token_parser::TokenParser, + token_types::{SimpleToken, TokenList}, +}; +use std::fmt::{self, Display}; + +/// Dimension with value and unit for calc expressions +#[derive(Debug, Clone, PartialEq)] +pub struct CalcDimension { + pub value: f64, + pub unit: String, +} + +impl CalcDimension { + pub fn new(value: f64, unit: impl Into) -> Self { + Self { + value, + unit: unit.into(), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CalcDimension { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", to_js_string(self.value), self.unit) + } +} + +/// Addition operation: { type: '+', left: CalcValue, right: CalcValue } +#[derive(Debug, Clone, PartialEq)] +pub struct Addition { + pub left: Box, + pub right: Box, +} + +impl Addition { + pub fn new(left: CalcValue, right: CalcValue) -> Self { + Self { + left: Box::new(left), + right: Box::new(right), + } + } +} + +/// Subtraction operation: { type: '-', left: CalcValue, right: CalcValue } +#[derive(Debug, Clone, PartialEq)] +pub struct Subtraction { + pub left: Box, + pub right: Box, +} + +impl Subtraction { + pub fn new(left: CalcValue, right: CalcValue) -> Self { + Self { + left: Box::new(left), + right: Box::new(right), + } + } +} + +/// Multiplication operation: { type: '*', left: CalcValue, right: CalcValue } +#[derive(Debug, Clone, PartialEq)] +pub struct Multiplication { + pub left: Box, + pub right: Box, +} + +impl Multiplication { + pub fn new(left: CalcValue, right: CalcValue) -> Self { + Self { + left: Box::new(left), + right: Box::new(right), + } + } +} + +/// Division operation: { type: '/', left: CalcValue, right: CalcValue } +#[derive(Debug, Clone, PartialEq)] +pub struct Division { + pub left: Box, + pub right: Box, +} + +impl Division { + pub fn new(left: CalcValue, right: CalcValue) -> Self { + Self { + left: Box::new(left), + right: Box::new(right), + } + } +} + +/// Group (parenthesized expression): { type: 'group', expr: CalcValue } +#[derive(Debug, Clone, PartialEq)] +pub struct Group { + pub expr: Box, +} + +impl Group { + pub fn new(expr: CalcValue) -> Self { + Self { + expr: Box::new(expr), + } + } +} + +/// Union type for calc values and operators during parsing +#[derive(Debug, Clone, PartialEq)] +pub enum CalcValueOrOperator { + Value(CalcValue), + Operator(String), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CalcValue { + Number(f64), + Dimension(CalcDimension), + Percentage(Percentage), + Constant(CalcConstant), + Addition(Addition), + Subtraction(Subtraction), + Multiplication(Multiplication), + Division(Division), + Group(Group), +} + +fn skip_whitespace(tokens: &mut TokenList) -> bool { + let mut consumed = false; + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + tokens.consume_next_token_infallible(); + consumed = true; + } + consumed +} + +impl CalcValue { + /// Parser for individual calc values (no operators) + pub fn value_parser() -> TokenParser { + TokenParser::new(Self::parse_calc_value, "calc_value") + } + + /// Parse a basic calc value: number, dimension, percentage, or constant. + fn parse_calc_value(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected Number, Dimension, Percentage, or Constant token".to_string(), + })?; + + match token { + SimpleToken::Number(value) => Ok(CalcValue::Number(value)), + SimpleToken::Dimension { value, unit } => { + Ok(CalcValue::Dimension(CalcDimension::new(value, unit))) + }, + SimpleToken::Percentage(value) => { + // The token already carries the authored percent: `50%` is `50`. + Ok(CalcValue::Percentage(Percentage::new(value))) + }, + SimpleToken::Ident(name) => { + // Calc constant: pi, e, infinity, -infinity, NaN + match CalcConstant::parse(&name) { + Some(constant) => Ok(CalcValue::Constant(constant)), + None => Err(CssParseError::ParseError { + message: format!("Unknown calc constant: {}", name), + }), + } + }, + SimpleToken::Function(name) if name == "calc" => { + let value = Self::parse_calc_expression(tokens)?; + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if matches!(close_token, SimpleToken::RightParen) { + Ok(value) + } else { + Err(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + }) + } + }, + _ => Err(CssParseError::ParseError { + message: format!( + "Expected Number, Dimension, Percentage, or Constant token, got {:?}", + token + ), + }), + } + } + + pub fn parser() -> TokenParser { + TokenParser::new(Self::parse_calc_expression, "calc_expression") + } + + /// Parse calc expression with operator precedence + fn parse_calc_expression(tokens: &mut TokenList) -> Result { + skip_whitespace(tokens); + + let first_value = match Self::try_parse_parenthesized_group(tokens) { + Ok(grouped) => CalcValue::Group(grouped), + Err(_) => Self::parse_calc_value(tokens)?, + }; + + let mut values_and_operators = vec![CalcValueOrOperator::Value(first_value)]; + + loop { + let had_ws_before = skip_whitespace(tokens); + + // RightParen ends the expression; the parent parser consumes it. + if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() { + break; + } + + let checkpoint = tokens.current_index; + match Self::try_parse_operator(tokens) { + Ok(operator) => { + let requires_surrounding_ws = matches!(operator.as_str(), "+" | "-"); + values_and_operators.push(CalcValueOrOperator::Operator(operator)); + + let had_ws_after = skip_whitespace(tokens); + if requires_surrounding_ws && (!had_ws_before || !had_ws_after) { + return Err(CssParseError::ParseError { + message: "calc '+' and '-' operators require whitespace on both sides".to_string(), + }); + } + + let next_value = match Self::try_parse_parenthesized_group(tokens) { + Ok(grouped) => CalcValue::Group(grouped), + Err(_) => Self::parse_calc_value(tokens)?, + }; + values_and_operators.push(CalcValueOrOperator::Value(next_value)); + }, + Err(_) => { + tokens.set_current_index(checkpoint); + break; + }, + } + } + + if values_and_operators.len() == 1 { + Self::extract_single_calc_value(&values_and_operators[0]) + } else { + Self::split_by_multiplication_or_division(values_and_operators) + } + } + + /// The `CalcValue` of the first element, which the parser guarantees is a `Value`. + fn extract_single_calc_value(item: &CalcValueOrOperator) -> Result { + if let CalcValueOrOperator::Value(value) = item { + Ok(value.clone()) + } else { + stylex_unreachable!("First element should always be a value") + } + } + + /// Apply operator precedence: `*` and `/` first, then `+` and `-`. + fn split_by_multiplication_or_division( + values_and_operators: Vec, + ) -> Result { + if values_and_operators.len() == 1 { + match &values_and_operators[0] { + CalcValueOrOperator::Value(value) => return Ok(value.clone()), + CalcValueOrOperator::Operator(_) => { + return Err(CssParseError::ParseError { + message: "Invalid operator".to_string(), + }); + }, + } + } + + let first_operator = values_and_operators + .iter() + .position(|item| matches!(item, CalcValueOrOperator::Operator(op) if op == "*" || op == "/")); + + match first_operator { + None => { + // No `*` or `/`: handle `+` and `-` + Self::compose_add_and_subtraction(values_and_operators) + }, + Some(op_index) => { + let left_slice = values_and_operators[..op_index].to_vec(); + let right_slice = values_and_operators[op_index + 1..].to_vec(); + + let left = Self::compose_add_and_subtraction(left_slice)?; + let right = Self::split_by_multiplication_or_division(right_slice)?; + + Self::apply_mult_div_operator(&values_and_operators[op_index], left, right) + }, + } + } + + /// Apply a `*` or `/` operator to two parsed values; other operators are unreachable. + fn apply_mult_div_operator( + op: &CalcValueOrOperator, + left: CalcValue, + right: CalcValue, + ) -> Result { + match op { + CalcValueOrOperator::Operator(operator) => match operator.as_str() { + "*" => Ok(CalcValue::Multiplication(Multiplication::new(left, right))), + "/" => Ok(CalcValue::Division(Division::new(left, right))), + _ => stylex_unreachable!( + "position() only matches '*' or '/' operators; got '{}'", + operator + ), + }, + CalcValueOrOperator::Value(_) => { + stylex_unreachable!("position() guarantees a CalcValueOrOperator::Operator at op_index") + }, + } + } + + /// Handle addition and subtraction operations + fn compose_add_and_subtraction( + values_and_operators: Vec, + ) -> Result { + if values_and_operators.len() == 1 { + match &values_and_operators[0] { + CalcValueOrOperator::Value(value) => return Ok(value.clone()), + CalcValueOrOperator::Operator(op) => { + return Err(CssParseError::ParseError { + message: format!("Invalid operator: {}", op), + }); + }, + } + } + + let first_operator = values_and_operators + .iter() + .position(|item| matches!(item, CalcValueOrOperator::Operator(op) if op == "+" || op == "-")); + + match first_operator { + None => Err(CssParseError::ParseError { + message: "No valid operator found".to_string(), + }), + Some(op_index) => { + let left_slice = values_and_operators[..op_index].to_vec(); + let right_slice = values_and_operators[op_index + 1..].to_vec(); + + let left = Self::compose_add_and_subtraction(left_slice)?; + let right = Self::compose_add_and_subtraction(right_slice)?; + + Self::apply_add_sub_operator(&values_and_operators[op_index], left, right) + }, + } + } + + /// Apply a `+` or `-` operator to two parsed values; other operators are unreachable. + fn apply_add_sub_operator( + op: &CalcValueOrOperator, + left: CalcValue, + right: CalcValue, + ) -> Result { + match op { + CalcValueOrOperator::Operator(operator) => match operator.as_str() { + "+" => Ok(CalcValue::Addition(Addition::new(left, right))), + "-" => Ok(CalcValue::Subtraction(Subtraction::new(left, right))), + _ => stylex_unreachable!( + "position() only matches '+' or '-' operators; got '{}'", + operator + ), + }, + CalcValueOrOperator::Value(_) => { + stylex_unreachable!("position() guarantees a CalcValueOrOperator::Operator at op_index") + }, + } + } + + /// Try to parse a parenthesized group + fn try_parse_parenthesized_group(tokens: &mut TokenList) -> Result { + let checkpoint = tokens.current_index; + + let open_paren_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected opening parenthesis".to_string(), + })?; + + if !matches!(open_paren_token, SimpleToken::LeftParen) { + tokens.set_current_index(checkpoint); + return Err(CssParseError::ParseError { + message: "Expected '(' token".to_string(), + }); + } + + skip_whitespace(tokens); + + // Leaves the position at the closing parenthesis. + let inner_expr = Self::parse_calc_expression(tokens)?; + + let close_paren_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_paren_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: "Expected ')' token".to_string(), + }); + } + + Ok(Group::new(inner_expr)) + } + + /// Try to parse an operator token + fn try_parse_operator(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected operator token".to_string(), + })?; + + match token { + SimpleToken::Delim('+') => Ok("+".to_string()), + SimpleToken::Delim('-') => Ok("-".to_string()), + SimpleToken::Delim('*') => Ok("*".to_string()), + SimpleToken::Delim('/') => Ok("/".to_string()), + _ => Err(CssParseError::ParseError { + message: format!("Expected operator (+, -, *, /), got {:?}", token), + }), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CalcValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CalcValue::Number(n) => write_js_number(f, *n), + CalcValue::Dimension(d) => write!(f, "{}", d), + CalcValue::Percentage(p) => write!(f, "{}", p), + CalcValue::Constant(c) => write!(f, "{}", c), + CalcValue::Addition(op) => write!(f, "{} + {}", op.left, op.right), + CalcValue::Subtraction(op) => write!(f, "{} - {}", op.left, op.right), + CalcValue::Multiplication(op) => write!(f, "{} * {}", op.left, op.right), + CalcValue::Division(op) => write!(f, "{} / {}", op.left, op.right), + CalcValue::Group(group) => write!(f, "({})", group.expr), + } + } +} + +/// Calc expression container +#[derive(Debug, Clone, PartialEq)] +pub struct Calc { + pub value: CalcValue, +} + +impl Calc { + /// Create a new calc expression + pub fn new(value: CalcValue) -> Self { + Calc { value } + } + + /// Alias for [`Calc::parser`]. + pub fn parse() -> TokenParser { + Self::parser() + } + + /// Parser for calc() expressions + pub fn parser() -> TokenParser { + TokenParser::new(Self::parse_calc, "calc_parser") + } + + /// Parse a `calc(...)` token stream. + fn parse_calc(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected calc function".to_string(), + })?; + + if let SimpleToken::Function(fn_name) = token { + if fn_name != "calc" { + return Err(CssParseError::ParseError { + message: format!("Expected calc function, got {}", fn_name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: "Expected function token".to_string(), + }); + } + + let calc_value = CalcValue::parse_calc_expression(tokens)?; + + let close_token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + }); + } + + Ok(Calc::new(calc_value)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Calc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "calc({})", self.value) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn calc_value_to_string(value: &CalcValue) -> String { + match value { + CalcValue::Number(n) => to_js_string(*n), + CalcValue::Dimension(d) => d.to_string(), + CalcValue::Percentage(p) => p.to_string(), + CalcValue::Constant(c) => c.to_string(), + CalcValue::Addition(op) => format!( + "{} + {}", + calc_value_to_string(&op.left), + calc_value_to_string(&op.right) + ), + CalcValue::Subtraction(op) => format!( + "{} - {}", + calc_value_to_string(&op.left), + calc_value_to_string(&op.right) + ), + CalcValue::Multiplication(op) => format!( + "{} * {}", + calc_value_to_string(&op.left), + calc_value_to_string(&op.right) + ), + CalcValue::Division(op) => format!( + "{} / {}", + calc_value_to_string(&op.left), + calc_value_to_string(&op.right) + ), + CalcValue::Group(group) => format!("({})", calc_value_to_string(&group.expr)), + } +} + +#[cfg(test)] +#[path = "../tests/css_types/calc_test.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/calc_coverage_test.rs"] +mod calc_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/calc_constant.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/calc_constant.rs new file mode 100644 index 000000000..28a76771c --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/calc_constant.rs @@ -0,0 +1,78 @@ +/*! +CSS calc constants: `pi`, `e`, `infinity`, `-infinity`, `NaN`. +*/ + +use crate::token_parser::TokenParser; +use std::fmt::{self, Display}; + +/// Valid calc constants +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CalcConstant { + Pi, + E, + Infinity, + NegativeInfinity, + NaN, +} + +impl CalcConstant { + /// All valid calc constants as strings + pub fn all_constants() -> &'static [&'static str] { + &["pi", "e", "infinity", "-infinity", "NaN"] + } + + /// Convert from string representation + pub fn parse(s: &str) -> Option { + match s { + "pi" => Some(CalcConstant::Pi), + "e" => Some(CalcConstant::E), + "infinity" => Some(CalcConstant::Infinity), + "-infinity" => Some(CalcConstant::NegativeInfinity), + "NaN" => Some(CalcConstant::NaN), + _ => None, + } + } + + /// Convert to string representation + pub fn as_str(&self) -> &'static str { + match self { + CalcConstant::Pi => "pi", + CalcConstant::E => "e", + CalcConstant::Infinity => "infinity", + CalcConstant::NegativeInfinity => "-infinity", + CalcConstant::NaN => "NaN", + } + } + + /// Check if a string is a valid calc constant + pub fn is_valid_constant(s: &str) -> bool { + Self::all_constants().contains(&s) + } + + /// Parser for calc constants + pub fn parser() -> TokenParser { + TokenParser::::one_of(vec![ + // Order matters - check longer strings first to avoid partial matches + TokenParser::::string("-infinity").map( + |_| CalcConstant::NegativeInfinity, + Some("to_negative_infinity"), + ), + TokenParser::::string("infinity") + .map(|_| CalcConstant::Infinity, Some("to_infinity")), + TokenParser::::string("pi").map(|_| CalcConstant::Pi, Some("to_pi")), + TokenParser::::string("e").map(|_| CalcConstant::E, Some("to_e")), + TokenParser::::string("NaN").map(|_| CalcConstant::NaN, Some("to_nan")), + ]) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CalcConstant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/calc_constant_tests.rs"] +mod tests; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/color.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/color.rs new file mode 100644 index 000000000..33f6c1e56 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/color.rs @@ -0,0 +1,2096 @@ +/*! +CSS ``: named, hex, rgb(a), hsl(a) and modern color spaces. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::{to_js_string, write_js_number}; + +use crate::{ + CssParseError, + css_types::{ + Angle, Percentage, + alpha_value::{alpha_as_number, parse_alpha_token}, + }, + token_parser::{TokenParser, tokens}, + token_types::{SimpleToken, TokenList}, +}; + +use std::fmt::{self, Display}; + +/// The alpha a modern colour space prints, or `None` where it prints no tail. +/// Matches upstream's truthiness test: an alpha of `0` or `NaN` prints no tail. +fn printed_alpha(alpha: Option) -> Option { + alpha.filter(|value| *value != 0.0 && !value.is_nan()) +} + +/// The numeric value of a Number token; 0.0 for others (callers guard with `tokens::number()`). +fn extract_number_from_token(token: SimpleToken) -> f64 { + if let SimpleToken::Number(v) = token { + v + } else { + 0.0 + } +} + +/// A legacy `rgb()`/`rgba()` channel, bounded to `0..=255` but kept fractional (upstream +/// `rgbNumberParser`); the alpha ([`crate::css_types::alpha_value::parse_alpha_token`]) is not. +fn parse_rgb_channel_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected number token".to_string(), + })?; + + match token { + SimpleToken::Number(value) if (0.0..=255.0).contains(&value) => Ok(value), + SimpleToken::Number(value) => Err(CssParseError::ParseError { + message: format!("RGB channel must be 0-255, got {}", value), + }), + _ => Err(CssParseError::ParseError { + message: format!("Expected Number token, got {:?}", token), + }), + } +} + +/// Parses numbers in range 0-255 for RGB color channels +fn rgb_number_parser() -> TokenParser { + tokens::number() + .map(extract_number_from_token, Some("extract_number")) + .where_fn(|v| *v >= 0.0 && *v <= 255.0, Some("0..255")) +} + +/// Read an optional `/ ` tail; with no slash, rewinds (including skipped +/// whitespace) and returns `None`. +fn parse_optional_slash_alpha(tokens: &mut TokenList) -> Result, CssParseError> { + let checkpoint = tokens.current_index; + + if let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + match tokens.peek_infallible() { + Some(SimpleToken::Delim('/')) => { + tokens.consume_next_token_infallible(); + + if let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + // A slash was written, so a malformed alpha is an error: no rewind. + alpha_as_number().run.as_ref()(tokens).map(Some) + }, + _ => { + tokens.set_current_index(checkpoint); + Ok(None) + }, + } +} + +/// Whether `token` is a Function token with the given name. +fn check_function_name(token: &SimpleToken, name: &str) -> bool { + if let SimpleToken::Function(fn_name) = token { + fn_name == name + } else { + false + } +} + +/// Function name parser helper +fn function_parser(name: &'static str) -> TokenParser<()> { + tokens::function() + .where_fn( + move |token| check_function_name(token, name), + Some(&format!("{}_function", name)), + ) + .map(|_| (), Some(&format!("{}_fn", name))) +} + +pub struct AdvancedColorParsers; + +impl AdvancedColorParsers { + /// `rgb(r, g, b)` with commas. + pub fn rgb_comma_full() -> TokenParser<(f64, f64, f64)> { + function_parser("rgb") + .flat_map(|_| rgb_number_parser(), Some("r")) + .flat_map( + |r| { + tokens::comma().flat_map( + move |_| rgb_number_parser().map(move |g| (r, g), Some("rg")), + Some("comma1"), + ) + }, + Some("rg_step"), + ) + .flat_map( + |(r, g)| { + tokens::comma().flat_map( + move |_| rgb_number_parser().map(move |b| (r, g, b), Some("rgb")), + Some("comma2"), + ) + }, + Some("rgb_step"), + ) + .flat_map( + |(r, g, b)| tokens::close_paren().map(move |_| (r, g, b), Some("final_rgb")), + Some("close"), + ) + } + + pub fn rgb_space_full() -> TokenParser<(f64, f64, f64)> { + function_parser("rgb") + .flat_map( + |_| { + rgb_number_parser() + .separated_by(tokens::whitespace()) + .one_or_more() + .where_fn(|vals| vals.len() >= 3, Some("has_3_values")) + }, + Some("rgb_values"), + ) + .flat_map( + |vals| tokens::close_paren().map(move |_| (vals[0], vals[1], vals[2]), Some("final_rgb")), + Some("close"), + ) + } + + /// `rgba(r, g, b, a)` with commas. + pub fn rgba_comma_full() -> TokenParser<(f64, f64, f64, f64)> { + function_parser("rgba") + .flat_map(|_| rgb_number_parser(), Some("r")) + .flat_map( + |r| { + tokens::comma().flat_map( + move |_| rgb_number_parser().map(move |g| (r, g), Some("rg")), + Some("comma1"), + ) + }, + Some("rg_step"), + ) + .flat_map( + |(r, g)| { + tokens::comma().flat_map( + move |_| rgb_number_parser().map(move |b| (r, g, b), Some("rgb")), + Some("comma2"), + ) + }, + Some("rgb_step"), + ) + .flat_map( + |(r, g, b)| { + tokens::comma().flat_map( + move |_| alpha_as_number().map(move |a| (r, g, b, a), Some("rgba")), + Some("comma3"), + ) + }, + Some("rgba_step"), + ) + .flat_map( + |(r, g, b, a)| tokens::close_paren().map(move |_| (r, g, b, a), Some("final_rgba")), + Some("close"), + ) + } + + /// `rgb(r g b / a)`: space-separated with a slash before alpha. + pub fn rgba_space_slash_full() -> TokenParser<(f64, f64, f64, f64)> { + function_parser("rgb") // Note: rgb function, not rgba! + .flat_map( + |_| { + tokens::whitespace() + .optional() + .flat_map(move |_| rgb_number_parser(), Some("r")) + }, + Some("optional_space_r"), + ) + .flat_map( + |r| { + tokens::whitespace().flat_map( + move |_| rgb_number_parser().map(move |g| (r, g), Some("rg")), + Some("space1"), + ) + }, + Some("rg_step"), + ) + .flat_map( + |(r, g)| { + tokens::whitespace().flat_map( + move |_| rgb_number_parser().map(move |b| (r, g, b), Some("rgb")), + Some("space2"), + ) + }, + Some("rgb_step"), + ) + .flat_map( + |(r, g, b)| { + tokens::whitespace().optional().flat_map( + move |_| { + tokens::delim('/').flat_map( + move |_| { + tokens::whitespace().optional().flat_map( + move |_| alpha_as_number().map(move |a| (r, g, b, a), Some("rgba")), + Some("alpha"), + ) + }, + Some("slash"), + ) + }, + Some("slash_alpha"), + ) + }, + Some("rgba_step"), + ) + .flat_map( + |(r, g, b, a)| { + tokens::whitespace().optional().flat_map( + move |_| tokens::close_paren().map(move |_| (r, g, b, a), Some("final_rgba")), + Some("close"), + ) + }, + Some("final_close"), + ) + } + + pub fn hsl_comma_full() -> TokenParser<(Angle, Percentage, Percentage)> { + function_parser("hsl") + .flat_map(|_| Angle::parser(), Some("h")) + .flat_map( + |h| { + tokens::comma().flat_map( + { + move |_| { + Percentage::parser().map( + { + let value = h.clone(); + move |s| (value.clone(), s) + }, + Some("hs"), + ) + } + }, + Some("comma1"), + ) + }, + Some("h_step"), + ) + .flat_map( + |(h, s)| { + tokens::comma().flat_map( + { + move |_| { + Percentage::parser().map( + { + let h_value = h.clone(); + let s_value = s.clone(); + move |l| (h_value.clone(), s_value.clone(), l) + }, + Some("hsl"), + ) + } + }, + Some("comma2"), + ) + }, + Some("s_step"), + ) + .flat_map( + |(h, s, l)| { + tokens::close_paren().map( + move |_| (h.clone(), s.clone(), l.clone()), + Some("final_hsl"), + ) + }, + Some("close"), + ) + } + + pub fn hsla_comma_full() -> TokenParser<(Angle, Percentage, Percentage, f64)> { + function_parser("hsla") + .flat_map(|_| Angle::parser(), Some("h")) + .flat_map( + |h| { + tokens::comma().flat_map( + { + move |_| { + Percentage::parser().map( + { + let value = h.clone(); + move |s| (value.clone(), s) + }, + Some("hs"), + ) + } + }, + Some("comma1"), + ) + }, + Some("h_step"), + ) + .flat_map( + |(h, s)| { + tokens::comma().flat_map( + { + move |_| { + Percentage::parser().map( + { + let h_value = h.clone(); + let s_value = s.clone(); + move |l| (h_value.clone(), s_value.clone(), l) + }, + Some("hsl"), + ) + } + }, + Some("comma2"), + ) + }, + Some("s_step"), + ) + .flat_map( + |(h, s, l)| { + tokens::comma().flat_map( + { + move |_| { + alpha_as_number().map( + { + let h_value = h.clone(); + let s_value = s.clone(); + let l_value = l.clone(); + move |a| (h_value.clone(), s_value.clone(), l_value.clone(), a) + }, + Some("hsla"), + ) + } + }, + Some("comma3"), + ) + }, + Some("l_step"), + ) + .flat_map( + |(h, s, l, a)| { + tokens::close_paren().map( + move |_| (h.clone(), s.clone(), l.clone(), a), + Some("final_hsla"), + ) + }, + Some("close"), + ) + } +} + +/// All CSS Level 4 named colors. +const NAMED_COLORS: &[&str] = &[ + "aliceblue", + "antiquewhite", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanchedalmond", + "blue", + "blueviolet", + "brown", + "burlywood", + "cadetblue", + "chartreuse", + "chocolate", + "coral", + "cornflowerblue", + "cornsilk", + "crimson", + "cyan", + "darkblue", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "firebrick", + "floralwhite", + "forestgreen", + "fuchsia", + "gainsboro", + "ghostwhite", + "gold", + "goldenrod", + "gray", + "grey", + "green", + "greenyellow", + "honeydew", + "hotpink", + "indianred", + "indigo", + "ivory", + "khaki", + "lavender", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightblue", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightgrey", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "lime", + "limegreen", + "linen", + "magenta", + "maroon", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "moccasin", + "navajowhite", + "navy", + "oldlace", + "olive", + "olivedrab", + "orange", + "orangered", + "orchid", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "peru", + "pink", + "plum", + "powderblue", + "purple", + "rebeccapurple", + "red", + "rosybrown", + "royalblue", + "saddlebrown", + "salmon", + "sandybrown", + "seagreen", + "seashell", + "sienna", + "silver", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "snow", + "springgreen", + "steelblue", + "tan", + "teal", + "thistle", + "tomato", + "turquoise", + "violet", + "wheat", + "white", + "whitesmoke", + "yellow", + "yellowgreen", + "transparent", + "currentcolor", +]; + +/// Base Color trait that all color types implement +pub trait ColorTrait { + fn to_string(&self) -> String; +} + +/// Main Color enum that encompasses all color types +#[derive(Debug, Clone, PartialEq)] +pub enum Color { + Named(NamedColor), + Hash(HashColor), + Rgb(Rgb), + Rgba(Rgba), + Hsl(Hsl), + Hsla(Hsla), + Lch(Lch), + Oklch(Oklch), + Oklab(Oklab), +} + +impl Color { + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![ + NamedColor::parse().map(Color::Named, Some("named")), + HashColor::parse().map(Color::Hash, Some("hash")), + Rgb::parse().map(Color::Rgb, Some("rgb")), + Rgba::parse().map(Color::Rgba, Some("rgba")), + Hsl::parse().map(Color::Hsl, Some("hsl")), + Hsla::parse().map(Color::Hsla, Some("hsla")), + Lch::parse().map(Color::Lch, Some("lch")), + Oklch::parse().map(Color::Oklch, Some("oklch")), + Oklab::parse().map(Color::Oklab, Some("oklab")), + ]) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Color::Named(named) => named.fmt(f), + Color::Hash(hash) => hash.fmt(f), + Color::Rgb(rgb) => rgb.fmt(f), + Color::Rgba(rgba) => rgba.fmt(f), + Color::Hsl(hsl) => hsl.fmt(f), + Color::Hsla(hsla) => hsla.fmt(f), + Color::Lch(lch) => lch.fmt(f), + Color::Oklch(oklch) => oklch.fmt(f), + Color::Oklab(oklab) => oklab.fmt(f), + } + } +} + +/// Named color like "red", "blue", etc. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NamedColor { + pub value: String, +} + +impl NamedColor { + pub fn new(value: impl Into) -> Self { + Self { + value: value.into(), + } + } + + /// Check if a string is a valid named color + pub fn is_valid_named_color(name: &str) -> bool { + NAMED_COLORS.contains(&name.to_lowercase().as_str()) + } + + /// The string value of an Ident token; unreachable for others (guarded by `tokens::ident()`). + fn extract_ident_value(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + stylex_unreachable!() + } + } + + /// Parser for named colors + pub fn parse() -> TokenParser { + tokens::ident() + .map(Self::extract_ident_value, Some("extract_ident")) + .where_fn( + |value| Self::is_valid_named_color(value), + Some("valid_named_color"), + ) + .map(NamedColor::new, Some("to_named_color")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for NamedColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +/// Hex color like #FF0000, #F00, #FF0000FF +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HashColor { + pub value: String, // Hex value without the # +} + +impl HashColor { + pub fn new(value: impl Into) -> Self { + Self { + value: value.into(), + } + } + + /// Validate hex color format + pub fn is_valid_hex(value: &str) -> bool { + let valid_lengths = [3, 6, 8]; // 3-digit, 6-digit, 8-digit (with alpha) + valid_lengths.contains(&value.len()) && value.chars().all(|c| c.is_ascii_hexdigit()) + } + + /// The `index`th digit of a 3-digit value, doubled (`#abc` reads `aa`, `bb`, `cc`); `None` if + /// absent (`new` is `pub` and the length guard counts bytes). + fn expanded_short_digit(&self, index: usize) -> Option { + let digit = self.value.chars().nth(index)?; + + // Doubling a hex digit is multiplying by `0x11`; `15 * 17` is 255, so no overflow. + let doubled = digit.to_digit(16)? * 17; + + u8::try_from(doubled).ok() + } + + /// The two-byte hex pair starting at `start`, or `None` if absent or not on a char boundary + /// (a slice panic would abort the NAPI host). + fn hex_pair(&self, start: usize) -> Option { + let pair = self.value.get(start..start + 2)?; + + u8::from_str_radix(pair, 16).ok() + } + + /// Get red component (0-255) + pub fn r(&self) -> u8 { + match self.value.len() { + // 3-digit hex: #RGB -> expand to #RRGGBB + 3 => self.expanded_short_digit(0).unwrap_or(0), + // 6-digit or 8-digit hex + 6 | 8 => self.hex_pair(0).unwrap_or(0), + _ => 0, + } + } + + /// Get green component (0-255) + pub fn g(&self) -> u8 { + match self.value.len() { + 3 => self.expanded_short_digit(1).unwrap_or(0), + 6 | 8 => self.hex_pair(2).unwrap_or(0), + _ => 0, + } + } + + /// Get blue component (0-255) + pub fn b(&self) -> u8 { + match self.value.len() { + 3 => self.expanded_short_digit(2).unwrap_or(0), + 6 | 8 => self.hex_pair(4).unwrap_or(0), + _ => 0, + } + } + + /// Get alpha component (0.0-1.0) + pub fn a(&self) -> f64 { + match self.value.len() { + 8 => f64::from(self.hex_pair(6).unwrap_or(255)) / 255.0, + _ => 1.0, + } + } + + /// The string value of a Hash token; unreachable for others (guarded by `tokens::hash()`). + fn extract_hash_value(token: SimpleToken) -> String { + if let SimpleToken::Hash(value) = token { + value + } else { + stylex_unreachable!() + } + } + + /// Parser for hex colors + pub fn parse() -> TokenParser { + tokens::hash() + .map(Self::extract_hash_value, Some("extract_hash")) + .where_fn(|value| Self::is_valid_hex(value), Some("valid_hex")) + .map(HashColor::new, Some("to_hash_color")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for HashColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "#{}", self.value) + } +} + +/// RGB color: rgb(255, 0, 0) +#[derive(Debug, Clone, PartialEq)] +pub struct Rgb { + pub r: f64, + pub g: f64, + pub b: f64, +} + +impl Rgb { + pub fn new(r: f64, g: f64, b: f64) -> Self { + Self { r, g, b } + } + + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![Self::comma_parser(), Self::space_parser()]) + } + + /// Parse comma-separated RGB: rgb(255, 0, 0) + fn comma_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected RGB function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "rgb" { + return Err(CssParseError::ParseError { + message: format!("Expected 'rgb' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let r = parse_rgb_channel_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let g = parse_rgb_channel_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let b = parse_rgb_channel_token(tokens)?; + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Rgb::new(r, g, b)) + }, + "rgb_comma_parser", + ) + } + + /// Parse space-separated RGB: rgb(255 0 0) + fn space_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected RGB function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "rgb" { + return Err(CssParseError::ParseError { + message: format!("Expected 'rgb' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + let r = parse_rgb_channel_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let g = parse_rgb_channel_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let b = parse_rgb_channel_token(tokens)?; + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Rgb::new(r, g, b)) + }, + "rgb_space_parser", + ) + } + + fn consume_comma_with_optional_whitespace(tokens: &mut TokenList) -> Result<(), CssParseError> { + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let comma_token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected comma".to_string(), + })?; + + if !matches!(comma_token, SimpleToken::Comma) { + return Err(CssParseError::ParseError { + message: format!("Expected ',' token, got {:?}", comma_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + Ok(()) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Rgb { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "rgb({},{},{})", + to_js_string(self.r), + to_js_string(self.g), + to_js_string(self.b) + ) + } +} + +/// RGBA color: rgba(255, 0, 0, 0.5) +#[derive(Debug, Clone, PartialEq)] +pub struct Rgba { + pub r: f64, + pub g: f64, + pub b: f64, + pub a: f64, +} + +impl Rgba { + pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self { + Self { r, g, b, a } + } + + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![Self::comma_parser(), Self::space_slash_parser()]) + } + + /// Parse comma-separated RGBA: rgba(255, 0, 0, 0.5) + fn comma_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected RGBA function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "rgba" { + return Err(CssParseError::ParseError { + message: format!("Expected 'rgba' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let r = parse_rgb_channel_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let g = parse_rgb_channel_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let b = parse_rgb_channel_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let a = parse_alpha_token(tokens)?; + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Rgba::new(r, g, b, a)) + }, + "rgba_comma_parser", + ) + } + + /// Parse space-separated RGBA with slash: `rgb(255 0 0 / 0.5)` or `rgba(255 0 0 / 0.5)`. + fn space_slash_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected RGB/RGBA function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "rgb" && name.to_lowercase() != "rgba" { + return Err(CssParseError::ParseError { + message: format!("Expected 'rgb' or 'rgba' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + let r = parse_rgb_channel_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let g = parse_rgb_channel_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let b = parse_rgb_channel_token(tokens)?; + + if let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let slash_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected slash".to_string(), + })?; + if !matches!(slash_token, SimpleToken::Delim('/')) { + return Err(CssParseError::ParseError { + message: format!("Expected '/' token, got {:?}", slash_token), + }); + } + + if let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let a = parse_alpha_token(tokens)?; + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Rgba::new(r, g, b, a)) + }, + "rgba_space_slash_parser", + ) + } + + fn consume_comma_with_optional_whitespace(tokens: &mut TokenList) -> Result<(), CssParseError> { + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let comma_token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected comma".to_string(), + })?; + + if !matches!(comma_token, SimpleToken::Comma) { + return Err(CssParseError::ParseError { + message: format!("Expected ',' token, got {:?}", comma_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + Ok(()) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Rgba { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "rgba({},{},{},{})", + to_js_string(self.r), + to_js_string(self.g), + to_js_string(self.b), + to_js_string(self.a) + ) + } +} + +/// HSL color: hsl(360deg, 100%, 50%) +#[derive(Debug, Clone, PartialEq)] +pub struct Hsl { + pub h: Angle, // hue angle (0-360deg) + pub s: Percentage, // saturation percentage (0-100%) + pub l: Percentage, // lightness percentage (0-100%) +} + +impl Hsl { + pub fn new(h: Angle, s: Percentage, l: Percentage) -> Self { + Self { h, s, l } + } + + /// Construct from primitive values (used by tests). + pub fn from_primitives(h: f64, s: f64, l: f64) -> Self { + Self { + h: Angle::new(h, "deg"), + s: Percentage::new(s), + l: Percentage::new(l), + } + } + + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![Self::comma_parser(), Self::space_parser()]) + } + + /// Parse comma-separated HSL: hsl(360deg, 100%, 50%) + fn comma_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected HSL function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "hsl" { + return Err(CssParseError::ParseError { + message: format!("Expected 'hsl' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let h = Self::parse_hsl_hue_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let s = Self::parse_hsl_percentage_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let l = Self::parse_hsl_percentage_token(tokens)?; + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Hsl::new(h, s, l)) + }, + "hsl_comma_parser", + ) + } + + /// Parse space-separated HSL: hsl(360deg 100% 50%) + fn space_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected HSL function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "hsl" { + return Err(CssParseError::ParseError { + message: format!("Expected 'hsl' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + let h = Self::parse_hsl_hue_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let s = Self::parse_hsl_percentage_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let l = Self::parse_hsl_percentage_token(tokens)?; + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Hsl::new(h, s, l)) + }, + "hsl_space_parser", + ) + } + + /// Helper: Parse HSL hue token (angle or number treated as degrees) + fn parse_hsl_hue_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected hue value token".to_string(), + })?; + + match token { + SimpleToken::Dimension { value, unit } => { + if Angle::is_valid_unit(&unit) { + Ok(Angle::new(value, unit)) + } else { + Err(CssParseError::ParseError { + message: format!("Invalid angle unit: {}", unit), + }) + } + }, + SimpleToken::Number(value) => { + // Treat numbers as degrees (CSS standard for HSL) + Ok(Angle::new(value, "deg")) + }, + _ => Err(CssParseError::ParseError { + message: format!( + "Expected Number or Dimension token for hue, got {:?}", + token + ), + }), + } + } + + /// Helper: Parse HSL percentage token + fn parse_hsl_percentage_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected percentage token".to_string(), + })?; + + if let SimpleToken::Percentage(value) = token { + // The token already carries the authored percent: `50%` is `50`. + Ok(Percentage::new(value)) + } else { + Err(CssParseError::ParseError { + message: format!("Expected Percentage token, got {:?}", token), + }) + } + } + + fn consume_comma_with_optional_whitespace(tokens: &mut TokenList) -> Result<(), CssParseError> { + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let comma_token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected comma".to_string(), + })?; + + if !matches!(comma_token, SimpleToken::Comma) { + return Err(CssParseError::ParseError { + message: format!("Expected ',' token, got {:?}", comma_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + Ok(()) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Hsl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "hsl({},{},{})", self.h, self.s, self.l) + } +} + +/// HSLA color: hsla(360deg, 100%, 50%, 0.5) +#[derive(Debug, Clone, PartialEq)] +pub struct Hsla { + pub h: Angle, // hue angle (0-360deg) + pub s: Percentage, // saturation percentage (0-100%) + pub l: Percentage, // lightness percentage (0-100%) + pub a: f64, // alpha, unbounded like every other legacy alpha +} + +impl Hsla { + pub fn new(h: Angle, s: Percentage, l: Percentage, a: f64) -> Self { + Self { h, s, l, a } + } + + /// Construct from primitive values (used by tests). + pub fn from_primitives(h: f64, s: f64, l: f64, a: f64) -> Self { + Self { + h: Angle::new(h, "deg"), + s: Percentage::new(s), + l: Percentage::new(l), + a, + } + } + + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![Self::comma_parser(), Self::space_slash_parser()]) + } + + /// Parse comma-separated HSLA: hsla(360deg, 100%, 50%, 0.5) + fn comma_parser() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected HSLA function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "hsla" { + return Err(CssParseError::ParseError { + message: format!("Expected 'hsla' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + let h = Self::parse_hsla_hue_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let s = Self::parse_hsla_percentage_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let l = Self::parse_hsla_percentage_token(tokens)?; + + Self::consume_comma_with_optional_whitespace(tokens)?; + + let a = parse_alpha_token(tokens)?; + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Hsla::new(h, s, l, a)) + }, + "hsla_comma_parser", + ) + } + + /// Parse space-separated HSLA with slash: hsl(360deg 100% 50% / 0.5) + fn space_slash_parser() -> TokenParser { + TokenParser::new( + |tokens| { + // Function("hsl"), not hsla + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected HSL function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "hsl" { + return Err(CssParseError::ParseError { + message: format!("Expected 'hsl' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + let h = Self::parse_hsla_hue_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let s = Self::parse_hsla_percentage_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let l = Self::parse_hsla_percentage_token(tokens)?; + + if let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let slash_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected slash".to_string(), + })?; + if !matches!(slash_token, SimpleToken::Delim('/')) { + return Err(CssParseError::ParseError { + message: format!("Expected '/' token, got {:?}", slash_token), + }); + } + + if let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let a = parse_alpha_token(tokens)?; + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Hsla::new(h, s, l, a)) + }, + "hsla_space_slash_parser", + ) + } + + /// Helper: Parse HSLA hue token (angle or number treated as degrees) + fn parse_hsla_hue_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected hue value token".to_string(), + })?; + + match token { + SimpleToken::Dimension { value, unit } => { + if Angle::is_valid_unit(&unit) { + Ok(Angle::new(value, unit)) + } else { + Err(CssParseError::ParseError { + message: format!("Invalid angle unit: {}", unit), + }) + } + }, + SimpleToken::Number(value) => { + // Treat numbers as degrees (CSS standard for HSL/HSLA) + Ok(Angle::new(value, "deg")) + }, + _ => Err(CssParseError::ParseError { + message: format!( + "Expected Number or Dimension token for hue, got {:?}", + token + ), + }), + } + } + + /// Helper: Parse HSLA percentage token + fn parse_hsla_percentage_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected percentage token".to_string(), + })?; + + if let SimpleToken::Percentage(value) = token { + // The token already carries the authored percent: `50%` is `50`. + Ok(Percentage::new(value)) + } else { + Err(CssParseError::ParseError { + message: format!("Expected Percentage token, got {:?}", token), + }) + } + } + + fn consume_comma_with_optional_whitespace(tokens: &mut TokenList) -> Result<(), CssParseError> { + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + let comma_token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected comma".to_string(), + })?; + + if !matches!(comma_token, SimpleToken::Comma) { + return Err(CssParseError::ParseError { + message: format!("Expected ',' token, got {:?}", comma_token), + }); + } + + while let Some(SimpleToken::Whitespace) = tokens.peek_infallible() { + tokens.consume_next_token_infallible(); + } + + Ok(()) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Hsla { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "hsla({},{},{},{})", + self.h, + self.s, + self.l, + to_js_string(self.a) + ) + } +} + +/// LCH color: lch(50% 100 270deg) or lch(50 100 270) +#[derive(Debug, Clone, PartialEq)] +pub struct Lch { + pub l: f64, // lightness (0-100) - can be percentage, number, or 'none' + pub c: f64, // chroma (0-150) - can be percentage or number + pub h: LchHue, // hue - can be angle or number + pub alpha: Option, // alpha (0-1) - optional with slash syntax +} + +#[derive(Debug, Clone, PartialEq)] +pub enum LchHue { + Angle(Angle), + Number(f64), +} + +impl LchHue { + pub fn from_angle(angle: Angle) -> Self { + LchHue::Angle(angle) + } + + pub fn from_number(number: f64) -> Self { + LchHue::Number(number) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl std::fmt::Display for LchHue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LchHue::Angle(angle) => write!(f, "{}", angle), + LchHue::Number(number) => write_js_number(f, *number), + } + } +} + +impl Lch { + pub fn new(l: f64, c: f64, h: LchHue, alpha: Option) -> Self { + Self { l, c, h, alpha } + } + + pub fn new_with_angle(l: f64, c: f64, h: Angle, alpha: Option) -> Self { + Self::new(l, c, LchHue::Angle(h), alpha) + } + + pub fn new_with_number(l: f64, c: f64, h: f64, alpha: Option) -> Self { + Self::new(l, c, LchHue::Number(h), alpha) + } + + /// Parses `lch(l c h)` and `lch(l c h / alpha)`. + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + let function_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected LCH function".to_string(), + })?; + + if let SimpleToken::Function(name) = function_token { + if name.to_lowercase() != "lch" { + return Err(CssParseError::ParseError { + message: format!("Expected 'lch' function, got '{}'", name), + }); + } + } else { + return Err(CssParseError::ParseError { + message: format!("Expected Function token, got {:?}", function_token), + }); + } + + let l = Self::parse_lch_lightness_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let c = Self::parse_lch_chroma_token(tokens)?; + + let whitespace_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected whitespace".to_string(), + })?; + if !matches!(whitespace_token, SimpleToken::Whitespace) { + return Err(CssParseError::ParseError { + message: format!("Expected whitespace token, got {:?}", whitespace_token), + }); + } + + let h = Self::parse_lch_hue_token(tokens)?; + + let alpha = parse_optional_slash_alpha(tokens)?; + + let close_token = + tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected closing parenthesis".to_string(), + })?; + + if !matches!(close_token, SimpleToken::RightParen) { + return Err(CssParseError::ParseError { + message: format!("Expected ')' token, got {:?}", close_token), + }); + } + + Ok(Lch::new(l, c, h, alpha)) + }, + "lch_parser", + ) + } + + /// Helper: Parse LCH lightness token (percentage or number) + fn parse_lch_lightness_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected lightness value token".to_string(), + })?; + + match token { + SimpleToken::Percentage(value) => { + // The token already carries the authored percent: `50%` is `50`. + Ok(value) + }, + SimpleToken::Number(value) => Ok(value), + _ => Err(CssParseError::ParseError { + message: format!( + "Expected Number or Percentage token for lightness, got {:?}", + token + ), + }), + } + } + + /// Helper: Parse LCH chroma token (number) + fn parse_lch_chroma_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected chroma value token".to_string(), + })?; + + if let SimpleToken::Number(value) = token { + Ok(value) + } else { + Err(CssParseError::ParseError { + message: format!("Expected Number token for chroma, got {:?}", token), + }) + } + } + + /// Helper: Parse LCH hue token (angle or number) + fn parse_lch_hue_token(tokens: &mut TokenList) -> Result { + let token = tokens + .consume_next_token_infallible() + .ok_or(CssParseError::ParseError { + message: "Expected hue value token".to_string(), + })?; + + match token { + SimpleToken::Dimension { value, unit } => { + if crate::css_types::Angle::is_valid_unit(&unit) { + let angle = crate::css_types::Angle::new(value, unit); + Ok(LchHue::Angle(angle)) + } else { + Err(CssParseError::ParseError { + message: format!("Invalid angle unit: {}", unit), + }) + } + }, + SimpleToken::Number(value) => { + // Treat numbers as plain numbers (not degrees) + Ok(LchHue::Number(value)) + }, + _ => Err(CssParseError::ParseError { + message: format!( + "Expected Number or Dimension token for hue, got {:?}", + token + ), + }), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Lch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match printed_alpha(self.alpha) { + Some(alpha) => write!( + f, + "lch({} {} {} / {})", + to_js_string(self.l), + to_js_string(self.c), + self.h, + to_js_string(alpha) + ), + None => write!( + f, + "lch({} {} {})", + to_js_string(self.l), + to_js_string(self.c), + self.h + ), + } + } +} + +/// OKLCH color: oklch(0.5 0.1 270deg) +#[derive(Debug, Clone, PartialEq)] +pub struct Oklch { + pub l: f64, // lightness (0-1) + pub c: f64, // chroma (0-0.4) + pub h: Angle, // hue angle + pub alpha: Option, // alpha (0-1) +} + +impl Oklch { + pub fn new(l: f64, c: f64, h: Angle, alpha: Option) -> Self { + Self { l, c, h, alpha } + } + + /// Parser for OKLCH colors; `none` maps to 0, alpha is optional after a slash. + pub fn parse() -> TokenParser { + TokenParser::new( + move |input| { + match input.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name.to_lowercase() == "oklch" => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected oklch() function".to_string(), + }); + }, + } + + let l = Self::parse_oklch_lc_value(input)?; + + match input.consume_next_token_infallible() { + Some(SimpleToken::Whitespace) => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected whitespace after lightness".to_string(), + }); + }, + } + + let c = Self::parse_oklch_lc_value(input)?; + + match input.consume_next_token_infallible() { + Some(SimpleToken::Whitespace) => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected whitespace after chroma".to_string(), + }); + }, + } + + let h = Self::parse_oklch_hue(input)?; + + let alpha = parse_optional_slash_alpha(input)?; + + match input.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected ) after oklch values".to_string(), + }); + }, + } + + Ok(Oklch::new(l, c, h, alpha)) + }, + "oklch_parser", + ) + } + + /// Parse OKLCH lightness/chroma value: number | 'none' + fn parse_oklch_lc_value(input: &mut crate::token_types::TokenList) -> Result { + match input.consume_next_token_infallible() { + Some(SimpleToken::Number(n)) => Ok(n), + Some(SimpleToken::Ident(keyword)) if keyword == "none" => Ok(0.0), + _ => Err(CssParseError::ParseError { + message: "Expected number or 'none'".to_string(), + }), + } + } + + /// Parse OKLCH hue: angle | number (interpreted as degrees) | 'none' + fn parse_oklch_hue(input: &mut crate::token_types::TokenList) -> Result { + match input.consume_next_token_infallible() { + Some(SimpleToken::Dimension { value, unit }) => { + if Angle::is_valid_unit(&unit) { + Ok(Angle::new(value, unit)) + } else { + Err(CssParseError::ParseError { + message: format!("Invalid angle unit: {}", unit), + }) + } + }, + Some(SimpleToken::Number(n)) => Ok(Angle::new(n, "deg")), + Some(SimpleToken::Ident(keyword)) if keyword == "none" => Ok(Angle::new(0.0, "deg")), + _ => Err(CssParseError::ParseError { + message: "Expected hue: angle, number, or 'none'".to_string(), + }), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Oklch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match printed_alpha(self.alpha) { + Some(alpha) => write!( + f, + "oklch({} {} {} / {})", + to_js_string(self.l), + to_js_string(self.c), + self.h, + to_js_string(alpha) + ), + None => write!( + f, + "oklch({} {} {})", + to_js_string(self.l), + to_js_string(self.c), + self.h + ), + } + } +} + +/// OKLAB color: oklab(0.5 0.1 0.1) +#[derive(Debug, Clone, PartialEq)] +pub struct Oklab { + pub l: f64, // lightness (0-1) + pub a: f64, // green-red (-0.4 to 0.4) + pub b: f64, // blue-yellow (-0.4 to 0.4) + pub alpha: Option, // alpha (0-1) +} + +impl Oklab { + pub fn new(l: f64, a: f64, b: f64, alpha: Option) -> Self { + Self { l, a, b, alpha } + } + + /// Parser for OKLAB colors; `none` maps to 0, alpha is optional after a slash. + pub fn parse() -> TokenParser { + TokenParser::new( + move |input| { + match input.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name.to_lowercase() == "oklab" => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected oklab() function".to_string(), + }); + }, + } + + let l = Self::parse_oklab_lab_value(input)?; + + match input.consume_next_token_infallible() { + Some(SimpleToken::Whitespace) => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected whitespace after lightness".to_string(), + }); + }, + } + + let a = Self::parse_oklab_lab_value(input)?; + + match input.consume_next_token_infallible() { + Some(SimpleToken::Whitespace) => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected whitespace after a component".to_string(), + }); + }, + } + + let b = Self::parse_oklab_lab_value(input)?; + + let alpha = parse_optional_slash_alpha(input)?; + + match input.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + _ => { + return Err(CssParseError::ParseError { + message: "Expected ) after oklab values".to_string(), + }); + }, + } + + Ok(Oklab::new(l, a, b, alpha)) + }, + "oklab_parser", + ) + } + + /// Parse OKLAB l/a/b value: number | 'none' + fn parse_oklab_lab_value( + input: &mut crate::token_types::TokenList, + ) -> Result { + match input.consume_next_token_infallible() { + Some(SimpleToken::Number(n)) => Ok(n), + Some(SimpleToken::Ident(keyword)) if keyword == "none" => Ok(0.0), + _ => Err(CssParseError::ParseError { + message: "Expected number or 'none'".to_string(), + }), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Oklab { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match printed_alpha(self.alpha) { + Some(alpha) => write!( + f, + "oklab({} {} {} / {})", + to_js_string(self.l), + to_js_string(self.a), + to_js_string(self.b), + to_js_string(alpha) + ), + None => write!( + f, + "oklab({} {} {})", + to_js_string(self.l), + to_js_string(self.a), + to_js_string(self.b) + ), + } + } +} + +#[cfg(test)] +#[path = "../tests/css_types/color_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/color_test.rs"] +mod color_test; + +#[cfg(test)] +#[path = "../tests/css_types/color_coverage_test.rs"] +mod color_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/common_types.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/common_types.rs new file mode 100644 index 000000000..f4bfde40a --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/common_types.rs @@ -0,0 +1,266 @@ +/*! +Shared CSS types and utilities used across value parsing. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::{to_js_string, write_js_number}; + +use crate::{ + token_parser::{TokenParser, tokens}, + token_types::SimpleToken, +}; +use std::fmt::{self, Display}; + +/// CSS-wide keywords that can be used with any CSS property +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CssWideKeyword { + Inherit, + Initial, + Unset, + Revert, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CssWideKeyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CssWideKeyword::Inherit => write!(f, "inherit"), + CssWideKeyword::Initial => write!(f, "initial"), + CssWideKeyword::Unset => write!(f, "unset"), + CssWideKeyword::Revert => write!(f, "revert"), + } + } +} + +impl CssWideKeyword { + fn extract_ident(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + stylex_unreachable!() + } + } + + fn ident_to_keyword(value: String) -> CssWideKeyword { + match value.as_str() { + "inherit" => CssWideKeyword::Inherit, + "initial" => CssWideKeyword::Initial, + "unset" => CssWideKeyword::Unset, + "revert" => CssWideKeyword::Revert, + _ => stylex_unreachable!(), + } + } + + /// Parser for CSS-wide keywords + pub fn parser() -> TokenParser { + tokens::ident() + .map(Self::extract_ident, Some(".value")) + .where_fn( + |value| matches!(value.as_str(), "inherit" | "initial" | "unset" | "revert"), + Some("css_wide_keyword"), + ) + .map(Self::ident_to_keyword, Some("to_keyword")) + } + + /// Parser specifically for 'inherit' + pub fn inherit_parser() -> TokenParser { + Self::parser().where_fn( + |keyword| matches!(keyword, CssWideKeyword::Inherit), + Some("inherit"), + ) + } + + /// Parser specifically for 'initial' + pub fn initial_parser() -> TokenParser { + Self::parser().where_fn( + |keyword| matches!(keyword, CssWideKeyword::Initial), + Some("initial"), + ) + } + + /// Parser specifically for 'unset' + pub fn unset_parser() -> TokenParser { + Self::parser().where_fn( + |keyword| matches!(keyword, CssWideKeyword::Unset), + Some("unset"), + ) + } + + /// Parser specifically for 'revert' + pub fn revert_parser() -> TokenParser { + Self::parser().where_fn( + |keyword| matches!(keyword, CssWideKeyword::Revert), + Some("revert"), + ) + } +} + +/// CSS 'auto' keyword +pub fn auto_parser() -> TokenParser { + TokenParser::::string("auto").map(|_| "auto".to_string(), Some("auto_keyword")) +} + +/// CSS variable reference: var(--name) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CssVariable { + pub name: String, +} + +impl CssVariable { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } + + fn extract_ident_string(tok: SimpleToken) -> String { + if let SimpleToken::Ident(s) = tok { + s + } else { + String::new() + } + } + + /// Parser for CSS variables: var(--name) + pub fn parser() -> TokenParser { + // var( + let fn_var = crate::token_parser::TokenParser::::fn_name("var"); + + // --ident + let dashed_ident = crate::token_parser::TokenParser::::token( + crate::token_types::SimpleToken::Ident(String::new()), + Some("Ident"), + ) + .map(Self::extract_ident_string, Some(".value")) + .where_fn(|s| s.starts_with("--"), Some("starts_with_--")); + + // ) + let close_paren = crate::token_parser::TokenParser::::token( + crate::token_types::SimpleToken::RightParen, + Some("RightParen"), + ); + + fn_var + .flat_map(move |_| dashed_ident.clone(), Some("name")) + .flat_map( + move |name| close_paren.map(move |_| name.clone(), Some(")")), + Some("close"), + ) + .map(CssVariable::new, Some("to_css_variable")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CssVariable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "var({})", self.name) + } +} + +/// CSS percentage value +#[derive(Debug, Clone, PartialEq)] +pub struct Percentage { + pub value: f64, +} + +impl Percentage { + pub fn new(value: f64) -> Self { + Self { value } + } + + fn token_to_percentage(token: SimpleToken) -> Percentage { + if let SimpleToken::Percentage(value) = token { + // The token already carries the authored percent: `50%` is `50`. + Percentage::new(value) + } else { + stylex_unreachable!() + } + } + + /// Parser for percentage values + pub fn parser() -> TokenParser { + tokens::percentage().map(Self::token_to_percentage, Some("to_percentage")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Percentage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}%", to_js_string(self.value)) + } +} + +/// CSS number value +#[derive(Debug, Clone, PartialEq)] +pub struct Number { + pub value: f64, +} + +impl Number { + pub fn new(value: f64) -> Self { + Self { value } + } + + fn token_to_number(token: SimpleToken) -> Number { + if let SimpleToken::Number(value) = token { + Number::new(value) + } else { + stylex_unreachable!() + } + } + + /// Parser for number values + pub fn parser() -> TokenParser { + tokens::number().map(Self::token_to_number, Some("to_number")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Number { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write_js_number(f, self.value) + } +} + +/// Union type for number or percentage values +#[derive(Debug, Clone, PartialEq)] +pub enum NumberOrPercentage { + Number(Number), + Percentage(Percentage), +} + +impl NumberOrPercentage { + /// This value as a fraction: a number as itself, a percentage divided by 100. + /// Unlike upstream `numberOrPercentage`, a negative number stays negative (see + /// [`crate::css_types::alpha_value::parse_alpha_token`]). + pub fn as_fraction(&self) -> f64 { + match self { + NumberOrPercentage::Number(number) => number.value, + NumberOrPercentage::Percentage(percentage) => percentage.value / 100.0, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for NumberOrPercentage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NumberOrPercentage::Number(n) => n.fmt(f), + NumberOrPercentage::Percentage(p) => p.fmt(f), + } + } +} + +/// Parser for number or percentage values +pub fn number_or_percentage_parser() -> TokenParser { + TokenParser::one_of(vec![ + Number::parser().map(NumberOrPercentage::Number, Some("number")), + Percentage::parser().map(NumberOrPercentage::Percentage, Some("percentage")), + ]) +} + +#[cfg(test)] +#[path = "../tests/css_types/common_types_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/common_types_coverage_test.rs"] +mod common_types_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/custom_ident.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/custom_ident.rs new file mode 100644 index 000000000..41d8a2fef --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/custom_ident.rs @@ -0,0 +1,92 @@ +/*! +CSS ``: identifiers excluding CSS reserved keywords. +*/ + +use stylex_macros::stylex_unreachable; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// List of reserved CSS keywords that cannot be used as custom identifiers +const RESERVED_KEYWORDS: &[&str] = &[ + "unset", + "initial", + "inherit", + "default", + "none", + "auto", + "normal", + "hidden", + "visible", + "revert", + "revert-layer", +]; + +/// CSS Custom Identifier +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomIdentifier { + pub value: String, +} + +impl CustomIdentifier { + /// Create a new CustomIdentifier + pub fn new(value: impl Into) -> Self { + Self { + value: value.into(), + } + } + + /// Check if a string is a reserved keyword + pub fn is_reserved_keyword(value: &str) -> bool { + RESERVED_KEYWORDS.contains(&value.to_lowercase().as_str()) + } + + /// Check if a string is a valid custom identifier + pub fn is_valid_custom_ident(value: &str) -> bool { + !Self::is_reserved_keyword(value) && !value.is_empty() + } + + /// Get the list of reserved keywords + pub fn reserved_keywords() -> &'static [&'static str] { + RESERVED_KEYWORDS + } + + /// Extract the identifier string from a `SimpleToken::Ident`; other variants are unreachable. + pub(crate) fn extract_ident_token(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + stylex_unreachable!() + } + } + + /// Parser for CSS custom identifiers + pub fn parser() -> TokenParser { + TokenParser::::token(SimpleToken::Ident(String::new()), Some("Ident")) + .map(Self::extract_ident_token, Some("extract_ident")) + .where_fn( + |value| Self::is_valid_custom_ident(value), + Some("valid_custom_ident"), + ) + .map(CustomIdentifier::new, Some("to_custom_identifier")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CustomIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/custom_ident_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/custom_ident_test.rs"] +mod custom_ident_test; + +#[cfg(test)] +#[path = "../tests/css_types/custom_ident_coverage_test.rs"] +mod custom_ident_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/dashed_ident.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/dashed_ident.rs new file mode 100644 index 000000000..365546e26 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/dashed_ident.rs @@ -0,0 +1,67 @@ +/*! +CSS ``: identifiers starting with `--`. +*/ + +use stylex_macros::stylex_unreachable; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// CSS Dashed Identifier for custom properties +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DashedIdentifier { + pub value: String, +} + +impl DashedIdentifier { + /// Create a new DashedIdentifier + pub fn new(value: impl Into) -> Self { + Self { + value: value.into(), + } + } + + /// Whether `value` starts with `--` and has at least one character after it. + pub fn is_valid_dashed_ident(value: &str) -> bool { + value.starts_with("--") && value.len() > 2 + } + + /// Extract the identifier string from a `SimpleToken::Ident`; other variants are unreachable. + pub(crate) fn extract_ident_token(token: SimpleToken) -> String { + if let SimpleToken::Ident(value) = token { + value + } else { + stylex_unreachable!() + } + } + + /// Parser for CSS dashed identifiers + pub fn parser() -> TokenParser { + TokenParser::::token(SimpleToken::Ident(String::new()), Some("Ident")) + .map(Self::extract_ident_token, Some("extract_ident")) + .where_fn( + |value| Self::is_valid_dashed_ident(value), + Some("valid_dashed_ident"), + ) + .map(DashedIdentifier::new, Some("to_dashed_identifier")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for DashedIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/dashed_ident_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/dashed_ident_test.rs"] +mod dashed_ident_test; + +#[cfg(test)] +#[path = "../tests/css_types/dashed_ident_coverage_test.rs"] +mod dashed_ident_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/dimension.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/dimension.rs new file mode 100644 index 000000000..b9b8ff53b --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/dimension.rs @@ -0,0 +1,92 @@ +/*! +CSS dimension: a length, time, frequency or resolution. +*/ + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +use super::{frequency::Frequency, length::Length, resolution::Resolution, time::Time}; + +/// Union type for all dimensional CSS values +#[derive(Debug, Clone, PartialEq)] +pub enum Dimension { + Length(Length), + Time(Time), + Frequency(Frequency), + Resolution(Resolution), +} + +impl Dimension { + /// Check if a unit belongs to any dimension type + #[allow(dead_code)] + fn is_valid_dimension_unit(unit: &str) -> bool { + Length::is_valid_unit(unit) + || Time::is_valid_unit(unit) + || Frequency::is_valid_unit(unit) + || Resolution::is_valid_unit(unit) + } + + /// Create a Dimension from value and unit + fn from_value_and_unit(value: f64, unit: impl Into) -> Option { + let unit = unit.into(); + + if Length::is_valid_unit(&unit) { + Some(Dimension::Length(Length::new(value, unit))) + } else if Time::is_valid_unit(&unit) { + Some(Dimension::Time(Time::new(value, unit))) + } else if Frequency::is_valid_unit(&unit) { + Some(Dimension::Frequency(Frequency::new(value, unit))) + } else if Resolution::is_valid_unit(&unit) { + Some(Dimension::Resolution(Resolution::new(value, unit))) + } else { + None + } + } + + /// Extract a `Dimension` from a `SimpleToken::Dimension`; `None` for other variants. + pub(crate) fn extract_dimension_token(token: SimpleToken) -> Option { + if let SimpleToken::Dimension { value, unit } = token { + Self::from_value_and_unit(value, unit) + } else { + None + } + } + + /// Parser for dimensional values + pub fn parse() -> TokenParser { + use crate::token_parser::tokens; + + tokens::dimension() + .map(Self::extract_dimension_token, Some("extract_dimension")) + .where_fn(|opt| opt.is_some(), Some("valid_dimension")) + .map(|opt| opt.unwrap(), Some("unwrap_dimension")) + } +} + +pub fn dimension() -> TokenParser { + Dimension::parse() +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Dimension { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Dimension::Length(length) => length.fmt(f), + Dimension::Time(time) => time.fmt(f), + Dimension::Frequency(frequency) => frequency.fmt(f), + Dimension::Resolution(resolution) => resolution.fmt(f), + } + } +} + +#[cfg(test)] +#[path = "../tests/css_types/dimension_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/dimension_test.rs"] +mod dimension_test; + +#[cfg(test)] +#[path = "../tests/css_types/dimension_coverage_test.rs"] +mod dimension_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/easing_function.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/easing_function.rs new file mode 100644 index 000000000..ea991c9b0 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/easing_function.rs @@ -0,0 +1,578 @@ +/*! +CSS easing function parser. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::{to_js_string, write_js_number_list}; + +use crate::{ + CssParseError, + token_parser::TokenParser, + token_types::{SimpleToken, TokenList}, +}; +use std::fmt::{self, Display, Write as _}; + +#[derive(Debug, Clone, PartialEq)] +pub enum EasingFunction { + Linear(LinearEasingFunction), + CubicBezier(CubicBezierEasingFunction), + CubicBezierKeyword(CubicBezierKeyword), + Steps(StepsEasingFunction), + StepsKeyword(StepsKeyword), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LinearEasingFunction { + pub points: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CubicBezierEasingFunction { + pub points: [f64; 4], +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CubicBezierKeyword { + pub keyword: CubicBezierKeywordType, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct StepsEasingFunction { + pub steps: u32, + pub start: StepsStartType, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct StepsKeyword { + pub keyword: StepsKeywordType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CubicBezierKeywordType { + Ease, + EaseIn, + EaseOut, + EaseInOut, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StepsStartType { + Start, + End, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StepsKeywordType { + StepStart, + StepEnd, +} + +impl EasingFunction { + pub fn parse() -> TokenParser { + TokenParser::one_of(vec![ + LinearEasingFunction::parse().map(EasingFunction::Linear, Some("linear")), + CubicBezierEasingFunction::parse().map(EasingFunction::CubicBezier, Some("cubic_bezier")), + StepsEasingFunction::parse().map(EasingFunction::Steps, Some("steps")), + CubicBezierKeyword::parse().map( + EasingFunction::CubicBezierKeyword, + Some("cubic_bezier_keyword"), + ), + StepsKeyword::parse().map(EasingFunction::StepsKeyword, Some("steps_keyword")), + ]) + } +} + +impl LinearEasingFunction { + pub fn new(points: Vec) -> Self { + Self { points } + } + + pub(crate) fn parse_tokens( + tokens: &mut TokenList, + ) -> Result { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "linear" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected linear() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected linear() function but reached end of input".to_string(), + }); + }, + } + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + let mut points = Vec::new(); + + // `linear()` needs at least one point. + if matches!(tokens.peek(), Ok(Some(SimpleToken::RightParen))) { + return Err(CssParseError::ParseError { + message: "Linear function must have at least one point".to_string(), + }); + } + + loop { + let number = match tokens.consume_next_token_infallible() { + Some(SimpleToken::Number(value)) => value, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected number in linear function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected number but reached end of input".to_string(), + }); + }, + }; + + points.push(number); + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + // Consume comma (more numbers) or closing paren (end) + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Comma) => { + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + continue; // parse next number + }, + Some(SimpleToken::RightParen) => { + break; // done parsing numbers (RightParen consumed) + }, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!( + "Expected comma or closing paren in linear function, got {:?}", + token + ), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Unexpected end of input in linear function".to_string(), + }); + }, + } + } + + Ok(LinearEasingFunction::new(points)) + } + + pub fn parse() -> TokenParser { + TokenParser::new(Self::parse_tokens, "linear_parser") + } +} + +impl CubicBezierEasingFunction { + pub fn new(points: [f64; 4]) -> Self { + Self { points } + } + + pub(crate) fn parse_tokens( + tokens: &mut TokenList, + ) -> Result { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "cubic-bezier" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected cubic-bezier() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected cubic-bezier() function but reached end of input".to_string(), + }); + }, + } + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + // Parse exactly 4 numbers separated by commas: x1, y1, x2, y2 + let mut numbers = Vec::new(); + + for i in 0..4 { + if i > 0 { + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Comma) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected comma in cubic-bezier function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected comma but reached end of input".to_string(), + }); + }, + } + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + } + + let number = match tokens.consume_next_token_infallible() { + Some(SimpleToken::Number(value)) => value, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected number in cubic-bezier function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected number but reached end of input".to_string(), + }); + }, + }; + + numbers.push(number); + } + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(CubicBezierEasingFunction::new([ + numbers[0], numbers[1], numbers[2], numbers[3], + ])) + } + + pub fn parse() -> TokenParser { + TokenParser::new(Self::parse_tokens, "cubic_bezier_parser") + } +} + +impl CubicBezierKeyword { + pub fn new(keyword: CubicBezierKeywordType) -> Self { + Self { keyword } + } + + /// Create from string representation + pub fn from_string(value: &str) -> Result { + let keyword = match value { + "ease" => CubicBezierKeywordType::Ease, + "ease-in" => CubicBezierKeywordType::EaseIn, + "ease-out" => CubicBezierKeywordType::EaseOut, + "ease-in-out" => CubicBezierKeywordType::EaseInOut, + _ => { + return Err(CssParseError::ParseError { + message: format!("Unknown cubic-bezier keyword: {}", value), + }); + }, + }; + Ok(Self::new(keyword)) + } + + pub(crate) fn is_easing_keyword(token: &SimpleToken) -> bool { + if let SimpleToken::Ident(value) = token { + matches!( + value.as_str(), + "ease" | "ease-in" | "ease-out" | "ease-in-out" + ) + } else { + false + } + } + + pub(crate) fn extract_keyword_token(token: SimpleToken) -> CubicBezierKeyword { + if let SimpleToken::Ident(value) = token { + let keyword = match value.as_str() { + "ease" => CubicBezierKeywordType::Ease, + "ease-in" => CubicBezierKeywordType::EaseIn, + "ease-out" => CubicBezierKeywordType::EaseOut, + "ease-in-out" => CubicBezierKeywordType::EaseInOut, + _ => stylex_unreachable!(), + }; + CubicBezierKeyword::new(keyword) + } else { + stylex_unreachable!() + } + } + + pub fn parse() -> TokenParser { + TokenParser::::ident() + .where_fn(Self::is_easing_keyword, Some("easing_keyword")) + .map(Self::extract_keyword_token, Some("to_keyword")) + } +} + +impl StepsEasingFunction { + pub fn new(steps: u32, start: StepsStartType) -> Self { + Self { steps, start } + } + + pub(crate) fn parse_tokens(tokens: &mut TokenList) -> Result { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "steps" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected steps() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected steps() function but reached end of input".to_string(), + }); + }, + } + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + // Parse step count (integer number) + let steps = match tokens.consume_next_token_infallible() { + Some(SimpleToken::Number(value)) => { + let int_value = value as u32; + if int_value as f64 == value && value >= 0.0 { + int_value + } else { + return Err(CssParseError::ParseError { + message: "Steps count must be a positive integer".to_string(), + }); + } + }, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected number for steps count, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected steps count but reached end of input".to_string(), + }); + }, + }; + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Comma) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected comma in steps function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected comma but reached end of input".to_string(), + }); + }, + } + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + // Parse start/end type + let start_type = match tokens.consume_next_token_infallible() { + Some(SimpleToken::Ident(value)) => match value.as_str() { + "start" => StepsStartType::Start, + "end" => StepsStartType::End, + _ => { + return Err(CssParseError::ParseError { + message: format!( + "Expected 'start' or 'end' in steps function, got '{}'", + value + ), + }); + }, + }, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!( + "Expected 'start' or 'end' in steps function, got {:?}", + token + ), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected 'start' or 'end' but reached end of input".to_string(), + }); + }, + }; + + while matches!(tokens.peek(), Ok(Some(SimpleToken::Whitespace))) { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(StepsEasingFunction::new(steps, start_type)) + } + + pub fn parse() -> TokenParser { + TokenParser::new(Self::parse_tokens, "steps_parser") + } +} + +impl StepsKeyword { + pub fn new(keyword: StepsKeywordType) -> Self { + Self { keyword } + } + + /// Create from string representation + pub fn from_string(value: &str) -> Result { + let keyword = match value { + "step-start" => StepsKeywordType::StepStart, + "step-end" => StepsKeywordType::StepEnd, + _ => { + return Err(CssParseError::ParseError { + message: format!("Unknown steps keyword: {}", value), + }); + }, + }; + Ok(Self::new(keyword)) + } + + pub(crate) fn is_steps_keyword(token: &SimpleToken) -> bool { + if let SimpleToken::Ident(value) = token { + matches!(value.as_str(), "step-start" | "step-end") + } else { + false + } + } + + pub(crate) fn extract_steps_keyword_token(token: SimpleToken) -> StepsKeyword { + if let SimpleToken::Ident(value) = token { + let keyword = match value.as_str() { + "step-start" => StepsKeywordType::StepStart, + "step-end" => StepsKeywordType::StepEnd, + _ => stylex_unreachable!(), + }; + StepsKeyword::new(keyword) + } else { + stylex_unreachable!() + } + } + + pub fn parse() -> TokenParser { + TokenParser::::ident() + .where_fn(Self::is_steps_keyword, Some("steps_keyword")) + .map(Self::extract_steps_keyword_token, Some("to_steps_keyword")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for EasingFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EasingFunction::Linear(linear) => linear.fmt(f), + EasingFunction::CubicBezier(cubic_bezier) => cubic_bezier.fmt(f), + EasingFunction::CubicBezierKeyword(keyword) => keyword.fmt(f), + EasingFunction::Steps(steps) => steps.fmt(f), + EasingFunction::StepsKeyword(keyword) => keyword.fmt(f), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for LinearEasingFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("linear(")?; + write_js_number_list(f, self.points.iter().copied())?; + f.write_char(')') + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CubicBezierEasingFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "cubic-bezier({}, {}, {}, {})", + to_js_string(self.points[0]), + to_js_string(self.points[1]), + to_js_string(self.points[2]), + to_js_string(self.points[3]) + ) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for CubicBezierKeyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let keyword_str = match self.keyword { + CubicBezierKeywordType::Ease => "ease", + CubicBezierKeywordType::EaseIn => "ease-in", + CubicBezierKeywordType::EaseOut => "ease-out", + CubicBezierKeywordType::EaseInOut => "ease-in-out", + }; + write!(f, "{}", keyword_str) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for StepsEasingFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let start_str = match self.start { + StepsStartType::Start => "start", + StepsStartType::End => "end", + }; + write!(f, "steps({}, {})", self.steps, start_str) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for StepsKeyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let keyword_str = match self.keyword { + StepsKeywordType::StepStart => "step-start", + StepsKeywordType::StepEnd => "step-end", + }; + write!(f, "{}", keyword_str) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/easing_function_test.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/easing_function_coverage_test.rs"] +mod easing_function_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/filter_function.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/filter_function.rs new file mode 100644 index 000000000..29e09cd2d --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/filter_function.rs @@ -0,0 +1,665 @@ +/*! +CSS filter function parser. +*/ + +use stylex_utils::number::to_js_string; + +use crate::{ + CssParseError, + css_types::{Angle, Length, common_types::number_or_percentage_parser}, + token_parser::TokenParser, + token_types::SimpleToken, +}; +use std::fmt::{self, Display}; + +#[derive(Debug, Clone, PartialEq)] +pub enum FilterFunction { + Blur(BlurFilterFunction), + Brightness(BrightnessFilterFunction), + Contrast(ContrastFilterFunction), + Grayscale(GrayscaleFilterFunction), + HueRotate(HueRotateFilterFunction), + Invert(InvertFilterFunction), + Opacity(OpacityFilterFunction), + Saturate(SaturateFilterFunction), + Sepia(SepiaFilterFunction), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct BlurFilterFunction { + pub radius: Length, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct BrightnessFilterFunction { + pub percentage: f64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ContrastFilterFunction { + pub amount: f64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GrayscaleFilterFunction { + pub amount: f64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct HueRotateFilterFunction { + pub angle: Angle, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct InvertFilterFunction { + pub amount: f64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OpacityFilterFunction { + pub amount: f64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SaturateFilterFunction { + pub amount: f64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SepiaFilterFunction { + pub amount: f64, +} + +impl FilterFunction { + pub fn parser() -> TokenParser { + TokenParser::one_of(vec![ + BlurFilterFunction::parse().map(FilterFunction::Blur, Some("blur")), + BrightnessFilterFunction::parse().map(FilterFunction::Brightness, Some("brightness")), + ContrastFilterFunction::parse().map(FilterFunction::Contrast, Some("contrast")), + GrayscaleFilterFunction::parse().map(FilterFunction::Grayscale, Some("grayscale")), + HueRotateFilterFunction::parse().map(FilterFunction::HueRotate, Some("hue_rotate")), + InvertFilterFunction::parse().map(FilterFunction::Invert, Some("invert")), + OpacityFilterFunction::parse().map(FilterFunction::Opacity, Some("opacity")), + SaturateFilterFunction::parse().map(FilterFunction::Saturate, Some("saturate")), + SepiaFilterFunction::parse().map(FilterFunction::Sepia, Some("sepia")), + ]) + } +} + +impl BlurFilterFunction { + pub fn new(radius: Length) -> Self { + Self { radius } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "blur" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected blur() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected blur() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let radius = (Length::parser().run)(tokens)?; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(BlurFilterFunction::new(radius)) + }, + "blur_parser", + ) + } +} + +impl BrightnessFilterFunction { + pub fn new(percentage: f64) -> Self { + Self { percentage } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "brightness" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected brightness() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected brightness() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + if value < 0.0 { + return Err(CssParseError::ParseError { + message: "brightness() amount must be non-negative".to_string(), + }); + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(BrightnessFilterFunction::new(value)) + }, + "brightness_parser", + ) + } +} + +impl ContrastFilterFunction { + pub fn new(amount: f64) -> Self { + Self { amount } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "contrast" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected contrast() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected contrast() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(ContrastFilterFunction::new(value)) + }, + "contrast_parser", + ) + } +} + +impl GrayscaleFilterFunction { + pub fn new(amount: f64) -> Self { + Self { amount } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "grayscale" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected grayscale() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected grayscale() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(GrayscaleFilterFunction::new(value)) + }, + "grayscale_parser", + ) + } +} + +impl HueRotateFilterFunction { + pub fn new(angle: Angle) -> Self { + Self { angle } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "hue-rotate" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected hue-rotate() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected hue-rotate() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let angle = (Angle::parser().run)(tokens)?; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(HueRotateFilterFunction::new(angle)) + }, + "hue_rotate_parser", + ) + } +} + +impl InvertFilterFunction { + pub fn new(amount: f64) -> Self { + Self { amount } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "invert" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected invert() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected invert() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(InvertFilterFunction::new(value)) + }, + "invert_parser", + ) + } +} + +impl OpacityFilterFunction { + pub fn new(amount: f64) -> Self { + Self { amount } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "opacity" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected opacity() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected opacity() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(OpacityFilterFunction::new(value)) + }, + "opacity_parser", + ) + } +} + +impl SaturateFilterFunction { + pub fn new(amount: f64) -> Self { + Self { amount } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "saturate" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected saturate() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected saturate() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(SaturateFilterFunction::new(value)) + }, + "saturate_parser", + ) + } +} + +impl SepiaFilterFunction { + pub fn new(amount: f64) -> Self { + Self { amount } + } + + pub fn parse() -> TokenParser { + TokenParser::new( + |tokens| { + match tokens.consume_next_token_infallible() { + Some(SimpleToken::Function(fn_name)) if fn_name == "sepia" => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected sepia() function, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected sepia() function but reached end of input".to_string(), + }); + }, + } + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + let number_or_percentage = (number_or_percentage_parser().run)(tokens)?; + let value = number_or_percentage.as_fraction(); + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + } + + match tokens.consume_next_token_infallible() { + Some(SimpleToken::RightParen) => {}, + Some(token) => { + return Err(CssParseError::ParseError { + message: format!("Expected closing paren, got {:?}", token), + }); + }, + None => { + return Err(CssParseError::ParseError { + message: "Expected closing paren but reached end of input".to_string(), + }); + }, + } + + Ok(SepiaFilterFunction::new(value)) + }, + "sepia_parser", + ) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for FilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FilterFunction::Blur(blur) => blur.fmt(f), + FilterFunction::Brightness(brightness) => brightness.fmt(f), + FilterFunction::Contrast(contrast) => contrast.fmt(f), + FilterFunction::Grayscale(grayscale) => grayscale.fmt(f), + FilterFunction::HueRotate(hue_rotate) => hue_rotate.fmt(f), + FilterFunction::Invert(invert) => invert.fmt(f), + FilterFunction::Opacity(opacity) => opacity.fmt(f), + FilterFunction::Saturate(saturate) => saturate.fmt(f), + FilterFunction::Sepia(sepia) => sepia.fmt(f), + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for BlurFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "blur({})", self.radius) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for BrightnessFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "brightness({})", to_js_string(self.percentage)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for ContrastFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "contrast({})", to_js_string(self.amount)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for GrayscaleFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "grayscale({})", to_js_string(self.amount)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for HueRotateFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "hue-rotate({})", self.angle) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for InvertFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invert({})", to_js_string(self.amount)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for OpacityFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "opacity({})", to_js_string(self.amount)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for SaturateFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "saturate({})", to_js_string(self.amount)) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for SepiaFilterFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "sepia({})", to_js_string(self.amount)) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/filter_function_test.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/filter_function_coverage_test.rs"] +mod filter_function_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/flex.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/flex.rs new file mode 100644 index 000000000..413084f4d --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/flex.rs @@ -0,0 +1,73 @@ +/*! +CSS ``: grid fraction values such as `1fr`. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::to_js_string; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// CSS flex fraction unit (e.g., 1fr) +#[derive(Debug, Clone, PartialEq)] +pub struct Flex { + pub fraction: f64, +} + +impl Flex { + /// Create a new Flex value + pub fn new(fraction: f64) -> Self { + Self { fraction } + } + + /// Whether a fraction value is valid for flex (non-negative). + pub fn is_valid_fraction(fraction: f64) -> bool { + fraction >= 0.0 + } + + /// Whether `token` is a `Dimension` with unit `"fr"` and a non-negative value. + pub(crate) fn is_valid_fr_dimension(token: &SimpleToken) -> bool { + if let SimpleToken::Dimension { value, unit } = token { + unit == "fr" && *value >= 0.0 + } else { + false + } + } + + /// Extract a `Flex` from a `SimpleToken::Dimension`; other variants are unreachable. + pub(crate) fn extract_dimension_token(token: SimpleToken) -> Flex { + if let SimpleToken::Dimension { value, unit: _ } = token { + Flex::new(value) + } else { + stylex_unreachable!() + } + } + + /// Parser for flex fraction values + pub fn parser() -> TokenParser { + TokenParser::::token( + SimpleToken::Dimension { + value: 0.0, + unit: String::new(), + }, + Some("Dimension"), + ) + .where_fn(Self::is_valid_fr_dimension, Some("valid_fr_unit")) + .map(Self::extract_dimension_token, Some("to_flex")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Flex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}fr", to_js_string(self.fraction)) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/flex_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/flex_coverage_test.rs"] +mod flex_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/frequency.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/frequency.rs new file mode 100644 index 000000000..8023fca62 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/frequency.rs @@ -0,0 +1,93 @@ +/*! +CSS ``: `Hz` and `KHz` units. +*/ + +use stylex_utils::number::to_js_string; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// Valid frequency units +pub const FREQUENCY_UNITS: &[&str] = &["Hz", "KHz"]; + +/// CSS Frequency value with unit +#[derive(Debug, Clone, PartialEq)] +pub struct Frequency { + pub value: f64, + pub unit: String, // "Hz" or "KHz" +} + +impl Frequency { + /// Create a new Frequency value + pub fn new(value: f64, unit: impl Into) -> Self { + Self { + value, + unit: unit.into(), + } + } + + /// All valid frequency units + pub fn units() -> &'static [&'static str] { + FREQUENCY_UNITS + } + + /// Check if a unit is a valid frequency unit + pub fn is_valid_unit(unit: &str) -> bool { + FREQUENCY_UNITS.contains(&unit) + } + + /// The `(value, unit)` of a `Dimension` with a valid frequency unit; `None` otherwise. + pub(crate) fn extract_frequency_token(token: SimpleToken) -> Option<(f64, String)> { + if let SimpleToken::Dimension { value, unit } = token { + if Self::is_valid_unit(&unit) { + Some((value, unit)) + } else { + None + } + } else { + None + } + } + + /// Parser for CSS frequency values + pub fn parser() -> TokenParser { + TokenParser::::token( + SimpleToken::Dimension { + value: 0.0, + unit: String::new(), + }, + Some("Dimension"), + ) + .map( + Self::extract_frequency_token, + Some("extract_frequency_dimension"), + ) + .where_fn(|opt| opt.is_some(), Some("valid_frequency")) + .map( + |opt| { + let (value, unit) = opt.unwrap(); + Frequency::new(value, unit) + }, + Some("to_frequency"), + ) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Frequency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.unit == "Hz" { + write!(f, "{}KHz", to_js_string(self.value / 1000.0)) + } else { + write!(f, "{}{}", to_js_string(self.value), self.unit) + } + } +} + +#[cfg(test)] +#[path = "../tests/css_types/frequency_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/frequency_coverage_test.rs"] +mod frequency_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/length.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/length.rs new file mode 100644 index 000000000..a077c5c95 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/length.rs @@ -0,0 +1,119 @@ +/*! +CSS ``: absolute, font-, viewport- and container-relative units. +*/ + +use stylex_utils::number::to_js_string; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// Font-relative length units +pub const UNITS_BASED_ON_FONT: &[&str] = &["ch", "em", "ex", "ic", "lh", "rem", "rlh"]; + +pub const UNITS_BASED_ON_VIEWPORT: &[&str] = &[ + "vh", "svh", "lvh", "dvh", "vw", "svw", "lvw", "dvw", "vmin", "svmin", "lvmin", "dvmin", "vmax", + "svmax", "lvmax", "dvmax", +]; + +/// Container-relative length units +pub const UNITS_BASED_ON_CONTAINER: &[&str] = &["cqw", "cqi", "cqh", "cqb", "cqmin", "cqmax"]; + +/// Absolute length units +pub const UNITS_BASED_ON_ABSOLUTE_UNITS: &[&str] = &["px", "cm", "mm", "in", "pt"]; + +/// CSS Length value with unit +#[derive(Debug, Clone, PartialEq)] +pub struct Length { + pub value: f64, + pub unit: String, +} + +impl Length { + /// Create a new Length value + pub fn new(value: f64, unit: impl Into) -> Self { + Self { + value, + unit: unit.into(), + } + } + + /// All valid length units + pub fn units() -> Vec<&'static str> { + let mut units = Vec::new(); + units.extend_from_slice(UNITS_BASED_ON_FONT); + units.extend_from_slice(UNITS_BASED_ON_VIEWPORT); + units.extend_from_slice(UNITS_BASED_ON_CONTAINER); + units.extend_from_slice(UNITS_BASED_ON_ABSOLUTE_UNITS); + units + } + + /// Check if a unit is a valid length unit + pub fn is_valid_unit(unit: &str) -> bool { + Self::units().contains(&unit) + } + + /// The `(value, unit)` of a `Dimension` token; `None` otherwise (unreachable from the parser). + pub(crate) fn extract_length_token(token: SimpleToken) -> Option<(f64, String)> { + if let SimpleToken::Dimension { value, unit } = token { + Some((value, unit)) + } else { + None + } + } + + /// Whether `opt` is `Some` with a valid length unit. + pub(crate) fn is_valid_length_opt(opt: &Option<(f64, String)>) -> bool { + if let Some((_, unit)) = opt { + Self::is_valid_unit(unit) + } else { + false + } + } + + /// Whether `token` is a `Number` with value `0.0`. + pub(crate) fn is_zero_number(token: &SimpleToken) -> bool { + if let SimpleToken::Number(value) = token { + *value == 0.0 + } else { + false + } + } + + /// Parser for CSS length values + pub fn parser() -> TokenParser { + let dimension_parser = TokenParser::::token( + SimpleToken::Dimension { + value: 0.0, + unit: String::new(), + }, + Some("Dimension"), + ) + .map(Self::extract_length_token, Some("extract_dimension")) + .where_fn(Self::is_valid_length_opt, Some("valid_length_unit")) + .map( + |opt| { + let (value, unit) = opt.unwrap(); + Length::new(value, unit) + }, + Some("to_length"), + ); + + // Unitless zero + let zero_parser = TokenParser::::token(SimpleToken::Number(0.0), Some("Number")) + .where_fn(Self::is_zero_number, Some("zero_value")) + .map(|_| Length::new(0.0, String::new()), Some("zero_length")); + + TokenParser::one_of(vec![dimension_parser, zero_parser]) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Length { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", to_js_string(self.value), self.unit) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/length_coverage_test.rs"] +mod length_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/length_percentage.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/length_percentage.rs new file mode 100644 index 000000000..7ace0e4f5 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/length_percentage.rs @@ -0,0 +1,91 @@ +/*! +CSS ``: a length or a percentage. +*/ + +use crate::{ + css_types::{Length, Percentage, calc::Calc}, + token_parser::TokenParser, +}; +use std::fmt::{self, Display}; + +/// Union type for length or percentage values +#[derive(Debug, Clone, PartialEq)] +pub enum LengthPercentage { + Length(Length), + Percentage(Percentage), + Calc(Calc), +} + +impl LengthPercentage { + /// Parser for length or percentage values + pub fn parser() -> TokenParser { + TokenParser::one_of(vec![ + Calc::parser().map(LengthPercentage::Calc, Some("calc")), + Percentage::parser().map(LengthPercentage::Percentage, Some("percentage")), + Length::parser().map(LengthPercentage::Length, Some("length")), + ]) + } + + /// Check if this is a length value + pub fn is_length(&self) -> bool { + matches!(self, LengthPercentage::Length(_)) + } + + /// Check if this is a percentage value + pub fn is_percentage(&self) -> bool { + matches!(self, LengthPercentage::Percentage(_)) + } + + /// Check if this is a calc value + pub fn is_calc(&self) -> bool { + matches!(self, LengthPercentage::Calc(_)) + } + + /// Get the length value if this is a length, None otherwise + pub fn as_length(&self) -> Option<&Length> { + match self { + LengthPercentage::Length(length) => Some(length), + _ => None, + } + } + + /// Get the percentage value if this is a percentage, None otherwise + pub fn as_percentage(&self) -> Option<&Percentage> { + match self { + LengthPercentage::Percentage(percentage) => Some(percentage), + _ => None, + } + } + + /// Get the calc value if this is a calc, None otherwise + pub fn as_calc(&self) -> Option<&Calc> { + match self { + LengthPercentage::Calc(calc) => Some(calc), + _ => None, + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for LengthPercentage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LengthPercentage::Length(length) => length.fmt(f), + LengthPercentage::Percentage(percentage) => percentage.fmt(f), + LengthPercentage::Calc(calc) => calc.fmt(f), + } + } +} + +/// Convenience function for creating the parser +pub fn length_percentage_parser() -> TokenParser { + LengthPercentage::parser() +} + +#[cfg(test)] +#[path = "../tests/css_types/length_percentage_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/length_percentage_coverage_test.rs"] +mod length_percentage_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/mod.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/mod.rs new file mode 100644 index 000000000..79e0d13cd --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/mod.rs @@ -0,0 +1,83 @@ +/*! +CSS type parsers, one module per value type. +*/ + +pub mod common_types; +pub mod dimension; +pub mod number; +pub mod percentage; + +pub mod alpha_value; +pub mod color; + +pub mod angle; +pub mod angle_percentage; +pub mod length; +pub mod length_percentage; +pub mod position; + +pub mod frequency; +pub mod resolution; +pub mod time; + +pub mod calc; +pub mod calc_constant; + +pub mod custom_ident; +pub mod dashed_ident; + +pub mod easing_function; +pub mod filter_function; +pub mod transform_function; + +pub mod basic_shape; +pub mod blend_mode; +pub mod flex; + +pub use alpha_value::AlphaValue; +pub use angle::Angle; +pub use angle_percentage::{AnglePercentage, angle_percentage_parser}; +pub use basic_shape::{BasicShape, CircleRadius}; +pub use blend_mode::BlendMode; +pub use calc::{ + Addition, Calc, CalcDimension, CalcValue, Division, Group, Multiplication, Subtraction, + calc_value_to_string, +}; +pub use calc_constant::CalcConstant; +pub use color::{Color, HashColor, Hsl, Hsla, Lch, NamedColor, Oklab, Oklch, Rgb, Rgba}; +pub use common_types::{CssVariable, CssWideKeyword, Number, NumberOrPercentage, Percentage}; +pub use custom_ident::CustomIdentifier; +pub use dashed_ident::DashedIdentifier; +pub use dimension::Dimension; +pub use easing_function::{ + CubicBezierEasingFunction, CubicBezierKeyword, EasingFunction, LinearEasingFunction, + StepsEasingFunction, StepsKeyword, +}; +pub use filter_function::{ + BlurFilterFunction, BrightnessFilterFunction, ContrastFilterFunction, FilterFunction, + GrayscaleFilterFunction, HueRotateFilterFunction, InvertFilterFunction, OpacityFilterFunction, + SaturateFilterFunction, SepiaFilterFunction, +}; +pub use flex::Flex; +pub use frequency::Frequency; +pub use length::Length; +pub use length_percentage::{LengthPercentage, length_percentage_parser}; +pub use position::{Horizontal, HorizontalKeyword, Position, Vertical, VerticalKeyword}; +pub use resolution::Resolution; +pub use time::Time; +pub use transform_function::{ + Axis, Matrix, Matrix3d, Perspective, Rotate, Rotate3d, RotateXYZ, Scale, Scale3d, ScaleAxis, + Skew, SkewAxis, TransformFunction, Translate, Translate3d, TranslateAxis, +}; + +#[cfg(test)] +#[path = "../tests/css_types/js_number_spelling_test.rs"] +mod js_number_spelling_test; + +#[cfg(test)] +#[path = "../tests/css_types/double_precision_test.rs"] +mod double_precision_test; + +#[cfg(test)] +#[path = "../tests/css_types/color_double_precision_test.rs"] +mod color_double_precision_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/number.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/number.rs new file mode 100644 index 000000000..07644eee2 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/number.rs @@ -0,0 +1,7 @@ +/*! +CSS ``: unitless numbers. +*/ + +pub use crate::css_types::common_types::Number; + +pub use Number as CssNumber; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/percentage.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/percentage.rs new file mode 100644 index 000000000..76ac167a4 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/percentage.rs @@ -0,0 +1,7 @@ +/*! +CSS `` values such as `50%`. +*/ + +pub use crate::css_types::common_types::Percentage; + +pub use Percentage as CssPercentage; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/position.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/position.rs new file mode 100644 index 000000000..7cead0383 --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/position.rs @@ -0,0 +1,421 @@ +use stylex_macros::stylex_unreachable; + +/** + * CSS `` parser. + */ +use crate::{ + CssParseError, + css_types::length_percentage::{LengthPercentage, length_percentage_parser}, + token_parser::TokenParser, + token_types::{SimpleToken, TokenList}, +}; +use std::fmt; + +#[derive(Debug, Clone, PartialEq)] +pub enum HorizontalKeyword { + Left, + Center, + Right, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for HorizontalKeyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + HorizontalKeyword::Left => "left", + HorizontalKeyword::Center => "center", + HorizontalKeyword::Right => "right", + }; + write!(f, "{}", s) + } +} + +impl HorizontalKeyword { + pub fn as_str(&self) -> &str { + match self { + HorizontalKeyword::Left => "left", + HorizontalKeyword::Center => "center", + HorizontalKeyword::Right => "right", + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum VerticalKeyword { + Top, + Center, + Bottom, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for VerticalKeyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + VerticalKeyword::Top => "top", + VerticalKeyword::Center => "center", + VerticalKeyword::Bottom => "bottom", + }; + write!(f, "{}", s) + } +} + +impl VerticalKeyword { + pub fn as_str(&self) -> &str { + match self { + VerticalKeyword::Top => "top", + VerticalKeyword::Center => "center", + VerticalKeyword::Bottom => "bottom", + } + } +} + +/// | LengthPercentage | HorizontalKeyword | [HorizontalKeyword, LengthPercentage] +#[derive(Debug, Clone, PartialEq)] +pub enum Horizontal { + Length(LengthPercentage), + Keyword(HorizontalKeyword), + KeywordWithOffset(HorizontalKeyword, LengthPercentage), // [HorizontalKeyword, LengthPercentage] +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for Horizontal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Horizontal::Length(lp) => write!(f, "{}", lp), + Horizontal::Keyword(k) => write!(f, "{}", k), + Horizontal::KeywordWithOffset(k, lp) => write!(f, "{} {}", k, lp), + } + } +} + +/// | LengthPercentage | VerticalKeyword | [VerticalKeyword, LengthPercentage] +#[derive(Debug, Clone, PartialEq)] +pub enum Vertical { + Length(LengthPercentage), + Keyword(VerticalKeyword), + KeywordWithOffset(VerticalKeyword, LengthPercentage), // [VerticalKeyword, LengthPercentage] +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for Vertical { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Vertical::Length(lp) => write!(f, "{}", lp), + Vertical::Keyword(k) => write!(f, "{}", k), + Vertical::KeywordWithOffset(k, lp) => write!(f, "{} {}", k, lp), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Position { + pub horizontal: Option, + pub vertical: Option, +} + +impl Position { + /// Create a new Position + pub fn new(horizontal: Option, vertical: Option) -> Self { + Self { + horizontal, + vertical, + } + } + + fn is_horizontal_ident(token: &SimpleToken) -> bool { + if let SimpleToken::Ident(value) = token { + matches!(value.as_str(), "left" | "center" | "right") + } else { + false + } + } + + fn token_to_horizontal_keyword(token: SimpleToken) -> HorizontalKeyword { + if let SimpleToken::Ident(value) = token { + match value.as_str() { + "left" => HorizontalKeyword::Left, + "center" => HorizontalKeyword::Center, + "right" => HorizontalKeyword::Right, + _ => stylex_unreachable!(), + } + } else { + stylex_unreachable!() + } + } + + fn horizontal_keyword_parser() -> TokenParser { + TokenParser::::token(SimpleToken::Ident(String::new()), Some("Ident")) + .where_fn(Self::is_horizontal_ident, Some("horizontal_keyword")) + .map( + Self::token_to_horizontal_keyword, + Some("to_horizontal_keyword"), + ) + } + + fn is_vertical_ident(token: &SimpleToken) -> bool { + if let SimpleToken::Ident(value) = token { + matches!(value.as_str(), "top" | "center" | "bottom") + } else { + false + } + } + + fn token_to_vertical_keyword(token: SimpleToken) -> VerticalKeyword { + if let SimpleToken::Ident(value) = token { + match value.as_str() { + "top" => VerticalKeyword::Top, + "center" => VerticalKeyword::Center, + "bottom" => VerticalKeyword::Bottom, + _ => stylex_unreachable!(), + } + } else { + stylex_unreachable!() + } + } + + fn vertical_keyword_parser() -> TokenParser { + TokenParser::::token(SimpleToken::Ident(String::new()), Some("Ident")) + .where_fn(Self::is_vertical_ident, Some("vertical_keyword")) + .map(Self::token_to_vertical_keyword, Some("to_vertical_keyword")) + } + + fn skip_required_whitespace(tokens: &mut TokenList) -> bool { + let start = tokens.current_index; + let mut consumed = false; + + while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() { + let _ = tokens.consume_next_token(); + consumed = true; + } + + if !consumed { + tokens.set_current_index(start); + } + + consumed + } + + fn parse_length_percentage(tokens: &mut TokenList) -> Result { + (length_percentage_parser().run)(tokens) + } + + fn parse_horizontal(tokens: &mut TokenList) -> Result { + let keyword = (Self::horizontal_keyword_parser().run)(tokens)?; + let after_keyword = tokens.current_index; + + if Self::skip_required_whitespace(tokens) { + match Self::parse_length_percentage(tokens) { + Ok(offset) => return Ok(Horizontal::KeywordWithOffset(keyword, offset)), + Err(_) => tokens.set_current_index(after_keyword), + } + } + + Ok(Horizontal::Keyword(keyword)) + } + + fn parse_vertical(tokens: &mut TokenList) -> Result { + let keyword = (Self::vertical_keyword_parser().run)(tokens)?; + let after_keyword = tokens.current_index; + + if Self::skip_required_whitespace(tokens) { + match Self::parse_length_percentage(tokens) { + Ok(offset) => return Ok(Vertical::KeywordWithOffset(keyword, offset)), + Err(_) => tokens.set_current_index(after_keyword), + } + } + + Ok(Vertical::Keyword(keyword)) + } + + fn parse_both_keywords(tokens: &mut TokenList) -> Result { + let start = tokens.current_index; + + if let Ok(horizontal) = Self::parse_horizontal(tokens) { + if Self::skip_required_whitespace(tokens) { + match Self::parse_vertical(tokens) { + Ok(vertical) => return Ok(Position::new(Some(horizontal), Some(vertical))), + Err(_) => tokens.set_current_index(start), + } + } else { + tokens.set_current_index(start); + } + } else { + tokens.set_current_index(start); + } + + if let Ok(vertical) = Self::parse_vertical(tokens) { + if Self::skip_required_whitespace(tokens) { + match Self::parse_horizontal(tokens) { + Ok(horizontal) => return Ok(Position::new(Some(horizontal), Some(vertical))), + Err(_) => tokens.set_current_index(start), + } + } else { + tokens.set_current_index(start); + } + } else { + tokens.set_current_index(start); + } + + Err(CssParseError::ParseError { + message: "Expected horizontal and vertical position keywords".to_string(), + }) + } + + fn parse_length_plus_vertical(tokens: &mut TokenList) -> Result { + let start = tokens.current_index; + let length = Self::parse_length_percentage(tokens)?; + + if !Self::skip_required_whitespace(tokens) { + tokens.set_current_index(start); + return Err(CssParseError::ParseError { + message: "Expected whitespace after position length".to_string(), + }); + } + + match Self::parse_vertical(tokens) { + Ok(vertical) => Ok(Position::new( + Some(Horizontal::Length(length)), + Some(vertical), + )), + Err(error) => { + tokens.set_current_index(start); + Err(error) + }, + } + } + + fn parse_horizontal_keyword_plus_vertical_length( + tokens: &mut TokenList, + ) -> Result { + let start = tokens.current_index; + let horizontal = (Self::horizontal_keyword_parser().run)(tokens)?; + + if !Self::skip_required_whitespace(tokens) { + tokens.set_current_index(start); + return Err(CssParseError::ParseError { + message: "Expected whitespace after horizontal position keyword".to_string(), + }); + } + + match Self::parse_length_percentage(tokens) { + Ok(vertical) => Ok(Position::new( + Some(Horizontal::Keyword(horizontal)), + Some(Vertical::Length(vertical)), + )), + Err(error) => { + tokens.set_current_index(start); + Err(error) + }, + } + } + + fn parse_vertical_keyword_plus_horizontal_length( + tokens: &mut TokenList, + ) -> Result { + let start = tokens.current_index; + let vertical = (Self::vertical_keyword_parser().run)(tokens)?; + + if !Self::skip_required_whitespace(tokens) { + tokens.set_current_index(start); + return Err(CssParseError::ParseError { + message: "Expected whitespace after vertical position keyword".to_string(), + }); + } + + match Self::parse_length_percentage(tokens) { + Ok(horizontal) => Ok(Position::new( + Some(Horizontal::Length(horizontal)), + Some(Vertical::Keyword(vertical)), + )), + Err(error) => { + tokens.set_current_index(start); + Err(error) + }, + } + } + + fn parse_numbers_only(tokens: &mut TokenList) -> Result { + let first = Self::parse_length_percentage(tokens)?; + let after_first = tokens.current_index; + let second = if Self::skip_required_whitespace(tokens) { + match Self::parse_length_percentage(tokens) { + Ok(length) => length, + Err(_) => { + tokens.set_current_index(after_first); + first.clone() + }, + } + } else { + first.clone() + }; + + Ok(Position::new( + Some(Horizontal::Length(first)), + Some(Vertical::Length(second)), + )) + } + + fn parse_position(tokens: &mut TokenList) -> Result { + let start = tokens.current_index; + + for parser in [ + Self::parse_both_keywords, + Self::parse_horizontal_keyword_plus_vertical_length, + Self::parse_vertical_keyword_plus_horizontal_length, + Self::parse_length_plus_vertical, + ] { + match parser(tokens) { + Ok(position) => return Ok(position), + Err(_) => tokens.set_current_index(start), + } + } + + match Self::parse_horizontal(tokens) { + Ok(horizontal) => return Ok(Position::new(Some(horizontal), None)), + Err(_) => tokens.set_current_index(start), + } + + match Self::parse_vertical(tokens) { + Ok(vertical) => return Ok(Position::new(None, Some(vertical))), + Err(_) => tokens.set_current_index(start), + } + + Self::parse_numbers_only(tokens) + } + + /// Position parser: one or two keywords/lengths, or a keyword with offset (`left 10px`). + pub fn parser() -> TokenParser { + TokenParser::new(Self::parse_position, "position") + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let parts: Vec = [ + self.horizontal.as_ref().map(|h| h.to_string()), + self.vertical.as_ref().map(|v| v.to_string()), + ] + .into_iter() + .flatten() + .collect(); + + write!(f, "{}", parts.join(" ")) + } +} + +pub fn position_parser() -> TokenParser { + Position::parser() +} + +#[cfg(test)] +#[path = "../tests/css_types/position_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/position_test.rs"] +mod position_test; + +#[cfg(test)] +#[path = "../tests/css_types/position_coverage_test.rs"] +mod position_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/resolution.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/resolution.rs new file mode 100644 index 000000000..8e877734b --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/resolution.rs @@ -0,0 +1,88 @@ +/*! +CSS ``: `dpi`, `dpcm` and `dppx` units. +*/ + +use stylex_macros::stylex_unreachable; +use stylex_utils::number::to_js_string; + +use crate::{token_parser::TokenParser, token_types::SimpleToken}; +use std::fmt::{self, Display}; + +/// Valid resolution units +pub const RESOLUTION_UNITS: &[&str] = &["dpi", "dpcm", "dppx"]; + +/// CSS Resolution value with unit +#[derive(Debug, Clone, PartialEq)] +pub struct Resolution { + pub value: f64, + pub unit: String, // "dpi", "dpcm", or "dppx" +} + +impl Resolution { + /// Create a new Resolution value + pub fn new(value: f64, unit: impl Into) -> Self { + Self { + value, + unit: unit.into(), + } + } + + /// All valid resolution units + pub fn units() -> &'static [&'static str] { + RESOLUTION_UNITS + } + + /// Check if a unit is a valid resolution unit + pub fn is_valid_unit(unit: &str) -> bool { + RESOLUTION_UNITS.contains(&unit) + } + + /// Whether `token` is a `Dimension` with a valid resolution unit. + pub(crate) fn is_valid_resolution_dimension(token: &SimpleToken) -> bool { + if let SimpleToken::Dimension { unit, .. } = token { + Self::is_valid_unit(unit) + } else { + false + } + } + + /// Extract a `Resolution` from a `SimpleToken::Dimension`; other variants are unreachable. + pub(crate) fn extract_dimension_token(token: SimpleToken) -> Resolution { + if let SimpleToken::Dimension { value, unit } = token { + Resolution::new(value, unit) + } else { + stylex_unreachable!() + } + } + + /// Parser for CSS resolution values + pub fn parser() -> TokenParser { + TokenParser::::token( + SimpleToken::Dimension { + value: 0.0, + unit: String::new(), + }, + Some("Dimension"), + ) + .where_fn( + Self::is_valid_resolution_dimension, + Some("valid_resolution_unit"), + ) + .map(Self::extract_dimension_token, Some("to_resolution")) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl Display for Resolution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", to_js_string(self.value), self.unit) + } +} + +#[cfg(test)] +#[path = "../tests/css_types/resolution_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "../tests/css_types/resolution_coverage_test.rs"] +mod resolution_coverage_test; diff --git a/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/time.rs b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/time.rs new file mode 100644 index 000000000..5e63dedfe --- /dev/null +++ b/packages/@stylexjs/rust-plugin/crates/stylex-css-parser/src/css_types/time.rs @@ -0,0 +1,90 @@ +/*! +CSS `