1use std::fmt::Write as _;
4
5use crate::Property;
6use crate::condition::Conditions;
7use crate::sheet::{BASE_LAYER, ORDER};
8use crate::style::Style;
9use crate::value::{
10 BoxSizing, Color, CssWideKeyword, Display, FontFamily, LineHeight, Quoted, Url, pct, px, quoted,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum Selector {
16 Root,
18 Everything,
20 Element(Element),
22}
23
24impl Selector {
25 fn write(self, out: &mut String, suffix: &str) {
26 match self {
27 Self::Root => {
28 let _ = write!(out, ":root{suffix}");
29 }
30 Self::Everything => {
31 let _ = write!(out, "*{suffix},::before{suffix},::after{suffix}");
32 }
33 Self::Element(e) => {
34 let _ = write!(out, "{}{suffix}", e.name());
35 }
36 }
37 }
38}
39
40impl From<Element> for Selector {
41 fn from(e: Element) -> Self {
42 Self::Element(e)
43 }
44}
45
46macro_rules! elements {
47 ($($variant:ident => $tag:literal),* $(,)?) => {
48 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
50 #[non_exhaustive]
51 pub enum Element {
52 $(
53 #[doc = concat!("`<", $tag, ">`.")]
54 $variant,
55 )*
56 }
57
58 impl Element {
59 #[must_use]
61 pub fn name(self) -> &'static str {
62 match self {
63 $(Self::$variant => $tag,)*
64 }
65 }
66 }
67 };
68}
69
70elements! {
71 A => "a", Abbr => "abbr", Address => "address", Article => "article", Aside => "aside",
72 Audio => "audio", B => "b", Blockquote => "blockquote", Body => "body", Button => "button",
73 Canvas => "canvas", Caption => "caption", Code => "code", Dd => "dd", Del => "del",
74 Details => "details", Dialog => "dialog", Div => "div", Dl => "dl", Dt => "dt", Em => "em",
75 Fieldset => "fieldset", Figcaption => "figcaption", Figure => "figure", Footer => "footer",
76 Form => "form", H1 => "h1", H2 => "h2", H3 => "h3", H4 => "h4", H5 => "h5", H6 => "h6",
77 Header => "header", Hr => "hr", Html => "html", I => "i", Iframe => "iframe", Img => "img",
78 Input => "input", Ins => "ins", Kbd => "kbd", Label => "label", Legend => "legend",
79 Li => "li", Main => "main", Mark => "mark", Menu => "menu", Meter => "meter", Nav => "nav",
80 Ol => "ol", Optgroup => "optgroup", Option => "option", Output => "output", P => "p",
81 Picture => "picture", Pre => "pre", Progress => "progress", Q => "q", S => "s",
82 Samp => "samp", Search => "search", Section => "section", Select => "select",
83 Small => "small", Span => "span", Strong => "strong", Sub => "sub", Summary => "summary",
84 Sup => "sup", Svg => "svg", Table => "table", Tbody => "tbody", Td => "td",
85 Textarea => "textarea", Tfoot => "tfoot", Th => "th", Thead => "thead", Time => "time",
86 Tr => "tr", U => "u", Ul => "ul", Video => "video",
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct FontFace {
92 family: &'static str,
93 src: Url,
94 weight: Option<(u16, u16)>,
95 italic: bool,
96}
97
98impl FontFace {
99 #[must_use]
102 pub fn new(family: &'static str, src: Url) -> Self {
103 Self {
104 family,
105 src,
106 weight: None,
107 italic: false,
108 }
109 }
110
111 #[must_use]
114 pub fn weights(mut self, min: u16, max: u16) -> Self {
115 self.weight = Some((min, max));
116 self
117 }
118
119 #[must_use]
121 pub fn italic(mut self) -> Self {
122 self.italic = true;
123 self
124 }
125
126 #[must_use]
128 pub fn family(&self) -> FontFamily {
129 FontFamily::Quoted(quoted(self.family))
130 }
131
132 fn write(&self, out: &mut String) {
133 let family: Quoted = quoted(self.family);
134 let _ = write!(
135 out,
136 "@font-face{{font-family:{family};src:{};font-display:swap",
137 self.src
138 );
139 if let Some((min, max)) = self.weight {
140 let _ = write!(out, ";font-weight:{min} {max}");
141 }
142 if self.italic {
143 out.push_str(";font-style:italic");
144 }
145 out.push('}');
146 }
147}
148
149#[derive(Debug, Clone, Default, PartialEq)]
171pub struct Global {
172 rules: Vec<(Selector, Style)>,
173 fonts: Vec<FontFace>,
174}
175
176impl Global {
177 #[must_use]
179 pub fn new() -> Self {
180 Self::default()
181 }
182
183 #[must_use]
186 pub fn preflight() -> Self {
187 Self::new()
188 .rule(
189 Selector::Everything,
190 Style::new().box_sizing(BoxSizing::BorderBox).margin(px(0)),
191 )
192 .rule(
193 Element::Body,
194 Style::new().line_height(LineHeight::Number(1.5)),
195 )
196 .rule(
197 Element::Img,
198 Style::new().display(Display::Block).max_width(pct(100)),
199 )
200 .rule(
201 Element::Svg,
202 Style::new().display(Display::Block).max_width(pct(100)),
203 )
204 .rule(
205 Element::Video,
206 Style::new().display(Display::Block).max_width(pct(100)),
207 )
208 .rule(Element::Button, Self::inherit_font())
209 .rule(Element::Input, Self::inherit_font())
210 .rule(Element::Select, Self::inherit_font())
211 .rule(Element::Textarea, Self::inherit_font())
212 }
213
214 fn inherit_font() -> Style {
215 Style::new()
216 .keyword(Property::Font, CssWideKeyword::Inherit)
217 .color(Color::CURRENT_COLOR)
218 }
219
220 #[must_use]
224 pub fn rule(mut self, selector: impl Into<Selector>, style: Style) -> Self {
225 let selector = selector.into();
226 if let Some((_, old)) = self.rules.iter_mut().find(|(s, _)| *s == selector) {
227 *old = std::mem::take(old).merge(style);
228 } else {
229 self.rules.push((selector, style));
230 }
231 self
232 }
233
234 #[must_use]
236 pub fn font(mut self, font: FontFace) -> Self {
237 self.fonts.push(font);
238 self
239 }
240
241 #[must_use]
243 pub fn merge(mut self, global: Self) -> Self {
244 for (selector, style) in global.rules {
245 self = self.rule(selector, style);
246 }
247 self.fonts.extend(global.fonts);
248 self
249 }
250
251 #[must_use]
253 pub fn is_empty(&self) -> bool {
254 self.rules.is_empty() && self.fonts.is_empty()
255 }
256
257 #[must_use]
267 pub fn to_css(&self) -> String {
268 let mut out = format!("{ORDER}@layer {BASE_LAYER}{{");
269 for (selector, style) in &self.rules {
270 let mut sorted: Vec<_> = style.atoms().iter().collect();
271 sorted.sort_by(|a, b| {
272 (&a.conditions, a.property.rank(), a.property).cmp(&(
273 &b.conditions,
274 b.property.rank(),
275 b.property,
276 ))
277 });
278 let mut i = 0;
279 while i < sorted.len() {
280 let conditions = &sorted[i].conditions;
281 assert!(
282 !conditions.has_relation(),
283 "a global rule takes no group or peer condition"
284 );
285 let mut suffix = String::new();
286 for (_, p) in &conditions.pseudo {
287 suffix.push(':');
288 suffix.push_str(p.name());
289 }
290 if let Some(e) = conditions.element {
291 assert!(
292 *selector != Selector::Everything,
293 "Selector::Everything already covers ::before and ::after"
294 );
295 suffix.push_str("::");
296 suffix.push_str(e.name());
297 }
298 let open = conditions.open(&mut out);
299 selector.write(&mut out, &suffix);
300 out.push('{');
301 let mut first = true;
302 while i < sorted.len() && sorted[i].conditions == *conditions {
303 if !first {
304 out.push(';');
305 }
306 first = false;
307 sorted[i].property.write_name(&mut out);
308 let _ = write!(out, ":{}", sorted[i].value);
309 i += 1;
310 }
311 out.push('}');
312 Conditions::close(open, &mut out);
313 }
314 }
315 out.push('}');
316 for font in &self.fonts {
317 font.write(&mut out);
318 }
319 out
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::{Element, FontFace, Global, Selector};
326 use crate::value::{Color, url};
327 use crate::{Media, Style};
328
329 #[test]
330 fn writes_the_preflight_in_the_base_layer() {
331 let css = Global::preflight().to_css();
332 assert!(css.starts_with(
333 "@layer hypersig-tokens,hypersig-base,hypersig-atoms,hypersig-toggled;\
334 @layer hypersig-base{*,::before,::after{box-sizing:border-box;margin:0}"
335 ));
336 assert!(css.contains("button{color:currentColor;font:inherit}"));
337 }
338
339 #[test]
340 fn writes_media_conditions_and_fonts() {
341 let inter = FontFace::new("Inter", url("/inter.woff2")).weights(100, 900);
342 let css = Global::new()
343 .rule(
344 Selector::Root,
345 Style::new().on(Media::Dark, Style::new().color(Color::WHITE)),
346 )
347 .rule(Element::Body, Style::new().font_family(inter.family()))
348 .font(inter)
349 .to_css();
350 assert!(css.contains(
351 "@media (prefers-color-scheme: dark){:root{color:white}}body{font-family:\"Inter\"}}"
352 ));
353 assert!(css.ends_with(
354 "@font-face{font-family:\"Inter\";src:url(\"/inter.woff2\");font-display:swap;font-weight:100 900}"
355 ));
356 }
357}