rswind/process/
variant.rs

1use smallvec::SmallVec;
2use smol_str::{format_smolstr, SmolStr};
3use thiserror::Error;
4
5use crate::{
6    common::StrReplaceExt,
7    css::{rule::RuleList, Rule},
8    parsing::VariantCandidate,
9};
10
11#[rustfmt::skip]
12pub trait VariantMatchingFn: Fn(RuleList) -> Option<RuleList> + Sync + Send {}
13
14#[rustfmt::skip]
15impl<T: Fn(RuleList) -> Option<RuleList> + Sync + Send> VariantMatchingFn for T {}
16
17pub trait VariantHandlerExt {
18    fn handle(&self, candidate: &VariantCandidate<'_>, rule: RuleList) -> RuleList;
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum VariantKind {
23    Static,
24    Dynamic,
25    Composable,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum VariantHandler {
30    Static(StaticHandler),
31    Dynamic(DynamicHandler),
32    Composable(ComposableHandler),
33}
34
35impl VariantHandler {
36    pub fn take_composable(self) -> Option<ComposableHandler> {
37        match self {
38            Self::Composable(handler) => Some(handler),
39            _ => None,
40        }
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct Variant {
46    pub handler: VariantHandler,
47    pub composable: bool,
48    pub kind: VariantKind,
49    pub ordering: VariantOrdering,
50    pub nested: bool,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
54pub enum VariantOrdering {
55    Unset,
56    /// Insert order
57    Insertion(u64),
58    /// Length in pixels
59    Length(u64),
60    /// Arbitrary variant, place it at the end
61    Arbitrary,
62}
63
64#[derive(Debug, Error)]
65pub enum OrderingParseError {
66    #[error("Invalid unit, only `px` and `rem` are supported")]
67    InvalidUnit,
68    #[error("Invalid value {0}, expected a u64 integer")]
69    InvalidValue(#[from] std::num::ParseIntError),
70}
71
72static PX_PER_REM: u64 = 16;
73
74impl VariantOrdering {
75    pub fn from_length(s: &str) -> Result<Self, OrderingParseError> {
76        match s {
77            _ if s.ends_with("px") => {
78                let value = s.trim_end_matches("px").parse::<u64>()?;
79                Ok(Self::Length(value))
80            }
81            _ if s.ends_with("rem") => {
82                let value = s.trim_end_matches("rem").parse::<u64>()?;
83                Ok(Self::Length(value * PX_PER_REM))
84            }
85            _ => Err(OrderingParseError::InvalidUnit),
86        }
87    }
88}
89
90impl Variant {
91    pub fn new_static<T>(matcher: T) -> Self
92    where
93        T: IntoIterator,
94        T::Item: Into<SmolStr>,
95        T::IntoIter: ExactSizeIterator,
96    {
97        let handler = StaticHandler::new(matcher);
98        Self {
99            nested: handler.is_nested(),
100            handler: VariantHandler::Static(handler),
101            composable: true,
102            kind: VariantKind::Static,
103            ordering: VariantOrdering::Unset,
104        }
105    }
106
107    pub fn new_composable(handler: fn(RuleList, &VariantCandidate) -> RuleList) -> Self {
108        Self {
109            handler: VariantHandler::Composable(ComposableHandler::new(handler)),
110            composable: true,
111            kind: VariantKind::Composable,
112            ordering: VariantOrdering::Unset,
113            // composable variants are always nested
114            nested: false,
115        }
116    }
117
118    pub fn new_dynamic(handler: fn(RuleList, &VariantCandidate) -> RuleList, nested: bool) -> Self {
119        Self {
120            handler: VariantHandler::Dynamic(DynamicHandler::new(handler)),
121            composable: true,
122            kind: VariantKind::Dynamic,
123            ordering: VariantOrdering::Unset,
124            nested,
125        }
126    }
127
128    pub fn with_ordering(self, ordering: VariantOrdering) -> Self {
129        Self { ordering, ..self }
130    }
131
132    pub fn process(&self, candidate: &VariantCandidate<'_>, rule: RuleList) -> RuleList {
133        match &self.handler {
134            VariantHandler::Static(handler) => handler.handle(candidate, rule),
135            VariantHandler::Dynamic(handler) => handler.handle(candidate, rule),
136            VariantHandler::Composable(handler) => handler.handle(candidate, rule),
137        }
138    }
139
140    pub fn take_composable(&self) -> Option<&ComposableHandler> {
141        match &self.handler {
142            VariantHandler::Composable(handler) => Some(handler),
143            _ => None,
144        }
145    }
146
147    pub fn is_composable(&self) -> bool {
148        self.composable
149    }
150}
151
152impl VariantHandlerExt for Variant {
153    fn handle(&self, candidate: &VariantCandidate<'_>, rule: RuleList) -> RuleList {
154        self.process(candidate, rule)
155    }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash)]
159pub enum StaticHandler {
160    // for single rule
161    Selector(SmolStr),
162    // for single rule
163    PseudoElement(SmolStr),
164    // for multiple rules
165    Nested(SmolStr),
166    // for multiple rules
167    Duplicate(SmallVec<[SmolStr; 2]>),
168}
169
170impl StaticHandler {
171    pub fn new<T>(matcher: T) -> Self
172    where
173        T: IntoIterator,
174        T::Item: Into<SmolStr>,
175        T::IntoIter: ExactSizeIterator,
176    {
177        let mut iter = matcher.into_iter();
178        let is_duplicate = iter.len() > 1;
179        if !is_duplicate {
180            let next = iter.next().unwrap().into();
181            match next.chars().next() {
182                Some('&') => {
183                    if next.starts_with("&::") {
184                        Self::PseudoElement(next)
185                    } else {
186                        Self::Selector(next)
187                    }
188                }
189                Some('@') => Self::Nested(next),
190                _ => Self::Selector(format_smolstr!("&:is({})", next)),
191            }
192        } else {
193            Self::new_duplicate(iter)
194        }
195    }
196
197    pub fn new_duplicate<T>(matcher: T) -> Self
198    where
199        T: IntoIterator,
200        T::Item: Into<SmolStr>,
201    {
202        Self::Duplicate(matcher.into_iter().map(Into::into).collect())
203    }
204
205    pub fn is_nested(&self) -> bool {
206        matches!(self, Self::Nested(_))
207    }
208}
209
210impl VariantHandlerExt for StaticHandler {
211    fn handle(&self, _candidate: &VariantCandidate<'_>, rules: RuleList) -> RuleList {
212        match self {
213            Self::Selector(a) | Self::PseudoElement(a) => rules
214                .into_iter()
215                .map(|rule| rule.modify_with(|selector| selector.replace_char('&', a)))
216                .collect(),
217            Self::Nested(a) => RuleList::new(Rule { selector: a.clone(), decls: vec![], rules }),
218            Self::Duplicate(list) => list
219                .iter()
220                .flat_map(move |a| {
221                    rules
222                        .clone()
223                        .into_iter()
224                        .map(|rule| rule.modify_with(|selector| selector.replace_char('&', a)))
225                })
226                .collect(),
227        }
228    }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232pub struct DynamicHandler {
233    pub handler: fn(RuleList, &VariantCandidate) -> RuleList,
234    pub composable: bool,
235}
236
237impl VariantHandlerExt for DynamicHandler {
238    fn handle(&self, candidate: &VariantCandidate<'_>, rule: RuleList) -> RuleList {
239        (self.handler)(rule, candidate)
240    }
241}
242
243impl DynamicHandler {
244    pub fn new(handler: fn(RuleList, &VariantCandidate) -> RuleList) -> Self {
245        Self { handler, composable: true }
246    }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub struct ComposableHandler {
251    pub handler: fn(RuleList, &VariantCandidate) -> RuleList,
252    pub composable: bool,
253}
254
255impl ComposableHandler {
256    pub fn new(handler: fn(RuleList, &VariantCandidate) -> RuleList) -> Self {
257        Self { handler, composable: true }
258    }
259
260    pub fn composable(self) -> Self {
261        Self { composable: true, ..self }
262    }
263}
264
265impl VariantHandlerExt for ComposableHandler {
266    fn handle(&self, candidate: &VariantCandidate<'_>, rule: RuleList) -> RuleList {
267        (self.handler)(rule, candidate)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use rswind_css_macro::css;
274    use smol_str::format_smolstr;
275
276    use super::{DynamicHandler, VariantHandlerExt};
277    use crate::{
278        context::DesignSystem,
279        css::{rule::RuleList, Decl, Rule},
280        parsing::candidate::CandidateParser,
281    };
282
283    #[test]
284    fn test_variant_process() {
285        let mut design = DesignSystem::default();
286        design.add_variant("hover", ["&:hover"]);
287        design.add_variant("active", ["&:active"]);
288
289        let candidates = vec![
290            CandidateParser::new("hover").parse_variant(&design.variants).unwrap(),
291            CandidateParser::new("active").parse_variant(&design.variants).unwrap(),
292        ];
293
294        let _input = css! {
295            ".flex" {
296                "display": "flex";
297            }
298        };
299
300        let selector = RuleList::new(Rule {
301            selector: "&".into(),
302            rules: RuleList::default(),
303            decls: vec![Decl { name: "display".into(), value: "flex".into() }],
304        });
305
306        let _res = candidates
307            .into_iter()
308            .map(|candidate| {
309                let processor = design.variants.get(candidate.key).unwrap();
310                (processor, candidate)
311            })
312            .fold(selector, |acc, (processor, candidate)| processor.process(&candidate, acc));
313    }
314
315    #[test]
316    fn test_dynamic_process() {
317        let mut design = DesignSystem::default();
318        design.add_variant("hover", ["&:hover"]);
319        design.add_variant("active", ["&:active"]);
320
321        let candidate = CandidateParser::new("hover").parse_variant(&design.variants).unwrap();
322        let input = css! {
323            ".flex" {
324                "display": "flex";
325            }
326        }
327        .to_rule_list();
328        // @media (hover: hover) and (pointer: fine) | &:hover
329        let variant = DynamicHandler::new(|rule, _| {
330            let hovered = rule
331                .into_iter()
332                .map(|rule| rule.modify_with(|s| format_smolstr!("{}:hover", s)))
333                .collect();
334            Rule {
335                selector: "@media (hover: hover) and (pointer: fine)".into(),
336                decls: vec![],
337                rules: hovered,
338            }
339            .to_rule_list()
340        });
341
342        let res = variant.handle(&candidate, input);
343
344        println!("{:#?}", res);
345    }
346}