This commit is contained in:
Mateusz Gruszczyński committed 2026-10-03 10:05:46 +02:00
1 parent 5e23e9af02
commit 3883a21bc4
56 files changed
+1640 -403

No files matched your search

+32 -13
View File
@@ -1,5 +1,13 @@
use anyhow::{Context, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RawElement {
Script,
Style,
Textarea,
Pre,
}
/// Conservative HTML minification.
///
/// HTML whitespace can be semantically significant, especially between inline
@@ -11,13 +19,15 @@ pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
let bytes = input.as_bytes();
let mut out = String::with_capacity(input.len());
let mut cursor = 0usize;
let mut raw_element: Option<String> = None;
let mut raw_element: Option<RawElement> = 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());
out.clear();
out.push_str(input);
return Ok(out.into_bytes());
};
let end = end + 3;
out.push_str(&input[cursor..end]);
@@ -26,7 +36,9 @@ pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
}
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());
out.clear();
out.push_str(input);
return Ok(out.into_bytes());
};
cursor = end + 3;
continue;
@@ -35,15 +47,12 @@ pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
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 let Some((name, closing, self_closing)) = raw_tag(tag) {
if closing {
if raw_element.as_deref() == Some(name.as_str()) {
if raw_element == Some(name) {
raw_element = None;
}
} else if !self_closing
&& raw_element.is_none()
&& matches!(name.as_str(), "script" | "style" | "textarea" | "pre")
{
} else if !self_closing && raw_element.is_none() {
raw_element = Some(name);
}
}
@@ -91,7 +100,7 @@ fn find_tag_end(bytes: &[u8], mut cursor: usize) -> Option<usize> {
None
}
fn tag_name(tag: &str) -> Option<(String, bool, bool)> {
fn raw_tag(tag: &str) -> Option<(RawElement, bool, bool)> {
let inner = tag.strip_prefix('<')?.strip_suffix('>')?.trim();
if inner.starts_with('!') || inner.starts_with('?') {
return None;
@@ -101,9 +110,19 @@ fn tag_name(tag: &str) -> Option<(String, bool, bool)> {
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))
.next()?;
let name = if name.eq_ignore_ascii_case("script") {
RawElement::Script
} else if name.eq_ignore_ascii_case("style") {
RawElement::Style
} else if name.eq_ignore_ascii_case("textarea") {
RawElement::Textarea
} else if name.eq_ignore_ascii_case("pre") {
RawElement::Pre
} else {
return None;
};
Some((name, closing, self_closing))
}
#[cfg(test)]