tailwind_rs_wasm/
lib.rs

1//! # Tailwind-RS WASM
2//! 
3//! A WASM-optimized implementation of Tailwind CSS for Rust web applications.
4//! This crate provides a subset of the full Tailwind-RS functionality that's
5//! optimized for WebAssembly and browser environments.
6
7#![cfg_attr(target_arch = "wasm32", no_std)]
8
9#[cfg(target_arch = "wasm32")]
10extern crate alloc;
11
12use serde::{Deserialize, Serialize};
13use wasm_bindgen::prelude::*;
14
15#[cfg(target_arch = "wasm32")]
16use alloc::{string::String, vec::Vec, format, vec, string::ToString};
17
18#[cfg(not(target_arch = "wasm32"))]
19use std::{string::String, vec::Vec, format, vec, string::ToString};
20
21#[cfg(target_arch = "wasm32")]
22use core::fmt;
23
24#[cfg(not(target_arch = "wasm32"))]
25use std::fmt;
26
27/// Initialize WASM-specific functionality
28#[cfg(target_arch = "wasm32")]
29pub fn init() {
30    console_error_panic_hook::set_once();
31    console_log::init_with_level(log::Level::Info).expect("Failed to initialize logging");
32}
33
34/// WASM-optimized class builder
35#[wasm_bindgen]
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct WasmClassBuilder {
38    classes: Vec<String>,
39}
40
41#[wasm_bindgen]
42impl WasmClassBuilder {
43    /// Create a new WASM class builder
44    #[wasm_bindgen(constructor)]
45    pub fn new() -> WasmClassBuilder {
46        WasmClassBuilder {
47            classes: Vec::new(),
48        }
49    }
50    
51    /// Add a class to the builder
52    pub fn class(&mut self, class: &str) {
53        self.classes.push(class.to_string());
54    }
55    
56    /// Add multiple classes at once
57    pub fn add_classes(&mut self, classes: &str) {
58        for class in classes.split_whitespace() {
59            if !class.is_empty() {
60                self.classes.push(class.to_string());
61            }
62        }
63    }
64    
65    /// Build the final class string
66    pub fn build(&self) -> String {
67        self.classes.join(" ")
68    }
69    
70    /// Get the number of classes
71    pub fn len(&self) -> usize {
72        self.classes.len()
73    }
74    
75    /// Check if the builder is empty
76    pub fn is_empty(&self) -> bool {
77        self.classes.is_empty()
78    }
79}
80
81impl Default for WasmClassBuilder {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87/// WASM-optimized responsive breakpoints
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89pub enum WasmBreakpoint {
90    Sm,
91    Md,
92    Lg,
93    Xl,
94    Xl2,
95}
96
97impl WasmBreakpoint {
98    /// Get the CSS media query for this breakpoint
99    pub fn media_query(&self) -> &'static str {
100        match self {
101            WasmBreakpoint::Sm => "(min-width: 640px)",
102            WasmBreakpoint::Md => "(min-width: 768px)",
103            WasmBreakpoint::Lg => "(min-width: 1024px)",
104            WasmBreakpoint::Xl => "(min-width: 1280px)",
105            WasmBreakpoint::Xl2 => "(min-width: 1536px)",
106        }
107    }
108    
109    /// Get the prefix for this breakpoint
110    pub fn prefix(&self) -> &'static str {
111        match self {
112            WasmBreakpoint::Sm => "sm:",
113            WasmBreakpoint::Md => "md:",
114            WasmBreakpoint::Lg => "lg:",
115            WasmBreakpoint::Xl => "xl:",
116            WasmBreakpoint::Xl2 => "2xl:",
117        }
118    }
119}
120
121/// WASM-optimized spacing system
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub enum WasmSpacing {
124    Px,
125    Zero,
126    P1,
127    P2,
128    P3,
129    P4,
130    P5,
131    P6,
132    P8,
133    P10,
134    P12,
135    P16,
136    P20,
137    P24,
138    P32,
139    P40,
140    P48,
141    P56,
142    P64,
143}
144
145impl WasmSpacing {
146    /// Get the CSS value for this spacing
147    pub fn css_value(&self) -> &'static str {
148        match self {
149            WasmSpacing::Px => "1px",
150            WasmSpacing::Zero => "0px",
151            WasmSpacing::P1 => "0.25rem",
152            WasmSpacing::P2 => "0.5rem",
153            WasmSpacing::P3 => "0.75rem",
154            WasmSpacing::P4 => "1rem",
155            WasmSpacing::P5 => "1.25rem",
156            WasmSpacing::P6 => "1.5rem",
157            WasmSpacing::P8 => "2rem",
158            WasmSpacing::P10 => "2.5rem",
159            WasmSpacing::P12 => "3rem",
160            WasmSpacing::P16 => "4rem",
161            WasmSpacing::P20 => "5rem",
162            WasmSpacing::P24 => "6rem",
163            WasmSpacing::P32 => "8rem",
164            WasmSpacing::P40 => "10rem",
165            WasmSpacing::P48 => "12rem",
166            WasmSpacing::P56 => "14rem",
167            WasmSpacing::P64 => "16rem",
168        }
169    }
170    
171    /// Get the Tailwind class name for padding
172    pub fn padding_class(&self) -> String {
173        match self {
174            WasmSpacing::Px => "p-px".to_string(),
175            WasmSpacing::Zero => "p-0".to_string(),
176            WasmSpacing::P1 => "p-1".to_string(),
177            WasmSpacing::P2 => "p-2".to_string(),
178            WasmSpacing::P3 => "p-3".to_string(),
179            WasmSpacing::P4 => "p-4".to_string(),
180            WasmSpacing::P5 => "p-5".to_string(),
181            WasmSpacing::P6 => "p-6".to_string(),
182            WasmSpacing::P8 => "p-8".to_string(),
183            WasmSpacing::P10 => "p-10".to_string(),
184            WasmSpacing::P12 => "p-12".to_string(),
185            WasmSpacing::P16 => "p-16".to_string(),
186            WasmSpacing::P20 => "p-20".to_string(),
187            WasmSpacing::P24 => "p-24".to_string(),
188            WasmSpacing::P32 => "p-32".to_string(),
189            WasmSpacing::P40 => "p-40".to_string(),
190            WasmSpacing::P48 => "p-48".to_string(),
191            WasmSpacing::P56 => "p-56".to_string(),
192            WasmSpacing::P64 => "p-64".to_string(),
193        }
194    }
195    
196    /// Get the Tailwind class name for margin
197    pub fn margin_class(&self) -> String {
198        match self {
199            WasmSpacing::Px => "m-px".to_string(),
200            WasmSpacing::Zero => "m-0".to_string(),
201            WasmSpacing::P1 => "m-1".to_string(),
202            WasmSpacing::P2 => "m-2".to_string(),
203            WasmSpacing::P3 => "m-3".to_string(),
204            WasmSpacing::P4 => "m-4".to_string(),
205            WasmSpacing::P5 => "m-5".to_string(),
206            WasmSpacing::P6 => "m-6".to_string(),
207            WasmSpacing::P8 => "m-8".to_string(),
208            WasmSpacing::P10 => "m-10".to_string(),
209            WasmSpacing::P12 => "m-12".to_string(),
210            WasmSpacing::P16 => "m-16".to_string(),
211            WasmSpacing::P20 => "m-20".to_string(),
212            WasmSpacing::P24 => "m-24".to_string(),
213            WasmSpacing::P32 => "m-32".to_string(),
214            WasmSpacing::P40 => "m-40".to_string(),
215            WasmSpacing::P48 => "m-48".to_string(),
216            WasmSpacing::P56 => "m-56".to_string(),
217            WasmSpacing::P64 => "m-64".to_string(),
218        }
219    }
220}
221
222/// WASM-optimized color system
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
224pub enum WasmColor {
225    // Grays
226    Gray50,
227    Gray100,
228    Gray200,
229    Gray300,
230    Gray400,
231    Gray500,
232    Gray600,
233    Gray700,
234    Gray800,
235    Gray900,
236    // Blues
237    Blue50,
238    Blue100,
239    Blue200,
240    Blue300,
241    Blue400,
242    Blue500,
243    Blue600,
244    Blue700,
245    Blue800,
246    Blue900,
247    // Reds
248    Red50,
249    Red100,
250    Red200,
251    Red300,
252    Red400,
253    Red500,
254    Red600,
255    Red700,
256    Red800,
257    Red900,
258    // Greens
259    Green50,
260    Green100,
261    Green200,
262    Green300,
263    Green400,
264    Green500,
265    Green600,
266    Green700,
267    Green800,
268    Green900,
269    // Special
270    White,
271    Black,
272    Transparent,
273}
274
275impl WasmColor {
276    /// Get the Tailwind class name for text color
277    pub fn text_class(&self) -> String {
278        match self {
279            WasmColor::Gray50 => "text-gray-50".to_string(),
280            WasmColor::Gray100 => "text-gray-100".to_string(),
281            WasmColor::Gray200 => "text-gray-200".to_string(),
282            WasmColor::Gray300 => "text-gray-300".to_string(),
283            WasmColor::Gray400 => "text-gray-400".to_string(),
284            WasmColor::Gray500 => "text-gray-500".to_string(),
285            WasmColor::Gray600 => "text-gray-600".to_string(),
286            WasmColor::Gray700 => "text-gray-700".to_string(),
287            WasmColor::Gray800 => "text-gray-800".to_string(),
288            WasmColor::Gray900 => "text-gray-900".to_string(),
289            WasmColor::Blue50 => "text-blue-50".to_string(),
290            WasmColor::Blue100 => "text-blue-100".to_string(),
291            WasmColor::Blue200 => "text-blue-200".to_string(),
292            WasmColor::Blue300 => "text-blue-300".to_string(),
293            WasmColor::Blue400 => "text-blue-400".to_string(),
294            WasmColor::Blue500 => "text-blue-500".to_string(),
295            WasmColor::Blue600 => "text-blue-600".to_string(),
296            WasmColor::Blue700 => "text-blue-700".to_string(),
297            WasmColor::Blue800 => "text-blue-800".to_string(),
298            WasmColor::Blue900 => "text-blue-900".to_string(),
299            WasmColor::Red50 => "text-red-50".to_string(),
300            WasmColor::Red100 => "text-red-100".to_string(),
301            WasmColor::Red200 => "text-red-200".to_string(),
302            WasmColor::Red300 => "text-red-300".to_string(),
303            WasmColor::Red400 => "text-red-400".to_string(),
304            WasmColor::Red500 => "text-red-500".to_string(),
305            WasmColor::Red600 => "text-red-600".to_string(),
306            WasmColor::Red700 => "text-red-700".to_string(),
307            WasmColor::Red800 => "text-red-800".to_string(),
308            WasmColor::Red900 => "text-red-900".to_string(),
309            WasmColor::Green50 => "text-green-50".to_string(),
310            WasmColor::Green100 => "text-green-100".to_string(),
311            WasmColor::Green200 => "text-green-200".to_string(),
312            WasmColor::Green300 => "text-green-300".to_string(),
313            WasmColor::Green400 => "text-green-400".to_string(),
314            WasmColor::Green500 => "text-green-500".to_string(),
315            WasmColor::Green600 => "text-green-600".to_string(),
316            WasmColor::Green700 => "text-green-700".to_string(),
317            WasmColor::Green800 => "text-green-800".to_string(),
318            WasmColor::Green900 => "text-green-900".to_string(),
319            WasmColor::White => "text-white".to_string(),
320            WasmColor::Black => "text-black".to_string(),
321            WasmColor::Transparent => "text-transparent".to_string(),
322        }
323    }
324    
325    /// Get the Tailwind class name for background color
326    pub fn bg_class(&self) -> String {
327        match self {
328            WasmColor::Gray50 => "bg-gray-50".to_string(),
329            WasmColor::Gray100 => "bg-gray-100".to_string(),
330            WasmColor::Gray200 => "bg-gray-200".to_string(),
331            WasmColor::Gray300 => "bg-gray-300".to_string(),
332            WasmColor::Gray400 => "bg-gray-400".to_string(),
333            WasmColor::Gray500 => "bg-gray-500".to_string(),
334            WasmColor::Gray600 => "bg-gray-600".to_string(),
335            WasmColor::Gray700 => "bg-gray-700".to_string(),
336            WasmColor::Gray800 => "bg-gray-800".to_string(),
337            WasmColor::Gray900 => "bg-gray-900".to_string(),
338            WasmColor::Blue50 => "bg-blue-50".to_string(),
339            WasmColor::Blue100 => "bg-blue-100".to_string(),
340            WasmColor::Blue200 => "bg-blue-200".to_string(),
341            WasmColor::Blue300 => "bg-blue-300".to_string(),
342            WasmColor::Blue400 => "bg-blue-400".to_string(),
343            WasmColor::Blue500 => "bg-blue-500".to_string(),
344            WasmColor::Blue600 => "bg-blue-600".to_string(),
345            WasmColor::Blue700 => "bg-blue-700".to_string(),
346            WasmColor::Blue800 => "bg-blue-800".to_string(),
347            WasmColor::Blue900 => "bg-blue-900".to_string(),
348            WasmColor::Red50 => "bg-red-50".to_string(),
349            WasmColor::Red100 => "bg-red-100".to_string(),
350            WasmColor::Red200 => "bg-red-200".to_string(),
351            WasmColor::Red300 => "bg-red-300".to_string(),
352            WasmColor::Red400 => "bg-red-400".to_string(),
353            WasmColor::Red500 => "bg-red-500".to_string(),
354            WasmColor::Red600 => "bg-red-600".to_string(),
355            WasmColor::Red700 => "bg-red-700".to_string(),
356            WasmColor::Red800 => "bg-red-800".to_string(),
357            WasmColor::Red900 => "bg-red-900".to_string(),
358            WasmColor::Green50 => "bg-green-50".to_string(),
359            WasmColor::Green100 => "bg-green-100".to_string(),
360            WasmColor::Green200 => "bg-green-200".to_string(),
361            WasmColor::Green300 => "bg-green-300".to_string(),
362            WasmColor::Green400 => "bg-green-400".to_string(),
363            WasmColor::Green500 => "bg-green-500".to_string(),
364            WasmColor::Green600 => "bg-green-600".to_string(),
365            WasmColor::Green700 => "bg-green-700".to_string(),
366            WasmColor::Green800 => "bg-green-800".to_string(),
367            WasmColor::Green900 => "bg-green-900".to_string(),
368            WasmColor::White => "bg-white".to_string(),
369            WasmColor::Black => "bg-black".to_string(),
370            WasmColor::Transparent => "bg-transparent".to_string(),
371        }
372    }
373}
374
375/// WASM-optimized utility functions
376pub mod utils {
377    use super::*;
378    
379    /// Validate a Tailwind class name
380    pub fn validate_class(class: &str) -> bool {
381        // Basic validation - check for common patterns
382        if class.is_empty() {
383            return false;
384        }
385        
386        // Check for valid characters
387        for ch in class.chars() {
388            if !ch.is_alphanumeric() && ch != '-' && ch != ':' && ch != '/' && ch != '[' && ch != ']' {
389                return false;
390            }
391        }
392        
393        true
394    }
395    
396    /// Parse responsive classes
397    pub fn parse_responsive_class(class: &str) -> Option<(Option<WasmBreakpoint>, String)> {
398        for breakpoint in [WasmBreakpoint::Sm, WasmBreakpoint::Md, WasmBreakpoint::Lg, WasmBreakpoint::Xl, WasmBreakpoint::Xl2] {
399            if class.starts_with(breakpoint.prefix()) {
400                let base_class = &class[breakpoint.prefix().len()..];
401                return Some((Some(breakpoint), base_class.to_string()));
402            }
403        }
404        
405        Some((None, class.to_string()))
406    }
407    
408    /// Generate responsive classes
409    pub fn generate_responsive_classes(base_class: &str, breakpoints: &[WasmBreakpoint]) -> Vec<String> {
410        let mut classes = vec![base_class.to_string()];
411        
412        for breakpoint in breakpoints {
413            classes.push(format!("{}{}", breakpoint.prefix(), base_class));
414        }
415        
416        classes
417    }
418}
419
420/// WASM-specific error types
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub enum WasmError {
423    InvalidClass(String),
424    ValidationError(String),
425    SerializationError(String),
426}
427
428impl fmt::Display for WasmError {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        match self {
431            WasmError::InvalidClass(msg) => write!(f, "Invalid class: {}", msg),
432            WasmError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
433            WasmError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
434        }
435    }
436}
437
438#[cfg(not(target_arch = "wasm32"))]
439impl std::error::Error for WasmError {}
440
441/// WASM-optimized theme system
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct WasmTheme {
444    pub name: String,
445    pub colors: Vec<WasmColor>,
446    pub spacing: Vec<WasmSpacing>,
447    pub breakpoints: Vec<WasmBreakpoint>,
448}
449
450impl WasmTheme {
451    /// Create a new WASM theme
452    pub fn new(name: String) -> Self {
453        Self {
454            name,
455            colors: vec![
456                WasmColor::White,
457                WasmColor::Black,
458                WasmColor::Gray500,
459                WasmColor::Blue500,
460                WasmColor::Red500,
461                WasmColor::Green500,
462            ],
463            spacing: vec![
464                WasmSpacing::P1,
465                WasmSpacing::P2,
466                WasmSpacing::P4,
467                WasmSpacing::P8,
468                WasmSpacing::P16,
469            ],
470            breakpoints: vec![
471                WasmBreakpoint::Sm,
472                WasmBreakpoint::Md,
473                WasmBreakpoint::Lg,
474                WasmBreakpoint::Xl,
475            ],
476        }
477    }
478    
479    /// Get the default theme
480    pub fn default() -> Self {
481        Self::new("default".to_string())
482    }
483}
484
485impl Default for WasmTheme {
486    fn default() -> Self {
487        Self::default()
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    
495    #[test]
496    fn test_wasm_class_builder() {
497        let mut builder = WasmClassBuilder::new();
498        builder.class("bg-blue-500");
499        builder.class("text-white");
500        builder.class("p-4");
501        
502        assert_eq!(builder.clone().build(), "bg-blue-500 text-white p-4");
503        assert_eq!(builder.len(), 3);
504        assert!(!builder.is_empty());
505    }
506    
507    #[test]
508    fn test_wasm_spacing() {
509        assert_eq!(WasmSpacing::P4.css_value(), "1rem");
510        assert_eq!(WasmSpacing::P4.padding_class(), "p-4");
511        assert_eq!(WasmSpacing::P4.margin_class(), "m-4");
512    }
513    
514    #[test]
515    fn test_wasm_color() {
516        assert_eq!(WasmColor::Blue500.text_class(), "text-blue-500");
517        assert_eq!(WasmColor::Blue500.bg_class(), "bg-blue-500");
518    }
519    
520    #[test]
521    fn test_wasm_breakpoint() {
522        assert_eq!(WasmBreakpoint::Md.media_query(), "(min-width: 768px)");
523        assert_eq!(WasmBreakpoint::Md.prefix(), "md:");
524    }
525    
526    #[test]
527    fn test_utils_validate_class() {
528        assert!(utils::validate_class("bg-blue-500"));
529        assert!(utils::validate_class("text-white"));
530        assert!(!utils::validate_class(""));
531        assert!(!utils::validate_class("invalid@class"));
532    }
533    
534    #[test]
535    fn test_utils_parse_responsive_class() {
536        let (bp, class) = utils::parse_responsive_class("md:bg-blue-500").unwrap();
537        assert_eq!(bp, Some(WasmBreakpoint::Md));
538        assert_eq!(class, "bg-blue-500");
539        
540        let (bp, class) = utils::parse_responsive_class("bg-blue-500").unwrap();
541        assert_eq!(bp, None);
542        assert_eq!(class, "bg-blue-500");
543    }
544    
545    #[test]
546    fn test_wasm_theme() {
547        let theme = WasmTheme::new("test".to_string());
548        assert_eq!(theme.name, "test");
549        assert!(!theme.colors.is_empty());
550        assert!(!theme.spacing.is_empty());
551        assert!(!theme.breakpoints.is_empty());
552    }
553}