front_matter_parser 0.0.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +19 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +50 -0
- data/.travis.yml +19 -1
- data/CHANGELOG.md +30 -0
- data/Dockerfile +5 -0
- data/Gemfile +0 -2
- data/README.md +76 -38
- data/Rakefile +5 -3
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +12 -0
- data/front_matter_parser.gemspec +12 -6
- data/lib/front_matter_parser.rb +10 -114
- data/lib/front_matter_parser/loader.rb +11 -0
- data/lib/front_matter_parser/loader/yaml.rb +26 -0
- data/lib/front_matter_parser/parsed.rb +25 -14
- data/lib/front_matter_parser/parser.rb +82 -0
- data/lib/front_matter_parser/syntax_parser.rb +28 -0
- data/lib/front_matter_parser/syntax_parser/factorizable.rb +30 -0
- data/lib/front_matter_parser/syntax_parser/indentation_comment.rb +49 -0
- data/lib/front_matter_parser/syntax_parser/multi_line_comment.rb +50 -0
- data/lib/front_matter_parser/syntax_parser/single_line_comment.rb +64 -0
- data/lib/front_matter_parser/version.rb +3 -1
- data/spec/fixtures/example +6 -0
- data/spec/front_matter_parser/loader/yaml_spec.rb +24 -0
- data/spec/front_matter_parser/parsed_spec.rb +11 -21
- data/spec/front_matter_parser/parser_spec.rb +111 -0
- data/spec/front_matter_parser/syntax_parser/indentation_comment_spec.rb +166 -0
- data/spec/front_matter_parser/syntax_parser/multi_line_comment_spec.rb +267 -0
- data/spec/front_matter_parser/syntax_parser/single_line_comment_spec.rb +175 -0
- data/spec/front_matter_parser_spec.rb +3 -296
- data/spec/spec_helper.rb +9 -3
- data/spec/support/matcher.rb +8 -0
- metadata +110 -46
- data/spec/fixtures/example.coffee +0 -4
- data/spec/fixtures/example.erb +0 -6
- data/spec/fixtures/example.foo +0 -0
- data/spec/fixtures/example.haml +0 -5
- data/spec/fixtures/example.liquid +0 -6
- data/spec/fixtures/example.md +0 -4
- data/spec/fixtures/example.sass +0 -4
- data/spec/fixtures/example.scss +0 -4
- data/spec/fixtures/example.slim +0 -5
- data/spec/support/strings.rb +0 -41
- data/spec/support/syntaxs.rb +0 -14
- data/spec/support/utils.rb +0 -6
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'front_matter_parser/loader/yaml'
|
4
|
+
|
5
|
+
module FrontMatterParser
|
6
|
+
# This module includes front matter loaders (from a string -usually extracted
|
7
|
+
# with a {SyntaxParser}- to hash). They must respond to a `::call` method
|
8
|
+
# which accepts the String as argument and respond with a Hash.
|
9
|
+
module Loader
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module FrontMatterParser
|
6
|
+
module Loader
|
7
|
+
# {Loader} that uses YAML library
|
8
|
+
class Yaml
|
9
|
+
# @!attribute [r] allowlist_classes
|
10
|
+
# Classes that may be parsed by #call.
|
11
|
+
attr_reader :allowlist_classes
|
12
|
+
|
13
|
+
def initialize(allowlist_classes: [])
|
14
|
+
@allowlist_classes = allowlist_classes
|
15
|
+
end
|
16
|
+
|
17
|
+
# Loads a hash front matter from a string
|
18
|
+
#
|
19
|
+
# @param string [String] front matter string representation
|
20
|
+
# @return [Hash] front matter hash representation
|
21
|
+
def call(string)
|
22
|
+
YAML.safe_load(string, allowlist_classes)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,18 +1,29 @@
|
|
1
|
-
#
|
2
|
-
class FrontMatterParser::Parsed
|
3
|
-
# @!attribute [rw] front_matter
|
4
|
-
# @return [Hash{String => String, Array, Hash}] the parsed front matter
|
5
|
-
# @!attribute [rw] content
|
6
|
-
# @return [String] the parsed content
|
7
|
-
attr_accessor :front_matter, :content
|
1
|
+
# frozen_string_literal: true
|
8
2
|
|
9
|
-
|
3
|
+
module FrontMatterParser
|
4
|
+
# Result of parsing front matter and content from a string
|
5
|
+
class Parsed
|
6
|
+
# @!attribute [rw] front_matter
|
7
|
+
# @see #initialize
|
8
|
+
attr_reader :front_matter
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@
|
10
|
+
# @!attribute [rw] content
|
11
|
+
# @see #initialize
|
12
|
+
attr_reader :content
|
13
|
+
|
14
|
+
# @param front_matter [Hash] parsed front_matter
|
15
|
+
# @param content [String] parsed content
|
16
|
+
def initialize(front_matter:, content:)
|
17
|
+
@front_matter = front_matter
|
18
|
+
@content = content
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns front matter value for given key
|
22
|
+
#
|
23
|
+
# @param key [String] key for desired value
|
24
|
+
# @return [String, Array, # Hash] desired value
|
25
|
+
def [](key)
|
26
|
+
front_matter[key]
|
27
|
+
end
|
17
28
|
end
|
18
29
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FrontMatterParser
|
4
|
+
# Entry point to parse a front matter from a string or file.
|
5
|
+
class Parser
|
6
|
+
# @!attribute [r] syntax_parser
|
7
|
+
# Current syntax parser in use. See {SyntaxParser}
|
8
|
+
attr_reader :syntax_parser
|
9
|
+
|
10
|
+
# @!attribute [r] loader
|
11
|
+
# Current loader in use. See {Loader} for details
|
12
|
+
attr_reader :loader
|
13
|
+
|
14
|
+
# Parses front matter and content from given pathname, inferring syntax from
|
15
|
+
# the extension or, otherwise, using syntax_parser argument.
|
16
|
+
#
|
17
|
+
# @param pathname [String]
|
18
|
+
# @param syntax_parser [Object] see {SyntaxParser}
|
19
|
+
# @param loader [Object] see {Loader}
|
20
|
+
# @return [Parsed] parsed front matter and content
|
21
|
+
def self.parse_file(pathname, syntax_parser: nil, loader: nil)
|
22
|
+
syntax_parser ||= syntax_from_pathname(pathname)
|
23
|
+
loader ||= Loader::Yaml.new
|
24
|
+
File.open(pathname) do |file|
|
25
|
+
new(syntax_parser, loader: loader).call(file.read)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
def self.syntax_from_pathname(pathname)
|
31
|
+
File.extname(pathname)[1..-1].to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
# @!visibility private
|
35
|
+
def self.syntax_parser_from_symbol(syntax)
|
36
|
+
Kernel.const_get(
|
37
|
+
"FrontMatterParser::SyntaxParser::#{syntax.capitalize}"
|
38
|
+
).new
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param syntax_parser [Object] Syntax parser to use. It can be one of two
|
42
|
+
# things:
|
43
|
+
#
|
44
|
+
# - An actual object which acts like a parser. See {SyntaxParser} for
|
45
|
+
# details.
|
46
|
+
#
|
47
|
+
# - A symbol, in which case it refers to a parser
|
48
|
+
# `FrontMatterParser::SyntaxParser::#{symbol.capitalize}` which can be
|
49
|
+
# initialized without arguments
|
50
|
+
#
|
51
|
+
# @param loader [Object] Front matter loader to use. See {Loader} for
|
52
|
+
# details.
|
53
|
+
def initialize(syntax_parser, loader: Loader::Yaml.new)
|
54
|
+
@syntax_parser = infer_syntax_parser(syntax_parser)
|
55
|
+
@loader = loader
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parses front matter and content from given string
|
59
|
+
#
|
60
|
+
# @param string [String]
|
61
|
+
# @return [Parsed] parsed front matter and content
|
62
|
+
# :reek:FeatureEnvy
|
63
|
+
def call(string)
|
64
|
+
match = syntax_parser.call(string)
|
65
|
+
front_matter, content =
|
66
|
+
if match
|
67
|
+
[loader.call(match[:front_matter]), match[:content]]
|
68
|
+
else
|
69
|
+
[{}, string]
|
70
|
+
end
|
71
|
+
Parsed.new(front_matter: front_matter, content: content)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def infer_syntax_parser(syntax_parser)
|
77
|
+
return syntax_parser unless syntax_parser.is_a?(Symbol)
|
78
|
+
|
79
|
+
self.class.syntax_parser_from_symbol(syntax_parser)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'front_matter_parser/syntax_parser/factorizable'
|
4
|
+
require 'front_matter_parser/syntax_parser/multi_line_comment'
|
5
|
+
require 'front_matter_parser/syntax_parser/indentation_comment'
|
6
|
+
require 'front_matter_parser/syntax_parser/single_line_comment'
|
7
|
+
|
8
|
+
module FrontMatterParser
|
9
|
+
# This module includes parsers for different syntaxes. They respond to
|
10
|
+
# a method `#call`, which takes a string as argument and responds with
|
11
|
+
# a hash interface with `:front_matter` and `:content` keys, or `nil` if no
|
12
|
+
# front matter is found.
|
13
|
+
#
|
14
|
+
# :reek:TooManyConstants
|
15
|
+
module SyntaxParser
|
16
|
+
Coffee = SingleLineComment['#']
|
17
|
+
Sass = SingleLineComment['//']
|
18
|
+
Scss = SingleLineComment['//']
|
19
|
+
|
20
|
+
Html = MultiLineComment['<!--', '-->']
|
21
|
+
Erb = MultiLineComment['<%#', '%>']
|
22
|
+
Liquid = MultiLineComment['{% comment %}', '{% endcomment %}']
|
23
|
+
Md = MultiLineComment['', '']
|
24
|
+
|
25
|
+
Slim = IndentationComment['/']
|
26
|
+
Haml = IndentationComment['-#']
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FrontMatterParser
|
4
|
+
module SyntaxParser
|
5
|
+
# This is just a helper to allow creating syntax parsers with a more terse
|
6
|
+
# syntax, without the need of explicitly creating descendant classes for the
|
7
|
+
# most general cases. See {SyntaxParser} for examples in use.
|
8
|
+
module Factorizable
|
9
|
+
# @param delimiters [String] Splat arguments with all comment delimiters
|
10
|
+
# used by the parser
|
11
|
+
#
|
12
|
+
# @return [Object] A base class of self with a `delimiters` class method
|
13
|
+
# added which returns an array with given comment delimiters
|
14
|
+
def [](*delimiters)
|
15
|
+
delimiters = delimiters.map { |delimiter| Regexp.escape(delimiter) }
|
16
|
+
create_base_class(delimiters)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_base_class(delimiters)
|
22
|
+
parser = Class.new(self)
|
23
|
+
parser.define_singleton_method(:delimiters) do
|
24
|
+
delimiters
|
25
|
+
end
|
26
|
+
parser
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FrontMatterParser
|
4
|
+
module SyntaxParser
|
5
|
+
# Parser for syntaxes which use comments ended by indentation
|
6
|
+
class IndentationComment
|
7
|
+
extend Factorizable
|
8
|
+
|
9
|
+
# @!attribute [r] regexp
|
10
|
+
# A regexp that returns two groups: front_matter and content
|
11
|
+
attr_reader :regexp
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@regexp = build_regexp(*self.class.delimiters)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @see SyntaxParser
|
18
|
+
def call(string)
|
19
|
+
string.match(regexp)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @see Factorizable
|
23
|
+
# :nocov:
|
24
|
+
def self.delimiters
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# rubocop:disable Metrics/MethodLength
|
31
|
+
def build_regexp(delimiter)
|
32
|
+
/
|
33
|
+
\A
|
34
|
+
[[:space:]]*
|
35
|
+
(?<multiline_comment_indentation>^[[:blank:]]*)
|
36
|
+
#{delimiter}
|
37
|
+
[[:space:]]*
|
38
|
+
---
|
39
|
+
(?<front_matter>.*?)
|
40
|
+
---
|
41
|
+
[[:blank:]]*$[\n\r]
|
42
|
+
(?<content>.*)
|
43
|
+
\z
|
44
|
+
/mx
|
45
|
+
end
|
46
|
+
# rubocop:enable Metrics/MethodLength
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FrontMatterParser
|
4
|
+
module SyntaxParser
|
5
|
+
# Parser for syntaxes which use end and finish comment delimiters
|
6
|
+
class MultiLineComment
|
7
|
+
extend Factorizable
|
8
|
+
|
9
|
+
# @!attribute [r] regexp
|
10
|
+
# A regexp that returns two groups: front_matter and content
|
11
|
+
attr_reader :regexp
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@regexp = build_regexp(*self.class.delimiters)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @see SyntaxParser
|
18
|
+
def call(string)
|
19
|
+
string.match(regexp)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @see Factorizable
|
23
|
+
# :nocov:
|
24
|
+
def self.delimiters
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# rubocop:disable Metrics/MethodLength
|
31
|
+
def build_regexp(start_delimiter, end_delimiter)
|
32
|
+
/
|
33
|
+
\A
|
34
|
+
[[:space:]]*
|
35
|
+
#{start_delimiter}
|
36
|
+
[[:space:]]*
|
37
|
+
---
|
38
|
+
(?<front_matter>.*?)
|
39
|
+
---
|
40
|
+
[[:space:]]*
|
41
|
+
#{end_delimiter}
|
42
|
+
[[:blank:]]*$[\n\r]
|
43
|
+
(?<content>.*)
|
44
|
+
\z
|
45
|
+
/mx
|
46
|
+
end
|
47
|
+
# rubocop:enable Metrics/MethodLength
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FrontMatterParser
|
4
|
+
module SyntaxParser
|
5
|
+
# Parser for syntaxes which each comment is for a single line
|
6
|
+
class SingleLineComment
|
7
|
+
extend Factorizable
|
8
|
+
|
9
|
+
# @!attribute [r] regexp
|
10
|
+
# A regexp that returns two groups: front_matter (with comment delimiter
|
11
|
+
# in it) and content
|
12
|
+
attr_reader :regexp
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@regexp = build_regexp(*self.class.delimiters)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @see SyntaxParser
|
19
|
+
def call(string)
|
20
|
+
match = string.match(regexp)
|
21
|
+
if match
|
22
|
+
front_matter = self.class.remove_delimiter(match[:front_matter])
|
23
|
+
{
|
24
|
+
front_matter: front_matter,
|
25
|
+
content: match[:content]
|
26
|
+
}
|
27
|
+
else
|
28
|
+
match
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @see Factorizable
|
33
|
+
# :nocov:
|
34
|
+
def self.delimiters
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
# @!visibility private
|
39
|
+
def self.remove_delimiter(front_matter)
|
40
|
+
delimiter = delimiters.first
|
41
|
+
front_matter.gsub(/^[\s\t]*#{delimiter}/, '')
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# rubocop:disable Metrics/MethodLength
|
47
|
+
def build_regexp(delimiter)
|
48
|
+
/
|
49
|
+
\A
|
50
|
+
[[:space:]]*
|
51
|
+
#{delimiter}[[:blank:]]*
|
52
|
+
---
|
53
|
+
(?<front_matter>.*?)
|
54
|
+
^[[:blank:]]*#{delimiter}[[:blank:]]*
|
55
|
+
---
|
56
|
+
[[:blank:]]*$[\n\r]
|
57
|
+
(?<content>.*)
|
58
|
+
\z
|
59
|
+
/mx
|
60
|
+
end
|
61
|
+
# rubocop:enable Metrics/MethodLength
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/spec/fixtures/example
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FrontMatterParser::Loader::Yaml do
|
6
|
+
describe '#call' do
|
7
|
+
it 'loads using yaml parser' do
|
8
|
+
string = "title: 'hello'"
|
9
|
+
|
10
|
+
expect(described_class.new.call(string)).to eq(
|
11
|
+
'title' => 'hello'
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'loads with classes in allowlist' do
|
16
|
+
string = 'timestamp: 2017-10-17 00:00:00Z'
|
17
|
+
params = { allowlist_classes: [Time] }
|
18
|
+
|
19
|
+
expect(described_class.new(**params).call(string)).to eq(
|
20
|
+
'timestamp' => Time.parse('2017-10-17 00:00:00Z')
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,28 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module FrontMatterParser
|
4
|
-
describe Parsed do
|
5
|
-
|
6
|
-
let(:sample) { {'title' => 'hello'} }
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
8
|
-
|
9
|
-
---
|
10
|
-
title: hello
|
11
|
-
---
|
12
|
-
Content) }
|
3
|
+
require 'spec_helper'
|
13
4
|
|
14
|
-
|
5
|
+
describe FrontMatterParser::Parsed do
|
6
|
+
subject(:parsed) do
|
7
|
+
described_class.new(front_matter: front_matter, content: content)
|
8
|
+
end
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
expect(parsed.to_hash).to eq(sample)
|
19
|
-
end
|
20
|
-
end
|
10
|
+
let(:front_matter) { { 'title' => 'hello' } }
|
11
|
+
let(:content) { 'Content' }
|
21
12
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
13
|
+
describe '#[]' do
|
14
|
+
it 'returns front_matter value for given key' do
|
15
|
+
expect(parsed['title']).to eq('hello')
|
26
16
|
end
|
27
17
|
end
|
28
18
|
end
|