json5-ruby 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +6 -0
- data/README.md +161 -0
- data/SECURITY.md +9 -0
- data/docs/conformance-matrix.md +90 -0
- data/docs/conformance-report.md +29 -0
- data/grammar/json5.y +74 -0
- data/lib/json5/ascii_classification.rb +31 -0
- data/lib/json5/diagnostic.rb +70 -0
- data/lib/json5/document.rb +20 -0
- data/lib/json5/document_nodes.rb +99 -0
- data/lib/json5/document_parser.rb +291 -0
- data/lib/json5/ecma_string.rb +106 -0
- data/lib/json5/errors.rb +67 -0
- data/lib/json5/generated_parser.ibex.json +196 -0
- data/lib/json5/generated_parser.rb +10066 -0
- data/lib/json5/input.rb +283 -0
- data/lib/json5/lexer.rb +599 -0
- data/lib/json5/limits.rb +47 -0
- data/lib/json5/line_map.rb +92 -0
- data/lib/json5/number_value.rb +119 -0
- data/lib/json5/object_value.rb +28 -0
- data/lib/json5/parser.rb +301 -0
- data/lib/json5/public_token_names.rb +43 -0
- data/lib/json5/ruby_value_builder.rb +236 -0
- data/lib/json5/token.rb +26 -0
- data/lib/json5/unicode_tables.rb +42 -0
- data/lib/json5/version.rb +7 -0
- data/lib/json5.rb +47 -0
- data/tool/sources.yml +21 -0
- data/tool/unicode/README.md +19 -0
- data/tool/unicode/UNICODE-LICENSE.txt +39 -0
- data/tool/unicode/checksums.txt +1 -0
- metadata +91 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class LineMap
|
|
5
|
+
def initialize(source)
|
|
6
|
+
Input.new(source, limits: Limits.unbounded)
|
|
7
|
+
@source = source
|
|
8
|
+
@line_starts = build_line_starts.freeze
|
|
9
|
+
freeze
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def position(byte_offset)
|
|
13
|
+
line, column = line_column(byte_offset)
|
|
14
|
+
Input::Position.new(byte_offset, line, column)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def line_column(byte_offset)
|
|
18
|
+
validate_byte_offset!(byte_offset)
|
|
19
|
+
|
|
20
|
+
next_line = @line_starts.bsearch_index { |start_byte| start_byte > byte_offset }
|
|
21
|
+
if crlf_middle?(byte_offset)
|
|
22
|
+
return [next_line + 1, 1]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
line_index = (next_line || @line_starts.length) - 1
|
|
26
|
+
[line_index + 1, column_at(@line_starts.fetch(line_index), byte_offset)]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_line_starts
|
|
32
|
+
starts = [0]
|
|
33
|
+
index = 0
|
|
34
|
+
while index < @source.bytesize
|
|
35
|
+
byte = @source.getbyte(index)
|
|
36
|
+
if byte == 0x0d
|
|
37
|
+
index += @source.getbyte(index + 1) == 0x0a ? 2 : 1
|
|
38
|
+
starts << index
|
|
39
|
+
elsif byte == 0x0a
|
|
40
|
+
index += 1
|
|
41
|
+
starts << index
|
|
42
|
+
elsif unicode_line_separator_at?(index)
|
|
43
|
+
index += 3
|
|
44
|
+
starts << index
|
|
45
|
+
else
|
|
46
|
+
index += utf8_width(byte)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
starts
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def column_at(line_start, byte_offset)
|
|
53
|
+
column = 1
|
|
54
|
+
index = line_start
|
|
55
|
+
while index < byte_offset
|
|
56
|
+
width = utf8_width(@source.getbyte(index))
|
|
57
|
+
break if index + width > byte_offset
|
|
58
|
+
|
|
59
|
+
index += width
|
|
60
|
+
column += 1
|
|
61
|
+
end
|
|
62
|
+
column
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def crlf_middle?(byte_offset)
|
|
66
|
+
byte_offset.positive? &&
|
|
67
|
+
@source.getbyte(byte_offset) == 0x0a &&
|
|
68
|
+
@source.getbyte(byte_offset - 1) == 0x0d
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def unicode_line_separator_at?(index)
|
|
72
|
+
@source.getbyte(index) == 0xe2 &&
|
|
73
|
+
@source.getbyte(index + 1) == 0x80 &&
|
|
74
|
+
[0xa8, 0xa9].include?(@source.getbyte(index + 2))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def utf8_width(byte)
|
|
78
|
+
return 1 if byte < 0x80
|
|
79
|
+
return 2 if byte < 0xe0
|
|
80
|
+
return 3 if byte < 0xf0
|
|
81
|
+
|
|
82
|
+
4
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_byte_offset!(byte_offset)
|
|
86
|
+
raise TypeError, "byte offset must be an Integer" unless byte_offset.is_a?(Integer)
|
|
87
|
+
return if byte_offset.between?(0, @source.bytesize)
|
|
88
|
+
|
|
89
|
+
raise RangeError, "byte offset is outside the source"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class NumberValue
|
|
5
|
+
attr_reader :raw, :kind
|
|
6
|
+
|
|
7
|
+
def initialize(raw, kind: nil)
|
|
8
|
+
@raw = raw.to_s.freeze
|
|
9
|
+
@kind = (kind || classify(@raw)).to_sym
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_native
|
|
14
|
+
case kind
|
|
15
|
+
when :infinity
|
|
16
|
+
raw.start_with?("-") ? -Float::INFINITY : Float::INFINITY
|
|
17
|
+
when :nan
|
|
18
|
+
Float::NAN
|
|
19
|
+
when :hex
|
|
20
|
+
sign = raw.start_with?("-") ? -1 : 1
|
|
21
|
+
sign * Integer(raw.delete_prefix("+").delete_prefix("-"), 16)
|
|
22
|
+
when :integer
|
|
23
|
+
return -0.0 if raw == "-0"
|
|
24
|
+
Integer(raw, 10)
|
|
25
|
+
else
|
|
26
|
+
decimal_to_float
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_f
|
|
31
|
+
return hex_to_f if kind == :hex
|
|
32
|
+
|
|
33
|
+
to_native.to_f
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def decimal_to_float
|
|
39
|
+
normalized = raw.dup
|
|
40
|
+
body_start = normalized.start_with?("+", "-") ? 1 : 0
|
|
41
|
+
normalized.insert(body_start, "0") if normalized.getbyte(body_start) == 0x2e
|
|
42
|
+
decimal_point = normalized.index(".", body_start)
|
|
43
|
+
if decimal_point && [nil, 0x45, 0x65].include?(normalized.getbyte(decimal_point + 1))
|
|
44
|
+
normalized.insert(decimal_point + 1, "0")
|
|
45
|
+
end
|
|
46
|
+
Float(normalized)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def classify(value)
|
|
50
|
+
body = value.delete_prefix("+").delete_prefix("-")
|
|
51
|
+
return :infinity if body == "Infinity"
|
|
52
|
+
return :nan if body == "NaN"
|
|
53
|
+
return :hex if body.start_with?("0x", "0X")
|
|
54
|
+
return :integer if !body.include?(".") && !body.include?("e") && !body.include?("E")
|
|
55
|
+
|
|
56
|
+
:float
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def hex_to_f
|
|
60
|
+
negative = raw.start_with?("-")
|
|
61
|
+
first_byte = raw.getbyte(0)
|
|
62
|
+
digits_start = first_byte == 0x2b || first_byte == 0x2d ? 3 : 2
|
|
63
|
+
digit_count = raw.bytesize - digits_start
|
|
64
|
+
first_nonzero = 0
|
|
65
|
+
first_nonzero += 1 while first_nonzero < digit_count && raw.getbyte(digits_start + first_nonzero) == 0x30
|
|
66
|
+
return negative ? -0.0 : 0.0 if first_nonzero == digit_count
|
|
67
|
+
|
|
68
|
+
first_digit = raw.getbyte(digits_start + first_nonzero)
|
|
69
|
+
first_digit -= 0x30 if first_digit <= 0x39
|
|
70
|
+
first_digit -= 0x37 if first_digit >= 0x41 && first_digit <= 0x46
|
|
71
|
+
first_digit -= 0x57 if first_digit >= 0x61 && first_digit <= 0x66
|
|
72
|
+
leading_bits = first_digit.bit_length
|
|
73
|
+
first_bit = first_nonzero * 4 + (4 - leading_bits)
|
|
74
|
+
bit_length = digit_count * 4 - first_bit
|
|
75
|
+
exponent = bit_length - 1
|
|
76
|
+
return negative ? -Float::INFINITY : Float::INFINITY if exponent > 1023
|
|
77
|
+
|
|
78
|
+
significand = 0
|
|
79
|
+
[bit_length, 53].min.times do |index|
|
|
80
|
+
significand = (significand << 1) | hex_bit(digits_start, first_bit + index)
|
|
81
|
+
end
|
|
82
|
+
if bit_length > 53
|
|
83
|
+
guard = hex_bit(digits_start, first_bit + 53)
|
|
84
|
+
sticky = nonzero_hex_bit?(digits_start, first_bit + 54, digit_count * 4)
|
|
85
|
+
significand += 1 if guard == 1 && (sticky || significand.odd?)
|
|
86
|
+
if significand == (1 << 53)
|
|
87
|
+
significand >>= 1
|
|
88
|
+
exponent += 1
|
|
89
|
+
return negative ? -Float::INFINITY : Float::INFINITY if exponent > 1023
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
significand <<= (53 - bit_length)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
result = significand.to_f * (2.0**(exponent - 52))
|
|
96
|
+
negative ? -result : result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def nonzero_hex_bit?(digits_start, bit_index, end_bit)
|
|
100
|
+
while bit_index < end_bit
|
|
101
|
+
return true if hex_bit(digits_start, bit_index) == 1
|
|
102
|
+
bit_index += 1
|
|
103
|
+
end
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def hex_bit(digits_start, bit_index)
|
|
108
|
+
byte = raw.getbyte(digits_start + (bit_index / 4))
|
|
109
|
+
nibble = if byte <= 0x39
|
|
110
|
+
byte - 0x30
|
|
111
|
+
elsif byte <= 0x46
|
|
112
|
+
byte - 0x37
|
|
113
|
+
else
|
|
114
|
+
byte - 0x57
|
|
115
|
+
end
|
|
116
|
+
(nibble >> (3 - (bit_index % 4))) & 1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class ObjectValue
|
|
5
|
+
Member = Data.define(:key, :value)
|
|
6
|
+
|
|
7
|
+
attr_reader :members
|
|
8
|
+
|
|
9
|
+
def initialize(members)
|
|
10
|
+
@members = members.map { |key, value| Member.new(key, value) }.freeze
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def [](key)
|
|
15
|
+
member = members.reverse_each.find { |candidate| candidate.key == key }
|
|
16
|
+
member&.value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
return enum_for(__method__) unless block
|
|
21
|
+
members.each(&block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
members.to_h { |member| [member.key, member.value] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/json5/parser.rb
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class Parser
|
|
5
|
+
DEFAULT_OPTIONS = {
|
|
6
|
+
duplicate_keys: :last,
|
|
7
|
+
number_mode: :native,
|
|
8
|
+
string_mode: :ruby,
|
|
9
|
+
lone_surrogate: :error,
|
|
10
|
+
diagnostics: nil,
|
|
11
|
+
limits: Limits.default,
|
|
12
|
+
filename: nil,
|
|
13
|
+
freeze: false,
|
|
14
|
+
warn_duplicate_keys: false,
|
|
15
|
+
preserve_trivia: true,
|
|
16
|
+
borrow_source: false
|
|
17
|
+
}.freeze
|
|
18
|
+
PARSE_OPTION_KEYS = (DEFAULT_OPTIONS.keys - %i[preserve_trivia borrow_source]).freeze
|
|
19
|
+
DOCUMENT_OPTION_KEYS = %i[diagnostics limits filename preserve_trivia borrow_source].freeze
|
|
20
|
+
|
|
21
|
+
def self.parse(source, **options)
|
|
22
|
+
new(source, **options).parse
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.load(io, **options)
|
|
26
|
+
unless io.respond_to?(:read)
|
|
27
|
+
raise TypeError, "io must respond to #read"
|
|
28
|
+
end
|
|
29
|
+
validate_option_keys!(options, PARSE_OPTION_KEYS)
|
|
30
|
+
validate_parse_options!(options)
|
|
31
|
+
limits = options.fetch(:limits, Limits.default)
|
|
32
|
+
source = read_source(io, limits, filename: options.fetch(:filename, nil))
|
|
33
|
+
parse(source, **options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.parse_document(source, **options)
|
|
37
|
+
validate_option_keys!(options, DOCUMENT_OPTION_KEYS)
|
|
38
|
+
merged = DEFAULT_OPTIONS.merge(options)
|
|
39
|
+
validate_limits_option!(merged.fetch(:limits))
|
|
40
|
+
validate_diagnostics_option!(merged.fetch(:diagnostics))
|
|
41
|
+
validate_boolean_option!(merged.fetch(:preserve_trivia), :preserve_trivia)
|
|
42
|
+
validate_boolean_option!(merged.fetch(:borrow_source), :borrow_source)
|
|
43
|
+
unless source.is_a?(String)
|
|
44
|
+
raise TypeError, "source must be a String"
|
|
45
|
+
end
|
|
46
|
+
if merged[:limits].max_input_bytes && source.bytesize > merged[:limits].max_input_bytes
|
|
47
|
+
Input.new(source, limits: merged.fetch(:limits), filename: merged.fetch(:filename))
|
|
48
|
+
end
|
|
49
|
+
owned_source = merged.fetch(:borrow_source) ? source : source.dup.freeze
|
|
50
|
+
collected_diagnostics = []
|
|
51
|
+
diagnostics = merged.fetch(:diagnostics)
|
|
52
|
+
diagnostic_target = case diagnostics
|
|
53
|
+
when Array
|
|
54
|
+
->(diagnostic) { collected_diagnostics << diagnostic; diagnostics << diagnostic }
|
|
55
|
+
when :ignore
|
|
56
|
+
:ignore
|
|
57
|
+
else
|
|
58
|
+
if diagnostics.respond_to?(:call)
|
|
59
|
+
->(diagnostic) { collected_diagnostics << diagnostic; diagnostics.call(diagnostic) }
|
|
60
|
+
else
|
|
61
|
+
lambda do |diagnostic|
|
|
62
|
+
collected_diagnostics << diagnostic
|
|
63
|
+
DiagnosticSink.write_default_warning(diagnostic)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
DocumentParser.new(
|
|
68
|
+
owned_source,
|
|
69
|
+
limits: merged.fetch(:limits),
|
|
70
|
+
diagnostics: diagnostic_target,
|
|
71
|
+
filename: merged.fetch(:filename),
|
|
72
|
+
preserve_trivia: merged.fetch(:preserve_trivia),
|
|
73
|
+
collected_diagnostics: collected_diagnostics
|
|
74
|
+
).parse
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.read_source(io, limits, filename: nil)
|
|
78
|
+
external_encoding = io.external_encoding if io.respond_to?(:external_encoding)
|
|
79
|
+
external_encoding = nil unless [Encoding::UTF_8, Encoding::US_ASCII].include?(external_encoding)
|
|
80
|
+
allow_binary_chunks = !external_encoding.nil?
|
|
81
|
+
unless limits.max_input_bytes
|
|
82
|
+
source = io.read
|
|
83
|
+
validate_read_chunk!(source, filename: filename, byte_offset: 0, preceding: nil,
|
|
84
|
+
allow_binary: allow_binary_chunks)
|
|
85
|
+
return normalize_read_encoding(source, external_encoding)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
source = String.new(encoding: Encoding::BINARY)
|
|
89
|
+
source_encoding = external_encoding
|
|
90
|
+
chunk_size = [limits.max_input_bytes + 1, 16 * 1024].min
|
|
91
|
+
while source.bytesize <= limits.max_input_bytes
|
|
92
|
+
chunk = io.read(chunk_size)
|
|
93
|
+
break if chunk.nil?
|
|
94
|
+
validate_read_chunk!(chunk, filename: filename, byte_offset: source.bytesize, preceding: source,
|
|
95
|
+
allow_binary: allow_binary_chunks)
|
|
96
|
+
break if chunk.empty?
|
|
97
|
+
source_encoding = merged_read_encoding(source_encoding, chunk.encoding)
|
|
98
|
+
remaining = limits.max_input_bytes + 1 - source.bytesize
|
|
99
|
+
if chunk.bytesize >= remaining
|
|
100
|
+
position_bytes = source + chunk.b.byteslice(0, remaining + 3).to_s
|
|
101
|
+
position_bytes.force_encoding(source_encoding || Encoding::UTF_8)
|
|
102
|
+
line, column = Input.position_for(position_bytes, limits.max_input_bytes)
|
|
103
|
+
raise LimitError.new(
|
|
104
|
+
"input exceeds max_input_bytes",
|
|
105
|
+
code: "maximum_input_size_exceeded",
|
|
106
|
+
filename: filename,
|
|
107
|
+
byte_offset: limits.max_input_bytes,
|
|
108
|
+
end_byte_offset: limits.max_input_bytes + 1,
|
|
109
|
+
line: line,
|
|
110
|
+
column: column,
|
|
111
|
+
excerpt: limit_excerpt(source, chunk, limits.max_input_bytes)
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
source << chunk.b
|
|
115
|
+
end
|
|
116
|
+
source.force_encoding(source_encoding || Encoding::UTF_8)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.normalize_read_encoding(source, external_encoding)
|
|
120
|
+
return source unless source.encoding == Encoding::ASCII_8BIT && external_encoding
|
|
121
|
+
|
|
122
|
+
source.dup.force_encoding(external_encoding)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.merged_read_encoding(current, chunk_encoding)
|
|
126
|
+
return current if chunk_encoding == Encoding::ASCII_8BIT
|
|
127
|
+
return Encoding::UTF_8 if current == Encoding::UTF_8 || chunk_encoding == Encoding::UTF_8
|
|
128
|
+
|
|
129
|
+
current || chunk_encoding
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.validate_read_chunk!(chunk, filename:, byte_offset:, preceding:, allow_binary: false)
|
|
133
|
+
raise TypeError, "io#read must return a String" unless chunk.is_a?(String)
|
|
134
|
+
return if [Encoding::UTF_8, Encoding::US_ASCII].include?(chunk.encoding) ||
|
|
135
|
+
(allow_binary && chunk.encoding == Encoding::ASCII_8BIT)
|
|
136
|
+
|
|
137
|
+
line, column = Input.position_for(preceding || chunk, byte_offset)
|
|
138
|
+
raise EncodingError.new(
|
|
139
|
+
"source must be UTF-8 or US-ASCII",
|
|
140
|
+
code: "invalid_encoding",
|
|
141
|
+
filename: filename,
|
|
142
|
+
byte_offset: byte_offset,
|
|
143
|
+
end_byte_offset: byte_offset + [chunk.bytesize, 1].min,
|
|
144
|
+
line: line,
|
|
145
|
+
column: column,
|
|
146
|
+
excerpt: limit_excerpt(preceding || String.new(encoding: Encoding::BINARY), chunk, byte_offset)
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.validate_parse_options!(options)
|
|
151
|
+
merged = DEFAULT_OPTIONS.merge(options)
|
|
152
|
+
validate_limits_option!(merged.fetch(:limits))
|
|
153
|
+
validate_diagnostics_option!(merged.fetch(:diagnostics))
|
|
154
|
+
validate_boolean_option!(merged.fetch(:freeze), :freeze)
|
|
155
|
+
validate_boolean_option!(merged.fetch(:warn_duplicate_keys), :warn_duplicate_keys)
|
|
156
|
+
validate_enum_option!(RubyValueBuilder::VALID_DUPLICATE_POLICIES, merged.fetch(:duplicate_keys), :duplicate_keys)
|
|
157
|
+
validate_enum_option!(RubyValueBuilder::VALID_NUMBER_MODES, merged.fetch(:number_mode), :number_mode)
|
|
158
|
+
validate_enum_option!(RubyValueBuilder::VALID_STRING_MODES, merged.fetch(:string_mode), :string_mode)
|
|
159
|
+
validate_enum_option!([:error, :replace], merged.fetch(:lone_surrogate), :lone_surrogate)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.limit_excerpt(source, chunk, byte_offset, radius: 40)
|
|
163
|
+
start_byte = [byte_offset - radius, 0].max
|
|
164
|
+
finish_byte = byte_offset + 1
|
|
165
|
+
excerpt = String.new(encoding: Encoding::BINARY)
|
|
166
|
+
if start_byte < source.bytesize
|
|
167
|
+
excerpt << source.byteslice(start_byte...[finish_byte, source.bytesize].min).to_s.b
|
|
168
|
+
end
|
|
169
|
+
chunk_start = [start_byte - source.bytesize, 0].max
|
|
170
|
+
chunk_finish = [finish_byte - source.bytesize, 0].max
|
|
171
|
+
excerpt << chunk.byteslice(chunk_start...chunk_finish).to_s.b
|
|
172
|
+
excerpt.force_encoding(Encoding::UTF_8).scrub
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def initialize(source, **options)
|
|
176
|
+
self.class.send(:validate_option_keys!, options, PARSE_OPTION_KEYS)
|
|
177
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
|
178
|
+
validate_options
|
|
179
|
+
@input = Input.new(
|
|
180
|
+
source,
|
|
181
|
+
limits: @options.fetch(:limits),
|
|
182
|
+
filename: @options.fetch(:filename)
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parse
|
|
187
|
+
builder = RubyValueBuilder.new(
|
|
188
|
+
number_mode: @options.fetch(:number_mode),
|
|
189
|
+
string_mode: @options.fetch(:string_mode),
|
|
190
|
+
lone_surrogate: @options.fetch(:lone_surrogate),
|
|
191
|
+
duplicate_keys: @options.fetch(:duplicate_keys),
|
|
192
|
+
limits: @options.fetch(:limits),
|
|
193
|
+
diagnostics: @options.fetch(:diagnostics),
|
|
194
|
+
warn_duplicate_keys: @options.fetch(:warn_duplicate_keys),
|
|
195
|
+
freeze_values: @options.fetch(:freeze),
|
|
196
|
+
source: @input.source,
|
|
197
|
+
filename: @options.fetch(:filename)
|
|
198
|
+
)
|
|
199
|
+
lexer = Lexer.new(
|
|
200
|
+
@input,
|
|
201
|
+
limits: @options.fetch(:limits),
|
|
202
|
+
diagnostics: @options.fetch(:diagnostics),
|
|
203
|
+
filename: @options.fetch(:filename)
|
|
204
|
+
)
|
|
205
|
+
@lexer = lexer
|
|
206
|
+
generated = GeneratedParser.new(lexer, builder)
|
|
207
|
+
generated.parse_tokens
|
|
208
|
+
rescue JSON5::Error => error
|
|
209
|
+
raise enrich_error(error)
|
|
210
|
+
rescue Ibex::Runtime::ParseError => error
|
|
211
|
+
raise enrich_error(translate_parse_error(error))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def validate_options
|
|
217
|
+
self.class.send(:validate_parse_options!, @options)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.validate_option_keys!(options, allowed_keys)
|
|
221
|
+
unknown_options = options.keys - allowed_keys
|
|
222
|
+
return if unknown_options.empty?
|
|
223
|
+
|
|
224
|
+
raise ArgumentError, "unknown options: #{unknown_options.map(&:to_s).sort.join(', ')}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.validate_limits_option!(limits)
|
|
228
|
+
return if limits.is_a?(Limits)
|
|
229
|
+
|
|
230
|
+
raise ArgumentError, "limits must be a JSON5::Limits"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def self.validate_diagnostics_option!(diagnostics)
|
|
234
|
+
return if diagnostics.nil? || diagnostics == :ignore || diagnostics.is_a?(Array) || diagnostics.respond_to?(:call)
|
|
235
|
+
|
|
236
|
+
raise ArgumentError, "diagnostics must be an Array, callable, :ignore, or nil"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def self.validate_boolean_option!(value, name)
|
|
240
|
+
return if value == true || value == false
|
|
241
|
+
|
|
242
|
+
raise ArgumentError, "#{name} must be true or false"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def self.validate_enum_option!(allowed, value, name)
|
|
246
|
+
return if allowed.include?(value)
|
|
247
|
+
|
|
248
|
+
raise ArgumentError, "#{name} must be one of #{allowed.inspect}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def translate_parse_error(error)
|
|
252
|
+
internal_expected = error.respond_to?(:expected_tokens) ? contextual_expected(error.expected_tokens) : nil
|
|
253
|
+
internal_unexpected = error.respond_to?(:token_name) ? error.token_name : nil
|
|
254
|
+
trailing_tokens = internal_unexpected != "$eof" && internal_expected&.include?("$eof") &&
|
|
255
|
+
@lexer&.last_token_nesting_depth == 0
|
|
256
|
+
expected = trailing_tokens ? ["end of input"] : PublicTokenNames.expected(internal_expected)
|
|
257
|
+
unexpected = PublicTokenNames.name(internal_unexpected || "token")
|
|
258
|
+
token = @lexer&.last_token
|
|
259
|
+
message = "unexpected #{unexpected}"
|
|
260
|
+
message += "; expected #{expected.join(', ')}" if expected && !expected.empty?
|
|
261
|
+
ParseError.new(
|
|
262
|
+
message,
|
|
263
|
+
code: trailing_tokens ? "trailing_tokens" : "unexpected_token",
|
|
264
|
+
filename: @options.fetch(:filename),
|
|
265
|
+
byte_offset: token&.start_byte || @input.index,
|
|
266
|
+
end_byte_offset: token&.end_byte || @input.index,
|
|
267
|
+
line: token&.line || @input.line,
|
|
268
|
+
column: token&.column || @input.column,
|
|
269
|
+
unexpected: unexpected,
|
|
270
|
+
expected: expected
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def contextual_expected(expected_tokens)
|
|
275
|
+
expected = Array(expected_tokens).dup
|
|
276
|
+
case @lexer&.last_token_container
|
|
277
|
+
when "{"
|
|
278
|
+
expected -= ["$eof", "']'"]
|
|
279
|
+
when "["
|
|
280
|
+
expected -= ["$eof", "'}'"]
|
|
281
|
+
end
|
|
282
|
+
expected
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def enrich_error(error)
|
|
286
|
+
return error if error.byte_offset && error.end_byte_offset && error.line && error.column && error.excerpt
|
|
287
|
+
|
|
288
|
+
token = @lexer&.last_token
|
|
289
|
+
offset = error.byte_offset || token&.start_byte || @input.index
|
|
290
|
+
finish = error.end_byte_offset || token&.end_byte || @input.index
|
|
291
|
+
error.with_context(
|
|
292
|
+
filename: error.filename || @options.fetch(:filename),
|
|
293
|
+
byte_offset: offset,
|
|
294
|
+
end_byte_offset: finish,
|
|
295
|
+
line: error.line || token&.line || @input.line,
|
|
296
|
+
column: error.column || token&.column || @input.column,
|
|
297
|
+
excerpt: error.excerpt || @input.excerpt_at(offset)
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
module PublicTokenNames
|
|
5
|
+
VALUE_TOKENS = ["STRING", "NUMBER", "TRUE", "FALSE", "NULL", "INFINITY", "NAN", "'{'", "'['"].freeze
|
|
6
|
+
MEMBER_NAME_TOKENS = %w[STRING IDENTIFIER_NAME TRUE FALSE NULL INFINITY NAN].freeze
|
|
7
|
+
NAMES = {
|
|
8
|
+
"$eof" => "end of input",
|
|
9
|
+
"EOF" => "end of input",
|
|
10
|
+
"STRING" => "string",
|
|
11
|
+
"NUMBER" => "number",
|
|
12
|
+
"IDENTIFIER_NAME" => "identifier",
|
|
13
|
+
"TRUE" => "true",
|
|
14
|
+
"FALSE" => "false",
|
|
15
|
+
"NULL" => "null",
|
|
16
|
+
"INFINITY" => "Infinity",
|
|
17
|
+
"NAN" => "NaN"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def name(token_name)
|
|
23
|
+
internal_name = token_name.to_s
|
|
24
|
+
return NAMES.fetch(internal_name) if NAMES.key?(internal_name)
|
|
25
|
+
return internal_name[1...-1] if internal_name.start_with?("'") && internal_name.end_with?("'")
|
|
26
|
+
|
|
27
|
+
internal_name.downcase.tr("_", " ")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def expected(token_names)
|
|
31
|
+
remaining = Array(token_names).map(&:to_s)
|
|
32
|
+
public_names = []
|
|
33
|
+
if (VALUE_TOKENS - remaining).empty?
|
|
34
|
+
public_names << "value"
|
|
35
|
+
remaining -= VALUE_TOKENS
|
|
36
|
+
elsif (MEMBER_NAME_TOKENS - remaining).empty?
|
|
37
|
+
public_names << "member name"
|
|
38
|
+
remaining -= MEMBER_NAME_TOKENS
|
|
39
|
+
end
|
|
40
|
+
public_names.concat(remaining.map { |token_name| name(token_name) }).uniq.freeze
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|