Skip to main content

atmos/os_lib/
dom_selector.rs

1// arm/v1/src/os_lib/dom_selector.rs
2//! W3C Selectors Level 3 / Level 4 準拠の DOM セレクタパーサ。
3//!
4//! `matches()`, `closest()`, `querySelector()`, `querySelectorAll()` で共有される
5//! 純粋なセレクタ字句解析・構文木構築ロジックを提供する。
6
7extern 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    /// `:not(sel1, sel2, ...)` の中身。
36    pub not: Vec<CompoundSel>,
37    /// `:is(sel1, sel2, ...)` / `:where(...)` の中身。
38    pub is_list: Vec<CompoundSel>,
39    /// `:first-child` / `:last-child` / `:only-child` / `:nth-child(...)`。
40    pub pos: Vec<PosPseudo>,
41    /// `:first-of-type` / `:last-of-type` / `:only-of-type` / `:nth-of-type(...)`。
42    pub pos_of_type: Vec<PosPseudo>,
43    /// `:scope`(呼び出し元の起点要素)。
44    pub is_scope: bool,
45    /// `:empty`(子要素および非空テキストを持たない要素)。
46    pub is_empty: bool,
47    /// `:root`(ドキュメントルート要素、通常 <html>)。
48    pub is_root: bool,
49    /// `:checked`(checkbox / radio の checked 属性またはプロパティ)。
50    pub is_checked: bool,
51    /// `:disabled`(disabled 属性を持つフォーム要素等)。
52    pub is_disabled: bool,
53    /// `:enabled`(disabled 属性を持たないフォーム要素等)。
54    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    /// `:nth-last-child()` / `:nth-last-of-type()`。
70    NthLast(NthFormula),
71}
72
73/// `:nth-child()` 引数(`odd`/`even`/`N`/`aN+b`/`aN-b`/`-aN+b`)を解析する。
74pub 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
102/// `position`(1始まり)が `an+b` 式(`n` は 0,1,2,...)のいずれかの `n` で表せるか判定する。
103pub 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    /// 直前のステップとの結合子(先頭ステップは `None`)。
124    pub combinator: Option<Combinator>,
125}
126
127/// `[attr...]` 1件を解析する。
128pub 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
156/// 単一コンパウンドセレクタ(`div.active#foo[data-x=y]:not(...)`)を解析する。
157pub 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'; // 't' = tag, '#' = id, '.' = class, ' ' = none
168    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
321/// トップレベルカンマで区切る。
322pub 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
346/// 1グループ分のセレクタ文字列をトークン分割する。
347pub 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
383/// トークン列から `SelStep` 列を組み立てる。
384pub 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}