1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::Write as _;
5
6use crate::condition::Conditions;
7use crate::style::{Atom, Prop, Style};
8
9pub const LAYER: &str = "hypersig-atoms";
11
12pub const TOGGLED_LAYER: &str = "hypersig-toggled";
15
16pub const TOKEN_LAYER: &str = "hypersig-tokens";
18
19pub const BASE_LAYER: &str = "hypersig-base";
22
23pub(crate) const ORDER: &str =
28 "@layer hypersig-tokens,hypersig-base,hypersig-atoms,hypersig-toggled;";
29
30#[derive(Debug, Clone, Default)]
54pub struct Sheet {
55 rules: BTreeSet<Key>,
56 keyframes: BTreeMap<&'static str, String>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
64struct Key {
65 toggled: bool,
66 conditions: Conditions,
67 rank: u32,
68 property: Prop,
69 value: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Bucket {
81 sort_key: String,
82 css: String,
83}
84
85impl Bucket {
86 #[must_use]
91 pub fn sort_key(&self) -> &str {
92 &self.sort_key
93 }
94
95 #[must_use]
98 pub fn css(&self) -> &str {
99 &self.css
100 }
101}
102
103impl Sheet {
104 #[must_use]
106 pub fn new() -> Self {
107 Self::default()
108 }
109
110 fn insert(&mut self, style: &Style, toggled: bool) {
111 for keyframes in style.keyframes_used() {
112 self.keyframes
113 .entry(keyframes.name())
114 .or_insert_with(|| keyframes.to_css());
115 }
116 for atom in style.atoms() {
117 self.rules.insert(Key {
118 toggled,
119 conditions: atom.conditions.clone(),
120 rank: atom.property.rank(),
121 property: atom.property,
122 value: atom.value.clone(),
123 });
124 }
125 }
126
127 pub fn add(&mut self, style: &Style) {
129 self.insert(style, false);
130 }
131
132 pub fn add_toggled(&mut self, style: &Style) {
134 self.insert(style, true);
135 }
136
137 pub fn extend(&mut self, other: &Self) {
139 self.rules.extend(other.rules.iter().cloned());
140 for (name, css) in &other.keyframes {
141 self.keyframes.entry(name).or_insert_with(|| css.clone());
142 }
143 }
144
145 #[must_use]
147 pub fn is_empty(&self) -> bool {
148 self.rules.is_empty() && self.keyframes.is_empty()
149 }
150
151 pub fn buckets(&self) -> impl Iterator<Item = Bucket> + '_ {
154 let mut rules = self.rules.iter().peekable();
155 let atoms = std::iter::from_fn(move || {
156 let first = rules.next()?;
157 let layer = if first.toggled { TOGGLED_LAYER } else { LAYER };
158 let key = first.bucket();
159 let mut css = format!("{ORDER}@layer {layer}.{key}{{");
160 let open = first.conditions.open(&mut css);
161 first.write_rule(&mut css);
162 while let Some(next) = rules.next_if(|k| k.same_bucket(first)) {
163 next.write_rule(&mut css);
164 }
165 Conditions::close(open, &mut css);
166 css.push('}');
167 Some(Bucket {
168 sort_key: format!("{} {key}", u8::from(first.toggled)),
169 css,
170 })
171 });
172 atoms.chain(self.keyframes.iter().map(|(name, css)| Bucket {
173 sort_key: format!("2 {name}"),
174 css: format!("{ORDER}{css}"),
175 }))
176 }
177
178 pub fn write_css(&self, out: &mut String) {
180 for bucket in self.buckets() {
181 out.push_str(&bucket.css);
182 }
183 }
184
185 #[must_use]
187 pub fn to_css(&self) -> String {
188 let mut out = String::new();
189 self.write_css(&mut out);
190 out
191 }
192}
193
194impl Key {
195 fn same_bucket(&self, other: &Self) -> bool {
196 self.toggled == other.toggled
197 && self.conditions == other.conditions
198 && self.rank == other.rank
199 }
200
201 fn bucket(&self) -> String {
207 let mut key = String::from("k");
208 for &m in &self.conditions.media {
209 let _ = write!(key, "{:02}", m as u8);
210 }
211 key.push('-');
212 for &c in &self.conditions.container {
213 let _ = write!(key, "{:02}", c as u8);
214 }
215 key.push('-');
216 for &(rank, p) in &self.conditions.group {
217 let _ = write!(key, "{rank}{:03}", p as u16);
218 }
219 key.push('-');
220 for &(rank, p) in &self.conditions.peer {
221 let _ = write!(key, "{rank}{:03}", p as u16);
222 }
223 key.push('-');
224 for &(rank, p) in &self.conditions.pseudo {
225 let _ = write!(key, "{rank}{:03}", p as u16);
226 }
227 key.push('-');
228 match self.conditions.element {
229 Some(e) => {
230 let _ = write!(key, "1{:03}", e as u16);
231 }
232 None => key.push('0'),
233 }
234 let _ = write!(key, "{}", self.rank);
235 key
236 }
237
238 fn write_rule(&self, out: &mut String) {
239 let atom = Atom {
240 conditions: self.conditions.clone(),
241 property: self.property,
242 value: self.value.clone(),
243 };
244 self.conditions.write_selector(out, |out| {
245 out.push('.');
246 escape_class(&atom.class_name(self.toggled), out);
247 });
248 out.push('{');
249 self.property.write_name(out);
250 let _ = write!(out, ":{}}}", self.value);
251 }
252}
253
254fn escape_class(name: &str, out: &mut String) {
258 for (i, c) in name.chars().enumerate() {
259 if i == 0 && c.is_ascii_digit() {
260 let _ = write!(out, "\\{:x} ", u32::from(c));
261 } else if c.is_ascii_alphanumeric() || c == '-' || c == '_' || !c.is_ascii() {
262 out.push(c);
263 } else {
264 out.push('\\');
265 out.push(c);
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::{Sheet, escape_class};
273 use crate::value::{Color, Display, pct, px, quoted, raw, rem};
274 use crate::{Condition, Container, Media, PseudoClass, PseudoElement, Style};
275
276 fn css(sheet: &Sheet) -> Vec<String> {
277 sheet.buckets().map(|b| b.css().to_owned()).collect()
278 }
279
280 #[test]
281 fn escapes_a_leading_digit_and_punctuation() {
282 let mut out = String::new();
283 escape_class("2xl:width_50%", &mut out);
284 assert_eq!(out, "\\32 xl\\:width_50\\%");
285 }
286
287 #[test]
288 fn writes_breakpoints_ascending_after_the_base_rules() {
289 let mut sheet = Sheet::new();
290 sheet.add(
291 &Style::new()
292 .on(Media::Lg, Style::new().width(pct(25)))
293 .on(Media::Md, Style::new().width(pct(50)))
294 .width(pct(100)),
295 );
296 let css = css(&sheet);
297 assert_eq!(css.len(), 3);
298 assert!(css[0].ends_with("{.width_100\\%{width:100%}}"));
299 assert!(css[1].ends_with("{@media (min-width: 48rem){.md\\:width_50\\%{width:50%}}}"));
300 assert!(css[2].ends_with("{@media (min-width: 64rem){.lg\\:width_25\\%{width:25%}}}"));
301 }
302
303 #[test]
304 fn bucket_keys_sort_as_the_buckets_do() {
305 let mut sheet = Sheet::new();
306 sheet.add(
307 &Style::new()
308 .on(
309 Media::Md,
310 Style::new().on(PseudoClass::Hover, Style::new().color(Color::RED)),
311 )
312 .on(Media::Md, Style::new().color(Color::BLUE))
313 .on(PseudoClass::Active, Style::new().color(Color::GREEN))
314 .padding_top(px(0))
315 .padding(px(1)),
316 );
317 sheet.add_toggled(&Style::new().color(Color::GRAY));
318 let keys: Vec<String> = sheet.buckets().map(|b| b.sort_key().to_owned()).collect();
319 let mut sorted = keys.clone();
320 sorted.sort();
321 assert_eq!(keys, sorted);
322 assert_eq!(keys.len(), 6);
323 }
324
325 #[test]
326 fn writes_container_queries_inside_media_queries() {
327 let mut sheet = Sheet::new();
328 sheet.add(
329 &Style::new().width(pct(100)).on(
330 Container::Xl3,
331 Style::new()
332 .width(pct(50))
333 .on(Media::Print, Style::new().width(pct(25))),
334 ),
335 );
336 let css = css(&sheet);
337 assert_eq!(css.len(), 3);
338 assert!(
339 css[1].ends_with(r"{@container (min-width: 48rem){.\@3xl\:width_50\%{width:50%}}}")
340 );
341 assert!(css[2].ends_with(
342 r"{@media print{@container (min-width: 48rem){.print\:\@3xl\:width_25\%{width:25%}}}}"
343 ));
344 }
345
346 #[test]
347 fn writes_group_and_peer_conditions_before_the_class() {
348 let style = Style::new()
349 .on(
350 Condition::Group(PseudoClass::Hover),
351 Style::new().display(Display::Block),
352 )
353 .on(
354 Condition::Peer(PseudoClass::Checked),
355 Style::new().color(Color::RED),
356 );
357 assert_eq!(
358 style.class_names().collect::<Vec<_>>(),
359 ["group-hover:display_block", "peer-checked:color_red"]
360 );
361 let mut sheet = Sheet::new();
362 sheet.add(&style);
363 let css = sheet.to_css();
364 assert!(css.contains("{.group:hover .group-hover\\:display_block{display:block}}"));
365 assert!(css.contains("{.peer:checked ~ .peer-checked\\:color_red{color:red}}"));
366 assert_eq!(
367 Style::new()
368 .group()
369 .peer()
370 .class_names()
371 .collect::<Vec<_>>(),
372 ["group", "peer"]
373 );
374 }
375
376 #[test]
377 fn writes_shorthands_before_longhands() {
378 let mut sheet = Sheet::new();
379 sheet.add(&Style::new().padding_top(px(0)).padding(rem(1)));
380 let css = css(&sheet);
381 assert!(css[0].ends_with("{.padding_1rem{padding:1rem}}"));
382 assert!(css[1].ends_with("{.padding-top_0{padding-top:0}}"));
383 }
384
385 #[test]
386 fn writes_active_after_hover() {
387 let mut sheet = Sheet::new();
388 sheet.add(
389 &Style::new()
390 .on(PseudoClass::Active, Style::new().color(Color::RED))
391 .on(PseudoClass::Hover, Style::new().color(Color::BLUE)),
392 );
393 let css = css(&sheet);
394 assert!(css[0].contains(":hover{color:blue}") && css[1].contains(":active{color:red}"));
395 }
396
397 #[test]
398 fn toggled_classes_have_their_own_names_and_layer() {
399 let style = Style::new().color(Color::RED);
400 assert_eq!(
401 style.toggled_class_names().collect::<Vec<_>>(),
402 ["when:color_red"]
403 );
404 let mut sheet = Sheet::new();
405 sheet.add_toggled(&style);
406 assert_eq!(
407 sheet.to_css(),
408 "@layer hypersig-tokens,hypersig-base,hypersig-atoms,hypersig-toggled;\
409 @layer hypersig-toggled.k-----00{.when\\:color_red{color:red}}"
410 );
411 }
412
413 #[test]
414 fn hashes_values_with_spaces_and_writes_pseudo_elements_last() {
415 let style = Style::new()
416 .display(Display::Flex)
417 .margin(raw("0 auto"))
418 .on(
419 PseudoClass::Hover,
420 Style::new().pseudo(PseudoElement::After, Style::new().content(quoted("×"))),
421 );
422 let names: Vec<String> = style.class_names().collect();
423 assert_eq!(names[0], "display_flex");
424 assert!(names[1].starts_with("margin_h") && names[1].len() == "margin_h".len() + 16);
425 let mut sheet = Sheet::new();
426 sheet.add(&style);
427 assert!(sheet.to_css().contains(":hover::after{content:\"×\"}"));
428 }
429
430 #[test]
431 fn escapes_the_parentheses_of_the_select_picker() {
432 let style =
433 Style::new().pseudo(PseudoElement::PickerSelect, Style::new().color(Color::RED));
434 let mut sheet = Sheet::new();
435 sheet.add(&style);
436 assert!(
437 sheet
438 .to_css()
439 .contains(".picker\\(select\\)\\:color_red::picker(select){color:red}")
440 );
441 }
442
443 #[test]
444 fn a_later_value_replaces_an_earlier_one() {
445 let style = Style::new()
446 .color(Color::RED)
447 .merge(Style::new().color(Color::BLUE));
448 assert_eq!(style.class_names().collect::<Vec<_>>(), ["color_blue"]);
449 }
450}