Skip to main content

hypersig_style/
style.rs

1//! [`Style`], a set of declarations that becomes atomic classes.
2
3use std::fmt::Write as _;
4
5use crate::condition::{Condition, Conditions, GROUP, PEER};
6use crate::generated::{Property, PseudoElement};
7use crate::keyframes::Keyframes;
8use crate::token::{Token, TokenValue};
9use crate::value::CssWideKeyword;
10
11/// A set of CSS declarations, each of which becomes one class.
12///
13/// A property method replaces an earlier value for the same property under the
14/// same conditions, so a later call wins as it would later in a rule.
15///
16/// ```
17/// use hypersig_style::value::{Color, Display, pct, px, quoted, rem};
18/// use hypersig_style::{Media, PseudoClass, PseudoElement, Style};
19///
20/// let card = Style::new()
21///     .display(Display::Flex)
22///     .padding(rem(1))
23///     .color(Color::hex(0x222222))
24///     .on(PseudoClass::Hover, Style::new().color(Color::CRIMSON))
25///     .on(Media::Md, Style::new().width(pct(50)))
26///     .pseudo(PseudoElement::After, Style::new().content(quoted("→")).margin_left(px(4)));
27/// let names: Vec<String> = card.class_names().collect();
28/// assert!(names.contains(&"hover:color_crimson".to_owned()));
29/// ```
30#[derive(Debug, Clone, Default, PartialEq)]
31pub struct Style {
32    atoms: Vec<Atom>,
33    keyframes: Vec<Keyframes>,
34    markers: Vec<&'static str>,
35}
36
37/// One declaration and the conditions it applies under.
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub(crate) struct Atom {
40    pub(crate) conditions: Conditions,
41    pub(crate) property: Prop,
42    pub(crate) value: String,
43}
44
45/// The property of a declaration: a standard one, or the custom property of
46/// a [`Token`].
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub(crate) enum Prop {
49    Standard(Property),
50    Custom(&'static str),
51}
52
53impl Prop {
54    /// The name after which the declaration's class is named, which for a
55    /// custom property starts with `--`.
56    pub(crate) fn write_name(self, out: &mut String) {
57        match self {
58            Self::Standard(p) => out.push_str(p.name()),
59            Self::Custom(name) => {
60                out.push_str("--");
61                out.push_str(name);
62            }
63        }
64    }
65
66    /// How many shorthands stand above the property. A custom property has none.
67    pub(crate) fn rank(self) -> u32 {
68        match self {
69            Self::Standard(p) => p.rank(),
70            Self::Custom(_) => 0,
71        }
72    }
73}
74
75impl Atom {
76    /// The class name, from the conditions, the property, and the value, with
77    /// `when:` in front for a toggled declaration.
78    ///
79    /// The value is written out where it uses only letters, digits, `.`, `#`,
80    /// `%`, and `-`. Any other value is replaced by a 64-bit FNV-1a hash of it,
81    /// so two different values never share a name unless their hashes collide.
82    pub(crate) fn class_name(&self, toggled: bool) -> String {
83        let mut name = String::new();
84        if toggled {
85            name.push_str("when:");
86        }
87        for m in &self.conditions.media {
88            name.push_str(m.name());
89            name.push(':');
90        }
91        for c in &self.conditions.container {
92            name.push_str(c.name());
93            name.push(':');
94        }
95        for (_, p) in &self.conditions.group {
96            name.push_str("group-");
97            name.push_str(p.name());
98            name.push(':');
99        }
100        for (_, p) in &self.conditions.peer {
101            name.push_str("peer-");
102            name.push_str(p.name());
103            name.push(':');
104        }
105        for (_, p) in &self.conditions.pseudo {
106            name.push_str(p.name());
107            name.push(':');
108        }
109        if let Some(e) = self.conditions.element {
110            name.push_str(e.name());
111            name.push(':');
112        }
113        self.property.write_name(&mut name);
114        name.push('_');
115        let readable = self
116            .value
117            .bytes()
118            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'#' | b'%' | b'-'));
119        if readable {
120            name.push_str(&self.value);
121        } else {
122            let _ = write!(name, "h{:016x}", fnv1a(self.value.as_bytes()));
123        }
124        name
125    }
126}
127
128/// The 64-bit FNV-1a hash, chosen because it is a few lines and stable across
129/// Rust versions, which `std`'s `DefaultHasher` does not promise.
130fn fnv1a(bytes: &[u8]) -> u64 {
131    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
132    const PRIME: u64 = 0x0100_0000_01b3;
133    bytes
134        .iter()
135        .fold(OFFSET, |h, &b| (h ^ u64::from(b)).wrapping_mul(PRIME))
136}
137
138impl Style {
139    /// An empty style.
140    #[must_use]
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    pub(crate) fn atoms(&self) -> &[Atom] {
146        &self.atoms
147    }
148
149    pub(crate) fn keyframes_used(&self) -> &[Keyframes] {
150        &self.keyframes
151    }
152
153    /// Animates the element with `keyframes`: sets `animation-name` to its
154    /// name, and carries the `@keyframes` rule to the page.
155    #[must_use]
156    pub fn keyframes(mut self, keyframes: &Keyframes) -> Self {
157        if !self.keyframes.iter().any(|k| k.name() == keyframes.name()) {
158            self.keyframes.push(keyframes.clone());
159        }
160        self.with(Property::AnimationName, keyframes.name().to_owned())
161    }
162
163    fn insert(&mut self, atom: Atom) {
164        if let Some(old) = self
165            .atoms
166            .iter_mut()
167            .find(|a| a.property == atom.property && a.conditions == atom.conditions)
168        {
169            *old = atom;
170        } else {
171            self.atoms.push(atom);
172        }
173    }
174
175    pub(crate) fn with(mut self, property: Property, value: String) -> Self {
176        self.insert(Atom {
177            conditions: Conditions::default(),
178            property: Prop::Standard(property),
179            value,
180        });
181        self
182    }
183
184    /// Sets `token` to `value` on the element and everything inside it, in
185    /// place of the default and the page's [`Theme`](crate::Theme).
186    #[must_use]
187    pub fn token<T: TokenValue>(mut self, token: &Token<T>, value: impl Into<T>) -> Self {
188        self.insert(Atom {
189            conditions: Conditions::default(),
190            property: Prop::Custom(token.name()),
191            value: value.into().to_string(),
192        });
193        self
194    }
195
196    /// Sets `property` to a keyword every property accepts, such as `inherit`.
197    #[must_use]
198    pub fn keyword(self, property: Property, value: CssWideKeyword) -> Self {
199        self.with(property, value.to_string())
200    }
201
202    /// Adds the declarations of `style`, which win over this style's own.
203    #[must_use]
204    pub fn merge(mut self, style: Self) -> Self {
205        for atom in style.atoms {
206            self.insert(atom);
207        }
208        self.keyframes.extend(style.keyframes);
209        for marker in style.markers {
210            if !self.markers.contains(&marker) {
211                self.markers.push(marker);
212            }
213        }
214        self
215    }
216
217    /// Adds the declarations of `style`, applied only under `condition`.
218    ///
219    /// Conditions nest: `style` may use `on` itself, and its declarations then
220    /// apply under both. The stylesheet writes the pseudo-classes of one
221    /// declaration in a fixed order, so `on(Hover, on(Focus, ..))` and
222    /// `on(Focus, on(Hover, ..))` produce the same class.
223    #[must_use]
224    pub fn on(mut self, condition: impl Into<Condition>, style: Self) -> Self {
225        let condition = condition.into();
226        for mut atom in style.atoms {
227            atom.conditions.add(condition);
228            self.insert(atom);
229        }
230        self.keyframes.extend(style.keyframes);
231        self
232    }
233
234    /// Adds the declarations of `style` to the pseudo-element `element`.
235    ///
236    /// Pseudo-classes in `style` apply to the element, so
237    /// `pseudo(Before, Style::new().on(Hover, ..))` is written
238    /// `:hover::before`.
239    ///
240    /// # Panics
241    ///
242    /// Panics if `style` already places a declaration on a pseudo-element,
243    /// because CSS has no pseudo-element of a pseudo-element.
244    #[must_use]
245    pub fn pseudo(mut self, element: PseudoElement, style: Self) -> Self {
246        for mut atom in style.atoms {
247            assert!(
248                atom.conditions.element.is_none(),
249                "a pseudo-element holds no pseudo-element"
250            );
251            atom.conditions.element = Some(element);
252            self.insert(atom);
253        }
254        self.keyframes.extend(style.keyframes);
255        self
256    }
257
258    /// Marks the element as the ancestor that [`Condition::Group`] tests,
259    /// with the class `group`. Any marked ancestor that matches counts, as
260    /// with the `group` of Tailwind CSS and Panda CSS.
261    #[must_use]
262    pub fn group(mut self) -> Self {
263        if !self.markers.contains(&GROUP) {
264            self.markers.push(GROUP);
265        }
266        self
267    }
268
269    /// Marks the element as the earlier sibling that [`Condition::Peer`]
270    /// tests, with the class `peer`.
271    #[must_use]
272    pub fn peer(mut self) -> Self {
273        if !self.markers.contains(&PEER) {
274            self.markers.push(PEER);
275        }
276        self
277    }
278
279    /// The class names an element needs for this style.
280    pub fn class_names(&self) -> impl Iterator<Item = String> + '_ {
281        self.markers
282            .iter()
283            .map(|&m| m.to_owned())
284            .chain(self.atoms.iter().map(|a| a.class_name(false)))
285    }
286
287    /// The class names for this style when it is switched on and off at run
288    /// time. Their rules sit in a later cascade layer than those of
289    /// [`Style::class_names`], so while switched on they override the
290    /// element's own declarations for the same property.
291    pub fn toggled_class_names(&self) -> impl Iterator<Item = String> + '_ {
292        self.markers
293            .iter()
294            .map(|&m| m.to_owned())
295            .chain(self.atoms.iter().map(|a| a.class_name(true)))
296    }
297
298    /// Whether the style has no declarations.
299    #[must_use]
300    pub fn is_empty(&self) -> bool {
301        self.atoms.is_empty() && self.markers.is_empty()
302    }
303}