Skip to main content

hypersig_style/value/
mod.rs

1//! The value types a property method accepts, and functions that build them.
2//!
3//! A property whose value is one of the hand-written types alone takes it
4//! directly, as `color` takes a [`Color`]. A property with keywords of its own
5//! takes an enum generated for it, such as [`Width`], which converts from each
6//! hand-written type it accepts.
7
8use std::borrow::Cow;
9use std::fmt;
10
11// Written by tests/generate.rs, whose output is kept as it writes it.
12#[rustfmt::skip]
13mod generated;
14
15// One enum per property with keywords of its own, a few hundred in all, each
16// reached as `value::Width` like the hand-written types here.
17pub use generated::*;
18
19/// A number with a unit, or a `var()` that stands for one.
20#[derive(Debug, Clone, PartialEq)]
21enum Amount {
22    Unit(f64, &'static str),
23    Var(String),
24}
25
26impl Amount {
27    /// Writes the amount, leaving the unit off zero where `zero_bare` is set,
28    /// as a length may be written but a duration may not.
29    ///
30    /// # Panics
31    ///
32    /// Panics if the number is NaN or infinite, which CSS cannot write.
33    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/// A length such as `16px` or `1.5rem`, or a [`Token`](crate::Token) that holds one.
49#[derive(Debug, Clone, PartialEq)]
50pub struct Length(Amount);
51
52impl Length {
53    /// A length of zero, written `0`.
54    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/// A percentage such as `50%`, or a [`Token`](crate::Token) that holds one.
99#[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/// The percentage `value%`.
115#[must_use]
116pub fn pct(value: impl Into<f64>) -> Percentage {
117    Percentage(Amount::Unit(value.into(), "%"))
118}
119
120/// A duration such as `200ms`, or a [`Token`](crate::Token) that holds one.
121#[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/// A duration in milliseconds.
137#[must_use]
138pub fn ms(value: impl Into<f64>) -> Time {
139    Time(Amount::Unit(value.into(), "ms"))
140}
141
142/// A duration in seconds.
143#[must_use]
144pub fn secs(value: impl Into<f64>) -> Time {
145    Time(Amount::Unit(value.into(), "s"))
146}
147
148/// An angle such as `90deg`, or a [`Token`](crate::Token) that holds one.
149#[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/// An angle in degrees.
165#[must_use]
166pub fn deg(value: impl Into<f64>) -> Angle {
167    Angle(Amount::Unit(value.into(), "deg"))
168}
169
170/// An angle in whole turns.
171#[must_use]
172pub fn turn(value: impl Into<f64>) -> Angle {
173    Angle(Amount::Unit(value.into(), "turn"))
174}
175
176/// A color: a named color such as [`Color::CRIMSON`], a hex or RGB value, a
177/// mix of two colors, or a [`Token`](crate::Token) that holds one.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Color(Cow<'static, str>);
180
181impl Color {
182    /// The element's `color`, written `currentColor`.
183    pub const CURRENT_COLOR: Self = Self::named("currentColor");
184
185    /// Fully transparent black, written `transparent`. MDN lists it apart
186    /// from the named colors, so the generator does not write it.
187    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    /// The color `#rrggbb` from the low 24 bits of `rgb`, as in `Color::hex(0xb83f45)`.
198    #[must_use]
199    pub fn hex(rgb: u32) -> Self {
200        Self(Cow::Owned(format!("#{:06x}", rgb & 0x00ff_ffff)))
201    }
202
203    /// The color with red, green, and blue channels from 0 to 255.
204    #[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    /// This color at `opacity` percent, mixed with `transparent` in sRGB.
210    #[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    /// `weight` percent of this color mixed with the rest of `other`, in sRGB.
219    #[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
234/// Writes `text` as a double-quoted CSS string.
235///
236/// A control character becomes a hex escape such as `\a `, because CSS reads
237/// a carriage return or a form feed as a newline, and a newline ends the
238/// string so that the text after it is parsed as rules. `<` becomes `\3c `,
239/// so the text cannot close the `<style>` element the stylesheet is written
240/// into.
241fn 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/// A CSS string, as `content` takes it.
256#[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/// The CSS string holding `text`, escaped as needed.
266#[must_use]
267pub fn quoted(text: impl Into<String>) -> Quoted {
268    Quoted(text.into())
269}
270
271/// A URL, written `url("...")`.
272#[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/// The URL `href`.
284#[must_use]
285pub fn url(href: impl Into<String>) -> Url {
286    Url(href.into())
287}
288
289/// A value written into the stylesheet as given, for the forms a property's
290/// generated type leaves out, such as `margin: 0 auto`.
291///
292/// Nothing checks that the browser accepts it. A value it rejects drops the
293/// declaration, and the element renders as if the property were not set.
294#[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/// The value `css`, unchecked.
304///
305/// # Panics
306///
307/// Panics if `css` could end more than its own declaration: where it contains
308/// `;`, `{`, `}`, `<`, a backslash, `/*`, or a control character, or leaves a
309/// quote or a parenthesis open. The first three would end the declaration or
310/// the rule, `<` could close the `<style>` element, and an escape, a comment,
311/// or an open quote or parenthesis would carry the value on into the rules
312/// after it.
313#[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
324/// Whether `css` ends where it ends, so the declaration after it is read as
325/// its own.
326fn 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/// A keyword every property accepts, which CSS calls a CSS-wide keyword.
347#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
348pub enum CssWideKeyword {
349    /// `inherit`: the parent's computed value.
350    Inherit,
351    /// `initial`: the property's initial value.
352    Initial,
353    /// `unset`: `inherit` for an inherited property, `initial` otherwise.
354    Unset,
355    /// `revert`: the value from the browser's or the user's stylesheet.
356    Revert,
357    /// `revert-layer`: the value from the previous cascade layer.
358    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        // CSS reads `\r`, `\x0c`, and `\r\n` as a newline, which ends the
381        // string and lets the rest be read as rules.
382        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}