tomlrb 1.3.0 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,17 +7,24 @@ module Tomlrb
7
7
  @current = @output
8
8
  @stack = []
9
9
  @array_names = []
10
+ @current_table = []
11
+ @keys = Keys.new
10
12
  @symbolize_keys = options[:symbolize_keys]
11
13
  end
12
14
 
13
15
  def set_context(identifiers, is_array_of_tables: false)
16
+ if identifiers.empty?
17
+ raise ParseError, 'Array needs a name'
18
+ end
19
+
20
+ @current_table = identifiers.dup
21
+ @keys.add_table_key identifiers, is_array_of_tables
14
22
  @current = @output
15
23
 
16
24
  deal_with_array_of_tables(identifiers, is_array_of_tables) do |identifierz|
17
25
  identifierz.each do |k|
18
26
  k = k.to_sym if @symbolize_keys
19
27
  if @current[k].is_a?(Array)
20
- @current[k] << {} if @current[k].empty?
21
28
  @current = @current[k].last
22
29
  else
23
30
  @current[k] ||= {}
@@ -28,7 +35,6 @@ module Tomlrb
28
35
  end
29
36
 
30
37
  def deal_with_array_of_tables(identifiers, is_array_of_tables)
31
- identifiers.map!{|n| n.gsub("\"", '')}
32
38
  stringified_identifier = identifiers.join('.')
33
39
 
34
40
  if is_array_of_tables
@@ -43,20 +49,50 @@ module Tomlrb
43
49
  if is_array_of_tables
44
50
  last_identifier = last_identifier.to_sym if @symbolize_keys
45
51
  @current[last_identifier] ||= []
52
+ raise ParseError, "Cannot use key #{last_identifier} for both table and array at once" unless @current[last_identifier].respond_to?(:<<)
46
53
  @current[last_identifier] << {}
47
54
  @current = @current[last_identifier].last
48
55
  end
49
56
  end
50
57
 
51
58
  def assign(k)
52
- k = k.to_sym if @symbolize_keys
53
- @current[k] = @stack.pop
59
+ @keys.add_pair_key k, @current_table
60
+ current = @current
61
+ while key = k.shift
62
+ key = key.to_sym if @symbolize_keys
63
+ current = assign_key_path(current, key, k.empty?)
64
+ end
54
65
  end
55
66
 
56
67
  def push(o)
57
68
  @stack << o
58
69
  end
59
70
 
71
+ def push_inline(inline_arrays)
72
+ merged_inline = {}
73
+
74
+ inline_arrays.each do |inline_array|
75
+ current = merged_inline
76
+ value = inline_array.pop
77
+ inline_array.each_with_index do |inline_key, inline_index|
78
+ last_key = inline_index == inline_array.size - 1
79
+
80
+ if last_key
81
+ if current[inline_key].nil?
82
+ current[inline_key] = value
83
+ else
84
+ raise Key::KeyConflict, "Inline key #{inline_key} is already used"
85
+ end
86
+ else
87
+ current[inline_key] ||= {}
88
+ current = current[inline_key]
89
+ end
90
+ end
91
+ end
92
+
93
+ push(merged_inline)
94
+ end
95
+
60
96
  def start_(type)
61
97
  push([type])
62
98
  end
@@ -64,10 +100,166 @@ module Tomlrb
64
100
  def end_(type)
65
101
  array = []
66
102
  while (value = @stack.pop) != [type]
67
- raise ParseError, 'Unclosed table' if value.nil?
103
+ raise ParseError, 'Unclosed table' if @stack.empty?
68
104
  array.unshift(value)
69
105
  end
70
106
  array
71
107
  end
108
+
109
+ def validate_value(value)
110
+ if value.nil?
111
+ raise ParseError, 'Value must be present'
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def assign_key_path(current, key, key_emptied)
118
+ if key_emptied
119
+
120
+ raise ParseError, "Cannot overwrite value with key #{key}" unless current.kind_of?(Hash)
121
+ current[key] = @stack.pop
122
+ return current
123
+ end
124
+ current[key] ||= {}
125
+ current = current[key]
126
+ current
127
+ end
128
+ end
129
+
130
+ class Keys
131
+ def initialize
132
+ @keys = {}
133
+ end
134
+
135
+ def add_table_key(keys, is_array_of_tables = false)
136
+ self << [keys, [], is_array_of_tables]
137
+ end
138
+
139
+ def add_pair_key(keys, context)
140
+ self << [context, keys, false]
141
+ end
142
+
143
+ def <<(keys)
144
+ table_keys, pair_keys, is_array_of_tables = keys
145
+ current = @keys
146
+ current = append_table_keys(current, table_keys, pair_keys.empty?, is_array_of_tables)
147
+ append_pair_keys(current, pair_keys, table_keys.empty?, is_array_of_tables)
148
+ end
149
+
150
+ private
151
+
152
+ def append_table_keys(current, table_keys, pair_keys_empty, is_array_of_tables)
153
+ table_keys.each_with_index do |key, index|
154
+ declared = (index == table_keys.length - 1) && pair_keys_empty
155
+ if index == 0
156
+ current = find_or_create_first_table_key(current, key, declared, is_array_of_tables)
157
+ else
158
+ current = current << [key, :table, declared, is_array_of_tables]
159
+ end
160
+ end
161
+
162
+ current.clear_children if is_array_of_tables
163
+ current
164
+ end
165
+
166
+ def find_or_create_first_table_key(current, key, declared, is_array_of_tables)
167
+ existed = current[key]
168
+ if existed && existed.type == :pair
169
+ raise Key::KeyConflict, "Key #{key} is already used as #{existed.type} key"
170
+ end
171
+ if existed && existed.declared? && declared && ! is_array_of_tables
172
+ raise Key::KeyConflict, "Key #{key} is already used"
173
+ end
174
+ k = existed || Key.new(key, :table, declared)
175
+ current[key] = k
176
+ k
177
+ end
178
+
179
+ def append_pair_keys(current, pair_keys, table_keys_empty, is_array_of_tables)
180
+ pair_keys.each_with_index do |key, index|
181
+ declared = index == pair_keys.length - 1
182
+ if index == 0 && table_keys_empty
183
+ current = find_or_create_first_pair_key(current, key, declared, table_keys_empty)
184
+ else
185
+ key = current << [key, :pair, declared, is_array_of_tables]
186
+ current = key
187
+ end
188
+ end
189
+ end
190
+
191
+ def find_or_create_first_pair_key(current, key, declared, table_keys_empty)
192
+ existed = current[key]
193
+ if existed && (existed.type == :pair) && declared && table_keys_empty
194
+ raise Key::KeyConflict, "Key #{key} is already used"
195
+ end
196
+ k = Key.new(key, :pair, declared)
197
+ current[key] = k
198
+ k
199
+ end
200
+ end
201
+
202
+ class Key
203
+ class KeyConflict < ParseError; end
204
+
205
+ attr_reader :key, :type
206
+
207
+ def initialize(key, type, declared = false)
208
+ @key = key
209
+ @type = type
210
+ @declared = declared
211
+ @children = {}
212
+ end
213
+
214
+ def declared?
215
+ @declared
216
+ end
217
+
218
+ def <<(key_type_declared)
219
+ key, type, declared, is_array_of_tables = key_type_declared
220
+ existed = @children[key]
221
+ validate_already_declared_as_different_key(type, declared, existed)
222
+ validate_already_declared_as_non_array_table(type, is_array_of_tables, declared, existed)
223
+ validate_path_already_created_as_different_type(type, declared, existed)
224
+ validate_path_already_declared_as_different_type(type, declared, existed)
225
+ validate_already_declared_as_same_key(declared, existed)
226
+ @children[key] = existed || self.class.new(key, type, declared)
227
+ end
228
+
229
+ def clear_children
230
+ @children.clear
231
+ end
232
+
233
+ private
234
+
235
+ def validate_already_declared_as_different_key(type, declared, existed)
236
+ if existed && existed.declared? && existed.type != type
237
+ raise KeyConflict, "Key #{existed.key} is already used as #{existed.type} key"
238
+ end
239
+ end
240
+
241
+ def validate_already_declared_as_non_array_table(type, is_array_of_tables, declared, existed)
242
+ if declared && type == :table && existed && existed.declared? && ! is_array_of_tables
243
+ raise KeyConflict, "Key #{existed.key} is already used"
244
+ end
245
+ end
246
+
247
+ def validate_path_already_created_as_different_type(type, declared, existed)
248
+ if declared && (type == :table) && existed && (existed.type == :pair) && (! existed.declared?)
249
+ raise KeyConflict, "Key #{existed.key} is already used as #{existed.type} key"
250
+ end
251
+ end
252
+
253
+ def validate_path_already_declared_as_different_type(type, declared, existed)
254
+ if ! declared && (type == :pair) && existed && (existed.type == :pair) && existed.declared?
255
+ raise KeyConflict, "Key #{key} is already used as #{type} key"
256
+ end
257
+ end
258
+
259
+ def validate_already_declared_as_same_key(declared, existed)
260
+ if existed && ! existed.declared? && declared
261
+ raise KeyConflict, "Key #{existed.key} is already used as #{existed.type} key"
262
+ end
263
+ end
72
264
  end
73
265
  end
@@ -0,0 +1,33 @@
1
+ require 'forwardable'
2
+
3
+ module Tomlrb
4
+ class LocalDate
5
+ extend Forwardable
6
+
7
+ def_delegators :@time, :year, :month, :day
8
+
9
+ def initialize(year, month, day)
10
+ @time = Time.new(year, month, day, 0, 0, 0, '-00:00')
11
+ end
12
+
13
+ # @param offset see {LocalDateTime#to_time}
14
+ # @return [Time] 00:00:00 of the date
15
+ def to_time(offset='-00:00')
16
+ return @time if offset == '-00:00'
17
+ Time.new(year, month, day, 0, 0, 0, offset)
18
+ end
19
+
20
+ def to_s
21
+ @time.strftime('%F')
22
+ end
23
+
24
+ def ==(other)
25
+ other.kind_of?(self.class) &&
26
+ to_time == other.to_time
27
+ end
28
+
29
+ def inspect
30
+ "#<#{self.class}: #{to_s}>"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ require 'forwardable'
2
+
3
+ module Tomlrb
4
+ class LocalDateTime
5
+ extend Forwardable
6
+
7
+ def_delegators :@time, :year, :month, :day, :hour, :min, :sec, :usec, :nsec
8
+
9
+ def initialize(year, month, day, hour, min, sec) # rubocop:disable Metrics/ParameterLists
10
+ @time = Time.new(year, month, day, hour, min, sec, '-00:00')
11
+ @sec = sec
12
+ end
13
+
14
+ # @param offset [String, Symbol, Numeric, nil] time zone offset.
15
+ # * when +String+, must be '+HH:MM' format, '-HH:MM' format, 'UTC', 'A'..'I' or 'K'..'Z'. Arguments excluding '+-HH:MM' are supporeted at Ruby >= 2.7.0
16
+ # * when +Symbol+, must be +:dst+(for summar time for local) or +:std+(for standard time).
17
+ # * when +Numeric+, it is time zone offset in second.
18
+ # * when +nil+, local time zone offset is used.
19
+ # @return [Time]
20
+ def to_time(offset='-00:00')
21
+ return @time if offset == '-00:00'
22
+ Time.new(year, month, day, hour, min, @sec, offset)
23
+ end
24
+
25
+ def to_s
26
+ frac = (@sec - sec)
27
+ frac_str = frac == 0 ? '' : "#{frac.to_s[1..-1]}"
28
+ @time.strftime("%FT%T") << frac_str
29
+ end
30
+
31
+ def ==(other)
32
+ other.kind_of?(self.class) &&
33
+ to_time == other.to_time
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class}: #{to_s}>"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+
3
+ module Tomlrb
4
+ class LocalTime
5
+ extend Forwardable
6
+
7
+ def_delegators :@time, :hour, :min, :sec, :usec, :nsec
8
+
9
+ def initialize(hour, min, sec)
10
+ @time = Time.new(0, 1, 1, hour, min, sec, '-00:00')
11
+ @sec = sec
12
+ end
13
+
14
+ # @param year [Integer]
15
+ # @param month [Integer]
16
+ # @param day [Integer]
17
+ # @param offset see {LocalDateTime#to_time}
18
+ # @return [Time] the time of the date specified by params
19
+ def to_time(year, month, day, offset='-00:00')
20
+ Time.new(year, month, day, hour, min, @sec, offset)
21
+ end
22
+
23
+ def to_s
24
+ frac = (@sec - sec)
25
+ frac_str = frac == 0 ? '' : "#{frac.to_s[1..-1]}"
26
+ @time.strftime("%T") << frac_str
27
+ end
28
+
29
+ def ==(other)
30
+ other.kind_of?(self.class) &&
31
+ @time == other.to_time(0, 1, 1)
32
+ end
33
+
34
+ def inspect
35
+ "#<#{self.class}: #{to_s}>"
36
+ end
37
+ end
38
+ end
data/lib/tomlrb/parser.y CHANGED
@@ -1,16 +1,19 @@
1
1
  class Tomlrb::GeneratedParser
2
- token IDENTIFIER STRING_MULTI STRING_BASIC STRING_LITERAL_MULTI STRING_LITERAL DATETIME INTEGER FLOAT FLOAT_INF FLOAT_NAN TRUE FALSE
2
+ token IDENTIFIER STRING_MULTI STRING_BASIC STRING_LITERAL_MULTI STRING_LITERAL DATETIME LOCAL_TIME INTEGER NON_DEC_INTEGER FLOAT FLOAT_KEYWORD BOOLEAN NEWLINE EOS
3
3
  rule
4
4
  expressions
5
5
  | expressions expression
6
+ | expressions EOS
6
7
  ;
7
8
  expression
8
9
  : table
9
10
  | assignment
10
11
  | inline_table
12
+ | NEWLINE
11
13
  ;
12
14
  table
13
- : table_start table_continued
15
+ : table_start table_continued NEWLINE
16
+ | table_start table_continued EOS
14
17
  ;
15
18
  table_start
16
19
  : '[' '[' { @handler.start_(:array_of_tables) }
@@ -27,55 +30,112 @@ rule
27
30
  | '.' table_continued
28
31
  ;
29
32
  table_identifier
30
- : IDENTIFIER { @handler.push(val[0]) }
31
- | STRING_BASIC { @handler.push(val[0]) }
32
- | STRING_LITERAL { @handler.push(val[0]) }
33
- | INTEGER { @handler.push(val[0]) }
34
- | TRUE { @handler.push(val[0]) }
35
- | FALSE { @handler.push(val[0]) }
33
+ : table_identifier '.' table_identifier_component { @handler.push(val[2]) }
34
+ | table_identifier '.' FLOAT { val[2].split('.').each { |k| @handler.push(k) } }
35
+ | FLOAT {
36
+ keys = val[0].split('.')
37
+ @handler.start_(:table)
38
+ keys.each { |key| @handler.push(key) }
39
+ }
40
+ | table_identifier_component { @handler.push(val[0]) }
41
+ ;
42
+ table_identifier_component
43
+ : IDENTIFIER
44
+ | STRING_BASIC { result = StringUtils.replace_escaped_chars(val[0]) }
45
+ | STRING_LITERAL
46
+ | INTEGER
47
+ | NON_DEC_INTEGER
48
+ | FLOAT_KEYWORD
49
+ | BOOLEAN
36
50
  ;
37
51
  inline_table
38
- : inline_table_start inline_continued
52
+ : inline_table_start inline_table_end
53
+ | inline_table_start inline_continued inline_table_end
39
54
  ;
40
55
  inline_table_start
41
56
  : '{' { @handler.start_(:inline) }
42
57
  ;
58
+ inline_table_end
59
+ : '}' {
60
+ array = @handler.end_(:inline)
61
+ @handler.push_inline(array)
62
+ }
63
+ ;
43
64
  inline_continued
44
- : '}' { array = @handler.end_(:inline); @handler.push(Hash[*array]) }
45
- | inline_assignment_key inline_assignment_value inline_next
65
+ : inline_assignment
66
+ | inline_assignment inline_next
46
67
  ;
47
68
  inline_next
48
- : '}' {
49
- array = @handler.end_(:inline)
50
- array.map!.with_index{ |n,i| i.even? ? n.to_sym : n } if @handler.symbolize_keys
51
- @handler.push(Hash[*array])
69
+ : ',' inline_continued
70
+ ;
71
+ inline_assignment
72
+ : inline_assignment_key '=' value {
73
+ keys = @handler.end_(:inline_keys)
74
+ @handler.push(keys)
52
75
  }
53
- | ',' inline_continued
54
76
  ;
55
77
  inline_assignment_key
56
- : IDENTIFIER { @handler.push(val[0]) }
57
- ;
58
- inline_assignment_value
59
- : '=' value
78
+ : inline_assignment_key '.' assignment_key_component {
79
+ @handler.push(val[2])
80
+ }
81
+ | inline_assignment_key '.' FLOAT { val[2].split('.').each { |k| @handler.push(k) } }
82
+ | FLOAT {
83
+ keys = val[0].split('.')
84
+ @handler.start_(:inline_keys)
85
+ keys.each { |key| @handler.push(key) }
86
+ }
87
+ | assignment_key_component {
88
+ @handler.start_(:inline_keys)
89
+ @handler.push(val[0])
90
+ }
60
91
  ;
61
92
  assignment
62
- : IDENTIFIER '=' value { @handler.assign(val[0]) }
63
- | STRING_BASIC '=' value { @handler.assign(val[0]) }
64
- | STRING_LITERAL '=' value { @handler.assign(val[0]) }
65
- | INTEGER '=' value { @handler.assign(val[0]) }
66
- | TRUE '=' value { @handler.assign(val[0]) }
67
- | FALSE '=' value { @handler.assign(val[0]) }
93
+ : assignment_key '=' value EOS {
94
+ keys = @handler.end_(:keys)
95
+ value = keys.pop
96
+ @handler.validate_value(value)
97
+ @handler.push(value)
98
+ @handler.assign(keys)
99
+ }
100
+ | assignment_key '=' value NEWLINE {
101
+ keys = @handler.end_(:keys)
102
+ value = keys.pop
103
+ @handler.validate_value(value)
104
+ @handler.push(value)
105
+ @handler.assign(keys)
106
+ }
107
+ ;
108
+ assignment_key
109
+ : assignment_key '.' assignment_key_component { @handler.push(val[2]) }
110
+ | assignment_key '.' FLOAT { val[2].split('.').each { |k| @handler.push(k) } }
111
+ | FLOAT {
112
+ keys = val[0].split('.')
113
+ @handler.start_(:keys)
114
+ keys.each { |key| @handler.push(key) }
115
+ }
116
+ | assignment_key_component { @handler.start_(:keys); @handler.push(val[0]) }
117
+ ;
118
+ assignment_key_component
119
+ : IDENTIFIER
120
+ | STRING_BASIC { result = StringUtils.replace_escaped_chars(val[0]) }
121
+ | STRING_LITERAL
122
+ | INTEGER
123
+ | NON_DEC_INTEGER
124
+ | FLOAT_KEYWORD
125
+ | BOOLEAN
68
126
  ;
69
127
  array
70
128
  : start_array array_continued
71
129
  ;
72
130
  array_continued
73
- : ']' { array = @handler.end_(:array); @handler.push(array) }
131
+ : ']' { array = @handler.end_(:array); @handler.push(array.compact) }
74
132
  | value array_next
133
+ | NEWLINE array_continued
75
134
  ;
76
135
  array_next
77
- : ']' { array = @handler.end_(:array); @handler.push(array) }
136
+ : ']' { array = @handler.end_(:array); @handler.push(array.compact) }
78
137
  | ',' array_continued
138
+ | NEWLINE array_continued
79
139
  ;
80
140
  start_array
81
141
  : '[' { @handler.start_(:array) }
@@ -91,12 +151,42 @@ rule
91
151
  ;
92
152
  literal
93
153
  | FLOAT { result = val[0].to_f }
94
- | FLOAT_INF { result = (val[0][0] == '-' ? -1 : 1) * Float::INFINITY }
95
- | FLOAT_NAN { result = Float::NAN }
154
+ | FLOAT_KEYWORD {
155
+ v = val[0]
156
+ result = if v.end_with?('nan')
157
+ Float::NAN
158
+ else
159
+ (v[0] == '-' ? -1 : 1) * Float::INFINITY
160
+ end
161
+ }
96
162
  | INTEGER { result = val[0].to_i }
97
- | TRUE { result = true }
98
- | FALSE { result = false }
99
- | DATETIME { result = Time.new(*val[0])}
163
+ | NON_DEC_INTEGER {
164
+ base = case val[0][1]
165
+ when "x" then 16
166
+ when "o" then 8
167
+ when "b" then 2
168
+ end
169
+ result = val[0].to_i(base)
170
+ }
171
+ | BOOLEAN { result = val[0] == 'true' ? true : false }
172
+ | DATETIME {
173
+ v = val[0]
174
+ result = if v[6].nil?
175
+ if v[4].nil?
176
+ LocalDate.new(v[0], v[1], v[2])
177
+ else
178
+ LocalDateTime.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f)
179
+ end
180
+ else
181
+ # Patch for 24:00:00 which Ruby parses
182
+ if v[3].to_i == 24 && v[4].to_i == 0 && v[5].to_i == 0
183
+ v[3] = (v[3].to_i + 1).to_s
184
+ end
185
+
186
+ Time.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f, v[6])
187
+ end
188
+ }
189
+ | LOCAL_TIME { result = LocalTime.new(*val[0]) }
100
190
  ;
101
191
  string
102
192
  : STRING_MULTI { result = StringUtils.replace_escaped_chars(StringUtils.multiline_replacements(val[0])) }
@@ -2,57 +2,73 @@ require 'strscan'
2
2
 
3
3
  module Tomlrb
4
4
  class Scanner
5
- COMMENT = /#.*/
5
+ COMMENT = /#[^\u0000-\u0008\u000A-\u001F\u007F]*/
6
6
  IDENTIFIER = /[A-Za-z0-9_-]+/
7
- SPACE = /[ \t\r\n]/
8
- STRING_BASIC = /(["])(?:\\?.)*?\1/
9
- STRING_MULTI = /"{3}([\s\S]*?"{3,4})/m
10
- STRING_LITERAL = /(['])(?:\\?.)*?\1/
11
- STRING_LITERAL_MULTI = /'{3}([\s\S]*?'{3})/m
7
+ SPACE = /[ \t]/
8
+ NEWLINE = /(?:[ \t]*(?:\r?\n)[ \t]*)+/
9
+ STRING_BASIC = /(["])(?:\\?[^\u0000-\u0008\u000A-\u001F\u007F])*?\1/
10
+ STRING_MULTI = /"{3}([^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]*?(?<!\\)"{3,5})/m
11
+ STRING_LITERAL = /(['])(?:\\?[^\u0000-\u0008\u000A-\u001F\u007F])*?\1/
12
+ STRING_LITERAL_MULTI = /'{3}([^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]*?'{3,5})/m
12
13
  DATETIME = /(-?\d{4})-(\d{2})-(\d{2})(?:(?:t|\s)(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?))?(z|[-+]\d{2}:\d{2})?/i
13
- FLOAT = /[+-]?(?:[0-9_]+\.[0-9_]*|\d+(?=[eE]))(?:[eE][+-]?[0-9_]+)?/
14
- FLOAT_INF = /[+-]?inf/
15
- FLOAT_NAN = /[+-]?nan/
14
+ LOCAL_TIME = /(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/
15
+ FLOAT = /[+-]?(?:(?:\d|[1-9](?:_?\d)*)\.\d(?:_?\d)*|\d+(?=[eE]))(?:[eE][+-]?[0-9]+(_[0-9])*[0-9]*)?(?!\w)/
16
+ FLOAT_KEYWORD = /[+-]?(?:inf|nan)\b/
16
17
  INTEGER = /[+-]?([1-9](_?\d)*|0)(?![A-Za-z0-9_-]+)/
17
- TRUE = /true/
18
- FALSE = /false/
18
+ NON_DEC_INTEGER = /0(?:x[0-9A-Fa-f]+(?:_[0-9A-Fa-f])*[0-9A-Fa-f]*|o[0-7]+(?:_[0-7])*[0-7]*|b[01]+(?:_[01])*[01]*)/
19
+ BOOLEAN = /true|false/
20
+ SPACED_ARRAY_OF_TABLES_START = /^\[[ \t]+\[(#{IDENTIFIER}|#{STRING_BASIC}|#{STRING_LITERAL}|#{INTEGER}|#{NON_DEC_INTEGER}|#{FLOAT_KEYWORD}|#{BOOLEAN})\]\]$/
21
+ SPACED_ARRAY_OF_TABLES_END = /^\[\[(#{IDENTIFIER}|#{STRING_BASIC}|#{STRING_LITERAL}|#{INTEGER}|#{NON_DEC_INTEGER}|#{FLOAT_KEYWORD}|#{BOOLEAN})\][ \t]+\]$/
22
+ SPACED_ARRAY_OF_TABLES_BOTH = /^\[[ \t]+\[(#{IDENTIFIER}|#{STRING_BASIC}|#{STRING_LITERAL}|#{INTEGER}|#{NON_DEC_INTEGER}|#{FLOAT_KEYWORD}|#{BOOLEAN})\][ \t]+\]$/
19
23
 
20
24
  def initialize(io)
21
25
  @ss = StringScanner.new(io.read)
26
+ @eos = false
22
27
  end
23
28
 
24
29
  def next_token
25
- return if @ss.eos?
26
-
27
30
  case
31
+ when @ss.scan(NEWLINE) then [:NEWLINE, nil]
32
+ when @ss.scan(SPACED_ARRAY_OF_TABLES_START) then raise ParseError.new("Array of tables has spaces in starting brackets")
33
+ when @ss.scan(SPACED_ARRAY_OF_TABLES_END) then raise ParseError.new("Array of tables has spaces in ending brackets")
34
+ when @ss.scan(SPACED_ARRAY_OF_TABLES_BOTH) then raise ParseError.new("Array of tables has spaces in starting and ending brackets")
28
35
  when @ss.scan(SPACE) then next_token
29
36
  when @ss.scan(COMMENT) then next_token
30
37
  when @ss.scan(DATETIME) then process_datetime
38
+ when @ss.scan(LOCAL_TIME) then process_local_time
31
39
  when text = @ss.scan(STRING_MULTI) then [:STRING_MULTI, text[3..-4]]
32
40
  when text = @ss.scan(STRING_BASIC) then [:STRING_BASIC, text[1..-2]]
33
41
  when text = @ss.scan(STRING_LITERAL_MULTI) then [:STRING_LITERAL_MULTI, text[3..-4]]
34
42
  when text = @ss.scan(STRING_LITERAL) then [:STRING_LITERAL, text[1..-2]]
35
43
  when text = @ss.scan(FLOAT) then [:FLOAT, text]
36
- when text = @ss.scan(FLOAT_INF) then [:FLOAT_INF, text]
37
- when text = @ss.scan(FLOAT_NAN) then [:FLOAT_NAN, text]
44
+ when text = @ss.scan(FLOAT_KEYWORD) then [:FLOAT_KEYWORD, text]
38
45
  when text = @ss.scan(INTEGER) then [:INTEGER, text]
39
- when text = @ss.scan(TRUE) then [:TRUE, text]
40
- when text = @ss.scan(FALSE) then [:FALSE, text]
46
+ when text = @ss.scan(NON_DEC_INTEGER) then [:NON_DEC_INTEGER, text]
47
+ when text = @ss.scan(BOOLEAN) then [:BOOLEAN, text]
41
48
  when text = @ss.scan(IDENTIFIER) then [:IDENTIFIER, text]
42
- else
43
- x = @ss.getch
44
- [x, x]
49
+ when @ss.eos? then process_eos
50
+ else x = @ss.getch; [x, x]
45
51
  end
46
52
  end
47
53
 
48
54
  def process_datetime
49
- if @ss[7].nil?
50
- offset = '+00:00'
51
- else
52
- offset = @ss[7].gsub('Z', '+00:00')
55
+ if @ss[7]
56
+ offset = @ss[7].gsub(/[zZ]/, '+00:00')
53
57
  end
54
- args = [@ss[1], @ss[2], @ss[3], @ss[4] || 0, @ss[5] || 0, @ss[6].to_f, offset]
58
+ args = [@ss[1], @ss[2], @ss[3], @ss[4], @ss[5], @ss[6], offset]
55
59
  [:DATETIME, args]
56
60
  end
61
+
62
+ def process_local_time
63
+ args = [@ss[1], @ss[2], @ss[3].to_f]
64
+ [:LOCAL_TIME, args]
65
+ end
66
+
67
+ def process_eos
68
+ return if @eos
69
+
70
+ @eos = true
71
+ [:EOS, nil]
72
+ end
57
73
  end
58
74
  end
@@ -12,7 +12,13 @@ module Tomlrb
12
12
  }.freeze
13
13
 
14
14
  def self.multiline_replacements(str)
15
- strip_spaces(str).gsub(/\\\n\s+/, '')
15
+ strip_spaces(str).gsub(/\\+\s*\n\s*/) {|matched|
16
+ if matched.match(/\\+/)[0].length.odd?
17
+ matched.gsub(/\\\s*\n\s*/, '')
18
+ else
19
+ matched
20
+ end
21
+ }
16
22
  end
17
23
 
18
24
  def self.replace_escaped_chars(str)