grimoire_css_lib/core/
spell.rs

1//! This module defines the `Spell` struct, which represents a parsed CSS spell string.
2//!
3//! A spell is a complex string format used to encode CSS properties and their associated targets,
4//! including screen size (area), pseudo-classes (effects), and specific focus areas.
5//! The `Spell` struct provides methods to parse such a string into its components and store
6//! them in a structured way.
7//!
8//! # Example
9//!
10//! A spell string might look like this:
11//!
12//! ```text
13//! "md__{_>_p}hover:display=none"
14//! ```
15//!
16//! This string is parsed into the following components:
17//!
18//! * `area`: "md" (medium screen size)
19//! * `focus`: "_>_p" (a specific selector path)
20//! * `effects`: "hover" (pseudo-class)
21//! * `component`: "display" (CSS property)
22//! * `component_target`: "none" (CSS value)
23//!
24//! The `Spell` struct is created by passing a spell string to the `Spell::new` function, which
25//! parses the string and returns a `Result` containing either the parsed `Spell` or a `GrimoireCSSError`
26//! if the string format is invalid.
27
28use std::collections::{HashMap, HashSet};
29
30use super::GrimoireCssError;
31
32#[derive(Eq, Hash, PartialEq, Debug, Clone)]
33pub struct Spell {
34    pub raw_spell: String,
35    pub component: String,
36    pub component_target: String,
37    pub effects: String,
38    pub area: String,
39    pub focus: String,
40    pub with_template: bool,
41    pub scroll_spells: Option<Vec<Spell>>,
42}
43
44impl Spell {
45    /// Example input: "md__{_>_p}hover:display=none"
46    pub fn new(
47        raw_spell: &str,
48        shared_spells: &HashSet<String>,
49        scrolls: &Option<HashMap<String, Vec<String>>>,
50    ) -> Result<Option<Self>, GrimoireCssError> {
51        let with_template = Self::check_for_template(raw_spell);
52
53        let raw_spell = if with_template {
54            raw_spell
55                .strip_prefix("g!")
56                .and_then(|s| s.strip_suffix(";"))
57                .unwrap_or(raw_spell)
58        } else {
59            raw_spell
60        };
61
62        // Split the input string by "__" to separate the area (screen size) and the rest
63        let (area, rest) = raw_spell.split_once("__").unwrap_or(("", raw_spell));
64
65        // Split the raw spell by "}" to get the focus and the rest
66        let (focus, rest) = rest
67            .split_once('}')
68            .map_or(("", rest), |(f, r)| (f.strip_prefix('{').unwrap_or(f), r));
69
70        // Split the rest by ":" to get the effects (pseudo-class) and the rest
71        let (effects, rest) = rest.split_once(':').unwrap_or(("", rest));
72
73        // Split the rest by "=" to separate the component (property) and component_target (value)
74        if let Some((component, component_target)) = rest.split_once("=") {
75            let mut spell = Spell {
76                raw_spell: raw_spell.to_string(),
77                component: component.to_string(),
78                component_target: component_target.to_string(),
79                effects: effects.to_string(),
80                area: area.to_string(),
81                focus: focus.to_string(),
82                with_template,
83                scroll_spells: None,
84            };
85
86            if let Some(raw_scroll_spells) =
87                Self::check_raw_scroll_spells(&spell.component, scrolls)
88            {
89                spell.scroll_spells = Self::parse_scroll(
90                    component,
91                    raw_scroll_spells,
92                    &spell.component_target,
93                    shared_spells,
94                    scrolls,
95                )?;
96            }
97
98            return Ok(Some(spell));
99        } else if let Some(raw_scroll_spells) = Self::check_raw_scroll_spells(rest, scrolls) {
100            return Ok(Some(Spell {
101                raw_spell: raw_spell.to_string(),
102                component: rest.to_string(),
103                component_target: String::new(),
104                effects: effects.to_string(),
105                area: area.to_string(),
106                focus: focus.to_string(),
107                with_template,
108                scroll_spells: Self::parse_scroll(
109                    rest,
110                    raw_scroll_spells,
111                    "",
112                    shared_spells,
113                    scrolls,
114                )?,
115            }));
116        }
117
118        Ok(None) // Return None if format is invalid
119    }
120
121    fn check_for_template(class_name: &str) -> bool {
122        class_name.starts_with("g!") && class_name.ends_with(";")
123    }
124
125    fn check_raw_scroll_spells<'a>(
126        spell_component: &'a str,
127        scrolls: &'a Option<HashMap<String, Vec<String>>>,
128    ) -> Option<&'a Vec<String>> {
129        if let Some(scrolls) = scrolls {
130            return scrolls.get(spell_component);
131        };
132
133        None
134    }
135
136    fn parse_scroll(
137        scroll_name: &str,
138        raw_scroll_spells: &[String],
139        component_target: &str,
140        shared_spells: &HashSet<String>,
141        scrolls: &Option<HashMap<String, Vec<String>>>,
142    ) -> Result<Option<Vec<Spell>>, GrimoireCssError> {
143        if raw_scroll_spells.is_empty() {
144            return Ok(None);
145        }
146
147        let scroll_variables: Vec<&str> = component_target.split('_').collect();
148        let count_of_variables = if component_target.is_empty() {
149            0
150        } else {
151            scroll_variables.len()
152        };
153        let mut count_of_used_variables = 0;
154
155        let mut spells = Vec::with_capacity(raw_scroll_spells.len());
156
157        for raw_spell in raw_scroll_spells.iter() {
158            if raw_spell.contains("=$") {
159                if count_of_used_variables > scroll_variables.len() - 1 {
160                    break;
161                }
162
163                let variabled_raw_spell = raw_spell.replace(
164                    "=$",
165                    format!("={}", scroll_variables[count_of_used_variables]).as_str(),
166                );
167
168                if let Ok(Some(spell)) = Spell::new(&variabled_raw_spell, shared_spells, scrolls) {
169                    spells.push(spell);
170                }
171
172                count_of_used_variables += 1;
173            } else if let Ok(Some(spell)) = Spell::new(raw_spell, shared_spells, scrolls) {
174                spells.push(spell);
175            }
176        }
177
178        if count_of_used_variables != count_of_variables {
179            return Err(GrimoireCssError::InvalidInput(format!(
180                "Not all variables used in scroll '{}'. Expected {}, but used {}",
181                scroll_name, count_of_variables, count_of_used_variables,
182            )));
183        }
184
185        if spells.is_empty() {
186            Ok(None)
187        } else {
188            Ok(Some(spells))
189        }
190    }
191
192    pub fn generate_spells_from_classes(
193        css_classes: Vec<String>,
194        shared_spells: &HashSet<String>,
195        scrolls: &Option<HashMap<String, Vec<String>>>,
196    ) -> Result<Vec<Spell>, GrimoireCssError> {
197        let mut spells = Vec::with_capacity(css_classes.len());
198
199        for cs in css_classes {
200            if !shared_spells.contains(&cs) {
201                if let Some(spell) = Spell::new(&cs, shared_spells, scrolls)? {
202                    spells.push(spell);
203                }
204            }
205        }
206
207        Ok(spells)
208    }
209}