squoosh 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/lib/squoosh.rb +608 -0
- data/lib/squoosh/version.rb +6 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6ad9840011bc3d7f13d58596a2b77d545745710bca32def00d78ddc9cc9f1989
|
4
|
+
data.tar.gz: c1a1a225d47047f35c5fbf69a05e4543a2f9368eb2c18a97390175c263b2a8d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9e9bcac78e2efebc699be79cd3eb17cc359d4a228677d0fa8bb552e410fcfcffdaadfe5e5585ba23bb87fbad677adeb4039673c9623c1ba401e0513b367abab7
|
7
|
+
data.tar.gz: '098de2bd1800df542f1f1e998aaf0e1fa5247fbec0214f7b4fbbd39677b697e1adfa15d1a5a8a5f01cf516da17d2deb29fce2a79c8355878278a014dc07a3244'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016, 2018 Stephen Checkoway
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Squoosh
|
2
|
+
|
3
|
+
Minifies HTML, JavaScript, and CSS, including inline JavaScript and CSS.
|
4
|
+
|
5
|
+
CSS minification is handled by [Sass](http://www.rubydoc.info/gems/sass)
|
6
|
+
whereas JavaScript minification is handled by
|
7
|
+
[Uglifier](http://www.rubydoc.info/gems/uglifier) which requires node.js.
|
8
|
+
|
9
|
+
HTML minification is handled as follows. First, an HTML 5 (which really means
|
10
|
+
the [WHATWG HTML](https://html.spec.whatwg.org/multipage/) living standard)
|
11
|
+
parser constructs a DOM as specified by the standard. Next, semantically
|
12
|
+
meaningless [inter-element whitespace
|
13
|
+
nodes](https://html.spec.whatwg.org/multipage/dom.html#inter-element-whitespace)
|
14
|
+
are removed from the DOM and semantically meaningfull runs of whitespace are
|
15
|
+
compressed to single spaces, except in `pre`, `textarea`, and
|
16
|
+
[foreign](https://html.spec.whatwg.org/multipage/syntax.html#elements-2) elements.
|
17
|
+
Then, inline JavaScript and CSS are compressed using Sass and Uglifier.
|
18
|
+
Finally, the DOM is serialized, compressing
|
19
|
+
[attributes](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2)
|
20
|
+
where possible and omitting [optional start and end
|
21
|
+
tags](https://html.spec.whatwg.org/multipage/syntax.html#optional-tags) where
|
22
|
+
possible.
|
23
|
+
|
24
|
+
Unlike some other HTML minifiers, Squoosh uses neither Java nor [regular
|
25
|
+
expressions](http://stackoverflow.com/a/1732454) to parse HTML.
|
26
|
+
|
27
|
+
## Limitations
|
28
|
+
Squoosh will not minify
|
29
|
+
|
30
|
+
- HTML 4 and earlier;
|
31
|
+
- XHTML, any version;
|
32
|
+
- [MathML](https://www.w3.org/TR/MathML3/) elements; nor
|
33
|
+
- [SVG](https://www.w3.org/TR/SVG11/) elements.
|
34
|
+
|
35
|
+
## Installation
|
36
|
+
|
37
|
+
Add this line to your application's Gemfile:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
gem 'squoosh'
|
41
|
+
```
|
42
|
+
|
43
|
+
And then execute:
|
44
|
+
|
45
|
+
$ bundle install
|
46
|
+
|
47
|
+
Or install it yourself as:
|
48
|
+
|
49
|
+
$ gem install squoosh
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
The three basic minification functions are
|
54
|
+
|
55
|
+
- `Squoosh::minify_html`
|
56
|
+
- `Squoosh::minify_js`
|
57
|
+
- `Squoosh::minify_css`
|
58
|
+
|
59
|
+
The `Squoosher` class caches (in memory) minified JavaScript and CSS which can
|
60
|
+
significantly speed up minifying HTML with repeated scripts and style sheets.
|
61
|
+
|
62
|
+
### Using with Jekyll
|
63
|
+
|
64
|
+
Create a `_plugins/squoosh.rb` file with the contents
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require 'squoosh'
|
68
|
+
|
69
|
+
Jekyll::Hooks.register [:documents, :pages], :post_render, priority: :high do |doc|
|
70
|
+
case File.extname(doc.destination('./'))
|
71
|
+
when '.html', '.htm'
|
72
|
+
doc.output = Squoosh::minify_html doc.output
|
73
|
+
when '.js'
|
74
|
+
doc.output = Squoosh::minify_js doc.output
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
CSS minification could be handled similarly, or `foo.css` files could simply
|
80
|
+
be renamed to `foo.scss` and
|
81
|
+
|
82
|
+
```yaml
|
83
|
+
sass:
|
84
|
+
style: compressed
|
85
|
+
```
|
86
|
+
|
87
|
+
added to `_config.yml`.
|
88
|
+
|
89
|
+
## Contributing
|
90
|
+
|
91
|
+
Bug reports and pull requests are welcome on GitHub at
|
92
|
+
https://github.com/stevecheckoway/squoosh.
|
93
|
+
|
94
|
+
|
95
|
+
## License
|
96
|
+
|
97
|
+
The gem is available as open source under the terms of the [MIT
|
98
|
+
License](http://opensource.org/licenses/MIT).
|
99
|
+
|
data/lib/squoosh.rb
ADDED
@@ -0,0 +1,608 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'squoosh/version'
|
4
|
+
require 'nokogumbo'
|
5
|
+
require 'sass'
|
6
|
+
require 'set'
|
7
|
+
require 'uglifier'
|
8
|
+
|
9
|
+
# @author Stephen Checkoway <s@pahtak.org>
|
10
|
+
# Minify HTML, JavaScript, and CSS.
|
11
|
+
#
|
12
|
+
# *Examples*
|
13
|
+
#
|
14
|
+
# html = <<-EOF
|
15
|
+
# <!DOCTYPE html>
|
16
|
+
# <html>
|
17
|
+
# <head>
|
18
|
+
# <!-- Set the title -->
|
19
|
+
# <title>My fancy title!</title>
|
20
|
+
# </head>
|
21
|
+
# <body>
|
22
|
+
# <p>Two</p>
|
23
|
+
# <p>paragraphs.</p>
|
24
|
+
# </body>
|
25
|
+
# </html>
|
26
|
+
# EOF
|
27
|
+
# compressed = Squoosh.minify_html(html)
|
28
|
+
# # "<!DOCTYPE html><title>My fancy title!</title><p>Two<p>paragraphs."
|
29
|
+
#
|
30
|
+
module Squoosh
|
31
|
+
# Minify HTML, JavaScript, and CSS using a single set of options. Minified
|
32
|
+
# versions of the JavaScript and CSS encountered are cached to speed up
|
33
|
+
# minification when the same scripts or inline style sheets appear multiple
|
34
|
+
# times.
|
35
|
+
class Squoosher
|
36
|
+
# Default options for minifying.
|
37
|
+
#
|
38
|
+
# * ++remove_comments++ Remove all comments that aren't "loud"
|
39
|
+
# * ++omit_tags++ Omit unnecessary start and end tags
|
40
|
+
# * ++loud_comments++ Keep all comments matching this regex
|
41
|
+
# * ++minify_javascript++ Minify JavaScript <tt><script></tt> and inline
|
42
|
+
# JavaScript
|
43
|
+
# * ++minify_css++ Minify CSS <tt><style></tt> and inline CSS
|
44
|
+
# * ++uglifier_options++ Options to pass to
|
45
|
+
# {https://www.rubydoc.info/gems/uglifier Uglifier}
|
46
|
+
# * ++sass_options++ Options to pass to
|
47
|
+
# {http://sass-lang.com/documentation/file.SASS_REFERENCE.html#options
|
48
|
+
# Sass}
|
49
|
+
DEFAULT_OPTIONS = {
|
50
|
+
remove_comments: true,
|
51
|
+
omit_tags: true,
|
52
|
+
compress_spaces: true,
|
53
|
+
loud_comments: /\A\s*!/,
|
54
|
+
minify_javascript: true,
|
55
|
+
minify_css: true,
|
56
|
+
uglifier_options: {
|
57
|
+
output: {
|
58
|
+
ascii_only: false,
|
59
|
+
comments: /\A!/
|
60
|
+
}
|
61
|
+
},
|
62
|
+
sass_options: {
|
63
|
+
style: :compressed
|
64
|
+
}
|
65
|
+
}.freeze
|
66
|
+
|
67
|
+
# Create a new instance of ++Squoosher++.
|
68
|
+
#
|
69
|
+
# @param options [Hash] options to override the default options
|
70
|
+
def initialize(options = {})
|
71
|
+
options.each do |key, _val|
|
72
|
+
unless DEFAULT_OPTIONS.include?(key)
|
73
|
+
raise ArgumentError, "Invalid option `#{key}'"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
77
|
+
@js_cache = {}
|
78
|
+
@css_cache = {}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Minify HTML and inline JavaScript and CSS.
|
82
|
+
#
|
83
|
+
# If the ++content++ does not start with an HTML document type
|
84
|
+
# <tt><!DOCTYPE html></tt>, then ++content++ is returned unchanged.
|
85
|
+
#
|
86
|
+
# @param content [String] the HTML to minify
|
87
|
+
# @return [String] the minified HTML
|
88
|
+
def minify_html(content)
|
89
|
+
doc = Nokogiri.HTML5(content)
|
90
|
+
return content unless doc&.internal_subset&.html5_dtd?
|
91
|
+
|
92
|
+
remove_comments(doc) if @options[:remove_comments]
|
93
|
+
compress_javascript(doc) if @options[:minify_javascript]
|
94
|
+
compress_css(doc) if @options[:minify_css]
|
95
|
+
doc.children.each { |c| compress_spaces(c) } if @options[:compress_spaces]
|
96
|
+
doc.children.map { |node| stringify_node(node) }.join
|
97
|
+
end
|
98
|
+
|
99
|
+
# Minify CSS using Sass.
|
100
|
+
#
|
101
|
+
# @param content [String] the CSS to minify
|
102
|
+
# @return [String] the minified CSS
|
103
|
+
def minify_css(content)
|
104
|
+
@css_cache[content] ||= begin
|
105
|
+
root = Sass::SCSS::CssParser.new(content, nil, nil).parse
|
106
|
+
root.options = @options[:sass_options]
|
107
|
+
root.render.rstrip
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Minify JavaScript using Uglify.
|
112
|
+
#
|
113
|
+
# @param content [String] the JavaScript to minify
|
114
|
+
# @return [String] the minified JavaScript
|
115
|
+
def minify_js(content)
|
116
|
+
@js_cache[content] ||= \
|
117
|
+
Uglifier.compile(content, @options[:uglifier_options])
|
118
|
+
end
|
119
|
+
|
120
|
+
# Element kinds
|
121
|
+
VOID_ELEMENTS = Set.new(%w[area base br col embed hr img input
|
122
|
+
keygen link meta param source track wbr]).freeze
|
123
|
+
RAW_TEXT_ELEMENTS = Set.new(%w[script style]).freeze
|
124
|
+
ESCAPABLE_RAW_TEXT_ELEMENTS = Set.new(%w[textarea title]).freeze
|
125
|
+
FOREIGN_ELEMENTS = Set.new(%w[math svg]).freeze
|
126
|
+
private_constant :VOID_ELEMENTS, :RAW_TEXT_ELEMENTS
|
127
|
+
private_constant :ESCAPABLE_RAW_TEXT_ELEMENTS, :FOREIGN_ELEMENTS
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def void_element?(node)
|
132
|
+
VOID_ELEMENTS.include? node.name
|
133
|
+
end
|
134
|
+
|
135
|
+
def raw_text_element?(node)
|
136
|
+
RAW_TEXT_ELEMENTS.include? node.name
|
137
|
+
end
|
138
|
+
|
139
|
+
def escapable_raw_text_element?(node)
|
140
|
+
ESCAPABLE_RAW_TEXT_ELEMENTS.include? node.name
|
141
|
+
end
|
142
|
+
|
143
|
+
def foreign_element?(node)
|
144
|
+
FOREIGN_ELEMENTS.include? node.name
|
145
|
+
end
|
146
|
+
|
147
|
+
def normal_element?(node)
|
148
|
+
!void_element?(node) &&
|
149
|
+
!raw_text_element?(node) &&
|
150
|
+
!escapable_raw_text_element?(node) &&
|
151
|
+
!foreign_element?(node)
|
152
|
+
end
|
153
|
+
|
154
|
+
HTML_WHITESPACE = "\t\n\f\r "
|
155
|
+
private_constant :HTML_WHITESPACE
|
156
|
+
|
157
|
+
def inter_element_whitespace?(node)
|
158
|
+
return false unless node.text?
|
159
|
+
|
160
|
+
node.content.each_char.all? { |c| HTML_WHITESPACE.include? c }
|
161
|
+
end
|
162
|
+
|
163
|
+
PHRASING_CONTENT = Set.new(%w[a abbr area audio b bdi bdo br button canvas
|
164
|
+
cite code data datalist del dfn em embed i
|
165
|
+
iframe img input ins kbd keygen label link
|
166
|
+
map mark math meta meter noscript object
|
167
|
+
output picture progress q ruby s samp script
|
168
|
+
select slot small span strong sub sup svg
|
169
|
+
template textarea time u var video
|
170
|
+
wbr]).freeze
|
171
|
+
private_constant :PHRASING_CONTENT
|
172
|
+
|
173
|
+
def phrasing_content?(node)
|
174
|
+
name = node.name
|
175
|
+
PHRASING_CONTENT.include?(name)
|
176
|
+
end
|
177
|
+
|
178
|
+
def remove_comments(doc)
|
179
|
+
doc.xpath('//comment()').each do |node|
|
180
|
+
next if preserve_comment?(node)
|
181
|
+
|
182
|
+
prev_node = node.previous_sibling
|
183
|
+
next_node = node.next_sibling
|
184
|
+
node.unlink
|
185
|
+
if prev_node&.text? && next_node&.text?
|
186
|
+
prev_node.content += next_node.content
|
187
|
+
next_node.unlink
|
188
|
+
end
|
189
|
+
end
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
def preserve_comment?(node)
|
194
|
+
content = node.content
|
195
|
+
return true if content.start_with? '[if '
|
196
|
+
return true if /\A\s*!/ =~ content
|
197
|
+
|
198
|
+
# Support other retained comments?
|
199
|
+
false
|
200
|
+
end
|
201
|
+
|
202
|
+
EVENT_HANDLERS_XPATH = (
|
203
|
+
# Select all attribute nodes whose names start with "on";
|
204
|
+
'//@*[starts-with(name(),"on")]' +
|
205
|
+
# and are not descendants of foreign elements
|
206
|
+
'[not(ancestor::math or ancestor::svg)]' +
|
207
|
+
# and
|
208
|
+
'[' +
|
209
|
+
# whose names are any of
|
210
|
+
%w[abort cancel canplay canplaythrough change click close contextmenu
|
211
|
+
cuechange dblclick drag dragend dragenter dragexit dragleave
|
212
|
+
dragover dragstart drop durationchange emptied ended input invalid
|
213
|
+
keydown keypress keyup loadeddata loadedmetadata loadend loadstart
|
214
|
+
mousedown mouseenter mouseleave mousemove mouseout mouseover
|
215
|
+
mouseup wheel pause play playing progress ratechange reset seeked
|
216
|
+
seeking select show stalled submit suspend timeupdate toggle
|
217
|
+
volumechange waiting cut copy paste blur error focus load resize
|
218
|
+
scroll].map { |n| "name()=\"on#{n}\"" }.join(' or ') +
|
219
|
+
# or whose parent is body or frameset
|
220
|
+
' or (parent::body or parent::frameset)' +
|
221
|
+
# and
|
222
|
+
' and (' +
|
223
|
+
# whose names are any of
|
224
|
+
%w[afterprint beforeprint beforeunload hashchange languagechange
|
225
|
+
message offline online pagehide pageshow popstate
|
226
|
+
rejectionhandled storage unhandledrejection
|
227
|
+
unload].map { |n| "name()=\"on#{n}\"" }.join(' or ') +
|
228
|
+
')' \
|
229
|
+
']'
|
230
|
+
).freeze
|
231
|
+
private_constant :EVENT_HANDLERS_XPATH
|
232
|
+
|
233
|
+
def compress_javascript(doc)
|
234
|
+
# Compress script elements.
|
235
|
+
doc.xpath('//script[not(ancestor::math or ancestor::svg)]').each do |node|
|
236
|
+
type = node['type']&.downcase
|
237
|
+
next unless type.nil? || type == 'text/javascript'
|
238
|
+
|
239
|
+
node.content = minify_js node.content
|
240
|
+
end
|
241
|
+
# Compress event handlers.
|
242
|
+
doc.xpath(EVENT_HANDLERS_XPATH).each do |attr|
|
243
|
+
attr.content = minify_js(attr.content)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def compress_css(doc)
|
248
|
+
# Compress style elements.
|
249
|
+
doc.xpath('//style[not(ancestor::math or ancestor::svg)]').each do |node|
|
250
|
+
type = node['type']&.downcase
|
251
|
+
next unless type.nil? || type == 'text/css'
|
252
|
+
|
253
|
+
node.content = minify_css node.content
|
254
|
+
end
|
255
|
+
# Compress style attributes
|
256
|
+
doc.xpath('//@style[not(ancestor::math or ancestor::svg)]').each do |node|
|
257
|
+
elm_type = node.parent.name
|
258
|
+
css = "#{elm_type}{#{node.content}}"
|
259
|
+
node.content = minify_css(css)[elm_type.length + 1..-2]
|
260
|
+
end
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
|
264
|
+
def compress_spaces(node)
|
265
|
+
if node.text?
|
266
|
+
if text_node_removable? node
|
267
|
+
node.unlink
|
268
|
+
else
|
269
|
+
content = node.content
|
270
|
+
content.gsub!(/[ \t\n\r\f]+/, ' ')
|
271
|
+
content.lstrip! if trim_left? node
|
272
|
+
content.rstrip! if trim_right? node
|
273
|
+
node.content = content
|
274
|
+
end
|
275
|
+
elsif node.element? &&
|
276
|
+
(node.name == 'pre' || node.name == 'textarea')
|
277
|
+
# Remove leading newline in pre and textarea tags unless there are two
|
278
|
+
# in a row.
|
279
|
+
if node.children[0]&.text?
|
280
|
+
content = node.children[0].content
|
281
|
+
if content.sub!(/\A\r\n?([^\r]|\z)/, '\1') ||
|
282
|
+
content.sub!(/\A\n([^\n]|\z)/, '\1')
|
283
|
+
node.children[0].content = content
|
284
|
+
end
|
285
|
+
end
|
286
|
+
elsif normal_element?(node) || node.name == 'title'
|
287
|
+
# Compress spaces in normal elements and title.
|
288
|
+
node.children.each { |c| compress_spaces c }
|
289
|
+
end
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
|
293
|
+
# Be conservative. If an element can be phrasing content, assume it is.
|
294
|
+
def text_node_removable?(node)
|
295
|
+
return false unless inter_element_whitespace?(node)
|
296
|
+
return false if phrasing_content?(node.parent)
|
297
|
+
|
298
|
+
prev_elm = node.previous_element
|
299
|
+
next_elm = node.next_element
|
300
|
+
prev_elm.nil? || !phrasing_content?(prev_elm) ||
|
301
|
+
next_elm.nil? || !phrasing_content?(next_elm)
|
302
|
+
end
|
303
|
+
|
304
|
+
def trim_left?(node)
|
305
|
+
prev_elm = node.previous_element
|
306
|
+
return !phrasing_content?(node.parent) if prev_elm.nil?
|
307
|
+
|
308
|
+
prev_elm.name == 'br'
|
309
|
+
end
|
310
|
+
|
311
|
+
def trim_right?(node)
|
312
|
+
next_elm = node.next_element
|
313
|
+
return !phrasing_content?(node.parent) if next_elm.nil?
|
314
|
+
|
315
|
+
next_elm.name == 'br'
|
316
|
+
end
|
317
|
+
|
318
|
+
def stringify_node(node)
|
319
|
+
return node.to_html(encoding: 'UTF-8') unless node.element?
|
320
|
+
|
321
|
+
output = StringIO.new
|
322
|
+
# Add start tag. 8.1.2.1
|
323
|
+
unless omit_start_tag? node
|
324
|
+
output << "<#{node.name}"
|
325
|
+
|
326
|
+
# Add attributes. 8.1.2.3
|
327
|
+
last_attr_unquoted = false
|
328
|
+
node.attributes.each do |name, attr|
|
329
|
+
last_attr_unquoted = false
|
330
|
+
# Make sure there are no character references.
|
331
|
+
# XXX: We should be able to compress a bit more by leaving bare & in
|
332
|
+
# some cases.
|
333
|
+
# value = (attr.value || '')
|
334
|
+
# value.gsub!(/&([a-zA-Z0-9]+;|#[0-9]+|#[xX][a-fA-F0-9]+)/, '&\1')
|
335
|
+
value = (attr.value || '').gsub('&', '&')
|
336
|
+
if value.empty?
|
337
|
+
output << ' ' + name
|
338
|
+
elsif /[\t\n\f\r "'`=<>]/ !~ value
|
339
|
+
last_attr_unquoted = true
|
340
|
+
output << " #{name}=#{value}"
|
341
|
+
elsif !value.include?('"')
|
342
|
+
output << " #{name}=\"#{value}\""
|
343
|
+
elsif !value.include?("'")
|
344
|
+
output << " #{name}='#{value}'"
|
345
|
+
else
|
346
|
+
# Contains both ' and ".
|
347
|
+
output << " #{name}=\"#{value.gsub('"', '"')}\""
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Close start tag.
|
352
|
+
if node_is_self_closing? node
|
353
|
+
output << ' ' if last_attr_unquoted
|
354
|
+
output << '/'
|
355
|
+
end
|
356
|
+
output << '>'
|
357
|
+
end
|
358
|
+
|
359
|
+
# Add content.
|
360
|
+
output << node.children.map { |c| stringify_node c }.join
|
361
|
+
|
362
|
+
# Add end tag. 8.1.2.2
|
363
|
+
output << "</#{node.name}>" unless omit_end_tag? node
|
364
|
+
output.string
|
365
|
+
end
|
366
|
+
|
367
|
+
def node_is_self_closing?(node)
|
368
|
+
foreign_element?(node) && node.children.empty?
|
369
|
+
end
|
370
|
+
|
371
|
+
def omit_start_tag?(node)
|
372
|
+
return false unless @options[:omit_tags]
|
373
|
+
return false unless node.attributes.empty?
|
374
|
+
|
375
|
+
case node.name
|
376
|
+
when 'html'
|
377
|
+
# An html element's start tag may be omitted if the first thing inside
|
378
|
+
# the html element is not a comment.
|
379
|
+
return node.children.empty? || !node.children[0].comment?
|
380
|
+
|
381
|
+
when 'head'
|
382
|
+
# A head element's start tag may be omitted if the element is empty,
|
383
|
+
# or if the first thing inside the head element is an element.
|
384
|
+
return node.children.empty? || node.children[0].element?
|
385
|
+
|
386
|
+
when 'body'
|
387
|
+
# A body element's start tag may be omitted if the element is empty,
|
388
|
+
# or if the first thing inside the body element is not a space
|
389
|
+
# character or a comment, except if the first thing inside the body
|
390
|
+
# element is a meta, link, script, style, or template element.
|
391
|
+
return true if node.children.empty?
|
392
|
+
|
393
|
+
c = node.children[0]
|
394
|
+
return !c.content.start_with?(' ') if c.text?
|
395
|
+
return false if c.comment?
|
396
|
+
|
397
|
+
return !c.element? ||
|
398
|
+
!%w[meta link script style template].include?(c.name)
|
399
|
+
|
400
|
+
when 'colgroup'
|
401
|
+
# A colgroup element's start tag may be omitted if the first thing
|
402
|
+
# inside the colgroup element is a col element, and if the element is
|
403
|
+
# not immediately preceded by another colgroup element whose end tag
|
404
|
+
# has been omitted. (It can't be omitted if the element is empty.)
|
405
|
+
return false if node.children.empty?
|
406
|
+
return false unless node.children[0].name == 'col'
|
407
|
+
|
408
|
+
prev_elm = node.previous_element
|
409
|
+
return prev_elm.nil? ||
|
410
|
+
prev_elm.name != 'col' ||
|
411
|
+
!omit_end_tag?(prev_elm)
|
412
|
+
|
413
|
+
when 'tbody'
|
414
|
+
# A tbody element's start tag may be omitted if the first thing inside
|
415
|
+
# the tbody element is a tr element, and if the element is not
|
416
|
+
# immediately preceded by a tbody, thead, or tfoot element whose end
|
417
|
+
# tag has been omitted. (It can't be omitted if the element is empty.)
|
418
|
+
return false if node.children.empty?
|
419
|
+
return false unless node.children[0].name == 'tr'
|
420
|
+
|
421
|
+
prev_elm = node.previous_element
|
422
|
+
return prev_elm.nil? ||
|
423
|
+
!%w[tbody thead tfoot].include?(prev_elm.name) ||
|
424
|
+
!omit_end_tag?(prev_elm)
|
425
|
+
end
|
426
|
+
false
|
427
|
+
end
|
428
|
+
|
429
|
+
def parent_contains_more_content?(node)
|
430
|
+
while (node = node.next_sibling)
|
431
|
+
next if node.comment?
|
432
|
+
next if node.processing_instruction?
|
433
|
+
next if inter_element_whitespace? node
|
434
|
+
|
435
|
+
return true
|
436
|
+
end
|
437
|
+
false
|
438
|
+
end
|
439
|
+
|
440
|
+
def omit_end_tag?(node)
|
441
|
+
return true if void_element? node
|
442
|
+
return false unless @options[:omit_tags]
|
443
|
+
return false if node.parent.name == 'noscript'
|
444
|
+
|
445
|
+
next_node = node.next_sibling
|
446
|
+
next_elm = node.next_element
|
447
|
+
case node.name
|
448
|
+
when 'html'
|
449
|
+
# An html element's end tag may be omitted if the html element is not
|
450
|
+
# immediately followed by a comment.
|
451
|
+
return next_node.nil? || !next_node.comment?
|
452
|
+
|
453
|
+
when 'head'
|
454
|
+
# A head element's end tag may be omitted if the head element is not
|
455
|
+
# immediately followed by a space character or a comment.
|
456
|
+
return next_node.nil? ||
|
457
|
+
(next_node.text? && !next_node.content.start_with?(' ')) ||
|
458
|
+
!next_node.comment?
|
459
|
+
|
460
|
+
when 'body'
|
461
|
+
# A body element's end tag may be omitted if the body element is not
|
462
|
+
# immediately followed by a comment.
|
463
|
+
return next_node.nil? || !next_node.comment?
|
464
|
+
|
465
|
+
when 'li'
|
466
|
+
# An li element's end tag may be omitted if the li element is
|
467
|
+
# immediately followed by another li element or if there is no more
|
468
|
+
# content in the parent element.
|
469
|
+
return next_elm&.name == 'li' || !parent_contains_more_content?(node)
|
470
|
+
|
471
|
+
when 'dt'
|
472
|
+
# A dt element's end tag may be omitted if the dt element is immediately
|
473
|
+
# followed by another dt element or a dd element.
|
474
|
+
return %w[dt dd].include? next_elm&.name
|
475
|
+
|
476
|
+
when 'dd'
|
477
|
+
# A dd element's end tag may be omitted if the dd element is immediately
|
478
|
+
# followed by another dd element or a dt element, or if there is no more
|
479
|
+
# content in the parent element.
|
480
|
+
return %w[dt dd].include?(next_elm&.name) ||
|
481
|
+
!parent_contains_more_content?(node)
|
482
|
+
|
483
|
+
when 'p'
|
484
|
+
# A p element's end tag may be omitted if the p element is immediately
|
485
|
+
# followed by an address, article, aside, blockquote, div, dl,
|
486
|
+
# fieldset, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr,
|
487
|
+
# main, nav, ol, p, pre, section, table, or ul, element, or if there
|
488
|
+
# is no more content in the parent element and the parent element is
|
489
|
+
# not an a element.
|
490
|
+
return true if %w[address article aside blockquote div dl
|
491
|
+
fieldset footer form h1 h2 h3 h4
|
492
|
+
h5 h6 header hgroup hr main nav ol
|
493
|
+
p pre section table ul].include? next_elm&.name
|
494
|
+
|
495
|
+
return node.parent.name != 'a' && !parent_contains_more_content?(node)
|
496
|
+
|
497
|
+
when 'rb', 'rt', 'rp'
|
498
|
+
# An rb element's end tag may be omitted if the rb element is
|
499
|
+
# immediately followed by an rb, rt, rtc or rp element, or if there is
|
500
|
+
# no more content in the parent element.
|
501
|
+
#
|
502
|
+
# An rt element's end tag may be omitted if the rt element is
|
503
|
+
# immediately followed by an rb, rt, rtc, or rp element, or if there
|
504
|
+
# is no more content in the parent element.
|
505
|
+
#
|
506
|
+
# An rp element's end tag may be omitted if the rp element is
|
507
|
+
# immediately followed by an rb, rt, rtc or rp element, or if there is
|
508
|
+
# no more content in the parent element.
|
509
|
+
return %w[rb rt rtc rp].include?(next_elm&.name) ||
|
510
|
+
!parent_contains_more_content?(node)
|
511
|
+
when 'rtc'
|
512
|
+
# An rtc element's end tag may be omitted if the rtc element is
|
513
|
+
# immediately followed by an rb, rtc or rp element, or if there is no
|
514
|
+
# more content in the parent element.
|
515
|
+
return %w[rb rtc rp].include?(next_elm&.name) ||
|
516
|
+
!parent_contains_more_content?(node)
|
517
|
+
|
518
|
+
when 'optgroup'
|
519
|
+
# An optgroup element's end tag may be omitted if the optgroup element
|
520
|
+
# is immediately followed by another optgroup element, or if there is
|
521
|
+
# no more content in the parent element.
|
522
|
+
return next_elm&.name == 'optgroup' ||
|
523
|
+
!parent_contains_more_content?(node)
|
524
|
+
|
525
|
+
when 'option'
|
526
|
+
# An option element's end tag may be omitted if the option element is
|
527
|
+
# immediately followed by another option element, or if it is
|
528
|
+
# immediately followed by an optgroup element, or if there is no more
|
529
|
+
# content in the parent element.
|
530
|
+
return %w[option optgroup].include?(next_elm&.name) ||
|
531
|
+
!parent_contains_more_content?(node)
|
532
|
+
|
533
|
+
when 'colgroup'
|
534
|
+
# A colgroup element's end tag may be omitted if the colgroup element is
|
535
|
+
# not immediately followed by a space character or a comment.
|
536
|
+
return next_node.nil? ||
|
537
|
+
(next_node.text? && !next_node.content.start_with(' ')) ||
|
538
|
+
!next_node.comment?
|
539
|
+
|
540
|
+
when 'thead'
|
541
|
+
# A thead element's end tag may be omitted if the thead element is
|
542
|
+
# immediately followed by a tbody or tfoot element.
|
543
|
+
return %w[tbody tfoot].include? next_elm&.name
|
544
|
+
|
545
|
+
when 'tbody'
|
546
|
+
# A tbody element's end tag may be omitted if the tbody element is
|
547
|
+
# immediately followed by a tbody or tfoot element, or if there is no
|
548
|
+
# more content in the parent element.
|
549
|
+
return %w[tbody tfoot].include?(next_elm&.name) ||
|
550
|
+
!parent_contains_more_content?(node)
|
551
|
+
|
552
|
+
when 'tfoot'
|
553
|
+
# A tfoot element's end tag may be omitted if the tfoot element is
|
554
|
+
# immediately followed by a tbody element, or if there is no more
|
555
|
+
# content in the parent element.
|
556
|
+
return next_elm&.name == 'tbody' ||
|
557
|
+
!parent_contains_more_content?(node)
|
558
|
+
|
559
|
+
when 'tr'
|
560
|
+
# A tr element's end tag may be omitted if the tr element is immediately
|
561
|
+
# followed by another tr element, or if there is no more content in the
|
562
|
+
# parent element.
|
563
|
+
return next_elm&.name == 'tr' ||
|
564
|
+
!parent_contains_more_content?(node)
|
565
|
+
|
566
|
+
when 'td', 'th'
|
567
|
+
# A td element's end tag may be omitted if the td element is immediately
|
568
|
+
# followed by a td or th element, or if there is no more content in the
|
569
|
+
# parent element.
|
570
|
+
#
|
571
|
+
# A th element's end tag may be omitted if the th element is immediately
|
572
|
+
# followed by a td or th element, or if there is no more content in the
|
573
|
+
# parent element.
|
574
|
+
return %w[td th].include?(next_elm&.name) ||
|
575
|
+
!parent_contains_more_content?(node)
|
576
|
+
end
|
577
|
+
false
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# Minify HTML convenience method.
|
582
|
+
#
|
583
|
+
# @param content [String] the HTML to minify
|
584
|
+
# @param options [Hash] options to override the ++Squoosher++ default options
|
585
|
+
# @return [String] the minified HTML
|
586
|
+
def self.minify_html(content, options = {})
|
587
|
+
Squoosher.new(options).minify_html content
|
588
|
+
end
|
589
|
+
|
590
|
+
# Minify CSS convenience method.
|
591
|
+
#
|
592
|
+
# @param content [String] the CSS to minify
|
593
|
+
# @param options [Hash] options to override the ++Squoosher++ default options
|
594
|
+
# @return [String] the minified CSS
|
595
|
+
def self.minify_css(content, options = {})
|
596
|
+
Squoosher.new(options).minify_css content
|
597
|
+
end
|
598
|
+
|
599
|
+
# Minify JavaScript convenience method.
|
600
|
+
#
|
601
|
+
# @param content [String] the JavaScript to minify
|
602
|
+
# @param options [Hash] options to override the ++Squoosher++ default options
|
603
|
+
# @return [String] the minified JavaScript
|
604
|
+
def self.minify_js(content, options = {})
|
605
|
+
Squoosher.new(options).minify_js content
|
606
|
+
end
|
607
|
+
end
|
608
|
+
# vim: set sw=2 sts=2 ts=8 expandtab:
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: squoosh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Checkoway
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-10-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: yard
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: nokogumbo
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sass
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.6'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.6'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: uglifier
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '4.1'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '4.1'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- s@pahtak.org
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- LICENSE.txt
|
147
|
+
- README.md
|
148
|
+
- lib/squoosh.rb
|
149
|
+
- lib/squoosh/version.rb
|
150
|
+
homepage: https://github.com/stevecheckoway/squoosh
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata:
|
154
|
+
bug_tracker_uri: https://github.com/stevecheckoway/squoosh/issues
|
155
|
+
changelog_uri: https://github.com/stevecheckoway/squoosh/blob/master/CHANGELOG.md
|
156
|
+
homepage_uri: https://github.com/stevecheckoway/squoosh
|
157
|
+
source_code_uri: https://github.com/stevecheckoway/squoosh
|
158
|
+
post_install_message:
|
159
|
+
rdoc_options: []
|
160
|
+
require_paths:
|
161
|
+
- lib
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '2.3'
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
requirements: []
|
173
|
+
rubyforge_project:
|
174
|
+
rubygems_version: 2.7.7
|
175
|
+
signing_key:
|
176
|
+
specification_version: 4
|
177
|
+
summary: Minify HTML/CSS/JavaScript files.
|
178
|
+
test_files: []
|