From 737a82df4086e20b4e9c40d40e527137c619216b Mon Sep 17 00:00:00 2001 From: Wilson Lin Date: Wed, 15 Dec 2021 15:29:06 +1100 Subject: [PATCH] Minify SVG element whitespace --- CHANGELOG.md | 4 + README.md | 2 +- rust/common/spec/tag/whitespace.rs | 133 +++++++++++++++++++++++++++-- rust/common/tests/mod.rs | 4 + rust/main/src/lib.rs | 9 +- rust/main/src/minify/content.rs | 4 +- rust/main/src/minify/element.rs | 5 ++ rust/onepass/src/unit/content.rs | 2 +- 8 files changed, 150 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0163466..e7afd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # minify-html changelog +## 0.8.0 + +- Minify whitespace in SVG elements. + ## 0.7.2 - Fix Node.js library build process on Windows. diff --git a/README.md b/README.md index 8ab4577..b4f1044 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ Remove any leading/trailing whitespace from any leading/trailing text nodes of a #### Element types -minify-html recognises elements based on one of a few ways it assumes they are used. By making these assumptions, it can apply optimal whitespace minification strategies. +minify-html assumes HTML and SVG elements are used in specific ways, based on standards and best practices. By making these assumptions, it can apply optimal whitespace minification strategies. If these assumptions do not hold, consider adjusting the HTML source or turning off whitespace minification. |Group|Elements|Expected children| |---|---|---| diff --git a/rust/common/spec/tag/whitespace.rs b/rust/common/spec/tag/whitespace.rs index 4c94956..7741c22 100644 --- a/rust/common/spec/tag/whitespace.rs +++ b/rust/common/spec/tag/whitespace.rs @@ -1,3 +1,4 @@ +use crate::common::spec::tag::ns::Namespace; use std::collections::HashMap; use lazy_static::lazy_static; @@ -44,14 +45,22 @@ static ROOT: &WhitespaceMinification = &WhitespaceMinification { trim: true, }; -static DEFAULT: &WhitespaceMinification = &WhitespaceMinification { +static DEFAULT_HTML: &WhitespaceMinification = &WhitespaceMinification { collapse: true, destroy_whole: false, trim: false, }; +// SVG 2 spec requires unknown elements to be treated like : +// https://www.w3.org/TR/SVG2/struct.html#UnknownElement. +static DEFAULT_SVG: &WhitespaceMinification = &WhitespaceMinification { + collapse: true, + destroy_whole: true, + trim: true, +}; + lazy_static! { - static ref TAG_WHITESPACE_MINIFICATION: HashMap<&'static [u8], &'static WhitespaceMinification> = { + static ref HTML_TAG_WHITESPACE_MINIFICATION: HashMap<&'static [u8], &'static WhitespaceMinification> = { let mut m = HashMap::<&'static [u8], &'static WhitespaceMinification>::new(); // Content tags. m.insert(b"address", CONTENT); @@ -164,20 +173,126 @@ lazy_static! { m }; + + static ref SVG_TAG_WHITESPACE_MINIFICATION: HashMap<&'static [u8], &'static WhitespaceMinification> = { + let mut m = HashMap::<&'static [u8], &'static WhitespaceMinification>::new(); + + // Content tags. + m.insert(b"desc", CONTENT); + m.insert(b"text", CONTENT); + m.insert(b"title", CONTENT); + + // Formatting tags. + m.insert(b"a", FORMATTING); + m.insert(b"altGlyph", FORMATTING); + m.insert(b"tspan", FORMATTING); + m.insert(b"textPath", FORMATTING); + m.insert(b"tref", FORMATTING); + + // Layout tags. + m.insert(b"altGlyphDef", LAYOUT); + m.insert(b"altGlyphItem", LAYOUT); + m.insert(b"animate", LAYOUT); + m.insert(b"animateColor", LAYOUT); + m.insert(b"animateMotion", LAYOUT); + m.insert(b"animateTransform", LAYOUT); + m.insert(b"circle", LAYOUT); + m.insert(b"clipPath", LAYOUT); + m.insert(b"cursor", LAYOUT); + m.insert(b"defs", LAYOUT); + m.insert(b"discard", LAYOUT); + m.insert(b"ellipse", LAYOUT); + m.insert(b"feBlend", LAYOUT); + m.insert(b"feColorMatrix", LAYOUT); + m.insert(b"feComponentTransfer", LAYOUT); + m.insert(b"feComposite", LAYOUT); + m.insert(b"feConvolveMatrix", LAYOUT); + m.insert(b"feDiffuseLighting", LAYOUT); + m.insert(b"feDisplacementMap", LAYOUT); + m.insert(b"feDistantLight", LAYOUT); + m.insert(b"feDropShadow", LAYOUT); + m.insert(b"feFlood", LAYOUT); + m.insert(b"feFuncA", LAYOUT); + m.insert(b"feFuncB", LAYOUT); + m.insert(b"feFuncG", LAYOUT); + m.insert(b"feFuncR", LAYOUT); + m.insert(b"feGaussianBlur", LAYOUT); + m.insert(b"feImage", LAYOUT); + m.insert(b"feMerge", LAYOUT); + m.insert(b"feMergeNode", LAYOUT); + m.insert(b"feMorphology", LAYOUT); + m.insert(b"feOffset", LAYOUT); + m.insert(b"fePointLight", LAYOUT); + m.insert(b"feSpecularLighting", LAYOUT); + m.insert(b"feSpotLight", LAYOUT); + m.insert(b"feTile", LAYOUT); + m.insert(b"feTurbulence", LAYOUT); + m.insert(b"filter", LAYOUT); + m.insert(b"font-face-format", LAYOUT); + m.insert(b"font-face-name", LAYOUT); + m.insert(b"font-face-src", LAYOUT); + m.insert(b"font-face-uri", LAYOUT); + m.insert(b"font-face", LAYOUT); + m.insert(b"font", LAYOUT); + m.insert(b"foreignObject", LAYOUT); + m.insert(b"g", LAYOUT); + m.insert(b"glyph", LAYOUT); + m.insert(b"glyphRef", LAYOUT); + m.insert(b"hatch", LAYOUT); + m.insert(b"hatchpath", LAYOUT); + m.insert(b"hkern", LAYOUT); + m.insert(b"image", LAYOUT); + m.insert(b"line", LAYOUT); + m.insert(b"linearGradient", LAYOUT); + m.insert(b"marker", LAYOUT); + m.insert(b"mask", LAYOUT); + m.insert(b"mesh", LAYOUT); + m.insert(b"meshgradient", LAYOUT); + m.insert(b"meshpatch", LAYOUT); + m.insert(b"meshrow", LAYOUT); + m.insert(b"metadata", LAYOUT); + m.insert(b"missing-glyph", LAYOUT); + m.insert(b"mpath", LAYOUT); + m.insert(b"path", LAYOUT); + m.insert(b"pattern", LAYOUT); + m.insert(b"polygon", LAYOUT); + m.insert(b"polyline", LAYOUT); + m.insert(b"radialGradient", LAYOUT); + m.insert(b"rect", LAYOUT); + m.insert(b"set", LAYOUT); + m.insert(b"solidcolor", LAYOUT); + m.insert(b"stop", LAYOUT); + m.insert(b"svg", LAYOUT); + m.insert(b"switch", LAYOUT); + m.insert(b"symbol", LAYOUT); + m.insert(b"use", LAYOUT); + m.insert(b"view", LAYOUT); + m.insert(b"vkern", LAYOUT); + + m + }; } pub fn get_whitespace_minification_for_tag( + ns: Namespace, // Use empty slice if root. tag_name: &[u8], descendant_of_pre: bool, ) -> &'static WhitespaceMinification { - if descendant_of_pre { - WHITESPACE_SENSITIVE - } else if tag_name.is_empty() { - ROOT - } else { - TAG_WHITESPACE_MINIFICATION + match ns { + Namespace::Html => { + if descendant_of_pre { + WHITESPACE_SENSITIVE + } else if tag_name.is_empty() { + ROOT + } else { + HTML_TAG_WHITESPACE_MINIFICATION + .get(tag_name) + .unwrap_or(&DEFAULT_HTML) + } + } + Namespace::Svg => SVG_TAG_WHITESPACE_MINIFICATION .get(tag_name) - .unwrap_or(&DEFAULT) + .unwrap_or(&DEFAULT_SVG), } } diff --git a/rust/common/tests/mod.rs b/rust/common/tests/mod.rs index 92aef82..bff50e7 100644 --- a/rust/common/tests/mod.rs +++ b/rust/common/tests/mod.rs @@ -29,6 +29,10 @@ fn test_collapse_destroy_whole_and_trim_whitespace() { b"", b"", ); + eval( + b" ", + b"", + ); // Tag names should be case insensitive. eval(b"", b""); } diff --git a/rust/main/src/lib.rs b/rust/main/src/lib.rs index 060591e..1e8f271 100644 --- a/rust/main/src/lib.rs +++ b/rust/main/src/lib.rs @@ -39,7 +39,14 @@ pub fn minify(src: &[u8], cfg: &Cfg) -> Vec { let mut code = Code::new(src); let parsed = parse_content(&mut code, Namespace::Html, EMPTY_SLICE, EMPTY_SLICE); let mut out = Vec::with_capacity(src.len()); - minify_content(cfg, &mut out, false, EMPTY_SLICE, parsed.children); + minify_content( + cfg, + &mut out, + Namespace::Html, + false, + EMPTY_SLICE, + parsed.children, + ); out } diff --git a/rust/main/src/minify/content.rs b/rust/main/src/minify/content.rs index 9d6cda0..f76d4e9 100644 --- a/rust/main/src/minify/content.rs +++ b/rust/main/src/minify/content.rs @@ -5,6 +5,7 @@ use crate::ast::{NodeData, ScriptOrStyleLang}; use crate::cfg::Cfg; use crate::common::gen::codepoints::TAG_NAME_CHAR; use crate::common::pattern::Replacer; +use crate::common::spec::tag::ns::Namespace; use crate::common::spec::tag::whitespace::{ get_whitespace_minification_for_tag, WhitespaceMinification, }; @@ -47,6 +48,7 @@ lazy_static! { pub fn minify_content( cfg: &Cfg, out: &mut Vec, + ns: Namespace, descendant_of_pre: bool, // Use empty slice if none. parent: &[u8], @@ -56,7 +58,7 @@ pub fn minify_content( collapse, destroy_whole, trim, - } = get_whitespace_minification_for_tag(parent, descendant_of_pre); + } = get_whitespace_minification_for_tag(ns, parent, descendant_of_pre); // TODO Document or fix: even though bangs/comments/etc. don't affect layout, we don't collapse/destroy-whole/trim combined text nodes across bangs/comments/etc., as that's too complex and is ambiguous about which nodes should whitespace be deleted from. let mut found_first_text_or_elem = false; diff --git a/rust/main/src/minify/element.rs b/rust/main/src/minify/element.rs index 773a27b..d2f8e61 100644 --- a/rust/main/src/minify/element.rs +++ b/rust/main/src/minify/element.rs @@ -105,6 +105,11 @@ pub fn minify_element( minify_content( cfg, out, + if tag_name == b"svg" { + Namespace::Svg + } else { + ns + }, descendant_of_pre || (ns == Namespace::Html && tag_name == b"pre"), tag_name, children, diff --git a/rust/onepass/src/unit/content.rs b/rust/onepass/src/unit/content.rs index 4ce977d..a18e659 100644 --- a/rust/onepass/src/unit/content.rs +++ b/rust/onepass/src/unit/content.rs @@ -64,7 +64,7 @@ pub fn process_content( collapse, destroy_whole, trim, - } = get_whitespace_minification_for_tag(proc.get_or_empty(parent), descendant_of_pre); + } = get_whitespace_minification_for_tag(ns, proc.get_or_empty(parent), descendant_of_pre); let handle_ws = collapse || destroy_whole || trim;