This commit is contained in:
gru committed 2026-10-03 00:27:09 +01:00
commit 5e23e9af02
240 files changed
+99957

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
use anyhow::{Context, Result};
/// Conservative HTML minification.
///
/// HTML whitespace can be semantically significant, especially between inline
/// elements, so this transformer deliberately preserves all whitespace. It only
/// removes ordinary comments outside raw-text elements. Conditional comments and
/// comment-looking bytes inside script/style/textarea/pre are preserved.
pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
let input = std::str::from_utf8(body).context("HTML is not UTF-8")?;
let bytes = input.as_bytes();
let mut out = String::with_capacity(input.len());
let mut cursor = 0usize;
let mut raw_element: Option<String> = None;
while cursor < bytes.len() {
if bytes[cursor] == b'<' {
if raw_element.is_none() && starts_with_ascii_ci(bytes, cursor, b"<!--[if") {
let Some(end) = find_bytes(bytes, cursor + 4, b"-->") else {
return Ok(body.to_vec());
};
let end = end + 3;
out.push_str(&input[cursor..end]);
cursor = end;
continue;
}
if raw_element.is_none() && bytes[cursor..].starts_with(b"<!--") {
let Some(end) = find_bytes(bytes, cursor + 4, b"-->") else {
return Ok(body.to_vec());
};
cursor = end + 3;
continue;
}
if let Some(end) = find_tag_end(bytes, cursor + 1) {
let tag = &input[cursor..=end];
out.push_str(tag);
if let Some((name, closing, self_closing)) = tag_name(tag) {
if closing {
if raw_element.as_deref() == Some(name.as_str()) {
raw_element = None;
}
} else if !self_closing
&& raw_element.is_none()
&& matches!(name.as_str(), "script" | "style" | "textarea" | "pre")
{
raw_element = Some(name);
}
}
cursor = end + 1;
continue;
}
}
let ch = input[cursor..]
.chars()
.next()
.expect("cursor is inside UTF-8 input");
out.push(ch);
cursor += ch.len_utf8();
}
Ok(out.into_bytes())
}
fn find_bytes(haystack: &[u8], start: usize, needle: &[u8]) -> Option<usize> {
haystack
.get(start..)?
.windows(needle.len())
.position(|window| window == needle)
.map(|offset| start + offset)
}
fn starts_with_ascii_ci(haystack: &[u8], start: usize, needle: &[u8]) -> bool {
haystack
.get(start..start.saturating_add(needle.len()))
.is_some_and(|value| value.eq_ignore_ascii_case(needle))
}
fn find_tag_end(bytes: &[u8], mut cursor: usize) -> Option<usize> {
let mut quote: Option<u8> = None;
while cursor < bytes.len() {
match (quote, bytes[cursor]) {
(Some(active), ch) if ch == active => quote = None,
(None, b'"' | b'\'') => quote = Some(bytes[cursor]),
(None, b'>') => return Some(cursor),
_ => {}
}
cursor += 1;
}
None
}
fn tag_name(tag: &str) -> Option<(String, bool, bool)> {
let inner = tag.strip_prefix('<')?.strip_suffix('>')?.trim();
if inner.starts_with('!') || inner.starts_with('?') {
return None;
}
let closing = inner.starts_with('/');
let inner = if closing { inner[1..].trim_start() } else { inner };
let self_closing = inner.ends_with('/');
let name = inner
.split(|ch: char| ch.is_ascii_whitespace() || ch == '/' || ch == '>')
.next()?
.to_ascii_lowercase();
(!name.is_empty()).then_some((name, closing, self_closing))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_semantic_inline_whitespace() {
let input = b"<span>Hello</span> <span>world</span>";
assert_eq!(minify(input).unwrap().as_slice(), input);
}
#[test]
fn strips_only_safe_comments() {
let input = b"<div><!-- remove --><b>x</b></div>";
assert_eq!(minify(input).unwrap(), b"<div><b>x</b></div>");
}
#[test]
fn preserves_comment_syntax_inside_script() {
let input = b"<script>const value = '<!--keep-->';</script>";
assert_eq!(minify(input).unwrap().as_slice(), input);
}
}