feature/pipeline
Thomas Reynolds 2016-04-25 11:08:34 -07:00
parent 11e478fad9
commit 029de6613b
4 changed files with 78 additions and 129 deletions

View File

@ -1,5 +1,6 @@
require 'memoist'
require 'middleman-core/contracts'
require 'rack/mime'
# Minify CSS Extension
class Middleman::Extensions::MinifyCss < ::Middleman::Extension
@ -12,14 +13,7 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
option :content_types, %w(text/css), 'Content types of resources that contain CSS'
option :inline_content_types, %w(text/html text/php), 'Content types of resources that contain inline CSS'
def ready
# Setup Rack middleware to minify CSS
app.use Rack, compressor: options[:compressor],
ignore: Array(options[:ignore]) + [/\.min\./],
inline: options[:inline],
content_types: options[:content_types],
inline_content_types: options[:inline_content_types]
end
INLINE_CSS_REGEX = /(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m
class SassCompressor
def self.compress(style, options={})
@ -29,97 +23,61 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
end
end
# Rack middleware to look for CSS and compress it
class Rack
extend Memoist
include Contracts
INLINE_CSS_REGEX = /(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m
def initialize(app, options_hash={}, &block)
super
# Init
# @param [Class] app
# @param [Hash] options
Contract RespondTo[:call], {
ignore: ArrayOf[PATH_MATCHER],
inline: Bool,
compressor: Or[Proc, RespondTo[:to_proc], RespondTo[:compress]]
} => Any
def initialize(app, options={})
@app = app
@ignore = options.fetch(:ignore)
@inline = options.fetch(:inline)
@compressor = options.fetch(:compressor)
@compressor = @compressor.to_proc if @compressor.respond_to? :to_proc
@compressor = @compressor.call if @compressor.is_a? Proc
@content_types = options[:content_types]
@inline_content_types = options[:inline_content_types]
end
# Rack interface
# @param [Rack::Environmemt] env
# @return [Array]
def call(env)
status, headers, response = @app.call(env)
content_type = headers['Content-Type'].try(:slice, /^[^;]*/)
path = env['PATH_INFO']
minified = if @inline && minifiable_inline?(content_type)
minify_inline(::Middleman::Util.extract_response_text(response))
elsif minifiable?(content_type) && !ignore?(path)
minify(::Middleman::Util.extract_response_text(response))
end
if minified
headers['Content-Length'] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
end
[status, headers, response]
end
private
# Whether the path should be ignored
# @param [String] path
# @return [Boolean]
def ignore?(path)
@ignore.any? { |ignore| ::Middleman::Util.path_match(ignore, path) }
end
memoize :ignore?
# Whether this type of content can be minified
# @param [String, nil] content_type
# @return [Boolean]
def minifiable?(content_type)
@content_types.include?(content_type)
end
memoize :minifiable?
# Whether this type of content contains inline content that can be minified
# @param [String, nil] content_type
# @return [Boolean]
def minifiable_inline?(content_type)
@inline_content_types.include?(content_type)
end
memoize :minifiable_inline?
# Minify the content
# @param [String] content
# @return [String]
def minify(content)
@compressor.compress(content)
end
memoize :minify
# Detect and minify inline content
# @param [String] content
# @return [String]
def minify_inline(content)
content.gsub(INLINE_CSS_REGEX) do
$1 + minify($2) + $3
end
end
memoize :minify_inline
@ignore = Array(options[:ignore]) + [/\.min\./]
@compressor = options[:compressor]
@compressor = @compressor.to_proc if @compressor.respond_to? :to_proc
@compressor = @compressor.call if @compressor.is_a? Proc
end
Contract ResourceList => ResourceList
def manipulate_resource_list(resources)
resources.each do |r|
type = r.content_type.try(:slice, /^[^;]*/)
if options[:inline] && minifiable_inline?(type)
r.filters << method(:minify_inline)
elsif minifiable?(type) && !ignore?(r.destination_path)
r.filters << method(:minify)
end
end
end
# Whether the path should be ignored
Contract String => Bool
def ignore?(path)
@ignore.any? { |ignore| ::Middleman::Util.path_match(ignore, path) }
end
memoize :ignore?
# Whether this type of content can be minified
Contract Maybe[String] => Bool
def minifiable?(content_type)
options[:content_types].include?(content_type)
end
memoize :minifiable?
# Whether this type of content contains inline content that can be minified
Contract Maybe[String] => Bool
def minifiable_inline?(content_type)
options[:inline_content_types].include?(content_type)
end
memoize :minifiable_inline?
# Minify the content
Contract String => String
def minify(content)
@compressor.compress(content)
end
memoize :minify
# Detect and minify inline content
Contract String => String
def minify_inline(content)
content.gsub(INLINE_CSS_REGEX) do
$1 + minify($2) + $3
end
end
memoize :minify_inline
end

View File

@ -1,24 +0,0 @@
require 'forwardable'
module Middleman
class Pipeline
extend Forwardable
attr_reader :filters
attr_reader :resource
def_delegators :filters, :<<, :push, :unshift, :insert, :shift, :pop, :first, :clear
def initialize(resource)
@resource = resource
@filters = []
end
def render(*args)
process resource.render *args
end
def process(body)
filters.inject(body){ |body, app| app.call(body) }
end
end
end

View File

@ -109,7 +109,7 @@ module Middleman
begin
# Write out the contents of the page
res.write resource.pipeline.render({}, rack: { request: req })
res.write resource.render({}, rack: { request: req })
# Valid content is a 200 status
res.status = 200

View File

@ -3,7 +3,6 @@ require 'middleman-core/sitemap/extensions/traversal'
require 'middleman-core/file_renderer'
require 'middleman-core/template_renderer'
require 'middleman-core/contracts'
require 'middleman-core/pipeline'
module Middleman
# Sitemap namespace
@ -40,8 +39,7 @@ module Middleman
attr_reader :metadata
attr_accessor :ignored
attr_reader :pipeline
attr_accessor :filters
# Initialize resource with parent store and URL
# @param [Middleman::Sitemap::Store] store
@ -53,7 +51,7 @@ module Middleman
@app = @store.app
@path = path
@ignored = false
@pipeline = Pipeline.new(self)
@filters = []
source = Pathname(source) if source && source.is_a?(String)
@ -144,6 +142,23 @@ module Middleman
# @return [String]
Contract Hash, Hash => String
def render(opts={}, locs={})
body = render_without_filters(opts, locs)
@filters.reduce(body) do |output, filter|
if filter.respond_to?(:execute_filter)
filter.execute_filter(output)
elsif filter.respond_to?(:call)
filter.call(output)
else
output
end
end
end
# Render this resource without content filters
# @return [String]
Contract Hash, Hash => String
def render_without_filters(opts={}, locs={})
return ::Middleman::FileRenderer.new(@app, file_descriptor[:full_path].to_s).template_data_for_file unless template?
md = metadata
@ -155,7 +170,7 @@ module Middleman
opts[:layout] = false if !opts.key?(:layout) && !@app.config.extensions_with_layout.include?(ext)
renderer = ::Middleman::TemplateRenderer.new(@app, file_descriptor[:full_path].to_s)
renderer.render(locs, opts)
renderer.render(locs, opts).to_str
end
# A path without the directory index - so foo/index.html becomes