pub fn iter_json_scripts(html: &str) -> Vec<(&str, &str)> {
let debug = std::env::var("YF_DEBUG").ok().as_deref() == Some("1");
if debug {
eprintln!(
"YF_DEBUG [iter_json_scripts]: html.len()={}; scanning for <script> blocks...",
html.len()
);
}
let mut res = Vec::new();
let mut pos = 0usize;
let mut total_scripts = 0usize;
let mut total_json_scripts = 0usize;
let mut total_svelte_fetched = 0usize;
while let Some(si) = html[pos..].find("<script") {
let si = pos + si;
total_scripts += 1;
let open_end = match html[si..].find('>') {
Some(x) => si + x,
None => break,
};
let tag_open = &html[si..=open_end];
let is_json = tag_open.contains("type=\"application/json\"");
if is_json {
total_json_scripts += 1;
if tag_open.contains("data-sveltekit-fetched") {
total_svelte_fetched += 1;
}
}
let close = match html[open_end + 1..].find("</script>") {
Some(x) => open_end + 1 + x,
None => break,
};
let inner = &html[open_end + 1..close];
if is_json {
res.push((tag_open, inner));
}
pos = close + "</script>".len();
}
if debug {
eprintln!(
"YF_DEBUG [iter_json_scripts]: total_scripts={total_scripts}, total_json_scripts={total_json_scripts}, svelte_fetched={total_svelte_fetched}"
);
if let Some((attrs, body)) = res.first() {
let a = if attrs.len() > 180 {
&attrs[..180]
} else {
attrs
};
let b = if body.len() > 120 { &body[..120] } else { body };
eprintln!(
"YF_DEBUG [iter_json_scripts]: first JSON script attrs[trunc]=`{a}` body[trunc]=`{b}`"
);
}
}
res
}
pub fn find_matching_brace(s: &str, start: usize) -> Option<usize> {
let bytes = s.as_bytes();
let i = start;
if bytes.get(i).copied()? != b'{' {
return None;
}
let mut depth = 0usize;
let mut in_str = false;
let mut j = i;
while j < bytes.len() {
let c = bytes[j];
if in_str {
if c == b'\\' {
j += 2;
continue;
} else if c == b'"' {
in_str = false;
}
j += 1;
continue;
}
match c {
b'"' => {
in_str = true;
}
b'{' => {
depth += 1;
}
b'}' => {
depth -= 1;
if depth == 0 {
return Some(j);
}
}
_ => {}
}
j += 1;
}
None
}
#[cfg(feature = "debug-dumps")]
pub fn parse_jsonish_string(s: &str) -> Option<serde_json::Value> {
let t = s.trim();
if t.starts_with('{') || t.starts_with('[') {
serde_json::from_str::<serde_json::Value>(t).ok()
} else {
None
}
}
#[cfg(feature = "debug-dumps")]
pub fn escape_html(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}