mirror of
https://github.com/middleman/middleman.git
synced 2022-11-09 12:20:27 -05:00
Implement yaml data postscript
This commit is contained in:
parent
c0ddf15add
commit
7383f67874
22 changed files with 215 additions and 181 deletions
|
@ -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.
|
||||
|
|
|
@ -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>"
|
||||
|
|
|
@ -30,17 +30,6 @@ Feature: Neighboring YAML Front Matter
|
|||
Then I should not see "---"
|
||||
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"
|
||||
|
|
|
@ -40,28 +40,6 @@ Feature: YAML Front Matter
|
|||
When I go to "/front-matter-encoding.html"
|
||||
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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
;;;
|
||||
"layout": false,
|
||||
"title": "This is the title"
|
||||
;;;
|
||||
|
||||
<h1><%= current_page.data.title %></h1>
|
||||
<?php echo "sup"; ?>
|
|
@ -1,7 +0,0 @@
|
|||
# encoding: UTF-8
|
||||
;;;
|
||||
"layout": false,
|
||||
"title": "This is the title"
|
||||
;;;
|
||||
|
||||
<h1><%= current_page.data.title %></h1>
|
|
@ -1,7 +0,0 @@
|
|||
<h2> Test</h2>
|
||||
;;;
|
||||
layout: false,
|
||||
title: "This is the title"
|
||||
;;;
|
||||
|
||||
<h1><%= current_page.data.title %></h1>
|
|
@ -1,6 +0,0 @@
|
|||
;;;
|
||||
"layout": false,
|
||||
"title": "This is the title"
|
||||
;;;
|
||||
|
||||
<h1><%= current_page.data.title %></h1>
|
|
@ -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)
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
<h1><%= current_page.data.title %></h1>
|
||||
<?php echo "sup"; ?>
|
|
@ -1,4 +0,0 @@
|
|||
;;;
|
||||
"layout": false,
|
||||
"title": "This is the title"
|
||||
;;;
|
|
@ -1 +0,0 @@
|
|||
<h1><%= current_page.data.title %></h1>
|
|
@ -1,4 +0,0 @@
|
|||
;;;
|
||||
"layout": false,
|
||||
"title": "This is the title"
|
||||
;;;
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: "With Content"
|
||||
---
|
||||
|
||||
## Header 2
|
||||
|
||||
Paragraph 1
|
||||
|
||||
Paragraph 2
|
||||
|
||||
### Header 3
|
|
@ -0,0 +1,4 @@
|
|||
%h1= data.examples.withcontent.name
|
||||
|
||||
:markdown
|
||||
<%= data.examples.withcontent.postscript.gsub("\n", "\n\s\s") %>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
153
middleman-core/lib/middleman-core/util/data.rb
Normal file
153
middleman-core/lib/middleman-core/util/data.rb
Normal 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
|
Loading…
Reference in a new issue