1use super::*;
4
5pub 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 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 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 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
67pub 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
117fn 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
183fn calc_skip_ws(bytes: &[u8], pos: &mut usize) {
185 while *pos < bytes.len() && bytes[*pos] == b' ' {
186 *pos += 1;
187 }
188}
189
190fn 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 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
233fn 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
265fn 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
299pub fn eval_calc(expr: &str, containing_width: i32) -> Option<i32> {
301 eval_calc_with_font(expr, containing_width, 16.0)
302}
303
304pub 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
313pub(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
336pub(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
364pub(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 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 if let Some(d) = parse_one_declaration(&buf) {
445 declarations.push(d);
446 }
447 declarations
448}
449
450pub(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
520pub fn eval_css_math(expr: &str, containing_width: i32) -> Option<i32> {
522 eval_css_math_with_font(expr, containing_width, 16.0)
523}
524
525pub 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 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