nodaire 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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