Skip to main content

atmos/os_lib/css/
selector.rs

1// 分割: css.rs より機械的に移動(2026-07-16 リファクタ フェーズ3)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5pub(crate) fn parse_selector(s: &str) -> Option<Selector> {
6    let chain = parse_selector_chain(s)?;
7    if chain.len() == 1 && chain[0].combinator.is_none() {
8        Some(Selector::Simple(chain[0].simple.clone()))
9    } else {
10        Some(Selector::Chain(chain))
11    }
12}
13
14#[inline]
15fn is_ident_byte(b: u8) -> bool {
16    b.is_ascii_alphanumeric() || b == b'-' || b == b'_'
17}
18
19pub(crate) fn parse_simple_selector(s: &str) -> Option<SimpleSelector> {
20    if s.is_empty() {
21        return None;
22    }
23
24    let mut selector = SimpleSelector::default();
25    let bytes = s.as_bytes();
26    let mut i = 0usize;
27    while i < bytes.len() {
28        match bytes[i] {
29            b'#' => {
30                i += 1;
31                let start = i;
32                while i < bytes.len() && is_ident_byte(bytes[i]) {
33                    i += 1;
34                }
35                if start < i {
36                    if let Some(id_str) = s.get(start..i) {
37                        selector.id = Some(String::from(id_str));
38                    }
39                }
40            }
41            b'.' => {
42                i += 1;
43                let start = i;
44                while i < bytes.len() && is_ident_byte(bytes[i]) {
45                    i += 1;
46                }
47                if start < i {
48                    if let Some(cls_str) = s.get(start..i) {
49                        selector.class.push(String::from(cls_str));
50                    }
51                }
52            }
53            b'*' => {
54                selector.universal = true;
55                i += 1;
56            }
57            b'[' => {
58                i += 1;
59                let inner_start = i;
60                while i < bytes.len() && bytes[i] != b']' {
61                    i += 1;
62                }
63                let inner = s.get(inner_start..i).unwrap_or("");
64                if i < bytes.len() {
65                    i += 1;
66                } // ']'
67                if let Some(attr) = parse_attr_selector(inner) {
68                    selector.attrs.push(attr);
69                }
70            }
71            b':' => {
72                i += 1;
73                if i < bytes.len() && bytes[i] == b':' {
74                    i += 1;
75                }
76                let start = i;
77                while i < bytes.len() && is_ident_byte(bytes[i]) {
78                    i += 1;
79                }
80                let name = s.get(start..i).unwrap_or("").to_ascii_lowercase();
81                let mut arg = String::new();
82                if i < bytes.len() && bytes[i] == b'(' {
83                    i += 1;
84                    let arg_start = i;
85                    let mut depth = 1;
86                    while i < bytes.len() && depth > 0 {
87                        match bytes[i] {
88                            b'(' => depth += 1,
89                            b')' => {
90                                depth -= 1;
91                                if depth == 0 {
92                                    break;
93                                }
94                            }
95                            _ => {}
96                        }
97                        i += 1;
98                    }
99                    arg = String::from(s.get(arg_start..i).unwrap_or(""));
100                    if i < bytes.len() {
101                        i += 1;
102                    } // ')'
103                }
104                if !name.is_empty() {
105                    match name.as_str() {
106                        "before" => selector.pseudo_element = Some(PseudoElement::Before),
107                        "after" => selector.pseudo_element = Some(PseudoElement::After),
108                        "marker" => selector.pseudo_element = Some(PseudoElement::Marker),
109                        "placeholder" => selector.pseudo_element = Some(PseudoElement::Placeholder),
110                        "first-line" => selector.pseudo_element = Some(PseudoElement::FirstLine),
111                        "first-letter" => selector.pseudo_element = Some(PseudoElement::FirstLetter),
112                        _ => {
113                            let pseudo = match name.as_str() {
114                                "first-child" => PseudoClass::FirstChild,
115                                "last-child" => PseudoClass::LastChild,
116                                "nth-child" => {
117                                    let (a, b, sel) = parse_nth_of(&arg);
118                                    PseudoClass::NthChild(a, b, sel.map(alloc::boxed::Box::new))
119                                }
120                                "nth-last-child" => {
121                                    let (a, b, sel) = parse_nth_of(&arg);
122                                    PseudoClass::NthLastChild(
123                                        a,
124                                        b,
125                                        sel.map(alloc::boxed::Box::new),
126                                    )
127                                }
128                                "not" => match parse_simple_selector(arg.trim()) {
129                                    Some(inner) => PseudoClass::Not(alloc::boxed::Box::new(inner)),
130                                    None => PseudoClass::Other(name),
131                                },
132                                "is" | "where" | "matches" | "any" => {
133                                    let is_where = name == "where";
134                                    let list: Vec<SimpleSelector> = split_top_level_commas(&arg)
135                                        .iter()
136                                        .filter_map(|s| parse_simple_selector(s.trim()))
137                                        .collect();
138                                    if list.is_empty() {
139                                        PseudoClass::Other(name)
140                                    } else if is_where {
141                                        PseudoClass::Where(list)
142                                    } else {
143                                        PseudoClass::Is(list)
144                                    }
145                                }
146                                "has" => {
147                                    let list: Vec<(HasCombinator, SimpleSelector)> =
148                                        split_top_level_commas(&arg)
149                                            .iter()
150                                            .filter_map(|s| {
151                                                let s = s.trim();
152                                                let (comb, rest) = match s.strip_prefix('>') {
153                                                    Some(r) => (HasCombinator::Child, r.trim()),
154                                                    None => match s.strip_prefix('+') {
155                                                        Some(r) => {
156                                                            (HasCombinator::NextSibling, r.trim())
157                                                        }
158                                                        None => match s.strip_prefix('~') {
159                                                            Some(r) => (
160                                                                HasCombinator::SubsequentSibling,
161                                                                r.trim(),
162                                                            ),
163                                                            None => {
164                                                                (HasCombinator::Descendant, s)
165                                                            }
166                                                        },
167                                                    },
168                                                };
169                                                parse_simple_selector(rest).map(|sel| (comb, sel))
170                                            })
171                                            .collect();
172                                    if list.is_empty() {
173                                        PseudoClass::Other(name)
174                                    } else {
175                                        PseudoClass::Has(list)
176                                    }
177                                }
178                                "checked" => PseudoClass::Checked,
179                                "disabled" => PseudoClass::Disabled,
180                                "lang" => {
181                                    let code = arg
182                                        .trim()
183                                        .trim_matches(|c| c == '\'' || c == '"')
184                                        .to_ascii_lowercase();
185                                    if code.is_empty() {
186                                        PseudoClass::Other(name)
187                                    } else {
188                                        PseudoClass::Lang(code)
189                                    }
190                                }
191                                "dir" => {
192                                    let d = arg.trim().to_ascii_lowercase();
193                                    if d == "ltr" || d == "rtl" {
194                                        PseudoClass::Dir(d)
195                                    } else {
196                                        PseudoClass::Other(name)
197                                    }
198                                }
199                                "first-of-type" => PseudoClass::FirstOfType,
200                                "last-of-type" => PseudoClass::LastOfType,
201                                "nth-of-type" => {
202                                    let (a, b) = parse_nth(&arg);
203                                    PseudoClass::NthOfType(a, b)
204                                }
205                                "nth-last-of-type" => {
206                                    let (a, b) = parse_nth(&arg);
207                                    PseudoClass::NthLastOfType(a, b)
208                                }
209                                "only-of-type" => PseudoClass::OnlyOfType,
210                                "only-child" => PseudoClass::OnlyChild,
211                                "target" => PseudoClass::Target,
212                                "empty" => PseudoClass::Empty,
213                                _ => PseudoClass::Other(name),
214                            };
215                            selector.pseudo_classes.push(pseudo);
216                        }
217                    }
218                }
219            }
220            b if is_ident_byte(b) => {
221                let start = i;
222                while i < bytes.len() && is_ident_byte(bytes[i]) {
223                    i += 1;
224                }
225                if start < i {
226                    if let Some(tag_str) = s.get(start..i) {
227                        selector.tag_name = Some(tag_str.to_ascii_lowercase());
228                    }
229                }
230            }
231            _ => i += 1,
232        }
233    }
234
235    if !selector.universal
236        && selector.tag_name.is_none()
237        && selector.id.is_none()
238        && selector.class.is_empty()
239        && selector.pseudo_classes.is_empty()
240        && selector.attrs.is_empty()
241    {
242        None
243    } else {
244        Some(selector)
245    }
246}
247
248/// `[name]` / `[name op value]` の中身をパース。
249pub(crate) fn parse_attr_selector(inner: &str) -> Option<AttrSelector> {
250    let inner = inner.trim();
251    if inner.is_empty() {
252        return None;
253    }
254    // 演算子を探す(長い順)。
255    for (token, op) in &[
256        ("^=", AttrOp::Prefix),
257        ("$=", AttrOp::Suffix),
258        ("*=", AttrOp::Contains),
259        ("~=", AttrOp::Word),
260        ("|=", AttrOp::Dash),
261        ("=", AttrOp::Eq),
262    ] {
263        if let Some(pos) = inner.find(token) {
264            let name = inner.get(..pos).unwrap_or("").trim().to_lowercase();
265            let mut raw = inner.get(pos + token.len()..).unwrap_or("").trim();
266            // `[attr=val i]`/`[attr=val s]`(Selectors Level 4 §6.4.1)。フラグは値の後ろに
267            // 空白で区切って置く。`s`(大文字小文字を区別。既定と同じ)は読み捨てるだけでよい。
268            // クォート付き値なら閉じクォートがフラグの直前に来るため、値中の空白+`i`と
269            // 混同しない(`"show i"` は末尾が `i"` であって ` i` ではないので誤判定しない)。
270            let mut case_insensitive = false;
271            for flag in [" i", " I", " s", " S"] {
272                if let Some(stripped) = raw.strip_suffix(flag) {
273                    case_insensitive = flag.eq_ignore_ascii_case(" i");
274                    raw = stripped.trim_end();
275                    break;
276                }
277            }
278            let value = raw.trim_matches('"').trim_matches('\'').to_string();
279            if name.is_empty() {
280                return None;
281            }
282            return Some(AttrSelector {
283                name,
284                op: op.clone(),
285                value,
286                case_insensitive,
287            });
288        }
289    }
290    Some(AttrSelector {
291        name: inner.to_lowercase(),
292        op: AttrOp::Exists,
293        value: String::new(),
294        case_insensitive: false,
295    })
296}
297
298/// `:nth-child(An+B of S)`(Selectors Level 4)用: `of` より前の `An+B` 部分を `parse_nth`
299/// に、`of` より後ろの `S` 部分を `parse_simple_selector` に、それぞれ渡す。`of` が無ければ
300/// 従来どおり `S` は `None`。以前は `parse_nth` が `S` 部分を丸ごと読み捨てるだけで
301/// フィルタ自体が非対応だったため、`:nth-child(2n+1 of .foo)` が `.foo` に関係なく
302/// 単なる `:nth-child(2n+1)` と同じ結果になっていた。
303pub(crate) fn parse_nth_of(arg: &str) -> (i64, i64, Option<SimpleSelector>) {
304    let full = arg.trim();
305    let lower = full.to_lowercase();
306    match lower.find(" of ") {
307        Some(pos) => {
308            let an_b_part = full.get(..pos).unwrap_or(full);
309            let sel_part = full.get(pos + 4..).unwrap_or("");
310            let (a, b) = parse_nth(an_b_part);
311            (a, b, parse_simple_selector(sel_part.trim()))
312        }
313        None => {
314            let (a, b) = parse_nth(full);
315            (a, b, None)
316        }
317    }
318}
319
320/// `:nth-child(...)` の引数 `an+b` / `odd` / `even` / `b` を (a, b) に。
321pub(crate) fn parse_nth(arg: &str) -> (i64, i64) {
322    let full = arg.trim().to_lowercase();
323    // Selectors Level 4 の `:nth-child(An+B of S)` 構文: `S` は呼び出し元
324    // (`parse_nth_of`)側で切り出す。ここでは念のため `of` 以降が残っていても
325    // 無害に切り捨てるだけの防御的処理として残す(`parse_nth` 単体呼び出しの
326    // `nth-of-type`/`nth-last-of-type` は `of S` 構文自体を持たないため通常は無関係)。
327    let s = full.split(" of ").next().unwrap_or(&full).trim();
328    match s {
329        "odd" => return (2, 1),
330        "even" => return (2, 0),
331        _ => {}
332    }
333    if let Some(npos) = s.find('n') {
334        // a 部分
335        let a_str = s.get(..npos).unwrap_or("").trim();
336        let a: i64 = match a_str {
337            "" | "+" => 1,
338            "-" => -1,
339            _ => a_str.parse().unwrap_or(1),
340        };
341        // b 部分(n の後ろ)
342        let b_str: String = s
343            .get(npos + 1..)
344            .unwrap_or("")
345            .chars()
346            .filter(|c| !c.is_whitespace())
347            .collect();
348        let b: i64 = if b_str.is_empty() {
349            0
350        } else {
351            b_str.parse().unwrap_or(0)
352        };
353        (a, b)
354    } else {
355        // 定数のみ → a=0, b=n
356        (0, s.parse().unwrap_or(0))
357    }
358}
359
360pub(crate) fn parse_selector_chain(s: &str) -> Option<Vec<SelectorStep>> {
361    if s.is_empty() {
362        return None;
363    }
364
365    let mut tokens: Vec<(Option<Combinator>, String)> = Vec::new();
366    let chars: Vec<char> = s.chars().collect();
367    let mut i = 0usize;
368    let mut current = String::new();
369    let mut pending_combinator: Option<Combinator> = None;
370    let mut depth = 0i32; // [] () の入れ子内では結合子を区切らない。
371
372    while i < chars.len() {
373        let c = chars[i];
374        if c == '[' || c == '(' {
375            depth += 1;
376            current.push(c);
377            i += 1;
378            continue;
379        }
380        if c == ']' || c == ')' {
381            depth -= 1;
382            current.push(c);
383            i += 1;
384            continue;
385        }
386        if depth > 0 {
387            current.push(c);
388            i += 1;
389            continue;
390        }
391        if c == '>' || c == '+' || c == '~' {
392            if !current.trim().is_empty() {
393                tokens.push((pending_combinator, current.trim().to_string()));
394                current.clear();
395            }
396            pending_combinator = Some(match c {
397                '>' => Combinator::Child,
398                '+' => Combinator::NextSibling,
399                _ => Combinator::SubsequentSibling,
400            });
401            i += 1;
402            while i < chars.len() && chars[i].is_ascii_whitespace() {
403                i += 1;
404            }
405            continue;
406        }
407        if c.is_ascii_whitespace() {
408            if !current.trim().is_empty() {
409                tokens.push((pending_combinator, current.trim().to_string()));
410                current.clear();
411                pending_combinator = Some(Combinator::Descendant);
412            }
413            i += 1;
414            while i < chars.len() && chars[i].is_ascii_whitespace() {
415                i += 1;
416            }
417            continue;
418        }
419        current.push(c);
420        i += 1;
421    }
422    if !current.trim().is_empty() {
423        tokens.push((pending_combinator, current.trim().to_string()));
424    }
425    if tokens.is_empty() {
426        return None;
427    }
428
429    let mut steps = Vec::new();
430    for (idx, (comb, tok)) in tokens.into_iter().enumerate() {
431        let simple = parse_simple_selector(&tok)?;
432        let combinator = if idx == 0 { None } else { comb };
433        steps.push(SelectorStep { simple, combinator });
434    }
435    Some(steps)
436}
437
438pub(crate) fn is_selector_ident_char(c: char) -> bool {
439    c.is_ascii_alphanumeric() || c == '-' || c == '_'
440}
441
442/// セレクタの特異度 (id数, class/属性/擬似クラス数, タグ数) を算出する。
443/// CSS Selectors Level 4 の規定に従い、`:not()`/`:is()` はその引数の特異度を
444/// (`:is()` は引数中の最大値を)加算し、`:where()` は常に0を加算する
445/// (それ以外の擬似クラスは従来通り class 数として1を加算)。
446pub(crate) fn simple_specificity(sel: &SimpleSelector) -> (u32, u32, u32) {
447    let mut spec = (
448        sel.id.as_ref().map_or(0u32, |_| 1),
449        sel.class.len() as u32 + sel.attrs.len() as u32,
450        sel.tag_name.as_ref().map_or(0u32, |_| 1),
451    );
452    for pc in &sel.pseudo_classes {
453        let add = match pc {
454            PseudoClass::Not(inner) => simple_specificity(inner.as_ref()),
455            PseudoClass::Is(list) => list.iter().map(simple_specificity).max().unwrap_or((0, 0, 0)),
456            PseudoClass::Has(list) => list
457                .iter()
458                .map(|(_, sel)| simple_specificity(sel))
459                .max()
460                .unwrap_or((0, 0, 0)),
461            PseudoClass::Where(_) => (0, 0, 0),
462            _ => (0, 1, 0),
463        };
464        spec.0 += add.0;
465        spec.1 += add.1;
466        spec.2 += add.2;
467    }
468    // 疑似要素(::before/::after 等)はタグ名セレクタと同じ重み(タグ数バケット)で
469    // 特異度に寄与する(CSS Selectors 仕様通り)。以前はここが未加算で、
470    // 疑似要素の有無が特異度計算に一切反映されないバグだった。
471    if sel.pseudo_element.is_some() {
472        spec.2 += 1;
473    }
474    spec
475}
476
477/// `::before` / `::after` の `content` 値を解析する。
478/// 文字列リテラル("..." / '...')、`attr(name)`(対象要素自身の属性値、無ければ空)、
479/// `counter(name)` / `counter(name, style)`(style 引数は無視し常に十進数表記)に対応。
480/// 複数トークンの連結(例 `attr(href) " "`)も可。それ以外の未対応関数形式や
481/// none/normal は「表示なし」として空文字列を返す(テキストとして誤描画しないため)。
482/// `quotes: "«" "»" "‹" "›"` のような文字列リテラルの並びから、入れ子レベルごとの
483/// 全ペア(レベル1, レベル2, ...)を取り出す。以前は最初の1組しか取り出さず、
484/// `open-quote`/`close-quote` の入れ子(`<q>` の入れ子)に対応する複数組指定を
485/// 常に無視していたが、ここで全ペアを保持し、呼び出し側(`quote_depth` を使う
486/// `parse_content_value`)で入れ子レベルに応じたペアを選べるようにする。
487/// 戻り値: `None`=`quotes` 未指定(呼び出し側で既定の二重引用符へフォールバック)、
488/// `Some(vec![])`=`quotes:none`(仕様どおり何も出力しない)、
489/// `Some(pairs)`=ペア列(レベルが手持ちのペア数を超える場合は最後のペアを繰り返す、仕様どおり)。
490pub(crate) fn parse_quotes_pairs(v: &str) -> Option<alloc::vec::Vec<(String, String)>> {
491    let t = v.trim();
492    if t.eq_ignore_ascii_case("none") {
493        return Some(alloc::vec::Vec::new());
494    }
495    let mut lits: alloc::vec::Vec<String> = alloc::vec::Vec::new();
496    let bytes = t.as_bytes();
497    let mut i = 0usize;
498    while i < bytes.len() {
499        if bytes[i] == b'"' || bytes[i] == b'\'' {
500            let quote = bytes[i];
501            let start = i + 1;
502            let mut j = start;
503            while j < bytes.len() && bytes[j] != quote {
504                j += 1;
505            }
506            if let Some(sub) = t.get(start..j) {
507                lits.push(String::from(sub));
508            }
509            i = j + 1;
510        } else {
511            i += 1;
512        }
513    }
514    let mut pairs: alloc::vec::Vec<(String, String)> = alloc::vec::Vec::new();
515    let mut it = lits.into_iter();
516    while let (Some(a), Some(b)) = (it.next(), it.next()) {
517        pairs.push((a, b));
518    }
519    if pairs.is_empty() {
520        None
521    } else {
522        Some(pairs)
523    }
524}
525
526/// CSS文字列内のエスケープ(`\` + 1〜6桁16進 + 任意の空白1つ=Unicodeコードポイント、
527/// または `\"` 等の単純なリテラルエスケープ)をデコードする。
528/// FontAwesome等のアイコンフォントは `content: "\f2c9";` のように private-use-area
529/// コードポイントをこの16進エスケープで埋め込むため、これを未対応のまま生バイト列
530/// (バックスラッシュ込みの文字そのまま)として扱うと、アイコン文字が表示されず
531/// 代わりに "\f2c9" という文字列がそのまま描画されてしまう。
532pub(crate) fn unescape_css_string(s: &str) -> String {
533    let mut out = String::with_capacity(s.len());
534    let mut chars = s.chars().peekable();
535    while let Some(c) = chars.next() {
536        if c != '\\' {
537            out.push(c);
538            continue;
539        }
540        let Some(&next) = chars.peek() else {
541            break;
542        };
543        if next.is_ascii_hexdigit() {
544            let mut hex = String::with_capacity(6);
545            while hex.len() < 6 {
546                match chars.peek() {
547                    Some(&h) if h.is_ascii_hexdigit() => {
548                        hex.push(h);
549                        chars.next();
550                    }
551                    _ => break,
552                }
553            }
554            if let Ok(code) = u32::from_str_radix(&hex, 16) {
555                if let Some(ch) = char::from_u32(code) {
556                    out.push(ch);
557                }
558            }
559            // 仕様どおり、16進の直後の空白1つは区切りとして消費する。
560            if let Some(&w) = chars.peek() {
561                if w.is_ascii_whitespace() {
562                    chars.next();
563                }
564            }
565        } else {
566            out.push(next);
567            chars.next();
568        }
569    }
570    out
571}
572
573pub(crate) fn parse_content_value(
574    v: &str,
575    attributes: &BTreeMap<String, String>,
576    counters: &BTreeMap<String, i64>,
577    quotes_pairs: Option<&[(String, String)]>,
578    quote_depth: usize,
579    counter_styles: &[CounterStyleDef],
580) -> String {
581    let t = v.trim();
582    let mut out = String::new();
583    let mut i = 0usize;
584    let bytes = t.as_bytes();
585    while i < bytes.len() {
586        // 空白は連結トークン間の区切りなので読み飛ばす。
587        if bytes[i].is_ascii_whitespace() {
588            i += 1;
589            continue;
590        }
591        if bytes[i] == b'"' || bytes[i] == b'\'' {
592            let quote = bytes[i];
593            let start = i + 1;
594            let mut j = start;
595            while j < bytes.len() && bytes[j] != quote {
596                j += 1;
597            }
598            if let Some(sub) = t.get(start..j) {
599                out.push_str(&unescape_css_string(sub));
600            }
601            i = j + 1;
602            continue;
603        }
604        if let Some(rest) = t.get(i..).and_then(|r| r.strip_prefix("attr(")) {
605            if let Some(close) = rest.find(')') {
606                if let Some(inner) = rest.get(..close) {
607                    // `attr(name, fallback)`(CSS Values and Units Level 5)の第2引数
608                    // フォールバックが丸ごと未対応で、それどころかカンマ以降を含めた
609                    // 文字列全体を属性名として扱ってしまい、フォールバック指定時は
610                    // 常に属性名が一致せず空文字列に落ちる(本来の属性値も読めなくなる)
611                    // バグだった。
612                    let mut parts = inner.splitn(2, ',');
613                    let name = parts.next().unwrap_or("").trim();
614                    let fallback = parts.next().map(|s| {
615                        let s = s.trim();
616                        s.trim_matches('"').trim_matches('\'')
617                    });
618                    match attributes.get(name) {
619                        Some(val) => out.push_str(val),
620                        None => {
621                            if let Some(fb) = fallback {
622                                out.push_str(fb);
623                            }
624                        }
625                    }
626                }
627                i += "attr(".len() + close + 1;
628                continue;
629            }
630        }
631        if let Some(rest) = t.get(i..).and_then(|r| r.strip_prefix("counter(")) {
632            if let Some(close) = rest.find(')') {
633                if let Some(inner) = rest.get(..close) {
634                    // `counter(name)` / `counter(name, style)`: style は upper-roman/lower-roman/
635                    // upper-alpha/lower-alpha に対応(list-style-type で使う既存の変換関数を再利用)。
636                    // それ以外の未対応 style(decimal含む)は十進数表記にフォールバックする。
637                    let mut parts = inner.splitn(2, ',');
638                    let name = parts.next().unwrap_or("").trim();
639                    let style = parts.next().map(|s| s.trim().to_lowercase());
640                    let val = counters.get(name).copied().unwrap_or(0);
641                    let val_u32 = val.max(0) as u32;
642                    let formatted = match style.as_deref() {
643                        Some("upper-roman") => super::super::web_engine::number_to_roman(val_u32, true),
644                        Some("lower-roman") => super::super::web_engine::number_to_roman(val_u32, false),
645                        Some("upper-alpha") | Some("upper-latin") => {
646                            super::super::web_engine::number_to_alpha(val_u32, true)
647                        }
648                        Some("lower-alpha") | Some("lower-latin") => {
649                            super::super::web_engine::number_to_alpha(val_u32, false)
650                        }
651                        // 上記の組み込みキーワードに一致しなければ、`@counter-style` で
652                        // 定義されたカスタムスタイル名として登録済みか調べる。
653                        Some(custom) => match counter_styles.iter().find(|d| d.name == custom) {
654                            Some(def) => format_with_counter_style(def, val),
655                            None => alloc::format!("{}", val),
656                        },
657                        None => alloc::format!("{}", val),
658                    };
659                    out.push_str(&formatted);
660                }
661                i += "counter(".len() + close + 1;
662                continue;
663            }
664        }
665        if let Some(rest) = t.get(i..).and_then(|r| r.strip_prefix("counters(")) {
666            if let Some(close) = rest.find(')') {
667                if let Some(inner) = rest.get(..close) {
668                    // `counters(name, "sep")` / `counters(name, "sep", style)`:
669                    // 本来は祖先スコープごとに積み上がった値を sep で連結するが、
670                    // counter-reset/counter-increment 自体が全カウンタをフラットに共有する
671                    // 簡略実装(本ファイル冒頭のコメント参照)のため、ここも同じ簡略化方針で
672                    // 単一の現在値をそのまま1セグメントとして出す(sep 自体は消費するが
673                    // 実質未使用)。style は counter() と同じ変換ロジックを再利用する。
674                    let mut parts = inner.splitn(3, ',');
675                    let name = parts.next().unwrap_or("").trim();
676                    let _sep = parts.next().map(|s| {
677                        s.trim()
678                            .trim_matches(|c| c == '"' || c == '\'')
679                            .to_string()
680                    });
681                    let style = parts.next().map(|s| s.trim().to_lowercase());
682                    let val = counters.get(name).copied().unwrap_or(0);
683                    let val_u32 = val.max(0) as u32;
684                    let formatted = match style.as_deref() {
685                        Some("upper-roman") => super::super::web_engine::number_to_roman(val_u32, true),
686                        Some("lower-roman") => super::super::web_engine::number_to_roman(val_u32, false),
687                        Some("upper-alpha") | Some("upper-latin") => {
688                            super::super::web_engine::number_to_alpha(val_u32, true)
689                        }
690                        Some("lower-alpha") | Some("lower-latin") => {
691                            super::super::web_engine::number_to_alpha(val_u32, false)
692                        }
693                        Some(custom) => match counter_styles.iter().find(|d| d.name == custom) {
694                            Some(def) => format_with_counter_style(def, val),
695                            None => alloc::format!("{}", val),
696                        },
697                        None => alloc::format!("{}", val),
698                    };
699                    out.push_str(&formatted);
700                }
701                i += "counters(".len() + close + 1;
702                continue;
703            }
704        }
705        // open-quote/close-quote: `quotes` プロパティが指定されていれば入れ子レベル
706        // (`quote_depth`、祖先の `<q>` の個数から算出、手持ちのペア数を超えたら
707        // 最後のペアを繰り返す仕様どおりの挙動)に応じたペアを使い、無指定なら既定の
708        // 二重引用符(“/”)、`quotes:none` なら何も出力しない。
709        // no-open-quote/no-close-quote は入れ子レベルを追跡しないため何もしない扱い。
710        if let Some(rest) = t.get(i..) {
711            if let Some(r2) = rest.strip_prefix("no-open-quote") {
712                let _ = r2;
713                i += "no-open-quote".len();
714                continue;
715            }
716            if let Some(r2) = rest.strip_prefix("no-close-quote") {
717                let _ = r2;
718                i += "no-close-quote".len();
719                continue;
720            }
721            if rest.starts_with("open-quote") {
722                match quotes_pairs {
723                    Some(pairs) if !pairs.is_empty() => {
724                        let idx = quote_depth.min(pairs.len() - 1);
725                        out.push_str(&pairs[idx].0);
726                    }
727                    Some(_) => {} // quotes:none — 何も出力しない
728                    None => out.push('\u{201C}'),
729                }
730                i += "open-quote".len();
731                continue;
732            }
733            if rest.starts_with("close-quote") {
734                match quotes_pairs {
735                    Some(pairs) if !pairs.is_empty() => {
736                        let idx = quote_depth.min(pairs.len() - 1);
737                        out.push_str(&pairs[idx].1);
738                    }
739                    Some(_) => {} // quotes:none — 何も出力しない
740                    None => out.push('\u{201D}'),
741                }
742                i += "close-quote".len();
743                continue;
744            }
745        }
746        // 未対応トークンは表示なし扱いで丸ごとスキップする。
747        // 次のホワイトスペースまで読み飛ばして解析を継続する。
748        while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
749            i += 1;
750        }
751    }
752    out
753}
754
755/// `counter-reset` / `counter-increment` 宣言をパースし、`name [value]` の
756/// 空白区切りペア列として (name, value) を返す。value 省略時は `default` を使う
757/// (reset のデフォルトは 0、increment のデフォルトは 1)。`none` は空を返す。
758pub(crate) fn parse_counter_decl(v: &str, default: i64) -> alloc::vec::Vec<(String, i64)> {
759    let t = v.trim();
760    if t.is_empty() || t.eq_ignore_ascii_case("none") {
761        return alloc::vec::Vec::new();
762    }
763    let tokens: alloc::vec::Vec<&str> = t.split_whitespace().collect();
764    let mut out = alloc::vec::Vec::new();
765    let mut i = 0usize;
766    while i < tokens.len() {
767        let name = tokens[i];
768        i += 1;
769        let value = if i < tokens.len() {
770            if let Ok(n) = tokens[i].parse::<i64>() {
771                i += 1;
772                n
773            } else {
774                default
775            }
776        } else {
777            default
778        };
779        out.push((name.to_string(), value));
780    }
781    out
782}
783
784/// `counter-reset` / `counter-increment` を現在のカウンタ集合に適用する。
785/// 戻り値は、この要素自身が `counter-reset` した(=新しいスコープを作った)カウンタ名の集合。
786/// 呼び出し側(`style_tree_with_ancestors_vars`)が、この集合を使って「入れ子の
787/// `counter-reset` の効果を兄弟要素へ漏らさない」というスコープ復元を行う
788/// (`apply_counter_decls` 自体はフラットな1つのマップを直接書き換えるだけで、
789/// スコープの概念は呼び出し側が別途管理する)。
790pub(crate) fn apply_counter_decls(
791    specified_values: &BTreeMap<String, String>,
792    counters: &mut BTreeMap<String, i64>,
793) -> alloc::collections::BTreeSet<String> {
794    let mut reset_names = alloc::collections::BTreeSet::new();
795    if let Some(reset) = specified_values.get("counter-reset") {
796        for (name, value) in parse_counter_decl(reset, 0) {
797            counters.insert(name.clone(), value);
798            reset_names.insert(name);
799        }
800    }
801    if let Some(incr) = specified_values.get("counter-increment") {
802        for (name, value) in parse_counter_decl(incr, 1) {
803            let cur = counters.get(&name).copied().unwrap_or(0);
804            counters.insert(name, cur + value);
805        }
806    }
807    reset_names
808}
809
810/// セレクタ末尾(サブジェクトのコンパウンドセレクタ)に付与された疑似要素を返す。
811/// `::before` / `::after` は末尾にのみ意味を持つため最後のステップだけを見る。
812pub(crate) fn selector_pseudo_element(sel: &Selector) -> Option<PseudoElement> {
813    match sel {
814        Selector::Simple(s) => s.pseudo_element,
815        Selector::Chain(steps) => steps.last().and_then(|s| s.simple.pseudo_element),
816    }
817}
818
819pub(crate) fn selector_specificity(sel: &Selector) -> (u32, u32, u32) {
820    match sel {
821        Selector::Simple(s) => simple_specificity(s),
822        Selector::Chain(steps) => {
823            let mut a = 0u32;
824            let mut b = 0u32;
825            let mut c = 0u32;
826            for step in steps {
827                let (sa, sb, sc) = simple_specificity(&step.simple);
828                a += sa;
829                b += sb;
830                c += sc;
831            }
832            (a, b, c)
833        }
834    }
835}
836
837fn is_node_disabled(node: &Node, ancestors: &[AncestorContext<'_>]) -> bool {
838    if let NodeType::Element { attributes, .. } = &node.node_type {
839        if attributes.contains_key("disabled") {
840            return true;
841        }
842    }
843    for anc in ancestors {
844        if let NodeType::Element { tag_name, attributes, .. } = &anc.node.node_type {
845            if tag_name == "fieldset" && attributes.contains_key("disabled") {
846                return true;
847            }
848        }
849    }
850    false
851}
852
853#[allow(clippy::too_many_arguments)]
854pub(crate) fn matches_simple_selector(
855    tag_name: &str,
856    id: &Option<String>,
857    classes: &Vec<String>,
858    attributes: &BTreeMap<String, String>,
859    sel: &SimpleSelector,
860    sibling_index: Option<usize>,
861    sibling_count: Option<usize>,
862    active_pseudo_states: &[&str],
863    type_index: Option<usize>,
864    type_count: Option<usize>,
865    is_empty: bool,
866    node_ptr: usize,
867    node_children: &[Node],
868    own_siblings: Option<&[Node]>,
869    is_disabled: bool,
870) -> bool {
871    if let Some(sel_tag) = &sel.tag_name {
872        if sel_tag != tag_name {
873            return false;
874        }
875    }
876    if let Some(sel_id) = &sel.id {
877        if id.as_ref() != Some(sel_id) {
878            return false;
879        }
880    }
881    for need in &sel.class {
882        if !classes.iter().any(|c| c == need) {
883            return false;
884        }
885    }
886    for attr in &sel.attrs {
887        if !attr_matches(attr, attributes) {
888            return false;
889        }
890    }
891    for pseudo in &sel.pseudo_classes {
892        match pseudo {
893            PseudoClass::FirstChild => {
894                if sibling_index != Some(0) {
895                    return false;
896                }
897            }
898            PseudoClass::LastChild => match (sibling_index, sibling_count) {
899                (Some(idx), Some(count)) if idx + 1 == count => {}
900                _ => return false,
901            },
902            PseudoClass::NthChild(a, b, filter) => match filter {
903                Some(sel) => match filtered_sibling_position(own_siblings, node_ptr, sel) {
904                    Some((idx, _count)) => {
905                        if !nth_matches(*a, *b, (idx + 1) as i64) {
906                            return false;
907                        }
908                    }
909                    None => return false,
910                },
911                None => match sibling_index {
912                    Some(idx) => {
913                        if !nth_matches(*a, *b, (idx + 1) as i64) {
914                            return false;
915                        }
916                    }
917                    None => return false,
918                },
919            },
920            PseudoClass::NthLastChild(a, b, filter) => match filter {
921                Some(sel) => match filtered_sibling_position(own_siblings, node_ptr, sel) {
922                    Some((idx, count)) => {
923                        if !nth_matches(*a, *b, (count - idx) as i64) {
924                            return false;
925                        }
926                    }
927                    None => return false,
928                },
929                None => match (sibling_index, sibling_count) {
930                    (Some(idx), Some(count)) => {
931                        if !nth_matches(*a, *b, (count - idx) as i64) {
932                            return false;
933                        }
934                    }
935                    _ => return false,
936                },
937            },
938            PseudoClass::Checked => {
939                if !attributes.contains_key("checked") {
940                    return false;
941                }
942            }
943            // `:disabled`は要素自身の`disabled`属性だけでなく、祖先の
944            // `<fieldset disabled>`による暗黙の無効化も反映する必要がある
945            // (`is_node_disabled`が計算した`is_disabled`パラメータが
946            // 既にこれを含む)。以前は自身の属性だけを見ていたため、
947            // `<fieldset disabled><input></fieldset>`のような暗黙無効化
948            // ケースで`:disabled`がマッチしないバグだった(同じロジックが
949            // `PseudoClass::Other("disabled")`アームに正しく実装済み
950            // だったが、パーサが`"disabled"`を専用の`PseudoClass::
951            // Disabled`列挙子へ先にマッピングするため、そちらのアームが
952            // 到達不能な死んだコードになっていた)。2026-07-18 発見・
953            // 修正)。
954            PseudoClass::Disabled => {
955                if !is_disabled {
956                    return false;
957                }
958            }
959            PseudoClass::FirstOfType => {
960                if type_index != Some(0) {
961                    return false;
962                }
963            }
964            PseudoClass::LastOfType => match (type_index, type_count) {
965                (Some(idx), Some(count)) if idx + 1 == count => {}
966                _ => return false,
967            },
968            PseudoClass::NthOfType(a, b) => match type_index {
969                Some(idx) => {
970                    if !nth_matches(*a, *b, (idx + 1) as i64) {
971                        return false;
972                    }
973                }
974                None => return false,
975            },
976            PseudoClass::NthLastOfType(a, b) => match (type_index, type_count) {
977                (Some(idx), Some(count)) => {
978                    if !nth_matches(*a, *b, (count - idx) as i64) {
979                        return false;
980                    }
981                }
982                _ => return false,
983            },
984            PseudoClass::OnlyOfType => match (type_index, type_count) {
985                (Some(0), Some(1)) => {}
986                _ => return false,
987            },
988            PseudoClass::OnlyChild => match (sibling_index, sibling_count) {
989                (Some(0), Some(1)) => {}
990                _ => return false,
991            },
992            PseudoClass::Target => {
993                let current = super::super::layout::CURRENT_TARGET_ID.lock();
994                if current.is_empty() || id.as_deref() != Some(current.as_str()) {
995                    return false;
996                }
997            }
998            PseudoClass::Empty => {
999                if !is_empty {
1000                    return false;
1001                }
1002            }
1003            PseudoClass::Not(inner) => {
1004                if matches_simple_selector(
1005                    tag_name,
1006                    id,
1007                    classes,
1008                    attributes,
1009                    inner,
1010                    sibling_index,
1011                    sibling_count,
1012                    active_pseudo_states,
1013                    type_index,
1014                    type_count,
1015                    is_empty,
1016                    node_ptr,
1017                    node_children,
1018                    own_siblings,
1019                    is_disabled,
1020                ) {
1021                    return false;
1022                }
1023            }
1024            PseudoClass::Is(list) | PseudoClass::Where(list) => {
1025                let any_match = list.iter().any(|inner| {
1026                    matches_simple_selector(
1027                        tag_name,
1028                        id,
1029                        classes,
1030                        attributes,
1031                        inner,
1032                        sibling_index,
1033                        sibling_count,
1034                        active_pseudo_states,
1035                        type_index,
1036                        type_count,
1037                        is_empty,
1038                        node_ptr,
1039                        node_children,
1040                        own_siblings,
1041                        is_disabled,
1042                    )
1043                });
1044                if !any_match {
1045                    return false;
1046                }
1047            }
1048            PseudoClass::Has(list) => {
1049                let (sibling_list, descendant_list): (
1050                    Vec<&(HasCombinator, SimpleSelector)>,
1051                    Vec<&(HasCombinator, SimpleSelector)>,
1052                ) = list.iter().partition(|(comb, _)| {
1053                    matches!(comb, HasCombinator::NextSibling | HasCombinator::SubsequentSibling)
1054                });
1055                let sibling_ok = !sibling_list.is_empty()
1056                    && own_siblings.zip(sibling_index).is_some_and(|(sibs, my_idx)| {
1057                        sibling_list.iter().any(|(comb, sel)| {
1058                            has_sibling_match(sibs, my_idx, *comb == HasCombinator::NextSibling, sel)
1059                        })
1060                    });
1061                let descendant_ok = !descendant_list.is_empty()
1062                    && {
1063                        let owned: Vec<(bool, SimpleSelector)> = descendant_list
1064                            .iter()
1065                            .map(|(comb, sel)| (*comb == HasCombinator::Child, sel.clone()))
1066                            .collect();
1067                        has_descendant_match(node_children, &owned)
1068                    };
1069                if !sibling_ok && !descendant_ok {
1070                    return false;
1071                }
1072            }
1073            PseudoClass::Dir(target) => {
1074                let effective = attributes
1075                    .get("dir")
1076                    .map(|v| v.trim().to_lowercase())
1077                    .filter(|v| v == "ltr" || v == "rtl")
1078                    .unwrap_or_else(|| String::from("ltr"));
1079                if effective != *target {
1080                    return false;
1081                }
1082            }
1083            PseudoClass::Lang(target) => {
1084                let own_lang = attributes.get("lang").map(|v| v.to_lowercase());
1085                let effective = match own_lang {
1086                    Some(l) if !l.is_empty() => l,
1087                    _ => super::super::layout::DOCUMENT_LANG.lock().clone(),
1088                };
1089                let matched = !effective.is_empty()
1090                    && (effective == *target
1091                        || effective.starts_with(&alloc::format!("{}-", target)));
1092                if !matched {
1093                    return false;
1094                }
1095            }
1096            PseudoClass::Other(p) => match p.as_str() {
1097                // `:checked`/`:disabled` は要素自身の属性から静的に判定できるため、
1098                // hover のような「動的状態リスト」の受け渡しなしに直接判定する。
1099                "checked" => {
1100                    if !attributes.contains_key("checked") {
1101                        return false;
1102                    }
1103                }
1104                "disabled" => {
1105                    if !is_disabled {
1106                        return false;
1107                    }
1108                }
1109                // `:enabled`(`:disabled` の対)が丸ごと未対応だった。既存の
1110                // `:disabled`/`:read-write` と同じ簡略方針(タグ種別は問わず
1111                // `disabled` 属性の有無だけで判定)に合わせ、単純にその否定とする。
1112                "enabled" => {
1113                    if is_disabled {
1114                        return false;
1115                    }
1116                }
1117                "required" => {
1118                    if !attributes.contains_key("required") {
1119                        return false;
1120                    }
1121                }
1122                // `:optional` は `:required` の否定(`required` 属性が無いフォーム要素)。
1123                "optional" => {
1124                    if attributes.contains_key("required") {
1125                        return false;
1126                    }
1127                }
1128                // `:valid`/`:invalid` が丸ごと未対応だった。`:checked`/`:required`/
1129                // `:in-range` 等と同じ簡略方針で、静的な HTML 属性のみから判定する
1130                // (`pattern`/`type=email` 等の書式チェックは JS 側の `validate_field`
1131                // に既にあるロジックをそのまま複製することになり css.rs 側の担当範囲を
1132                // 超えるため対象外。ここでは最も頻出する「`required` な空欄」と
1133                // `:in-range`/`:out-of-range` で既に対応済みの min/max 範囲のみを見る)。
1134                // checkbox/radio は「値」でなく「チェック状態」で`required`を判定する
1135                // 別のセマンティクス(JS側の`is_disabled`と対になる`required_satisfied`
1136                // で2026-07-17に修正済みの規則と同じ)。以前はここも他のinputと同じ
1137                // 「value属性が空か」で判定しており、`value`属性を持たないchecked済み
1138                // チェックボックスが常に`:invalid`になる等、チェック状態と無関係な
1139                // 結果になっていた。radioのグループ判定は`own_siblings`(同じ親を持つ
1140                // 兄弟のみ)の範囲に限定する簡略実装(JS側は文書全体を`name`で走査する
1141                // 完全な実装だが、CSSセレクタマッチングはこの関数の他の兄弟系疑似
1142                // クラスと同じスコープに合わせる)。
1143                // `:user-valid`/`:user-invalid`(CSS Selectors Level 4)が丸ごと
1144                // 未対応だった。`:valid`/`:invalid`と同一の妥当性判定ロジックに
1145                // 加え、ユーザーが実際にフォームコントロールを操作したこと
1146                // (`interp.rs`の`dispatch_event`が`input`/`change`発火時に
1147                // 記録する内部専用属性`_user_interacted`。`:indeterminate`の
1148                // `_indeterminate`と同じ方針)も要求する点のみ異なる。
1149                // 2026-07-18 発見・実装。
1150                "valid" | "invalid" | "user-valid" | "user-invalid" => {
1151                    let value = attributes.get("value").map(|s| s.trim()).unwrap_or("");
1152                    let ty = attributes.get("type").map(|s| s.as_str()).unwrap_or("");
1153                    let required_empty = if !attributes.contains_key("required") {
1154                        false
1155                    } else if tag_name == "input" && ty == "checkbox" {
1156                        !attributes.contains_key("checked")
1157                    } else if tag_name == "input" && ty == "radio" {
1158                        let group_checked = attributes.get("name").is_some_and(|nm| {
1159                            own_siblings.is_some_and(|sibs| {
1160                                sibs.iter().any(|sib| {
1161                                    if let NodeType::Element {
1162                                        tag_name: st,
1163                                        attributes: sa,
1164                                        ..
1165                                    } = &sib.node_type
1166                                    {
1167                                        st == "input"
1168                                            && sa.get("type").map(|t| t == "radio").unwrap_or(false)
1169                                            && sa.get("name") == Some(nm)
1170                                            && sa.contains_key("checked")
1171                                    } else {
1172                                        false
1173                                    }
1174                                })
1175                            })
1176                        });
1177                        !(attributes.contains_key("checked") || group_checked)
1178                    } else {
1179                        value.is_empty()
1180                    };
1181                    let min = attributes.get("min").and_then(|v| v.parse::<f64>().ok());
1182                    let max = attributes.get("max").and_then(|v| v.parse::<f64>().ok());
1183                    let out_of_range = if min.is_some() || max.is_some() {
1184                        value.parse::<f64>().ok().map(|v| {
1185                            !(min.is_none_or(|m| v >= m) && max.is_none_or(|m| v <= m))
1186                        })
1187                    } else {
1188                        None
1189                    }
1190                    .unwrap_or(false);
1191                    let is_invalid = required_empty || out_of_range;
1192                    let is_user_variant = p.as_str().starts_with("user-");
1193                    if is_user_variant && !attributes.contains_key("_user_interacted") {
1194                        return false;
1195                    }
1196                    let want_valid = p.as_str() == "valid" || p.as_str() == "user-valid";
1197                    if is_invalid == want_valid {
1198                        return false;
1199                    }
1200                }
1201                "readonly" | "read-only" => {
1202                    if !attributes.contains_key("readonly") {
1203                        return false;
1204                    }
1205                }
1206                // `:read-write`(`:read-only`/`:readonly` の対)が丸ごと未対応
1207                // だった。既存の `:read-only` と同じ簡略方針(タグ種別は問わず
1208                // `readonly` 属性の有無だけで判定)に合わせ、単純にその否定とする。
1209                "read-write" => {
1210                    if attributes.contains_key("readonly") {
1211                        return false;
1212                    }
1213                }
1214                // `:indeterminate` は本来 JS で設定される DOM プロパティ(HTML属性としては
1215                // 反映されない)。静的 HTML に `indeterminate` 属性が直接書かれている
1216                // ケース、`<progress>`(value 未指定)、そして `checkbox.indeterminate =
1217                // true`(JS の setter が書き込む内部専用の `_indeterminate` 属性キー。
1218                // 実属性 `indeterminate` とは別物で `hasAttribute('indeterminate')` は
1219                // 常に false のまま)の3パターンいずれかで真とする。
1220                "indeterminate" => {
1221                    let is_indeterminate_progress =
1222                        tag_name == "progress" && !attributes.contains_key("value");
1223                    let matched = attributes.contains_key("indeterminate")
1224                        || attributes.contains_key("_indeterminate")
1225                        || is_indeterminate_progress;
1226                    if !matched {
1227                        return false;
1228                    }
1229                }
1230                // `:focus-visible` はキーボード操作かポインタ操作かを区別する仕組みが
1231                // 無いため、`:focus` と同一視する簡略実装(`:where()`≒`:is()` と同じ方針)。
1232                "focus-visible" => {
1233                    if !active_pseudo_states.contains(&"focus") {
1234                        return false;
1235                    }
1236                }
1237                // `:focus-within` は自分自身、または子孫のいずれかが現在フォーカス中の
1238                // 場合に一致する。id ではなくノード実体のポインタ同一性で判定するため、
1239                // id を持たない要素(`<div>`/`<form>` のラッパー等)でも正しく機能する。
1240                "focus-within" => {
1241                    if !super::super::layout::FOCUS_WITHIN_PTRS.lock().contains(&node_ptr) {
1242                        return false;
1243                    }
1244                }
1245                // `:link`/`:visited` は `:checked`/`:disabled` と同様、要素自身の属性から
1246                // 静的に判定できる。閲覧履歴は保持していないため `:visited` は常に不一致
1247                // (プライバシー上安全側の簡略実装。多くの単純なエンジンや履歴を持たない
1248                // 環境と同じ挙動)。
1249                "link" => {
1250                    if !(tag_name == "a" || tag_name == "area") || !attributes.contains_key("href")
1251                    {
1252                        return false;
1253                    }
1254                }
1255                "visited" => {
1256                    return false;
1257                }
1258                // `:defined`(Selectors Level 4。標準組み込み要素は常に「定義済み」、
1259                // 未定義の`customElements`要素のみ不一致になる仕様)が丸ごと未対応
1260                // だった。この処理系は`customElements.define()`自体が未実装(別途
1261                // TODO.md「HTML5」節に大規模ギャップとして記載済み)で、パース
1262                // されるタグは全て標準組み込み要素のみのため、常に一致する
1263                // 「正直な簡略実装」(`:visited`が常に不一致なのと対称)。
1264                // 2026-07-18 発見・実装。
1265                "defined" => {}
1266                // `:fullscreen`が丸ごと未対応だった。`element.requestFullscreen()`が
1267                // 書き込む内部専用属性`_fullscreen`(`_indeterminate`と同じ方針)の
1268                // 有無で判定する。2026-07-18 発見・実装。
1269                "fullscreen" => {
1270                    if !attributes.contains_key("_fullscreen") {
1271                        return false;
1272                    }
1273                }
1274                // `:modal`が丸ごと未対応だった。フルスクリーン要素、および
1275                // `dialog.showModal()`(`.show()`とは区別される。`dom.rs`の
1276                // `dom_dialog_show_modal`が書き込む内部専用属性`_modal`)で
1277                // 開いている`<dialog>`のいずれかに一致する。2026-07-18
1278                // 発見・実装。
1279                "modal" => {
1280                    if !attributes.contains_key("_fullscreen") && !attributes.contains_key("_modal")
1281                    {
1282                        return false;
1283                    }
1284                }
1285                // `:popover-open`が丸ごと未対応だった。`element.showPopover()`/
1286                // `.hidePopover()`/`.togglePopover()`(`dom.rs`)が既に
1287                // 内部専用属性`_popover_open`を書き込んでいたため、CSS側の
1288                // 判定を追加するだけで済んだ(`:fullscreen`/`:modal`と違い
1289                // JS側の新規実装が不要だった兄弟ギャップ)。2026-07-18
1290                // 発見・実装。
1291                "popover-open" => {
1292                    if !attributes.contains_key("_popover_open") {
1293                        return false;
1294                    }
1295                }
1296                // `:root` は文書のルート要素(HTML文書では常に <html>)にのみ一致する。
1297                "root" => {
1298                    if tag_name != "html" {
1299                        return false;
1300                    }
1301                }
1302                // `:placeholder-shown` は `value` が空(または未指定)かつ `placeholder` 属性を
1303                // 持つ input/textarea にのみ一致する。checked/disabled と同様、属性から
1304                // 静的判定できる。**簡略化**: 初期パース時の `value` 属性のみを見るため、
1305                // ユーザーが実際に入力欄へ入力した後の再判定(動的な再マッチング)は非対応。
1306                "placeholder-shown" => {
1307                    let has_placeholder = attributes
1308                        .get("placeholder")
1309                        .map(|p| !p.is_empty())
1310                        .unwrap_or(false);
1311                    let value_empty = attributes
1312                        .get("value")
1313                        .map(|v| v.is_empty())
1314                        .unwrap_or(true);
1315                    if !has_placeholder || !value_empty {
1316                        return false;
1317                    }
1318                }
1319                // `:in-range`/`:out-of-range` は `min`/`max` 属性を持つ input の `value` が
1320                // その範囲内/範囲外かで判定する。checked/required 等と同様、静的な HTML 属性
1321                // から判定できる(JS による動的な value 変更後の再判定は非対応の簡略実装)。
1322                // min/max のどちらも指定が無ければ範囲制約自体が存在しないため、どちらにも
1323                // 一致しない(実ブラウザと同じ挙動)。
1324                "in-range" | "out-of-range" => {
1325                    let min = attributes.get("min").and_then(|v| v.parse::<f64>().ok());
1326                    let max = attributes.get("max").and_then(|v| v.parse::<f64>().ok());
1327                    if min.is_none() && max.is_none() {
1328                        return false;
1329                    }
1330                    let value = attributes.get("value").and_then(|v| v.parse::<f64>().ok());
1331                    let in_range = match value {
1332                        Some(v) => min.is_none_or(|m| v >= m) && max.is_none_or(|m| v <= m),
1333                        // value 未指定は制約違反ではない(実ブラウザ準拠)。
1334                        None => true,
1335                    };
1336                    let want_in_range = p.as_str() == "in-range";
1337                    if in_range != want_in_range {
1338                        return false;
1339                    }
1340                }
1341                // `:default` は「グループ内で既定選択された要素」(フォーム送信時にJSが何も
1342                // 変更しなければ選ばれる状態)を指す。`:checked`/`:required` と同じく静的な
1343                // HTML属性から判定する簡略実装: checkbox/radio は `checked` 属性、
1344                // `<option>` は `selected` 属性の有無で判定する(ユーザー操作後の動的な
1345                // 「既定値からの変化」追跡は非対応。実ブラウザは操作後も初期選択を
1346                // `:default` として扱い続けるが、ここでは常に静的属性を見る近似)。
1347                "default" => {
1348                    let is_checkable = attributes
1349                        .get("type")
1350                        .map(|t| t == "checkbox" || t == "radio")
1351                        .unwrap_or(false);
1352                    let matched = if tag_name == "option" {
1353                        attributes.contains_key("selected")
1354                    } else if tag_name == "input" && is_checkable {
1355                        attributes.contains_key("checked")
1356                    } else {
1357                        false
1358                    };
1359                    if !matched {
1360                        return false;
1361                    }
1362                }
1363                _ => {
1364                    if !active_pseudo_states.contains(&p.as_str()) {
1365                        return false;
1366                    }
1367                }
1368            },
1369        }
1370    }
1371    true
1372}
1373
1374/// 親の子ノード一覧の中で、`node` と同じタグ名を持つノードだけを数えた「型フィルタ済み」
1375/// 兄弟位置(0-based)と総数を返す。`:first-of-type`/`:last-of-type`/`:nth-of-type`/
1376/// `:only-of-type` 用。祖先が無い(ルート要素)場合は自分だけの単一グループとして扱う。
1377/// サブジェクト(末尾コンパウンド)位置でのみ計算する(祖先位置での of-type 判定は非対応)。
1378pub(crate) fn type_sibling_position(
1379    node: &Node,
1380    ancestors: &[AncestorContext<'_>],
1381    sibling_index: Option<usize>,
1382) -> (Option<usize>, Option<usize>) {
1383    let NodeType::Element { tag_name, .. } = &node.node_type else {
1384        return (None, None);
1385    };
1386    let Some(parent_ctx) = ancestors.last() else {
1387        return (Some(0), Some(1));
1388    };
1389    let Some(my_idx) = sibling_index else {
1390        return (None, None);
1391    };
1392    let mut type_idx = None;
1393    let mut type_count = 0usize;
1394    for (i, child) in parent_ctx.node.children.iter().enumerate() {
1395        if let NodeType::Element { tag_name: t, .. } = &child.node_type {
1396            if t == tag_name {
1397                if i == my_idx {
1398                    type_idx = Some(type_count);
1399                }
1400                type_count += 1;
1401            }
1402        }
1403    }
1404    (type_idx, Some(type_count))
1405}
1406
1407/// nth: position が a*n + b(n>=0)で表せるか。
1408pub(crate) fn nth_matches(a: i64, b: i64, position: i64) -> bool {
1409    if a == 0 {
1410        return position == b;
1411    }
1412    let diff = position - b;
1413    diff % a == 0 && diff / a >= 0
1414}
1415
1416/// 属性セレクタの照合。
1417pub(crate) fn attr_matches(attr: &AttrSelector, attributes: &BTreeMap<String, String>) -> bool {
1418    // 属性名は小文字保持されている前提(dom 側で lower 化されていなければ線形探索)。
1419    let val = attributes.get(&attr.name).or_else(|| {
1420        attributes
1421            .iter()
1422            .find(|(k, _)| k.to_lowercase() == attr.name)
1423            .map(|(_, v)| v)
1424    });
1425    let val = match val {
1426        Some(v) => v,
1427        None => return false, // 属性が無ければどの演算子も不一致。
1428    };
1429    // `[attr=val i]`(大文字小文字を無視。Selectors Level 4 §6.4.1)。フラグが無ければ
1430    // 通常どおり大文字小文字を区別して比較する(`val`/`attr.value` をそのまま使う)。
1431    let (val_cmp, need_cmp): (String, String) = if attr.case_insensitive {
1432        (val.to_lowercase(), attr.value.to_lowercase())
1433    } else {
1434        (val.clone(), attr.value.clone())
1435    };
1436    let val = &val_cmp;
1437    let attr_value = &need_cmp;
1438    match attr.op {
1439        AttrOp::Exists => true,
1440        AttrOp::Eq => val == attr_value,
1441        AttrOp::Prefix => !attr_value.is_empty() && val.starts_with(attr_value.as_str()),
1442        AttrOp::Suffix => !attr_value.is_empty() && val.ends_with(attr_value.as_str()),
1443        AttrOp::Contains => !attr_value.is_empty() && val.contains(attr_value.as_str()),
1444        AttrOp::Word => val.split_whitespace().any(|w| w == attr_value),
1445        AttrOp::Dash => {
1446            val == attr_value || val.starts_with(&alloc::format!("{}-", attr_value))
1447        }
1448    }
1449}
1450
1451/// 1 ノードに対して**一度だけ**計算すればよい照合用の前提情報。
1452///
1453/// 【2026-07-27 性能修正】従来これらは `matches_selector` の内部で
1454/// **ルールごとに毎回**計算されていた。実サイト規模(2403 ルール × 365 ノード
1455/// = 約 88 万回)では、祖先を辿る `is_node_disabled` と兄弟を数える
1456/// `type_sibling_position` が支配的なコストになり、1 回のカスケードに
1457/// 約 46 秒を要していた。ノード単位で 1 回計算して使い回す。
1458pub(crate) struct NodeMatchContext<'a> {
1459    pub is_disabled: bool,
1460    pub type_index: Option<usize>,
1461    pub type_count: Option<usize>,
1462    pub own_siblings: Option<&'a [Node]>,
1463}
1464
1465impl<'a> NodeMatchContext<'a> {
1466    pub fn new(
1467        current: &Node,
1468        ancestors: &'a [AncestorContext<'a>],
1469        sibling_index: Option<usize>,
1470    ) -> Self {
1471        let is_disabled = is_node_disabled(current, ancestors);
1472        // of-type 系はサブジェクト(末尾コンパウンド)でのみ判定する。
1473        let (type_index, type_count) = type_sibling_position(current, ancestors, sibling_index);
1474        // `:has(+ sel)`/`:has(~ sel)` 用。サブジェクト位置の自分の兄弟一覧。
1475        let own_siblings = ancestors.last().map(|a| a.node.children.as_slice());
1476        Self {
1477            is_disabled,
1478            type_index,
1479            type_count,
1480            own_siblings,
1481        }
1482    }
1483}
1484
1485pub(crate) fn matches_selector(
1486    selector: &Selector,
1487    current: &Node,
1488    ancestors: &[AncestorContext<'_>],
1489    sibling_index: Option<usize>,
1490    sibling_count: Option<usize>,
1491    active_pseudo_states: &[&str],
1492    ctx: &NodeMatchContext<'_>,
1493) -> bool {
1494    let is_disabled = ctx.is_disabled;
1495    let (type_index, type_count) = (ctx.type_index, ctx.type_count);
1496    let own_siblings = ctx.own_siblings;
1497    match selector {
1498        Selector::Simple(simple) => matches_selector_step_node(
1499            current,
1500            simple,
1501            sibling_index,
1502            sibling_count,
1503            active_pseudo_states,
1504            type_index,
1505            type_count,
1506            own_siblings,
1507            is_disabled,
1508        ),
1509        Selector::Chain(steps) => matches_selector_chain(
1510            steps,
1511            current,
1512            ancestors,
1513            sibling_index,
1514            sibling_count,
1515            active_pseudo_states,
1516            type_index,
1517            type_count,
1518            own_siblings,
1519            is_disabled,
1520        ),
1521    }
1522}
1523
1524#[allow(clippy::too_many_arguments)]
1525pub(crate) fn matches_selector_step_node(
1526    node: &Node,
1527    simple: &SimpleSelector,
1528    sibling_index: Option<usize>,
1529    sibling_count: Option<usize>,
1530    active_pseudo_states: &[&str],
1531    type_index: Option<usize>,
1532    type_count: Option<usize>,
1533    own_siblings: Option<&[Node]>,
1534    is_disabled: bool,
1535) -> bool {
1536    match &node.node_type {
1537        NodeType::Element {
1538            tag_name,
1539            classes,
1540            id,
1541            attributes,
1542        } => matches_simple_selector(
1543            tag_name,
1544            id,
1545            classes,
1546            attributes,
1547            simple,
1548            sibling_index,
1549            sibling_count,
1550            active_pseudo_states,
1551            type_index,
1552            type_count,
1553            node.children.is_empty(),
1554            node as *const Node as usize,
1555            &node.children,
1556            own_siblings,
1557            is_disabled,
1558        ),
1559        NodeType::Text(_) => false,
1560    }
1561}
1562
1563/// `:has(sel1, sel2, ...)` 判定用: `children` 以下にリスト中いずれかの単純セレクタへ
1564/// 一致する要素が存在するかを探索する。タプルの `bool` が true(`> sel` 形式)の枝は
1565/// `children`(直接子)のみを見て、それより深くは再帰しない。false(通常の `:has(.foo)`)の
1566/// 枝は任意の深さまで再帰する。子孫側の疑似クラス判定は sibling_index 等の文脈を持たない
1567/// ため `None`/空リストで簡略化して渡す(`:has(.foo)` のようなタグ/クラス/id/属性ベースの
1568/// 判定が主な実用対象で、`:has(:first-child)` 等の兄弟文脈依存の疑似クラスは子孫探索側では
1569/// 正しく判定できない既知の制約)。
1570pub(crate) fn has_descendant_match(children: &[Node], list: &[(bool, SimpleSelector)]) -> bool {
1571    for child in children {
1572        if let NodeType::Element {
1573            tag_name,
1574            id,
1575            classes,
1576            attributes,
1577        } = &child.node_type
1578        {
1579            for (_, sel) in list {
1580                let is_disabled = attributes.contains_key("disabled");
1581                if matches_simple_selector(
1582                    tag_name,
1583                    id,
1584                    classes,
1585                    attributes,
1586                    sel,
1587                    None,
1588                    None,
1589                    &[],
1590                    None,
1591                    None,
1592                    child.children.is_empty(),
1593                    child as *const Node as usize,
1594                    &child.children,
1595                    None,
1596                    is_disabled,
1597                ) {
1598                    return true;
1599                }
1600            }
1601        }
1602        // 直接子限定(`> sel`)の枝は、これより深い子孫へは適用しない。
1603        let deeper_list: Vec<(bool, SimpleSelector)> = list
1604            .iter()
1605            .filter(|(direct_only, _)| !direct_only)
1606            .cloned()
1607            .collect();
1608        if !deeper_list.is_empty() && has_descendant_match(&child.children, &deeper_list) {
1609            return true;
1610        }
1611    }
1612    false
1613}
1614
1615/// `:has(+ sel)`/`:has(~ sel)` 判定用: `siblings`(親の子ノード一覧)中の `my_idx` より
1616/// 後ろの要素ノードで `sel` に一致するものがあるか探索する。`adjacent` が true なら
1617/// 直後の要素兄弟1つのみ(`+`)、false なら以降の要素兄弟いずれか(`~`)を対象にする。
1618pub(crate) fn has_sibling_match(siblings: &[Node], my_idx: usize, adjacent: bool, sel: &SimpleSelector) -> bool {
1619    for sib in siblings.iter().skip(my_idx + 1) {
1620        if let NodeType::Element {
1621            tag_name,
1622            id,
1623            classes,
1624            attributes,
1625        } = &sib.node_type
1626        {
1627            let is_disabled = attributes.contains_key("disabled");
1628            if matches_simple_selector(
1629                tag_name,
1630                id,
1631                classes,
1632                attributes,
1633                sel,
1634                None,
1635                None,
1636                &[],
1637                None,
1638                None,
1639                sib.children.is_empty(),
1640                sib as *const Node as usize,
1641                &sib.children,
1642                None,
1643                is_disabled,
1644            ) {
1645                return true;
1646            }
1647            if adjacent {
1648                return false;
1649            }
1650        }
1651    }
1652    false
1653}
1654
1655/// `:nth-child(An+B of S)`/`:nth-last-child(An+B of S)` 判定用: `own_siblings`(親の子ノード
1656/// 一覧)のうち `S` に一致する要素だけを数えた「フィルタ済み」0-based 位置と総数を返す。
1657/// 自分自身が `S` に一致しない場合は `None`(=不一致。フィルタ済み集合に属さない)。
1658pub(crate) fn filtered_sibling_position(
1659    own_siblings: Option<&[Node]>,
1660    node_ptr: usize,
1661    sel: &SimpleSelector,
1662) -> Option<(usize, usize)> {
1663    let siblings = own_siblings?;
1664    let mut my_pos = None;
1665    let mut count = 0usize;
1666    for sib in siblings {
1667        if let NodeType::Element {
1668            tag_name,
1669            id,
1670            classes,
1671            attributes,
1672        } = &sib.node_type
1673        {
1674            let is_disabled = attributes.contains_key("disabled");
1675            let is_match = matches_simple_selector(
1676                tag_name,
1677                id,
1678                classes,
1679                attributes,
1680                sel,
1681                None,
1682                None,
1683                &[],
1684                None,
1685                None,
1686                sib.children.is_empty(),
1687                sib as *const Node as usize,
1688                &sib.children,
1689                None,
1690                is_disabled,
1691            );
1692            if is_match {
1693                if sib as *const Node as usize == node_ptr {
1694                    my_pos = Some(count);
1695                }
1696                count += 1;
1697            }
1698        }
1699    }
1700    my_pos.map(|idx| (idx, count))
1701}
1702
1703#[allow(clippy::too_many_arguments)]
1704pub(crate) fn matches_selector_chain(
1705    steps: &[SelectorStep],
1706    current: &Node,
1707    ancestors: &[AncestorContext<'_>],
1708    sibling_index: Option<usize>,
1709    sibling_count: Option<usize>,
1710    active_pseudo_states: &[&str],
1711    type_index: Option<usize>,
1712    type_count: Option<usize>,
1713    own_siblings: Option<&[Node]>,
1714    is_disabled: bool,
1715) -> bool {
1716    if steps.is_empty() {
1717        return false;
1718    }
1719    let mut idx = steps.len() - 1;
1720    if !matches_selector_step_node(
1721        current,
1722        &steps[idx].simple,
1723        sibling_index,
1724        sibling_count,
1725        active_pseudo_states,
1726        type_index,
1727        type_count,
1728        own_siblings,
1729        is_disabled,
1730    ) {
1731        return false;
1732    }
1733
1734    let mut anc_pos = ancestors.len();
1735    // 兄弟結合子用に「現在ノードの兄弟内インデックス」を追跡する。
1736    let mut cur_sib_index = sibling_index;
1737    while idx > 0 {
1738        let comb = steps[idx].combinator.unwrap_or(Combinator::Descendant);
1739        idx -= 1;
1740        match comb {
1741            Combinator::Child => {
1742                if anc_pos == 0 {
1743                    return false;
1744                }
1745                anc_pos -= 1;
1746                let anc = ancestors[anc_pos];
1747                let anc_disabled = is_node_disabled(anc.node, &ancestors[0..anc_pos]);
1748                // 祖先位置の of-type 判定は非対応(None,None で常に不一致)。実用上は
1749                // サブジェクト位置での使用がほとんどのため許容する。
1750                if !matches_selector_step_node(
1751                    anc.node,
1752                    &steps[idx].simple,
1753                    anc.sibling_index,
1754                    anc.sibling_count,
1755                    active_pseudo_states,
1756                    None,
1757                    None,
1758                    None,
1759                    anc_disabled,
1760                ) {
1761                    return false;
1762                }
1763                cur_sib_index = anc.sibling_index;
1764            }
1765            Combinator::Descendant => {
1766                let mut found = false;
1767                while anc_pos > 0 {
1768                    anc_pos -= 1;
1769                    let anc = ancestors[anc_pos];
1770                    let anc_disabled = is_node_disabled(anc.node, &ancestors[0..anc_pos]);
1771                    if matches_selector_step_node(
1772                        anc.node,
1773                        &steps[idx].simple,
1774                        anc.sibling_index,
1775                        anc.sibling_count,
1776                        active_pseudo_states,
1777                        None,
1778                        None,
1779                        None,
1780                        anc_disabled,
1781                    ) {
1782                        found = true;
1783                        cur_sib_index = anc.sibling_index;
1784                        break;
1785                    }
1786                }
1787                if !found {
1788                    return false;
1789                }
1790            }
1791            Combinator::NextSibling | Combinator::SubsequentSibling => {
1792                // 親(兄弟を列挙するため)は ancestors の末尾側を消費せず参照する。
1793                if anc_pos == 0 {
1794                    return false;
1795                }
1796                let parent = ancestors[anc_pos - 1].node;
1797                let ci = match cur_sib_index {
1798                    Some(i) => i,
1799                    None => return false,
1800                };
1801                let count = Some(parent.children.len());
1802                let adjacent = comb == Combinator::NextSibling;
1803                let mut found = false;
1804                let mut j = ci;
1805                while j > 0 {
1806                    j -= 1;
1807                    if matches!(parent.children[j].node_type, NodeType::Element { .. }) {
1808                        let sib_disabled = is_node_disabled(&parent.children[j], &ancestors[0..anc_pos - 1]);
1809                        let m = matches_selector_step_node(
1810                            &parent.children[j],
1811                            &steps[idx].simple,
1812                            Some(j),
1813                            count,
1814                            active_pseudo_states,
1815                            None,
1816                            None,
1817                            None,
1818                            sib_disabled,
1819                        );
1820                        if m {
1821                            found = true;
1822                            cur_sib_index = Some(j);
1823                            break;
1824                        }
1825                        if adjacent {
1826                            // 直後兄弟は最初の要素兄弟のみが対象。一致しなければ失敗。
1827                            break;
1828                        }
1829                    }
1830                }
1831                if !found {
1832                    return false;
1833                }
1834            }
1835        }
1836    }
1837    true
1838}
1839