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//! - `"` 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 `]`
99//! - `_` is replaced by `_` (useful when a real `_` is needed instead of a space)
100//! - ``` 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}