Skip to main content

atmos/os_lib/css/
cascade.rs

1// 分割: css.rs より機械的に移動(2026-07-16 リファクタ フェーズ3)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5
6/// 計算量: **O(R×Sel + N × (C × M + K))**
7///
8/// - R: ルール数、Sel: セレクタ数(索引構築ぶん)
9/// - N: DOM ノード数
10/// - C: 1 ノードあたりの候補ルール数(索引で絞った後)
11/// - M: 1 ルールの照合コスト(セレクタ長・祖先チェーンの深さに比例)
12/// - K: ノードごとの固定コスト(継承マップ/CSS 変数/カウンタの複製)
13///
14/// 実測(sugi-lab.net: R=2403, N=358): 索引構築 27 tick、走査 291 tick。
15/// **照合(C×M)は既に全体の一部でしかなく、K が支配的**。
16/// 詳細は `spec/resource_loading.md`。
17pub fn style_tree<'a>(root: &'a Node, stylesheet: &StyleSheet) -> StyledNode<'a> {
18    // ツリー走査の間、ローカルに右端キー索引を構築し、各ノードのカスケードで
19    // 照合するルールを絞り込む(グローバル Mutex ロックを完全排除)。
20    let rule_index = super::rule_index::RuleIndex::build(&stylesheet.rules);
21    let mut ancestors = Vec::new();
22    style_tree_with_ancestors_vars(
23        root,
24        stylesheet,
25        Some(&rule_index),
26        &mut ancestors,
27        None,
28        None,
29        &alloc::rc::Rc::new(BTreeMap::new()),
30        &alloc::rc::Rc::new(BTreeMap::new()),
31        &BTreeMap::new(),
32    )
33    .0
34}
35
36/// HTML5レガシー`width`/`height`属性値をCSS長さへ変換する("rules for parsing
37/// dimension values"の簡略版)。`%`で終われば百分率、それ以外は先頭の数字列を
38/// pxとして扱う(`"400"` → `"400px"`。末尾の単位無しゴミは許容)。
39fn html_legacy_dimension_to_css(raw: &str) -> Option<String> {
40    let s = raw.trim();
41    if s.is_empty() {
42        return None;
43    }
44    if let Some(pct) = s.strip_suffix('%') {
45        let pct = pct.trim();
46        return pct.parse::<f32>().ok().map(|_| alloc::format!("{}%", pct));
47    }
48    let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
49    if digits.is_empty() {
50        return None;
51    }
52    Some(alloc::format!("{}px", digits))
53}
54
55#[derive(Debug, Clone, Copy)]
56pub(crate) struct AncestorContext<'a> {
57    pub(crate) node: &'a Node,
58    pub(crate) sibling_index: Option<usize>,
59    pub(crate) sibling_count: Option<usize>,
60}
61
62#[inline]
63fn insert_ua_pairs(specified_values: &mut BTreeMap<String, String>, pairs: &[(&'static str, &'static str)]) {
64    for &(k, v) in pairs {
65        specified_values.insert(String::from(k), String::from(v));
66    }
67}
68
69/// カスケードで勝ち残った宣言 1 件(!important, @layer 順位, 詳細度, 出現順, 値)。
70type AppliedDecl = (bool, Option<usize>, (u32, u32, u32), usize, String);
71
72/// 一括指定とその個別指定。レイアウトは「一括 → 個別」の順に適用するため、
73/// 両方が残っていると**優先度に関係なく個別指定が勝つ**。
74pub(crate) const BOX_SHORTHANDS: &[(&str, [&str; 4])] = &[
75    ("margin", ["margin-top", "margin-right", "margin-bottom", "margin-left"]),
76    ("padding", ["padding-top", "padding-right", "padding-bottom", "padding-left"]),
77    (
78        "border-width",
79        ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width"],
80    ),
81    ("inset", ["top", "right", "bottom", "left"]),
82];
83
84/// `a` が `b` 以上の優先度か(`compute_styles_with_vars` の置換判定と同じ規則)。
85fn decl_outranks(a: &AppliedDecl, b: &AppliedDecl) -> bool {
86    if a.0 != b.0 {
87        return a.0;
88    }
89    if a.0 {
90        let ar = a.1.map(|l| -(l as i64)).unwrap_or(i64::MIN);
91        let br = b.1.map(|l| -(l as i64)).unwrap_or(i64::MIN);
92        ar > br || (ar == br && (a.2 > b.2 || (a.2 == b.2 && a.3 >= b.3)))
93    } else {
94        let ar = a.1.map(|l| l as i64).unwrap_or(i64::MAX);
95        let br = b.1.map(|l| l as i64).unwrap_or(i64::MAX);
96        ar > br || (ar == br && (a.2 > b.2 || (a.2 == b.2 && a.3 >= b.3)))
97    }
98}
99
100/// 【2026-09-26 バグ修正】一括指定が勝ったとき、それより優先度の低い個別指定を捨てる。
101///
102/// 従来はキーごとに独立して勝者を決めていたため、UA 既定の `ul{margin-left:20px}` が
103/// 作者の `*{margin:0}` に負けず、www.sugi-lab.net のナビ(`ul.nav-list`)が 20px
104/// 右へずれてコンテナからはみ出していた。作者ルール同士
105/// (`.a{padding-left:9px}` の後に詳細度の高い `.a.b{padding:0}`)でも同じことが起きる。
106///
107/// UA 既定(`specified_values` に先に入っている値)は作者宣言より常に低いので、
108/// 作者側に一括指定があれば UA の個別指定は無条件に消す。
109/// 同じ規則内の `margin:0; margin-left:5px` は出現順が同じ扱いになり個別指定が残る
110/// (従来どおり)。逆順の `margin-left:5px; margin:0` は区別できない(簡略実装)。
111fn drop_overridden_longhands(
112    applied: &mut BTreeMap<String, AppliedDecl>,
113    specified_values: &mut BTreeMap<String, String>,
114) {
115    for (short, longs) in BOX_SHORTHANDS {
116        let Some(sh) = applied.get(*short) else {
117            continue;
118        };
119        let sh_rank: AppliedDecl = (sh.0, sh.1, sh.2, sh.3, String::new());
120        for long in longs {
121            specified_values.remove(*long);
122            let long_loses = applied.get(*long).is_some_and(|l| !decl_outranks(l, &sh_rank));
123            if long_loses {
124                applied.remove(*long);
125            }
126        }
127    }
128}
129
130pub(crate) fn compute_styles<'a>(
131    root: &'a Node,
132    stylesheet: &StyleSheet,
133    ancestors: &[AncestorContext<'a>],
134    sibling_index: Option<usize>,
135    sibling_count: Option<usize>,
136    active_pseudo_states: &[&str],
137) -> BTreeMap<String, String> {
138    let (vals, _, _, _) = compute_styles_with_vars(
139        root,
140        stylesheet,
141        None,
142        ancestors,
143        sibling_index,
144        sibling_count,
145        active_pseudo_states,
146        &alloc::rc::Rc::new(BTreeMap::new()),
147        &BTreeMap::new(),
148    );
149    vals
150}
151
152/// `root` にスタイルを適用する。`counters_in` は直前までの文書順カウンタ状態
153/// (`counter-reset`/`counter-increment`は全カウンタをフラットに共有する簡略化実装で、
154/// 実 CSS 仕様のようなネストごとのスコープ分離は行わない)。戻り値の第3要素は
155/// この要素自身の counter-reset/counter-increment 適用後のカウンタ状態。
156pub(crate) fn compute_styles_with_vars<'a>(
157    root: &'a Node,
158    stylesheet: &StyleSheet,
159    rule_index: Option<&super::rule_index::RuleIndex>,
160    ancestors: &[AncestorContext<'a>],
161    sibling_index: Option<usize>,
162    sibling_count: Option<usize>,
163    active_pseudo_states: &[&str],
164    parent_vars: &alloc::rc::Rc<BTreeMap<String, String>>,
165    counters_in: &BTreeMap<String, i64>,
166) -> (
167    BTreeMap<String, String>,
168    alloc::rc::Rc<BTreeMap<String, String>>,
169    BTreeMap<String, i64>,
170    alloc::collections::BTreeSet<String>,
171) {
172    compute_styles_with_vars_stack(
173        root,
174        stylesheet,
175        rule_index,
176        ancestors,
177        sibling_index,
178        sibling_count,
179        active_pseudo_states,
180        parent_vars,
181        counters_in,
182        None,
183    )
184}
185
186pub(crate) fn compute_styles_with_vars_stack<'a>(
187    root: &'a Node,
188    stylesheet: &StyleSheet,
189    rule_index: Option<&super::rule_index::RuleIndex>,
190    ancestors: &[AncestorContext<'a>],
191    sibling_index: Option<usize>,
192    sibling_count: Option<usize>,
193    active_pseudo_states: &[&str],
194    parent_vars: &alloc::rc::Rc<BTreeMap<String, String>>,
195    counters_in: &BTreeMap<String, i64>,
196    counter_stack: Option<&crate::os_lib::css::counter_scope::CounterScopeStack>,
197) -> (
198    BTreeMap<String, String>,
199    alloc::rc::Rc<BTreeMap<String, String>>,
200    BTreeMap<String, i64>,
201    alloc::collections::BTreeSet<String>,
202) {
203    let mut specified_values = BTreeMap::new();
204    // 【2026-08-05 性能】以前は全ノードで `parent_vars.clone()` していた。
205    //
206    // CSS 変数を定義するノードは `:root` などごく一部なのに、
207    // 358 ノードすべてが 10 数個の `String` ペアを複製していた
208    // (1 レイアウトあたり数千回の確保)。
209    // `Rc` にして**書き込むノードだけ**が複製するようにする
210    // (`Rc::make_mut` が必要なときだけ実体を複製する)。
211    let mut css_vars: alloc::rc::Rc<BTreeMap<String, String>> = parent_vars.clone();
212    let mut counters_out = if counters_in.is_empty() {
213        BTreeMap::new()
214    } else {
215        counters_in.clone()
216    };
217    let mut counters_reset_names = alloc::collections::BTreeSet::new();
218
219    match &root.node_type {
220        NodeType::Element {
221            tag_name,
222            attributes,
223            classes: _,
224            id: _,
225        } => {
226            match tag_name.as_str() {
227                "h1" => {
228                    insert_ua_pairs(&mut specified_values, &[
229                        ("display", "block"),
230                        ("font-size", "32"),
231                        ("font-weight", "bold"),
232                        ("margin-top", "21"),
233                        ("margin-bottom", "21"),
234                    ]);
235                }
236                "h2" => {
237                    insert_ua_pairs(&mut specified_values, &[
238                        ("display", "block"),
239                        ("font-size", "24"),
240                        ("font-weight", "bold"),
241                        ("margin-top", "20"),
242                        ("margin-bottom", "20"),
243                    ]);
244                }
245                "h3" => {
246                    insert_ua_pairs(&mut specified_values, &[
247                        ("display", "block"),
248                        ("font-size", "18"),
249                        ("font-weight", "bold"),
250                        ("margin-top", "18"),
251                        ("margin-bottom", "18"),
252                    ]);
253                }
254                "p" => {
255                    insert_ua_pairs(&mut specified_values, &[
256                        ("display", "block"),
257                        ("margin-top", "16"),
258                        ("margin-bottom", "16"),
259                    ]);
260                }
261                "body" => {
262                    insert_ua_pairs(&mut specified_values, &[
263                        ("display", "block"),
264                        ("margin", "8"),
265                    ]);
266                }
267                "ul" | "ol" => {
268                    insert_ua_pairs(&mut specified_values, &[
269                        ("display", "block"),
270                        ("margin-left", "20px"),
271                    ]);
272                    // UA既定として `list-item` カウンタをこのリストのスコープでリセットする
273                    // (実 CSS の `ol`/`ul` UA スタイルシートにある `counter-reset: list-item`
274                    // に相当)。ネストした `<ol>` はこのリセットにより自動的に 1 から再スタート
275                    // する(既存の counter-reset ネストスコープ折り畳みロジックがそのまま効く)。
276                    // author が `counter-reset` を明示指定すればここは通常のカスケードで
277                    // 上書きされる(同一キーの specified_values 挿入は後勝ちのため)。
278                    //
279                    // `<ol start="N">`(HTML属性、丸ごと未対応だった): `counter-increment`
280                    // は表示前に加算されるため、初期値を `N-1` にリセットして最初の `<li>` が
281                    // ちょうど `N` になるようにする(`counter-reset:list-item 10` で最初の
282                    // `<li>` が 11 になる既存の挙動と同じオフセット規則)。
283                    let is_ol = tag_name.eq_ignore_ascii_case("ol");
284                    let start_attr = if is_ol {
285                        attributes.get("start").and_then(|s| s.trim().parse::<i64>().ok())
286                    } else {
287                        None
288                    };
289                    // `<ol reversed>`(HTML属性、丸ごと未対応だった): `<li>` 側の
290                    // counter-increment を -1 にする(下記 "li" 分岐が祖先の reversed を
291                    // 見て切り替える)ため、こちら側は初期値を「開始値+1」にする必要がある
292                    // (増分が -1 のため、最初の li は reset-1=開始値になる)。開始値は
293                    // 明示 `start` があればそれ、無ければ直下の `<li>` 子要素数
294                    // (降順のデフォルトは要素数から1まで数える仕様)。
295                    let reversed = is_ol && attributes.contains_key("reversed");
296                    let start_val = if reversed {
297                        let li_count = root
298                            .children
299                            .iter()
300                            .filter(|c| {
301                                matches!(&c.node_type, NodeType::Element { tag_name, .. } if tag_name.eq_ignore_ascii_case("li"))
302                            })
303                            .count() as i64;
304                        start_attr.unwrap_or(li_count) + 1
305                    } else {
306                        start_attr.map(|n| n - 1).unwrap_or(0)
307                    };
308                    specified_values.insert(
309                        String::from("counter-reset"),
310                        alloc::format!("list-item {start_val}"),
311                    );
312                }
313                "li" => {
314                    specified_values.insert(String::from("display"), String::from("block"));
315                    // UA既定: `li` は毎回 `list-item` カウンタを1つ進める
316                    // (実 CSS の `li { counter-increment: list-item; }` に相当)。
317                    // `content: counter(list-item)` で著者が独自の番号書式を組み立てられるようにする。
318                    // 直近の祖先が `<ol reversed>` なら降順(-1)に切り替える
319                    // (`<ol reversed>` 自体は上の "ol" 分岐で初期値側を調整済み)。
320                    let parent_reversed = ancestors.last().is_some_and(|a| {
321                        matches!(&a.node.node_type, NodeType::Element { tag_name, attributes, .. }
322                            if tag_name.eq_ignore_ascii_case("ol") && attributes.contains_key("reversed"))
323                    });
324                    let incr = if parent_reversed { "list-item -1" } else { "list-item" };
325                    specified_values
326                        .insert(String::from("counter-increment"), String::from(incr));
327                }
328                "pre" => {
329                    insert_ua_pairs(&mut specified_values, &[
330                        ("display", "block"),
331                        // HTML の UA スタイル(`pre { white-space: pre; font-family: monospace }`)。
332                        // 【2026-10-16】`white-space` が無く、`<pre>` の改行・連続空白が畳まれて
333                        // 1 行になっていた(JS で `pre.textContent += '\n' + ...` するログ表示等)。
334                        ("white-space", "pre"),
335                        ("font-family", "monospace"),
336                        ("background-color", "#f6f8fa"),
337                        ("color", "#24292e"),
338                        ("border-color", "#d0d7de"),
339                        ("border-style", "solid"),
340                        ("border-width", "1px"),
341                        ("border-radius", "6px"),
342                        ("padding", "12px"),
343                        ("margin-top", "10px"),
344                        ("margin-bottom", "10px"),
345                    ]);
346                }
347                "code" => {
348                    insert_ua_pairs(&mut specified_values, &[
349                        ("display", "inline"),
350                        ("background-color", "#f0f0f0"),
351                        ("color", "#d63384"),
352                        ("padding", "2px 4px"),
353                        ("border-radius", "3px"),
354                    ]);
355                }
356                "blockquote" => {
357                    insert_ua_pairs(&mut specified_values, &[
358                        ("display", "block"),
359                        ("margin-left", "20px"),
360                        ("margin-right", "20px"),
361                        ("margin-top", "10px"),
362                        ("margin-bottom", "10px"),
363                        ("border-left", "4px solid #BD93F9"),
364                    ]);
365                }
366                "div" | "html" | "form" | "header" | "footer" | "main" | "section" | "nav"
367                | "hr" | "article" | "aside" | "figure" | "figcaption" | "fieldset" | "legend"
368                | "dl" | "dt" | "dd" | "details" | "summary" => {
369                    specified_values.insert(String::from("display"), String::from("block"));
370                }
371                // `<dialog>`(丸ごと未対応だった)。仕様上 `open` 属性が無い間は既定で
372                // 非表示(`display: none`)、`open` 属性が付くと表示される。`show()`/
373                // `showModal()`/`close()` はこの `open` 属性を JS 側で付け外しするだけの
374                // 簡略実装(`::backdrop` 描画・モーダルフォーカストラップは対象外)。
375                "dialog" => {
376                    specified_values.insert(
377                        String::from("display"),
378                        if attributes.contains_key("open") {
379                            String::from("block")
380                        } else {
381                            String::from("none")
382                        },
383                    );
384                }
385                // 実際の描画(動画再生・SVG図形・埋め込み文書等)は非対応でも、既定 display だけは
386                // 仕様に合わせておく(無指定時に `_ => inline` へ落ちてレイアウトが崩れるのを防ぐ)。
387                "select" | "video" | "audio" | "iframe" | "svg" | "canvas" => {
388                    specified_values.insert(String::from("display"), String::from("inline-block"));
389                }
390                "center" => {
391                    insert_ua_pairs(&mut specified_values, &[
392                        ("display", "block"),
393                        ("text-align", "center"),
394                    ]);
395                }
396                "img" => {
397                    specified_values.insert(String::from("display"), String::from("inline-block"));
398                }
399                "a" => {
400                    insert_ua_pairs(&mut specified_values, &[
401                        ("display", "inline"),
402                        ("text-decoration", "underline"),
403                    ]);
404                    // 【2026-10-03】UA の `a:link { color: -webkit-link }` 相当。リンクは親の文字色を
405                    // 継承せず、作成者が色を指定しなければテーマのリンク色で描く(描画側が
406                    // `-webkit-link` を解釈する)。従来は本文の色を継承していた。
407                    if attributes.contains_key("href") {
408                        specified_values.insert(String::from("color"), String::from("-webkit-link"));
409                    }
410                }
411                "strong" | "b" => {
412                    insert_ua_pairs(&mut specified_values, &[
413                        ("display", "inline"),
414                        ("font-weight", "bold"),
415                    ]);
416                }
417                "em" | "i" => {
418                    insert_ua_pairs(&mut specified_values, &[
419                        ("display", "inline"),
420                        ("font-style", "italic"),
421                    ]);
422                }
423                "u" => {
424                    insert_ua_pairs(&mut specified_values, &[
425                        ("display", "inline"),
426                        ("text-decoration", "underline"),
427                    ]);
428                }
429                "s" | "del" | "strike" => {
430                    insert_ua_pairs(&mut specified_values, &[
431                        ("display", "inline"),
432                        ("text-decoration", "line-through"),
433                    ]);
434                }
435                "input" | "textarea" | "button" => {
436                    insert_ua_pairs(&mut specified_values, &[
437                        ("display", "inline-block"),
438                        ("border-radius", "6px"),
439                    ]);
440                }
441                "table" => {
442                    insert_ua_pairs(&mut specified_values, &[
443                        ("display", "table"),
444                        ("border-color", "#6272A4"),
445                    ]);
446                }
447                "thead" | "tbody" | "tfoot" => {
448                    specified_values
449                        .insert(String::from("display"), String::from("table-row-group"));
450                }
451                "tr" => {
452                    specified_values.insert(String::from("display"), String::from("table-row"));
453                }
454                "td" => {
455                    insert_ua_pairs(&mut specified_values, &[
456                        ("display", "table-cell"),
457                        ("padding", "4px 8px"),
458                    ]);
459                }
460                "th" => {
461                    insert_ua_pairs(&mut specified_values, &[
462                        ("display", "table-cell"),
463                        ("padding", "4px 8px"),
464                        ("font-weight", "bold"),
465                        ("text-align", "center"),
466                    ]);
467                }
468                "caption" => {
469                    insert_ua_pairs(&mut specified_values, &[
470                        ("display", "block"),
471                        ("text-align", "center"),
472                    ]);
473                }
474                // `noscript`: この処理系は JS を実行する(スクリプト有効)ため、HTML5 仕様
475                // どおり常に非表示にする(スクリプト無効時のみ中身を表示する要素のため)。
476                "head" | "title" | "meta" | "link" | "style" | "script" | "template"
477                | "noscript" => {
478                    specified_values.insert(String::from("display"), String::from("none"));
479                }
480                _ => {
481                    specified_values.insert(String::from("display"), String::from("inline"));
482                }
483            }
484
485            // HTML5 のレガシー`width`/`height`属性(`<img>`/`<iframe>`/`<video>`/
486            // `<embed>`/`<object>`/`<canvas>`)。実ブラウザでは UA スタイルシート相当の
487            // 優先度(著者CSSより弱い)でサイズを決めるが、CSS指定が無い場合はこの
488            // 属性だけがサイズの手がかりになる。以前はどこにも配線されておらず、
489            // `<iframe width="100%" height="400">`のようなレガシー記法(実サイト
490            // www.sugi-lab.net の Access ページの地図埋め込み等で使用)が完全に
491            // 無視され、サイズ未指定要素として折りたたまれるバグだった
492            // (2026-07-21発見・修正)。`hidden`属性と同じ「タグ別UA既定値を
493            // 上書きする形で適用し、後続の著者スタイルカスケードでauthorが
494            // width/heightを明示指定すればそちらが優先される」という方針。
495            if matches!(
496                tag_name.as_str(),
497                "img" | "iframe" | "video" | "embed" | "object" | "canvas"
498            ) {
499                if let Some(w) = attributes.get("width").and_then(|v| html_legacy_dimension_to_css(v)) {
500                    specified_values.insert(String::from("width"), w);
501                }
502                if let Some(h) = attributes.get("height").and_then(|v| html_legacy_dimension_to_css(v)) {
503                    specified_values.insert(String::from("height"), h);
504                }
505            }
506
507            // HTML5 のグローバル真偽属性 `hidden`。実ブラウザでは UA スタイルシートの
508            // `[hidden] { display: none }` が担う。ここではタグ別 UA 既定値を上書きする形で
509            // 適用し、後続の著者スタイルカスケードで author が `display` を明示指定すれば
510            // そちらが(この処理系の「著者スタイル優先」という簡略化方針どおり)優先される。
511            if attributes.contains_key("hidden") {
512                specified_values.insert(String::from("display"), String::from("none"));
513            }
514
515            // Popover API(`popover` グローバル属性。丸ごと未対応だった)。仕様上
516            // `[popover]` 要素は既定で非表示、`showPopover()` 呼び出しで表示に
517            // 切り替わる(`:popover-open` 状態)。`<dialog>` の `open` 属性と同じ
518            // 「JS 側で内部専用属性を付け外しするだけ」の簡略実装とし、
519            // `_popover_open` という内部専用属性名を使う(`::backdrop` 描画・
520            // ライトディスミス・`beforetoggle`/`toggle` イベントは対象外)。
521            if attributes.contains_key("popover") && !attributes.contains_key("_popover_open") {
522                specified_values.insert(String::from("display"), String::from("none"));
523            }
524
525            // <details> が `open` 属性を持っていない場合、最初の <summary> 要素以外の子要素を非表示(display: none)にする。
526            if let Some(parent_ctx) = ancestors.last() {
527                if let NodeType::Element { tag_name: parent_tag, attributes: parent_attrs, .. } = &parent_ctx.node.node_type {
528                    if parent_tag == "details" && !parent_attrs.contains_key("open") {
529                        // 親の children の中で最初の <summary> 要素の位置を特定する。
530                        let mut first_summary_idx = None;
531                        for (idx, child) in parent_ctx.node.children.iter().enumerate() {
532                            if let NodeType::Element { tag_name: child_tag, .. } = &child.node_type {
533                                if child_tag == "summary" {
534                                    first_summary_idx = Some(idx);
535                                    break;
536                                }
537                             }
538                        }
539
540                        let is_first_summary = if let Some(fs_idx) = first_summary_idx {
541                            sibling_index == Some(fs_idx)
542                        } else {
543                            false
544                        };
545
546                        if !is_first_summary {
547                            specified_values.insert(String::from("display"), String::from("none"));
548                        }
549                    }
550                }
551            }
552
553            // HTML5 のグローバル真偽属性 `inert`(丸ごと未対応だった)。仕様上の完全な
554            // 意味論(フォーカス不可・アクセシビリティツリーから除外・検索対象外等)は
555            // フォーカス管理/イベント配送の複数箇所にまたがる大改修が必要なため対象外
556            // だが、「クリック等のポインタ操作を受け付けない」という中核効果は既存の
557            // `pointer-events` の実装済みの挙動をそのまま使って実現できる(実ブラウザの
558            // UA スタイルシートも `[inert] { pointer-events: none; }` 相当を持つ)。
559            // `hidden` と同様、著者スタイルが `pointer-events` を明示指定すればそちらが
560            // 優先される。
561            if attributes.contains_key("inert") {
562                specified_values.insert(String::from("pointer-events"), String::from("none"));
563            }
564
565            // HTML5 のグローバル属性 `dir="ltr"/"rtl"`。実 HTML では CSS `direction` の
566            // プレゼンテーショナルヒントとして働く。`auto`(内容の文字種から双方向判定する
567            // モード)はこの処理系にその判定機構が無いため非対応(値を無視し既定の継承へ
568            // フォールバックする)。`hidden` と同様、後続の著者スタイルカスケードで
569            // `direction` が明示指定されていればそちらが優先される。
570            if let Some(dv) = attributes.get("dir").map(|v| v.trim().to_lowercase()) {
571                if dv == "ltr" || dv == "rtl" {
572                    specified_values.insert(String::from("direction"), dv);
573                }
574            }
575
576            // 値ごとに (important, specificity, source_order, value) を保持。
577            // 比較順: !important が最優先 → 特異度 → ソース順。
578            let mut applied: BTreeMap<String, AppliedDecl> = BTreeMap::new();
579
580            // 【2026-07-27 性能修正】祖先を辿る `is_node_disabled` と兄弟を数える
581            // `type_sibling_position` は**このノードについて一度計算すれば十分**
582            // なのに、従来は `matches_selector` の内部でルールごとに再計算して
583            // いた。実サイト規模(2403 ルール × 365 ノード ≒ 88 万回)ではこれが
584            // 支配的コストとなり、1 回のカスケードに約 46 秒かかっていた。
585            // ループの外で 1 回だけ求めて使い回す。
586            let node_ctx = crate::os_lib::css::selector::NodeMatchContext::new(
587                root,
588                ancestors,
589                sibling_index,
590            );
591
592            // 【2026-07-28 性能修正】右端キー索引でこのノードに関係し得るルールだけへ絞る。
593            // 実サイトは Font Awesome 込みで 2403 ルールあり、従来は全ノードが全ルールを
594            // 総当たりしていた(≒88 万回の照合)。1 回のレイアウトに約 50 秒かかり、
595            // JS タイマーが毎フレーム DOM を dirty にするため描画が一度も完了しなかった。
596            // 索引が使えない場合は従来どおり全ルールを見る(結果は変わらず遅いだけ)。
597            let rule_orders: alloc::vec::Vec<usize> = match &root.node_type {
598                NodeType::Element {
599                    tag_name,
600                    classes,
601                    id,
602                    ..
603                } => {
604                    if let Some(idx) = rule_index {
605                        idx.candidates(tag_name.as_str(), id.as_deref(), classes.as_slice())
606                            .unwrap_or_else(|_| (0..stylesheet.rules.len()).collect())
607                    } else {
608                        (0..stylesheet.rules.len()).collect()
609                    }
610                }
611                // 【2026-07-31 性能修正】テキストノードはどのセレクタにも一致しないため空リスト
612                _ => alloc::vec::Vec::new(),
613            };
614
615            for rule_order in rule_orders {
616                let rule = &stylesheet.rules[rule_order];
617                // 【2026-07-24追加】`@media`条件の動的判定。パース時の静的除外
618                // (旧実装)だと、CSS解析はコンテンツ変化時の一度きりしか走らない
619                // ため、その一度きりの評価時点のウィンドウ幅と実際の表示時の
620                // ウィンドウ幅がズレると、モバイルファーストCSSのナビゲーション等が
621                // 永久に非表示のままになるバグがあった。ここで毎回(レイアウトの
622                // たびに)動的に再評価することで、ウィンドウ幅の変化に追従する。
623                if let Some(q) = &rule.media_query {
624                    if !crate::os_lib::css::media_query_matches(q) {
625                        continue;
626                    }
627                }
628                // 【2026-09-03】`@container` も同じく動的判定する。
629                //
630                // 基準はビューポートではなく「問い合わせコンテナ」なので、
631                // レイアウト側が `CONTAINER_HINT_*` へ現在のコンテナ寸法を
632                // 入れてからカスケードを呼ぶ。まだレイアウトが一度も
633                // 走っていない初回は 0 が入っており、その場合は
634                // ビューポート幅で代用する(0 のまま評価すると
635                // `min-width` 系が軒並み偽になり、コンテナクエリを持つ
636                // ページが初回描画で崩れるため)。
637                if let Some(q) = &rule.container_query {
638                    let mut cw = crate::os_lib::layout::CONTAINER_HINT_W
639                        .load(core::sync::atomic::Ordering::Relaxed);
640                    let mut ch = crate::os_lib::layout::CONTAINER_HINT_H
641                        .load(core::sync::atomic::Ordering::Relaxed);
642                    if cw <= 0 {
643                        cw = crate::os_lib::layout::VIEWPORT_HINT_W
644                            .load(core::sync::atomic::Ordering::Relaxed);
645                    }
646                    if ch <= 0 {
647                        ch = crate::os_lib::layout::VIEWPORT_HINT_H
648                            .load(core::sync::atomic::Ordering::Relaxed);
649                    }
650                    let size = crate::os_lib::css::container_query::ContainerSize {
651                        width: cw,
652                        height: ch,
653                    };
654                    // 入れ子は `&&` 連結。すべて満たす必要がある。
655                    let all_ok = q.split("&&").all(|one| {
656                        // 保持形式は `名前|条件`。名前は現状使わない
657                        // (最も近い祖先のコンテナを常に対象とする簡略実装)。
658                        let cond = one.split_once('|').map(|(_, c)| c).unwrap_or(one);
659                        crate::os_lib::css::container_query::condition_matches(cond, size)
660                    });
661                    if !all_ok {
662                        continue;
663                    }
664                }
665                let mut matched_spec: Option<(u32, u32, u32)> = None;
666                let mut matched_pseudo_el: Option<PseudoElement> = None;
667                for selector in &rule.selectors {
668                    if matches_selector(
669                        selector,
670                        root,
671                        ancestors,
672                        sibling_index,
673                        sibling_count,
674                        active_pseudo_states,
675                        &node_ctx,
676                    ) {
677                        let spec = selector_specificity(selector);
678                        if matched_spec.is_none_or(|cur| spec > cur) {
679                            matched_spec = Some(spec);
680                            matched_pseudo_el = selector_pseudo_element(selector);
681                        }
682                    }
683                }
684                if let Some(spec) = matched_spec {
685                    // ::before / ::after / ::marker にマッチしたルールは、要素自体には
686                    // 対象プロパティ以外を適用しない(疑似要素専用の宣言をそのまま親要素へ
687                    // 流し込まないため)。content は合成プロパティ(x-before-content /
688                    // x-after-content)へ、::marker の color は x-marker-color へ振り替え、
689                    // flatten_layout 側で読み出して描画する。
690                    // 注意: "--" 始まりのキーは下の後処理で specified_values ではなく
691                    // css_vars 側へ入ってしまい styled.value() から読めなくなるため、
692                    // ダッシュ2つ始まりにしない。
693                    for decl in &rule.declarations {
694                        let key: Option<alloc::borrow::Cow<'_, str>> = match matched_pseudo_el {
695                            // 【2026-10-03】::before/::after は全宣言を残す(従来は content/display/
696                            // font-family のみ)。位置指定された疑似要素を背景・位置を持つ箱として描くため
697                            // (`layout::pseudo_box`)。
698                            Some(PseudoElement::Before) => {
699                                Some(alloc::borrow::Cow::Owned(alloc::format!("x-before-{}", decl.name)))
700                            }
701                            Some(PseudoElement::After) => {
702                                Some(alloc::borrow::Cow::Owned(alloc::format!("x-after-{}", decl.name)))
703                            }
704                            Some(PseudoElement::Marker) => {
705                                matches!(decl.name.as_str(), "color" | "font-size" | "content")
706                                    .then(|| alloc::borrow::Cow::Owned(alloc::format!("x-marker-{}", decl.name)))
707                            }
708                            Some(PseudoElement::Placeholder) => {
709                                (decl.name == "color").then(|| alloc::borrow::Cow::Borrowed("x-placeholder-color"))
710                            }
711                            Some(PseudoElement::FirstLine) => matches!(
712                                decl.name.as_str(),
713                                "color" | "font-size" | "font-weight" | "font-style"
714                            )
715                            .then(|| alloc::borrow::Cow::Owned(alloc::format!("x-first-line-{}", decl.name))),
716                            Some(PseudoElement::FirstLetter) => matches!(
717                                decl.name.as_str(),
718                                "color" | "font-size" | "font-weight" | "font-style"
719                            )
720                            .then(|| alloc::borrow::Cow::Owned(alloc::format!("x-first-letter-{}", decl.name))),
721                            Some(PseudoElement::Selection) => matches!(
722                                decl.name.as_str(),
723                                "color" | "background-color" | "background"
724                            )
725                            .then(|| alloc::borrow::Cow::Owned(alloc::format!("x-selection-{}", decl.name))),
726                            None => Some(alloc::borrow::Cow::Borrowed(decl.name.as_str())),
727                        };
728                        let Some(key) = key else {
729                            continue;
730                        };
731                        let should_replace = match applied.get(key.as_ref()) {
732                            Some((cur_imp, cur_layer, cur_spec, cur_order, _)) => {
733                                if decl.important != *cur_imp {
734                                    decl.important // important は非 important に常に勝つ
735                                } else if decl.important {
736                                    // !important: 早期レイヤーが優先、unlayered が最弱 (W3C 反転仕様)
737                                    let cur_rank = cur_layer.map(|l| -(l as i64)).unwrap_or(i64::MIN);
738                                    let new_rank = rule.layer.map(|l| -(l as i64)).unwrap_or(i64::MIN);
739                                    new_rank > cur_rank
740                                        || (new_rank == cur_rank
741                                            && (spec > *cur_spec
742                                                || (spec == *cur_spec && rule_order >= *cur_order)))
743                                } else {
744                                    // 通常宣言: `@layer` の優先度(無し=最優先、後のレイヤーほど
745                                    // 優先)→ specificity → source order の順で比較する。
746                                    let cur_rank = cur_layer.map(|l| l as i64).unwrap_or(i64::MAX);
747                                    let new_rank = rule.layer.map(|l| l as i64).unwrap_or(i64::MAX);
748                                    new_rank > cur_rank
749                                        || (new_rank == cur_rank
750                                            && (spec > *cur_spec
751                                                || (spec == *cur_spec && rule_order >= *cur_order)))
752                                }
753                            }
754                            None => true,
755                        };
756                        if should_replace {
757                            // content の実解析(attr()/counter() 展開)はこの要素自身の
758                            // counter-reset/counter-increment 確定後まで遅延させる必要があるため、
759                            // ここでは生の宣言値のまま保持する(下の apply_counter_decls 後に解決)。
760                            applied.insert(
761                                key.into_owned(),
762                                (decl.important, rule.layer, spec, rule_order, decl.value.clone()),
763                            );
764                        }
765                    }
766                }
767            }
768            // カスケード優先順位(CSS仕様): author !important > inline style !important
769            // > inline style(通常)> author 通常。インライン style="" の上書きより先に、
770            // どのキーが author 側の `!important` で確定したかを覚えておく必要がある
771            // (`applied` を消費してしまう前に記録する)。
772            drop_overridden_longhands(&mut applied, &mut specified_values);
773            let mut important_keys: alloc::collections::BTreeSet<String> =
774                alloc::collections::BTreeSet::new();
775            for (k, (is_important, _, _, _, _)) in &applied {
776                if *is_important {
777                    important_keys.insert(k.clone());
778                }
779            }
780            for (k, (_, _, _, _, v)) in applied {
781                if k.starts_with("--") {
782                    alloc::rc::Rc::make_mut(&mut css_vars).insert(k, v);
783                } else {
784                    let resolved = if v.contains("var(") {
785                        resolve_css_var(&v, &css_vars)
786                    } else {
787                        v
788                    };
789                    let resolved = if resolved.contains("env(") {
790                        resolve_css_env(&resolved)
791                    } else {
792                        resolved
793                    };
794                    specified_values.insert(k, resolved);
795                }
796            }
797
798            if let Some(style_attr) = attributes.get("style") {
799                for decl in style_attr.split(';') {
800                    let mut parts = decl.splitn(2, ':');
801                    if let (Some(k), Some(v)) = (parts.next(), parts.next()) {
802                        let k_trim = k.trim().to_string();
803                        let raw_value = v.trim();
804                        // `!important` を切り出す(スタイルシート側の宣言パースと同じ規則)。
805                        let (v_trim, inline_important) = match raw_value.rfind('!') {
806                            Some(pos)
807                                if raw_value
808                                    .get(pos + 1..)
809                                    .unwrap_or("")
810                                    .trim()
811                                    .eq_ignore_ascii_case("important") =>
812                            {
813                                (
814                                    String::from(raw_value.get(..pos).unwrap_or("").trim()),
815                                    true,
816                                )
817                            }
818                            _ => (String::from(raw_value), false),
819                        };
820                        // 値が空の宣言(`display: `)は不正として捨てる(CSS 構文)。カスタムプロパティは除く。
821                        // 【2026-10-18】YouTube の操作部の包み `div style="display: "` が display:"" になり
822                        // インラインの箱として組まれ、中の absolute(中央の再生ボタン)が配置されなかった。
823                        if v_trim.is_empty() && !k_trim.starts_with("--") {
824                            continue;
825                        }
826                        if !k_trim.is_empty() {
827                            // author 側が `!important` で確定済みのキーは、インライン側が
828                            // `!important` でない限り上書きしない(CSS仕様のカスケード優先順位)。
829                            if important_keys.contains(&k_trim) && !inline_important {
830                                continue;
831                            }
832                            if k_trim.starts_with("--") {
833                                alloc::rc::Rc::make_mut(&mut css_vars).insert(k_trim, v_trim);
834                            } else {
835                                let resolved = if v_trim.contains("var(") {
836                                    resolve_css_var(&v_trim, &css_vars)
837                                } else {
838                                    v_trim
839                                };
840                                let resolved = if resolved.contains("env(") {
841                                    resolve_css_env(&resolved)
842                                } else {
843                                    resolved
844                                };
845                                // インライン一括指定は作者の通常個別指定より強い
846                                // (`drop_overridden_longhands` と同じ理由)。
847                                if let Some((_, longs)) =
848                                    BOX_SHORTHANDS.iter().find(|(sh, _)| *sh == k_trim.as_str())
849                                {
850                                    for long in longs {
851                                        if !important_keys.contains(*long) {
852                                            specified_values.remove(*long);
853                                        }
854                                    }
855                                }
856                                specified_values.insert(k_trim, resolved);
857                            }
858                        }
859                    }
860                }
861            }
862            expand_border_shorthands(&mut specified_values);
863            expand_place_shorthands(&mut specified_values);
864            expand_grid_template_shorthand(&mut specified_values);
865            expand_text_emphasis_shorthand(&mut specified_values);
866            expand_font_shorthand(&mut specified_values);
867            expand_background_shorthand(&mut specified_values);
868
869            // counter-reset/counter-increment をこの要素の確定値から適用してから、
870            // 保留していた ::before/::after の content(counter() 参照を含む)を解決する。
871            counters_reset_names = apply_counter_decls(&specified_values, &mut counters_out);
872            let mut stack_buf;
873            let current_stack = if let Some(st) = counter_stack {
874                stack_buf = st.clone();
875                crate::os_lib::css::counter_scope::apply_counter_decls_to_stack(&specified_values, &mut stack_buf);
876                Some(&stack_buf)
877            } else {
878                None
879            };
880            // `quotes` は同一要素(::before/::after の元となる要素自身)への明示指定のみ対応。
881            // 祖先からの継承は、この時点ではまだ継承伝搬(style_tree_with_ancestors_vars 側で
882            // 後段に実行)が済んでいないため参照できない、という簡略化の制約がある。
883            let quotes_pairs = specified_values
884                .get("quotes")
885                .and_then(|v| parse_quotes_pairs(v));
886            // 入れ子レベル(`open-quote`/`close-quote` が何組目のペアを使うか)は、
887            // 祖先の `<q>` 要素の個数から近似する(`<q>` は quotation を表す代表的な要素で、
888            // 実用上のネスト深さと一致する)。以前は入れ子レベルを一切追跡せず常に最初の
889            // ペアだけを使っていたため、入れ子の `<q>` で内側/外側とも同じ引用符になる
890            // バグがあった。
891            let quote_depth = ancestors
892                .iter()
893                .filter(|a| {
894                    matches!(&a.node.node_type, NodeType::Element { tag_name, .. } if tag_name == "q")
895                })
896                .count();
897            for key in ["x-before-content", "x-after-content", "x-marker-content"] {
898                if let Some(raw) = specified_values.get(key).cloned() {
899                    // `content:none`/`normal` は疑似要素を生成しない。空文字(`content:''`、箱は作る)と
900                    // 区別できるよう、キーごと消す(2026-10-03。`layout::pseudo_box`)。
901                    if key != "x-marker-content"
902                        && matches!(raw.trim().to_ascii_lowercase().as_str(), "none" | "normal")
903                    {
904                        specified_values.remove(key);
905                        continue;
906                    }
907                    let resolved = crate::os_lib::css::selector::parse_content_value_with_stack(
908                        &raw,
909                        attributes,
910                        &counters_out,
911                        current_stack,
912                        quotes_pairs.as_deref(),
913                        quote_depth,
914                        &stylesheet.counter_styles,
915                    );
916                    specified_values.insert(String::from(key), resolved);
917                }
918            }
919        }
920        NodeType::Text(_) => {
921            specified_values.insert(String::from("display"), String::from("inline"));
922        }
923    }
924    (specified_values, css_vars, counters_out, counters_reset_names)
925}
926
927/// 親から子へ継承される CSS プロパティ(テキストノードのレイアウト計測に必須)
928pub(crate) const INHERITED_PROPS: &[&str] = &[
929    "color",
930    "-webkit-text-fill-color",
931    // 【2026-08-03 発見・修正】`font-family` が抜けていた。
932    // CSS 2.1 §15.3 で font-family は**継承プロパティ**である。
933    // 抜けていたため `body { font-family: 'Noto Sans JP', sans-serif }` が
934    // 子孫へ伝わらず、テキスト要素の family が常に空になっていた。
935    // その結果、Web フォントを取得・展開・登録できても
936    // **描画で一度も選ばれなかった**
937    // (実測: 使われたのは要素自身に family 指定のあるアイコンフォントだけ)。
938    "font-family",
939    "font-size",
940    "font-weight",
941    "font-style",
942    "line-height",
943    "text-align",
944    "letter-spacing",
945    "word-spacing",
946    "white-space",
947    "text-transform",
948    "list-style-type",
949    "list-style-image",
950    "list-style-position",
951    "list-style",
952    "tab-size",
953    "direction",
954    "visibility",
955    // `text-indent` は継承プロパティ(ブロック要素に指定した値が子孫のテキストノードへ
956    // 伝播すべき)だが、以前は INHERITED_PROPS に含まれておらず、通常の書き方
957    // (`p{text-indent:2em}` のように親要素へ指定しテキストノード自身には指定しない)
958    // では常に無視されるバグだった(`list-style-image`/`list-style-position` と同種)。
959    "text-indent",
960    // `border-collapse` も継承プロパティ(`layout.rs` が `TableNode` 自身の値しか見ないため、
961    // `body{border-collapse:collapse}` のような祖先要素への指定が配下の `<table>` へ
962    // 伝播しないバグがあった。`table` 要素自身への直接指定は元々動作するため見落とされていた)。
963    "border-collapse",
964    // `text-emphasis-style`/`text-emphasis-color` も継承プロパティ(実 CSS 仕様)。
965    "text-emphasis-style",
966    "text-emphasis-color",
967    // `cursor` も継承プロパティ(実 CSS 仕様。祖先要素への指定が子孫へ伝播する)。
968    "cursor",
969    // `word-break`/`overflow-wrap`/`word-wrap` も継承プロパティ(CSS Text Module Specs)。
970    "word-break",
971    "overflow-wrap",
972    "word-wrap",
973];
974
975pub(crate) fn style_tree_with_ancestors<'a>(
976    root: &'a Node,
977    stylesheet: &StyleSheet,
978    ancestors: &[AncestorContext<'a>],
979    sibling_index: Option<usize>,
980    sibling_count: Option<usize>,
981) -> StyledNode<'a> {
982    let mut stack = ancestors.to_vec();
983    style_tree_with_ancestors_vars(
984        root,
985        stylesheet,
986        None,
987        &mut stack,
988        sibling_index,
989        sibling_count,
990        &alloc::rc::Rc::new(BTreeMap::new()),
991        &alloc::rc::Rc::new(BTreeMap::new()),
992        &BTreeMap::new(),
993    )
994    .0
995}
996
997/// `counter-reset`/`counter-increment`(CSS カウンタ)は文書順(深さ優先)で全カウンタを
998/// フラット共有する簡略化実装のため、`counters_in`/戻り値で状態を兄弟間・親子間に
999/// 連鎖させる必要がある(`.map()` では各要素が独立してしまうため使えない)。
1000pub(crate) fn style_tree_with_ancestors_vars<'a>(
1001    root: &'a Node,
1002    stylesheet: &StyleSheet,
1003    rule_index: Option<&super::rule_index::RuleIndex>,
1004    ancestors_stack: &mut Vec<AncestorContext<'a>>,
1005    sibling_index: Option<usize>,
1006    sibling_count: Option<usize>,
1007    parent_vars: &alloc::rc::Rc<BTreeMap<String, String>>,
1008    parent_inherited: &alloc::rc::Rc<BTreeMap<String, String>>,
1009    counters_in: &BTreeMap<String, i64>,
1010) -> (StyledNode<'a>, BTreeMap<String, i64>, alloc::collections::BTreeSet<String>) {
1011    let stack_init = crate::os_lib::css::counter_scope::CounterScopeStack::from_map(counters_in);
1012    let (node, counters_out, resets, _final_stack) = style_tree_with_ancestors_vars_stack(
1013        root,
1014        stylesheet,
1015        rule_index,
1016        ancestors_stack,
1017        sibling_index,
1018        sibling_count,
1019        parent_vars,
1020        parent_inherited,
1021        counters_in,
1022        &stack_init,
1023    );
1024    (node, counters_out, resets)
1025}
1026
1027pub(crate) fn style_tree_with_ancestors_vars_stack<'a>(
1028    root: &'a Node,
1029    stylesheet: &StyleSheet,
1030    rule_index: Option<&super::rule_index::RuleIndex>,
1031    ancestors_stack: &mut Vec<AncestorContext<'a>>,
1032    sibling_index: Option<usize>,
1033    sibling_count: Option<usize>,
1034    parent_vars: &alloc::rc::Rc<BTreeMap<String, String>>,
1035    parent_inherited: &alloc::rc::Rc<BTreeMap<String, String>>,
1036    counters_in: &BTreeMap<String, i64>,
1037    counter_stack_in: &crate::os_lib::css::counter_scope::CounterScopeStack,
1038) -> (
1039    StyledNode<'a>,
1040    BTreeMap<String, i64>,
1041    alloc::collections::BTreeSet<String>,
1042    crate::os_lib::css::counter_scope::CounterScopeStack,
1043) {
1044    let (mut specified_values, css_vars, counters_after_self, counters_reset_self) = compute_styles_with_vars_stack(
1045        root,
1046        stylesheet,
1047        rule_index,
1048        ancestors_stack.as_slice(),
1049        sibling_index,
1050        sibling_count,
1051        &[],
1052        parent_vars,
1053        counters_in,
1054        Some(counter_stack_in),
1055    );
1056
1057    // `all: unset` 等の一括リセットは、個別プロパティの CSS-wide keyword 処理より先に
1058    // 行う(`all` 自身も含め、その後に個別の inherit/initial 等が残っていれば通常どおり処理する)。
1059    resolve_all_shorthand(&mut specified_values);
1060    // CSS-wide keywords(initial/inherit/unset/revert)を除去してから継承伝搬を行う。
1061    // 除去しておかないと、例えば `color:inherit` の文字列 "inherit" 自体が
1062    // specified_values に残ってしまい、後続の色パース等が本来の値ではなく
1063    // 壊れた文字列を見てしまう(かつ次の継承伝搬ステップが「既に指定済み」と
1064    // 誤認して親の値で埋めなくなる)。
1065    strip_css_wide_keywords(&mut specified_values);
1066
1067    // 継承プロパティの伝搬: 自身で指定されていないものは親の値を引き継ぐ。
1068    // 特にテキストノードはこれが無いと layout が常に既定 16px で計測してしまい、
1069    // flatten(描画)側の継承サイズと食い違って折り返し幅・行送りが崩れる。
1070    for (k, v) in parent_inherited.iter() {
1071        if !specified_values.contains_key(k) {
1072            specified_values.insert(k.clone(), v.clone());
1073        }
1074    }
1075    // `margin-block`/`margin-inline`/`padding-block`/`padding-inline`/`inset-block`/
1076    // `inset-inline`(-start/-end をまとめて指定するショートハンド)が丸ごと未対応で、
1077    // longhand の `-start`/`-end` 個別指定しかできなかった。`resolve_logical_properties`
1078    // が理解する longhand へ展開してから渡す(展開自体は方向非依存: `-start`/`-end` は
1079    // 1〜2値の shorthand が持つ「1番目=start(値省略時は end にも流用)/2番目=end」という
1080    // 構文規則のみに従うため、`direction` 解決より前に済ませてよい)。
1081    expand_logical_box_shorthands(&mut specified_values);
1082    // 論理プロパティ(margin-inline-start 等)の解決は、`direction` の継承伝搬が終わった
1083    // 直後(=物理プロパティへの変換時点で inherited な direction も見える状態)で行う。
1084    resolve_logical_properties(&mut specified_values);
1085    // currentColor キーワードの解決は、継承伝搬により `color` が確定した直後に行う必要がある
1086    // (継承された色を参照する `border-color:currentColor` 等が正しく解決されるようにするため)。
1087    resolve_current_color(&mut specified_values);
1088    // 【2026-07-31 性能修正】:hover / :focus / :active の事前計算を、
1089    // **そのノードの候補ルールが実際にその状態を参照している時だけ**走らせる。
1090    // 仕様は `spec/dynamic_pseudo_states.md`。
1091    //
1092    // 従来はノード 1 個につきカスケードを 4 回(通常+3 状態)走らせていた。
1093    // 実測で `style_tree` のノード走査は 358 ノードで約 600 tick かかっており、
1094    // セレクタ照合は索引で既に 2.0% まで落ちている(照合はボトルネックではない)。
1095    // 残っていたのがこの 4 倍の固定コスト。
1096    //
1097    // 判定は候補ルールの**セレクタ構文だけ**を見る(D-3)。
1098    // 候補が取れない場合は安全側に倒して従来どおり走らせる(D-4)。
1099    let (uses_hover, uses_focus, uses_active) = match &root.node_type {
1100        NodeType::Element {
1101            tag_name,
1102            classes,
1103            id,
1104            ..
1105        } => {
1106            if let Some(idx) = rule_index {
1107                match idx.candidates(tag_name.as_str(), id.as_deref(), classes.as_slice()) {
1108                    Ok(cands) => match idx.states_of(&cands) {
1109                        Some(m) => (
1110                            m & super::rule_index::STATE_HOVER != 0,
1111                            m & super::rule_index::STATE_FOCUS != 0,
1112                            m & super::rule_index::STATE_ACTIVE != 0,
1113                        ),
1114                        None => (true, true, true),
1115                    },
1116                    Err(_) => (true, true, true),
1117                }
1118            } else {
1119                (true, true, true)
1120            }
1121        }
1122        // テキストノードはどのセレクタにも一致しないため、
1123        // どの状態でも差分は生じ得ない(`selector.rs` の `NodeType::Text(_) => false`)。
1124        _ => (false, false, false),
1125    };
1126
1127    let hover_opt = if !uses_hover {
1128        None
1129    } else {
1130    let (hover_values_raw, _, _, _) = compute_styles_with_vars(
1131        root,
1132        stylesheet,
1133        rule_index,
1134        ancestors_stack.as_slice(),
1135        sibling_index,
1136        sibling_count,
1137        &["hover"],
1138        &css_vars,
1139        &counters_after_self,
1140    );
1141    let mut has_hover_diff = false;
1142    for (k, v) in &hover_values_raw {
1143        if specified_values.get(k) != Some(v) {
1144            has_hover_diff = true;
1145            break;
1146        }
1147    }
1148    if has_hover_diff {
1149        Some(hover_values_raw)
1150    } else {
1151        None
1152    }
1153    };
1154
1155    let focus_opt = if !uses_focus {
1156        None
1157    } else {
1158    let (focus_values_raw, _, _, _) = compute_styles_with_vars(
1159        root,
1160        stylesheet,
1161        rule_index,
1162        ancestors_stack.as_slice(),
1163        sibling_index,
1164        sibling_count,
1165        &["focus"],
1166        &css_vars,
1167        &counters_after_self,
1168    );
1169    let mut has_focus_diff = false;
1170    for (k, v) in &focus_values_raw {
1171        if specified_values.get(k) != Some(v) {
1172            has_focus_diff = true;
1173            break;
1174        }
1175    }
1176    if has_focus_diff {
1177        Some(focus_values_raw)
1178    } else {
1179        None
1180    }
1181    };
1182
1183    let active_opt = if !uses_active {
1184        None
1185    } else {
1186    let (active_values_raw, _, _, _) = compute_styles_with_vars(
1187        root,
1188        stylesheet,
1189        rule_index,
1190        ancestors_stack.as_slice(),
1191        sibling_index,
1192        sibling_count,
1193        &["active"],
1194        &css_vars,
1195        &counters_after_self,
1196    );
1197    let mut has_active_diff = false;
1198    for (k, v) in &active_values_raw {
1199        if specified_values.get(k) != Some(v) {
1200            has_active_diff = true;
1201            break;
1202        }
1203    }
1204    if has_active_diff {
1205        Some(active_values_raw)
1206    } else {
1207        None
1208    }
1209    };
1210
1211    // 子へ渡す継承プロパティ集合(自身の確定値から抽出)
1212    let mut has_any_new_inherited = false;
1213    for k in INHERITED_PROPS {
1214        if let Some(v) = specified_values.get(*k) {
1215            if parent_inherited.get(*k) != Some(v) {
1216                has_any_new_inherited = true;
1217                break;
1218            }
1219        }
1220    }
1221    let inherited_for_children = if !has_any_new_inherited {
1222        parent_inherited.clone()
1223    } else {
1224        let mut map = BTreeMap::new();
1225        for k in INHERITED_PROPS {
1226            if let Some(v) = specified_values.get(*k) {
1227                map.insert(String::from(*k), v.clone());
1228            }
1229        }
1230        alloc::rc::Rc::new(map)
1231    };
1232
1233    ancestors_stack.push(AncestorContext {
1234        node: root,
1235        sibling_index,
1236        sibling_count,
1237    });
1238    let child_count = root.children.len();
1239    let mut counters_running = counters_after_self;
1240    let mut stack_running = counter_stack_in.clone();
1241    let pushed_resets_self = crate::os_lib::css::counter_scope::apply_counter_decls_to_stack(&specified_values, &mut stack_running);
1242    let mut children = alloc::vec::Vec::with_capacity(child_count);
1243    for (idx, child) in root.children.iter().enumerate() {
1244        let counters_before_child = if counters_running.is_empty() {
1245            None
1246        } else {
1247            Some(counters_running.clone())
1248        };
1249        let (child_styled, counters_after_child, reset_names_in_child, stack_after_child) =
1250            style_tree_with_ancestors_vars_stack(
1251                child,
1252                stylesheet,
1253                rule_index,
1254                ancestors_stack,
1255                Some(idx),
1256                Some(child_count),
1257                &css_vars,
1258                &inherited_for_children,
1259                &counters_running,
1260                &stack_running,
1261            );
1262        counters_running = counters_after_child;
1263        stack_running = stack_after_child;
1264        if !reset_names_in_child.is_empty() {
1265            if let Some(before) = &counters_before_child {
1266                for name in &reset_names_in_child {
1267                    match before.get(name) {
1268                        Some(v) => {
1269                            counters_running.insert(name.clone(), *v);
1270                        }
1271                        None => {
1272                            counters_running.remove(name);
1273                        }
1274                    }
1275                }
1276            } else {
1277                for name in &reset_names_in_child {
1278                    counters_running.remove(name);
1279                }
1280            }
1281        }
1282        children.push(child_styled);
1283    }
1284    ancestors_stack.pop();
1285
1286    for name in &pushed_resets_self {
1287        stack_running.pop_reset(name);
1288    }
1289
1290    (
1291        StyledNode {
1292            node: root,
1293            specified_values,
1294            hover_values: hover_opt,
1295            focus_values: focus_opt,
1296            active_values: active_opt,
1297            css_vars: css_vars.clone(),
1298            children,
1299            edges_cache: Default::default(),
1300        },
1301        counters_running,
1302        counters_reset_self,
1303        stack_running,
1304    )
1305}
1306