Skip to main content

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