Compare commits

...

6 Commits

Author SHA1 Message Date
Thomas Reynolds 439ecb1887 Minor performance tweak 2016-04-28 10:08:43 -07:00
Thomas Reynolds 87e0f240ff continuing 2016-04-28 09:36:05 -07:00
Thomas Reynolds 2804a61c61 Finish conversion 2016-04-28 09:35:37 -07:00
Thomas Reynolds 0d3030f28c More WIP WIP 2016-04-28 09:35:37 -07:00
Thomas Reynolds 029de6613b More WIP 2016-04-28 09:35:37 -07:00
Brad Gessler 11e478fad9 WIP - Rendering pipeline. 2016-04-28 09:35:37 -07:00
47 changed files with 379 additions and 618 deletions

View File

@ -1,8 +1,9 @@
master
===
# Next
# 4.2.0
* Remove Rack support in favor of `resource.filters << proc { |oldbody| newbody }`
* Expose `development?` and `production?` helpers to template context.
# 4.1.8

View File

@ -188,23 +188,6 @@ Feature: Assets get file hashes appended to them and references to them are upda
When I go to "/partials/"
Then I should see 'href="../stylesheets/uses_partials-ec347271.css'
Scenario: The asset hash should change when a Rack-based filter changes
Given a fixture app "asset-hash-app"
And a file named "config.rb" with:
"""
activate :asset_hash
activate :relative_assets
activate :directory_indexes
require 'lib/middleware.rb'
use ::Middleware
"""
Given the Server is running at "asset-hash-app"
When I go to "/"
Then I should see 'href="stylesheets/site-5ad7def0.css'
When I go to "stylesheets/site-5ad7def0.css"
Then I should see 'background-image: url("../images/100px-5fd6fb90.jpg")'
Then I should see 'Added by Rack filter'
Scenario: Hashed-asset files are not produced for ignored paths
Given a fixture app "asset-hash-app"
And a file named "config.rb" with:

View File

@ -0,0 +1,17 @@
Feature: Generic block based pages
Scenario: Static Ruby Endpoints
Given an empty app
And a file named "config.rb" with:
"""
endpoint "hello.html" do
"world"
end
"""
And a file named "source/index.html.erb" with:
"""
Hi
"""
And the Server is running at "empty_app"
When I go to "/hello.html"
Then I should see "world"

View File

@ -3,8 +3,9 @@ Feature: Extension author could use some hooks
Scenario: When build
Given a fixture app "extension-api-deprecations-app"
When I run `middleman build`
Then the exit status should be 0
And the exit status should be 0
And the output should contain "`set :layout` is deprecated"
And the output should contain "Project built successfully"
And the file "build/index.html" should contain "In Index"
And the file "build/index.html" should not contain "In Layout"

View File

@ -7,7 +7,6 @@ Feature: Extension author could use some hooks
And the output should contain "/// after_configuration ///"
And the output should contain "/// ready ///"
And the output should contain "/// before_build ///"
And the output should contain "/// before ///"
And the output should contain "/// before_render ///"
And the output should contain "/// after_render ///"
And the output should contain "/// after_build ///"

View File

@ -1,65 +0,0 @@
Feature: Support Rack apps mounted using map
Scenario: Mounted Rack App at /sinatra
Given the Server is running at "sinatra-app"
When I go to "/"
Then I should see "Hello World (Middleman)"
When I go to "/sinatra/"
Then I should see "Hello World (Sinatra)"
Scenario: Built Mounted Rack App at /sinatra
Given a successfully built app at "sinatra-app"
When I cd to "build"
Then the following files should exist:
| index.html |
Then the following files should not exist:
| sinatra/index.html |
| sinatra/index2.html |
Scenario: Static Ruby Endpoints
Given a fixture app "sinatra-app"
And a file named "config.rb" with:
"""
endpoint "hello.html" do
"world"
end
"""
And the Server is running at "sinatra-app"
When I go to "/hello.html"
Then I should see "world"
Scenario: Built Mounted Rack App at /sinatra (including rack endpoints)
Given a fixture app "sinatra-app"
And a file named "config.rb" with:
"""
require "sinatra"
class MySinatra < Sinatra::Base
get "/" do
"Hello World (Sinatra)"
end
get "/derp.html" do
"De doo"
end
end
map "/sinatra" do
run MySinatra
end
endpoint "sinatra/index2.html", path: "/sinatra/"
endpoint "dedoo.html", path: "/sinatra/derp.html"
endpoint "hello.html" do
"world"
end
"""
And a successfully built app at "sinatra-app"
When I cd to "build"
Then the following files should exist:
| index.html |
| sinatra/index2.html |
| dedoo.html |
And the file "sinatra/index2.html" should contain 'Hello World (Sinatra)'
And the file "dedoo.html" should contain 'De doo'

View File

@ -1,14 +0,0 @@
require "sinatra"
class MySinatra < Sinatra::Base
get "/" do
"Hello World (Sinatra)"
end
get "/derp.html" do
"De doo"
end
end
map "/sinatra" do
run MySinatra
end

View File

@ -1,5 +0,0 @@
---
layout: false
---
Hello World (Middleman)

View File

@ -157,12 +157,12 @@ module Middleman
define_setting :layout, :_auto_layout, 'Default layout name'
# Which file extensions have a layout by default.
# @return [Array.<String>]
define_setting :extensions_with_layout, %w(.htm .html .xhtml .php), 'Which file extensions have a layout by default.'
# @return [Set.<String>]
define_setting :extensions_with_layout, %w(.htm .html .xhtml .php), 'Which file extensions have a layout by default.', set: true
# Which file extensions are "assets."
# @return [Array.<String>]
define_setting :asset_extensions, %w(.css .png .jpg .jpeg .webp .svg .svgz .js .gif .ttf .otf .woff .woff2 .eot .ico .map), 'Which file extensions are treated as assets.'
define_setting :asset_extensions, %w(.css .png .jpg .jpeg .webp .svg .svgz .js .gif .ttf .otf .woff .woff2 .eot .ico .map), 'Which file extensions are treated as assets.', set: true
# Default string encoding for templates and output.
# @return [String]

View File

@ -2,7 +2,6 @@ require 'pathname'
require 'fileutils'
require 'tempfile'
require 'parallel'
require 'middleman-core/rack'
require 'middleman-core/callback_manager'
require 'middleman-core/contracts'
@ -39,9 +38,6 @@ module Middleman
@cleaning = opts.fetch(:clean)
@parallel = opts.fetch(:parallel, true)
rack_app = ::Middleman::Rack.new(@app).to_app
@rack = ::Rack::MockRequest.new(rack_app)
@callbacks = ::Middleman::CallbackManager.new
@callbacks.install_methods!(self, [:on_build_event])
end
@ -227,15 +223,7 @@ module Middleman
if resource.binary?
export_file!(output_file, resource.file_descriptor[:full_path])
else
response = @rack.get(::URI.escape(resource.request_path))
# If we get a response, save it to a tempfile.
if response.status == 200
export_file!(output_file, binary_encode(response.body))
else
trigger(:error, output_file, response.body)
return false
end
export_file!(output_file, binary_encode(resource.render))
end
rescue => e
trigger(:error, output_file, "#{e}\n#{e.backtrace.join("\n")}")

View File

@ -1,3 +1,5 @@
require 'set'
module Middleman
module Configuration
# A class that manages a collection of documented settings.
@ -129,23 +131,37 @@ module Middleman
def initialize(key, default, description, options={})
@value_set = false
@array_wrapped_value = nil
@array_wrapped_default = nil
self.key = key
self.default = default
self.description = description
self.options = options
@array_wrapped_default = if self.default && options[:set] && self.default.is_a?(Array)
Set.new(self.default)
end
end
# The user-supplied value for this setting, overriding the default
def value=(value)
@value = value
@value_set = true
@array_wrapped_value = if @value && options[:set] && @value.is_a?(Array)
Set.new(@value)
end
end
# The effective value of the setting, which may be the default
# if the user has not set a value themselves. Note that even if the
# user sets the value to nil it will override the default.
def value
value_set? ? @value : default
if value_set?
@array_wrapped_value ? @array_wrapped_value : @value
else
@array_wrapped_default ? @array_wrapped_default : default
end
end
# Whether or not there has been a value set beyond the default

View File

@ -19,12 +19,6 @@ Middleman::Extensions.register :data, auto_activate: :before_sitemap do
Middleman::CoreExtensions::Data
end
# Rewrite embedded URLs via Rack
Middleman::Extensions.register :inline_url_rewriter, auto_activate: :before_sitemap do
require 'middleman-core/core_extensions/inline_url_rewriter'
Middleman::CoreExtensions::InlineURLRewriter
end
# Catch and show exceptions at the Rack level
Middleman::Extensions.register :show_exceptions, auto_activate: :before_configuration, modes: [:server] do
require 'middleman-core/core_extensions/show_exceptions'

View File

@ -1,135 +0,0 @@
require 'rack'
require 'rack/response'
require 'memoist'
require 'middleman-core/util'
require 'middleman-core/contracts'
module Middleman
module CoreExtensions
class InlineURLRewriter < ::Middleman::Extension
include Contracts
expose_to_application rewrite_inline_urls: :add
REWRITER_DESCRIPTOR = {
id: Symbol,
proc: Or[Proc, Method],
url_extensions: ArrayOf[String],
source_extensions: ArrayOf[String],
ignore: ArrayOf[::Middleman::Util::IGNORE_DESCRIPTOR],
after: Maybe[Symbol]
}.freeze
def initialize(app, options_hash={}, &block)
super
@rewriters = {}
end
Contract REWRITER_DESCRIPTOR => Any
def add(options)
@rewriters[options] = options
end
def after_configuration
return if @rewriters.empty?
rewriters = @rewriters.values.sort do |a, b|
if b[:after] && b[:after] == a[:id]
1
else
0
end
end
app.use Rack, rewriters: rewriters, middleman_app: @app
end
class Rack
extend Memoist
include Contracts
Contract RespondTo[:call], {
middleman_app: IsA['Middleman::Application'],
rewriters: ArrayOf[REWRITER_DESCRIPTOR]
} => Any
def initialize(app, options={})
@rack_app = app
@middleman_app = options.fetch(:middleman_app)
@rewriters = options.fetch(:rewriters)
all_source_exts = @rewriters
.reduce([]) { |sum, rewriter| sum + rewriter[:source_extensions] }
.flatten
.uniq
@source_exts_regex_text = Regexp.union(all_source_exts).to_s
@all_asset_exts = @rewriters
.reduce([]) { |sum, rewriter| sum + rewriter[:url_extensions] }
.flatten
.uniq
end
def call(env)
status, headers, response = @rack_app.call(env)
# Allow configuration or upstream request to skip all rewriting
return [status, headers, response] if env['bypass_inline_url_rewriter'] == 'true'
path = ::Middleman::Util.full_path(env['PATH_INFO'], @middleman_app)
return [status, headers, response] unless path =~ /(^\/$)|(#{@source_exts_regex_text}$)/
return [status, headers, response] unless body = ::Middleman::Util.extract_response_text(response)
dirpath = ::Pathname.new(File.dirname(path))
rewritten = ::Middleman::Util.instrument 'inline_url_rewriter', path: path do
::Middleman::Util.rewrite_paths(body, path, @all_asset_exts, @middleman_app) do |asset_path|
uri = ::Middleman::Util.parse_uri(asset_path)
relative_path = uri.host.nil?
full_asset_path = if relative_path
dirpath.join(asset_path).to_s
else
asset_path
end
@rewriters.each do |rewriter|
uid = rewriter.fetch(:id)
# Allow upstream request to skip this specific rewriting
next if env["bypass_inline_url_rewriter_#{uid}"] == 'true'
exts = rewriter.fetch(:url_extensions)
next unless exts.include?(::File.extname(asset_path))
source_exts = rewriter.fetch(:source_extensions)
next unless source_exts.include?(::File.extname(path))
ignore = rewriter.fetch(:ignore)
next if ignore.any? { |r| ::Middleman::Util.should_ignore?(r, full_asset_path) }
rewrite_ignore = Array(rewriter[:rewrite_ignore] || [])
next if rewrite_ignore.any? { |i| ::Middleman::Util.path_match(i, path) }
proc = rewriter.fetch(:proc)
result = proc.call(asset_path, dirpath, path)
asset_path = result if result
end
asset_path
end
end
::Rack::Response.new(
rewritten,
status,
headers
).finish
end
end
end
end
end

View File

@ -60,7 +60,7 @@ module Middleman
# There are also some less common hooks that can be listened to from within an extension's `initialize` method:
#
# * `app.before_render {|body, path, locs, template_class| ... }` - Manipulate template sources before they are rendered.
# * `app.after_render {|content, path, locs, template_class| ... }` - Manipulate output text after a template has been rendered. It is also common to install a Rack middleware to do this instead.
# * `app.after_render {|content, path, locs, template_class| ... }` - Manipulate output text after a template has been rendered.
# * `app.ready { ... }` - Run code once Middleman is ready to serve or build files (after `after_configuration`).
#

View File

@ -1,18 +1,15 @@
require 'middleman-core/util'
require 'middleman-core/rack'
class Middleman::Extensions::AssetHash < ::Middleman::Extension
option :sources, %w(.css .htm .html .js .php .xhtml), 'List of extensions that are searched for hashable assets.'
option :exts, nil, 'List of extensions that get asset hashes appended to them.'
option :sources, %w(.css .htm .html .js .php .xhtml), 'List of extensions that are searched for hashable assets.', set: true
option :exts, nil, 'List of extensions that get asset hashes appended to them.', set: true
option :ignore, [], 'Regexes of filenames to skip adding asset hashes to'
option :rewrite_ignore, [], 'Regexes of filenames to skip processing for path rewrites'
def initialize(app, options_hash={}, &block)
super
require 'addressable/uri'
require 'digest/sha1'
require 'rack/mock'
# Allow specifying regexes to ignore, plus always ignore apple touch icons
@ignore = Array(options.ignore) + [/^apple-touch-icon/]
@ -20,14 +17,6 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
# Exclude .ico from the default list because browsers expect it
# to be named "favicon.ico"
@exts = options.exts || (app.config[:asset_extensions] - %w(.ico))
app.rewrite_inline_urls id: :asset_hash,
url_extensions: @exts.sort.reverse,
source_extensions: options.sources,
ignore: @ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url),
after: :asset_host
end
Contract String, Or[String, Pathname], Any => Maybe[String]
@ -45,7 +34,6 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
replacement_path = "/#{asset_page.destination_path}"
replacement_path = Pathname.new(replacement_path).relative_path_from(dirpath).to_s if relative_path
replacement_path
end
@ -53,9 +41,18 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
# @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources)
@rack_client ||= begin
rack_app = ::Middleman::Rack.new(app).to_app
::Rack::MockRequest.new(rack_app)
resources.each do |r|
next unless r.destination_path.end_with?('/', *options.sources)
next if Array(options.rewrite_ignore || []).any? do |i|
::Middleman::Util.path_match(i, "/#{r.destination_path}")
end
r.filters << ::Middleman::InlineURLRewriter.new(:asset_hash,
app,
r,
url_extensions: @exts,
ignore: options.ignore,
proc: method(:rewrite_url))
end
# Process resources in order: binary images and fonts, then SVG, then JS/CSS.
@ -81,15 +78,9 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
digest = if resource.binary?
::Digest::SHA1.file(resource.source_file).hexdigest[0..7]
else
# Render through the Rack interface so middleware and mounted apps get a shot
response = @rack_client.get(
::URI.escape(resource.destination_path),
'bypass_inline_url_rewriter_asset_hash' => 'true'
)
raise "#{resource.path} should be in the sitemap!" unless response.status == 200
::Digest::SHA1.hexdigest(response.body)[0..7]
# Render without asset hash
body = resource.render { |f| !f.respond_to?(:filter_name) || f.filter_name != :asset_hash }
::Digest::SHA1.hexdigest(body)[0..7]
end
resource.destination_path = resource.destination_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }

View File

@ -1,5 +1,3 @@
require 'addressable/uri'
class Middleman::Extensions::AssetHost < ::Middleman::Extension
option :host, nil, 'The asset host to use or a Proc to determine asset host', required: true
option :exts, nil, 'List of extensions that get cache busters strings appended to them.'
@ -7,15 +5,22 @@ class Middleman::Extensions::AssetHost < ::Middleman::Extension
option :ignore, [], 'Regexes of filenames to skip adding query strings to'
option :rewrite_ignore, [], 'Regexes of filenames to skip processing for host rewrites'
def initialize(app, options_hash={}, &block)
super
Contract ResourceList => ResourceList
def manipulate_resource_list(resources)
resources.each do |r|
next unless r.destination_path.end_with?('/', *options.sources)
next if Array(options.rewrite_ignore || []).any? do |i|
::Middleman::Util.path_match(i, "/#{r.destination_path}")
end
app.rewrite_inline_urls id: :asset_host,
url_extensions: options.exts || app.config[:asset_extensions],
source_extensions: options.sources,
ignore: options.ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url)
r.filters << ::Middleman::InlineURLRewriter.new(:asset_host,
app,
r,
after_filter: :asset_hash,
url_extensions: options.exts || app.config[:asset_extensions],
ignore: options.ignore,
proc: method(:rewrite_url))
end
end
Contract String, Or[String, Pathname], Any => String

View File

@ -1,19 +1,25 @@
# The Cache Buster extension
class Middleman::Extensions::CacheBuster < ::Middleman::Extension
option :exts, nil, 'List of extensions that get cache busters strings appended to them.'
option :sources, %w(.css .htm .html .js .php .xhtml), 'List of extensions that are searched for bustable assets.'
option :exts, nil, 'List of extensions that get cache busters strings appended to them.', set: true
option :sources, %w(.css .htm .html .js .php .xhtml), 'List of extensions that are searched for bustable assets.', set: true
option :ignore, [], 'Regexes of filenames to skip adding query strings to'
option :rewrite_ignore, [], 'Regexes of filenames to skip processing for path rewrites'
def initialize(app, options_hash={}, &block)
super
Contract ResourceList => ResourceList
def manipulate_resource_list(resources)
resources.each do |r|
next unless r.destination_path.end_with?('/', *options.sources)
next if Array(options.rewrite_ignore || []).any? do |i|
::Middleman::Util.path_match(i, "/#{r.destination_path}")
end
app.rewrite_inline_urls id: :cache_buster,
url_extensions: options.exts || app.config[:asset_extensions],
source_extensions: options.sources,
ignore: options.ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url)
r.filters << ::Middleman::InlineURLRewriter.new(:cache_buster,
app,
r,
url_extensions: options.exts || app.config[:asset_extensions],
ignore: options.ignore,
proc: method(:rewrite_url))
end
end
Contract String, Or[String, Pathname], Any => String

View File

@ -1,3 +1,5 @@
require 'set'
# Directory Indexes extension
class Middleman::Extensions::DirectoryIndexes < ::Middleman::Extension
# This should run after most other sitemap manipulators so that it
@ -11,7 +13,7 @@ class Middleman::Extensions::DirectoryIndexes < ::Middleman::Extension
index_file = app.config[:index_file]
new_index_path = "/#{index_file}"
extensions = %w(.htm .html .php .xhtml)
extensions = Set.new(%w(.htm .html .php .xhtml))
resources.each do |resource|
# Check if it would be pointless to reroute

View File

@ -10,7 +10,7 @@
# to .css, .htm, .html, .js, and .xhtml
#
class Middleman::Extensions::Gzip < ::Middleman::Extension
option :exts, %w(.css .htm .html .js .svg .xhtml), 'File extensions to Gzip when building.'
option :exts, %w(.css .htm .html .js .svg .xhtml), 'File extensions to Gzip when building.', set: true
option :ignore, [], 'Patterns to avoid gzipping'
option :overwrite, false, 'Overwrite original files instead of adding .gz extension.'

View File

@ -1,5 +1,5 @@
require 'memoist'
require 'middleman-core/contracts'
require 'rack/mime'
# Minify CSS Extension
class Middleman::Extensions::MinifyCss < ::Middleman::Extension
@ -9,17 +9,10 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
require 'sass'
SassCompressor
}, 'Set the CSS compressor to use.'
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'
option :content_types, %w(text/css), 'Content types of resources that contain CSS', set: true
option :inline_content_types, %w(text/html text/php), 'Content types of resources that contain inline CSS', set: true
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 +22,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

@ -9,122 +9,79 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
require 'uglifier'
::Uglifier.new
}, 'Set the JS compressor to use.'
option :content_types, %w(application/javascript), 'Content types of resources that contain JS'
option :inline_content_types, %w(text/html text/php), 'Content types of resources that contain inline JS'
option :content_types, %w(application/javascript), 'Content types of resources that contain JS', set: true
option :inline_content_types, %w(text/html text/php), 'Content types of resources that contain inline JS', set: true
def ready
# Setup Rack middleware to minify JS
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]
INLINE_JS_REGEX = /(<script[^>]*>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m
def initialize(app, options_hash={}, &block)
super
@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
# Rack middleware to look for JS and compress it
class Rack
extend Memoist
include Contracts
INLINE_JS_REGEX = /(<script[^>]*>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m
# 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)
type = headers['Content-Type'].try(:slice, /^[^;]*/)
@path = env['PATH_INFO']
minified = if @inline && minifiable_inline?(type)
minify_inline(::Middleman::Util.extract_response_text(response))
elsif minifiable?(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)
rescue ExecJS::ProgramError => e
warn "WARNING: Couldn't compress JavaScript in #{@path}: #{e.message}"
content
end
memoize :minify
# Detect and minify inline content
# @param [String] content
# @return [String]
def minify_inline(content)
content.gsub(INLINE_JS_REGEX) do |match|
first = $1
inline_content = $2
last = $3
# Only compress script tags that contain JavaScript (as opposed to
# something like jQuery templates, identified with a "text/html" type).
if !first.include?('type=') || first.include?('text/javascript')
first + minify(inline_content) + last
else
match
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
memoize :minify_inline
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)
rescue ::ExecJS::ProgramError => e
warn "WARNING: Couldn't compress JavaScript in #{@path}: #{e.message}"
content
end
memoize :minify
# Detect and minify inline content
Contract String => String
def minify_inline(content)
content.gsub(INLINE_JS_REGEX) do |match|
first = $1
inline_content = $2
last = $3
# Only compress script tags that contain JavaScript (as opposed to
# something like jQuery templates, identified with a "text/html" type).
if !first.include?('type=') || first.include?('text/javascript')
first + minify(inline_content) + last
else
match
end
end
end
memoize :minify_inline
end

View File

@ -1,26 +1,28 @@
require 'addressable/uri'
# Relative Assets extension
class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
option :exts, nil, 'List of extensions that get converted to relative paths.'
option :sources, %w(.css .htm .html .xhtml), 'List of extensions that are searched for relative assets.'
option :exts, nil, 'List of extensions that get converted to relative paths.', set: true
option :sources, %w(.css .htm .html .xhtml), 'List of extensions that are searched for relative assets.', set: true
option :ignore, [], 'Regexes of filenames to skip converting to relative paths.'
option :rewrite_ignore, [], 'Regexes of filenames to skip processing for path rewrites.'
option :helpers_only, false, 'Allow only Ruby helpers to change paths.'
def initialize(app, options_hash={}, &block)
super
Contract ResourceList => ResourceList
def manipulate_resource_list(resources)
return resources if options[:helpers_only]
if options[:helpers_only]
return
resources.each do |r|
next unless r.destination_path.end_with?('/', *options.sources)
next if Array(options.rewrite_ignore || []).any? do |i|
::Middleman::Util.path_match(i, "/#{r.destination_path}")
end
r.filters << ::Middleman::InlineURLRewriter.new(:relative_assets,
app,
r,
url_extensions: options.exts || app.config[:asset_extensions],
ignore: options.ignore,
proc: method(:rewrite_url))
end
app.rewrite_inline_urls id: :relative_assets,
url_extensions: options.exts || app.config[:asset_extensions],
source_extensions: options.sources,
ignore: options.ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url)
end
def mark_as_relative(file_path, opts, current_resource)
@ -53,7 +55,7 @@ class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
end
def asset_path(kind, source, options={})
super(kind, source, app.extensions[:relative_assets].mark_as_relative(super, options, current_resource))
super(kind, source, app.extensions[:relative_assets].mark_as_relative(super, options, current_resource))
end
end

View File

@ -0,0 +1,49 @@
require 'middleman-core/util'
require 'middleman-core/contracts'
module Middleman
class InlineURLRewriter
include Contracts
attr_reader :filter_name
attr_reader :after_filter
def initialize(filter_name, app, resource, options={})
@filter_name = filter_name
@app = app
@resource = resource
@options = options
@after_filter = @options.fetch(:after_filter, nil)
end
Contract String => String
def execute_filter(body)
path = "/#{@resource.destination_path}"
dirpath = ::Pathname.new(File.dirname(path))
::Middleman::Util.instrument 'inline_url_rewriter', path: path do
::Middleman::Util.rewrite_paths(body, path, @options.fetch(:url_extensions), @app) do |asset_path|
uri = ::Middleman::Util.parse_uri(asset_path)
relative_path = uri.host.nil?
full_asset_path = if relative_path
dirpath.join(asset_path).to_s
else
asset_path
end
exts = @options.fetch(:url_extensions)
next unless exts.include?(::File.extname(asset_path))
next if @options.fetch(:ignore).any? { |r| ::Middleman::Util.should_ignore?(r, full_asset_path) }
result = @options.fetch(:proc).call(asset_path, dirpath, path)
asset_path = result if result
asset_path
end
end
end
end
end

View File

@ -93,9 +93,6 @@ module Middleman
request_path = ::Middleman::Util.full_path(request_path, @middleman)
full_request_path = File.join(env['SCRIPT_NAME'], request_path) # Path including rack mount
# Run before callbacks
@middleman.execute_callbacks(:before)
# Get the resource object for this path
resource = @middleman.sitemap.find_resource_by_destination_path(request_path.gsub(' ', '%20'))

View File

@ -9,14 +9,13 @@ module Middleman
# Expose `endpoint`
expose_to_config :endpoint
EndpointDescriptor = Struct.new(:path, :request_path, :block) do
EndpointDescriptor = Struct.new(:path, :block) do
def execute_descriptor(app, resources)
r = EndpointResource.new(
r = ::Middleman::Sitemap::CallbackResource.new(
app.sitemap,
path,
request_path
&block
)
r.output = block if block
resources + [r]
end
@ -24,43 +23,10 @@ module Middleman
# Setup a proxy from a path to a target
# @param [String] path
# @param [Hash] opts The :path value gives a request path if it
# differs from the output path
Contract String, Or[{ path: String }, Proc] => EndpointDescriptor
def endpoint(path, opts={}, &block)
if block_given?
EndpointDescriptor.new(path, path, block)
else
EndpointDescriptor.new(path, opts[:path] || path, nil)
end
end
end
class EndpointResource < ::Middleman::Sitemap::Resource
Contract Maybe[Proc]
attr_accessor :output
def initialize(store, path, request_path)
super(store, path)
@request_path = ::Middleman::Util.normalize_path(request_path)
end
Contract String
attr_reader :request_path
Contract Bool
def template?
true
end
Contract Args[Any] => String
def render(*)
return output.call if output
end
Contract Bool
def ignored?
false
Contract String, Proc => EndpointDescriptor
def endpoint(path, &block)
EndpointDescriptor.new(path, block)
end
end
end

View File

@ -3,6 +3,7 @@ require 'middleman-core/sitemap/extensions/traversal'
require 'middleman-core/file_renderer'
require 'middleman-core/template_renderer'
require 'middleman-core/contracts'
require 'middleman-core/inline_url_rewriter'
module Middleman
# Sitemap namespace
@ -39,24 +40,26 @@ module Middleman
attr_reader :metadata
attr_accessor :ignored
attr_accessor :filters
# Initialize resource with parent store and URL
# @param [Middleman::Sitemap::Store] store
# @param [String] path
# @param [String] source
Contract IsA['Middleman::Sitemap::Store'], String, Maybe[Or[IsA['Middleman::SourceFile'], String]] => Any
def initialize(store, path, source=nil)
Contract IsA['Middleman::Sitemap::Store'], String, Maybe[Any] => Any
def initialize(store, path, source_file=nil)
@store = store
@app = @store.app
@path = path
@ignored = false
@filters = []
source = Pathname(source) if source && source.is_a?(String)
source_file = Pathname(source_file) if source_file && source_file.is_a?(String)
@file_descriptor = if source && source.is_a?(Pathname)
::Middleman::SourceFile.new(source.relative_path_from(@app.source_dir), source, @app.source_dir, Set.new([:source]), 0)
@file_descriptor = if source_file && source_file.is_a?(Pathname)
::Middleman::SourceFile.new(source_file.relative_path_from(@app.source_dir), source_file, @app.source_dir, Set.new([:source]), 0)
else
source
source_file
end
@destination_path = @path
@ -138,8 +141,45 @@ module Middleman
# Render this resource
# @return [String]
Contract Hash, Hash => String
# Contract Maybe[Hash], Maybe[Hash], Maybe[Proc] => String
def render(opts={}, locs={})
body = render_without_filters(opts, locs)
return body if @filters.empty?
sortable_filters = @filters.select { |f| f.respond_to?(:filter_name) }.sort do |a, b|
if b.after_filter == a.filter_name
1
else
-1
end
end.reverse
n = 0
sorted_filters = @filters.sort_by do |m|
n += 1
idx = sortable_filters.index(m)
[idx.nil? ? 0 : idx, n]
end
sorted_filters.reduce(body) do |output, filter|
if block_given? && !yield(filter)
output
elsif 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
@ -151,7 +191,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
@ -204,9 +244,10 @@ module Middleman
end
class StringResource < Resource
def initialize(store, path, contents=nil, &block)
Contract IsA['Middleman::Sitemap::Store'], String, Maybe[Or[String, Proc]] => Any
def initialize(store, path, contents)
@request_path = path
@contents = block_given? ? block : contents
@contents = contents
super(store, path)
end
@ -215,7 +256,28 @@ module Middleman
end
def render(*)
@contents.respond_to?(:call) ? @contents.call : @contents
@contents
end
def binary?
false
end
end
class CallbackResource < Resource
Contract IsA['Middleman::Sitemap::Store'], String, Proc => Any
def initialize(store, path, &block)
@request_path = path
@contents = block
super(store, path)
end
def template?
true
end
def render(*)
@contents.call
end
def binary?

View File

@ -94,7 +94,7 @@ module Middleman
.transpose
.map(&::Regexp.method(:union))
match = /
/
\A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
(?<start>#{start_delims})[ ]*\r?\n
(?<frontmatter>.*?)[ ]*\r?\n?

View File

@ -56,10 +56,8 @@ module Middleman
Contract String => String
def step_through_extensions(path)
while ::Middleman::Util.tilt_class(path)
ext = ::File.extname(path)
break if ext.empty?
while ext = File.extname(path)
break if ext.empty? || !::Middleman::Util.tilt_class(ext)
yield ext if block_given?
# Strip templating extensions as long as Tilt knows them

View File

@ -25,13 +25,15 @@ module Middleman
def tilt_class(path)
::Tilt[path]
end
memoize :tilt_class
# memoize :tilt_class
# Normalize a path to not include a leading slash
# @param [String] path
# @return [String]
Contract String => String
Contract Any => String
def normalize_path(path)
return path unless path.is_a?(String)
# The tr call works around a bug in Ruby's Unicode handling
::URI.decode(path).sub(%r{^/}, '').tr('', '')
end

View File

@ -6,22 +6,9 @@ module Middleman
module_function
# Extract the text of a Rack response as a string.
# Useful for extensions implemented as Rack middleware.
# @param response The response from #call
# @return [String] The whole response as a string.
Contract RespondTo[:each] => String
def extract_response_text(response)
# The rack spec states all response bodies must respond to each
result = ''
response.each do |part, _|
result << part
end
result
end
Contract String, String, ArrayOf[String], IsA['::Middleman::Application'], Proc => String
Contract String, String, SetOf[String], IsA['::Middleman::Application'], Proc => String
def rewrite_paths(body, path, exts, app, &_block)
exts = exts.sort_by(&:length).reverse
matcher = /([\'\"\(,]\s*|# sourceMappingURL=)([^\s\'\"\)\(>]+(#{::Regexp.union(exts)}))/
url_fn_prefix = 'url('

View File

@ -1,5 +1,5 @@
module Middleman
# Current Version
# @return [String]
VERSION = '4.1.7'.freeze unless const_defined?(:VERSION)
VERSION = '4.2.0'.freeze unless const_defined?(:VERSION)
end