Skip to main content

atmos/os_lib/css/
counter_scope.rs

1//! CSS Lists and Counters Module Level 3 準拠のカウンタスコープ管理モジュール
2//!
3//! `counter-reset` による新しいネストレベルの生成、`counter-increment` による
4//! 最深スコープの更新、および `counters(name, sep, style)` の複数深度連結を純粋ロジックとして提供する。
5
6extern crate alloc;
7use alloc::collections::BTreeMap;
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use crate::os_lib::css::types::CounterStyleDef;
12
13/// カウンタのスコープスタック管理
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
15pub struct CounterScopeStack {
16    /// カウンタ名 -> スコープスタック(外側から内側への値のリスト)
17    counters: BTreeMap<String, Vec<i64>>,
18}
19
20impl CounterScopeStack {
21    pub fn new() -> Self {
22        Self {
23            counters: BTreeMap::new(),
24        }
25    }
26
27    /// 既存のフラットマップからスタックを初期化する(後方互換用)。
28    pub fn from_map(map: &BTreeMap<String, i64>) -> Self {
29        let mut counters = BTreeMap::new();
30        for (k, &v) in map {
31            counters.insert(k.clone(), alloc::vec![v]);
32        }
33        Self { counters }
34    }
35
36    /// 現在の最深スコープの値だけをフラットマップとして取り出す。
37    pub fn to_flat_map(&self) -> BTreeMap<String, i64> {
38        let mut map = BTreeMap::new();
39        for (k, v) in &self.counters {
40            if let Some(&top) = v.last() {
41                map.insert(k.clone(), top);
42            }
43        }
44        map
45    }
46
47    /// `counter-reset: name value` を適用する。
48    /// 仕様(CSS Lists and Counters §3):
49    /// 要素に `counter-reset` がある場合、その名前の新しいカウンタスコープをスタックにプッシュする。
50    pub fn push_reset(&mut self, name: &str, value: i64) {
51        self.counters
52            .entry(name.to_string())
53            .or_insert_with(Vec::new)
54            .push(value);
55    }
56
57    /// 要素のスコープを抜ける際に、その要素で `push_reset` されたレベルをポップする。
58    pub fn pop_reset(&mut self, name: &str) {
59        if let Some(stack) = self.counters.get_mut(name) {
60            stack.pop();
61            if stack.is_empty() {
62                self.counters.remove(name);
63            }
64        }
65    }
66
67    /// `counter-increment: name value` を適用する。
68    /// 仕様: スタックの最深(最も内側)のカウンタに value を加算する。
69    /// もしまだ一度も reset されていない場合は、暗黙のルートスコープ(初期値 0)を生成して加算する。
70    pub fn increment(&mut self, name: &str, value: i64) {
71        let stack = self.counters.entry(name.to_string()).or_insert_with(Vec::new);
72        if let Some(top) = stack.last_mut() {
73            *top += value;
74        } else {
75            stack.push(value);
76        }
77    }
78
79    /// `counter-set: name value` を適用する。
80    /// 仕様: 最深スコープの値を上書きする。スタックが空なら暗黙のルートスコープを作成。
81    pub fn set(&mut self, name: &str, value: i64) {
82        let stack = self.counters.entry(name.to_string()).or_insert_with(Vec::new);
83        if let Some(top) = stack.last_mut() {
84            *top = value;
85        } else {
86            stack.push(value);
87        }
88    }
89
90    /// 現在(最深スコープ)のカウンタ値を取得する。
91    /// スコープが存在しない場合は 0 を返す。
92    pub fn get_current(&self, name: &str) -> i64 {
93        self.counters
94            .get(name)
95            .and_then(|stack| stack.last().copied())
96            .unwrap_or(0)
97    }
98
99    /// カウンタのすべてのスコープ(外側から内側)の値を取得する。
100    pub fn get_all(&self, name: &str) -> &[i64] {
101        self.counters
102            .get(name)
103            .map(|s| s.as_slice())
104            .unwrap_or(&[])
105    }
106
107    /// `counter(name, style)` の書式化文字列を生成する。
108    pub fn format_counter(
109        &self,
110        name: &str,
111        style: Option<&str>,
112        counter_styles: &[CounterStyleDef],
113    ) -> String {
114        let val = self.get_current(name);
115        format_single_counter_value(val, style, counter_styles)
116    }
117
118    /// `counters(name, sep, style)` の書式化文字列を生成する。
119    /// すべてのネストレベルの値を `sep` で連結する。
120    pub fn format_counters(
121        &self,
122        name: &str,
123        sep: &str,
124        style: Option<&str>,
125        counter_styles: &[CounterStyleDef],
126    ) -> String {
127        let all = self.get_all(name);
128        if all.is_empty() {
129            // スコープが空の場合は現在値(0)を1つ出力
130            return format_single_counter_value(0, style, counter_styles);
131        }
132        let formatted_pieces: Vec<String> = all
133            .iter()
134            .map(|&val| format_single_counter_value(val, style, counter_styles))
135            .collect();
136        formatted_pieces.join(sep)
137    }
138}
139
140/// 単一のカウンタ数値を指定されたスタイルまたはカスタムスタイルでフォーマットする。
141pub fn format_single_counter_value(
142    val: i64,
143    style: Option<&str>,
144    counter_styles: &[CounterStyleDef],
145) -> String {
146    let val_u32 = val.max(0) as u32;
147    match style.map(|s| s.to_ascii_lowercase()).as_deref() {
148        Some("upper-roman") => crate::os_lib::web_engine::number_to_roman(val_u32, true),
149        Some("lower-roman") => crate::os_lib::web_engine::number_to_roman(val_u32, false),
150        Some("upper-alpha") | Some("upper-latin") => {
151            crate::os_lib::web_engine::number_to_alpha(val_u32, true)
152        }
153        Some("lower-alpha") | Some("lower-latin") => {
154            crate::os_lib::web_engine::number_to_alpha(val_u32, false)
155        }
156        Some("decimal") | None => format!("{}", val),
157        Some(custom) => {
158            if let Some(def) = counter_styles.iter().find(|d| d.name == custom) {
159                crate::os_lib::css::types::format_with_counter_style(def, val)
160            } else {
161                format!("{}", val)
162            }
163        }
164    }
165}
166
167/// `counter-reset` / `counter-increment` / `counter-set` を `CounterScopeStack` に適用する。
168/// 戻り値は、この要素自身が `counter-reset` した(=スコープをプッシュした)カウンタ名のリスト(順序保持)。
169pub fn apply_counter_decls_to_stack(
170    specified_values: &BTreeMap<String, String>,
171    stack: &mut CounterScopeStack,
172) -> Vec<String> {
173    let mut pushed_resets = Vec::new();
174    if let Some(reset) = specified_values.get("counter-reset") {
175        for (name, value) in crate::os_lib::css::selector::parse_counter_decl(reset, 0) {
176            stack.push_reset(&name, value);
177            pushed_resets.push(name);
178        }
179    }
180    if let Some(incr) = specified_values.get("counter-increment") {
181        for (name, value) in crate::os_lib::css::selector::parse_counter_decl(incr, 1) {
182            stack.increment(&name, value);
183        }
184    }
185    if let Some(set) = specified_values.get("counter-set") {
186        for (name, value) in crate::os_lib::css::selector::parse_counter_decl(set, 0) {
187            stack.set(&name, value);
188        }
189    }
190    pushed_resets
191}