summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Speicher <rspeicher@gmail.com>2016-02-21 19:22:02 -0500
committerRobert Speicher <rspeicher@gmail.com>2016-02-21 19:23:41 -0500
commita8a8e1b5d662eb955a6a88d55f17ecf659c9a7ca (patch)
treec3bb386d139e7e7ae4fd28e1e05bdc11ae3f8d55
parent2cd24b05ba3cce6f3edf42419ab153f13f4c74a0 (diff)
downloadgitlab-ce-rs-frontmatter.tar.gz
Add YamlFrontMatterFilter to the PreProcessPipeliners-frontmatter
This filter will detect YAML Front Matter and convert it to an HTML table for prettier formatting.
-rw-r--r--lib/banzai/filter/yaml_front_matter_filter.rb70
-rw-r--r--lib/banzai/pipeline/pre_process_pipeline.rb1
-rw-r--r--spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb137
3 files changed, 208 insertions, 0 deletions
diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb
new file mode 100644
index 00000000000..ed1371f17c4
--- /dev/null
+++ b/lib/banzai/filter/yaml_front_matter_filter.rb
@@ -0,0 +1,70 @@
+require 'html/pipeline/filter'
+require 'yaml'
+
+module Banzai
+ module Filter
+ class YamlFrontMatterFilter < HTML::Pipeline::Filter
+ DELIM = '---'.freeze
+
+ # Hat-tip to Middleman: https://git.io/v2e0z
+ PATTERN = %r{
+ \A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
+ (?<start>#{DELIM})[ ]*\r?\n
+ (?<frontmatter>.*?)[ ]*\r?\n?
+ ^(?<stop>#{DELIM})[ ]*\r?\n?
+ \r?\n?
+ (?<additional_content>.*)
+ }mx.freeze
+
+ def call
+ match = PATTERN.match(html)
+
+ return html unless match
+
+ frontmatter = load_yaml(match['frontmatter'])
+
+ return html unless frontmatter
+
+ tableize(frontmatter) << match['additional_content']
+ end
+
+ private
+
+ def load_yaml(yaml_string)
+ YAML.safe_load(yaml_string, [Date, DateTime, Symbol, Time])
+ end
+
+ def escape(html)
+ ERB::Util.html_escape(html)
+ end
+
+ # Converts a YAML object into an HTML table
+ #
+ # Each row is a key-value pair
+ def tableize(yaml)
+ table = "<table>\n"
+
+ if yaml.respond_to?(:each_pair)
+ yaml.each_pair do |key, value|
+ table << "<tr><th>#{escape(key)}</th><td>"
+
+ case value
+ when Array
+ value.inject(table) { |memo, v| memo << tableize(v) }
+ when Hash
+ table << tableize(value)
+ else
+ table << escape(value)
+ end
+
+ table << "</td></tr>\n"
+ end
+ else
+ table << "<tr><td>#{escape(yaml)}</td></tr>"
+ end
+
+ table << "</table>\n"
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/pre_process_pipeline.rb b/lib/banzai/pipeline/pre_process_pipeline.rb
index c174f0b862d..b3d1e9b661e 100644
--- a/lib/banzai/pipeline/pre_process_pipeline.rb
+++ b/lib/banzai/pipeline/pre_process_pipeline.rb
@@ -3,6 +3,7 @@ module Banzai
class PreProcessPipeline < BasePipeline
def self.filters
[
+ Filter::YamlFrontMatterFilter
]
end
diff --git a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
new file mode 100644
index 00000000000..451b392c17a
--- /dev/null
+++ b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
@@ -0,0 +1,137 @@
+require 'rails_helper'
+
+describe Banzai::Filter::YamlFrontMatterFilter, lib: true do
+ include FilterSpecHelper
+
+ def parse(html)
+ Nokogiri::HTML.parse(html)
+ end
+
+ it 'allows for `encoding:` before the frontmatter' do
+ content = <<-MD.strip_heredoc
+ # encoding: UTF-8
+ ---
+ foo: foo
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ expect(output).not_to match 'encoding'
+ end
+
+ it 'converts YAML frontmatter to a table' do
+ content = <<-MD.strip_heredoc
+ ---
+ foo: foo string
+ bar: :bar_symbol
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = parse(filter(content))
+
+ aggregate_failures do
+ expect(output.xpath('.//tr[1]/th').text).to eq 'foo'
+ expect(output.xpath('.//tr[1]/td').text).to eq 'foo string'
+ expect(output.xpath('.//tr[2]/th').text).to eq 'bar'
+ expect(output.xpath('.//tr[2]/td').text).to eq 'bar_symbol'
+ end
+ end
+
+ it 'loads YAML safely' do
+ pending "Unsure what we can actually put here to verify this."
+
+ content = <<-MD.strip_heredoc
+ ---
+ ---
+ MD
+
+ expect { filter(content) }.to raise_error(Psych::DisallowedClass)
+ end
+
+ it 'HTML-escapes all YAML keys' do
+ content = <<-MD.strip_heredoc
+ ---
+ <b>foo</b>: 'foo'
+ ---
+ MD
+
+ output = filter(content)
+
+ expect(output).to match '&lt;b&gt;foo&lt;/b&gt;'
+ end
+
+ it 'HTML-escapes all YAML values' do
+ content = <<-MD.strip_heredoc
+ ---
+ foo: <th>foo</th>
+ ---
+ MD
+
+ output = filter(content)
+
+ expect(output).to match '&lt;th&gt;foo&lt;/th&gt;'
+ end
+
+ it 'supports Arrays' do
+ content = <<-MD.strip_heredoc
+ ---
+ foo:
+ - bar
+ - baz
+ ---
+ MD
+
+ output = parse(filter(content))
+
+ aggregate_failures do
+ expect(output.css('table').length).to eq 3
+ expect(output.xpath('.//table/tr[1]/td/table[1]/tr[1]/td[1]').text).to eq 'bar'
+ end
+ end
+
+ it 'supports Arrays of Hashes' do
+ content = <<-MD.strip_heredoc
+ ---
+ mvps:
+ - version: 8.4
+ name: Kyungchul Shin
+ date: January 22nd, 2016
+ - version: 8.3
+ name: Greg Smethells
+ date: December 22nd, 2015
+ - version: 8.2
+ name: Cristian Bica
+ date: November 22nd, 2015
+ ---
+ MD
+
+ output = parse(filter(content))
+
+ aggregate_failures do
+ expect(output.css('table').length).to eq 4
+ expect(output.xpath('.//table/tr[1]/td/table[2]/tr[2]/th[1]').text).to eq 'name'
+ expect(output.xpath('.//table/tr[1]/td/table[2]/tr[2]/td[1]').text).to eq 'Greg Smethells'
+ end
+ end
+
+ context 'on content without frontmatter' do
+ it 'returns the content unmodified' do
+ content = <<-MD.strip_heredoc
+ # This is some Markdown
+
+ It has no YAML frontmatter to parse.
+ MD
+
+ expect(filter(content)).to eq content
+ end
+ end
+end