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.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +19 -0
  3. data/.gitignore +1 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +50 -0
  6. data/.travis.yml +19 -1
  7. data/CHANGELOG.md +30 -0
  8. data/Dockerfile +5 -0
  9. data/Gemfile +0 -2
  10. data/README.md +76 -38
  11. data/Rakefile +5 -3
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/docker-compose.yml +12 -0
  15. data/front_matter_parser.gemspec +12 -6
  16. data/lib/front_matter_parser.rb +10 -114
  17. data/lib/front_matter_parser/loader.rb +11 -0
  18. data/lib/front_matter_parser/loader/yaml.rb +26 -0
  19. data/lib/front_matter_parser/parsed.rb +25 -14
  20. data/lib/front_matter_parser/parser.rb +82 -0
  21. data/lib/front_matter_parser/syntax_parser.rb +28 -0
  22. data/lib/front_matter_parser/syntax_parser/factorizable.rb +30 -0
  23. data/lib/front_matter_parser/syntax_parser/indentation_comment.rb +49 -0
  24. data/lib/front_matter_parser/syntax_parser/multi_line_comment.rb +50 -0
  25. data/lib/front_matter_parser/syntax_parser/single_line_comment.rb +64 -0
  26. data/lib/front_matter_parser/version.rb +3 -1
  27. data/spec/fixtures/example +6 -0
  28. data/spec/front_matter_parser/loader/yaml_spec.rb +24 -0
  29. data/spec/front_matter_parser/parsed_spec.rb +11 -21
  30. data/spec/front_matter_parser/parser_spec.rb +111 -0
  31. data/spec/front_matter_parser/syntax_parser/indentation_comment_spec.rb +166 -0
  32. data/spec/front_matter_parser/syntax_parser/multi_line_comment_spec.rb +267 -0
  33. data/spec/front_matter_parser/syntax_parser/single_line_comment_spec.rb +175 -0
  34. data/spec/front_matter_parser_spec.rb +3 -296
  35. data/spec/spec_helper.rb +9 -3
  36. data/spec/support/matcher.rb +8 -0
  37. metadata +110 -46
  38. data/spec/fixtures/example.coffee +0 -4
  39. data/spec/fixtures/example.erb +0 -6
  40. data/spec/fixtures/example.foo +0 -0
  41. data/spec/fixtures/example.haml +0 -5
  42. data/spec/fixtures/example.liquid +0 -6
  43. data/spec/fixtures/example.md +0 -4
  44. data/spec/fixtures/example.sass +0 -4
  45. data/spec/fixtures/example.scss +0 -4
  46. data/spec/fixtures/example.slim +0 -5
  47. data/spec/support/strings.rb +0 -41
  48. data/spec/support/syntaxs.rb +0 -14
  49. 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
- # Parsed is the result of calling {FrontMatterParser.parse} or {FrontMatterParser.parse_file} in {FrontMatterParser}. It keeps the front matter and the content parsed and it has some useful methods to work with them.
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
- alias_method :to_hash, :front_matter
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
- # Returns the front matter value for the given key
12
- #
13
- # @param key [String] The key of the front matter
14
- # @return [String, Array, Hash] The value of the front matter for the given key
15
- def [](key)
16
- @front_matter[key]
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrontMatterParser
2
- VERSION = "0.0.4"
4
+ VERSION = '1.0.0'
3
5
  end
@@ -0,0 +1,6 @@
1
+ <!--
2
+ ---
3
+ title: hello
4
+ ---
5
+ -->
6
+ Content
@@ -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
- require 'spec_helper'
2
-
3
- module FrontMatterParser
4
- describe Parsed do
5
-
6
- let(:sample) { {'title' => 'hello'} }
1
+ # frozen_string_literal: true
7
2
 
8
- let(:string) { %Q(
9
- ---
10
- title: hello
11
- ---
12
- Content) }
3
+ require 'spec_helper'
13
4
 
14
- let(:parsed) { FrontMatterParser.parse(string) }
5
+ describe FrontMatterParser::Parsed do
6
+ subject(:parsed) do
7
+ described_class.new(front_matter: front_matter, content: content)
8
+ end
15
9
 
16
- describe "#to_hash" do
17
- it "returns @front_matter" do
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
- describe "#[]" do
23
- it "returns the front matter value for the given key" do
24
- expect(parsed['title']).to eq('hello')
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