Skip to main content

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