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