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