nodaire 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9b7c87d8f3775b1d06a7cf203296efcf40bf95904ba9ed7e8a1f09d0d946e21
4
- data.tar.gz: 2185bc3eea0363808665f4dbc6f6a04c7cb483544c92698dfd00200eee61673f
3
+ metadata.gz: 76e0ecf8fe992a52851c1c2f92aba32c4fa1e8c66a5442f7766521860624bdda
4
+ data.tar.gz: ecee4fc90ee93e2728177cce12b4d53383a0cbd6133cf8b4fc8a1e321a54b8e9
5
5
  SHA512:
6
- metadata.gz: ac327bc203d061ee6bef827b3d43b5c3c6e8cd6f754c5e4cce9bf63dac8996452de348de057218c27da87170d45db9e8ca81f3d52ac25df8106136944940d867
7
- data.tar.gz: '06508af2ec0ea395bc4b45c59c77d0b41490c6abf8cbc7ff565e86195b855e6bf24c4bcb2243d73a3da17f2e1b780ec04c50618709b41b0f098680503b148677'
6
+ metadata.gz: 47c3f94ae222712071c9a0cce903c9b400cfbc6ebda95fb3ff384b49cc42cade26cb3207bbaf298bfad7e5d60bfa6d402ddd48b0999f936b7cf5bcfa86040f52
7
+ data.tar.gz: f326c0167bcb4ba1af947e6689cb3059693d96116d83a65e6f1bb12f21801bf7d70f8898ac4c196a1d8abc01f1211314021d9a031b18d717fec6f8e1556e7a08
data/README.md CHANGED
@@ -86,6 +86,6 @@ bundle install
86
86
  Analyse the code and run unit tests using Bundler:
87
87
 
88
88
  ```sh
89
- bundle exec rubocop
90
- bundle exec rspec
89
+ bundle exec rake rubocop
90
+ bundle exec rake spec
91
91
  ```
data/lib/nodaire/base.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  #
6
6
  module Nodaire
7
7
  # The version number.
8
- VERSION = '0.3.0'
8
+ VERSION = '0.4.0'
9
9
  # The date when this version was released.
10
- DATE = '2019-08-18'
10
+ DATE = '2019-08-22'
11
11
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base'
4
+
3
5
  module Nodaire
4
6
  ##
5
7
  # This exception is raised if a parser error occurs.
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base'
4
- require_relative 'api/indental'
4
+ require_relative 'indental/indental'
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- require_relative '../parsers/indental_parser'
5
+ require_relative 'parser'
6
6
 
7
7
  ##
8
8
  # Interface for documents in _Indental_ format.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lexer'
4
+ require_relative '../util'
5
+
6
+ class Nodaire::Indental
7
+ # @private
8
+ INDENT_CHARS_ERROR = 'Indented with non-space characters'
9
+ # @private
10
+ INDENT_LEVEL_ERROR = 'Unexpected indent level'
11
+
12
+ # @private
13
+ class Lexer < Nodaire::Lexer
14
+ Token = Struct.new(:type, :key, :value, :line_num)
15
+
16
+ def self.tokenize(source)
17
+ lines_with_number(strip_js_wrapper(source))
18
+ .reject { |line, _| line.match(/^\s*(;.*)?$/) }
19
+ .map { |line, num| token_for_line(line, num) }
20
+ end
21
+
22
+ def self.token_for_line(line, num)
23
+ return error_token(INDENT_CHARS_ERROR, num) unless spaces_indent?(line)
24
+
25
+ case line.match(/^\s*/)[0].size
26
+ when 0 then category_token(line, num)
27
+ when 2 then key_or_list_token(line, num)
28
+ when 4 then list_item_token(line, num)
29
+ else error_token(INDENT_LEVEL_ERROR, num)
30
+ end
31
+ end
32
+
33
+ def self.spaces_indent?(line)
34
+ indent = line.match(/^\s*/)[0]
35
+ indent.match(/[^ ]/).nil?
36
+ end
37
+
38
+ def self.category_token(string, line_num)
39
+ Token.new :category, normalize(string), nil, line_num
40
+ end
41
+
42
+ def self.key_or_list_token(string, line_num)
43
+ key_value = string.match(/^(.+?) :( .+)?$/)
44
+
45
+ if key_value
46
+ key, value = key_value.captures
47
+ Token.new :key_value, normalize(key), normalize(value), line_num
48
+ else
49
+ Token.new :list_name, normalize(string), nil, line_num
50
+ end
51
+ end
52
+
53
+ def self.list_item_token(string, line_num)
54
+ Token.new :list_item, nil, normalize(string), line_num
55
+ end
56
+
57
+ def self.error_token(message, line_num)
58
+ Token.new :error, nil, normalize(message), line_num
59
+ end
60
+
61
+ def self.normalize(input)
62
+ Nodaire.squeeze(input)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lexer'
4
+ require_relative '../parser'
5
+ require_relative '../util'
6
+
7
+ class Nodaire::Indental
8
+ # @private
9
+ class Parser < Nodaire::Parser
10
+ attr_reader :data
11
+
12
+ def initialize(source, strict, options = {})
13
+ super(strict, options)
14
+
15
+ @symbolize_names = option(:symbolize_names, false)
16
+ @data = {}
17
+ @category = nil
18
+
19
+ parse! Nodaire::Indental::Lexer.tokenize(source)
20
+ end
21
+
22
+ private
23
+
24
+ Category = Struct.new(:name, :list_id, keyword_init: true)
25
+
26
+ attr_accessor :category
27
+ attr_reader :symbolize_names
28
+
29
+ def parse!(tokens)
30
+ tokens.each { |token| parse_token!(normalize_token(token)) }
31
+ end
32
+
33
+ def parse_token!(token)
34
+ case token.type
35
+ when :category then parse_category! token
36
+ when :key_value then parse_key_value! token
37
+ when :list_name then parse_list_name! token
38
+ when :list_item then parse_list_item! token
39
+ when :error then parse_error! token
40
+ end
41
+ end
42
+
43
+ def parse_category!(token)
44
+ if data.include?(token.key)
45
+ oops! 'Duplicate category', token.line_num
46
+ self.category = nil
47
+ else
48
+ add_category! token
49
+ end
50
+ end
51
+
52
+ def parse_key_value!(token)
53
+ return oops!('No category specified', token.line_num) if category.nil?
54
+
55
+ if data[category.name].include?(token.key)
56
+ oops! 'Duplicate key', token.line_num
57
+ category.list_id = nil
58
+ else
59
+ add_key_value! token
60
+ end
61
+ end
62
+
63
+ def parse_list_name!(token)
64
+ return oops!('No category specified', token.line_num) if category.nil?
65
+
66
+ if data[category.name].include?(token.key)
67
+ oops! 'Duplicate key for list', token.line_num
68
+ category.list_id = nil
69
+ else
70
+ add_list! token
71
+ end
72
+ end
73
+
74
+ def parse_list_item!(token)
75
+ if category.nil? || category.list_id.nil?
76
+ oops! 'No list specified', token.line_num
77
+ else
78
+ add_list_item! token
79
+ end
80
+ end
81
+
82
+ def add_category!(token)
83
+ self.category = Category.new(
84
+ name: token.key,
85
+ list_id: nil
86
+ )
87
+ data[token.key] = {}
88
+ end
89
+
90
+ def add_key_value!(token)
91
+ data[category.name][token.key] = token.value
92
+ category.list_id = nil
93
+ end
94
+
95
+ def add_list!(token)
96
+ data[category.name][token.key] = []
97
+ category.list_id = token.key
98
+ end
99
+
100
+ def add_list_item!(token)
101
+ data[category.name][category.list_id] << token.value
102
+ end
103
+
104
+ def parse_error!(token)
105
+ oops! token.value, token.line_num
106
+ end
107
+
108
+ def normalize_token(token)
109
+ token.tap { |t| t.key = normalize_key(t.key) }
110
+ end
111
+
112
+ def normalize_key(key)
113
+ return if key.nil?
114
+
115
+ if symbolize_names
116
+ Nodaire.symbolize(key)
117
+ else
118
+ key.upcase
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Nodaire
6
+ # @private
7
+ class Lexer
8
+ JS_WRAPPER_REGEXP = %r{
9
+ ^ \s*[^\n`]+ = [[:blank:]]* ` [[:blank:]]* \n
10
+ (.*\n)
11
+ [[:blank:]]* ` \s* $
12
+ }mx.freeze
13
+
14
+ def self.lines_with_number(source)
15
+ (source || '')
16
+ .split("\n").each_with_index
17
+ .map { |line, idx| [line, idx + 1] }
18
+ end
19
+
20
+ def self.strip_js_wrapper(source)
21
+ (source || '').sub(JS_WRAPPER_REGEXP, '\1')
22
+ end
23
+ end
24
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
3
+ require_relative 'base'
4
+ require_relative 'errors'
4
5
 
5
6
  # @private
6
7
  class Nodaire::Parser
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base'
4
- require_relative 'api/tablatal'
4
+ require_relative 'tablatal/tablatal'
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../parser'
4
+ require_relative '../util'
5
+
6
+ class Nodaire::Tablatal
7
+ # @private
8
+ class Parser < Nodaire::Parser
9
+ attr_reader :data
10
+
11
+ def initialize(string, strict, options = {})
12
+ super(strict, options)
13
+
14
+ @symbolize_names = option(:symbolize_names, false)
15
+ @data = []
16
+ @keys = []
17
+
18
+ parse! string
19
+ end
20
+
21
+ def keys
22
+ @keys.map(&:name)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :symbolize_names
28
+
29
+ Key = Struct.new(:name, :range, keyword_init: true)
30
+
31
+ def parse!(string)
32
+ lines = (string || '').strip.split("\n")
33
+ .reject { |line| line.match(/^\s*(;.*)?$/) }
34
+ return if lines.empty?
35
+
36
+ @keys = filter_keys(make_keys(lines.shift.scan(/(\S+\s*)/).flatten))
37
+ @data = lines.map { |line| make_line(line) }.compact
38
+ end
39
+
40
+ def make_keys(segs)
41
+ keys = []
42
+ start = 0
43
+ segs.each_with_index do |seg, idx|
44
+ len = seg.size if idx < segs.size - 1
45
+ range_end = len ? start + len - 1 : -1
46
+ keys << Key.new(name: normalize_key(seg), range: start..range_end)
47
+ start += len if len
48
+ end
49
+ keys
50
+ end
51
+
52
+ def filter_keys(keys)
53
+ result = []
54
+ keys.each do |key|
55
+ if result.any? { |k| k.name == key.name }
56
+ oops! "Duplicate key #{key.name}", 1
57
+ else
58
+ result << key
59
+ end
60
+ end
61
+ result
62
+ end
63
+
64
+ def make_line(line)
65
+ @keys.map { |key| [key.name, Nodaire.squeeze(line[key.range])] }.to_h
66
+ end
67
+
68
+ def normalize_key(string)
69
+ if symbolize_names
70
+ Nodaire.symbolize(string)
71
+ else
72
+ Nodaire.squeeze(string).upcase
73
+ end
74
+ end
75
+ end
76
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'csv'
4
4
 
5
- require_relative '../parsers/tablatal_parser'
5
+ require_relative 'parser'
6
6
 
7
7
  ##
8
8
  # Interface for documents in _Tablatal_ format.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Nodaire
6
+ ##
7
+ # Normalize the whitespace in a string.
8
+ #
9
+ # This strips the string and replaces each sequence of whitespace with
10
+ # a single space.
11
+ #
12
+ # @param [String] string
13
+ #
14
+ # @since 0.4.0
15
+ # @return [String]
16
+ #
17
+ def self.squeeze(string)
18
+ (string || '').gsub(/\s+/, ' ').strip
19
+ end
20
+
21
+ ##
22
+ # Convert a string into a normalized symbol.
23
+ #
24
+ # This converts to lower case and replaces each sequence of non-alphanumeric
25
+ # characters with an underscore.
26
+ #
27
+ # @param [String] string
28
+ #
29
+ # @since 0.4.0
30
+ # @return [Symbol]
31
+ #
32
+ def self.symbolize(string)
33
+ squeeze(string).downcase.gsub(/[^a-z0-9]+/, '_').to_sym
34
+ end
35
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nodaire
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-18 00:00:00.000000000 Z
11
+ date: 2019-08-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Nodaire is a collection of text file parsers.
14
14
  email: nodaire@liamcooke.com
@@ -19,15 +19,18 @@ files:
19
19
  - LICENSE
20
20
  - README.md
21
21
  - lib/nodaire.rb
22
- - lib/nodaire/api/indental.rb
23
- - lib/nodaire/api/tablatal.rb
24
22
  - lib/nodaire/base.rb
25
23
  - lib/nodaire/errors.rb
26
24
  - lib/nodaire/indental.rb
27
- - lib/nodaire/parsers/indental_parser.rb
28
- - lib/nodaire/parsers/parser.rb
29
- - lib/nodaire/parsers/tablatal_parser.rb
25
+ - lib/nodaire/indental/indental.rb
26
+ - lib/nodaire/indental/lexer.rb
27
+ - lib/nodaire/indental/parser.rb
28
+ - lib/nodaire/lexer.rb
29
+ - lib/nodaire/parser.rb
30
30
  - lib/nodaire/tablatal.rb
31
+ - lib/nodaire/tablatal/parser.rb
32
+ - lib/nodaire/tablatal/tablatal.rb
33
+ - lib/nodaire/util.rb
31
34
  homepage: https://github.com/slisne/nodaire
32
35
  licenses:
33
36
  - MIT
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'parser'
4
-
5
- class Nodaire::Indental
6
- # @private
7
- class Parser < Nodaire::Parser
8
- attr_reader :data
9
-
10
- def initialize(string, strict, options = {})
11
- super(strict, options)
12
-
13
- @symbolize_names = option(:symbolize_names, false)
14
- @data = {}
15
- @category_ids = {}
16
- @category = nil
17
-
18
- parse! string
19
- end
20
-
21
- private
22
-
23
- Category = Struct.new(:name, :key_ids, :list_id, keyword_init: true)
24
-
25
- attr_accessor :category
26
- attr_reader :symbolize_names, :category_ids
27
-
28
- def parse!(string)
29
- lines = lines_to_parse(string)
30
- lines = lines[1...-1] if js_wrapper?(lines)
31
- lines.each { |line, num| parse_line! line, num }
32
- end
33
-
34
- def lines_to_parse(string)
35
- (string || '')
36
- .split("\n").each_with_index
37
- .reject { |line, _| line.match(/^\s*(;.*)?$/) }
38
- .map { |line, idx| [line, idx + 1] }
39
- end
40
-
41
- def js_wrapper?(lines)
42
- !lines.empty? &&
43
- lines.first[0].match(/=\s*`\s*$/) &&
44
- lines.last[0].strip == '`'
45
- end
46
-
47
- def parse_line!(line, num)
48
- case line.match(/^\s*/)[0].size
49
- when 0 then parse_category! line.strip, num
50
- when 2 then parse_key_or_list! line.strip, num
51
- when 4 then parse_list_item! line.strip, num
52
- else oops! 'Unexpected indent', num
53
- end
54
- end
55
-
56
- def parse_category!(cat, num)
57
- id = normalize_sym(cat)
58
-
59
- if category_ids.include?(id)
60
- oops! 'Duplicate category', num
61
- self.category = nil
62
- else
63
- self.category = Category.new(
64
- name: symbolize_names ? id : normalize_text(cat),
65
- key_ids: {},
66
- list_id: nil
67
- )
68
- data[category.name] = {}
69
- category_ids[id] = category.name
70
- end
71
- end
72
-
73
- def parse_key_or_list!(line, num)
74
- return oops!('No category specified', num) if category.nil?
75
-
76
- if line.include?(' : ')
77
- key, value = line.split(' : ', 2)
78
- parse_key_value!(key, value, num)
79
- else
80
- parse_list!(line, num)
81
- end
82
- end
83
-
84
- def parse_key_value!(key, value, num)
85
- id = normalize_sym(key)
86
- key_name = symbolize_names ? id : normalize_text(key)
87
-
88
- if category.key_ids.include?(id)
89
- oops! 'Duplicate key', num
90
- else
91
- data[category.name][key_name] = normalize_text(value)
92
- category.key_ids[id] = key_name
93
- end
94
-
95
- category.list_id = nil
96
- end
97
-
98
- def parse_list!(key, num)
99
- id = normalize_sym(key)
100
- list_name = symbolize_names ? id : normalize_text(key)
101
-
102
- if category.key_ids.include?(id)
103
- oops! 'Duplicate key for list', num
104
- category.list_id = nil
105
- else
106
- data[category.name][list_name] = []
107
- category.key_ids[id] = list_name
108
- category.list_id = id
109
- end
110
- end
111
-
112
- def parse_list_item!(item, num)
113
- if category.nil? || category.list_id.nil?
114
- oops! 'No list specified', num
115
- else
116
- list_name = category.key_ids[category.list_id]
117
- data[category.name][list_name] << normalize_text(item)
118
- end
119
- end
120
-
121
- def normalize_text(string)
122
- string.split.join(' ')
123
- end
124
-
125
- def normalize_sym(key)
126
- key.downcase.gsub(/[\s_-]+/, ' ').split.join('_').to_sym
127
- end
128
- end
129
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'parser'
4
-
5
- class Nodaire::Tablatal
6
- # @private
7
- class Parser < Nodaire::Parser
8
- attr_reader :data
9
-
10
- def initialize(string, strict, options = {})
11
- super(strict, options)
12
-
13
- @symbolize_names = option(:symbolize_names, false)
14
-
15
- @data = []
16
- @keys = []
17
- @key_ids = {}
18
-
19
- parse! string
20
- end
21
-
22
- def keys
23
- @keys.map(&:name)
24
- end
25
-
26
- private
27
-
28
- attr_reader :symbolize_names, :key_ids
29
-
30
- Key = Struct.new(:name, :range, keyword_init: true)
31
-
32
- def parse!(string)
33
- lines = (string || '').strip.split("\n")
34
- .reject { |line| line.match(/^\s*(;.*)?$/) }
35
- return if lines.empty?
36
-
37
- @keys = make_keys(lines.shift.scan(/(\S+\s*)/).flatten)
38
- @data = lines.map { |line| make_line(line) }.compact
39
- end
40
-
41
- def make_keys(segs)
42
- [].tap do |keys|
43
- start = 0
44
- segs.each_with_index do |seg, idx|
45
- len = seg.size if idx < segs.size - 1
46
- id = normalize_sym(seg)
47
- key_name = symbolize_names ? id : normalize_text(seg)
48
-
49
- if key_ids.include?(id)
50
- oops! "Duplicate key #{key_name}", 1
51
- else
52
- range_end = len ? start + len - 1 : -1
53
- key_ids[id] = key_name
54
- keys << Key.new(name: key_name, range: start..range_end)
55
- end
56
-
57
- start += len if len
58
- end
59
- end
60
- end
61
-
62
- def make_line(line)
63
- @keys.map { |key| [key.name, normalize_text(line[key.range])] }.to_h
64
- end
65
-
66
- def normalize_text(string)
67
- string ? string.split.join(' ') : ''
68
- end
69
-
70
- def normalize_sym(key)
71
- key.downcase.gsub(/[_-]+/, ' ').split.join('_').to_sym
72
- end
73
- end
74
- end