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