jsonrepair_test.go

   1package jsonrepair
   2
   3import (
   4	"encoding/json"
   5	"reflect"
   6	"strings"
   7	"testing"
   8)
   9
  10func TestRepairJSON(t *testing.T) {
  11	cases := []struct {
  12		name  string
  13		input string
  14		opts  []Option
  15		want  string
  16	}{
  17		{
  18			name:  "valid_object",
  19			input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
  20			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
  21		},
  22		{
  23			name:  "array_spacing",
  24			input: "{\"employees\":[\"John\", \"Anna\", \"Peter\"]} ",
  25			want:  "{\"employees\": [\"John\", \"Anna\", \"Peter\"]}",
  26		},
  27		{
  28			name:  "colon_in_string",
  29			input: "{\"key\": \"value:value\"}",
  30			want:  "{\"key\": \"value:value\"}",
  31		},
  32		{
  33			name:  "trailing_comma_in_string",
  34			input: "{\"text\": \"The quick brown fox,\"}",
  35			want:  "{\"text\": \"The quick brown fox,\"}",
  36		},
  37		{
  38			name:  "apostrophe_in_string",
  39			input: "{\"text\": \"The quick brown fox won't jump\"}",
  40			want:  "{\"text\": \"The quick brown fox won't jump\"}",
  41		},
  42		{
  43			name:  "missing_brace",
  44			input: "{\"key\": \"\"",
  45			want:  "{\"key\": \"\"}",
  46		},
  47		{
  48			name:  "nested_object",
  49			input: "{\"key1\": {\"key2\": [1, 2, 3]}}",
  50			want:  "{\"key1\": {\"key2\": [1, 2, 3]}}",
  51		},
  52		{
  53			name:  "large_integer",
  54			input: "{\"key\": 12345678901234567890}",
  55			want:  "{\"key\": 12345678901234567890}",
  56		},
  57		{
  58			name:  "unicode_escape",
  59			input: "{\"key\": \"value☺\"}",
  60			want:  "{\"key\": \"value\\u263a\"}",
  61		},
  62		{
  63			name:  "escaped_newline",
  64			input: "{\"key\": \"value\\nvalue\"}",
  65			want:  "{\"key\": \"value\\nvalue\"}",
  66		},
  67	}
  68
  69	for _, tc := range cases {
  70		t.Run(tc.name, func(t *testing.T) {
  71			got, err := RepairJSON(tc.input, tc.opts...)
  72			if err != nil {
  73				t.Fatalf("unexpected error: %v", err)
  74			}
  75			if got != tc.want {
  76				t.Fatalf("got %q want %q", got, tc.want)
  77			}
  78		})
  79	}
  80}
  81
  82func TestRepairJSONMultipleTopLevel(t *testing.T) {
  83	cases := []struct {
  84		name  string
  85		input string
  86		want  string
  87	}{
  88		{
  89			name:  "array_then_object",
  90			input: "[]{}",
  91			want:  "[]",
  92		},
  93		{
  94			name:  "array_then_object_with_value",
  95			input: "[]{\"key\":\"value\"}",
  96			want:  "{\"key\": \"value\"}",
  97		},
  98		{
  99			name:  "object_then_array",
 100			input: "{\"key\":\"value\"}[1,2,3,True]",
 101			want:  "[{\"key\": \"value\"}, [1, 2, 3, true]]",
 102		},
 103		{
 104			name:  "embedded_code_blocks",
 105			input: "lorem ```json {\"key\":\"value\"} ``` ipsum ```json [1,2,3,True] ``` 42",
 106			want:  "[{\"key\": \"value\"}, [1, 2, 3, true]]",
 107		},
 108		{
 109			name:  "array_followed_by_array",
 110			input: "[{\"key\":\"value\"}][{\"key\":\"value_after\"}]",
 111			want:  "[{\"key\": \"value_after\"}]",
 112		},
 113	}
 114
 115	for _, tc := range cases {
 116		t.Run(tc.name, func(t *testing.T) {
 117			got, err := RepairJSON(tc.input)
 118			if err != nil {
 119				t.Fatalf("unexpected error: %v", err)
 120			}
 121			if got != tc.want {
 122				t.Fatalf("got %q want %q", got, tc.want)
 123			}
 124		})
 125	}
 126}
 127
 128func TestRepairJSONEnsureASCII(t *testing.T) {
 129	got, err := RepairJSON("{'test_中国人_ascii':'统一码'}", WithEnsureASCII(false))
 130	if err != nil {
 131		t.Fatalf("unexpected error: %v", err)
 132	}
 133	want := "{\"test_中国人_ascii\": \"统一码\"}"
 134	if got != want {
 135		t.Fatalf("got %q want %q", got, want)
 136	}
 137}
 138
 139func TestRepairJSONStreamStable(t *testing.T) {
 140	cases := []struct {
 141		name  string
 142		input string
 143		opts  []Option
 144		want  string
 145	}{
 146		{
 147			name:  "default_trailing_backslash",
 148			input: "{\"key\": \"val\\",
 149			want:  "{\"key\": \"val\\\\\"}",
 150		},
 151		{
 152			name:  "default_trailing_newline",
 153			input: "{\"key\": \"val\\n",
 154			want:  "{\"key\": \"val\"}",
 155		},
 156		{
 157			name:  "default_split_object",
 158			input: "{\"key\": \"val\\n123,`key2:value2",
 159			want:  "{\"key\": \"val\\n123\", \"key2\": \"value2\"}",
 160		},
 161		{
 162			name:  "stable_trailing_backslash",
 163			input: "{\"key\": \"val\\",
 164			opts:  []Option{WithStreamStable()},
 165			want:  "{\"key\": \"val\"}",
 166		},
 167		{
 168			name:  "stable_trailing_newline",
 169			input: "{\"key\": \"val\\n",
 170			opts:  []Option{WithStreamStable()},
 171			want:  "{\"key\": \"val\\n\"}",
 172		},
 173		{
 174			name:  "stable_split_object",
 175			input: "{\"key\": \"val\\n123,`key2:value2",
 176			opts:  []Option{WithStreamStable()},
 177			want:  "{\"key\": \"val\\n123,`key2:value2\"}",
 178		},
 179		{
 180			name:  "stable_complete_stream",
 181			input: "{\"key\": \"val\\n123,`key2:value2`\"}",
 182			opts:  []Option{WithStreamStable()},
 183			want:  "{\"key\": \"val\\n123,`key2:value2`\"}",
 184		},
 185	}
 186
 187	for _, tc := range cases {
 188		t.Run(tc.name, func(t *testing.T) {
 189			got, err := RepairJSON(tc.input, tc.opts...)
 190			if err != nil {
 191				t.Fatalf("unexpected error: %v", err)
 192			}
 193			if got != tc.want {
 194				t.Fatalf("got %q want %q", got, tc.want)
 195			}
 196		})
 197	}
 198}
 199
 200func TestLoads(t *testing.T) {
 201	cases := []struct {
 202		name  string
 203		input string
 204		want  any
 205	}{
 206		{
 207			name:  "empty_array",
 208			input: "[]",
 209			want:  []any{},
 210		},
 211		{
 212			name:  "empty_object",
 213			input: "{}",
 214			want:  map[string]any{},
 215		},
 216		{
 217			name:  "bools_nulls",
 218			input: "{\"key\": true, \"key2\": false, \"key3\": null}",
 219			want: map[string]any{
 220				"key":  true,
 221				"key2": false,
 222				"key3": nil,
 223			},
 224		},
 225		{
 226			name:  "simple_object",
 227			input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
 228			want: map[string]any{
 229				"name": "John",
 230				"age":  json.Number("30"),
 231				"city": "New York",
 232			},
 233		},
 234		{
 235			name:  "array_numbers",
 236			input: "[1, 2, 3, 4]",
 237			want: []any{
 238				json.Number("1"),
 239				json.Number("2"),
 240				json.Number("3"),
 241				json.Number("4"),
 242			},
 243		},
 244		{
 245			name:  "string_array",
 246			input: "{\"employees\":[\"John\", \"Anna\", \"Peter\"]} ",
 247			want: map[string]any{
 248				"employees": []any{"John", "Anna", "Peter"},
 249			},
 250		},
 251		{
 252			name:  "string_quotes_repaired",
 253			input: "[{\"foo\": \"foo bar \"foobar\" foo bar baz.\", \"tag\": \"#foo-bar-foobar\"}]",
 254			want: []any{
 255				map[string]any{
 256					"foo": "foo bar \"foobar\" foo bar baz.",
 257					"tag": "#foo-bar-foobar",
 258				},
 259			},
 260		},
 261	}
 262
 263	for _, tc := range cases {
 264		t.Run(tc.name, func(t *testing.T) {
 265			got, err := Loads(tc.input)
 266			if err != nil {
 267				t.Fatalf("unexpected error: %v", err)
 268			}
 269			if !reflect.DeepEqual(got, tc.want) {
 270				t.Fatalf("got %#v want %#v", got, tc.want)
 271			}
 272		})
 273	}
 274}
 275
 276func TestRepairJSONSkipJSONLoads(t *testing.T) {
 277	cases := []struct {
 278		name  string
 279		input string
 280		opts  []Option
 281		want  string
 282	}{
 283		{
 284			name:  "valid_json",
 285			input: "{\"key\": true, \"key2\": false, \"key3\": null}",
 286			opts:  []Option{WithSkipJSONLoads()},
 287			want:  "{\"key\": true, \"key2\": false, \"key3\": null}",
 288		},
 289		{
 290			name:  "missing_value",
 291			input: "{\"key\": true, \"key2\": false, \"key3\": }",
 292			opts:  []Option{WithSkipJSONLoads()},
 293			want:  "{\"key\": true, \"key2\": false, \"key3\": \"\"}",
 294		},
 295	}
 296
 297	for _, tc := range cases {
 298		t.Run(tc.name, func(t *testing.T) {
 299			got, err := RepairJSON(tc.input, tc.opts...)
 300			if err != nil {
 301				t.Fatalf("unexpected error: %v", err)
 302			}
 303			if got != tc.want {
 304				t.Fatalf("got %q want %q", got, tc.want)
 305			}
 306		})
 307	}
 308
 309	got, err := Loads("{\"key\": true, \"key2\": false, \"key3\": }", WithSkipJSONLoads())
 310	if err != nil {
 311		t.Fatalf("unexpected error: %v", err)
 312	}
 313	want := map[string]any{
 314		"key":  true,
 315		"key2": false,
 316		"key3": "",
 317	}
 318	if !reflect.DeepEqual(got, want) {
 319		t.Fatalf("got %#v want %#v", got, want)
 320	}
 321}
 322
 323func TestRepairJSONWithLog(t *testing.T) {
 324	cases := []struct {
 325		name      string
 326		input     string
 327		wantValue any
 328		wantLog   []LogEntry
 329	}{
 330		{
 331			name:      "valid_json",
 332			input:     "{}",
 333			wantValue: map[string]any{},
 334			wantLog:   []LogEntry{},
 335		},
 336		{
 337			name:  "missing_quote",
 338			input: "{\"key\": \"value}",
 339			wantValue: map[string]any{
 340				"key": "value",
 341			},
 342			wantLog: []LogEntry{
 343				{
 344					Context: "y\": \"value}",
 345					Text:    "While parsing a string missing the left delimiter in object value context, we found a , or } and we couldn't determine that a right delimiter was present. Stopping here",
 346				},
 347				{
 348					Context: "y\": \"value}",
 349					Text:    "While parsing a string, we missed the closing quote, ignoring",
 350				},
 351			},
 352		},
 353	}
 354
 355	for _, tc := range cases {
 356		t.Run(tc.name, func(t *testing.T) {
 357			gotValue, gotLog, err := RepairJSONWithLog(tc.input)
 358			if err != nil {
 359				t.Fatalf("unexpected error: %v", err)
 360			}
 361			if !reflect.DeepEqual(gotValue, tc.wantValue) {
 362				t.Fatalf("got %#v want %#v", gotValue, tc.wantValue)
 363			}
 364			if !reflect.DeepEqual(gotLog, tc.wantLog) {
 365				t.Fatalf("got %#v want %#v", gotLog, tc.wantLog)
 366			}
 367		})
 368	}
 369}
 370
 371func TestRepairJSONStrict(t *testing.T) {
 372	cases := []struct {
 373		name    string
 374		input   string
 375		opts    []Option
 376		wantErr string
 377	}{
 378		{
 379			name:    "multiple_top_level",
 380			input:   "{\"key\":\"value\"}[\"value\"]",
 381			opts:    []Option{WithStrict()},
 382			wantErr: "multiple top-level JSON elements",
 383		},
 384		{
 385			name:    "duplicate_keys_in_array",
 386			input:   "[{\"key\": \"first\", \"key\": \"second\"}]",
 387			opts:    []Option{WithStrict(), WithSkipJSONLoads()},
 388			wantErr: "duplicate key found",
 389		},
 390		{
 391			name:    "empty_key",
 392			input:   "{\"\" : \"value\"}",
 393			opts:    []Option{WithStrict(), WithSkipJSONLoads()},
 394			wantErr: "empty key found",
 395		},
 396		{
 397			name:    "missing_colon",
 398			input:   "{\"missing\" \"colon\"}",
 399			opts:    []Option{WithStrict()},
 400			wantErr: "missing ':' after key",
 401		},
 402		{
 403			name:    "empty_value",
 404			input:   "{\"key\": , \"key2\": \"value2\"}",
 405			opts:    []Option{WithStrict(), WithSkipJSONLoads()},
 406			wantErr: "parsed value is empty",
 407		},
 408		{
 409			name:    "empty_object_with_extra",
 410			input:   "{\"dangling\"}",
 411			opts:    []Option{WithStrict()},
 412			wantErr: "parsed object is empty",
 413		},
 414		{
 415			name:    "immediate_doubled_quotes",
 416			input:   "{\"key\": \"\"\"\"}",
 417			opts:    []Option{WithStrict()},
 418			wantErr: "doubled quotes followed by another quote",
 419		},
 420		{
 421			name:    "doubled_quotes_followed_by_string",
 422			input:   "{\"key\": \"\" \"value\"}",
 423			opts:    []Option{WithStrict()},
 424			wantErr: "doubled quotes followed by another quote while parsing a string",
 425		},
 426	}
 427
 428	for _, tc := range cases {
 429		t.Run(tc.name, func(t *testing.T) {
 430			_, err := RepairJSON(tc.input, tc.opts...)
 431			if err == nil {
 432				t.Fatalf("expected error")
 433			}
 434			if !strings.Contains(err.Error(), tc.wantErr) {
 435				t.Fatalf("got %q want %q", err.Error(), tc.wantErr)
 436			}
 437		})
 438	}
 439}
 440
 441func TestParseArrayObjects(t *testing.T) {
 442	cases := []struct {
 443		name  string
 444		input string
 445		want  any
 446	}{
 447		{
 448			name:  "empty_array",
 449			input: "[]",
 450			want:  []any{},
 451		},
 452		{
 453			name:  "numbers_array",
 454			input: "[1, 2, 3, 4]",
 455			want: []any{
 456				json.Number("1"),
 457				json.Number("2"),
 458				json.Number("3"),
 459				json.Number("4"),
 460			},
 461		},
 462		{
 463			name:  "unfinished_array",
 464			input: "[",
 465			want:  []any{},
 466		},
 467	}
 468
 469	for _, tc := range cases {
 470		t.Run(tc.name, func(t *testing.T) {
 471			got, err := Loads(tc.input)
 472			if err != nil {
 473				t.Fatalf("unexpected error: %v", err)
 474			}
 475			if !reflect.DeepEqual(got, tc.want) {
 476				t.Fatalf("got %#v want %#v", got, tc.want)
 477			}
 478		})
 479	}
 480}
 481
 482func TestParseArrayEdgeCases(t *testing.T) {
 483	cases := []struct {
 484		name  string
 485		input string
 486		want  string
 487	}{
 488		{
 489			name:  "nested_newlines",
 490			input: "[[1\n\n]",
 491			want:  "[[1]]",
 492		},
 493		{
 494			name:  "array_with_object_end",
 495			input: "[{]",
 496			want:  "[]",
 497		},
 498		{
 499			name:  "just_open_bracket",
 500			input: "[",
 501			want:  "[]",
 502		},
 503		{
 504			name:  "dangling_quote",
 505			input: "[\"",
 506			want:  "[]",
 507		},
 508		{
 509			name:  "just_close_bracket",
 510			input: "]",
 511			want:  "",
 512		},
 513		{
 514			name:  "trailing_comma",
 515			input: "[1, 2, 3,",
 516			want:  "[1, 2, 3]",
 517		},
 518		{
 519			name:  "ellipsis_end",
 520			input: "[1, 2, 3, ...]",
 521			want:  "[1, 2, 3]",
 522		},
 523		{
 524			name:  "ellipsis_middle",
 525			input: "[1, 2, ... , 3]",
 526			want:  "[1, 2, 3]",
 527		},
 528		{
 529			name:  "ellipsis_string",
 530			input: "[1, 2, '...', 3]",
 531			want:  "[1, 2, \"...\", 3]",
 532		},
 533		{
 534			name:  "ellipsis_bools",
 535			input: "[true, false, null, ...]",
 536			want:  "[true, false, null]",
 537		},
 538		{
 539			name:  "missing_commas",
 540			input: "[\"a\" \"b\" \"c\" 1",
 541			want:  "[\"a\", \"b\", \"c\", 1]",
 542		},
 543		{
 544			name:  "object_array_missing_end",
 545			input: "{\"employees\":[\"John\", \"Anna\",",
 546			want:  "{\"employees\": [\"John\", \"Anna\"]}",
 547		},
 548		{
 549			name:  "object_array_missing_quote",
 550			input: "{\"employees\":[\"John\", \"Anna\", \"Peter",
 551			want:  "{\"employees\": [\"John\", \"Anna\", \"Peter\"]}",
 552		},
 553		{
 554			name:  "nested_object_array",
 555			input: "{\"key1\": {\"key2\": [1, 2, 3",
 556			want:  "{\"key1\": {\"key2\": [1, 2, 3]}}",
 557		},
 558		{
 559			name:  "missing_array_quote",
 560			input: "{\"key\": [\"value]}",
 561			want:  "{\"key\": [\"value\"]}",
 562		},
 563		{
 564			name:  "embedded_quotes",
 565			input: "[\"lorem \"ipsum\" sic\"]",
 566			want:  "[\"lorem \\\"ipsum\\\" sic\"]",
 567		},
 568		{
 569			name:  "array_closes_object",
 570			input: "{\"key1\": [\"value1\", \"value2\"}, \"key2\": [\"value3\", \"value4\"]}",
 571			want:  "{\"key1\": [\"value1\", \"value2\"], \"key2\": [\"value3\", \"value4\"]}",
 572		},
 573		{
 574			name:  "rows_missing_bracket",
 575			input: "{\"headers\": [\"A\", \"B\", \"C\"], \"rows\": [[\"r1a\", \"r1b\", \"r1c\"], [\"r2a\", \"r2b\", \"r2c\"], \"r3a\", \"r3b\", \"r3c\"], [\"r4a\", \"r4b\", \"r4c\"], [\"r5a\", \"r5b\", \"r5c\"]]}",
 576			want:  "{\"headers\": [\"A\", \"B\", \"C\"], \"rows\": [[\"r1a\", \"r1b\", \"r1c\"], [\"r2a\", \"r2b\", \"r2c\"], [\"r3a\", \"r3b\", \"r3c\"], [\"r4a\", \"r4b\", \"r4c\"], [\"r5a\", \"r5b\", \"r5c\"]]}",
 577		},
 578		{
 579			name:  "array_missing_commas",
 580			input: "{\"key\": [\"value\" \"value1\" \"value2\"]}",
 581			want:  "{\"key\": [\"value\", \"value1\", \"value2\"]}",
 582		},
 583		{
 584			name:  "array_many_quotes",
 585			input: "{\"key\": [\"lorem \"ipsum\" dolor \"sit\" amet, \"consectetur\" \", \"lorem \"ipsum\" dolor\", \"lorem\"]}",
 586			want:  "{\"key\": [\"lorem \\\"ipsum\\\" dolor \\\"sit\\\" amet, \\\"consectetur\\\" \", \"lorem \\\"ipsum\\\" dolor\", \"lorem\"]}",
 587		},
 588		{
 589			name:  "quoted_key_characters",
 590			input: "{\"k\"e\"y\": \"value\"}",
 591			want:  "{\"k\\\"e\\\"y\": \"value\"}",
 592		},
 593		{
 594			name:  "array_object_mixed",
 595			input: "[\"key\":\"value\"}]",
 596			want:  "[{\"key\": \"value\"}]",
 597		},
 598		{
 599			name:  "array_object_followed_by_literal",
 600			input: "[{\"key\": \"value\", \"key",
 601			want:  "[{\"key\": \"value\"}, [\"key\"]]",
 602		},
 603		{
 604			name:  "set_like_array",
 605			input: "{'key1', 'key2'}",
 606			want:  "[\"key1\", \"key2\"]",
 607		},
 608	}
 609
 610	for _, tc := range cases {
 611		t.Run(tc.name, func(t *testing.T) {
 612			got, err := RepairJSON(tc.input)
 613			if err != nil {
 614				t.Fatalf("unexpected error: %v", err)
 615			}
 616			if got != tc.want {
 617				t.Fatalf("got %q want %q", got, tc.want)
 618			}
 619		})
 620	}
 621}
 622
 623func TestParseArrayMissingQuotes(t *testing.T) {
 624	cases := []struct {
 625		name  string
 626		input string
 627		want  string
 628	}{
 629		{
 630			name:  "value_missing_quote",
 631			input: "[\"value1\" value2\", \"value3\"]",
 632			want:  "[\"value1\", \"value2\", \"value3\"]",
 633		},
 634		{
 635			name:  "comment_token",
 636			input: "{\"bad_one\":[\"Lorem Ipsum\", \"consectetur\" comment\" ], \"good_one\":[ \"elit\", \"sed\", \"tempor\"]}",
 637			want:  "{\"bad_one\": [\"Lorem Ipsum\", \"consectetur\", \"comment\"], \"good_one\": [\"elit\", \"sed\", \"tempor\"]}",
 638		},
 639		{
 640			name:  "comment_token_no_space",
 641			input: "{\"bad_one\": [\"Lorem Ipsum\",\"consectetur\" comment],\"good_one\": [\"elit\",\"sed\",\"tempor\"]}",
 642			want:  "{\"bad_one\": [\"Lorem Ipsum\", \"consectetur\", \"comment\"], \"good_one\": [\"elit\", \"sed\", \"tempor\"]}",
 643		},
 644	}
 645
 646	for _, tc := range cases {
 647		t.Run(tc.name, func(t *testing.T) {
 648			got, err := RepairJSON(tc.input)
 649			if err != nil {
 650				t.Fatalf("unexpected error: %v", err)
 651			}
 652			if got != tc.want {
 653				t.Fatalf("got %q want %q", got, tc.want)
 654			}
 655		})
 656	}
 657}
 658
 659func TestParseComment(t *testing.T) {
 660	cases := []struct {
 661		name  string
 662		input string
 663		want  string
 664	}{
 665		{
 666			name:  "just_slash",
 667			input: "/",
 668			want:  "",
 669		},
 670		{
 671			name:  "block_comment_prefix",
 672			input: "/* comment */ {\"key\": \"value\"}",
 673			want:  "{\"key\": \"value\"}",
 674		},
 675		{
 676			name:  "line_comment",
 677			input: "{ \"key\": { \"key2\": \"value2\" // comment }, \"key3\": \"value3\" }",
 678			want:  "{\"key\": {\"key2\": \"value2\"}, \"key3\": \"value3\"}",
 679		},
 680		{
 681			name:  "hash_comment",
 682			input: "{ \"key\": { \"key2\": \"value2\" # comment }, \"key3\": \"value3\" }",
 683			want:  "{\"key\": {\"key2\": \"value2\"}, \"key3\": \"value3\"}",
 684		},
 685		{
 686			name:  "block_comment_inside",
 687			input: "{ \"key\": { \"key2\": \"value2\" /* comment */ }, \"key3\": \"value3\" }",
 688			want:  "{\"key\": {\"key2\": \"value2\"}, \"key3\": \"value3\"}",
 689		},
 690		{
 691			name:  "array_block_comment",
 692			input: "[ \"value\", /* comment */ \"value2\" ]",
 693			want:  "[\"value\", \"value2\"]",
 694		},
 695		{
 696			name:  "unterminated_comment",
 697			input: "{ \"key\": \"value\" /* comment",
 698			want:  "{\"key\": \"value\"}",
 699		},
 700	}
 701
 702	for _, tc := range cases {
 703		t.Run(tc.name, func(t *testing.T) {
 704			got, err := RepairJSON(tc.input)
 705			if err != nil {
 706				t.Fatalf("unexpected error: %v", err)
 707			}
 708			if got != tc.want {
 709				t.Fatalf("got %q want %q", got, tc.want)
 710			}
 711		})
 712	}
 713}
 714
 715func TestParseNumber(t *testing.T) {
 716	cases := []struct {
 717		name  string
 718		input string
 719		want  any
 720	}{
 721		{
 722			name:  "integer",
 723			input: "1",
 724			want:  json.Number("1"),
 725		},
 726		{
 727			name:  "float",
 728			input: "1.2",
 729			want:  json.Number("1.2"),
 730		},
 731		{
 732			name:  "underscored_integer",
 733			input: "{\"value\": 82_461_110}",
 734			want: map[string]any{
 735				"value": json.Number("82461110"),
 736			},
 737		},
 738		{
 739			name:  "underscored_float",
 740			input: "{\"value\": 1_234.5_6}",
 741			want: map[string]any{
 742				"value": json.Number("1234.56"),
 743			},
 744		},
 745	}
 746
 747	for _, tc := range cases {
 748		t.Run(tc.name, func(t *testing.T) {
 749			got, err := Loads(tc.input)
 750			if err != nil {
 751				t.Fatalf("unexpected error: %v", err)
 752			}
 753			if !reflect.DeepEqual(got, tc.want) {
 754				t.Fatalf("got %#v want %#v", got, tc.want)
 755			}
 756		})
 757	}
 758}
 759
 760func TestParseNumberEdgeCases(t *testing.T) {
 761	cases := []struct {
 762		name  string
 763		input string
 764		want  string
 765	}{
 766		{
 767			name:  "leading_dash",
 768			input: " - { \"test_key\": [\"test_value\", \"test_value2\"] }",
 769			want:  "{\"test_key\": [\"test_value\", \"test_value2\"]}",
 770		},
 771		{
 772			name:  "fraction",
 773			input: "{\"key\": 1/3}",
 774			want:  "{\"key\": \"1/3\"}",
 775		},
 776		{
 777			name:  "leading_decimal",
 778			input: "{\"key\": .25}",
 779			want:  "{\"key\": 0.25}",
 780		},
 781		{
 782			name:  "fraction_in_object",
 783			input: "{\"here\": \"now\", \"key\": 1/3, \"foo\": \"bar\"}",
 784			want:  "{\"here\": \"now\", \"key\": \"1/3\", \"foo\": \"bar\"}",
 785		},
 786		{
 787			name:  "fraction_long",
 788			input: "{\"key\": 12345/67890}",
 789			want:  "{\"key\": \"12345/67890\"}",
 790		},
 791		{
 792			name:  "array_incomplete",
 793			input: "[105,12",
 794			want:  "[105, 12]",
 795		},
 796		{
 797			name:  "object_numbers",
 798			input: "{\"key\", 105,12,",
 799			want:  "{\"key\": \"105,12\"}",
 800		},
 801		{
 802			name:  "fraction_trailing",
 803			input: "{\"key\": 1/3, \"foo\": \"bar\"}",
 804			want:  "{\"key\": \"1/3\", \"foo\": \"bar\"}",
 805		},
 806		{
 807			name:  "dash_number",
 808			input: "{\"key\": 10-20}",
 809			want:  "{\"key\": \"10-20\"}",
 810		},
 811		{
 812			name:  "double_dot",
 813			input: "{\"key\": 1.1.1}",
 814			want:  "{\"key\": \"1.1.1\"}",
 815		},
 816		{
 817			name:  "dash_array",
 818			input: "[- ",
 819			want:  "[]",
 820		},
 821		{
 822			name:  "trailing_decimal",
 823			input: "{\"key\": 1. }",
 824			want:  "{\"key\": 1.0}",
 825		},
 826		{
 827			name:  "exponent",
 828			input: "{\"key\": 1e10 }",
 829			want:  "{\"key\": 10000000000.0}",
 830		},
 831		{
 832			name:  "bad_exponent",
 833			input: "{\"key\": 1e }",
 834			want:  "{\"key\": 1}",
 835		},
 836		{
 837			name:  "non_number_suffix",
 838			input: "{\"key\": 1notanumber }",
 839			want:  "{\"key\": \"1notanumber\"}",
 840		},
 841		{
 842			name:  "uuid_literal",
 843			input: "{\"rowId\": 57eeeeb1-450b-482c-81b9-4be77e95dee2}",
 844			want:  "{\"rowId\": \"57eeeeb1-450b-482c-81b9-4be77e95dee2\"}",
 845		},
 846		{
 847			name:  "array_non_number",
 848			input: "[1, 2notanumber]",
 849			want:  "[1, \"2notanumber\"]",
 850		},
 851	}
 852
 853	for _, tc := range cases {
 854		t.Run(tc.name, func(t *testing.T) {
 855			got, err := RepairJSON(tc.input)
 856			if err != nil {
 857				t.Fatalf("unexpected error: %v", err)
 858			}
 859			if got != tc.want {
 860				t.Fatalf("got %q want %q", got, tc.want)
 861			}
 862		})
 863	}
 864}
 865
 866func TestParseObjectObjects(t *testing.T) {
 867	cases := []struct {
 868		name  string
 869		input string
 870		want  any
 871	}{
 872		{
 873			name:  "empty_object",
 874			input: "{}",
 875			want:  map[string]any{},
 876		},
 877		{
 878			name:  "object_values",
 879			input: "{ \"key\": \"value\", \"key2\": 1, \"key3\": True }",
 880			want: map[string]any{
 881				"key":  "value",
 882				"key2": json.Number("1"),
 883				"key3": true,
 884			},
 885		},
 886		{
 887			name:  "unfinished_object",
 888			input: "{",
 889			want:  map[string]any{},
 890		},
 891		{
 892			name:  "object_with_literals",
 893			input: "{ \"key\": value, \"key2\": 1 \"key3\": null }",
 894			want: map[string]any{
 895				"key":  "value",
 896				"key2": json.Number("1"),
 897				"key3": nil,
 898			},
 899		},
 900	}
 901
 902	for _, tc := range cases {
 903		t.Run(tc.name, func(t *testing.T) {
 904			got, err := Loads(tc.input)
 905			if err != nil {
 906				t.Fatalf("unexpected error: %v", err)
 907			}
 908			if !reflect.DeepEqual(got, tc.want) {
 909				t.Fatalf("got %#v want %#v", got, tc.want)
 910			}
 911		})
 912	}
 913}
 914
 915func TestParseObjectEdgeCases(t *testing.T) {
 916	cases := []struct {
 917		name  string
 918		input string
 919		want  string
 920	}{
 921		{
 922			name:  "empty_trim",
 923			input: "   {  }   ",
 924			want:  "{}",
 925		},
 926		{
 927			name:  "just_open",
 928			input: "{",
 929			want:  "{}",
 930		},
 931		{
 932			name:  "just_close",
 933			input: "}",
 934			want:  "",
 935		},
 936		{
 937			name:  "dangling_quote",
 938			input: "{\"",
 939			want:  "{}",
 940		},
 941		{
 942			name:  "object_array_merge",
 943			input: "{foo: [}",
 944			want:  "{\"foo\": []}",
 945		},
 946		{
 947			name:  "empty_key",
 948			input: "{\"\": \"value\"",
 949			want:  "{\"\": \"value\"}",
 950		},
 951		{
 952			name:  "embedded_quotes",
 953			input: "{\"key\": \"v\"alue\"}",
 954			want:  "{\"key\": \"v\\\"alue\\\"\"}",
 955		},
 956		{
 957			name:  "comment_literal",
 958			input: "{\"value_1\": true, COMMENT \"value_2\": \"data\"}",
 959			want:  "{\"value_1\": true, \"value_2\": \"data\"}",
 960		},
 961		{
 962			name:  "comment_literal_trailing",
 963			input: "{\"value_1\": true, SHOULD_NOT_EXIST \"value_2\": \"data\" AAAA }",
 964			want:  "{\"value_1\": true, \"value_2\": \"data\"}",
 965		},
 966		{
 967			name:  "empty_key_bool",
 968			input: "{\"\" : true, \"key2\": \"value2\"}",
 969			want:  "{\"\": true, \"key2\": \"value2\"}",
 970		},
 971		{
 972			name:  "double_quotes",
 973			input: "{\"\"answer\"\":[{\"\"traits\"\":''Female aged 60+'',\"\"answer1\"\":\"\"5\"\"}]}",
 974			want:  "{\"answer\": [{\"traits\": \"Female aged 60+\", \"answer1\": \"5\"}]}",
 975		},
 976		{
 977			name:  "missing_quotes",
 978			input: "{ \"words\": abcdef\", \"numbers\": 12345\", \"words2\": ghijkl\" }",
 979			want:  "{\"words\": \"abcdef\", \"numbers\": 12345, \"words2\": \"ghijkl\"}",
 980		},
 981		{
 982			name:  "broken_split_key",
 983			input: "{\"number\": 1,\"reason\": \"According...\"\"ans\": \"YES\"}",
 984			want:  "{\"number\": 1, \"reason\": \"According...\", \"ans\": \"YES\"}",
 985		},
 986		{
 987			name:  "nested_braces_in_string",
 988			input: "{ \"a\" : \"{ b\": {} }\" }",
 989			want:  "{\"a\": \"{ b\"}",
 990		},
 991		{
 992			name:  "literal_after_string",
 993			input: "{\"b\": \"xxxxx\" true}",
 994			want:  "{\"b\": \"xxxxx\"}",
 995		},
 996		{
 997			name:  "string_with_quotes",
 998			input: "{\"key\": \"Lorem \"ipsum\" s,\"}",
 999			want:  "{\"key\": \"Lorem \\\"ipsum\\\" s,\"}",
1000		},
1001		{
1002			name:  "literal_list",
1003			input: "{\"lorem\": ipsum, sic, datum.\",}",
1004			want:  "{\"lorem\": \"ipsum, sic, datum.\"}",
1005		},
1006		{
1007			name:  "multiple_keys",
1008			input: "{\"lorem\": sic tamet. \"ipsum\": sic tamet, quick brown fox. \"sic\": ipsum}",
1009			want:  "{\"lorem\": \"sic tamet.\", \"ipsum\": \"sic tamet\", \"sic\": \"ipsum\"}",
1010		},
1011		{
1012			name:  "unfinished_string",
1013			input: "{\"lorem_ipsum\": \"sic tamet, quick brown fox. }",
1014			want:  "{\"lorem_ipsum\": \"sic tamet, quick brown fox.\"}",
1015		},
1016		{
1017			name:  "missing_quotes_keys",
1018			input: "{\"key\":value, \" key2\":\"value2\" }",
1019			want:  "{\"key\": \"value\", \" key2\": \"value2\"}",
1020		},
1021		{
1022			name:  "missing_quotes_key_separator",
1023			input: "{\"key\":value \"key2\":\"value2\" }",
1024			want:  "{\"key\": \"value\", \"key2\": \"value2\"}",
1025		},
1026		{
1027			name:  "single_quotes_braces",
1028			input: "{'text': 'words{words in brackets}more words'}",
1029			want:  "{\"text\": \"words{words in brackets}more words\"}",
1030		},
1031		{
1032			name:  "literal_with_braces",
1033			input: "{text:words{words in brackets}}",
1034			want:  "{\"text\": \"words{words in brackets}\"}",
1035		},
1036		{
1037			name:  "literal_with_braces_suffix",
1038			input: "{text:words{words in brackets}m}",
1039			want:  "{\"text\": \"words{words in brackets}m\"}",
1040		},
1041		{
1042			name:  "trailing_markdown",
1043			input: "{\"key\": \"value, value2\"```",
1044			want:  "{\"key\": \"value, value2\"}",
1045		},
1046		{
1047			name:  "trailing_markdown_quote",
1048			input: "{\"key\": \"value}```",
1049			want:  "{\"key\": \"value\"}",
1050		},
1051		{
1052			name:  "bare_keys",
1053			input: "{key:value,key2:value2}",
1054			want:  "{\"key\": \"value\", \"key2\": \"value2\"}",
1055		},
1056		{
1057			name:  "missing_key_quote",
1058			input: "{\"key:\"value\"}",
1059			want:  "{\"key\": \"value\"}",
1060		},
1061		{
1062			name:  "missing_value_quote",
1063			input: "{\"key:value}",
1064			want:  "{\"key\": \"value\"}",
1065		},
1066		{
1067			name:  "array_double_quotes",
1068			input: "[{\"lorem\": {\"ipsum\": \"sic\"}, \"\"\"\" \"lorem\": {\"ipsum\": \"sic\"}]",
1069			want:  "[{\"lorem\": {\"ipsum\": \"sic\"}}, {\"lorem\": {\"ipsum\": \"sic\"}}]",
1070		},
1071		{
1072			name:  "arrays_in_object",
1073			input: "{ \"key\": [\"arrayvalue\"], [\"arrayvalue1\"], [\"arrayvalue2\"], \"key3\": \"value3\" }",
1074			want:  "{\"key\": [\"arrayvalue\", \"arrayvalue1\", \"arrayvalue2\"], \"key3\": \"value3\"}",
1075		},
1076		{
1077			name:  "nested_arrays_in_object",
1078			input: "{ \"key\": [[1, 2, 3], \"a\", \"b\"], [[4, 5, 6], [7, 8, 9]] }",
1079			want:  "{\"key\": [[1, 2, 3], \"a\", \"b\", [4, 5, 6], [7, 8, 9]]}",
1080		},
1081		{
1082			name:  "array_key_missing_value",
1083			input: "{ \"key\": [\"arrayvalue\"], \"key3\": \"value3\", [\"arrayvalue1\"] }",
1084			want:  "{\"key\": [\"arrayvalue\"], \"key3\": \"value3\", \"arrayvalue1\": \"\"}",
1085		},
1086		{
1087			name:  "json_string_literal",
1088			input: "{\"key\": \"{\\\"key\\\":[\\\"value\\\"],\\\"key2\\\":\"value2\"}\"}",
1089			want:  "{\"key\": \"{\\\"key\\\":[\\\"value\\\"],\\\"key2\\\":\\\"value2\\\"}\"}",
1090		},
1091		{
1092			name:  "empty_value",
1093			input: "{\"key\": , \"key2\": \"value2\"}",
1094			want:  "{\"key\": \"\", \"key2\": \"value2\"}",
1095		},
1096		{
1097			name:  "array_missing_object_end",
1098			input: "{\"array\":[{\"key\": \"value\"], \"key2\": \"value2\"}",
1099			want:  "{\"array\": [{\"key\": \"value\"}], \"key2\": \"value2\"}",
1100		},
1101		{
1102			name:  "object_double_close",
1103			input: "[{\"key\":\"value\"}},{\"key\":\"value\"}]",
1104			want:  "[{\"key\": \"value\"}, {\"key\": \"value\"}]",
1105		},
1106	}
1107
1108	for _, tc := range cases {
1109		t.Run(tc.name, func(t *testing.T) {
1110			got, err := RepairJSON(tc.input)
1111			if err != nil {
1112				t.Fatalf("unexpected error: %v", err)
1113			}
1114			if got != tc.want {
1115				t.Fatalf("got %q want %q", got, tc.want)
1116			}
1117		})
1118	}
1119}
1120
1121func TestParseObjectMergeAtEnd(t *testing.T) {
1122	cases := []struct {
1123		name  string
1124		input string
1125		want  string
1126	}{
1127		{
1128			name:  "merge_key",
1129			input: "{\"key\": \"value\"}, \"key2\": \"value2\"}",
1130			want:  "{\"key\": \"value\", \"key2\": \"value2\"}",
1131		},
1132		{
1133			name:  "merge_empty_value",
1134			input: "{\"key\": \"value\"}, \"key2\": }",
1135			want:  "{\"key\": \"value\", \"key2\": \"\"}",
1136		},
1137		{
1138			name:  "merge_array_discard",
1139			input: "{\"key\": \"value\"}, []",
1140			want:  "{\"key\": \"value\"}",
1141		},
1142		{
1143			name:  "merge_array_keep",
1144			input: "{\"key\": \"value\"}, [\"abc\"]",
1145			want:  "[{\"key\": \"value\"}, [\"abc\"]]",
1146		},
1147		{
1148			name:  "merge_object",
1149			input: "{\"key\": \"value\"}, {}",
1150			want:  "{\"key\": \"value\"}",
1151		},
1152		{
1153			name:  "merge_empty_key",
1154			input: "{\"key\": \"value\"}, \"\" : \"value2\"}",
1155			want:  "{\"key\": \"value\", \"\": \"value2\"}",
1156		},
1157		{
1158			name:  "merge_missing_colon",
1159			input: "{\"key\": \"value\"}, \"key2\" \"value2\"}",
1160			want:  "{\"key\": \"value\", \"key2\": \"value2\"}",
1161		},
1162		{
1163			name:  "merge_multiple_keys",
1164			input: "{\"key1\": \"value1\"}, \"key2\": \"value2\", \"key3\": \"value3\"}",
1165			want:  "{\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"value3\"}",
1166		},
1167	}
1168
1169	for _, tc := range cases {
1170		t.Run(tc.name, func(t *testing.T) {
1171			got, err := RepairJSON(tc.input)
1172			if err != nil {
1173				t.Fatalf("unexpected error: %v", err)
1174			}
1175			if got != tc.want {
1176				t.Fatalf("got %q want %q", got, tc.want)
1177			}
1178		})
1179	}
1180}
1181
1182func TestParseStringBasics(t *testing.T) {
1183	cases := []struct {
1184		name  string
1185		input string
1186		want  string
1187	}{
1188		{
1189			name:  "just_quote",
1190			input: "\"",
1191			want:  "",
1192		},
1193		{
1194			name:  "newline",
1195			input: "\n",
1196			want:  "",
1197		},
1198		{
1199			name:  "space",
1200			input: " ",
1201			want:  "",
1202		},
1203		{
1204			name:  "string_literal",
1205			input: "string",
1206			want:  "",
1207		},
1208		{
1209			name:  "string_before_object",
1210			input: "stringbeforeobject {}",
1211			want:  "{}",
1212		},
1213	}
1214
1215	for _, tc := range cases {
1216		t.Run(tc.name, func(t *testing.T) {
1217			got, err := RepairJSON(tc.input)
1218			if err != nil {
1219				t.Fatalf("unexpected error: %v", err)
1220			}
1221			if got != tc.want {
1222				t.Fatalf("got %q want %q", got, tc.want)
1223			}
1224		})
1225	}
1226}
1227
1228func TestMissingAndMixedQuotes(t *testing.T) {
1229	cases := []struct {
1230		name  string
1231		input string
1232		want  string
1233	}{
1234		{
1235			name:  "mixed_quotes",
1236			input: "{'key': 'string', 'key2': false, \"key3\": null, \"key4\": unquoted}",
1237			want:  "{\"key\": \"string\", \"key2\": false, \"key3\": null, \"key4\": \"unquoted\"}",
1238		},
1239		{
1240			name:  "missing_last_quote",
1241			input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York",
1242			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1243		},
1244		{
1245			name:  "missing_quotes_key",
1246			input: "{\"name\": \"John\", \"age\": 30, city: \"New York\"}",
1247			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1248		},
1249		{
1250			name:  "missing_quotes_value",
1251			input: "{\"name\": \"John\", \"age\": 30, \"city\": New York}",
1252			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1253		},
1254		{
1255			name:  "missing_quotes_value_with_name",
1256			input: "{\"name\": John, \"age\": 30, \"city\": \"New York\"}",
1257			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1258		},
1259		{
1260			name:  "slanted_delimiter",
1261			input: "{“slanted_delimiter”: \"value\"}",
1262			want:  "{\"slanted_delimiter\": \"value\"}",
1263		},
1264		{
1265			name:  "shortened_string",
1266			input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New",
1267			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New\"}",
1268		},
1269		{
1270			name:  "missing_quote_in_middle",
1271			input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York, \"gender\": \"male\"}",
1272			want:  "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\", \"gender\": \"male\"}",
1273		},
1274		{
1275			name:  "comment_literal_in_array",
1276			input: "[{\"key\": \"value\", COMMENT \"notes\": \"lorem \"ipsum\", sic.\" }]",
1277			want:  "[{\"key\": \"value\", \"notes\": \"lorem \\\"ipsum\\\", sic.\"}]",
1278		},
1279		{
1280			name:  "double_quote_prefix",
1281			input: "{\"key\": \"\"value\"}",
1282			want:  "{\"key\": \"value\"}",
1283		},
1284		{
1285			name:  "numeric_key",
1286			input: "{\"key\": \"value\", 5: \"value\"}",
1287			want:  "{\"key\": \"value\", \"5\": \"value\"}",
1288		},
1289		{
1290			name:  "escaped_quotes",
1291			input: "{\"foo\": \"\\\\\"bar\\\\\"\"",
1292			want:  "{\"foo\": \"\\\"bar\\\"\"}",
1293		},
1294		{
1295			name:  "empty_key_prefix",
1296			input: "{\"\" key\":\"val\"",
1297			want:  "{\" key\": \"val\"}",
1298		},
1299		{
1300			name:  "missing_comma",
1301			input: "{\"key\": value \"key2\" : \"value2\" ",
1302			want:  "{\"key\": \"value\", \"key2\": \"value2\"}",
1303		},
1304		{
1305			name:  "ellipsis_quotes",
1306			input: "{\"key\": \"lorem ipsum ... \"sic \" tamet. ...}",
1307			want:  "{\"key\": \"lorem ipsum ... \\\"sic \\\" tamet. ...\"}",
1308		},
1309		{
1310			name:  "trailing_comma",
1311			input: "{\"key\": value , }",
1312			want:  "{\"key\": \"value\"}",
1313		},
1314		{
1315			name:  "comment_in_string",
1316			input: "{\"comment\": \"lorem, \"ipsum\" sic \"tamet\". To improve\"}",
1317			want:  "{\"comment\": \"lorem, \\\"ipsum\\\" sic \\\"tamet\\\". To improve\"}",
1318		},
1319		{
1320			name:  "value_with_embedded_quotes",
1321			input: "{\"key\": \"v\"alu\"e\"} key:",
1322			want:  "{\"key\": \"v\\\"alu\\\"e\"}",
1323		},
1324		{
1325			name:  "value_with_embedded_quote",
1326			input: "{\"key\": \"v\"alue\", \"key2\": \"value2\"}",
1327			want:  "{\"key\": \"v\\\"alue\", \"key2\": \"value2\"}",
1328		},
1329		{
1330			name:  "array_value_with_quote",
1331			input: "[{\"key\": \"v\"alu,e\", \"key2\": \"value2\"}]",
1332			want:  "[{\"key\": \"v\\\"alu,e\", \"key2\": \"value2\"}]",
1333		},
1334	}
1335
1336	for _, tc := range cases {
1337		t.Run(tc.name, func(t *testing.T) {
1338			got, err := RepairJSON(tc.input)
1339			if err != nil {
1340				t.Fatalf("unexpected error: %v", err)
1341			}
1342			if got != tc.want {
1343				t.Fatalf("got %q want %q", got, tc.want)
1344			}
1345		})
1346	}
1347}
1348
1349func TestEscaping(t *testing.T) {
1350	cases := []struct {
1351		name  string
1352		input string
1353		want  string
1354	}{
1355		{
1356			name:  "just_quotes",
1357			input: "'\"'",
1358			want:  "",
1359		},
1360		{
1361			name:  "escaped_chars",
1362			input: "{\"key\": 'string\"\n\t\\\\le'",
1363			want:  "{\"key\": \"string\\\"\\n\\t\\\\le\"}",
1364		},
1365		{
1366			name:  "html_escape",
1367			input: "{\"real_content\": \"Some string: Some other string \\t Some string <a href=\\\"https://domain.com\\\">Some link</a>\"",
1368			want:  "{\"real_content\": \"Some string: Some other string \\t Some string <a href=\\\"https://domain.com\\\">Some link</a>\"}",
1369		},
1370		{
1371			name:  "newline_in_key",
1372			input: "{\"key_1\n\": \"value\"}",
1373			want:  "{\"key_1\": \"value\"}",
1374		},
1375		{
1376			name:  "tab_in_key",
1377			input: "{\"key\t_\": \"value\"}",
1378			want:  "{\"key\\t_\": \"value\"}",
1379		},
1380		{
1381			name:  "unicode_escape",
1382			input: "{\"key\": '\u0076\u0061\u006c\u0075\u0065'}",
1383			want:  "{\"key\": \"value\"}",
1384		},
1385		{
1386			name:  "unicode_escape_skip_loads",
1387			input: "{\"key\": \"\\u0076\\u0061\\u006C\\u0075\\u0065\"}",
1388			want:  "{\"key\": \"value\"}",
1389		},
1390		{
1391			name:  "escaped_single_quote",
1392			input: "{\"key\": \"valu\\'e\"}",
1393			want:  "{\"key\": \"valu'e\"}",
1394		},
1395		{
1396			name:  "escaped_object",
1397			input: "{'key': \"{\\\"key\\\": 1, \\\"key2\\\": 1}\"}",
1398			want:  "{\"key\": \"{\\\"key\\\": 1, \\\"key2\\\": 1}\"}",
1399		},
1400	}
1401
1402	for _, tc := range cases {
1403		t.Run(tc.name, func(t *testing.T) {
1404			opts := []Option{}
1405			if tc.name == "unicode_escape_skip_loads" {
1406				opts = append(opts, WithSkipJSONLoads())
1407			}
1408			got, err := RepairJSON(tc.input, opts...)
1409			if err != nil {
1410				t.Fatalf("unexpected error: %v", err)
1411			}
1412			if got != tc.want {
1413				t.Fatalf("got %q want %q", got, tc.want)
1414			}
1415		})
1416	}
1417}
1418
1419func TestMarkdown(t *testing.T) {
1420	cases := []struct {
1421		name  string
1422		input string
1423		want  string
1424	}{
1425		{
1426			name:  "markdown_link",
1427			input: "{ \"content\": \"[LINK](\"https://google.com\")\" }",
1428			want:  "{\"content\": \"[LINK](\\\"https://google.com\\\")\"}",
1429		},
1430		{
1431			name:  "markdown_incomplete",
1432			input: "{ \"content\": \"[LINK](\" }",
1433			want:  "{\"content\": \"[LINK](\"}",
1434		},
1435		{
1436			name:  "markdown_in_object",
1437			input: "{ \"content\": \"[LINK](\", \"key\": true }",
1438			want:  "{\"content\": \"[LINK](\", \"key\": true}",
1439		},
1440	}
1441
1442	for _, tc := range cases {
1443		t.Run(tc.name, func(t *testing.T) {
1444			got, err := RepairJSON(tc.input)
1445			if err != nil {
1446				t.Fatalf("unexpected error: %v", err)
1447			}
1448			if got != tc.want {
1449				t.Fatalf("got %q want %q", got, tc.want)
1450			}
1451		})
1452	}
1453}
1454
1455func TestLeadingTrailingCharacters(t *testing.T) {
1456	cases := []struct {
1457		name  string
1458		input string
1459		want  string
1460	}{
1461		{
1462			name:  "wrapped_markdown",
1463			input: "````{ \"key\": \"value\" }```",
1464			want:  "{\"key\": \"value\"}",
1465		},
1466		{
1467			name:  "trailing_markdown_block",
1468			input: "{    \"a\": \"\",    \"b\": [ { \"c\": 1} ] \n}```",
1469			want:  "{\"a\": \"\", \"b\": [{\"c\": 1}]}",
1470		},
1471		{
1472			name:  "preface_text",
1473			input: "Based on the information extracted, here is the filled JSON output: ```json { 'a': 'b' } ```",
1474			want:  "{\"a\": \"b\"}",
1475		},
1476		{
1477			name:  "multiline_markdown",
1478			input: "\n                       The next 64 elements are:\n                       ```json\n                       { \"key\": \"value\" }\n                       ```",
1479			want:  "{\"key\": \"value\"}",
1480		},
1481	}
1482
1483	for _, tc := range cases {
1484		t.Run(tc.name, func(t *testing.T) {
1485			got, err := RepairJSON(tc.input)
1486			if err != nil {
1487				t.Fatalf("unexpected error: %v", err)
1488			}
1489			if got != tc.want {
1490				t.Fatalf("got %q want %q", got, tc.want)
1491			}
1492		})
1493	}
1494}
1495
1496func TestStringJSONLLMBlock(t *testing.T) {
1497	cases := []struct {
1498		name  string
1499		input string
1500		want  string
1501	}{
1502		{
1503			name:  "backticks",
1504			input: "{\"key\": \"``\"",
1505			want:  "{\"key\": \"``\"}",
1506		},
1507		{
1508			name:  "backticks_json",
1509			input: "{\"key\": \"```json\"",
1510			want:  "{\"key\": \"```json\"}",
1511		},
1512		{
1513			name:  "json_block_inside_string",
1514			input: "{\"key\": \"```json {\"key\": [{\"key1\": 1},{\"key2\": 2}]}```\"}",
1515			want:  "{\"key\": {\"key\": [{\"key1\": 1}, {\"key2\": 2}]}}",
1516		},
1517		{
1518			name:  "response_prefix",
1519			input: "{\"response\": \"```json{}\"",
1520			want:  "{\"response\": \"```json{}\"}",
1521		},
1522	}
1523
1524	for _, tc := range cases {
1525		t.Run(tc.name, func(t *testing.T) {
1526			got, err := RepairJSON(tc.input)
1527			if err != nil {
1528				t.Fatalf("unexpected error: %v", err)
1529			}
1530			if got != tc.want {
1531				t.Fatalf("got %q want %q", got, tc.want)
1532			}
1533		})
1534	}
1535}
1536
1537func TestParseBooleanOrNull(t *testing.T) {
1538	loadCases := []struct {
1539		name  string
1540		input string
1541		want  any
1542	}{
1543		{
1544			name:  "upper_true",
1545			input: "True",
1546			want:  "",
1547		},
1548		{
1549			name:  "upper_false",
1550			input: "False",
1551			want:  "",
1552		},
1553		{
1554			name:  "upper_null",
1555			input: "Null",
1556			want:  "",
1557		},
1558		{
1559			name:  "lower_true",
1560			input: "true",
1561			want:  true,
1562		},
1563		{
1564			name:  "lower_false",
1565			input: "false",
1566			want:  false,
1567		},
1568		{
1569			name:  "lower_null",
1570			input: "null",
1571			want:  nil,
1572		},
1573	}
1574
1575	for _, tc := range loadCases {
1576		t.Run(tc.name, func(t *testing.T) {
1577			got, err := Loads(tc.input)
1578			if err != nil {
1579				t.Fatalf("unexpected error: %v", err)
1580			}
1581			if !reflect.DeepEqual(got, tc.want) {
1582				t.Fatalf("got %#v want %#v", got, tc.want)
1583			}
1584		})
1585	}
1586
1587	stringCases := []struct {
1588		name  string
1589		input string
1590		want  string
1591	}{
1592		{
1593			name:  "bools_in_object",
1594			input: "  {\"key\": true, \"key2\": false, \"key3\": null}",
1595			want:  "{\"key\": true, \"key2\": false, \"key3\": null}",
1596		},
1597		{
1598			name:  "uppercase_bools",
1599			input: "{\"key\": TRUE, \"key2\": FALSE, \"key3\": Null}   ",
1600			want:  "{\"key\": true, \"key2\": false, \"key3\": null}",
1601		},
1602	}
1603
1604	for _, tc := range stringCases {
1605		t.Run(tc.name, func(t *testing.T) {
1606			got, err := RepairJSON(tc.input)
1607			if err != nil {
1608				t.Fatalf("unexpected error: %v", err)
1609			}
1610			if got != tc.want {
1611				t.Fatalf("got %q want %q", got, tc.want)
1612			}
1613		})
1614	}
1615}