atmos/os_lib/web_engine/
doc_title.rs1use crate::os_lib::dom::{Node, NodeType};
16use alloc::string::String;
17use alloc::vec::Vec;
18
19const MAX_DEPTH: usize = 512;
21
22pub fn document_title(root: &Node) -> Option<String> {
30 let mut stack: Vec<(&Node, usize)> = Vec::new();
31 stack.push((root, 0));
32 while let Some((node, depth)) = stack.pop() {
33 let tag = match &node.node_type {
34 NodeType::Element { tag_name, .. } => tag_name,
35 NodeType::Text(_) => continue,
36 };
37 if tag.eq_ignore_ascii_case("title") {
38 let mut raw = String::new();
39 for child in &node.children {
40 if let NodeType::Text(t) = &child.node_type {
41 raw.push_str(t);
42 }
43 }
44 let title = collapse_ascii_whitespace(&raw);
45 return if title.is_empty() { None } else { Some(title) };
46 }
47 if tag.eq_ignore_ascii_case("svg") || depth >= MAX_DEPTH {
48 continue;
49 }
50 for child in node.children.iter().rev() {
52 stack.push((child, depth + 1));
53 }
54 }
55 None
56}
57
58fn collapse_ascii_whitespace(s: &str) -> String {
61 let mut out = String::with_capacity(s.len());
62 let mut pending_space = false;
63 for c in s.chars() {
64 if matches!(c, '\t' | '\n' | '\x0C' | '\r' | ' ') {
65 pending_space = !out.is_empty();
66 } else {
67 if pending_space {
68 out.push(' ');
69 pending_space = false;
70 }
71 out.push(c);
72 }
73 }
74 out
75}