json_mend 0.3.5 → 0.3.6
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 +4 -4
- data/.rubocop.yml +9 -0
- data/.tool-versions +1 -1
- data/lib/json_mend/parser.rb +47 -34
- data/lib/json_mend/version.rb +1 -1
- data/lib/json_mend.rb +7 -6
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0db6ff9aaecf22de21a04b136e235fa04d12d43f3c1db614f96eec2c881ce7b9
|
|
4
|
+
data.tar.gz: 00aa675a9392fdcd9d528f788144ad7a3064c689e9a6a698132c15ff0a9b4130
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22e7132bcfe8f2109be0684afc22bcd21ac8d358dfb0f3b04ad41b14bba5853d82a9d6f1a54d996f8c4ecd9ab0fded3675c147b98fa218513336d7f1856c2a6b
|
|
7
|
+
data.tar.gz: f347eb625959326c68c2ffb283ba0d7a170a981927540d16c1b9aa526cde70fb1755cd40e8587af308ed17c83adccddca4a0ea49f986c8536419cfaf99af88bb
|
data/.rubocop.yml
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
inherit_mode:
|
|
2
|
+
merge:
|
|
3
|
+
- Exclude
|
|
4
|
+
|
|
1
5
|
plugins:
|
|
2
6
|
- rubocop-performance
|
|
3
7
|
- rubocop-rspec
|
|
8
|
+
- rubocop-rake
|
|
4
9
|
|
|
5
10
|
AllCops:
|
|
6
11
|
NewCops: enable
|
|
12
|
+
ParserEngine: parser_prism
|
|
13
|
+
CacheRootDirectory: .rubocop_cache
|
|
14
|
+
MaxFilesInCache: 8000
|
|
15
|
+
UseCache: false
|
|
7
16
|
SuggestExtensions: false
|
|
8
17
|
|
|
9
18
|
Metrics/AbcSize:
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 4.0.
|
|
1
|
+
ruby 4.0.6
|
data/lib/json_mend/parser.rb
CHANGED
|
@@ -42,6 +42,7 @@ module JsonMend
|
|
|
42
42
|
INVALID_NUMBER_TRAILERS = ['-', 'e', 'E', ','].freeze
|
|
43
43
|
|
|
44
44
|
# Pre-compile regexes for performance
|
|
45
|
+
STRING_START_REGEX = /[\p{L}$_]/
|
|
45
46
|
NUMBER_REGEX = /[#{Regexp.escape(NUMBER_CHARS.to_a.join)}]+/
|
|
46
47
|
NUMBER_NO_COMMA_REGEX = /[#{Regexp.escape(NUMBER_CHARS.dup.tap { |s| s.delete(',') }.to_a.join)}]+/
|
|
47
48
|
INVALID_NUMBER_TRAILERS_REGEX = /[#{Regexp.union(*INVALID_NUMBER_TRAILERS)}]+\z/
|
|
@@ -53,6 +54,7 @@ module JsonMend
|
|
|
53
54
|
def initialize(json_string)
|
|
54
55
|
@scanner = StringScanner.new(json_string)
|
|
55
56
|
@context = []
|
|
57
|
+
@current_context = nil
|
|
56
58
|
@depth = 0
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -178,7 +180,7 @@ module JsonMend
|
|
|
178
180
|
with_depth_check do
|
|
179
181
|
object = {}
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
push_context(:object)
|
|
182
184
|
|
|
183
185
|
loop do
|
|
184
186
|
skip_whitespaces
|
|
@@ -222,7 +224,7 @@ module JsonMend
|
|
|
222
224
|
object[key] = value
|
|
223
225
|
end
|
|
224
226
|
|
|
225
|
-
|
|
227
|
+
pop_context
|
|
226
228
|
|
|
227
229
|
object
|
|
228
230
|
end
|
|
@@ -299,7 +301,7 @@ module JsonMend
|
|
|
299
301
|
end
|
|
300
302
|
|
|
301
303
|
# If no merge happened, proceed with standard key parsing.
|
|
302
|
-
|
|
304
|
+
push_context(:object_key)
|
|
303
305
|
is_bracketed = false
|
|
304
306
|
|
|
305
307
|
if char == '['
|
|
@@ -310,7 +312,7 @@ module JsonMend
|
|
|
310
312
|
else
|
|
311
313
|
key = parse_string.to_s
|
|
312
314
|
end
|
|
313
|
-
|
|
315
|
+
pop_context
|
|
314
316
|
|
|
315
317
|
# If the key is empty, consume any stray characters to prevent infinite loops.
|
|
316
318
|
@scanner.getch if key.empty? && !@scanner.check(/[:{\[}\]]/) && !@scanner.eos?
|
|
@@ -320,18 +322,18 @@ module JsonMend
|
|
|
320
322
|
|
|
321
323
|
# Parses the value part of a key-value pair.
|
|
322
324
|
def parse_object_value(colon_found: true)
|
|
323
|
-
|
|
325
|
+
push_context(:object_value)
|
|
324
326
|
skip_whitespaces
|
|
325
327
|
|
|
326
328
|
# Handle cases where the value is missing (e.g. "key": } or "key": ,)
|
|
327
329
|
if @scanner.eos? || @scanner.check(/[,}]/)
|
|
328
|
-
|
|
330
|
+
pop_context
|
|
329
331
|
return colon_found ? '' : :inferred_true
|
|
330
332
|
end
|
|
331
333
|
|
|
332
334
|
# Delegate to the main JSON value parser.
|
|
333
335
|
value = parse_json
|
|
334
|
-
|
|
336
|
+
pop_context
|
|
335
337
|
|
|
336
338
|
# If parse_json returned JSON_STOP_TOKEN (nothing found due to garbage->terminator),
|
|
337
339
|
# treat it as empty string for object values to be safe.
|
|
@@ -342,7 +344,7 @@ module JsonMend
|
|
|
342
344
|
def try_to_merge_dangling_array(object)
|
|
343
345
|
return false unless peek_char == '['
|
|
344
346
|
|
|
345
|
-
prev_key = object.keys
|
|
347
|
+
prev_key = object.keys[-1]
|
|
346
348
|
return false unless prev_key && object[prev_key].is_a?(Array)
|
|
347
349
|
|
|
348
350
|
@scanner.getch # Consume '['
|
|
@@ -365,7 +367,7 @@ module JsonMend
|
|
|
365
367
|
def parse_array
|
|
366
368
|
with_depth_check do
|
|
367
369
|
arr = []
|
|
368
|
-
|
|
370
|
+
push_context(:array)
|
|
369
371
|
char = peek_char
|
|
370
372
|
# Stop when you find the closing bracket or an invalid character like '}'
|
|
371
373
|
while !@scanner.eos? && !TERMINATORS_ARRAY.include?(char)
|
|
@@ -409,7 +411,7 @@ module JsonMend
|
|
|
409
411
|
unless @scanner.scan(']')
|
|
410
412
|
@scanner.scan('}') # Consume } if it was the closer
|
|
411
413
|
end
|
|
412
|
-
|
|
414
|
+
pop_context
|
|
413
415
|
|
|
414
416
|
arr
|
|
415
417
|
end
|
|
@@ -447,7 +449,7 @@ module JsonMend
|
|
|
447
449
|
|
|
448
450
|
doubled_quotes = rest.first
|
|
449
451
|
|
|
450
|
-
string_parts =
|
|
452
|
+
string_parts = +''
|
|
451
453
|
|
|
452
454
|
# Here things get a bit hairy because a string missing the final quote can also be a key or a value in an object
|
|
453
455
|
# In that case we need to use the ":|,|}" characters as terminators of the string
|
|
@@ -576,12 +578,12 @@ module JsonMend
|
|
|
576
578
|
end
|
|
577
579
|
|
|
578
580
|
break if context_termination_reached?(
|
|
579
|
-
char
|
|
580
|
-
missing_quotes
|
|
581
|
+
char,
|
|
582
|
+
missing_quotes
|
|
581
583
|
)
|
|
582
584
|
|
|
583
585
|
if current_context?(:object_value) && TERMINATORS_OBJECT_VALUE.include?(char) &&
|
|
584
|
-
(string_parts.empty? || string_parts
|
|
586
|
+
(string_parts.empty? || string_parts[-1] != rstring_delimiter)
|
|
585
587
|
|
|
586
588
|
is_break = check_rstring_delimiter_missing(
|
|
587
589
|
string_parts:,
|
|
@@ -592,7 +594,7 @@ module JsonMend
|
|
|
592
594
|
break if is_break
|
|
593
595
|
end
|
|
594
596
|
|
|
595
|
-
if char == ']' && context_contain?(:array) && string_parts
|
|
597
|
+
if char == ']' && context_contain?(:array) && string_parts[-1] != rstring_delimiter
|
|
596
598
|
i = skip_to_character(rstring_delimiter)
|
|
597
599
|
# No delimiter found
|
|
598
600
|
break unless peek_char(i)
|
|
@@ -610,7 +612,7 @@ module JsonMend
|
|
|
610
612
|
@scanner.getch # Consume the character
|
|
611
613
|
char = peek_char
|
|
612
614
|
|
|
613
|
-
if !@scanner.eos? && string_parts
|
|
615
|
+
if !@scanner.eos? && string_parts[-1] == '\\'
|
|
614
616
|
# This is a special case, if people use real strings this might happen
|
|
615
617
|
is_next, string_parts, char = parse_escape_sequence(
|
|
616
618
|
string_parts:,
|
|
@@ -629,7 +631,7 @@ module JsonMend
|
|
|
629
631
|
break if is_break
|
|
630
632
|
end
|
|
631
633
|
|
|
632
|
-
if char == rstring_delimiter && string_parts
|
|
634
|
+
if char == rstring_delimiter && string_parts[-1] != '\\'
|
|
633
635
|
if check_doubled_quotes(doubled_quotes, rstring_delimiter)
|
|
634
636
|
# Consumed in helper
|
|
635
637
|
elsif check_missing_quotes_in_object_value(missing_quotes, lstring_delimiter, rstring_delimiter)
|
|
@@ -934,18 +936,18 @@ module JsonMend
|
|
|
934
936
|
char:,
|
|
935
937
|
rstring_delimiter:
|
|
936
938
|
)
|
|
937
|
-
if !@scanner.eos? && string_parts
|
|
939
|
+
if !@scanner.eos? && string_parts[-1] == '\\'
|
|
938
940
|
# This is a special case, if people use real strings this might happen
|
|
939
941
|
if char == rstring_delimiter || ESCAPE_START_CHARS.include?(char)
|
|
940
|
-
string_parts.
|
|
942
|
+
string_parts.chop!
|
|
941
943
|
string_parts << ESCAPE_MAPPING.fetch(char, char)
|
|
942
944
|
|
|
943
945
|
@scanner.getch # Consume the character
|
|
944
946
|
char = peek_char
|
|
945
|
-
while !@scanner.eos? && string_parts
|
|
947
|
+
while !@scanner.eos? && string_parts[-1] == '\\' && (char == rstring_delimiter || char == '\\')
|
|
946
948
|
# this is a bit of a special case, if I don't do this it will close the loop or create a train of \\
|
|
947
949
|
# I don't love it though
|
|
948
|
-
string_parts.
|
|
950
|
+
string_parts.chop!
|
|
949
951
|
string_parts << char
|
|
950
952
|
@scanner.getch # Consume the character
|
|
951
953
|
char = peek_char
|
|
@@ -959,7 +961,7 @@ module JsonMend
|
|
|
959
961
|
|
|
960
962
|
# Validate valid hex digits
|
|
961
963
|
if (hex_str = @scanner.scan(hex_regex))
|
|
962
|
-
string_parts.
|
|
964
|
+
string_parts.chop!
|
|
963
965
|
hex_val = hex_str.to_i(16)
|
|
964
966
|
|
|
965
967
|
if char == 'u' && hex_val.between?(0xD800, 0xDBFF)
|
|
@@ -1000,7 +1002,7 @@ module JsonMend
|
|
|
1000
1002
|
@scanner.pos = entry_pos
|
|
1001
1003
|
end
|
|
1002
1004
|
elsif STRING_DELIMITERS.include?(char) && char != rstring_delimiter
|
|
1003
|
-
string_parts.
|
|
1005
|
+
string_parts.chop!
|
|
1004
1006
|
string_parts << char
|
|
1005
1007
|
@scanner.getch # Consume the character
|
|
1006
1008
|
char = peek_char
|
|
@@ -1045,19 +1047,19 @@ module JsonMend
|
|
|
1045
1047
|
# we need to update the index only if we had a closing quote
|
|
1046
1048
|
if char == rstring_delimiter
|
|
1047
1049
|
@scanner.getch
|
|
1048
|
-
elsif missing_quotes && current_context?(:object_key) && string_parts
|
|
1049
|
-
string_parts.
|
|
1050
|
+
elsif missing_quotes && current_context?(:object_key) && string_parts[-1] == ','
|
|
1051
|
+
string_parts.chop!
|
|
1050
1052
|
end
|
|
1051
1053
|
|
|
1052
|
-
final_str = string_parts
|
|
1054
|
+
final_str = string_parts
|
|
1053
1055
|
final_str = final_str.rstrip if missing_quotes || final_str.end_with?("\n")
|
|
1054
1056
|
|
|
1055
1057
|
final_str
|
|
1056
1058
|
end
|
|
1057
1059
|
|
|
1058
1060
|
def context_termination_reached?(
|
|
1059
|
-
char
|
|
1060
|
-
missing_quotes
|
|
1061
|
+
char,
|
|
1062
|
+
missing_quotes
|
|
1061
1063
|
)
|
|
1062
1064
|
return false unless missing_quotes
|
|
1063
1065
|
|
|
@@ -1302,12 +1304,13 @@ module JsonMend
|
|
|
1302
1304
|
def peek_char(offset = 0)
|
|
1303
1305
|
# Handle the common 0-offset case
|
|
1304
1306
|
if offset.zero?
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
return nil if
|
|
1307
|
+
pos = @scanner.pos
|
|
1308
|
+
str = @scanner.string
|
|
1309
|
+
return nil if pos >= str.bytesize
|
|
1308
1310
|
|
|
1311
|
+
byte = str.getbyte(pos)
|
|
1309
1312
|
# Fast path: If it's a standard ASCII char (0-127), return it directly.
|
|
1310
|
-
return
|
|
1313
|
+
return byte.chr if byte < 128
|
|
1311
1314
|
|
|
1312
1315
|
# Slow path: If it's a multibyte char (e.g. “), use regex to match the full character.
|
|
1313
1316
|
return @scanner.check(/./m)
|
|
@@ -1324,8 +1327,18 @@ module JsonMend
|
|
|
1324
1327
|
res
|
|
1325
1328
|
end
|
|
1326
1329
|
|
|
1330
|
+
def push_context(value)
|
|
1331
|
+
@context.push(value)
|
|
1332
|
+
@current_context = value
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def pop_context
|
|
1336
|
+
@context.pop
|
|
1337
|
+
@current_context = @context.last
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1327
1340
|
def current_context?(value)
|
|
1328
|
-
@
|
|
1341
|
+
@current_context == value
|
|
1329
1342
|
end
|
|
1330
1343
|
|
|
1331
1344
|
def context_contain?(value)
|
|
@@ -1334,7 +1347,7 @@ module JsonMend
|
|
|
1334
1347
|
|
|
1335
1348
|
# Checks if the character signifies the start of a string or literal
|
|
1336
1349
|
def string_start?(char)
|
|
1337
|
-
STRING_DELIMITERS.include?(char) || char
|
|
1350
|
+
STRING_DELIMITERS.include?(char) || (char && STRING_START_REGEX.match?(char))
|
|
1338
1351
|
end
|
|
1339
1352
|
|
|
1340
1353
|
# Checks if the character signifies the start of a number
|
data/lib/json_mend/version.rb
CHANGED
data/lib/json_mend.rb
CHANGED
|
@@ -15,21 +15,22 @@ module JsonMend
|
|
|
15
15
|
# @return [Object, String] The repaired JSON object or string.
|
|
16
16
|
def repair(json_string, return_objects: false)
|
|
17
17
|
# First, attempt to parse the string with the standard library.
|
|
18
|
-
|
|
18
|
+
begin
|
|
19
19
|
parsed = JSON.parse(
|
|
20
20
|
json_string,
|
|
21
21
|
allow_trailing_comma: true,
|
|
22
22
|
allow_control_characters: true
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
JSON.generate(parsed)
|
|
25
|
+
# If the user wants objects, return them immediately
|
|
26
|
+
return parsed if return_objects
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
# Otherwise, generate the string once. This acts as both the
|
|
29
|
+
# UTF-8 verification step AND the final string output.
|
|
30
|
+
return JSON.generate(parsed)
|
|
30
31
|
rescue JSON::ParserError, JSON::GeneratorError
|
|
31
32
|
parser = Parser.new(json_string)
|
|
32
|
-
parser.parse
|
|
33
|
+
repaired_json = parser.parse
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# Avoids returning `null` for empty results, returns the object directly
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: json_mend
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleksii Vasyliev
|
|
@@ -77,14 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - ">="
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: 3.
|
|
80
|
+
version: 3.3.0
|
|
81
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
82
|
requirements:
|
|
83
83
|
- - ">="
|
|
84
84
|
- !ruby/object:Gem::Version
|
|
85
85
|
version: '0'
|
|
86
86
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.16
|
|
88
88
|
specification_version: 4
|
|
89
89
|
summary: Repair broken JSON
|
|
90
90
|
test_files: []
|