Make Frontmatter a class-based extension to simplify file
This commit is contained in:
parent
5104579623
commit
950aace674
|
@ -38,6 +38,7 @@ module Middleman
|
||||||
# @private
|
# @private
|
||||||
def registered(app)
|
def registered(app)
|
||||||
app.define_hook :initialized
|
app.define_hook :initialized
|
||||||
|
app.define_hook :instance_available
|
||||||
app.define_hook :after_configuration
|
app.define_hook :after_configuration
|
||||||
app.define_hook :before_configuration
|
app.define_hook :before_configuration
|
||||||
app.define_hook :build_config
|
app.define_hook :build_config
|
||||||
|
@ -69,15 +70,19 @@ module Middleman
|
||||||
# @param [Hash] options Per-extension options hash
|
# @param [Hash] options Per-extension options hash
|
||||||
# @return [void]
|
# @return [void]
|
||||||
def register(extension, options={}, &block)
|
def register(extension, options={}, &block)
|
||||||
extend extension
|
if extension.instance_of?(Class) && extension.ancestors.include?(::Middleman::Extension)
|
||||||
if extension.respond_to?(:registered)
|
extension.new(self, options, &block)
|
||||||
if extension.method(:registered).arity === 1
|
else
|
||||||
extension.registered(self, &block)
|
extend extension
|
||||||
else
|
if extension.respond_to?(:registered)
|
||||||
extension.registered(self, options, &block)
|
if extension.method(:registered).arity === 1
|
||||||
|
extension.registered(self, &block)
|
||||||
|
else
|
||||||
|
extension.registered(self, options, &block)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
extension
|
||||||
end
|
end
|
||||||
extension
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,11 +138,12 @@ module Middleman
|
||||||
super
|
super
|
||||||
|
|
||||||
self.class.inst = self
|
self.class.inst = self
|
||||||
run_hook :before_configuration
|
|
||||||
|
|
||||||
# Search the root of the project for required files
|
# Search the root of the project for required files
|
||||||
$LOAD_PATH.unshift(root)
|
$LOAD_PATH.unshift(root)
|
||||||
|
|
||||||
|
run_hook :initialized
|
||||||
|
|
||||||
if config[:autoload_sprockets]
|
if config[:autoload_sprockets]
|
||||||
begin
|
begin
|
||||||
require "middleman-sprockets"
|
require "middleman-sprockets"
|
||||||
|
@ -146,6 +152,8 @@ module Middleman
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
run_hook :before_configuration
|
||||||
|
|
||||||
# Check for and evaluate local configuration
|
# Check for and evaluate local configuration
|
||||||
local_config = File.join(root, "config.rb")
|
local_config = File.join(root, "config.rb")
|
||||||
if File.exists? local_config
|
if File.exists? local_config
|
||||||
|
@ -156,7 +164,7 @@ module Middleman
|
||||||
run_hook :build_config if build?
|
run_hook :build_config if build?
|
||||||
run_hook :development_config if development?
|
run_hook :development_config if development?
|
||||||
|
|
||||||
run_hook :initialized
|
run_hook :instance_available
|
||||||
|
|
||||||
# This is for making the tests work - since the tests
|
# This is for making the tests work - since the tests
|
||||||
# don't completely reload middleman, I18n.load_path can get
|
# don't completely reload middleman, I18n.load_path can get
|
||||||
|
|
|
@ -1,187 +1,55 @@
|
||||||
require "active_support/core_ext/hash/keys"
|
require "active_support/core_ext/hash/keys"
|
||||||
require 'pathname'
|
require 'pathname'
|
||||||
|
|
||||||
|
# Parsing YAML frontmatter
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
|
# Parsing JSON frontmatter
|
||||||
|
require "active_support/json"
|
||||||
|
|
||||||
# Extensions namespace
|
# Extensions namespace
|
||||||
module Middleman::CoreExtensions
|
module Middleman::CoreExtensions
|
||||||
|
|
||||||
# Frontmatter namespace
|
class FrontMatter < ::Middleman::Extension
|
||||||
module FrontMatter
|
|
||||||
|
|
||||||
# Setup extension
|
YAML_ERRORS = [ StandardError ]
|
||||||
class << self
|
|
||||||
|
|
||||||
# Once registered
|
# https://github.com/tenderlove/psych/issues/23
|
||||||
def registered(app)
|
if defined?(Psych) && defined?(Psych::SyntaxError)
|
||||||
# Parsing YAML frontmatter
|
YAML_ERRORS << Psych::SyntaxError
|
||||||
require "yaml"
|
|
||||||
|
|
||||||
# Parsing JSON frontmatter
|
|
||||||
require "active_support/json"
|
|
||||||
|
|
||||||
app.send :include, InstanceMethods
|
|
||||||
|
|
||||||
app.before_configuration do
|
|
||||||
files.changed { |file| frontmatter_manager.clear_data(file) }
|
|
||||||
files.deleted { |file| frontmatter_manager.clear_data(file) }
|
|
||||||
end
|
|
||||||
|
|
||||||
app.after_configuration do
|
|
||||||
::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods
|
|
||||||
|
|
||||||
ignore %r{\.frontmatter$}
|
|
||||||
|
|
||||||
sitemap.provides_metadata do |path|
|
|
||||||
fmdata = frontmatter_manager.data(path).first || {}
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
[:layout, :layout_engine].each do |opt|
|
|
||||||
data[opt] = fmdata[opt] unless fmdata[opt].nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
{ :options => data, :page => ::Middleman::Util.recursively_enhance(fmdata).freeze }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
alias :included :registered
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class FrontmatterManager
|
def initialize(app, options_hash={}, &block)
|
||||||
attr_reader :app
|
super
|
||||||
delegate :logger, :to => :app
|
|
||||||
|
|
||||||
def initialize(app)
|
@cache = {}
|
||||||
@app = app
|
end
|
||||||
@cache = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def data(path)
|
def before_configuration
|
||||||
p = normalize_path(path)
|
ext = self
|
||||||
@cache[p] ||= begin
|
app.files.changed { |file| ext.clear_data(file) }
|
||||||
file_data, content = frontmatter_and_content(p)
|
app.files.deleted { |file| ext.clear_data(file) }
|
||||||
|
end
|
||||||
|
|
||||||
if @app.files.exists?("#{path}.frontmatter")
|
def after_configuration
|
||||||
external_data, _ = frontmatter_and_content("#{p}.frontmatter")
|
app.extensions[:frontmatter] = self
|
||||||
|
app.ignore %r{\.frontmatter$}
|
||||||
|
|
||||||
[
|
::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods
|
||||||
external_data.deep_merge(file_data),
|
|
||||||
content
|
|
||||||
]
|
|
||||||
else
|
|
||||||
[file_data, content]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_data(file)
|
app.sitemap.provides_metadata do |path|
|
||||||
# Copied from Sitemap::Store#file_to_path, but without
|
fmdata = data(path).first || {}
|
||||||
# removing the file extension
|
|
||||||
file = File.join(@app.root, file)
|
|
||||||
prefix = @app.source_dir.sub(/\/$/, "") + "/"
|
|
||||||
return unless file.include?(prefix)
|
|
||||||
path = file.sub(prefix, "").sub(/\.frontmatter$/, "")
|
|
||||||
|
|
||||||
@cache.delete(path)
|
|
||||||
end
|
|
||||||
|
|
||||||
YAML_ERRORS = [ StandardError ]
|
|
||||||
|
|
||||||
# https://github.com/tenderlove/psych/issues/23
|
|
||||||
if defined?(Psych) && defined?(Psych::SyntaxError)
|
|
||||||
YAML_ERRORS << Psych::SyntaxError
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse YAML frontmatter out of a string
|
|
||||||
# @param [String] content
|
|
||||||
# @return [Array<Hash, String>]
|
|
||||||
def parse_yaml_front_matter(content)
|
|
||||||
yaml_regex = /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
|
||||||
if content =~ yaml_regex
|
|
||||||
content = content.sub(yaml_regex, "")
|
|
||||||
|
|
||||||
begin
|
|
||||||
data = YAML.load($1) || {}
|
|
||||||
data = data.symbolize_keys
|
|
||||||
rescue *YAML_ERRORS => e
|
|
||||||
logger.error "YAML Exception: #{e.message}"
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
[data, content]
|
|
||||||
rescue
|
|
||||||
[{}, content]
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_json_front_matter(content)
|
|
||||||
json_regex = /\A(;;;\s*\n.*?\n?)^(;;;\s*$\n?)/m
|
|
||||||
|
|
||||||
if content =~ json_regex
|
|
||||||
content = content.sub(json_regex, "")
|
|
||||||
|
|
||||||
begin
|
|
||||||
json = ($1+$2).sub(";;;", "{").sub(";;;", "}")
|
|
||||||
data = ActiveSupport::JSON.decode(json).symbolize_keys
|
|
||||||
rescue => e
|
|
||||||
logger.error "JSON Exception: #{e.message}"
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
[data, content]
|
|
||||||
rescue
|
|
||||||
[{}, content]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get the frontmatter and plain content from a file
|
|
||||||
# @param [String] path
|
|
||||||
# @return [Array<Thor::CoreExt::HashWithIndifferentAccess, String>]
|
|
||||||
def frontmatter_and_content(path)
|
|
||||||
full_path = if Pathname(path).relative?
|
|
||||||
File.join(@app.source_dir, path)
|
|
||||||
else
|
|
||||||
path
|
|
||||||
end
|
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
content = nil
|
[:layout, :layout_engine].each do |opt|
|
||||||
|
data[opt] = fmdata[opt] unless fmdata[opt].nil?
|
||||||
return [data, content] unless @app.files.exists?(full_path)
|
|
||||||
|
|
||||||
if !::Middleman::Util.binary?(full_path)
|
|
||||||
content = File.read(full_path)
|
|
||||||
|
|
||||||
begin
|
|
||||||
if content =~ /\A.*coding:/
|
|
||||||
lines = content.split(/\n/)
|
|
||||||
lines.shift
|
|
||||||
content = lines.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
if result = parse_yaml_front_matter(content)
|
|
||||||
data, content = result
|
|
||||||
elsif result = parse_json_front_matter(content)
|
|
||||||
data, content = result
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
# Probably a binary file, move on
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
[data, content]
|
{ :options => data, :page => ::Middleman::Util.recursively_enhance(fmdata).freeze }
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_path(path)
|
|
||||||
path.sub(%r{^#{@app.source_dir}\/}, "")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module ResourceInstanceMethods
|
module ResourceInstanceMethods
|
||||||
|
|
||||||
def ignored?
|
def ignored?
|
||||||
if !proxy? && raw_data[:ignored] == true
|
if !proxy? && raw_data[:ignored] == true
|
||||||
true
|
true
|
||||||
|
@ -195,7 +63,7 @@ module Middleman::CoreExtensions
|
||||||
# @private
|
# @private
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def raw_data
|
def raw_data
|
||||||
app.frontmatter_manager.data(source_file).first
|
app.extensions[:frontmatter].data(source_file).first
|
||||||
end
|
end
|
||||||
|
|
||||||
# This page's frontmatter
|
# This page's frontmatter
|
||||||
|
@ -217,19 +85,132 @@ module Middleman::CoreExtensions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
helpers do
|
||||||
|
|
||||||
# Access the Frontmatter API
|
|
||||||
def frontmatter_manager
|
|
||||||
@_frontmatter_manager ||= FrontmatterManager.new(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get the template data from a path
|
# Get the template data from a path
|
||||||
# @param [String] path
|
# @param [String] path
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def template_data_for_file(path)
|
def template_data_for_file(path)
|
||||||
frontmatter_manager.data(path).last
|
extensions[:frontmatter].data(path).last
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def data(path)
|
||||||
|
p = normalize_path(path)
|
||||||
|
@cache[p] ||= begin
|
||||||
|
file_data, content = frontmatter_and_content(p)
|
||||||
|
|
||||||
|
if app.files.exists?("#{path}.frontmatter")
|
||||||
|
external_data, _ = frontmatter_and_content("#{p}.frontmatter")
|
||||||
|
|
||||||
|
[
|
||||||
|
external_data.deep_merge(file_data),
|
||||||
|
content
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[file_data, content]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_data(file)
|
||||||
|
# Copied from Sitemap::Store#file_to_path, but without
|
||||||
|
# removing the file extension
|
||||||
|
file = File.join(app.root, file)
|
||||||
|
prefix = app.source_dir.sub(/\/$/, "") + "/"
|
||||||
|
return unless file.include?(prefix)
|
||||||
|
path = file.sub(prefix, "").sub(/\.frontmatter$/, "")
|
||||||
|
|
||||||
|
@cache.delete(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Parse YAML frontmatter out of a string
|
||||||
|
# @param [String] content
|
||||||
|
# @return [Array<Hash, String>]
|
||||||
|
def parse_yaml_front_matter(content)
|
||||||
|
yaml_regex = /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
||||||
|
if content =~ yaml_regex
|
||||||
|
content = content.sub(yaml_regex, "")
|
||||||
|
|
||||||
|
begin
|
||||||
|
data = YAML.load($1) || {}
|
||||||
|
data = data.symbolize_keys
|
||||||
|
rescue *YAML_ERRORS => e
|
||||||
|
app.logger.error "YAML Exception: #{e.message}"
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
[data, content]
|
||||||
|
rescue
|
||||||
|
[{}, content]
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_json_front_matter(content)
|
||||||
|
json_regex = /\A(;;;\s*\n.*?\n?)^(;;;\s*$\n?)/m
|
||||||
|
|
||||||
|
if content =~ json_regex
|
||||||
|
content = content.sub(json_regex, "")
|
||||||
|
|
||||||
|
begin
|
||||||
|
json = ($1+$2).sub(";;;", "{").sub(";;;", "}")
|
||||||
|
data = ActiveSupport::JSON.decode(json).symbolize_keys
|
||||||
|
rescue => e
|
||||||
|
app.logger.error "JSON Exception: #{e.message}"
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
[data, content]
|
||||||
|
rescue
|
||||||
|
[{}, content]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get the frontmatter and plain content from a file
|
||||||
|
# @param [String] path
|
||||||
|
# @return [Array<Thor::CoreExt::HashWithIndifferentAccess, String>]
|
||||||
|
def frontmatter_and_content(path)
|
||||||
|
full_path = if Pathname(path).relative?
|
||||||
|
File.join(app.source_dir, path)
|
||||||
|
else
|
||||||
|
path
|
||||||
|
end
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
content = nil
|
||||||
|
|
||||||
|
return [data, content] unless app.files.exists?(full_path)
|
||||||
|
|
||||||
|
if !::Middleman::Util.binary?(full_path)
|
||||||
|
content = File.read(full_path)
|
||||||
|
|
||||||
|
begin
|
||||||
|
if content =~ /\A.*coding:/
|
||||||
|
lines = content.split(/\n/)
|
||||||
|
lines.shift
|
||||||
|
content = lines.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
if result = parse_yaml_front_matter(content)
|
||||||
|
data, content = result
|
||||||
|
elsif result = parse_json_front_matter(content)
|
||||||
|
data, content = result
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
# Probably a binary file, move on
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[data, content]
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_path(path)
|
||||||
|
path.sub(%r{^#{app.source_dir}\/}, "")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -133,7 +133,8 @@ module Middleman
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_accessor :app, :options
|
attr_accessor :options
|
||||||
|
attr_reader :app
|
||||||
|
|
||||||
def initialize(klass, options_hash={})
|
def initialize(klass, options_hash={})
|
||||||
@_helpers = []
|
@_helpers = []
|
||||||
|
@ -148,14 +149,21 @@ module Middleman
|
||||||
yield @options if block_given?
|
yield @options if block_given?
|
||||||
|
|
||||||
ext = self
|
ext = self
|
||||||
|
|
||||||
klass.initialized do
|
klass.initialized do
|
||||||
ext.app = self
|
ext.app = self
|
||||||
|
end
|
||||||
|
|
||||||
(ext.class.defined_helpers || []).each do |m|
|
if ext.respond_to?(:before_configuration)
|
||||||
ext.app.class.send(:include, m)
|
klass.before_configuration do
|
||||||
|
ext.before_configuration
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
klass.instance_available do
|
||||||
|
ext.app ||= self
|
||||||
|
end
|
||||||
|
|
||||||
klass.after_configuration do
|
klass.after_configuration do
|
||||||
if ext.respond_to?(:after_configuration)
|
if ext.respond_to?(:after_configuration)
|
||||||
ext.after_configuration
|
ext.after_configuration
|
||||||
|
@ -176,5 +184,13 @@ module Middleman
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def app=(app)
|
||||||
|
@app = app
|
||||||
|
|
||||||
|
(self.class.defined_helpers || []).each do |m|
|
||||||
|
app.class.send(:include, m)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,7 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
|
||||||
|
|
||||||
# Rack middleware to look for CSS and compress it
|
# Rack middleware to look for CSS and compress it
|
||||||
class Rack
|
class Rack
|
||||||
|
INLINE_CSS_REGEX = /(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m
|
||||||
|
|
||||||
# Init
|
# Init
|
||||||
# @param [Class] app
|
# @param [Class] app
|
||||||
|
@ -46,26 +47,16 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
|
||||||
def call(env)
|
def call(env)
|
||||||
status, headers, response = @app.call(env)
|
status, headers, response = @app.call(env)
|
||||||
|
|
||||||
path = env["PATH_INFO"]
|
if inline_html_content?(env["PATH_INFO"])
|
||||||
|
minified = ::Middleman::Util.extract_response_text(response)
|
||||||
if (path.end_with?('.html') || path.end_with?('.php')) && @inline
|
minified.gsub!(INLINE_CSS_REGEX) do |match|
|
||||||
uncompressed_source = ::Middleman::Util.extract_response_text(response)
|
$1 << @compressor.compress($2) << $3
|
||||||
|
|
||||||
minified = uncompressed_source.gsub(/(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m) do |match|
|
|
||||||
first = $1
|
|
||||||
css = $2
|
|
||||||
last = $3
|
|
||||||
|
|
||||||
minified_css = @compressor.compress(css)
|
|
||||||
|
|
||||||
first << minified_css << last
|
|
||||||
end
|
end
|
||||||
|
|
||||||
headers["Content-Length"] = ::Rack::Utils.bytesize(minified).to_s
|
headers["Content-Length"] = ::Rack::Utils.bytesize(minified).to_s
|
||||||
response = [minified]
|
response = [minified]
|
||||||
elsif path.end_with?('.css') && @ignore.none? {|ignore| Middleman::Util.path_match(ignore, path) }
|
elsif standalone_css_content?(env["PATH_INFO"])
|
||||||
uncompressed_source = ::Middleman::Util.extract_response_text(response)
|
minified_css = @compressor.compress(::Middleman::Util.extract_response_text(response))
|
||||||
minified_css = @compressor.compress(uncompressed_source)
|
|
||||||
|
|
||||||
headers["Content-Length"] = ::Rack::Utils.bytesize(minified_css).to_s
|
headers["Content-Length"] = ::Rack::Utils.bytesize(minified_css).to_s
|
||||||
response = [minified_css]
|
response = [minified_css]
|
||||||
|
@ -73,5 +64,14 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
|
||||||
|
|
||||||
[status, headers, response]
|
[status, headers, response]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def inline_html_content?(path)
|
||||||
|
(path.end_with?('.html') || path.end_with?('.php')) && @inline
|
||||||
|
end
|
||||||
|
|
||||||
|
def standalone_css_content?(path)
|
||||||
|
path.end_with?('.css') && @ignore.none? {|ignore| Middleman::Util.path_match(ignore, path) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue