Merge pull request #1604 from middleman/frontmatter
Refactor frontmatter parsing
This commit is contained in:
commit
11fc90f93c
|
@ -1,153 +1,82 @@
|
||||||
# Core Pathname library used for traversal
|
require 'yaml'
|
||||||
|
require 'json'
|
||||||
require 'pathname'
|
require 'pathname'
|
||||||
|
require 'middleman-core/util'
|
||||||
# DbC
|
|
||||||
require 'middleman-core/contracts'
|
require 'middleman-core/contracts'
|
||||||
|
|
||||||
# Shared util methods
|
module Middleman::Util::Data
|
||||||
require 'middleman-core/util'
|
include Contracts
|
||||||
|
|
||||||
# Parsing YAML data
|
module_function
|
||||||
require 'yaml'
|
|
||||||
|
|
||||||
# Parsing JSON data
|
# Get the frontmatter and plain content from a file
|
||||||
require 'json'
|
# @param [String] path
|
||||||
|
# @return [Array<Hash, String>]
|
||||||
|
Contract Pathname, Maybe[Symbol] => [Hash, Maybe[String]]
|
||||||
|
def parse(full_path, known_type=nil)
|
||||||
|
return [{}, nil] if Middleman::Util.binary?(full_path)
|
||||||
|
|
||||||
module Middleman
|
# Avoid weird race condition when a file is renamed
|
||||||
module Util
|
begin
|
||||||
module Data
|
content = File.read(full_path)
|
||||||
include Contracts
|
rescue EOFError, IOError, Errno::ENOENT
|
||||||
|
return [{}, nil]
|
||||||
|
end
|
||||||
|
|
||||||
module_function
|
case known_type
|
||||||
|
when :yaml
|
||||||
|
return [parse_yaml(content, full_path), nil]
|
||||||
|
when :json
|
||||||
|
return [parse_json(content, full_path), nil]
|
||||||
|
end
|
||||||
|
|
||||||
YAML_ERRORS = [StandardError]
|
/
|
||||||
|
(?<start>^[-;]{3})[ ]*\r?\n
|
||||||
|
(?<frontmatter>.*?)[ ]*\r?\n
|
||||||
|
(?<stop>^[-.;]{3})[ ]*\r?\n?
|
||||||
|
(?<additional_content>.*)
|
||||||
|
/mx =~ content
|
||||||
|
|
||||||
# https://github.com/tenderlove/psych/issues/23
|
case [start, stop]
|
||||||
if defined?(Psych) && defined?(Psych::SyntaxError)
|
when %w[--- ---], %w[--- ...]
|
||||||
YAML_ERRORS << Psych::SyntaxError
|
[parse_yaml(frontmatter, full_path), additional_content]
|
||||||
end
|
when %w[;;; ;;;]
|
||||||
|
[parse_json(frontmatter, full_path), additional_content]
|
||||||
|
else
|
||||||
|
[{}, content]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Get the frontmatter and plain content from a file
|
# Parse YAML frontmatter out of a string
|
||||||
# @param [String] path
|
# @param [String] content
|
||||||
# @return [Array<Hash, String>]
|
# @return [Array<Hash, String>]
|
||||||
Contract Pathname, Maybe[Symbol] => [Hash, Maybe[String]]
|
Contract String, Pathname, Bool => Hash
|
||||||
def parse(full_path, known_type=nil)
|
def parse_yaml(content, full_path)
|
||||||
data = {}
|
symbolize_recursive(YAML.load(content))
|
||||||
|
rescue StandardError, Psych::SyntaxError => error
|
||||||
|
warn "YAML Exception parsing #{full_path}: #{error.message}"
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
return [data, nil] if ::Middleman::Util.binary?(full_path)
|
# Parse JSON frontmatter out of a string
|
||||||
|
# @param [String] content
|
||||||
|
# @return [Array<Hash, String>]
|
||||||
|
Contract String, Pathname => Hash
|
||||||
|
def parse_json(content, full_path)
|
||||||
|
symbolize_recursive(JSON.parse(content))
|
||||||
|
rescue StandardError => error
|
||||||
|
warn "JSON Exception parsing #{full_path}: #{error.message}"
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
# Avoid weird race condition when a file is renamed.
|
def symbolize_recursive(value)
|
||||||
content = begin
|
case value
|
||||||
File.read(full_path)
|
when Hash
|
||||||
rescue ::EOFError
|
value.map { |k, v| [k.to_sym, symbolize_recursive(v)] }.to_h
|
||||||
rescue ::IOError
|
when Array
|
||||||
rescue ::Errno::ENOENT
|
value.map { |v| symbolize_recursive(v) }
|
||||||
''
|
else
|
||||||
end
|
value
|
||||||
|
|
||||||
begin
|
|
||||||
if content =~ /\A.*coding:/
|
|
||||||
lines = content.split(/\n/)
|
|
||||||
lines.shift
|
|
||||||
content = lines.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
if known_type
|
|
||||||
if known_type == :yaml
|
|
||||||
result = parse_yaml(content, full_path, true)
|
|
||||||
elsif known_type == :json
|
|
||||||
result = parse_json(content, full_path)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
result = parse_yaml(content, full_path, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
return result if result
|
|
||||||
rescue
|
|
||||||
# Probably a binary file, move on
|
|
||||||
end
|
|
||||||
|
|
||||||
[data, content]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse YAML frontmatter out of a string
|
|
||||||
# @param [String] content
|
|
||||||
# @return [Array<Hash, String>]
|
|
||||||
Contract String, Pathname, Bool => Maybe[[Hash, String]]
|
|
||||||
def parse_yaml(content, full_path, require_yaml=false)
|
|
||||||
total_delims = content.scan(/^(?:---|\.\.\.)\s*(?:\n|$)/).length
|
|
||||||
has_first_line_delim = !content.match(/\A(---\s*(?:\n|$))/).nil?
|
|
||||||
# has_closing_delim = (total_delims > 1 && has_first_line_delim) || (!has_first_line_delim && total_delims == 1)
|
|
||||||
|
|
||||||
parts = content.split(/^(?:---|\.\.\.)\s*(?:\n|$)/)
|
|
||||||
parts.shift if parts[0].empty?
|
|
||||||
|
|
||||||
yaml_string = nil
|
|
||||||
additional_content = nil
|
|
||||||
|
|
||||||
if require_yaml
|
|
||||||
yaml_string = parts[0]
|
|
||||||
additional_content = parts[1]
|
|
||||||
else
|
|
||||||
if total_delims > 1
|
|
||||||
if has_first_line_delim
|
|
||||||
yaml_string = parts[0]
|
|
||||||
additional_content = parts[1]
|
|
||||||
else
|
|
||||||
additional_content = content
|
|
||||||
end
|
|
||||||
else
|
|
||||||
additional_content = parts[0]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return [{}, additional_content] if yaml_string.nil?
|
|
||||||
|
|
||||||
begin
|
|
||||||
data = map_value(::YAML.load(yaml_string) || {})
|
|
||||||
rescue *YAML_ERRORS => e
|
|
||||||
$stderr.puts "YAML Exception parsing #{full_path}: #{e.message}"
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
[data, additional_content]
|
|
||||||
rescue
|
|
||||||
[{}, additional_content]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse JSON frontmatter out of a string
|
|
||||||
# @param [String] content
|
|
||||||
# @return [Array<Hash, String>]
|
|
||||||
Contract String, Pathname => Maybe[[Hash, String]]
|
|
||||||
def parse_json(content, full_path)
|
|
||||||
begin
|
|
||||||
data = map_value(::JSON.parse(content))
|
|
||||||
rescue => e
|
|
||||||
$stderr.puts "JSON Exception parsing #{full_path}: #{e.message}"
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
[data, nil]
|
|
||||||
rescue
|
|
||||||
[{}, nil]
|
|
||||||
end
|
|
||||||
|
|
||||||
def symbolize_recursive(hash)
|
|
||||||
{}.tap do |h|
|
|
||||||
hash.each { |key, value| h[key.to_sym] = map_value(value) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def map_value(thing)
|
|
||||||
case thing
|
|
||||||
when Hash
|
|
||||||
symbolize_recursive(thing)
|
|
||||||
when Array
|
|
||||||
thing.map { |v| map_value(v) }
|
|
||||||
else
|
|
||||||
thing
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue