toml-rb 4.2.2 → 5.0.0.rc.1

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: cee703f8a66e85b3edc5e6a8bc4011cef1e170dce1ac8f6e6364b4d7952ea189
4
- data.tar.gz: ba24eb31557a9411f88e18030f9cccf2a736054a490facb7161698c2f144e408
3
+ metadata.gz: deb05c1f85b54d08dda1640e125ecdbbeff043acbe1b753b7c537ca9260a8caf
4
+ data.tar.gz: 5c135ebaab7f60c0c88c3283f5c6a4db5ba6b79e672e362adf81c11becdd2ca7
5
5
  SHA512:
6
- metadata.gz: 129c26a9e6dbdaaa75818ed6c8fd6aa7d1d0525e99c32d91b2c9923a7f8ddfc6ef4a254061e54d71948bc142dcb0b35b2f98ccd1595a57e874085320e20eb082
7
- data.tar.gz: dea826c02f430769b2e760b302192698d83f407c1383d916486edaa97e771692da5de7c467688fbe2e56cffae09e96de42def754c140ba01c890dc82dcc1e926
6
+ metadata.gz: a33c28b6a6ec07a22260bce3d517374e9f09121e527a36e4319da46f888ab6ed4d8f5b48aebb6c7e3705009b4729e9255764c0fd3c2b4869771263e84ce5f9d9
7
+ data.tar.gz: b3e67a5f28e98de500e67eac01c921c8e5eb6e2ced829b46942eadf48f79a45f1881dc12a80f48b514efed52b2fdce707f705a2adc3d76d9f332ea30e90c15e1
data/README.md CHANGED
@@ -11,7 +11,7 @@ toml-rb
11
11
 
12
12
  A [TOML](https://github.com/toml-lang/toml) parser using [Citrus](http://mjackson.github.io/citrus) library.
13
13
 
14
- TOML specs supported: `1.0.0`
14
+ toml-rb implements [TOML 1.1.0](https://toml.io/en/v1.1.0) and passes all [toml-test](https://github.com/toml-lang/toml-test) v2.2.0 decoder tests for that version.
15
15
 
16
16
  Installation
17
17
  ------------
@@ -42,6 +42,16 @@ TomlRB.parse(stream)
42
42
  # You want symbols as your keys? No problem!
43
43
  TomlRB.load_file(path, symbolize_keys: true)
44
44
  # Works the same for TomlRB.parse
45
+
46
+ # Local datetimes, dates and times are subclasses of Time. Offset datetimes are Time.
47
+ times = <<-EOS
48
+ odt = 1979-05-27T07:32:00Z
49
+ ldt = 1979-05-27T07:32:00
50
+ ld = 1979-05-27
51
+ lt = 07:32:00
52
+ EOS
53
+ TomlRB.parse(times).transform_values(&:class)
54
+ # => {"odt"=>Time, "ldt"=>TomlRB::LocalDateTime, "ld"=>TomlRB::LocalDate, "lt"=>TomlRB::LocalTime}
45
55
  ```
46
56
 
47
57
  Dumper Usage
@@ -78,11 +88,13 @@ TOML 1.0.0 and TOML 1.1.0. The harness lives in:
78
88
  - `tool/toml-test-decoder` — toml-test decoder shim.
79
89
  - `.github/workflows/conformance.yml` — CI workflow (per-version matrix).
80
90
  - `.github/toml-test-skip-1.0.txt` and `.github/toml-test-skip-1.1.txt` —
81
- skip-lists of currently-failing tests.
91
+ skip-lists of toml-test names for each spec version.
82
92
 
83
- The skip-lists are a living gap report: every entry is a known limitation in
84
- the current parser. The goal for v5.0.0 is **empty skip-lists** — full
85
- conformance against both spec versions.
93
+ The TOML 1.1.0 skip-list is empty. The TOML 1.0.0 skip-list has 9 tests with
94
+ documents that TOML 1.0.0 forbids and TOML 1.1.0 allows. `toml-rb` accepts
95
+ TOML 1.1.0 syntax and has no option to select the spec version, so these tests
96
+ fail. CI runs toml-test with `-skip-must-err`, so the run fails when a skipped
97
+ test passes.
86
98
 
87
99
  Contributing
88
100
  ------------
@@ -1,12 +1,41 @@
1
+ require "date"
2
+
1
3
  module TomlRB
4
+ LocalDateTime = Class.new(Time)
5
+ LocalDate = Class.new(Time)
6
+ LocalTime = Class.new(Time)
7
+
8
+ module DateSkeletonParser
9
+ def value
10
+ year, mon, day = [:year, :mon, :day].map { |s| capture(s).value }
11
+ unless Date.valid_date?(year.to_i, mon.to_i, day.to_i, Date::GREGORIAN)
12
+ raise ParseError, "Invalid date: #{year}-#{mon}-#{day}"
13
+ end
14
+ [year, mon, day]
15
+ end
16
+ end
17
+
18
+ module TimeSkeletonParser
19
+ def value
20
+ hour, min, sec = [:hour, :mim, :sec].map { |s| capture(s)&.value || "0" }
21
+ unless hour.to_i.between?(0, 23) && min.to_i.between?(0, 59) && sec.to_i.between?(0, 60)
22
+ raise ParseError, "Invalid time: #{hour}:#{min}:#{sec}"
23
+ end
24
+ [hour, min, sec, capture(:sec_frac) || "0"]
25
+ end
26
+ end
27
+
2
28
  module OffsetDateTimeParser
3
29
  def value
4
30
  skeleton = captures[:datetime_skeleton].first
5
31
  year, mon, day, hour, min, sec, sec_frac = skeleton.value
6
- offset = captures[:date_offset].first || "+00:00"
7
- sec = "#{sec}.#{sec_frac}".to_f
32
+ offset = (captures[:date_offset].first || "+00:00").to_s
33
+ unless offset[1, 2].to_i.between?(0, 23) && offset[4, 2].to_i.between?(0, 59)
34
+ raise ParseError, "Invalid UTC offset: #{offset}"
35
+ end
36
+ sec = "#{sec}.#{sec_frac}".to_r
8
37
 
9
- Time.new(year, mon, day, hour, min, sec, offset.to_s)
38
+ Time.new(year, mon, day, hour, min, sec, offset)
10
39
  end
11
40
  end
12
41
 
@@ -14,25 +43,25 @@ module TomlRB
14
43
  def value
15
44
  year, mon, day = captures[:date_skeleton].first.value
16
45
  hour, min, sec, sec_frac = captures[:time_skeleton].first.value
17
- usec = sec_frac.to_s.ljust(6, "0")
46
+ sec = "#{sec}.#{sec_frac}".to_r
18
47
 
19
- Time.local(year, mon, day, hour, min, sec, usec)
48
+ LocalDateTime.local(year, mon, day, hour, min, sec)
20
49
  end
21
50
  end
22
51
 
23
52
  module LocalDateParser
24
53
  def value
25
54
  year, mon, day = captures[:date_skeleton].first.value
26
- Time.local(year, mon, day)
55
+ LocalDate.local(year, mon, day)
27
56
  end
28
57
  end
29
58
 
30
59
  module LocalTimeParser
31
60
  def value
32
61
  hour, min, sec, sec_frac = captures[:time_skeleton].first.value
33
- usec = sec_frac.to_s.ljust(6, "0")
62
+ sec = "#{sec}.#{sec_frac}".to_r
34
63
 
35
- Time.at(3600 * hour.to_i + 60 * min.to_i + sec.to_i, usec.to_i)
64
+ LocalTime.utc(1970, 1, 1, hour, min, sec)
36
65
  end
37
66
  end
38
67
  end
@@ -41,13 +41,13 @@ module TomlRB
41
41
  simple_pairs = []
42
42
  table_array_pairs = []
43
43
 
44
- hash.keys.sort.each do |key|
44
+ sorted_keys(hash).each do |key|
45
45
  val = hash[key]
46
46
  element = [key, val]
47
47
 
48
48
  if val.is_a? Hash
49
49
  nested_pairs << element
50
- elsif val.is_a?(Array) && val.first.is_a?(Hash)
50
+ elsif val.is_a?(Array) && !val.empty? && val.all?(Hash)
51
51
  table_array_pairs << element
52
52
  else
53
53
  simple_pairs << element
@@ -57,6 +57,17 @@ module TomlRB
57
57
  [simple_pairs, nested_pairs, table_array_pairs]
58
58
  end
59
59
 
60
+ def sorted_keys(hash)
61
+ keys = hash.keys
62
+ raise Error, "Cannot dump keys with duplicate names: #{keys.inspect}" if keys.map(&:to_s).uniq!
63
+
64
+ begin
65
+ keys.sort
66
+ rescue ArgumentError
67
+ keys.sort_by(&:to_s)
68
+ end
69
+ end
70
+
60
71
  def dump_pairs(simple, nested, table_array, prefix = [])
61
72
  # First add simple pairs, under the prefix
62
73
  dump_simple_pairs simple
@@ -101,16 +112,30 @@ module TomlRB
101
112
  end
102
113
 
103
114
  def to_toml(obj)
104
- if obj.is_a?(Time) || obj.is_a?(DateTime)
105
- obj.strftime("%Y-%m-%dT%H:%M:%SZ")
115
+ if obj.is_a?(LocalDate)
116
+ obj.strftime("%Y-%m-%d")
117
+ elsif obj.is_a?(LocalTime)
118
+ obj.strftime("%H:%M:%S") + sec_fraction(obj)
119
+ elsif obj.is_a?(LocalDateTime)
120
+ obj.strftime("%Y-%m-%dT%H:%M:%S") + sec_fraction(obj)
121
+ elsif obj.is_a?(Time) || obj.is_a?(DateTime)
122
+ zone = obj.strftime("%:z").sub("+00:00", "Z")
123
+ obj.strftime("%Y-%m-%dT%H:%M:%S") + sec_fraction(obj) + zone
106
124
  elsif obj.is_a?(Date)
107
125
  obj.strftime("%Y-%m-%d")
108
126
  elsif obj.is_a?(Regexp)
109
- obj.inspect.inspect
110
- elsif obj.is_a?(String)
111
- escape_string(obj)
127
+ escape_string(obj.inspect)
128
+ elsif obj.is_a?(String) || obj.is_a?(Symbol)
129
+ escape_string(obj.to_s)
112
130
  elsif obj.is_a?(Array)
113
131
  "[" + obj.map(&method(:to_toml)).join(", ") + "]"
132
+ elsif obj.is_a?(Hash)
133
+ pairs = sorted_keys(obj).map do |key|
134
+ val = obj[key]
135
+ key = quote_key(key) unless bare_key? key
136
+ "#{key} = #{to_toml(val)}"
137
+ end
138
+ "{" + pairs.join(", ") + "}"
114
139
  elsif obj.is_a?(Float) && (obj.nan? || obj.infinite?)
115
140
  # Ruby renders these as Infinity/-Infinity/NaN, which are invalid TOML.
116
141
  if obj.nan?
@@ -120,8 +145,13 @@ module TomlRB
120
145
  else
121
146
  "inf"
122
147
  end
123
- else
148
+ elsif obj.is_a?(Integer) || obj.is_a?(Float) || obj == true || obj == false
149
+ obj.inspect
150
+ elsif defined?(BigDecimal) && obj.is_a?(BigDecimal) && obj.finite?
151
+ # BigDecimal#inspect is a valid TOML float, for example 0.15e1.
124
152
  obj.inspect
153
+ else
154
+ raise Error, "Cannot dump #{obj.class} to TOML"
125
155
  end
126
156
  end
127
157
 
@@ -135,7 +165,8 @@ module TomlRB
135
165
  end
136
166
 
137
167
  # Serialize a Ruby string as a TOML basic string. Ruby's String#inspect
138
- # emits \a \v \e for 0x07/0x0B/0x1B, which TOML (and this parser) reject.
168
+ # emits \a and \v, which are not TOML escapes, and \e, which is not a TOML 1.0
169
+ # escape. Write ESC as \u001B to keep the output valid TOML 1.0.
139
170
  def escape_string(str)
140
171
  escaped = str.gsub(/[\x00-\x1f\x7f"\\]/) do |char|
141
172
  BASIC_ESCAPES[char] || format('\\u%04X', char.ord)
@@ -143,5 +174,9 @@ module TomlRB
143
174
 
144
175
  "\"#{escaped}\""
145
176
  end
177
+
178
+ def sec_fraction(time)
179
+ time.strftime(".%9N").sub(/\.?0+\z/, "")
180
+ end
146
181
  end
147
182
  end
@@ -2,19 +2,23 @@ grammar TomlRB::Document
2
2
  include TomlRB::Primitive
3
3
 
4
4
  rule document
5
- (comment | table_array | table | keyvalue | line_break)*
5
+ (comment | table_array | table | keyvalue_line | line_break | trailing_space)*
6
6
  end
7
7
 
8
8
  rule table_array
9
- (space? '[[' stripped_key ']]' comment?) <TomlRB::TableArrayParser>
9
+ (space? '[[' stripped_key ']]' line_end) <TomlRB::TableArrayParser>
10
10
  end
11
11
 
12
12
  rule table
13
- (space? '[' stripped_key ']' comment?) <TomlRB::TableParser>
13
+ (space? '[' stripped_key ']' line_end) <TomlRB::TableParser>
14
+ end
15
+
16
+ rule keyvalue_line
17
+ (keyvalue line_end) { capture(:keyvalue).value }
14
18
  end
15
19
 
16
20
  rule keyvalue
17
- (stripped_key '=' space? v:(toml_values) comment? space) <TomlRB::KeyvalueParser>
21
+ (stripped_key '=' space? v:(toml_values) space) <TomlRB::KeyvalueParser>
18
22
  end
19
23
 
20
24
  ### Values
@@ -24,7 +28,7 @@ grammar TomlRB::Document
24
28
  end
25
29
 
26
30
  rule array
27
- ("[" array_comments (array_elements)? space ","? array_comments "]" indent?) <TomlRB::ArrayParser>
31
+ ("[" array_comments (array_elements (array_comments ",")?)? array_comments "]") <TomlRB::ArrayParser>
28
32
  end
29
33
 
30
34
  rule array_array
@@ -34,15 +38,21 @@ grammar TomlRB::Document
34
38
  end
35
39
 
36
40
  rule array_elements
37
- (toml_values (indent? "," array_comments toml_values)*) {
38
- captures[:toml_values].map(&:value).map do |v|
39
- v.respond_to?(:value) ? v.value : v
40
- end
41
+ (toml_values (array_comments "," array_comments toml_values)*) {
42
+ captures[:toml_values].map(&:value)
41
43
  }
42
44
  end
43
45
 
44
46
  ### Helpers
45
47
 
48
+ rule line_end
49
+ (space (comment | line_break | !.))
50
+ end
51
+
52
+ rule trailing_space
53
+ ([ \t]+ !.) { nil }
54
+ end
55
+
46
56
  rule stripped_key
47
57
  (space? key space?) { captures[:key].first.value }
48
58
  end
@@ -1,6 +1,6 @@
1
1
  grammar TomlRB::Helper
2
2
  rule comment
3
- (space? "#" /[^\n]*/u line_break?) { nil }
3
+ (space? "#" /[^\x00-\x08\x0a-\x1f\x7f]*(?=\r?\n|\z)/u line_break?) { nil }
4
4
  end
5
5
 
6
6
  rule space
@@ -14,19 +14,19 @@ grammar TomlRB::Primitive
14
14
  end
15
15
 
16
16
  rule basic_string
17
- (/"(?:[^"\\\n]|\\.)*"/ space) <TomlRB::BasicString>
17
+ (/"(?:[^"\\\x00-\x08\x0a-\x1f\x7f]|\\.)*"/ space) <TomlRB::BasicString>
18
18
  end
19
19
 
20
20
  rule literal_string
21
- (/'[^'\n]*'/ space) <TomlRB::LiteralString>
21
+ (/'[^'\x00-\x08\x0a-\x1f\x7f]*'/ space) <TomlRB::LiteralString>
22
22
  end
23
23
 
24
24
  rule multiline_string
25
- ('"""' line_break* (text:~('"""' !'"')|'') '"""' space) <TomlRB::MultilineString>
25
+ ('"""' /\r?\n/? text:/(?:[^"\\\x00-\x08\x0b-\x1f\x7f]|\r\n|\\(?:[^\r]|\r\n)|"{1,2}(?!"))*"{0,2}(?=""")/ '"""' space) <TomlRB::MultilineString>
26
26
  end
27
27
 
28
28
  rule multiline_literal
29
- ("'''" line_break* (text:~("'''" !"'")|'') "'''" space) <TomlRB::MultilineLiteral>
29
+ ("'''" /\r?\n/? text:/(?:[^'\x00-\x08\x0b-\x1f\x7f]|\r\n|'{1,2}(?!'))*'{0,2}(?=''')/ "'''" space) <TomlRB::MultilineLiteral>
30
30
  end
31
31
 
32
32
  ##
@@ -38,7 +38,7 @@ grammar TomlRB::Primitive
38
38
  end
39
39
 
40
40
  rule offset_datetime
41
- ( skeleton:datetime_skeleton ("Z" | date_offset) ) <TomlRB::OffsetDateTimeParser>
41
+ ( skeleton:datetime_skeleton ([Zz] | date_offset) ) <TomlRB::OffsetDateTimeParser>
42
42
  end
43
43
 
44
44
  rule local_datetime
@@ -54,21 +54,17 @@ grammar TomlRB::Primitive
54
54
  end
55
55
 
56
56
  rule datetime_skeleton
57
- (date_skeleton ("T"|" ") time_skeleton) {
57
+ (date_skeleton ([Tt]|" ") time_skeleton) {
58
58
  capture(:date_skeleton).value + capture(:time_skeleton).value
59
59
  }
60
60
  end
61
61
 
62
62
  rule date_skeleton
63
- (year:/\d\d\d\d/ "-" mon:/\d\d/ "-" day:/\d\d/) {
64
- [:year,:mon,:day].map{ |s| capture(s).value }
65
- }
63
+ (year:/\d\d\d\d/ "-" mon:/\d\d/ "-" day:/\d\d/) <TomlRB::DateSkeletonParser>
66
64
  end
67
65
 
68
66
  rule time_skeleton
69
- ( hour:([0-2][0-9]) ":" mim:([0-6][0-9]) ":" sec:([0-6][0-9]) ([,\.] sec_frac:(/\d/1*6))? ) {
70
- [:hour,:mim,:sec].map{ |s| capture(s).value } + [capture(:sec_frac) || '0']
71
- }
67
+ ( hour:([0-2][0-9]) ":" mim:([0-6][0-9]) (":" sec:([0-6][0-9]) ("." sec_frac:(/\d/1*9) /\d/*)?)? ) <TomlRB::TimeSkeletonParser>
72
68
  end
73
69
 
74
70
  rule date_offset
@@ -99,7 +95,11 @@ grammar TomlRB::Primitive
99
95
  end
100
96
 
101
97
  rule exponential_float
102
- ((fractional_float | decimal_integer) [eE] decimal_integer) { to_str.to_f }
98
+ ((fractional_float | decimal_integer) [eE] exponent) { to_str.to_f }
99
+ end
100
+
101
+ rule exponent
102
+ sign? [0-9] ([0-9] | '_' [0-9])*
103
103
  end
104
104
 
105
105
  rule fractional_float
@@ -125,7 +125,7 @@ grammar TomlRB::Primitive
125
125
  end
126
126
 
127
127
  rule decimal_integer
128
- (sign? [0-9] ([0-9] | '_' [0-9])*) { to_str.to_i }
128
+ (sign? ('0' | [1-9] ([0-9] | '_' [0-9])*)) { to_str.to_i }
129
129
  end
130
130
 
131
131
  rule sign
@@ -10,10 +10,8 @@ module TomlRB
10
10
 
11
11
  def value(symbolize_keys = false)
12
12
  result = {}
13
- @pairs.each do |kv|
14
- update = kv.assign({}, [], symbolize_keys)
15
- result.merge!(update) { |key, _, _| fail ValueOverwriteError.new(key) }
16
- end
13
+ tables = {}.compare_by_identity
14
+ @pairs.each { |kv| kv.assign(result, tables, symbolize_keys) }
17
15
  result
18
16
  end
19
17
  end
@@ -10,36 +10,23 @@ module TomlRB
10
10
  @symbolize_keys = false
11
11
  end
12
12
 
13
- def assign(hash, fully_defined_paths, symbolize_keys = false)
13
+ def assign(hash, tables, symbolize_keys = false)
14
14
  @symbolize_keys = symbolize_keys
15
- keys = symbolize_keys ? @dotted_keys.map(&:to_sym) : @dotted_keys
16
- depth = @dotted_keys.size
17
- update = keys.reverse.inject(visit_value(@value)) { |k1, k2| {k2 => k1} }
15
+ *keys, last_key = symbolize_keys ? @dotted_keys.map(&:to_sym) : @dotted_keys
18
16
 
19
- parent_inline_table = fully_defined_paths.find { |k| k.size < depth && @dotted_keys.first(k.size) == k }
20
- fail ValueOverwriteError.new(@dotted_keys.first) if parent_inline_table
21
-
22
- if @value.is_a?(InlineTable)
23
- child_keys_exist = fully_defined_paths.find { |k| k.size > depth && k.first(depth) == @dotted_keys }
24
- fail ValueOverwriteError.new(@dotted_keys.first) if child_keys_exist
25
-
26
- existing_hash = hash.dig(*keys)
27
- fail ValueOverwriteError.new(@dotted_keys.first) if existing_hash.is_a?(Hash) && !existing_hash.empty?
17
+ keys.each do |key|
18
+ unless hash.key?(key)
19
+ hash[key] = {}
20
+ tables[hash[key]] = :dotted
21
+ end
22
+ hash = hash[key]
23
+ fail ValueOverwriteError.new(key) unless [:implicit, :dotted].include?(tables[hash])
28
24
 
29
- fully_defined_paths << @dotted_keys
25
+ tables[hash] = :dotted
30
26
  end
27
+ fail ValueOverwriteError.new(last_key) if hash.key?(last_key)
31
28
 
32
- dotted_key_merge(hash, update)
33
- end
34
-
35
- def dotted_key_merge(hash, update)
36
- hash.merge!(update) do |key, old, new|
37
- if old.is_a?(Hash) && new.is_a?(Hash)
38
- dotted_key_merge(old, new)
39
- else
40
- fail ValueOverwriteError.new(key)
41
- end
42
- end
29
+ hash[last_key] = visit_value(@value)
43
30
  end
44
31
 
45
32
  def accept_visitor(parser)
@@ -49,6 +36,7 @@ module TomlRB
49
36
  private
50
37
 
51
38
  def visit_value(a_value)
39
+ return a_value.map { |v| visit_value(v) } if a_value.is_a?(Array)
52
40
  return a_value unless a_value.respond_to? :accept_visitor
53
41
 
54
42
  a_value.accept_visitor self
@@ -4,8 +4,10 @@ module TomlRB
4
4
 
5
5
  def initialize(content, symbolize_keys: false)
6
6
  @hash = {}
7
- @visited_keys = []
8
- @fully_defined_paths = []
7
+ # Tables and arrays of tables that headers or dotted keys made, by identity:
8
+ # :implicit (super-table of a header), :explicit (header) or :dotted (dotted key).
9
+ # Inline tables and static arrays are absent, so no header or dotted key can enter them.
10
+ @tables = {}.compare_by_identity
9
11
  @current = @hash
10
12
  @symbolize_keys = symbolize_keys
11
13
 
@@ -28,20 +30,15 @@ module TomlRB
28
30
  # Read about the Visitor pattern
29
31
  # http://en.wikipedia.org/wiki/Visitor_pattern
30
32
  def visit_table_array(table_array)
31
- @fully_defined_paths = []
32
- table_array_key = table_array.full_key
33
- @visited_keys.reject! { |k| k.start_with? table_array_key }
34
-
35
- @current = table_array.navigate_keys @hash, @symbolize_keys
33
+ @current = table_array.navigate_keys @hash, @tables, @symbolize_keys
36
34
  end
37
35
 
38
36
  def visit_table(table)
39
- @fully_defined_paths = []
40
- @current = table.navigate_keys @hash, @visited_keys, @symbolize_keys
37
+ @current = table.navigate_keys @hash, @tables, @symbolize_keys
41
38
  end
42
39
 
43
40
  def visit_keyvalue(keyvalue)
44
- keyvalue.assign @current, @fully_defined_paths, @symbolize_keys
41
+ keyvalue.assign @current, @tables, @symbolize_keys
45
42
  end
46
43
  end
47
44
  end
@@ -2,9 +2,9 @@ module TomlRB
2
2
  # Used in primitive.citrus
3
3
  module BasicString
4
4
  SPECIAL_CHARS = {
5
- "\\0" => "\0",
6
5
  "\\t" => "\t",
7
6
  "\\b" => "\b",
7
+ "\\e" => "\e",
8
8
  "\\f" => "\f",
9
9
  "\\n" => "\n",
10
10
  "\\r" => "\r",
@@ -21,12 +21,19 @@ module TomlRB
21
21
  # Replace the unicode escaped characters with the corresponding character
22
22
  # e.g. \u03B4 => ?
23
23
  def self.decode_unicode(str)
24
- [str[2..-1].to_i(16)].pack("U")
24
+ code = str[2..-1].to_i(16)
25
+ if code.between?(0xD800, 0xDFFF) || code > 0x10FFFF
26
+ fail ParseError.new "Escape sequence #{str} is not a Unicode scalar value"
27
+ end
28
+
29
+ [code].pack("U")
25
30
  end
26
31
 
27
32
  def self.transform_escaped_chars(str)
28
- str.gsub(/\\(u[\da-fA-F]{4}|U[\da-fA-F]{8}|.)/) do |m|
29
- if m.size == 2
33
+ str.gsub(/\\(x[\da-fA-F]{2}|u[\da-fA-F]{4}|U[\da-fA-F]{8}|[ \t]*\r?\n[ \t\r\n]*|.)/) do |m|
34
+ if m.include?("\n")
35
+ ""
36
+ elsif m.size == 2
30
37
  SPECIAL_CHARS[m] || parse_error(m)
31
38
  else
32
39
  decode_unicode(m).force_encoding("UTF-8")
@@ -47,22 +54,13 @@ module TomlRB
47
54
 
48
55
  module MultilineString
49
56
  def value
50
- return "" if captures[:text].empty?
51
- aux = captures[:text].first.value
52
-
53
- # Remove spaces on multilined Singleline strings
54
- aux.gsub!(/\\\r?\n[\n\t\r ]*/, "")
55
-
56
- TomlRB::BasicString.transform_escaped_chars aux
57
+ TomlRB::BasicString.transform_escaped_chars captures[:text].first.value
57
58
  end
58
59
  end
59
60
 
60
61
  module MultilineLiteral
61
62
  def value
62
- return "" if captures[:text].empty?
63
- aux = captures[:text].first.value
64
-
65
- aux.gsub(/\\\r?\n[\n\t\r ]*/, "")
63
+ captures[:text].first.value
66
64
  end
67
65
  end
68
66
  end
data/lib/toml-rb/table.rb CHANGED
@@ -4,24 +4,22 @@ module TomlRB
4
4
  @dotted_keys = dotted_keys
5
5
  end
6
6
 
7
- def navigate_keys(hash, visited_keys, symbolize_keys = false)
8
- ensure_key_not_defined(visited_keys)
7
+ def navigate_keys(hash, tables, symbolize_keys = false)
9
8
  current = hash
10
9
  keys = symbolize_keys ? @dotted_keys.map(&:to_sym) : @dotted_keys
11
- keys.each_with_index do |key, index|
12
- current[key] = {} unless current.key?(key)
13
- element = current[key]
14
-
15
- # If this is the final key and it's already an array (from [[key]]), that's invalid
16
- is_final_key = (index == keys.length - 1)
17
- if is_final_key && element.is_a?(Array)
18
- fail ValueOverwriteError.new(key)
10
+ keys.each do |key|
11
+ unless current.key?(key)
12
+ current[key] = {}
13
+ tables[current[key]] = :implicit
19
14
  end
15
+ element = current[key]
16
+ fail ValueOverwriteError.new(key) unless tables.key?(element)
20
17
 
21
18
  current = element.is_a?(Array) ? element.last : element
22
- # check that key has not been defined before as a scalar value
23
- fail ValueOverwriteError.new(key) unless current.is_a?(Hash)
24
19
  end
20
+ fail ValueOverwriteError.new(full_key) unless tables[current] == :implicit
21
+
22
+ tables[current] = :explicit
25
23
  current
26
24
  end
27
25
 
@@ -32,14 +30,6 @@ module TomlRB
32
30
  def full_key
33
31
  @dotted_keys.join(".")
34
32
  end
35
-
36
- private
37
-
38
- # Fail if the key was already defined with a ValueOverwriteError
39
- def ensure_key_not_defined(visited_keys)
40
- fail ValueOverwriteError.new(full_key) if visited_keys.include?(full_key)
41
- visited_keys << full_key
42
- end
43
33
  end
44
34
 
45
35
  # Used in document.citrus
@@ -4,21 +4,20 @@ module TomlRB
4
4
  @dotted_keys = dotted_keys
5
5
  end
6
6
 
7
- def navigate_keys(hash, symbolize_keys = false)
7
+ def navigate_keys(hash, tables, symbolize_keys = false)
8
8
  current = hash
9
- keys = symbolize_keys ? @dotted_keys.map(&:to_sym) : @dotted_keys
10
- last_key = keys.pop
9
+ *keys, last_key = symbolize_keys ? @dotted_keys.map(&:to_sym) : @dotted_keys
11
10
 
12
11
  # Go over the parent keys
13
12
  keys.each do |key|
14
- current[key] = {} unless current[key]
15
-
16
- if current[key].is_a? Array
17
- current[key] << {} if current[key].empty?
18
- current = current[key].last
19
- else
20
- current = current[key]
13
+ unless current.key?(key)
14
+ current[key] = {}
15
+ tables[current[key]] = :implicit
21
16
  end
17
+ element = current[key]
18
+ fail ValueOverwriteError.new(key) unless tables.key?(element)
19
+
20
+ current = element.is_a?(Array) ? element.last : element
22
21
  end
23
22
 
24
23
  # Define Table Array
@@ -26,19 +25,20 @@ module TomlRB
26
25
  fail TomlRB::ParseError,
27
26
  "#{last_key} was defined as hash but is now redefined as a table!"
28
27
  end
29
- current[last_key] = [] unless current[last_key]
30
- current[last_key] << {}
28
+ unless current.key?(last_key)
29
+ current[last_key] = []
30
+ tables[current[last_key]] = :explicit
31
+ end
32
+ fail ValueOverwriteError.new(last_key) unless tables.key?(current[last_key])
31
33
 
34
+ current[last_key] << {}
35
+ tables[current[last_key].last] = :explicit
32
36
  current[last_key].last
33
37
  end
34
38
 
35
39
  def accept_visitor(parser)
36
40
  parser.visit_table_array self
37
41
  end
38
-
39
- def full_key
40
- @dotted_keys.join(".")
41
- end
42
42
  end
43
43
 
44
44
  # Used in document.citrus
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TomlRB
4
- VERSION = "4.2.2"
4
+ VERSION = "5.0.0.rc.1"
5
5
  end
data/lib/toml-rb.rb CHANGED
@@ -67,7 +67,7 @@ module TomlRB
67
67
  # Raises Errno::ENOENT if the file cannot be found.
68
68
  # Raises Errno::EACCES if the file cannot be accessed.
69
69
  def self.load_file(path, symbolize_keys: false)
70
- TomlRB.parse(File.read(path), symbolize_keys: symbolize_keys)
70
+ TomlRB.parse(File.read(path, encoding: "UTF-8"), symbolize_keys: symbolize_keys)
71
71
  end
72
72
 
73
73
  # Public: Returns a *TomlRB* string from a Ruby Hash.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toml-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.2
4
+ version: 5.0.0.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emiliano Mancuso
@@ -30,20 +30,6 @@ dependencies:
30
30
  - - ">"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '3.0'
33
- - !ruby/object:Gem::Dependency
34
- name: racc
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.7'
40
- type: :runtime
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '1.7'
47
33
  - !ruby/object:Gem::Dependency
48
34
  name: minitest
49
35
  requirement: !ruby/object:Gem::Requirement