squoosh 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []