From f0aefed3e456ede69cefbf687b714e71708ea792 Mon Sep 17 00:00:00 2001 From: Mathias Myrland Date: Thu, 29 May 2025 20:36:17 +0200 Subject: [PATCH 1/5] initial token support added --- Cargo.lock | 10 + Cargo.toml | 2 +- crates/dwind-design-tokens/Cargo.toml | 17 + crates/dwind-design-tokens/README.md | 178 +++++ .../examples/circular_reference_demo.rs | 164 +++++ .../examples/parse_example.rs | 109 +++ crates/dwind-design-tokens/src/error.rs | 32 + crates/dwind-design-tokens/src/expressions.rs | 263 +++++++ crates/dwind-design-tokens/src/lib.rs | 270 +++++++ crates/dwind-design-tokens/src/parser.rs | 237 ++++++ crates/dwind-design-tokens/src/types.rs | 336 +++++++++ crates/dwind-design-tokens/src/validation.rs | 696 ++++++++++++++++++ .../dwind-design-tokens/tests/integration.rs | 280 +++++++ .../dwind/resources/design-tokens.tokens.json | 39 + 14 files changed, 2632 insertions(+), 1 deletion(-) create mode 100644 crates/dwind-design-tokens/Cargo.toml create mode 100644 crates/dwind-design-tokens/README.md create mode 100644 crates/dwind-design-tokens/examples/circular_reference_demo.rs create mode 100644 crates/dwind-design-tokens/examples/parse_example.rs create mode 100644 crates/dwind-design-tokens/src/error.rs create mode 100644 crates/dwind-design-tokens/src/expressions.rs create mode 100644 crates/dwind-design-tokens/src/lib.rs create mode 100644 crates/dwind-design-tokens/src/parser.rs create mode 100644 crates/dwind-design-tokens/src/types.rs create mode 100644 crates/dwind-design-tokens/src/validation.rs create mode 100644 crates/dwind-design-tokens/tests/integration.rs create mode 100644 crates/dwind/resources/design-tokens.tokens.json diff --git a/Cargo.lock b/Cargo.lock index 996a4e7..4d1e7ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,6 +278,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "dwind-design-tokens" +version = "0.1.0" +dependencies = [ + "nom", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "dwind-macros" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 37099c6..d126615 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" members = [ "crates/dominator-css-bindgen", "crates/dwind", - "crates/dwind-base", "crates/dwind-build", + "crates/dwind-base", "crates/dwind-build", "crates/dwind-design-tokens", "crates/dwind-macros", "crates/dwui", "crates/example-html-macro", diff --git a/crates/dwind-design-tokens/Cargo.toml b/crates/dwind-design-tokens/Cargo.toml new file mode 100644 index 0000000..869b3e2 --- /dev/null +++ b/crates/dwind-design-tokens/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dwind-design-tokens" +version = "0.1.0" +edition = "2021" +description = "Design token parsing and data structures for DWIND" +homepage = "https://github.com/JedimEmO/dwind" +repository = "https://github.com/JedimEmO/dwind" +license = "MIT" +keywords = ["web", "wasm", "css", "design-tokens", "style"] + +[dependencies] +serde.workspace = true +serde_json.workspace = true +nom.workspace = true +thiserror.workspace = true + +[dev-dependencies] \ No newline at end of file diff --git a/crates/dwind-design-tokens/README.md b/crates/dwind-design-tokens/README.md new file mode 100644 index 0000000..eccc734 --- /dev/null +++ b/crates/dwind-design-tokens/README.md @@ -0,0 +1,178 @@ +# DWIND Design Tokens + +A Rust library for parsing and working with design token files in the DWIND ecosystem. This crate provides data structures and parsing capabilities for design tokens following the Design Tokens Community Group specification. + +## Features + +- **JSON Parsing**: Parse design token JSON files into structured Rust types +- **Nested Groups**: Support for hierarchical token organization +- **Expression Parsing**: Parse token references and arithmetic expressions using nom +- **Enhanced Validation**: Comprehensive validation with detailed error reporting and circular reference detection +- **Multiple Token Types**: Support for dimension, number, borderRadius, and color tokens +- **Error Handling**: Robust error handling with thiserror + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +dwind-design-tokens = "0.1.0" +``` + +### Basic Example + +```rust +use dwind_design_tokens::prelude::*; + +let json = r#" +{ + "spacing": { + "small": { + "$value": "8px", + "$type": "dimension", + "$description": "Small spacing unit" + }, + "medium": { + "$value": "{small} * 2", + "$type": "dimension", + "$description": "Medium spacing unit" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["spacing"], + "activeThemes": [], + "activeSets": ["spacing"] + } +} +"#; + +// Parse the token file +let token_file = parse_tokens(json)?; + +// Validate the tokens +let validation_report = validate_token_file(&token_file)?; +assert!(validation_report.is_valid()); + +// Access tokens +let small_token = token_file.find_token("spacing.small").unwrap(); +println!("Small spacing: {:?}", small_token.value); + +// Check for references +let medium_token = token_file.find_token("spacing.medium").unwrap(); +assert!(medium_token.has_references()); +let refs = medium_token.get_references(); +assert_eq!(refs, vec!["small"]); +``` + +### Loading from File + +```rust +use dwind_design_tokens::prelude::*; + +let token_file = parse_tokens_from_file("tokens.json")?; +let validation_report = validate_token_file(&token_file)?; + +if !validation_report.is_valid() { + println!("Validation errors: {}", validation_report.summary()); +} +``` + +## Supported Token Types + +- **Dimension**: Values with units (e.g., "8px", "1.5rem") +- **Number**: Numeric values (e.g., "42", "3.14") +- **BorderRadius**: Border radius values (e.g., "4px", "50%") +- **Color**: Color values (e.g., "#ff0000", "rgb(255, 0, 0)") + +## Expression Support + +The library supports expressions in token values: + +- **Token References**: `{token_name}`, `{group.token_name}` +- **Arithmetic**: `+`, `-`, `*`, `/` +- **Parentheses**: `({a} + {b}) * 2` + +Examples: +- `{base} * 2` +- `{medium} + {small}` +- `({width} - {padding}) / 2` + +## Validation + +The validation system provides comprehensive checking including **enhanced circular reference detection**: + +- **Missing References**: Tokens that reference non-existent tokens +- **Enhanced Circular Reference Detection**: Detects all types of circular dependencies: + - Direct self-references: `{a: "{a}"}` + - Simple circular references: `{a: "{b}", b: "{a}"}` + - Complex circular chains: `{a: "{b}", b: "{c}", c: "{d}", d: "{a}"}` + - Circular references in expressions: `{a: "{b} + 5", b: "{a} * 2"}` +- **Type Mismatches**: Values that don't match their declared type +- **Invalid Values**: Malformed dimension, number, or color values + +### Circular Reference Detection Example + +```rust +use dwind_design_tokens::prelude::*; + +let circular_json = r#" +{ + "spacing": { + "a": {"$value": "{b}", "$type": "dimension"}, + "b": {"$value": "{c}", "$type": "dimension"}, + "c": {"$value": "{a}", "$type": "dimension"} + }, + "$themes": [], + "$metadata": {"tokenSetOrder": ["spacing"], "activeThemes": [], "activeSets": ["spacing"]} +} +"#; + +let token_file = parse_tokens(circular_json)?; +let report = validate_token_file(&token_file)?; + +if !report.is_valid() { + for circular_ref in &report.circular_references { + println!("Detected: {}", circular_ref); + // Output: "Circular reference detected: spacing.a -> spacing.b -> spacing.c -> spacing.a. This creates an infinite dependency loop." + } +} +``` + +## Data Structures + +### Core Types + +- `DesignTokenFile`: Root structure containing all token sets +- `TokenNode`: Either a token or a group of tokens +- `DesignToken`: Individual token with value, type, and description +- `TokenValue`: Literal value, expression, or structured color +- `Expr`: Expression AST for token references and arithmetic + +### Error Handling + +All operations return `TokenResult` which is an alias for `Result`. The `TokenError` enum provides detailed error information for different failure scenarios. + +## Phase 1 Implementation + +This is Phase 1 of the design token support, focusing on: + +- ✅ JSON parsing and data structures +- ✅ Expression AST creation with nom +- ✅ Validation and error handling +- ✅ **Enhanced circular reference detection** +- ❌ Expression evaluation (planned for Phase 2) +- ❌ Circular reference resolution (planned for Phase 2) + +## Integration + +This crate is designed to be used by other parts of the DWIND ecosystem: + +- `dwind-build`: Build-time token processing +- Future code generation tools +- Runtime token resolution systems + +## License + +MIT \ No newline at end of file diff --git a/crates/dwind-design-tokens/examples/circular_reference_demo.rs b/crates/dwind-design-tokens/examples/circular_reference_demo.rs new file mode 100644 index 0000000..c6a0a7c --- /dev/null +++ b/crates/dwind-design-tokens/examples/circular_reference_demo.rs @@ -0,0 +1,164 @@ +//! Demonstration of the enhanced circular reference detection system +//! +//! This example shows how the validation system now detects various types +//! of circular references in design token files. + +use dwind_design_tokens::prelude::*; + +fn main() { + println!("=== Enhanced Circular Reference Detection Demo ===\n"); + + // Example 1: Direct self-reference + println!("1. Testing direct self-reference:"); + let self_ref_json = r#" + { + "test": { + "self_ref": { + "$value": "{self_ref}", + "$type": "dimension", + "$description": "This token references itself" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + match parse_tokens(self_ref_json) { + Ok(file) => { + match validate_token_file(&file) { + Ok(report) => { + if !report.is_valid() { + println!("✓ Detected circular reference:"); + for circular_ref in &report.circular_references { + println!(" - {}", circular_ref); + } + } + } + Err(e) => println!("✗ Validation error: {}", e), + } + } + Err(e) => println!("✗ Parse error: {}", e), + } + + println!(); + + // Example 2: Complex circular chain + println!("2. Testing complex circular chain (A → B → C → A):"); + let chain_json = r#" + { + "spacing": { + "a": { + "$value": "{b}", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{c}", + "$type": "dimension", + "$description": "" + }, + "c": { + "$value": "{a}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["spacing"], + "activeThemes": [], + "activeSets": ["spacing"] + } + } + "#; + + match parse_tokens(chain_json) { + Ok(file) => { + match validate_token_file(&file) { + Ok(report) => { + if !report.is_valid() { + println!("✓ Detected circular reference chain:"); + for circular_ref in &report.circular_references { + println!(" - {}", circular_ref); + } + } + } + Err(e) => println!("✗ Validation error: {}", e), + } + } + Err(e) => println!("✗ Parse error: {}", e), + } + + println!(); + + // Example 3: Valid references (should pass) + println!("3. Testing valid references (should pass):"); + let valid_json = r#" + { + "spacing": { + "base": { + "$value": "8px", + "$type": "dimension", + "$description": "" + }, + "small": { + "$value": "{base}", + "$type": "dimension", + "$description": "" + }, + "medium": { + "$value": "{base} * 2", + "$type": "dimension", + "$description": "" + }, + "large": { + "$value": "{medium} + {small}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["spacing"], + "activeThemes": [], + "activeSets": ["spacing"] + } + } + "#; + + match parse_tokens(valid_json) { + Ok(file) => { + match validate_token_file(&file) { + Ok(report) => { + if report.is_valid() { + println!("✓ All references are valid - no circular dependencies detected"); + println!(" Summary: {}", report.summary()); + } else { + println!("✗ Unexpected validation issues:"); + for error in &report.errors { + println!(" - Error: {}", error); + } + for circular_ref in &report.circular_references { + println!(" - Circular: {}", circular_ref); + } + } + } + Err(e) => println!("✗ Validation error: {}", e), + } + } + Err(e) => println!("✗ Parse error: {}", e), + } + + println!("\n=== Demo Complete ==="); + println!("The enhanced validation system now detects:"); + println!("• Direct self-references (A → A)"); + println!("• Simple circular references (A → B → A)"); + println!("• Complex circular chains (A → B → C → D → A)"); + println!("• Circular references within mathematical expressions"); + println!("• Mixed scenarios with both valid and circular references"); +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/examples/parse_example.rs b/crates/dwind-design-tokens/examples/parse_example.rs new file mode 100644 index 0000000..12cdacf --- /dev/null +++ b/crates/dwind-design-tokens/examples/parse_example.rs @@ -0,0 +1,109 @@ +use dwind_design_tokens::prelude::*; + +fn main() -> Result<(), Box> { + // The example JSON from the task + let json = r##" + { + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a}*3", + "$type": "dimension", + "$description": "" + }, + "primary": { + "$value": "rgb(193, 51, 51)", + "$type": "color", + "$description": "" + }, + "medium": { + "$value": "12px", + "$type": "borderRadius", + "$description": "" + }, + "rounded-md": { + "$value": "{medium}+12", + "$type": "borderRadius", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": [ + "test" + ], + "activeThemes": [], + "activeSets": [ + "test" + ] + } + } + "##; + + println!("Parsing design tokens..."); + + // Parse the tokens + let token_file = parse_tokens(json)?; + println!("✅ Successfully parsed {} token sets", token_file.sets.len()); + + // Validate the tokens + let validation_report = validate_token_file(&token_file)?; + println!("✅ Validation completed: {}", validation_report.summary()); + + if !validation_report.is_valid() { + println!("❌ Validation failed!"); + for error in &validation_report.errors { + println!(" Error: {}", error); + } + for missing in &validation_report.missing_references { + println!(" Missing reference: {}", missing); + } + return Ok(()); + } + + // Display all tokens + println!("\n📋 All tokens:"); + let all_tokens = token_file.get_all_tokens(); + for (path, token) in &all_tokens { + println!(" {} ({:?}): {:?}", path, token.token_type, token.value); + if token.has_references() { + let refs = token.get_references(); + println!(" References: {:?}", refs); + } + } + + // Test specific token access + println!("\n🔍 Testing token access:"); + + if let Some(token_a) = token_file.find_token("test.a") { + println!(" test.a: {:?}", token_a.value); + } + + if let Some(token_b) = token_file.find_token("test.b") { + println!(" test.b: {:?}", token_b.value); + if let TokenValue::Expression(expr) = &token_b.value { + println!(" Expression AST: {:#?}", expr); + } + } + + if let Some(rounded_md) = token_file.find_token("test.rounded-md") { + println!(" test.rounded-md: {:?}", rounded_md.value); + if let TokenValue::Expression(expr) = &rounded_md.value { + println!(" Expression AST: {:#?}", expr); + } + } + + // Show all references + println!("\n🔗 Token references:"); + let all_references = token_file.get_all_references(); + for (token_path, refs) in &all_references { + println!(" {} references: {:?}", token_path, refs); + } + + println!("\n✅ Design token parsing completed successfully!"); + Ok(()) +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/src/error.rs b/crates/dwind-design-tokens/src/error.rs new file mode 100644 index 0000000..d07dbef --- /dev/null +++ b/crates/dwind-design-tokens/src/error.rs @@ -0,0 +1,32 @@ +use thiserror::Error; + +/// Errors that can occur when working with design tokens +#[derive(Debug, Error)] +pub enum TokenError { + #[error("Token not found: {0}")] + NotFound(String), + + #[error("Circular reference detected: {0}")] + CircularReference(String), + + #[error("Invalid expression: {0}")] + InvalidExpression(String), + + #[error("Type mismatch in token: {0}")] + TypeMismatch(String), + + #[error("Deserialization error: {0}")] + Deserialization(#[from] serde_json::Error), + + #[error("Expression parsing error: {0}")] + ExpressionParsing(String), + + #[error("Invalid token value: {0}")] + InvalidValue(String), + + #[error("Missing required field: {0}")] + MissingField(String), +} + +/// Result type for token operations +pub type TokenResult = Result; \ No newline at end of file diff --git a/crates/dwind-design-tokens/src/expressions.rs b/crates/dwind-design-tokens/src/expressions.rs new file mode 100644 index 0000000..c03b72a --- /dev/null +++ b/crates/dwind-design-tokens/src/expressions.rs @@ -0,0 +1,263 @@ +use crate::error::{TokenError, TokenResult}; +use nom::{ + branch::alt, + bytes::complete::{tag, take_while1}, + character::complete::{char, multispace0}, + combinator::{map, recognize}, + multi::many0, + number::complete::double, + sequence::{delimited, pair, preceded, terminated}, + IResult, +}; +use serde::{Deserialize, Serialize}; + +/// Binary operators supported in expressions +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BinaryOperator { + Add, + Sub, + Mul, + Div, +} + +/// Expression AST node +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Expr { + /// Reference to another token (e.g., {token_name} or {group.token_name}) + Reference(String), + /// Numeric literal + Literal(f64), + /// Binary operation + BinaryOp { + op: BinaryOperator, + left: Box, + right: Box, + }, +} + +impl Expr { + /// Parse an expression string into an AST + pub fn parse(input: &str) -> TokenResult { + match parse_expression(input.trim()) { + Ok((remaining, expr)) => { + if remaining.trim().is_empty() { + Ok(expr) + } else { + Err(TokenError::ExpressionParsing(format!( + "Unexpected remaining input: '{}'", + remaining + ))) + } + } + Err(e) => Err(TokenError::ExpressionParsing(format!( + "Failed to parse expression '{}': {}", + input, e + ))), + } + } + + /// Check if this expression contains any token references + pub fn has_references(&self) -> bool { + match self { + Expr::Reference(_) => true, + Expr::Literal(_) => false, + Expr::BinaryOp { left, right, .. } => left.has_references() || right.has_references(), + } + } + + /// Get all token references in this expression + pub fn get_references(&self) -> Vec { + match self { + Expr::Reference(name) => vec![name.clone()], + Expr::Literal(_) => vec![], + Expr::BinaryOp { left, right, .. } => { + let mut refs = left.get_references(); + refs.extend(right.get_references()); + refs + } + } + } +} + +// Nom parser functions + +fn parse_expression(input: &str) -> IResult<&str, Expr> { + parse_additive(input) +} + +fn parse_additive(input: &str) -> IResult<&str, Expr> { + let (input, init) = parse_multiplicative(input)?; + + let (input, ops) = many0(pair( + delimited(multispace0, alt((tag("+"), tag("-"))), multispace0), + parse_multiplicative, + ))(input)?; + + Ok(( + input, + ops.into_iter().fold(init, |acc, (op, val)| Expr::BinaryOp { + op: match op { + "+" => BinaryOperator::Add, + "-" => BinaryOperator::Sub, + _ => unreachable!(), + }, + left: Box::new(acc), + right: Box::new(val), + }), + )) +} + +fn parse_multiplicative(input: &str) -> IResult<&str, Expr> { + let (input, init) = parse_primary(input)?; + + let (input, ops) = many0(pair( + delimited(multispace0, alt((tag("*"), tag("/"))), multispace0), + parse_primary, + ))(input)?; + + Ok(( + input, + ops.into_iter().fold(init, |acc, (op, val)| Expr::BinaryOp { + op: match op { + "*" => BinaryOperator::Mul, + "/" => BinaryOperator::Div, + _ => unreachable!(), + }, + left: Box::new(acc), + right: Box::new(val), + }), + )) +} + +fn parse_primary(input: &str) -> IResult<&str, Expr> { + delimited( + multispace0, + alt(( + parse_parenthesized, + parse_reference, + parse_literal, + )), + multispace0, + )(input) +} + +fn parse_parenthesized(input: &str) -> IResult<&str, Expr> { + delimited(char('('), parse_expression, char(')'))(input) +} + +fn parse_reference(input: &str) -> IResult<&str, Expr> { + map( + delimited( + char('{'), + take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '.' || c == '-'), + char('}'), + ), + |s: &str| Expr::Reference(s.to_string()), + )(input) +} + +fn parse_literal(input: &str) -> IResult<&str, Expr> { + map(double, Expr::Literal)(input) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_literal() { + let expr = Expr::parse("42").unwrap(); + assert_eq!(expr, Expr::Literal(42.0)); + } + + #[test] + fn test_parse_reference() { + let expr = Expr::parse("{token_name}").unwrap(); + assert_eq!(expr, Expr::Reference("token_name".to_string())); + } + + #[test] + fn test_parse_dotted_reference() { + let expr = Expr::parse("{group.token_name}").unwrap(); + assert_eq!(expr, Expr::Reference("group.token_name".to_string())); + } + + #[test] + fn test_parse_simple_addition() { + let expr = Expr::parse("{a} + 3").unwrap(); + assert_eq!( + expr, + Expr::BinaryOp { + op: BinaryOperator::Add, + left: Box::new(Expr::Reference("a".to_string())), + right: Box::new(Expr::Literal(3.0)), + } + ); + } + + #[test] + fn test_parse_multiplication() { + let expr = Expr::parse("{a}*3").unwrap(); + assert_eq!( + expr, + Expr::BinaryOp { + op: BinaryOperator::Mul, + left: Box::new(Expr::Reference("a".to_string())), + right: Box::new(Expr::Literal(3.0)), + } + ); + } + + #[test] + fn test_parse_complex_expression() { + let expr = Expr::parse("({a} + {b}) * 2").unwrap(); + assert_eq!( + expr, + Expr::BinaryOp { + op: BinaryOperator::Mul, + left: Box::new(Expr::BinaryOp { + op: BinaryOperator::Add, + left: Box::new(Expr::Reference("a".to_string())), + right: Box::new(Expr::Reference("b".to_string())), + }), + right: Box::new(Expr::Literal(2.0)), + } + ); + } + + #[test] + fn test_operator_precedence() { + let expr = Expr::parse("2 + 3 * 4").unwrap(); + assert_eq!( + expr, + Expr::BinaryOp { + op: BinaryOperator::Add, + left: Box::new(Expr::Literal(2.0)), + right: Box::new(Expr::BinaryOp { + op: BinaryOperator::Mul, + left: Box::new(Expr::Literal(3.0)), + right: Box::new(Expr::Literal(4.0)), + }), + } + ); + } + + #[test] + fn test_has_references() { + let expr1 = Expr::parse("42").unwrap(); + assert!(!expr1.has_references()); + + let expr2 = Expr::parse("{token}").unwrap(); + assert!(expr2.has_references()); + + let expr3 = Expr::parse("{a} + 3").unwrap(); + assert!(expr3.has_references()); + } + + #[test] + fn test_get_references() { + let expr = Expr::parse("{a} + {b} * {c}").unwrap(); + let refs = expr.get_references(); + assert_eq!(refs, vec!["a", "b", "c"]); + } +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/src/lib.rs b/crates/dwind-design-tokens/src/lib.rs new file mode 100644 index 0000000..663d1f8 --- /dev/null +++ b/crates/dwind-design-tokens/src/lib.rs @@ -0,0 +1,270 @@ +//! # DWIND Design Tokens +//! +//! A library for parsing and working with design token files in the DWIND ecosystem. +//! This crate provides data structures and parsing capabilities for design tokens +//! following the Design Tokens Community Group specification. +//! +//! ## Features +//! +//! - Parse design token JSON files into structured Rust types +//! - Support for nested token groups and hierarchical organization +//! - Expression parsing with nom for token references and arithmetic +//! - Comprehensive validation with detailed error reporting +//! - Support for multiple token types: dimension, number, borderRadius, color +//! - Robust error handling with thiserror +//! +//! ## Usage +//! +//! ```rust +//! use dwind_design_tokens::{parse_tokens, validate_token_file}; +//! +//! let json = r#" +//! { +//! "spacing": { +//! "small": { +//! "$value": "8px", +//! "$type": "dimension", +//! "$description": "Small spacing unit" +//! }, +//! "medium": { +//! "$value": "{small} * 2", +//! "$type": "dimension", +//! "$description": "Medium spacing unit" +//! } +//! }, +//! "$themes": [], +//! "$metadata": { +//! "tokenSetOrder": ["spacing"], +//! "activeThemes": [], +//! "activeSets": ["spacing"] +//! } +//! } +//! "#; +//! +//! // Parse the token file +//! let token_file = parse_tokens(json).unwrap(); +//! +//! // Validate the tokens +//! let validation_report = validate_token_file(&token_file).unwrap(); +//! assert!(validation_report.is_valid()); +//! +//! // Access tokens +//! let small_token = token_file.find_token("spacing.small").unwrap(); +//! println!("Small spacing: {:?}", small_token.value); +//! ``` + +pub mod error; +pub mod expressions; +pub mod parser; +pub mod types; +pub mod validation; + +// Re-export commonly used types and functions +pub use error::{TokenError, TokenResult}; +pub use expressions::{BinaryOperator, Expr}; +pub use parser::{parse_tokens, parse_tokens_from_file}; +pub use types::{ + ColorValue, DesignToken, DesignTokenFile, Metadata, TokenNode, TokenType, TokenValue, +}; +pub use validation::{validate_token_file, ValidationReport}; + +/// Prelude module for convenient imports +pub mod prelude { + pub use crate::{ + parse_tokens, parse_tokens_from_file, validate_token_file, BinaryOperator, ColorValue, + DesignToken, DesignTokenFile, Expr, Metadata, TokenError, TokenNode, TokenResult, + TokenType, TokenValue, ValidationReport, + }; +} + +#[cfg(test)] +mod integration_tests { + use super::*; + + #[test] + fn test_full_integration() { + let json = r##" + { + "colors": { + "primary": { + "$value": "rgb(193, 51, 51)", + "$type": "color", + "$description": "Primary brand color" + }, + "secondary": { + "$value": "#6c757d", + "$type": "color", + "$description": "Secondary color" + } + }, + "spacing": { + "base": { + "$value": "8px", + "$type": "dimension", + "$description": "Base spacing unit" + }, + "small": { + "$value": "{base} / 2", + "$type": "dimension", + "$description": "Small spacing" + }, + "medium": { + "$value": "{base} * 2", + "$type": "dimension", + "$description": "Medium spacing" + }, + "large": { + "$value": "{medium} + {base}", + "$type": "dimension", + "$description": "Large spacing" + } + }, + "borderRadius": { + "small": { + "$value": "4px", + "$type": "borderRadius", + "$description": "Small border radius" + }, + "medium": { + "$value": "{small} * 2", + "$type": "borderRadius", + "$description": "Medium border radius" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["colors", "spacing", "borderRadius"], + "activeThemes": [], + "activeSets": ["colors", "spacing", "borderRadius"] + } + } + "##; + + // Parse the tokens + let token_file = parse_tokens(json).expect("Failed to parse tokens"); + + // Validate the tokens + let validation_report = validate_token_file(&token_file).expect("Validation failed"); + + if !validation_report.is_valid() { + println!("Validation issues: {}", validation_report.summary()); + for error in &validation_report.errors { + println!("Error: {}", error); + } + for missing in &validation_report.missing_references { + println!("Missing reference: {}", missing); + } + } + + assert!(validation_report.is_valid(), "Validation should pass"); + + // Test token access + let primary_color = token_file.find_token("colors.primary").unwrap(); + assert_eq!(primary_color.token_type, TokenType::Color); + + let base_spacing = token_file.find_token("spacing.base").unwrap(); + assert_eq!(base_spacing.token_type, TokenType::Dimension); + assert!(!base_spacing.has_references()); + + let medium_spacing = token_file.find_token("spacing.medium").unwrap(); + assert!(medium_spacing.has_references()); + let refs = medium_spacing.get_references(); + assert_eq!(refs, vec!["base"]); + + // Test getting all tokens + let all_tokens = token_file.get_all_tokens(); + assert_eq!(all_tokens.len(), 8); // 2 colors + 4 spacing + 2 border radius = 8 total tokens + + // Test getting all references + let all_references = token_file.get_all_references(); + assert!(all_references.len() >= 3); // At least small, medium spacing and medium border radius have references + } + + #[test] + fn test_expression_parsing_integration() { + let json = r##" + { + "test": { + "a": { + "$value": "5", + "$type": "number", + "$description": "" + }, + "b": { + "$value": "{a} * 3 + 2", + "$type": "number", + "$description": "" + }, + "c": { + "$value": "({a} + 1) * 2", + "$type": "number", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "##; + + let token_file = parse_tokens(json).expect("Failed to parse tokens"); + let validation_report = validate_token_file(&token_file).expect("Validation failed"); + assert!(validation_report.is_valid()); + + let token_b = token_file.find_token("test.b").unwrap(); + let token_c = token_file.find_token("test.c").unwrap(); + + assert!(token_b.has_references()); + assert!(token_c.has_references()); + + // Check that expressions were parsed correctly + if let TokenValue::Expression(expr) = &token_b.value { + assert!(expr.has_references()); + let refs = expr.get_references(); + assert_eq!(refs, vec!["a"]); + } else { + panic!("Expected expression for token b"); + } + } + + #[test] + fn test_validation_errors() { + let json = r##" + { + "test": { + "invalid_dimension": { + "$value": "not-a-dimension", + "$type": "dimension", + "$description": "" + }, + "missing_ref": { + "$value": "{nonexistent} * 2", + "$type": "dimension", + "$description": "" + }, + "invalid_number": { + "$value": "abc", + "$type": "number", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "##; + + let token_file = parse_tokens(json).expect("Failed to parse tokens"); + let validation_report = validate_token_file(&token_file).expect("Validation failed"); + + assert!(!validation_report.is_valid()); + assert!(!validation_report.errors.is_empty()); + assert!(!validation_report.missing_references.is_empty()); + } +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/src/parser.rs b/crates/dwind-design-tokens/src/parser.rs new file mode 100644 index 0000000..53b45e5 --- /dev/null +++ b/crates/dwind-design-tokens/src/parser.rs @@ -0,0 +1,237 @@ +use crate::error::{TokenError, TokenResult}; +use crate::expressions::Expr; +use crate::types::{DesignTokenFile, TokenValue}; +use serde_json; +use std::path::Path; + +/// Parse a design token file from JSON string +pub fn parse_tokens(json: &str) -> TokenResult { + let mut file: DesignTokenFile = serde_json::from_str(json)?; + + // Post-process to convert expression strings to Expression variants + process_token_file(&mut file)?; + + Ok(file) +} + +/// Parse a design token file from a file path +pub fn parse_tokens_from_file>(path: P) -> TokenResult { + let content = std::fs::read_to_string(path) + .map_err(|e| TokenError::InvalidValue(format!("Failed to read file: {}", e)))?; + parse_tokens(&content) +} + +/// Process the token file to convert expression strings to Expression variants +fn process_token_file(file: &mut DesignTokenFile) -> TokenResult<()> { + for (_, node) in file.sets.iter_mut() { + process_token_node(node)?; + } + Ok(()) +} + +/// Recursively process token nodes to detect and parse expressions +fn process_token_node(node: &mut crate::types::TokenNode) -> TokenResult<()> { + use crate::types::TokenNode; + + match node { + TokenNode::Token(token) => { + // Check if the value is a string that looks like an expression + if let TokenValue::Literal(ref value_str) = token.value { + if is_expression(value_str) { + // Parse the expression + let expr = Expr::parse(value_str)?; + token.value = TokenValue::Expression(expr); + } + } + } + TokenNode::Group(group) => { + for (_, child_node) in group.iter_mut() { + process_token_node(child_node)?; + } + } + } + + Ok(()) +} + +/// Determine if a string value should be treated as an expression +fn is_expression(value: &str) -> bool { + // Check for token references (contains {}) + if value.contains('{') && value.contains('}') { + return true; + } + + // Check for arithmetic operators with potential references + // This is a simple heuristic - we could make it more sophisticated + let has_operators = value.contains('+') || value.contains('-') || value.contains('*') || value.contains('/'); + let has_numbers = value.chars().any(|c| c.is_ascii_digit()); + + // If it has operators and numbers, it might be an expression + // But we need to be careful not to catch CSS values like "rgb(255, 0, 0)" + if has_operators && has_numbers && !value.starts_with("rgb") && !value.starts_with("hsl") { + return true; + } + + false +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{TokenType, DesignToken}; + + #[test] + fn test_is_expression() { + assert!(is_expression("{a}*3")); + assert!(is_expression("{medium}+12")); + assert!(is_expression("2 + 3")); + assert!(!is_expression("5px")); + assert!(!is_expression("rgb(255, 0, 0)")); + assert!(!is_expression("solid")); + } + + #[test] + fn test_parse_simple_tokens() { + let json = r#" + { + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let result = parse_tokens(json); + assert!(result.is_ok()); + + let file = result.unwrap(); + let token = file.find_token("test.a").unwrap(); + assert_eq!(token.token_type, TokenType::Dimension); + + if let TokenValue::Literal(value) = &token.value { + assert_eq!(value, "5px"); + } else { + panic!("Expected literal value"); + } + } + + #[test] + fn test_parse_expression_tokens() { + let json = r#" + { + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a}*3", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let result = parse_tokens(json); + assert!(result.is_ok()); + + let file = result.unwrap(); + let token_a = file.find_token("test.a").unwrap(); + let token_b = file.find_token("test.b").unwrap(); + + assert!(!token_a.has_references()); + assert!(token_b.has_references()); + + let refs = token_b.get_references(); + assert_eq!(refs, vec!["a"]); + } + + #[test] + fn test_parse_nested_groups() { + let json = r#" + { + "colors": { + "primary": { + "500": { + "$value": "rgb(193, 51, 51)", + "$type": "color", + "$description": "" + } + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["colors"], + "activeThemes": [], + "activeSets": ["colors"] + } + } + "#; + + let result = parse_tokens(json); + assert!(result.is_ok()); + + let file = result.unwrap(); + let token = file.find_token("colors.primary.500").unwrap(); + assert_eq!(token.token_type, TokenType::Color); + } + + #[test] + fn test_get_all_references() { + let json = r#" + { + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a}*3", + "$type": "dimension", + "$description": "" + }, + "c": { + "$value": "{a} + {b}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let result = parse_tokens(json); + assert!(result.is_ok()); + + let file = result.unwrap(); + let references = file.get_all_references(); + + assert_eq!(references.len(), 2); + assert_eq!(references.get("test.b").unwrap(), &vec!["a"]); + assert_eq!(references.get("test.c").unwrap(), &vec!["a", "b"]); + } +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/src/types.rs b/crates/dwind-design-tokens/src/types.rs new file mode 100644 index 0000000..985f519 --- /dev/null +++ b/crates/dwind-design-tokens/src/types.rs @@ -0,0 +1,336 @@ +use crate::expressions::Expr; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Supported token types +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TokenType { + Dimension, + Number, + BorderRadius, + Color, +} + +/// Color value with structured components +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ColorValue { + pub color_space: String, + pub components: [f32; 3], + pub alpha: f32, + pub hex: String, +} + +/// Token value variants +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TokenValue { + /// Structured color object + Color(ColorValue), + /// Literal string value (e.g., "5px", "rgb(255, 0, 0)") + Literal(String), + /// Expression that needs to be evaluated + Expression(Expr), +} + +impl TokenValue { + /// Check if this value contains any token references + pub fn has_references(&self) -> bool { + match self { + TokenValue::Expression(expr) => expr.has_references(), + _ => false, + } + } + + /// Get all token references in this value + pub fn get_references(&self) -> Vec { + match self { + TokenValue::Expression(expr) => expr.get_references(), + _ => vec![], + } + } + + /// Check if this is a literal value + pub fn is_literal(&self) -> bool { + matches!(self, TokenValue::Literal(_) | TokenValue::Color(_)) + } +} + +/// A design token with its metadata +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DesignToken { + #[serde(rename = "$value")] + pub value: TokenValue, + + #[serde(rename = "$type")] + pub token_type: TokenType, + + #[serde(rename = "$description")] + pub description: Option, +} + +impl DesignToken { + /// Create a new design token + pub fn new(value: TokenValue, token_type: TokenType, description: Option) -> Self { + Self { + value, + token_type, + description, + } + } + + /// Check if this token has any references to other tokens + pub fn has_references(&self) -> bool { + self.value.has_references() + } + + /// Get all token references in this token + pub fn get_references(&self) -> Vec { + self.value.get_references() + } +} + +/// A node in the token tree - either a token or a group +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TokenNode { + /// A leaf token + Token(DesignToken), + /// A group containing other tokens or groups + Group(HashMap), +} + +impl TokenNode { + /// Check if this node is a token + pub fn is_token(&self) -> bool { + matches!(self, TokenNode::Token(_)) + } + + /// Check if this node is a group + pub fn is_group(&self) -> bool { + matches!(self, TokenNode::Group(_)) + } + + /// Get the token if this node is a token + pub fn as_token(&self) -> Option<&DesignToken> { + match self { + TokenNode::Token(token) => Some(token), + _ => None, + } + } + + /// Get the group if this node is a group + pub fn as_group(&self) -> Option<&HashMap> { + match self { + TokenNode::Group(group) => Some(group), + _ => None, + } + } + + /// Recursively collect all tokens in this node and its children + pub fn collect_tokens(&self) -> Vec<(String, &DesignToken)> { + self.collect_tokens_with_prefix("") + } + + /// Recursively collect all tokens with a path prefix + fn collect_tokens_with_prefix(&self, prefix: &str) -> Vec<(String, &DesignToken)> { + match self { + TokenNode::Token(token) => vec![(prefix.to_string(), token)], + TokenNode::Group(group) => { + let mut tokens = Vec::new(); + for (key, node) in group { + let path = if prefix.is_empty() { + key.clone() + } else { + format!("{}.{}", prefix, key) + }; + tokens.extend(node.collect_tokens_with_prefix(&path)); + } + tokens + } + } + } + + /// Find a token by path (e.g., "group.subgroup.token") + pub fn find_token(&self, path: &str) -> Option<&DesignToken> { + let parts: Vec<&str> = path.split('.').collect(); + self.find_token_by_parts(&parts) + } + + fn find_token_by_parts(&self, parts: &[&str]) -> Option<&DesignToken> { + if parts.is_empty() { + return None; + } + + match self { + TokenNode::Token(token) => { + // If we have a token and no more parts, return it + if parts.len() == 1 { + Some(token) + } else { + None // Can't navigate deeper into a token + } + } + TokenNode::Group(group) => { + if parts.len() == 1 { + // Look for a direct token in this group + if let Some(TokenNode::Token(token)) = group.get(parts[0]) { + Some(token) + } else { + None + } + } else { + // Navigate deeper into the tree + let key = parts[0]; + let remaining = &parts[1..]; + group.get(key)?.find_token_by_parts(remaining) + } + } + } + } +} + +/// Metadata about the token file +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Metadata { + pub token_set_order: Vec, + pub active_themes: Vec, + pub active_sets: Vec, +} + +/// The root design token file structure +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DesignTokenFile { + /// Token sets (flattened into the root) + #[serde(flatten)] + pub sets: HashMap, + + /// Theme definitions (not implemented in Phase 1) + #[serde(rename = "$themes")] + pub themes: Vec, + + /// File metadata + #[serde(rename = "$metadata")] + pub metadata: Metadata, +} + +impl DesignTokenFile { + /// Get all tokens from all sets + pub fn get_all_tokens(&self) -> Vec<(String, &DesignToken)> { + let mut all_tokens = Vec::new(); + for (set_name, node) in &self.sets { + let tokens = node.collect_tokens(); + for (token_path, token) in tokens { + let full_path = if token_path.is_empty() { + set_name.clone() + } else { + format!("{}.{}", set_name, token_path) + }; + all_tokens.push((full_path, token)); + } + } + all_tokens + } + + /// Find a token by its full path (e.g., "set.group.token") + pub fn find_token(&self, path: &str) -> Option<&DesignToken> { + let parts: Vec<&str> = path.split('.').collect(); + if parts.is_empty() { + return None; + } + + let set_name = parts[0]; + let remaining_path = parts[1..].join("."); + + let set = self.sets.get(set_name)?; + if remaining_path.is_empty() { + set.as_token() + } else { + set.find_token(&remaining_path) + } + } + + /// Get all token references in the file + pub fn get_all_references(&self) -> HashMap> { + let mut references = HashMap::new(); + let all_tokens = self.get_all_tokens(); + + for (path, token) in all_tokens { + let token_refs = token.get_references(); + if !token_refs.is_empty() { + references.insert(path, token_refs); + } + } + + references + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::expressions::Expr; + + #[test] + fn test_token_value_has_references() { + let literal = TokenValue::Literal("5px".to_string()); + assert!(!literal.has_references()); + + let expr = TokenValue::Expression(Expr::parse("{a} + 3").unwrap()); + assert!(expr.has_references()); + } + + #[test] + fn test_design_token_creation() { + let token = DesignToken::new( + TokenValue::Literal("5px".to_string()), + TokenType::Dimension, + Some("Test dimension".to_string()), + ); + + assert_eq!(token.token_type, TokenType::Dimension); + assert_eq!(token.description, Some("Test dimension".to_string())); + assert!(!token.has_references()); + } + + #[test] + fn test_token_node_find_token() { + let mut group = HashMap::new(); + group.insert( + "test".to_string(), + TokenNode::Token(DesignToken::new( + TokenValue::Literal("5px".to_string()), + TokenType::Dimension, + None, + )), + ); + + let node = TokenNode::Group(group); + let found = node.find_token("test"); + assert!(found.is_some()); + assert_eq!(found.unwrap().token_type, TokenType::Dimension); + } + + #[test] + fn test_collect_tokens() { + let mut inner_group = HashMap::new(); + inner_group.insert( + "inner".to_string(), + TokenNode::Token(DesignToken::new( + TokenValue::Literal("10px".to_string()), + TokenType::Dimension, + None, + )), + ); + + let mut outer_group = HashMap::new(); + outer_group.insert("group".to_string(), TokenNode::Group(inner_group)); + + let node = TokenNode::Group(outer_group); + let tokens = node.collect_tokens(); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].0, "group.inner"); + } +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/src/validation.rs b/crates/dwind-design-tokens/src/validation.rs new file mode 100644 index 0000000..140b617 --- /dev/null +++ b/crates/dwind-design-tokens/src/validation.rs @@ -0,0 +1,696 @@ +use crate::error::{TokenError, TokenResult}; +use crate::types::{DesignTokenFile, TokenNode, TokenType, TokenValue}; +use std::collections::HashSet; + +/// Validation context for tracking state during validation +#[derive(Debug)] +pub struct ValidationContext { + /// All available token paths in the file + available_tokens: HashSet, + /// Tokens currently being validated (for circular reference detection) + validation_stack: Vec, +} + +impl ValidationContext { + fn new(file: &DesignTokenFile) -> Self { + let available_tokens = file + .get_all_tokens() + .into_iter() + .map(|(path, _)| path) + .collect(); + + Self { + available_tokens, + validation_stack: Vec::new(), + } + } + + fn push_token(&mut self, path: &str) -> TokenResult<()> { + if self.validation_stack.contains(&path.to_string()) { + let cycle_start = self.validation_stack.iter() + .position(|p| p == path) + .unwrap(); + let cycle = &self.validation_stack[cycle_start..]; + let cycle_chain = cycle.iter() + .chain(std::iter::once(&path.to_string())) + .cloned() + .collect::>() + .join(" -> "); + + return Err(TokenError::CircularReference(format!( + "Circular reference detected: {}. This creates an infinite dependency loop.", + cycle_chain + ))); + } + self.validation_stack.push(path.to_string()); + Ok(()) + } + + fn pop_token(&mut self) { + self.validation_stack.pop(); + } + + fn token_exists(&self, path: &str) -> bool { + self.available_tokens.contains(path) + } + + /// Check if we're already validating a token (indicates potential cycle) + fn is_validating(&self, path: &str) -> bool { + self.validation_stack.contains(&path.to_string()) + } + + /// Get the current validation path for error reporting + fn get_validation_path(&self) -> String { + self.validation_stack.join(" -> ") + } + + /// Validate a token's references recursively for circular dependencies + fn validate_token_references_recursively( + &mut self, + token_path: &str, + file: &DesignTokenFile, + report: &mut ValidationReport, + ) -> TokenResult<()> { + // Push current token to validation stack (detects cycles) + if let Err(e) = self.push_token(token_path) { + // If we get a circular reference error, add it to the report instead of failing + if let TokenError::CircularReference(msg) = e { + report.add_circular_reference(msg); + return Ok(()); // Continue validation of other tokens + } else { + return Err(e); + } + } + + // Get the token and its references + if let Some(token) = file.find_token(token_path) { + let references = token.get_references(); + + for reference in references { + // Resolve the reference path + let resolved_ref = resolve_reference(&reference, token_path); + + // Check if referenced token exists + if !self.token_exists(&resolved_ref) { + report.add_missing_reference(format!( + "Token '{}' references missing token '{}' (resolved as '{}')", + token_path, reference, resolved_ref + )); + continue; + } + + // If the referenced token has its own references, validate them recursively + if let Some(referenced_token) = file.find_token(&resolved_ref) { + if referenced_token.has_references() { + // Recursive call - this will detect cycles via the validation stack + self.validate_token_references_recursively(&resolved_ref, file, report)?; + } + } + } + } + + // Pop current token from validation stack + self.pop_token(); + Ok(()) + } +} + +/// Validate a design token file +pub fn validate_token_file(file: &DesignTokenFile) -> TokenResult { + let mut context = ValidationContext::new(file); + let mut report = ValidationReport::new(); + + // Validate each token set + for (set_name, node) in &file.sets { + validate_token_node(node, set_name, &mut context, &mut report)?; + } + + // Validate references + validate_all_references(file, &mut context, &mut report)?; + + Ok(report) +} + +/// Validation report containing warnings and errors +#[derive(Debug, Default)] +pub struct ValidationReport { + pub errors: Vec, + pub warnings: Vec, + pub circular_references: Vec, + pub missing_references: Vec, + pub type_mismatches: Vec, +} + +impl ValidationReport { + fn new() -> Self { + Self::default() + } + + fn add_error(&mut self, error: String) { + self.errors.push(error); + } + + fn add_warning(&mut self, warning: String) { + self.warnings.push(warning); + } + + fn add_circular_reference(&mut self, reference: String) { + self.circular_references.push(reference); + } + + fn add_missing_reference(&mut self, reference: String) { + self.missing_references.push(reference); + } + + fn add_type_mismatch(&mut self, mismatch: String) { + self.type_mismatches.push(mismatch); + } + + /// Check if the validation passed (no errors) + pub fn is_valid(&self) -> bool { + self.errors.is_empty() && self.circular_references.is_empty() && self.missing_references.is_empty() + } + + /// Get a summary of the validation results + pub fn summary(&self) -> String { + let mut parts = Vec::new(); + + if !self.errors.is_empty() { + parts.push(format!("{} errors", self.errors.len())); + } + if !self.warnings.is_empty() { + parts.push(format!("{} warnings", self.warnings.len())); + } + if !self.circular_references.is_empty() { + parts.push(format!("{} circular references", self.circular_references.len())); + } + if !self.missing_references.is_empty() { + parts.push(format!("{} missing references", self.missing_references.len())); + } + if !self.type_mismatches.is_empty() { + parts.push(format!("{} type mismatches", self.type_mismatches.len())); + } + + if parts.is_empty() { + "Validation passed".to_string() + } else { + format!("Validation issues: {}", parts.join(", ")) + } + } +} + +/// Validate a token node recursively +fn validate_token_node( + node: &TokenNode, + path: &str, + context: &mut ValidationContext, + report: &mut ValidationReport, +) -> TokenResult<()> { + match node { + TokenNode::Token(token) => { + validate_design_token(token, path, context, report)?; + } + TokenNode::Group(group) => { + for (key, child_node) in group { + let child_path = if path.is_empty() { + key.clone() + } else { + format!("{}.{}", path, key) + }; + validate_token_node(child_node, &child_path, context, report)?; + } + } + } + Ok(()) +} + +/// Validate a single design token +fn validate_design_token( + token: &crate::types::DesignToken, + path: &str, + _context: &mut ValidationContext, + report: &mut ValidationReport, +) -> TokenResult<()> { + // Validate token value based on type + match (&token.value, &token.token_type) { + (TokenValue::Literal(value), TokenType::Dimension) => { + if !is_valid_dimension(value) { + report.add_error(format!("Invalid dimension value '{}' at {}", value, path)); + } + } + (TokenValue::Literal(value), TokenType::Number) => { + if !is_valid_number(value) { + report.add_error(format!("Invalid number value '{}' at {}", value, path)); + } + } + (TokenValue::Literal(value), TokenType::BorderRadius) => { + if !is_valid_dimension(value) { + report.add_error(format!("Invalid border radius value '{}' at {}", value, path)); + } + } + (TokenValue::Literal(value), TokenType::Color) => { + if !is_valid_color(value) { + report.add_error(format!("Invalid color value '{}' at {}", value, path)); + } + } + (TokenValue::Color(_), TokenType::Color) => { + // Structured color is always valid for color type + } + (TokenValue::Expression(_), _) => { + // Expression validation will be done in the reference validation phase + } + (value_type, token_type) => { + report.add_type_mismatch(format!( + "Type mismatch at {}: {:?} value for {:?} token", + path, value_type, token_type + )); + } + } + + Ok(()) +} + +/// Validate all token references in the file for both missing references and circular dependencies +fn validate_all_references( + file: &DesignTokenFile, + context: &mut ValidationContext, + report: &mut ValidationReport, +) -> TokenResult<()> { + let all_tokens = file.get_all_tokens(); + + // Validate each token that has references + for (token_path, token) in all_tokens { + if token.has_references() { + // Clear the validation stack for each top-level validation + context.validation_stack.clear(); + + // Perform recursive validation for circular dependencies + context.validate_token_references_recursively(&token_path, file, report)?; + } + } + + Ok(()) +} + +/// Resolve a reference relative to the current token path +fn resolve_reference(reference: &str, current_path: &str) -> String { + if reference.contains('.') { + // Absolute reference + reference.to_string() + } else { + // Relative reference - resolve within the same group + let path_parts: Vec<&str> = current_path.split('.').collect(); + if path_parts.len() > 1 { + let parent_path = path_parts[..path_parts.len() - 1].join("."); + format!("{}.{}", parent_path, reference) + } else { + reference.to_string() + } + } +} + +/// Validate if a string is a valid dimension value +fn is_valid_dimension(value: &str) -> bool { + // Simple validation - should end with a unit or be a number + if value.parse::().is_ok() { + return true; + } + + let units = ["px", "em", "rem", "%", "vh", "vw", "pt", "pc", "in", "cm", "mm"]; + units.iter().any(|unit| value.ends_with(unit)) +} + +/// Validate if a string is a valid number +fn is_valid_number(value: &str) -> bool { + value.parse::().is_ok() +} + +/// Validate if a string is a valid color value +fn is_valid_color(value: &str) -> bool { + // Simple validation for common color formats + if value.starts_with('#') && (value.len() == 4 || value.len() == 7 || value.len() == 9) { + return value[1..].chars().all(|c| c.is_ascii_hexdigit()); + } + + if value.starts_with("rgb(") && value.ends_with(')') { + return true; // More detailed validation could be added + } + + if value.starts_with("rgba(") && value.ends_with(')') { + return true; + } + + if value.starts_with("hsl(") && value.ends_with(')') { + return true; + } + + if value.starts_with("hsla(") && value.ends_with(')') { + return true; + } + + // Named colors (basic set) + let named_colors = [ + "red", "green", "blue", "white", "black", "transparent", + "yellow", "orange", "purple", "pink", "gray", "grey", + ]; + named_colors.contains(&value) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::parse_tokens; + + #[test] + fn test_validate_simple_tokens() { + let json = r#" + { + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "red", + "$type": "color", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(report.is_valid()); + } + + #[test] + fn test_validate_invalid_values() { + let json = r#" + { + "test": { + "a": { + "$value": "invalid-dimension", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "not-a-number", + "$type": "number", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert_eq!(report.errors.len(), 2); + } + + #[test] + fn test_validate_missing_references() { + let json = r#" + { + "test": { + "a": { + "$value": "{missing}*3", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert_eq!(report.missing_references.len(), 1); + } + + #[test] + fn test_is_valid_dimension() { + assert!(is_valid_dimension("5px")); + assert!(is_valid_dimension("1.5em")); + assert!(is_valid_dimension("100%")); + assert!(is_valid_dimension("42")); + assert!(!is_valid_dimension("invalid")); + } + + #[test] + fn test_is_valid_color() { + assert!(is_valid_color("#ff0000")); + assert!(is_valid_color("#f00")); + assert!(is_valid_color("rgb(255, 0, 0)")); + assert!(is_valid_color("red")); + assert!(!is_valid_color("invalid-color")); + } + + #[test] + fn test_direct_self_reference() { + let json = r#" + { + "test": { + "a": { + "$value": "{a}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert!(!report.circular_references.is_empty()); + assert!(report.circular_references[0].contains("test.a -> test.a")); + } + + #[test] + fn test_simple_circular_reference() { + let json = r#" + { + "test": { + "a": { + "$value": "{b}", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert!(!report.circular_references.is_empty()); + // Should detect the circular reference between a and b + let circular_ref = &report.circular_references[0]; + assert!(circular_ref.contains("test.a") && circular_ref.contains("test.b")); + } + + #[test] + fn test_complex_circular_chain() { + let json = r#" + { + "test": { + "a": { + "$value": "{b}", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{c}", + "$type": "dimension", + "$description": "" + }, + "c": { + "$value": "{d}", + "$type": "dimension", + "$description": "" + }, + "d": { + "$value": "{a}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert!(!report.circular_references.is_empty()); + // Should detect the circular reference in the chain a -> b -> c -> d -> a + let circular_ref = &report.circular_references[0]; + assert!(circular_ref.contains("test.a") && + circular_ref.contains("test.b") && + circular_ref.contains("test.c") && + circular_ref.contains("test.d")); + } + + #[test] + fn test_mixed_circular_and_valid_references() { + let json = r#" + { + "test": { + "valid": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "also_valid": { + "$value": "{valid} * 2", + "$type": "dimension", + "$description": "" + }, + "circular_a": { + "$value": "{circular_b}", + "$type": "dimension", + "$description": "" + }, + "circular_b": { + "$value": "{circular_a}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert!(!report.circular_references.is_empty()); + // Should only detect circular references, not affect valid ones + let circular_ref = &report.circular_references[0]; + assert!(circular_ref.contains("circular_a") && circular_ref.contains("circular_b")); + assert!(!circular_ref.contains("valid") && !circular_ref.contains("also_valid")); + } + + #[test] + fn test_expression_circular_references() { + let json = r#" + { + "test": { + "a": { + "$value": "{b} + 5", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a} * 2", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(!report.is_valid()); + assert!(!report.circular_references.is_empty()); + // Should detect circular references even within mathematical expressions + let circular_ref = &report.circular_references[0]; + assert!(circular_ref.contains("test.a") && circular_ref.contains("test.b")); + } + + #[test] + fn test_no_false_positives_for_valid_references() { + let json = r#" + { + "test": { + "base": { + "$value": "10px", + "$type": "dimension", + "$description": "" + }, + "double": { + "$value": "{base} * 2", + "$type": "dimension", + "$description": "" + }, + "triple": { + "$value": "{base} * 3", + "$type": "dimension", + "$description": "" + }, + "combined": { + "$value": "{double} + {triple}", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["test"], + "activeThemes": [], + "activeSets": ["test"] + } + } + "#; + + let file = parse_tokens(json).unwrap(); + let report = validate_token_file(&file).unwrap(); + assert!(report.is_valid()); + assert!(report.circular_references.is_empty()); + } +} \ No newline at end of file diff --git a/crates/dwind-design-tokens/tests/integration.rs b/crates/dwind-design-tokens/tests/integration.rs new file mode 100644 index 0000000..3a2d756 --- /dev/null +++ b/crates/dwind-design-tokens/tests/integration.rs @@ -0,0 +1,280 @@ +use dwind_design_tokens::prelude::*; + +#[test] +fn test_parse_example_tokens() { + let json = r##" + { + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a}*3", + "$type": "dimension", + "$description": "" + }, + "primary": { + "$value": "rgb(193, 51, 51)", + "$type": "color", + "$description": "" + }, + "medium": { + "$value": "12px", + "$type": "borderRadius", + "$description": "" + }, + "rounded-md": { + "$value": "{medium}+12", + "$type": "borderRadius", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": [ + "test" + ], + "activeThemes": [], + "activeSets": [ + "test" + ] + } + } + "##; + + let result = parse_tokens(json); + assert!(result.is_ok(), "Failed to parse tokens: {:?}", result.err()); + + let token_file = result.unwrap(); + + // Test finding tokens + let token_a = token_file.find_token("test.a").unwrap(); + assert_eq!(token_a.token_type, TokenType::Dimension); + assert!(!token_a.has_references()); + + let token_b = token_file.find_token("test.b").unwrap(); + assert_eq!(token_b.token_type, TokenType::Dimension); + assert!(token_b.has_references()); + + let refs = token_b.get_references(); + assert_eq!(refs, vec!["a"]); + + // Test validation + let validation_report = validate_token_file(&token_file).unwrap(); + if !validation_report.is_valid() { + println!("Validation errors: {}", validation_report.summary()); + for error in &validation_report.errors { + println!("Error: {}", error); + } + for missing in &validation_report.missing_references { + println!("Missing reference: {}", missing); + } + } + assert!(validation_report.is_valid()); +} + +#[test] +fn test_complex_expressions() { + let json = r##" + { + "math": { + "base": { + "$value": "10", + "$type": "number", + "$description": "" + }, + "double": { + "$value": "{base} * 2", + "$type": "number", + "$description": "" + }, + "complex": { + "$value": "({base} + 5) * 3", + "$type": "number", + "$description": "" + }, + "multi_ref": { + "$value": "{base} + {double}", + "$type": "number", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["math"], + "activeThemes": [], + "activeSets": ["math"] + } + } + "##; + + let token_file = parse_tokens(json).unwrap(); + let validation_report = validate_token_file(&token_file).unwrap(); + assert!(validation_report.is_valid()); + + let complex_token = token_file.find_token("math.complex").unwrap(); + assert!(complex_token.has_references()); + + let multi_ref_token = token_file.find_token("math.multi_ref").unwrap(); + let refs = multi_ref_token.get_references(); + assert_eq!(refs.len(), 2); + assert!(refs.contains(&"base".to_string())); + assert!(refs.contains(&"double".to_string())); +} + +#[test] +fn test_nested_groups() { + let json = r##" + { + "colors": { + "primary": { + "100": { + "$value": "#f0f0f0", + "$type": "color", + "$description": "" + }, + "500": { + "$value": "#808080", + "$type": "color", + "$description": "" + }, + "900": { + "$value": "#101010", + "$type": "color", + "$description": "" + } + }, + "secondary": { + "base": { + "$value": "rgb(100, 150, 200)", + "$type": "color", + "$description": "" + } + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["colors"], + "activeThemes": [], + "activeSets": ["colors"] + } + } + "##; + + let token_file = parse_tokens(json).unwrap(); + let validation_report = validate_token_file(&token_file).unwrap(); + assert!(validation_report.is_valid()); + + // Test deep token access + let primary_100 = token_file.find_token("colors.primary.100").unwrap(); + assert_eq!(primary_100.token_type, TokenType::Color); + + let secondary_base = token_file.find_token("colors.secondary.base").unwrap(); + assert_eq!(secondary_base.token_type, TokenType::Color); + + // Test getting all tokens + let all_tokens = token_file.get_all_tokens(); + assert_eq!(all_tokens.len(), 4); +} + +#[test] +fn test_validation_errors() { + let json = r##" + { + "invalid": { + "bad_dimension": { + "$value": "not-a-dimension", + "$type": "dimension", + "$description": "" + }, + "missing_ref": { + "$value": "{nonexistent} * 2", + "$type": "dimension", + "$description": "" + }, + "bad_number": { + "$value": "abc123", + "$type": "number", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["invalid"], + "activeThemes": [], + "activeSets": ["invalid"] + } + } + "##; + + let token_file = parse_tokens(json).unwrap(); + let validation_report = validate_token_file(&token_file).unwrap(); + + assert!(!validation_report.is_valid()); + assert!(!validation_report.errors.is_empty()); + assert!(!validation_report.missing_references.is_empty()); +} + +#[test] +fn test_circular_reference_detection() { + let json = r##" + { + "spacing": { + "base": { + "$value": "8px", + "$type": "dimension", + "$description": "" + }, + "circular_a": { + "$value": "{circular_b}", + "$type": "dimension", + "$description": "" + }, + "circular_b": { + "$value": "{circular_c}", + "$type": "dimension", + "$description": "" + }, + "circular_c": { + "$value": "{circular_a}", + "$type": "dimension", + "$description": "" + }, + "valid_ref": { + "$value": "{base} * 2", + "$type": "dimension", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["spacing"], + "activeThemes": [], + "activeSets": ["spacing"] + } + } + "##; + + let token_file = parse_tokens(json).unwrap(); + let validation_report = validate_token_file(&token_file).unwrap(); + + // Should detect circular references but not affect valid tokens + assert!(!validation_report.is_valid()); + assert!(!validation_report.circular_references.is_empty()); + + // The circular reference should involve all three circular tokens + let circular_ref = &validation_report.circular_references[0]; + assert!(circular_ref.contains("circular_a")); + assert!(circular_ref.contains("circular_b")); + assert!(circular_ref.contains("circular_c")); + + // Valid tokens should still be accessible + let base_token = token_file.find_token("spacing.base").unwrap(); + assert!(!base_token.has_references()); + + let valid_ref_token = token_file.find_token("spacing.valid_ref").unwrap(); + assert!(valid_ref_token.has_references()); + assert_eq!(valid_ref_token.get_references(), vec!["base"]); +} \ No newline at end of file diff --git a/crates/dwind/resources/design-tokens.tokens.json b/crates/dwind/resources/design-tokens.tokens.json new file mode 100644 index 0000000..d2516d2 --- /dev/null +++ b/crates/dwind/resources/design-tokens.tokens.json @@ -0,0 +1,39 @@ +{ + "test": { + "a": { + "$value": "5px", + "$type": "dimension", + "$description": "" + }, + "b": { + "$value": "{a}*3", + "$type": "dimension", + "$description": "" + }, + "primary": { + "$value": "rgb(193, 51, 51)", + "$type": "color", + "$description": "" + }, + "medium": { + "$value": "12px", + "$type": "borderRadius", + "$description": "" + }, + "rounded-md": { + "$value": "{medium}+12", + "$type": "borderRadius", + "$description": "" + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": [ + "test" + ], + "activeThemes": [], + "activeSets": [ + "test" + ] + } +} \ No newline at end of file From aee51d4fbd493b8a5ec6670a4cf2eb82ee915265 Mon Sep 17 00:00:00 2001 From: Mathias Myrland Date: Thu, 29 May 2025 22:25:05 +0200 Subject: [PATCH 2/5] added initial design token -> rust generator --- Cargo.lock | 1 + crates/dwind-build/Cargo.toml | 2 +- crates/dwind-build/src/design_tokens.rs | 201 ++++++++++++++++++++++++ crates/dwind-build/src/lib.rs | 1 + crates/dwind/build.rs | 16 ++ crates/dwind/src/lib.rs | 5 + crates/dwind/src/modules/colors.rs | 2 +- 7 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 crates/dwind-build/src/design_tokens.rs diff --git a/Cargo.lock b/Cargo.lock index 4d1e7ce..56f8f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,6 +274,7 @@ dependencies = [ name = "dwind-build" version = "0.1.0" dependencies = [ + "dwind-design-tokens", "serde", "serde_json", ] diff --git a/crates/dwind-build/Cargo.toml b/crates/dwind-build/Cargo.toml index 9b60523..e20a98d 100644 --- a/crates/dwind-build/Cargo.toml +++ b/crates/dwind-build/Cargo.toml @@ -9,6 +9,6 @@ license = "MIT" keywords = ["web", "wasm", "css", "style"] [dependencies] - +dwind-design-tokens = { path = "../dwind-design-tokens" } serde.workspace = true serde_json.workspace = true \ No newline at end of file diff --git a/crates/dwind-build/src/design_tokens.rs b/crates/dwind-build/src/design_tokens.rs new file mode 100644 index 0000000..de1f316 --- /dev/null +++ b/crates/dwind-build/src/design_tokens.rs @@ -0,0 +1,201 @@ +use dwind_design_tokens::{DesignTokenFile, TokenType, TokenValue}; +use std::fs; +use std::io::Write; +use std::path::Path; + +/// Color utility class prefixes and their corresponding generator macros +const COLOR_UTILITY_PREFIXES: &[(&str, &str)] = &[ + ("bg", "background-color"), + ("text", "color"), +]; + +/// Generate Tailwind-like color utility classes from a design token file +pub fn render_design_token_colors_to_rust_file( + token_file_path: impl AsRef, + output_file_path: impl AsRef, +) -> Result<(), Box> { + // Parse the design token file + let token_file_content = fs::read_to_string(token_file_path)?; + let design_token_file: DesignTokenFile = serde_json::from_str(&token_file_content)?; + + // Extract color tokens + let color_tokens = extract_color_tokens(&design_token_file); + + // Generate utility classes + let generated_code = generate_color_utility_classes(&color_tokens); + + // Write to output file + let mut output_file = fs::File::create(output_file_path)?; + output_file.write_all(generated_code.as_bytes())?; + + Ok(()) +} + +/// Extract all color tokens from the design token file +fn extract_color_tokens(design_token_file: &DesignTokenFile) -> Vec<(String, String)> { + let mut color_tokens = Vec::new(); + let all_tokens = design_token_file.get_all_tokens(); + + for (path, token) in all_tokens { + // Only process color tokens + if token.token_type != TokenType::Color { + continue; + } + + // Extract color value based on token value type + let color_value = match &token.value { + TokenValue::Color(color_value) => Some(color_value.hex.clone()), + TokenValue::Literal(literal) => Some(literal.clone()), + TokenValue::Expression(_) => { + // Skip expression tokens for now - they need to be resolved first + eprintln!( + "Warning: Skipping color token '{}' with expression value (not yet supported)", + path + ); + None + } + }; + + if let Some(value) = color_value { + color_tokens.push((path, value)); + } + } + + color_tokens +} + +/// Generate Rust code for color utility classes using dwgenerate_map! macro +fn generate_color_utility_classes(color_tokens: &[(String, String)]) -> String { + let mut output = String::new(); + + // Add header comment + output.push_str("// Auto-generated color utility classes from design tokens\n"); + output.push_str("// Do not edit this file manually\n\n"); + + for (prefix, generator_prefix) in COLOR_UTILITY_PREFIXES { + for (token_path, color_value) in color_tokens { + let css_rule = format!("{generator_prefix}: {color_value};"); + let utility_name = path_to_class_name(prefix, token_path) + .to_uppercase() + .replace("-", "_"); + let utility_prefix = path_to_class_name(prefix, token_path) + .replace("-", "_"); + + output.push_str(&format!( + "# [doc (hidden)] pub static {utility_name}_RAW: &str = \"{css_rule}\";\n" + )); + output.push_str(&format!( + "#[doc = \"Generated from design token file. class content: {css_rule}\"]\n" + )); + output.push_str(&format!("pub static {utility_name}: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| {{ dominator::class!{{#![prefix=\"{utility_prefix}\"].raw({utility_name}_RAW)}} }});\n")); + } + } + + output +} + +/// Convert token path to CSS class name for dwgenerate_map! +/// Example: "colors.primary.500" -> "colors-primary-500" +fn path_to_class_name(prefix: &str, token_path: &str) -> String { + let sanitized_path = sanitize_token_path(token_path); + if prefix.is_empty() { + sanitized_path.to_lowercase() + } else { + format!( + "{}-{}", + prefix.to_lowercase(), + sanitized_path.to_lowercase() + ) + } +} + +/// Sanitize token path for use in CSS class names +/// Convert dots to dashes and handle special characters +fn sanitize_token_path(path: &str) -> String { + path.replace('.', "_") + .replace('-', "_") + .replace(' ', "_") + .chars() + .filter(|c| c.is_alphanumeric() || *c == '_') + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use dwind_design_tokens::{ColorValue, DesignToken, TokenType, TokenValue}; + use std::collections::HashMap; + + #[test] + fn test_sanitize_token_path() { + assert_eq!( + sanitize_token_path("colors.primary.500"), + "colors_primary_500" + ); + assert_eq!( + sanitize_token_path("colors.primary-dark.100"), + "colors_primary_dark_100" + ); + assert_eq!( + sanitize_token_path("colors.text color.50"), + "colors_text_color_50" + ); + } + + #[test] + fn test_extract_color_tokens() { + let mut sets = HashMap::new(); + let mut test_group = HashMap::new(); + + // Add a color token + test_group.insert( + "primary".to_string(), + dwind_design_tokens::TokenNode::Token(DesignToken::new( + TokenValue::Color(ColorValue { + color_space: "srgb".to_string(), + components: [1.0, 0.0, 0.0], + alpha: 1.0, + hex: "#ff0000".to_string(), + }), + TokenType::Color, + Some("Primary color".to_string()), + )), + ); + + sets.insert( + "colors".to_string(), + dwind_design_tokens::TokenNode::Group(test_group), + ); + + let design_token_file = DesignTokenFile { + sets, + themes: vec![], + metadata: dwind_design_tokens::Metadata { + token_set_order: vec!["colors".to_string()], + active_themes: vec![], + active_sets: vec!["colors".to_string()], + }, + }; + + let color_tokens = extract_color_tokens(&design_token_file); + assert_eq!(color_tokens.len(), 1); + assert_eq!(color_tokens[0].0, "colors.primary"); + assert_eq!(color_tokens[0].1, "#ff0000"); + } + + #[test] + fn test_generate_color_utility_classes() { + let color_tokens = vec![ + ("colors.primary.500".to_string(), "#ff0000".to_string()), + ("colors.secondary.100".to_string(), "#0000ff".to_string()), + ]; + + let result = generate_color_utility_classes(&color_tokens); + + assert!(result.contains("BG_")); + assert!(result.contains("TEXT_")); + assert!(result.contains("colors_primary_500")); + assert!(result.contains("#ff0000")); + assert!(result.contains("#0000ff")); + } +} diff --git a/crates/dwind-build/src/lib.rs b/crates/dwind-build/src/lib.rs index 40fd5c1..f97f747 100644 --- a/crates/dwind-build/src/lib.rs +++ b/crates/dwind-build/src/lib.rs @@ -1 +1,2 @@ pub mod colors; +pub mod design_tokens; diff --git a/crates/dwind/build.rs b/crates/dwind/build.rs index 0cc1670..bbc7ee3 100644 --- a/crates/dwind/build.rs +++ b/crates/dwind/build.rs @@ -1,4 +1,5 @@ use dwind_build::colors::render_color_json_file_to_rust_file; +use dwind_build::design_tokens::render_design_token_colors_to_rust_file; use std::path::Path; use std::{env, fs}; @@ -41,6 +42,21 @@ fn main() { Path::new(&out_dir).join("colors_generated.rs"), ); + // Generate design token color utilities + if let Err(e) = render_design_token_colors_to_rust_file( + "resources/design-tokens.tokens.json", + Path::new(&out_dir).join("design_tokens_generated.rs"), + ) { + eprintln!("Warning: Failed to generate design token colors: {}", e); + eprintln!("This is expected if the design token file doesn't exist or is malformed."); + // Create an empty file so the include! doesn't fail + fs::write( + Path::new(&out_dir).join("design_token_colors_generated.rs"), + "// No design token colors generated\n", + ).unwrap(); + } + println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=resources/colors.json"); + println!("cargo::rerun-if-changed=resources/design-tokens-extended.tokens.json"); } diff --git a/crates/dwind/src/lib.rs b/crates/dwind/src/lib.rs index bf0de6f..e886c72 100644 --- a/crates/dwind/src/lib.rs +++ b/crates/dwind/src/lib.rs @@ -38,6 +38,11 @@ pub mod base { include!(concat!(env!("OUT_DIR"), "/base.rs")); } +pub mod tokens { + use crate::bg_color_generator; + include!(concat!(env!("OUT_DIR"), "/design_tokens_generated.rs")); +} + pub mod box_shadow { include!(concat!(env!("OUT_DIR"), "/box_shadow.rs")); } diff --git a/crates/dwind/src/modules/colors.rs b/crates/dwind/src/modules/colors.rs index 566c3c0..f8f3807 100644 --- a/crates/dwind/src/modules/colors.rs +++ b/crates/dwind/src/modules/colors.rs @@ -3,7 +3,7 @@ use dwind_macros::dwgenerate_map; #[macro_export] macro_rules! bg_color_generator { ($color:tt) => { - const_format::formatcp!("background: {};", $color) + const_format::formatcp!("background: \"{}\";", $color) }; } From dd59d3dde6fe361c6280796bb76511373c7c8acb Mon Sep 17 00:00:00 2001 From: Mathias Myrland Date: Thu, 29 May 2025 22:42:11 +0200 Subject: [PATCH 3/5] added support for border radi and dimensions --- crates/dwind-build/src/design_tokens.rs | 297 ++++++++++++++++-- crates/dwind-design-tokens/src/expressions.rs | 111 +++++++ 2 files changed, 383 insertions(+), 25 deletions(-) diff --git a/crates/dwind-build/src/design_tokens.rs b/crates/dwind-build/src/design_tokens.rs index de1f316..39d6ee3 100644 --- a/crates/dwind-build/src/design_tokens.rs +++ b/crates/dwind-build/src/design_tokens.rs @@ -1,4 +1,5 @@ -use dwind_design_tokens::{DesignTokenFile, TokenType, TokenValue}; +use dwind_design_tokens::{DesignTokenFile, TokenType, TokenValue, parse_tokens}; +use std::collections::HashMap; use std::fs; use std::io::Write; use std::path::Path; @@ -9,20 +10,84 @@ const COLOR_UTILITY_PREFIXES: &[(&str, &str)] = &[ ("text", "color"), ]; -/// Generate Tailwind-like color utility classes from a design token file -pub fn render_design_token_colors_to_rust_file( +/// Dimension utility class prefixes for margin and padding +const DIMENSION_UTILITY_PREFIXES: &[(&str, &str)] = &[ + // Margin utilities + ("m", "margin"), + ("ml", "margin-left"), + ("mr", "margin-right"), + ("mt", "margin-top"), + ("mb", "margin-bottom"), + ("mx", "margin-left,margin-right"), + ("my", "margin-top,margin-bottom"), + + // Padding utilities + ("p", "padding"), + ("pl", "padding-left"), + ("pr", "padding-right"), + ("pt", "padding-top"), + ("pb", "padding-bottom"), + ("px", "padding-left,padding-right"), + ("py", "padding-top,padding-bottom"), +]; + +/// Border radius utility class prefixes +const BORDER_RADIUS_UTILITY_PREFIXES: &[(&str, &str)] = &[ + ("rounded", "border-radius"), + ("rounded-t", "border-top-left-radius,border-top-right-radius"), + ("rounded-r", "border-top-right-radius,border-bottom-right-radius"), + ("rounded-b", "border-bottom-left-radius,border-bottom-right-radius"), + ("rounded-l", "border-top-left-radius,border-bottom-left-radius"), + ("rounded-tl", "border-top-left-radius"), + ("rounded-tr", "border-top-right-radius"), + ("rounded-bl", "border-bottom-left-radius"), + ("rounded-br", "border-bottom-right-radius"), +]; + +/// Generate Tailwind-like utility classes from a design token file +pub fn render_design_tokens_to_rust_file( token_file_path: impl AsRef, output_file_path: impl AsRef, ) -> Result<(), Box> { // Parse the design token file let token_file_content = fs::read_to_string(token_file_path)?; - let design_token_file: DesignTokenFile = serde_json::from_str(&token_file_content)?; + let design_token_file: DesignTokenFile = parse_tokens(&token_file_content)?; - // Extract color tokens - let color_tokens = extract_color_tokens(&design_token_file); + // Resolve all token expressions first + let resolved_tokens = resolve_token_expressions(&design_token_file)?; + + // Extract tokens by type + let color_tokens = extract_color_tokens(&design_token_file, &resolved_tokens); + let dimension_tokens = extract_dimension_tokens(&design_token_file, &resolved_tokens); + let border_radius_tokens = extract_border_radius_tokens(&design_token_file, &resolved_tokens); // Generate utility classes - let generated_code = generate_color_utility_classes(&color_tokens); + let mut generated_code = String::new(); + + // Add header comment + generated_code.push_str("// Auto-generated utility classes from design tokens\n"); + generated_code.push_str("// Do not edit this file manually\n\n"); + + // Generate color utilities + if !color_tokens.is_empty() { + generated_code.push_str("// Color utilities\n"); + generated_code.push_str(&generate_color_utility_classes(&color_tokens)); + generated_code.push_str("\n"); + } + + // Generate dimension utilities (margin and padding) + if !dimension_tokens.is_empty() { + generated_code.push_str("// Dimension utilities (margin and padding)\n"); + generated_code.push_str(&generate_dimension_utility_classes(&dimension_tokens)); + generated_code.push_str("\n"); + } + + // Generate border radius utilities + if !border_radius_tokens.is_empty() { + generated_code.push_str("// Border radius utilities\n"); + generated_code.push_str(&generate_border_radius_utility_classes(&border_radius_tokens)); + generated_code.push_str("\n"); + } // Write to output file let mut output_file = fs::File::create(output_file_path)?; @@ -31,8 +96,82 @@ pub fn render_design_token_colors_to_rust_file( Ok(()) } +/// Generate Tailwind-like color utility classes from a design token file (legacy function) +pub fn render_design_token_colors_to_rust_file( + token_file_path: impl AsRef, + output_file_path: impl AsRef, +) -> Result<(), Box> { + render_design_tokens_to_rust_file(token_file_path, output_file_path) +} + +/// Resolve all token expressions in the design token file +pub fn resolve_token_expressions(design_token_file: &DesignTokenFile) -> Result, Box> { + let mut resolved = HashMap::new(); + let all_tokens = design_token_file.get_all_tokens(); + + // First pass: collect all literal values + for (path, token) in &all_tokens { + match &token.value { + TokenValue::Literal(literal) => { + resolved.insert(path.clone(), literal.clone()); + } + TokenValue::Color(color_value) => { + resolved.insert(path.clone(), color_value.hex.clone()); + } + _ => {} // Skip expressions for now + } + } + + // Second pass: resolve expressions (simple dependency resolution) + let mut changed = true; + let mut max_iterations = 10; // Prevent infinite loops + + while changed && max_iterations > 0 { + changed = false; + max_iterations -= 1; + + for (path, token) in &all_tokens { + if resolved.contains_key(path) { + continue; // Already resolved + } + + if let TokenValue::Expression(expr) = &token.value { + // Create a context with both full paths and relative references + let mut context = resolved.clone(); + + // Add relative references for tokens in the same set + let path_parts: Vec<&str> = path.split('.').collect(); + if path_parts.len() >= 2 { + let set_name = path_parts[0]; + for (full_path, value) in &resolved { + let full_path_parts: Vec<&str> = full_path.split('.').collect(); + if full_path_parts.len() >= 2 && full_path_parts[0] == set_name { + // Add relative reference (e.g., "test.a" -> "a") + let relative_name = full_path_parts[1..].join("."); + context.insert(relative_name, value.clone()); + } + } + } + + // Try to evaluate the expression + match expr.evaluate(&context) { + Ok(value) => { + resolved.insert(path.clone(), value); + changed = true; + } + Err(_) => { + // Cannot resolve yet, dependencies not ready + } + } + } + } + } + + Ok(resolved) +} + /// Extract all color tokens from the design token file -fn extract_color_tokens(design_token_file: &DesignTokenFile) -> Vec<(String, String)> { +fn extract_color_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &HashMap) -> Vec<(String, String)> { let mut color_tokens = Vec::new(); let all_tokens = design_token_file.get_all_tokens(); @@ -42,28 +181,61 @@ fn extract_color_tokens(design_token_file: &DesignTokenFile) -> Vec<(String, Str continue; } - // Extract color value based on token value type - let color_value = match &token.value { - TokenValue::Color(color_value) => Some(color_value.hex.clone()), - TokenValue::Literal(literal) => Some(literal.clone()), - TokenValue::Expression(_) => { - // Skip expression tokens for now - they need to be resolved first - eprintln!( - "Warning: Skipping color token '{}' with expression value (not yet supported)", - path - ); - None - } - }; - - if let Some(value) = color_value { - color_tokens.push((path, value)); + // Get resolved value from the resolved_tokens map + if let Some(resolved_value) = resolved_tokens.get(&path) { + color_tokens.push((path, resolved_value.clone())); + } else { + eprintln!("Warning: Could not resolve color token '{}'", path); } } color_tokens } +/// Extract all dimension tokens from the design token file +fn extract_dimension_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &HashMap) -> Vec<(String, String)> { + let mut dimension_tokens = Vec::new(); + let all_tokens = design_token_file.get_all_tokens(); + + for (path, token) in all_tokens { + // Only process dimension tokens + if token.token_type != TokenType::Dimension { + continue; + } + + // Get resolved value from the resolved_tokens map + if let Some(resolved_value) = resolved_tokens.get(&path) { + dimension_tokens.push((path, resolved_value.clone())); + } else { + eprintln!("Warning: Could not resolve dimension token '{}'", path); + } + } + + dimension_tokens +} + +/// Extract all border radius tokens from the design token file +fn extract_border_radius_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &HashMap) -> Vec<(String, String)> { + let mut border_radius_tokens = Vec::new(); + let all_tokens = design_token_file.get_all_tokens(); + + for (path, token) in all_tokens { + // Only process border radius tokens + if token.token_type != TokenType::BorderRadius { + continue; + } + + // Get resolved value from the resolved_tokens map + if let Some(resolved_value) = resolved_tokens.get(&path) { + border_radius_tokens.push((path, resolved_value.clone())); + } else { + eprintln!("Warning: Could not resolve border radius token '{}'", path); + } + } + + border_radius_tokens +} + /// Generate Rust code for color utility classes using dwgenerate_map! macro fn generate_color_utility_classes(color_tokens: &[(String, String)]) -> String { let mut output = String::new(); @@ -94,6 +266,80 @@ fn generate_color_utility_classes(color_tokens: &[(String, String)]) -> String { output } +/// Generate Rust code for dimension utility classes (margin and padding) +fn generate_dimension_utility_classes(dimension_tokens: &[(String, String)]) -> String { + let mut output = String::new(); + + for (prefix, css_properties) in DIMENSION_UTILITY_PREFIXES { + for (token_path, dimension_value) in dimension_tokens { + let css_rule = if css_properties.contains(',') { + // Handle multi-property cases like mx, my, px, py + let properties: Vec<&str> = css_properties.split(',').collect(); + properties + .iter() + .map(|prop| format!("{}: {}", prop, dimension_value)) + .collect::>() + .join("; ") + ";" + } else { + format!("{}: {};", css_properties, dimension_value) + }; + + let utility_name = path_to_class_name(prefix, token_path) + .to_uppercase() + .replace("-", "_"); + let utility_prefix = path_to_class_name(prefix, token_path) + .replace("-", "_"); + + output.push_str(&format!( + "# [doc (hidden)] pub static {utility_name}_RAW: &str = \"{css_rule}\";\n" + )); + output.push_str(&format!( + "#[doc = \"Generated from design token file. class content: {css_rule}\"]\n" + )); + output.push_str(&format!("pub static {utility_name}: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| {{ dominator::class!{{#![prefix=\"{utility_prefix}\"].raw({utility_name}_RAW)}} }});\n")); + } + } + + output +} + +/// Generate Rust code for border radius utility classes +fn generate_border_radius_utility_classes(border_radius_tokens: &[(String, String)]) -> String { + let mut output = String::new(); + + for (prefix, css_properties) in BORDER_RADIUS_UTILITY_PREFIXES { + for (token_path, border_radius_value) in border_radius_tokens { + let css_rule = if css_properties.contains(',') { + // Handle multi-property cases like rounded-t, rounded-r, etc. + let properties: Vec<&str> = css_properties.split(',').collect(); + properties + .iter() + .map(|prop| format!("{}: {}", prop, border_radius_value)) + .collect::>() + .join("; ") + ";" + } else { + format!("{}: {};", css_properties, border_radius_value) + }; + + let utility_name = path_to_class_name(prefix, token_path) + .to_uppercase() + .replace("-", "_"); + let utility_prefix = path_to_class_name(prefix, token_path) + .replace("-", "_"); + + output.push_str(&format!( + "# [doc (hidden)] pub static {utility_name}_RAW: &str = \"{css_rule}\";\n" + )); + output.push_str(&format!( + "#[doc = \"Generated from design token file. class content: {css_rule}\"]\n" + )); + output.push_str(&format!("pub static {utility_name}: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| {{ dominator::class!{{#![prefix=\"{utility_prefix}\"].raw({utility_name}_RAW)}} }});\n")); + } + } + + output +} + /// Convert token path to CSS class name for dwgenerate_map! /// Example: "colors.primary.500" -> "colors-primary-500" fn path_to_class_name(prefix: &str, token_path: &str) -> String { @@ -177,7 +423,8 @@ mod tests { }, }; - let color_tokens = extract_color_tokens(&design_token_file); + let resolved_tokens = resolve_token_expressions(&design_token_file).unwrap(); + let color_tokens = extract_color_tokens(&design_token_file, &resolved_tokens); assert_eq!(color_tokens.len(), 1); assert_eq!(color_tokens[0].0, "colors.primary"); assert_eq!(color_tokens[0].1, "#ff0000"); diff --git a/crates/dwind-design-tokens/src/expressions.rs b/crates/dwind-design-tokens/src/expressions.rs index c03b72a..7122e75 100644 --- a/crates/dwind-design-tokens/src/expressions.rs +++ b/crates/dwind-design-tokens/src/expressions.rs @@ -77,6 +77,77 @@ impl Expr { } } } + + /// Evaluate this expression given a context of resolved token values + pub fn evaluate(&self, context: &std::collections::HashMap) -> TokenResult { + match self { + Expr::Reference(name) => { + context.get(name) + .cloned() + .ok_or_else(|| TokenError::ExpressionParsing(format!("Undefined reference: {}", name))) + } + Expr::Literal(value) => { + // For dimension/border-radius tokens, we need to preserve the unit + // Try to format as integer if it's a whole number, otherwise as float + if value.fract() == 0.0 { + Ok(format!("{}px", *value as i64)) + } else { + Ok(format!("{}px", value)) + } + } + Expr::BinaryOp { op, left, right } => { + let left_val = left.evaluate(context)?; + let right_val = right.evaluate(context)?; + + // Parse numeric values from CSS values (e.g., "12px" -> 12.0) + let left_num = parse_css_value(&left_val)?; + let right_num = parse_css_value(&right_val)?; + + let result = match op { + BinaryOperator::Add => left_num + right_num, + BinaryOperator::Sub => left_num - right_num, + BinaryOperator::Mul => left_num * right_num, + BinaryOperator::Div => { + if right_num == 0.0 { + return Err(TokenError::ExpressionParsing("Division by zero".to_string())); + } + left_num / right_num + } + }; + + // Format result back to CSS value + if result.fract() == 0.0 { + Ok(format!("{}px", result as i64)) + } else { + Ok(format!("{}px", result)) + } + } + } + } +} + +/// Parse a CSS value to extract the numeric part +/// Supports values like "12px", "1.5em", "100%", or plain numbers +fn parse_css_value(value: &str) -> TokenResult { + let trimmed = value.trim(); + + // Try to parse as plain number first + if let Ok(num) = trimmed.parse::() { + return Ok(num); + } + + // Extract numeric part from CSS values + let numeric_part = trimmed + .chars() + .take_while(|c| c.is_numeric() || *c == '.' || *c == '-') + .collect::(); + + if numeric_part.is_empty() { + return Err(TokenError::ExpressionParsing(format!("Cannot parse numeric value from: {}", value))); + } + + numeric_part.parse::() + .map_err(|_| TokenError::ExpressionParsing(format!("Invalid numeric value: {}", numeric_part))) } // Nom parser functions @@ -260,4 +331,44 @@ mod tests { let refs = expr.get_references(); assert_eq!(refs, vec!["a", "b", "c"]); } + + #[test] + fn test_parse_css_value() { + assert_eq!(parse_css_value("12px").unwrap(), 12.0); + assert_eq!(parse_css_value("1.5em").unwrap(), 1.5); + assert_eq!(parse_css_value("100%").unwrap(), 100.0); + assert_eq!(parse_css_value("42").unwrap(), 42.0); + assert_eq!(parse_css_value("-5px").unwrap(), -5.0); + } + + #[test] + fn test_evaluate_literal() { + let expr = Expr::parse("12").unwrap(); + let context = std::collections::HashMap::new(); + assert_eq!(expr.evaluate(&context).unwrap(), "12px"); + } + + #[test] + fn test_evaluate_reference() { + let expr = Expr::parse("{base}").unwrap(); + let mut context = std::collections::HashMap::new(); + context.insert("base".to_string(), "16px".to_string()); + assert_eq!(expr.evaluate(&context).unwrap(), "16px"); + } + + #[test] + fn test_evaluate_addition() { + let expr = Expr::parse("{base} + 4").unwrap(); + let mut context = std::collections::HashMap::new(); + context.insert("base".to_string(), "16px".to_string()); + assert_eq!(expr.evaluate(&context).unwrap(), "20px"); + } + + #[test] + fn test_evaluate_multiplication() { + let expr = Expr::parse("{base} * 2").unwrap(); + let mut context = std::collections::HashMap::new(); + context.insert("base".to_string(), "8px".to_string()); + assert_eq!(expr.evaluate(&context).unwrap(), "16px"); + } } \ No newline at end of file From 389ae24ffe044512ad39ae58152e4098629cd773 Mon Sep 17 00:00:00 2001 From: Mathias Myrland Date: Sat, 14 Jun 2025 19:41:26 +0200 Subject: [PATCH 4/5] update version, switch to NPM + rollup for building example app --- .github/workflows/pages.yml | 7 +- Cargo.lock | 7 +- LLM.txt | 113 + crates/dwind-build/Cargo.toml | 2 +- crates/dwind/Cargo.toml | 4 +- crates/dwind/build.rs | 17 +- crates/dwind/src/lib.rs | 5 - crates/dwind/src/modules/colors.rs | 2 +- crates/dwui/Cargo.toml | 4 +- .../dominator-css-bindgen-test/Cargo.toml | 1 + .../tests/dominator-css-bindgen-test/build.rs | 16 + .../resources/design-tokens.tokens.json | 0 .../dominator-css-bindgen-test/src/lib.rs | 7 + examples/webpage/.gitignore | 1 + examples/webpage/.npmrc | 1 + examples/webpage/Cargo.toml | 4 +- examples/webpage/README.md | 6 +- examples/webpage/Trunk.toml | 9 - examples/webpage/index.html | 9 + examples/webpage/package-lock.json | 2050 +++++++++++++++++ examples/webpage/package.json | 21 + examples/webpage/rollup.config.js | 37 + 22 files changed, 2275 insertions(+), 48 deletions(-) create mode 100644 LLM.txt rename crates/{dwind => tests/dominator-css-bindgen-test}/resources/design-tokens.tokens.json (100%) create mode 100644 examples/webpage/.npmrc delete mode 100644 examples/webpage/Trunk.toml create mode 100644 examples/webpage/index.html create mode 100644 examples/webpage/package-lock.json create mode 100644 examples/webpage/package.json create mode 100644 examples/webpage/rollup.config.js diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index bf78d76..e210128 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -49,13 +49,12 @@ jobs: # Rust toolchain name. toolchain: stable target: wasm32-unknown-unknown - - uses: jetli/trunk-action@v0.5.0 + - uses: actions/setup-node@v4 with: - # Optional version of trunk to install(eg. 'v0.19.1', 'latest') - version: 'latest' + node-version: '24.x' - name: Install and Build 🔧 working-directory: examples/webpage - run: trunk build --release --public-url="/dwind/examples" + run: npm ci && npm run build - name: Docs run: cargo doc --no-deps - name: Populate docs diff --git a/Cargo.lock b/Cargo.lock index 56f8f85..4c09a7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ version = "0.1.0" dependencies = [ "dominator", "dominator-css-bindgen", + "dwind-build", "once_cell", ] @@ -247,7 +248,7 @@ dependencies = [ [[package]] name = "dwind" -version = "0.3.2" +version = "0.4.0" dependencies = [ "const_format", "dominator", @@ -272,7 +273,7 @@ dependencies = [ [[package]] name = "dwind-build" -version = "0.1.0" +version = "0.2.0" dependencies = [ "dwind-design-tokens", "serde", @@ -307,7 +308,7 @@ dependencies = [ [[package]] name = "dwui" -version = "0.4.0" +version = "0.5.0" dependencies = [ "const_format", "dominator", diff --git a/LLM.txt b/LLM.txt new file mode 100644 index 0000000..8eaeade --- /dev/null +++ b/LLM.txt @@ -0,0 +1,113 @@ +# DWIND - Type-Safe CSS Utilities for DOMINATOR + +DWIND brings Tailwind-like utilities to DOMINATOR web apps with compile-time validation. + +## Quick Start + +```rust +use dwind::prelude::*; +use dwind_macros::dwclass; +use dwui::prelude::*; + +// Initialize CSS (once at app start) +dwind::stylesheet(); + +// Use utilities +html!("div", { + .dwclass!("flex justify-center p-4 bg-apple-500 @md:bg-charm-700") +}) +``` + +## Discovery Guide for LLMs + +### 1. Find Available Styles +```bash +# List all CSS utility modules +ls crates/dwind/src/modules/ + +# Search for specific utilities (e.g., flexbox) +grep -r "flex" crates/dwind/resources/css/ + +# View generated docs +# https://jedimemo.github.io/dwind/doc/dwind/index.html +``` + +### 2. Component Library (dwui) +```bash +# List available components +ls crates/dwui/src/components/ + +# Components include: button, card, text_input, select, toggle, dropdown +``` + +### 3. Key Patterns + +**Responsive Design**: `@xs:`, `@sm:`, `@md:`, `@lg:`, `@xl:` prefixes +```rust +.dwclass!("text-sm @md:text-lg @xl:text-2xl") +``` + +**Pseudo-classes**: `hover:`, `focus:`, `disabled:` prefixes +```rust +.dwclass!("bg-gray-200 hover:bg-gray-300 disabled:opacity-50") +``` + +**Theming**: Use `is(.light *)` or `is(.dark *)` selectors +```rust +.dwclass!("is(.light *):bg-white is(.dark *):bg-black") +``` + +**Reactive Classes**: Apply classes conditionally based on signals +```rust +.dwclass_signal!("bg-blue-500", is_active.signal()) +.dwclass_signal!("text-xl", size.signal().eq(TextSize::ExtraLarge)) +``` + +## Architecture + +- **dwind**: Core utilities (spacing, colors, typography, layout) +- **dwind-macros**: `dwclass!` and `dwclass_signal!` macros +- **dwui**: Pre-built components using dwind utilities +- **dominator-css-bindgen**: CSS-to-Rust code generator (build-time) + +## Component Usage (dwui) + +Components use macro syntax generated by `futures-signals-component-macro`: + +```rust +use dwui::prelude::*; + +// Button component +button!({ + .content(Some(text("Click me"))) + .button_type(ButtonType::Flat) + .disabled(false) + .on_click(|_| println!("Clicked!")) +}) + +// Card with nested components +card!({ + .scheme(ColorScheme::Void) + .apply(|b| { + dwclass!(b, "p-4 w-64 flex flex-col gap-4") + .children([ + text_input!({ + .value(some_mutable) + .label("Enter name".to_string()) + }), + button!({ + .content(Some(text("Submit"))) + .button_type_signal(button_type.signal()) + }) + ]) + }) +}) +``` + +## Signal-based Properties + +Component properties with `#[signal]` attribute support both static and dynamic values: +- `.property(value)` - static value +- `.property_signal(signal)` - dynamic signal value + +All CSS classes are validated at compile-time. Invalid classes cause compilation errors. \ No newline at end of file diff --git a/crates/dwind-build/Cargo.toml b/crates/dwind-build/Cargo.toml index e20a98d..5387f8d 100644 --- a/crates/dwind-build/Cargo.toml +++ b/crates/dwind-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dwind-build" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "utility crate for building DWIND color swatches etc." homepage = "https://github.com/JedimEmO/dwind" diff --git a/crates/dwind/Cargo.toml b/crates/dwind/Cargo.toml index 5ee6aa2..62fb16f 100644 --- a/crates/dwind/Cargo.toml +++ b/crates/dwind/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dwind" -version = "0.3.2" +version = "0.4.0" edition = "2021" description = "Style your DOMINATOR applications using a tailwind-like syntax and utility class collection!" homepage = "https://github.com/JedimEmO/dwind" @@ -21,6 +21,6 @@ serde = { workspace = true, features = ["derive"] } [build-dependencies] dominator-css-bindgen = { path = "../dominator-css-bindgen", version = "0.1.0" } -dwind-build = { path = "../dwind-build", version = "0.1.0" } +dwind-build = { path = "../dwind-build", version = "0.2.0" } serde.workspace = true serde_json.workspace = true \ No newline at end of file diff --git a/crates/dwind/build.rs b/crates/dwind/build.rs index bbc7ee3..dcdc72a 100644 --- a/crates/dwind/build.rs +++ b/crates/dwind/build.rs @@ -1,5 +1,4 @@ use dwind_build::colors::render_color_json_file_to_rust_file; -use dwind_build::design_tokens::render_design_token_colors_to_rust_file; use std::path::Path; use std::{env, fs}; @@ -41,22 +40,8 @@ fn main() { "resources/colors.json", Path::new(&out_dir).join("colors_generated.rs"), ); - - // Generate design token color utilities - if let Err(e) = render_design_token_colors_to_rust_file( - "resources/design-tokens.tokens.json", - Path::new(&out_dir).join("design_tokens_generated.rs"), - ) { - eprintln!("Warning: Failed to generate design token colors: {}", e); - eprintln!("This is expected if the design token file doesn't exist or is malformed."); - // Create an empty file so the include! doesn't fail - fs::write( - Path::new(&out_dir).join("design_token_colors_generated.rs"), - "// No design token colors generated\n", - ).unwrap(); - } + println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=resources/colors.json"); - println!("cargo::rerun-if-changed=resources/design-tokens-extended.tokens.json"); } diff --git a/crates/dwind/src/lib.rs b/crates/dwind/src/lib.rs index e886c72..bf0de6f 100644 --- a/crates/dwind/src/lib.rs +++ b/crates/dwind/src/lib.rs @@ -38,11 +38,6 @@ pub mod base { include!(concat!(env!("OUT_DIR"), "/base.rs")); } -pub mod tokens { - use crate::bg_color_generator; - include!(concat!(env!("OUT_DIR"), "/design_tokens_generated.rs")); -} - pub mod box_shadow { include!(concat!(env!("OUT_DIR"), "/box_shadow.rs")); } diff --git a/crates/dwind/src/modules/colors.rs b/crates/dwind/src/modules/colors.rs index f8f3807..566c3c0 100644 --- a/crates/dwind/src/modules/colors.rs +++ b/crates/dwind/src/modules/colors.rs @@ -3,7 +3,7 @@ use dwind_macros::dwgenerate_map; #[macro_export] macro_rules! bg_color_generator { ($color:tt) => { - const_format::formatcp!("background: \"{}\";", $color) + const_format::formatcp!("background: {};", $color) }; } diff --git a/crates/dwui/Cargo.toml b/crates/dwui/Cargo.toml index 1d09ac2..a5c2619 100644 --- a/crates/dwui/Cargo.toml +++ b/crates/dwui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dwui" -version = "0.4.0" +version = "0.5.0" edition = "2021" description = "UI Component library built on the DWIND style crate!" homepage = "https://github.com/JedimEmO/dwind" @@ -10,7 +10,7 @@ keywords = ["web", "wasm", "css", "style"] [dependencies] const_format.workspace = true -dwind = { path = "../dwind", version = "0.3.2" } +dwind = { path = "../dwind", version = "0.4.0" } dwind-macros = { path = "../dwind-macros", version = "0.2.2" } dominator.workspace = true futures-signals.workspace = true diff --git a/crates/tests/dominator-css-bindgen-test/Cargo.toml b/crates/tests/dominator-css-bindgen-test/Cargo.toml index fc55ac3..eedef75 100644 --- a/crates/tests/dominator-css-bindgen-test/Cargo.toml +++ b/crates/tests/dominator-css-bindgen-test/Cargo.toml @@ -11,3 +11,4 @@ once_cell.workspace = true [build-dependencies] dominator-css-bindgen = { path = "../../dominator-css-bindgen", version = "0.1.0" } +dwind-build = { path = "../../dwind-build", version = "0.2.0" } diff --git a/crates/tests/dominator-css-bindgen-test/build.rs b/crates/tests/dominator-css-bindgen-test/build.rs index fc42543..0fc2dd0 100644 --- a/crates/tests/dominator-css-bindgen-test/build.rs +++ b/crates/tests/dominator-css-bindgen-test/build.rs @@ -1,5 +1,6 @@ use std::path::Path; use std::{env, fs}; +use dwind_build::design_tokens::render_design_token_colors_to_rust_file; fn main() { let out = dominator_css_bindgen::css::generate_rust_bindings_from_file("resources/simple.css") @@ -10,6 +11,21 @@ fn main() { fs::write(dest_path, out).unwrap(); + // Generate design token color utilities + if let Err(e) = render_design_token_colors_to_rust_file( + "resources/design-tokens.tokens.json", + Path::new(&out_dir).join("design_tokens_generated.rs"), + ) { + eprintln!("Warning: Failed to generate design token colors: {}", e); + eprintln!("This is expected if the design token file doesn't exist or is malformed."); + // Create an empty file so the include! doesn't fail + fs::write( + Path::new(&out_dir).join("design_token_colors_generated.rs"), + "// No design token colors generated\n", + ).unwrap(); + } + println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=resources/simple.css"); + println!("cargo::rerun-if-changed=resources/design-tokens.tokens.json"); } diff --git a/crates/dwind/resources/design-tokens.tokens.json b/crates/tests/dominator-css-bindgen-test/resources/design-tokens.tokens.json similarity index 100% rename from crates/dwind/resources/design-tokens.tokens.json rename to crates/tests/dominator-css-bindgen-test/resources/design-tokens.tokens.json diff --git a/crates/tests/dominator-css-bindgen-test/src/lib.rs b/crates/tests/dominator-css-bindgen-test/src/lib.rs index 21ecf5e..27e12c2 100644 --- a/crates/tests/dominator-css-bindgen-test/src/lib.rs +++ b/crates/tests/dominator-css-bindgen-test/src/lib.rs @@ -1,7 +1,13 @@ include!(concat!(env!("OUT_DIR"), "/generated.rs")); +pub mod tokens { + include!(concat!(env!("OUT_DIR"), "/design_tokens_generated.rs")); +} + #[cfg(test)] mod tests { + use crate::tokens::TEXT_TEST_PRIMARY_RAW; + #[test] fn sanity_check() { let rust_file = @@ -10,6 +16,7 @@ mod tests { println!("{rust_file}"); + println!("{}",TEXT_TEST_PRIMARY_RAW); // just check that it exists //let multiline = &super::WITHPSEUDO_HOVER_ACTIVE; } diff --git a/examples/webpage/.gitignore b/examples/webpage/.gitignore index 36170a7..fe2cbb6 100644 --- a/examples/webpage/.gitignore +++ b/examples/webpage/.gitignore @@ -1,2 +1,3 @@ node_modules public +dist diff --git a/examples/webpage/.npmrc b/examples/webpage/.npmrc new file mode 100644 index 0000000..80bcbed --- /dev/null +++ b/examples/webpage/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps = true diff --git a/examples/webpage/Cargo.toml b/examples/webpage/Cargo.toml index e9316a4..77f2ec0 100644 --- a/examples/webpage/Cargo.toml +++ b/examples/webpage/Cargo.toml @@ -14,8 +14,8 @@ crate-type = ["cdylib"] [dependencies] const_format.workspace = true dwind-macros = { path = "../../crates/dwind-macros", version = "0.2.2" } -dwind = { path = "../../crates/dwind", version = "0.3.2" } -dwui = { path = "../../crates/dwui", version = "0.4.0" } +dwind = { path = "../../crates/dwind", version = "0.4.0" } +dwui = { path = "../../crates/dwui", version = "0.5.0" } dominator.workspace = true example-html-highlight-macro = { path = "../../crates/example-html-macro", version = "0.1.0"} futures.workspace = true diff --git a/examples/webpage/README.md b/examples/webpage/README.md index 7000063..427e185 100644 --- a/examples/webpage/README.md +++ b/examples/webpage/README.md @@ -12,12 +12,12 @@ rustup target add wasm32-unknown-unknown To run the application -### Trunk +### NPM -First install the trunk utility: https://trunkrs.dev/guide/getting-started/installation.html +You need NPM then do ```shell -trunk serve --open +npm run watch ``` \ No newline at end of file diff --git a/examples/webpage/Trunk.toml b/examples/webpage/Trunk.toml deleted file mode 100644 index 37572c3..0000000 --- a/examples/webpage/Trunk.toml +++ /dev/null @@ -1,9 +0,0 @@ -[build] -target = "public/index.html" -minify = "always" - -[serve] -port=8811 - -[clean] -cargo = true \ No newline at end of file diff --git a/examples/webpage/index.html b/examples/webpage/index.html new file mode 100644 index 0000000..e42f0ac --- /dev/null +++ b/examples/webpage/index.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/webpage/package-lock.json b/examples/webpage/package-lock.json new file mode 100644 index 0000000..322deee --- /dev/null +++ b/examples/webpage/package-lock.json @@ -0,0 +1,2050 @@ +{ + "name": "DWIND and DWUI example", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "DWIND and DWUI example", + "version": "0.1.0", + "devDependencies": { + "@wasm-tool/rollup-plugin-rust": "^3.0.5", + "binaryen": "^121.0.0", + "rimraf": "^6.0.1", + "rollup": "^4.30.1", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-livereload": "^2.0.5", + "rollup-plugin-serve": "^1.1.1", + "rollup-plugin-terser": "^7.0.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", + "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "dev": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@wasm-tool/rollup-plugin-rust": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@wasm-tool/rollup-plugin-rust/-/rollup-plugin-rust-3.0.5.tgz", + "integrity": "sha512-L3TyDtmrmAY4XdlZAZKMomDManfrKHLQHjDeFNJKi4LjPQRwWUnPoS0yV44NWy+pKXVKlv7wbdkaTmhiFJRmLg==", + "dev": true, + "dependencies": { + "@iarna/toml": "^2.2.5", + "@rollup/pluginutils": "^5.1.4", + "chalk": "^5.4.1", + "glob": "^11.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^6.0.1", + "tar": "^7.4.3" + }, + "peerDependencies": { + "binaryen": "^121.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binaryen": { + "version": "121.0.0", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-121.0.0.tgz", + "integrity": "sha512-St5LX+CmVdDQMf+DDHWdne7eDK+8tH9TE4Kc+Xk3s5+CzVYIKeJbWuXgsKVbkdLJXGUc2eflFqjThQy555mBag==", + "dev": true, + "bin": { + "wasm-as": "bin/wasm-as", + "wasm-ctor-eval": "bin/wasm-ctor-eval", + "wasm-dis": "bin/wasm-dis", + "wasm-merge": "bin/wasm-merge", + "wasm-metadce": "bin/wasm-metadce", + "wasm-opt": "bin/wasm-opt", + "wasm-reduce": "bin/wasm-reduce", + "wasm-shell": "bin/wasm-shell", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/livereload": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", + "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.0", + "livereload-js": "^3.3.1", + "opts": ">= 1.2.0", + "ws": "^7.4.3" + }, + "bin": { + "livereload": "bin/livereload.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/livereload-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz", + "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/opts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", + "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", + "dev": true + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-livereload": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz", + "integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==", + "dev": true, + "dependencies": { + "livereload": "^0.9.1" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-serve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-serve/-/rollup-plugin-serve-1.1.1.tgz", + "integrity": "sha512-H0VarZRtFR0lfiiC9/P8jzCDvtFf1liOX4oSdIeeYqUCKrmFA7vNiQ0rg2D+TuoP7leaa/LBR8XBts5viF6lnw==", + "dev": true, + "dependencies": { + "mime": "^2", + "opener": "1" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.42.0.tgz", + "integrity": "sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/examples/webpage/package.json b/examples/webpage/package.json new file mode 100644 index 0000000..d5fb37c --- /dev/null +++ b/examples/webpage/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "type": "module", + "name": "DWIND and DWUI example", + "author": "Mathias Myrland ", + "version": "0.1.0", + "scripts": { + "build": "rimraf dist/js && rollup --config", + "watch": "rimraf dist/js && rollup --config --watch" + }, + "devDependencies": { + "@wasm-tool/rollup-plugin-rust": "^3.0.5", + "binaryen": "^121.0.0", + "rimraf": "^6.0.1", + "rollup": "^4.30.1", + "rollup-plugin-livereload": "^2.0.5", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-serve": "^1.1.1", + "rollup-plugin-terser": "^7.0.2" + } +} \ No newline at end of file diff --git a/examples/webpage/rollup.config.js b/examples/webpage/rollup.config.js new file mode 100644 index 0000000..e3e079e --- /dev/null +++ b/examples/webpage/rollup.config.js @@ -0,0 +1,37 @@ +import rust from "@wasm-tool/rollup-plugin-rust"; +import serve from "rollup-plugin-serve"; +import livereload from "rollup-plugin-livereload"; +import {terser} from "rollup-plugin-terser"; +import copy from 'rollup-plugin-copy' + +const is_watch = !!process.env.ROLLUP_WATCH; + +export default { + input: { + example: "Cargo.toml", + }, + output: { + dir: "dist/js", + format: "es", + sourcemap: true, + }, + plugins: [ + rust({ + optimize: {wasmOpt: false} + }), + + copy({ + targets: [ + {src: 'index.html', dest: 'dist/'} + ] + }), + is_watch && serve({ + contentBase: "dist", + open: true, + }), + + is_watch && livereload("dist"), + + !is_watch && terser(), + ], +}; \ No newline at end of file From f97faf256c3aac076c8142037c20df9d7f3c2e9a Mon Sep 17 00:00:00 2001 From: Mathias Myrland Date: Sat, 14 Jun 2025 20:37:15 +0200 Subject: [PATCH 5/5] added grid layout example --- crates/dwind-build/src/design_tokens.rs | 92 ++++++---- .../examples/circular_reference_demo.rs | 70 ++++---- .../examples/parse_example.rs | 15 +- crates/dwind-design-tokens/src/error.rs | 2 +- crates/dwind-design-tokens/src/expressions.rs | 49 +++--- crates/dwind-design-tokens/src/lib.rs | 8 +- crates/dwind-design-tokens/src/parser.rs | 38 ++-- crates/dwind-design-tokens/src/types.rs | 2 +- crates/dwind-design-tokens/src/validation.rs | 62 +++++-- .../dwind-design-tokens/tests/integration.rs | 18 +- crates/dwind/build.rs | 1 - .../tests/dominator-css-bindgen-test/build.rs | 7 +- .../dominator-css-bindgen-test/src/lib.rs | 2 +- examples/webpage/src/pages/docs/doc_main.rs | 2 + .../webpage/src/pages/docs/doc_pages/grid.rs | 162 ++++++++++++++++++ .../webpage/src/pages/docs/doc_pages/mod.rs | 1 + examples/webpage/src/pages/docs/mod.rs | 8 +- examples/webpage/src/router.rs | 4 + 18 files changed, 383 insertions(+), 160 deletions(-) create mode 100644 examples/webpage/src/pages/docs/doc_pages/grid.rs diff --git a/crates/dwind-build/src/design_tokens.rs b/crates/dwind-build/src/design_tokens.rs index 39d6ee3..d85f240 100644 --- a/crates/dwind-build/src/design_tokens.rs +++ b/crates/dwind-build/src/design_tokens.rs @@ -1,14 +1,11 @@ -use dwind_design_tokens::{DesignTokenFile, TokenType, TokenValue, parse_tokens}; +use dwind_design_tokens::{parse_tokens, DesignTokenFile, TokenType, TokenValue}; use std::collections::HashMap; use std::fs; use std::io::Write; use std::path::Path; /// Color utility class prefixes and their corresponding generator macros -const COLOR_UTILITY_PREFIXES: &[(&str, &str)] = &[ - ("bg", "background-color"), - ("text", "color"), -]; +const COLOR_UTILITY_PREFIXES: &[(&str, &str)] = &[("bg", "background-color"), ("text", "color")]; /// Dimension utility class prefixes for margin and padding const DIMENSION_UTILITY_PREFIXES: &[(&str, &str)] = &[ @@ -20,7 +17,6 @@ const DIMENSION_UTILITY_PREFIXES: &[(&str, &str)] = &[ ("mb", "margin-bottom"), ("mx", "margin-left,margin-right"), ("my", "margin-top,margin-bottom"), - // Padding utilities ("p", "padding"), ("pl", "padding-left"), @@ -34,10 +30,22 @@ const DIMENSION_UTILITY_PREFIXES: &[(&str, &str)] = &[ /// Border radius utility class prefixes const BORDER_RADIUS_UTILITY_PREFIXES: &[(&str, &str)] = &[ ("rounded", "border-radius"), - ("rounded-t", "border-top-left-radius,border-top-right-radius"), - ("rounded-r", "border-top-right-radius,border-bottom-right-radius"), - ("rounded-b", "border-bottom-left-radius,border-bottom-right-radius"), - ("rounded-l", "border-top-left-radius,border-bottom-left-radius"), + ( + "rounded-t", + "border-top-left-radius,border-top-right-radius", + ), + ( + "rounded-r", + "border-top-right-radius,border-bottom-right-radius", + ), + ( + "rounded-b", + "border-bottom-left-radius,border-bottom-right-radius", + ), + ( + "rounded-l", + "border-top-left-radius,border-bottom-left-radius", + ), ("rounded-tl", "border-top-left-radius"), ("rounded-tr", "border-top-right-radius"), ("rounded-bl", "border-bottom-left-radius"), @@ -55,7 +63,7 @@ pub fn render_design_tokens_to_rust_file( // Resolve all token expressions first let resolved_tokens = resolve_token_expressions(&design_token_file)?; - + // Extract tokens by type let color_tokens = extract_color_tokens(&design_token_file, &resolved_tokens); let dimension_tokens = extract_dimension_tokens(&design_token_file, &resolved_tokens); @@ -63,29 +71,31 @@ pub fn render_design_tokens_to_rust_file( // Generate utility classes let mut generated_code = String::new(); - + // Add header comment generated_code.push_str("// Auto-generated utility classes from design tokens\n"); generated_code.push_str("// Do not edit this file manually\n\n"); - + // Generate color utilities if !color_tokens.is_empty() { generated_code.push_str("// Color utilities\n"); generated_code.push_str(&generate_color_utility_classes(&color_tokens)); generated_code.push_str("\n"); } - + // Generate dimension utilities (margin and padding) if !dimension_tokens.is_empty() { generated_code.push_str("// Dimension utilities (margin and padding)\n"); generated_code.push_str(&generate_dimension_utility_classes(&dimension_tokens)); generated_code.push_str("\n"); } - + // Generate border radius utilities if !border_radius_tokens.is_empty() { generated_code.push_str("// Border radius utilities\n"); - generated_code.push_str(&generate_border_radius_utility_classes(&border_radius_tokens)); + generated_code.push_str(&generate_border_radius_utility_classes( + &border_radius_tokens, + )); generated_code.push_str("\n"); } @@ -105,10 +115,12 @@ pub fn render_design_token_colors_to_rust_file( } /// Resolve all token expressions in the design token file -pub fn resolve_token_expressions(design_token_file: &DesignTokenFile) -> Result, Box> { +pub fn resolve_token_expressions( + design_token_file: &DesignTokenFile, +) -> Result, Box> { let mut resolved = HashMap::new(); let all_tokens = design_token_file.get_all_tokens(); - + // First pass: collect all literal values for (path, token) in &all_tokens { match &token.value { @@ -121,24 +133,24 @@ pub fn resolve_token_expressions(design_token_file: &DesignTokenFile) -> Result< _ => {} // Skip expressions for now } } - + // Second pass: resolve expressions (simple dependency resolution) let mut changed = true; let mut max_iterations = 10; // Prevent infinite loops - + while changed && max_iterations > 0 { changed = false; max_iterations -= 1; - + for (path, token) in &all_tokens { if resolved.contains_key(path) { continue; // Already resolved } - + if let TokenValue::Expression(expr) = &token.value { // Create a context with both full paths and relative references let mut context = resolved.clone(); - + // Add relative references for tokens in the same set let path_parts: Vec<&str> = path.split('.').collect(); if path_parts.len() >= 2 { @@ -152,7 +164,7 @@ pub fn resolve_token_expressions(design_token_file: &DesignTokenFile) -> Result< } } } - + // Try to evaluate the expression match expr.evaluate(&context) { Ok(value) => { @@ -166,12 +178,15 @@ pub fn resolve_token_expressions(design_token_file: &DesignTokenFile) -> Result< } } } - + Ok(resolved) } /// Extract all color tokens from the design token file -fn extract_color_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &HashMap) -> Vec<(String, String)> { +fn extract_color_tokens( + design_token_file: &DesignTokenFile, + resolved_tokens: &HashMap, +) -> Vec<(String, String)> { let mut color_tokens = Vec::new(); let all_tokens = design_token_file.get_all_tokens(); @@ -193,7 +208,10 @@ fn extract_color_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &H } /// Extract all dimension tokens from the design token file -fn extract_dimension_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &HashMap) -> Vec<(String, String)> { +fn extract_dimension_tokens( + design_token_file: &DesignTokenFile, + resolved_tokens: &HashMap, +) -> Vec<(String, String)> { let mut dimension_tokens = Vec::new(); let all_tokens = design_token_file.get_all_tokens(); @@ -215,7 +233,10 @@ fn extract_dimension_tokens(design_token_file: &DesignTokenFile, resolved_tokens } /// Extract all border radius tokens from the design token file -fn extract_border_radius_tokens(design_token_file: &DesignTokenFile, resolved_tokens: &HashMap) -> Vec<(String, String)> { +fn extract_border_radius_tokens( + design_token_file: &DesignTokenFile, + resolved_tokens: &HashMap, +) -> Vec<(String, String)> { let mut border_radius_tokens = Vec::new(); let all_tokens = design_token_file.get_all_tokens(); @@ -250,8 +271,7 @@ fn generate_color_utility_classes(color_tokens: &[(String, String)]) -> String { let utility_name = path_to_class_name(prefix, token_path) .to_uppercase() .replace("-", "_"); - let utility_prefix = path_to_class_name(prefix, token_path) - .replace("-", "_"); + let utility_prefix = path_to_class_name(prefix, token_path).replace("-", "_"); output.push_str(&format!( "# [doc (hidden)] pub static {utility_name}_RAW: &str = \"{css_rule}\";\n" @@ -279,7 +299,8 @@ fn generate_dimension_utility_classes(dimension_tokens: &[(String, String)]) -> .iter() .map(|prop| format!("{}: {}", prop, dimension_value)) .collect::>() - .join("; ") + ";" + .join("; ") + + ";" } else { format!("{}: {};", css_properties, dimension_value) }; @@ -287,8 +308,7 @@ fn generate_dimension_utility_classes(dimension_tokens: &[(String, String)]) -> let utility_name = path_to_class_name(prefix, token_path) .to_uppercase() .replace("-", "_"); - let utility_prefix = path_to_class_name(prefix, token_path) - .replace("-", "_"); + let utility_prefix = path_to_class_name(prefix, token_path).replace("-", "_"); output.push_str(&format!( "# [doc (hidden)] pub static {utility_name}_RAW: &str = \"{css_rule}\";\n" @@ -316,7 +336,8 @@ fn generate_border_radius_utility_classes(border_radius_tokens: &[(String, Strin .iter() .map(|prop| format!("{}: {}", prop, border_radius_value)) .collect::>() - .join("; ") + ";" + .join("; ") + + ";" } else { format!("{}: {};", css_properties, border_radius_value) }; @@ -324,8 +345,7 @@ fn generate_border_radius_utility_classes(border_radius_tokens: &[(String, Strin let utility_name = path_to_class_name(prefix, token_path) .to_uppercase() .replace("-", "_"); - let utility_prefix = path_to_class_name(prefix, token_path) - .replace("-", "_"); + let utility_prefix = path_to_class_name(prefix, token_path).replace("-", "_"); output.push_str(&format!( "# [doc (hidden)] pub static {utility_name}_RAW: &str = \"{css_rule}\";\n" diff --git a/crates/dwind-design-tokens/examples/circular_reference_demo.rs b/crates/dwind-design-tokens/examples/circular_reference_demo.rs index c6a0a7c..dd710eb 100644 --- a/crates/dwind-design-tokens/examples/circular_reference_demo.rs +++ b/crates/dwind-design-tokens/examples/circular_reference_demo.rs @@ -1,5 +1,5 @@ //! Demonstration of the enhanced circular reference detection system -//! +//! //! This example shows how the validation system now detects various types //! of circular references in design token files. @@ -29,19 +29,17 @@ fn main() { "#; match parse_tokens(self_ref_json) { - Ok(file) => { - match validate_token_file(&file) { - Ok(report) => { - if !report.is_valid() { - println!("✓ Detected circular reference:"); - for circular_ref in &report.circular_references { - println!(" - {}", circular_ref); - } + Ok(file) => match validate_token_file(&file) { + Ok(report) => { + if !report.is_valid() { + println!("✓ Detected circular reference:"); + for circular_ref in &report.circular_references { + println!(" - {}", circular_ref); } } - Err(e) => println!("✗ Validation error: {}", e), } - } + Err(e) => println!("✗ Validation error: {}", e), + }, Err(e) => println!("✗ Parse error: {}", e), } @@ -78,19 +76,17 @@ fn main() { "#; match parse_tokens(chain_json) { - Ok(file) => { - match validate_token_file(&file) { - Ok(report) => { - if !report.is_valid() { - println!("✓ Detected circular reference chain:"); - for circular_ref in &report.circular_references { - println!(" - {}", circular_ref); - } + Ok(file) => match validate_token_file(&file) { + Ok(report) => { + if !report.is_valid() { + println!("✓ Detected circular reference chain:"); + for circular_ref in &report.circular_references { + println!(" - {}", circular_ref); } } - Err(e) => println!("✗ Validation error: {}", e), } - } + Err(e) => println!("✗ Validation error: {}", e), + }, Err(e) => println!("✗ Parse error: {}", e), } @@ -132,25 +128,23 @@ fn main() { "#; match parse_tokens(valid_json) { - Ok(file) => { - match validate_token_file(&file) { - Ok(report) => { - if report.is_valid() { - println!("✓ All references are valid - no circular dependencies detected"); - println!(" Summary: {}", report.summary()); - } else { - println!("✗ Unexpected validation issues:"); - for error in &report.errors { - println!(" - Error: {}", error); - } - for circular_ref in &report.circular_references { - println!(" - Circular: {}", circular_ref); - } + Ok(file) => match validate_token_file(&file) { + Ok(report) => { + if report.is_valid() { + println!("✓ All references are valid - no circular dependencies detected"); + println!(" Summary: {}", report.summary()); + } else { + println!("✗ Unexpected validation issues:"); + for error in &report.errors { + println!(" - Error: {}", error); + } + for circular_ref in &report.circular_references { + println!(" - Circular: {}", circular_ref); } } - Err(e) => println!("✗ Validation error: {}", e), } - } + Err(e) => println!("✗ Validation error: {}", e), + }, Err(e) => println!("✗ Parse error: {}", e), } @@ -161,4 +155,4 @@ fn main() { println!("• Complex circular chains (A → B → C → D → A)"); println!("• Circular references within mathematical expressions"); println!("• Mixed scenarios with both valid and circular references"); -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/examples/parse_example.rs b/crates/dwind-design-tokens/examples/parse_example.rs index 12cdacf..d34f108 100644 --- a/crates/dwind-design-tokens/examples/parse_example.rs +++ b/crates/dwind-design-tokens/examples/parse_example.rs @@ -45,15 +45,18 @@ fn main() -> Result<(), Box> { "##; println!("Parsing design tokens..."); - + // Parse the tokens let token_file = parse_tokens(json)?; - println!("✅ Successfully parsed {} token sets", token_file.sets.len()); + println!( + "✅ Successfully parsed {} token sets", + token_file.sets.len() + ); // Validate the tokens let validation_report = validate_token_file(&token_file)?; println!("✅ Validation completed: {}", validation_report.summary()); - + if !validation_report.is_valid() { println!("❌ Validation failed!"); for error in &validation_report.errors { @@ -78,11 +81,11 @@ fn main() -> Result<(), Box> { // Test specific token access println!("\n🔍 Testing token access:"); - + if let Some(token_a) = token_file.find_token("test.a") { println!(" test.a: {:?}", token_a.value); } - + if let Some(token_b) = token_file.find_token("test.b") { println!(" test.b: {:?}", token_b.value); if let TokenValue::Expression(expr) = &token_b.value { @@ -106,4 +109,4 @@ fn main() -> Result<(), Box> { println!("\n✅ Design token parsing completed successfully!"); Ok(()) -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/src/error.rs b/crates/dwind-design-tokens/src/error.rs index d07dbef..121d72a 100644 --- a/crates/dwind-design-tokens/src/error.rs +++ b/crates/dwind-design-tokens/src/error.rs @@ -29,4 +29,4 @@ pub enum TokenError { } /// Result type for token operations -pub type TokenResult = Result; \ No newline at end of file +pub type TokenResult = Result; diff --git a/crates/dwind-design-tokens/src/expressions.rs b/crates/dwind-design-tokens/src/expressions.rs index 7122e75..2189f58 100644 --- a/crates/dwind-design-tokens/src/expressions.rs +++ b/crates/dwind-design-tokens/src/expressions.rs @@ -79,13 +79,14 @@ impl Expr { } /// Evaluate this expression given a context of resolved token values - pub fn evaluate(&self, context: &std::collections::HashMap) -> TokenResult { + pub fn evaluate( + &self, + context: &std::collections::HashMap, + ) -> TokenResult { match self { - Expr::Reference(name) => { - context.get(name) - .cloned() - .ok_or_else(|| TokenError::ExpressionParsing(format!("Undefined reference: {}", name))) - } + Expr::Reference(name) => context.get(name).cloned().ok_or_else(|| { + TokenError::ExpressionParsing(format!("Undefined reference: {}", name)) + }), Expr::Literal(value) => { // For dimension/border-radius tokens, we need to preserve the unit // Try to format as integer if it's a whole number, otherwise as float @@ -98,23 +99,25 @@ impl Expr { Expr::BinaryOp { op, left, right } => { let left_val = left.evaluate(context)?; let right_val = right.evaluate(context)?; - + // Parse numeric values from CSS values (e.g., "12px" -> 12.0) let left_num = parse_css_value(&left_val)?; let right_num = parse_css_value(&right_val)?; - + let result = match op { BinaryOperator::Add => left_num + right_num, BinaryOperator::Sub => left_num - right_num, BinaryOperator::Mul => left_num * right_num, BinaryOperator::Div => { if right_num == 0.0 { - return Err(TokenError::ExpressionParsing("Division by zero".to_string())); + return Err(TokenError::ExpressionParsing( + "Division by zero".to_string(), + )); } left_num / right_num } }; - + // Format result back to CSS value if result.fract() == 0.0 { Ok(format!("{}px", result as i64)) @@ -130,24 +133,28 @@ impl Expr { /// Supports values like "12px", "1.5em", "100%", or plain numbers fn parse_css_value(value: &str) -> TokenResult { let trimmed = value.trim(); - + // Try to parse as plain number first if let Ok(num) = trimmed.parse::() { return Ok(num); } - + // Extract numeric part from CSS values let numeric_part = trimmed .chars() .take_while(|c| c.is_numeric() || *c == '.' || *c == '-') .collect::(); - + if numeric_part.is_empty() { - return Err(TokenError::ExpressionParsing(format!("Cannot parse numeric value from: {}", value))); + return Err(TokenError::ExpressionParsing(format!( + "Cannot parse numeric value from: {}", + value + ))); } - - numeric_part.parse::() - .map_err(|_| TokenError::ExpressionParsing(format!("Invalid numeric value: {}", numeric_part))) + + numeric_part.parse::().map_err(|_| { + TokenError::ExpressionParsing(format!("Invalid numeric value: {}", numeric_part)) + }) } // Nom parser functions @@ -203,11 +210,7 @@ fn parse_multiplicative(input: &str) -> IResult<&str, Expr> { fn parse_primary(input: &str) -> IResult<&str, Expr> { delimited( multispace0, - alt(( - parse_parenthesized, - parse_reference, - parse_literal, - )), + alt((parse_parenthesized, parse_reference, parse_literal)), multispace0, )(input) } @@ -371,4 +374,4 @@ mod tests { context.insert("base".to_string(), "8px".to_string()); assert_eq!(expr.evaluate(&context).unwrap(), "16px"); } -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/src/lib.rs b/crates/dwind-design-tokens/src/lib.rs index 663d1f8..a64dc1a 100644 --- a/crates/dwind-design-tokens/src/lib.rs +++ b/crates/dwind-design-tokens/src/lib.rs @@ -145,7 +145,7 @@ mod integration_tests { // Validate the tokens let validation_report = validate_token_file(&token_file).expect("Validation failed"); - + if !validation_report.is_valid() { println!("Validation issues: {}", validation_report.summary()); for error in &validation_report.errors { @@ -155,7 +155,7 @@ mod integration_tests { println!("Missing reference: {}", missing); } } - + assert!(validation_report.is_valid(), "Validation should pass"); // Test token access @@ -262,9 +262,9 @@ mod integration_tests { let token_file = parse_tokens(json).expect("Failed to parse tokens"); let validation_report = validate_token_file(&token_file).expect("Validation failed"); - + assert!(!validation_report.is_valid()); assert!(!validation_report.errors.is_empty()); assert!(!validation_report.missing_references.is_empty()); } -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/src/parser.rs b/crates/dwind-design-tokens/src/parser.rs index 53b45e5..0fb9408 100644 --- a/crates/dwind-design-tokens/src/parser.rs +++ b/crates/dwind-design-tokens/src/parser.rs @@ -7,10 +7,10 @@ use std::path::Path; /// Parse a design token file from JSON string pub fn parse_tokens(json: &str) -> TokenResult { let mut file: DesignTokenFile = serde_json::from_str(json)?; - + // Post-process to convert expression strings to Expression variants process_token_file(&mut file)?; - + Ok(file) } @@ -32,7 +32,7 @@ fn process_token_file(file: &mut DesignTokenFile) -> TokenResult<()> { /// Recursively process token nodes to detect and parse expressions fn process_token_node(node: &mut crate::types::TokenNode) -> TokenResult<()> { use crate::types::TokenNode; - + match node { TokenNode::Token(token) => { // Check if the value is a string that looks like an expression @@ -50,7 +50,7 @@ fn process_token_node(node: &mut crate::types::TokenNode) -> TokenResult<()> { } } } - + Ok(()) } @@ -60,26 +60,26 @@ fn is_expression(value: &str) -> bool { if value.contains('{') && value.contains('}') { return true; } - + // Check for arithmetic operators with potential references // This is a simple heuristic - we could make it more sophisticated - let has_operators = value.contains('+') || value.contains('-') || value.contains('*') || value.contains('/'); + let has_operators = + value.contains('+') || value.contains('-') || value.contains('*') || value.contains('/'); let has_numbers = value.chars().any(|c| c.is_ascii_digit()); - + // If it has operators and numbers, it might be an expression // But we need to be careful not to catch CSS values like "rgb(255, 0, 0)" if has_operators && has_numbers && !value.starts_with("rgb") && !value.starts_with("hsl") { return true; } - + false } - #[cfg(test)] mod tests { use super::*; - use crate::types::{TokenType, DesignToken}; + use crate::types::{DesignToken, TokenType}; #[test] fn test_is_expression() { @@ -113,11 +113,11 @@ mod tests { let result = parse_tokens(json); assert!(result.is_ok()); - + let file = result.unwrap(); let token = file.find_token("test.a").unwrap(); assert_eq!(token.token_type, TokenType::Dimension); - + if let TokenValue::Literal(value) = &token.value { assert_eq!(value, "5px"); } else { @@ -152,14 +152,14 @@ mod tests { let result = parse_tokens(json); assert!(result.is_ok()); - + let file = result.unwrap(); let token_a = file.find_token("test.a").unwrap(); let token_b = file.find_token("test.b").unwrap(); - + assert!(!token_a.has_references()); assert!(token_b.has_references()); - + let refs = token_b.get_references(); assert_eq!(refs, vec!["a"]); } @@ -188,7 +188,7 @@ mod tests { let result = parse_tokens(json); assert!(result.is_ok()); - + let file = result.unwrap(); let token = file.find_token("colors.primary.500").unwrap(); assert_eq!(token.token_type, TokenType::Color); @@ -226,12 +226,12 @@ mod tests { let result = parse_tokens(json); assert!(result.is_ok()); - + let file = result.unwrap(); let references = file.get_all_references(); - + assert_eq!(references.len(), 2); assert_eq!(references.get("test.b").unwrap(), &vec!["a"]); assert_eq!(references.get("test.c").unwrap(), &vec!["a", "b"]); } -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/src/types.rs b/crates/dwind-design-tokens/src/types.rs index 985f519..2632d2a 100644 --- a/crates/dwind-design-tokens/src/types.rs +++ b/crates/dwind-design-tokens/src/types.rs @@ -333,4 +333,4 @@ mod tests { assert_eq!(tokens.len(), 1); assert_eq!(tokens[0].0, "group.inner"); } -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/src/validation.rs b/crates/dwind-design-tokens/src/validation.rs index 140b617..3bc0f07 100644 --- a/crates/dwind-design-tokens/src/validation.rs +++ b/crates/dwind-design-tokens/src/validation.rs @@ -27,16 +27,19 @@ impl ValidationContext { fn push_token(&mut self, path: &str) -> TokenResult<()> { if self.validation_stack.contains(&path.to_string()) { - let cycle_start = self.validation_stack.iter() + let cycle_start = self + .validation_stack + .iter() .position(|p| p == path) .unwrap(); let cycle = &self.validation_stack[cycle_start..]; - let cycle_chain = cycle.iter() + let cycle_chain = cycle + .iter() .chain(std::iter::once(&path.to_string())) .cloned() .collect::>() .join(" -> "); - + return Err(TokenError::CircularReference(format!( "Circular reference detected: {}. This creates an infinite dependency loop.", cycle_chain @@ -168,13 +171,15 @@ impl ValidationReport { /// Check if the validation passed (no errors) pub fn is_valid(&self) -> bool { - self.errors.is_empty() && self.circular_references.is_empty() && self.missing_references.is_empty() + self.errors.is_empty() + && self.circular_references.is_empty() + && self.missing_references.is_empty() } /// Get a summary of the validation results pub fn summary(&self) -> String { let mut parts = Vec::new(); - + if !self.errors.is_empty() { parts.push(format!("{} errors", self.errors.len())); } @@ -182,10 +187,16 @@ impl ValidationReport { parts.push(format!("{} warnings", self.warnings.len())); } if !self.circular_references.is_empty() { - parts.push(format!("{} circular references", self.circular_references.len())); + parts.push(format!( + "{} circular references", + self.circular_references.len() + )); } if !self.missing_references.is_empty() { - parts.push(format!("{} missing references", self.missing_references.len())); + parts.push(format!( + "{} missing references", + self.missing_references.len() + )); } if !self.type_mismatches.is_empty() { parts.push(format!("{} type mismatches", self.type_mismatches.len())); @@ -245,7 +256,10 @@ fn validate_design_token( } (TokenValue::Literal(value), TokenType::BorderRadius) => { if !is_valid_dimension(value) { - report.add_error(format!("Invalid border radius value '{}' at {}", value, path)); + report.add_error(format!( + "Invalid border radius value '{}' at {}", + value, path + )); } } (TokenValue::Literal(value), TokenType::Color) => { @@ -283,7 +297,7 @@ fn validate_all_references( if token.has_references() { // Clear the validation stack for each top-level validation context.validation_stack.clear(); - + // Perform recursive validation for circular dependencies context.validate_token_references_recursively(&token_path, file, report)?; } @@ -316,7 +330,9 @@ fn is_valid_dimension(value: &str) -> bool { return true; } - let units = ["px", "em", "rem", "%", "vh", "vw", "pt", "pc", "in", "cm", "mm"]; + let units = [ + "px", "em", "rem", "%", "vh", "vw", "pt", "pc", "in", "cm", "mm", + ]; units.iter().any(|unit| value.ends_with(unit)) } @@ -350,8 +366,18 @@ fn is_valid_color(value: &str) -> bool { // Named colors (basic set) let named_colors = [ - "red", "green", "blue", "white", "black", "transparent", - "yellow", "orange", "purple", "pink", "gray", "grey", + "red", + "green", + "blue", + "white", + "black", + "transparent", + "yellow", + "orange", + "purple", + "pink", + "gray", + "grey", ]; named_colors.contains(&value) } @@ -568,10 +594,12 @@ mod tests { assert!(!report.circular_references.is_empty()); // Should detect the circular reference in the chain a -> b -> c -> d -> a let circular_ref = &report.circular_references[0]; - assert!(circular_ref.contains("test.a") && - circular_ref.contains("test.b") && - circular_ref.contains("test.c") && - circular_ref.contains("test.d")); + assert!( + circular_ref.contains("test.a") + && circular_ref.contains("test.b") + && circular_ref.contains("test.c") + && circular_ref.contains("test.d") + ); } #[test] @@ -693,4 +721,4 @@ mod tests { assert!(report.is_valid()); assert!(report.circular_references.is_empty()); } -} \ No newline at end of file +} diff --git a/crates/dwind-design-tokens/tests/integration.rs b/crates/dwind-design-tokens/tests/integration.rs index 3a2d756..538f6ad 100644 --- a/crates/dwind-design-tokens/tests/integration.rs +++ b/crates/dwind-design-tokens/tests/integration.rs @@ -48,7 +48,7 @@ fn test_parse_example_tokens() { assert!(result.is_ok(), "Failed to parse tokens: {:?}", result.err()); let token_file = result.unwrap(); - + // Test finding tokens let token_a = token_file.find_token("test.a").unwrap(); assert_eq!(token_a.token_type, TokenType::Dimension); @@ -57,7 +57,7 @@ fn test_parse_example_tokens() { let token_b = token_file.find_token("test.b").unwrap(); assert_eq!(token_b.token_type, TokenType::Dimension); assert!(token_b.has_references()); - + let refs = token_b.get_references(); assert_eq!(refs, vec!["a"]); @@ -116,7 +116,7 @@ fn test_complex_expressions() { let complex_token = token_file.find_token("math.complex").unwrap(); assert!(complex_token.has_references()); - + let multi_ref_token = token_file.find_token("math.multi_ref").unwrap(); let refs = multi_ref_token.get_references(); assert_eq!(refs.len(), 2); @@ -211,7 +211,7 @@ fn test_validation_errors() { let token_file = parse_tokens(json).unwrap(); let validation_report = validate_token_file(&token_file).unwrap(); - + assert!(!validation_report.is_valid()); assert!(!validation_report.errors.is_empty()); assert!(!validation_report.missing_references.is_empty()); @@ -259,22 +259,22 @@ fn test_circular_reference_detection() { let token_file = parse_tokens(json).unwrap(); let validation_report = validate_token_file(&token_file).unwrap(); - + // Should detect circular references but not affect valid tokens assert!(!validation_report.is_valid()); assert!(!validation_report.circular_references.is_empty()); - + // The circular reference should involve all three circular tokens let circular_ref = &validation_report.circular_references[0]; assert!(circular_ref.contains("circular_a")); assert!(circular_ref.contains("circular_b")); assert!(circular_ref.contains("circular_c")); - + // Valid tokens should still be accessible let base_token = token_file.find_token("spacing.base").unwrap(); assert!(!base_token.has_references()); - + let valid_ref_token = token_file.find_token("spacing.valid_ref").unwrap(); assert!(valid_ref_token.has_references()); assert_eq!(valid_ref_token.get_references(), vec!["base"]); -} \ No newline at end of file +} diff --git a/crates/dwind/build.rs b/crates/dwind/build.rs index dcdc72a..0cc1670 100644 --- a/crates/dwind/build.rs +++ b/crates/dwind/build.rs @@ -40,7 +40,6 @@ fn main() { "resources/colors.json", Path::new(&out_dir).join("colors_generated.rs"), ); - println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=resources/colors.json"); diff --git a/crates/tests/dominator-css-bindgen-test/build.rs b/crates/tests/dominator-css-bindgen-test/build.rs index 0fc2dd0..f2ec617 100644 --- a/crates/tests/dominator-css-bindgen-test/build.rs +++ b/crates/tests/dominator-css-bindgen-test/build.rs @@ -1,6 +1,6 @@ +use dwind_build::design_tokens::render_design_token_colors_to_rust_file; use std::path::Path; use std::{env, fs}; -use dwind_build::design_tokens::render_design_token_colors_to_rust_file; fn main() { let out = dominator_css_bindgen::css::generate_rust_bindings_from_file("resources/simple.css") @@ -22,9 +22,10 @@ fn main() { fs::write( Path::new(&out_dir).join("design_token_colors_generated.rs"), "// No design token colors generated\n", - ).unwrap(); + ) + .unwrap(); } - + println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=resources/simple.css"); println!("cargo::rerun-if-changed=resources/design-tokens.tokens.json"); diff --git a/crates/tests/dominator-css-bindgen-test/src/lib.rs b/crates/tests/dominator-css-bindgen-test/src/lib.rs index 27e12c2..be18c6f 100644 --- a/crates/tests/dominator-css-bindgen-test/src/lib.rs +++ b/crates/tests/dominator-css-bindgen-test/src/lib.rs @@ -16,7 +16,7 @@ mod tests { println!("{rust_file}"); - println!("{}",TEXT_TEST_PRIMARY_RAW); + println!("{}", TEXT_TEST_PRIMARY_RAW); // just check that it exists //let multiline = &super::WITHPSEUDO_HOVER_ACTIVE; } diff --git a/examples/webpage/src/pages/docs/doc_main.rs b/examples/webpage/src/pages/docs/doc_main.rs index 6ab27ff..31c1cf7 100644 --- a/examples/webpage/src/pages/docs/doc_main.rs +++ b/examples/webpage/src/pages/docs/doc_main.rs @@ -1,5 +1,6 @@ use crate::pages::docs::doc_pages::colors::colors_page; use crate::pages::docs::doc_pages::flex::flex_page; +use crate::pages::docs::doc_pages::grid::grid_page; use crate::pages::docs::doc_pages::pseudoclass_themes::pseudo_class_themes; use crate::pages::docs::doc_pages::responsive_design::responsive_design; use crate::pages::docs::DocPage; @@ -14,6 +15,7 @@ pub fn doc_main_view( current_doc.map(|doc| { doc.map(|doc| match doc { DocPage::Flex => flex_page(), + DocPage::Grid => grid_page(), DocPage::Colors => colors_page(), DocPage::Responsiveness => responsive_design(), DocPage::Pseudoclasses => pseudo_class_themes(), diff --git a/examples/webpage/src/pages/docs/doc_pages/grid.rs b/examples/webpage/src/pages/docs/doc_pages/grid.rs new file mode 100644 index 0000000..187dcf8 --- /dev/null +++ b/examples/webpage/src/pages/docs/doc_pages/grid.rs @@ -0,0 +1,162 @@ +use crate::pages::docs::code_widget::code; +use crate::pages::docs::doc_pages::doc_page::{doc_page_sub_header, doc_page_title}; +use crate::pages::docs::example_box::example_box; +use dominator::Dom; +use dwind::background_scratched_generator; +use dwind::prelude::*; +use dwind_macros::dwclass; +use dwind_macros::dwgenerate; +use example_html_highlight_macro::example_html; + +pub fn grid_page() -> Dom { + html!("div", { + .dwclass!("w-full") + .child(doc_page_title("Grid")) + .text("Using CSS Grid for powerful layout systems") + .child(doc_page_sub_header("Basic Grid")) + .child(html!("p", { + .text("Create responsive grid layouts with specified column counts") + })) + .child(example_box(grid_example_basic(), true)) + .child(code(&GRID_EXAMPLE_BASIC_EXAMPLE_HTML_MAP)) + .child(doc_page_sub_header("Column Spanning")) + .child(html!("p", { + .text("Span elements across multiple columns") + })) + .child(example_box(grid_example_col_span(), true)) + .child(code(&GRID_EXAMPLE_COL_SPAN_EXAMPLE_HTML_MAP)) + .child(doc_page_sub_header("Row Spanning")) + .child(html!("p", { + .text("Span elements across multiple rows") + })) + .child(example_box(grid_example_row_span(), true)) + .child(code(&GRID_EXAMPLE_ROW_SPAN_EXAMPLE_HTML_MAP)) + .child(doc_page_sub_header("Responsive Grid")) + .child(html!("p", { + .text("Adapt grid layouts based on container size") + })) + .child(example_box(grid_example_responsive(), true)) + .child(code(&GRID_EXAMPLE_RESPONSIVE_EXAMPLE_HTML_MAP)) + }) +} + +dwgenerate!( + "background-scratched", + "background-scratched-[#1c2e4f,#191919]" +); + +#[example_html(themes = ["base16-ocean.dark", "base16-ocean.light"])] +fn grid_example_basic() -> Dom { + html!("div", { + .dwclass!("w-full rounded-lg background-scratched p-4") + .child(html!("div", { + .dwclass!("w-full grid grid-cols-3") + .children( + (1..=9).map(|i| { + html!("div", { + .dwclass!("bg-apple-700 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text(&i.to_string()) + }) + }) + ) + })) + }) +} + +#[example_html(themes = ["base16-ocean.dark", "base16-ocean.light"])] +fn grid_example_col_span() -> Dom { + html!("div", { + .dwclass!("w-full rounded-lg background-scratched p-4") + .child(html!("div", { + .dwclass!("w-full grid grid-cols-4") + .children([ + html!("div", { + .dwclass!("bg-charm-700 rounded-lg flex align-items-center justify-center h-20 font-bold col-span-2 m-2") + .text("Span 2") + }), + html!("div", { + .dwclass!("bg-charm-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("1") + }), + html!("div", { + .dwclass!("bg-charm-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("2") + }), + html!("div", { + .dwclass!("bg-charm-700 rounded-lg flex align-items-center justify-center h-20 font-bold col-span-3 m-2") + .text("Span 3") + }), + html!("div", { + .dwclass!("bg-charm-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("3") + }), + html!("div", { + .dwclass!("bg-charm-700 rounded-lg flex align-items-center justify-center h-20 font-bold col-span-full m-2") + .text("Full Width") + }), + ]) + })) + }) +} + +#[example_html(themes = ["base16-ocean.dark", "base16-ocean.light"])] +fn grid_example_row_span() -> Dom { + html!("div", { + .dwclass!("w-full rounded-lg background-scratched p-4") + .child(html!("div", { + .dwclass!("w-full grid grid-cols-3") + .children([ + html!("div", { + .dwclass!("bg-candlelight-700 rounded-lg flex align-items-center justify-center font-bold row-span-2 m-2") + .text("Span 2 Rows") + }), + html!("div", { + .dwclass!("bg-candlelight-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("1") + }), + html!("div", { + .dwclass!("bg-candlelight-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("2") + }), + html!("div", { + .dwclass!("bg-candlelight-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("3") + }), + html!("div", { + .dwclass!("bg-candlelight-700 rounded-lg flex align-items-center justify-center font-bold row-span-2 m-2") + .text("Span 2 Rows") + }), + html!("div", { + .dwclass!("bg-candlelight-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("4") + }), + html!("div", { + .dwclass!("bg-candlelight-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("5") + }), + html!("div", { + .dwclass!("bg-candlelight-600 rounded-lg flex align-items-center justify-center h-20 font-bold m-2") + .text("6") + }), + ]) + })) + }) +} + +#[example_html(themes = ["base16-ocean.dark", "base16-ocean.light"])] +fn grid_example_responsive() -> Dom { + html!("div", { + .dwclass!("w-full rounded-lg background-scratched p-4") + .child(html!("div", { + .dwclass!("w-full grid grid-cols-2 @md:grid-cols-3 @lg:grid-cols-4") + .children( + (1..=8).map(|i| { + html!("div", { + .dwclass!("bg-apple-600 rounded-lg flex align-items-center justify-center h-16 @md:h-20 font-bold m-2") + .text(&i.to_string()) + }) + }) + ) + })) + }) +} diff --git a/examples/webpage/src/pages/docs/doc_pages/mod.rs b/examples/webpage/src/pages/docs/doc_pages/mod.rs index 6ef04f0..c38268b 100644 --- a/examples/webpage/src/pages/docs/doc_pages/mod.rs +++ b/examples/webpage/src/pages/docs/doc_pages/mod.rs @@ -1,5 +1,6 @@ pub mod colors; pub mod doc_page; pub mod flex; +pub mod grid; pub mod pseudoclass_themes; pub mod responsive_design; diff --git a/examples/webpage/src/pages/docs/mod.rs b/examples/webpage/src/pages/docs/mod.rs index 043aac3..5d436af 100644 --- a/examples/webpage/src/pages/docs/mod.rs +++ b/examples/webpage/src/pages/docs/mod.rs @@ -21,6 +21,7 @@ pub enum DocPage { Pseudoclasses, // Flex Flex, + Grid, Justify, Align, @@ -36,6 +37,7 @@ impl DocPage { match self { DocPage::Colors => go_to_url("#/docs/colors"), DocPage::Flex => go_to_url("#/docs/flex"), + DocPage::Grid => go_to_url("#/docs/grid"), DocPage::Justify => {} DocPage::Align => {} DocPage::Border => {} @@ -58,6 +60,7 @@ impl Display for DocPage { match self { DocPage::Colors => write!(f, "Colors"), DocPage::Flex => write!(f, "Flex"), + DocPage::Grid => write!(f, "Grid"), DocPage::Justify => write!(f, "Justify"), DocPage::Align => write!(f, "Align"), DocPage::Border => write!(f, "Border"), @@ -82,7 +85,10 @@ pub fn doc_sections() -> Vec { }, DocSection { title: "Flex and Grid".to_string(), - docs: vec![DocPage::Flex /*, DocPage::Justify, DocPage::Align*/], + docs: vec![ + DocPage::Flex, + DocPage::Grid, /*, DocPage::Justify, DocPage::Align*/ + ], }, /*DocSection { title: "Borders".to_string(), diff --git a/examples/webpage/src/router.rs b/examples/webpage/src/router.rs index 9fabbca..dddb875 100644 --- a/examples/webpage/src/router.rs +++ b/examples/webpage/src/router.rs @@ -51,6 +51,10 @@ pub fn make_app_router() -> AppRouter { .insert("#/docs/flex", Box::new(|_| Ok(DocPage::Flex))) .unwrap_throw(); + router + .insert("#/docs/grid", Box::new(|_| Ok(DocPage::Grid))) + .unwrap_throw(); + router .insert( "#/docs/responsive-design",