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::VariantType)** (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/// Structure used to add pseudo-selectors, pseudo-elements, pseudo classes and media queries to
152/// CSS rules.
153///
154/// Variant are useful to <i>conditionally</i> apply utility classes.
155///
156/// See [`config::BUILTIN_VARIANTS`] for a list of all default variants.
157///
158/// See [Tailwind's documentation](https://tailwindcss.com/docs/hover-focus-and-other-states) to learn more about variants.
159///
160/// [`config::BUILTIN_VARIANTS`]: crate::config::BUILTIN_VARIANTS
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum VariantType {
163 /// A CSS [pseudo element](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements).
164 ///
165 /// # Example
166 ///
167 /// If the variant is `VariantType::PseudoClass("before")` and the original class is `".bg-red-500"`, the class will become `".bg-red-500::before"`).
168 PseudoElement(&'static str),
169
170 /// A CSS [pseudo class](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes).
171 ///
172 /// # Example
173 ///
174 /// If the variant is `VariantType::PseudoClass("hover")` and the original class is `".bg-red-500"`, the class will become `".bg-red-500:hover"`).
175 PseudoClass(&'static str),
176
177 /// Wrap the original class to make another one.
178 ///
179 /// # Example
180 ///
181 /// If the variant is `VariantType::WrapClass("&[open]")` and the original class is `".bg-red-500"`, the class will become `".bg-red-500[open]"`).
182 WrapClass(Cow<'static, str>),
183
184 /// Add a `@` CSS rule (like `@media`, `@supports`).
185 ///
186 /// # Example
187 ///
188 /// If the variant is `VariantType::AtRule("@media (orientation: portrait)")` and the original
189 /// class is `".bg-red-500"`, the class will become `"@media (orientation: portrait) { .bg-red-500 { ... } }"`).
190 AtRule(Cow<'static, str>),
191
192 /// A variant applied to a group element like `group-hover`.
193 ///
194 /// This variant should not be built manually.
195 Group(&'static str),
196
197 /// A variant applied to a peer element like `peer-focus`.
198 ///
199 /// This variant should not be built manually.
200 Peer(&'static str),
201
202 /// A negated variant applied to a peer element like `peer-not-hover`.
203 ///
204 /// This variant should not be built manually.
205 PeerNot(&'static str),
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub(crate) enum Variant<'a> {
210 Builtin(usize, VariantType),
211 Arbitrary(Cow<'a, str>),
212}
213
214/// A parsed selector, aka a utility class, containing the variants, the namespace and the modifier.
215///
216/// See [`crate::selector`] for more informations.
217#[derive(Clone, Debug)]
218pub(crate) struct Selector<'a> {
219 pub(crate) order: usize,
220 pub(crate) full: &'a str,
221 pub(crate) modifier: Modifier<'a>,
222 pub(crate) variants: Vec<Variant<'a>>,
223 pub(crate) is_important: bool,
224 pub(crate) plugin: &'static (dyn Plugin + Sync + Send),
225}
226
227#[cfg(not(test))]
228impl<'a> PartialEq for Selector<'a> {
229 fn eq(&self, other: &Self) -> bool {
230 self.full == other.full
231 }
232}
233
234// Use a stricter implementation when testing
235#[cfg(test)]
236impl<'a> PartialEq for Selector<'a> {
237 fn eq(&self, other: &Self) -> bool {
238 // Does not test order because it can change
239 self.full == other.full
240 && self.modifier == other.modifier
241 && self.variants == other.variants
242 && self.is_important == other.is_important
243 }
244}
245
246impl<'a> Eq for Selector<'a> {}
247
248impl<'a> PartialOrd for Selector<'a> {
249 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
250 Some(self.cmp(other))
251 }
252}
253
254impl<'a> Ord for Selector<'a> {
255 fn cmp(&self, other: &Self) -> Ordering {
256 if self.variants.is_empty() && !other.variants.is_empty() {
257 Ordering::Less
258 } else if !self.variants.is_empty() && other.variants.is_empty() {
259 Ordering::Greater
260 } else if !self.variants.is_empty() && !other.variants.is_empty() {
261 let mut compared = None;
262
263 // Compare variants in the lexicographic order
264 for variant_i in 0..self.variants.len() {
265 if variant_i >= other.variants.len() {
266 compared = Some(Ordering::Greater);
267 break;
268 }
269
270 let res = match self.variants.get(variant_i).as_ref().unwrap() {
271 Variant::Builtin(order, _) => order,
272 Variant::Arbitrary(_) => &1_000_000,
273 }
274 .cmp(&match other.variants.get(variant_i).unwrap() {
275 Variant::Builtin(order, _) => *order,
276 Variant::Arbitrary(_) => 1_000_001,
277 });
278
279 if res != Ordering::Equal {
280 compared = Some(res);
281 break;
282 }
283 }
284
285 compared.unwrap_or(Ordering::Less).then_with(|| {
286 self.order
287 .cmp(&other.order)
288 .then_with(|| self.full.cmp(other.full))
289 })
290 } else {
291 self.order
292 .cmp(&other.order)
293 .then_with(|| self.full.cmp(other.full))
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use crate::{config::Config, selector::parse};
301
302 use std::collections::BTreeSet;
303
304 #[test]
305 fn sorting_test() {
306 let config = Config::default();
307
308 let selectors1 = parse(
309 "lg:bg-red-500",
310 None,
311 None,
312 &config,
313 &config.get_derived_variants(),
314 );
315 let selectors2 = parse(
316 "bg-red-500",
317 None,
318 None,
319 &config,
320 &config.get_derived_variants(),
321 );
322
323 let mut selectors = BTreeSet::new();
324 selectors.insert(selectors1[0].as_ref().unwrap());
325 selectors.insert(selectors2[0].as_ref().unwrap());
326
327 let mut iter = selectors.iter();
328 assert!(
329 iter.next().unwrap().full == "bg-red-500"
330 && iter.next().unwrap().full == "lg:bg-red-500"
331 );
332 }
333}