diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..5222525 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,34 @@ +# Copilot instructions (grimoire-css) + +## Project shape (architecture guide) + +Note: This document captures the current architecture conventions for this repo. +- Rust workspace is a single crate with **bin + lib**: `src/main.rs` calls `grimoire_css_lib::start_as_cli` (keep `main.rs` thin). +- Command routing lives in `src/commands/handler.rs` (`init` / `build` / `shorten`). Add new CLI modes by wiring them here. +- Core pipeline logic lives in `src/core/` (config → parse → build → optimize → output). Keep it side-effect-light where practical. +- External integrations live in `src/infrastructure/` (e.g. LightningCSS optimizer + miette diagnostics). Don’t mix vendor glue into `src/core/`. + +## Runtime modes & config conventions +- FS mode uses a single repo config file at `grimoire/config/grimoire.config.json` (created by `init`; used by `build`). See `src/core/filesystem.rs` and `src/commands/init.rs`. +- Optimizer uses `.browserslistrc` from repo root; if missing, it is created with `defaults` (see `src/infrastructure/lightning_css_optimizer.rs`). +- Parallel project builds are opt-in via `GRIMOIRE_CSS_JOBS` (project-level isolation) — see `src/core/css_builder/css_builder_fs.rs`. +- Locking: setting `lock: true` in config enables tracking + cleanup of stale generated files via `grimoire/grimoire.lock.json` (see `src/core/file_tracker.rs`). +- Config supports external scroll/variable files: `grimoire.*.scrolls.json` and `grimoire.*.variables.json` are loaded and merged during `ConfigFs::load` (see `src/core/config/config_fs.rs`). + +## Error/reporting pattern +- Prefer returning `GrimoireCssError` from core logic; attach source context when you have file content/spans using `GrimoireCssError::with_source(...)` and `SourceFile` (pattern in `src/core/parser/parser_fs.rs`). +- CLI pretty-printing goes through `GrimoireCssDiagnostic` + `miette` (see `src/infrastructure/diagnostics.rs` and `src/lib.rs`). + +## Developer workflows (match CI) +- Format: `cargo fmt -- --check` +- Lint: `cargo clippy -- -D warnings` +- Tests: `cargo test` +- Coverage (CI uses this): `./scripts/coverage.sh` (requires `grcov` + `llvm-tools-preview`). + +## Local running tips +- CLI (debug): `cargo run -- build` / `cargo run -- init` / `cargo run -- shorten` +- Release binary: `cargo build --release` → `target/release/grimoire_css` +- Benchmark harness expects the release binary at `../target/release/grimoire_css` (see `benchmark/README.md`). + +## Versioning convention +- PR branches `rc/x.y.z` must match `version = "x.y.z"` in `Cargo.toml` (enforced by `.github/workflows/version_check.yml`). diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index cc30112..6f2e436 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -53,6 +53,13 @@ jobs: with: profile: minimal toolchain: stable + components: clippy,rustfmt + + - name: Print Rust toolchain versions + run: | + rustup show active-toolchain + rustc --version + cargo --version - name: Install llvm-tools-preview run: rustup component add llvm-tools-preview @@ -134,6 +141,13 @@ jobs: profile: minimal toolchain: stable target: ${{ matrix.target }} + components: clippy,rustfmt + + - name: Print Rust toolchain versions + run: | + rustup show active-toolchain + rustc --version + cargo --version - name: Cache Cargo registry uses: actions/cache@v4 @@ -187,6 +201,13 @@ jobs: with: profile: minimal toolchain: stable + components: clippy,rustfmt + + - name: Print Rust toolchain versions + run: | + rustup show active-toolchain + rustc --version + cargo --version - name: Cache Cargo registry uses: actions/cache@v4 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 050bb1b..6ee3df5 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -21,6 +21,13 @@ jobs: with: profile: minimal toolchain: stable + components: clippy,rustfmt + + - name: Print Rust toolchain versions + run: | + rustup show active-toolchain + rustc --version + cargo --version - name: Cache cargo registry uses: actions/cache@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0660e57..e8be83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## [v1.7.0] - 2025-12-27 + +> Full release notes: [releases/v1.7.0.md](./releases/v1.7.0.md) + +### Added + +- **Scroll templates in `g!…;`**: Use config-defined `scrolls` inside templated syntax with variable arguments. +- **Rustc-like diagnostics**: File/snippet output with labeled spans and optional help text. +- **Opt-in parallel builds**: Enable multi-core filesystem builds via `GRIMOIRE_CSS_JOBS`. +- **Repro sandbox**: Added `repro/` scenarios for quickly validating features and diagnostics. +- **Contributor instructions**: Added `.github/copilot-instructions.md` describing the repo’s architecture conventions. + +### Improved + +- Deterministic scroll expansion under templated selectors, including correct propagation of prefixes (`md__`, `{...}`, `hover:`). +- Reduced redundant work and lowered clone/allocation pressure in hot paths (output unchanged). + +### Fixed + +- Malformed function-like spell values now produce clearer, earlier errors. +- Color function argument validation now returns a proper error instead of being silently ignored. + +--- + +## [v1.6.0] - 2025-07-21 + +> Full release notes: [releases/v1.6.0.md](./releases/v1.6.0.md) + +### Added + +- Extracted the color module into `grimoire_css_color_toolkit` for independent usage. +- Comprehensive support for curly-bracket class syntax (`class={}`, `className={}`) with nested bracket handling. + +### Improved + +- Migrated unit handling from `u32` to `f64` for better precision in responsive calculations. +- Upgraded to Rust Edition 2024 and set MSRV to Rust 1.88. + +--- + ## [v1.5.0] - 2025-05-19 > Full release notes: [releases/v1.5.0.md](./releases/v1.5.0.md) diff --git a/Cargo.lock b/Cargo.lock index 3a39f11..ca991a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,25 +2,40 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.16", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "serde", "version_check", @@ -29,19 +44,13 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -103,9 +112,33 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] [[package]] name = "base64-simd" @@ -118,9 +151,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.6.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -134,29 +167,38 @@ dependencies = [ "wyz", ] +[[package]] +name = "browserslist-data" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e977366ea69a6e756ae616c0d5def0da9a3521fca5f91f447fdf613c928a15a" +dependencies = [ + "ahash 0.8.12", + "chrono", +] + [[package]] name = "browserslist-rs" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf0ca73de70c3da94e4194e4a01fe732378f55d47cf4c0588caab22a0dbfa14" +checksum = "8dd48a6ca358df4f7000e3fb5f08738b1b91a0e5d5f862e2f77b2b14647547f5" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", + "browserslist-data", "chrono", "either", - "indexmap", "itertools 0.13.0", "nom", - "once_cell", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" @@ -180,43 +222,37 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" -version = "1.7.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.1.21" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets", + "windows-link", ] [[package]] @@ -250,7 +286,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", ] [[package]] @@ -267,15 +303,15 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "console" -version = "0.15.8" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.61.2", ] [[package]] @@ -315,9 +351,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -334,9 +370,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "cssparser" @@ -347,7 +383,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf", "smallvec", ] @@ -367,7 +403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.111", ] [[package]] @@ -385,9 +421,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-url" @@ -398,11 +434,27 @@ dependencies = [ "matches", ] +[[package]] +name = "dhat" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "thousands", +] + [[package]] name = "dtoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" [[package]] name = "dtoa-short" @@ -415,37 +467,43 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "funty" @@ -454,46 +512,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] -name = "fxhash" -version = "0.2.1" +name = "getrandom" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "byteorder", + "cfg-if", + "libc", + "wasi", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasip2", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "grimoire_css" -version = "1.6.0" +version = "1.7.0" dependencies = [ "console", + "dhat", "glob", "grimoire_css_color_toolkit", "indicatif", "lazy_static", "lightningcss", + "miette", "once_cell", "regex", "serde", "serde_json", "tempfile", + "thiserror 2.0.17", ] [[package]] @@ -520,6 +590,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -528,14 +604,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -551,36 +628,34 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] name = "indicatif" -version = "0.17.8" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", - "instant", - "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", ] [[package]] -name = "instant" -version = "0.1.13" +name = "is_ci" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" @@ -608,16 +683,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -629,17 +705,17 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "lightningcss" -version = "1.0.0-alpha.59" +version = "1.0.0-alpha.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e225b3fa0a8bd5562c8833b1a32afa88761c4e661d3177b8cdc4e13cbf078e" +checksum = "b407ca668368d1d5a86cea58ac82d9f9f9ca4bac1e9dce6f16f875f0f081a911" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "bitflags", "browserslist-rs", "const-str", @@ -647,16 +723,18 @@ dependencies = [ "cssparser-color", "dashmap", "data-encoding", - "getrandom", + "getrandom 0.3.4", + "indexmap", "itertools 0.10.5", "lazy_static", "lightningcss-derive", "parcel_selectors", "parcel_sourcemap", - "paste", + "pastey", "pathdiff", "rayon", "serde", + "serde-content", "smallvec", ] @@ -674,25 +752,24 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "matches" @@ -702,9 +779,39 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.6.2" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "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.111", +] [[package]] name = "minimal-lexical" @@ -712,6 +819,21 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mintex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" + [[package]] name = "nom" version = "7.1.3" @@ -732,16 +854,19 @@ dependencies = [ ] [[package]] -name = "number_prefix" -version = "0.4.0" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" @@ -755,19 +880,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "parcel_selectors" -version = "0.27.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4d26c18a8377a64728c04bf3b2e48ec43b0c77e687a18e03eb837d65e08a14" +checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196" dependencies = [ "bitflags", "cssparser", - "fxhash", "log", - "phf 0.10.1", + "phf", "phf_codegen", "precomputed-hash", + "rustc-hash 2.1.1", "smallvec", ] @@ -785,125 +916,98 @@ dependencies = [ "vlq", ] +[[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.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "phf" -version = "0.10.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", - "phf_shared 0.11.2", + "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared", ] [[package]] name = "phf_generator" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.10.0", - "rand", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared 0.11.2", + "phf_shared", "rand", ] [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", ] [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "portable-atomic" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "precomputed-hash" @@ -913,9 +1017,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -942,13 +1046,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 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" @@ -961,18 +1071,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", "rand_core", ] @@ -981,15 +1079,12 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -997,9 +1092,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1007,18 +1102,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.6" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1028,9 +1123,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1039,9 +1134,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rend" @@ -1081,24 +1176,42 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" -version = "0.38.38" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] -name = "ryu" -version = "1.0.15" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" @@ -1114,33 +1227,54 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-content" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1166,15 +1300,15 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "strsim" @@ -1182,6 +1316,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "1.0.109" @@ -1195,9 +1350,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1212,42 +1367,88 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.13.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[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.2", ] [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -1260,9 +1461,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" @@ -1276,6 +1483,18 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1284,9 +1503,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "version_check" @@ -1302,41 +1525,51 @@ checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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.93" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1344,39 +1577,93 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-sys" -version = "0.52.0" +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.111", +] + +[[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.111", +] + +[[package]] +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets", + "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]] @@ -1385,7 +1672,25 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -1394,14 +1699,31 @@ 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", + "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", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1410,48 +1732,102 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "wyz" version = "0.5.1" @@ -1463,21 +1839,26 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.111", ] + +[[package]] +name = "zmij" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" diff --git a/Cargo.toml b/Cargo.toml index a43291e..e060fd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grimoire_css" -version = "1.6.0" +version = "1.7.0" edition = "2024" rust-version = "1.88" authors = ["Dmitrii Shatokhin "] @@ -27,17 +27,27 @@ crate-type = ["lib"] lto = true codegen-units = 1 +[profile.profiling] +inherits = "release" +debug = 2 + [dependencies] -console = "0.15.8" -glob = "0.3.1" +console = "0.16.2" +dhat = { version = "0.3.3", optional = true } +glob = "0.3.3" grimoire_css_color_toolkit = "1.0.0" -indicatif = "0.17.8" +indicatif = "0.18.3" lazy_static = "1.5.0" -lightningcss = { version = "1.0.0-alpha.59", features = ["browserslist"] } -once_cell = "1.20" -regex = "1.11.0" +lightningcss = { version = "1.0.0-alpha.68", features = ["browserslist"] } +miette = { version = "7.6.0", features = ["fancy"] } +once_cell = "1.21" +regex = "1.12.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "2.0.17" + +[features] +heap-profile = ["dhat"] [dev-dependencies] -tempfile = "3.13.0" +tempfile = "3.24.0" diff --git a/README.md b/README.md index b554fc3..39087ef 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,9 @@ --- - - [![Current Crates.io Version](https://img.shields.io/crates/v/grimoire_css.svg)](https://crates.io/crates/grimoire_css) [![Crates.io Downloads](https://img.shields.io/crates/d/grimoire_css.svg)](https://crates.io/crates/grimoire_css) [![Test Status](https://github.com/persevie/grimoire-css/actions/workflows/quality.yml/badge.svg)](https://github.com/persevie/grimoire-css/actions/workflows/quality.yml) @@ -16,13 +14,10 @@ ![license](https://shields.io/badge/license-MIT-blue) --- - - > For the best experience and access to advanced features like playgrounds and interactive previews, please visit the [Grimoire CSS site](https://grimoirecss.com). The documentation is the same in both places. - @@ -316,7 +311,6 @@ For example, the predefined scroll `g-anim` allows you to apply an animation and Full Animations List (use [grimoirecss.com](https://grimoirecss.com) for better experience with preview) - - back-in-down - back-in-left - back-in-right @@ -1047,9 +1041,9 @@ For example, the predefined scroll `g-anim` allows you to apply an animation and - zoom-out-left - zoom-out-right - zoom-out-up - - - + + + ## Create Your Own Animations @@ -1098,6 +1092,8 @@ This means you’re not limited by file types or formats - you define the `input If you want to use spells outside the traditional `class` or `className` attributes, Grimoire CSS provides a clever solution with its **template syntax**: `g!;`. This syntax lets you wrap your spell in a template, enabling the parser to collect spells from any text-based content. +Template syntax works for scrolls too, by the same rules as spells (including prefixes and modifiers). For example: `g!complex-card=120px_red_100px;`. + Let’s say you have both a classic spell and a templated spell that are essentially the same. Don’t worry - Grimoire CSS is smart enough to combine them into one, as long as it doesn’t affect the CSS cascade. The result? Clean, efficient CSS output like this: ```css @@ -1437,6 +1433,21 @@ There are only 3 commands you need to know: - **`build`**: Kicks off the build process, parsing all your input files and generating the compiled CSS. If you haven’t already run `init`, the `build` command will handle that for you automatically. - **`shorten`**: Automatically converts all full-length component names in your spells (as defined in your config) to their corresponding shorthand forms. This helps keep your code concise and consistent. Run this command to refactor your files, making your spell syntax as brief as possible without losing clarity or functionality. +**Optional parallel project builds** + +If your config defines multiple independent projects (multiple output files), Grimoire CSS can build them in parallel. + +- Enable by setting the `GRIMOIRE_CSS_JOBS` environment variable to a positive integer (e.g. `4`). +- Default is `1` (fully sequential; same behavior as before). +- Values are capped to the machine’s available parallelism. +- Higher values can reduce wall-clock build time, but may increase peak memory usage due to multiple optimizations running simultaneously. + +Example: + +```bash +GRIMOIRE_CSS_JOBS=4 grimoire_css build +``` + Grimoire CSS’s CLI is built for developers who want power without bloat. It’s direct, no-nonsense, and integrates smoothly into any project or bundler. Here’s a refined version of the remaining parts, keeping the technical depth and making them more engaging and polished: @@ -1450,6 +1461,8 @@ Migrating to Grimoire CSS is simple thanks to the Grimoire CSS Transmutator. You In both modes, the Transmutator returns JSON that conforms to the external Scrolls convention by default, so you can immediately leverage your existing CSS classes as Grimoire CSS Scrolls. +You can also run the compiled CSS from Tailwind or any other framework through the Transmutator, include the produced JSON as external scrolls alongside your config, and keep using your existing class names powered by Grimoire CSS. + ```json { "classes": [ @@ -1512,9 +1525,7 @@ The core of Grimoire CSS is architected entirely in Rust, ensuring top-notch per The `grimoire-css-js` takes the core crate and wraps it into a Node.js-compatible interface, which is then compiled into an npm package. Whether you’re working with Rust, Node.js, or need a direct CLI, Grimoire CSS is ready to integrate into your workflow and bring powerful CSS management wherever you need it. - > For the best experience and access to online playground and transmutator (aka **Desk**), please visit the [Grimoire CSS site](https://grimoirecss.com). The documentation is the same in both places. - ## Installation @@ -1571,7 +1582,6 @@ grimoire-css-js build ``` - # The Arcane Circle Grimoire CSS gives you the freedom to create styles that work exactly the way you want them to - no rigid rules or constraints. Whether you’re crafting dynamic interactions or fine-tuning layouts, Grimoire adapts to your needs, making each step straightforward and rewarding. @@ -1584,18 +1594,16 @@ The Arcane Circle, or simply the Circle, is a place where you can share your con ## The First Member -Hello! My name is Dmitrii Shatokhin, and I am the creator of Grimoire CSS. I invented the Spell concept and all the other ideas behind the project. Grimoire CSS is the result of countless hours of work and dedication, and I am proud to have made it open source. +Hello! My name is [Dmitrii Shatokhin](https://dmtrshat.github.io/), and I am the creator of Grimoire CSS. I invented the Spell concept and all the other ideas behind the project. Grimoire CSS is the result of countless hours of work and dedication, and I am proud to have made it open source. But this is just the beginning. I am committed to the ongoing development of Grimoire CSS and its entire ecosystem - there are many plans and tasks ahead, which I strive to manage transparently on GitHub. My vision is to grow the Arcane Circle community and bring all these ideas to life. I would be truly grateful for any support you can offer - whether it’s starring the project on GitHub, leaving feedback, recommending it to others, contributing to its development, helping to promote Grimoire CSS, or even sponsoring the project or my work. Thank you! - - # Release Information ## Release Notes @@ -1605,11 +1613,8 @@ For detailed information about each release, including new features, improvement ### Changelog A concise list of version-specific changes can be found in our [Changelog](https://github.com/persevie/grimoire-css/blob/main/CHANGELOG.md). - - Craft Your Code, Cast Your Spells - diff --git a/RELEASES.md b/RELEASES.md index ca97b8a..5cd172a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,85 @@ This document combines all release notes in chronological order, providing a comprehensive view of Grimoire CSS's evolution. + +--- + +# v1.7.0 Scryforge: Deterministic Scrollcraft + +Grimoire CSS sharpens both **power** and **clarity** with **Scryforge** — a release focused on templated scroll composition, rustc-like diagnostics, and measurable performance wins. Spells now expand more deterministically under templated selectors, errors read like a compiler, and large projects build faster with an opt-in multi-core path. + +## Key Highlights + +- **Scroll Templates Inside `g!…;`**: Use config-defined `scrolls` directly in `g!…;` with variable arguments (e.g. `g!complex-card=120px_red_100px;`) while keeping output deterministic. +- **Prefix-Safe Expansion**: Prefix modifiers like `md__`, focus blocks `{...}`, and `hover:` are preserved by applying them to each expanded spell. +- **Rustc-like Diagnostics**: Errors render with file context + labeled span + optional help text, powered by a structured error model and `miette` diagnostics. +- **Faster Builds (Same Output)**: Reduced redundant work and clone/allocation pressure; optional parallel project builds via `GRIMOIRE_CSS_JOBS`. +- **Better Repro & Contributor UX**: Added a `repro/` sandbox for feature/error scenarios and added `.github/copilot-instructions.md` as an architecture guide. + +## Full Details + +### Scroll Templates Inside `g!…;` + +You can now reference scrolls in templated `g!…;` syntax, including argument passing: + +- Example: `g!complex-card=120px_red_100px;` +- Supports variable-like arguments and prefix modifiers. +- Scroll expansion is flattened into real property spells so generated CSS is emitted under the *outer templated selector*. +- Output remains deterministic and the template-flattening path avoids unnecessary cloning. + +### Prefix Semantics Preserved + +Scrolls expanded under templates keep the semantics that made Grimoire CSS predictable in complex UIs: + +- Responsive prefixes like `md__`. +- Focus blocks using `{...}`. +- Effects like `hover:`. + +These prefixes apply to each expanded spell during flattening so behavior matches user intent. + +### Rustc-like Diagnostics (File + Span) + +This release substantially upgrades the error/reporting system: + +- Introduced `SourceFile` to carry file identity + full content, enabling readable snippets for every error. +- Parsing now tracks spans (`start`, `len`) for each extracted class/spell token and propagates them through spell generation. +- Error model upgraded from plain strings to structured compile errors (message / label / help / span / source). +- Added a diagnostics adapter mapping `GrimoireCssError` to `miette::Diagnostic` for polished CLI output. + +User-facing validation got stricter and clearer: + +- Better errors for malformed function-like values / parentheses (`spell_value_validator`). +- Color function argument validation now returns a proper error instead of being silently ignored. + +### Performance Improvements + Opt-in Parallelism + +Grimoire CSS stays output-stable while getting faster and leaner: + +- Reduced redundant passes and duplicated work. +- Lowered allocation and clone pressure in hot paths. +- Added opt-in parallelism for filesystem builds via `GRIMOIRE_CSS_JOBS`. + +Safe default remains single-threaded; scaling is opt-in based on machine and project size. + +### Repro Sandbox + Architecture Guide + +To improve maintenance and debugging velocity: + +- Added `repro/` containing minimal scenarios for reproducing features and diagnostics. +- Added `.github/copilot-instructions.md` documenting the project’s architecture conventions. + +## Migration Notes + +### For Users + +- **Optional parallel builds**: Set `GRIMOIRE_CSS_JOBS` to enable multi-core builds in filesystem mode. Without it, behavior remains unchanged. +- **Stricter validation**: Invalid color function arguments that were previously ignored now raise proper errors (with spans and help text). + +### For Contributors + +- Prefer adding/using minimal scenarios under `repro/` when improving parser/diagnostics behavior. +- Follow the architecture guide in `.github/copilot-instructions.md` when introducing new commands, core pipeline changes, or infrastructure glue. + --- # v1.6.0 Chromaspire: The Color Convergence @@ -174,19 +253,16 @@ Grimoire CSS enhances its magical arsenal with the **v1.4.0 Aetheric Flow** rele ### Enhancements #### Argument Handling Improvements - - Replaced `&[String]` with `Vec` for more flexible argument processing - Enhanced compatibility with NodeJS wrapper implementation - Improved argument collection and processing through `env::args()` #### Visual Feedback Enhancement - - Added new spinner variations for different operation states - Enhanced progress visualization during lengthy operations - Improved user experience with more engaging loading indicators #### CLI Flow Optimization - - Streamlined `start_as_cli` workflow for better usability - Enhanced command processing and execution flow - Improved overall CLI interaction experience diff --git a/benchmark/.python-version b/benchmark/.python-version new file mode 100644 index 0000000..95ed564 --- /dev/null +++ b/benchmark/.python-version @@ -0,0 +1 @@ +3.14.2 diff --git a/benchmark/__pycache__/main.cpython-314.pyc b/benchmark/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..047a204 Binary files /dev/null and b/benchmark/__pycache__/main.cpython-314.pyc differ diff --git a/benchmark/core/__pycache__/benchmark_formatter.cpython-314.pyc b/benchmark/core/__pycache__/benchmark_formatter.cpython-314.pyc new file mode 100644 index 0000000..cc50380 Binary files /dev/null and b/benchmark/core/__pycache__/benchmark_formatter.cpython-314.pyc differ diff --git a/benchmark/core/__pycache__/metrics_collector.cpython-314.pyc b/benchmark/core/__pycache__/metrics_collector.cpython-314.pyc new file mode 100644 index 0000000..923bc13 Binary files /dev/null and b/benchmark/core/__pycache__/metrics_collector.cpython-314.pyc differ diff --git a/benchmark/core/__pycache__/project_creator.cpython-314.pyc b/benchmark/core/__pycache__/project_creator.cpython-314.pyc new file mode 100644 index 0000000..0756bb8 Binary files /dev/null and b/benchmark/core/__pycache__/project_creator.cpython-314.pyc differ diff --git a/benchmark/core/__pycache__/report_generator.cpython-314.pyc b/benchmark/core/__pycache__/report_generator.cpython-314.pyc new file mode 100644 index 0000000..e277879 Binary files /dev/null and b/benchmark/core/__pycache__/report_generator.cpython-314.pyc differ diff --git a/benchmark/core/benchmark_formatter.py b/benchmark/core/benchmark_formatter.py index afa5b45..627432b 100644 --- a/benchmark/core/benchmark_formatter.py +++ b/benchmark/core/benchmark_formatter.py @@ -267,6 +267,10 @@ def generate_pretty_results(input_file): } } + jobs_value = raw_results.get("system_info", {}).get("benchmark", {}).get("grimoire_css_jobs", None) + if jobs_value is not None: + pretty_results["metadata"]["system"]["grimoire_css_jobs"] = jobs_value + return pretty_results diff --git a/benchmark/core/metrics_collector.py b/benchmark/core/metrics_collector.py index 1dfcf3e..3b34e34 100644 --- a/benchmark/core/metrics_collector.py +++ b/benchmark/core/metrics_collector.py @@ -19,6 +19,7 @@ from pathlib import Path import platform import traceback +import os class ProcessMonitor: @@ -28,12 +29,29 @@ def __init__(self): """Initialize the process monitor.""" self.is_windows = platform.system() == "Windows" self.is_macos = platform.system() == "Darwin" + # Backward-compatible primary memory series. self.memory_samples = [] self.peak_memory_bytes = 0 + # Additional memory series for better cross-run comparability. + self.memory_samples_rss = [] + self.peak_memory_bytes_rss = 0 + self.memory_samples_uss = [] + self.peak_memory_bytes_uss = 0 + # Partial USS series: sums USS only for processes where psutil reports it. + # This is useful on macOS where USS may be unavailable for some children. + self.memory_samples_uss_partial = [] + self.peak_memory_bytes_uss_partial = 0 + self.uss_coverage_samples = [] # fraction in [0..1] + self.process_count_samples = [] + self.uss_available_count_samples = [] self.cpu_user_time = 0 self.cpu_system_time = 0 self.io_read_bytes = 0 self.io_write_bytes = 0 + # Which memory measurement is used for the backward-compatible primary fields. + # - macOS/Linux: prefer 'uss' if available for the whole process tree, else 'rss' + # - Windows: prefer 'private' if available, else 'rss' + self.memory_measurement = "unknown" # Tracks all processes we're monitoring self.monitored_processes = set() # Maps PIDs to their last CPU times for delta calculations @@ -49,10 +67,20 @@ def start_monitoring(self, pid): # Reset metrics for new monitoring session self.memory_samples = [] self.peak_memory_bytes = 0 + self.memory_samples_rss = [] + self.peak_memory_bytes_rss = 0 + self.memory_samples_uss = [] + self.peak_memory_bytes_uss = 0 + self.memory_samples_uss_partial = [] + self.peak_memory_bytes_uss_partial = 0 + self.uss_coverage_samples = [] + self.process_count_samples = [] + self.uss_available_count_samples = [] self.cpu_user_time = 0 self.cpu_system_time = 0 self.io_read_bytes = 0 self.io_write_bytes = 0 + self.memory_measurement = "unknown" try: # Store initial process state @@ -107,7 +135,13 @@ def _monitor_process_tree(self, pid): self._update_process_list() # Reset per-iteration counters - current_total_memory = 0 + current_total_primary = 0 + current_total_rss = 0 + current_total_uss = 0 + uss_valid_for_all = True + current_total_uss_partial = 0 + uss_available_count = 0 + process_count = 0 # Check all processes in our monitoring list for proc in list(self.monitored_processes): @@ -117,9 +151,23 @@ def _monitor_process_tree(self, pid): self.monitored_processes.remove(proc) continue - # Measure memory using the appropriate platform-specific method - memory_used = self._get_process_memory(proc) - current_total_memory += memory_used + process_count += 1 + + # Measure memory + rss_bytes, uss_bytes, private_bytes = self._get_process_memory_components(proc) + + # RSS is always available. + current_total_rss += rss_bytes + + # USS is only valid if available for *all* monitored processes. + if uss_bytes is None: + uss_valid_for_all = False + else: + current_total_uss += uss_bytes + current_total_uss_partial += uss_bytes + uss_available_count += 1 + + # Primary metric is chosen after the loop based on platform and availability. # Measure CPU time delta self._update_cpu_times(proc) @@ -130,11 +178,61 @@ def _monitor_process_tree(self, pid): # Process no longer exists or can't be accessed self.monitored_processes.discard(proc) - # Update memory metrics only if we got a valid reading - if current_total_memory > 0: - self.memory_samples.append(current_total_memory) - self.peak_memory_bytes = max( - self.peak_memory_bytes, current_total_memory) + # Choose a stable primary memory metric per-run to avoid mixing RSS/USS + # across samples (which makes peak comparisons meaningless). + # + # - macOS/Linux: primary is RSS (always available) + # - Windows: primary is private working set if available, otherwise RSS + if self.is_windows: + # Prefer summing private memory if psutil provides it. + current_total_private = 0 + private_valid_for_all = True + for proc in list(self.monitored_processes): + try: + if proc.is_running(): + _, _, p = self._get_process_memory_components(proc) + if p is None: + private_valid_for_all = False + break + current_total_private += p + except (psutil.NoSuchProcess, psutil.AccessDenied): + private_valid_for_all = False + break + + if private_valid_for_all and current_total_private > 0: + self.memory_measurement = "private" + current_total_primary = current_total_private + else: + self.memory_measurement = "rss" + current_total_primary = current_total_rss + else: + self.memory_measurement = "rss" + current_total_primary = current_total_rss + + # Update memory metrics only if we got a valid reading. + if current_total_primary > 0: + self.memory_samples.append(current_total_primary) + self.peak_memory_bytes = max(self.peak_memory_bytes, current_total_primary) + + if current_total_rss > 0: + self.memory_samples_rss.append(current_total_rss) + self.peak_memory_bytes_rss = max(self.peak_memory_bytes_rss, current_total_rss) + + if uss_valid_for_all and current_total_uss > 0: + self.memory_samples_uss.append(current_total_uss) + self.peak_memory_bytes_uss = max(self.peak_memory_bytes_uss, current_total_uss) + + # Always record partial-USS (may undercount) and coverage. + if current_total_uss_partial > 0: + self.memory_samples_uss_partial.append(current_total_uss_partial) + self.peak_memory_bytes_uss_partial = max( + self.peak_memory_bytes_uss_partial, current_total_uss_partial + ) + + if process_count > 0: + self.uss_coverage_samples.append(uss_available_count / process_count) + self.process_count_samples.append(process_count) + self.uss_available_count_samples.append(uss_available_count) time.sleep(sampling_interval) @@ -146,6 +244,32 @@ def _monitor_process_tree(self, pid): print(f"Error in monitoring thread: {e}") traceback.print_exc() + def _get_process_memory_components(self, proc): + """Return (rss_bytes, uss_bytes_or_None, private_bytes_or_None).""" + try: + memory_info = proc.memory_info() + rss = getattr(memory_info, 'rss', 0) or 0 + + uss = None + private = None + + # Windows: private working set. + if self.is_windows: + private = getattr(memory_info, 'private', None) + return rss, uss, private + + # macOS/Linux: try USS if available. + try: + memory_full = proc.memory_full_info() + if hasattr(memory_full, 'uss'): + uss = getattr(memory_full, 'uss') + except Exception: + uss = None + + return rss, uss, private + except (psutil.NoSuchProcess, psutil.AccessDenied): + return 0, None, None + def _update_process_list(self): """Update the list of processes we're monitoring to include new children.""" processes_to_check = list(self.monitored_processes) @@ -183,6 +307,7 @@ def _get_process_memory(self, proc): try: if self.is_windows: # On Windows, use private working set for exclusive memory usage + self.memory_measurement = "private" return proc.memory_info().private elif self.is_macos: # On macOS, use rss - shared memory for better accuracy @@ -190,14 +315,22 @@ def _get_process_memory(self, proc): try: # Try to get more accurate measurement on macOS if available memory_full = proc.memory_full_info() - return getattr(memory_full, 'uss', memory_info.rss) + if hasattr(memory_full, 'uss'): + self.memory_measurement = "uss" + return getattr(memory_full, 'uss') + + self.memory_measurement = "rss" + return memory_info.rss except: + self.memory_measurement = "rss" return memory_info.rss else: # On Linux, USS (Unique Set Size) is most accurate try: + self.memory_measurement = "uss" return proc.memory_full_info().uss except: + self.memory_measurement = "rss" return proc.memory_info().rss except (psutil.NoSuchProcess, psutil.AccessDenied): return 0 @@ -263,7 +396,27 @@ def get_metrics(self): "peak_bytes": self.peak_memory_bytes, "peak_mb": self.peak_memory_bytes / (1024 * 1024), "avg_bytes": statistics.mean(self.memory_samples) if self.memory_samples else 0, - "avg_mb": statistics.mean(self.memory_samples) / (1024 * 1024) if self.memory_samples else 0 + "avg_mb": statistics.mean(self.memory_samples) / (1024 * 1024) if self.memory_samples else 0, + "measurement": self.memory_measurement, + # Additional series (may be empty if not measurable). + "rss_peak_bytes": self.peak_memory_bytes_rss, + "rss_peak_mb": self.peak_memory_bytes_rss / (1024 * 1024), + "rss_avg_bytes": statistics.mean(self.memory_samples_rss) if self.memory_samples_rss else 0, + "rss_avg_mb": statistics.mean(self.memory_samples_rss) / (1024 * 1024) if self.memory_samples_rss else 0, + "uss_peak_bytes": self.peak_memory_bytes_uss, + "uss_peak_mb": self.peak_memory_bytes_uss / (1024 * 1024), + "uss_avg_bytes": statistics.mean(self.memory_samples_uss) if self.memory_samples_uss else 0, + "uss_avg_mb": statistics.mean(self.memory_samples_uss) / (1024 * 1024) if self.memory_samples_uss else 0, + "uss_is_complete": bool(self.memory_samples_uss), + "uss_partial_peak_bytes": self.peak_memory_bytes_uss_partial, + "uss_partial_peak_mb": self.peak_memory_bytes_uss_partial / (1024 * 1024), + "uss_partial_avg_bytes": statistics.mean(self.memory_samples_uss_partial) if self.memory_samples_uss_partial else 0, + "uss_partial_avg_mb": statistics.mean(self.memory_samples_uss_partial) / (1024 * 1024) if self.memory_samples_uss_partial else 0, + "uss_coverage_avg": statistics.mean(self.uss_coverage_samples) if self.uss_coverage_samples else 0, + "uss_process_count_avg": statistics.mean(self.process_count_samples) if self.process_count_samples else 0, + "uss_process_count_max": max(self.process_count_samples) if self.process_count_samples else 0, + "uss_available_count_avg": statistics.mean(self.uss_available_count_samples) if self.uss_available_count_samples else 0, + "uss_available_count_max": max(self.uss_available_count_samples) if self.uss_available_count_samples else 0, }, "cpu": { "user_time": self.cpu_user_time, @@ -487,7 +640,23 @@ class GrimoireMetricsCollector(MetricsCollector): def __init__(self, output_dir="grimoire_css_output", executable="../target/release/grimoire_css"): """Initialize the Grimoire CSS metrics collector.""" super().__init__(output_dir) - self.executable = executable + # Allow overriding the binary path for profiling / custom builds. + # By default we run the release binary to keep benchmarks comparable. + # + # Priority order: + # 1) GRIMOIRE_CSS_EXECUTABLE: explicit path wins. + # 2) GRIMOIRE_CSS_USE_PROFILING=1: prefer ../target/profiling/grimoire_css if present. + # 3) Fallback to the default release binary. + overridden = os.environ.get("GRIMOIRE_CSS_EXECUTABLE") + use_profiling = os.environ.get("GRIMOIRE_CSS_USE_PROFILING") == "1" + + if overridden: + self.executable = overridden + elif use_profiling: + profiling_candidate = Path("../target/profiling/grimoire_css") + self.executable = str(profiling_candidate) if profiling_candidate.exists() else executable + else: + self.executable = executable def run_benchmark(self): """Run the Grimoire CSS benchmark and collect metrics.""" @@ -501,6 +670,11 @@ def run_benchmark(self): process, elapsed_time, process_metrics, stdout, stderr = self.run_process( cmd) + # dhat (heap profiling) drastically slows execution and changes allocation behavior. + # If it's enabled, the reported build time is not comparable to normal runs. + if stderr and "dhat:" in stderr: + print("Warning: dhat heap profiling detected in Grimoire process output. Build time is not comparable; disable heap profiling for performance benchmarks.") + # Step 3: Analyze output files output_metrics = self.output_analyzer.analyze() self.output_files_size = output_metrics["total_size_bytes"] @@ -514,6 +688,12 @@ def run_benchmark(self): process ) + # Add run metadata for reproducibility. + result["run"] = { + "executable": str(self.executable), + "argv": cmd, + } + return result except Exception as e: print(f"Error running Grimoire CSS benchmark: {e}") diff --git a/benchmark/core/report_generator.py b/benchmark/core/report_generator.py index 6cdfe6c..0135c1b 100644 --- a/benchmark/core/report_generator.py +++ b/benchmark/core/report_generator.py @@ -64,6 +64,10 @@ def generate_report(results): lines.append( f"Memory: {system_info.get('memory', {}).get('total_gb', 'Unknown')} GB") + jobs_value = system_info.get('benchmark', {}).get('grimoire_css_jobs', None) + if jobs_value is not None: + lines.append(f"GRIMOIRE_CSS_JOBS: {jobs_value}") + # Input summary lines.append("\nINPUT SUMMARY") lines.append("-" * 80) @@ -89,6 +93,35 @@ def generate_report(results): f"Peak Memory: {metrics['process']['memory']['peak_mb']:.2f} MB") lines.append( f"Average Memory: {metrics['process']['memory']['avg_mb']:.2f} MB") + + mem = metrics.get('process', {}).get('memory', {}) + # If available, show both RSS and USS for better comparability. + if 'rss_peak_mb' in mem: + lines.append(f"Peak RSS: {mem.get('rss_peak_mb', 0.0):.2f} MB") + lines.append(f"Average RSS: {mem.get('rss_avg_mb', 0.0):.2f} MB") + + # USS may be unavailable for some processes on macOS; if so, report a partial total + coverage. + if mem.get('uss_is_complete') and 'uss_peak_mb' in mem: + lines.append(f"Peak USS: {mem.get('uss_peak_mb', 0.0):.2f} MB") + lines.append(f"Average USS: {mem.get('uss_avg_mb', 0.0):.2f} MB") + elif 'uss_coverage_avg' in mem: + coverage_pct = float(mem.get('uss_coverage_avg', 0.0)) * 100.0 + proc_avg = float(mem.get('uss_process_count_avg', 0.0)) + proc_max = int(mem.get('uss_process_count_max', 0) or 0) + uss_avg = float(mem.get('uss_available_count_avg', 0.0)) + uss_max = int(mem.get('uss_available_count_max', 0) or 0) + + if mem.get('uss_partial_peak_bytes', 0) and 'uss_partial_peak_mb' in mem: + lines.append(f"Peak USS (partial): {mem.get('uss_partial_peak_mb', 0.0):.2f} MB") + lines.append(f"Average USS (partial): {mem.get('uss_partial_avg_mb', 0.0):.2f} MB") + else: + lines.append("USS: unavailable for monitored process tree") + + lines.append( + f"USS Coverage (avg): {coverage_pct:.1f}% (avg {uss_avg:.1f}/{proc_avg:.1f} procs, max {uss_max}/{proc_max})" + ) + if mem.get('measurement'): + lines.append(f"Primary Memory Metric: {mem.get('measurement')}") if "memory_efficiency" in metrics["throughput"]: lines.append( f"Memory Efficiency: {metrics['throughput']['memory_efficiency']:.2f} classes/MB") @@ -162,6 +195,10 @@ def generate_comparison_report(results): lines.append( f"Memory: {system_info.get('memory', {}).get('total_gb', 'Unknown')} GB") + jobs_value = system_info.get('benchmark', {}).get('grimoire_css_jobs', None) + if jobs_value is not None: + lines.append(f"GRIMOIRE_CSS_JOBS: {jobs_value}") + # Performance comparison lines.append("\nPERFORMANCE COMPARISON") lines.append("-" * 80) diff --git a/benchmark/main.py b/benchmark/main.py index 2335ec0..608ed43 100644 --- a/benchmark/main.py +++ b/benchmark/main.py @@ -28,6 +28,8 @@ import psutil import time import datetime +import subprocess +import os from pathlib import Path from core.project_creator import create_benchmark_projects @@ -66,12 +68,46 @@ def parse_args(): def collect_system_info(): """Collect information about the system for benchmark context.""" + # Benchmark configuration (captured as part of system info so it is persisted + # into all output formats). + jobs_raw = os.environ.get("GRIMOIRE_CSS_JOBS") + jobs_value = None + if jobs_raw is not None: + jobs_raw = jobs_raw.strip() + if jobs_raw == "": + jobs_value = None + else: + try: + jobs_value = int(jobs_raw) + except ValueError: + jobs_value = jobs_raw + + # Best-effort git metadata to make benchmark results reproducible. + git_sha = None + git_dirty = None + try: + git_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + git_dirty = subprocess.call( + ["git", "diff", "--quiet"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) != 0 + except Exception: + pass + return { "os": { "name": platform.system(), "version": platform.version(), "release": platform.release() }, + "benchmark": { + "grimoire_css_jobs": jobs_value, + }, "cpu": { "name": platform.processor() or platform.machine(), "cores_logical": psutil.cpu_count(logical=True), @@ -80,6 +116,11 @@ def collect_system_info(): "memory": { "total_gb": round(psutil.virtual_memory().total / (1024**3), 1) }, + "psutil_version": getattr(psutil, "__version__", "unknown"), + "git": { + "sha": git_sha, + "dirty": git_dirty, + }, "python_version": platform.python_version(), "timestamp": time.time(), "timestamp_human": time.strftime("%Y-%m-%d %H:%M:%S") @@ -232,6 +273,8 @@ def main(): print(f"OS: {system_info['os']['name']} {system_info['os']['release']}") print(f"CPU: {system_info['cpu']['name']}") print(f"Memory: {system_info['memory']['total_gb']} GB") + jobs_value = system_info.get('benchmark', {}).get('grimoire_css_jobs', None) + print(f"GRIMOIRE_CSS_JOBS: {jobs_value if jobs_value is not None else '(unset)'}") # Run benchmarks framework_results = run_framework_benchmark(args.framework, system_info) diff --git a/benchmark/results/result_20250323_111448.json b/benchmark/results/result_20250323_111448.json deleted file mode 100644 index 2adf288..0000000 --- a/benchmark/results/result_20250323_111448.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "system_info": { - "os": { - "name": "Darwin", - "version": "Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:23 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6020", - "release": "24.3.0" - }, - "cpu": { - "name": "arm", - "cores_logical": 12, - "cores_physical": 12 - }, - "memory": { - "total_gb": 32.0 - }, - "python_version": "3.13.2", - "timestamp": 1742724888.643092, - "timestamp_human": "2025-03-23 11:14:48" - }, - "grimoire": { - "input": { - "unique_class_count": 400006, - "total_input_size_bytes": 49187280, - "file_count": 100000 - }, - "output": { - "file_count": 10, - "total_size_bytes": 5296770, - "total_size_kb": 5172.626953125, - "avg_size_bytes": 529677.0, - "avg_size_kb": 517.2626953125 - }, - "process": { - "memory": { - "peak_bytes": 116604928, - "peak_mb": 111.203125, - "avg_bytes": 47982004.589147285, - "avg_mb": 45.75920542635659, - "std_dev_mb": 25.961544766980627 - }, - "cpu": { - "user_time": 0.755105886, - "system_time": 1.333121863, - "total_time": 2.0882277489999996 - }, - "io": { - "read_bytes": 49187280, - "read_mb": 46.90864562988281, - "write_bytes": 5296770, - "write_mb": 5.051393508911133 - } - }, - "throughput": { - "build_time_seconds": 2.09773588180542, - "classes_per_second": 190684.6345478603, - "memory_efficiency": 3597.0751721230854, - "bytes_processed_per_second": 23447794.56108978, - "bytes_generated_per_second": 2524993.754428859, - "cpu_utilization": 0.9954674309154511 - }, - "exit_code": 0, - "success": true - }, - "tailwind": { - "input": { - "unique_class_count": 400006, - "total_input_size_bytes": 49187280, - "file_count": 100000 - }, - "output": { - "file_count": 10, - "total_size_bytes": 5937710, - "total_size_kb": 5798.544921875, - "avg_size_bytes": 593771.0, - "avg_size_kb": 579.8544921875 - }, - "process": { - "memory": { - "peak_bytes": 361725952, - "peak_mb": 344.96875, - "avg_bytes": 191161685.33333334, - "avg_mb": 182.30598958333334, - "std_dev_mb": 74.08982503042134 - }, - "cpu": { - "user_time": 7.772634282, - "system_time": 60.89118616200001, - "total_time": 68.663820444 - }, - "io": { - "read_bytes": 49187280, - "read_mb": 46.90864562988281, - "write_bytes": 5937710, - "write_mb": 5.662641525268555 - } - }, - "throughput": { - "build_time_seconds": 10.575389862060547, - "classes_per_second": 37824.23203470074, - "memory_efficiency": 1159.5427122021922, - "bytes_processed_per_second": 4651107.9630700415, - "bytes_generated_per_second": 561464.8800137072, - "cpu_utilization": 6.492793300257707 - }, - "exit_code": 0, - "success": true - } -} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012340.json b/benchmark/results/result_20251227_012340.json new file mode 100644 index 0000000..36e19af --- /dev/null +++ b/benchmark/results/result_20251227_012340.json @@ -0,0 +1,140 @@ +{ + "system_info": { + "os": { + "name": "Darwin", + "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020", + "release": "25.2.0" + }, + "benchmark": { + "grimoire_css_jobs": 1 + }, + "cpu": { + "name": "arm", + "cores_logical": 12, + "cores_physical": 12 + }, + "memory": { + "total_gb": 32.0 + }, + "psutil_version": "7.0.0", + "git": { + "sha": "8f60e2643076e9065a21408a98d0da64b5aea71b", + "dirty": true + }, + "python_version": "3.14.2", + "timestamp": 1766795020.693991, + "timestamp_human": "2025-12-27 01:23:40" + }, + "grimoire": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5296800, + "total_size_kb": 5172.65625, + "avg_size_bytes": 529680.0, + "avg_size_kb": 517.265625 + }, + "process": { + "memory": { + "peak_bytes": 164986880, + "peak_mb": 157.34375, + "avg_bytes": 86189106.9683258, + "avg_mb": 82.19633766968326, + "measurement": "rss", + "rss_peak_bytes": 164986880, + "rss_peak_mb": 157.34375, + "rss_avg_bytes": 86189106.9683258, + "rss_avg_mb": 82.19633766968326, + "uss_peak_bytes": 138870784, + "uss_peak_mb": 132.4375, + "uss_avg_bytes": 66468405.28506787, + "uss_avg_mb": 63.38921097285068, + "uss_is_complete": true, + "uss_partial_peak_bytes": 138870784, + "uss_partial_peak_mb": 132.4375, + "uss_partial_avg_bytes": 66468405.28506787, + "uss_partial_avg_mb": 63.38921097285068, + "uss_coverage_avg": 1.0, + "uss_process_count_avg": 1, + "uss_process_count_max": 1, + "uss_available_count_avg": 1, + "uss_available_count_max": 1, + "std_dev_mb": 51.10911577288717 + }, + "cpu": { + "user_time": 0.807057117, + "system_time": 2.02749662, + "total_time": 2.8345537370000002 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5296800, + "write_mb": 5.051422119140625 + } + }, + "throughput": { + "build_time_seconds": 3.7795708179473877, + "classes_per_second": 105833.70950494203, + "memory_efficiency": 2542.2427010923534, + "bytes_processed_per_second": 13093359.638879737, + "bytes_generated_per_second": 1401428.959830045 + }, + "exit_code": 0, + "success": true, + "run": { + "executable": "../target/release/grimoire_css", + "argv": [ + "../target/release/grimoire_css", + "build" + ] + } + }, + "tailwind": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5937710, + "total_size_kb": 5798.544921875, + "avg_size_bytes": 593771.0, + "avg_size_kb": 579.8544921875 + }, + "process": { + "memory": { + "peak_bytes": 321273856, + "peak_mb": 306.390625, + "avg_bytes": 172912465.2890995, + "avg_mb": 164.9021771327014, + "std_dev_mb": 71.15805121031616 + }, + "cpu": { + "user_time": 8.919657383, + "system_time": 42.144116483, + "total_time": 51.06377386599999 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5937710, + "write_mb": 5.662641525268555 + } + }, + "throughput": { + "build_time_seconds": 9.40093445777893, + "classes_per_second": 42549.59991440102, + "memory_efficiency": 1305.5425569891377, + "bytes_processed_per_second": 5264080.950915584, + "bytes_generated_per_second": 631608.4881420231 + }, + "exit_code": 0, + "success": true + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012340.txt b/benchmark/results/result_20251227_012340.txt new file mode 100644 index 0000000..6478fa0 --- /dev/null +++ b/benchmark/results/result_20251227_012340.txt @@ -0,0 +1,126 @@ + + +================================================================================================================================================================ +GRIMOIRE PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:23:40 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 1 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 3.78 s +Processing Speed: 105833.71 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 157.34 MB +Average Memory: 82.20 MB +Peak RSS: 157.34 MB +Average RSS: 82.20 MB +Peak USS: 132.44 MB +Average USS: 63.39 MB +Primary Memory Metric: rss +Memory Efficiency: 2542.24 classes/MB +Memory Stability (Std Dev): 51.11 MB +Memory per Class: 412.46 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 807.06 ms +System CPU Time: 2.03 s +Total CPU Time: 2.83 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.05 MB +Output File Count: 10 +Output Size: 5.05 MB + +================================================================================ +TAILWIND PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:23:40 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 1 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 9.40 s +Processing Speed: 42549.60 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 306.39 MB +Average Memory: 164.90 MB +Memory Efficiency: 1305.54 classes/MB +Memory Stability (Std Dev): 71.16 MB +Memory per Class: 803.17 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 8.92 s +System CPU Time: 42.14 s +Total CPU Time: 51.06 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.66 MB +Output File Count: 10 +Output Size: 5.66 MB + +================================================================================ +CSS FRAMEWORKS PERFORMANCE COMPARISON +================================================================================ +Generated: 2025-12-27 01:23:40 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 1 + +PERFORMANCE COMPARISON +-------------------------------------------------------------------------------- +Metric | Grimoire CSS | Tailwind CSS | Difference | Ratio (G/T) +------------------------------------------------------------------------------------------------- +Build Time | 3.78 s | 9.40 s | 5.62 s | 2.49x +Classes/sec | 105833.71 | 42549.60 | 63284.11 | 2.49x +Peak Memory | 157.34 MB | 306.39 MB | 149.05 MB | 1.95x +Memory Efficiency | 2542.24 classes/MB | 1305.54 classes/MB | 1236.70 classes/MB | 1.95x +Output Size | 5.05 MB | 5.66 MB | 625.89 KB | 1.12x + +Notes: +- Build Time: lower is better +- Classes/sec: higher is better +- Peak Memory: lower is better +- Memory Efficiency: higher is better +- Output Size: lower is better \ No newline at end of file diff --git a/benchmark/results/result_20251227_012340_pretty.json b/benchmark/results/result_20251227_012340_pretty.json new file mode 100644 index 0000000..b69ac99 --- /dev/null +++ b/benchmark/results/result_20251227_012340_pretty.json @@ -0,0 +1,119 @@ +{ + "charts": [ + { + "title": "Grimoire CSS vs Tailwind CSS - Build Time", + "chartTitle": "Build Time", + "chartSubtitle": "Total time taken to compile CSS (lower is better)", + "chartId": "chart_time", + "highlightText": "2.5x faster", + "grimoireHeight": 34.17357295366463, + "tailwindHeight": 85.0, + "grimoireValue": "3.78s", + "tailwindValue": "9.40s", + "grimoireRawValue": 3.78, + "tailwindRawValue": 9.4 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Peak Memory Usage", + "chartTitle": "Peak Memory Usage", + "chartSubtitle": "Maximum memory consumed during compilation (lower is better)", + "chartId": "chart_peak_memory", + "highlightText": "1.9x less", + "grimoireHeight": 43.650874598398694, + "tailwindHeight": 85.0, + "grimoireValue": "157.34 MB", + "tailwindValue": "306.39 MB", + "grimoireRawValue": 157.34, + "tailwindRawValue": 306.39 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Average Memory Usage", + "chartTitle": "Average Memory Usage", + "chartSubtitle": "Average memory consumed during compilation (lower is better)", + "chartId": "chart_avg_memory", + "highlightText": "2.0x less", + "grimoireHeight": 42.36868683850482, + "tailwindHeight": 85.0, + "grimoireValue": "82.2 MB", + "tailwindValue": "164.9 MB", + "grimoireRawValue": 82.2, + "tailwindRawValue": 164.9 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (User Time)", + "chartTitle": "CPU Usage (User Time)", + "chartSubtitle": "CPU time spent in user mode during compilation (lower is better)", + "chartId": "chart_cpu_user", + "highlightText": "11.1x less", + "grimoireHeight": 7.690862103710917, + "tailwindHeight": 85.0, + "grimoireValue": "807.06ms", + "tailwindValue": "8.92s", + "grimoireRawValue": 0.81, + "tailwindRawValue": 8.92 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (System Time)", + "chartTitle": "CPU Usage (System Time)", + "chartSubtitle": "CPU time spent in system mode during compilation (lower is better)", + "chartId": "chart_cpu_system", + "highlightText": "20.8x less", + "grimoireHeight": 4.089235392311926, + "tailwindHeight": 85.0, + "grimoireValue": "2.03s", + "tailwindValue": "42.14s", + "grimoireRawValue": 2.03, + "tailwindRawValue": 42.14 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Output Size", + "chartTitle": "Output Size", + "chartSubtitle": "Size of the generated CSS file (lower is better)", + "chartId": "chart_output", + "highlightText": "1.1x less", + "grimoireHeight": 75.82519186689818, + "tailwindHeight": 85.0, + "grimoireValue": "5.05 MB", + "tailwindValue": "5.66 MB", + "grimoireRawValue": 5172.66, + "tailwindRawValue": 5798.54 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Processing Speed", + "chartTitle": "Processing Speed", + "chartSubtitle": "Number of utility classes processed per second (higher is better)", + "chartId": "chart_classes_per_second", + "highlightText": "2.5x faster", + "grimoireHeight": 85.0, + "tailwindHeight": 34.17357295366463, + "grimoireValue": "105833.71 classes/s", + "tailwindValue": "42549.60 classes/s", + "grimoireRawValue": 105833.71, + "tailwindRawValue": 42549.6 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Memory Efficiency", + "chartTitle": "Memory Efficiency", + "chartSubtitle": "Number of utility classes processed per MB of memory (higher is better)", + "chartId": "chart_memory_efficiency", + "highlightText": "1.9x more efficient", + "grimoireHeight": 85.0, + "tailwindHeight": 43.650874598398694, + "grimoireValue": "2542.24 classes/MB", + "tailwindValue": "1305.54 classes/MB", + "grimoireRawValue": 2542.24, + "tailwindRawValue": 1305.54 + } + ], + "metadata": { + "timestamp": 1766795020.693991, + "timestamp_human": "2025-12-27 01:23:40", + "system": { + "os": "Darwin 25.2.0", + "cpu": "arm", + "cores": "12 physical, 12 logical", + "memory": "32.0 GB", + "grimoire_css_jobs": 1 + } + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012409.json b/benchmark/results/result_20251227_012409.json new file mode 100644 index 0000000..a05142f --- /dev/null +++ b/benchmark/results/result_20251227_012409.json @@ -0,0 +1,140 @@ +{ + "system_info": { + "os": { + "name": "Darwin", + "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020", + "release": "25.2.0" + }, + "benchmark": { + "grimoire_css_jobs": 2 + }, + "cpu": { + "name": "arm", + "cores_logical": 12, + "cores_physical": 12 + }, + "memory": { + "total_gb": 32.0 + }, + "psutil_version": "7.0.0", + "git": { + "sha": "8f60e2643076e9065a21408a98d0da64b5aea71b", + "dirty": true + }, + "python_version": "3.14.2", + "timestamp": 1766795049.0435998, + "timestamp_human": "2025-12-27 01:24:09" + }, + "grimoire": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5296800, + "total_size_kb": 5172.65625, + "avg_size_bytes": 529680.0, + "avg_size_kb": 517.265625 + }, + "process": { + "memory": { + "peak_bytes": 156024832, + "peak_mb": 148.796875, + "avg_bytes": 96777847.82978724, + "avg_mb": 92.29454787234043, + "measurement": "rss", + "rss_peak_bytes": 156024832, + "rss_peak_mb": 148.796875, + "rss_avg_bytes": 96777847.82978724, + "rss_avg_mb": 92.29454787234043, + "uss_peak_bytes": 91914240, + "uss_peak_mb": 87.65625, + "uss_avg_bytes": 44642042.55319149, + "uss_avg_mb": 42.57396941489362, + "uss_is_complete": true, + "uss_partial_peak_bytes": 91914240, + "uss_partial_peak_mb": 87.65625, + "uss_partial_avg_bytes": 44642042.55319149, + "uss_partial_avg_mb": 42.57396941489362, + "uss_coverage_avg": 1.0, + "uss_process_count_avg": 1, + "uss_process_count_max": 1, + "uss_available_count_avg": 1, + "uss_available_count_max": 1, + "std_dev_mb": 42.757697156760315 + }, + "cpu": { + "user_time": 0.8715153340000001, + "system_time": 2.1863928049999997, + "total_time": 3.057908139 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5296800, + "write_mb": 5.051422119140625 + } + }, + "throughput": { + "build_time_seconds": 1.5986220836639404, + "classes_per_second": 250219.23823497523, + "memory_efficiency": 2688.2688228499424, + "bytes_processed_per_second": 30956209.41666106, + "bytes_generated_per_second": 3313353.452405756 + }, + "exit_code": 0, + "success": true, + "run": { + "executable": "../target/release/grimoire_css", + "argv": [ + "../target/release/grimoire_css", + "build" + ] + } + }, + "tailwind": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5937710, + "total_size_kb": 5798.544921875, + "avg_size_bytes": 593771.0, + "avg_size_kb": 579.8544921875 + }, + "process": { + "memory": { + "peak_bytes": 325632000, + "peak_mb": 310.546875, + "avg_bytes": 172060098.1642512, + "avg_mb": 164.08929649758454, + "std_dev_mb": 70.69412801340499 + }, + "cpu": { + "user_time": 8.739942736000001, + "system_time": 42.878009256999995, + "total_time": 51.617951993 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5937710, + "write_mb": 5.662641525268555 + } + }, + "throughput": { + "build_time_seconds": 9.343071699142456, + "classes_per_second": 42813.114667279515, + "memory_efficiency": 1288.0696352201257, + "bytes_processed_per_second": 5296682.032798928, + "bytes_generated_per_second": 635520.1149259066 + }, + "exit_code": 0, + "success": true + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012409.txt b/benchmark/results/result_20251227_012409.txt new file mode 100644 index 0000000..b70c755 --- /dev/null +++ b/benchmark/results/result_20251227_012409.txt @@ -0,0 +1,126 @@ + + +================================================================================================================================================================ +GRIMOIRE PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:09 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 2 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 1.60 s +Processing Speed: 250219.24 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 148.80 MB +Average Memory: 92.29 MB +Peak RSS: 148.80 MB +Average RSS: 92.29 MB +Peak USS: 87.66 MB +Average USS: 42.57 MB +Primary Memory Metric: rss +Memory Efficiency: 2688.27 classes/MB +Memory Stability (Std Dev): 42.76 MB +Memory per Class: 390.06 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 871.52 ms +System CPU Time: 2.19 s +Total CPU Time: 3.06 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.05 MB +Output File Count: 10 +Output Size: 5.05 MB + +================================================================================ +TAILWIND PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:09 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 2 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 9.34 s +Processing Speed: 42813.11 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 310.55 MB +Average Memory: 164.09 MB +Memory Efficiency: 1288.07 classes/MB +Memory Stability (Std Dev): 70.69 MB +Memory per Class: 814.07 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 8.74 s +System CPU Time: 42.88 s +Total CPU Time: 51.62 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.66 MB +Output File Count: 10 +Output Size: 5.66 MB + +================================================================================ +CSS FRAMEWORKS PERFORMANCE COMPARISON +================================================================================ +Generated: 2025-12-27 01:24:09 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 2 + +PERFORMANCE COMPARISON +-------------------------------------------------------------------------------- +Metric | Grimoire CSS | Tailwind CSS | Difference | Ratio (G/T) +------------------------------------------------------------------------------------------------- +Build Time | 1.60 s | 9.34 s | 7.74 s | 5.84x +Classes/sec | 250219.24 | 42813.11 | 207406.12 | 5.84x +Peak Memory | 148.80 MB | 310.55 MB | 161.75 MB | 2.09x +Memory Efficiency | 2688.27 classes/MB | 1288.07 classes/MB | 1400.20 classes/MB | 2.09x +Output Size | 5.05 MB | 5.66 MB | 625.89 KB | 1.12x + +Notes: +- Build Time: lower is better +- Classes/sec: higher is better +- Peak Memory: lower is better +- Memory Efficiency: higher is better +- Output Size: lower is better \ No newline at end of file diff --git a/benchmark/results/result_20251227_012409_pretty.json b/benchmark/results/result_20251227_012409_pretty.json new file mode 100644 index 0000000..7ab91a7 --- /dev/null +++ b/benchmark/results/result_20251227_012409_pretty.json @@ -0,0 +1,119 @@ +{ + "charts": [ + { + "title": "Grimoire CSS vs Tailwind CSS - Build Time", + "chartTitle": "Build Time", + "chartSubtitle": "Total time taken to compile CSS (lower is better)", + "chartId": "chart_time", + "highlightText": "5.8x faster", + "grimoireHeight": 14.543704842156655, + "tailwindHeight": 85.0, + "grimoireValue": "1.60s", + "tailwindValue": "9.34s", + "grimoireRawValue": 1.6, + "tailwindRawValue": 9.34 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Peak Memory Usage", + "chartTitle": "Peak Memory Usage", + "chartSubtitle": "Maximum memory consumed during compilation (lower is better)", + "chartId": "chart_peak_memory", + "highlightText": "2.1x less", + "grimoireHeight": 40.72729559748427, + "tailwindHeight": 85.0, + "grimoireValue": "148.8 MB", + "tailwindValue": "310.55 MB", + "grimoireRawValue": 148.8, + "tailwindRawValue": 310.55 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Average Memory Usage", + "chartTitle": "Average Memory Usage", + "chartSubtitle": "Average memory consumed during compilation (lower is better)", + "chartId": "chart_avg_memory", + "highlightText": "1.8x less", + "grimoireHeight": 47.80955697049026, + "tailwindHeight": 85.0, + "grimoireValue": "92.29 MB", + "tailwindValue": "164.09 MB", + "grimoireRawValue": 92.29, + "tailwindRawValue": 164.09 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (User Time)", + "chartTitle": "CPU Usage (User Time)", + "chartSubtitle": "CPU time spent in user mode during compilation (lower is better)", + "chartId": "chart_cpu_user", + "highlightText": "10.0x less", + "grimoireHeight": 8.475891161719849, + "tailwindHeight": 85.0, + "grimoireValue": "871.52ms", + "tailwindValue": "8.74s", + "grimoireRawValue": 0.87, + "tailwindRawValue": 8.74 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (System Time)", + "chartTitle": "CPU Usage (System Time)", + "chartSubtitle": "CPU time spent in system mode during compilation (lower is better)", + "chartId": "chart_cpu_system", + "highlightText": "19.6x less", + "grimoireHeight": 4.334235465809559, + "tailwindHeight": 85.0, + "grimoireValue": "2.19s", + "tailwindValue": "42.88s", + "grimoireRawValue": 2.19, + "tailwindRawValue": 42.88 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Output Size", + "chartTitle": "Output Size", + "chartSubtitle": "Size of the generated CSS file (lower is better)", + "chartId": "chart_output", + "highlightText": "1.1x less", + "grimoireHeight": 75.82519186689818, + "tailwindHeight": 85.0, + "grimoireValue": "5.05 MB", + "tailwindValue": "5.66 MB", + "grimoireRawValue": 5172.66, + "tailwindRawValue": 5798.54 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Processing Speed", + "chartTitle": "Processing Speed", + "chartSubtitle": "Number of utility classes processed per second (higher is better)", + "chartId": "chart_classes_per_second", + "highlightText": "5.8x faster", + "grimoireHeight": 85.0, + "tailwindHeight": 14.543704842156655, + "grimoireValue": "250219.24 classes/s", + "tailwindValue": "42813.11 classes/s", + "grimoireRawValue": 250219.24, + "tailwindRawValue": 42813.11 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Memory Efficiency", + "chartTitle": "Memory Efficiency", + "chartSubtitle": "Number of utility classes processed per MB of memory (higher is better)", + "chartId": "chart_memory_efficiency", + "highlightText": "2.1x more efficient", + "grimoireHeight": 85.0, + "tailwindHeight": 40.72729559748427, + "grimoireValue": "2688.27 classes/MB", + "tailwindValue": "1288.07 classes/MB", + "grimoireRawValue": 2688.27, + "tailwindRawValue": 1288.07 + } + ], + "metadata": { + "timestamp": 1766795049.0435998, + "timestamp_human": "2025-12-27 01:24:09", + "system": { + "os": "Darwin 25.2.0", + "cpu": "arm", + "cores": "12 physical, 12 logical", + "memory": "32.0 GB", + "grimoire_css_jobs": 2 + } + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012425.json b/benchmark/results/result_20251227_012425.json new file mode 100644 index 0000000..6e7736b --- /dev/null +++ b/benchmark/results/result_20251227_012425.json @@ -0,0 +1,140 @@ +{ + "system_info": { + "os": { + "name": "Darwin", + "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020", + "release": "25.2.0" + }, + "benchmark": { + "grimoire_css_jobs": 4 + }, + "cpu": { + "name": "arm", + "cores_logical": 12, + "cores_physical": 12 + }, + "memory": { + "total_gb": 32.0 + }, + "psutil_version": "7.0.0", + "git": { + "sha": "8f60e2643076e9065a21408a98d0da64b5aea71b", + "dirty": true + }, + "python_version": "3.14.2", + "timestamp": 1766795065.6670752, + "timestamp_human": "2025-12-27 01:24:25" + }, + "grimoire": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5296800, + "total_size_kb": 5172.65625, + "avg_size_bytes": 529680.0, + "avg_size_kb": 517.265625 + }, + "process": { + "memory": { + "peak_bytes": 267829248, + "peak_mb": 255.421875, + "avg_bytes": 127034514.28571428, + "avg_mb": 121.14955357142857, + "measurement": "rss", + "rss_peak_bytes": 267829248, + "rss_peak_mb": 255.421875, + "rss_avg_bytes": 127034514.28571428, + "rss_avg_mb": 121.14955357142857, + "uss_peak_bytes": 149585920, + "uss_peak_mb": 142.65625, + "uss_avg_bytes": 67461822.17142858, + "uss_avg_mb": 64.33660714285715, + "uss_is_complete": true, + "uss_partial_peak_bytes": 149585920, + "uss_partial_peak_mb": 142.65625, + "uss_partial_avg_bytes": 67461822.17142858, + "uss_partial_avg_mb": 64.33660714285715, + "uss_coverage_avg": 0.9859154929577465, + "uss_process_count_avg": 1, + "uss_process_count_max": 1, + "uss_available_count_avg": 0.9859154929577465, + "uss_available_count_max": 1, + "std_dev_mb": 85.23104615436908 + }, + "cpu": { + "user_time": 0.922854154, + "system_time": 2.790858709, + "total_time": 3.713712863 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5296800, + "write_mb": 5.051422119140625 + } + }, + "throughput": { + "build_time_seconds": 1.2039422988891602, + "classes_per_second": 332246.8197762243, + "memory_efficiency": 1566.0600721844987, + "bytes_processed_per_second": 41104361.933009885, + "bytes_generated_per_second": 4399546.394280848 + }, + "exit_code": 0, + "success": true, + "run": { + "executable": "../target/release/grimoire_css", + "argv": [ + "../target/release/grimoire_css", + "build" + ] + } + }, + "tailwind": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5937710, + "total_size_kb": 5798.544921875, + "avg_size_bytes": 593771.0, + "avg_size_kb": 579.8544921875 + }, + "process": { + "memory": { + "peak_bytes": 317636608, + "peak_mb": 302.921875, + "avg_bytes": 173636806.90647483, + "avg_mb": 165.5929631294964, + "std_dev_mb": 70.57462560423957 + }, + "cpu": { + "user_time": 8.718745665, + "system_time": 43.216031122, + "total_time": 51.934776787 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5937710, + "write_mb": 5.662641525268555 + } + }, + "throughput": { + "build_time_seconds": 9.371271133422852, + "classes_per_second": 42684.28416006122, + "memory_efficiency": 1320.4922886470315, + "bytes_processed_per_second": 5280743.593417385, + "bytes_generated_per_second": 633607.7481338708 + }, + "exit_code": 0, + "success": true + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012425.txt b/benchmark/results/result_20251227_012425.txt new file mode 100644 index 0000000..f9a9dc9 --- /dev/null +++ b/benchmark/results/result_20251227_012425.txt @@ -0,0 +1,126 @@ + + +================================================================================================================================================================ +GRIMOIRE PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:25 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 4 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 1.20 s +Processing Speed: 332246.82 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 255.42 MB +Average Memory: 121.15 MB +Peak RSS: 255.42 MB +Average RSS: 121.15 MB +Peak USS: 142.66 MB +Average USS: 64.34 MB +Primary Memory Metric: rss +Memory Efficiency: 1566.06 classes/MB +Memory Stability (Std Dev): 85.23 MB +Memory per Class: 669.56 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 922.85 ms +System CPU Time: 2.79 s +Total CPU Time: 3.71 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.05 MB +Output File Count: 10 +Output Size: 5.05 MB + +================================================================================ +TAILWIND PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:25 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 4 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 9.37 s +Processing Speed: 42684.28 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 302.92 MB +Average Memory: 165.59 MB +Memory Efficiency: 1320.49 classes/MB +Memory Stability (Std Dev): 70.57 MB +Memory per Class: 794.08 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 8.72 s +System CPU Time: 43.22 s +Total CPU Time: 51.93 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.66 MB +Output File Count: 10 +Output Size: 5.66 MB + +================================================================================ +CSS FRAMEWORKS PERFORMANCE COMPARISON +================================================================================ +Generated: 2025-12-27 01:24:25 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 4 + +PERFORMANCE COMPARISON +-------------------------------------------------------------------------------- +Metric | Grimoire CSS | Tailwind CSS | Difference | Ratio (G/T) +------------------------------------------------------------------------------------------------ +Build Time | 1.20 s | 9.37 s | 8.17 s | 7.78x +Classes/sec | 332246.82 | 42684.28 | 289562.54 | 7.78x +Peak Memory | 255.42 MB | 302.92 MB | 47.50 MB | 1.19x +Memory Efficiency | 1566.06 classes/MB | 1320.49 classes/MB | 245.57 classes/MB | 1.19x +Output Size | 5.05 MB | 5.66 MB | 625.89 KB | 1.12x + +Notes: +- Build Time: lower is better +- Classes/sec: higher is better +- Peak Memory: lower is better +- Memory Efficiency: higher is better +- Output Size: lower is better \ No newline at end of file diff --git a/benchmark/results/result_20250323_111448_pretty.json b/benchmark/results/result_20251227_012425_pretty.json similarity index 58% rename from benchmark/results/result_20250323_111448_pretty.json rename to benchmark/results/result_20251227_012425_pretty.json index b67bf1a..e52b960 100644 --- a/benchmark/results/result_20250323_111448_pretty.json +++ b/benchmark/results/result_20251227_012425_pretty.json @@ -5,65 +5,65 @@ "chartTitle": "Build Time", "chartSubtitle": "Total time taken to compile CSS (lower is better)", "chartId": "chart_time", - "highlightText": "5.0x faster", - "grimoireHeight": 16.860612448260003, + "highlightText": "7.8x faster", + "grimoireHeight": 10.920086928292807, "tailwindHeight": 85.0, - "grimoireValue": "2.10s", - "tailwindValue": "10.58s", - "grimoireRawValue": 2.1, - "tailwindRawValue": 10.58 + "grimoireValue": "1.20s", + "tailwindValue": "9.37s", + "grimoireRawValue": 1.2, + "tailwindRawValue": 9.37 }, { "title": "Grimoire CSS vs Tailwind CSS - Peak Memory Usage", "chartTitle": "Peak Memory Usage", "chartSubtitle": "Maximum memory consumed during compilation (lower is better)", "chartId": "chart_peak_memory", - "highlightText": "3.1x less", - "grimoireHeight": 27.400353292870733, + "highlightText": "1.2x less", + "grimoireHeight": 71.67148088925569, "tailwindHeight": 85.0, - "grimoireValue": "111.2 MB", - "tailwindValue": "344.97 MB", - "grimoireRawValue": 111.2, - "tailwindRawValue": 344.97 + "grimoireValue": "255.42 MB", + "tailwindValue": "302.92 MB", + "grimoireRawValue": 255.42, + "tailwindRawValue": 302.92 }, { "title": "Grimoire CSS vs Tailwind CSS - Average Memory Usage", "chartTitle": "Average Memory Usage", "chartSubtitle": "Average memory consumed during compilation (lower is better)", "chartId": "chart_avg_memory", - "highlightText": "4.0x less", - "grimoireHeight": 21.33518745121853, + "highlightText": "1.4x less", + "grimoireHeight": 62.186894050071736, "tailwindHeight": 85.0, - "grimoireValue": "45.76 MB", - "tailwindValue": "182.31 MB", - "grimoireRawValue": 45.76, - "tailwindRawValue": 182.31 + "grimoireValue": "121.15 MB", + "tailwindValue": "165.59 MB", + "grimoireRawValue": 121.15, + "tailwindRawValue": 165.59 }, { "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (User Time)", "chartTitle": "CPU Usage (User Time)", "chartSubtitle": "CPU time spent in user mode during compilation (lower is better)", "chartId": "chart_cpu_user", - "highlightText": "10.3x less", - "grimoireHeight": 8.257689475836834, + "highlightText": "9.4x less", + "grimoireHeight": 8.997005544604335, "tailwindHeight": 85.0, - "grimoireValue": "755.11ms", - "tailwindValue": "7.77s", - "grimoireRawValue": 0.76, - "tailwindRawValue": 7.77 + "grimoireValue": "922.85ms", + "tailwindValue": "8.72s", + "grimoireRawValue": 0.92, + "tailwindRawValue": 8.72 }, { "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (System Time)", "chartTitle": "CPU Usage (System Time)", "chartSubtitle": "CPU time spent in system mode during compilation (lower is better)", "chartId": "chart_cpu_system", - "highlightText": "45.7x less", - "grimoireHeight": 2, + "highlightText": "15.5x less", + "grimoireHeight": 5.489235917923912, "tailwindHeight": 85.0, - "grimoireValue": "1.33s", - "tailwindValue": "60.89s", - "grimoireRawValue": 1.33, - "tailwindRawValue": 60.89 + "grimoireValue": "2.79s", + "tailwindValue": "43.22s", + "grimoireRawValue": 2.79, + "tailwindRawValue": 43.22 }, { "title": "Grimoire CSS vs Tailwind CSS - Output Size", @@ -71,11 +71,11 @@ "chartSubtitle": "Size of the generated CSS file (lower is better)", "chartId": "chart_output", "highlightText": "1.1x less", - "grimoireHeight": 75.82476240840325, + "grimoireHeight": 75.82519186689818, "tailwindHeight": 85.0, "grimoireValue": "5.05 MB", "tailwindValue": "5.66 MB", - "grimoireRawValue": 5172.63, + "grimoireRawValue": 5172.66, "tailwindRawValue": 5798.54 }, { @@ -83,36 +83,37 @@ "chartTitle": "Processing Speed", "chartSubtitle": "Number of utility classes processed per second (higher is better)", "chartId": "chart_classes_per_second", - "highlightText": "5.0x faster", + "highlightText": "7.8x faster", "grimoireHeight": 85.0, - "tailwindHeight": 16.860612448260003, - "grimoireValue": "190684.63 classes/s", - "tailwindValue": "37824.23 classes/s", - "grimoireRawValue": 190684.63, - "tailwindRawValue": 37824.23 + "tailwindHeight": 10.92008692829281, + "grimoireValue": "332246.82 classes/s", + "tailwindValue": "42684.28 classes/s", + "grimoireRawValue": 332246.82, + "tailwindRawValue": 42684.28 }, { "title": "Grimoire CSS vs Tailwind CSS - Memory Efficiency", "chartTitle": "Memory Efficiency", "chartSubtitle": "Number of utility classes processed per MB of memory (higher is better)", "chartId": "chart_memory_efficiency", - "highlightText": "3.1x more efficient", + "highlightText": "1.2x more efficient", "grimoireHeight": 85.0, - "tailwindHeight": 27.400353292870733, - "grimoireValue": "3597.08 classes/MB", - "tailwindValue": "1159.54 classes/MB", - "grimoireRawValue": 3597.08, - "tailwindRawValue": 1159.54 + "tailwindHeight": 71.67148088925569, + "grimoireValue": "1566.06 classes/MB", + "tailwindValue": "1320.49 classes/MB", + "grimoireRawValue": 1566.06, + "tailwindRawValue": 1320.49 } ], "metadata": { - "timestamp": 1742724888.643092, - "timestamp_human": "2025-03-23 11:14:48", + "timestamp": 1766795065.6670752, + "timestamp_human": "2025-12-27 01:24:25", "system": { - "os": "Darwin 24.3.0", + "os": "Darwin 25.2.0", "cpu": "arm", "cores": "12 physical, 12 logical", - "memory": "32.0 GB" + "memory": "32.0 GB", + "grimoire_css_jobs": 4 } } } \ No newline at end of file diff --git a/benchmark/results/result_20251227_012441.json b/benchmark/results/result_20251227_012441.json new file mode 100644 index 0000000..32eb8af --- /dev/null +++ b/benchmark/results/result_20251227_012441.json @@ -0,0 +1,140 @@ +{ + "system_info": { + "os": { + "name": "Darwin", + "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020", + "release": "25.2.0" + }, + "benchmark": { + "grimoire_css_jobs": 6 + }, + "cpu": { + "name": "arm", + "cores_logical": 12, + "cores_physical": 12 + }, + "memory": { + "total_gb": 32.0 + }, + "psutil_version": "7.0.0", + "git": { + "sha": "8f60e2643076e9065a21408a98d0da64b5aea71b", + "dirty": true + }, + "python_version": "3.14.2", + "timestamp": 1766795081.854123, + "timestamp_human": "2025-12-27 01:24:41" + }, + "grimoire": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5296800, + "total_size_kb": 5172.65625, + "avg_size_bytes": 529680.0, + "avg_size_kb": 517.265625 + }, + "process": { + "memory": { + "peak_bytes": 151404544, + "peak_mb": 144.390625, + "avg_bytes": 89052997.81818181, + "avg_mb": 84.92755681818181, + "measurement": "rss", + "rss_peak_bytes": 151404544, + "rss_peak_mb": 144.390625, + "rss_avg_bytes": 89052997.81818181, + "rss_avg_mb": 84.92755681818181, + "uss_peak_bytes": 94502912, + "uss_peak_mb": 90.125, + "uss_avg_bytes": 49753526.85714286, + "uss_avg_mb": 47.448660714285715, + "uss_is_complete": true, + "uss_partial_peak_bytes": 94502912, + "uss_partial_peak_mb": 90.125, + "uss_partial_avg_bytes": 49753526.85714286, + "uss_partial_avg_mb": 47.448660714285715, + "uss_coverage_avg": 1.0, + "uss_process_count_avg": 1, + "uss_process_count_max": 1, + "uss_available_count_avg": 1, + "uss_available_count_max": 1, + "std_dev_mb": 53.88139312427017 + }, + "cpu": { + "user_time": 0.922690456, + "system_time": 4.8702180749999995, + "total_time": 5.792908530999999 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5296800, + "write_mb": 5.051422119140625 + } + }, + "throughput": { + "build_time_seconds": 1.3034098148345947, + "classes_per_second": 306891.9655563293, + "memory_efficiency": 2770.3045124986475, + "bytes_processed_per_second": 37967552.05981016, + "bytes_generated_per_second": 4063802.4508601497 + }, + "exit_code": 0, + "success": true, + "run": { + "executable": "../target/release/grimoire_css", + "argv": [ + "../target/release/grimoire_css", + "build" + ] + } + }, + "tailwind": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5937710, + "total_size_kb": 5798.544921875, + "avg_size_bytes": 593771.0, + "avg_size_kb": 579.8544921875 + }, + "process": { + "memory": { + "peak_bytes": 321994752, + "peak_mb": 307.078125, + "avg_bytes": 173116153.8164251, + "avg_mb": 165.09642964975845, + "std_dev_mb": 71.08418887937042 + }, + "cpu": { + "user_time": 8.819792976, + "system_time": 41.545980609, + "total_time": 50.365773585 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5937710, + "write_mb": 5.662641525268555 + } + }, + "throughput": { + "build_time_seconds": 9.26749849319458, + "classes_per_second": 43162.240629846034, + "memory_efficiency": 1302.6196509438762, + "bytes_processed_per_second": 5339874.620572109, + "bytes_generated_per_second": 640702.558987223 + }, + "exit_code": 0, + "success": true + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012441.txt b/benchmark/results/result_20251227_012441.txt new file mode 100644 index 0000000..b59c6ab --- /dev/null +++ b/benchmark/results/result_20251227_012441.txt @@ -0,0 +1,126 @@ + + +================================================================================================================================================================ +GRIMOIRE PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:41 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 6 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 1.30 s +Processing Speed: 306891.97 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 144.39 MB +Average Memory: 84.93 MB +Peak RSS: 144.39 MB +Average RSS: 84.93 MB +Peak USS: 90.12 MB +Average USS: 47.45 MB +Primary Memory Metric: rss +Memory Efficiency: 2770.30 classes/MB +Memory Stability (Std Dev): 53.88 MB +Memory per Class: 378.51 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 922.69 ms +System CPU Time: 4.87 s +Total CPU Time: 5.79 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.05 MB +Output File Count: 10 +Output Size: 5.05 MB + +================================================================================ +TAILWIND PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:41 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 6 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 9.27 s +Processing Speed: 43162.24 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 307.08 MB +Average Memory: 165.10 MB +Memory Efficiency: 1302.62 classes/MB +Memory Stability (Std Dev): 71.08 MB +Memory per Class: 804.97 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 8.82 s +System CPU Time: 41.55 s +Total CPU Time: 50.37 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.66 MB +Output File Count: 10 +Output Size: 5.66 MB + +================================================================================ +CSS FRAMEWORKS PERFORMANCE COMPARISON +================================================================================ +Generated: 2025-12-27 01:24:41 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 6 + +PERFORMANCE COMPARISON +-------------------------------------------------------------------------------- +Metric | Grimoire CSS | Tailwind CSS | Difference | Ratio (G/T) +------------------------------------------------------------------------------------------------- +Build Time | 1.30 s | 9.27 s | 7.96 s | 7.11x +Classes/sec | 306891.97 | 43162.24 | 263729.72 | 7.11x +Peak Memory | 144.39 MB | 307.08 MB | 162.69 MB | 2.13x +Memory Efficiency | 2770.30 classes/MB | 1302.62 classes/MB | 1467.68 classes/MB | 2.13x +Output Size | 5.05 MB | 5.66 MB | 625.89 KB | 1.12x + +Notes: +- Build Time: lower is better +- Classes/sec: higher is better +- Peak Memory: lower is better +- Memory Efficiency: higher is better +- Output Size: lower is better \ No newline at end of file diff --git a/benchmark/results/result_20251227_012441_pretty.json b/benchmark/results/result_20251227_012441_pretty.json new file mode 100644 index 0000000..06d0507 --- /dev/null +++ b/benchmark/results/result_20251227_012441_pretty.json @@ -0,0 +1,119 @@ +{ + "charts": [ + { + "title": "Grimoire CSS vs Tailwind CSS - Build Time", + "chartTitle": "Build Time", + "chartSubtitle": "Total time taken to compile CSS (lower is better)", + "chartId": "chart_time", + "highlightText": "7.1x faster", + "grimoireHeight": 11.954664394312775, + "tailwindHeight": 85.0, + "grimoireValue": "1.30s", + "tailwindValue": "9.27s", + "grimoireRawValue": 1.3, + "tailwindRawValue": 9.27 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Peak Memory Usage", + "chartTitle": "Peak Memory Usage", + "chartSubtitle": "Maximum memory consumed during compilation (lower is better)", + "chartId": "chart_peak_memory", + "highlightText": "2.1x less", + "grimoireHeight": 39.96768941128581, + "tailwindHeight": 85.0, + "grimoireValue": "144.39 MB", + "tailwindValue": "307.08 MB", + "grimoireRawValue": 144.39, + "tailwindRawValue": 307.08 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Average Memory Usage", + "chartTitle": "Average Memory Usage", + "chartSubtitle": "Average memory consumed during compilation (lower is better)", + "chartId": "chart_avg_memory", + "highlightText": "1.9x less", + "grimoireHeight": 43.725005712478264, + "tailwindHeight": 85.0, + "grimoireValue": "84.93 MB", + "tailwindValue": "165.1 MB", + "grimoireRawValue": 84.93, + "tailwindRawValue": 165.1 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (User Time)", + "chartTitle": "CPU Usage (User Time)", + "chartSubtitle": "CPU time spent in user mode during compilation (lower is better)", + "chartId": "chart_cpu_user", + "highlightText": "9.6x less", + "grimoireHeight": 8.892350304980672, + "tailwindHeight": 85.0, + "grimoireValue": "922.69ms", + "tailwindValue": "8.82s", + "grimoireRawValue": 0.92, + "tailwindRawValue": 8.82 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (System Time)", + "chartTitle": "CPU Usage (System Time)", + "chartSubtitle": "CPU time spent in system mode during compilation (lower is better)", + "chartId": "chart_cpu_system", + "highlightText": "8.5x less", + "grimoireHeight": 9.96410555983659, + "tailwindHeight": 85.0, + "grimoireValue": "4.87s", + "tailwindValue": "41.55s", + "grimoireRawValue": 4.87, + "tailwindRawValue": 41.55 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Output Size", + "chartTitle": "Output Size", + "chartSubtitle": "Size of the generated CSS file (lower is better)", + "chartId": "chart_output", + "highlightText": "1.1x less", + "grimoireHeight": 75.82519186689818, + "tailwindHeight": 85.0, + "grimoireValue": "5.05 MB", + "tailwindValue": "5.66 MB", + "grimoireRawValue": 5172.66, + "tailwindRawValue": 5798.54 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Processing Speed", + "chartTitle": "Processing Speed", + "chartSubtitle": "Number of utility classes processed per second (higher is better)", + "chartId": "chart_classes_per_second", + "highlightText": "7.1x faster", + "grimoireHeight": 85.0, + "tailwindHeight": 11.954664394312775, + "grimoireValue": "306891.97 classes/s", + "tailwindValue": "43162.24 classes/s", + "grimoireRawValue": 306891.97, + "tailwindRawValue": 43162.24 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Memory Efficiency", + "chartTitle": "Memory Efficiency", + "chartSubtitle": "Number of utility classes processed per MB of memory (higher is better)", + "chartId": "chart_memory_efficiency", + "highlightText": "2.1x more efficient", + "grimoireHeight": 85.0, + "tailwindHeight": 39.967689411285804, + "grimoireValue": "2770.30 classes/MB", + "tailwindValue": "1302.62 classes/MB", + "grimoireRawValue": 2770.3, + "tailwindRawValue": 1302.62 + } + ], + "metadata": { + "timestamp": 1766795081.854123, + "timestamp_human": "2025-12-27 01:24:41", + "system": { + "os": "Darwin 25.2.0", + "cpu": "arm", + "cores": "12 physical, 12 logical", + "memory": "32.0 GB", + "grimoire_css_jobs": 6 + } + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012458.json b/benchmark/results/result_20251227_012458.json new file mode 100644 index 0000000..6637b36 --- /dev/null +++ b/benchmark/results/result_20251227_012458.json @@ -0,0 +1,140 @@ +{ + "system_info": { + "os": { + "name": "Darwin", + "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020", + "release": "25.2.0" + }, + "benchmark": { + "grimoire_css_jobs": 8 + }, + "cpu": { + "name": "arm", + "cores_logical": 12, + "cores_physical": 12 + }, + "memory": { + "total_gb": 32.0 + }, + "psutil_version": "7.0.0", + "git": { + "sha": "8f60e2643076e9065a21408a98d0da64b5aea71b", + "dirty": true + }, + "python_version": "3.14.2", + "timestamp": 1766795098.173505, + "timestamp_human": "2025-12-27 01:24:58" + }, + "grimoire": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5296800, + "total_size_kb": 5172.65625, + "avg_size_bytes": 529680.0, + "avg_size_kb": 517.265625 + }, + "process": { + "memory": { + "peak_bytes": 181878784, + "peak_mb": 173.453125, + "avg_bytes": 90574532.92307693, + "avg_mb": 86.37860576923077, + "measurement": "rss", + "rss_peak_bytes": 181878784, + "rss_peak_mb": 173.453125, + "rss_avg_bytes": 90574532.92307693, + "rss_avg_mb": 86.37860576923077, + "uss_peak_bytes": 108969984, + "uss_peak_mb": 103.921875, + "uss_avg_bytes": 28411326.35897436, + "uss_avg_mb": 27.095152243589745, + "uss_is_complete": true, + "uss_partial_peak_bytes": 108969984, + "uss_partial_peak_mb": 103.921875, + "uss_partial_avg_bytes": 28411326.35897436, + "uss_partial_avg_mb": 27.095152243589745, + "uss_coverage_avg": 1.0, + "uss_process_count_avg": 1, + "uss_process_count_max": 1, + "uss_available_count_avg": 1, + "uss_available_count_max": 1, + "std_dev_mb": 55.383478271518264 + }, + "cpu": { + "user_time": 0.92461585, + "system_time": 4.9346328239999995, + "total_time": 5.859248674 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5296800, + "write_mb": 5.051422119140625 + } + }, + "throughput": { + "build_time_seconds": 1.3175179958343506, + "classes_per_second": 303605.7201986728, + "memory_efficiency": 2306.1331411584542, + "bytes_processed_per_second": 37560989.797836475, + "bytes_generated_per_second": 4020286.6425711866 + }, + "exit_code": 0, + "success": true, + "run": { + "executable": "../target/release/grimoire_css", + "argv": [ + "../target/release/grimoire_css", + "build" + ] + } + }, + "tailwind": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5937710, + "total_size_kb": 5798.544921875, + "avg_size_bytes": 593771.0, + "avg_size_kb": 579.8544921875 + }, + "process": { + "memory": { + "peak_bytes": 319864832, + "peak_mb": 305.046875, + "avg_bytes": 173194673.23076922, + "avg_mb": 165.17131159855768, + "std_dev_mb": 71.40594682739257 + }, + "cpu": { + "user_time": 8.87050304, + "system_time": 41.328194268000004, + "total_time": 50.19869730800001 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5937710, + "write_mb": 5.662641525268555 + } + }, + "throughput": { + "build_time_seconds": 9.233954429626465, + "classes_per_second": 43319.035527900174, + "memory_efficiency": 1311.2935511960252, + "bytes_processed_per_second": 5359272.712157177, + "bytes_generated_per_second": 643030.0306604605 + }, + "exit_code": 0, + "success": true + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012458.txt b/benchmark/results/result_20251227_012458.txt new file mode 100644 index 0000000..66271d3 --- /dev/null +++ b/benchmark/results/result_20251227_012458.txt @@ -0,0 +1,126 @@ + + +================================================================================================================================================================ +GRIMOIRE PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:58 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 8 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 1.32 s +Processing Speed: 303605.72 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 173.45 MB +Average Memory: 86.38 MB +Peak RSS: 173.45 MB +Average RSS: 86.38 MB +Peak USS: 103.92 MB +Average USS: 27.10 MB +Primary Memory Metric: rss +Memory Efficiency: 2306.13 classes/MB +Memory Stability (Std Dev): 55.38 MB +Memory per Class: 454.69 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 924.62 ms +System CPU Time: 4.93 s +Total CPU Time: 5.86 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.05 MB +Output File Count: 10 +Output Size: 5.05 MB + +================================================================================ +TAILWIND PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:24:58 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 8 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 9.23 s +Processing Speed: 43319.04 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 305.05 MB +Average Memory: 165.17 MB +Memory Efficiency: 1311.29 classes/MB +Memory Stability (Std Dev): 71.41 MB +Memory per Class: 799.65 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 8.87 s +System CPU Time: 41.33 s +Total CPU Time: 50.20 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.66 MB +Output File Count: 10 +Output Size: 5.66 MB + +================================================================================ +CSS FRAMEWORKS PERFORMANCE COMPARISON +================================================================================ +Generated: 2025-12-27 01:24:58 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 8 + +PERFORMANCE COMPARISON +-------------------------------------------------------------------------------- +Metric | Grimoire CSS | Tailwind CSS | Difference | Ratio (G/T) +------------------------------------------------------------------------------------------------ +Build Time | 1.32 s | 9.23 s | 7.92 s | 7.01x +Classes/sec | 303605.72 | 43319.04 | 260286.68 | 7.01x +Peak Memory | 173.45 MB | 305.05 MB | 131.59 MB | 1.76x +Memory Efficiency | 2306.13 classes/MB | 1311.29 classes/MB | 994.84 classes/MB | 1.76x +Output Size | 5.05 MB | 5.66 MB | 625.89 KB | 1.12x + +Notes: +- Build Time: lower is better +- Classes/sec: higher is better +- Peak Memory: lower is better +- Memory Efficiency: higher is better +- Output Size: lower is better \ No newline at end of file diff --git a/benchmark/results/result_20251227_012458_pretty.json b/benchmark/results/result_20251227_012458_pretty.json new file mode 100644 index 0000000..ff7e108 --- /dev/null +++ b/benchmark/results/result_20251227_012458_pretty.json @@ -0,0 +1,119 @@ +{ + "charts": [ + { + "title": "Grimoire CSS vs Tailwind CSS - Build Time", + "chartTitle": "Build Time", + "chartSubtitle": "Total time taken to compile CSS (lower is better)", + "chartId": "chart_time", + "highlightText": "7.0x faster", + "grimoireHeight": 12.12795996551718, + "tailwindHeight": 85.0, + "grimoireValue": "1.32s", + "tailwindValue": "9.23s", + "grimoireRawValue": 1.32, + "tailwindRawValue": 9.23 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Peak Memory Usage", + "chartTitle": "Peak Memory Usage", + "chartSubtitle": "Maximum memory consumed during compilation (lower is better)", + "chartId": "chart_peak_memory", + "highlightText": "1.8x less", + "grimoireHeight": 48.331967423039494, + "tailwindHeight": 85.0, + "grimoireValue": "173.45 MB", + "tailwindValue": "305.05 MB", + "grimoireRawValue": 173.45, + "tailwindRawValue": 305.05 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Average Memory Usage", + "chartTitle": "Average Memory Usage", + "chartSubtitle": "Average memory consumed during compilation (lower is better)", + "chartId": "chart_avg_memory", + "highlightText": "1.9x less", + "grimoireHeight": 44.451917341611335, + "tailwindHeight": 85.0, + "grimoireValue": "86.38 MB", + "tailwindValue": "165.17 MB", + "grimoireRawValue": 86.38, + "tailwindRawValue": 165.17 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (User Time)", + "chartTitle": "CPU Usage (User Time)", + "chartSubtitle": "CPU time spent in user mode during compilation (lower is better)", + "chartId": "chart_cpu_user", + "highlightText": "9.6x less", + "grimoireHeight": 8.859965088293349, + "tailwindHeight": 85.0, + "grimoireValue": "924.62ms", + "tailwindValue": "8.87s", + "grimoireRawValue": 0.92, + "tailwindRawValue": 8.87 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (System Time)", + "chartTitle": "CPU Usage (System Time)", + "chartSubtitle": "CPU time spent in system mode during compilation (lower is better)", + "chartId": "chart_cpu_system", + "highlightText": "8.4x less", + "grimoireHeight": 10.149095489632145, + "tailwindHeight": 85.0, + "grimoireValue": "4.93s", + "tailwindValue": "41.33s", + "grimoireRawValue": 4.93, + "tailwindRawValue": 41.33 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Output Size", + "chartTitle": "Output Size", + "chartSubtitle": "Size of the generated CSS file (lower is better)", + "chartId": "chart_output", + "highlightText": "1.1x less", + "grimoireHeight": 75.82519186689818, + "tailwindHeight": 85.0, + "grimoireValue": "5.05 MB", + "tailwindValue": "5.66 MB", + "grimoireRawValue": 5172.66, + "tailwindRawValue": 5798.54 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Processing Speed", + "chartTitle": "Processing Speed", + "chartSubtitle": "Number of utility classes processed per second (higher is better)", + "chartId": "chart_classes_per_second", + "highlightText": "7.0x faster", + "grimoireHeight": 85.0, + "tailwindHeight": 12.127959965517183, + "grimoireValue": "303605.72 classes/s", + "tailwindValue": "43319.04 classes/s", + "grimoireRawValue": 303605.72, + "tailwindRawValue": 43319.04 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Memory Efficiency", + "chartTitle": "Memory Efficiency", + "chartSubtitle": "Number of utility classes processed per MB of memory (higher is better)", + "chartId": "chart_memory_efficiency", + "highlightText": "1.8x more efficient", + "grimoireHeight": 85.0, + "tailwindHeight": 48.331967423039494, + "grimoireValue": "2306.13 classes/MB", + "tailwindValue": "1311.29 classes/MB", + "grimoireRawValue": 2306.13, + "tailwindRawValue": 1311.29 + } + ], + "metadata": { + "timestamp": 1766795098.173505, + "timestamp_human": "2025-12-27 01:24:58", + "system": { + "os": "Darwin 25.2.0", + "cpu": "arm", + "cores": "12 physical, 12 logical", + "memory": "32.0 GB", + "grimoire_css_jobs": 8 + } + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012514.json b/benchmark/results/result_20251227_012514.json new file mode 100644 index 0000000..b3a8310 --- /dev/null +++ b/benchmark/results/result_20251227_012514.json @@ -0,0 +1,140 @@ +{ + "system_info": { + "os": { + "name": "Darwin", + "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020", + "release": "25.2.0" + }, + "benchmark": { + "grimoire_css_jobs": 10 + }, + "cpu": { + "name": "arm", + "cores_logical": 12, + "cores_physical": 12 + }, + "memory": { + "total_gb": 32.0 + }, + "psutil_version": "7.0.0", + "git": { + "sha": "8f60e2643076e9065a21408a98d0da64b5aea71b", + "dirty": true + }, + "python_version": "3.14.2", + "timestamp": 1766795114.513845, + "timestamp_human": "2025-12-27 01:25:14" + }, + "grimoire": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5296800, + "total_size_kb": 5172.65625, + "avg_size_bytes": 529680.0, + "avg_size_kb": 517.265625 + }, + "process": { + "memory": { + "peak_bytes": 271646720, + "peak_mb": 259.0625, + "avg_bytes": 63904206.451612905, + "avg_mb": 60.94380040322581, + "measurement": "rss", + "rss_peak_bytes": 271646720, + "rss_peak_mb": 259.0625, + "rss_avg_bytes": 63904206.451612905, + "rss_avg_mb": 60.94380040322581, + "uss_peak_bytes": 195018752, + "uss_peak_mb": 185.984375, + "uss_avg_bytes": 53297812.64516129, + "uss_avg_mb": 50.828755040322584, + "uss_is_complete": true, + "uss_partial_peak_bytes": 195018752, + "uss_partial_peak_mb": 185.984375, + "uss_partial_avg_bytes": 53297812.64516129, + "uss_partial_avg_mb": 50.828755040322584, + "uss_coverage_avg": 0.992, + "uss_process_count_avg": 1, + "uss_process_count_max": 1, + "uss_available_count_avg": 0.992, + "uss_available_count_max": 1, + "std_dev_mb": 41.60914957602498 + }, + "cpu": { + "user_time": 1.144358405, + "system_time": 21.18281145, + "total_time": 22.327169854999998 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5296800, + "write_mb": 5.051422119140625 + } + }, + "throughput": { + "build_time_seconds": 2.486582040786743, + "classes_per_second": 160865.79627730278, + "memory_efficiency": 1544.052110977081, + "bytes_processed_per_second": 19901728.231071133, + "bytes_generated_per_second": 2130152.922010213 + }, + "exit_code": 0, + "success": true, + "run": { + "executable": "../target/release/grimoire_css", + "argv": [ + "../target/release/grimoire_css", + "build" + ] + } + }, + "tailwind": { + "input": { + "unique_class_count": 400006, + "total_input_size_bytes": 49487280, + "file_count": 100000 + }, + "output": { + "file_count": 10, + "total_size_bytes": 5937710, + "total_size_kb": 5798.544921875, + "avg_size_bytes": 593771.0, + "avg_size_kb": 579.8544921875 + }, + "process": { + "memory": { + "peak_bytes": 320389120, + "peak_mb": 305.546875, + "avg_bytes": 172127066.2095238, + "avg_mb": 164.15316220238094, + "std_dev_mb": 70.51225332667009 + }, + "cpu": { + "user_time": 8.663403975, + "system_time": 42.962273485000004, + "total_time": 51.62567746 + }, + "io": { + "read_bytes": 49487280, + "read_mb": 47.19474792480469, + "write_bytes": 5937710, + "write_mb": 5.662641525268555 + } + }, + "throughput": { + "build_time_seconds": 9.339990854263306, + "classes_per_second": 42827.23679728384, + "memory_efficiency": 1309.1477371516237, + "bytes_processed_per_second": 5298429.171096155, + "bytes_generated_per_second": 635729.7445628322 + }, + "exit_code": 0, + "success": true + } +} \ No newline at end of file diff --git a/benchmark/results/result_20251227_012514.txt b/benchmark/results/result_20251227_012514.txt new file mode 100644 index 0000000..191c067 --- /dev/null +++ b/benchmark/results/result_20251227_012514.txt @@ -0,0 +1,126 @@ + + +================================================================================================================================================================ +GRIMOIRE PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:25:14 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 10 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 2.49 s +Processing Speed: 160865.80 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 259.06 MB +Average Memory: 60.94 MB +Peak RSS: 259.06 MB +Average RSS: 60.94 MB +Peak USS: 185.98 MB +Average USS: 50.83 MB +Primary Memory Metric: rss +Memory Efficiency: 1544.05 classes/MB +Memory Stability (Std Dev): 41.61 MB +Memory per Class: 679.11 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 1.14 s +System CPU Time: 21.18 s +Total CPU Time: 22.33 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.05 MB +Output File Count: 10 +Output Size: 5.05 MB + +================================================================================ +TAILWIND PERFORMANCE BENCHMARK REPORT +================================================================================ +Generated: 2025-12-27 01:25:14 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 10 + +INPUT SUMMARY +-------------------------------------------------------------------------------- +Unique Utility Classes: 400006 +Total Input Size: 47.19 MB +Input HTML Files: 100000 + +PERFORMANCE METRICS +-------------------------------------------------------------------------------- +Build Time: 9.34 s +Processing Speed: 42827.24 classes/second + +MEMORY USAGE +-------------------------------------------------------------------------------- +Peak Memory: 305.55 MB +Average Memory: 164.15 MB +Memory Efficiency: 1309.15 classes/MB +Memory Stability (Std Dev): 70.51 MB +Memory per Class: 800.96 bytes/class + +CPU USAGE +-------------------------------------------------------------------------------- +User CPU Time: 8.66 s +System CPU Time: 42.96 s +Total CPU Time: 51.63 s + +I/O & OUTPUT METRICS +-------------------------------------------------------------------------------- +Total Read: 47.19 MB +Total Written: 5.66 MB +Output File Count: 10 +Output Size: 5.66 MB + +================================================================================ +CSS FRAMEWORKS PERFORMANCE COMPARISON +================================================================================ +Generated: 2025-12-27 01:25:14 + +SYSTEM INFORMATION +-------------------------------------------------------------------------------- +OS: Darwin 25.2.0 +CPU: arm +Cores: 12 physical, 12 logical +Memory: 32.0 GB +GRIMOIRE_CSS_JOBS: 10 + +PERFORMANCE COMPARISON +-------------------------------------------------------------------------------- +Metric | Grimoire CSS | Tailwind CSS | Difference | Ratio (G/T) +------------------------------------------------------------------------------------------------ +Build Time | 2.49 s | 9.34 s | 6.85 s | 3.76x +Classes/sec | 160865.80 | 42827.24 | 118038.56 | 3.76x +Peak Memory | 259.06 MB | 305.55 MB | 46.48 MB | 1.18x +Memory Efficiency | 1544.05 classes/MB | 1309.15 classes/MB | 234.90 classes/MB | 1.18x +Output Size | 5.05 MB | 5.66 MB | 625.89 KB | 1.12x + +Notes: +- Build Time: lower is better +- Classes/sec: higher is better +- Peak Memory: lower is better +- Memory Efficiency: higher is better +- Output Size: lower is better \ No newline at end of file diff --git a/benchmark/results/result_20251227_012514_pretty.json b/benchmark/results/result_20251227_012514_pretty.json new file mode 100644 index 0000000..db5c9ea --- /dev/null +++ b/benchmark/results/result_20251227_012514_pretty.json @@ -0,0 +1,119 @@ +{ + "charts": [ + { + "title": "Grimoire CSS vs Tailwind CSS - Build Time", + "chartTitle": "Build Time", + "chartSubtitle": "Total time taken to compile CSS (lower is better)", + "chartId": "chart_time", + "highlightText": "3.8x faster", + "grimoireHeight": 22.629516106045934, + "tailwindHeight": 85.0, + "grimoireValue": "2.49s", + "tailwindValue": "9.34s", + "grimoireRawValue": 2.49, + "tailwindRawValue": 9.34 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Peak Memory Usage", + "chartTitle": "Peak Memory Usage", + "chartSubtitle": "Maximum memory consumed during compilation (lower is better)", + "chartId": "chart_peak_memory", + "highlightText": "1.2x less", + "grimoireHeight": 72.06852467399642, + "tailwindHeight": 85.0, + "grimoireValue": "259.06 MB", + "tailwindValue": "305.55 MB", + "grimoireRawValue": 259.06, + "tailwindRawValue": 305.55 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Average Memory Usage", + "chartTitle": "Average Memory Usage", + "chartSubtitle": "Average memory consumed during compilation (lower is better)", + "chartId": "chart_avg_memory", + "highlightText": "2.7x less", + "grimoireHeight": 31.557253998479826, + "tailwindHeight": 85.0, + "grimoireValue": "60.94 MB", + "tailwindValue": "164.15 MB", + "grimoireRawValue": 60.94, + "tailwindRawValue": 164.15 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (User Time)", + "chartTitle": "CPU Usage (User Time)", + "chartSubtitle": "CPU time spent in user mode during compilation (lower is better)", + "chartId": "chart_cpu_user", + "highlightText": "7.6x less", + "grimoireHeight": 11.227741971365244, + "tailwindHeight": 85.0, + "grimoireValue": "1.14s", + "tailwindValue": "8.66s", + "grimoireRawValue": 1.14, + "tailwindRawValue": 8.66 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - CPU Usage (System Time)", + "chartTitle": "CPU Usage (System Time)", + "chartSubtitle": "CPU time spent in system mode during compilation (lower is better)", + "chartId": "chart_cpu_system", + "highlightText": "2.0x less", + "grimoireHeight": 41.90976936727164, + "tailwindHeight": 85.0, + "grimoireValue": "21.18s", + "tailwindValue": "42.96s", + "grimoireRawValue": 21.18, + "tailwindRawValue": 42.96 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Output Size", + "chartTitle": "Output Size", + "chartSubtitle": "Size of the generated CSS file (lower is better)", + "chartId": "chart_output", + "highlightText": "1.1x less", + "grimoireHeight": 75.82519186689818, + "tailwindHeight": 85.0, + "grimoireValue": "5.05 MB", + "tailwindValue": "5.66 MB", + "grimoireRawValue": 5172.66, + "tailwindRawValue": 5798.54 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Processing Speed", + "chartTitle": "Processing Speed", + "chartSubtitle": "Number of utility classes processed per second (higher is better)", + "chartId": "chart_classes_per_second", + "highlightText": "3.8x faster", + "grimoireHeight": 85.0, + "tailwindHeight": 22.629516106045926, + "grimoireValue": "160865.80 classes/s", + "tailwindValue": "42827.24 classes/s", + "grimoireRawValue": 160865.8, + "tailwindRawValue": 42827.24 + }, + { + "title": "Grimoire CSS vs Tailwind CSS - Memory Efficiency", + "chartTitle": "Memory Efficiency", + "chartSubtitle": "Number of utility classes processed per MB of memory (higher is better)", + "chartId": "chart_memory_efficiency", + "highlightText": "1.2x more efficient", + "grimoireHeight": 85.0, + "tailwindHeight": 72.06852467399642, + "grimoireValue": "1544.05 classes/MB", + "tailwindValue": "1309.15 classes/MB", + "grimoireRawValue": 1544.05, + "tailwindRawValue": 1309.15 + } + ], + "metadata": { + "timestamp": 1766795114.513845, + "timestamp_human": "2025-12-27 01:25:14", + "system": { + "os": "Darwin 25.2.0", + "cpu": "arm", + "cores": "12 physical, 12 logical", + "memory": "32.0 GB", + "grimoire_css_jobs": 10 + } + } +} \ No newline at end of file diff --git a/content/about.md b/content/about.md index f302c57..cda7a20 100644 --- a/content/about.md +++ b/content/about.md @@ -295,6 +295,8 @@ This means you’re not limited by file types or formats - you define the `input If you want to use spells outside the traditional `class` or `className` attributes, Grimoire CSS provides a clever solution with its **template syntax**: `g!;`. This syntax lets you wrap your spell in a template, enabling the parser to collect spells from any text-based content. +Template syntax works for scrolls too, by the same rules as spells (including prefixes and modifiers). For example: `g!complex-card=120px_red_100px;`. + Let’s say you have both a classic spell and a templated spell that are essentially the same. Don’t worry - Grimoire CSS is smart enough to combine them into one, as long as it doesn’t affect the CSS cascade. The result? Clean, efficient CSS output like this: ```css @@ -634,6 +636,21 @@ There are only 3 commands you need to know: - **`build`**: Kicks off the build process, parsing all your input files and generating the compiled CSS. If you haven’t already run `init`, the `build` command will handle that for you automatically. - **`shorten`**: Automatically converts all full-length component names in your spells (as defined in your config) to their corresponding shorthand forms. This helps keep your code concise and consistent. Run this command to refactor your files, making your spell syntax as brief as possible without losing clarity or functionality. +**Optional parallel project builds** + +If your config defines multiple independent projects (multiple output files), Grimoire CSS can build them in parallel. + +- Enable by setting the `GRIMOIRE_CSS_JOBS` environment variable to a positive integer (e.g. `4`). +- Default is `1` (fully sequential; same behavior as before). +- Values are capped to the machine’s available parallelism. +- Higher values can reduce wall-clock build time, but may increase peak memory usage due to multiple optimizations running simultaneously. + +Example: + +```bash +GRIMOIRE_CSS_JOBS=4 grimoire_css build +``` + Grimoire CSS’s CLI is built for developers who want power without bloat. It’s direct, no-nonsense, and integrates smoothly into any project or bundler. Here’s a refined version of the remaining parts, keeping the technical depth and making them more engaging and polished: diff --git a/releases/v1.7.0.md b/releases/v1.7.0.md new file mode 100644 index 0000000..dd56f2c --- /dev/null +++ b/releases/v1.7.0.md @@ -0,0 +1,75 @@ +# v1.7.0 Scryforge: Deterministic Scrollcraft + +Grimoire CSS sharpens both **power** and **clarity** with **Scryforge** — a release focused on templated scroll composition, rustc-like diagnostics, and measurable performance wins. Spells now expand more deterministically under templated selectors, errors read like a compiler, and large projects build faster with an opt-in multi-core path. + +## Key Highlights + +- **Scroll Templates Inside `g!…;`**: Use config-defined `scrolls` directly in `g!…;` with variable arguments (e.g. `g!complex-card=120px_red_100px;`) while keeping output deterministic. +- **Prefix-Safe Expansion**: Prefix modifiers like `md__`, focus blocks `{...}`, and `hover:` are preserved by applying them to each expanded spell. +- **Rustc-like Diagnostics**: Errors render with file context + labeled span + optional help text, powered by a structured error model and `miette` diagnostics. +- **Faster Builds (Same Output)**: Reduced redundant work and clone/allocation pressure; optional parallel project builds via `GRIMOIRE_CSS_JOBS`. +- **Better Repro & Contributor UX**: Added a `repro/` sandbox for feature/error scenarios and added `.github/copilot-instructions.md` as an architecture guide. + +## Full Details + +### Scroll Templates Inside `g!…;` + +You can now reference scrolls in templated `g!…;` syntax, including argument passing: + +- Example: `g!complex-card=120px_red_100px;` +- Supports variable-like arguments and prefix modifiers. +- Scroll expansion is flattened into real property spells so generated CSS is emitted under the *outer templated selector*. +- Output remains deterministic and the template-flattening path avoids unnecessary cloning. + +### Prefix Semantics Preserved + +Scrolls expanded under templates keep the semantics that made Grimoire CSS predictable in complex UIs: + +- Responsive prefixes like `md__`. +- Focus blocks using `{...}`. +- Effects like `hover:`. + +These prefixes apply to each expanded spell during flattening so behavior matches user intent. + +### Rustc-like Diagnostics (File + Span) + +This release substantially upgrades the error/reporting system: + +- Introduced `SourceFile` to carry file identity + full content, enabling readable snippets for every error. +- Parsing now tracks spans (`start`, `len`) for each extracted class/spell token and propagates them through spell generation. +- Error model upgraded from plain strings to structured compile errors (message / label / help / span / source). +- Added a diagnostics adapter mapping `GrimoireCssError` to `miette::Diagnostic` for polished CLI output. + +User-facing validation got stricter and clearer: + +- Better errors for malformed function-like values / parentheses (`spell_value_validator`). +- Color function argument validation now returns a proper error instead of being silently ignored. + +### Performance Improvements + Opt-in Parallelism + +Grimoire CSS stays output-stable while getting faster and leaner: + +- Reduced redundant passes and duplicated work. +- Lowered allocation and clone pressure in hot paths. +- Added opt-in parallelism for filesystem builds via `GRIMOIRE_CSS_JOBS`. + +Safe default remains single-threaded; scaling is opt-in based on machine and project size. + +### Repro Sandbox + Architecture Guide + +To improve maintenance and debugging velocity: + +- Added `repro/` containing minimal scenarios for reproducing features and diagnostics. +- Added `.github/copilot-instructions.md` documenting the project’s architecture conventions. + +## Migration Notes + +### For Users + +- **Optional parallel builds**: Set `GRIMOIRE_CSS_JOBS` to enable multi-core builds in filesystem mode. Without it, behavior remains unchanged. +- **Stricter validation**: Invalid color function arguments that were previously ignored now raise proper errors (with spans and help text). + +### For Contributors + +- Prefer adding/using minimal scenarios under `repro/` when improving parser/diagnostics behavior. +- Follow the architecture guide in `.github/copilot-instructions.md` when introducing new commands, core pipeline changes, or infrastructure glue. diff --git a/repro/error_scenarios/.browserslistrc b/repro/error_scenarios/.browserslistrc new file mode 100644 index 0000000..496d1ef --- /dev/null +++ b/repro/error_scenarios/.browserslistrc @@ -0,0 +1 @@ +defaults \ No newline at end of file diff --git a/repro/error_scenarios/1.html b/repro/error_scenarios/1.html new file mode 100644 index 0000000..ccbea38 --- /dev/null +++ b/repro/error_scenarios/1.html @@ -0,0 +1 @@ +
diff --git a/repro/error_scenarios/2.html b/repro/error_scenarios/2.html new file mode 100644 index 0000000..23998ba --- /dev/null +++ b/repro/error_scenarios/2.html @@ -0,0 +1 @@ +
diff --git a/repro/error_scenarios/3.html b/repro/error_scenarios/3.html new file mode 100644 index 0000000..02b496f --- /dev/null +++ b/repro/error_scenarios/3.html @@ -0,0 +1 @@ +
diff --git a/repro/error_scenarios/4.html b/repro/error_scenarios/4.html new file mode 100644 index 0000000..860b17f --- /dev/null +++ b/repro/error_scenarios/4.html @@ -0,0 +1 @@ +
diff --git a/repro/error_scenarios/5.html b/repro/error_scenarios/5.html new file mode 100644 index 0000000..583da83 --- /dev/null +++ b/repro/error_scenarios/5.html @@ -0,0 +1 @@ +
diff --git a/repro/error_scenarios/6.html b/repro/error_scenarios/6.html new file mode 100644 index 0000000..2d70f06 --- /dev/null +++ b/repro/error_scenarios/6.html @@ -0,0 +1 @@ +
diff --git a/repro/error_scenarios/grimoire/config/grimoire.config.json b/repro/error_scenarios/grimoire/config/grimoire.config.json new file mode 100644 index 0000000..568a0b5 --- /dev/null +++ b/repro/error_scenarios/grimoire/config/grimoire.config.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/persevie/grimoire-css/main/src/core/config/config-schema.json", + "variables": null, + "scrolls": [ + { + "name": "complex-card", + "spells": ["h=$", "c=$", "w=$", "hover:c=blue", "md__h=10px"] + }, + { "name": "simple-box", "spells": ["w=50px", "h=50px", "c=green"] } + ], + "projects": [ + { + "projectName": "1", + "inputPaths": ["templated_scroll.html"], + "outputDirPath": ".", + "singleOutputFileName": "1.css" + } + ], + "shared": null, + "critical": null, + "lock": null +} diff --git a/repro/templated_scrolls/grimoire/config/grimoire.config.json b/repro/templated_scrolls/grimoire/config/grimoire.config.json new file mode 100644 index 0000000..cef6ba6 --- /dev/null +++ b/repro/templated_scrolls/grimoire/config/grimoire.config.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/persevie/grimoire-css/main/src/core/config/config-schema.json", + "variables": null, + "scrolls": [ + { + "name": "complex-card", + "spells": ["h=$", "c=$", "w=$", "hover:c=blue", "md__h=10px"] + }, + { "name": "simple-box", "spells": ["w=50px", "h=50px", "c=green"] } + ], + "projects": [ + { + "projectName": "templated_scrolls", + "inputPaths": ["templated_scrolls.html"], + "outputDirPath": ".", + "singleOutputFileName": "templated_scrolls.css" + } + ], + "shared": null, + "critical": null, + "lock": null +} diff --git a/repro/templated_scrolls/templated_scrolls.html b/repro/templated_scrolls/templated_scrolls.html new file mode 100644 index 0000000..51153ab --- /dev/null +++ b/repro/templated_scrolls/templated_scrolls.html @@ -0,0 +1,2 @@ +g!complex-card=120px_red_100px; +g!hover:simple-box; diff --git a/src/commands/shorten.rs b/src/commands/shorten.rs index cf214f6..af0f1f4 100644 --- a/src/commands/shorten.rs +++ b/src/commands/shorten.rs @@ -44,10 +44,10 @@ pub fn shorten(current_dir: &Path) -> Result<(), GrimoireCssError> { let mut changed = false; for part in parts { if let Ok(Some(spell)) = - Spell::new(part, &config.shared_spells, &config.scrolls) + Spell::new(part, &config.shared_spells, &config.scrolls, (0, 0), None) { - if let Some(short) = get_shorten_component(&spell.component) { - let short_part = part.replacen(&spell.component, short, 1); + if let Some(short) = get_shorten_component(spell.component()) { + let short_part = part.replacen(spell.component(), short, 1); if short_part != part { changed = true; } @@ -68,11 +68,15 @@ pub fn shorten(current_dir: &Path) -> Result<(), GrimoireCssError> { replaced_count += count; } } - } else if let Ok(Some(spell)) = - Spell::new(raw_spell, &config.shared_spells, &config.scrolls) - && let Some(short) = get_shorten_component(&spell.component) + } else if let Ok(Some(spell)) = Spell::new( + raw_spell, + &config.shared_spells, + &config.scrolls, + (0, 0), + None, + ) && let Some(short) = get_shorten_component(spell.component()) { - let short_spell = raw_spell.replacen(&spell.component, short, 1); + let short_spell = raw_spell.replacen(spell.component(), short, 1); if raw_spell != &short_spell && new_content.contains(raw_spell) { let count = new_content.matches(raw_spell).count(); new_content = new_content.replace(raw_spell, &short_spell); diff --git a/src/core/css_builder/css_builder_base.rs b/src/core/css_builder/css_builder_base.rs index 8583dfe..2bb6333 100644 --- a/src/core/css_builder/css_builder_base.rs +++ b/src/core/css_builder/css_builder_base.rs @@ -5,6 +5,21 @@ use crate::core::{CssOptimizer, GrimoireCssError, css_generator::CssGenerator, spell::Spell}; use std::collections::HashMap; +#[derive(Debug, Clone, Copy)] +struct PieceRange { + start: usize, + end: usize, + spell_index: usize, +} + +#[derive(Debug, Clone)] +struct MediaEntry { + min_width: Option, + start: usize, + end: usize, + spell_index: usize, +} + /// Core CSS builder that handles spell compilation and optimization pub struct CssBuilder<'a> { css_generator: CssGenerator<'a>, @@ -49,22 +64,105 @@ impl<'a> CssBuilder<'a> { /// # Errors /// /// Returns a `GrimoireCSSError` if CSS generation fails. + #[allow(dead_code)] pub fn combine_spells_to_css(&self, spells: &[Spell]) -> Result, GrimoireCssError> { - let mut base_rules = Vec::new(); - let mut media_queries = Vec::new(); + let (raw_css, pieces) = self.build_joined_css_and_pieces(spells)?; + self.validate_or_isolate(spells, &raw_css, &pieces)?; + Ok(pieces + .iter() + .map(|p| raw_css[p.start..p.end].to_string()) + .collect()) + } + + /// Memory-efficient variant that returns a single joined CSS string. + pub fn combine_spells_to_css_string( + &self, + spells: &[Spell], + ) -> Result { + let (raw_css, pieces) = self.build_joined_css_and_pieces(spells)?; + self.validate_or_isolate(spells, &raw_css, &pieces)?; + Ok(raw_css) + } + + /// Builds and returns optimized CSS in one step. + /// + /// This avoids the common `validate()` then `optimize()` double-parse on the success path. + /// On failure, it still performs rule isolation to produce a precise, spell-linked error. + pub fn combine_spells_to_optimized_css_string( + &self, + spells: &[Spell], + ) -> Result { + let (raw_css, pieces) = self.build_joined_css_and_pieces(spells)?; + + match self.optimizer.optimize(&raw_css) { + Ok(css) => Ok(css), + Err(optimize_err) => match self.validate_or_isolate(spells, &raw_css, &pieces) { + // Optimization may fail even if parsing succeeds (e.g. minify stage). + Ok(()) => Err(optimize_err), + Err(isolated_err) => Err(isolated_err), + }, + } + } + + /// Optimizes and minifies CSS. + /// + /// # Arguments + /// + /// * `raw_css` - Raw CSS string to optimize. + /// + /// # Returns + /// + /// Optimized and minified CSS string. + pub fn optimize_css(&self, raw_css: &str) -> Result { + self.optimizer.optimize(raw_css) + } + + fn create_compile_error(&self, spell: &Spell, error: GrimoireCssError) -> GrimoireCssError { + GrimoireCssError::CompileError { + message: format!("Invalid CSS generated: {}", error), + span: spell.span, + label: "This spell generated invalid CSS".to_string(), + help: Some( + "This usually means the spell value is not valid CSS after Grimoire transformations.\n\ +If you intended spaces inside a value, encode them as '_' (underscores)." + .to_string(), + ), + source_file: spell.source.clone(), + } + } + + fn build_joined_css_and_pieces( + &self, + spells: &[Spell], + ) -> Result<(String, Vec), GrimoireCssError> { + use once_cell::sync::Lazy; + + static MIN_WIDTH_RE: Lazy = + Lazy::new(|| regex::Regex::new(r"min-width:\s*(\\d+)").unwrap()); + + fn extract_min_width(re: ®ex::Regex, s: &str) -> Option { + re.captures(s) + .and_then(|cap| cap.get(1)) + .and_then(|m| m.as_str().parse::().ok()) + } - for spell in spells { + let mut base_css = String::new(); + let mut base_pieces: Vec = Vec::new(); + + let mut media_css = String::new(); + let mut media_entries: Vec = Vec::new(); + + for (spell_index, spell) in spells.iter().enumerate() { match &spell.scroll_spells { Some(ss) if !ss.is_empty() => { - let mut local_scroll_css_vec = Vec::new(); - let mut local_scroll_additional_css_vec = Vec::new(); + let mut combined_scroll_css = String::new(); for s in ss { if let Some(css) = self.css_generator.generate_css(s)? { let class_name = self.css_generator.generate_css_class_name( &spell.raw_spell, - &spell.effects, - &spell.focus, + spell.effects(), + spell.focus(), spell.with_template, )?; @@ -74,76 +172,184 @@ impl<'a> CssBuilder<'a> { &css.0, ); - local_scroll_css_vec.push(updated_css); + combined_scroll_css.push_str(&updated_css); if let Some(additional_css) = css.2 { - local_scroll_additional_css_vec.push(additional_css); + let start = base_css.len(); + base_css.push_str(&additional_css); + let end = base_css.len(); + base_pieces.push(PieceRange { + start, + end, + spell_index, + }); } } } - let combined_css = local_scroll_css_vec.join(""); - let wrapped_css = if spell.area.is_empty() { - combined_css + let wrapped_css = if spell.area().is_empty() { + combined_scroll_css } else { self.css_generator - .wrap_base_css_with_media_query(&spell.area, &combined_css) + .wrap_base_css_with_media_query(spell.area(), &combined_scroll_css) }; if wrapped_css.trim_start().starts_with("@media") { - media_queries.push(wrapped_css); + let start = media_css.len(); + media_css.push_str(&wrapped_css); + let end = media_css.len(); + media_entries.push(MediaEntry { + min_width: extract_min_width(&MIN_WIDTH_RE, &wrapped_css), + start, + end, + spell_index, + }); } else { - base_rules.push(wrapped_css); - } - - for add_css in local_scroll_additional_css_vec { - base_rules.push(add_css); + let start = base_css.len(); + base_css.push_str(&wrapped_css); + let end = base_css.len(); + base_pieces.push(PieceRange { + start, + end, + spell_index, + }); } } _ => { if let Some(css) = self.css_generator.generate_css(spell)? { if css.0.trim_start().starts_with("@media") { - media_queries.push(css.0); + let start = media_css.len(); + media_css.push_str(&css.0); + let end = media_css.len(); + media_entries.push(MediaEntry { + min_width: extract_min_width(&MIN_WIDTH_RE, &css.0), + start, + end, + spell_index, + }); } else { - base_rules.push(css.0); + let start = base_css.len(); + base_css.push_str(&css.0); + let end = base_css.len(); + base_pieces.push(PieceRange { + start, + end, + spell_index, + }); } if let Some(additional_css) = css.2 { - base_rules.push(additional_css); + let start = base_css.len(); + base_css.push_str(&additional_css); + let end = base_css.len(); + base_pieces.push(PieceRange { + start, + end, + spell_index, + }); } } } } } - media_queries.sort_by(|a, b| { - fn extract_min_width(s: &str) -> Option { - let re = regex::Regex::new(r"min-width:\s*(\\d+)").unwrap(); - re.captures(s) - .and_then(|cap| cap.get(1)) - .and_then(|m| m.as_str().parse::().ok()) - } - match (extract_min_width(a), extract_min_width(b)) { - (Some(aw), Some(bw)) => aw.cmp(&bw), - (Some(_), None) => std::cmp::Ordering::Less, - (None, Some(_)) => std::cmp::Ordering::Greater, - (None, None) => a.cmp(b), + // Sort media queries by min-width, then by the text itself (stable deterministic output). + media_entries.sort_by(|a, b| match (a.min_width, b.min_width) { + (Some(aw), Some(bw)) => aw.cmp(&bw), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => { + let aslice = &media_css[a.start..a.end]; + let bslice = &media_css[b.start..b.end]; + aslice.cmp(bslice) } }); - base_rules.extend(media_queries); - Ok(base_rules) + + // Final output: base rules, then sorted media queries. + let mut raw_css = base_css; + let mut pieces = base_pieces; + + raw_css.reserve(media_css.len()); + + for m in media_entries { + let start = raw_css.len(); + raw_css.push_str(&media_css[m.start..m.end]); + let end = raw_css.len(); + pieces.push(PieceRange { + start, + end, + spell_index: m.spell_index, + }); + } + + Ok((raw_css, pieces)) } - /// Optimizes and minifies CSS. - /// - /// # Arguments - /// - /// * `raw_css` - Raw CSS string to optimize. - /// - /// # Returns - /// - /// Optimized and minified CSS string. - pub fn optimize_css(&self, raw_css: &str) -> Result { - self.optimizer.optimize(raw_css) + fn validate_or_isolate( + &self, + spells: &[Spell], + raw_css: &str, + pieces: &[PieceRange], + ) -> Result<(), GrimoireCssError> { + if pieces.is_empty() { + return Ok(()); + } + + if let Err(e) = self.optimizer.validate(raw_css) { + if let Some((spell_index, rule_error)) = self.find_first_invalid_piece(raw_css, pieces) + { + return Err(self.create_compile_error(&spells[spell_index], rule_error)); + } + + if let Some(first) = spells.first() { + return Err(self.create_compile_error(first, e)); + } + return Err(e); + } + + Ok(()) + } + + /// Returns the first invalid piece in source order (by spell index), if any. + fn find_first_invalid_piece( + &self, + raw_css: &str, + pieces: &[PieceRange], + ) -> Option<(usize, GrimoireCssError)> { + if pieces.is_empty() { + return None; + } + + // If the entire slice validates, nothing to isolate. + let full_start = pieces.first()?.start; + let full_end = pieces.last()?.end; + if self + .optimizer + .validate(&raw_css[full_start..full_end]) + .is_ok() + { + return None; + } + + if pieces.len() == 1 { + let p = pieces[0]; + let rule_error = self.optimizer.validate(&raw_css[p.start..p.end]).err()?; + return Some((p.spell_index, rule_error)); + } + + let mid = pieces.len() / 2; + let (left, right) = pieces.split_at(mid); + + let left_start = left.first()?.start; + let left_end = left.last()?.end; + if self + .optimizer + .validate(&raw_css[left_start..left_end]) + .is_err() + { + return self.find_first_invalid_piece(raw_css, left); + } + + self.find_first_invalid_piece(raw_css, right) } } diff --git a/src/core/css_builder/css_builder_fs.rs b/src/core/css_builder/css_builder_fs.rs index 4d5fea6..5b28dd1 100644 --- a/src/core/css_builder/css_builder_fs.rs +++ b/src/core/css_builder/css_builder_fs.rs @@ -13,24 +13,29 @@ use crate::{ buffer::add_message, core::{ ConfigFs, ConfigFsCssCustomProperties, CssOptimizer, GrimoireCssError, - build_info::BuildInfo, file_tracker::FileTracker, parser::ParserFs, spell::Spell, + build_info::BuildInfo, file_tracker::FileTracker, parser::ParserFs, + source_file::SourceFile, spell::Spell, }, }; use regex::Regex; use std::{ collections::HashSet, - fs, + env, fs, path::{Path, PathBuf}, + sync::Arc, + thread, }; use super::CssBuilder; +type CriticalCssEntries = Vec<(PathBuf, Arc)>; +type CriticalCssResult = Option; + /// Manages the process of compiling and building CSS files with filesystem persistence. pub struct CssBuilderFs<'a> { css_builder: CssBuilder<'a>, config: &'a ConfigFs, current_dir: &'a Path, - parser: ParserFs, inline_css_regex: Regex, } @@ -52,14 +57,12 @@ impl<'a> CssBuilderFs<'a> { optimizer: &'a O, ) -> Result { let css_builder = CssBuilder::new(optimizer, &config.variables, &config.custom_animations)?; - let parser = ParserFs::new(current_dir); let inline_css_regex = Regex::new(r#"(?s)"#)?; Ok(Self { css_builder, config, current_dir, - parser, inline_css_regex, }) } @@ -72,67 +75,80 @@ impl<'a> CssBuilderFs<'a> { /// /// Returns a `GrimoireCSSError` if any step in the build process fails. pub fn build(&mut self) -> Result<(), GrimoireCssError> { - let mut project_build_info = Vec::new(); + let lock_enabled = self.config.lock.unwrap_or(false); - for project in &self.config.projects { - let project_output_dir_path = project - .output_dir_path - .as_deref() - .map(|d| self.current_dir.join(d)) - .unwrap_or_else(|| self.current_dir.join("grimoire/dist")); + let jobs = Self::jobs_from_env()?; - if let Some(single_output_file_name) = &project.single_output_file_name { - let classes = self - .parser - .collect_classes_single_output(&project.input_paths)?; - let bundle_output_full_path = project_output_dir_path.join(single_output_file_name); + // Only collect output paths when we actually need them for file tracking. + let mut compiled_project_paths: Option> = lock_enabled.then(Vec::new); - let spells = Spell::generate_spells_from_classes( - classes, - &self.config.shared_spells, - &self.config.scrolls, - )?; + if jobs <= 1 || self.config.projects.len() <= 1 { + for project in &self.config.projects { + let outputs = self.build_project(project)?; + if let Some(paths) = &mut compiled_project_paths { + paths.extend(outputs); + } + } + } else { + let mut all_outputs: Vec = Vec::new(); + let this: &CssBuilderFs<'a> = &*self; + + // NOTE: Parallelism is intentionally limited to project-level isolation. Each project + // builds its own parser/builder instances to avoid shared mutable state. + thread::scope(|scope| { + let projects = &self.config.projects; + let chunk_size = projects.len().div_ceil(jobs); + let mut handles = Vec::new(); + + for chunk in projects.chunks(chunk_size) { + handles.push(scope.spawn(move || { + let mut outputs = Vec::new(); + for project in chunk { + outputs.extend(this.build_project(project)?); + } + Ok::<_, GrimoireCssError>(outputs) + })); + } - project_build_info.push(BuildInfo { - file_path: bundle_output_full_path, - spells, - }); - } else { - let classes = self.parser.collect_classes_multiple_output( - &project.input_paths, - &project_output_dir_path, - )?; + for h in handles { + match h.join() { + Ok(Ok(outputs)) => all_outputs.extend(outputs), + Ok(Err(e)) => return Err(e), + Err(_) => { + return Err(GrimoireCssError::InvalidInput( + "Project build thread panicked".to_string(), + )); + } + } + } - for (file_path, classes) in classes { - let spells = Spell::generate_spells_from_classes( - classes, - &self.config.shared_spells, - &self.config.scrolls, - )?; + Ok(()) + })?; - project_build_info.push(BuildInfo { file_path, spells }); - } + if let Some(paths) = &mut compiled_project_paths { + paths.extend(all_outputs); } } - - let compiled_css: Vec<(PathBuf, String)> = self.compile_css(&project_build_info)?; let compiled_shared_css: Option> = self.compile_shared_css()?; - let compiled_critical_css: Option> = self.compile_critical_css()?; - - Self::write_compiled_css(&compiled_css)?; + let compiled_critical_css: CriticalCssResult = self.compile_critical_css()?; if let Some(compiled_shared_css) = &compiled_shared_css { Self::write_compiled_css(compiled_shared_css)?; } // Track file changes if locking is enabled - if self.config.lock.unwrap_or(false) { - let all_compiled_paths = compiled_css.iter().map(|(path, _)| path.as_path()).chain( - compiled_shared_css - .as_ref() - .into_iter() - .flat_map(|css| css.iter().map(|(path, _)| path.as_path())), - ); + if lock_enabled { + let all_compiled_paths = compiled_project_paths + .as_ref() + .expect("compiled_project_paths must be collected when lock is enabled") + .iter() + .map(|p| p.as_path()) + .chain( + compiled_shared_css + .as_ref() + .into_iter() + .flat_map(|css| css.iter().map(|(path, _)| path.as_path())), + ); FileTracker::track(self.current_dir, all_compiled_paths)?; } @@ -157,6 +173,7 @@ impl<'a> CssBuilderFs<'a> { /// # Errors /// /// Returns a `GrimoireCSSError` if spell assembly or CSS optimization fails. + #[allow(dead_code)] fn compile_css( &self, project_build_info: &[BuildInfo], @@ -164,14 +181,9 @@ impl<'a> CssBuilderFs<'a> { let compiled_css: Result, GrimoireCssError> = project_build_info .iter() .map(|build_info| { - let assembled_spells = - self.css_builder.combine_spells_to_css(&build_info.spells)?; - let raw_css = if assembled_spells.len() == 1 { - assembled_spells[0].clone() - } else { - assembled_spells.concat() - }; - let css = self.css_builder.optimize_css(&raw_css)?; + let css = self + .css_builder + .combine_spells_to_optimized_css_string(&build_info.spells)?; Ok((build_info.file_path.clone(), css)) }) .collect(); @@ -263,9 +275,9 @@ impl<'a> CssBuilderFs<'a> { /// # Errors /// /// Returns a `GrimoireCSSError` if CSS composition or optimization fails. - fn compile_critical_css(&self) -> Result>, GrimoireCssError> { + fn compile_critical_css(&self) -> Result { self.config.critical.as_ref().map_or(Ok(None), |critical| { - let mut compiled_critical_css = Vec::new(); + let mut compiled_critical_css: CriticalCssEntries = Vec::new(); for critical_item in critical { if critical_item.file_to_inline_paths.is_empty() { @@ -287,11 +299,12 @@ impl<'a> CssBuilderFs<'a> { } if !composed_css.is_empty() { - let optimized_css = self.css_builder.optimize_css(&composed_css)?; + let optimized_css: Arc = + Arc::from(self.css_builder.optimize_css(&composed_css)?); for path_to_inline in &critical_item.file_to_inline_paths { compiled_critical_css - .push((PathBuf::from(&path_to_inline), optimized_css.clone())); + .push((PathBuf::from(&path_to_inline), Arc::clone(&optimized_css))); } } } @@ -347,7 +360,7 @@ impl<'a> CssBuilderFs<'a> { ) } - /// Composes additional CSS from shared styles. + /// Composes additional (raw, unoptimized) CSS from shared styles. /// /// # Arguments /// @@ -355,7 +368,7 @@ impl<'a> CssBuilderFs<'a> { /// /// # Returns /// - /// Composed and optimized CSS string. + /// Composed (raw) CSS string. /// /// # Errors /// @@ -379,21 +392,27 @@ impl<'a> CssBuilderFs<'a> { ))); } } - } else if let Some(spell) = - Spell::new(item, &self.config.shared_spells, &self.config.scrolls)? - { + } else if let Some(spell) = Spell::new( + item, + &self.config.shared_spells, + &self.config.scrolls, + (0, 0), + None, + )? { spells.push(spell); } } - let assembled_spells = self.css_builder.combine_spells_to_css(&spells)?; - let mut raw_css = assembled_spells.join(""); + let mut raw_css = self.css_builder.combine_spells_to_css_string(&spells)?; if !files_content.is_empty() { - raw_css.push_str(&files_content.join("")); + for contents in files_content { + raw_css.push_str(&contents); + } } - self.css_builder.optimize_css(&raw_css) + // Important: callers are responsible for running optimization exactly once. + Ok(raw_css) } /// Injects critical CSS into HTML files. @@ -407,11 +426,11 @@ impl<'a> CssBuilderFs<'a> { /// Returns a `GrimoireCSSError` if reading or writing HTML files fails. fn inject_critical_css_into_html( &self, - inline_shared_css: &[(PathBuf, String)], + inline_shared_css: &[(PathBuf, Arc)], ) -> Result<(), GrimoireCssError> { for (file_path, css) in inline_shared_css { let path = self.current_dir.join(file_path); - self.embed_critical_css(&path, css)?; + self.embed_critical_css(&path, css.as_ref())?; } Ok(()) @@ -450,11 +469,120 @@ impl<'a> CssBuilderFs<'a> { fs::write(html_file_path, updated_html_content)?; Ok(()) } + + fn build_project( + &self, + project: &'a crate::core::ConfigFsProject, + ) -> Result, GrimoireCssError> { + let project_output_dir_path = project + .output_dir_path + .as_deref() + .map(|d| self.current_dir.join(d)) + .unwrap_or_else(|| self.current_dir.join("grimoire/dist")); + + let parser = ParserFs::new(self.current_dir); + + let mut outputs = Vec::new(); + + if let Some(single_output_file_name) = &project.single_output_file_name { + let parsing_results = parser.collect_classes_single_output(&project.input_paths)?; + let bundle_output_full_path = project_output_dir_path.join(single_output_file_name); + + let mut all_spells = Vec::new(); + for (file_path, classes) in parsing_results { + let source = Arc::new(SourceFile::new_path_only( + Some(file_path.clone()), + file_path.to_string_lossy().to_string(), + )); + let spells = Spell::generate_spells_from_classes( + classes, + &self.config.shared_spells, + &self.config.scrolls, + Some(source), + )?; + + // `ParserFs::collect_classes_single_output` already deduplicates class tokens. + all_spells.extend(spells); + } + + let css = self + .css_builder + .combine_spells_to_optimized_css_string(&all_spells)?; + + Self::create_output_directory_if_needed(&bundle_output_full_path)?; + fs::write(&bundle_output_full_path, css)?; + outputs.push(bundle_output_full_path); + } else { + let mut out_paths = Vec::new(); + parser.for_each_classes_multiple_output( + &project.input_paths, + &project_output_dir_path, + |output_file_path, source_path, classes| { + let source = Arc::new(SourceFile::new_path_only( + Some(source_path.clone()), + source_path.to_string_lossy().to_string(), + )); + let spells = Spell::generate_spells_from_classes( + classes, + &self.config.shared_spells, + &self.config.scrolls, + Some(source), + )?; + + let css = self + .css_builder + .combine_spells_to_optimized_css_string(&spells)?; + Self::create_output_directory_if_needed(&output_file_path)?; + fs::write(&output_file_path, css)?; + out_paths.push(output_file_path); + Ok(()) + }, + )?; + outputs.extend(out_paths); + } + + Ok(outputs) + } + + fn jobs_from_env() -> Result { + match env::var("GRIMOIRE_CSS_JOBS") { + Ok(v) => Self::parse_jobs(&v).map(Self::cap_jobs_to_machine), + Err(env::VarError::NotPresent) => Ok(1), + Err(e) => Err(GrimoireCssError::InvalidInput(format!( + "Failed to read GRIMOIRE_CSS_JOBS: {e}" + ))), + } + } + + fn parse_jobs(raw: &str) -> Result { + let trimmed = raw.trim(); + let jobs: usize = trimmed.parse().map_err(|_| { + GrimoireCssError::InvalidInput(format!( + "Invalid GRIMOIRE_CSS_JOBS value '{trimmed}': expected a positive integer" + )) + })?; + + if jobs == 0 { + return Err(GrimoireCssError::InvalidInput( + "GRIMOIRE_CSS_JOBS must be >= 1".to_string(), + )); + } + + Ok(jobs) + } + + fn cap_jobs_to_machine(requested: usize) -> usize { + let max = thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1); + requested.clamp(1, max) + } } #[cfg(test)] mod tests { use super::*; + use crate::core::ConfigFsCritical; use std::path::Path; struct MockOptimizer; @@ -463,6 +591,10 @@ mod tests { fn optimize(&self, css: &str) -> Result { Ok(css.to_string() + "_optimized") } + + fn validate(&self, _raw_css: &str) -> Result<(), GrimoireCssError> { + Ok(()) + } } fn create_test_config() -> ConfigFs { @@ -486,25 +618,29 @@ mod tests { let optimizer = MockOptimizer; let builder = CssBuilderFs::new(&config, current_dir, &optimizer).unwrap(); + let spell = Spell::new( + "display=grid", + &config.shared_spells, + &config.scrolls, + (0, 0), + None, + ) + .unwrap() + .unwrap(); + let build_info = BuildInfo { file_path: PathBuf::from("test_output.css"), - spells: vec![Spell { - raw_spell: "d=grid".to_string(), - component: "display".to_string(), - component_target: "grid".to_string(), - effects: String::new(), - area: String::new(), - focus: String::new(), - with_template: false, - scroll_spells: None, - }], + spells: vec![spell], }; let result = builder.compile_css(&[build_info]); assert!(result.is_ok()); let compiled_css = result.unwrap(); - assert_eq!(compiled_css[0].1, ".d\\=grid{display:grid;}_optimized"); + assert_eq!( + compiled_css[0].1, + ".display\\=grid{display:grid;}_optimized" + ); } #[test] @@ -514,34 +650,38 @@ mod tests { let optimizer = MockOptimizer; let builder = CssBuilderFs::new(&config, current_dir, &optimizer).unwrap(); - let spells = vec![Spell { - raw_spell: "d=grid".to_string(), - component: "display".to_string(), - component_target: "grid".to_string(), - effects: String::new(), - area: String::new(), - focus: String::new(), - with_template: false, - scroll_spells: None, - }]; - - let result = builder.css_builder.combine_spells_to_css(&spells); + let spell = Spell::new( + "display=grid", + &config.shared_spells, + &config.scrolls, + (0, 0), + None, + ) + .unwrap() + .unwrap(); + + let spells = vec![spell]; + + let result = builder.css_builder.combine_spells_to_css_string(&spells); assert!(result.is_ok()); let assembled_css = result.unwrap(); - assert_eq!(assembled_css[0], ".d\\=grid{display:grid;}"); + assert_eq!(assembled_css, ".display\\=grid{display:grid;}"); } #[test] fn test_cssbuilder_write_compiled_css() { let file_path = PathBuf::from("test_output.css"); - let css = vec![(file_path.clone(), ".d\\=grid{display:grid;}".to_string())]; + let css = vec![( + file_path.clone(), + ".display\\=grid{display:grid;}".to_string(), + )]; let result = CssBuilderFs::write_compiled_css(&css); assert!(result.is_ok()); let written_content = std::fs::read_to_string(&file_path).unwrap(); - assert_eq!(written_content, ".d\\=grid{display:grid;}"); + assert_eq!(written_content, ".display\\=grid{display:grid;}"); std::fs::remove_file(file_path).unwrap(); } @@ -553,11 +693,56 @@ mod tests { let optimizer = MockOptimizer; let builder = CssBuilderFs::new(&config, current_dir, &optimizer).unwrap(); - let raw_css = ".d\\=grid{display:grid;}"; + let raw_css = ".display\\=grid{display:grid;}"; let result = builder.css_builder.optimize_css(raw_css); assert!(result.is_ok()); let optimized_css = result.unwrap(); - assert_eq!(optimized_css, ".d\\=grid{display:grid;}_optimized"); + assert_eq!(optimized_css, ".display\\=grid{display:grid;}_optimized"); + } + + #[test] + fn test_compose_extra_css_is_raw_not_optimized() { + let config = create_test_config(); + let current_dir = Path::new("."); + let optimizer = MockOptimizer; + let builder = CssBuilderFs::new(&config, current_dir, &optimizer).unwrap(); + + let raw = builder + .compose_extra_css(&["display=grid".to_string()]) + .unwrap(); + // compose_extra_css returns raw CSS; optimization is the caller's responsibility. + assert_eq!(raw, ".display\\=grid{display:grid;}"); + } + + #[test] + fn test_compile_critical_css_shares_payload_across_files() { + let mut config = create_test_config(); + config.critical = Some(vec![ConfigFsCritical { + file_to_inline_paths: vec!["a.html".to_string(), "b.html".to_string()], + styles: Some(vec!["display=grid".to_string()]), + css_custom_properties: None, + }]); + + let current_dir = Path::new("."); + let optimizer = MockOptimizer; + let builder = CssBuilderFs::new(&config, current_dir, &optimizer).unwrap(); + + let compiled = builder.compile_critical_css().unwrap().unwrap(); + assert_eq!(compiled.len(), 2); + assert!(Arc::ptr_eq(&compiled[0].1, &compiled[1].1)); + assert_eq!( + compiled[0].1.as_ref(), + ".display\\=grid{display:grid;}_optimized" + ); + } + + #[test] + fn test_parse_jobs_defaults_and_validation() { + assert_eq!(CssBuilderFs::parse_jobs("1").unwrap(), 1); + assert_eq!(CssBuilderFs::parse_jobs(" 4 ").unwrap(), 4); + assert!(CssBuilderFs::parse_jobs("0").is_err()); + assert!(CssBuilderFs::parse_jobs("-1").is_err()); + assert!(CssBuilderFs::parse_jobs("abc").is_err()); } } diff --git a/src/core/css_builder/css_builder_in_memory.rs b/src/core/css_builder/css_builder_in_memory.rs index b7833ba..16dbdf9 100644 --- a/src/core/css_builder/css_builder_in_memory.rs +++ b/src/core/css_builder/css_builder_in_memory.rs @@ -4,10 +4,12 @@ //! and is suitable for environments where file I/O is not desired. use std::collections::HashSet; +use std::sync::Arc; use crate::core::{ CssOptimizer, GrimoireCssError, compiled_css::CompiledCssInMemory, - config::config_in_memory::ConfigInMemory, parser::Parser, spell::Spell, + config::config_in_memory::ConfigInMemory, parser::Parser, source_file::SourceFile, + spell::Spell, }; use super::CssBuilder; @@ -63,17 +65,21 @@ impl<'a> CssBuilderInMemory<'a> { self.parser .collect_candidates(&content, &mut class_names, &mut seen_class_names)?; + let source = Arc::new(SourceFile::new(None, project.name.clone(), content)); + // Generate spells using empty shared_spells set since we're working in memory let spells = Spell::generate_spells_from_classes( class_names, &HashSet::new(), &self.config.scrolls, + Some(source), )?; // Combine spells into CSS - let assembled_spells = self.css_builder.combine_spells_to_css(&spells)?; - let raw_css = assembled_spells.join(""); - let css = self.css_builder.optimize_css(&raw_css)?; + // Avoid validate() + optimize() double-parsing for the common success path. + let css = self + .css_builder + .combine_spells_to_optimized_css_string(&spells)?; results.push(CompiledCssInMemory { name: project.name.clone(), @@ -97,6 +103,10 @@ mod tests { fn optimize(&self, css: &str) -> Result { Ok(css.to_string()) } + + fn validate(&self, _css: &str) -> Result<(), GrimoireCssError> { + Ok(()) + } } #[test] @@ -137,4 +147,87 @@ mod tests { assert_eq!(result[0].name, "test"); assert!(result[0].content.eq(".display\\=flex{display:flex;}")); } + + #[test] + fn test_builder_with_templated_scroll_invocation() { + let mut scrolls_map: HashMap> = HashMap::new(); + scrolls_map.insert( + "complex-card".to_string(), + vec!["h=$".to_string(), "c=$".to_string(), "w=$".to_string()], + ); + + let config = ConfigInMemory { + projects: vec![ConfigInMemoryEntry { + name: "test".to_string(), + content: vec!["
".to_string()], + }], + variables: None, + scrolls: Some(scrolls_map), + custom_animations: HashMap::new(), + browserslist_content: None, + }; + + let optimizer = MockOptimizer; + let mut builder = CssBuilderInMemory::new(&config, &optimizer).unwrap(); + let result = builder.build().unwrap(); + + assert_eq!(result.len(), 1); + let css = &result[0].content; + + // The output must use the outer template selector, not the inner scroll spell selectors. + assert!(css.contains(".g\\!complex-card\\=120px\\_red\\_100px\\;{height:120px;}")); + assert!(css.contains(".g\\!complex-card\\=120px\\_red\\_100px\\;{color:red;}")); + assert!(css.contains(".g\\!complex-card\\=120px\\_red\\_100px\\;{width:100px;}")); + + assert!(!css.contains(".h\\=120px")); + assert!(!css.contains(".c\\=red")); + assert!(!css.contains(".w\\=100px")); + } + + #[test] + fn test_builder_with_templated_scroll_invocation_with_prefixes() { + let mut scrolls_map: HashMap> = HashMap::new(); + scrolls_map.insert( + "complex-card".to_string(), + vec!["h=$".to_string(), "c=$".to_string(), "w=$".to_string()], + ); + + let config = ConfigInMemory { + projects: vec![ConfigInMemoryEntry { + name: "test".to_string(), + // Prefixes live on the scroll invocation and must apply to all expanded spells. + // - md__ => @media (min-width: 768px) + // - hover: => :hover pseudo + // - {_>_p} => " > p" focus selector + content: vec![ + "
_p}hover:complex-card=120px_red_100px;>
".to_string(), + ], + }], + variables: None, + scrolls: Some(scrolls_map), + custom_animations: HashMap::new(), + browserslist_content: None, + }; + + let optimizer = MockOptimizer; + let mut builder = CssBuilderInMemory::new(&config, &optimizer).unwrap(); + let result = builder.build().unwrap(); + + assert_eq!(result.len(), 1); + let css = &result[0].content; + + // Ensure the area prefix becomes a media query. + assert!(css.contains("@media (min-width: 768px)")); + // Ensure effects+focus survive the selector replacement. + assert!(css.contains(":hover > p")); + + // Ensure the outer template selector is used (not inner h=/c=/w= selectors). + assert!(css.contains( + ".g\\!md\\_\\_\\{\\_\\>\\_p\\}hover\\:complex-card\\=120px\\_red\\_100px\\;" + )); + + assert!(!css.contains(".md\\_\\_\\{\\_\\>\\_p\\}hover\\:h\\=120px")); + assert!(!css.contains(".md\\_\\_\\{\\_\\>\\_p\\}hover\\:c\\=red")); + assert!(!css.contains(".md\\_\\_\\{\\_\\>\\_p\\}hover\\:w\\=100px")); + } } diff --git a/src/core/css_generator/color_functions.rs b/src/core/css_generator/color_functions.rs index b272845..ea17559 100644 --- a/src/core/css_generator/color_functions.rs +++ b/src/core/css_generator/color_functions.rs @@ -45,21 +45,62 @@ static SPELL_COLOR_FUNCTIONS: &[(&str, SpellColorFunc)] = &[ /// * `Some(String)` containing the resulting color in hex form (e.g., `"#808080"`), /// if parsing and the color transformation succeed. /// * `None` if the string is not in a valid format, or the color transformation failed. -pub fn try_handle_color_function(adapted_target: &str) -> Option { - let (func_name, args_str) = parse_function_call(adapted_target)?; - // Split arguments by spaces. - let args: Vec<&str> = args_str.split(' ').map(|s| s.trim()).collect(); +pub fn try_handle_color_function( + adapted_target: &str, +) -> Result, crate::core::GrimoireCssError> { + let Some((func_name, args_str)) = parse_function_call(adapted_target) else { + return Ok(None); + }; - // Find the corresponding handler. - if let Some((_, handler)) = SPELL_COLOR_FUNCTIONS + let Some((_, handler)) = SPELL_COLOR_FUNCTIONS .iter() .find(|(name, _)| *name == func_name) - { - let result_color = handler(&args)?; - Some(result_color.to_hex_string()) + else { + return Ok(None); + }; + + let args: Vec<&str> = if args_str.is_empty() { + Vec::new() } else { - None + args_str + .split(' ') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect() + }; + + if let Some(result_color) = handler(&args) { + return Ok(Some(result_color.to_hex_string())); } + + let expected_usage = match func_name { + "g-grayscale" => "g-grayscale(color)", + "g-complement" => "g-complement(color)", + "g-invert" => "g-invert(color [weight])", + "g-mix" => "g-mix(color1 color2 weight)", + "g-adjust-hue" => "g-adjust-hue(color degrees)", + "g-adjust-color" => { + "g-adjust-color(color [red_delta green_delta blue_delta hue_delta sat_delta light_delta alpha_delta])" + } + "g-change-color" => "g-change-color(color [red green blue hue sat light alpha])", + "g-scale-color" => { + "g-scale-color(color [red_scale green_scale blue_scale saturation_scale lightness_scale alpha_scale])" + } + "g-rgba" => "g-rgba(color alpha)", + "g-lighten" => "g-lighten(color amount)", + "g-darken" => "g-darken(color amount)", + "g-saturate" => "g-saturate(color amount)", + "g-desaturate" => "g-desaturate(color amount)", + "g-opacify" => "g-opacify(color amount)", + "g-fade-in" => "g-fade-in(color amount)", + "g-transparentize" => "g-transparentize(color amount)", + "g-fade-out" => "g-fade-out(color amount)", + _ => "(see docs)", + }; + + Err(crate::core::GrimoireCssError::InvalidInput(format!( + "Invalid arguments for Grimoire color function '{func_name}'.\nExpected: {expected_usage}\nGot: '{adapted_target}'" + ))) } /// Parses a string like `"g-func(arg1 arg2)"` and returns a tuple `( "g-func", "arg1 arg2" )`. @@ -356,8 +397,11 @@ mod tests { /// A helper to compare Option equality with Some("#rrggbb"). /// This is just to reduce boilerplate in our tests. - fn assert_hex_eq(got: Option, expected_hex: &str) { - assert_eq!(got, Some(expected_hex.to_string())); + fn assert_hex_eq( + got: Result, crate::core::GrimoireCssError>, + expected_hex: &str, + ) { + assert_eq!(got.unwrap(), Some(expected_hex.to_string())); } #[test] @@ -405,13 +449,13 @@ mod tests { #[test] fn test_invalid_spell_function() { let res = try_handle_color_function("g-unknown(#fff)"); - assert_eq!(res, None); + assert_eq!(res.unwrap(), None); } #[test] fn test_invalid_args() { // e.g. "g-grayscale()" with no args let res = try_handle_color_function("g-grayscale()"); - assert_eq!(res, None); + assert!(res.is_err()); } } diff --git a/src/core/css_generator/css_generator_base.rs b/src/core/css_generator/css_generator_base.rs index 7f830d0..9baf817 100644 --- a/src/core/css_generator/css_generator_base.rs +++ b/src/core/css_generator/css_generator_base.rs @@ -18,7 +18,6 @@ //! The module also includes internal helper functions to manage specific CSS-related tasks such as //! unit stripping, handling of regex patterns, and combining base CSS with media queries. -use crate::buffer::add_message; use crate::core::GrimoireCssError; use crate::core::animations::ANIMATIONS; use crate::core::component::get_css_property; @@ -92,16 +91,75 @@ impl<'a> CssGenerator<'a> { /// * `Ok(Some(String, String))` 0: containing the generated CSS string if the spell's component is recognized; 1: css class name /// * `Ok(None)` if the spell's component is not recognized. /// * `Err(GrimoireCSSError)` if there is an error during CSS generation. + fn create_compile_error(&self, spell: &Spell, error: GrimoireCssError) -> GrimoireCssError { + if let GrimoireCssError::InvalidSpellFormat { message, help, .. } = error { + return GrimoireCssError::InvalidSpellFormat { + message, + span: spell.span, + label: "Error in this spell".to_string(), + help, + source_file: spell.source.clone(), + }; + } + + if let GrimoireCssError::CompileError { + message, + label, + help, + .. + } = error + { + return GrimoireCssError::CompileError { + message, + span: spell.span, + label, + help, + source_file: spell.source.clone(), + }; + } + + if let GrimoireCssError::InvalidInput(msg) = &error + && msg.starts_with("Unknown animation") + { + return GrimoireCssError::CompileError { + message: format!("Invalid input: {msg}"), + span: spell.span, + label: "Error in this spell".to_string(), + help: Some( + "The animation name is not known.\n\ +\ +Fix options:\n\ +- Use a built-in animation name supported by Grimoire CSS\n\ +- Or define a custom animation in config -> custom_animations\n" + .to_string(), + ), + source_file: spell.source.clone(), + }; + } + + let message = error.to_string(); + + GrimoireCssError::CompileError { + message, + span: spell.span, + label: "Error in this spell".to_string(), + help: None, + source_file: spell.source.clone(), + } + } + pub fn generate_css(&self, spell: &Spell) -> Result, GrimoireCssError> { // generate css class name - let css_class_name = self.generate_css_class_name( - &spell.raw_spell, - &spell.effects, - &spell.focus, - spell.with_template, - )?; + let css_class_name = self + .generate_css_class_name( + &spell.raw_spell, + spell.effects(), + spell.focus(), + spell.with_template, + ) + .map_err(|e| self.create_compile_error(spell, e))?; - let component_str = spell.component.as_str(); + let component_str = spell.component(); // match component and get css property let css_property: Option<&str> = if component_str.starts_with("--") { @@ -114,17 +172,21 @@ impl<'a> CssGenerator<'a> { match css_property { Some(css_property) => { // adapt target - let adapted_target = self.adapt_targets(&spell.component_target, self.variables)?; + let adapted_target = self + .adapt_targets(spell.component_target(), self.variables) + .map_err(|e| self.create_compile_error(spell, e))?; // generate base css without any media queries (except for the mrs function) - let (base_css, additional_css) = self.generate_base_and_additional_css( - &adapted_target, - &css_class_name.0, - css_property, - )?; - - if !spell.area.is_empty() { + let (base_css, additional_css) = self + .generate_base_and_additional_css( + &adapted_target, + &css_class_name.0, + css_property, + ) + .map_err(|e| self.create_compile_error(spell, e))?; + + if !spell.area().is_empty() { return Ok(Some(( - self.wrap_base_css_with_media_query(&spell.area, &base_css), + self.wrap_base_css_with_media_query(spell.area(), &base_css), css_class_name, additional_css, ))); @@ -212,17 +274,43 @@ impl<'a> CssGenerator<'a> { .map(|c| match c { '!' | '"' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | '.' | '/' | ':' | ';' | '<' | '=' | '>' | '?' | '@' | '[' | '\\' | ']' | '^' | '_' - | '`' | '{' | '|' | '}' | '~' => format!("\\{c}"), + | '`' | '{' | '|' | '}' | '~' => Ok(format!("\\{c}")), ' ' => { - add_message("HTML does not support spaces. To separate values use underscore ('_') instead".to_string()); - c.to_string() + // IMPORTANT: + // - In HTML attributes, spaces are class separators, not part of a single class token. + // - If a user tries to encode a value that contains spaces (e.g. calc(100vh - 50px)), + // the correct Grimoire convention is to use underscores instead: calc(100vh_-_50px). + // + // We intentionally return a "context-carrying" error variant here. The outer layer + // (generate_css/create_compile_error) will attach the actual source and span. + Err(GrimoireCssError::CompileError { + message: "Spaces are not allowed inside a single spell token.".to_string(), + span: (0, 0), + label: "Error in this spell".to_string(), + help: Some(format!( + "You likely wrote a value with spaces inside a class attribute (HTML treats spaces as class separators).\n\ +Fix: replace spaces with '_' inside the value, e.g.:\n\ + h=calc(100vh - 50px) -> h=calc(100vh_-_50px)\n\n\ +Offending spell: '{class_name}'" + )), + source_file: None, + }) } - _ => c.to_string(), + _ => Ok(c.to_string()), }) - .collect::(); + .collect::>()?; if escaped.is_empty() { - return Err(GrimoireCssError::InvalidSpellFormat(class_name.to_string())); + return Err(GrimoireCssError::CompileError { + message: format!("Empty spell token: '{class_name}'"), + span: (0, 0), + label: "Error in this spell".to_string(), + help: Some( + "Spell tokens must not be empty. Check for extra spaces or malformed templates in class/className." + .to_string(), + ), + source_file: None, + }); } Ok(escaped) @@ -305,7 +393,7 @@ impl<'a> CssGenerator<'a> { self.handle_animation_name(adapted_target, css_class_name) } _ => { - if let Some(css_str) = try_handle_color_function(adapted_target) { + if let Some(css_str) = try_handle_color_function(adapted_target)? { self.handle_generic_css(&css_str, css_class_name, property) } else { self.handle_generic_css(adapted_target, css_class_name, property) @@ -347,9 +435,10 @@ impl<'a> CssGenerator<'a> { return Ok((base_css, Some(keyframes))); } - Err(GrimoireCssError::InvalidSpellFormat( - adapted_target.to_string(), - )) + Err(GrimoireCssError::InvalidInput(format!( + "Unknown animation: {}", + adapted_target + ))) } /// Handles CSS generation for the `animation` property. @@ -1105,16 +1194,12 @@ mod tests { let config = ConfigFs::default(); let generator = CssGenerator::new(&config.variables, &config.custom_animations).unwrap(); - let spell = Spell { - raw_spell: "bg-c=pink".to_string(), - component: "bg-c".to_string(), - component_target: "pink".to_string(), - effects: "".to_string(), - area: "".to_string(), - focus: "".to_string(), - with_template: false, - scroll_spells: None, - }; + let shared_spells = std::collections::HashSet::new(); + let scrolls: Option>> = None; + + let spell = Spell::new("bg-c=pink", &shared_spells, &scrolls, (0, 0), None) + .unwrap() + .unwrap(); let result = generator.generate_css(&spell); @@ -1130,16 +1215,15 @@ mod tests { // --- COMPLEX --- - let spell_complex = Spell { - raw_spell: "{[data-theme='light']_p}font-sz=mrs(14px_16px_380px_800px)".to_string(), - component: "font-sz".to_string(), - component_target: "mrs(14px_16px_380px_800px)".to_string(), - effects: "".to_string(), - area: "".to_string(), - focus: "[data-theme='light']_p".to_string(), - with_template: true, - scroll_spells: None, - }; + let spell_complex = Spell::new( + "{[data-theme='light']_p}font-sz=mrs(14px_16px_380px_800px)", + &shared_spells, + &scrolls, + (0, 0), + None, + ) + .unwrap() + .unwrap(); let result = generator.generate_css(&spell_complex); @@ -1153,7 +1237,7 @@ mod tests { assert_eq!( css, - r".g\!\{\[data-theme\=\'light\'\]\_p\}font-sz\=mrs\(14px\_16px\_380px\_800px\)\;[data-theme='light'] p{font-size:14px;}@media screen and (min-width: 380px) {.g\!\{\[data-theme\=\'light\'\]\_p\}font-sz\=mrs\(14px\_16px\_380px\_800px\)\;[data-theme='light'] p{font-size: calc(14px + 2 * ((100vw - 380px) / 420));}}@media screen and (min-width: 800px) {.g\!\{\[data-theme\=\'light\'\]\_p\}font-sz\=mrs\(14px\_16px\_380px\_800px\)\;[data-theme='light'] p{font-size: 16px;}}" + r".\{\[data-theme\=\'light\'\]\_p\}font-sz\=mrs\(14px\_16px\_380px\_800px\)[data-theme='light'] p{font-size:14px;}@media screen and (min-width: 380px) {.\{\[data-theme\=\'light\'\]\_p\}font-sz\=mrs\(14px\_16px\_380px\_800px\)[data-theme='light'] p{font-size: calc(14px + 2 * ((100vw - 380px) / 420));}}@media screen and (min-width: 800px) {.\{\[data-theme\=\'light\'\]\_p\}font-sz\=mrs\(14px\_16px\_380px\_800px\)[data-theme='light'] p{font-size: 16px;}}" ); } diff --git a/src/core/css_optimizer.rs b/src/core/css_optimizer.rs index e8c3e84..04a2c05 100644 --- a/src/core/css_optimizer.rs +++ b/src/core/css_optimizer.rs @@ -39,4 +39,16 @@ pub trait CssOptimizer: Sync + Send { /// * `Ok(String)` - The optimized and minified CSS string. /// * `Err(GrimoireCSSError)` - An error indicating that the optimization process failed. fn optimize(&self, raw_css: &str) -> Result; + + /// Validates a given raw CSS string. + /// + /// # Arguments + /// + /// * `raw_css` - A string containing the raw CSS code that needs to be validated. + /// + /// # Returns + /// + /// * `Ok(())` - If the CSS is valid. + /// * `Err(GrimoireCssError)` - An error indicating that the validation failed. + fn validate(&self, raw_css: &str) -> Result<(), GrimoireCssError>; } diff --git a/src/core/grimoire_css_error.rs b/src/core/grimoire_css_error.rs index 3770efa..9406873 100644 --- a/src/core/grimoire_css_error.rs +++ b/src/core/grimoire_css_error.rs @@ -5,66 +5,108 @@ //! serialization/deserialization processes, and custom application-specific errors related to //! invalid input or spell formats. -use regex; -use serde_json; -use std::fmt; -use std::io; +use std::sync::Arc; +use thiserror::Error; + +use super::source_file::SourceFile; /// Represents all possible errors that can occur in the Grimoire CSS system. /// /// This enum consolidates different error types from various operations into /// a single error type, making error handling consistent throughout the application. -#[derive(Debug)] +#[derive(Debug, Error)] pub enum GrimoireCssError { /// IO errors during file operations - Io(io::Error), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + /// Regular expression parsing or execution errors - Regex(regex::Error), + #[error("Regex error: {0}")] + Regex(#[from] regex::Error), + /// JSON serialization/deserialization errors - Serde(serde_json::Error), + #[error("Serialization/Deserialization error: {0}")] + Serde(#[from] serde_json::Error), + /// Invalid spell format (e.g., malformed class names or templates) - InvalidSpellFormat(String), + #[error("Invalid spell format: {message}")] + InvalidSpellFormat { + message: String, + span: (usize, usize), + label: String, + help: Option, + source_file: Option>, + }, + /// General input validation errors + #[error("Invalid input: {0}")] InvalidInput(String), + /// Invalid file or directory path errors + #[error("Invalid path: {0}")] InvalidPath(String), + /// Errors in glob pattern syntax or matching + #[error("Glob pattern error: {0}")] GlobPatternError(String), + /// Runtime errors that don't fit other categories + #[error("Runtime error: {0}")] RuntimeError(String), -} - -impl fmt::Display for GrimoireCssError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - GrimoireCssError::Io(e) => write!(f, "IO error: {e}"), - GrimoireCssError::Regex(e) => write!(f, "Regex error: {e}"), - GrimoireCssError::Serde(e) => write!(f, "Serialization/Deserialization error: {e}"), - GrimoireCssError::InvalidSpellFormat(s) => write!(f, "Invalid spell format: {s}"), - GrimoireCssError::InvalidInput(s) => write!(f, "Invalid input: {s}"), - GrimoireCssError::InvalidPath(s) => write!(f, "Invalid path: {s}"), - GrimoireCssError::GlobPatternError(s) => write!(f, "Glob pattern error: {s}"), - GrimoireCssError::RuntimeError(s) => write!(f, "Runtime error: {s}"), - } - } -} -impl std::error::Error for GrimoireCssError {} + /// CSS Optimization errors (e.g. from LightningCSS) + #[error("CSS Optimization failed: {0}")] + OptimizationError(String), -impl From for GrimoireCssError { - fn from(error: io::Error) -> Self { - GrimoireCssError::Io(error) - } + /// Error with source context for better reporting + #[error("{message}")] + CompileError { + message: String, + span: (usize, usize), + label: String, + help: Option, + source_file: Option>, + }, } -impl From for GrimoireCssError { - fn from(error: regex::Error) -> Self { - GrimoireCssError::Regex(error) +impl GrimoireCssError { + pub fn with_source(self, source: Arc) -> Self { + match self { + GrimoireCssError::InvalidSpellFormat { + message, + span, + label, + help, + source_file: existing, + } => GrimoireCssError::InvalidSpellFormat { + message, + span, + label, + help, + source_file: existing.or(Some(source)), + }, + GrimoireCssError::CompileError { + message, + span, + label, + help, + source_file: existing, + } => GrimoireCssError::CompileError { + message, + span, + label, + help, + source_file: existing.or(Some(source)), + }, + other => other, + } } -} -impl From for GrimoireCssError { - fn from(error: serde_json::Error) -> Self { - GrimoireCssError::Serde(error) + pub fn source(&self) -> Option<&Arc> { + match self { + GrimoireCssError::InvalidSpellFormat { source_file, .. } => source_file.as_ref(), + GrimoireCssError::CompileError { source_file, .. } => source_file.as_ref(), + _ => None, + } } } diff --git a/src/core/mod.rs b/src/core/mod.rs index d8c6238..ee8d0f0 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -18,7 +18,9 @@ pub mod css_builder; pub mod css_optimizer; pub mod grimoire_css_error; pub mod parser; +pub mod source_file; pub mod spell; +pub mod spell_value_validator; pub use compiled_css::*; pub use config::*; @@ -26,5 +28,6 @@ pub use css_builder::*; pub use css_optimizer::*; pub use filesystem::*; pub use grimoire_css_error::*; +pub use source_file::*; // Exception: This external dependency was part of the Grimoire CSS and is now included as a separate crate, but should still be part of the main module and available for use. pub use grimoire_css_color_toolkit_lib::*; diff --git a/src/core/parser/parser_base.rs b/src/core/parser/parser_base.rs index 9f28ca7..2ed611c 100644 --- a/src/core/parser/parser_base.rs +++ b/src/core/parser/parser_base.rs @@ -99,66 +99,99 @@ impl Parser { result.into_iter().collect() } - /// Collects class names from content based on the given regular expression and optional predicate/splitter functions. + /// Collects class names from content based on the given regular expression. /// /// # Arguments /// /// * `content` - The content to be parsed. /// * `regex` - A regular expression used to search for class names. - /// * `predicate` - An optional function used to filter the results. - /// * `splitter` - An optional function used to split the result into multiple class names. - /// * `class_names` - A mutable reference to a vector to store the collected class names. + /// * `split_by_whitespace` - Whether to split the matched value by whitespace. + /// * `class_names` - A mutable reference to a vector to store the collected class names and their spans. /// * `seen_class_names` - A mutable reference to a `HashSet` used to track seen class names. + /// * `collection_type` - The type of collection being performed. /// /// # Errors /// - /// Returns a `GrimoireCSSError` if there is an issue during processing. - fn collect_classes( + /// Returns a `GrimoireCssError` if there is an issue during processing. + fn collect_classes( content: &str, regex: &Regex, - mut predicate: Option

, - mut splitter: Option, - class_names: &mut Vec, + split_by_whitespace: bool, + class_names: &mut Vec<(String, (usize, usize))>, seen_class_names: &mut HashSet, collection_type: CollectionType, - ) -> Result<(), GrimoireCssError> - where - P: FnMut(&str) -> bool, - S: FnMut(&str) -> Vec, - { + ) -> Result<(), GrimoireCssError> { for cap in regex.captures_iter(content) { - let class_value = match collection_type { - CollectionType::TemplatedSpell => cap.get(1).map(|m| m.as_str()).unwrap_or(""), - CollectionType::CurlyClass => cap.get(1).map(|m| m.as_str()).unwrap_or(""), - CollectionType::RegularClass => cap - .get(2) - .or_else(|| cap.get(3)) - .or_else(|| cap.get(4)) - .map(|m| m.as_str()) - .unwrap_or(""), - }; - - let classes = if let Some(splitter_fn) = &mut splitter { - let splitted = splitter_fn(class_value); - - if matches!(collection_type, CollectionType::CurlyClass) { - splitted - .into_iter() - .map(|s| Self::clean_unpaired_brackets(&s)) - .collect() - } else { - splitted + let match_obj = match collection_type { + CollectionType::TemplatedSpell => cap.get(1), + CollectionType::CurlyClass => cap.get(1), + CollectionType::RegularClass => { + cap.get(2).or_else(|| cap.get(3)).or_else(|| cap.get(4)) } - } else { - vec![class_value.to_string()] }; - for class in classes { - let should_include = predicate.as_mut().is_none_or(|p| p(&class)); + if let Some(m) = match_obj { + let full_value = m.as_str(); + let base_offset = m.start(); + + if split_by_whitespace { + for part in full_value.split_whitespace() { + // Calculate the offset of the part within the full content + let part_start = part.as_ptr() as usize - full_value.as_ptr() as usize; + let start = base_offset + part_start; + let length = part.len(); + + // For regular `class` / `className` tokens we can check the HashSet + // by `&str` first to avoid allocating `String` for duplicates. + if !matches!(collection_type, CollectionType::CurlyClass) + && !part.is_empty() + && seen_class_names.contains(part) + { + continue; + } + + let mut class_string = part.to_string(); + + if matches!(collection_type, CollectionType::CurlyClass) { + class_string = Self::clean_unpaired_brackets(&class_string); + } + + if !class_string.is_empty() && !seen_class_names.contains(&class_string) { + seen_class_names.insert(class_string.clone()); + class_names.push((class_string, (start, length))); + } + } + } else { + let start = base_offset; + let length = full_value.len(); + let class_string = full_value.to_string(); + + if class_string.contains(' ') { + // IMPORTANT: + // - In HTML attributes, spaces are class separators, not part of a single class token. + // - If a user tries to encode a value that contains spaces (e.g. calc(100vh - 50px)), + // the correct Grimoire convention is to use underscores instead: calc(100vh_-_50px). + // + // We return a Diagnostic-style error so the CLI can render it like rustc. + return Err(GrimoireCssError::CompileError { + message: "Spaces are not allowed inside a single spell token." + .to_string(), + span: (start, length), + label: "Error in this spell".to_string(), + help: Some(format!( + "You likely wrote a value with spaces inside a class attribute (HTML treats spaces as class separators).\n\ +Fix: replace spaces with '_' inside the value, e.g.:\n\ + h=calc(100vh - 50px) -> h=calc(100vh_-_50px)\n\n\ +Offending spell: '{class_string}'" + )), + source_file: None, + }); + } - if should_include && !seen_class_names.contains(&class) { - seen_class_names.insert(class.clone()); - class_names.push(class); + if !class_string.is_empty() && !seen_class_names.contains(&class_string) { + seen_class_names.insert(class_string.clone()); + class_names.push((class_string, (start, length))); + } } } } @@ -171,7 +204,7 @@ impl Parser { /// # Arguments /// /// * `content` - The content to parse - /// * `class_names` - A mutable reference to a vector that stores the collected class names + /// * `class_names` - A mutable reference to a vector that stores the collected class names and their spans /// * `seen_class_names` - A mutable reference to a HashSet for tracking seen class names /// /// # Returns @@ -180,66 +213,54 @@ impl Parser { pub fn collect_candidates( &self, content: &str, - class_names: &mut Vec, + class_names: &mut Vec<(String, (usize, usize))>, seen_class_names: &mut HashSet, ) -> Result<(), GrimoireCssError> { - let whitespace_splitter = |input: &str| { - input - .split_whitespace() - .map(String::from) - .collect::>() - }; - // Collect all 'className' matches - Self::collect_classes:: bool, fn(&str) -> Vec>( + Self::collect_classes( content, &self.class_name_regex, - None, - Some(whitespace_splitter), + true, class_names, seen_class_names, CollectionType::RegularClass, )?; // Collect all 'class' matches - Self::collect_classes:: bool, fn(&str) -> Vec>( + Self::collect_classes( content, &self.class_regex, - None, - Some(whitespace_splitter), + true, class_names, seen_class_names, CollectionType::RegularClass, )?; // Collect all 'templated class' (starts with 'g!', ends with ';') matches - Self::collect_classes:: bool, fn(&str) -> Vec>( + Self::collect_classes( content, &self.tepmplated_spell_regex, - None, - None, + false, class_names, seen_class_names, CollectionType::TemplatedSpell, )?; // Collect all curly 'className' matches - Self::collect_classes:: bool, fn(&str) -> Vec>( + Self::collect_classes( content, &self.curly_class_name_regex, - None, - Some(whitespace_splitter), + true, class_names, seen_class_names, CollectionType::CurlyClass, )?; // Collect all curly 'class' matches - Self::collect_classes:: bool, fn(&str) -> Vec>( + Self::collect_classes( content, &self.curly_class_regex, - None, - Some(whitespace_splitter), + true, class_names, seen_class_names, CollectionType::CurlyClass, @@ -271,11 +292,13 @@ mod tests { .unwrap(); assert_eq!(class_names.len(), 5); - assert!(class_names.contains(&"test1".to_string())); - assert!(class_names.contains(&"test2".to_string())); - assert!(class_names.contains(&"test3".to_string())); - assert!(class_names.contains(&"test4".to_string())); - assert!(class_names.contains(&"g!display=block;".to_string())); + + let names: Vec = class_names.iter().map(|(n, _)| n.clone()).collect(); + assert!(names.contains(&"test1".to_string())); + assert!(names.contains(&"test2".to_string())); + assert!(names.contains(&"test3".to_string())); + assert!(names.contains(&"test4".to_string())); + assert!(names.contains(&"g!display=block;".to_string())); } #[test] @@ -295,8 +318,9 @@ mod tests { .unwrap(); assert_eq!(class_names.len(), 2); - assert!(class_names.contains(&"g!display=flex;".to_string())); - assert!(class_names.contains(&"g!color=red;".to_string())); + let names: Vec = class_names.iter().map(|(n, _)| n.clone()).collect(); + assert!(names.contains(&"g!display=flex;".to_string())); + assert!(names.contains(&"g!color=red;".to_string())); } #[test] @@ -319,8 +343,9 @@ mod tests { .unwrap(); assert_eq!(class_names.len(), 6); + let names: Vec = class_names.iter().map(|(n, _)| n.clone()).collect(); for i in 1..=6 { - assert!(class_names.contains(&format!("test{i}"))); + assert!(names.contains(&format!("test{i}"))); } } @@ -341,15 +366,16 @@ mod tests { assert_eq!(class_names.len(), 9); - assert!(class_names.contains(&"isError".to_string())); - assert!(class_names.contains(&"?".to_string())); - assert!(class_names.contains(&"color=red".to_string())); - assert!(class_names.contains(&"regular-class-error".to_string())); - assert!(class_names.contains(&":".to_string())); - assert!(class_names.contains(&"color=green".to_string())); - assert!(class_names.contains(&"regular-class-success".to_string())); - assert!(class_names.contains(&"display=grid".to_string())); - assert!(class_names.contains(&"state-${state}".to_string())); + let names: Vec = class_names.iter().map(|(n, _)| n.clone()).collect(); + assert!(names.contains(&"isError".to_string())); + assert!(names.contains(&"?".to_string())); + assert!(names.contains(&"color=red".to_string())); + assert!(names.contains(&"regular-class-error".to_string())); + assert!(names.contains(&":".to_string())); + assert!(names.contains(&"color=green".to_string())); + assert!(names.contains(&"regular-class-success".to_string())); + assert!(names.contains(&"display=grid".to_string())); + assert!(names.contains(&"state-${state}".to_string())); } #[test] @@ -368,13 +394,38 @@ mod tests { .unwrap(); // Should clean unpaired brackets and quotes - assert!(class_names.contains(&"class-with-{unpaired}".to_string())); - assert!(class_names.contains(&"brackets".to_string())); - assert!(class_names.contains(&"and".to_string())); - assert!(class_names.contains(&"quotes".to_string())); - assert!(class_names.contains(&"normal-class".to_string())); - assert!(class_names.contains(&"{paired}".to_string())); - assert!(class_names.contains(&"[brackets]".to_string())); - assert!(class_names.contains(&"(work)".to_string())); + let names: Vec = class_names.iter().map(|(n, _)| n.clone()).collect(); + assert!(names.contains(&"class-with-{unpaired}".to_string())); + assert!(names.contains(&"brackets".to_string())); + assert!(names.contains(&"and".to_string())); + assert!(names.contains(&"quotes".to_string())); + assert!(names.contains(&"normal-class".to_string())); + assert!(names.contains(&"{paired}".to_string())); + assert!(names.contains(&"[brackets]".to_string())); + assert!(names.contains(&"(work)".to_string())); + } + + #[test] + fn test_spans() { + let parser = Parser::new(); + let mut class_names = Vec::new(); + let mut seen_class_names = HashSet::new(); + + let content = r#"

"#; + // 012345678901234567890123456 + // foo is at 12..15 + // bar is at 16..19 + + parser + .collect_candidates(content, &mut class_names, &mut seen_class_names) + .unwrap(); + + assert_eq!(class_names.len(), 2); + + let foo = class_names.iter().find(|(n, _)| n == "foo").unwrap(); + assert_eq!(foo.1, (12, 3)); + + let bar = class_names.iter().find(|(n, _)| n == "bar").unwrap(); + assert_eq!(bar.1, (16, 3)); } } diff --git a/src/core/parser/parser_fs.rs b/src/core/parser/parser_fs.rs index c7a294f..7537198 100644 --- a/src/core/parser/parser_fs.rs +++ b/src/core/parser/parser_fs.rs @@ -2,13 +2,22 @@ //! with filesystem-specific functionality for collecting CSS classes from files and directories. use super::Parser; +use crate::core::SourceFile; use crate::{buffer::add_message, core::GrimoireCssError}; use std::{ collections::HashSet, fs, path::{Path, PathBuf}, + sync::Arc, }; +type Span = (usize, usize); +type ClassWithSpan = (String, Span); +type ClassesWithSpans = Vec; + +type SingleOutputFileClasses = (PathBuf, ClassesWithSpans); +type MultipleOutputFileClasses = (PathBuf, PathBuf, ClassesWithSpans); + /// `ParserFs` extends the base `Parser` with filesystem-specific functionality. /// It handles file reading, directory traversal, and path resolution. pub struct ParserFs { @@ -37,7 +46,7 @@ impl ParserFs { /// /// # Returns /// - /// A vector of unique class names found in the input files. + /// A vector of tuples containing file path and found classes with spans. /// /// # Errors /// @@ -45,16 +54,16 @@ impl ParserFs { pub fn collect_classes_single_output( &self, input_paths: &Vec, - ) -> Result, GrimoireCssError> { - let mut class_names: Vec = Vec::new(); + ) -> Result, GrimoireCssError> { + let mut results = Vec::new(); let mut seen_class_names: HashSet = HashSet::new(); for input_path in input_paths { let path = self.current_dir.join(input_path); - self.collect_spells_from_path(&path, &mut class_names, &mut seen_class_names)?; + self.collect_spells_from_path(&path, &mut results, &mut seen_class_names)?; } - Ok(class_names) + Ok(results) } /// Collects class names or templated spells from multiple input paths, producing multiple outputs. @@ -66,23 +75,24 @@ impl ParserFs { /// /// # Returns /// - /// A vector of tuples, where each tuple contains the path to the output CSS file and a vector of class names. + /// A vector of tuples: (OutputCssPath, InputSourcePath, ClassesWithSpans). /// /// # Errors /// /// Returns a `GrimoireCSSError` if any file or directory cannot be processed. + #[allow(dead_code)] pub fn collect_classes_multiple_output( &self, input_paths: &Vec, output_dir_path: &Path, - ) -> Result)>, GrimoireCssError> { - let mut res: Vec<(PathBuf, Vec)> = Vec::new(); + ) -> Result, GrimoireCssError> { + let mut res = Vec::new(); for input_path_string in input_paths { let path = self.current_dir.join(input_path_string); if path.is_file() { - let mut class_names: Vec = Vec::new(); + let mut class_names = Vec::new(); let mut seen_class_names: HashSet = HashSet::new(); let output_file_path = path.with_extension("css"); @@ -92,13 +102,20 @@ impl ParserFs { })?); let file_content = fs::read_to_string(&path)?; - self.base_parser.collect_candidates( + if let Err(e) = self.base_parser.collect_candidates( &file_content, &mut class_names, &mut seen_class_names, - )?; - - res.push((bundle_output_full_path, class_names)); + ) { + let src = Arc::new(SourceFile::new( + Some(path.clone()), + path.to_string_lossy().to_string(), + file_content.clone(), + )); + return Err(e.with_source(src)); + } + + res.push((bundle_output_full_path, path, class_names)); } else if path.is_dir() { let entries = &self.get_sorted_directory_entries(&path)?; @@ -123,12 +140,81 @@ impl ParserFs { Ok(res) } + /// Streaming variant of `collect_classes_multiple_output`. + /// + /// Calls `visitor` once per input file instead of building a large in-memory vector. + pub fn for_each_classes_multiple_output( + &self, + input_paths: &Vec, + output_dir_path: &Path, + mut visitor: F, + ) -> Result<(), GrimoireCssError> + where + F: FnMut(PathBuf, PathBuf, ClassesWithSpans) -> Result<(), GrimoireCssError>, + { + for input_path_string in input_paths { + let path = self.current_dir.join(input_path_string); + self.visit_classes_multiple_output_path(&path, output_dir_path, &mut visitor)?; + } + + Ok(()) + } + + fn visit_classes_multiple_output_path( + &self, + path: &Path, + output_dir_path: &Path, + visitor: &mut F, + ) -> Result<(), GrimoireCssError> + where + F: FnMut(PathBuf, PathBuf, ClassesWithSpans) -> Result<(), GrimoireCssError>, + { + if path.is_file() { + let mut class_names = Vec::new(); + let mut seen_class_names: HashSet = HashSet::new(); + + let output_file_path = path.with_extension("css"); + let bundle_output_full_path = + output_dir_path.join(output_file_path.file_name().ok_or_else(|| { + GrimoireCssError::InvalidPath(output_file_path.to_string_lossy().into()) + })?); + + let file_content = fs::read_to_string(path)?; + if let Err(e) = self.base_parser.collect_candidates( + &file_content, + &mut class_names, + &mut seen_class_names, + ) { + let src = Arc::new(SourceFile::new( + Some(path.to_path_buf()), + path.to_string_lossy().to_string(), + file_content.clone(), + )); + return Err(e.with_source(src)); + } + + visitor(bundle_output_full_path, path.to_path_buf(), class_names)?; + return Ok(()); + } + + if path.is_dir() { + let entries = self.get_sorted_directory_entries(path)?; + for entry in entries { + self.visit_classes_multiple_output_path(&entry, output_dir_path, visitor)?; + } + return Ok(()); + } + + add_message(format!("Invalid path: {}", path.display())); + Ok(()) + } + /// Recursively collects CSS class names or templated spells from a given file or directory path. /// /// # Arguments /// /// * `path` - The path of the file or directory to process. - /// * `class_names` - A mutable reference to a vector that stores the collected class names. + /// * `results` - A mutable reference to a vector that stores the collected results. /// * `seen_class_names` - A mutable reference to a HashSet for tracking seen class names. /// /// # Errors @@ -137,18 +223,34 @@ impl ParserFs { fn collect_spells_from_path( &self, path: &Path, - class_names: &mut Vec, + results: &mut Vec, seen_class_names: &mut HashSet, ) -> Result<(), GrimoireCssError> { if path.is_file() { let file_content = fs::read_to_string(path)?; - self.base_parser - .collect_candidates(&file_content, class_names, seen_class_names)?; + let mut class_names = Vec::new(); + + if let Err(e) = self.base_parser.collect_candidates( + &file_content, + &mut class_names, + seen_class_names, + ) { + let src = Arc::new(SourceFile::new( + Some(path.to_path_buf()), + path.to_string_lossy().to_string(), + file_content.clone(), + )); + return Err(e.with_source(src)); + } + + if !class_names.is_empty() { + results.push((path.to_path_buf(), class_names)); + } } else if path.is_dir() { let entries = &self.get_sorted_directory_entries(path)?; for entry in entries { - self.collect_spells_from_path(entry, class_names, seen_class_names)?; + self.collect_spells_from_path(entry, results, seen_class_names)?; } } else { add_message(format!("Invalid path: {}", path.display())); @@ -156,7 +258,6 @@ impl ParserFs { Ok(()) } - /// Retrieves and sorts all entries in a given directory. /// /// # Arguments @@ -188,7 +289,7 @@ impl ParserFs { let mut seen = std::collections::HashSet::new(); self.base_parser .collect_candidates(content, &mut raw_spells, &mut seen)?; - Ok(raw_spells) + Ok(raw_spells.into_iter().map(|(s, _)| s).collect()) } } @@ -213,11 +314,16 @@ mod tests { parser.collect_classes_single_output(&vec![test_file.to_str().unwrap().to_string()]); assert!(result.is_ok()); - let classes = result.unwrap(); + let results = result.unwrap(); + assert_eq!(results.len(), 1); + let (path, classes) = &results[0]; + assert_eq!(path, &test_file); assert_eq!(classes.len(), 3); - assert!(classes.contains(&"test1".to_string())); - assert!(classes.contains(&"test2".to_string())); - assert!(classes.contains(&"test3".to_string())); + + let class_names: Vec = classes.iter().map(|(n, _)| n.clone()).collect(); + assert!(class_names.contains(&"test1".to_string())); + assert!(class_names.contains(&"test2".to_string())); + assert!(class_names.contains(&"test3".to_string())); } #[test] @@ -246,12 +352,14 @@ mod tests { assert_eq!(outputs.len(), 2); // Check first file output - assert_eq!(outputs[0].1.len(), 1); - assert!(outputs[0].1.contains(&"file1-class".to_string())); + let (_, _, classes1) = &outputs[0]; + assert_eq!(classes1.len(), 1); + assert_eq!(classes1[0].0, "file1-class"); // Check second file output - assert_eq!(outputs[1].1.len(), 1); - assert!(outputs[1].1.contains(&"file2-class".to_string())); + let (_, _, classes2) = &outputs[1]; + assert_eq!(classes2.len(), 1); + assert_eq!(classes2[0].0, "file2-class"); } #[test] @@ -271,9 +379,11 @@ mod tests { parser.collect_classes_single_output(&vec![sub_dir.to_str().unwrap().to_string()]); assert!(result.is_ok()); - let classes = result.unwrap(); + let results = result.unwrap(); + assert_eq!(results.len(), 1); + let (_, classes) = &results[0]; assert_eq!(classes.len(), 1); - assert!(classes.contains(&"nested-class".to_string())); + assert_eq!(classes[0].0, "nested-class"); } #[test] @@ -294,4 +404,26 @@ mod tests { assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 0); } + + #[test] + fn test_collect_classes_single_output_dedups_across_files() { + let temp_dir = tempdir().unwrap(); + let a = temp_dir.path().join("a.html"); + let b = temp_dir.path().join("b.html"); + + // Same token appears in both files. + fs::write(&a, r#"
"#).unwrap(); + fs::write(&b, r#"
"#).unwrap(); + + let parser = ParserFs::new(temp_dir.path()); + let input_paths = vec!["a.html".to_string(), "b.html".to_string()]; + + let results = parser.collect_classes_single_output(&input_paths).unwrap(); + + // Dedup is global across all inputs in single-output mode. + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, a); + assert_eq!(results[0].1.len(), 1); + assert_eq!(results[0].1[0].0, "h=10px".to_string()); + } } diff --git a/src/core/source_file.rs b/src/core/source_file.rs new file mode 100644 index 0000000..69b6182 --- /dev/null +++ b/src/core/source_file.rs @@ -0,0 +1,26 @@ +use std::{path::PathBuf, sync::Arc}; + +#[derive(Debug, Clone)] +pub struct SourceFile { + pub name: String, + pub path: Option, + pub content: Option>, +} + +impl SourceFile { + pub fn new(path: Option, name: String, content: String) -> Self { + Self { + name, + path, + content: Some(Arc::new(content)), + } + } + + pub fn new_path_only(path: Option, name: String) -> Self { + Self { + name, + path, + content: None, + } + } +} diff --git a/src/core/spell.rs b/src/core/spell.rs index fd7e807..df053d1 100644 --- a/src/core/spell.rs +++ b/src/core/spell.rs @@ -26,30 +26,96 @@ //! if the string format is invalid. use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use super::{ + GrimoireCssError, component::get_css_property, source_file::SourceFile, spell_value_validator, +}; + +#[derive(Debug, Clone)] +struct SpellParts { + area: std::ops::Range, + focus: std::ops::Range, + effects: std::ops::Range, + component: std::ops::Range, + component_target: std::ops::Range, +} -use super::{GrimoireCssError, component::get_css_property}; - -#[derive(Eq, Hash, PartialEq, Debug, Clone)] +#[derive(Debug, Clone)] pub struct Spell { pub raw_spell: String, - pub component: String, - pub component_target: String, - pub effects: String, - pub area: String, - pub focus: String, pub with_template: bool, pub scroll_spells: Option>, + pub span: (usize, usize), + pub source: Option>, + parts: Option, +} + +impl PartialEq for Spell { + fn eq(&self, other: &Self) -> bool { + self.raw_spell == other.raw_spell + && self.with_template == other.with_template + && self.scroll_spells == other.scroll_spells + } +} + +impl Eq for Spell {} + +impl Hash for Spell { + fn hash(&self, state: &mut H) { + self.raw_spell.hash(state); + self.with_template.hash(state); + self.scroll_spells.hash(state); + } } impl Spell { + pub fn area(&self) -> &str { + self.parts + .as_ref() + .map(|p| &self.raw_spell[p.area.clone()]) + .unwrap_or("") + } + + pub fn focus(&self) -> &str { + self.parts + .as_ref() + .map(|p| &self.raw_spell[p.focus.clone()]) + .unwrap_or("") + } + + pub fn effects(&self) -> &str { + self.parts + .as_ref() + .map(|p| &self.raw_spell[p.effects.clone()]) + .unwrap_or("") + } + + pub fn component(&self) -> &str { + self.parts + .as_ref() + .map(|p| &self.raw_spell[p.component.clone()]) + .unwrap_or("") + } + + pub fn component_target(&self) -> &str { + self.parts + .as_ref() + .map(|p| &self.raw_spell[p.component_target.clone()]) + .unwrap_or("") + } + /// Example input: "md__{_>_p}hover:display=none" pub fn new( raw_spell: &str, shared_spells: &HashSet, scrolls: &Option>>, + span: (usize, usize), + source: Option>, ) -> Result, GrimoireCssError> { let with_template = Self::check_for_template(raw_spell); - let raw_spell = if with_template { + let raw_spell_cleaned = if with_template { raw_spell .strip_prefix("g!") .and_then(|s| s.strip_suffix(";")) @@ -58,112 +124,270 @@ impl Spell { raw_spell }; - let raw_spell_split: Vec<&str> = raw_spell.split("--").filter(|s| !s.is_empty()).collect(); + let raw_spell_split: Vec<&str> = raw_spell_cleaned + .split("--") + .filter(|s| !s.is_empty()) + .collect(); + // Template spell: keep outer spell and parse inner spells. if with_template && !raw_spell_split.is_empty() { let mut scroll_spells: Vec = Vec::new(); + for rs in raw_spell_split { - if let Some(spell) = Spell::new(rs, shared_spells, scrolls)? { - scroll_spells.push(spell); + if let Some(spell) = Spell::new(rs, shared_spells, scrolls, span, source.clone())? { + let mut spell = spell; + + // If a template part is a scroll invocation (e.g. complex-card=120px_red_100px), + // `Spell::new` will produce a *container spell* whose `scroll_spells` are the + // real property spells. + // + // For templates we want to flatten those property spells into the template list + // so the builder can generate CSS and unify the class name to the outer template. + let area = spell.area().to_string(); + let focus = spell.focus().to_string(); + let effects = spell.effects().to_string(); + + if let Some(inner_scroll_spells) = spell.scroll_spells.take() { + let has_prefix = + !area.is_empty() || !focus.is_empty() || !effects.is_empty(); + + if has_prefix { + let mut prefix = String::new(); + + if !area.is_empty() { + prefix.push_str(&area); + prefix.push_str("__"); + } + + if !focus.is_empty() { + prefix.push('{'); + prefix.push_str(&focus); + prefix.push('}'); + } + + if !effects.is_empty() { + prefix.push_str(&effects); + prefix.push(':'); + } + + for inner in inner_scroll_spells { + let combined = format!("{prefix}{}", inner.raw_spell); + if let Some(reparsed) = Spell::new( + &combined, + shared_spells, + scrolls, + span, + source.clone(), + )? { + scroll_spells.push(reparsed); + } + } + } else { + scroll_spells.extend(inner_scroll_spells); + } + } else { + scroll_spells.push(spell); + } } } return Ok(Some(Spell { - raw_spell: raw_spell.to_string(), - component: String::new(), - component_target: String::new(), - effects: String::new(), - area: String::new(), - focus: String::new(), + raw_spell: raw_spell_cleaned.to_string(), with_template, scroll_spells: Some(scroll_spells), + span, + source, + parts: None, })); } - // Split the input string by "__" to separate the area (screen size) and the rest - let (area, rest) = raw_spell.split_once("__").unwrap_or(("", raw_spell)); + let raw = raw_spell_cleaned.to_string(); + + // Parse into byte ranges within `raw`. + let mut area_range = 0..0; + let mut focus_range = 0..0; + let mut effects_range = 0..0; + + let mut rest_start = 0usize; + if let Some(pos) = raw.find("__") { + area_range = 0..pos; + rest_start = pos + 2; + } + + let mut after_focus_start = rest_start; + if rest_start < raw.len() + && let Some(close_rel) = raw[rest_start..].find('}') + { + let focus_part_start = if raw.as_bytes().get(rest_start) == Some(&b'{') { + rest_start + 1 + } else { + rest_start + }; + focus_range = focus_part_start..(rest_start + close_rel); + after_focus_start = rest_start + close_rel + 1; + } + + let mut after_effects_start = after_focus_start; + if after_focus_start < raw.len() + && let Some(colon_rel) = raw[after_focus_start..].find(':') + { + effects_range = after_focus_start..(after_focus_start + colon_rel); + after_effects_start = after_focus_start + colon_rel + 1; + } + + // component=target + if after_effects_start <= raw.len() + && let Some(eq_rel) = raw[after_effects_start..].find('=') + { + let component_range = after_effects_start..(after_effects_start + eq_rel); + let component_target_range = (after_effects_start + eq_rel + 1)..raw.len(); + + let component_target = &raw[component_target_range.clone()]; + if let Some(err) = spell_value_validator::validate_component_target(component_target) { + let message = match err { + spell_value_validator::SpellValueValidationError::UnexpectedClosingParen => { + format!( + "Invalid value '{component_target}': unexpected ')'.\n\n\ +If you intended a CSS function (e.g. calc(...)), ensure parentheses are balanced." + ) + } + spell_value_validator::SpellValueValidationError::UnclosedParen => { + format!( + "Invalid value '{component_target}': unclosed '('.\n\n\ +Common cause: spaces inside a class attribute split the spell into multiple tokens.\n\ +Fix: replace spaces with '_' inside the value, e.g.:\n\ + h=calc(100vh - 50px) -> h=calc(100vh_-_50px)" + ) + } + }; + + if let Some(src) = &source { + return Err(GrimoireCssError::CompileError { + message, + span, + label: "invalid spell value".to_string(), + help: Some( + "In HTML class attributes, spaces split classes.\n\ +Use '_' inside spell values to represent spaces." + .to_string(), + ), + source_file: Some(src.clone()), + }); + } - // Split the raw spell by "}" to get the focus and the rest - let (focus, rest) = rest - .split_once('}') - .map_or(("", rest), |(f, r)| (f.strip_prefix('{').unwrap_or(f), r)); + return Err(GrimoireCssError::InvalidInput(message)); + } - // Split the rest by ":" to get the effects (pseudo-class) and the rest - let (effects, rest) = rest.split_once(':').unwrap_or(("", rest)); + let parts = SpellParts { + area: area_range, + focus: focus_range, + effects: effects_range, + component: component_range.clone(), + component_target: component_target_range.clone(), + }; - // Split the rest by "=" to separate the component (property) and component_target (value) - if let Some((component, component_target)) = rest.split_once("=") { let mut spell = Spell { - raw_spell: raw_spell.to_string(), - component: component.to_string(), - component_target: component_target.to_string(), - effects: effects.to_string(), - area: area.to_string(), - focus: focus.to_string(), + raw_spell: raw, with_template, scroll_spells: None, + span, + source: source.clone(), + parts: Some(parts), }; - if let Some(raw_scroll_spells) = - Self::check_raw_scroll_spells(&spell.component, scrolls) - { + let component = spell.component(); + + if let Some(raw_scroll_spells) = Self::check_raw_scroll_spells(component, scrolls) { spell.scroll_spells = Self::parse_scroll( component, raw_scroll_spells, - &spell.component_target, + spell.component_target(), shared_spells, scrolls, + span, + source, )?; + } else if !component.starts_with("--") && get_css_property(component).is_none() { + let message = format!("Unknown component or scroll: '{component}'"); + if let Some(src) = &source { + return Err(GrimoireCssError::InvalidSpellFormat { + message, + span, + label: "Error in this spell".to_string(), + help: Some( + "Check that the component name exists (built-in CSS property alias) or that the scroll is defined in config.scrolls." + .to_string(), + ), + source_file: Some(src.clone()), + }); + } else { + return Err(GrimoireCssError::InvalidInput(message)); + } } return Ok(Some(spell)); - } else if let Some(raw_scroll_spells) = Self::check_raw_scroll_spells(rest, scrolls) { - return Ok(Some(Spell { - raw_spell: raw_spell.to_string(), - component: rest.to_string(), - component_target: String::new(), - effects: effects.to_string(), - area: area.to_string(), - focus: focus.to_string(), + } + + // scroll (no '=') + if after_effects_start <= raw.len() + && let Some(raw_scroll_spells) = + Self::check_raw_scroll_spells(&raw[after_effects_start..], scrolls) + { + let component_range = after_effects_start..raw.len(); + let parts = SpellParts { + area: area_range, + focus: focus_range, + effects: effects_range, + component: component_range.clone(), + component_target: 0..0, + }; + + let mut spell = Spell { + raw_spell: raw, with_template, - scroll_spells: Self::parse_scroll( - rest, - raw_scroll_spells, - "", - shared_spells, - scrolls, - )?, - })); + scroll_spells: None, + span, + source: source.clone(), + parts: Some(parts), + }; + + let component = spell.component(); + spell.scroll_spells = Self::parse_scroll( + component, + raw_scroll_spells, + "", + shared_spells, + scrolls, + span, + source, + )?; + + return Ok(Some(spell)); } Ok(None) // Return None if format is invalid } - fn check_for_template(class_name: &str) -> bool { - class_name.starts_with("g!") && class_name.ends_with(";") + fn check_for_template(raw_spell: &str) -> bool { + raw_spell.starts_with("g!") && raw_spell.ends_with(';') } fn check_raw_scroll_spells<'a>( - spell_component: &'a str, + scroll_name: &str, scrolls: &'a Option>>, ) -> Option<&'a Vec> { - if get_css_property(spell_component).is_some() { - return None; - } - - if let Some(scrolls) = scrolls { - return scrolls.get(spell_component); - }; - - None + scrolls.as_ref()?.get(scroll_name) } + #[allow(clippy::too_many_arguments)] fn parse_scroll( scroll_name: &str, raw_scroll_spells: &[String], component_target: &str, shared_spells: &HashSet, scrolls: &Option>>, + span: (usize, usize), + source: Option>, ) -> Result>, GrimoireCssError> { if raw_scroll_spells.is_empty() { return Ok(None); @@ -181,7 +405,7 @@ impl Spell { for raw_spell in raw_scroll_spells.iter() { if raw_spell.contains("=$") { - if count_of_used_variables > scroll_variables.len() - 1 { + if count_of_used_variables > scroll_variables.len().saturating_sub(1) { break; } @@ -190,20 +414,44 @@ impl Spell { format!("={}", scroll_variables[count_of_used_variables]).as_str(), ); - if let Ok(Some(spell)) = Spell::new(&variabled_raw_spell, shared_spells, scrolls) { + if let Ok(Some(spell)) = Spell::new( + &variabled_raw_spell, + shared_spells, + scrolls, + span, + source.clone(), + ) { spells.push(spell); } count_of_used_variables += 1; - } else if let Ok(Some(spell)) = Spell::new(raw_spell, shared_spells, scrolls) { + } else if let Ok(Some(spell)) = + Spell::new(raw_spell, shared_spells, scrolls, span, source.clone()) + { spells.push(spell); } } if count_of_used_variables != count_of_variables { - return Err(GrimoireCssError::InvalidInput(format!( - "Not all variables used in scroll '{scroll_name}'. Expected {count_of_variables}, but used {count_of_used_variables}", - ))); + let message = format!( + "Variable count mismatch for scroll '{scroll_name}'. Provided {count_of_variables} arguments, but scroll definition uses {count_of_used_variables}", + ); + + if let Some(src) = &source { + return Err(GrimoireCssError::InvalidSpellFormat { + message, + span, + label: "Error in this spell".to_string(), + help: Some( + "Pass exactly N arguments separated by '_' (underscores).\n\ +Example: complex-card=arg1_arg2_arg3" + .to_string(), + ), + source_file: Some(src.clone()), + }); + } else { + return Err(GrimoireCssError::InvalidInput(message)); + } } if spells.is_empty() { @@ -214,15 +462,16 @@ impl Spell { } pub fn generate_spells_from_classes( - css_classes: Vec, + css_classes: Vec<(String, (usize, usize))>, shared_spells: &HashSet, scrolls: &Option>>, + source: Option>, ) -> Result, GrimoireCssError> { let mut spells = Vec::with_capacity(css_classes.len()); - for cs in css_classes { + for (cs, span) in css_classes { if !shared_spells.contains(&cs) - && let Some(spell) = Spell::new(&cs, shared_spells, scrolls)? + && let Some(spell) = Spell::new(&cs, shared_spells, scrolls, span, source.clone())? { spells.push(spell); } @@ -234,24 +483,76 @@ impl Spell { #[cfg(test)] mod tests { + use crate::core::source_file::SourceFile; use crate::core::spell::Spell; use std::collections::{HashMap, HashSet}; + use std::sync::Arc; #[test] fn test_multiple_raw_spells_in_template() { let shared_spells = HashSet::new(); let scrolls: Option>> = None; let raw = "g!color=red--display=flex;"; - let spell = Spell::new(raw, &shared_spells, &scrolls) + let spell = Spell::new(raw, &shared_spells, &scrolls, (0, 0), None) .expect("parse ok") .expect("not None"); assert!(spell.with_template); assert!(spell.scroll_spells.is_some()); let spells = spell.scroll_spells.as_ref().unwrap(); assert_eq!(spells.len(), 2); - assert_eq!(spells[0].component, "color"); - assert_eq!(spells[0].component_target, "red"); - assert_eq!(spells[1].component, "display"); - assert_eq!(spells[1].component_target, "flex"); + assert_eq!(spells[0].component(), "color"); + assert_eq!(spells[0].component_target(), "red"); + assert_eq!(spells[1].component(), "display"); + assert_eq!(spells[1].component_target(), "flex"); + } + + #[test] + fn test_scroll_can_be_used_inside_template_attribute() { + let shared_spells = HashSet::new(); + let mut scrolls_map: HashMap> = HashMap::new(); + scrolls_map.insert( + "complex-card".to_string(), + vec!["h=$".to_string(), "c=$".to_string(), "w=$".to_string()], + ); + let scrolls = Some(scrolls_map); + + // This is the desired HTML usage pattern: use scroll invocation via g! ... ; + // (i.e. not inside class="..."). + let raw = "g!complex-card=120px_red_100px;"; + let spell = Spell::new(raw, &shared_spells, &scrolls, (0, 0), None) + .expect("parse ok") + .expect("not None"); + + assert!(spell.with_template); + let spells = spell.scroll_spells.as_ref().expect("template spells"); + assert_eq!(spells.len(), 3); + assert_eq!(spells[0].component(), "h"); + assert_eq!(spells[0].component_target(), "120px"); + assert_eq!(spells[1].component(), "c"); + assert_eq!(spells[1].component_target(), "red"); + assert_eq!(spells[2].component(), "w"); + assert_eq!(spells[2].component_target(), "100px"); + } + + #[test] + fn test_non_grimoire_plain_class_is_ignored() { + let shared_spells = HashSet::new(); + let scrolls: Option>> = None; + + // Plain CSS class (no '=') must not be treated as a spell. + let spell = Spell::new( + "red", + &shared_spells, + &scrolls, + (12, 3), + Some(Arc::new(SourceFile::new( + None, + "test".to_string(), + "
".to_string(), + ))), + ) + .expect("parsing must not fail"); + + assert!(spell.is_none()); } } diff --git a/src/core/spell_value_validator.rs b/src/core/spell_value_validator.rs new file mode 100644 index 0000000..27c48eb --- /dev/null +++ b/src/core/spell_value_validator.rs @@ -0,0 +1,45 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpellValueValidationError { + UnexpectedClosingParen, + UnclosedParen, +} + +pub fn validate_component_target(component_target: &str) -> Option { + let mut depth: i32 = 0; + let mut in_single_quote = false; + let mut in_double_quote = false; + let mut escape_next = false; + + for ch in component_target.chars() { + if escape_next { + escape_next = false; + continue; + } + + match ch { + '\\' if in_single_quote || in_double_quote => { + escape_next = true; + } + '\'' if !in_double_quote => { + in_single_quote = !in_single_quote; + } + '"' if !in_single_quote => { + in_double_quote = !in_double_quote; + } + '(' if !in_single_quote && !in_double_quote => depth += 1, + ')' if !in_single_quote && !in_double_quote => { + depth -= 1; + if depth < 0 { + return Some(SpellValueValidationError::UnexpectedClosingParen); + } + } + _ => {} + } + } + + if depth != 0 { + return Some(SpellValueValidationError::UnclosedParen); + } + + None +} diff --git a/src/infrastructure/diagnostics.rs b/src/infrastructure/diagnostics.rs new file mode 100644 index 0000000..119a46f --- /dev/null +++ b/src/infrastructure/diagnostics.rs @@ -0,0 +1,175 @@ +use crate::core::{GrimoireCssError, SourceFile}; +use miette::{Diagnostic, LabeledSpan, SourceCode}; +use std::sync::Arc; +use thiserror::Error; + +fn named_source_from(source: &Arc) -> miette::NamedSource { + let content = if let Some(content) = &source.content { + (**content).clone() + } else if let Some(path) = &source.path { + std::fs::read_to_string(path).unwrap_or_default() + } else { + String::new() + }; + + miette::NamedSource::new(source.name.clone(), content) +} + +#[derive(Debug, Error)] +pub enum GrimoireCssDiagnostic { + #[error("IO error: {0}")] + Io(String), + + #[error("Regex error: {0}")] + Regex(String), + + #[error("Serialization/Deserialization error: {0}")] + Serde(String), + + #[error("Invalid input: {0}")] + InvalidInput(String), + + #[error("Invalid path: {0}")] + InvalidPath(String), + + #[error("Glob pattern error: {0}")] + GlobPatternError(String), + + #[error("Runtime error: {0}")] + RuntimeError(String), + + #[error("CSS Optimization failed: {0}")] + OptimizationError(String), + + #[error("Invalid spell format: {message}")] + InvalidSpellFormat { + message: String, + src: miette::NamedSource, + span: (usize, usize), + label: String, + help: Option, + }, + + #[error("{message}")] + CompileError { + message: String, + src: miette::NamedSource, + span: (usize, usize), + label: String, + help: Option, + }, +} + +impl Diagnostic for GrimoireCssDiagnostic { + fn code<'a>(&'a self) -> Option> { + match self { + GrimoireCssDiagnostic::Io(_) => Some(Box::new("grimoire_css::io")), + GrimoireCssDiagnostic::Regex(_) => Some(Box::new("grimoire_css::regex")), + GrimoireCssDiagnostic::Serde(_) => Some(Box::new("grimoire_css::serde")), + GrimoireCssDiagnostic::InvalidInput(_) => Some(Box::new("grimoire_css::invalid_input")), + GrimoireCssDiagnostic::InvalidPath(_) => Some(Box::new("grimoire_css::invalid_path")), + GrimoireCssDiagnostic::GlobPatternError(_) => { + Some(Box::new("grimoire_css::glob_pattern")) + } + GrimoireCssDiagnostic::RuntimeError(_) => Some(Box::new("grimoire_css::runtime")), + GrimoireCssDiagnostic::OptimizationError(_) => { + Some(Box::new("grimoire_css::optimization")) + } + GrimoireCssDiagnostic::InvalidSpellFormat { .. } => { + Some(Box::new("grimoire_css::invalid_spell_format")) + } + GrimoireCssDiagnostic::CompileError { .. } => { + Some(Box::new("grimoire_css::compile_error")) + } + } + } + + fn help<'a>(&'a self) -> Option> { + match self { + GrimoireCssDiagnostic::InvalidSpellFormat { help, .. } + | GrimoireCssDiagnostic::CompileError { help, .. } => help + .as_deref() + .map(|h| Box::new(h) as Box), + _ => None, + } + } + + fn source_code(&self) -> Option<&dyn SourceCode> { + match self { + GrimoireCssDiagnostic::InvalidSpellFormat { src, .. } + | GrimoireCssDiagnostic::CompileError { src, .. } => Some(src), + _ => None, + } + } + + fn labels(&self) -> Option + '_>> { + match self { + GrimoireCssDiagnostic::InvalidSpellFormat { span, label, .. } + | GrimoireCssDiagnostic::CompileError { span, label, .. } => { + Some(Box::new(std::iter::once( + LabeledSpan::new_primary_with_span(Some(label.clone()), *span), + ))) + } + _ => None, + } + } +} + +impl From<&GrimoireCssError> for GrimoireCssDiagnostic { + fn from(value: &GrimoireCssError) -> Self { + match value { + GrimoireCssError::Io(e) => GrimoireCssDiagnostic::Io(e.to_string()), + GrimoireCssError::Regex(e) => GrimoireCssDiagnostic::Regex(e.to_string()), + GrimoireCssError::Serde(e) => GrimoireCssDiagnostic::Serde(e.to_string()), + GrimoireCssError::InvalidInput(msg) => GrimoireCssDiagnostic::InvalidInput(msg.clone()), + GrimoireCssError::InvalidPath(msg) => GrimoireCssDiagnostic::InvalidPath(msg.clone()), + GrimoireCssError::GlobPatternError(msg) => { + GrimoireCssDiagnostic::GlobPatternError(msg.clone()) + } + GrimoireCssError::RuntimeError(msg) => GrimoireCssDiagnostic::RuntimeError(msg.clone()), + GrimoireCssError::OptimizationError(msg) => { + GrimoireCssDiagnostic::OptimizationError(msg.clone()) + } + GrimoireCssError::InvalidSpellFormat { + message, + span, + label, + help, + source_file, + } => { + let src = source_file + .as_ref() + .map(named_source_from) + .unwrap_or_else(|| miette::NamedSource::new("unknown", "".to_string())); + + GrimoireCssDiagnostic::InvalidSpellFormat { + message: message.clone(), + src, + span: *span, + label: label.clone(), + help: help.clone(), + } + } + GrimoireCssError::CompileError { + message, + span, + label, + help, + source_file, + } => { + let src = source_file + .as_ref() + .map(named_source_from) + .unwrap_or_else(|| miette::NamedSource::new("unknown", "".to_string())); + + GrimoireCssDiagnostic::CompileError { + message: message.clone(), + src, + span: *span, + label: label.clone(), + help: help.clone(), + } + } + } + } +} diff --git a/src/infrastructure/lightning_css_optimizer.rs b/src/infrastructure/lightning_css_optimizer.rs index 4036753..7b1b5b5 100644 --- a/src/infrastructure/lightning_css_optimizer.rs +++ b/src/infrastructure/lightning_css_optimizer.rs @@ -7,7 +7,7 @@ use lightningcss::{ stylesheet::{MinifyOptions, ParserOptions, StyleSheet}, targets::{Browsers, Targets}, }; -use std::{env, fs, path::Path}; +use std::{fs, path::Path}; use crate::{ buffer::add_message, @@ -47,12 +47,6 @@ impl LightningCssOptimizer { add_message("Created missing '.browserslistrc' file with 'defaults'".to_string()); } - // SAFETY: We're setting an environment variable in a controlled manner. - // This is safe as long as no other threads are concurrently reading this variable. - unsafe { - env::set_var("BROWSERSLIST_CONFIG", &browserslist_config_path); - } - let content = fs::read_to_string(&browserslist_config_path) .expect("Failed to read '.browserslistrc' file"); @@ -79,8 +73,9 @@ impl CssOptimizer for LightningCssOptimizer { /// /// Returns a `Result` containing the optimized CSS string or a `GrimoireCSSError` if optimization fails. fn optimize(&self, raw_css: &str) -> Result { - let mut stylesheet = StyleSheet::parse(raw_css, ParserOptions::default()) - .map_err(|e| GrimoireCssError::InvalidInput(format!("Failed to parse CSS: {e}")))?; + let mut stylesheet = StyleSheet::parse(raw_css, ParserOptions::default()).map_err(|e| { + GrimoireCssError::OptimizationError(format!("Failed to parse CSS: {e}")) + })?; // Apply minification and optimization based on the browser targets. stylesheet @@ -88,7 +83,9 @@ impl CssOptimizer for LightningCssOptimizer { targets: self.targets, unused_symbols: Default::default(), }) - .map_err(|e| GrimoireCssError::InvalidInput(format!("Failed to minify CSS: {e}")))?; + .map_err(|e| { + GrimoireCssError::OptimizationError(format!("Failed to minify CSS: {e}")) + })?; // Generate the final CSS as a string. stylesheet @@ -97,6 +94,14 @@ impl CssOptimizer for LightningCssOptimizer { ..Default::default() }) .map(|res| res.code) - .map_err(|e| GrimoireCssError::InvalidInput(format!("Failed to generate CSS: {e}"))) + .map_err(|e| { + GrimoireCssError::OptimizationError(format!("Failed to generate CSS: {e}")) + }) + } + + fn validate(&self, raw_css: &str) -> Result<(), GrimoireCssError> { + StyleSheet::parse(raw_css, ParserOptions::default()) + .map(|_| ()) + .map_err(|e| GrimoireCssError::OptimizationError(format!("Failed to parse CSS: {e}"))) } } diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index a954821..61d32a3 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -1,6 +1,8 @@ //! The `infrastructure` module provides integration with external libraries and services //! that power Grimoire CSS's core functionality. This includes CSS optimization, //! minification, and other low-level operations that require external dependencies. +pub mod diagnostics; pub mod lightning_css_optimizer; +pub use diagnostics::*; pub use lightning_css_optimizer::*; diff --git a/src/lib.rs b/src/lib.rs index b7fdbe0..ed8339a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,8 @@ use commands::{handle_in_memory, process_mode_and_handle}; use console::style; use core::{compiled_css::CompiledCssInMemory, config::ConfigInMemory}; use indicatif::{ProgressBar, ProgressStyle}; -use infrastructure::LightningCssOptimizer; +use infrastructure::{GrimoireCssDiagnostic, LightningCssOptimizer}; +use miette::GraphicalReportHandler; use std::time::{Duration, Instant}; pub use core::{GrimoireCssError, color, component, config, spell::Spell}; @@ -192,7 +193,15 @@ pub fn start_as_cli(args: Vec) -> Result<(), GrimoireCssError> { print!("\r\x1b[2K{GRIMM_CURSED}\n"); println!(); - println!("{} {}", style(" Cursed! ").white().on_red().bright(), e); + println!("{}", style(" Cursed! ").white().on_red().bright()); + println!(); + + let diagnostic: GrimoireCssDiagnostic = (&e).into(); + let mut out = String::new(); + GraphicalReportHandler::new() + .render_report(&mut out, &diagnostic) + .unwrap(); + println!("{out}"); Err(e) } diff --git a/src/main.rs b/src/main.rs index ffe6611..59b33a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,13 @@ use grimoire_css_lib::start_as_cli; use std::env; +#[cfg(feature = "heap-profile")] +use dhat::Alloc; + +#[cfg(feature = "heap-profile")] +#[global_allocator] +static ALLOC: Alloc = Alloc; + /// The entry point for the Grimoire CSS system (CLI). /// /// This function: @@ -15,6 +22,9 @@ use std::env; /// logging, error styling, spinners, and time measurements. /// - If an error is encountered, it exits with a non-zero status code. fn main() { + #[cfg(feature = "heap-profile")] + let _profiler = dhat::Profiler::new_heap(); + let args: Vec = env::args().collect(); // By calling `start_as_cli`, we rely on the library's built-in logging,