middleman-more 3.0.0.beta.1 → 3.0.0.beta.2

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.
Files changed (76) hide show
  1. data/Rakefile +1 -0
  2. data/features/asset_hash.feature +84 -0
  3. data/features/cache_buster.feature +5 -4
  4. data/features/coffee-script.feature +2 -2
  5. data/features/compass-sprites.feature +6 -0
  6. data/features/gzip.feature +21 -0
  7. data/features/{ignore_already_minified.features → ignore_already_minified.feature} +0 -0
  8. data/features/minify_css.feature +50 -2
  9. data/features/minify_javascript.feature +89 -6
  10. data/features/relative_assets.feature +47 -6
  11. data/features/sass_partials.feature +5 -1
  12. data/features/sprockets_gems.feature +7 -2
  13. data/fixtures/asset-hash-app/config.rb +8 -0
  14. data/fixtures/asset-hash-app/source/images/100px.gif +0 -0
  15. data/fixtures/asset-hash-app/source/images/100px.jpg +0 -0
  16. data/fixtures/asset-hash-app/source/images/100px.png +0 -0
  17. data/fixtures/asset-hash-app/source/index.html.erb +308 -0
  18. data/fixtures/asset-hash-app/source/javascripts/application.js +8 -0
  19. data/fixtures/asset-hash-app/source/layout.erb +18 -0
  20. data/fixtures/asset-hash-app/source/other.html.erb +2 -0
  21. data/fixtures/asset-hash-app/source/partials.html.erb +1 -0
  22. data/fixtures/asset-hash-app/source/stylesheets/_partial.sass +2 -0
  23. data/fixtures/asset-hash-app/source/stylesheets/site.css.scss +4 -0
  24. data/fixtures/asset-hash-app/source/stylesheets/uses_partials.css.sass +4 -0
  25. data/fixtures/asset-hash-app/source/subdir/index.html.erb +308 -0
  26. data/fixtures/asset-hash-host-app/config.rb +6 -0
  27. data/fixtures/asset-hash-host-app/source/images/100px.gif +0 -0
  28. data/fixtures/asset-hash-host-app/source/images/100px.jpg +0 -0
  29. data/fixtures/asset-hash-host-app/source/images/100px.png +0 -0
  30. data/fixtures/asset-hash-host-app/source/index.html.erb +6 -0
  31. data/fixtures/asset-hash-host-app/source/layout.erb +17 -0
  32. data/fixtures/asset-hash-host-app/source/other.html.erb +2 -0
  33. data/fixtures/asset-hash-host-app/source/stylesheets/site.css.scss +4 -0
  34. data/fixtures/asset-hash-host-app/source/subdir/index.html.erb +6 -0
  35. data/fixtures/cache-buster-app/source/cache-buster.html.erb +2 -1
  36. data/fixtures/compass-sprites-app/config.rb +0 -0
  37. data/fixtures/compass-sprites-app/source/images/icon/arrow_down.png +0 -0
  38. data/fixtures/compass-sprites-app/source/images/icon/arrow_left.png +0 -0
  39. data/fixtures/compass-sprites-app/source/images/icon/arrow_right.png +0 -0
  40. data/fixtures/compass-sprites-app/source/images/icon/arrow_up.png +0 -0
  41. data/fixtures/compass-sprites-app/source/stylesheets/site.css.scss +3 -0
  42. data/fixtures/gzip-app/config.rb +1 -0
  43. data/fixtures/gzip-app/source/index.html +0 -0
  44. data/fixtures/gzip-app/source/javascripts/test.js +1 -0
  45. data/fixtures/gzip-app/source/stylesheets/test.css +1 -0
  46. data/fixtures/minify-css-app/source/inline-css.html.haml +5 -0
  47. data/fixtures/minify-css-app/source/more-css/site.css +3 -0
  48. data/fixtures/minify-css-app/source/stylesheets/site.css.sass +5 -1
  49. data/fixtures/minify-js-app/config.rb +1 -1
  50. data/fixtures/minify-js-app/source/inline-js.html.haml +22 -1
  51. data/fixtures/minify-js-app/source/more-js/other.js +8 -0
  52. data/fixtures/passthrough-app/config.rb +4 -4
  53. data/fixtures/passthrough-app/source/inline-css.html.haml +5 -4
  54. data/fixtures/passthrough-app/source/inline-js.html.haml +22 -1
  55. data/fixtures/relative-assets-app/config.rb +1 -0
  56. data/fixtures/relative-assets-app/source/relative_image.html.erb +8 -1
  57. data/fixtures/sprockets-app/source/library/js/vendored_include.js +1 -0
  58. data/fixtures/sprockets-app/vendor/assets/javascripts/vendored_js.js +1 -0
  59. data/lib/middleman-more.rb +23 -9
  60. data/lib/middleman-more/core_extensions/compass.rb +22 -38
  61. data/lib/middleman-more/core_extensions/sprockets.rb +41 -123
  62. data/lib/middleman-more/extensions/asset_hash.rb +132 -0
  63. data/lib/middleman-more/extensions/cache_buster.rb +5 -6
  64. data/lib/middleman-more/extensions/gzip.rb +57 -0
  65. data/lib/middleman-more/extensions/minify_css.rb +73 -7
  66. data/lib/middleman-more/extensions/minify_css/rainpress.rb +168 -0
  67. data/lib/middleman-more/extensions/minify_javascript.rb +45 -21
  68. data/lib/middleman-more/extensions/relative_assets.rb +2 -1
  69. data/lib/middleman-more/renderers/haml.rb +4 -0
  70. data/lib/middleman-more/renderers/liquid.rb +6 -1
  71. data/lib/middleman-more/renderers/markdown.rb +10 -1
  72. data/lib/middleman-more/renderers/sass.rb +12 -6
  73. data/lib/middleman-more/renderers/slim.rb +5 -0
  74. data/middleman-more.gemspec +9 -9
  75. metadata +192 -178
  76. data/lib/middleman-more/extensions/minify_css/cssmin.rb +0 -55
@@ -0,0 +1,132 @@
1
+ require 'digest/sha1'
2
+ module Middleman::Extensions
3
+ module AssetHash
4
+ class << self
5
+ def registered(app, options)
6
+ exts = options[:exts] || %w(.ico .manifest .jpg .jpeg .png .gif .js .css)
7
+
8
+ app.ready do
9
+ sitemap.register_resource_list_manipulator(
10
+ :asset_hash,
11
+ AssetHashManager.new(self, exts)
12
+ )
13
+
14
+ use Middleware, :exts => exts, :middleman_app => self
15
+ end
16
+ end
17
+ alias :included :registered
18
+ end
19
+
20
+ class AssetHashManager
21
+ def initialize(app, exts)
22
+ @app = app
23
+ @exts = exts
24
+ end
25
+
26
+ # Update the main sitemap resource list
27
+ # @return [void]
28
+ def manipulate_resource_list(resources)
29
+ resources.each do |resource|
30
+ if @exts.include? resource.ext
31
+ # figure out the path Sprockets would use for this asset
32
+ if resource.ext == '.js'
33
+ sprockets_path = resource.path.sub(@app.js_dir,'').sub(/^\//,'')
34
+ elsif resource.ext == '.css'
35
+ sprockets_path = resource.path.sub(@app.css_dir,'').sub(/^\//,'')
36
+ end
37
+
38
+ # See if Sprockets knows about the file
39
+ asset = @app.sprockets.find_asset(sprockets_path) if sprockets_path
40
+
41
+ if asset # if it's a Sprockets asset, ask sprockets for its digest
42
+ digest = asset.digest[0..7]
43
+ elsif resource.template? # if it's a template, render it out
44
+ digest = Digest::SHA1.hexdigest(resource.render)[0..7]
45
+ else # if it's a static file, just hash it
46
+ digest = Digest::SHA1.file(resource.source_file).hexdigest[0..7]
47
+ end
48
+
49
+ resource.destination_path = resource.destination_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # The asset hash middleware is responsible for rewriting references to
56
+ # assets to include their new, hashed name.
57
+ class Middleware
58
+ def initialize(app, options={})
59
+ @rack_app = app
60
+ @exts = options[:exts]
61
+ @exts_regex_text = @exts.map {|e| Regexp.escape(e) }.join('|')
62
+ @middleman_app = options[:middleman_app]
63
+ end
64
+
65
+ def call(env)
66
+ status, headers, response = @rack_app.call(env)
67
+
68
+ path = @middleman_app.full_path(env["PATH_INFO"])
69
+ dirpath = Pathname.new(File.dirname(path))
70
+
71
+ if path =~ /(^\/$)|(\.(htm|html|php|css|js)$)/
72
+ body = extract_response_text(response)
73
+
74
+ if body
75
+ # TODO: This regex will change some paths in plan HTML (not in a tag) - is that OK?
76
+ body.gsub! /([=\'\"\(]\s*)([^\s\'\"\)]+(#{@exts_regex_text}))/ do |match|
77
+ asset_path = $2
78
+ relative_path = Pathname.new(asset_path).relative?
79
+
80
+ asset_path = dirpath.join(asset_path).to_s if relative_path
81
+
82
+ if asset_page = @middleman_app.sitemap.find_resource_by_path(asset_path)
83
+ replacement_path = "/#{asset_page.destination_path}"
84
+ replacement_path = Pathname.new(replacement_path).relative_path_from(dirpath).to_s if relative_path
85
+
86
+ "#{$1}#{replacement_path}"
87
+ else
88
+ match
89
+ end
90
+ end
91
+
92
+ status, headers, response = Rack::Response.new(body, status, headers).finish
93
+ end
94
+ end
95
+ [status, headers, response]
96
+ end
97
+
98
+ private
99
+
100
+ def extract_response_text(response)
101
+ case(response)
102
+ when String
103
+ response
104
+ when Array
105
+ response.join
106
+ when Rack::Response
107
+ response.body.join
108
+ when Rack::File
109
+ File.read(response.path)
110
+ else
111
+ response.to_s
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ register :asset_hash, AssetHash
118
+ end
119
+
120
+
121
+ # =================Temp Generate Test data==============================
122
+ # ["jpg", "png", "gif"].each do |ext|
123
+ # [["<p>", "</p>"], ["<p><img src=", " /></p>"], ["<p>background-image:url(", ");</p>"]].each do |outer|
124
+ # [["",""], ["'", "'"], ['"','"']].each do |inner|
125
+ # [["", ""], ["/", ""], ["../", ""], ["../../", ""], ["../../../", ""], ["http://example.com/", ""], ["a","a"], ["1","1"], [".", "."], ["-","-"], ["_","_"]].each do |path_parts|
126
+ # name = 'images/100px.'
127
+ # puts outer[0] + inner[0] + path_parts[0] + name + ext + path_parts[1] + inner[1] + outer[1]
128
+ # end
129
+ # end
130
+ # end
131
+ # puts "<br /><br /><br />"
132
+ # end
@@ -51,10 +51,9 @@ module Middleman::Extensions
51
51
  real_path_dynamic = File.join(build_dir, prefix, path)
52
52
  real_path_dynamic = File.expand_path(real_path_dynamic, root)
53
53
  http_path << "?" + File.mtime(real_path_dynamic).strftime("%s") if File.readable?(real_path_dynamic)
54
- elsif sitemap.exists?(real_path_static)
55
- page = sitemap.page(real_path_static)
56
- if !page.template?
57
- http_path << "?" + File.mtime(result[0]).strftime("%s")
54
+ elsif resource = sitemap.find_resource_by_path(real_path_static)
55
+ if !resource.template?
56
+ http_path << "?" + File.mtime(resource.source_file).strftime("%s")
58
57
  else
59
58
  # It's a template, possible with partials. We can't really know when
60
59
  # it's updated, so generate fresh cache buster every time durin
@@ -70,5 +69,5 @@ module Middleman::Extensions
70
69
  end
71
70
 
72
71
  # Register the extension
73
- register :cache_buster, CacheBuster
74
- end
72
+ # register :cache_buster, CacheBuster
73
+ end
@@ -0,0 +1,57 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+ require 'find'
4
+
5
+ module Middleman::Extensions
6
+
7
+ # This extension Gzips assets and pages when building.
8
+ # Gzipped assets and pages can be served directly by Apache or
9
+ # Nginx with the proper configuration, and pre-zipping means that we
10
+ # can use a more agressive compression level at no CPU cost per request.
11
+ #
12
+ # Use Nginx's gzip_static directive, or AddEncoding and mod_rewrite in Apache
13
+ # to serve your Gzipped files whenever the normal (non-.gz) filename is requested.
14
+ #
15
+ # Pass the :exts options to customize which file extensions get zipped (defaults
16
+ # to .html, .htm, .js and .css.
17
+ #
18
+ module Gzip
19
+ class << self
20
+ def registered(app, options={})
21
+ exts = options[:exts] || %w(.js .css .html .htm)
22
+
23
+ app.send :include, InstanceMethods
24
+
25
+ app.after_build do |builder|
26
+ Find.find(self.class.inst.build_dir) do |path|
27
+ next if File.directory? path
28
+ if exts.include? File.extname(path)
29
+ new_size = gzip_file(path, builder)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ alias :included :registered
36
+ end
37
+
38
+ module InstanceMethods
39
+ def gzip_file(path, builder)
40
+ input_file = File.open(path, 'r').read
41
+ output_filename = path + '.gz'
42
+ File.open(output_filename, 'w') do |f|
43
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
44
+ gz.write input_file
45
+ gz.close
46
+ end
47
+
48
+ old_size = File.size(path)
49
+ new_size = File.size(output_filename)
50
+
51
+ size_change_word = (old_size - new_size) > 0 ? 'smaller' : 'larger'
52
+
53
+ builder.say_status :gzip, "#{output_filename} (#{number_to_human_size((old_size - new_size).abs)} #{size_change_word})"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -9,18 +9,84 @@ module Middleman::Extensions
9
9
 
10
10
  # Once registered
11
11
  def registered(app)
12
- # Tell Sprockets to use the built in CSSMin
12
+ app.set :css_compressor, false
13
+
13
14
  app.after_configuration do
14
- if !css_compressor
15
- require "middleman-more/extensions/minify_css/cssmin"
16
- set :css_compressor, ::CSSMin
15
+ unless respond_to?(:css_compressor) && css_compressor
16
+ require "middleman-more/extensions/minify_css/rainpress"
17
+ set :css_compressor, ::Rainpress
17
18
  end
19
+
20
+ # Setup Rack to watch for inline JS
21
+ use InlineCSSRack, :compressor => css_compressor
18
22
  end
19
23
  end
20
24
  alias :included :registered
21
25
  end
22
26
  end
23
-
27
+
28
+ # Rack middleware to look for JS in HTML and compress it
29
+ class InlineCSSRack
30
+
31
+ # Init
32
+ # @param [Class] app
33
+ # @param [Hash] options
34
+ def initialize(app, options={})
35
+ @app = app
36
+ @compressor = options[:compressor]
37
+ end
38
+
39
+ # Rack interface
40
+ # @param [Rack::Environmemt] env
41
+ # @return [Array]
42
+ def call(env)
43
+ status, headers, response = @app.call(env)
44
+
45
+ path = env["PATH_INFO"]
46
+
47
+ if path.end_with?('.html') || path.end_with?('.php')
48
+ uncompressed_source = extract_response_text(response)
49
+
50
+ minified = uncompressed_source.gsub(/(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m) do |match|
51
+ first = $1
52
+ css = $2
53
+ last = $3
54
+
55
+ minified_css = @compressor.compress(css)
56
+
57
+ first << minified_css << last
58
+ end
59
+
60
+ headers["Content-Length"] = ::Rack::Utils.bytesize(minified).to_s
61
+ response = [minified]
62
+ elsif path.end_with?('.css') && path !~ /\.min\./
63
+ uncompressed_source = extract_response_text(response)
64
+ minified_css = @compressor.compress(uncompressed_source)
65
+
66
+ headers["Content-Length"] = ::Rack::Utils.bytesize(minified_css).to_s
67
+ response = [minified_css]
68
+ end
69
+
70
+ [status, headers, response]
71
+ end
72
+
73
+ private
74
+
75
+ def extract_response_text(response)
76
+ case(response)
77
+ when String
78
+ response
79
+ when Array
80
+ response.join
81
+ when Rack::Response
82
+ response.body.join
83
+ when Rack::File
84
+ File.read(response.path)
85
+ else
86
+ response.to_s
87
+ end
88
+ end
89
+ end
24
90
  # Register extension
25
- register :minify_css, MinifyCss
26
- end
91
+ # register :minify_css, MinifyCss
92
+ end
@@ -0,0 +1,168 @@
1
+ # == Information
2
+ #
3
+ # This is the main class of Rainpress, create an instance of it to compress
4
+ # your CSS-styles.
5
+ #
6
+ # Author:: Uwe L. Korn <uwelk@xhochy.org>
7
+ #
8
+ # <b>Options:</b>
9
+ #
10
+ # * <tt>:comments</tt> - if set to false, comments will not be removed
11
+ # * <tt>:newlines</tt> - if set to false, newlines will not be removed
12
+ # * <tt>:spaces</tt> - if set to false, spaces will not be removed
13
+ # * <tt>:colors</tt> - if set to false, colors will not be modified
14
+ # * <tt>:misc</tt> - if set to false, miscellaneous compression parts will be skipped
15
+ class Rainpress
16
+ # Quick-compress the styles.
17
+ # This eliminates the need to create an instance of the class
18
+ def self.compress(style, options = {})
19
+ self.new(style, options).compress!
20
+ end
21
+
22
+ def initialize(style, opts = {})
23
+ @style = style
24
+ @opts = {
25
+ :comments => true,
26
+ :newlines => true,
27
+ :spaces => true,
28
+ :colors => true,
29
+ :misc => true
30
+ }
31
+ @opts.merge! opts
32
+ end
33
+
34
+ # Run the compressions and return the newly compressed text
35
+ def compress!
36
+ remove_comments! if @opts[:comments]
37
+ remove_newlines! if @opts[:newlines]
38
+ remove_spaces! if @opts[:spaces]
39
+ shorten_colors! if @opts[:colors]
40
+ do_misc! if @opts[:misc]
41
+ @style
42
+ end
43
+
44
+ # Remove all comments out of the CSS-Document
45
+ #
46
+ # Only /* text */ comments are supported.
47
+ # Attention: If you are doing css hacks for IE using the comment tricks,
48
+ # they will be removed using this function. Please consider for IE css style
49
+ # corrections the usage of conditionals comments in your (X)HTML document.
50
+ def remove_comments!
51
+ input = @style
52
+ @style = ''
53
+
54
+ while input.length > 0 do
55
+ pos = input.index("/*");
56
+
57
+ # No more comments
58
+ if pos == nil
59
+ @style += input
60
+ input = '';
61
+ else # Comment beginning at pos
62
+ @style += input[0..(pos-1)] if pos > 0 # only append text if there is some
63
+ input = input[(pos+2)..-1]
64
+ # Comment ending at pos
65
+ pos = input.index("*/")
66
+ input = input[(pos+2)..-1]
67
+ end
68
+ end
69
+ end
70
+
71
+ # Remove all newline characters
72
+ #
73
+ # We take care of Windows(\r\n), Unix(\n) and Mac(\r) newlines.
74
+ def remove_newlines!
75
+ @style.gsub! /\n|\r/, ''
76
+ end
77
+
78
+ # Remove unneeded spaces
79
+ #
80
+ # 1. Turn mutiple spaces into a single
81
+ # 2. Remove spaces around ;:{},
82
+ # 3. Remove tabs
83
+ def remove_spaces!
84
+ @style.gsub! /\s*(\s|;|:|\}|\{|,)\s*/, '\1'
85
+ @style.gsub! "\t", ''
86
+ end
87
+
88
+ # Replace color values with their shorter equivalent
89
+ #
90
+ # 1. Turn rgb(,,)-colors into #-values
91
+ # 2. Shorten #AABBCC down to #ABC
92
+ # 3. Replace names with their shorter hex-equivalent
93
+ # * white -> #fff
94
+ # * black -> #000
95
+ # 4. Replace #-values with their shorter name
96
+ # * #f00 -> red
97
+ def shorten_colors!
98
+ # rgb(50,101,152) to #326598
99
+ @style.gsub! /rgb\s*\(\s*([0-9,\s]+)\s*\)/ do |match|
100
+ out = '#'
101
+ $1.split(',').each do |num|
102
+ out += '0' if num.to_i < 16
103
+ out += num.to_i.to_s(16) # convert to hex
104
+ end
105
+ out
106
+ end
107
+ # Convert #AABBCC to #ABC, keep if preceed by a '='
108
+ @style.gsub! /([^\"'=\s])(\s*)#([\da-f])\3([\da-f])\4([\da-f])\5/i, '\1#\3\4\5'
109
+
110
+ # At the moment we assume that colours only appear before ';' or '}' and
111
+ # after a ':', if there could be an occurence of a color before or after
112
+ # an other character, submit either a bug report or, better, a patch that
113
+ # enables Rainpress to take care of this.
114
+
115
+ # shorten several names to numbers
116
+ ## shorten white -> #fff
117
+ @style.gsub! /:\s*white\s*(;|\})/, ':#fff\1'
118
+
119
+ ## shorten black -> #000
120
+ @style.gsub! /:\s*black\s*(;|\})/, ':#000\1'
121
+
122
+ # shotern several numbers to names
123
+ ## shorten #f00 or #ff0000 -> red
124
+ @style.gsub! /:\s*#f{1,2}0{2,4}(;|\})/i, ':red\1'
125
+ end
126
+
127
+ # Do miscellaneous compression methods on the style.
128
+ def do_misc!
129
+ # Replace 0(pt,px,em,%) with 0 but only when preceded by : or a white-space
130
+ @style.gsub! /([\s:]+)(0)(px|em|%|in|cm|mm|pc|pt|ex)/i, '\1\2'
131
+
132
+ # Replace :0 0 0 0(;|}) with :0(;|})
133
+ @style.gsub! /:0 0 0 0(;|\})/, ':0\1'
134
+
135
+ # Replace :0 0 0(;|}) with :0(;|})
136
+ @style.gsub! /:0 0 0(;|\})/, ':0\1'
137
+
138
+ # Replace :0 0(;|}) with :0(;|})
139
+ @style.gsub! /:0 0(;|\})/, ':0\1'
140
+
141
+ # Replace background-position:0; with background-position:0 0;
142
+ @style.gsub! 'background-position:0;', 'background-position:0 0;'
143
+
144
+ # Replace 0.6 to .6, but only when preceded by : or a white-space
145
+ @style.gsub! /[:\s]0+\.(\d+)/ do |match|
146
+ match.sub '0', '' # only first '0' !!
147
+ end
148
+
149
+ # Replace multiple ';' with a single ';'
150
+ @style.gsub! /[;]+/, ';'
151
+
152
+ # Replace ;} with }
153
+ @style.gsub! ';}', '}'
154
+
155
+ # Replace font-weight:normal; with 400
156
+ @style.gsub! /font-weight[\s]*:[\s]*normal[\s]*(;|\})/i,'font-weight:400\1'
157
+ @style.gsub! /font[\s]*:[\s]*normal[\s;\}]*/ do |match|
158
+ match.sub 'normal', '400'
159
+ end
160
+
161
+ # Replace font-weight:bold; with 700
162
+ @style.gsub! /font-weight[\s]*:[\s]*bold[\s]*(;|\})/,'font-weight:700\1'
163
+ @style.gsub! /font[\s]*:[\s]*bold[\s;\}]*/ do |match|
164
+ match.sub 'bold', '700'
165
+ end
166
+ end
167
+
168
+ end