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//! ### Automatic replacements
86//!
87//! Because HTML classes can't contain several symbols (e.g spaces), replacement characters must be
88//! used **in arbitrary values, variants and CSS properties**.
89//!
90//! - `_` is replaced by a space (e.g instead of writing `content-[hello world]` which is
91//!   considered two different classes by browsers, use `content-[hello_world]`)
92//! - `&#34;` is replaced by `"`
93//! - `&#39;` is replaced by `'`
94//! - `&#40;` is replaced by `(`
95//! - `&#41;` is replaced by `)`
96//! - `&#91;` is replaced by `[`
97//! - `&#92;` is replaced by `\`
98//! - `&#93;` is replaced by `]`
99//! - `&#95;` is replaced by `_` (useful when a real `_` is needed instead of a space)
100//! - `&#96;` is replaced by `` ` ``
101//!
102//! [`BUILTIN_SCREENS`]: crate::config::BUILTIN_SCREENS
103//! [`BUILTIN_COLORS`]: crate::config::BUILTIN_COLORS
104pub(crate) mod parser;
105
106use crate::plugins::Plugin;
107
108use std::{borrow::Cow, cmp::Ordering};
109
110pub(crate) use parser::parse;
111
112/// The modifier is the rest of the selector after the namespace, it is used to clarify the
113/// CSS needed to be generated.
114#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
115pub enum Modifier<'a> {
116    /// A builtin static modifier (e.g. `bg-red-500`).
117    Builtin {
118        /// Whether the value is negative (e.g. `-translate-2` is negative).
119        is_negative: bool,
120
121        /// The inner value of the modifier.
122        value: &'a str,
123    },
124
125    /// A dynamic modifier capable of automatically generating a rule from a CSS value
126    /// (e.g. `bg-[rgb(12_12_12)]`).
127    ///
128    /// All underscores in the value will be replaced by spaces except in `url()`, if you really
129    /// want to keep one of them, you can prefix it with a backslash `\_` and it will be used as
130    /// is.
131    ///
132    /// Sometimes the value is ambiguous, for example `bg-[var(--foo)]` can be handled by either
133    /// the [`background color`](crate::plugins::background::background_color) or the
134    /// [`background size`](crate::plugins::background::background_size) utility. In this case,
135    /// you need to provide a [CSS type](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types)
136    /// hint (see the list of hints below) before the arbitrary value. For example
137    /// `bg-[length:var(--foo)]` will generate `background-size: var(--foo);` (using the
138    /// [`background size`](crate::plugins::background::background_size) utility).
139    ///
140    /// List of all type hints:
141    /// - `color`
142    /// - `length`
143    /// - `line-width`
144    /// - `image`
145    /// - `url`
146    /// - `position`
147    /// - `percentage`
148    /// - `number`
149    /// - `generic-name`
150    /// - `family-name`
151    /// - `absolute-size`
152    /// - `relative-size`
153    /// - `shadow`
154    Arbitrary {
155        /// The rest of the modifier without the arbitrary value (e.g. `bg` in `bg-[rgb(12_12_12)]`).
156        prefix: &'a str,
157
158        /// The type hint needed for ambiguous values.
159        hint: &'a str,
160
161        /// The inner value of the modifier.
162        ///
163        /// All escaped characters (prefixed by a backslash) are already unescaped.
164        value: Cow<'a, str>,
165    },
166}
167
168/// A selector variant.
169#[derive(Debug, Clone, Eq)]
170pub struct Variant<'a> {
171    pub(crate) order: usize,
172    pub(crate) prefixed: bool,
173    pub(crate) template: Cow<'a, str>,
174}
175
176impl<'a> Variant<'a> {
177    pub(crate) const fn new_const(counter: &mut usize, template: &'static str) -> Self {
178        *counter += 1;
179
180        Self {
181            order: *counter - 1,
182            prefixed: false,
183            template: Cow::Borrowed(template),
184        }
185    }
186
187    /// Create a new variant.
188    ///
189    /// The order is used to decide where the generated class having this variant will be placed in
190    /// the generated CSS. [`Config::last_variant_order`] can be used to insert a variant after all
191    /// the others.
192    ///
193    /// The template is a string which defines how the class will be modified. If it starts with `@`,
194    /// a CSS block will wrap the inner class (like media queries), otherwise just the class name will
195    /// be modified.
196    ///
197    /// The template should contain `&` which will be replaced by the complete class name.
198    ///
199    /// # Example
200    ///
201    /// ```
202    /// use encre_css::{Config, selector::Variant};
203    /// use std::borrow::Cow;
204    ///
205    /// let mut config = Config::default();
206    /// config.register_variant(
207    ///     "headings",
208    ///     // Insert the classes having this variant after all the other variants
209    ///     Variant::new(config.last_variant_order(), "& :where(h1, h2, h3, h4, h5, h6)")
210    /// );
211    ///
212    /// let generated = encre_css::generate(
213    ///     ["headings:text-gray-700"],
214    ///     &config,
215    /// );
216    ///
217    /// assert!(generated.ends_with(".headings\\:text-gray-700 :where(h1, h2, h3, h4, h5, h6) {
218    ///   color: oklch(37.3% .034 259.733);
219    /// }"));
220    /// ```
221    ///
222    /// [`Config::last_variant_order`]: crate::Config::last_variant_order
223    pub fn new<T: Into<Cow<'a, str>>>(order: usize, template: T) -> Self {
224        Self {
225            order,
226            prefixed: false,
227            template: template.into(),
228        }
229    }
230
231    /// Defines a prefixed variant which is composed of a prefix and an arbitrary value which will
232    /// be inserted into the variant template.
233    ///
234    /// A prefixed variant can have `{}` in its template which will be replaced by the arbitrary
235    /// value.
236    ///
237    /// # Example
238    ///
239    /// ```
240    /// use encre_css::{Config, selector::Variant};
241    /// use std::borrow::Cow;
242    ///
243    /// let mut config = Config::default();
244    /// config.register_variant(
245    ///     "media",
246    ///     // Insert the classes having this variant after all the other variants
247    ///     Variant::new(config.last_variant_order(), "@media {}").with_prefixed()
248    /// );
249    ///
250    /// let generated = encre_css::generate(
251    ///     ["media-[print]:flex"],
252    ///     &config,
253    /// );
254    ///
255    /// assert!(generated.ends_with(r"@media print {
256    ///   .media-\[print\]\:flex {
257    ///     display: flex;
258    ///   }
259    /// }"));
260    /// ```
261    #[must_use]
262    pub const fn with_prefixed(mut self) -> Self {
263        self.prefixed = true;
264        self
265    }
266}
267
268impl PartialEq for Variant<'_> {
269    fn eq(&self, other: &Self) -> bool {
270        // Does not test order because it can change
271        self.template == other.template
272    }
273}
274
275/// A parsed selector, aka a utility class, containing the variants, the namespace and the modifier.
276///
277/// See [`crate::selector`] for more information.
278#[derive(Clone, Debug)]
279pub(crate) struct Selector<'a> {
280    pub(crate) layer: i8,
281    pub(crate) order: usize,
282    pub(crate) full: &'a str,
283    pub(crate) modifier: Modifier<'a>,
284    pub(crate) variants: Vec<Variant<'a>>,
285    pub(crate) is_important: bool,
286    pub(crate) plugin: &'static (dyn Plugin + Sync + Send),
287}
288
289impl PartialEq for Selector<'_> {
290    fn eq(&self, other: &Self) -> bool {
291        // Does not test order because it can change
292        self.full == other.full
293            && self.modifier == other.modifier
294            && self.variants == other.variants
295            && self.is_important == other.is_important
296            && self.layer == other.layer
297    }
298}
299
300impl Eq for Selector<'_> {}
301
302impl PartialOrd for Selector<'_> {
303    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
304        Some(self.cmp(other))
305    }
306}
307
308impl Ord for Selector<'_> {
309    fn cmp(&self, other: &Self) -> Ordering {
310        // We need to check the order as well as the strict egality because the PartialEq
311        // implementation for Selector does not check if two selectors have the same plugin which
312        // can lead two selectors having the same modifier to be recognized as the same although
313        // they use different plugins
314        if self.order == other.order && self == other {
315            return Ordering::Equal;
316        }
317
318        self.layer.cmp(&other.layer).then_with(|| {
319            if self.variants.is_empty() && !other.variants.is_empty() {
320                Ordering::Less
321            } else if !self.variants.is_empty() && other.variants.is_empty() {
322                Ordering::Greater
323            } else if !self.variants.is_empty() && !other.variants.is_empty() {
324                let mut compared = None;
325
326                // Compare variants in the lexicographic order
327                for variant_i in 0..self.variants.len() {
328                    if variant_i >= other.variants.len() {
329                        compared = Some(Ordering::Greater);
330                        break;
331                    }
332
333                    let res = self
334                        .variants
335                        .get(variant_i)
336                        .as_ref()
337                        .unwrap()
338                        .order
339                        .cmp(&other.variants.get(variant_i).unwrap().order);
340
341                    if res != Ordering::Equal {
342                        compared = Some(res);
343                        break;
344                    }
345                }
346
347                compared.unwrap_or(Ordering::Less).then_with(|| {
348                    self.order.cmp(&other.order).then_with(|| {
349                        self.full
350                            .cmp(other.full)
351                            .then_with(|| self.modifier.cmp(&other.modifier))
352                    })
353                })
354            } else {
355                self.order.cmp(&other.order).then_with(|| {
356                    self.full
357                        .cmp(other.full)
358                        .then_with(|| self.modifier.cmp(&other.modifier))
359                })
360            }
361        })
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use crate::{config::Config, selector::parse};
368
369    use std::collections::BTreeSet;
370
371    #[test]
372    fn sorting_test() {
373        let config = Config::default();
374
375        let selectors1 = parse(
376            "lg:bg-red-500",
377            None,
378            None,
379            &config,
380            &config.get_derived_variants(),
381        );
382        let selectors2 = parse(
383            "bg-red-500",
384            None,
385            None,
386            &config,
387            &config.get_derived_variants(),
388        );
389
390        let mut selectors = BTreeSet::new();
391        selectors.insert(selectors1[0].as_ref().unwrap());
392        selectors.insert(selectors2[0].as_ref().unwrap());
393
394        let mut iter = selectors.iter();
395        assert!(
396            iter.next().unwrap().full == "bg-red-500"
397                && iter.next().unwrap().full == "lg:bg-red-500"
398        );
399    }
400
401    #[test]
402    fn layers_test() {
403        let config = Config::default();
404
405        let selectors1 = parse(
406            "lg:bg-red-500",
407            None,
408            None,
409            &config,
410            &config.get_derived_variants(),
411        );
412        let mut selectors2 = parse(
413            "bg-red-500",
414            None,
415            None,
416            &config,
417            &config.get_derived_variants(),
418        );
419        selectors2[0].as_mut().unwrap().layer = 42;
420
421        let mut selectors = BTreeSet::new();
422        selectors.insert(selectors1[0].as_ref().unwrap());
423        selectors.insert(selectors2[0].as_ref().unwrap());
424
425        let mut iter = selectors.iter();
426        assert!(
427            iter.next().unwrap().full == "lg:bg-red-500"
428                && iter.next().unwrap().full == "bg-red-500"
429        );
430    }
431}