Implement yaml data postscript

This commit is contained in:
Thomas Reynolds 2015-06-16 15:30:37 -07:00
parent c0ddf15add
commit 7383f67874
22 changed files with 215 additions and 181 deletions

View file

@ -1,6 +1,9 @@
master
===
* Removed ability to use JSON as frontmatter. Still allowed in data/ folder.
* Added YAML data postscript. Like frontmatter, but reversed. Attach content after the key/value data as a `:postscript` key to the data structure (if Hash).
# 4.0.0.beta.2
* Fixed regression causing exceptions to be silently thrown away outside of `--verbose` mode in the dev server.

View file

@ -51,3 +51,10 @@ Feature: Local Data API
Then I should see "title1:Hello"
Then I should see "title2:More"
Then I should see "title3:Stuff"
Scenario: Using data postscript
Given the Server is running at "nested-data-app"
When I go to "/extracontent.html"
Then I should see "<h1>With Content</h1>"
Then I should see '<h2 id="header-2">Header 2</h2>'
Then I should see "<p>Paragraph 1</p>"

View file

@ -31,17 +31,6 @@ Feature: Neighboring YAML Front Matter
When I go to "/front-matter-encoding.html.erb.frontmatter"
Then I should see "File Not Found"
Scenario: Rendering html (json)
Given the Server is running at "frontmatter-neighbor-app"
When I go to "/json-front-matter.html.erb.frontmatter"
Then I should see "File Not Found"
When I go to "/json-front-matter-2.php"
Then I should see "<h1>This is the title</h1>"
Then I should see "<?php"
Then I should not see ";;;"
When I go to "/json-front-matter-2.php.erb.frontmatter"
Then I should see "File Not Found"
Scenario: A template changes frontmatter during preview
Given the Server is running at "frontmatter-neighbor-app"
And the file "source/front-matter-change.html.erb" has the contents

View file

@ -41,28 +41,6 @@ Feature: YAML Front Matter
Then I should see "<h1>This is the title</h1>"
Then I should not see "---"
Scenario: Rendering html (json)
Given the Server is running at "frontmatter-app"
When I go to "/json-front-matter.html"
Then I should see "<h1>This is the title</h1>"
Then I should not see ";;;"
When I go to "/json-front-matter-2.php"
Then I should see "<h1>This is the title</h1>"
Then I should see "<?php"
Then I should not see ";;;"
Scenario: JSON not on first line, no encoding
Given the Server is running at "frontmatter-app"
When I go to "/json-front-matter-line-2.html"
Then I should see "<h1></h1>"
Then I should see ";;;"
Scenario: JSON not on first line, with encoding
Given the Server is running at "frontmatter-app"
When I go to "/json-front-matter-encoding.html"
Then I should see "<h1>This is the title</h1>"
Then I should not see ";;;"
Scenario: A template changes frontmatter during preview
Given the Server is running at "frontmatter-app"
And the file "source/front-matter-change.html.erb" has the contents

View file

@ -1,7 +1,10 @@
<h2> Test</h2>
<h1><%= current_page.data.title %></h1>
---
layout: false
title: This is the title
---
<h1><%= current_page.data.title %></h1>
<div>Stuff</div>

View file

@ -1,7 +0,0 @@
;;;
"layout": false,
"title": "This is the title"
;;;
<h1><%= current_page.data.title %></h1>
<?php echo "sup"; ?>

View file

@ -1,7 +0,0 @@
# encoding: UTF-8
;;;
"layout": false,
"title": "This is the title"
;;;
<h1><%= current_page.data.title %></h1>

View file

@ -1,7 +0,0 @@
<h2> Test</h2>
;;;
layout: false,
title: "This is the title"
;;;
<h1><%= current_page.data.title %></h1>

View file

@ -1,6 +0,0 @@
;;;
"layout": false,
"title": "This is the title"
;;;
<h1><%= current_page.data.title %></h1>

View file

@ -15,7 +15,7 @@ class NeighborFrontmatter < ::Middleman::Extension
next unless file
fmdata = app.extensions[:front_matter].frontmatter_and_content(file[:full_path]).first
fmdata = ::Middleman::Util::Data.parse(file[:full_path], :yaml).first
opts = fmdata.extract!(:layout, :layout_engine, :renderer_options, :directory_index, :content_type)
opts[:renderer_options].symbolize_keys! if opts.key?(:renderer_options)
ignored = fmdata.delete(:ignored)

View file

@ -1,2 +0,0 @@
<h1><%= current_page.data.title %></h1>
<?php echo "sup"; ?>

View file

@ -1,4 +0,0 @@
;;;
"layout": false,
"title": "This is the title"
;;;

View file

@ -1 +0,0 @@
<h1><%= current_page.data.title %></h1>

View file

@ -1,4 +0,0 @@
;;;
"layout": false,
"title": "This is the title"
;;;

View file

@ -26,7 +26,7 @@ class NeighborFrontmatter < ::Middleman::Extension
end
def apply_neighbor_data(resource, file)
fmdata = app.extensions[:front_matter].frontmatter_and_content(file[:full_path]).first
fmdata = ::Middleman::Util::Data.parse(file[:full_path], :yaml).first
opts = fmdata.extract!(:layout, :layout_engine, :renderer_options, :directory_index, :content_type)
opts[:renderer_options].symbolize_keys! if opts.key?(:renderer_options)
ignored = fmdata.delete(:ignored)

View file

@ -0,0 +1,11 @@
---
name: "With Content"
---
## Header 2
Paragraph 1
Paragraph 2
### Header 3

View file

@ -0,0 +1,4 @@
%h1= data.examples.withcontent.name
:markdown
<%= data.examples.withcontent.postscript.gsub("\n", "\n\s\s") %>

View file

@ -33,7 +33,6 @@ module Middleman
# Set the value of a setting by key. Creates the setting if it doesn't exist.
# @param [Symbol] key
# @param [Object] val
# rubocop:disable UselessSetterCall
def []=(key, val)
setting_obj = setting(key) || define_setting(key)
setting_obj.value = val

View file

@ -1,6 +1,5 @@
require 'yaml'
require 'active_support/json'
require 'middleman-core/contracts'
require 'middleman-core/util/data'
module Middleman
module CoreExtensions
@ -100,9 +99,10 @@ module Middleman
basename = File.basename(data_path, extension)
if %w(.yaml .yml).include?(extension)
data = ::YAML.load_file(file[:full_path])
data, postscript = ::Middleman::Util::Data.parse(file[:full_path], :yaml)
data[:postscript] = postscript if !postscript.nil? && data.is_a?(Hash)
elsif extension == '.json'
data = ::ActiveSupport::JSON.decode(file[:full_path].read)
data, _postscript = ::Middleman::Util::Data.parse(file[:full_path], :json)
else
return
end

View file

@ -1,11 +1,12 @@
require 'active_support/core_ext/hash/keys'
# Core Pathname library used for traversal
require 'pathname'
# Parsing YAML frontmatter
require 'yaml'
# DbC
require 'middleman-core/contracts'
# Parsing JSON frontmatter
require 'active_support/json'
require 'active_support/core_ext/hash/keys'
require 'middleman-core/util/data'
# Extensions namespace
module Middleman::CoreExtensions
@ -13,13 +14,6 @@ module Middleman::CoreExtensions
# Try to run after routing but before directory_indexes
self.resource_list_manipulator_priority = 90
YAML_ERRORS = [StandardError]
# https://github.com/tenderlove/psych/issues/23
if defined?(Psych) && defined?(Psych::SyntaxError)
YAML_ERRORS << Psych::SyntaxError
end
def initialize(app, options_hash={}, &block)
super
@ -71,7 +65,7 @@ module Middleman::CoreExtensions
return [{}, nil] unless file
@cache[file[:full_path]] ||= frontmatter_and_content(file[:full_path])
@cache[file[:full_path]] ||= ::Middleman::Util::Data.parse(file[:full_path])
end
Contract ArrayOf[IsA['Middleman::SourceFile']], ArrayOf[IsA['Middleman::SourceFile']] => Any
@ -80,93 +74,5 @@ module Middleman::CoreExtensions
@cache.delete(file[:full_path])
end
end
# Get the frontmatter and plain content from a file
# @param [String] path
# @return [Array<Middleman::Util::IndifferentHash, String>]
Contract Pathname => [Hash, Maybe[String]]
def frontmatter_and_content(full_path)
data = {}
return [data, nil] if ::Middleman::Util.binary?(full_path)
# Avoid weird race condition when a file is renamed.
content = begin
File.read(full_path)
rescue ::EOFError
rescue ::IOError
rescue ::Errno::ENOENT
''
end
begin
if content =~ /\A.*coding:/
lines = content.split(/\n/)
lines.shift
content = lines.join("\n")
end
result = parse_yaml_front_matter(content, full_path) || parse_json_front_matter(content, full_path)
return result if result
rescue
# Probably a binary file, move on
end
[data, content]
end
private
# Parse YAML frontmatter out of a string
# @param [String] content
# @return [Array<Hash, String>]
Contract String, Pathname => Maybe[[Hash, String]]
def parse_yaml_front_matter(content, full_path)
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 parsing #{full_path}: #{e.message}"
return nil
end
else
return nil
end
[data, content]
rescue
[{}, 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_front_matter(content, full_path)
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 parsing #{full_path}: #{e.message}"
return nil
end
else
return nil
end
[data, content]
rescue
[{}, content]
end
end
end

View file

@ -27,7 +27,26 @@ module Middleman
next unless resource.source_file[:relative_path].to_s =~ %r{\.liquid$}
# Convert data object into a hash for liquid
resource.add_metadata locals: { data: app.extensions[:data].data_store.to_h }
resource.add_metadata locals: {
data: stringify_recursive(app.extensions[:data].data_store.to_h)
}
end
end
def stringify_recursive(hash)
{}.tap do |h|
hash.each { |key, value| h[key.to_s] = map_value(value) }
end
end
def map_value(thing)
case thing
when Hash
stringify_recursive(thing)
when Array
thing.map { |v| map_value(v) }
else
thing
end
end
end

View file

@ -0,0 +1,153 @@
# Core Pathname library used for traversal
require 'pathname'
# DbC
require 'middleman-core/contracts'
# Shared util methods
require 'middleman-core/util'
# Parsing YAML data
require 'yaml'
# Parsing JSON data
require 'active_support/json'
module Middleman
module Util
module Data
include Contracts
module_function
YAML_ERRORS = [StandardError]
# https://github.com/tenderlove/psych/issues/23
if defined?(Psych) && defined?(Psych::SyntaxError)
YAML_ERRORS << Psych::SyntaxError
end
# Get the frontmatter and plain content from a file
# @param [String] path
# @return [Array<Middleman::Util::IndifferentHash, String>]
Contract Pathname, Maybe[Symbol] => [Hash, Maybe[String]]
def parse(full_path, known_type=nil)
data = {}
return [data, nil] if ::Middleman::Util.binary?(full_path)
# Avoid weird race condition when a file is renamed.
content = begin
File.read(full_path)
rescue ::EOFError
rescue ::IOError
rescue ::Errno::ENOENT
''
end
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(::ActiveSupport::JSON.decode(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