use super::is_ws;
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Data,
Escaped,
DoubleEscaped,
}
fn script_tag_at(b: &[u8], i: usize, closing: bool) -> bool {
let s = i + 1 + usize::from(closing);
if closing && b.get(i + 1) != Some(&b'/') {
return false;
}
b.len() >= s + 6
&& b[s..s + 6].eq_ignore_ascii_case(b"script")
&& b.get(s + 6)
.is_some_and(|&c| is_ws(c) || c == b'/' || c == b'>')
}
pub(super) fn end(b: &[u8], start: usize) -> Option<usize> {
let mut state = State::Data;
let mut i = start;
loop {
if state == State::Data {
i += memchr::memchr(b'<', &b[i..])?;
if script_tag_at(b, i, true) {
return Some(i);
}
if b[i..].starts_with(b"<!--") {
state = State::Escaped;
i += 2;
} else {
i += 1;
}
continue;
}
i += memchr::memchr2(b'<', b'-', &b[i..])?;
match (state, b[i]) {
(State::Data, _) => unreachable!(),
(_, b'-') => {
if b[i..].starts_with(b"-->") {
state = State::Data;
i += 3;
} else {
i += 1;
}
}
(State::Escaped, _) => {
if script_tag_at(b, i, true) {
return Some(i);
}
if script_tag_at(b, i, false) {
state = State::DoubleEscaped;
i += 7;
} else {
i += 1;
}
}
(State::DoubleEscaped, _) => {
if script_tag_at(b, i, true) {
state = State::Escaped;
i += 8;
} else {
i += 1;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::end;
#[test]
fn script_ends() {
let cases: &[(&str, Option<usize>)] = &[
("a()</script>x", Some(3)),
("a()</SCRIPT >x", Some(3)),
("a()</scripts>x</script>", Some(14)),
("<!-- a() --></script>", Some(12)),
("<!--<script></script>--></script>", Some(24)),
("<!--<script></script></script>", Some(21)),
("<!--<script>--></script>x", Some(15)),
("<!--<script>-></script></script>x", Some(23)),
("<!-- <sCrIpt>'</script>BAR", None),
("<!--></script>", Some(5)),
("<!--a</script>", Some(5)),
("x</script", None),
("<!--<script></script>--><!-- --></script>", Some(32)),
];
let bad: Vec<_> = cases
.iter()
.filter(|(s, want)| end(s.as_bytes(), 0) != *want)
.map(|(s, want)| (s, want, end(s.as_bytes(), 0)))
.collect();
assert!(bad.is_empty(), "{bad:?}");
}
}