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