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//! - `"` is replaced by `"`
92//! - `'` is replaced by `'`
93//! - `(` is replaced by `(`
94//! - `)` is replaced by `)`
95//! - `[` is replaced by `[`
96//! - `\` is replaced by `\`
97//! - `]` is replaced by `]`
98//! - `_` is replaced by `_` (useful when a real `_` is needed instead of a space)
99//! - ``` 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}