feat(html/minifier): Compress CSS (#4973)

This commit is contained in:
Alexander Akait 2022-06-15 07:04:57 +03:00 committed by GitHub
parent 94e553da3b
commit 3e6c0f567a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 184 additions and 172 deletions

3
Cargo.lock generated
View File

@ -3966,6 +3966,9 @@ dependencies = [
"serde_json",
"swc_atoms",
"swc_common",
"swc_css_codegen",
"swc_css_minifier",
"swc_css_parser",
"swc_html_ast",
"swc_html_codegen",
"swc_html_parser",

View File

@ -19,7 +19,7 @@ mod emit;
mod list;
pub mod writer;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy)]
pub struct CodegenConfig {
pub minify: bool,
}

View File

@ -20,6 +20,9 @@ serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61"
swc_atoms = { version = "0.2.9", path = "../swc_atoms" }
swc_common = { version = "0.18.0", path = "../swc_common" }
swc_css_codegen = { version = "0.103.0", path = "../swc_css_codegen" }
swc_css_parser = { version = "0.102.0", path = "../swc_css_parser" }
swc_css_minifier = { version = "0.68.0", path = "../swc_css_minifier" }
swc_html_ast = { version = "0.10.0", path = "../swc_html_ast" }
swc_html_visit = { version = "0.10.0", path = "../swc_html_visit" }

View File

@ -2,12 +2,16 @@
use serde_json::Value;
use swc_atoms::{js_word, JsWord};
use swc_common::collections::AHashSet;
use swc_common::{collections::AHashSet, sync::Lrc, FileName, FilePathMapping, SourceMap};
use swc_css_codegen::{
writer::basic::{BasicCssWriter, BasicCssWriterConfig},
CodeGenerator, CodegenConfig, Emit,
};
use swc_css_parser::parse_file;
use swc_html_ast::*;
use swc_html_visit::{VisitMut, VisitMutWith};
use crate::option::{CollapseWhitespaces, MinifyOptions};
pub mod option;
static HTML_BOOLEAN_ATTRIBUTES: &[&str] = &[
@ -251,6 +255,7 @@ enum MetaElementContentType {
enum TextChildrenType {
Json,
Css,
}
#[inline(always)]
@ -278,6 +283,7 @@ struct Minifier {
remove_empty_attributes: bool,
collapse_boolean_attributes: bool,
minify_css: bool,
}
impl Minifier {
@ -559,6 +565,34 @@ impl Minifier {
collapsed
}
// TODO source map url output?
fn minify_css(&self, data: String) -> Option<String> {
let mut errors: Vec<_> = vec![];
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let fm = cm.new_source_file(FileName::Anon, data);
let mut stylesheet = match parse_file(&fm, Default::default(), &mut errors) {
Ok(stylesheet) => stylesheet,
_ => return None,
};
// Avoid compress potential invalid CSS
if !errors.is_empty() {
return None;
}
swc_css_minifier::minify(&mut stylesheet);
let mut minified = String::new();
let wr = BasicCssWriter::new(&mut minified, None, BasicCssWriterConfig::default());
let mut gen = CodeGenerator::new(wr, CodegenConfig { minify: true });
gen.emit(&stylesheet).unwrap();
Some(minified)
}
}
impl VisitMut for Minifier {
@ -628,6 +662,34 @@ impl VisitMut for Minifier {
{
self.current_element_text_children_type = Some(TextChildrenType::Json);
}
"style" if self.minify_css => {
let mut type_attribute_value = None;
for attribute in &n.attributes {
if &*attribute.name == "type" && attribute.value.is_some() {
type_attribute_value = Some(
attribute
.value
.as_ref()
.unwrap()
.trim()
.to_ascii_lowercase(),
);
break;
}
}
if type_attribute_value.is_none()
|| type_attribute_value == Some("text/css".into())
{
self.current_element_text_children_type = Some(TextChildrenType::Css);
} else {
self.current_element_text_children_type = None;
}
self.meta_element_content_type = None;
}
"pre" if whitespace_minification_mode.is_some() => {
self.descendant_of_pre = true;
}
@ -856,17 +918,28 @@ impl VisitMut for Minifier {
fn visit_mut_text(&mut self, n: &mut Text) {
n.visit_mut_children_with(self);
if let Some(TextChildrenType::Json) = self.current_element_text_children_type {
let json = match serde_json::from_str::<Value>(&*n.data) {
Ok(json) => json,
_ => return,
};
let minified_json = match serde_json::to_string(&json) {
Ok(minified_json) => minified_json,
_ => return,
};
match self.current_element_text_children_type {
Some(TextChildrenType::Json) if n.data.len() > 0 => {
let json = match serde_json::from_str::<Value>(&*n.data) {
Ok(json) => json,
_ => return,
};
let minified = match serde_json::to_string(&json) {
Ok(minified_json) => minified_json,
_ => return,
};
n.data = minified_json.into()
n.data = minified.into()
}
Some(TextChildrenType::Css) if n.data.len() > 0 => {
let minified = match self.minify_css(n.data.to_string()) {
Some(minified) => minified,
None => return,
};
n.data = minified.into()
}
_ => {}
}
}
}
@ -885,5 +958,7 @@ pub fn minify(document: &mut Document, options: &MinifyOptions) {
remove_empty_attributes: options.remove_empty_attributes,
collapse_boolean_attributes: options.collapse_boolean_attributes,
minify_css: options.minify_css,
});
}

View File

@ -16,6 +16,8 @@ pub struct MinifyOptions {
pub remove_empty_attributes: bool,
#[serde(default = "true_by_default")]
pub collapse_boolean_attributes: bool,
#[serde(default = "true_by_default")]
pub minify_css: bool,
}
/// Implement default using serde.

View File

@ -1,19 +1,7 @@
<!doctype html><html lang=en><title>Document</title><style>
<!doctype html><html lang=en><title>Document</title><style>h1{color:red}p{color:blue}</style><style>h1{color:red}p{color:blue}</style><style>h1{color:red}p{color:blue}</style><style type=" ">
h1 {color:red;}
p {color:blue;}
</style><style>
h1 {color:red;}
p {color:blue;}
</style><style>
h1 {color:red;}
p {color:blue;}
</style><style type=" ">
h1 {color:red;}
p {color:blue;}
</style><style>
h1 {color:red;}
p {color:blue;}
</style><style type=unknown/unknown>
</style><style>h1{color:red}p{color:blue}</style><style type=unknown/unknown>
h1 {color:red;}
p {color:blue;}
</style><body>

View File

@ -15,8 +15,7 @@
<script><!--
alert(1);
--></script>
<style><!-- p { color: red } --></style>
<script>/*<![CDATA[*/alert(8)/*]]>*/</script>
<style>p{color:red}</style><script>/*<![CDATA[*/alert(8)/*]]>*/</script>
<script> /*
<![CDATA[ */
alert(10)

View File

@ -1,9 +1,4 @@
<!doctype html><html lang=en><meta charset=UTF-8><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><meta http-equiv=X-UA-Compatible content="ie=edge"><title>Document</title><link href=mobile.css rel=stylesheet media="screen and (max-width: 600px)"><style media="all and (max-width: 500px)">
p {
color: blue;
background-color: yellow;
}
</style><body>
<!doctype html><html lang=en><meta charset=UTF-8><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><meta http-equiv=X-UA-Compatible content="ie=edge"><title>Document</title><link href=mobile.css rel=stylesheet media="screen and (max-width: 600px)"><style media="all and (max-width: 500px)">p{color:blue;background-color:yellow}</style><body>
<img srcset="elva-fairy-480w.jpg 480w,elva-fairy-800w.jpg 800w" sizes="(max-width: 600px) 480px,800px" src=elva-fairy-800w.jpg alt="Elva dressed as a fairy">
<img srcset="elva-fairy-320w.jpg,elva-fairy-480w.jpg 1.5x,elva-fairy-640w.jpg 2x" src=elva-fairy-640w.jpg alt="Elva dressed as a fairy">
<picture>

View File

@ -0,0 +1,3 @@
{
"minifyCss": false
}

View File

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<style type="text/css">
a {
color: red;
}
</style>
<style type="TEXT/CSS">
a {
color: red;
}
</style>
<style>
a {
color: red;
}
</style>
<style type="not/css">
K e E p
</style>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!doctype html><html lang=en><title>Document</title><body>
<style>
a {
color: red;
}
</style>
<style>
a {
color: red;
}
</style>
<style>
a {
color: red;
}
</style>
<style type=not/css>
K e E p
</style>

View File

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<style type="text/css">
a {
color: red;
}
</style>
<style type="TEXT/CSS">
a {
color: red;
}
</style>
<style>
a {
color: red;
}
</style>
<style type="not/css">
K e E p
</style>
</body>
</html>

View File

@ -0,0 +1,5 @@
<!doctype html><html lang=en><title>Document</title><body>
<style>a{color:red}</style><style>a{color:red}</style><style>a{color:red}</style><style type=not/css>
K e E p
</style>

View File

@ -1,67 +1,4 @@
<!doctype html><meta charset=utf-8><title>Coding Horror</title><meta name=description content="a blog by Jeff Atwood on programming and human factors"><meta name=HandheldFriendly content=True><meta name=MobileOptimized content=320><meta name=viewport content="width=device-width,initial-scale=1.0"><meta name=google-site-verification content=sl0m9SU_4V0JcvjWlOX4dUFBR6VS2P4tlxjJMo0gphU><link rel=stylesheet href="https://blog.codinghorror.com/assets/css/screen.css?v=a86e63fc1d"><link rel=stylesheet href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700"><link rel=alternate type=application/rss+xml title="Coding Horror" href=https://blog.codinghorror.com/rss/><script src=https://code.jquery.com/jquery-2.2.3.min.js async></script><script src="https://blog.codinghorror.com/assets/js/video-resize.js?v=a86e63fc1d" async></script><meta name=description content="programming and human factors"><link rel=icon href=/favicon.png type=image/png><link rel=canonical href=https://blog.codinghorror.com/><meta name=referrer content=no-referrer-when-downgrade><link rel=next href=https://blog.codinghorror.com/page/2/><meta property=og:site_name content="Coding Horror"><meta property=og:type content=website><meta property=og:title content="Coding Horror"><meta property=og:description content="programming and human factors"><meta property=og:url content=https://blog.codinghorror.com/><meta name=twitter:card content=summary><meta name=twitter:title content="Coding Horror"><meta name=twitter:description content="programming and human factors"><meta name=twitter:url content=https://blog.codinghorror.com/><script type=application/ld+json>{"@context":"https://schema.org","@type":"WebSite","description":"programming and human factors","mainEntityOfPage":{"@id":"https://blog.codinghorror.com/","@type":"WebPage"},"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://blog.codinghorror.com/content/images/2014/Feb/coding_horror_official_logo_small.png"},"name":"Coding Horror","url":"https://blog.codinghorror.com/"},"url":"https://blog.codinghorror.com/"}</script><meta name=generator content="Ghost 4.48"><link rel=alternate type=application/rss+xml title="Coding Horror" href=https://blog.codinghorror.com/rss/><script defer src=https://unpkg.com/@tryghost/portal@~1.22.0/umd/portal.min.js data-ghost=https://blog.codinghorror.com/ crossorigin=anonymous></script><style id=gh-members-styles>.gh-post-upgrade-cta-content,
.gh-post-upgrade-cta {
display: flex;
flex-direction: column;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-align: center;
width: 100%;
color: #ffffff;
font-size: 16px;
}
.gh-post-upgrade-cta-content {
border-radius: 8px;
padding: 40px 4vw;
}
.gh-post-upgrade-cta h2 {
color: #ffffff;
font-size: 28px;
letter-spacing: -0.2px;
margin: 0;
padding: 0;
}
.gh-post-upgrade-cta p {
margin: 20px 0 0;
padding: 0;
}
.gh-post-upgrade-cta small {
font-size: 16px;
letter-spacing: -0.2px;
}
.gh-post-upgrade-cta a {
color: #ffffff;
cursor: pointer;
font-weight: 500;
box-shadow: none;
text-decoration: underline;
}
.gh-post-upgrade-cta a:hover {
color: #ffffff;
opacity: 0.8;
box-shadow: none;
text-decoration: underline;
}
.gh-post-upgrade-cta a.gh-btn {
display: block;
background: #ffffff;
text-decoration: none;
margin: 28px 0 0;
padding: 8px 18px;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
}
.gh-post-upgrade-cta a.gh-btn:hover {
opacity: 0.92;
}</style><script defer src="/public/cards.min.js?v=a86e63fc1d"></script><style>:root {--ghost-accent-color: #15171A;}</style><link rel=stylesheet href="/public/cards.min.css?v=a86e63fc1d"><body class=home-template> <header class=site-head> <div class=site-head-content> <a class=blog-logo href=https://blog.codinghorror.com><img src="https://blog.codinghorror.com/assets/images/codinghorror-app-icon.png?v=a86e63fc1d" alt="Coding Horror Logo" width=158 height=158></a> <h1 class=blog-title><a href=https://blog.codinghorror.com>Coding Horror</a></h1> <h2 class=blog-description>programming and human factors</h2> <div class=site-search> <script>(function() {
<!doctype html><meta charset=utf-8><title>Coding Horror</title><meta name=description content="a blog by Jeff Atwood on programming and human factors"><meta name=HandheldFriendly content=True><meta name=MobileOptimized content=320><meta name=viewport content="width=device-width,initial-scale=1.0"><meta name=google-site-verification content=sl0m9SU_4V0JcvjWlOX4dUFBR6VS2P4tlxjJMo0gphU><link rel=stylesheet href="https://blog.codinghorror.com/assets/css/screen.css?v=a86e63fc1d"><link rel=stylesheet href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700"><link rel=alternate type=application/rss+xml title="Coding Horror" href=https://blog.codinghorror.com/rss/><script src=https://code.jquery.com/jquery-2.2.3.min.js async></script><script src="https://blog.codinghorror.com/assets/js/video-resize.js?v=a86e63fc1d" async></script><meta name=description content="programming and human factors"><link rel=icon href=/favicon.png type=image/png><link rel=canonical href=https://blog.codinghorror.com/><meta name=referrer content=no-referrer-when-downgrade><link rel=next href=https://blog.codinghorror.com/page/2/><meta property=og:site_name content="Coding Horror"><meta property=og:type content=website><meta property=og:title content="Coding Horror"><meta property=og:description content="programming and human factors"><meta property=og:url content=https://blog.codinghorror.com/><meta name=twitter:card content=summary><meta name=twitter:title content="Coding Horror"><meta name=twitter:description content="programming and human factors"><meta name=twitter:url content=https://blog.codinghorror.com/><script type=application/ld+json>{"@context":"https://schema.org","@type":"WebSite","description":"programming and human factors","mainEntityOfPage":{"@id":"https://blog.codinghorror.com/","@type":"WebPage"},"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://blog.codinghorror.com/content/images/2014/Feb/coding_horror_official_logo_small.png"},"name":"Coding Horror","url":"https://blog.codinghorror.com/"},"url":"https://blog.codinghorror.com/"}</script><meta name=generator content="Ghost 4.48"><link rel=alternate type=application/rss+xml title="Coding Horror" href=https://blog.codinghorror.com/rss/><script defer src=https://unpkg.com/@tryghost/portal@~1.22.0/umd/portal.min.js data-ghost=https://blog.codinghorror.com/ crossorigin=anonymous></script><style id=gh-members-styles>.gh-post-upgrade-cta-content,.gh-post-upgrade-cta{display:flex;flex-direction:column;align-items:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;text-align:center;width:100%;color:#fff;font-size:16px}.gh-post-upgrade-cta-content{border-radius:8px;padding:40px 4vw}.gh-post-upgrade-cta h2{color:#fff;font-size:28px;letter-spacing:-.2px;margin:0;padding:0}.gh-post-upgrade-cta p{margin:20px 0 0;padding:0}.gh-post-upgrade-cta small{font-size:16px;letter-spacing:-.2px}.gh-post-upgrade-cta a{color:#fff;cursor:pointer;font-weight:500;box-shadow:none;text-decoration:underline}.gh-post-upgrade-cta a:hover{color:#fff;opacity:.8;box-shadow:none;text-decoration:underline}.gh-post-upgrade-cta a.gh-btn{display:block;background:#fff;text-decoration:none;margin:28px 0 0;padding:8px 18px;border-radius:4px;font-size:16px;font-weight:600}.gh-post-upgrade-cta a.gh-btn:hover{opacity:.92}</style><script defer src="/public/cards.min.js?v=a86e63fc1d"></script><style>:root{--ghost-accent-color:#15171A}</style><link rel=stylesheet href="/public/cards.min.css?v=a86e63fc1d"><body class=home-template> <header class=site-head> <div class=site-head-content> <a class=blog-logo href=https://blog.codinghorror.com><img src="https://blog.codinghorror.com/assets/images/codinghorror-app-icon.png?v=a86e63fc1d" alt="Coding Horror Logo" width=158 height=158></a> <h1 class=blog-title><a href=https://blog.codinghorror.com>Coding Horror</a></h1> <h2 class=blog-description>programming and human factors</h2> <div class=site-search> <script>(function() {
var gcse = document.createElement('script');
gcse.type = 'text/javascript';
gcse.async = true;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long