atmos/os_lib/css/types.rs
1// 分割: css.rs より機械的に移動(2026-07-16 リファクタ フェーズ3)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5#[derive(Debug, Clone)]
6pub struct StyleSheet {
7 pub rules: Vec<Rule>,
8 /// @keyframes 定義(name → キーフレーム列)。
9 pub keyframes: Vec<Keyframes>,
10 /// @counter-style 定義(name → カスタムカウンタ書式)。
11 pub counter_styles: Vec<CounterStyleDef>,
12 /// @font-face 定義(font-family名 → フォントファイルURL)。
13 pub font_faces: Vec<FontFaceDef>,
14}
15
16/// `@font-face { font-family: 'X'; src: url(...) format('truetype'), ...; }`。
17/// `src`が複数候補(`local()`/複数`url()`)を持つ場合、TTF/OTF形式を明示する
18/// `format()`ヒントを優先し、無ければ最初に見つかった`url()`を採用する簡略実装
19/// (WOFF2形式のURLしか無い場合はこの処理系のフォントローダが読めないため
20/// `src_url`はSome(...)のまま残るが、実際の読込・パース側でTTF/OTFシグネチャ
21/// チェックにより安全に拒否される)。
22#[derive(Debug, Clone)]
23pub struct FontFaceDef {
24 pub family: String,
25 pub src_url: Option<String>,
26 /// 【2026-08-05】`@font-face` の `font-weight`(未指定は 400)。
27 ///
28 /// 同じ family 名でウェイト違いを複数定義するのは CSS では普通。
29 /// Font Awesome も `.fas`(solid)は 900、`.far`(regular)は 400 で、
30 /// **同じ `"Font Awesome 7 Free"` を使う**。
31 /// ウェイトを持たないと後勝ちで上書きされ、solid の字形が失われる
32 /// (実測: `Font Awesome 7 Free` に regular が入り、アイコンが出なかった)。
33 pub weight: u16,
34}
35
36/// `@counter-style` の簡略実装。`system`/`symbols`/`suffix` のみ対応
37/// (`negative`/`range`/`pad`/`fallback`/`prefix`等は非対応で無視する)。
38/// `system` は `cyclic`(既定。`(value-1) % symbols.len()` で循環)と
39/// `numeric`(`symbols` を桁の数字として使う位取り記数法。0除く正の整数のみ)に対応、
40/// それ以外(`alphabetic`/`additive`等)は `cyclic` と同一視する。
41#[derive(Debug, Clone)]
42pub struct CounterStyleDef {
43 pub name: String,
44 pub system: String,
45 pub symbols: Vec<String>,
46 pub suffix: String,
47}
48
49/// カスタム `@counter-style` を使って値を書式化する。`system: numeric` は
50/// `symbols` を桁の数字として使う位取り記数法(0 は先頭symbolの1文字、
51/// 負値・0以下は素通しの十進数表記にフォールバック)。それ以外(既定の
52/// `cyclic` 扱い含む)は `(value-1).rem_euclid(len)` で symbols を循環させる。
53pub fn format_with_counter_style(def: &CounterStyleDef, value: i64) -> String {
54 if def.symbols.is_empty() {
55 return alloc::format!("{}", value);
56 }
57 if def.system.trim() == "numeric" {
58 if value <= 0 {
59 return alloc::format!("{}", value);
60 }
61 let base = def.symbols.len() as i64;
62 let mut n = value;
63 let mut digits: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
64 while n > 0 {
65 let d = (n % base) as usize;
66 digits.push(def.symbols[d].as_str());
67 n /= base;
68 }
69 digits.iter().rev().copied().collect::<Vec<&str>>().concat()
70 } else {
71 let len = def.symbols.len() as i64;
72 let idx = (value - 1).rem_euclid(len) as usize;
73 def.symbols[idx].clone()
74 }
75}
76
77/// @keyframes アニメーション定義。
78#[derive(Debug, Clone)]
79pub struct Keyframes {
80 pub name: String,
81 /// (offset 0.0..=1.0, そのオフセットでの宣言群)。offset 昇順。
82 pub frames: Vec<(f32, Vec<Declaration>)>,
83}
84
85#[derive(Debug, Clone)]
86pub struct Rule {
87 pub selectors: Vec<Selector>,
88 pub declarations: Vec<Declaration>,
89 /// `@layer` カスケードレイヤーの優先順位(`None` = レイヤー無し = 最優先)。
90 /// 数値が大きいほど後から宣言されたレイヤーで、通常宣言(非 `!important`)では
91 /// 後のレイヤーほど勝つ。`!important` との組み合わせ時の優先順位反転(仕様上の
92 /// 細かい挙動)は非対応の簡略実装(`!important` は従来通りレイヤーを見ずに勝つ)。
93 pub layer: Option<usize>,
94 /// 【2026-07-24追加】このルールを包む`@media`条件文字列(`None` = 無条件)。
95 /// 以前は`@media`をCSS解析時(`parse_css`呼び出し時点)に一度だけ静的評価し、
96 /// マッチしなかったブロックを丸ごと除外していたため、その一度きりの解析時点の
97 /// ウィンドウ幅と実際の表示時のウィンドウ幅がズレていると(起動直後のプレース
98 /// ホルダーページ表示中など)、モバイルファーストCSS(既定非表示→`min-width`
99 /// メディアクエリで表示)のナビゲーション等が永久に除外されたままになり、
100 /// ページが白紙同然になるバグの一因だった。ルール自体は常に保持し、カスケード
101 /// (要素へのマッチング)時に毎回`media_query_matches`で動的に判定することで、
102 /// ウィンドウ幅が変わってもレイアウトのたびに正しく再評価されるようにする。
103 pub media_query: Option<String>,
104 /// `@container` の条件式(`media_query` と同じく**動的評価**するため保持する)。
105 ///
106 /// `@media` との違いは基準がビューポートではなく「問い合わせコンテナ」で
107 /// あること。コンテナ寸法はレイアウトが決まるまで分からないので、
108 /// 解析時に評価して落とすことはできない(`@media` を静的評価して
109 /// ページが白紙化した過去の失敗と同じ轍を踏まないこと)。
110 ///
111 /// 形式は `名前|条件` または `|条件`(名前省略時)。
112 pub container_query: Option<String>,
113}
114
115#[derive(Debug, Clone, PartialEq)]
116pub enum Selector {
117 Simple(SimpleSelector),
118 Chain(Vec<SelectorStep>),
119}
120
121/// 属性セレクタの照合演算子。
122#[derive(Debug, Clone, PartialEq)]
123pub enum AttrOp {
124 Exists, // [attr]
125 Eq, // [attr=val]
126 Prefix, // [attr^=val]
127 Suffix, // [attr$=val]
128 Contains, // [attr*=val]
129 Word, // [attr~=val](空白区切りの一語)
130 Dash, // [attr|=val](val または val-...)
131}
132
133#[derive(Debug, Clone, PartialEq)]
134pub struct AttrSelector {
135 pub name: String,
136 pub op: AttrOp,
137 pub value: String,
138 /// `[attr=val i]`(Selectors Level 4 §6.4.1。大文字小文字を無視して比較)。
139 pub case_insensitive: bool,
140}
141
142#[derive(Debug, Clone, PartialEq)]
143pub enum PseudoClass {
144 FirstChild,
145 LastChild,
146 /// :nth-child(an+b) — (a, b)、1-based。第3要素は Selectors Level 4 の
147 /// `:nth-child(An+B of S)` 構文の `S`(省略時 `None`)。指定時は親の子要素のうち
148 /// `S` に一致するものだけを対象に 1-based 位置を数える。
149 NthChild(i64, i64, Option<alloc::boxed::Box<SimpleSelector>>),
150 /// :nth-last-child(an+b [of S])。
151 NthLastChild(i64, i64, Option<alloc::boxed::Box<SimpleSelector>>),
152 /// :not(simple)。
153 Not(alloc::boxed::Box<SimpleSelector>),
154 /// checked 属性の有無で判定(動的な操作状態ではなく DOM 属性を直接見る簡易実装)。
155 Checked,
156 /// disabled 属性の有無で判定。
157 Disabled,
158 /// 同じタグ名の兄弟の中で最初の要素。サブジェクト(末尾コンパウンド)でのみ判定可能
159 /// (祖先位置での判定は非対応。実用上ほとんどがサブジェクト用途のため許容する)。
160 FirstOfType,
161 LastOfType,
162 /// :nth-of-type(an+b) — (a, b)、同タグ名の兄弟内で1-based。
163 NthOfType(i64, i64),
164 /// :nth-last-of-type(an+b) — 同タグ名の兄弟内で末尾から1-based。
165 NthLastOfType(i64, i64),
166 /// 同じタグ名の兄弟が自分しかいない。
167 OnlyOfType,
168 /// 子要素・テキストを一切持たない(空白のみのテキストノードも「空」とみなす)。
169 Empty,
170 /// 兄弟(タグ名問わず)が自分しかいない。
171 OnlyChild,
172 /// 現在の URL フラグメント(`location.hash`)が自分自身の id と一致する。
173 Target,
174 /// `:is(sel1, sel2, ...)`(`:matches()`/`:any()` も同義)— リスト中のいずれかの
175 /// 単純セレクタにマッチすれば真。特異度はリスト中の最大値を採用する。
176 Is(Vec<SimpleSelector>),
177 /// `:where(sel1, sel2, ...)` — マッチ判定は `:is()` と全く同じだが、特異度は常に0。
178 Where(Vec<SimpleSelector>),
179 /// `:lang(code)` — 要素自身の `lang` 属性(無ければ文書既定言語 `DOCUMENT_LANG`、
180 /// 本来は祖先から継承されるが `<html lang>` のみを追跡する簡略実装)が `code` と
181 /// 完全一致、または `code-` で始まるサブタグ一致(`lang="en-US"` は `:lang(en)` にも一致)。
182 Lang(String),
183 /// `:dir(ltr|rtl)` — 要素自身の `dir` 属性の値が一致すれば真。`:lang()` と同じ
184 /// 簡略方針で、祖先からの継承(`dir` 未指定時に親の値を辿る)は非対応。`dir`
185 /// 属性が無い/`auto`/不正値の要素は既定で `ltr` 扱いにする(この処理系のデフォルト
186 /// 文書方向は常に LTR のため)。
187 Dir(String),
188 /// `:has(sel1, sel2, ...)` — 各枝が子孫(任意の深さ)/直接子(`> sel`)/直後兄弟
189 /// (`+ sel`)/以降兄弟いずれか(`~ sel`)のいずれかに一致すれば真。
190 /// `+`/`~` は `:has()` がサブジェクト位置(コンパウンドセレクタの末尾)にある場合のみ
191 /// 正しく判定できる(祖先位置での of-type 判定と同じ既知の制約。`type_sibling_position`
192 /// 参照)。複合セレクタチェーン(`:has(a b)` 等)は非対応で、単純セレクタ
193 /// (タグ/クラス/id/属性、および sibling_index 等の文脈を必要としない疑似クラス)
194 /// のみを対象にする。
195 Has(Vec<(HasCombinator, SimpleSelector)>),
196 Other(String),
197}
198
199/// 疑似要素(`::before` / `::after`、レガシー単コロンも含む)。
200/// 動的な状態を持つ疑似クラスとは別枠で扱う(要素自体のマッチ判定には影響しない)。
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum PseudoElement {
203 Before,
204 After,
205 Marker,
206 FirstLine,
207 FirstLetter,
208 Placeholder,
209}
210
211#[derive(Debug, Clone, PartialEq)]
212pub struct SelectorStep {
213 pub simple: SimpleSelector,
214 pub combinator: Option<Combinator>,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum Combinator {
219 Descendant,
220 Child,
221 /// `a + b`(直後の兄弟)。
222 NextSibling,
223 /// `a ~ b`(後続の兄弟いずれか)。
224 SubsequentSibling,
225}
226
227/// `:has(sel)` 引数中の各枝の結合子(`Combinator` のサブセット。`:has()` は複合セレクタ
228/// チェーンを対象にしないため独立した enum にしている)。
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum HasCombinator {
231 /// `:has(sel)` — 任意の深さの子孫。
232 Descendant,
233 /// `:has(> sel)` — 直接子のみ。
234 Child,
235 /// `:has(+ sel)` — 直後の兄弟のみ。
236 NextSibling,
237 /// `:has(~ sel)` — 以降の兄弟いずれか。
238 SubsequentSibling,
239}
240
241#[derive(Debug, Clone, PartialEq, Default)]
242pub struct SimpleSelector {
243 pub tag_name: Option<String>,
244 pub id: Option<String>,
245 pub class: Vec<String>,
246 pub pseudo_classes: Vec<PseudoClass>,
247 pub attrs: Vec<AttrSelector>,
248 /// `*`(ユニバーサルセレクタ)。
249 pub universal: bool,
250 /// `::before` / `::after`(末尾のコンパウンドセレクタにのみ有効)。
251 pub pseudo_element: Option<PseudoElement>,
252}
253
254#[derive(Debug, Clone)]
255pub struct Declaration {
256 pub name: String,
257 pub value: String,
258 /// `!important` 指定。カスケードで特異度に関わらず優先する。
259 pub important: bool,
260}
261
262// 算出されたスタイル(DOMノードに紐づく)
263#[derive(Debug, Clone)]
264pub struct StyledNode<'a> {
265 pub node: &'a Node,
266 pub specified_values: BTreeMap<String, String>,
267 pub hover_values: Option<BTreeMap<String, String>>,
268 /// :focus 適用時に差分が生じる算出値(hover_values と同じ方式)。
269 /// 事前に通常値と focus 時の値を両方計算しておき、実際にどちらを使うかは
270 /// 描画側が focused_id と element_id の一致を見て選ぶ。フォーカス変更自体は
271 /// DOM を変更しないため dom_needs_rebuild では再レイアウトが保証されず、
272 /// この事前計算方式(hover と同様)が必要。
273 pub focus_values: Option<BTreeMap<String, String>>,
274 /// :active 適用時に差分が生じる算出値(hover_values/focus_values と同じ事前計算方式)。
275 /// 実際の適用可否は描画側が「要素がホバー中 かつ マウス左ボタン押下中」を見て選ぶ。
276 pub active_values: Option<BTreeMap<String, String>>,
277 /// この要素のスコープで有効な CSS 変数(`--foo`)。
278 ///
279 /// 親から受け継いだ内容をそのまま持つノードが大半なので `Rc` で共有し、
280 /// 変数を定義するノードだけが実体を複製する(`cascade.rs` 参照)。
281 pub css_vars: alloc::rc::Rc<BTreeMap<String, String>>,
282 pub children: Vec<StyledNode<'a>>,
283}
284
285impl<'a> StyledNode<'a> {
286 pub fn value(&self, name: &str) -> Option<String> {
287 self.specified_values.get(name).cloned()
288 }
289
290 pub fn value_ref(&self, name: &str) -> Option<&str> {
291 self.specified_values.get(name).map(|s| s.as_str())
292 }
293}
294