use std::borrow::Cow;
use std::ops::Range;
use std::str;
#[derive(Debug, Default, Clone)]
pub struct PrescanReport {
pub head_range: Option<Range<usize>>,
pub had_custom_elements: bool,
pub had_cdata: bool,
pub had_unescaped_lt: bool,
pub has_script_or_style: bool,
pub has_svg: bool,
}
const STRIP_CONTENT_TAGS: [&[u8]; 2] = [b"script", b"style"];
const SVG_TAG: &[u8] = b"svg";
const HEAD_TAG: &[u8] = b"head";
const CDATA_START: &[u8] = b"<![CDATA[";
const DOCTYPE: &[u8] = b"doctype";
const EMPTY_COMMENT: &[u8] = b"<!---->";
const SELF_CLOSING: [(&[u8], &str); 3] = [(b"<br/>", "<br>"), (b"<hr/>", "<hr>"), (b"<img/>", "<img>")];
#[must_use]
pub fn run(html: &str) -> (Cow<'_, str>, PrescanReport) {
let bytes = html.as_bytes();
let len = bytes.len();
if len == 0 {
return (Cow::Borrowed(html), PrescanReport::default());
}
let mut report = PrescanReport::default();
let mut idx = 0usize;
let mut last = 0usize;
let mut output: Option<String> = None;
let mut svg_depth = 0usize;
let mut head_open_end: Option<usize> = None;
while idx < len {
if bytes[idx] != b'<' {
idx += 1;
continue;
}
if bytes[idx..].starts_with(CDATA_START) {
report.had_cdata = true;
}
if bytes[idx..].starts_with(EMPTY_COMMENT) {
let out = output.get_or_insert_with(|| String::with_capacity(html.len()));
out.push_str(&html[last..idx]);
out.push_str("<!-- -->");
idx += EMPTY_COMMENT.len();
last = idx;
continue;
}
{
let mut replaced = false;
for (pattern, replacement) in &SELF_CLOSING {
if bytes[idx..].starts_with(pattern) {
let out = output.get_or_insert_with(|| String::with_capacity(html.len()));
out.push_str(&html[last..idx]);
out.push_str(replacement);
idx += pattern.len();
last = idx;
replaced = true;
break;
}
}
if replaced {
continue;
}
}
if matches_tag_start(bytes, idx + 1, SVG_TAG) {
if let Some(open_end) = find_tag_end(bytes, idx + 1 + SVG_TAG.len()) {
svg_depth += 1;
report.has_svg = true;
idx = open_end;
continue;
}
} else if matches_end_tag_start(bytes, idx + 1, SVG_TAG) {
if let Some(close_end) = find_tag_end(bytes, idx + 2 + SVG_TAG.len()) {
if svg_depth > 0 {
svg_depth = svg_depth.saturating_sub(1);
}
idx = close_end;
continue;
}
}
if svg_depth == 0 {
let mut handled = false;
for tag in &STRIP_CONTENT_TAGS {
if matches_tag_start(bytes, idx + 1, tag) {
if let Some(open_end) = find_tag_end(bytes, idx + 1 + tag.len()) {
report.has_script_or_style = true;
let remove_end = find_closing_tag(bytes, open_end, tag).unwrap_or(len);
let out = output.get_or_insert_with(|| String::with_capacity(html.len()));
out.push_str(&html[last..idx]);
out.push_str(&html[idx..open_end]);
out.push_str("</");
out.push_str(str::from_utf8(tag).unwrap());
out.push('>');
last = remove_end;
idx = remove_end;
handled = true;
break;
}
}
}
if handled {
continue;
}
if idx + 2 < len && bytes[idx + 1] == b'!' {
let mut cursor = idx + 2;
while cursor < len && bytes[cursor].is_ascii_whitespace() {
cursor += 1;
}
if cursor + DOCTYPE.len() <= len && bytes[cursor..cursor + DOCTYPE.len()].eq_ignore_ascii_case(DOCTYPE)
{
if let Some(end) = find_tag_end(bytes, cursor + DOCTYPE.len()) {
let out = output.get_or_insert_with(|| String::with_capacity(html.len()));
out.push_str(&html[last..idx]);
last = end;
idx = end;
continue;
}
}
}
if matches_tag_start(bytes, idx + 1, HEAD_TAG) {
if let Some(open_end) = find_tag_end(bytes, idx + 1 + HEAD_TAG.len()) {
let flushed_so_far = if let Some(ref out) = output {
out.len() + (open_end - last)
} else {
open_end
};
head_open_end = Some(flushed_so_far);
idx = open_end;
continue;
}
} else if matches_end_tag_start(bytes, idx + 1, HEAD_TAG) {
if let Some(close_end) = find_tag_end(bytes, idx + 2 + HEAD_TAG.len()) {
if let Some(start) = head_open_end.take() {
let flushed_so_far = if let Some(ref out) = output {
out.len() + (idx - last)
} else {
idx
};
report.head_range = Some(start..flushed_so_far);
}
idx = close_end;
continue;
}
}
{
let tag_start = idx + 1;
if tag_start < len && (bytes[tag_start].is_ascii_alphabetic()) {
let name_end = {
let mut e = tag_start;
while e < len && (bytes[e].is_ascii_alphanumeric() || bytes[e] == b'-' || bytes[e] == b'_') {
e += 1;
}
e
};
let tag_name = &bytes[tag_start..name_end];
if tag_name.contains(&b'-') {
report.had_custom_elements = true;
}
}
}
}
let is_valid_tag = if idx + 1 < len {
match bytes[idx + 1] {
b'!' => {
idx + 2 < len
&& (bytes[idx + 2] == b'-'
|| bytes[idx + 2].is_ascii_alphabetic()
|| bytes[idx + 2].is_ascii_uppercase())
}
b'/' => idx + 2 < len && (bytes[idx + 2].is_ascii_alphabetic() || bytes[idx + 2].is_ascii_uppercase()),
b'?' => true,
c if c.is_ascii_alphabetic() || c.is_ascii_uppercase() => true,
_ => false,
}
} else {
false
};
if !is_valid_tag {
report.had_unescaped_lt = true;
let out = output.get_or_insert_with(|| String::with_capacity(html.len() + 4));
out.push_str(&html[last..idx]);
out.push_str("<");
idx += 1;
last = idx;
continue;
}
idx += 1;
}
if let Some(start) = head_open_end.take() {
let end = if let Some(ref out) = output {
out.len() + (len - last)
} else {
len
};
report.head_range = Some(start..end);
}
let cow = if let Some(mut out) = output {
if last < len {
out.push_str(&html[last..]);
}
Cow::Owned(out)
} else {
Cow::Borrowed(html)
};
(cow, report)
}
fn matches_tag_start(bytes: &[u8], mut start: usize, tag: &[u8]) -> bool {
if start >= bytes.len() || start + tag.len() > bytes.len() {
return false;
}
if !bytes[start..start + tag.len()].eq_ignore_ascii_case(tag) {
return false;
}
start += tag.len();
matches!(
bytes.get(start),
Some(b'>' | b'/' | b' ' | b'\t' | b'\n' | b'\r') | None
)
}
fn matches_end_tag_start(bytes: &[u8], start: usize, tag: &[u8]) -> bool {
if start >= bytes.len() || bytes[start] != b'/' {
return false;
}
matches_tag_start(bytes, start + 1, tag)
}
fn find_tag_end(bytes: &[u8], mut idx: usize) -> Option<usize> {
let len = bytes.len();
let mut in_quote: Option<u8> = None;
while idx < len {
match bytes[idx] {
b'"' | b'\'' => {
if let Some(current) = in_quote {
if current == bytes[idx] {
in_quote = None;
}
} else {
in_quote = Some(bytes[idx]);
}
}
b'>' if in_quote.is_none() => return Some(idx + 1),
_ => {}
}
idx += 1;
}
None
}
fn find_closing_tag(bytes: &[u8], mut idx: usize, tag: &[u8]) -> Option<usize> {
let len = bytes.len();
let mut depth = 1usize;
while idx < len {
if bytes[idx] == b'<' {
if matches_tag_start(bytes, idx + 1, tag) {
if let Some(next) = find_tag_end(bytes, idx + 1 + tag.len()) {
depth += 1;
idx = next;
continue;
}
} else if matches_end_tag_start(bytes, idx + 1, tag) {
if let Some(close) = find_tag_end(bytes, idx + 2 + tag.len()) {
depth -= 1;
if depth == 0 {
return Some(close);
}
idx = close;
continue;
}
}
}
idx += 1;
}
None
}