1
0
Fork 0
mirror of https://github.com/middleman/middleman.git synced 2022-11-09 12:20:27 -05:00

Merge branch 'master' of github.com:middleman/middleman

This commit is contained in:
Thomas Reynolds 2018-11-11 15:01:26 -08:00
commit 08dbfb6e32
45 changed files with 401 additions and 582 deletions

View file

@ -1,6 +1,7 @@
master
===
* Remove Rack support in favor of `resource.filters << proc { |oldbody| newbody }`
* `manipulate_resource_list_container!` as a faster, less functional approach.
# 4.3.0.rc.4

76
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at me@tdreyno.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View file

@ -189,23 +189,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-08ee47a7.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-41d4c812.css'
When I go to "stylesheets/site-41d4c812.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,6 +3,7 @@ Feature: Extension author could use some hooks
Scenario: When build
Given a successfully built app at "extension-api-deprecations-app"
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

@ -5,7 +5,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,66 +0,0 @@
@wip
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,16 +0,0 @@
class Middleware
def initialize(app)
@app = app
end
def call(env)
status, headers, response = @app.call(env)
body = ''
response.each { |part| body += part }
if /css$/.match?(env['PATH_INFO'])
body += "\n/* Added by Rack filter */"
status, headers, response = Rack::Response.new(body, status, headers).finish
end
[status, headers, response]
end
end

View file

@ -1,16 +0,0 @@
class Middleware
def initialize(app)
@app = app
end
def call(env)
status, headers, response = @app.call(env)
body = ''
response.each { |part| body += part }
if /css$/.match?(env['PATH_INFO'])
body += "\n/* Added by Rack filter */"
status, headers, response = Rack::Response.new(body, status, headers).finish
end
[status, headers, response]
end
end

View file

@ -1,16 +0,0 @@
class Middleware
def initialize(app)
@app = app
end
def call(env)
status, headers, response = @app.call(env)
body = ''
response.each { |part| body += part }
if /css$/.match?(env['PATH_INFO'])
body += "\n/* Added by Rack filter */"
status, headers, response = Rack::Response.new(body, status, headers).finish
end
[status, headers, response]
end
end

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

@ -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'
@ -37,9 +36,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
@ -256,15 +252,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 StandardError => 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,33 @@ 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 = (Set.new(self.default) if self.default && options[:set] && self.default.is_a?(Array))
end
# The user-supplied value for this setting, overriding the default
def value=(value)
@value = value
@value_set = true
@array_wrapped_value = (Set.new(@value) if @value && options[:set] && @value.is_a?(Array))
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 || @value
else
@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,137 +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: SetOf[String],
source_extensions: SetOf[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(Set.new) do |sum, rewriter|
sum + rewriter[:source_extensions]
end
@source_exts_regex_text = Regexp.union(all_source_exts.to_a).to_s
@all_asset_exts = @rewriters.reduce(Set.new) do |sum, rewriter|
sum + rewriter[:url_extensions]
end
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}$)/
body = ::Middleman::Util.extract_response_text(response)
return [status, headers, response] unless body
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

@ -61,8 +61,9 @@ 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.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.
# * `app.ready { ... }` - Run code once Middleman is ready to serve or build files (after `after_configuration`).
#
# @see http://middlemanapp.com/advanced/custom/ Middleman Custom Extensions Documentation

View file

@ -1,5 +1,4 @@
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.'
@ -11,9 +10,7 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
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/]
@ -24,14 +21,6 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
# to be named "favicon.ico"
@set_of_exts = Set.new(options.exts || (app.config[:asset_extensions] - %w[.ico]))
@set_of_sources = Set.new options.sources
app.rewrite_inline_urls id: :asset_hash,
url_extensions: @set_of_exts,
source_extensions: @set_of_sources,
ignore: @ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url),
after: :asset_host
end
Contract String, Or[String, Pathname], Any => Maybe[String]
@ -51,16 +40,23 @@ 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
# Update the main sitemap resource list
Contract IsA['Middleman::Sitemap::ResourceListContainer'] => Any
def manipulate_resource_list_container!(resource_list)
@rack_client ||= begin
rack_app = ::Middleman::Rack.new(app).to_app
::Rack::MockRequest.new(rack_app)
resource_list.by_extensions(@set_of_sources).each do |r|
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: @set_of_exts,
ignore: options.ignore,
proc: method(:rewrite_url))
end
# Process resources in order: binary images and fonts, then SVG, then JS/CSS.
@ -89,15 +85,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_list.update!(resource, :destination_path) do

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.'
@ -13,13 +11,23 @@ class Middleman::Extensions::AssetHost < ::Middleman::Extension
require 'set'
@set_of_exts = Set.new(options.exts || app.config[:asset_extensions])
@set_of_sources = Set.new options.sources
end
app.rewrite_inline_urls id: :asset_host,
url_extensions: @set_of_exts,
source_extensions: @set_of_sources,
ignore: options.ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url)
Contract IsA['Middleman::Sitemap::ResourceListContainer'] => Any
def manipulate_resource_list_container!(resource_list)
resource_list.by_extensions(@set_of_sources).each do |r|
next if Array(options.rewrite_ignore || []).any? do |i|
::Middleman::Util.path_match(i, "/#{r.destination_path}")
end
r.filters << ::Middleman::InlineURLRewriter.new(:asset_host,
app,
r,
after_filter: :asset_hash,
url_extensions: @set_of_exts,
ignore: options.ignore,
proc: method(:rewrite_url))
end
end
Contract String, Or[String, Pathname], Any => String

View file

@ -11,13 +11,22 @@ class Middleman::Extensions::CacheBuster < ::Middleman::Extension
require 'set'
@set_of_exts = Set.new(options.exts || app.config[:asset_extensions])
@set_of_sources = Set.new options.sources
end
app.rewrite_inline_urls id: :cache_buster,
url_extensions: @set_of_exts,
source_extensions: @set_of_sources,
ignore: options.ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url)
Contract IsA['Middleman::Sitemap::ResourceListContainer'] => Any
def manipulate_resource_list_container!(resource_list)
resource_list.by_extensions(@set_of_sources).each do |r|
next if Array(options.rewrite_ignore || []).any? do |i|
::Middleman::Util.path_match(i, "/#{r.destination_path}")
end
r.filters << ::Middleman::InlineURLRewriter.new(:cache_buster,
app,
r,
url_extensions: @set_of_exts,
ignore: options.ignore,
proc: method(:rewrite_url))
end
end
Contract String, Or[String, Pathname], Any => String

View file

@ -1,6 +1,7 @@
require 'active_support/core_ext/object/try'
require 'memoist'
require 'middleman-core/contracts'
require 'rack/mime'
# Minify CSS Extension
class Middleman::Extensions::MinifyCss < ::Middleman::Extension
@ -13,14 +14,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.freeze
class SassCompressor
def self.compress(style, options = {})
@ -30,97 +24,62 @@ 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.freeze
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::Environment] 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'] = minified.bytesize.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
Regexp.last_match(1) + minify(Regexp.last_match(2)) + Regexp.last_match(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 IsA['Middleman::Sitemap::ResourceListContainer'] => Any
def manipulate_resource_list_container!(resource_list)
resource_list.by_binary(false).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
# @param [String] content
# @return [String]
def minify_inline(content)
content.gsub(INLINE_CSS_REGEX) do
Regexp.last_match(1) + minify(Regexp.last_match(2)) + Regexp.last_match(3)
end
end
memoize :minify_inline
end

View file

@ -10,122 +10,79 @@ class Middleman::Extensions::MinifyJavaScript < ::Middleman::Extension
require 'uglifier'
::Uglifier.new
}, 'Set the JS compressor to use.'
option :content_types, %w[application/javascript application/ld+json], 'Content types of resources that contain JS'
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'
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.freeze
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.freeze
# 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::Environment] 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'] = minified.bytesize.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 = Regexp.last_match(1)
inline_content = Regexp.last_match(2)
last = Regexp.last_match(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 IsA['Middleman::Sitemap::ResourceListContainer'] => Any
def manipulate_resource_list_container!(resource_list)
resource_list.by_binary(false).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 = Regexp.last_match(1)
inline_content = Regexp.last_match(2)
last = Regexp.last_match(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,5 +1,3 @@
require 'addressable/uri'
# Relative Assets extension
class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
option :exts, nil, 'List of extensions that get converted to relative paths.'
@ -11,18 +9,27 @@ class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
def initialize(app, options_hash = {}, &block)
super
return if options[:helpers_only]
require 'set'
@set_of_exts = Set.new(options.exts || app.config[:asset_extensions])
@set_of_sources = Set.new options.sources
end
app.rewrite_inline_urls id: :relative_assets,
url_extensions: @set_of_exts,
source_extensions: @set_of_sources,
ignore: options.ignore,
rewrite_ignore: options.rewrite_ignore,
proc: method(:rewrite_url)
Contract IsA['Middleman::Sitemap::ResourceListContainer'] => Any
def manipulate_resource_list_container!(resource_list)
return if options[:helpers_only]
resource_list.by_extensions(@set_of_sources).each do |r|
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: @set_of_exts,
ignore: options.ignore,
proc: method(:rewrite_url))
end
end
def mark_as_relative(file_path, opts, current_resource)

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

@ -91,9 +91,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

@ -24,7 +24,6 @@ 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)

View file

@ -4,6 +4,7 @@ require 'middleman-core/file_renderer'
require 'middleman-core/template_renderer'
require 'middleman-core/contracts'
require 'set'
require 'middleman-core/inline_url_rewriter'
module Middleman
# Sitemap namespace
@ -40,6 +41,7 @@ module Middleman
attr_reader :metadata
attr_accessor :ignored
attr_accessor :filters
# Initialize resource with parent store and URL
# @param [Middleman::Sitemap::Store] store
@ -51,6 +53,7 @@ module Middleman
@app = @store.app
@path = path
@ignored = false
@filters = []
source = Pathname(source) if source&.is_a?(String)
@ -142,8 +145,45 @@ module Middleman
# Render this resource
# @return [String]
Contract Hash, Hash, Maybe[Proc] => String
def render(opts = {}, locs = {}, &_block)
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(opts = {}, locs = {})
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 +195,7 @@ module Middleman
opts[:layout] = false if !opts.key?(:layout) && !@set_of_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
@ -239,9 +279,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
@ -250,7 +291,7 @@ module Middleman
end
def render(*)
@contents.respond_to?(:call) ? @contents.call : @contents
@contents
end
def binary?

View file

@ -56,9 +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?

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