encre_css/selector/
mod.rs

1//! Define the structures used to parse scanned classes.
2//!
3//! ## Discover what is possible to do with classes by learning some vocabulary
4//!
5//! <style>
6//! .with-hints {
7//!   font-family: sans-serif;
8//!   overflow: visible !important;
9//!   height: 3rem;
10//!   font-size: 1rem;
11//! }
12//!
13//! .with-hints > b > span:nth-child(even) {
14//!   padding: 0 0.35rem;
15//! }
16//!
17//! .with-hints > b > span:nth-child(odd) {
18//!   position: relative;
19//!   text-decoration: underline;
20//!   text-underline-offset: 6px;
21//! }
22//!
23//! .with-hints span:nth-child(odd)::after {
24//!   content: counter(hints);
25//!   position: absolute;
26//!   bottom: -1.75rem;
27//!   left: 50%;
28//!   transform: translateX(-50%);
29//!   font-size: 0.7rem;
30//!   border: 2px solid currentColor;
31//!   border-radius: 50%;
32//!   width: 1.1rem;
33//!   height: 1.1rem;
34//!   display: flex;
35//!   justify-content: center;
36//!   align-items: center;
37//!   counter-increment: hints;
38//! }
39//! </style>
40//!
41//! <p class="with-hints" style="counter-reset: hints; margin-top: 2rem;"><b><span>hover:xl</span><span>:</span><span>bg</span><span>-</span><span>red-500</span></b></p>
42//!
43//! 1. The **[variants](crate::selector::Variant)** (used to add pseudo-selectors, pseudo-elements, pseudo classes, media queries), in this case
44//!    the class will be applied only on a screen larger than 1280px (see [`BUILTIN_SCREENS`]) and
45//!    if hovered;
46//! 2. The **namespace** (basically the name of the plugin), in this case `bg` for changing the background;
47//! 3. The **[modifier](crate::selector::Modifier)** (used to clarify the CSS needed to be generated), in this case the
48//!    background color will become `rgb(239 68 68)` (see [`BUILTIN_COLORS`]).
49//!
50//! <p class="with-hints" style="margin-top: 3rem;"><b><span style="counter-set: hints 3;">[&>*]</span><span>:</span><span style="counter-set: hints 4;">[@supports_(display:flex)]</span><span>:</span><span style="counter-set: hints 1;">flex</span><span>-</span><span style="counter-set: hints 5;">[2_2_10%]</span></b></p>
51//!
52//! 4. The **arbitrary variant** (used to modify the class generated), in this case the class
53//!    will be `.\[\&\>\*]\:flex-\[2_2_10\%\]>*`.
54//!
55//! 5. Another **arbitrary variant** (with another syntax used to add [at rules](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule)),
56//!    in this case the rule will become `@supports (display:flex) { <rule content> }` (spaces need to be replaced with underscores in arbitrary
57//!    variants).
58//!
59//! 6. The **[arbitrary value](crate::selector::Modifier::Arbitrary)**
60//!    (used to specify a value not included in your design system), in this case the background
61//!    color will become `2 2 10%` (spaces need to be replaced with underscores in arbitrary
62//!    values).
63//!
64//! <p class="with-hints" style="margin-top: 3rem;"><b><span>[mask-type:luminance]</span></b></p>
65//!
66//! 7. The **arbitrary CSS property** (used to use a CSS property not supported by `encre-css`), in
67//!    this case the rule content will be `.\[mask-type\:luminance\] { mask-type: luminance; }`.
68//!
69//! <p class="with-hints" style="margin-top: 3rem;"><b><span>dark:(text-white,bg-gray-500)</span></b></p>
70//!
71//! 8. The **variant group** (used to group together several classes conditionally enabled by the
72//!    same variant), in this case the class will be expanded to `dark:text-white` and
73//!    `dark:bg-gray-500`.
74//!
75//! <p class="with-hints" style="margin-top: 3rem;"><b><span>(hover,focus-visible):bg-blue-400</span></b></p>
76//!
77//! 9. The **variant group without any namespace or modifier** (used to group together several classes conditionally enabled by the
78//!    same variant but sharing the same modifier), in this case the class will be expanded to `hover:bg-blue-400` and
79//!    `focus-visible:bg-blue-400`.
80//!
81//! As you can see, by default variants are separated by `:`, modifiers by `-` (the dash after the
82//! first modifier can be omitted, e.g. `m1` instead of `m-1`), arbitrary values/variants are surrounded by `[]` and variant
83//! groups are surrounded by `()`.
84//!
85//! [`BUILTIN_SCREENS`]: crate::config::BUILTIN_SCREENS
86//! [`BUILTIN_COLORS`]: crate::config::BUILTIN_COLORS
87pub(crate) mod parser;
88
89use crate::plugins::Plugin;
90
91use std::{borrow::Cow, cmp::Ordering};
92
93pub(crate) use parser::parse;
94
95/// The modifier is the rest of the selector after the namespace, it is used to clarify the
96/// CSS needed to be generated.
97#[derive(Debug, PartialEq, Eq, Clone)]
98pub enum Modifier<'a> {
99    /// A builtin static modifier (e.g. `bg-red-500`).
100    Builtin {
101        /// Whether the value is negative (e.g. `-translate-2` is negative).
102        is_negative: bool,
103
104        /// The inner value of the modifier.
105        value: &'a str,
106    },
107
108    /// A dynamic modifier capable of automatically generating a rule from a CSS value
109    /// (e.g. `bg-[rgb(12_12_12)]`).
110    ///
111    /// All underscores in the value will be replaced by spaces except in `url()`, if you really
112    /// want to keep one of them, you can prefix it with a backslash `\_` and it will be used as
113    /// is.
114    ///
115    /// Sometimes the value is ambiguous, for example `bg-[var(--foo)]` can be handled by either
116    /// the [`background color`](crate::plugins::background::background_color) or the
117    /// [`background size`](crate::plugins::background::background_size) utility. In this case,
118    /// you need to provide a [CSS type](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types)
119    /// hint (see the list of hints below) before the arbitrary value. For example
120    /// `bg-[length:var(--foo)]` will generate `background-size: var(--foo);` (using the
121    /// [`background size`](crate::plugins::background::background_size) utility).
122    ///
123    /// List of all type hints:
124    /// - `color`
125    /// - `length`
126    /// - `line-width`
127    /// - `image`
128    /// - `url`
129    /// - `position`
130    /// - `percentage`
131    /// - `number`
132    /// - `generic-name`
133    /// - `family-name`
134    /// - `absolute-size`
135    /// - `relative-size`
136    /// - `shadow`
137    Arbitrary {
138        /// The rest of the modifier without the arbitrary value (e.g. `bg` in `bg-[rgb(12_12_12)]`).
139        prefix: &'a str,
140
141        /// The type hint needed for ambiguous values.
142        hint: &'a str,
143
144        /// The inner value of the modifier.
145        ///
146        /// All escaped characters (prefixed by a backslash) are already unescaped.
147        value: Cow<'a, str>,
148    },
149}
150
151/// A selector variant.
152#[derive(Debug, Clone, Eq)]
153pub struct Variant<'a> {
154    pub(crate) order: usize,
155    pub(crate) prefixed: bool,
156    pub(crate) template: Cow<'a, str>,
157}
158
159impl<'a> Variant<'a> {
160    pub(crate) const fn new_const(counter: &mut usize, template: &'static str) -> Self {
161        *counter += 1;
162
163        Self {
164            order: *counter - 1,
165            prefixed: false,
166            template: Cow::Borrowed(template),
167        }
168    }
169
170    /// Create a new variant.
171    ///
172    /// The order is used to decide where the generated class having this variant will be placed in
173    /// the generated CSS. [`Config::last_variant_order`] can be used to insert a variant after all
174    /// the others.
175    ///
176    /// The template is a string which defines how the class will be modified. If it starts with `@`,
177    /// a CSS block will wrap the inner class (like media queries), otherwise just the class name will
178    /// be modified.
179    ///
180    /// The template should contain `&` which will be replaced by the complete class name.
181    ///
182    /// # Example
183    ///
184    /// ```
185    /// use encre_css::{Config, selector::Variant};
186    /// use std::borrow::Cow;
187    ///
188    /// let mut config = Config::default();
189    /// config.register_variant(
190    ///     "headings",
191    ///     // Insert the classes having this variant after all the other variants
192    ///     Variant::new(config.last_variant_order(), "& :where(h1, h2, h3, h4, h5, h6)")
193    /// );
194    ///
195    /// let generated = encre_css::generate(
196    ///     ["headings:text-gray-700"],
197    ///     &config,
198    /// );
199    ///
200    /// assert!(generated.ends_with(".headings\\:text-gray-700 :where(h1, h2, h3, h4, h5, h6) {
201    ///   color: oklch(37.3% .034 259.733);
202    /// }"));
203    /// ```
204    ///
205    /// [`Config::last_variant_order`]: crate::Config::last_variant_order
206    pub fn new<T: Into<Cow<'a, str>>>(order: usize, template: T) -> Self {
207        Self {
208            order,
209            prefixed: false,
210            template: template.into(),
211        }
212    }
213
214    /// Defines a prefixed variant which is composed of a prefix and an arbitrary value which will
215    /// be inserted into the variant template.
216    ///
217    /// A prefixed variant can have `{}` in its template which will be replaced by the arbitrary
218    /// value.
219    ///
220    /// # Example
221    ///
222    /// ```
223    /// use encre_css::{Config, selector::Variant};
224    /// use std::borrow::Cow;
225    ///
226    /// let mut config = Config::default();
227    /// config.register_variant(
228    ///     "media",
229    ///     // Insert the classes having this variant after all the other variants
230    ///     Variant::new(config.last_variant_order(), "@media {}").with_prefixed()
231    /// );
232    ///
233    /// let generated = encre_css::generate(
234    ///     ["media-[print]:flex"],
235    ///     &config,
236    /// );
237    ///
238    /// assert!(generated.ends_with(r"@media print {
239    ///   .media-\[print\]\:flex {
240    ///     display: flex;
241    ///   }
242    /// }"));
243    /// ```
244    #[must_use]
245    pub const fn with_prefixed(mut self) -> Self {
246        self.prefixed = true;
247        self
248    }
249}
250
251impl PartialEq for Variant<'_> {
252    fn eq(&self, other: &Self) -> bool {
253        // Does not test order because it can change
254        self.template == other.template
255    }
256}
257
258/// A parsed selector, aka a utility class, containing the variants, the namespace and the modifier.
259///
260/// See [`crate::selector`] for more information.
261#[derive(Clone, Debug)]
262pub(crate) struct Selector<'a> {
263    pub(crate) order: usize,
264    pub(crate) full: &'a str,
265    pub(crate) modifier: Modifier<'a>,
266    pub(crate) variants: Vec<Variant<'a>>,
267    pub(crate) is_important: bool,
268    pub(crate) plugin: &'static (dyn Plugin + Sync + Send),
269}
270
271impl PartialEq for Selector<'_> {
272    fn eq(&self, other: &Self) -> bool {
273        // Does not test order because it can change
274        self.full == other.full
275            && self.modifier == other.modifier
276            && self.variants == other.variants
277            && self.is_important == other.is_important
278    }
279}
280
281impl Eq for Selector<'_> {}
282
283impl PartialOrd for Selector<'_> {
284    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
285        Some(self.cmp(other))
286    }
287}
288
289impl Ord for Selector<'_> {
290    fn cmp(&self, other: &Self) -> Ordering {
291        // We need to check the order as well as the strict egality because the PartialEq
292        // implementation for Selector does not check if two selectors have the same plugin which
293        // can lead two selectors having the same modifier to be recognized as the same although
294        // they use different plugins
295        if self.order == other.order && self == other {
296            return Ordering::Equal;
297        }
298
299        if self.variants.is_empty() && !other.variants.is_empty() {
300            Ordering::Less
301        } else if !self.variants.is_empty() && other.variants.is_empty() {
302            Ordering::Greater
303        } else if !self.variants.is_empty() && !other.variants.is_empty() {
304            let mut compared = None;
305
306            // Compare variants in the lexicographic order
307            for variant_i in 0..self.variants.len() {
308                if variant_i >= other.variants.len() {
309                    compared = Some(Ordering::Greater);
310                    break;
311                }
312
313                let res = self
314                    .variants
315                    .get(variant_i)
316                    .as_ref()
317                    .unwrap()
318                    .order
319                    .cmp(&other.variants.get(variant_i).unwrap().order);
320
321                if res != Ordering::Equal {
322                    compared = Some(res);
323                    break;
324                }
325            }
326
327            compared.unwrap_or(Ordering::Less).then_with(|| {
328                self.order
329                    .cmp(&other.order)
330                    .then_with(|| self.full.cmp(other.full))
331            })
332        } else {
333            self.order
334                .cmp(&other.order)
335                .then_with(|| self.full.cmp(other.full))
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use crate::{config::Config, selector::parse};
343
344    use std::collections::BTreeSet;
345
346    #[test]
347    fn sorting_test() {
348        let config = Config::default();
349
350        let selectors1 = parse(
351            "lg:bg-red-500",
352            None,
353            None,
354            &config,
355            &config.get_derived_variants(),
356        );
357        let selectors2 = parse(
358            "bg-red-500",
359            None,
360            None,
361            &config,
362            &config.get_derived_variants(),
363        );
364
365        let mut selectors = BTreeSet::new();
366        selectors.insert(selectors1[0].as_ref().unwrap());
367        selectors.insert(selectors2[0].as_ref().unwrap());
368
369        let mut iter = selectors.iter();
370        assert!(
371            iter.next().unwrap().full == "bg-red-500"
372                && iter.next().unwrap().full == "lg:bg-red-500"
373        );
374    }
375}