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 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]+)/, '&amp;\1')
335
+ value = (attr.value || '').gsub('&', '&amp;')
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('"', '&#34;')}\""
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:
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squoosh
4
+ # The version of squoosh.
5
+ VERSION = '0.2.0'
6
+ end
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: []