Make Frontmatter a class-based extension to simplify file

This commit is contained in:
Thomas Reynolds 2013-05-24 17:11:46 -07:00
parent 5104579623
commit 950aace674
5 changed files with 206 additions and 201 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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