cabin_tailwind/
registry.rs

1use std::borrow::Cow;
2use std::collections::hash_map::DefaultHasher;
3use std::collections::{HashMap, HashSet};
4use std::fmt::{self, Write};
5use std::hash::Hasher;
6
7use once_cell::race::OnceBox;
8use twox_hash::XxHash32;
9
10use super::Utility;
11
12#[linkme::distributed_slice]
13pub static STYLES: [fn(&mut StyleRegistry)] = [..];
14
15static REGISTRY: OnceBox<StyleRegistry> = OnceBox::new();
16
17pub struct StyleRegistry {
18    out: String,
19    hashes: HashSet<u32>,
20}
21
22impl StyleRegistry {
23    pub fn global() -> &'static Self {
24        REGISTRY.get_or_init(|| {
25            let mut registry = Self {
26                out: Default::default(),
27                hashes: Default::default(),
28            };
29
30            registry.out.push_str(include_str!("./base.css"));
31
32            #[cfg(feature = "preflight")]
33            registry
34                .out
35                .push_str(include_str!("./preflight/preflight-v3.2.4.css"));
36
37            #[cfg(feature = "forms")]
38            registry
39                .out
40                .push_str(include_str!("./forms/forms-v0.5.3.css"));
41
42            for f in STYLES {
43                (f)(&mut registry);
44            }
45            Box::new(registry)
46        })
47    }
48
49    pub fn add(&mut self, styles: &[&dyn Utility]) -> String {
50        let mut sorted = styles
51            .iter()
52            .map(|s| (hash_style(*s), *s))
53            .collect::<Vec<_>>();
54        sorted.sort_by_key(|(hash, _)| *hash);
55
56        let grouped = sorted.into_iter().map(|(_, s)| s).fold(
57            HashMap::<_, Vec<_>>::new(),
58            |mut grouped, style| {
59                let mut hasher = DefaultHasher::new();
60                style.hash_modifier(&mut hasher);
61                let hash = hasher.finish();
62                grouped.entry(hash).or_default().push(style);
63                grouped
64            },
65        );
66        let mut grouped = grouped
67            .into_values()
68            .map(|styles| (styles.iter().map(|s| s.order()).max().unwrap_or(0), styles))
69            .collect::<Vec<_>>();
70        grouped.sort_by_key(|(order, _)| *order);
71
72        // As everything is written to a string, all unwraps below are fine.
73        let mut all_names = String::with_capacity(8);
74        for (_, mut styles) in grouped {
75            styles.sort_by_key(|s| s.order());
76
77            let pos = self.out.len();
78
79            writeln!(&mut self.out, "@keyframes ").unwrap();
80            let animation_name_offset1 = self.out.len();
81            write!(&mut self.out, "          {{").unwrap();
82            writeln!(&mut self.out, "  from {{").unwrap();
83            let before_animate_from = self.out.len();
84            for style in &styles {
85                style.write_animate_from(&mut self.out).unwrap();
86            }
87            let has_animate_from = self.out.len() > before_animate_from;
88            writeln!(&mut self.out, "  }}").unwrap();
89            writeln!(&mut self.out, "  to {{").unwrap();
90            let before_animate_to = self.out.len();
91            for style in &styles {
92                style.write_animate_to(&mut self.out).unwrap();
93            }
94            let has_animate_to = self.out.len() > before_animate_to;
95            writeln!(&mut self.out, "  }}").unwrap();
96            writeln!(&mut self.out, "}}").unwrap();
97
98            let has_animation = has_animate_from || has_animate_to;
99            if !has_animation {
100                self.out.truncate(pos);
101            }
102
103            // already grouped by variants, so just writing it once (from the first), is enough
104            if let Some(style) = styles.get(0) {
105                style.selector_prefix(&mut self.out).unwrap();
106            }
107            let class_name_offset = self.out.len();
108            write!(&mut self.out, "          ").unwrap();
109            // already grouped by variants, so just writing it once (from the first), is enough
110            if let Some(style) = styles.get(0) {
111                style.selector_suffix(&mut self.out).unwrap();
112            }
113            writeln!(&mut self.out, " {{").unwrap();
114            let mut animation_name_offset2 = 0;
115            if has_animation {
116                // TODO: make easing function, delay, duration, etc. customizable
117                write!(&mut self.out, "animation: 250ms ease-in-out 2 alternate ").unwrap();
118                animation_name_offset2 = self.out.len();
119                writeln!(&mut self.out, "         ;").unwrap();
120            }
121            for style in &styles {
122                style.declarations(&mut self.out).unwrap();
123            }
124            write!(&mut self.out, "}}").unwrap();
125            if let Some(style) = styles.get(0) {
126                style.suffix(&mut self.out).unwrap();
127            }
128            writeln!(&mut self.out).unwrap();
129
130            let mut hasher = XxHash32::default();
131            hasher.write(self.out[pos..].as_bytes());
132            let hash = hasher.finish() as u32;
133
134            // write actual class name, prepend `_` as it class names must not start with a number
135            let name = styles
136                .get(0)
137                .and_then(|s| s.override_class_name().map(Cow::Borrowed))
138                .unwrap_or_else(|| Cow::Owned(format!("_{hash:x}")));
139
140            if !self.hashes.insert(hash) {
141                // already known, remove just written stuff from output
142                self.out.truncate(pos);
143            } else {
144                let offset = class_name_offset + 9 - name.len();
145                self.out.replace_range(offset..offset + 1, ".");
146                self.out
147                    .replace_range(offset + 1..offset + 1 + name.len(), &name);
148
149                if has_animation {
150                    let offset = animation_name_offset1 + 9 - name.len();
151                    self.out.replace_range(offset..offset + name.len(), &name);
152                    let offset = animation_name_offset2 + 9 - name.len();
153                    self.out.replace_range(offset..offset + name.len(), &name);
154                }
155            }
156
157            if !all_names.is_empty() {
158                all_names.push(' ');
159            }
160            all_names.push_str(&name);
161        }
162
163        all_names
164    }
165
166    pub fn style_sheet(&self) -> &str {
167        &self.out
168    }
169}
170
171fn hash_style(style: &dyn Utility) -> u64 {
172    struct HashWriter(DefaultHasher);
173
174    impl fmt::Write for HashWriter {
175        fn write_str(&mut self, s: &str) -> fmt::Result {
176            self.0.write(s.as_bytes());
177            Ok(())
178        }
179    }
180
181    let mut writer = HashWriter(DefaultHasher::default());
182    style.declarations(&mut writer).ok();
183    style.hash_modifier(&mut writer.0);
184    writer.0.finish()
185}
186
187#[test]
188fn test_deduplication() {
189    // Generate same class name if styles are the same just in a different order.
190
191    use crate::utilities::{p, BLOCK};
192
193    let mut r = StyleRegistry {
194        out: Default::default(),
195        hashes: Default::default(),
196    };
197    let a = r.add(&[&BLOCK, &p(4)]);
198    let b = r.add(&[&p(4), &BLOCK]);
199    assert_eq!(a, b);
200    insta::assert_snapshot!(r.out);
201}
202
203#[test]
204fn test_order() {
205    // Test order of @media statements
206
207    use super::Responsive;
208    use crate::utilities::BLOCK;
209
210    let mut r = StyleRegistry {
211        out: Default::default(),
212        hashes: Default::default(),
213    };
214    r.add(&[
215        &BLOCK.sm().max_md(),
216        &BLOCK.md(),
217        &BLOCK.max_sm(),
218        &BLOCK.max_md(),
219        &BLOCK.sm(),
220    ]);
221    insta::assert_snapshot!(r.out);
222}