1use alloc::string::String;
4use alloc::vec::Vec;
5
6pub(super) fn ascii_eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
7 if a.len() != b.len() {
8 return false;
9 }
10 for i in 0..a.len() {
11 if !a[i].eq_ignore_ascii_case(&b[i]) {
12 return false;
13 }
14 }
15 true
16}
17
18pub(crate) use super::css_units::parse_font_size;
21
22pub fn hash_str(s: &str) -> u32 {
23 let mut h = 5381u32;
24 for c in s.bytes() {
25 h = h.wrapping_shl(5).wrapping_add(h).wrapping_add(c as u32);
26 }
27 h
28}
29
30pub fn url_encode(s: &str) -> String {
31 let mut encoded = String::new();
32 for b in s.as_bytes() {
33 match *b {
34 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
35 encoded.push(*b as char);
36 }
37 b' ' => {
38 encoded.push('+');
39 }
40 _ => {
41 encoded.push_str(&alloc::format!("%{:02X}", b));
42 }
43 }
44 }
45 encoded
46}
47
48pub(super) fn resolve_relative_path(base_path: &str, rel_path: &str) -> String {
49 super::rel_path::resolve_relative_path(base_path, rel_path)
53}
54
55pub(super) fn extract_list_style(styled: &crate::os_lib::css::StyledNode) -> super::ListStyleType {
58 if let Some(t) = styled.specified_values.get("list-style-type") {
59 return super::ListStyleType::parse(t);
60 }
61 if let Some(s) = styled.specified_values.get("list-style") {
62 for tok in s.split_whitespace() {
63 let parsed = super::ListStyleType::parse(tok);
67 if parsed != super::ListStyleType::Unset {
68 return parsed;
69 }
70 }
71 }
72 super::ListStyleType::Unset
73}
74
75pub(super) fn extract_list_style_inside(styled: &crate::os_lib::css::StyledNode) -> bool {
78 if let Some(p) = styled.specified_values.get("list-style-position") {
79 return p.trim().eq_ignore_ascii_case("inside");
80 }
81 if let Some(s) = styled.specified_values.get("list-style") {
82 for tok in s.split_whitespace() {
83 if tok.eq_ignore_ascii_case("inside") {
84 return true;
85 }
86 }
87 }
88 false
89}
90
91pub(super) fn extract_list_style_image(styled: &crate::os_lib::css::StyledNode) -> String {
95 if let Some(v) = styled.specified_values.get("list-style-image") {
96 if v.trim().eq_ignore_ascii_case("none") {
97 return String::new();
98 }
99 if let Some(path) = extract_url_path(v) {
100 return path;
101 }
102 }
103 if let Some(s) = styled.specified_values.get("list-style") {
104 if let Some(path) = extract_url_path(s) {
105 return path;
106 }
107 }
108 String::new()
109}
110
111pub(super) fn parse_bg_size(v: Option<&str>) -> super::BgSizeMode {
113 use super::{BgSizeMode, BgSizeVal};
114 fn parse_val(tok: &str) -> BgSizeVal {
115 let t = tok.trim();
116 if t == "auto" {
117 BgSizeVal::Auto
118 } else if let Some(pct) = t.strip_suffix('%') {
119 pct.parse::<f32>()
120 .map(|p| BgSizeVal::Percent(p / 100.0))
121 .unwrap_or(BgSizeVal::Auto)
122 } else if let Some(px) = t.strip_suffix("px") {
123 px.parse::<i32>().map(BgSizeVal::Px).unwrap_or(BgSizeVal::Auto)
124 } else {
125 BgSizeVal::Auto
126 }
127 }
128 let Some(v) = v else {
129 return BgSizeMode::Stretch;
130 };
131 let v = v.trim().to_ascii_lowercase();
132 match v.as_str() {
133 "cover" => BgSizeMode::Cover,
134 "contain" => BgSizeMode::Contain,
135 "" | "auto" => BgSizeMode::Stretch,
136 _ => {
137 let toks: Vec<&str> = v.split_whitespace().collect();
138 match toks.len() {
139 1 => BgSizeMode::Explicit(parse_val(toks[0]), BgSizeVal::Auto),
140 2 => BgSizeMode::Explicit(parse_val(toks[0]), parse_val(toks[1])),
141 _ => BgSizeMode::Stretch,
142 }
143 }
144 }
145}
146
147pub(crate) fn parse_bg_repeat(v: Option<&str>) -> (super::BgRepeatMode, super::BgRepeatMode) {
151 use super::BgRepeatMode;
152 fn parse_one(t: &str) -> BgRepeatMode {
153 match t {
154 "no-repeat" => BgRepeatMode::NoRepeat,
155 "space" => BgRepeatMode::Space,
156 "round" => BgRepeatMode::Round,
157 _ => BgRepeatMode::Repeat,
158 }
159 }
160 let Some(v) = v else {
161 return (BgRepeatMode::Repeat, BgRepeatMode::Repeat);
162 };
163 let v = v.trim().to_ascii_lowercase();
164 match v.as_str() {
165 "no-repeat" => (BgRepeatMode::NoRepeat, BgRepeatMode::NoRepeat),
166 "repeat-x" => (BgRepeatMode::Repeat, BgRepeatMode::NoRepeat),
167 "repeat-y" => (BgRepeatMode::NoRepeat, BgRepeatMode::Repeat),
168 "repeat" => (BgRepeatMode::Repeat, BgRepeatMode::Repeat),
169 _ => {
170 let toks: Vec<&str> = v.split_whitespace().collect();
171 match toks.len() {
172 2 => (parse_one(toks[0]), parse_one(toks[1])),
173 1 => {
174 let m = parse_one(toks[0]);
175 (m, m)
176 }
177 _ => (BgRepeatMode::Repeat, BgRepeatMode::Repeat),
178 }
179 }
180 }
181}
182
183pub(super) fn parse_bg_position(v: Option<&str>) -> (f32, f32) {
185 fn parse_axis(tok: &str, is_x: bool) -> f32 {
186 let t = tok.trim();
187 match t {
188 "left" | "top" => 0.0,
189 "center" => 0.5,
190 "right" | "bottom" => 1.0,
191 _ => {
192 if let Some(pct) = t.strip_suffix('%') {
193 pct.parse::<f32>().map(|p| p / 100.0).unwrap_or(0.0)
194 } else {
195 let _ = is_x;
196 0.0
197 }
198 }
199 }
200 }
201 let Some(v) = v else {
202 return (0.0, 0.0);
203 };
204 let toks: Vec<&str> = v.split_whitespace().collect();
205 match toks.len() {
206 1 => (parse_axis(toks[0], true), 0.5),
207 2 => {
208 let first_is_y_kw = matches!(toks[0].trim(), "top" | "bottom");
214 let second_is_x_kw = matches!(toks[1].trim(), "left" | "right");
215 if first_is_y_kw || second_is_x_kw {
216 (parse_axis(toks[1], true), parse_axis(toks[0], false))
217 } else {
218 (parse_axis(toks[0], true), parse_axis(toks[1], false))
219 }
220 }
221 _ => (0.0, 0.0),
222 }
223}
224
225pub(crate) fn parse_text_decoration_shorthand(
231 value: &str,
232) -> (bool, bool, bool, bool, Option<String>, Option<u32>) {
233 let v = value.trim().to_lowercase();
234 if v == "none" {
235 return (false, false, false, true, None, None);
236 }
237 let mut underline = false;
238 let mut line_through = false;
239 let mut overline = false;
240 let mut style: Option<String> = None;
241 let mut color: Option<u32> = None;
242 for tok in v.split_whitespace() {
243 match tok {
244 "underline" => underline = true,
245 "line-through" => line_through = true,
246 "overline" => overline = true,
247 "blink" => {} "solid" | "dotted" | "dashed" | "wavy" | "double" => style = Some(String::from(tok)),
249 _ => {
250 if let Some(c) = crate::os_lib::css::parse_color(tok) {
251 color = Some(c);
252 }
253 }
254 }
255 }
256 (underline, line_through, overline, false, style, color)
257}
258
259pub(super) fn parse_outline(styled: &crate::os_lib::css::StyledNode) -> Option<(u32, i32, String)> {
263 fn parse_width_tok(t: &str) -> Option<i32> {
264 match t {
265 "thin" => Some(1),
266 "medium" => Some(3),
267 "thick" => Some(5),
268 _ => t
269 .strip_suffix("px")
270 .and_then(|n| n.trim().parse::<i32>().ok()),
271 }
272 }
273 let mut color: Option<u32> = None;
274 let mut width: Option<i32> = None;
275 let mut has_none_style = false;
276 let mut style = String::from("solid");
277
278 if let Some(w) = styled.specified_values.get("outline-width") {
279 width = parse_width_tok(w.trim());
280 }
281 if let Some(c) = styled.specified_values.get("outline-color") {
282 color = crate::os_lib::css::parse_color(c);
283 }
284 if let Some(s) = styled.specified_values.get("outline-style") {
285 let s_l = s.trim().to_lowercase();
286 if s_l == "none" || s_l == "hidden" {
290 has_none_style = true;
291 } else if matches!(s_l.as_str(), "dashed" | "dotted") {
292 style = s_l;
293 } else if matches!(s_l.as_str(), "groove" | "ridge" | "inset" | "outset") {
294 style = String::from("solid");
295 }
296 }
297 if let Some(shorthand) = styled.specified_values.get("outline") {
298 let s = shorthand.trim();
299 if s.eq_ignore_ascii_case("none") {
300 has_none_style = true;
301 } else {
302 for tok in s.split_whitespace() {
303 if let Some(w) = parse_width_tok(tok) {
304 width = width.or(Some(w));
305 } else if let Some(c) = crate::os_lib::css::parse_color(tok) {
306 color = color.or(Some(c));
307 } else if tok.eq_ignore_ascii_case("hidden") {
308 has_none_style = true;
309 } else if matches!(tok, "dashed" | "dotted") {
310 style = String::from(tok);
311 } else if matches!(tok, "groove" | "ridge" | "inset" | "outset") {
312 style = String::from("solid");
313 }
314 }
315 }
316 }
317
318 if has_none_style {
319 return None;
320 }
321 let w = width.unwrap_or(3); let c = color?; if w <= 0 {
324 return None;
325 }
326 Some((c, w, style))
327}
328
329pub(super) fn parse_object_fit(v: Option<&str>) -> super::ObjectFit {
331 use super::ObjectFit;
332 match v.map(|s| s.trim().to_ascii_lowercase()) {
333 Some(ref s) if s == "contain" => ObjectFit::Contain,
334 Some(ref s) if s == "cover" => ObjectFit::Cover,
335 Some(ref s) if s == "none" => ObjectFit::None,
336 Some(ref s) if s == "scale-down" => ObjectFit::ScaleDown,
337 _ => ObjectFit::Fill,
338 }
339}
340
341pub(super) fn parse_object_position(v: Option<&str>) -> (f32, f32) {
344 match v {
345 Some(_) => parse_bg_position(v),
346 None => (0.5, 0.5),
347 }
348}
349
350pub(super) fn parse_transform_origin(v: Option<&str>) -> (f32, f32) {
353 match v {
354 Some(_) => parse_bg_position(v),
355 None => (0.5, 0.5),
356 }
357}
358
359pub(super) fn get_base_path(path: &str) -> String {
360 if path == "/" {
361 return String::from("/");
362 }
363 if let Some(last_slash_idx) = path.rfind('/') {
364 if last_slash_idx == 0 {
365 String::from("/")
366 } else {
367 String::from(path.get(..=last_slash_idx).unwrap_or("/"))
368 }
369 } else {
370 String::from("/")
371 }
372}
373
374pub(super) fn resolve_dom_paths(node: &mut crate::os_lib::dom::Node, base_path: &str) {
375 if let crate::os_lib::dom::NodeType::Element {
376 tag_name,
377 attributes,
378 ..
379 } = &mut node.node_type
380 {
381 if tag_name == "picture" {
382 let mut source_url = None;
384 for child in &node.children {
385 if let crate::os_lib::dom::NodeType::Element { tag_name: c_tag, attributes: c_attrs, .. } = &child.node_type {
386 if c_tag == "source" {
387 if let Some(srcset) = c_attrs.get("srcset").or_else(|| c_attrs.get("src")) {
388 let url = srcset.split_whitespace().next().unwrap_or(srcset);
390 source_url = Some(String::from(url));
391 break;
392 }
393 }
394 }
395 }
396 if let Some(url) = source_url {
397 for child in &mut node.children {
398 if let crate::os_lib::dom::NodeType::Element { tag_name: c_tag, attributes: c_attrs, .. } = &mut child.node_type {
399 if c_tag == "img" {
400 c_attrs.insert(String::from("src"), resolve_relative_path(base_path, &url));
401 break;
402 }
403 }
404 }
405 }
406 } else if tag_name == "video" || tag_name == "audio" {
407 let mut source_url = None;
409 for child in &node.children {
410 if let crate::os_lib::dom::NodeType::Element { tag_name: c_tag, attributes: c_attrs, .. } = &child.node_type {
411 if c_tag == "source" {
412 if let Some(src) = c_attrs.get("src") {
413 source_url = Some(src.clone());
414 break;
415 }
416 }
417 }
418 }
419 if let Some(url) = source_url {
420 attributes.insert(String::from("src"), resolve_relative_path(base_path, &url));
421 }
422 if tag_name == "video" {
423 if let Some(src) = attributes.get_mut("src") {
424 *src = resolve_relative_path(base_path, src);
425 }
426 if let Some(video_src) = attributes.get_mut("video_src") {
427 *video_src = resolve_relative_path(base_path, video_src);
428 }
429 }
430 } else if tag_name == "img" {
431 if let Some(src) = attributes.get_mut("src") {
432 *src = resolve_relative_path(base_path, src);
433 }
434 } else if tag_name == "a" || tag_name == "link" {
435 if let Some(href) = attributes.get_mut("href") {
436 *href = resolve_relative_path(base_path, href);
437 }
438 } else if tag_name == "script" {
439 if let Some(src) = attributes.get_mut("src") {
440 *src = resolve_relative_path(base_path, src);
441 }
442 }
443 }
444 for child in &mut node.children {
445 resolve_dom_paths(child, base_path);
446 }
447}
448
449pub(super) fn parse_transform(s: Option<&str>) -> (i32, i32, f32, f32, f32, f32, f32) {
451 let s = match s {
452 Some(v) if v.trim() != "none" => v.trim(),
453 _ => return (0, 0, 1.0, 1.0, 0.0, 0.0, 0.0),
454 };
455 let mut tx = 0i32;
456 let mut ty = 0i32;
457 let mut scale_x = 1.0f32;
458 let mut scale_y = 1.0f32;
459 let mut rotate = 0.0f32;
460 let mut skew_x = 0.0f32;
461 let mut skew_y = 0.0f32;
462 let mut rest = s;
464 while !rest.is_empty() {
465 rest = rest.trim_start();
466 if let Some(after) = rest.strip_prefix("translateX(") {
467 if let Some((inside, after_rest)) = after.split_once(')') {
468 let val = inside.trim();
469 tx += parse_px_val(val);
470 rest = after_rest;
471 } else {
472 break;
473 }
474 } else if let Some(after) = rest.strip_prefix("translateY(") {
475 if let Some((inside, after_rest)) = after.split_once(')') {
476 let val = inside.trim();
477 ty += parse_px_val(val);
478 rest = after_rest;
479 } else {
480 break;
481 }
482 } else if let Some(after) = rest.strip_prefix("translate(") {
483 if let Some((args, after_rest)) = after.split_once(')') {
484 let mut it = args.splitn(2, ',');
485 tx += parse_px_val(it.next().unwrap_or("").trim());
486 ty += parse_px_val(it.next().unwrap_or("0").trim());
487 rest = after_rest;
488 } else {
489 break;
490 }
491 } else if let Some(after) = rest.strip_prefix("translate3d(") {
492 if let Some((args, after_rest)) = after.split_once(')') {
493 let mut it = args.splitn(3, ',');
494 tx += parse_px_val(it.next().unwrap_or("").trim());
495 ty += parse_px_val(it.next().unwrap_or("0").trim());
496 rest = after_rest;
497 } else {
498 break;
499 }
500 } else if let Some(after) = rest.strip_prefix("scale(") {
501 if let Some((inside, after_rest)) = after.split_once(')') {
502 let mut it = inside.split(',');
503 let vx = it.next().unwrap_or("1").trim().parse::<f32>().unwrap_or(1.0);
504 let vy = it
506 .next()
507 .map(|v| v.trim().parse::<f32>().unwrap_or(vx))
508 .unwrap_or(vx);
509 scale_x *= vx;
510 scale_y *= vy;
511 rest = after_rest;
512 } else {
513 break;
514 }
515 } else if let Some(after) = rest.strip_prefix("scaleX(") {
516 if let Some((inside, after_rest)) = after.split_once(')') {
517 scale_x *= inside.trim().parse::<f32>().unwrap_or(1.0);
518 rest = after_rest;
519 } else {
520 break;
521 }
522 } else if let Some(after) = rest.strip_prefix("scaleY(") {
523 if let Some((inside, after_rest)) = after.split_once(')') {
524 scale_y *= inside.trim().parse::<f32>().unwrap_or(1.0);
525 rest = after_rest;
526 } else {
527 break;
528 }
529 } else if let Some(after) = rest.strip_prefix("rotate(") {
530 if let Some((inside, after_rest)) = after.split_once(')') {
531 let val = inside.trim();
532 rotate += if let Some(deg) = val.strip_suffix("deg") {
533 deg.trim().parse::<f32>().unwrap_or(0.0)
534 } else if let Some(rad) = val.strip_suffix("rad") {
535 rad.trim().parse::<f32>().unwrap_or(0.0) * 57.295_78
536 } else {
537 val.parse::<f32>().unwrap_or(0.0)
538 };
539 rest = after_rest;
540 } else {
541 break;
542 }
543 } else if let Some(after) = rest.strip_prefix("matrix(") {
544 if let Some((args_str, after_rest)) = after.split_once(')') {
545 let args: Vec<&str> = args_str.split(',').collect();
547 tx += args
548 .get(4)
549 .and_then(|v| v.trim().parse::<f32>().ok())
550 .map(|v| v as i32)
551 .unwrap_or(0);
552 ty += args
553 .get(5)
554 .and_then(|v| v.trim().parse::<f32>().ok())
555 .map(|v| v as i32)
556 .unwrap_or(0);
557 rest = after_rest;
558 } else {
559 break;
560 }
561 } else if let Some(after) = rest.strip_prefix("skewX(") {
562 if let Some((inside, after_rest)) = after.split_once(')') {
563 let val = inside.trim();
564 skew_x += if let Some(deg) = val.strip_suffix("deg") {
565 deg.trim().parse::<f32>().unwrap_or(0.0)
566 } else if let Some(rad) = val.strip_suffix("rad") {
567 rad.trim().parse::<f32>().unwrap_or(0.0) * 57.295_78
568 } else {
569 val.parse::<f32>().unwrap_or(0.0)
570 };
571 rest = after_rest;
572 } else {
573 break;
574 }
575 } else if let Some(after) = rest.strip_prefix("skewY(") {
576 if let Some((inside, after_rest)) = after.split_once(')') {
577 let val = inside.trim();
578 skew_y += if let Some(deg) = val.strip_suffix("deg") {
579 deg.trim().parse::<f32>().unwrap_or(0.0)
580 } else if let Some(rad) = val.strip_suffix("rad") {
581 rad.trim().parse::<f32>().unwrap_or(0.0) * 57.295_78
582 } else {
583 val.parse::<f32>().unwrap_or(0.0)
584 };
585 rest = after_rest;
586 } else {
587 break;
588 }
589 } else if let Some(after) = rest.strip_prefix("skew(") {
590 if let Some((args, after_rest)) = after.split_once(')') {
591 let mut it = args.splitn(2, ',');
592 let ax_str = it.next().unwrap_or("").trim();
593 skew_x += if let Some(deg) = ax_str.strip_suffix("deg") {
594 deg.trim().parse::<f32>().unwrap_or(0.0)
595 } else if let Some(rad) = ax_str.strip_suffix("rad") {
596 rad.trim().parse::<f32>().unwrap_or(0.0) * 57.295_78
597 } else {
598 ax_str.parse::<f32>().unwrap_or(0.0)
599 };
600 if let Some(ay_str) = it.next() {
601 let ay_str = ay_str.trim();
602 skew_y += if let Some(deg) = ay_str.strip_suffix("deg") {
603 deg.trim().parse::<f32>().unwrap_or(0.0)
604 } else if let Some(rad) = ay_str.strip_suffix("rad") {
605 rad.trim().parse::<f32>().unwrap_or(0.0) * 57.295_78
606 } else {
607 ay_str.parse::<f32>().unwrap_or(0.0)
608 };
609 }
610 rest = after_rest;
611 } else {
612 break;
613 }
614 } else {
615 if let Some((_, after_rest)) = rest.split_once(')') {
617 rest = after_rest;
618 } else {
619 break;
620 }
621 }
622 }
623 (tx, ty, scale_x, scale_y, rotate, skew_x, skew_y)
624}
625
626pub(super) fn parse_px_val(s: &str) -> i32 {
627 if let Some(n) = s.strip_suffix("px") {
628 n.trim().parse::<f32>().ok().map(|v| v as i32).unwrap_or(0)
629 } else if let Some(n) = s.strip_suffix('%') {
630 n.trim().parse::<f32>().ok().map(|v| v as i32).unwrap_or(0)
631 } else {
632 s.parse::<f32>().ok().map(|v| v as i32).unwrap_or(0)
633 }
634}
635
636pub(super) fn extract_url_path(s: &str) -> Option<String> {
637 let t = s.trim();
638 if let Some(start_idx) = t.find("url(") {
639 let after_url = t.get(start_idx + 4..).unwrap_or("");
640 if let Some(end_idx) = after_url.find(')') {
641 let path_raw = after_url.get(..end_idx).unwrap_or("");
642 let path_clean = path_raw
643 .trim_matches(|c| c == '\'' || c == '"' || c == '`')
644 .trim();
645 return Some(String::from(path_clean));
646 }
647 }
648 None
649}
650
651pub(crate) fn extract_all_url_paths(s: &str) -> alloc::vec::Vec<alloc::string::String> {
654 let mut out = alloc::vec::Vec::new();
655 let mut rest = s;
656 while let Some(start_idx) = rest.find("url(") {
657 let after_url = rest.get(start_idx + 4..).unwrap_or("");
658 let Some(end_idx) = after_url.find(')') else {
659 break;
660 };
661 let path_raw = after_url.get(..end_idx).unwrap_or("");
662 let path_clean = path_raw
663 .trim_matches(|c| c == '\'' || c == '"' || c == '`')
664 .trim();
665 if !path_clean.is_empty() {
666 out.push(alloc::string::String::from(path_clean));
667 }
668 rest = after_url.get(end_idx + 1..).unwrap_or("");
669 }
670 out
671}