Skip to main content

atmos/os_lib/css/
values.rs

1// 分割: css.rs より機械的に移動(2026-07-16 リファクタ フェーズ3)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// Replace all `var(--name)` and `var(--name, fallback)` occurrences in a value string.
6/// カスタムプロパティが別のカスタムプロパティを参照する間接参照
7/// (例: `--a: var(--b); --b: red;` のとき `var(--a)` は `red` に解決される)に対応するため、
8/// 1パスの置換結果にまだ `var(` が残っていれば再帰的に解決する。循環参照時に無限再帰へ
9/// 陥らないよう、最大再帰深度(8)を設けて打ち切る。
10pub fn resolve_css_var(value: &str, vars: &BTreeMap<String, String>) -> String {
11    resolve_css_var_depth(value, vars, 0)
12}
13
14pub(crate) fn resolve_css_var_depth(value: &str, vars: &BTreeMap<String, String>, depth: u32) -> String {
15    if depth >= 8 {
16        return String::from(value);
17    }
18    let mut result = String::new();
19    let mut rest = value;
20    while let Some(start) = rest.find("var(") {
21        result.push_str(rest.get(..start).unwrap_or(""));
22        let after = rest.get(start + 4..).unwrap_or("");
23        // find matching closing paren
24        let mut depth_paren = 1usize;
25        let mut end = 0;
26        for (i, c) in after.char_indices() {
27            match c {
28                '(' => depth_paren += 1,
29                ')' => {
30                    depth_paren -= 1;
31                    if depth_paren == 0 {
32                        end = i;
33                        break;
34                    }
35                }
36                _ => {}
37            }
38        }
39        let inner = after.get(..end).unwrap_or("");
40        // split on first comma for fallback
41        let (var_name, fallback) = if let Some(comma_pos) = inner.find(',') {
42            (
43                inner.get(..comma_pos).unwrap_or("").trim(),
44                Some(inner.get(comma_pos + 1..).unwrap_or("").trim()),
45            )
46        } else {
47            (inner.trim(), None)
48        };
49        let resolved = vars
50            .get(var_name)
51            .cloned()
52            .or_else(|| fallback.map(|s| s.to_string()))
53            .unwrap_or_default();
54        // 参照先の値自体が別の var(...) を含む場合(間接参照)は再帰的に解決する。
55        let resolved = if resolved.contains("var(") {
56            resolve_css_var_depth(&resolved, vars, depth + 1)
57        } else {
58            resolved
59        };
60        result.push_str(&resolved);
61        rest = after.get(end + 1..).unwrap_or("");
62    }
63    result.push_str(rest);
64    result
65}
66
67/// `env(safe-area-inset-*)`等(CSS Environment Variables)を解決する。
68/// この処理系にはノッチ/デバイスセーフエリアという概念自体が存在しない
69/// (実機はRaspberry Pi 3B、ヘッドレスQEMU)ため、`var()`のような真の
70/// 環境変数テーブルは持たず、既知の`safe-area-inset-*`/
71/// `viewport-segment-*`系は常に`"0px"`、フォールバック指定があればそちらを
72/// 優先する「正直な既定値ゼロ」として解決する(丸ごと未対応だった。
73/// 2026-07-18 発見・実装)。
74pub fn resolve_css_env(value: &str) -> String {
75    let mut result = String::new();
76    let mut rest = value;
77    while let Some(start) = rest.find("env(") {
78        result.push_str(rest.get(..start).unwrap_or(""));
79        let after = rest.get(start + 4..).unwrap_or("");
80        let mut depth_paren = 1usize;
81        let mut end = 0;
82        for (i, c) in after.char_indices() {
83            match c {
84                '(' => depth_paren += 1,
85                ')' => {
86                    depth_paren -= 1;
87                    if depth_paren == 0 {
88                        end = i;
89                        break;
90                    }
91                }
92                _ => {}
93            }
94        }
95        let inner = after.get(..end).unwrap_or("");
96        let (_env_name, fallback) = if let Some(comma_pos) = inner.find(',') {
97            (
98                inner.get(..comma_pos).unwrap_or("").trim(),
99                Some(inner.get(comma_pos + 1..).unwrap_or("").trim()),
100            )
101        } else {
102            (inner.trim(), None)
103        };
104        let resolved = fallback.unwrap_or("0px").to_string();
105        let resolved = if resolved.contains("env(") {
106            resolve_css_env(&resolved)
107        } else {
108            resolved
109        };
110        result.push_str(&resolved);
111        rest = after.get(end + 1..).unwrap_or("");
112    }
113    result.push_str(rest);
114    result
115}
116
117/// `calc()`内の単一トークン(`10px`/`50%`/`2rem`/裸の数値等)をpxへ変換する。
118fn parse_calc_unit(s: &str, containing_width: i32) -> Option<f32> {
119    parse_calc_unit_with_font(s, containing_width, 16.0)
120}
121
122fn parse_calc_unit_with_font(s: &str, containing_width: i32, font_px: f32) -> Option<f32> {
123    let s = s.trim();
124    if s.is_empty() {
125        return None;
126    }
127    let font = if font_px > 0.0 { font_px } else { 16.0 };
128    if let Some(n) = s.strip_suffix("px") {
129        return n.trim().parse::<f32>().ok();
130    }
131    if let Some(n) = s.strip_suffix("vh") {
132        let h = crate::os_lib::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
133        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * h as f32);
134    }
135    if let Some(n) = s.strip_suffix("vw") {
136        let w = crate::os_lib::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
137        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * w as f32);
138    }
139    if let Some(n) = s.strip_suffix("vmin") {
140        let w = crate::os_lib::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
141        let h = crate::os_lib::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
142        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * w.min(h) as f32);
143    }
144    if let Some(n) = s.strip_suffix("vmax") {
145        let w = crate::os_lib::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
146        let h = crate::os_lib::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
147        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * w.max(h) as f32);
148    }
149    if let Some(n) = s.strip_suffix("rem") {
150        return n.trim().parse::<f32>().ok().map(|v| v * 16.0);
151    }
152    if let Some(n) = s.strip_suffix("em") {
153        return n.trim().parse::<f32>().ok().map(|v| v * font);
154    }
155    if let Some(n) = s.strip_suffix('%') {
156        return n
157            .trim()
158            .parse::<f32>()
159            .ok()
160            .map(|v| v / 100.0 * containing_width as f32);
161    }
162    if let Some(n) = s.strip_suffix("in") {
163        return n.trim().parse::<f32>().ok().map(|v| v * 96.0);
164    }
165    if let Some(n) = s.strip_suffix("cm") {
166        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 2.54);
167    }
168    if let Some(n) = s.strip_suffix("mm") {
169        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 25.4);
170    }
171    if let Some(n) = s.strip_suffix("pt") {
172        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 72.0);
173    }
174    if let Some(n) = s.strip_suffix("pc") {
175        return n.trim().parse::<f32>().ok().map(|v| v * 16.0);
176    }
177    if let Some(n) = s.strip_suffix('q') {
178        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 101.6);
179    }
180    s.parse::<f32>().ok()
181}
182
183/// バイト位置`*pos`以降の空白をスキップする。
184fn calc_skip_ws(bytes: &[u8], pos: &mut usize) {
185    while *pos < bytes.len() && bytes[*pos] == b' ' {
186        *pos += 1;
187    }
188}
189
190/// `factor := '(' expr ')' | <数値+単位トークン>`。
191fn parse_calc_factor(s: &str, pos: &mut usize, containing_width: i32) -> Option<f32> {
192    parse_calc_factor_with_font(s, pos, containing_width, 16.0)
193}
194
195fn parse_calc_factor_with_font(s: &str, pos: &mut usize, containing_width: i32, font_px: f32) -> Option<f32> {
196    let bytes = s.as_bytes();
197    calc_skip_ws(bytes, pos);
198    if bytes.get(*pos) == Some(&b'(') {
199        *pos += 1;
200        let start = *pos;
201        let mut depth = 1i32;
202        while *pos < bytes.len() && depth > 0 {
203            match bytes[*pos] {
204                b'(' => depth += 1,
205                b')' => depth -= 1,
206                _ => {}
207            }
208            *pos += 1;
209        }
210        if depth != 0 {
211            return None;
212        }
213        let inner = s.get(start..pos.saturating_sub(1)).unwrap_or("");
214        return parse_calc_expr_with_font(inner.trim(), containing_width, font_px);
215    }
216    let start = *pos;
217    while *pos < bytes.len() {
218        let c = bytes[*pos];
219        // 【2026-10-18】`*`・`/` でも区切る(従来は `215px*-1` が 1 語扱いで解析に失敗していた。
220        // YouTube の `bottom:calc(var(--controls-height)*-1)`)。
221        if c == b' ' || c == b'(' || c == b')' || c == b'*' || c == b'/' {
222            break;
223        }
224        if (c == b'+' || c == b'-') && *pos > start {
225            break;
226        }
227        *pos += 1;
228    }
229    let token = s.get(start..*pos).unwrap_or("");
230    parse_calc_unit_with_font(token, containing_width, font_px)
231}
232
233/// `term := factor (('*' | '/') factor)*`。乗除算は加減算より優先度が高い。
234fn parse_calc_term(s: &str, pos: &mut usize, containing_width: i32) -> Option<f32> {
235    parse_calc_term_with_font(s, pos, containing_width, 16.0)
236}
237
238fn parse_calc_term_with_font(s: &str, pos: &mut usize, containing_width: i32, font_px: f32) -> Option<f32> {
239    let bytes = s.as_bytes();
240    let mut value = parse_calc_factor_with_font(s, pos, containing_width, font_px)?;
241    loop {
242        calc_skip_ws(bytes, pos);
243        match bytes.get(*pos) {
244            Some(b'*') => {
245                *pos += 1;
246                calc_skip_ws(bytes, pos);
247                let rhs = parse_calc_factor_with_font(s, pos, containing_width, font_px)?;
248                value *= rhs;
249            }
250            Some(b'/') => {
251                *pos += 1;
252                calc_skip_ws(bytes, pos);
253                let rhs = parse_calc_factor_with_font(s, pos, containing_width, font_px)?;
254                if rhs == 0.0 {
255                    return None;
256                }
257                value /= rhs;
258            }
259            _ => break,
260        }
261    }
262    Some(value)
263}
264
265/// `expr := term (('+' | '-') term)*`。任意個数の項を左結合で畳み込む。
266fn parse_calc_expr(s: &str, containing_width: i32) -> Option<f32> {
267    parse_calc_expr_with_font(s, containing_width, 16.0)
268}
269
270fn parse_calc_expr_with_font(s: &str, containing_width: i32, font_px: f32) -> Option<f32> {
271    let bytes = s.as_bytes();
272    let mut pos = 0usize;
273    let mut value = parse_calc_term_with_font(s, &mut pos, containing_width, font_px)?;
274    loop {
275        calc_skip_ws(bytes, &mut pos);
276        match bytes.get(pos) {
277            Some(b'+') => {
278                pos += 1;
279                calc_skip_ws(bytes, &mut pos);
280                let rhs = parse_calc_term_with_font(s, &mut pos, containing_width, font_px)?;
281                value += rhs;
282            }
283            Some(b'-') => {
284                pos += 1;
285                calc_skip_ws(bytes, &mut pos);
286                let rhs = parse_calc_term_with_font(s, &mut pos, containing_width, font_px)?;
287                value -= rhs;
288            }
289            _ => break,
290        }
291    }
292    calc_skip_ws(bytes, &mut pos);
293    if pos != bytes.len() {
294        return None;
295    }
296    Some(value)
297}
298
299/// `calc()`式をpxへ評価する。四則演算・任意個数の項・丸括弧によるネストに対応する。
300pub fn eval_calc(expr: &str, containing_width: i32) -> Option<i32> {
301    eval_calc_with_font(expr, containing_width, 16.0)
302}
303
304/// 要素のフォントサイズ(`font_px`)を考慮して `calc()` 式を px へ評価する。
305pub fn eval_calc_with_font(expr: &str, containing_width: i32, font_px: f32) -> Option<i32> {
306    let inner = expr.trim();
307    let inner = inner
308        .strip_prefix("calc(")
309        .and_then(|s| s.strip_suffix(')'))?;
310    parse_calc_expr_with_font(inner.trim(), containing_width, font_px).map(|v| v as i32)
311}
312
313/// `min()` / `max()` / `clamp()` の引数を最上位のカンマだけで分割する。
314/// 引数内に `calc()`/`min()`/`max()`/`clamp()` がネストしていても、その内側の
315/// カンマは分割対象にしない(括弧の深さを追跡する)。
316pub(crate) fn split_top_level_commas(s: &str) -> alloc::vec::Vec<&str> {
317    let mut parts = alloc::vec::Vec::new();
318    let bytes = s.as_bytes();
319    let mut depth = 0i32;
320    let mut start = 0usize;
321    for (i, &b) in bytes.iter().enumerate() {
322        match b {
323            b'(' => depth += 1,
324            b')' => depth -= 1,
325            b',' if depth == 0 => {
326                parts.push(s.get(start..i).unwrap_or("").trim());
327                start = i + 1;
328            }
329            _ => {}
330        }
331    }
332    parts.push(s.get(start..).unwrap_or("").trim());
333    parts
334}
335
336/// 単一の `name: value` 宣言(`!important` 込み)をパースする。
337/// 空の name/value や区切りの `:` が無い場合は `None`。
338pub(crate) fn parse_one_declaration(raw: &str) -> Option<Declaration> {
339    let mut kv = raw.splitn(2, ':');
340    let name = kv.next()?.trim();
341    let raw_value = kv.next()?.trim();
342    let (value, important) = match raw_value.rfind('!') {
343        Some(pos)
344            if raw_value
345                .get(pos + 1..)
346                .unwrap_or("")
347                .trim()
348                .eq_ignore_ascii_case("important") =>
349        {
350            (raw_value.get(..pos).unwrap_or("").trim(), true)
351        }
352        _ => (raw_value, false),
353    };
354    if name.is_empty() || value.is_empty() {
355        return None;
356    }
357    Some(Declaration {
358        name: String::from(name),
359        value: String::from(value),
360        important,
361    })
362}
363
364/// CSS Nesting のルール本体をパースする。トップレベル(このネストの深さ)の `;` で
365/// 宣言を区切り、トップレベルの `{` はネストしたルールの開始とみなして対応する `}`
366/// までを取り出し再帰する。ネストしたセレクタの各カンマ枝は、`&` を含めば親セレクタの
367/// テキストで置換し、含まなければ親セレクタの後ろに半角スペースで連結する(子孫結合子
368/// 扱い)。戻り値はこのレベル自身の宣言一覧(ネストしたルールは `out_rules` に積む)。
369pub(crate) fn parse_nested_body(
370    parent_selector_texts: &[String],
371    body: &str,
372    out_rules: &mut Vec<Rule>,
373    layer: Option<usize>,
374) -> Vec<Declaration> {
375    let mut declarations = Vec::new();
376    let chars: Vec<char> = body.chars().collect();
377    let mut i = 0usize;
378    let mut buf = String::new();
379    while i < chars.len() {
380        match chars[i] {
381            '{' => {
382                let nested_selector_text = buf.trim().to_string();
383                buf.clear();
384                // 対応する `}` を探す(ネストの深さを追跡)。
385                let mut depth = 1i32;
386                let start = i + 1;
387                let mut j = start;
388                while j < chars.len() && depth > 0 {
389                    match chars[j] {
390                        '{' => depth += 1,
391                        '}' => depth -= 1,
392                        _ => {}
393                    }
394                    if depth > 0 {
395                        j += 1;
396                    }
397                }
398                let inner_body: String = chars[start..j.min(chars.len())].iter().collect();
399                i = j;
400                if !nested_selector_text.is_empty() {
401                    let mut resolved_texts: Vec<String> = Vec::new();
402                    for nb in split_top_level_commas(&nested_selector_text) {
403                        let nb = nb.trim();
404                        if nb.is_empty() {
405                            continue;
406                        }
407                        for parent in parent_selector_texts {
408                            let resolved = if nb.contains('&') {
409                                nb.replace('&', parent)
410                            } else {
411                                alloc::format!("{} {}", parent, nb)
412                            };
413                            resolved_texts.push(resolved);
414                        }
415                    }
416                    let nested_selectors: Vec<Selector> = resolved_texts
417                        .iter()
418                        .filter_map(|s| parse_selector(s.trim()))
419                        .collect();
420                    let nested_decls =
421                        parse_nested_body(&resolved_texts, &inner_body, out_rules, layer);
422                    if !nested_selectors.is_empty() && !nested_decls.is_empty() {
423                        out_rules.push(Rule {
424                            selectors: nested_selectors,
425                            declarations: nested_decls,
426                            layer,
427                            media_query: None,
428                            container_query: None,
429                        });
430                    }
431                }
432            }
433            ';' => {
434                if let Some(d) = parse_one_declaration(&buf) {
435                    declarations.push(d);
436                }
437                buf.clear();
438            }
439            c => buf.push(c),
440        }
441        i += 1;
442    }
443    // 末尾の `;` は省略可能なので、残った buf も最後の宣言として扱う。
444    if let Some(d) = parse_one_declaration(&buf) {
445        declarations.push(d);
446    }
447    declarations
448}
449
450/// 単一の長さ値(`10px` / `50%` / `calc(...)` など、四則演算を含まない1トークン、
451/// もしくは `calc()`/`min()`/`max()`/`clamp()` の再帰評価)を px へ解決する。
452pub(crate) fn eval_length_token(s: &str, containing_width: i32) -> Option<f32> {
453    eval_length_token_with_font(s, containing_width, 16.0)
454}
455
456pub(crate) fn eval_length_token_with_font(s: &str, containing_width: i32, font_px: f32) -> Option<f32> {
457    let t = s.trim();
458    if t.starts_with("calc(") || t.starts_with("min(") || t.starts_with("max(") || t.starts_with("clamp(")
459        || t.starts_with("abs(") || t.starts_with("sign(") || t.starts_with("round(")
460        || t.starts_with("mod(") || t.starts_with("rem(")
461    {
462        return eval_css_math_with_font(t, containing_width, font_px).map(|v| v as f32);
463    }
464    let font = if font_px > 0.0 { font_px } else { 16.0 };
465    if let Some(n) = t.strip_suffix("px") {
466        return n.trim().parse::<f32>().ok();
467    }
468    if let Some(n) = t.strip_suffix("vh") {
469        let h = crate::os_lib::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
470        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * h as f32);
471    }
472    if let Some(n) = t.strip_suffix("vw") {
473        let w = crate::os_lib::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
474        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * w as f32);
475    }
476    if let Some(n) = t.strip_suffix("vmin") {
477        let w = crate::os_lib::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
478        let h = crate::os_lib::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
479        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * w.min(h) as f32);
480    }
481    if let Some(n) = t.strip_suffix("vmax") {
482        let w = crate::os_lib::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
483        let h = crate::os_lib::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
484        return n.trim().parse::<f32>().ok().map(|v| v / 100.0 * w.max(h) as f32);
485    }
486    if let Some(n) = t.strip_suffix("rem") {
487        return n.trim().parse::<f32>().ok().map(|v| v * 16.0);
488    }
489    if let Some(n) = t.strip_suffix("em") {
490        return n.trim().parse::<f32>().ok().map(|v| v * font);
491    }
492    if let Some(n) = t.strip_suffix('%') {
493        return n
494            .trim()
495            .parse::<f32>()
496            .ok()
497            .map(|v| v / 100.0 * containing_width as f32);
498    }
499    if let Some(n) = t.strip_suffix("in") {
500        return n.trim().parse::<f32>().ok().map(|v| v * 96.0);
501    }
502    if let Some(n) = t.strip_suffix("cm") {
503        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 2.54);
504    }
505    if let Some(n) = t.strip_suffix("mm") {
506        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 25.4);
507    }
508    if let Some(n) = t.strip_suffix("pt") {
509        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 72.0);
510    }
511    if let Some(n) = t.strip_suffix("pc") {
512        return n.trim().parse::<f32>().ok().map(|v| v * 16.0);
513    }
514    if let Some(n) = t.strip_suffix('q') {
515        return n.trim().parse::<f32>().ok().map(|v| v * 96.0 / 101.6);
516    }
517    t.parse::<f32>().ok()
518}
519
520/// CSS の数式関数(`calc()` / `min()` / `max()` / `clamp()` / `abs()` / `sign()` / `round()` / `mod()` / `rem()`)を px へ評価する。
521pub fn eval_css_math(expr: &str, containing_width: i32) -> Option<i32> {
522    eval_css_math_with_font(expr, containing_width, 16.0)
523}
524
525/// 要素のフォントサイズ(`font_px`)を考慮して CSS 数式関数を px へ評価する。
526pub fn eval_css_math_with_font(expr: &str, containing_width: i32, font_px: f32) -> Option<i32> {
527    let t = expr.trim();
528    if t.starts_with("calc(") {
529        return eval_calc_with_font(t, containing_width, font_px);
530    }
531    if let Some(inner) = t.strip_prefix("min(").and_then(|s| s.strip_suffix(')')) {
532        let vals: alloc::vec::Vec<f32> = split_top_level_commas(inner)
533            .iter()
534            .filter_map(|a| eval_length_token_with_font(a, containing_width, font_px))
535            .collect();
536        return vals
537            .into_iter()
538            .fold(None, |acc, v| Some(acc.map_or(v, |m: f32| m.min(v))))
539            .map(|v| v as i32);
540    }
541    if let Some(inner) = t.strip_prefix("max(").and_then(|s| s.strip_suffix(')')) {
542        let vals: alloc::vec::Vec<f32> = split_top_level_commas(inner)
543            .iter()
544            .filter_map(|a| eval_length_token_with_font(a, containing_width, font_px))
545            .collect();
546        return vals
547            .into_iter()
548            .fold(None, |acc, v| Some(acc.map_or(v, |m: f32| m.max(v))))
549            .map(|v| v as i32);
550    }
551    if let Some(inner) = t.strip_prefix("clamp(").and_then(|s| s.strip_suffix(')')) {
552        let parts = split_top_level_commas(inner);
553        if parts.len() != 3 {
554            return None;
555        }
556        let min_v = eval_length_token_with_font(parts[0], containing_width, font_px)?;
557        let pref_v = eval_length_token_with_font(parts[1], containing_width, font_px)?;
558        let max_v = eval_length_token_with_font(parts[2], containing_width, font_px)?;
559        let effective_max = max_v.max(min_v);
560        return Some(pref_v.clamp(min_v, effective_max) as i32);
561    }
562    // W3C CSS Values and Units Level 4 数学関数
563    if let Some(inner) = t.strip_prefix("abs(").and_then(|s| s.strip_suffix(')')) {
564        let v = eval_length_token_with_font(inner.trim(), containing_width, font_px)?;
565        return Some(v.abs() as i32);
566    }
567    if let Some(inner) = t.strip_prefix("sign(").and_then(|s| s.strip_suffix(')')) {
568        let v = eval_length_token_with_font(inner.trim(), containing_width, font_px)?;
569        if v > 0.0 {
570            return Some(1);
571        } else if v < 0.0 {
572            return Some(-1);
573        } else {
574            return Some(0);
575        }
576    }
577    if let Some(inner) = t.strip_prefix("round(").and_then(|s| s.strip_suffix(')')) {
578        let parts = split_top_level_commas(inner);
579        let (strategy, val_tok, step_tok) = match parts.len() {
580            1 => ("nearest", parts[0], "1px"),
581            2 => {
582                let p0 = parts[0].trim().to_ascii_lowercase();
583                if p0 == "up" || p0 == "down" || p0 == "nearest" || p0 == "to-zero" {
584                    (parts[0].trim(), parts[1], "1px")
585                } else {
586                    ("nearest", parts[0], parts[1])
587                }
588            }
589            3 => (parts[0].trim(), parts[1], parts[2]),
590            _ => return None,
591        };
592        let val = eval_length_token_with_font(val_tok, containing_width, font_px)?;
593        let step = eval_length_token_with_font(step_tok, containing_width, font_px)?.abs();
594        if step == 0.0 {
595            return Some(val as i32);
596        }
597        let rounded = match strategy.to_ascii_lowercase().as_str() {
598            "up" => libm::ceilf(val / step) * step,
599            "down" => libm::floorf(val / step) * step,
600            "to-zero" => libm::truncf(val / step) * step,
601            _ => libm::roundf(val / step) * step,
602        };
603        return Some(rounded as i32);
604    }
605    if let Some(inner) = t.strip_prefix("mod(").and_then(|s| s.strip_suffix(')')) {
606        let parts = split_top_level_commas(inner);
607        if parts.len() != 2 {
608            return None;
609        }
610        let a = eval_length_token_with_font(parts[0], containing_width, font_px)?;
611        let b = eval_length_token_with_font(parts[1], containing_width, font_px)?;
612        if b == 0.0 {
613            return None;
614        }
615        let m = (a % b + b) % b;
616        return Some(m as i32);
617    }
618    if let Some(inner) = t.strip_prefix("rem(").and_then(|s| s.strip_suffix(')')) {
619        let parts = split_top_level_commas(inner);
620        if parts.len() != 2 {
621            return None;
622        }
623        let a = eval_length_token_with_font(parts[0], containing_width, font_px)?;
624        let b = eval_length_token_with_font(parts[1], containing_width, font_px)?;
625        if b == 0.0 {
626            return None;
627        }
628        return Some((a % b) as i32);
629    }
630    None
631}
632
633// 簡易CSSパーサ(tag / #id / .class と宣言ブロックをサポート)