diff --git a/Cargo.lock b/Cargo.lock index 2c5f3380..56991e13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1494,7 +1494,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "time 0.3.41", + "time", "version_check", ] @@ -3150,13 +3150,12 @@ dependencies = [ "gpui", "gpui-component-macros", "html5ever 0.27.0", - "html5minify", "image", "indexset", "indoc", "itertools 0.13.0", "markdown", - "markup5ever_rcdom 0.3.0", + "markup5ever_rcdom", "num-traits", "once_cell", "paste", @@ -3437,20 +3436,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "html5ever" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" -dependencies = [ - "log", - "mac", - "markup5ever 0.10.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -3479,16 +3464,6 @@ dependencies = [ "syn 2.0.105", ] -[[package]] -name = "html5minify" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f0717993787d29fc1177a9a5233f7979517fdaa8eb498fb45ae36ffe6c65a17" -dependencies = [ - "html5ever 0.25.2", - "markup5ever_rcdom 0.1.0", -] - [[package]] name = "http" version = "1.3.1" @@ -4382,20 +4357,6 @@ dependencies = [ "unicode-id", ] -[[package]] -name = "markup5ever" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" -dependencies = [ - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "markup5ever" version = "0.11.0" @@ -4424,18 +4385,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "markup5ever_rcdom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" -dependencies = [ - "html5ever 0.25.2", - "markup5ever 0.10.1", - "tendril", - "xml5ever 0.16.2", -] - [[package]] name = "markup5ever_rcdom" version = "0.3.0" @@ -4445,7 +4394,7 @@ dependencies = [ "html5ever 0.27.0", "markup5ever 0.12.1", "tendril", - "xml5ever 0.18.1", + "xml5ever", ] [[package]] @@ -5701,7 +5650,7 @@ dependencies = [ "indexmap 2.10.0", "quick-xml 0.38.1", "serde", - "time 0.3.41", + "time", ] [[package]] @@ -8034,17 +7983,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.41" @@ -9147,12 +9085,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -10467,18 +10399,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" -[[package]] -name = "xml5ever" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865" -dependencies = [ - "log", - "mac", - "markup5ever 0.10.1", - "time 0.1.45", -] - [[package]] name = "xml5ever" version = "0.18.1" diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index bf06bb2e..1c029bf8 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -87,8 +87,6 @@ markdown = "1.0.0" # HTML Parser html5ever = "0.27" markup5ever_rcdom = "0.3.0" -# For minifying HTML -html5minify = "0.3" # Calendar chrono = "0.4.38" diff --git a/crates/ui/src/text/html.rs b/crates/ui/src/text/html.rs index fec848d3..5f0de714 100644 --- a/crates/ui/src/text/html.rs +++ b/crates/ui/src/text/html.rs @@ -13,7 +13,6 @@ use gpui::{ use html5ever::tendril::TendrilSink; use html5ever::{local_name, parse_document, LocalName, ParseOpts}; use markup5ever_rcdom::{Node, NodeData, RcDom}; -use regex::bytes::Regex; use crate::v_flex; @@ -85,18 +84,14 @@ pub(super) fn parse_html(source: &str) -> Result { // TODO: Find a better and light-weight HTML minifier fn cleanup_html(source: &str) -> Vec { - // Replace all \n to space - let re = Regex::new(r"\s*(<.+?>)\s*").unwrap(); - let source: Vec = re.replace_all(source.as_bytes(), b" $1 ").into(); - let mut w = std::io::Cursor::new(vec![]); - let mut r = std::io::Cursor::new(source.clone()); - let mut minify = html5minify::Minifier::new(&mut w); + let mut r = std::io::Cursor::new(source); + let mut minify = super::html5minify::Minifier::new(&mut w); minify.omit_doctype(true); if let Ok(()) = minify.minify(&mut r) { w.into_inner() } else { - source + source.bytes().collect() } } diff --git a/crates/ui/src/text/html5minify/mod.rs b/crates/ui/src/text/html5minify/mod.rs new file mode 100644 index 00000000..0b09a393 --- /dev/null +++ b/crates/ui/src/text/html5minify/mod.rs @@ -0,0 +1,895 @@ +//! HTML5 markup minifier. +//! +//! This is a fork of the `html5minify` crate. +//! https://github.com/martingallagher/html5minify + +use std::{cell::RefCell, io, rc::Rc, str}; + +use html5ever::{ + parse_document, + tendril::{fmt::UTF8, Tendril, TendrilSink}, + Attribute, ParseOpts, QualName, +}; +use markup5ever_rcdom::{Node, NodeData, RcDom}; + +/// Defines the minify trait. +#[allow(dead_code)] +pub(crate) trait Minify { + /// Minifies the source returning the minified HTML5. + /// + /// # Errors + /// + /// Will return `Err` if unable to read from the input reader or unable to + /// write to the output writer. + fn minify(&self) -> Result, io::Error>; +} + +/// Minifies the HTML input to the destination writer. +/// Outputs HTML5; non-HTML5 input will be transformed to HTML5. +/// +/// # Errors +/// +/// Will return `Err` if unable to read from the input reader or unable to write +/// to the output writer. +#[inline] +#[allow(dead_code)] +pub(crate) fn minify(mut r: &mut R, w: &mut W) -> io::Result<()> { + Minifier::new(w).minify(&mut r) +} + +impl Minify for T +where + T: AsRef<[u8]>, +{ + #[inline] + fn minify(&self) -> Result, io::Error> { + let mut minified = vec![]; + + minify(&mut self.as_ref(), &mut minified)?; + + Ok(minified) + } +} + +/// Minifier implementation for `io::Write`. +#[allow(clippy::struct_excessive_bools)] +pub struct Minifier<'a, W: io::Write> { + w: &'a mut W, + omit_doctype: bool, + collapse_whitespace: bool, + preserve_comments: bool, + preceding_whitespace: bool, +} + +/// Holds node positional context. +struct Context<'a> { + parent: &'a Node, + parent_context: Option<&'a Context<'a>>, + left: Option<&'a [Rc]>, + right: Option<&'a [Rc]>, +} + +impl<'a> Context<'a> { + /// Determine whether to trim whitespace. + /// Uses naive HTML5 whitespace collapsing rules. + fn trim(&self, preceding_whitespace: bool) -> (bool, bool) { + (preceding_whitespace || self.trim_left(), self.trim_right()) + } + + fn trim_left(&self) -> bool { + self.left.map_or_else( + || is_block_element(self.parent) || self.parent_trim_left(), + |siblings| { + siblings + .iter() + .rev() + .find_map(Self::is_block_element) + .unwrap_or_else(|| self.parent_trim_left()) + }, + ) + } + + fn parent_trim_left(&self) -> bool { + self.parent_context.map_or(true, Context::trim_left) + } + + fn trim_right(&self) -> bool { + self.right.map_or(true, |siblings| { + siblings + .iter() + .find_map(Self::is_block_element) + .unwrap_or(true) + }) + } + + fn next_element(&self) -> Option<&Rc> { + self.right.and_then(|siblings| { + siblings + .iter() + .find(|node| matches!(node.data, NodeData::Element { .. })) + }) + } + + fn is_block_element(node: &Rc) -> Option { + if let NodeData::Element { name, .. } = &node.data { + Some(is_block_element_name(name.local.as_ref())) + } else { + None + } + } +} + +impl<'a, W> Minifier<'a, W> +where + W: io::Write, +{ + /// Creates a new `Minifier` instance. + #[inline] + pub fn new(w: &'a mut W) -> Self { + Self { + w, + omit_doctype: false, + collapse_whitespace: true, + preserve_comments: false, + preceding_whitespace: false, + } + } + + /// Collapse whitespace between elements and in text when whitespace isn't preserved by default. + /// Enabled by default. + #[inline] + #[allow(dead_code)] + pub fn collapse_whitespace(&mut self, collapse: bool) -> &mut Self { + self.collapse_whitespace = collapse; + self + } + + /// Omit writing the HTML5 doctype. + /// Disabled by default. + #[inline] + #[allow(dead_code)] + pub fn omit_doctype(&mut self, omit: bool) -> &mut Self { + self.omit_doctype = omit; + self + } + + /// Preserve HTML comments. + /// Disabled by default. + #[inline] + #[allow(dead_code)] + pub fn preserve_comments(&mut self, preserve: bool) -> &mut Self { + self.preserve_comments = preserve; + self + } + + /// Minifies the given reader input. + /// + /// # Errors + /// + /// Will return `Err` if unable to write to the output writer. + #[inline] + #[allow(dead_code)] + pub fn minify(&mut self, mut r: &mut R) -> io::Result<()> { + let dom = parse_document(RcDom::default(), ParseOpts::default()) + .from_utf8() + .read_from(&mut r)?; + + if !self.omit_doctype { + self.w.write_all(b"")?; + } + + self.minify_node(&None, &dom.document) + } + + fn minify_node<'b>(&mut self, ctx: &'b Option, node: &'b Node) -> io::Result<()> { + match &node.data { + NodeData::Text { contents } => { + // Check if whitespace collapsing disabled + let contents = contents.borrow(); + let contents = contents.as_ref(); + + if !self.collapse_whitespace { + return self.w.write_all(contents.as_bytes()); + } + + // Check if parent is whitespace preserving element or contains code ( ", + "", + true, + false, + ), + ( + " ", + "", + true, + false, + ), + ("

A", "

A", true, false), + ("

A", "

A", true, false), + // Retain whitespace, whitespace before

+ ( + "

A ", + "

A ", + false, + false, + ), + // Retain whitespace, touching

+ ("

A", "

A", false, false), + // Comments ignored + ("

A", "

A", false, false), + // Comments preserved + ( + "

A", + "

A", + false, + true, + ), + // Retain end tag if touching inline element + ("

Some text

", "

Some text

", false, false), + ] { + let mut w = vec![]; + let mut minifier = Minifier::new(&mut w); + minifier + .omit_doctype(true) + .collapse_whitespace(collapse_whitespace) + .preserve_comments(preserve_comments); + minifier.minify(&mut input.as_bytes()).unwrap(); + + let s = str::from_utf8(&w).unwrap(); + + assert_eq!(expected, s); + } + } +} diff --git a/crates/ui/src/text/mod.rs b/crates/ui/src/text/mod.rs index 7156ccc4..ddbbf9c1 100644 --- a/crates/ui/src/text/mod.rs +++ b/crates/ui/src/text/mod.rs @@ -1,5 +1,6 @@ mod element; mod html; +mod html5minify; mod markdown; mod text_view; mod utils;