1use regex::Regex;
2use std::any;
3use std::cell::RefCell;
4use std::collections::hash_map::DefaultHasher;
5use std::collections::HashSet;
6use std::hash::Hasher;
7use wasm_bindgen::{prelude::*, JsCast};
8
9thread_local! {
10 static STYLED: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
11 static SHEET: RefCell<Option<web_sys::CssStyleSheet>> = RefCell::new(None);
12 static CLASS_SELECTER: Regex = Regex::new(r"\.([a-zA-Z][a-zA-Z0-9\-_]*)").unwrap();
13}
14
15fn hash_of_type<C>() -> u64 {
16 let mut hasher = DefaultHasher::new();
17 hasher.write(any::type_name::<C>().as_bytes());
18 hasher.finish()
19}
20
21fn styled_class_prefix<C>() -> String {
22 let mut hasher = DefaultHasher::new();
23 hasher.write(any::type_name::<C>().as_bytes());
24 format!("{:X}", hasher.finish())
25}
26
27fn styled_class<C>(class_name: &str) -> String {
28 format!("_{}__{}", styled_class_prefix::<C>(), class_name)
29}
30
31pub trait Styled: Sized {
32 fn style() -> Style;
33 fn styled<T>(node: T) -> T {
34 STYLED.with(|styled| {
35 let component_id = hash_of_type::<Self>();
36 if styled.borrow().get(&component_id).is_none() {
37 let style = Self::style();
38 style.write::<Self>();
39 styled.borrow_mut().insert(component_id);
40 }
41 });
42
43 node
44 }
45 fn class(class_name: &str) -> String {
46 styled_class::<Self>(class_name)
47 }
48}
49
50#[derive(Clone, PartialEq)]
51enum Rule {
52 Selecter(String, Vec<(String, String)>),
53 Keyframes(String, Style),
54 Media(String, Style),
55}
56
57#[derive(Clone)]
58pub struct Style {
59 rules: Vec<Rule>,
60}
61
62impl Style {
63 pub fn new() -> Self {
64 Self { rules: vec![] }
65 }
66
67 pub fn add(
68 &mut self,
69 selector: impl Into<String>,
70 property: impl Into<String>,
71 value: impl Into<String>,
72 ) {
73 let selecter = selector.into();
74 let property = property.into();
75 let value = value.into();
76
77 for rule in self.rules.iter_mut() {
78 match rule {
79 Rule::Selecter(s, defs) if *s == selecter => {
80 for (p, v) in defs.iter_mut() {
81 if *p == property {
82 *v = value;
83 return;
84 }
85 }
86 defs.push((property, value));
87 return;
88 }
89 _ => {}
90 }
91 }
92 self.rules
93 .push(Rule::Selecter(selecter, vec![(property, value)]));
94 }
95
96 pub fn add_keyframes(&mut self, name: impl Into<String>, style: Style) {
97 let name = name.into();
98 self.rules.push(Rule::Keyframes(name, style));
99 }
100
101 pub fn add_media(&mut self, query: impl Into<String>, style: Style) {
102 let query = query.into();
103 self.rules.push(Rule::Media(query, style));
104 }
105
106 pub fn append(&mut self, other: &Self) {
107 for rule in &other.rules {
108 match rule {
109 Rule::Selecter(s, defs) => {
110 for (p, v) in defs {
111 self.add(s, p, v);
112 }
113 }
114 Rule::Keyframes(n, s) => {
115 self.add_keyframes(n, s.clone());
116 }
117 Rule::Media(q, s) => {
118 self.add_media(q, s.clone());
119 }
120 }
121 }
122 }
123
124 fn rules<C>(&self) -> Vec<String> {
125 let mut str_rules = vec![];
126
127 for rule in self.rules.iter() {
128 let str_rule = match rule {
129 Rule::Selecter(selecter, defs) => {
130 let mut str_rule = String::new();
131 let str_selecter = CLASS_SELECTER.with(|class_selecter| {
132 class_selecter.replace_all(
133 selecter,
134 format!("._{}__$1", styled_class_prefix::<C>()).as_str(),
135 )
136 });
137 str_rule += &str_selecter;
138 str_rule += "{";
139 for (property, value) in defs {
140 str_rule += format!("{}:{};", property, value).as_str();
141 }
142 str_rule += "}";
143 str_rule
144 }
145
146 Rule::Keyframes(name, keyframes) => {
147 let mut str_rule = String::from("@keyframes ");
148 str_rule += name;
149 str_rule += "{";
150 for child_rule in &keyframes.rules::<C>() {
151 str_rule += child_rule;
152 }
153 str_rule += "}";
154
155 str_rule
156 }
157
158 Rule::Media(query, media_style) => {
159 let mut str_rule = String::from("@media ");
160 str_rule += query;
161 str_rule += "{";
162 for child_rule in &media_style.rules::<C>() {
163 str_rule += child_rule;
164 }
165 str_rule += "}";
166
167 str_rule
168 }
169 };
170
171 str_rules.push(str_rule);
172 }
173
174 str_rules
175 }
176
177 fn write<C>(&self) {
178 Self::add_style_element();
179
180 for rule in &self.rules::<C>() {
181 SHEET.with(|sheet| {
182 if let Some(sheet) = sheet.borrow().as_ref() {
183 if let Err(err) = sheet
184 .insert_rule_with_index(rule.as_str(), sheet.css_rules().unwrap().length())
185 {
186 web_sys::console::log_1(&JsValue::from(err));
187 }
188 }
189 });
190 }
191 }
192
193 fn add_style_element() {
194 SHEET.with(|sheet| {
195 if sheet.borrow().is_none() {
196 let style_element = web_sys::window()
197 .unwrap()
198 .document()
199 .unwrap()
200 .create_element("style")
201 .unwrap()
202 .dyn_into::<web_sys::HtmlStyleElement>()
203 .unwrap();
204
205 let head = web_sys::window()
206 .unwrap()
207 .document()
208 .unwrap()
209 .get_elements_by_tag_name("head")
210 .item(0)
211 .unwrap();
212
213 let _ = head.append_child(&style_element);
214
215 *sheet.borrow_mut() = Some(
216 style_element
217 .sheet()
218 .unwrap()
219 .dyn_into::<web_sys::CssStyleSheet>()
220 .unwrap(),
221 );
222 }
223 });
224 }
225}
226
227macro_rules! return_if {
228 ($x:ident = $y:expr; $z: expr) => {{
229 let $x = $y;
230 if $z {
231 return $x;
232 }
233 }};
234}
235
236impl std::fmt::Debug for Style {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 for rule in &self.rules {
239 match rule {
240 Rule::Selecter(selecter, defs) => {
241 return_if!(x = write!(f, "{} {}\n", selecter, "{"); x.is_err());
242 for (property, value) in defs {
243 return_if!(x = write!(f, " {}: {};\n", property, value); x.is_err());
244 }
245 return_if!(x = write!(f, "{}\n", "}"); x.is_err());
246 }
247
248 Rule::Keyframes(name, keyframes) => {
249 return_if!(x = write!(f, "@keyframes {} {}\n", name, "{"); x.is_err());
250
251 for a_line in format!("{:?}", keyframes).split("\n") {
252 if a_line != "" {
253 return_if!(x = write!(f, " {}\n", a_line); x.is_err());
254 }
255 }
256
257 return_if!(x = write!(f, "{}\n", "}"); x.is_err());
258 }
259
260 Rule::Media(query, style) => {
261 return_if!(x = write!(f, "@media {} {}\n", query, "{"); x.is_err());
262
263 for a_line in format!("{:?}", style).split("\n") {
264 if a_line != "" {
265 return_if!(x = write!(f, " {}\n", a_line); x.is_err());
266 }
267 }
268
269 return_if!(x = write!(f, "{}\n", "}"); x.is_err());
270 }
271 }
272 }
273
274 write!(f, "")
275 }
276}
277
278impl PartialEq for Style {
279 fn eq(&self, other: &Self) -> bool {
280 self.rules.eq(&other.rules)
281 }
282}
283
284#[macro_export]
285macro_rules! style {
286
287 {
288 instance: $inst:ident;
289 @charset $import:expr;
290 $($others:tt)*
291 } => {{
292 $inst.append(&($import));
293
294 style! {
295 instance: $inst;
296 $($others)*
297 }
298 }};
299
300 {
301 instance: $inst:ident;
302 @extends $extends:expr;
303 $($others:tt)*
304 } => {{
305 $inst.append(&($extends));
306
307 style! {
308 instance: $inst;
309 $($others)*
310 }
311 }};
312
313 {
314 instance: $inst:ident;
315 @keyframes $name:tt {$($keyframes:tt)*}
316 $($others:tt)*
317 } => {{
318 $inst.add_keyframes($name, style!{$($keyframes)*});
319
320 style! {
321 instance: $inst;
322 $($others)*
323 }
324 }};
325
326 {
327 instance: $inst:ident;
328 @media $query:tt {$($media_style:tt)*}
329 $($others:tt)*
330 } => {{
331 $inst.add_media($query, style!{$($media_style)*});
332
333 style! {
334 instance: $inst;
335 $($others)*
336 }
337 }};
338
339 {
340 instance: $inst:ident;
341 $selector:literal {$(
342 $property:tt : $value:expr;
343 )*}
344 $($others:tt)*
345 } => {{
346 $(
347 $inst.add(format!("{}", $selector), format!("{}", $property), format!("{}", $value));
348 )*
349
350 style! {
351 instance: $inst;
352 $($others)*
353 }
354 }};
355
356 {
357 instance: $inst:ident;
358 } => {{}};
359
360 {
361 $($others:tt)*
362 } => {{
363 #[allow(unused_mut)]
364 let mut instance = Style::new();
365
366 style! {
367 instance: instance;
368 $($others)*
369 };
370
371 instance
372 }};
373}
374
375#[cfg(test)]
376mod tests {
377 use super::Rule;
378 use super::Style;
379
380 #[test]
381 fn it_works() {
382 assert!(true);
383 }
384
385 #[test]
386 fn debug_style() {
387 let style = Style {
388 rules: vec![
389 Rule::Selecter(
390 String::from("foo"),
391 vec![
392 (String::from("width"), String::from("100px")),
393 (String::from("height"), String::from("100px")),
394 ],
395 ),
396 Rule::Selecter(
397 String::from("bar"),
398 vec![
399 (String::from("width"), String::from("100px")),
400 (String::from("height"), String::from("100px")),
401 ],
402 ),
403 ],
404 };
405
406 let style_str = concat!(
407 "foo {\n",
408 " width: 100px;\n",
409 " height: 100px;\n",
410 "}\n",
411 "bar {\n",
412 " width: 100px;\n",
413 " height: 100px;\n",
414 "}\n",
415 );
416
417 assert_eq!(format!("{:?}", style), style_str);
418 }
419
420 #[test]
421 fn debug_style_with_media() {
422 let media_style = Style {
423 rules: vec![
424 Rule::Selecter(
425 String::from("foo"),
426 vec![
427 (String::from("width"), String::from("100px")),
428 (String::from("height"), String::from("100px")),
429 ],
430 ),
431 Rule::Selecter(
432 String::from("bar"),
433 vec![
434 (String::from("width"), String::from("100px")),
435 (String::from("height"), String::from("100px")),
436 ],
437 ),
438 ],
439 };
440 let style = Style {
441 rules: vec![
442 Rule::Selecter(
443 String::from("foo"),
444 vec![
445 (String::from("width"), String::from("100px")),
446 (String::from("height"), String::from("100px")),
447 ],
448 ),
449 Rule::Selecter(
450 String::from("bar"),
451 vec![
452 (String::from("width"), String::from("100px")),
453 (String::from("height"), String::from("100px")),
454 ],
455 ),
456 Rule::Media(String::from("query"), media_style),
457 ],
458 };
459
460 let style_str = concat!(
461 "foo {\n",
462 " width: 100px;\n",
463 " height: 100px;\n",
464 "}\n",
465 "bar {\n",
466 " width: 100px;\n",
467 " height: 100px;\n",
468 "}\n",
469 "@media query {\n",
470 " foo {\n",
471 " width: 100px;\n",
472 " height: 100px;\n",
473 " }\n",
474 " bar {\n",
475 " width: 100px;\n",
476 " height: 100px;\n",
477 " }\n",
478 "}\n",
479 );
480
481 assert_eq!(format!("{:?}", style), style_str);
482 }
483
484 #[test]
485 fn gen_style_by_manual() {
486 let style_a = Style {
487 rules: vec![
488 Rule::Selecter(
489 String::from("foo"),
490 vec![
491 (String::from("width"), String::from("100px")),
492 (String::from("height"), String::from("100px")),
493 ],
494 ),
495 Rule::Selecter(
496 String::from("bar"),
497 vec![
498 (String::from("width"), String::from("100px")),
499 (String::from("height"), String::from("100px")),
500 ],
501 ),
502 ],
503 };
504
505 let mut style_b = Style::new();
506 style_b.add("foo", "width", "100px");
507 style_b.add("foo", "height", "100px");
508 style_b.add("bar", "width", "100px");
509 style_b.add("bar", "height", "100px");
510
511 assert_eq!(style_a, style_b);
512 }
513
514 #[test]
515 fn gen_style_with_media_by_manual() {
516 let media_style_a = Style {
517 rules: vec![
518 Rule::Selecter(
519 String::from("foo"),
520 vec![
521 (String::from("width"), String::from("100px")),
522 (String::from("height"), String::from("100px")),
523 ],
524 ),
525 Rule::Selecter(
526 String::from("bar"),
527 vec![
528 (String::from("width"), String::from("100px")),
529 (String::from("height"), String::from("100px")),
530 ],
531 ),
532 ],
533 };
534 let style_a = Style {
535 rules: vec![
536 Rule::Selecter(
537 String::from("foo"),
538 vec![
539 (String::from("width"), String::from("100px")),
540 (String::from("height"), String::from("100px")),
541 ],
542 ),
543 Rule::Selecter(
544 String::from("bar"),
545 vec![
546 (String::from("width"), String::from("100px")),
547 (String::from("height"), String::from("100px")),
548 ],
549 ),
550 Rule::Media(String::from("query"), media_style_a),
551 ],
552 };
553
554 let mut media_style_b = Style::new();
555 media_style_b.add("foo", "width", "100px");
556 media_style_b.add("foo", "height", "100px");
557 media_style_b.add("bar", "width", "100px");
558 media_style_b.add("bar", "height", "100px");
559 let mut style_b = Style::new();
560 style_b.add("foo", "width", "100px");
561 style_b.add("foo", "height", "100px");
562 style_b.add("bar", "width", "100px");
563 style_b.add("bar", "height", "100px");
564 style_b.add_media("query", media_style_b);
565
566 assert_eq!(style_a, style_b);
567 }
568
569 #[test]
570 fn gen_style_by_macro() {
571 let mut style_a = Style::new();
572 style_a.add("foo", "width", "100px");
573 style_a.add("foo", "height", "100px");
574 style_a.add("bar", "width", "100px");
575 style_a.add("bar", "height", "100px");
576
577 let style_b = style! {
578 "foo" {
579 "width": "100px";
580 "height": "100px";
581 }
582
583 "bar" {
584 "width": "100px";
585 "height": "100px";
586 }
587 };
588
589 assert_eq!(style_a, style_b);
590 }
591
592 #[test]
593 fn gen_style_with_media_by_macro() {
594 let mut media_style_a = Style::new();
595 media_style_a.add("foo", "width", "100px");
596 media_style_a.add("foo", "height", "100px");
597 media_style_a.add("bar", "width", "100px");
598 media_style_a.add("bar", "height", "100px");
599 let mut style_a = Style::new();
600 style_a.add("foo", "width", "100px");
601 style_a.add("foo", "height", "100px");
602 style_a.add("bar", "width", "100px");
603 style_a.add("bar", "height", "100px");
604 style_a.add_media("query", media_style_a);
605
606 let style_b = style! {
607 "foo" {
608 "width": "100px";
609 "height": "100px";
610 }
611
612 "bar" {
613 "width": "100px";
614 "height": "100px";
615 }
616
617 @media "query" {
618 "foo" {
619 "width": "100px";
620 "height": "100px";
621 }
622
623 "bar" {
624 "width": "100px";
625 "height": "100px";
626 }
627 }
628 };
629
630 assert_eq!(style_a, style_b);
631 }
632}