diff --git a/.gitignore b/.gitignore index cd7810d..c5093d1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ benchmark/input benchmark/grimoire_css_output benchmark/tailwind_css_output benchmark/node_modules + +test_app diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d4036..0660e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,4 +109,4 @@ **Initial Release** -Grimoire CSS debuts as a powerful CSS system engine designed for flexibility and performance. +Grimoire CSS debuts as a powerful CSS engine designed for flexibility and performance. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfbe3fd..5cb29d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ - [4. Finalizing a Release](#4-finalizing-a-release) - [5. Handling Hotfixes](#5-handling-hotfixes) - [6. Handling Chores](#6-handling-chores) - - [7. Updating Feature and Fix Branches](#7-updating-feature-and-fix-branches) + - [7. Updating Feature, Refactor, and Fix Branches](#7-updating-feature-refactor-and-fix-branches) - [Automated Tagging and Publishing](#automated-tagging-and-publishing) - [Repository Security and Permissions](#repository-security-and-permissions) - [Project Management](#project-management) @@ -62,6 +62,10 @@ We follow a structured Git workflow to manage the lifecycle of code and contribu - Naming convention: `hotfix/{hotfix-description}` +- **Refactor Branches**: Branches for code refactoring that improves code structure, readability, or performance without changing functionality. + + - Naming convention: `refactor/{refactor-description}` + - **Chore Branches**: Branches for maintenance tasks such as updating documentation, CI configurations, or other non-feature/non-fix changes. - Naming convention: `chore/{chore-description}` @@ -88,6 +92,10 @@ We encourage contributors to propose new features, fixes, or chores, even if the ```bash git checkout -b fix/{fix-description} main ``` + - For refactoring: + ```bash + git checkout -b refactor/{refactor-description} main + ``` - For hotfixes: ```bash git checkout -b hotfix/{hotfix-description} main @@ -105,7 +113,7 @@ We encourage contributors to propose new features, fixes, or chores, even if the - **Open a Pull Request**: - **Target Branch**: - - For features and regular fixes: + - For features, refactoring, and regular fixes: - Open a PR against the appropriate `rc/{version}` branch. - If unsure which release your contribution will fit into, you can initially target your PR to `main`, and maintainers will retarget it as needed. - For hotfixes and chores: @@ -131,9 +139,9 @@ We encourage contributors to propose new features, fixes, or chores, even if the git checkout -b rc/1.2.0 main ``` -- **Merging Feature and Fix Branches**: +- **Merging Feature, Refactor, and Fix Branches**: - - Maintainers will merge feature (`feature/**`) and fix (`fix/**`) branches intended for the release into the `rc/{version}` branch via pull requests. + - Maintainers will merge feature (`feature/**`), refactor (`refactor/**`), and fix (`fix/**`) branches intended for the release into the `rc/{version}` branch via pull requests. - **Testing and Stabilization**: - Perform thorough testing on the `rc/{version}` branch. @@ -224,10 +232,10 @@ We encourage contributors to propose new features, fixes, or chores, even if the - **Note**: - Merging `chore/**` branches into `main` will **not** trigger tagging or a new release. -#### 7. Updating Feature and Fix Branches +#### 7. Updating Feature, Refactor, and Fix Branches - **Sync with `main`**: - - Regularly merge `main` into your feature (`feature/**`) and fix (`fix/**`) branches to keep them up to date. + - Regularly merge `main` into your feature (`feature/**`), refactor (`refactor/**`), and fix (`fix/**`) branches to keep them up to date. ```bash git checkout feature/{feature-name} git merge main diff --git a/Cargo.lock b/Cargo.lock index cf6587a..3a39f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -169,6 +219,52 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "console" version = "0.15.8" @@ -385,10 +481,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "grimoire_css" -version = "1.5.0" +version = "1.6.0" dependencies = [ "console", "glob", + "grimoire_css_color_toolkit", "indicatif", "lazy_static", "lightningcss", @@ -399,6 +496,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "grimoire_css_color_toolkit" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496f6157cfdc7bb03852a87a00ba37445b737e8354a9cec0c49d2832eb28ef6a" +dependencies = [ + "clap", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -414,6 +520,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -470,6 +582,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -625,6 +743,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "outref" version = "0.1.0" @@ -1052,6 +1176,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -1146,6 +1276,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index d136ace..a43291e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "grimoire_css" -version = "1.5.0" -edition = "2021" +version = "1.6.0" +edition = "2024" +rust-version = "1.88" authors = ["Dmitrii Shatokhin "] -description = "A magical CSS system engine for all environments" +description = "A magical CSS engine for all environments" license = "MIT" keywords = ["css", "css-compiler", "styling", "web", "system"] categories = ["web-programming", "development-tools"] @@ -29,6 +30,7 @@ codegen-units = 1 [dependencies] console = "0.15.8" glob = "0.3.1" +grimoire_css_color_toolkit = "1.0.0" indicatif = "0.17.8" lazy_static = "1.5.0" lightningcss = { version = "1.0.0-alpha.59", features = ["browserslist"] } diff --git a/README.md b/README.md index 04c7049..13b256b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ --- -**Grimoire CSS** is a comprehensive CSS system engine crafted in Rust,
focusing on unmatched flexibility, reusable dynamic styling, and optimized performance for every environment. Whether you need filesystem-based CSS generation or pure in-memory processing, Grimoire CSS adapts to your needs without compromising on performance or features. +**Grimoire CSS** is a comprehensive CSS engine crafted in Rust,
focusing on unmatched flexibility, reusable dynamic styling, and optimized performance for every environment. Whether you need filesystem-based CSS generation or pure in-memory processing, Grimoire CSS adapts to your needs without compromising on performance or features.
@@ -75,9 +75,9 @@ # Why Grimoire CSS -**Grimoire CSS** is a comprehensive CSS system engine crafted in Rust,
focusing on unmatched flexibility, reusable dynamic styling, and optimized performance for every environment. Whether you need filesystem-based CSS generation or pure in-memory processing, Grimoire CSS adapts to your needs without compromising on performance or features. +**Grimoire CSS** is a comprehensive CSS engine crafted in Rust,
focusing on unmatched flexibility, reusable dynamic styling, and optimized performance for every environment. Whether you need filesystem-based CSS generation or pure in-memory processing, Grimoire CSS adapts to your needs without compromising on performance or features. -1. **True CSS System Engine.** Grimoire CSS is a one-of-a-kind CSS System Engine - exceptionally powerful and endlessly flexible. It doesn't rely on bundlers, optimizers, deduplicators, preprocessors, or postprocessors. Grimoire CSS handles everything itself, internally and efficiently. +1. **True CSS engine.** Grimoire CSS is a one-of-a-kind CSS engine - exceptionally powerful and endlessly flexible. It doesn't rely on bundlers, optimizers, deduplicators, preprocessors, or postprocessors. Grimoire CSS handles everything itself, internally and efficiently. 2. **Performance.** Despite its vast capabilities, every part of Grimoire CSS is optimized for maximum efficiency. It outperforms even specialized tools. Compared to TailwindCSS v4.x, it generates CSS **5× faster** and **28× more efficiently**, even considering that Grimoire CSS actually generates real CSS. 3. **Universality.** Its native parser handles any source file without plugins or configuration, running in both filesystem-based and in-memory modes. Available as a standalone binary, a Rust crate, and an npm library. 4. **Intelligent CSS Generation.** Grimoire CSS respects the CSS cascade, optimizes your styles, applies necessary prefixes, and ensures full browser compatibility via .browserslistrc, both in file system and in-memory modes. diff --git a/RELEASES.md b/RELEASES.md index cc3016f..ca97b8a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,87 @@ This document combines all release notes in chronological order, providing a com --- +# v1.6.0 Chromaspire: The Color Convergence + +Grimoire CSS refines its arcane precision with **Chromaspire**, a release dedicated to mastery over color and stability. With a fully decoupled color toolkit, improved resilience, and groundwork for precise float-based styling, spellcasters now wield both grace and robustness. + +## Key Highlights + +- **Decoupled Color System**: Color module extracted to `grimoire_css_color_toolkit` crate for independent usage and improved maintainability. +- **Enhanced Parser Support**: Comprehensive support for curly bracket class syntax (`class={}`, `className={}`) with robust nested bracket handling. +- **Float-Based Precision**: Migration from `u32` to `f64` for unit handling, enabling precise floating-point calculations for responsive units. +- **Modern Rust Standards**: Upgraded to Rust Edition 2024 with minimum version 1.88 for enhanced language features and performance. +- **Improved String Formatting**: Modernized string interpolation using the latest Rust formatting conventions. +- **Enhanced Documentation**: Refined project description and branding consistency across the ecosystem. +- **Robust Refactoring Support**: Added comprehensive support for refactor branches in contribution workflow. + +## Full Details + +### Color System Decoupling + +- **Independent Color Toolkit**: Extracted the complete color module to `grimoire_css_color_toolkit` v1.0.0 as a standalone crate. +- **CSS Color Module Level 4 Compliance**: Maintained full compliance with CSS Color specifications for `rgb()`, `hsl()`, `hwb()`, hex codes, and named colors. +- **External Availability**: Color toolkit now available for independent use in other projects requiring CSS-compliant color parsing and manipulation. +- **Seamless Integration**: Existing color functionality remains fully accessible through the main Grimoire CSS module. + +### Parser Enhancements + +- **Curly Bracket Class Support**: Added comprehensive parsing for `class={}` and `className={}` syntax patterns. +- **Nested Bracket Handling**: Robust regex patterns that correctly handle nested curly brackets within class declarations. +- **Framework Compatibility**: Enhanced support for modern JavaScript frameworks and CSS-in-JS solutions. +- **Collection Type Management**: Implemented `CollectionType` enum for precise handling of different class collection scenarios. + +### Precision and Performance + +- **Float-Based Units**: Migrated unit handling from `u32` to `f64` for precise floating-point calculations. +- **Responsive Design Support**: Enhanced accuracy for `mfs`/`mrs` (minimum/maximum font-size) and other fluid sizing calculations. +- **Mathematical Precision**: Improved handling of complex responsive calculations with decimal precision. +- **Memory Efficiency**: Optimized data structures while maintaining calculation accuracy. + +### Language and Tooling Modernization + +- **Rust Edition 2024**: Upgraded to the latest Rust edition for access to newest language features and optimizations. +- **Minimum Rust Version**: Set minimum supported Rust version to 1.88 for stability and security. +- **Modern String Formatting**: Migrated to contemporary Rust string interpolation patterns for improved readability and performance. +- **Dependency Updates**: Updated Clap to v4.5.41 with enhanced CLI argument parsing capabilities. + +### Documentation + +- **Consistent Terminology**: Standardized project description as "A magical CSS engine" across all documentation. +- **Enhanced Contributing Guidelines**: Added comprehensive support for refactor branches in the development workflow. + +## Migration Notes + +### For Library Users + +- **Color Module**: If using color functions directly, update imports to reference the new `grimoire_css_color_toolkit` crate or continue using through the main module. +- **Unit Calculations**: Float-based calculations may produce slightly different results due to improved precision. +- **No Breaking Changes**: All existing APIs remain compatible with previous versions. + +### For Contributors + +- **Refactor Branches**: New `refactor/{description}` branch naming convention available for code improvement contributions. +- **Modern Rust**: Development now requires Rust 1.88+ for optimal experience. +- **Testing**: Enhanced test coverage for curly bracket parsing and float-based calculations. + +## Technical Improvements + +### Core Architecture + +- **Modular Design**: Color system decoupling improves overall architecture and reduces main crate complexity. +- **Type Safety**: Enhanced type safety with `CollectionType` enum for parser state management. +- **Error Handling**: Improved error handling for complex parsing scenarios. +- **Code Organization**: Better separation of concerns between core functionality and specialized modules. + +### Parser Robustness + +- **Regex Optimization**: Efficient regex patterns for curly bracket class detection with proper nesting support. +- **Quote Handling**: Enhanced quote detection and matching for complex class declarations. +- **State Management**: Improved parser state tracking for reliable multi-pattern matching. +- **Performance**: Optimized parsing pipeline for faster processing of complex markup. + +--- + # v1.5.0 Arcane Nexus: Unified Spellcraft Grimoire CSS continues its magical ascendance with the **v1.5.0 Arcane Nexus** release, forging powerful new commands, extensible configurations, advanced template syntax, and a unified ecosystem that binds the circle of spellcasters together. This update focuses on seamless migration, modular scroll and variable support, a public color toolkit, and a next-generation Transmutator CLI & web UI - all while refining performance, parsing reliability, and community engagement. @@ -276,9 +357,3 @@ Grimoire CSS takes a major leap forward with the **v1.1.0 Arcana** release, brin - Usage examples. - Basic configuration guidelines. - Improved logo. - ---- - -# v1.0.0: Initial Release - -The debut release of Grimoire CSS, introducing a powerful CSS system engine designed for flexibility and performance. diff --git a/releases/v1.6.0.md b/releases/v1.6.0.md new file mode 100644 index 0000000..c6bf73c --- /dev/null +++ b/releases/v1.6.0.md @@ -0,0 +1,78 @@ +# v1.6.0 Chromaspire: The Color Convergence + +Grimoire CSS refines its arcane precision with **Chromaspire**, a release dedicated to mastery over color and stability. With a fully decoupled color toolkit, improved resilience, and groundwork for precise float-based styling, spellcasters now wield both grace and robustness. + +## Key Highlights + +- **Decoupled Color System**: Color module extracted to `grimoire_css_color_toolkit` crate for independent usage and improved maintainability. +- **Enhanced Parser Support**: Comprehensive support for curly bracket class syntax (`class={}`, `className={}`) with robust nested bracket handling. +- **Float-Based Precision**: Migration from `u32` to `f64` for unit handling, enabling precise floating-point calculations for responsive units. +- **Modern Rust Standards**: Upgraded to Rust Edition 2024 with minimum version 1.88 for enhanced language features and performance. +- **Improved String Formatting**: Modernized string interpolation using the latest Rust formatting conventions. +- **Enhanced Documentation**: Refined project description and branding consistency across the ecosystem. +- **Robust Refactoring Support**: Added comprehensive support for refactor branches in contribution workflow. + +## Full Details + +### Color System Decoupling + +- **Independent Color Toolkit**: Extracted the complete color module to `grimoire_css_color_toolkit` v1.0.0 as a standalone crate. +- **CSS Color Module Level 4 Compliance**: Maintained full compliance with CSS Color specifications for `rgb()`, `hsl()`, `hwb()`, hex codes, and named colors. +- **External Availability**: Color toolkit now available for independent use in other projects requiring CSS-compliant color parsing and manipulation. +- **Seamless Integration**: Existing color functionality remains fully accessible through the main Grimoire CSS module. + +### Parser Enhancements + +- **Curly Bracket Class Support**: Added comprehensive parsing for `class={}` and `className={}` syntax patterns. +- **Nested Bracket Handling**: Robust regex patterns that correctly handle nested curly brackets within class declarations. +- **Framework Compatibility**: Enhanced support for modern JavaScript frameworks and CSS-in-JS solutions. +- **Collection Type Management**: Implemented `CollectionType` enum for precise handling of different class collection scenarios. + +### Precision and Performance + +- **Float-Based Units**: Migrated unit handling from `u32` to `f64` for precise floating-point calculations. +- **Responsive Design Support**: Enhanced accuracy for `mfs`/`mrs` (minimum/maximum font-size) and other fluid sizing calculations. +- **Mathematical Precision**: Improved handling of complex responsive calculations with decimal precision. +- **Memory Efficiency**: Optimized data structures while maintaining calculation accuracy. + +### Language and Tooling Modernization + +- **Rust Edition 2024**: Upgraded to the latest Rust edition for access to newest language features and optimizations. +- **Minimum Rust Version**: Set minimum supported Rust version to 1.88 for stability and security. +- **Modern String Formatting**: Migrated to contemporary Rust string interpolation patterns for improved readability and performance. +- **Dependency Updates**: Updated Clap to v4.5.41 with enhanced CLI argument parsing capabilities. + +### Documentation + +- **Consistent Terminology**: Standardized project description as "A magical CSS engine" across all documentation. +- **Enhanced Contributing Guidelines**: Added comprehensive support for refactor branches in the development workflow. + +## Migration Notes + +### For Library Users + +- **Color Module**: If using color functions directly, update imports to reference the new `grimoire_css_color_toolkit` crate or continue using through the main module. +- **Unit Calculations**: Float-based calculations may produce slightly different results due to improved precision. +- **No Breaking Changes**: All existing APIs remain compatible with previous versions. + +### For Contributors + +- **Refactor Branches**: New `refactor/{description}` branch naming convention available for code improvement contributions. +- **Modern Rust**: Development now requires Rust 1.88+ for optimal experience. +- **Testing**: Enhanced test coverage for curly bracket parsing and float-based calculations. + +## Technical Improvements + +### Core Architecture + +- **Modular Design**: Color system decoupling improves overall architecture and reduces main crate complexity. +- **Type Safety**: Enhanced type safety with `CollectionType` enum for parser state management. +- **Error Handling**: Improved error handling for complex parsing scenarios. +- **Code Organization**: Better separation of concerns between core functionality and specialized modules. + +### Parser Robustness + +- **Regex Optimization**: Efficient regex patterns for curly bracket class detection with proper nesting support. +- **Quote Handling**: Enhanced quote detection and matching for complex class declarations. +- **State Management**: Improved parser state tracking for reliable multi-pattern matching. +- **Performance**: Optimized parsing pipeline for faster processing of complex markup. diff --git a/scripts/generate_releases.sh b/scripts/generate_releases.sh index ee75953..454f462 100755 --- a/scripts/generate_releases.sh +++ b/scripts/generate_releases.sh @@ -37,11 +37,11 @@ if ! grep -q "v1.0.0" "$temp_file"; then # v1.0.0: Initial Release -The debut release of Grimoire CSS, introducing a powerful CSS system engine designed for flexibility and performance. +The debut release of Grimoire CSS, introducing a powerful CSS engine designed for flexibility and performance. EOF fi # Replace the old RELEASES.md with new content mv "$temp_file" RELEASES.md -echo "RELEASES.md has been updated with full release notes in chronological order." \ No newline at end of file +echo "RELEASES.md has been updated with full release notes in chronological order." diff --git a/src/commands/init.rs b/src/commands/init.rs index f334d27..c51dd3e 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -32,7 +32,7 @@ pub fn init(current_dir: &Path, mode: &str) -> Result match err { GrimoireCssError::Serde(_) => { - let err_msg = format!("Failed to parse config. {}", err); + let err_msg = format!("Failed to parse config. {err}"); Err(GrimoireCssError::InvalidInput(err_msg)) } _ => { diff --git a/src/commands/shorten.rs b/src/commands/shorten.rs index 1346063..6817144 100644 --- a/src/commands/shorten.rs +++ b/src/commands/shorten.rs @@ -1,7 +1,7 @@ use crate::buffer::add_message; use crate::commands::init::init; use crate::core::parser::parser_fs::ParserFs; -use crate::core::{component::get_shorten_component, spell::Spell, GrimoireCssError}; +use crate::core::{GrimoireCssError, component::get_shorten_component, spell::Spell}; use std::fs; use std::path::{Path, PathBuf}; @@ -120,7 +120,7 @@ pub fn shorten(current_dir: &Path) -> Result<(), GrimoireCssError> { if unit == "B" { format!("{} {}", value as isize, unit) } else { - format!("{:.2} {}", value, unit) + format!("{value:.2} {unit}") } } Ok(()) diff --git a/src/core/color.rs b/src/core/color.rs deleted file mode 100644 index d8e14eb..0000000 --- a/src/core/color.rs +++ /dev/null @@ -1,1695 +0,0 @@ -//! This module provides color parsing and manipulation strictly following the -//! [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/) specification. -//! -//! Unlike SASS, which sometimes deviates or extends CSS behaviors, this module -//! implements color functions and parsing rules in accordance with the standard CSS spec, -//! ensuring predictable and interoperable color handling for web-oriented applications. -//! -//! # Overview -//! - `Color` struct: Represents an RGBA color with optional transparency. -//! - Named colors: Predefined according to the CSS spec (e.g., `"red"`, `"blue"`). -//! - CSS-like parsing: Supports hex codes, `rgb()/hsl()/hwb()` notations, etc. -//! - Color transformations: `grayscale()`, `lighten()`, `darken()`, etc. (modeled after -//! CSS, not SASS). -//! -//! # Examples -//! ```rust -//! use grimoire_css_lib::color::Color; -//! let c = Color::try_from_str("rgb(255, 0, 0)").unwrap(); // strictly CSS parsing -//! ``` - -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; - -/// A color in RGBA form, optionally with an alpha channel. -/// -/// Internally, stores: -/// - `r`, `g`, `b` as 8-bit values. -/// - `a` as an f32 in [0..1]. -/// - a boolean `has_alpha` indicating whether the color has transparency. -/// -/// This struct offers methods to create and manipulate colors in a -/// CSS/Sass-like manner (e.g. `grayscale()`, `invert()`, `mix()`, etc.). -#[derive(Debug, Clone, Copy)] -pub struct Color { - r: u8, - g: u8, - b: u8, - a: f32, - has_alpha: bool, -} - -impl PartialEq for Color { - fn eq(&self, other: &Self) -> bool { - self.r == other.r - && self.g == other.g - && self.b == other.b - && self.a.to_bits() == other.a.to_bits() - && self.has_alpha == other.has_alpha - } -} - -impl Eq for Color {} - -impl Hash for Color { - fn hash(&self, state: &mut H) { - self.r.hash(state); - self.g.hash(state); - self.b.hash(state); - self.a.to_bits().hash(state); - self.has_alpha.hash(state); - } -} - -// Named colors: According to CSS spec, these are predefined. -static NAME_TO_COLOR: Lazy> = Lazy::new(|| { - let mut m = HashMap::new(); - - m.insert("aliceblue", Color::new_internal(240, 248, 255, 1.0, false)); - m.insert( - "antiquewhite", - Color::new_internal(250, 235, 215, 1.0, false), - ); - m.insert("aqua", Color::new_internal(0, 255, 255, 1.0, false)); - m.insert("aquamarine", Color::new_internal(127, 255, 212, 1.0, false)); - m.insert("azure", Color::new_internal(240, 255, 255, 1.0, false)); - m.insert("beige", Color::new_internal(245, 245, 220, 1.0, false)); - m.insert("bisque", Color::new_internal(255, 228, 196, 1.0, false)); - m.insert("black", Color::new_internal(0, 0, 0, 1.0, false)); - m.insert( - "blanchedalmond", - Color::new_internal(255, 235, 205, 1.0, false), - ); - m.insert("blue", Color::new_internal(0, 0, 255, 1.0, false)); - m.insert("blueviolet", Color::new_internal(138, 43, 226, 1.0, false)); - m.insert("brown", Color::new_internal(165, 42, 42, 1.0, false)); - m.insert("burlywood", Color::new_internal(222, 184, 135, 1.0, false)); - m.insert("cadetblue", Color::new_internal(95, 158, 160, 1.0, false)); - m.insert("chartreuse", Color::new_internal(127, 255, 0, 1.0, false)); - m.insert("chocolate", Color::new_internal(210, 105, 30, 1.0, false)); - m.insert("coral", Color::new_internal(255, 127, 80, 1.0, false)); - m.insert( - "cornflowerblue", - Color::new_internal(100, 149, 237, 1.0, false), - ); - m.insert("cornsilk", Color::new_internal(255, 248, 220, 1.0, false)); - m.insert("crimson", Color::new_internal(220, 20, 60, 1.0, false)); - m.insert("cyan", Color::new_internal(0, 255, 255, 1.0, false)); - m.insert("darkblue", Color::new_internal(0, 0, 139, 1.0, false)); - m.insert("darkcyan", Color::new_internal(0, 139, 139, 1.0, false)); - m.insert( - "darkgoldenrod", - Color::new_internal(184, 134, 11, 1.0, false), - ); - m.insert("darkgray", Color::new_internal(169, 169, 169, 1.0, false)); - m.insert("darkgreen", Color::new_internal(0, 100, 0, 1.0, false)); - m.insert("darkgrey", Color::new_internal(169, 169, 169, 1.0, false)); - m.insert("darkkhaki", Color::new_internal(189, 183, 107, 1.0, false)); - m.insert("darkmagenta", Color::new_internal(139, 0, 139, 1.0, false)); - m.insert( - "darkolivegreen", - Color::new_internal(85, 107, 47, 1.0, false), - ); - m.insert("darkorange", Color::new_internal(255, 140, 0, 1.0, false)); - m.insert("darkorchid", Color::new_internal(153, 50, 204, 1.0, false)); - m.insert("darkred", Color::new_internal(139, 0, 0, 1.0, false)); - m.insert("darksalmon", Color::new_internal(233, 150, 122, 1.0, false)); - m.insert( - "darkseagreen", - Color::new_internal(143, 188, 143, 1.0, false), - ); - m.insert( - "darkslateblue", - Color::new_internal(72, 61, 139, 1.0, false), - ); - m.insert("darkslategray", Color::new_internal(47, 79, 79, 1.0, false)); - m.insert("darkslategrey", Color::new_internal(47, 79, 79, 1.0, false)); - m.insert( - "darkturquoise", - Color::new_internal(0, 206, 209, 1.0, false), - ); - m.insert("darkviolet", Color::new_internal(148, 0, 211, 1.0, false)); - m.insert("deeppink", Color::new_internal(255, 20, 147, 1.0, false)); - m.insert("deepskyblue", Color::new_internal(0, 191, 255, 1.0, false)); - m.insert("dimgray", Color::new_internal(105, 105, 105, 1.0, false)); - m.insert("dimgrey", Color::new_internal(105, 105, 105, 1.0, false)); - m.insert("dodgerblue", Color::new_internal(30, 144, 255, 1.0, false)); - m.insert("firebrick", Color::new_internal(178, 34, 34, 1.0, false)); - m.insert( - "floralwhite", - Color::new_internal(255, 250, 240, 1.0, false), - ); - m.insert("forestgreen", Color::new_internal(34, 139, 34, 1.0, false)); - m.insert("fuchsia", Color::new_internal(255, 0, 255, 1.0, false)); - m.insert("gainsboro", Color::new_internal(220, 220, 220, 1.0, false)); - m.insert("ghostwhite", Color::new_internal(248, 248, 255, 1.0, false)); - m.insert("gold", Color::new_internal(255, 215, 0, 1.0, false)); - m.insert("goldenrod", Color::new_internal(218, 165, 32, 1.0, false)); - m.insert("gray", Color::new_internal(128, 128, 128, 1.0, false)); - m.insert("green", Color::new_internal(0, 128, 0, 1.0, false)); - m.insert("greenyellow", Color::new_internal(173, 255, 47, 1.0, false)); - m.insert("grey", Color::new_internal(128, 128, 128, 1.0, false)); - m.insert("honeydew", Color::new_internal(240, 255, 240, 1.0, false)); - m.insert("hotpink", Color::new_internal(255, 105, 180, 1.0, false)); - m.insert("indianred", Color::new_internal(205, 92, 92, 1.0, false)); - m.insert("indigo", Color::new_internal(75, 0, 130, 1.0, false)); - m.insert("ivory", Color::new_internal(255, 255, 240, 1.0, false)); - m.insert("khaki", Color::new_internal(240, 230, 140, 1.0, false)); - m.insert("lavender", Color::new_internal(230, 230, 250, 1.0, false)); - m.insert( - "lavenderblush", - Color::new_internal(255, 240, 245, 1.0, false), - ); - m.insert("lawngreen", Color::new_internal(124, 252, 0, 1.0, false)); - m.insert( - "lemonchiffon", - Color::new_internal(255, 250, 205, 1.0, false), - ); - m.insert("lightblue", Color::new_internal(173, 216, 230, 1.0, false)); - m.insert("lightcoral", Color::new_internal(240, 128, 128, 1.0, false)); - m.insert("lightcyan", Color::new_internal(224, 255, 255, 1.0, false)); - m.insert( - "lightgoldenrodyellow", - Color::new_internal(250, 250, 210, 1.0, false), - ); - m.insert("lightgray", Color::new_internal(211, 211, 211, 1.0, false)); - m.insert("lightgreen", Color::new_internal(144, 238, 144, 1.0, false)); - m.insert("lightgrey", Color::new_internal(211, 211, 211, 1.0, false)); - m.insert("lightpink", Color::new_internal(255, 182, 193, 1.0, false)); - m.insert( - "lightsalmon", - Color::new_internal(255, 160, 122, 1.0, false), - ); - m.insert( - "lightseagreen", - Color::new_internal(32, 178, 170, 1.0, false), - ); - m.insert( - "lightskyblue", - Color::new_internal(135, 206, 250, 1.0, false), - ); - m.insert( - "lightslategray", - Color::new_internal(119, 136, 153, 1.0, false), - ); - m.insert( - "lightslategrey", - Color::new_internal(119, 136, 153, 1.0, false), - ); - m.insert( - "lightsteelblue", - Color::new_internal(176, 196, 222, 1.0, false), - ); - m.insert( - "lightyellow", - Color::new_internal(255, 255, 224, 1.0, false), - ); - m.insert("lime", Color::new_internal(0, 255, 0, 1.0, false)); - m.insert("limegreen", Color::new_internal(50, 205, 50, 1.0, false)); - m.insert("linen", Color::new_internal(250, 240, 230, 1.0, false)); - m.insert("magenta", Color::new_internal(255, 0, 255, 1.0, false)); - m.insert("maroon", Color::new_internal(128, 0, 0, 1.0, false)); - m.insert( - "mediumaquamarine", - Color::new_internal(102, 205, 170, 1.0, false), - ); - m.insert("mediumblue", Color::new_internal(0, 0, 205, 1.0, false)); - m.insert( - "mediumorchid", - Color::new_internal(186, 85, 211, 1.0, false), - ); - m.insert( - "mediumpurple", - Color::new_internal(147, 112, 219, 1.0, false), - ); - m.insert( - "mediumseagreen", - Color::new_internal(60, 179, 113, 1.0, false), - ); - m.insert( - "mediumslateblue", - Color::new_internal(123, 104, 238, 1.0, false), - ); - m.insert( - "mediumspringgreen", - Color::new_internal(0, 250, 154, 1.0, false), - ); - m.insert( - "mediumturquoise", - Color::new_internal(72, 209, 204, 1.0, false), - ); - m.insert( - "mediumvioletred", - Color::new_internal(199, 21, 133, 1.0, false), - ); - m.insert("midnightblue", Color::new_internal(25, 25, 112, 1.0, false)); - m.insert("mintcream", Color::new_internal(245, 255, 250, 1.0, false)); - m.insert("mistyrose", Color::new_internal(255, 228, 225, 1.0, false)); - m.insert("moccasin", Color::new_internal(255, 228, 181, 1.0, false)); - m.insert( - "navajowhite", - Color::new_internal(255, 222, 173, 1.0, false), - ); - m.insert("navy", Color::new_internal(0, 0, 128, 1.0, false)); - m.insert("oldlace", Color::new_internal(253, 245, 230, 1.0, false)); - m.insert("olive", Color::new_internal(128, 128, 0, 1.0, false)); - m.insert("olivedrab", Color::new_internal(107, 142, 35, 1.0, false)); - m.insert("orange", Color::new_internal(255, 165, 0, 1.0, false)); - m.insert("orangered", Color::new_internal(255, 69, 0, 1.0, false)); - m.insert("orchid", Color::new_internal(218, 112, 214, 1.0, false)); - m.insert( - "palegoldenrod", - Color::new_internal(238, 232, 170, 1.0, false), - ); - m.insert("palegreen", Color::new_internal(152, 251, 152, 1.0, false)); - m.insert( - "paleturquoise", - Color::new_internal(175, 238, 238, 1.0, false), - ); - m.insert( - "palevioletred", - Color::new_internal(219, 112, 147, 1.0, false), - ); - m.insert("papayawhip", Color::new_internal(255, 239, 213, 1.0, false)); - m.insert("peachpuff", Color::new_internal(255, 218, 185, 1.0, false)); - m.insert("peru", Color::new_internal(205, 133, 63, 1.0, false)); - m.insert("pink", Color::new_internal(255, 192, 203, 1.0, false)); - m.insert("plum", Color::new_internal(221, 160, 221, 1.0, false)); - m.insert("powderblue", Color::new_internal(176, 224, 230, 1.0, false)); - m.insert("purple", Color::new_internal(128, 0, 128, 1.0, false)); - m.insert( - "rebeccapurple", - Color::new_internal(102, 51, 153, 1.0, false), - ); - m.insert("red", Color::new_internal(255, 0, 0, 1.0, false)); - m.insert("rosybrown", Color::new_internal(188, 143, 143, 1.0, false)); - m.insert("royalblue", Color::new_internal(65, 105, 225, 1.0, false)); - m.insert("saddlebrown", Color::new_internal(139, 69, 19, 1.0, false)); - m.insert("salmon", Color::new_internal(250, 128, 114, 1.0, false)); - m.insert("sandybrown", Color::new_internal(244, 164, 96, 1.0, false)); - m.insert("seagreen", Color::new_internal(46, 139, 87, 1.0, false)); - m.insert("seashell", Color::new_internal(255, 245, 238, 1.0, false)); - m.insert("sienna", Color::new_internal(160, 82, 45, 1.0, false)); - m.insert("silver", Color::new_internal(192, 192, 192, 1.0, false)); - m.insert("skyblue", Color::new_internal(135, 206, 235, 1.0, false)); - m.insert("slateblue", Color::new_internal(106, 90, 205, 1.0, false)); - m.insert("slategray", Color::new_internal(112, 128, 144, 1.0, false)); - m.insert("slategrey", Color::new_internal(112, 128, 144, 1.0, false)); - m.insert("snow", Color::new_internal(255, 250, 250, 1.0, false)); - m.insert("springgreen", Color::new_internal(0, 255, 127, 1.0, false)); - m.insert("steelblue", Color::new_internal(70, 130, 180, 1.0, false)); - m.insert("tan", Color::new_internal(210, 180, 140, 1.0, false)); - m.insert("teal", Color::new_internal(0, 128, 128, 1.0, false)); - m.insert("thistle", Color::new_internal(216, 191, 216, 1.0, false)); - m.insert("tomato", Color::new_internal(255, 99, 71, 1.0, false)); - m.insert("turquoise", Color::new_internal(64, 224, 208, 1.0, false)); - m.insert("violet", Color::new_internal(238, 130, 238, 1.0, false)); - m.insert("wheat", Color::new_internal(245, 222, 179, 1.0, false)); - m.insert("white", Color::new_internal(255, 255, 255, 1.0, false)); - m.insert("whitesmoke", Color::new_internal(245, 245, 245, 1.0, false)); - m.insert("yellow", Color::new_internal(255, 255, 0, 1.0, false)); - m.insert("yellowgreen", Color::new_internal(154, 205, 50, 1.0, false)); - m.insert("transparent", Color::new_internal(0, 0, 0, 0.0, false)); - - m -}); - -static COLOR_TO_NAME: Lazy> = Lazy::new(|| { - let mut m = HashMap::new(); - for (name, color) in NAME_TO_COLOR.iter() { - m.insert(*color, *name); - } - m -}); - -impl Color { - // ----------------------------------------- - // Constructors - // ----------------------------------------- - - // Internal constructor ensuring correct alpha handling (private). - fn new_internal(r: u8, g: u8, b: u8, a: f32, has_alpha: bool) -> Self { - let a_clamped = clamp(a, 0.0, 1.0); - // If we previously didn't have alpha and after clamp a=1.0, then no alpha. - // If we previously had alpha or a!=1.0 now, has_alpha=true. - let final_has_alpha = if has_alpha { true } else { a_clamped != 1.0 }; - - Color { - r, - g, - b, - a: a_clamped, - has_alpha: final_has_alpha, - } - } - - /// Creates a new color from RGBA components. - /// - /// # Arguments - /// - /// * `r` - Red channel (0..255). - /// * `g` - Green channel (0..255). - /// * `b` - Blue channel (0..255). - /// * `a` - Alpha channel (0..1). If `a` < 1.0, color is considered to have alpha. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let c = Color::new(255, 0, 0, 1.0); // Fully opaque red - /// ``` - pub fn new(r: u8, g: u8, b: u8, a: f32) -> Self { - Self::new_internal(r, g, b, a, a != 1.0) - } - - /// Creates a color from RGB and alpha components. - /// - /// Equivalent to [`Color::new`], provided for clarity. - pub fn from_rgb(r: u8, g: u8, b: u8, a: f32) -> Self { - Self::new_internal(r, g, b, a, a != 1.0) - } - - /// Creates a color from [HSL](https://www.w3.org/TR/css-color-4/#the-hsl-notation) values, plus alpha. - /// - /// # Arguments - /// - /// * `h` - Hue, in degrees (0..360). - /// * `s` - Saturation, in percent (0..100). - /// * `l` - Lightness, in percent (0..100). - /// * `a` - Alpha channel (0..1). - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// // Pure red via HSL - /// let c = Color::from_hsl(0.0, 100.0, 50.0, 1.0); - /// ``` - pub fn from_hsl(h: f32, s: f32, l: f32, a: f32) -> Self { - let h_norm = normalize_hue(h); - let s_f = clamp(s / 100.0, 0.0, 1.0); - let l_f = clamp(l / 100.0, 0.0, 1.0); - let (r, g, b, a_cl) = hsl_to_srgb(h_norm, s_f, l_f, clamp(a, 0.0, 1.0)); - Self::new_internal( - (r * 255.0).round() as u8, - (g * 255.0).round() as u8, - (b * 255.0).round() as u8, - a_cl, - a_cl != 1.0, - ) - } - - /// Creates a color from [HWB](https://www.w3.org/TR/css-color-4/#the-hwb-notation) values, plus alpha. - /// - /// # Arguments - /// - /// * `h` - Hue in degrees (0..360). - /// * `w` - Whiteness (0..100). - /// * `bk` - Blackness (0..100). - /// * `a` - Alpha channel (0..1). - /// - /// See [the CSS spec](https://www.w3.org/TR/css-color-4/#the-hwb-notation). - pub fn from_hwb(h: f32, w: f32, bk: f32, a: f32) -> Self { - // h is in degrees already - let (r, g, b, a_cl) = Self::hwb_to_srgb(h, w, bk, a); - Self::new_internal( - clamp((r * 255.0).round() as u8, 0, 255), - clamp((g * 255.0).round() as u8, 0, 255), - clamp((b * 255.0).round() as u8, 0, 255), - a_cl, - a_cl != 1.0, - ) - } - - /// Attempts to create a color from a hex string (e.g. `"#ff00ff"`, `"#fff"`). - /// Returns `None` if the string is invalid. - /// - /// Supports 3-, 4-, 6-, and 8-digit hex forms (including alpha). - pub fn from_hex(hex: &str) -> Option { - Self::try_from_hex_str(hex) - } - - /// Attempts to create a color from a general CSS-like string: - /// - Named color (e.g. `"red"`, `"aliceblue"`) - /// - Hex code (e.g. `"#fff"`, `"#ff00ff"`, `"#ffffffff"`) - /// - Functional notation (e.g. `"rgb(...)"`, `"hsl(...)"`, `"hwb(...)"`) - /// - /// Returns `None` if the color cannot be parsed. - pub fn try_from_str(input: &str) -> Option { - let input = input.trim(); - - // Try hex - if input.starts_with('#') { - if let Some(c) = Self::try_from_hex_str(input) { - return Some(c); - } - } - - // Try functional syntax (rgb/hsl/hwb) - if let Some(c) = Self::try_from_function_syntax(input) { - return Some(c); - } - - // Try named color - if let Some(c) = Self::try_from_named_str(input) { - return Some(c); - } - - None - } - - // ----------------------------------------- - // Public getters - // ----------------------------------------- - - /// Returns the red channel (0..255) - pub fn red(&self) -> u8 { - self.r - } - - /// Returns the green channel (0..255) - pub fn green(&self) -> u8 { - self.g - } - - /// Returns the blue channel (0..255) - pub fn blue(&self) -> u8 { - self.b - } - - /// Returns the alpha channel (0..1) - pub fn alpha(&self) -> f32 { - self.a - } - - /// Alias for `alpha()` - pub fn opacity(&self) -> f32 { - self.a - } - - // ----------------------------------------- - // Color space conversions - // ----------------------------------------- - - /// Converts the current color to [HSL](https://www.w3.org/TR/css-color-4/#the-hsl-notation) and - /// returns `(h, s, l)` where: - /// - `h` is in [0..360] degrees, - /// - `s` and `l` are in [0..100] percent. - /// - /// This does not change the alpha channel. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let c = Color::new(255, 0, 0, 1.0); - /// let (h, s, l) = c.to_hsl(); - /// assert_eq!(h, 0.0); - /// assert_eq!(s, 100.0); - /// assert_eq!(l, 50.0); - /// ``` - pub fn to_hsl(&self) -> (f32, f32, f32) { - Self::rgb_to_hsl(*self) - } - - /// Returns `(r,g,b,a)` with: - /// - `r,g,b` in [0..255] - /// - `a` in [0..1] - pub fn to_rgba(&self) -> (u8, u8, u8, f32) { - (self.r, self.g, self.b, self.a) - } - - /// Returns a CSS hex string (e.g. `"#7fffd4"` or `"#7fffd480"` if alpha < 1.0). - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let c = Color::new(127, 255, 212, 1.0); - /// assert_eq!(c.to_hex_string(), "#7fffd4"); - /// ``` - pub fn to_hex_string(&self) -> String { - if self.has_alpha { - let a = (self.a * 255.0).round() as u8; - format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, a) - } else { - format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b) - } - } - - /// Returns the named color string (e.g. `"red"`, `"blue"`) if this color - /// matches one of the predefined colors exactly, otherwise `None`. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let c = Color::new(255, 0, 0, 1.0); - /// assert_eq!(c.to_named_color_str(), Some("red")); - /// ``` - pub fn to_named_color_str(&self) -> Option<&'static str> { - COLOR_TO_NAME.get(self).copied() - } - - // ----------------------------------------- - // Color operations - // ----------------------------------------- - - /// Converts the color to grayscale by setting saturation to 0%. - /// - /// Other channels (hue, lightness, alpha) remain unchanged. - pub fn grayscale(&self) -> Self { - let (h, _s, l) = self.to_hsl(); - Self::from_hsl(h, 0.0, l, self.a) - } - - /// Returns the complementary color by adding 180° to the hue. - pub fn complement(&self) -> Self { - let (h, s, l) = self.to_hsl(); - Self::from_hsl(h + 180.0, s, l, self.a) - } - - /// Inverts the color. - /// - /// `weight` controls how much the color is inverted (0..100%, defaults to 100). - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let white = Color::new(255, 255, 255, 1.0); - /// let black = white.invert(None); - /// assert_eq!(black.to_hex_string(), "#000000"); - /// ``` - pub fn invert(&self, weight: Option) -> Self { - let w = weight.unwrap_or(100.0) / 100.0; - let inv_r = 255 - self.r; - let inv_g = 255 - self.g; - let inv_b = 255 - self.b; - - let r = (self.r as f32 * (1.0 - w) + inv_r as f32 * w).round() as u8; - let g = (self.g as f32 * (1.0 - w) + inv_g as f32 * w).round() as u8; - let b = (self.b as f32 * (1.0 - w) + inv_b as f32 * w).round() as u8; - - Color::new_internal(r, g, b, self.a, self.has_alpha || self.a != 1.0) - } - - /// Mixes two colors by a given weight (0..100%). - /// - /// A `weight` of 50% returns an average of the two colors. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let red = Color::new(255, 0, 0, 1.0); - /// let blue = Color::new(0, 0, 255, 1.0); - /// let purple = Color::mix(red, blue, 50.0); - /// assert_eq!(purple.to_hex_string(), "#800080"); - /// ``` - pub fn mix(c1: Color, c2: Color, weight: f32) -> Self { - let w = clamp(weight, 0.0, 100.0) / 100.0; - let r = (c1.r as f32 * w + c2.r as f32 * (1.0 - w)).round() as u8; - let g = (c1.g as f32 * w + c2.g as f32 * (1.0 - w)).round() as u8; - let b = (c1.b as f32 * w + c2.b as f32 * (1.0 - w)).round() as u8; - let a = c1.a * w + c2.a * (1.0 - w); - // If either had alpha originally, result should keep has_alpha = true if a != 1.0 - let has_alpha = c1.has_alpha || c2.has_alpha || a != 1.0; - Color::new_internal(r, g, b, a, has_alpha) - } - - /// Adjusts the hue by `degrees`. - /// - /// If `degrees` is positive, hue rotates “forward”; - /// if negative, it rotates “backward”. Values can wrap beyond 360°. - pub fn adjust_hue(&self, degrees: f32) -> Self { - let (h, s, l) = Self::rgb_to_hsl(*self); - Self::from_hsl(h + degrees, s, l, self.a) - } - - /// Adjusts color by optionally modifying RGB deltas or HSL deltas. - /// - /// # Arguments - /// - /// * `red_delta`, `green_delta`, `blue_delta` - The integer deltas to add to each channel. - /// * `hue_delta`, `sat_delta`, `light_delta`, `alpha_delta` - The float deltas for hue, saturation, lightness, alpha. - /// - /// Missing arguments (i.e. `None`) leave that component unchanged. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let c = Color::new(128, 128, 128, 1.0); - /// // Make it slightly redder - /// let c2 = c.adjust_color(Some(10), None, None, None, None, None, None); - /// ``` - #[allow(clippy::too_many_arguments)] - pub fn adjust_color( - &self, - red_delta: Option, - green_delta: Option, - blue_delta: Option, - hue_delta: Option, - sat_delta: Option, - light_delta: Option, - alpha_delta: Option, - ) -> Self { - let (h, s, l) = self.to_hsl(); - let mut r = self.r as i32; - let mut g = self.g as i32; - let mut b = self.b as i32; - - if let Some(rd) = red_delta { - r = clamp(r + rd, 0, 255); - } - if let Some(gd) = green_delta { - g = clamp(g + gd, 0, 255); - } - if let Some(bd) = blue_delta { - b = clamp(b + bd, 0, 255); - } - - let h_new = h + hue_delta.unwrap_or(0.0); - let s_new = s + sat_delta.unwrap_or(0.0); - let l_new = l + light_delta.unwrap_or(0.0); - let a_new = clamp(self.a + alpha_delta.unwrap_or(0.0), 0.0, 1.0); - - let mut out_color = Self::from_hsl(h_new, s_new, l_new, a_new); - out_color.r = r as u8; - out_color.g = g as u8; - out_color.b = b as u8; - out_color.has_alpha = out_color.has_alpha || a_new != 1.0; - out_color - } - - /// Changes color by setting absolute values (if provided) for RGB or HSL. - /// - /// # Arguments - /// - /// * `red`, `green`, `blue` - Final values for each channel (0..255). - /// * `hue_val`, `sat_val`, `light_val` - Final HSL values. - /// * `alpha_val` - Final alpha in [0..1]. - /// - /// Missing arguments (i.e. `None`) leave that component unchanged. - #[allow(clippy::too_many_arguments)] - pub fn change_color( - &self, - red: Option, - green: Option, - blue: Option, - hue_val: Option, - sat_val: Option, - light_val: Option, - alpha_val: Option, - ) -> Self { - let (h, s, l) = self.to_hsl(); - - let r = red.unwrap_or(self.r); - let g = green.unwrap_or(self.g); - let b = blue.unwrap_or(self.b); - - let h_new = hue_val.unwrap_or(h); - let s_new = sat_val.unwrap_or(s); - let l_new = light_val.unwrap_or(l); - let a_new = clamp(alpha_val.unwrap_or(self.a), 0.0, 1.0); - - let mut out_color = Self::from_hsl(h_new, s_new, l_new, a_new); - - if red.is_some() { - out_color.r = r; - } - if green.is_some() { - out_color.g = g; - } - if blue.is_some() { - out_color.b = b; - } - - out_color.has_alpha = out_color.has_alpha || a_new != 1.0; - out_color - } - - /// Scales color channels by given percentages. - /// - /// Positive scale values increase the channel, negative decrease. - /// E.g. `red_scale=10.0` => +10% red from current value. - /// - /// Missing arguments (i.e. `None`) leave that channel unchanged. - pub fn scale_color( - &self, - red_scale: Option, - green_scale: Option, - blue_scale: Option, - saturation_scale: Option, - lightness_scale: Option, - alpha_scale: Option, - ) -> Self { - let (mut h, mut s, mut l) = self.to_hsl(); - let mut a = self.a; - - if let Some(ss) = saturation_scale { - s = scale_hsl(s, ss); - s = clamp(s, 0.0, 100.0); - } - - if let Some(ls) = lightness_scale { - l = scale_hsl(l, ls); - l = clamp(l, 0.0, 100.0); - } - - if let Some(as_) = alpha_scale { - a = scale_alpha(a, as_); - } - - h = normalize_hue(h); - let mut new_color = Self::from_hsl(h, s, l, a); - - if let Some(rs) = red_scale { - new_color.r = scale_channel(new_color.r, rs); - } - if let Some(gs) = green_scale { - new_color.g = scale_channel(new_color.g, gs); - } - if let Some(bs) = blue_scale { - new_color.b = scale_channel(new_color.b, bs); - } - - new_color.has_alpha = new_color.has_alpha || a != 1.0; - new_color - } - - /// Returns a new color with the same RGB, but alpha set to `alpha`. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let c = Color::new(255, 0, 0, 1.0); - /// let half_transparent_red = c.rgba(0.5); - /// ``` - pub fn rgba(&self, alpha: f32) -> Self { - let new_a = clamp(alpha, 0.0, 1.0); - // Once alpha introduced, keep has_alpha true - let new_has_alpha = self.has_alpha || new_a != 1.0; - Self::new_internal(self.r, self.g, self.b, new_a, new_has_alpha) - } - - /// Lightens the color by `amount` percent. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let red = Color::new(255, 0, 0, 1.0); - /// let lighter_red = red.lighten(10.0); // ~ #ff3333 - /// ``` - pub fn lighten(&self, amount: f32) -> Self { - let (h, s, l) = self.to_hsl(); - Self::from_hsl(h, s, clamp(l + amount, 0.0, 100.0), self.a) - } - - /// Darkens the color by `amount` percent. - pub fn darken(&self, amount: f32) -> Self { - let (h, s, l) = self.to_hsl(); - Self::from_hsl(h, s, clamp(l - amount, 0.0, 100.0), self.a) - } - - /// Increases saturation by `amount` percent - pub fn saturate(&self, amount: f32) -> Self { - let (h, s, l) = Self::rgb_to_hsl(*self); - Self::from_hsl(h, clamp(s + amount, 0.0, 100.0), l, self.a) - } - - /// Decreases saturation by `amount` percent - pub fn desaturate(&self, amount: f32) -> Self { - let (h, s, l) = Self::rgb_to_hsl(*self); - Self::from_hsl(h, clamp(s - amount, 0.0, 100.0), l, self.a) - } - - /// Increases alpha by `amount` in [0..1], making the color more opaque. - /// - /// # Examples - /// - /// ``` - /// use grimoire_css_lib::color::Color; - /// - /// let mut c = Color::new(255, 0, 0, 0.5); - /// c = c.opacify(0.3); // new alpha = 0.8 - /// ``` - pub fn opacify(&self, amount: f32) -> Self { - let new_a = clamp(self.a + amount, 0.0, 1.0); - Self::new_internal( - self.r, - self.g, - self.b, - new_a, - self.has_alpha || new_a != 1.0, - ) - } - - /// Alias for `opacify` - pub fn fade_in(&self, amount: f32) -> Self { - self.opacify(amount) - } - - /// Decreases alpha (increases transparency) by `amount` (0..1) - pub fn transparentize(&self, amount: f32) -> Self { - let new_a = clamp(self.a - amount, 0.0, 1.0); - Self::new_internal( - self.r, - self.g, - self.b, - new_a, - self.has_alpha || new_a != 1.0, - ) - } - - /// Alias for `transparentize` - pub fn fade_out(&self, amount: f32) -> Self { - self.transparentize(amount) - } - - // ----------------------------------------- - // Private helper methods - // ----------------------------------------- - - fn try_from_hex_str(s: &str) -> Option { - let hex = s.trim_start_matches('#'); - match hex.len() { - 3 => { - let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?; - let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?; - let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?; - Some(Self::new_internal(r, g, b, 1.0, false)) - } - 4 => { - // #RGBA is allowed in CSS Color Level 4 - let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?; - let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?; - let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?; - let a = u8::from_str_radix(&hex[3..4].repeat(2), 16).ok()? as f32 / 255.0; - Some(Self::new_internal(r, g, b, a, true)) - } - 6 => { - let r = u8::from_str_radix(&hex[0..2], 16).ok()?; - let g = u8::from_str_radix(&hex[2..4], 16).ok()?; - let b = u8::from_str_radix(&hex[4..6], 16).ok()?; - Some(Self::new_internal(r, g, b, 1.0, false)) - } - 8 => { - let r = u8::from_str_radix(&hex[0..2], 16).ok()?; - let g = u8::from_str_radix(&hex[2..4], 16).ok()?; - let b = u8::from_str_radix(&hex[4..6], 16).ok()?; - let a = u8::from_str_radix(&hex[6..8], 16).ok()? as f32 / 255.0; - Some(Self::new_internal(r, g, b, a, true)) - } - _ => None, - } - } - - fn try_from_named_str(name: &str) -> Option { - let lower = name.to_ascii_lowercase(); - NAME_TO_COLOR.get(lower.as_str()).copied() - } - - fn try_from_function_syntax(s: &str) -> Option { - let lower = s.to_ascii_lowercase(); - let input = lower.as_str().trim(); - - if input.starts_with("rgb(") || input.starts_with("rgba(") { - Self::try_from_rgb_func(input) - } else if input.starts_with("hsl(") || input.starts_with("hsla(") { - Self::try_from_hsl_func(input) - } else if input.starts_with("hwb(") { - Self::try_from_hwb_func(input) - } else { - None - } - } - - fn try_from_rgb_func(input: &str) -> Option { - let mut s = input.trim(); - let mut has_alpha = false; - if s.starts_with("rgba(") { - s = &s[5..]; - } else { - s = &s[4..]; - } - if !s.ends_with(')') { - return None; - } - s = s[..s.len() - 1].trim(); - - let legacy = s.contains(','); - - let (r_val, mut s, rperc) = parse_number_or_percentage(s)?; - if !consume_divider_required(&mut s, legacy) { - return None; - } - let (g_val, mut s, gperc) = parse_number_or_percentage(s)?; - if !consume_divider_required(&mut s, legacy) { - return None; - } - let (b_val, s, bperc) = parse_number_or_percentage(s)?; - - // According to CSS: - // If percentage => fraction * 255 - // If number => directly in 0..255 - let r = if rperc { - (r_val * 255.0).round() as u8 - } else { - clamp(r_val, 0.0, 255.0).round() as u8 - }; - let g = if gperc { - (g_val * 255.0).round() as u8 - } else { - clamp(g_val, 0.0, 255.0).round() as u8 - }; - let b = if bperc { - (b_val * 255.0).round() as u8 - } else { - clamp(b_val, 0.0, 255.0).round() as u8 - }; - - let mut a = 1.0; - - let s = s.trim(); - if (!legacy && s.starts_with('/')) || (legacy && s.starts_with(',')) { - has_alpha = true; - let mut s2 = consume_divider(s, legacy); - if let Some((val, rest)) = parse_alpha_value(s2) { - a = val; - s2 = rest; - if !s2.trim().is_empty() { - return None; - } - } else if s2.to_lowercase().starts_with("none") { - s2 = s2[4..].trim(); - if !s2.is_empty() { - return None; - } - } else { - return None; - } - } else if !s.is_empty() { - return None; - } - - Some(Color::new_internal(r, g, b, a, has_alpha || a != 1.0)) - } - - fn try_from_hsl_func(input: &str) -> Option { - let mut s = input.trim(); - let mut has_alpha = false; - if s.starts_with("hsla(") { - s = &s[5..]; - } else { - s = &s[4..]; - } - if !s.ends_with(')') { - return None; - } - s = s[..s.len() - 1].trim(); - - let legacy = s.contains(','); - - let (h_val, mut s) = parse_hue_or_none(s)?; - if legacy && !consume_legacy_comma(&mut s) { - return None; - } - let (sat_val, mut s) = parse_sat_light_none(s)?; - if legacy && !consume_legacy_comma(&mut s) { - return None; - } - let (l_val, s) = parse_sat_light_none(s)?; - - let mut a = 1.0; - let s = s.trim(); - if (!legacy && s.starts_with('/')) || (legacy && s.starts_with(',')) { - has_alpha = true; - let mut s2 = consume_divider(s, legacy); - if let Some((val, rest)) = parse_alpha_value(s2) { - a = val; - s2 = rest; - if !s2.trim().is_empty() { - return None; - } - } else if s2.to_lowercase().starts_with("none") { - s2 = s2[4..].trim(); - if !s2.is_empty() { - return None; - } - // none means a=1.0 but alpha specified => has_alpha=true - } else { - return None; - } - } else if !s.is_empty() { - return None; - } - - let h = normalize_hue(h_val); - let s_frac = clamp(sat_val, 0.0, 1.0); - let l_frac = clamp(l_val, 0.0, 1.0); - - let (r, g, b, a_cl) = hsl_to_srgb(h, s_frac, l_frac, a); - Some(Color::new_internal( - (r * 255.0).round() as u8, - (g * 255.0).round() as u8, - (b * 255.0).round() as u8, - a_cl, - has_alpha || a_cl != 1.0, - )) - } - - fn try_from_hwb_func(input: &str) -> Option { - let mut s = input.trim(); - let mut has_alpha = false; - s = &s[4..]; // skip "hwb(" - if !s.ends_with(')') { - return None; - } - s = s[..s.len() - 1].trim(); - - let (h_val, s) = parse_hue_or_none(s)?; - let (w_val, s) = parse_wb_none(s)?; - let (b_val, s) = parse_wb_none(s)?; - - let mut a = 1.0; - let s = s.trim(); - - if let Some(stripped) = s.strip_prefix('/') { - has_alpha = true; - let mut s2 = stripped.trim(); - if let Some((val, rest)) = parse_alpha_value(s2) { - a = val; - s2 = rest; - if !s2.trim().is_empty() { - return None; - } - } else if s2.to_lowercase().starts_with("none") { - s2 = s2[4..].trim(); - if !s2.is_empty() { - return None; - } - } else { - return None; - } - } else if !s.is_empty() { - return None; - } - - let h = normalize_hue(h_val); - let w = clamp(w_val, 0.0, 1.0); - let bk = clamp(b_val, 0.0, 1.0); - - let (r, g, b, a_cl) = Self::hwb_to_srgb(h, w, bk, a); - Some(Color::new_internal( - clamp((r * 255.0).round() as u8, 0, 255), - clamp((g * 255.0).round() as u8, 0, 255), - clamp((b * 255.0).round() as u8, 0, 255), - a_cl, - has_alpha || a_cl != 1.0, - )) - } - - // ----------------------------------------- - // Private conversion functions - // ----------------------------------------- - - fn rgb_to_hsl(c: Color) -> (f32, f32, f32) { - let r = c.r as f32 / 255.0; - let g = c.g as f32 / 255.0; - let b = c.b as f32 / 255.0; - - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let delta = max - min; - - let l = (max + min) / 2.0; - - if delta.abs() < f32::EPSILON { - (0.0, 0.0, l * 100.0) - } else { - let s = if l > 0.5 { - delta / (2.0 - max - min) - } else { - delta / (max + min) - }; - - let h = if (max - r).abs() < f32::EPSILON { - (g - b) / delta + if g < b { 6.0 } else { 0.0 } - } else if (max - g).abs() < f32::EPSILON { - (b - r) / delta + 2.0 - } else { - (r - g) / delta + 4.0 - }; - - (normalize_hue(h * 60.0), s * 100.0, l * 100.0) - } - } - - fn hwb_to_srgb(h: f32, mut w: f32, mut bk: f32, a: f32) -> (f32, f32, f32, f32) { - if w + bk > 1.0 { - let sum = w + bk; - w /= sum; - bk /= sum; - } - - let (base_r, base_g, base_b, _) = hsl_to_srgb(h, 1.0, 0.5, 1.0); - let r = base_r * (1.0 - w - bk) + w; - let g = base_g * (1.0 - w - bk) + w; - let b = base_b * (1.0 - w - bk) + w; - - (r, g, b, a) - } -} - -// ----------------------------------------- -// Global helper functions -// ----------------------------------------- - -fn clamp(value: T, min_val: T, max_val: T) -> T { - if value < min_val { - min_val - } else if value > max_val { - max_val - } else { - value - } -} - -fn normalize_hue(h: f32) -> f32 { - let mut hue = h % 360.0; - if hue < 0.0 { - hue += 360.0; - } - hue -} - -// Converts HSL (h in degrees, s,l in 0..1) to sRGB (0..1) -fn hsl_to_srgb(h: f32, s: f32, l: f32, a: f32) -> (f32, f32, f32, f32) { - let t2 = if l <= 0.5 { - l * (s + 1.) - } else { - l + s - l * s - }; - let t1 = 2. * l - t2; - let hue2rgb = |mut h: f32| { - if h < 0.0 { - h += 1.0; - } else if h > 1.0 { - h -= 1.0; - } - if 6.0 * h < 1.0 { - t1 + (t2 - t1) * 6.0 * h - } else if 2.0 * h < 1.0 { - t2 - } else if 3.0 * h < 2.0 { - t1 + (t2 - t1) * (2.0 / 3.0 - h) * 6.0 - } else { - t1 - } - }; - - let r = hue2rgb(h / 360.0 + 1.0 / 3.0); - let g = hue2rgb(h / 360.0); - let b = hue2rgb(h / 360.0 - 1.0 / 3.0); - (r, g, b, a) -} - -// ----------------------------------------- -// Parsing utilities -// ----------------------------------------- - -fn parse_number(s: &str) -> Option<(f32, &str)> { - let s = s.trim(); - let mut end = 0; - let chars: Vec = s.chars().collect(); - if chars.is_empty() { - return None; - } - - if chars[end] == '+' || chars[end] == '-' { - end += 1; - } - - let mut has_digit = false; - while end < chars.len() && chars[end].is_ascii_digit() { - has_digit = true; - end += 1; - } - - if end < chars.len() && chars[end] == '.' { - end += 1; - while end < chars.len() && chars[end].is_ascii_digit() { - has_digit = true; - end += 1; - } - } - - if end < chars.len() && (chars[end] == 'e' || chars[end] == 'E') { - end += 1; - if end < chars.len() && (chars[end] == '+' || chars[end] == '-') { - end += 1; - } - let start_exp = end; - while end < chars.len() && chars[end].is_ascii_digit() { - end += 1; - } - if end == start_exp { - return None; - } - } - - if !has_digit { - return None; - } - - let val_str: String = chars[..end].iter().collect(); - let val: f32 = val_str.parse().ok()?; - Some((val, s[end..].trim_start())) -} - -fn parse_percentage(s: &str) -> Option<(f32, &str)> { - let (val, rest) = parse_number(s)?; - let rest = rest.trim_start(); - - rest.strip_prefix('%') - .map(|stripped| (val / 100.0, stripped.trim_start())) -} - -fn parse_number_or_percentage(s: &str) -> Option<(f32, &str, bool)> { - if let Some((val, rest)) = parse_percentage(s) { - // perc = true means the value is now a fraction of 1 - Some((val, rest, true)) - } else if let Some((val, rest)) = parse_number(s) { - // perc = false means the value is as-is (could be fraction like 0.5 or just a number) - Some((val, rest, false)) - } else { - None - } -} - -fn parse_alpha_value(s: &str) -> Option<(f32, &str)> { - if let Some((val, rest, _)) = parse_number_or_percentage(s) { - Some((clamp(val, 0.0, 1.0), rest)) - } else { - None - } -} - -fn consume_divider(s: &str, legacy: bool) -> &str { - let s = s.trim_start(); - if !s.is_empty() { - let c = s.chars().next().unwrap(); - - if c == ',' && legacy || c == '/' && !legacy { - return s[1..].trim_start(); - } - } - s -} - -fn consume_divider_required(s: &mut &str, legacy: bool) -> bool { - let original = *s; - *s = consume_divider(s, legacy); - if *s == original { - !legacy - } else { - true - } -} - -fn consume_legacy_comma(s: &mut &str) -> bool { - let ss = s.trim_start(); - if let Some(stripped) = ss.strip_prefix(',') { - *s = stripped.trim_start(); - true - } else { - false - } -} - -// Always returns hue in degrees: -fn parse_hue_or_none(s: &str) -> Option<(f32, &str)> { - let s = s.trim(); - if s.to_lowercase().starts_with("none") { - return Some((0.0, &s[4..])); - } - - let (val, rest) = parse_number(s)?; - let rest = rest.trim_start(); - // According to CSS: - // If unit is deg => val is degrees - // If grad => val*(360/400)=val*0.9 degrees - // If rad => val*(180/pi) degrees - // If turn => val*360 degrees - // If no unit => degrees by default - if let Some(stripped) = rest.strip_prefix("deg") { - Some((val, stripped)) - } else if let Some(stripped) = rest.strip_prefix("grad") { - Some((val * (360.0 / 400.0), stripped)) - } else if let Some(stripped) = rest.strip_prefix("rad") { - Some((val * (180.0 / std::f32::consts::PI), stripped)) - } else if let Some(stripped) = rest.strip_prefix("turn") { - Some((val * 360.0, stripped)) - } else if is_ident_start(rest) { - None - } else { - // no unit means degrees - Some((val, rest)) - } -} - -fn is_ident_start(s: &str) -> bool { - if s.is_empty() { - return false; - } - let c = s.chars().next().unwrap(); - c.is_ascii_alphabetic() || c == '-' || c == '_' -} - -fn parse_sat_light_none(s: &str) -> Option<(f32, &str)> { - let s = s.trim(); - if s.to_lowercase().starts_with("none") { - return Some((0.0, &s[4..])); - } - if let Some((val, rest, _perc)) = parse_number_or_percentage(s) { - // If perc is true, val is fraction of 1 (e.g. 50% -> val=0.5) - // For HSL s/l in functional syntax: - // - If percentage is given, we have fraction (0..1) already correct. - // - If no unit is given, modern syntax also expects 0..1. - // So we can return val as is, since both perc=true and perc=false - // should yield a fraction 0..1 for s/l in modern syntax. - // If the user gave a large number, it will be clamped later. - Some((val, rest)) - } else { - None - } -} - -fn parse_wb_none(s: &str) -> Option<(f32, &str)> { - let s = s.trim(); - if s.to_lowercase().starts_with("none") { - return Some((0.0, &s[4..])); - } - if let Some((val, rest, _perc)) = parse_number_or_percentage(s) { - // w,b also treated as fractions 0..1 - Some((val, rest)) - } else { - None - } -} - -// ----------------------------------------- -// Additional utilities -// ----------------------------------------- - -fn scale_channel(val: u8, scale: f32) -> u8 { - let val_f = val as f32; - if scale > 0.0 { - let diff = 255.0 - val_f; - (val_f + diff * (scale / 100.0)).round().clamp(0.0, 255.0) as u8 - } else { - let diff = val_f; - (val_f - diff * (-scale / 100.0)).round().clamp(0.0, 255.0) as u8 - } -} - -fn scale_hsl(val: f32, scale: f32) -> f32 { - if scale > 0.0 { - let diff = 100.0 - val; - val + diff * (scale / 100.0) - } else { - let diff = val; - val - diff * (-scale / 100.0) - } -} - -fn scale_alpha(val: f32, scale: f32) -> f32 { - if scale > 0.0 { - let diff = 1.0 - val; - clamp(val + diff * (scale / 100.0), 0.0, 1.0) - } else { - let diff = val; - clamp(val - diff * (-scale / 100.0), 0.0, 1.0) - } -} - -// ----------------------------------------- -// Tests -// ----------------------------------------- -#[cfg(test)] -mod tests { - use super::*; - - // According to CSS spec, named colors must map correctly. - // Test if we correctly parse and reverse-lookup named colors. - #[test] - fn test_named_colors_css_spec() { - let c = Color::try_from_str("aliceblue").unwrap(); - assert_eq!(c.to_hex_string(), "#f0f8ff"); // aliceblue is #F0F8FF by CSS - assert_eq!(c.to_named_color_str(), Some("aliceblue")); - - let c2 = Color::try_from_str("black").unwrap(); - assert_eq!(c2.to_hex_string(), "#000000"); - assert_eq!(c2.to_named_color_str(), Some("black")); - } - - // Test hex parsing according to CSS: - // - #RGB and #RRGGBB forms - // - #RGBA and #RRGGBBAA forms for alpha - #[test] - fn test_hex_parsing_css_spec() { - // #fff should equal #ffffff with a=1.0 and no alpha in output (has_alpha=false) - let c = Color::try_from_str("#fff").unwrap(); - assert_eq!(c.to_hex_string(), "#ffffff"); - assert!(!c.has_alpha); - - // #ffffff is fully opaque white - let c = Color::try_from_str("#ffffff").unwrap(); - assert_eq!(c.to_hex_string(), "#ffffff"); - - // #ffff means #ffffff with alpha=1.0 (this is not a standard CSS form, but #FFF vs #FFFFFF are) - // Actually, the CSS spec allows #RRGGBBAA in Level 4. - // Let's test #RRGGBBAA: #ffffffff => white with alpha=1.0 - // This is valid in modern CSS (CSS Color Level 4). - let c = Color::try_from_str("#ffffffff").unwrap(); - assert_eq!(c.to_hex_string(), "#ffffffff"); - // Since we parsed an 8-digit hex, has_alpha should be true - assert!(c.has_alpha); - } - - // Test rgb(...) parsing (according to CSS): - #[test] - fn test_rgb_parsing_css_spec() { - // rgb(255, 0, 255) => #ff00ff (legacy syntax with commas) - let c = Color::try_from_str("rgb(255, 0, 255)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff00ff"); - assert!(!c.has_alpha); - - // Modern syntax: rgb(100% 0% 100%) - // 100% of 255 = 255, so this is also #ff00ff - let c = Color::try_from_str("rgb(100% 0% 100%)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff00ff"); - - // With alpha: rgb(128 128 128 / 0.5) - // 0.5 means alpha=0.5 - // This should produce a mid-gray with half transparency - let c = Color::try_from_str("rgb(128 128 128 / 0.5)").unwrap(); - assert_eq!(c.to_hex_string(), "#80808080"); - assert!(c.has_alpha); - - // Percentage alpha: rgb(0% 0% 100% / 50%) - // 0% = 0, 100% = 255, alpha=0.5 - let c = Color::try_from_str("rgb(0% 0% 100% / 50%)").unwrap(); - assert_eq!(c.to_hex_string(), "#0000ff80"); - assert!(c.has_alpha); - } - - // Test hsl(...) parsing according to CSS: - // hsl(h, s%, l%) or hsl(h s l / a) - // If s,l given as percentages, they are s/100 and l/100 for computations. - // If no unit, modern syntax expects fractions in [0..1]. - #[test] - fn test_hsl_parsing_css_spec() { - // hsl(0, 100%, 50%) = pure red #ff0000 - let c = Color::try_from_str("hsl(0,100%,50%)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff0000"); - - // Modern syntax: hsl(0 1 0.5) same as hsl(0deg,100%,50%) - let c = Color::try_from_str("hsl(0 1 0.5)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff0000"); - - // With alpha: hsla(0,100%,50%,0.5) - let c = Color::try_from_str("hsla(0,100%,50%,0.5)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff000080"); - assert!(c.has_alpha); - - // Modern syntax with slash: hsl(0 1 0.5 / 0.5) - let c = Color::try_from_str("hsl(0 1 0.5 / 0.5)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff000080"); - assert!(c.has_alpha); - } - - // Test hwb(...) parsing according to CSS: - // hwb(h w b / a) - // w,b are fractions 0..1 (either given as percent or fraction) - // h is a hue in degrees. - #[test] - fn test_hwb_parsing_css_spec() { - // hwb(0 0% 0%) = pure red (#ff0000) - let c = Color::try_from_str("hwb(0 0% 0%)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff0000"); - - // Modern syntax: hwb(0 0 0) same as above if we treat 0 as fraction 0.0 - let c = Color::try_from_str("hwb(0 0 0)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff0000"); - - // With alpha: hwb(0 0 0 / 0.5) - let c = Color::try_from_str("hwb(0 0 0 / 0.5)").unwrap(); - assert_eq!(c.to_hex_string(), "#ff000080"); - assert!(c.has_alpha); - } - - // Test hue normalization and clamping behavior: - // Hue should wrap around: hsl(-30,100%,50%) = hsl(330,100%,50%) - #[test] - fn test_hue_normalization() { - let c = Color::try_from_str("hsl(-30,100%,50%)").unwrap(); - // hsl(330,100%,50%) is a magenta-ish color (#ff00bf approximately) - // Let's just check hue: expected #ff00bf or close - // Since this code uses precise calculations, let's verify hex: - // hsl_to_srgb(330°,1,0.5) => hue2rgb computations - // At 330°, we get a color close to #ff00bf indeed. - let hex = c.to_hex_string(); - // Accept a close match because floating math: - // We can just assert hue & channel correctness: - let (h, _, _) = c.to_hsl(); - assert!( - (h.round() - 330.0).abs() < 0.1, - "Hue should normalize to ~330, got {}", - h.round() - ); - // Check approximate color: - assert_eq!(hex, "#ff007f"); - } - - // Test that alpha is handled correctly: - // If alpha is 1.0 and was not specified initially, has_alpha=false. - // If alpha is changed, has_alpha should become true if alpha != 1.0. - #[test] - fn test_alpha_handling() { - let c = Color::try_from_str("#ff0000").unwrap(); - assert_eq!(c.to_hex_string(), "#ff0000"); - assert!(!c.has_alpha); - - let c2 = c.rgba(0.5); - assert_eq!(c2.to_hex_string(), "#ff000080"); - assert!(c2.has_alpha); - - let c3 = c2.rgba(1.0); - // This is a design choice: once alpha was introduced, we keep has_alpha - assert!( - c3.has_alpha, - "Once alpha introduced, we keep has_alpha true." - ); - } - - // Test some conversions: - // CSS requires clamping channels to [0..255] and alpha to [0..1]. - #[test] - fn test_clamping() { - let c = Color::new(255, 255, 255, 2.0); - // Clamped to 255,255,255 and alpha=1.0 - assert_eq!(c.to_hex_string(), "#ffffffff"); - - let c2 = Color::new(0, 0, 0, -1.0); - // alpha clamped to 0.0 - assert_eq!(c2.to_hex_string(), "#00000000"); - assert!(c2.has_alpha); - } - - // Test mixing colors: - // CSS does not fully define a "mix" function like SASS, but we can verify correctness of arithmetic. - // According to our logic: mix(#ff0000,#0000ff,50%) => average of red and blue - #[test] - fn test_mix() { - let red = Color::try_from_str("red").unwrap(); - let blue = Color::try_from_str("blue").unwrap(); - let mixed = Color::mix(red, blue, 50.0); - // Mix should be #800080 (purple) if we do a simple linear blend. - // (255*0.5=127.5 ~128, but rounding might give #800080) - assert_eq!(mixed.to_hex_string(), "#800080"); - } - - // Test hsl adjustments: - // hsl(0,100%,50%) is red. Adjust hue by +120deg: - // hsl(120,100%,50%) = #00ff00 (green) - #[test] - fn test_adjust_hue() { - let red = Color::try_from_str("red").unwrap(); - let green = red.adjust_hue(120.0); - assert_eq!(green.to_hex_string(), "#00ff00"); - } - - // Test saturation changes: - // Start with hsl(0,50%,50%) ~ #ff8080 (less saturated red) - // Saturate by 50% => hsl(0, ~75%,50%) => more red - #[test] - fn test_saturate_desaturate() { - let c = Color::try_from_str("hsl(0,50%,50%)").unwrap(); - // ~#ff8080 (check exact: s=50% l=50% red hue) - assert_eq!(c.to_hex_string(), "#bf4040"); - let satur = c.saturate(50.0); - // Now s=100% (50%+50% of difference = 50+(50)=100%) - assert_eq!(satur.to_hex_string(), "#ff0000"); - - let desat = satur.desaturate(100.0); - // s=0% => gray: l=50% = #808080 - assert_eq!(desat.to_hex_string(), "#808080"); - } - - // Test lighten/darken: - // hsl(0,100%,50%) = red - // lighten by 10% => l=60% => hsl(0,100%,60%) ~ #ff3333 - // darken by 10% => l=40% => hsl(0,100%,40%) ~ #cc0000 - #[test] - fn test_lighten_darken() { - let red = Color::try_from_str("red").unwrap(); - let lighter = red.lighten(10.0); - // l=60% => check result approximately #ff3333 - assert_eq!(lighter.to_hex_string(), "#ff3333"); - - let darker = red.darken(10.0); - // l=40% => ~ #cc0000 - assert_eq!(darker.to_hex_string(), "#cc0000"); - } - - // Test alpha operations: - // from red (#ff0000, a=1.0) -> fade_out(0.5) => a=0.5 => #ff000080 - #[test] - fn test_alpha_operations() { - let red = Color::try_from_str("red").unwrap(); - let faded = red.fade_out(0.5); - assert_eq!(faded.to_hex_string(), "#ff000080"); - assert!(faded.has_alpha); - - let opaque = faded.fade_in(0.5); - // back to alpha=1.0, but has_alpha was true before - assert_eq!(opaque.to_hex_string(), "#ff0000ff"); - assert!(opaque.has_alpha); - } -} diff --git a/src/core/component.rs b/src/core/component.rs index a75a92e..bb59241 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -599,8 +599,7 @@ mod tests { for &(_, abbreviation) in PROPERTIES.iter() { assert!( seen.insert(abbreviation), - "Duplicate abbreviation found: {}", - abbreviation + "Duplicate abbreviation found: {abbreviation}" ); } } diff --git a/src/core/config/config_fs.rs b/src/core/config/config_fs.rs index a256e96..31b9c2f 100644 --- a/src/core/config/config_fs.rs +++ b/src/core/config/config_fs.rs @@ -570,7 +570,7 @@ impl ConfigFs { } } Err(e) => { - add_message(format!("Failed to read glob pattern {}: {}", pattern, e)); + add_message(format!("Failed to read glob pattern {pattern}: {e}")); } } } @@ -653,23 +653,20 @@ impl ConfigFs { } add_message(format!( - "Loaded external scrolls from '{}'", - file_name + "Loaded external scrolls from '{file_name}'" )); } } Err(err) => { add_message(format!( - "Failed to parse external scroll file '{}': {}", - file_name, err + "Failed to parse external scroll file '{file_name}': {err}" )); } } } Err(err) => { add_message(format!( - "Failed to read external scroll file '{}': {}", - file_name, err + "Failed to read external scroll file '{file_name}': {err}" )); } } @@ -677,10 +674,7 @@ impl ConfigFs { } } Err(err) => { - add_message(format!( - "Failed to search for external scroll files: {}", - err - )); + add_message(format!("Failed to search for external scroll files: {err}")); } } @@ -761,23 +755,20 @@ impl ConfigFs { } add_message(format!( - "Loaded external variables from '{}'", - file_name + "Loaded external variables from '{file_name}'" )); } } Err(err) => { add_message(format!( - "Failed to parse external variables file '{}': {}", - file_name, err + "Failed to parse external variables file '{file_name}': {err}" )); } } } Err(err) => { add_message(format!( - "Failed to read external variables file '{}': {}", - file_name, err + "Failed to read external variables file '{file_name}': {err}" )); } } @@ -786,8 +777,7 @@ impl ConfigFs { } Err(err) => { add_message(format!( - "Failed to search for external variables files: {}", - err + "Failed to search for external variables files: {err}" )); } } diff --git a/src/core/css_builder/css_builder_base.rs b/src/core/css_builder/css_builder_base.rs index 12a2bf9..8583dfe 100644 --- a/src/core/css_builder/css_builder_base.rs +++ b/src/core/css_builder/css_builder_base.rs @@ -2,7 +2,7 @@ //! //! Both filesystem and in-memory builders extend this functionality. -use crate::core::{css_generator::CssGenerator, spell::Spell, CssOptimizer, GrimoireCssError}; +use crate::core::{CssOptimizer, GrimoireCssError, css_generator::CssGenerator, spell::Spell}; use std::collections::HashMap; /// Core CSS builder that handles spell compilation and optimization @@ -69,7 +69,7 @@ impl<'a> CssBuilder<'a> { )?; let updated_css = self.css_generator.replace_class_name( - &css.1 .1, + &css.1.1, &class_name.0, &css.0, ); diff --git a/src/core/css_builder/css_builder_fs.rs b/src/core/css_builder/css_builder_fs.rs index 86cad7b..4d5fea6 100644 --- a/src/core/css_builder/css_builder_fs.rs +++ b/src/core/css_builder/css_builder_fs.rs @@ -12,8 +12,8 @@ use crate::{ buffer::add_message, core::{ - build_info::BuildInfo, file_tracker::FileTracker, parser::ParserFs, spell::Spell, ConfigFs, - ConfigFsCssCustomProperties, CssOptimizer, GrimoireCssError, + ConfigFs, ConfigFsCssCustomProperties, CssOptimizer, GrimoireCssError, + build_info::BuildInfo, file_tracker::FileTracker, parser::ParserFs, spell::Spell, }, }; use regex::Regex; @@ -335,7 +335,7 @@ impl<'a> CssBuilderFs<'a> { let variables = css_custom_properties_item .css_variables .iter() - .map(|(var_name, var_value)| format!("--{}: {};", var_name, var_value)) + .map(|(var_name, var_value)| format!("--{var_name}: {var_value};")) .collect::>() .join(" "); format!( @@ -375,9 +375,8 @@ impl<'a> CssBuilderFs<'a> { Ok(contents) => files_content.push(contents), Err(err) => { return Err(GrimoireCssError::InvalidInput(format!( - "Error reading file {}; {}", - item, err - ))) + "Error reading file {item}; {err}" + ))); } } } else if let Some(spell) = @@ -434,10 +433,7 @@ impl<'a> CssBuilderFs<'a> { shared_css_str: &str, ) -> Result<(), GrimoireCssError> { let html_content = fs::read_to_string(html_file_path)?; - let critical_css = format!( - "", - shared_css_str - ); + let critical_css = format!(""); // Remove existing critical CSS let cleaned_html_content = self.inline_css_regex.replace(&html_content, "").to_string(); @@ -445,10 +441,10 @@ impl<'a> CssBuilderFs<'a> { // Insert the critical CSS just before the closing tag let updated_html_content = if let Some(index) = cleaned_html_content.rfind("") { let (before_head, after_head) = cleaned_html_content.split_at(index); - format!("{}{}{}", before_head, critical_css, after_head) + format!("{before_head}{critical_css}{after_head}") } else { // If is not found, append the critical CSS at the end - format!("{}{}", cleaned_html_content, critical_css) + format!("{cleaned_html_content}{critical_css}") }; fs::write(html_file_path, updated_html_content)?; @@ -504,7 +500,7 @@ mod tests { }], }; - let result = builder.compile_css(&vec![build_info]); + let result = builder.compile_css(&[build_info]); assert!(result.is_ok()); let compiled_css = result.unwrap(); diff --git a/src/core/css_builder/css_builder_in_memory.rs b/src/core/css_builder/css_builder_in_memory.rs index 6b4bc1c..b7833ba 100644 --- a/src/core/css_builder/css_builder_in_memory.rs +++ b/src/core/css_builder/css_builder_in_memory.rs @@ -6,8 +6,8 @@ use std::collections::HashSet; use crate::core::{ - compiled_css::CompiledCssInMemory, config::config_in_memory::ConfigInMemory, parser::Parser, - spell::Spell, CssOptimizer, GrimoireCssError, + CssOptimizer, GrimoireCssError, compiled_css::CompiledCssInMemory, + config::config_in_memory::ConfigInMemory, parser::Parser, spell::Spell, }; use super::CssBuilder; @@ -132,7 +132,7 @@ mod tests { let mut builder = CssBuilderInMemory::new(&config, &optimizer).unwrap(); let result = builder.build().unwrap(); - println!("result: {:?}", result); + println!("result: {result:?}"); assert_eq!(result.len(), 1); assert_eq!(result[0].name, "test"); assert!(result[0].content.eq(".display\\=flex{display:flex;}")); diff --git a/src/core/css_generator/color_functions.rs b/src/core/css_generator/color_functions.rs index 9c08feb..b272845 100644 --- a/src/core/css_generator/color_functions.rs +++ b/src/core/css_generator/color_functions.rs @@ -37,7 +37,7 @@ static SPELL_COLOR_FUNCTIONS: &[(&str, SpellColorFunc)] = &[ /// /// # Arguments /// -/// * `adapted_target` - A string slice in the form of `"g-func(...)"`, +/// * `adapted_target` - A string slice in the form of `"g-func(...)"`, /// for example: `"g-lighten(#ff0000 10)"`. /// /// # Returns diff --git a/src/core/css_generator/css_generator_base.rs b/src/core/css_generator/css_generator_base.rs index 4f0fe1d..f782167 100644 --- a/src/core/css_generator/css_generator_base.rs +++ b/src/core/css_generator/css_generator_base.rs @@ -19,10 +19,10 @@ //! 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; use crate::core::spell::Spell; -use crate::core::GrimoireCssError; use super::color_functions; @@ -162,7 +162,7 @@ impl<'a> CssGenerator<'a> { } fn wrap_size_area(&self, area: &str, base_css: &str) -> String { - format!("@media (min-width: {}){{{}}}", area, base_css) + format!("@media (min-width: {area}){{{base_css}}}") } pub fn generate_css_class_name( @@ -176,9 +176,9 @@ impl<'a> CssGenerator<'a> { let mut escaped_class_name = self.escape_css_class_name(raw_spell)?; if with_template { - escaped_class_name = format!(".g\\!{}\\;", escaped_class_name); + escaped_class_name = format!(".g\\!{escaped_class_name}\\;"); } else { - escaped_class_name = format!(".{}", escaped_class_name); + escaped_class_name = format!(".{escaped_class_name}"); } let effects_string = Self::generate_effect(effects)?; @@ -186,7 +186,7 @@ impl<'a> CssGenerator<'a> { let base_class_name = escaped_class_name.clone(); if !effects_string.is_empty() { - escaped_class_name.push_str(&format!(":{}", effects_string)); + escaped_class_name.push_str(&format!(":{effects_string}")); } if !spell_focus.is_empty() { @@ -212,7 +212,7 @@ impl<'a> CssGenerator<'a> { .map(|c| match c { '!' | '"' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | '.' | '/' | ':' | ';' | '<' | '=' | '>' | '?' | '@' | '[' | '\\' | ']' | '^' | '_' - | '`' | '{' | '|' | '}' | '~' => format!("\\{}", c), + | '`' | '{' | '|' | '}' | '~' => format!("\\{c}"), ' ' => { add_message("HTML does not support spaces. To separate values use underscore ('_') instead".to_string()); c.to_string() @@ -267,7 +267,7 @@ impl<'a> CssGenerator<'a> { if let Some(v) = variables { for (key, value) in v { - let placeholder = format!("${}", key); + let placeholder = format!("${key}"); replaced_target = replaced_target.replace(&placeholder, value); } } @@ -300,8 +300,10 @@ impl<'a> CssGenerator<'a> { ) -> Result<(String, Option), GrimoireCssError> { match property { "g-anim" => self.handle_g_anim(adapted_target, css_class_name), - "animation" => self.handle_animation(adapted_target, css_class_name), - "animation-name" => self.handle_animation_name(adapted_target, css_class_name), + "animation" | "anim" => self.handle_animation(adapted_target, css_class_name), + "animation-name" | "anim-n" => { + self.handle_animation_name(adapted_target, css_class_name) + } _ => { if let Some(css_str) = try_handle_color_function(adapted_target) { self.handle_generic_css(&css_str, css_class_name, property) @@ -370,7 +372,7 @@ impl<'a> CssGenerator<'a> { css_class_name: &str, ) -> Result<(String, Option), GrimoireCssError> { let additional_css = self.get_additional_css(adapted_target)?; - let base_css = format!("{}{{animation:{};}}", css_class_name, adapted_target); + let base_css = format!("{css_class_name}{{animation:{adapted_target};}}"); Ok((base_css, additional_css)) } @@ -393,7 +395,7 @@ impl<'a> CssGenerator<'a> { css_class_name: &str, ) -> Result<(String, Option), GrimoireCssError> { let additional_css = self.get_additional_css(adapted_target)?; - let base_css = format!("{}{{animation-name:{};}}", css_class_name, adapted_target); + let base_css = format!("{css_class_name}{{animation-name:{adapted_target};}}"); Ok((base_css, additional_css)) } @@ -417,7 +419,7 @@ impl<'a> CssGenerator<'a> { css_class_name: &str, property: &str, ) -> Result<(String, Option), GrimoireCssError> { - let base_css = format!("{}{{{}:{};}}", css_class_name, property, adapted_target); + let base_css = format!("{css_class_name}{{{property}:{adapted_target};}}"); let captures = self .base_css_regex .captures_iter(adapted_target) @@ -428,7 +430,7 @@ impl<'a> CssGenerator<'a> { self.handle_grimoire_functions(adapted_target, captures, property, css_class_name)? { Ok(( - format!("{}{{{}:{};}}{}", css_class_name, property, base, media), + format!("{css_class_name}{{{property}:{base};}}{media}"), None, )) } else { @@ -461,12 +463,14 @@ impl<'a> CssGenerator<'a> { self.get_keyframe_class_from_animation(animation, grimoire_animation_name)?; return Ok(Some(keyframes)); } - } + }; - if let Some(custom_animation) = self.custom_animations.get(adapted_target) { - let (keyframes, _) = - self.get_keyframe_class_from_animation(custom_animation, adapted_target)?; - return Ok(Some(keyframes)); + for adapted_target_item in adapted_target.split_whitespace() { + if let Some(custom_animation) = self.custom_animations.get(adapted_target_item) { + let (keyframes, _) = + self.get_keyframe_class_from_animation(custom_animation, adapted_target_item)?; + return Ok(Some(keyframes)); + } } Ok(None) @@ -508,7 +512,7 @@ impl<'a> CssGenerator<'a> { if let Some((base_value, media_queries)) = self.handle_mrs(args, &mut screen_sizes_state)? { - let key = format!("mrs_{}", calculations_base_count); + let key = format!("mrs_{calculations_base_count}"); calculations_base_count += 1; // Add media sizes in the order returned from handle_mrs @@ -534,7 +538,7 @@ impl<'a> CssGenerator<'a> { } "mfs" => { let clamp_value = self.handle_mfs(args)?; - let key = format!("mfs_{}", calculations_base_count); + let key = format!("mfs_{calculations_base_count}"); calculations_base_count += 1; calculation_map.insert( @@ -676,15 +680,15 @@ impl<'a> CssGenerator<'a> { max_vw: Option<&str>, screen_sizes_state: &mut HashSet, ) -> Result, GrimoireCssError> { - let min_size_value: u32 = self.strip_unit(min_size)?; - let max_size_value: u32 = self.strip_unit(max_size)?; - let min_vw_value: u32 = match min_vw { + let min_size_value: f64 = self.strip_unit(min_size)?; + let max_size_value: f64 = self.strip_unit(max_size)?; + let min_vw_value: f64 = match min_vw { Some(i) => self.strip_unit(i)?, - None => 480, + None => 480.0, }; - let max_vw_value: u32 = match max_vw { + let max_vw_value: f64 = match max_vw { Some(i) => self.strip_unit(i)?, - None => 1280, + None => 1280.0, }; let min_size_unit = self.mrs_regex.find(min_size).map_or("", |m| m.as_str()); @@ -698,8 +702,8 @@ impl<'a> CssGenerator<'a> { None => "px", }; - let full_min_vw = format!("{}{}", min_vw_value, min_vw_unit); - let full_max_vw = format!("{}{}", max_vw_value, max_vw_unit); + let full_min_vw = format!("{min_vw_value}{min_vw_unit}"); + let full_max_vw = format!("{max_vw_value}{max_vw_unit}"); // update state and handle different screen sizes if screen_sizes_state.is_empty() { @@ -714,8 +718,7 @@ impl<'a> CssGenerator<'a> { )); } else if screen_sizes_state.len() != 2 { return Err(GrimoireCssError::InvalidInput(format!( - "Unexpected screen size state: {:?}", - screen_sizes_state + "Unexpected screen size state: {screen_sizes_state:?}" ))); } @@ -729,16 +732,12 @@ impl<'a> CssGenerator<'a> { let base = min_size.to_owned(); let media: [(String, String); 2] = [ ( - format!("{}{}", min_vw_value, min_vw_unit), + format!("{min_vw_value}{min_vw_unit}"), format!( - "calc({} + {} * ((100vw - {}{}) / {}))", - min_size, size_diff, min_vw_value, min_vw_unit, vw_diff + "calc({min_size} + {size_diff} * ((100vw - {min_vw_value}{min_vw_unit}) / {vw_diff}))" ), ), - ( - format!("{}{}", max_vw_value, max_vw_unit), - max_size.to_string(), - ), + (format!("{max_vw_value}{max_vw_unit}"), max_size.to_string()), ]; Ok(Some((base, media))) @@ -767,17 +766,17 @@ impl<'a> CssGenerator<'a> { min_vw: Option<&str>, max_vw: Option<&str>, ) -> Result { - let min_size_value: f64 = self.strip_unit(min_size)? as f64; - let max_size_value: f64 = self.strip_unit(max_size)? as f64; + let min_size_value: f64 = self.strip_unit(min_size)?; + let max_size_value: f64 = self.strip_unit(max_size)?; + let min_vw_value: f64 = match min_vw { - Some(i) => self.strip_unit(i)? as f64, + Some(i) => self.strip_unit(i)?, None => 480.0, }; let max_vw_value: f64 = match max_vw { - Some(i) => self.strip_unit(i)? as f64, + Some(i) => self.strip_unit(i)?, None => 1280.0, }; - let min_size_unit = self.mrs_regex.find(min_size).map_or("", |m| m.as_str()); let max_size_unit = self.mrs_regex.find(max_size).map_or("", |m| m.as_str()); @@ -801,7 +800,7 @@ impl<'a> CssGenerator<'a> { let preferred = format!("{}vw + {}{}", slope * 100.0, intercept, min_size_unit); - Ok(format!("clamp({}, {}, {})", min_size, preferred, max_size)) + Ok(format!("clamp({min_size}, {preferred}, {max_size})")) } /// Strips the unit from a CSS size value and returns the numeric part. @@ -812,20 +811,16 @@ impl<'a> CssGenerator<'a> { /// /// # Returns /// - /// * `Ok(u32)` containing the numeric part of the value. + /// * `Ok(f64)` containing the numeric part of the value. /// * `Err(GrimoireCSSError)` if there is an error during unit stripping. - fn strip_unit(&self, value: &str) -> Result { + fn strip_unit(&self, value: &str) -> Result { if let Some(captures) = self.unit_regex.captures(value) { - captures[1].parse::().map_err(|_| { - GrimoireCssError::InvalidInput(format!( - "Failed to parse unit from value: {}", - value - )) + captures[1].parse::().map_err(|_| { + GrimoireCssError::InvalidInput(format!("Failed to parse unit from value: {value}")) }) } else { Err(GrimoireCssError::InvalidInput(format!( - "No numeric value found in: {}", - value + "No numeric value found in: {value}" ))) } } @@ -859,15 +854,14 @@ impl<'a> CssGenerator<'a> { Ok((keyframes.trim().to_string(), class_block)) } else { Err(GrimoireCssError::InvalidInput(format!( - "No keyframes found in animation: {}", - animation_name + "No keyframes found in animation: {animation_name}" ))) } } } #[cfg(test)] mod tests { - use crate::core::{css_generator::CssGenerator, spell::Spell, ConfigFs, GrimoireCssError}; + use crate::core::{ConfigFs, GrimoireCssError, css_generator::CssGenerator, spell::Spell}; #[test] fn test_escape_css_class_name() { diff --git a/src/core/file_tracker.rs b/src/core/file_tracker.rs index 528f676..ecded33 100644 --- a/src/core/file_tracker.rs +++ b/src/core/file_tracker.rs @@ -45,10 +45,7 @@ impl FileTracker { if file_path.exists() { fs::remove_file(&file_path)?; } else { - eprintln!( - "Warning: File {} does not exist and cannot be deleted.", - file - ); + eprintln!("Warning: File {file} does not exist and cannot be deleted."); } } } @@ -86,8 +83,7 @@ mod tests { let lock_file_path = cwd.join("grimoire/grimoire.lock.json"); assert!( lock_file_path.exists(), - "Lock file was not created at expected path: {:?}", - lock_file_path + "Lock file was not created at expected path: {lock_file_path:?}" ); } @@ -120,19 +116,16 @@ mod tests { assert!( !old_file1.exists(), - "Old file was not deleted: {:?}", - old_file1 + "Old file was not deleted: {old_file1:?}" ); assert!( !old_file2.exists(), - "Old file was not deleted: {:?}", - old_file2 + "Old file was not deleted: {old_file2:?}" ); assert!( new_file.exists(), - "New file was unexpectedly deleted: {:?}", - new_file + "New file was unexpectedly deleted: {new_file:?}" ); } @@ -150,8 +143,7 @@ mod tests { let lock_file_path = cwd.join("grimoire/grimoire.lock.json"); assert!( lock_file_path.exists(), - "Lock file was not created at expected path: {:?}", - lock_file_path + "Lock file was not created at expected path: {lock_file_path:?}" ); } } diff --git a/src/core/grimoire_css_error.rs b/src/core/grimoire_css_error.rs index 63345d8..3770efa 100644 --- a/src/core/grimoire_css_error.rs +++ b/src/core/grimoire_css_error.rs @@ -37,14 +37,14 @@ pub enum GrimoireCssError { 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), + 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}"), } } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 987aee0..d8c6238 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -11,7 +11,6 @@ mod css_generator; mod file_tracker; mod filesystem; -pub mod color; pub mod compiled_css; pub mod component; pub mod config; @@ -21,10 +20,11 @@ pub mod grimoire_css_error; pub mod parser; pub mod spell; -pub use color::*; pub use compiled_css::*; pub use config::*; pub use css_builder::*; pub use css_optimizer::*; pub use filesystem::*; pub use grimoire_css_error::*; +// 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 34841ae..91fc18b 100644 --- a/src/core/parser/parser_base.rs +++ b/src/core/parser/parser_base.rs @@ -7,12 +7,22 @@ use std::collections::HashSet; use crate::core::GrimoireCssError; +/// Represents the type of class collection being performed +#[derive(Debug, Clone, Copy)] +enum CollectionType { + TemplatedSpell, + CurlyClass, + RegularClass, +} + /// Base `Parser` is responsible for extracting CSS class names and templated spells from content. /// It uses regular expressions to find class names and spell-like patterns. pub struct Parser { tepmplated_spell_regex: Regex, class_name_regex: Regex, class_regex: Regex, + curly_class_name_regex: Regex, + curly_class_regex: Regex, } impl Parser { @@ -22,12 +32,71 @@ impl Parser { let class_name_regex = Regex::new(r#"className=("([^"]*)"|'([^']*)'|`([^`]*)`)"#).unwrap(); let class_regex = Regex::new(r#"class=("([^"]*)"|'([^']*)'|`([^`]*)`)"#).unwrap(); let tepmplated_spell_regex = Regex::new(r#"(g![^;]*;)"#).unwrap(); + let curly_class_name_regex = Regex::new(r#"className=\{((?:[^{}]|\{[^}]*\})*)\}"#).unwrap(); + let curly_class_regex = Regex::new(r#"class=\{((?:[^{}]|\{[^}]*\})*)\}"#).unwrap(); Self { tepmplated_spell_regex, class_name_regex, class_regex, + curly_class_name_regex, + curly_class_regex, + } + } + + /// Removes unpaired brackets and quotes from a string + fn clean_unpaired_brackets(s: &str) -> String { + let chars: Vec = s.chars().collect(); + let mut result = Vec::with_capacity(chars.len()); + let mut stack = Vec::new(); + let mut keep = vec![false; chars.len()]; + + // First pass: mark paired brackets + for (i, &ch) in chars.iter().enumerate() { + match ch { + '(' | '[' | '{' => stack.push((ch, i)), + ')' => { + if let Some((open, open_idx)) = stack.pop() { + if open == '(' { + keep[open_idx] = true; + keep[i] = true; + } + } + } + ']' => { + if let Some((open, open_idx)) = stack.pop() { + if open == '[' { + keep[open_idx] = true; + keep[i] = true; + } + } + } + '}' => { + if let Some((open, open_idx)) = stack.pop() { + if open == '{' { + keep[open_idx] = true; + keep[i] = true; + } + } + } + _ => {} + } + } + + // Second pass: build result, keeping only paired brackets and other chars + for (i, &ch) in chars.iter().enumerate() { + match ch { + '(' | ')' | '[' | ']' | '{' | '}' => { + if keep[i] { + result.push(ch); + } + } + '\'' | '"' | '`' => {} // Remove quotes + _ => result.push(ch), + } } + + result.into_iter().collect() } /// Collects class names from content based on the given regular expression and optional predicate/splitter functions. @@ -51,25 +120,35 @@ impl Parser { mut splitter: Option, class_names: &mut Vec, seen_class_names: &mut HashSet, - is_templated_spell: bool, + collection_type: CollectionType, ) -> Result<(), GrimoireCssError> where P: FnMut(&str) -> bool, S: FnMut(&str) -> Vec, { for cap in regex.captures_iter(content) { - let class_value = if is_templated_spell { - cap.get(1).map(|m| m.as_str()).unwrap_or("") - } else { - cap.get(2) + 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("") + .unwrap_or(""), }; let classes = if let Some(splitter_fn) = &mut splitter { - splitter_fn(class_value) + 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 + } } else { vec![class_value.to_string()] }; @@ -119,7 +198,7 @@ impl Parser { Some(whitespace_splitter), class_names, seen_class_names, - false, + CollectionType::RegularClass, )?; // Collect all 'class' matches @@ -130,7 +209,7 @@ impl Parser { Some(whitespace_splitter), class_names, seen_class_names, - false, + CollectionType::RegularClass, )?; // Collect all 'templated class' (starts with 'g!', ends with ';') matches @@ -141,7 +220,29 @@ impl Parser { None, class_names, seen_class_names, - true, + CollectionType::TemplatedSpell, + )?; + + // Collect all curly 'className' matches + Self::collect_classes:: bool, fn(&str) -> Vec>( + content, + &self.curly_class_name_regex, + None, + Some(whitespace_splitter), + class_names, + seen_class_names, + CollectionType::CurlyClass, + )?; + + // Collect all curly 'class' matches + Self::collect_classes:: bool, fn(&str) -> Vec>( + content, + &self.curly_class_regex, + None, + Some(whitespace_splitter), + class_names, + seen_class_names, + CollectionType::CurlyClass, )?; Ok(()) @@ -219,7 +320,61 @@ mod tests { assert_eq!(class_names.len(), 6); for i in 1..=6 { - assert!(class_names.contains(&format!("test{}", i))); + assert!(class_names.contains(&format!("test{i}"))); } } + + #[test] + fn test_collect_curly_class_and_classname_attributes() { + let parser = Parser::new(); + let mut class_names = Vec::new(); + let mut seen_class_names = HashSet::new(); + + let content = r#" +
+
+ "#; + + parser + .collect_candidates(content, &mut class_names, &mut seen_class_names) + .unwrap(); + + 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())); + } + + #[test] + fn test_clean_unpaired_brackets() { + let parser = Parser::new(); + let mut class_names = Vec::new(); + let mut seen_class_names = HashSet::new(); + + let content = r#" +
+
+ "#; + + parser + .collect_candidates(content, &mut class_names, &mut seen_class_names) + .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())); + } } diff --git a/src/core/parser/parser_fs.rs b/src/core/parser/parser_fs.rs index a7aee63..c7a294f 100644 --- a/src/core/parser/parser_fs.rs +++ b/src/core/parser/parser_fs.rs @@ -281,7 +281,7 @@ mod tests { let temp_dir = tempdir().unwrap(); let parser = ParserFs::new(temp_dir.path()); let result = parser.collect_classes_single_output(&vec!["nonexistent.html".to_string()]); - println!("{:?}", result); + println!("{result:?}"); assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 0); } diff --git a/src/core/spell.rs b/src/core/spell.rs index dfada97..fe9fd36 100644 --- a/src/core/spell.rs +++ b/src/core/spell.rs @@ -27,7 +27,7 @@ use std::collections::{HashMap, HashSet}; -use super::{component::get_css_property, GrimoireCssError}; +use super::{GrimoireCssError, component::get_css_property}; #[derive(Eq, Hash, PartialEq, Debug, Clone)] pub struct Spell { @@ -202,8 +202,7 @@ impl Spell { if count_of_used_variables != count_of_variables { return Err(GrimoireCssError::InvalidInput(format!( - "Not all variables used in scroll '{}'. Expected {}, but used {}", - scroll_name, count_of_variables, count_of_used_variables, + "Not all variables used in scroll '{scroll_name}'. Expected {count_of_variables}, but used {count_of_used_variables}", ))); } diff --git a/src/infrastructure/lightning_css_optimizer.rs b/src/infrastructure/lightning_css_optimizer.rs index ca1e5e3..4036753 100644 --- a/src/infrastructure/lightning_css_optimizer.rs +++ b/src/infrastructure/lightning_css_optimizer.rs @@ -25,7 +25,7 @@ pub struct LightningCssOptimizer { impl LightningCssOptimizer { fn from_content(browserslist_content: &str) -> Result { let browsers = Browsers::from_browserslist(browserslist_content.lines()).map_err(|e| { - GrimoireCssError::InvalidInput(format!("Failed to parse browserslist: {}", e)) + GrimoireCssError::InvalidInput(format!("Failed to parse browserslist: {e}")) })?; Ok(Self { @@ -47,7 +47,11 @@ impl LightningCssOptimizer { add_message("Created missing '.browserslistrc' file with 'defaults'".to_string()); } - env::set_var("BROWSERSLIST_CONFIG", &browserslist_config_path); + // 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"); @@ -76,7 +80,7 @@ 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)))?; + .map_err(|e| GrimoireCssError::InvalidInput(format!("Failed to parse CSS: {e}")))?; // Apply minification and optimization based on the browser targets. stylesheet @@ -84,7 +88,7 @@ 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::InvalidInput(format!("Failed to minify CSS: {e}")))?; // Generate the final CSS as a string. stylesheet @@ -93,6 +97,6 @@ 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::InvalidInput(format!("Failed to generate CSS: {e}"))) } } diff --git a/src/lib.rs b/src/lib.rs index 83aae8c..b7fdbe0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Core library module that orchestrates the core functionality of the Grimoire CSS system engine. +//! Core library module that orchestrates the core functionality of the Grimoire CSS engine. //! //! This module provides two main functions: //! - [`start`] - Pure function that executes core CSS processing logic @@ -22,7 +22,7 @@ use indicatif::{ProgressBar, ProgressStyle}; use infrastructure::LightningCssOptimizer; use std::time::{Duration, Instant}; -pub use core::{color, component, config, spell::Spell, GrimoireCssError}; +pub use core::{GrimoireCssError, color, component, config, spell::Spell}; static GRIMM_CALM: &str = " |(• ε •)|"; static GRIMM_HAPPY: &str = " ヽ(• ε •)ノ"; @@ -143,9 +143,9 @@ pub fn start_as_cli(args: Vec) -> Result<(), GrimoireCssError> { ); println!(); - println!("{}", GRIMM_CURSED); + println!("{GRIMM_CURSED}"); println!(); - println!("{}", message); + println!("{message}"); return Err(GrimoireCssError::InvalidInput(message)); } @@ -164,7 +164,7 @@ pub fn start_as_cli(args: Vec) -> Result<(), GrimoireCssError> { Ok(_) => { pb.finish_and_clear(); - print!("\r\x1b[2K{} Spells cast successfully.\n", GRIMM_HAPPY); + print!("\r\x1b[2K{GRIMM_HAPPY} Spells cast successfully.\n"); let duration = start_time.elapsed(); @@ -176,7 +176,7 @@ pub fn start_as_cli(args: Vec) -> Result<(), GrimoireCssError> { "{}", style(format!( "{}", - style(format!(" Enchanted in {:.2?}! ", duration)) + style(format!(" Enchanted in {duration:.2?}! ")) .white() .on_color256(55) .bright(), @@ -189,7 +189,7 @@ pub fn start_as_cli(args: Vec) -> Result<(), GrimoireCssError> { } Err(e) => { pb.finish_and_clear(); - print!("\r\x1b[2K{}\n", GRIMM_CURSED); + print!("\r\x1b[2K{GRIMM_CURSED}\n"); println!(); println!("{} {}", style(" Cursed! ").white().on_red().bright(), e); @@ -205,7 +205,7 @@ fn output_saved_messages() { if !messages.is_empty() { println!(); for msg in &messages { - println!(" • {}", msg); + println!(" • {msg}"); } } }