Minify SVG element whitespace

This commit is contained in:
Wilson Lin 2021-12-15 15:29:06 +11:00
parent 04707e5e06
commit 737a82df40
8 changed files with 150 additions and 13 deletions

View File

@ -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.

View File

@ -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|
|---|---|---|

View File

@ -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 <g>:
// 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),
}
}

View File

@ -29,6 +29,10 @@ fn test_collapse_destroy_whole_and_trim_whitespace() {
b"<ul> \n&#32;a<pre></pre> <pre></pre>b </ul>",
b"<ul>a<pre></pre><pre></pre>b</ul>",
);
eval(
b"<svg> <path> </path> <path> </path> </svg>",
b"<svg><path></path><path></path></svg>",
);
// Tag names should be case insensitive.
eval(b"<uL> \n&#32;a b </UL>", b"<ul>a b</ul>");
}

View File

@ -39,7 +39,14 @@ pub fn minify(src: &[u8], cfg: &Cfg) -> Vec<u8> {
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
}

View File

@ -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<u8>,
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;

View File

@ -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,

View File

@ -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;