json-repair 0.1.0 → 0.3.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.
@@ -0,0 +1,766 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'repair/string_utils'
4
+
5
+ module JSON
6
+ class Repairer
7
+ include Repair::StringUtils
8
+
9
+ CONTROL_CHARACTERS = {
10
+ "\b" => '\b',
11
+ "\f" => '\f',
12
+ "\n" => '\n',
13
+ "\r" => '\r',
14
+ "\t" => '\t'
15
+ }.freeze
16
+
17
+ ESCAPE_CHARACTERS = {
18
+ '"' => '"',
19
+ '\\' => '\\',
20
+ '/' => '/',
21
+ 'b' => "\b",
22
+ 'f' => "\f",
23
+ 'n' => "\n",
24
+ 'r' => "\r",
25
+ 't' => "\t"
26
+ }.freeze
27
+
28
+ MARKDOWN_OPEN_BLOCKS = ['```', '[```', '{```'].freeze
29
+ MARKDOWN_CLOSE_BLOCKS = ['```', '```]', '```}'].freeze
30
+
31
+ def initialize(json)
32
+ @json = json
33
+ @index = 0
34
+ @output = +''
35
+ end
36
+
37
+ def repair
38
+ parse_markdown_code_block(MARKDOWN_OPEN_BLOCKS)
39
+
40
+ processed = parse_value
41
+
42
+ throw_unexpected_end unless processed
43
+
44
+ parse_markdown_code_block(MARKDOWN_CLOSE_BLOCKS)
45
+
46
+ processed_comma = parse_character(COMMA)
47
+ parse_whitespace_and_skip_comments if processed_comma
48
+
49
+ if start_of_value?(@json[@index]) && ends_with_comma_or_newline?(@output)
50
+ # start of a new value after end of the root level object: looks like
51
+ # newline delimited JSON -> turn into a root level array
52
+ unless processed_comma
53
+ # repair missing comma
54
+ @output = insert_before_last_whitespace(@output, ',')
55
+ end
56
+
57
+ parse_newline_delimited_json
58
+ elsif processed_comma
59
+ # repair: remove trailing comma
60
+ @output = strip_last_occurrence(@output, ',')
61
+ end
62
+
63
+ # repair redundant end quotes
64
+ while @json[@index] == CLOSING_BRACE || @json[@index] == CLOSING_BRACKET
65
+ @index += 1
66
+ parse_whitespace_and_skip_comments
67
+ end
68
+
69
+ if @index >= @json.length
70
+ # reached the end of the document properly
71
+ return @output
72
+ end
73
+
74
+ throw_unexpected_character
75
+ end
76
+
77
+ private
78
+
79
+ def parse_value
80
+ parse_whitespace_and_skip_comments
81
+ process = parse_object ||
82
+ parse_array ||
83
+ parse_string ||
84
+ parse_number ||
85
+ parse_keywords ||
86
+ parse_unquoted_string(false) ||
87
+ parse_regex
88
+ parse_whitespace_and_skip_comments
89
+
90
+ process
91
+ end
92
+
93
+ def parse_whitespace_and_skip_comments(skip_newline: true)
94
+ start = @index
95
+
96
+ changed = parse_whitespace(skip_newline: skip_newline)
97
+ loop do
98
+ changed = parse_comment
99
+ changed = parse_whitespace(skip_newline: skip_newline) if changed
100
+ break unless changed
101
+ end
102
+
103
+ @index > start
104
+ end
105
+
106
+ def parse_whitespace(skip_newline: true)
107
+ whitespace = +''
108
+ while @json[@index] && (
109
+ (skip_newline ? whitespace?(@json[@index]) : whitespace_except_newline?(@json[@index])) ||
110
+ special_whitespace?(@json[@index])
111
+ )
112
+ ws = skip_newline ? whitespace?(@json[@index]) : whitespace_except_newline?(@json[@index])
113
+ whitespace << (ws ? @json[@index] : ' ')
114
+
115
+ @index += 1
116
+ end
117
+
118
+ unless whitespace.empty?
119
+ @output << whitespace
120
+ return true
121
+ end
122
+
123
+ false
124
+ end
125
+
126
+ def parse_comment
127
+ if @json[@index] == '/' && @json[@index + 1] == '*'
128
+ # Block comment
129
+ @index += 2
130
+ @index += 1 until @json[@index].nil? || (@json[@index] == '*' && @json[@index + 1] == '/')
131
+ @index += 2
132
+ true
133
+ elsif @json[@index] == '/' && @json[@index + 1] == '/'
134
+ # Line comment
135
+ @index += 2
136
+ @index += 1 until @json[@index].nil? || @json[@index] == "\n"
137
+ true
138
+ else
139
+ false
140
+ end
141
+ end
142
+
143
+ # Find and skip over a Markdown fenced code block:
144
+ # ``` ... ```
145
+ # or
146
+ # ```json ... ```
147
+ def parse_markdown_code_block(blocks)
148
+ return false unless skip_markdown_code_block(blocks)
149
+
150
+ if function_name_char_start?(@json[@index])
151
+ # strip the optional language specifier like "json"
152
+ @index += 1 while @index < @json.length && function_name_char?(@json[@index])
153
+ end
154
+
155
+ parse_whitespace_and_skip_comments
156
+
157
+ true
158
+ end
159
+
160
+ def skip_markdown_code_block(blocks)
161
+ parse_whitespace(skip_newline: true)
162
+
163
+ blocks.each do |block|
164
+ if @json[@index, block.length] == block
165
+ @index += block.length
166
+ return true
167
+ end
168
+ end
169
+
170
+ false
171
+ end
172
+
173
+ # Parse an object like '{"key": "value"}'
174
+ def parse_object
175
+ return false unless @json[@index] == OPENING_BRACE
176
+
177
+ @output << '{'
178
+ @index += 1
179
+ parse_whitespace_and_skip_comments
180
+
181
+ # repair: skip leading comma like in {, message: "hi"}
182
+ parse_whitespace_and_skip_comments if skip_character(COMMA)
183
+
184
+ initial = true
185
+ while @index < @json.length && @json[@index] != CLOSING_BRACE
186
+ processed_comma = true
187
+ if initial
188
+ initial = false
189
+ else
190
+ processed_comma = parse_character(COMMA)
191
+ unless processed_comma
192
+ # repair missing comma
193
+ @output = insert_before_last_whitespace(@output, ',')
194
+ end
195
+ parse_whitespace_and_skip_comments
196
+ end
197
+
198
+ skip_ellipsis
199
+
200
+ processed_key = parse_string || parse_unquoted_string(true)
201
+ unless processed_key
202
+ if @json[@index] == CLOSING_BRACE || @json[@index] == OPENING_BRACE ||
203
+ @json[@index] == CLOSING_BRACKET || @json[@index] == OPENING_BRACKET ||
204
+ @json[@index].nil?
205
+ # repair trailing comma
206
+ @output = strip_last_occurrence(@output, ',')
207
+ else
208
+ throw_object_key_expected
209
+ end
210
+ break
211
+ end
212
+
213
+ parse_whitespace_and_skip_comments
214
+ processed_colon = parse_character(COLON)
215
+ truncated_text = @index >= @json.length
216
+ unless processed_colon
217
+ if start_of_value?(@json[@index]) || truncated_text
218
+ # repair missing colon
219
+ @output = insert_before_last_whitespace(@output, ':')
220
+ else
221
+ throw_colon_expected
222
+ end
223
+ end
224
+
225
+ processed_value = parse_value
226
+ unless processed_value
227
+ if processed_colon || truncated_text
228
+ # repair missing object value
229
+ @output << 'null'
230
+ else
231
+ throw_colon_expected
232
+ end
233
+ end
234
+ end
235
+
236
+ if @json[@index] == CLOSING_BRACE
237
+ @output << '}'
238
+ @index += 1
239
+ else
240
+ # repair missing end bracket
241
+ @output = insert_before_last_whitespace(@output, '}')
242
+ end
243
+
244
+ true
245
+ end
246
+
247
+ def skip_character(char)
248
+ if @json[@index] == char
249
+ @index += 1
250
+ true
251
+ else
252
+ false
253
+ end
254
+ end
255
+
256
+ # Skip ellipsis like "[1,2,3,...]" or "[1,2,3,...,9]" or "[...,7,8,9]"
257
+ # or a similar construct in objects.
258
+ def skip_ellipsis
259
+ parse_whitespace_and_skip_comments
260
+
261
+ if @json[@index] == DOT &&
262
+ @json[@index + 1] == DOT &&
263
+ @json[@index + 2] == DOT
264
+ # repair: remove the ellipsis (three dots) and optionally a comma
265
+ @index += 3
266
+ parse_whitespace_and_skip_comments
267
+ skip_character(COMMA)
268
+ end
269
+ end
270
+
271
+ # Parse a string enclosed by double quotes "...". Can contain escaped quotes
272
+ # Repair strings enclosed in single quotes or special quotes
273
+ # Repair an escaped string
274
+ #
275
+ # The function can run in two stages:
276
+ # - First, it assumes the string has a valid end quote
277
+ # - If it turns out that the string does not have a valid end quote followed
278
+ # by a delimiter (which should be the case), the function runs again in a
279
+ # more conservative way, stopping the string at the first next delimiter
280
+ # and fixing the string by inserting a quote there, or stopping at a
281
+ # stop index detected in the first iteration.
282
+ def parse_string(stop_at_delimiter: false, stop_at_index: -1)
283
+ skip_escape_chars = @json[@index] == BACKSLASH
284
+ if skip_escape_chars
285
+ # repair: remove the first escape character
286
+ @index += 1
287
+ end
288
+
289
+ return false unless quote?(@json[@index])
290
+
291
+ # double quotes are correct JSON,
292
+ # single quotes come from JavaScript for example, we assume it will have a correct single end quote too
293
+ # otherwise, we will match any double-quote-like start with a double-quote-like end,
294
+ # or any single-quote-like start with a single-quote-like end
295
+ is_end_quote = if double_quote?(@json[@index])
296
+ method(:double_quote?)
297
+ elsif single_quote?(@json[@index])
298
+ method(:single_quote?)
299
+ elsif single_quote_like?(@json[@index])
300
+ method(:single_quote_like?)
301
+ else
302
+ method(:double_quote_like?)
303
+ end
304
+
305
+ i_before = @index
306
+ o_before = @output.length
307
+
308
+ str = +'"'
309
+ @index += 1
310
+
311
+ loop do
312
+ if @index >= @json.length
313
+ # end of text, we are missing an end quote
314
+
315
+ i_prev = prev_non_whitespace_index(@index - 1)
316
+ if !stop_at_delimiter && delimiter?(@json[i_prev])
317
+ # if the text ends with a delimiter, like ["hello],
318
+ # so the missing end quote should be inserted before this delimiter
319
+ # retry parsing the string, stopping at the first next delimiter
320
+ @index = i_before
321
+ @output = @output[0...o_before]
322
+
323
+ return parse_string(stop_at_delimiter: true)
324
+ end
325
+
326
+ # repair missing quote
327
+ str = insert_before_last_whitespace(str, '"')
328
+ @output << str
329
+
330
+ return true
331
+ end
332
+
333
+ if @index == stop_at_index
334
+ # use the stop index detected in the first iteration, and repair end quote
335
+ str = insert_before_last_whitespace(str, '"')
336
+ @output << str
337
+
338
+ return true
339
+ end
340
+
341
+ if is_end_quote.call(@json[@index])
342
+ # end quote
343
+ # let us check what is before and after the quote to verify whether this is a legit end quote
344
+ i_quote = @index
345
+ o_quote = str.length
346
+ str << '"'
347
+ @index += 1
348
+ @output << str
349
+
350
+ parse_whitespace_and_skip_comments(skip_newline: false)
351
+
352
+ if stop_at_delimiter ||
353
+ @index >= @json.length ||
354
+ delimiter?(@json[@index]) ||
355
+ quote?(@json[@index]) ||
356
+ digit?(@json[@index])
357
+ # The quote is followed by the end of the text, a delimiter, or a next value
358
+ parse_concatenated_string
359
+
360
+ return true
361
+ end
362
+
363
+ i_prev_char = prev_non_whitespace_index(i_quote - 1)
364
+ prev_char = @json[i_prev_char]
365
+
366
+ if prev_char == ','
367
+ # A comma followed by a quote, like '{"a":"b,c,"d":"e"}'.
368
+ # We assume that the quote is a start quote, and that the end quote
369
+ # should have been located right before the comma but is missing.
370
+ @index = i_before
371
+ @output = @output[0...o_before]
372
+
373
+ return parse_string(stop_at_delimiter: false, stop_at_index: i_prev_char)
374
+ end
375
+
376
+ if delimiter?(prev_char)
377
+ # This is not the right end quote: it is preceded by a delimiter,
378
+ # and NOT followed by a delimiter. So, there is an end quote missing
379
+ # parse the string again and then stop at the first next delimiter
380
+ @index = i_before
381
+ @output = @output[...o_before]
382
+
383
+ return parse_string(stop_at_delimiter: true)
384
+ end
385
+
386
+ # revert to right after the quote but before any whitespace, and continue parsing the string
387
+ @output = @output[...o_before]
388
+ @index = i_quote + 1
389
+
390
+ # repair unescaped quote
391
+ str = "#{str[...o_quote]}\\#{str[o_quote..]}"
392
+ elsif stop_at_delimiter && unquoted_string_delimiter?(@json[@index])
393
+ # we're in the mode to stop the string at the first delimiter
394
+ # because there is an end quote missing
395
+
396
+ # test start of an url like "https://..." (this would be parsed as a comment)
397
+ if @json[@index - 1] == ':' &&
398
+ REGEX_URL_START.match?(@json[(i_before + 1)..(@index + 1)] || '')
399
+ while @index < @json.length && REGEX_URL_CHAR.match?(@json[@index])
400
+ str << @json[@index]
401
+ @index += 1
402
+ end
403
+ end
404
+
405
+ # repair missing quote
406
+ str = insert_before_last_whitespace(str, '"')
407
+ @output << str
408
+
409
+ parse_concatenated_string
410
+
411
+ return true
412
+ elsif @json[@index] == BACKSLASH
413
+ # handle escaped content like \n or ★
414
+ char = @json[@index + 1]
415
+ escape_char = ESCAPE_CHARACTERS[char]
416
+ if escape_char
417
+ str << @json[@index, 2]
418
+ @index += 2
419
+ elsif char == 'u'
420
+ j = 2
421
+ j += 1 while j < 6 && @json[@index + j] && hex?(@json[@index + j])
422
+ if j == 6
423
+ str << @json[@index, 6]
424
+ @index += 6
425
+ elsif @index + j >= @json.length
426
+ # repair invalid or truncated unicode char at the end of the text
427
+ # by removing the unicode char and ending the string here
428
+ @index = @json.length
429
+ else
430
+ throw_invalid_unicode_character
431
+ end
432
+ elsif char == "\n"
433
+ # repair a backslash escaped newline (like in Bash scripts)
434
+ str << '\n'
435
+ @index += 2
436
+ else
437
+ # repair invalid escape character: remove it
438
+ str << char
439
+ @index += 2
440
+ end
441
+ else
442
+ # handle regular characters
443
+ char = @json[@index]
444
+
445
+ if char == DOUBLE_QUOTE && @json[@index - 1] != BACKSLASH
446
+ # repair unescaped double quote
447
+ str << "\\#{char}"
448
+ elsif control_character?(char)
449
+ # unescaped control character
450
+ str << CONTROL_CHARACTERS[char]
451
+ else
452
+ throw_invalid_character(char) unless valid_string_character?(char)
453
+ str << char
454
+ end
455
+ @index += 1
456
+ end
457
+
458
+ if skip_escape_chars
459
+ # repair: skipped escape character (nothing to do)
460
+ skip_escape_character
461
+ end
462
+ end
463
+ end
464
+
465
+ # Repair an unquoted string by adding quotes around it
466
+ # Repair a MongoDB function call like NumberLong("2")
467
+ # Repair a JSONP function call like callback({...});
468
+ def parse_unquoted_string(is_key)
469
+ # NOTE: that the symbol can end with whitespaces: we stop at the next delimiter
470
+ # also, note that we allow strings to contain a slash / in order to support repairing regular expressions
471
+ start = @index
472
+
473
+ if function_name_char_start?(@json[@index])
474
+ @index += 1 while @index < @json.length && function_name_char?(@json[@index])
475
+
476
+ j = @index
477
+ j += 1 while whitespace?(@json[j])
478
+
479
+ if @json[j] == '('
480
+ # repair a MongoDB function call like NumberLong("2")
481
+ # repair a JSONP function call like callback({...});
482
+ @index = j + 1
483
+
484
+ parse_value
485
+
486
+ if @json[@index] == ')'
487
+ # Repair: skip close bracket of function call
488
+ @index += 1
489
+ # Repair: skip semicolon after JSONP call
490
+ @index += 1 if @json[@index] == ';'
491
+ end
492
+
493
+ return true
494
+ end
495
+ end
496
+
497
+ while @index < @json.length &&
498
+ !unquoted_string_delimiter?(@json[@index]) &&
499
+ !quote?(@json[@index]) &&
500
+ (!is_key || @json[@index] != ':')
501
+ @index += 1
502
+ end
503
+
504
+ # test start of an url like "https://..." (this would be parsed as a comment)
505
+ if @json[@index - 1] == ':' &&
506
+ REGEX_URL_START.match?(@json[start...(@index + 2)] || '')
507
+ @index += 1 while @index < @json.length && REGEX_URL_CHAR.match?(@json[@index])
508
+ end
509
+
510
+ return false if @index <= start
511
+
512
+ # Repair unquoted string
513
+ # Also, repair undefined into null
514
+
515
+ # First, go back to prevent getting trailing whitespaces in the string
516
+ @index -= 1 while @index.positive? && whitespace?(@json[@index - 1])
517
+
518
+ symbol = @json[start...@index]
519
+ @output << (symbol == 'undefined' ? 'null' : symbol.inspect)
520
+
521
+ if @json[@index] == '"'
522
+ # We had a missing start quote, but now we encountered the end quote, so we can skip that one
523
+ @index += 1
524
+ end
525
+
526
+ true
527
+ end
528
+
529
+ # Parse a regular expression literal like /foo/ or /foo\/bar/
530
+ def parse_regex
531
+ return false unless @json[@index] == '/'
532
+
533
+ start = @index
534
+ @index += 1
535
+
536
+ @index += 1 while @index < @json.length && (@json[@index] != '/' || @json[@index - 1] == BACKSLASH)
537
+ @index += 1
538
+
539
+ @output << @json[start...@index].inspect
540
+
541
+ true
542
+ end
543
+
544
+ def parse_character(char)
545
+ if @json[@index] == char
546
+ @output << @json[@index]
547
+ @index += 1
548
+ true
549
+ else
550
+ false
551
+ end
552
+ end
553
+
554
+ # Parse a number like 2.4 or 2.4e6
555
+ def parse_number
556
+ start = @index
557
+ if @json[@index] == '-'
558
+ @index += 1
559
+ if at_end_of_number?
560
+ repair_number_ending_with_numeric_symbol(start)
561
+ return true
562
+ end
563
+ unless digit?(@json[@index])
564
+ @index = start
565
+ return false
566
+ end
567
+ end
568
+
569
+ # Note that in JSON leading zeros like "00789" are not allowed.
570
+ # We will allow all leading zeros here though and at the end of parse_number
571
+ # check against trailing zeros and repair that if needed.
572
+ # Leading zeros can have meaning, so we should not clear them.
573
+ @index += 1 while digit?(@json[@index])
574
+
575
+ if @json[@index] == '.'
576
+ @index += 1
577
+ if at_end_of_number?
578
+ repair_number_ending_with_numeric_symbol(start)
579
+ return true
580
+ end
581
+ unless digit?(@json[@index])
582
+ @index = start
583
+ return false
584
+ end
585
+ @index += 1 while digit?(@json[@index])
586
+ end
587
+
588
+ if @json[@index] && @json[@index].downcase == 'e'
589
+ @index += 1
590
+ @index += 1 if ['-', '+'].include?(@json[@index])
591
+ if at_end_of_number?
592
+ repair_number_ending_with_numeric_symbol(start)
593
+ return true
594
+ end
595
+ unless digit?(@json[@index])
596
+ @index = start
597
+ return false
598
+ end
599
+ @index += 1 while digit?(@json[@index])
600
+ end
601
+
602
+ # if we're not at the end of the number by this point, allow this to be parsed as another type
603
+ unless at_end_of_number?
604
+ @index = start
605
+ return false
606
+ end
607
+
608
+ if @index > start
609
+ # repair a number with leading zeros like "00789"
610
+ num = @json[start...@index]
611
+ has_invalid_leading_zero = num.match?(/^0\d/)
612
+
613
+ @output << (has_invalid_leading_zero ? "\"#{num}\"" : num)
614
+ return true
615
+ end
616
+
617
+ false
618
+ end
619
+
620
+ def at_end_of_number?
621
+ @index >= @json.length || delimiter?(@json[@index]) || whitespace?(@json[@index])
622
+ end
623
+
624
+ # Parse an array like '["item1", "item2", ...]'
625
+ def parse_array
626
+ if @json[@index] == OPENING_BRACKET
627
+ @output << '['
628
+ @index += 1
629
+ parse_whitespace_and_skip_comments
630
+
631
+ # repair: skip leading comma like in [,1,2,3]
632
+ parse_whitespace_and_skip_comments if skip_character(COMMA)
633
+
634
+ initial = true
635
+ while @index < @json.length && @json[@index] != CLOSING_BRACKET
636
+ if initial
637
+ initial = false
638
+ else
639
+ processed_comma = parse_character(COMMA)
640
+ # repair missing comma
641
+ @output = insert_before_last_whitespace(@output, ',') unless processed_comma
642
+ end
643
+
644
+ skip_ellipsis
645
+
646
+ processed_value = parse_value
647
+ next if processed_value
648
+
649
+ # repair trailing comma
650
+ @output = strip_last_occurrence(@output, ',')
651
+ break
652
+ end
653
+
654
+ if @json[@index] == CLOSING_BRACKET
655
+ @output << ']'
656
+ @index += 1
657
+ else
658
+ # repair missing closing array bracket
659
+ @output = insert_before_last_whitespace(@output, ']')
660
+ end
661
+
662
+ true
663
+ else
664
+ false
665
+ end
666
+ end
667
+
668
+ def prev_non_whitespace_index(start)
669
+ prev = start
670
+ prev -= 1 while prev.positive? && whitespace?(@json[prev])
671
+ prev
672
+ end
673
+
674
+ # Repair concatenated strings like "hello" + "world", change this into "helloworld"
675
+ def parse_concatenated_string
676
+ processed = false
677
+
678
+ parse_whitespace_and_skip_comments
679
+ while @json[@index] == PLUS
680
+ processed = true
681
+ @index += 1
682
+ parse_whitespace_and_skip_comments
683
+
684
+ # repair: remove the end quote of the first string
685
+ @output = strip_last_occurrence(@output, '"', strip_remaining_text: true)
686
+ start = @output.length
687
+ parsed_str = parse_string
688
+ @output = if parsed_str
689
+ # repair: remove the start quote of the second string
690
+ remove_at_index(@output, start, 1)
691
+ else
692
+ # repair: remove the '+' because it is not followed by a string
693
+ insert_before_last_whitespace(@output, '"')
694
+ end
695
+ end
696
+
697
+ processed
698
+ end
699
+
700
+ def repair_number_ending_with_numeric_symbol(start)
701
+ # repair numbers cut off at the end
702
+ # this will only be called when we end after a '.', '-', or 'e' and does not
703
+ # change the number more than it needs to make it valid JSON
704
+ @output << "#{@json[start...@index]}0"
705
+ end
706
+
707
+ # Parse and repair Newline Delimited JSON (NDJSON):
708
+ # multiple JSON objects separated by a newline character
709
+ def parse_newline_delimited_json
710
+ # repair NDJSON
711
+ initial = true
712
+ processed_value = true
713
+ while processed_value
714
+ if initial
715
+ initial = false
716
+ else
717
+ # parse optional comma, insert when missing
718
+ processed_comma = parse_character(COMMA)
719
+ unless processed_comma
720
+ # repair: add missing comma
721
+ @output = insert_before_last_whitespace(@output, ',')
722
+ end
723
+ end
724
+
725
+ processed_value = parse_value
726
+ end
727
+
728
+ unless processed_value
729
+ # repair: remove trailing comma
730
+ @output = strip_last_occurrence(@output, ',')
731
+ end
732
+
733
+ # repair: wrap the output inside array brackets
734
+ @output = "[\n#{@output}\n]"
735
+ end
736
+
737
+ def skip_escape_character
738
+ skip_character(BACKSLASH)
739
+ end
740
+
741
+ def throw_invalid_character(char)
742
+ raise JSONRepairError, "Invalid character #{char.inspect} at index #{@index}"
743
+ end
744
+
745
+ def throw_unexpected_character
746
+ raise JSONRepairError, "Unexpected character #{@json[@index].inspect} at index #{@index}"
747
+ end
748
+
749
+ def throw_unexpected_end
750
+ raise JSONRepairError, 'Unexpected end of json string'
751
+ end
752
+
753
+ def throw_object_key_expected
754
+ raise JSONRepairError, 'Object key expected'
755
+ end
756
+
757
+ def throw_colon_expected
758
+ raise JSONRepairError, 'Colon expected'
759
+ end
760
+
761
+ def throw_invalid_unicode_character
762
+ chars = @json[@index, 6]
763
+ raise JSONRepairError, "Invalid unicode character #{chars.inspect} at index #{@index}"
764
+ end
765
+ end
766
+ end