cabin_tailwind/
registry.rs1use 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 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 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 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 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 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 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 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 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}