Skip to main content

atmos/os_lib/css/
rule_index.rs

1//! セレクタ右端キーによるルールインデックス(カスケード高速化)。
2//!
3//! ## 背景
4//! カスケードは「全ルール × 全ノード」を総当たりで `matches_selector` に掛けていた。
5//! 実サイト(www.sugi-lab.net)では Font Awesome を含めて **2403 ルール**あり、
6//! ノード数 365 との組み合わせで 1 回のレイアウトに約 50 秒かかっていた。
7//! JS タイマーが毎フレーム DOM を dirty にするため、この 50 秒が延々と繰り返され、
8//! ページが一度も描画完了しない状態になっていた。
9//!
10//! ## 手法
11//! ブラウザ実装と同じく、セレクタの**右端コンパウンド**から絞り込みキーを 1 つ取り、
12//! `#id` / `.class` / `tag` のバケットへ振り分けておく。ある要素に対しては
13//! 「その要素の id / class / タグ名に対応するバケット」+「ユニバーサルバケット」
14//! だけを照合すればよく、`.fa-xxx` のような数千件のアイコンルールは
15//! 対象要素がそのクラスを持たない限り一切触らない。
16//!
17//! キーの選択は右端コンパウンド内で **id > class > tag > universal** の順。
18//! 右端が `:hover` や `[attr]` だけで id/class/tag を持たない場合は
19//! **必ず universal 扱い**にする(絞り込みで取りこぼすと描画が壊れるため、
20//! 判断がつかないものは常に照合する側へ倒す)。
21//!
22//! 本モジュールはグローバル状態にもハードウェアにも依存しない純粋ロジックのみで構成し、
23//! ホスト側の単体試験で契約を固定する。
24
25extern crate alloc;
26
27use super::types::{Rule, Selector, SimpleSelector};
28use alloc::collections::BTreeMap;
29use alloc::string::{String, ToString};
30use alloc::vec::Vec;
31
32/// ルールを振り分けるバケットの種類。
33#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
34pub enum RuleKey {
35    /// 右端が `#id` を含む。
36    Id(String),
37    /// 右端が `.class` を含む(id は含まない)。最初のクラスを代表キーにする。
38    Class(String),
39    /// 右端がタグ名のみ(id/class 無し)。
40    Tag(String),
41    /// 右端から絞り込みキーを決められない(`*`、擬似クラスのみ、属性のみ 等)。
42    /// このバケットのルールは全要素に対して照合する。
43    Universal,
44}
45
46/// インデックス操作の引数が不正だった場合のエラー。
47#[derive(Debug, PartialEq, Eq, Clone)]
48pub enum IndexError {
49    /// 要素のタグ名が空。要素ノードには必ずタグ名がある前提が崩れている。
50    EmptyTagName,
51}
52
53/// 右端コンパウンドから絞り込みキーを決める。
54fn key_from_simple(s: &SimpleSelector) -> RuleKey {
55    if let Some(id) = &s.id {
56        if !id.is_empty() {
57            return RuleKey::Id(id.clone());
58        }
59    }
60    if let Some(c) = s.class.iter().find(|c| !c.is_empty()) {
61        return RuleKey::Class(c.clone());
62    }
63    if let Some(t) = &s.tag_name {
64        if !t.is_empty() {
65            // HTML のタグ名は大文字小文字を区別しないので正規化する。
66            return RuleKey::Tag(t.to_lowercase());
67        }
68    }
69    // `*` / 擬似クラスのみ / 属性のみ → 絞り込めないので全件照合側へ。
70    RuleKey::Universal
71}
72
73/// セレクタ 1 本の絞り込みキーを返す。チェーンなら**右端**のステップを見る。
74pub fn rightmost_key(sel: &Selector) -> RuleKey {
75    match sel {
76        Selector::Simple(s) => key_from_simple(s),
77        Selector::Chain(steps) => match steps.last() {
78            Some(step) => key_from_simple(&step.simple),
79            // 空チェーンは本来あり得ないが、取りこぼすより全件照合が安全。
80            None => RuleKey::Universal,
81        },
82    }
83}
84
85/// スタイルシート内のルールを右端キーで振り分けた索引。
86/// 保持するのは元の `rules` スライスに対する**添字**のみで、ルール本体は複製しない。
87#[derive(Debug, Default)]
88pub struct RuleIndex {
89    by_id: BTreeMap<String, Vec<usize>>,
90    by_class: BTreeMap<String, Vec<usize>>,
91    by_tag: BTreeMap<String, Vec<usize>>,
92    universal: Vec<usize>,
93    rule_count: usize,
94    /// ルールごとの動的疑似クラス使用ビット(bit0=hover, bit1=focus, bit2=active)。
95    ///
96    /// 「そのルールが `:hover` を使うか」は**ノードに依存しない**。
97    /// 以前はカスケードがノードごとに候補ルール全部へ `rule_uses_state` を
98    /// 呼んでおり、同じ答えを N×C 回計算し直していた。
99    /// 索引を作るのと同じ 1 回の走査で求めておく。
100    state_masks: Vec<u8>,
101}
102
103/// `state_masks` のビット位置。
104pub const STATE_HOVER: u8 = 1 << 0;
105pub const STATE_FOCUS: u8 = 1 << 1;
106pub const STATE_ACTIVE: u8 = 1 << 2;
107
108impl RuleIndex {
109    /// 与えられた候補ルール群が使う動的疑似クラスのビットを OR して返す。
110    ///
111    /// 範囲外の添字が混ざっていたら `None`。呼び出し側は
112    /// 「全状態を計算する」へフォールバックする(取りこぼしより遅い方を選ぶ)。
113    ///
114    /// 計算量: **O(C)**。
115    ///
116    /// 【2026-08-05】グローバルの `ACTIVE` を介さずに問い合わせられるよう、
117    /// `active_states_of` から本体をこちらへ移した。
118    /// 試験がグローバル状態に依存していたため、**並列実行される他の試験が
119    /// `clear_active()` した瞬間に `None` が返って落ちていた**
120    /// (実測: 8 回に 1 回程度。`spec/logging_policy.md` T-1 の再発)。
121    /// 索引そのものの性質は索引だけで確かめられる。
122    pub fn states_of(&self, cands: &[usize]) -> Option<u8> {
123        let mut m = 0u8;
124        for &i in cands {
125            match self.state_masks.get(i) {
126                Some(bits) => m |= bits,
127                None => return None,
128            }
129            if m == STATE_HOVER | STATE_FOCUS | STATE_ACTIVE {
130                break;
131            }
132        }
133        Some(m)
134    }
135
136    /// ルール列から索引を構築する。
137    ///
138    /// 1 ルールが複数セレクタ(`a, b, c`)を持つ場合、**いずれか**のセレクタが
139    /// マッチすればそのルールは適用対象なので、全セレクタのキーへ登録する。
140    pub fn build(rules: &[Rule]) -> Self {
141        let mut idx = RuleIndex {
142            rule_count: rules.len(),
143            ..Default::default()
144        };
145        idx.state_masks = rules
146            .iter()
147            .map(|r| {
148                let mut m = 0u8;
149                if super::pseudo_state::rule_uses_state(r, "hover") {
150                    m |= STATE_HOVER;
151                }
152                if super::pseudo_state::rule_uses_state(r, "focus") {
153                    m |= STATE_FOCUS;
154                }
155                if super::pseudo_state::rule_uses_state(r, "active") {
156                    m |= STATE_ACTIVE;
157                }
158                m
159            })
160            .collect();
161        for (i, rule) in rules.iter().enumerate() {
162            if rule.selectors.is_empty() {
163                // セレクタが無いルールは判定不能。取りこぼさないよう全件照合側へ。
164                idx.universal.push(i);
165                continue;
166            }
167            for sel in &rule.selectors {
168                match rightmost_key(sel) {
169                    RuleKey::Id(v) => push_unique(idx.by_id.entry(v).or_default(), i),
170                    RuleKey::Class(v) => push_unique(idx.by_class.entry(v).or_default(), i),
171                    RuleKey::Tag(v) => push_unique(idx.by_tag.entry(v).or_default(), i),
172                    RuleKey::Universal => push_unique(&mut idx.universal, i),
173                }
174            }
175        }
176        idx
177    }
178
179    /// 索引が対象としているルール総数。
180    pub fn rule_count(&self) -> usize {
181        self.rule_count
182    }
183
184    /// 常時照合が必要な(絞り込めなかった)ルール数。
185    /// これが総数に近いほど索引の効果は薄い。
186    pub fn universal_count(&self) -> usize {
187        self.universal.len()
188    }
189
190    /// ある要素について照合すべきルールの添字を**昇順・重複なし**で返す。
191    /// 昇順なのはカスケードの後勝ち規則(同特異度なら後のルールが勝つ)を
192    /// 元の宣言順のまま保つため。
193    ///
194    /// 引数が不正(タグ名が空)なときは黙って戻らず、エラーログを出して `Err` を返す。
195    /// 呼び出し側は「全ルールを照合する」従来動作へフォールバックすること
196    /// (絞り込みを誤って描画が欠けるより、遅くても正しい方を選ぶ)。
197    pub fn candidates(
198        &self,
199        tag: &str,
200        id: Option<&str>,
201        classes: &[String],
202    ) -> Result<Vec<usize>, IndexError> {
203        if tag.trim().is_empty() {
204            crate::error!(
205                "[CSS][RULEIDX] 要素のタグ名が空です (id={:?} classes={})。全ルール照合へフォールバックします",
206                id,
207                classes.len()
208            );
209            return Err(IndexError::EmptyTagName);
210        }
211
212        let mut list: Vec<usize> = Vec::with_capacity(self.universal.len() + 16);
213        list.extend_from_slice(&self.universal);
214
215        if let Some(id) = id {
216            if !id.is_empty() {
217                if let Some(v) = self.by_id.get(id) {
218                    list.extend_from_slice(v);
219                }
220            }
221        }
222        for c in classes {
223            if !c.is_empty() {
224                if let Some(v) = self.by_class.get(c) {
225                    list.extend_from_slice(v);
226                }
227            }
228        }
229        if let Some(v) = self.by_tag.get(&tag.to_lowercase()) {
230            list.extend_from_slice(v);
231        }
232
233        list.sort_unstable();
234        list.dedup();
235
236        Ok(list)
237    }
238}
239
240/// 既に末尾にある添字は積まない(同一ルールが複数セレクタで同じバケットへ来る場合)。
241fn push_unique(v: &mut Vec<usize>, i: usize) {
242    if v.last() != Some(&i) {
243        v.push(i);
244    }
245}
246
247/// `RuleKey` を人が読める文字列にする(診断ログ用)。
248impl RuleKey {
249    pub fn describe(&self) -> String {
250        match self {
251            RuleKey::Id(v) => {
252                let mut s = String::from("#");
253                s.push_str(v);
254                s
255            }
256            RuleKey::Class(v) => {
257                let mut s = String::from(".");
258                s.push_str(v);
259                s
260            }
261            RuleKey::Tag(v) => v.clone(),
262            RuleKey::Universal => "*".to_string(),
263        }
264    }
265}