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 parent_src = attributes.get("src").map(|s| s.as_str());
409 let mut candidates = Vec::new();
410 for child in &node.children {
411 if let crate::os_lib::dom::NodeType::Element { tag_name: c_tag, attributes: c_attrs, .. } = &child.node_type {
412 if c_tag == "source" {
413 if let Some(src) = c_attrs.get("src") {
414 candidates.push(super::media_controls::MediaSourceCandidate {
415 src: src.clone(),
416 type_attr: c_attrs.get("type").cloned(),
417 media_attr: c_attrs.get("media").cloned(),
418 });
419 }
420 }
421 }
422 }
423 if let Some(selected) = super::media_controls::select_media_source(
424 tag_name,
425 parent_src,
426 &candidates,
427 1024,
428 768,
429 ) {
430 attributes.insert(String::from("src"), resolve_relative_path(base_path, &selected));
431 }
432 if tag_name == "video" {
433 if let Some(src) = attributes.get_mut("src") {
434 *src = resolve_relative_path(base_path, src);
435 }
436 if let Some(video_src) = attributes.get_mut("video_src") {
437 *video_src = resolve_relative_path(base_path, video_src);
438 }
439 }
440 } else if tag_name == "img" {
441 if let Some(src) = attributes.get_mut("src") {
442 *src = resolve_relative_path(base_path, src);
443 }
444 } else if tag_name == "a" || tag_name == "link" {
445 if let Some(href) = attributes.get_mut("href") {
446 *href = resolve_relative_path(base_path, href);
447 }
448 } else if tag_name == "script" {
449 if let Some(src) = attributes.get_mut("src") {
450 *src = resolve_relative_path(base_path, src);
451 }
452 }
453 }
454 for child in &mut node.children {
455 resolve_dom_paths(child, base_path);
456 }
457}
458
459pub(super) fn parse_transform_with_size(
462 s: Option<&str>,
463 elem_w: f32,
464 elem_h: f32,
465 font_px: f32,
466) -> (i32, i32, f32, f32, f32, f32, f32) {
467 super::css_units::parse_transform_with_size(s, elem_w, elem_h, font_px)
468}
469
470pub(super) fn parse_transform(s: Option<&str>) -> (i32, i32, f32, f32, f32, f32, f32) {
472 super::css_units::parse_transform_with_size(s, 0.0, 0.0, 16.0)
473}
474
475
476pub(super) fn parse_px_val(s: &str) -> i32 {
477 if let Some(n) = s.strip_suffix("px") {
478 n.trim().parse::<f32>().ok().map(|v| v as i32).unwrap_or(0)
479 } else if let Some(n) = s.strip_suffix('%') {
480 n.trim().parse::<f32>().ok().map(|v| v as i32).unwrap_or(0)
481 } else {
482 s.parse::<f32>().ok().map(|v| v as i32).unwrap_or(0)
483 }
484}
485
486pub(super) fn extract_url_path(s: &str) -> Option<String> {
487 let t = s.trim();
488 if let Some(start_idx) = t.find("url(") {
489 let after_url = t.get(start_idx + 4..).unwrap_or("");
490 if let Some(end_idx) = after_url.find(')') {
491 let path_raw = after_url.get(..end_idx).unwrap_or("");
492 let path_clean = path_raw
493 .trim_matches(|c| c == '\'' || c == '"' || c == '`')
494 .trim();
495 return Some(String::from(path_clean));
496 }
497 }
498 None
499}
500
501pub(crate) fn extract_all_url_paths(s: &str) -> alloc::vec::Vec<alloc::string::String> {
504 let mut out = alloc::vec::Vec::new();
505 let mut rest = s;
506 while let Some(start_idx) = rest.find("url(") {
507 let after_url = rest.get(start_idx + 4..).unwrap_or("");
508 let Some(end_idx) = after_url.find(')') else {
509 break;
510 };
511 let path_raw = after_url.get(..end_idx).unwrap_or("");
512 let path_clean = path_raw
513 .trim_matches(|c| c == '\'' || c == '"' || c == '`')
514 .trim();
515 if !path_clean.is_empty() {
516 out.push(alloc::string::String::from(path_clean));
517 }
518 rest = after_url.get(end_idx + 1..).unwrap_or("");
519 }
520 out
521}