2013-12-28 01:26:31 +01:00
|
|
|
require 'active_support/core_ext/hash/keys'
|
2013-02-06 22:19:09 +01:00
|
|
|
require 'pathname'
|
2012-10-25 00:02:32 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
# Parsing YAML frontmatter
|
2013-12-28 01:26:31 +01:00
|
|
|
require 'yaml'
|
2012-08-14 00:39:06 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
# Parsing JSON frontmatter
|
2013-12-28 01:26:31 +01:00
|
|
|
require 'active_support/json'
|
2012-08-14 00:39:06 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
# Extensions namespace
|
|
|
|
module Middleman::CoreExtensions
|
|
|
|
class FrontMatter < ::Middleman::Extension
|
2014-05-31 07:32:39 +02:00
|
|
|
# Try to run after routing but before directory_indexes
|
|
|
|
self.resource_list_manipulator_priority = 90
|
|
|
|
|
2014-04-29 19:50:21 +02:00
|
|
|
YAML_ERRORS = [StandardError]
|
2012-08-14 00:39:06 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
# https://github.com/tenderlove/psych/issues/23
|
|
|
|
if defined?(Psych) && defined?(Psych::SyntaxError)
|
|
|
|
YAML_ERRORS << Psych::SyntaxError
|
2012-05-09 06:05:55 +02:00
|
|
|
end
|
2012-08-14 00:39:06 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
def initialize(app, options_hash={}, &block)
|
|
|
|
super
|
2013-05-23 21:50:46 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
@cache = {}
|
|
|
|
end
|
2012-05-09 22:15:39 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
def before_configuration
|
2014-07-06 01:44:04 +02:00
|
|
|
file_watcher.changed(&method(:clear_data))
|
|
|
|
file_watcher.deleted(&method(:clear_data))
|
2013-05-25 02:11:46 +02:00
|
|
|
end
|
2012-05-09 22:15:39 +02:00
|
|
|
|
2014-04-28 06:54:53 +02:00
|
|
|
# Modify each resource to add data & options from frontmatter.
|
|
|
|
def manipulate_resource_list(resources)
|
|
|
|
resources.each do |resource|
|
2014-05-31 06:50:10 +02:00
|
|
|
next if resource.source_file.blank?
|
|
|
|
|
|
|
|
fmdata = data(resource.source_file).first.dup
|
2014-04-28 06:54:53 +02:00
|
|
|
|
|
|
|
# Copy over special options
|
2014-04-28 08:21:12 +02:00
|
|
|
# TODO: Should we make people put these under "options" instead of having
|
|
|
|
# special known keys?
|
|
|
|
opts = fmdata.extract!(:layout, :layout_engine, :renderer_options, :directory_index, :content_type)
|
2014-05-31 08:19:33 +02:00
|
|
|
opts[:renderer_options].symbolize_keys! if opts.key?(:renderer_options)
|
2012-08-14 00:39:06 +02:00
|
|
|
|
2014-04-28 06:54:53 +02:00
|
|
|
ignored = fmdata.delete(:ignored)
|
2013-04-13 01:14:16 +02:00
|
|
|
|
2014-04-28 06:54:53 +02:00
|
|
|
# TODO: Enhance data? NOOOO
|
|
|
|
# TODO: stringify-keys? immutable/freeze?
|
2014-01-04 01:18:16 +01:00
|
|
|
|
2014-04-28 06:54:53 +02:00
|
|
|
resource.add_metadata options: opts, page: fmdata
|
2014-03-30 01:21:49 +01:00
|
|
|
|
2014-07-04 19:38:25 +02:00
|
|
|
resource.ignore! if ignored == true && !resource.is_a?(::Middleman::Sitemap::ProxyResource)
|
2011-11-28 07:04:19 +01:00
|
|
|
|
2014-04-28 06:54:53 +02:00
|
|
|
# TODO: Save new template here somewhere?
|
2012-05-09 06:05:55 +02:00
|
|
|
end
|
|
|
|
end
|
2013-12-28 19:14:15 +01:00
|
|
|
|
2014-04-28 06:54:53 +02:00
|
|
|
def after_configuration
|
|
|
|
app.ignore %r{\.frontmatter$}
|
2012-05-09 06:05:55 +02:00
|
|
|
end
|
2012-08-14 00:39:06 +02:00
|
|
|
|
2014-01-02 06:19:05 +01:00
|
|
|
# Get the template data from a path
|
|
|
|
# @param [String] path
|
|
|
|
# @return [String]
|
|
|
|
def template_data_for_file(path)
|
|
|
|
data(path).last
|
2013-05-25 02:11:46 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def data(path)
|
|
|
|
p = normalize_path(path)
|
|
|
|
@cache[p] ||= begin
|
2013-05-29 06:36:25 +02:00
|
|
|
data, content = frontmatter_and_content(p)
|
2013-05-25 02:11:46 +02:00
|
|
|
|
2014-07-06 01:44:04 +02:00
|
|
|
if file_watcher.exists?("#{path}.frontmatter")
|
2013-05-25 02:11:46 +02:00
|
|
|
external_data, _ = frontmatter_and_content("#{p}.frontmatter")
|
2013-05-29 06:36:25 +02:00
|
|
|
data = external_data.deep_merge(data)
|
2013-05-25 02:11:46 +02:00
|
|
|
end
|
2013-05-29 06:36:25 +02:00
|
|
|
|
|
|
|
[data, content]
|
2013-05-25 02:11:46 +02:00
|
|
|
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)
|
2013-12-28 01:26:31 +01:00
|
|
|
prefix = app.source_dir.sub(/\/$/, '') + '/'
|
2013-05-25 02:11:46 +02:00
|
|
|
return unless file.include?(prefix)
|
2013-12-28 01:26:31 +01:00
|
|
|
path = file.sub(prefix, '').sub(/\.frontmatter$/, '')
|
2013-05-25 02:11:46 +02:00
|
|
|
|
|
|
|
@cache.delete(path)
|
|
|
|
end
|
|
|
|
|
2014-04-29 19:50:21 +02:00
|
|
|
private
|
2014-04-29 19:44:24 +02:00
|
|
|
|
2013-05-25 02:11:46 +02:00
|
|
|
# Parse YAML frontmatter out of a string
|
|
|
|
# @param [String] content
|
|
|
|
# @return [Array<Hash, String>]
|
2013-07-09 07:16:03 +02:00
|
|
|
def parse_yaml_front_matter(content, full_path)
|
2013-05-25 02:11:46 +02:00
|
|
|
yaml_regex = /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
|
|
|
if content =~ yaml_regex
|
2013-12-28 01:26:31 +01:00
|
|
|
content = content.sub(yaml_regex, '')
|
2013-05-25 02:11:46 +02:00
|
|
|
|
|
|
|
begin
|
|
|
|
data = YAML.load($1) || {}
|
|
|
|
data = data.symbolize_keys
|
|
|
|
rescue *YAML_ERRORS => e
|
2013-07-09 07:16:03 +02:00
|
|
|
app.logger.error "YAML Exception parsing #{full_path}: #{e.message}"
|
2013-05-25 02:11:46 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
[data, content]
|
|
|
|
rescue
|
|
|
|
[{}, content]
|
|
|
|
end
|
|
|
|
|
2013-07-09 07:16:03 +02:00
|
|
|
def parse_json_front_matter(content, full_path)
|
2013-05-25 02:11:46 +02:00
|
|
|
json_regex = /\A(;;;\s*\n.*?\n?)^(;;;\s*$\n?)/m
|
|
|
|
|
|
|
|
if content =~ json_regex
|
2013-12-28 01:26:31 +01:00
|
|
|
content = content.sub(json_regex, '')
|
2013-05-25 02:11:46 +02:00
|
|
|
|
|
|
|
begin
|
2014-04-29 19:50:21 +02:00
|
|
|
json = ($1 + $2).sub(';;;', '{').sub(';;;', '}')
|
2013-05-25 02:11:46 +02:00
|
|
|
data = ActiveSupport::JSON.decode(json).symbolize_keys
|
|
|
|
rescue => e
|
2013-07-09 07:16:03 +02:00
|
|
|
app.logger.error "JSON Exception parsing #{full_path}: #{e.message}"
|
2013-05-25 02:11:46 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
[data, content]
|
|
|
|
rescue
|
|
|
|
[{}, content]
|
|
|
|
end
|
|
|
|
|
|
|
|
# Get the frontmatter and plain content from a file
|
|
|
|
# @param [String] path
|
2014-01-03 01:34:08 +01:00
|
|
|
# @return [Array<Middleman::Util::HashWithIndifferentAccess, String>]
|
2013-05-25 02:11:46 +02:00
|
|
|
def frontmatter_and_content(path)
|
|
|
|
full_path = if Pathname(path).relative?
|
|
|
|
File.join(app.source_dir, path)
|
|
|
|
else
|
|
|
|
path
|
2012-05-07 23:41:39 +02:00
|
|
|
end
|
2013-05-25 02:11:46 +02:00
|
|
|
|
|
|
|
data = {}
|
|
|
|
|
2014-07-06 01:44:04 +02:00
|
|
|
return [data, nil] if !file_watcher.exists?(full_path) || ::Middleman::Util.binary?(full_path)
|
2013-05-25 02:11:46 +02:00
|
|
|
|
2013-06-01 02:46:12 +02:00
|
|
|
content = File.read(full_path)
|
2013-12-28 19:14:15 +01:00
|
|
|
|
2013-06-01 02:46:12 +02:00
|
|
|
begin
|
|
|
|
if content =~ /\A.*coding:/
|
|
|
|
lines = content.split(/\n/)
|
|
|
|
lines.shift
|
|
|
|
content = lines.join("\n")
|
2013-05-25 02:11:46 +02:00
|
|
|
end
|
2013-06-01 02:46:12 +02:00
|
|
|
|
2013-07-09 07:16:03 +02:00
|
|
|
result = parse_yaml_front_matter(content, full_path) || parse_json_front_matter(content, full_path)
|
2013-06-01 02:46:12 +02:00
|
|
|
return result if result
|
|
|
|
rescue
|
|
|
|
# Probably a binary file, move on
|
2013-05-25 02:11:46 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
[data, content]
|
|
|
|
end
|
|
|
|
|
|
|
|
def normalize_path(path)
|
2013-12-28 01:26:31 +01:00
|
|
|
path.sub(%r{^#{Regexp.escape(app.source_dir)}\/}, '')
|
2011-06-06 20:39:57 +02:00
|
|
|
end
|
|
|
|
end
|
2012-05-19 22:27:38 +02:00
|
|
|
end
|