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