diff --git a/.travis.yml b/.travis.yml index 75cc44e..2322a17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,11 @@ language: ruby rvm: - - 2.1.1 - - 2.0.0 + - 2.5 + - 2.4 + - 2.3 + - 2.2 + - 2.1 + - 2.0 - 1.9.3 - jruby-18mode # JRuby in 1.8 mode - jruby-19mode # JRuby in 1.9 mode diff --git a/CHANGELOG b/CHANGELOG index 2a0cf57..89d9778 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +0.3.1 - 23/09/2016 + Add option flag to disable "remove spaces inside tags" +0.3.0 - 09/09/2015 + Fix CDATA errors +0.2.0 - 32/03/2015 + Remove warnings in ruby 2.2 + Support for custom html templates + Disable remove_http_protocol by default in the middleware 0.1.2 - 04/09/2014 Preserve custom attributes similar to simple attributes, such as ng-disabled (fixes #12) 0.1.1 - 08/05/2014 diff --git a/README.md b/README.md index 9f933f7..0eca79f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Htmlcompressor provides tools to minify html code. It includes - HtmlCompressor::Compressor class which is a raw port of [google's htmlcompressor](http://code.google.com/p/htmlcompressor/) -- HtmlCompressor::Rack a rack middleware to compress html pages on the fly +- HtmlCompressor::Rack a rack middleware to compress html pages on the fly. +Beware that the compressor has proved to be too slow in many circunstances to be used for on the fly or real time execution. I encourage you to use it only for build-time optimisations or decrease the number of optimisations that are run at execution time. Please note that Htmlcompressor is still in alpha version and need some additional love. @@ -22,62 +23,92 @@ Using the compressor class is straightforward: The compressor ships with basic and safe default options that may be overwritten passing the options hash to the constructor: ```ruby - options = { - :enabled => true, - :remove_multi_spaces => true, - :remove_comments => true, - :remove_intertag_spaces => false, - :remove_quotes => false, - :compress_css => false, - :compress_javascript => false, - :simple_doctype => false, - :remove_script_attributes => false, - :remove_style_attributes => false, - :remove_link_attributes => false, - :remove_form_attributes => false, - :remove_input_attributes => false, - :remove_javascript_protocol => false, - :remove_http_protocol => false, - :remove_https_protocol => false, - :preserve_line_breaks => false, - :simple_boolean_attributes => false - } +options = { + :enabled => true, + :remove_spaces_inside_tags => true, + :remove_multi_spaces => true, + :remove_comments => true, + :remove_intertag_spaces => false, + :remove_quotes => false, + :compress_css => false, + :compress_javascript => false, + :simple_doctype => false, + :remove_script_attributes => false, + :remove_style_attributes => false, + :remove_link_attributes => false, + :remove_form_attributes => false, + :remove_input_attributes => false, + :remove_javascript_protocol => false, + :remove_http_protocol => false, + :remove_https_protocol => false, + :preserve_line_breaks => false, + :simple_boolean_attributes => false, + :compress_js_templates => false +} ``` -Using rack middleware is as easy as: +Htmlcompressor also ships a rack middleware that can be used with rails, sinatra or rack. + +**However keep in mind that compression is slow (especially when also compressing javascript or css) and can negatively impact the performance of your website. Take your measurements and adopt a different strategy if necessary** + +Using rack middleware (in rails) is as easy as: + +```ruby +config.middleware.use HtmlCompressor::Rack, options +``` + +And in sinatra: ```ruby - config.middleware.use HtmlCompressor::Rack, options +use HtmlCompressor::Rack, options ``` The middleware uses a little more aggressive options by default: ```ruby - options = { - :enabled => true, - :remove_multi_spaces => true, - :remove_comments => true, - :remove_intertag_spaces => false, - :remove_quotes => true, - :compress_css => false, - :compress_javascript => false, - :simple_doctype => false, - :remove_script_attributes => true, - :remove_style_attributes => true, - :remove_link_attributes => true, - :remove_form_attributes => false, - :remove_input_attributes => true, - :remove_javascript_protocol => true, - :remove_http_protocol => true, - :remove_https_protocol => false, - :preserve_line_breaks => false, - :simple_boolean_attributes => true - } +options = { + :enabled => true, + :remove_multi_spaces => true, + :remove_comments => true, + :remove_intertag_spaces => false, + :remove_quotes => true, + :compress_css => false, + :compress_javascript => false, + :simple_doctype => false, + :remove_script_attributes => true, + :remove_style_attributes => true, + :remove_link_attributes => true, + :remove_form_attributes => false, + :remove_input_attributes => true, + :remove_javascript_protocol => true, + :remove_http_protocol => false, + :remove_https_protocol => false, + :preserve_line_breaks => false, + :simple_boolean_attributes => true +} ``` Rails 2.3 users may need to add ```ruby - require 'htmlcompressor' +require 'htmlcompressor' +``` + +## Javascript template compression + +You can compress javascript templates that are present in the html. +Setting the `:compress_js_templates` options to `true` will by default compress the content of script tags marked with `type="text/x-jquery-tmpl"`. +For compressing other types of templates, you can pass a string (or an array of strings) containing the type: `:compress_js_templates => ['text/html']`. +Please note that activating template compression will disable the removal of quotes from attributes values, as this could lead to unexpected errors with compiled templates. + + +## Custom preservation rules + +If you need to define custom preservation rules, you can list regular expressions in the `preserve_patterns` option. For example, to preserve PHP blocks you might want to define: + +```ruby +options = { + :preserve_patterns => [/<\?php.*?\?>/im] +} ``` ## CSS and JavaScript Compression @@ -87,48 +118,46 @@ In order to minify in page javascript and css, you need to supply a compressor i A compressor can be `:yui` or `:closure` or any object that responds to `:compress`. E.g.: `compressed = compressor.compress(source)` ```ruby +class MyCompressor - class MyCompressor - - def compress(source) - return 'minified' - end - + def compress(source) + return 'minified' end - options = { - :compress_css => true, - :css_compressor => MyCompressor.new, - :compress_javascript => true, - :javascript_compressor => MyCompressor.new - } +end +options = { + :compress_css => true, + :css_compressor => MyCompressor.new, + :compress_javascript => true, + :javascript_compressor => MyCompressor.new +} ``` Please note that in order to use yui or closure compilers you need to manually add them to the Gemfile ```ruby - gem 'yui-compressor' +gem 'yui-compressor' - ... +... - options = { - :compress_javscript => true, - :javascript_compressor => :yui, - :compress_css => true - :css_compressor => :yui - } +options = { + :compress_javascript => true, + :javascript_compressor => :yui, + :compress_css => true, + :css_compressor => :yui +} ``` ```ruby - gem 'closure-compiler' +gem 'closure-compiler' - ... +... - options = { - :compress_javascript => true, - :javascript_compressor => :closure - } +options = { + :compress_javascript => true, + :javascript_compressor => :closure +} ``` ## Statistics diff --git a/htmlcompressor.gemspec b/htmlcompressor.gemspec index 73e80da..b39cc9c 100644 --- a/htmlcompressor.gemspec +++ b/htmlcompressor.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'yui-compressor', '~> 0.9' gem.add_development_dependency 'closure-compiler', '~> 1.1' - gem.add_development_dependency 'rake' + gem.add_development_dependency 'rake', '~> 10.3.2' gem.add_development_dependency 'minitest', '~> 5.0' gem.files = `git ls-files`.split($\) diff --git a/lib/htmlcompressor/compressor.rb b/lib/htmlcompressor/compressor.rb index e8a0275..4852a3e 100644 --- a/lib/htmlcompressor/compressor.rb +++ b/lib/htmlcompressor/compressor.rb @@ -107,6 +107,7 @@ class Compressor # default settings :remove_comments => true, :remove_multi_spaces => true, + :remove_spaces_inside_tags => true, # optional settings :javascript_compressor => :yui, @@ -128,15 +129,28 @@ class Compressor :preserve_line_breaks => false, :remove_surrounding_spaces => nil, - :preserve_patterns => nil, - :javascript_compressor => nil, - :css_compressor => nil + :preserve_patterns => nil } def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) + if @options[:compress_js_templates] + @options[:remove_quotes] = false + + js_template_types = [ 'text/x-jquery-tmpl' ] + + unless @options[:compress_js_templates].is_a? TrueClass + js_template_types << @options[:compress_js_templates] + js_template_types.flatten! + end + + @options[:js_template_types] = js_template_types + else + @options[:js_template_types] = [] + end + detect_external_compressors end @@ -314,7 +328,7 @@ def preserve_blocks(html, preBlocks, taBlocks, scriptBlocks, styleBlocks, eventB scriptBlocks << group_2 index += 1 group_1 + message_format(TEMP_SCRIPT_BLOCK, index) + group_3 - elsif type == 'text/x-jquery-tmpl' + elsif @options[:js_template_types].include?(type) # jquery template, ignore so it gets compressed with the rest of html match else @@ -371,6 +385,16 @@ def preserve_blocks(html, preBlocks, taBlocks, scriptBlocks, styleBlocks, eventB def return_blocks(html, preBlocks, taBlocks, scriptBlocks, styleBlocks, eventBlocks, condCommentBlocks, skipBlocks, lineBreakBlocks, userBlocks) + # put skip blocks back + html = html.gsub(TEMP_SKIP_PATTERN) do |match| + i = $1.to_i + if skipBlocks.size > i + skipBlocks[i] + else + '' + end + end + # put line breaks back if @options[:preserve_line_breaks] html = html.gsub(TEMP_LINE_BREAK_PATTERN) do |match| @@ -441,15 +465,6 @@ def return_blocks(html, preBlocks, taBlocks, scriptBlocks, styleBlocks, eventBlo end end - # put skip blocks back - html = html.gsub(TEMP_SKIP_PATTERN) do |match| - i = $1.to_i - if skipBlocks.size > i - skipBlocks[i] - else - '' - end - end # put user blocks back unless @options[:preserve_patterns].nil? @@ -514,7 +529,11 @@ def compress_javascript(source) end if javascript_compressor.nil? - raise MissingCompressorError, "No JavaScript Compressor. Please set the :javascript_compressor option" + if @options[:javascript_compressor].is_a?(Symbol) + raise NotFoundCompressorError, "JavaScript Compressor \"#{@options[:javascript_compressor]}\" not found, please check :javascript_compressor option" + else + raise MissingCompressorError, "No JavaScript Compressor. Please set the :javascript_compressor option" + end end # detect CDATA wrapper @@ -527,7 +546,7 @@ def compress_javascript(source) result = javascript_compressor.compress(source).strip if cdataWrapper - result = "" + result = "/**/" end result @@ -542,7 +561,11 @@ def compress_css_styles(source) end if css_compressor.nil? - raise MissingCompressorError, "No CSS Compressor. Please set the :css_compressor option" + if @options[:css_compressor].is_a?(Symbol) + raise NotFoundCompressorError, "CSS Compressor \"#{@options[:css_compressor]}\" not found, please check :css_compressor option" + else + raise MissingCompressorError, "No CSS Compressor. Please set the :css_compressor option" + end end # detect CDATA wrapper @@ -744,24 +767,25 @@ def remove_intertag_spaces(html) def remove_spaces_inside_tags(html) #remove spaces around equals sign inside tags - html = html.gsub(TAG_PROPERTY_PATTERN, '\1=') + if @options[:remove_spaces_inside_tags] + html = html.gsub(TAG_PROPERTY_PATTERN, '\1=') - #remove ending spaces inside tags + #remove ending spaces inside tags - html.gsub!(TAG_END_SPACE_PATTERN) do |match| + html.gsub!(TAG_END_SPACE_PATTERN) do |match| - group_1 = $1 - group_2 = $2 + group_1 = $1 + group_2 = $2 - # keep space if attribute value is unquoted before trailing slash - if group_2.start_with?("/") and (TAG_LAST_UNQUOTED_VALUE_PATTERN =~ group_1) - "#{group_1} #{group_2}" - else - "#{group_1}#{group_2}" + # keep space if attribute value is unquoted before trailing slash + if group_2.start_with?("/") and (TAG_LAST_UNQUOTED_VALUE_PATTERN =~ group_1) + "#{group_1} #{group_2}" + else + "#{group_1}#{group_2}" + end end end - html end diff --git a/lib/htmlcompressor/exceptions.rb b/lib/htmlcompressor/exceptions.rb index dd6e1ee..8b38a39 100644 --- a/lib/htmlcompressor/exceptions.rb +++ b/lib/htmlcompressor/exceptions.rb @@ -1,3 +1,4 @@ module HtmlCompressor class MissingCompressorError < StandardError; end + class NotFoundCompressorError < StandardError; end end diff --git a/lib/htmlcompressor/rack.rb b/lib/htmlcompressor/rack.rb index f72474b..706ae84 100644 --- a/lib/htmlcompressor/rack.rb +++ b/lib/htmlcompressor/rack.rb @@ -17,7 +17,7 @@ class Rack :remove_form_attributes => false, :remove_input_attributes => true, :remove_javascript_protocol => true, - :remove_http_protocol => true, + :remove_http_protocol => false, :remove_https_protocol => false, :preserve_line_breaks => false, :simple_boolean_attributes => true diff --git a/lib/htmlcompressor/version.rb b/lib/htmlcompressor/version.rb index 5f48a8c..4a4f47c 100644 --- a/lib/htmlcompressor/version.rb +++ b/lib/htmlcompressor/version.rb @@ -1,3 +1,3 @@ module HtmlCompressor - VERSION = "0.1.2" + VERSION = "0.4.0" end diff --git a/test/compressor_closure_test.rb b/test/compressor_closure_test.rb index e174579..4c46201 100644 --- a/test/compressor_closure_test.rb +++ b/test/compressor_closure_test.rb @@ -11,7 +11,8 @@ def test_compress_java_script_closure compressor = Compressor.new( :compress_javascript => true, :javascript_compressor => :closure, - :remove_intertag_spaces => true + :remove_intertag_spaces => true, + :compress_js_templates => true ) assert_equal result, compressor.compress(source) diff --git a/test/compressor_test.rb b/test/compressor_test.rb index 50969ce..01418a9 100644 --- a/test/compressor_test.rb +++ b/test/compressor_test.rb @@ -22,6 +22,16 @@ def test_remove_spaces_inside_tags assert_equal result, compressor.compress(source) end + def test_remove_spaces_inside_tags_disabled + source = read_resource("testRemoveSpacesInsideTags.html") + result = read_resource("testRemoveSpacesInsideTagsDisabledResult.html") + + compressor = Compressor.new(:remove_multi_spaces => false, :remove_spaces_inside_tags => false) + + assert_equal result, compressor.compress(source) + end + + def test_remove_comments source = read_resource("testRemoveComments.html") result = read_resource("testRemoveCommentsResult.html") @@ -82,7 +92,7 @@ def test_compress source = read_resource("testCompress.html") result = read_resource("testCompressResult.html") - compressor = Compressor.new + compressor = Compressor.new(:compress_js_templates => true) assert_equal result, compressor.compress(source) end @@ -207,6 +217,55 @@ def test_preserve_textarea assert_equal result, compressor.compress(source) end + def test_compress_custom_html_templates + source = read_resource("testCompressCustomHtmlTemplates.html") + result = read_resource("testCompressCustomHtmlTemplatesResult.html") + compressor = Compressor.new(:compress_js_templates => ['text/html'], :remove_quotes => true) + assert_equal result, compressor.compress(source) + end + + def test_dont_replace_javascript_inside_js_templates + source = read_resource("testCompressCustomHtmlTemplates.html") + result = read_resource("testCompressCustomHtmlTemplates.html") + compressor = Compressor.new(:compress_js_templates => false, :remove_quotes => true) + assert_equal result, compressor.compress(source) + end + + def test_javascript_compressor_not_found + source = read_resource("testCompressJavaScript.html"); + + compressor = Compressor.new( + :compress_javascript => true, + :javascript_compressor => :not_existing_compressor, + :remove_intertag_spaces => true, + :compress_js_templates => true + ) + + exception = assert_raises(NotFoundCompressorError) do + compressor.compress(source) + end + + expect_message = 'JavaScript Compressor "not_existing_compressor" not found, please check :javascript_compressor option' + assert_equal(expect_message, exception.message) + end + + def test_css_compressor_not_found + source = read_resource("testCompressCss.html"); + + compressor = Compressor.new( + :enabled => true, + :compress_css => true, + :css_compressor => :not_existing_compressor, + :compress_javascript => false + ) + + exception = assert_raises(NotFoundCompressorError) do + compressor.compress(source) + end + + expect_message = 'CSS Compressor "not_existing_compressor" not found, please check :css_compressor option' + assert_equal(expect_message, exception.message) + end end end diff --git a/test/compressor_yui_test.rb b/test/compressor_yui_test.rb index a832678..d669f83 100644 --- a/test/compressor_yui_test.rb +++ b/test/compressor_yui_test.rb @@ -11,7 +11,8 @@ def test_compress_javascript_yui compressor = Compressor.new( :compress_javascript => true, :javascript_compressor => :yui, - :remove_intertag_spaces => true + :remove_intertag_spaces => true, + :compress_js_templates => true ) assert_equal result, compressor.compress(source) diff --git a/test/resources/html/testCompressCustomHtmlTemplates.html b/test/resources/html/testCompressCustomHtmlTemplates.html new file mode 100644 index 0000000..9591ce6 --- /dev/null +++ b/test/resources/html/testCompressCustomHtmlTemplates.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/resources/html/testCompressCustomHtmlTemplatesResult.html b/test/resources/html/testCompressCustomHtmlTemplatesResult.html new file mode 100644 index 0000000..b47cb0a --- /dev/null +++ b/test/resources/html/testCompressCustomHtmlTemplatesResult.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/resources/html/testCompressJavaScriptClosureResult.html b/test/resources/html/testCompressJavaScriptClosureResult.html index 1cbc5a4..927ebaa 100644 --- a/test/resources/html/testCompressJavaScriptClosureResult.html +++ b/test/resources/html/testCompressJavaScriptClosureResult.html @@ -1,4 +1,4 @@ -
++