1extern crate alloc;
8
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum AttrOp {
14 Exists,
15 Eq,
16 Prefix,
17 Suffix,
18 Contains,
19 Word,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct AttrSel {
24 pub name: String,
25 pub op: AttrOp,
26 pub value: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct CompoundSel {
31 pub tag: Option<String>,
32 pub id: Option<String>,
33 pub classes: Vec<String>,
34 pub attrs: Vec<AttrSel>,
35 pub not: Vec<CompoundSel>,
37 pub is_list: Vec<CompoundSel>,
39 pub pos: Vec<PosPseudo>,
41 pub pos_of_type: Vec<PosPseudo>,
43 pub is_scope: bool,
45 pub is_empty: bool,
47 pub is_root: bool,
49 pub is_checked: bool,
51 pub is_disabled: bool,
53 pub is_enabled: bool,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct NthFormula {
59 pub a: i32,
60 pub b: i32,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum PosPseudo {
65 First,
66 Last,
67 Only,
68 Nth(NthFormula),
69 NthLast(NthFormula),
71}
72
73pub fn parse_nth_formula(s: &str) -> Option<NthFormula> {
75 let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
76 let s = s.to_lowercase();
77 if s == "odd" {
78 return Some(NthFormula { a: 2, b: 1 });
79 }
80 if s == "even" {
81 return Some(NthFormula { a: 2, b: 0 });
82 }
83 if let Some(n_pos) = s.find('n') {
84 let a_part = s.get(..n_pos).unwrap_or("");
85 let a = match a_part {
86 "" | "+" => 1,
87 "-" => -1,
88 _ => a_part.parse::<i32>().ok()?,
89 };
90 let b_part = s.get(n_pos + 1..).unwrap_or("");
91 let b = if b_part.is_empty() {
92 0
93 } else {
94 b_part.parse::<i32>().ok()?
95 };
96 return Some(NthFormula { a, b });
97 }
98 let n = s.parse::<i32>().ok()?;
99 Some(NthFormula { a: 0, b: n })
100}
101
102pub fn nth_formula_matches(f: NthFormula, position: usize) -> bool {
104 let p = position as i32;
105 if f.a == 0 {
106 return p == f.b;
107 }
108 let diff = p - f.b;
109 diff % f.a == 0 && diff / f.a >= 0
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Combinator {
114 Descendant,
115 Child,
116 Adjacent,
117 General,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct SelStep {
122 pub compound: CompoundSel,
123 pub combinator: Option<Combinator>,
125}
126
127pub fn parse_attr_sel(inner: &str) -> Option<AttrSel> {
129 let inner = inner.trim();
130 for (token, op) in [
131 ("^=", AttrOp::Prefix),
132 ("$=", AttrOp::Suffix),
133 ("*=", AttrOp::Contains),
134 ("~=", AttrOp::Word),
135 ("=", AttrOp::Eq),
136 ] {
137 if let Some((name, val)) = inner.split_once(token) {
138 let val = val.trim().trim_matches(|c| c == '"' || c == '\'');
139 return Some(AttrSel {
140 name: name.trim().to_string(),
141 op,
142 value: val.to_string(),
143 });
144 }
145 }
146 if inner.is_empty() {
147 return None;
148 }
149 Some(AttrSel {
150 name: inner.to_string(),
151 op: AttrOp::Exists,
152 value: String::new(),
153 })
154}
155
156pub fn parse_compound(s: &str) -> Option<CompoundSel> {
158 let s = s.trim();
159 if s.is_empty() {
160 return None;
161 }
162 if s == "*" {
163 return Some(CompoundSel::default());
164 }
165 let mut c = CompoundSel::default();
166 let mut cur = String::new();
167 let mut mode = 't'; let chars: Vec<char> = s.chars().collect();
169 let mut i = 0;
170 let flush = |mode: char, cur: &mut String, c: &mut CompoundSel| {
171 if cur.is_empty() {
172 return;
173 }
174 match mode {
175 't' => {
176 if cur.as_str() != "*" {
177 c.tag = Some(cur.to_lowercase());
178 }
179 }
180 '#' => c.id = Some(cur.clone()),
181 '.' => c.classes.push(cur.clone()),
182 _ => {}
183 }
184 cur.clear();
185 };
186 while i < chars.len() {
187 let ch = chars[i];
188 match ch {
189 '#' | '.' => {
190 flush(mode, &mut cur, &mut c);
191 mode = ch;
192 i += 1;
193 }
194 '[' => {
195 flush(mode, &mut cur, &mut c);
196 let start = i + 1;
197 let mut depth = 1;
198 let mut j = start;
199 while j < chars.len() && depth > 0 {
200 match chars[j] {
201 '[' => depth += 1,
202 ']' => depth -= 1,
203 _ => {}
204 }
205 if depth > 0 {
206 j += 1;
207 }
208 }
209 let inner: String = chars[start..j.min(chars.len())].iter().collect();
210 if let Some(attr) = parse_attr_sel(&inner) {
211 c.attrs.push(attr);
212 }
213 i = if j < chars.len() { j + 1 } else { j };
214 mode = ' ';
215 }
216 ':' => {
217 flush(mode, &mut cur, &mut c);
218 let mut j = i + 1;
219 let name_start = j;
220 while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '-') {
221 j += 1;
222 }
223 let name: String = chars[name_start..j].iter().collect::<String>().to_lowercase();
224 if j < chars.len() && chars[j] == '(' {
225 let paren_start = j + 1;
226 let mut depth = 1;
227 let mut k = paren_start;
228 while k < chars.len() && depth > 0 {
229 match chars[k] {
230 '(' => depth += 1,
231 ')' => depth -= 1,
232 _ => {}
233 }
234 if depth > 0 {
235 k += 1;
236 }
237 }
238 if name == "not" {
239 let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
240 for part in inner.split(',') {
241 if let Some(inner_compound) = parse_compound(part.trim()) {
242 c.not.push(inner_compound);
243 }
244 }
245 } else if name == "nth-child" {
246 let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
247 if let Some(formula) = parse_nth_formula(&inner) {
248 c.pos.push(PosPseudo::Nth(formula));
249 }
250 } else if name == "nth-of-type" {
251 let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
252 if let Some(formula) = parse_nth_formula(&inner) {
253 c.pos_of_type.push(PosPseudo::Nth(formula));
254 }
255 } else if name == "nth-last-child" {
256 let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
257 if let Some(formula) = parse_nth_formula(&inner) {
258 c.pos.push(PosPseudo::NthLast(formula));
259 }
260 } else if name == "nth-last-of-type" {
261 let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
262 if let Some(formula) = parse_nth_formula(&inner) {
263 c.pos_of_type.push(PosPseudo::NthLast(formula));
264 }
265 } else if name == "is" || name == "where" {
266 let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
267 for part in inner.split(',') {
268 if let Some(inner_compound) = parse_compound(part.trim()) {
269 c.is_list.push(inner_compound);
270 }
271 }
272 }
273 j = if k < chars.len() { k + 1 } else { k };
274 } else {
275 match name.as_str() {
276 "first-child" => c.pos.push(PosPseudo::First),
277 "last-child" => c.pos.push(PosPseudo::Last),
278 "only-child" => c.pos.push(PosPseudo::Only),
279 "first-of-type" => c.pos_of_type.push(PosPseudo::First),
280 "last-of-type" => c.pos_of_type.push(PosPseudo::Last),
281 "only-of-type" => c.pos_of_type.push(PosPseudo::Only),
282 "scope" => c.is_scope = true,
283 "empty" => c.is_empty = true,
284 "root" => c.is_root = true,
285 "checked" => c.is_checked = true,
286 "disabled" => c.is_disabled = true,
287 "enabled" => c.is_enabled = true,
288 _ => {}
289 }
290 }
291 i = j;
292 mode = ' ';
293 }
294 _ => {
295 cur.push(ch);
296 i += 1;
297 }
298 }
299 }
300 flush(mode, &mut cur, &mut c);
301 if c.tag.is_none()
302 && c.id.is_none()
303 && c.classes.is_empty()
304 && c.attrs.is_empty()
305 && c.not.is_empty()
306 && c.is_list.is_empty()
307 && c.pos.is_empty()
308 && c.pos_of_type.is_empty()
309 && !c.is_scope
310 && !c.is_empty
311 && !c.is_root
312 && !c.is_checked
313 && !c.is_disabled
314 && !c.is_enabled
315 {
316 return None;
317 }
318 Some(c)
319}
320
321pub fn split_top_level_commas(s: &str) -> Vec<String> {
323 let mut parts = Vec::new();
324 let mut cur = String::new();
325 let mut depth = 0i32;
326 for ch in s.chars() {
327 match ch {
328 '[' | '(' => {
329 depth += 1;
330 cur.push(ch);
331 }
332 ']' | ')' => {
333 depth -= 1;
334 cur.push(ch);
335 }
336 ',' if depth == 0 => {
337 parts.push(core::mem::take(&mut cur));
338 }
339 _ => cur.push(ch),
340 }
341 }
342 parts.push(cur);
343 parts
344}
345
346pub fn tokenize_selector_group(s: &str) -> Vec<String> {
348 let mut tokens = Vec::new();
349 let mut cur = String::new();
350 let mut depth = 0i32;
351 for ch in s.chars() {
352 match ch {
353 '[' | '(' => {
354 depth += 1;
355 cur.push(ch);
356 }
357 ']' | ')' => {
358 depth -= 1;
359 cur.push(ch);
360 }
361 '>' | '+' | '~' if depth == 0 => {
362 if !cur.trim().is_empty() {
363 tokens.push(cur.trim().to_string());
364 cur.clear();
365 }
366 tokens.push(ch.to_string());
367 }
368 c if c.is_whitespace() && depth == 0 => {
369 if !cur.trim().is_empty() {
370 tokens.push(cur.trim().to_string());
371 cur.clear();
372 }
373 }
374 _ => cur.push(ch),
375 }
376 }
377 if !cur.trim().is_empty() {
378 tokens.push(cur.trim().to_string());
379 }
380 tokens
381}
382
383pub fn build_steps(tokens: &[String]) -> Vec<SelStep> {
385 let mut steps: Vec<SelStep> = Vec::new();
386 let mut pending: Option<Combinator> = None;
387 for tok in tokens {
388 match tok.as_str() {
389 ">" => pending = Some(Combinator::Child),
390 "+" => pending = Some(Combinator::Adjacent),
391 "~" => pending = Some(Combinator::General),
392 _ => {
393 let combinator = if steps.is_empty() {
394 None
395 } else {
396 Some(pending.take().unwrap_or(Combinator::Descendant))
397 };
398 if let Some(compound) = parse_compound(tok) {
399 steps.push(SelStep { compound, combinator });
400 }
401 }
402 }
403 }
404 steps
405}