isaribi/
styled.rs

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}