tomlrb 1.3.0 → 2.0.2

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.
@@ -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)