1use std::collections::{HashMap, HashSet};
29
30use super::{GrimoireCssError, component::get_css_property};
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 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 let raw_spell = if with_template {
53 raw_spell
54 .strip_prefix("g!")
55 .and_then(|s| s.strip_suffix(";"))
56 .unwrap_or(raw_spell)
57 } else {
58 raw_spell
59 };
60
61 let raw_spell_split: Vec<&str> = raw_spell.split("--").filter(|s| !s.is_empty()).collect();
62
63 if with_template && !raw_spell_split.is_empty() {
64 let mut scroll_spells: Vec<Spell> = Vec::new();
65 for rs in raw_spell_split {
66 if let Some(spell) = Spell::new(rs, shared_spells, scrolls)? {
67 scroll_spells.push(spell);
68 }
69 }
70
71 return Ok(Some(Spell {
72 raw_spell: raw_spell.to_string(),
73 component: String::new(),
74 component_target: String::new(),
75 effects: String::new(),
76 area: String::new(),
77 focus: String::new(),
78 with_template,
79 scroll_spells: Some(scroll_spells),
80 }));
81 }
82
83 let (area, rest) = raw_spell.split_once("__").unwrap_or(("", raw_spell));
85
86 let (focus, rest) = rest
88 .split_once('}')
89 .map_or(("", rest), |(f, r)| (f.strip_prefix('{').unwrap_or(f), r));
90
91 let (effects, rest) = rest.split_once(':').unwrap_or(("", rest));
93
94 if let Some((component, component_target)) = rest.split_once("=") {
96 let mut spell = Spell {
97 raw_spell: raw_spell.to_string(),
98 component: component.to_string(),
99 component_target: component_target.to_string(),
100 effects: effects.to_string(),
101 area: area.to_string(),
102 focus: focus.to_string(),
103 with_template,
104 scroll_spells: None,
105 };
106
107 if let Some(raw_scroll_spells) =
108 Self::check_raw_scroll_spells(&spell.component, scrolls)
109 {
110 spell.scroll_spells = Self::parse_scroll(
111 component,
112 raw_scroll_spells,
113 &spell.component_target,
114 shared_spells,
115 scrolls,
116 )?;
117 }
118
119 return Ok(Some(spell));
120 } else if let Some(raw_scroll_spells) = Self::check_raw_scroll_spells(rest, scrolls) {
121 return Ok(Some(Spell {
122 raw_spell: raw_spell.to_string(),
123 component: rest.to_string(),
124 component_target: String::new(),
125 effects: effects.to_string(),
126 area: area.to_string(),
127 focus: focus.to_string(),
128 with_template,
129 scroll_spells: Self::parse_scroll(
130 rest,
131 raw_scroll_spells,
132 "",
133 shared_spells,
134 scrolls,
135 )?,
136 }));
137 }
138
139 Ok(None) }
141
142 fn check_for_template(class_name: &str) -> bool {
143 class_name.starts_with("g!") && class_name.ends_with(";")
144 }
145
146 fn check_raw_scroll_spells<'a>(
147 spell_component: &'a str,
148 scrolls: &'a Option<HashMap<String, Vec<String>>>,
149 ) -> Option<&'a Vec<String>> {
150 if get_css_property(spell_component).is_some() {
151 return None;
152 }
153
154 if let Some(scrolls) = scrolls {
155 return scrolls.get(spell_component);
156 };
157
158 None
159 }
160
161 fn parse_scroll(
162 scroll_name: &str,
163 raw_scroll_spells: &[String],
164 component_target: &str,
165 shared_spells: &HashSet<String>,
166 scrolls: &Option<HashMap<String, Vec<String>>>,
167 ) -> Result<Option<Vec<Spell>>, GrimoireCssError> {
168 if raw_scroll_spells.is_empty() {
169 return Ok(None);
170 }
171
172 let scroll_variables: Vec<&str> = component_target.split('_').collect();
173 let count_of_variables = if component_target.is_empty() {
174 0
175 } else {
176 scroll_variables.len()
177 };
178 let mut count_of_used_variables = 0;
179
180 let mut spells = Vec::with_capacity(raw_scroll_spells.len());
181
182 for raw_spell in raw_scroll_spells.iter() {
183 if raw_spell.contains("=$") {
184 if count_of_used_variables > scroll_variables.len() - 1 {
185 break;
186 }
187
188 let variabled_raw_spell = raw_spell.replace(
189 "=$",
190 format!("={}", scroll_variables[count_of_used_variables]).as_str(),
191 );
192
193 if let Ok(Some(spell)) = Spell::new(&variabled_raw_spell, shared_spells, scrolls) {
194 spells.push(spell);
195 }
196
197 count_of_used_variables += 1;
198 } else if let Ok(Some(spell)) = Spell::new(raw_spell, shared_spells, scrolls) {
199 spells.push(spell);
200 }
201 }
202
203 if count_of_used_variables != count_of_variables {
204 return Err(GrimoireCssError::InvalidInput(format!(
205 "Not all variables used in scroll '{scroll_name}'. Expected {count_of_variables}, but used {count_of_used_variables}",
206 )));
207 }
208
209 if spells.is_empty() {
210 Ok(None)
211 } else {
212 Ok(Some(spells))
213 }
214 }
215
216 pub fn generate_spells_from_classes(
217 css_classes: Vec<String>,
218 shared_spells: &HashSet<String>,
219 scrolls: &Option<HashMap<String, Vec<String>>>,
220 ) -> Result<Vec<Spell>, GrimoireCssError> {
221 let mut spells = Vec::with_capacity(css_classes.len());
222
223 for cs in css_classes {
224 if !shared_spells.contains(&cs) {
225 if let Some(spell) = Spell::new(&cs, shared_spells, scrolls)? {
226 spells.push(spell);
227 }
228 }
229 }
230
231 Ok(spells)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use crate::core::spell::Spell;
238 use std::collections::{HashMap, HashSet};
239
240 #[test]
241 fn test_multiple_raw_spells_in_template() {
242 let shared_spells = HashSet::new();
243 let scrolls: Option<HashMap<String, Vec<String>>> = None;
244 let raw = "g!color=red--display=flex;";
245 let spell = Spell::new(raw, &shared_spells, &scrolls)
246 .expect("parse ok")
247 .expect("not None");
248 assert!(spell.with_template);
249 assert!(spell.scroll_spells.is_some());
250 let spells = spell.scroll_spells.as_ref().unwrap();
251 assert_eq!(spells.len(), 2);
252 assert_eq!(spells[0].component, "color");
253 assert_eq!(spells[0].component_target, "red");
254 assert_eq!(spells[1].component, "display");
255 assert_eq!(spells[1].component_target, "flex");
256 }
257}