1use std::borrow::Cow;
9use std::fmt;
10
11#[rustfmt::skip]
13mod generated;
14
15pub use generated::*;
18
19#[derive(Debug, Clone, PartialEq)]
21enum Amount {
22 Unit(f64, &'static str),
23 Var(String),
24}
25
26impl Amount {
27 fn write(&self, f: &mut fmt::Formatter<'_>, zero_bare: bool) -> fmt::Result {
34 match self {
35 Self::Unit(n, unit) => {
36 assert!(n.is_finite(), "a CSS number is finite");
37 write!(f, "{n}")?;
38 if !(zero_bare && *n == 0.0) {
39 f.write_str(unit)?;
40 }
41 Ok(())
42 }
43 Self::Var(css) => f.write_str(css),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct Length(Amount);
51
52impl Length {
53 pub const ZERO: Self = Self(Amount::Unit(0.0, ""));
55
56 pub(crate) fn var(css: String) -> Self {
57 Self(Amount::Var(css))
58 }
59}
60
61impl fmt::Display for Length {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 self.0.write(f, true)
64 }
65}
66
67macro_rules! lengths {
68 ($($name:ident => $unit:literal: $doc:literal),* $(,)?) => {$(
69 #[doc = concat!("A length in `", $unit, "`, ", $doc, ".")]
70 #[must_use]
71 pub fn $name(value: impl Into<f64>) -> Length {
72 Length(Amount::Unit(value.into(), $unit))
73 }
74 )*};
75}
76
77lengths! {
78 px => "px": "CSS pixels",
79 rem => "rem": "the root element's font size",
80 em => "em": "the element's font size",
81 ch => "ch": "the width of `0` in the element's font",
82 ex => "ex": "the x-height of the element's font",
83 lh => "lh": "the element's line height",
84 vw => "vw": "1% of the viewport's width",
85 vh => "vh": "1% of the viewport's height",
86 vmin => "vmin": "1% of the viewport's smaller side",
87 vmax => "vmax": "1% of the viewport's larger side",
88 svh => "svh": "1% of the small viewport's height, with browser toolbars shown",
89 lvh => "lvh": "1% of the large viewport's height, with browser toolbars hidden",
90 dvh => "dvh": "1% of the viewport's current height",
91 cqw => "cqw": "1% of the query container's width",
92 cqh => "cqh": "1% of the query container's height",
93 cm => "cm": "centimeters",
94 mm => "mm": "millimeters",
95 pt => "pt": "points",
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub struct Percentage(Amount);
101
102impl Percentage {
103 pub(crate) fn var(css: String) -> Self {
104 Self(Amount::Var(css))
105 }
106}
107
108impl fmt::Display for Percentage {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 self.0.write(f, false)
111 }
112}
113
114#[must_use]
116pub fn pct(value: impl Into<f64>) -> Percentage {
117 Percentage(Amount::Unit(value.into(), "%"))
118}
119
120#[derive(Debug, Clone, PartialEq)]
122pub struct Time(Amount);
123
124impl Time {
125 pub(crate) fn var(css: String) -> Self {
126 Self(Amount::Var(css))
127 }
128}
129
130impl fmt::Display for Time {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 self.0.write(f, false)
133 }
134}
135
136#[must_use]
138pub fn ms(value: impl Into<f64>) -> Time {
139 Time(Amount::Unit(value.into(), "ms"))
140}
141
142#[must_use]
144pub fn secs(value: impl Into<f64>) -> Time {
145 Time(Amount::Unit(value.into(), "s"))
146}
147
148#[derive(Debug, Clone, PartialEq)]
150pub struct Angle(Amount);
151
152impl Angle {
153 pub(crate) fn var(css: String) -> Self {
154 Self(Amount::Var(css))
155 }
156}
157
158impl fmt::Display for Angle {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 self.0.write(f, false)
161 }
162}
163
164#[must_use]
166pub fn deg(value: impl Into<f64>) -> Angle {
167 Angle(Amount::Unit(value.into(), "deg"))
168}
169
170#[must_use]
172pub fn turn(value: impl Into<f64>) -> Angle {
173 Angle(Amount::Unit(value.into(), "turn"))
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Color(Cow<'static, str>);
180
181impl Color {
182 pub const CURRENT_COLOR: Self = Self::named("currentColor");
184
185 pub const TRANSPARENT: Self = Self::named("transparent");
188
189 pub(crate) const fn named(name: &'static str) -> Self {
190 Self(Cow::Borrowed(name))
191 }
192
193 pub(crate) fn var(css: String) -> Self {
194 Self(Cow::Owned(css))
195 }
196
197 #[must_use]
199 pub fn hex(rgb: u32) -> Self {
200 Self(Cow::Owned(format!("#{:06x}", rgb & 0x00ff_ffff)))
201 }
202
203 #[must_use]
205 pub fn rgb(red: u8, green: u8, blue: u8) -> Self {
206 Self(Cow::Owned(format!("rgb({red} {green} {blue})")))
207 }
208
209 #[must_use]
211 pub fn opacity(&self, opacity: u8) -> Self {
212 let opacity = opacity.min(100);
213 Self(Cow::Owned(format!(
214 "color-mix(in srgb, {self} {opacity}%, transparent)"
215 )))
216 }
217
218 #[must_use]
220 pub fn mix(&self, other: &Self, weight: u8) -> Self {
221 let weight = weight.min(100);
222 Self(Cow::Owned(format!(
223 "color-mix(in srgb, {self} {weight}%, {other})"
224 )))
225 }
226}
227
228impl fmt::Display for Color {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.write_str(&self.0)
231 }
232}
233
234fn quote(text: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str("\"")?;
243 for c in text.chars() {
244 match c {
245 '"' => f.write_str("\\\"")?,
246 '\\' => f.write_str("\\\\")?,
247 '<' => f.write_str("\\3c ")?,
248 c if c.is_control() => write!(f, "\\{:x} ", u32::from(c))?,
249 c => write!(f, "{c}")?,
250 }
251 }
252 f.write_str("\"")
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct Quoted(String);
258
259impl fmt::Display for Quoted {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 quote(&self.0, f)
262 }
263}
264
265#[must_use]
267pub fn quoted(text: impl Into<String>) -> Quoted {
268 Quoted(text.into())
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct Url(String);
274
275impl fmt::Display for Url {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 f.write_str("url(")?;
278 quote(&self.0, f)?;
279 f.write_str(")")
280 }
281}
282
283#[must_use]
285pub fn url(href: impl Into<String>) -> Url {
286 Url(href.into())
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct Raw(String);
296
297impl fmt::Display for Raw {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 f.write_str(&self.0)
300 }
301}
302
303#[must_use]
314pub fn raw(css: impl Into<String>) -> Raw {
315 let css = css.into();
316 assert!(
317 stays_in_declaration(&css),
318 "a raw CSS value holds no `;`, `{{`, `}}`, `<`, backslash, `/*`, or control \
319 character, and closes every quote and parenthesis it opens",
320 );
321 Raw(css)
322}
323
324fn stays_in_declaration(css: &str) -> bool {
327 let mut depth = 0_usize;
328 let mut quote = None;
329 for c in css.chars() {
330 match (quote, c) {
331 (_, ';' | '{' | '}' | '<' | '\\') => return false,
332 (_, c) if c.is_control() => return false,
333 (Some(open), c) if c == open => quote = None,
334 (None, '"' | '\'') => quote = Some(c),
335 (None, '(') => depth += 1,
336 (None, ')') => match depth.checked_sub(1) {
337 Some(less) => depth = less,
338 None => return false,
339 },
340 _ => {}
341 }
342 }
343 quote.is_none() && depth == 0 && !css.contains("/*")
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
348pub enum CssWideKeyword {
349 Inherit,
351 Initial,
353 Unset,
355 Revert,
357 RevertLayer,
359}
360
361impl fmt::Display for CssWideKeyword {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 f.write_str(match self {
364 Self::Inherit => "inherit",
365 Self::Initial => "initial",
366 Self::Unset => "unset",
367 Self::Revert => "revert",
368 Self::RevertLayer => "revert-layer",
369 })
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::{quoted, raw, stays_in_declaration, url};
376
377 #[test]
378 fn a_string_escapes_every_character_that_would_end_it() {
379 assert_eq!(quoted("a\"b\\c<d").to_string(), r#""a\"b\\c\3c d""#);
380 assert_eq!(
383 url("x\r);}a{b:c}\x0c\n").to_string(),
384 r#"url("x\d );}a{b:c}\c \a ")"#,
385 );
386 }
387
388 #[test]
389 fn a_raw_value_keeps_quotes_and_parentheses_it_closes() {
390 assert_eq!(
391 raw("'Helvetica Neue', calc(100% - (2rem))").to_string(),
392 "'Helvetica Neue', calc(100% - (2rem))",
393 );
394 for open in [
395 "'Inter", "url(x", "a)", "a /* b", "\\66 oo", "a\nb", "'a;b'",
396 ] {
397 assert!(!stays_in_declaration(open), "{open:?}");
398 }
399 }
400}