buffer
3 months ago
core.js
3 months ago
core.js.map
3 months ago
stack.js
3 months ago
stack.js.map
3 months ago
stream.js
3 months ago
stream.js.map
3 months ago
core.js
730 lines
| 1 | import { JSONRepairError } from '../utils/JSONRepairError.js'; |
| 2 | import { codeAsterisk, codeBackslash, codeCloseParenthesis, codeClosingBrace, codeClosingBracket, codeColon, codeComma, codeDot, codeDoubleQuote, codeLowercaseE, codeMinus, codeNewline, codeOpeningBrace, codeOpeningBracket, codeOpenParenthesis, codePlus, codeSemicolon, codeSlash, codeUppercaseE, isControlCharacter, isDelimiter, isDelimiterExceptSlash, isDigit, isDoubleQuote, isDoubleQuoteLike, isFunctionName, isHex, isQuote, isSingleQuote, isSingleQuoteLike, isSpecialWhitespace, isStartOfValue, isValidStringCharacter, isWhitespace } from '../utils/stringUtils.js'; |
| 3 | import { createInputBuffer } from './buffer/InputBuffer.js'; |
| 4 | import { createOutputBuffer } from './buffer/OutputBuffer.js'; |
| 5 | import { Caret, createStack, StackType } from './stack.js'; |
| 6 | const controlCharacters = { |
| 7 | '\b': '\\b', |
| 8 | '\f': '\\f', |
| 9 | '\n': '\\n', |
| 10 | '\r': '\\r', |
| 11 | '\t': '\\t' |
| 12 | }; |
| 13 | |
| 14 | // map with all escape characters |
| 15 | const escapeCharacters = { |
| 16 | '"': '"', |
| 17 | '\\': '\\', |
| 18 | '/': '/', |
| 19 | b: '\b', |
| 20 | f: '\f', |
| 21 | n: '\n', |
| 22 | r: '\r', |
| 23 | t: '\t' |
| 24 | // note that \u is handled separately in parseString() |
| 25 | }; |
| 26 | export function jsonrepairCore(_ref) { |
| 27 | let { |
| 28 | onData, |
| 29 | bufferSize = 65536, |
| 30 | chunkSize = 65536 |
| 31 | } = _ref; |
| 32 | const input = createInputBuffer(); |
| 33 | const output = createOutputBuffer({ |
| 34 | write: onData, |
| 35 | bufferSize, |
| 36 | chunkSize |
| 37 | }); |
| 38 | let i = 0; |
| 39 | let iFlushed = 0; |
| 40 | const stack = createStack(); |
| 41 | function flushInputBuffer() { |
| 42 | while (iFlushed < i - bufferSize - chunkSize) { |
| 43 | iFlushed += chunkSize; |
| 44 | input.flush(iFlushed); |
| 45 | } |
| 46 | } |
| 47 | function transform(chunk) { |
| 48 | input.push(chunk); |
| 49 | while (i < input.currentLength() - bufferSize && parse()) { |
| 50 | // loop until there is nothing more to process |
| 51 | } |
| 52 | flushInputBuffer(); |
| 53 | } |
| 54 | function flush() { |
| 55 | input.close(); |
| 56 | while (parse()) { |
| 57 | // loop until there is nothing more to process |
| 58 | } |
| 59 | output.flush(); |
| 60 | } |
| 61 | function parse() { |
| 62 | parseWhitespaceAndSkipComments(); |
| 63 | switch (stack.type) { |
| 64 | case StackType.object: |
| 65 | { |
| 66 | switch (stack.caret) { |
| 67 | case Caret.beforeKey: |
| 68 | return skipEllipsis() || parseObjectKey() || parseUnexpectedColon() || parseRepairTrailingComma() || parseRepairObjectEndOrComma(); |
| 69 | case Caret.beforeValue: |
| 70 | return parseValue() || parseRepairMissingObjectValue(); |
| 71 | case Caret.afterValue: |
| 72 | return parseObjectComma() || parseObjectEnd() || parseRepairObjectEndOrComma(); |
| 73 | default: |
| 74 | return false; |
| 75 | } |
| 76 | } |
| 77 | case StackType.array: |
| 78 | { |
| 79 | switch (stack.caret) { |
| 80 | case Caret.beforeValue: |
| 81 | return skipEllipsis() || parseValue() || parseRepairTrailingComma() || parseRepairArrayEnd(); |
| 82 | case Caret.afterValue: |
| 83 | return parseArrayComma() || parseArrayEnd() || parseRepairMissingComma() || parseRepairArrayEnd(); |
| 84 | default: |
| 85 | return false; |
| 86 | } |
| 87 | } |
| 88 | case StackType.ndJson: |
| 89 | { |
| 90 | switch (stack.caret) { |
| 91 | case Caret.beforeValue: |
| 92 | return parseValue() || parseRepairTrailingComma(); |
| 93 | case Caret.afterValue: |
| 94 | return parseArrayComma() || parseRepairMissingComma() || parseRepairNdJsonEnd(); |
| 95 | default: |
| 96 | return false; |
| 97 | } |
| 98 | } |
| 99 | case StackType.functionCall: |
| 100 | { |
| 101 | switch (stack.caret) { |
| 102 | case Caret.beforeValue: |
| 103 | return parseValue(); |
| 104 | case Caret.afterValue: |
| 105 | return parseFunctionCallEnd(); |
| 106 | default: |
| 107 | return false; |
| 108 | } |
| 109 | } |
| 110 | case StackType.root: |
| 111 | { |
| 112 | switch (stack.caret) { |
| 113 | case Caret.beforeValue: |
| 114 | return parseValue() || parseUnexpectedEnd(); |
| 115 | case Caret.afterValue: |
| 116 | return parseRootEnd(); |
| 117 | default: |
| 118 | return false; |
| 119 | } |
| 120 | } |
| 121 | default: |
| 122 | return false; |
| 123 | } |
| 124 | } |
| 125 | function parseValue() { |
| 126 | return parseObjectStart() || parseArrayStart() || parseString() || parseNumber() || parseKeywords() || parseRepairUnquotedString(); |
| 127 | } |
| 128 | function parseObjectStart() { |
| 129 | if (parseCharacter(codeOpeningBrace)) { |
| 130 | parseWhitespaceAndSkipComments(); |
| 131 | skipEllipsis(); |
| 132 | if (skipCharacter(codeComma)) { |
| 133 | parseWhitespaceAndSkipComments(); |
| 134 | } |
| 135 | if (parseCharacter(codeClosingBrace)) { |
| 136 | return stack.update(Caret.afterValue); |
| 137 | } |
| 138 | return stack.push(StackType.object, Caret.beforeKey); |
| 139 | } |
| 140 | return false; |
| 141 | } |
| 142 | function parseArrayStart() { |
| 143 | if (parseCharacter(codeOpeningBracket)) { |
| 144 | parseWhitespaceAndSkipComments(); |
| 145 | skipEllipsis(); |
| 146 | if (skipCharacter(codeComma)) { |
| 147 | parseWhitespaceAndSkipComments(); |
| 148 | } |
| 149 | if (parseCharacter(codeClosingBracket)) { |
| 150 | return stack.update(Caret.afterValue); |
| 151 | } |
| 152 | return stack.push(StackType.array, Caret.beforeValue); |
| 153 | } |
| 154 | return false; |
| 155 | } |
| 156 | function parseRepairUnquotedString() { |
| 157 | const unquotedStringEnd = findNextDelimiter(); |
| 158 | if (unquotedStringEnd !== null) { |
| 159 | const symbol = input.substring(i, unquotedStringEnd); |
| 160 | i = unquotedStringEnd; |
| 161 | if (skipCharacter(codeOpenParenthesis) && isFunctionName(symbol.trim())) { |
| 162 | // A MongoDB function call like NumberLong("2") |
| 163 | // Or a JSONP function call like callback({...}); |
| 164 | // we strip the function call |
| 165 | |
| 166 | return stack.push(StackType.functionCall, Caret.beforeValue); |
| 167 | } |
| 168 | output.push(symbol === 'undefined' ? 'null' : JSON.stringify(symbol)); |
| 169 | if (input.charCodeAt(i) === codeDoubleQuote) { |
| 170 | // we had a missing start quote, but now we encountered the end quote, so we can skip that one |
| 171 | i++; |
| 172 | } |
| 173 | return stack.update(Caret.afterValue); |
| 174 | } |
| 175 | return false; |
| 176 | } |
| 177 | function parseRepairMissingObjectValue() { |
| 178 | // repair missing object value |
| 179 | output.push('null'); |
| 180 | return stack.update(Caret.afterValue); |
| 181 | } |
| 182 | function parseRepairTrailingComma() { |
| 183 | // repair trailing comma |
| 184 | if (output.endsWithIgnoringWhitespace(',')) { |
| 185 | output.stripLastOccurrence(','); |
| 186 | return stack.update(Caret.afterValue); |
| 187 | } |
| 188 | return false; |
| 189 | } |
| 190 | function parseUnexpectedColon() { |
| 191 | if (input.charCodeAt(i) === codeColon) { |
| 192 | throwObjectKeyExpected(); |
| 193 | } |
| 194 | return false; |
| 195 | } |
| 196 | function parseUnexpectedEnd() { |
| 197 | if (input.isEnd(i)) { |
| 198 | throwUnexpectedEnd(); |
| 199 | } else { |
| 200 | throwUnexpectedCharacter(); |
| 201 | } |
| 202 | return false; |
| 203 | } |
| 204 | function parseObjectKey() { |
| 205 | const parsedKey = parseString() || parseUnquotedKey(); |
| 206 | if (parsedKey) { |
| 207 | parseWhitespaceAndSkipComments(); |
| 208 | if (parseCharacter(codeColon)) { |
| 209 | // expect a value after the : |
| 210 | return stack.update(Caret.beforeValue); |
| 211 | } |
| 212 | const truncatedText = input.isEnd(i); |
| 213 | if (isStartOfValue(input.charAt(i)) || truncatedText) { |
| 214 | // repair missing colon |
| 215 | output.insertBeforeLastWhitespace(':'); |
| 216 | return stack.update(Caret.beforeValue); |
| 217 | } |
| 218 | throwColonExpected(); |
| 219 | } |
| 220 | return false; |
| 221 | } |
| 222 | function parseObjectComma() { |
| 223 | if (parseCharacter(codeComma)) { |
| 224 | return stack.update(Caret.beforeKey); |
| 225 | } |
| 226 | return false; |
| 227 | } |
| 228 | function parseObjectEnd() { |
| 229 | if (parseCharacter(codeClosingBrace)) { |
| 230 | return stack.pop(); |
| 231 | } |
| 232 | return false; |
| 233 | } |
| 234 | function parseRepairObjectEndOrComma() { |
| 235 | // repair missing object end and trailing comma |
| 236 | if (input.charAt(i) === '{') { |
| 237 | output.stripLastOccurrence(','); |
| 238 | output.insertBeforeLastWhitespace('}'); |
| 239 | return stack.pop(); |
| 240 | } |
| 241 | |
| 242 | // repair missing comma |
| 243 | if (!input.isEnd(i) && isStartOfValue(input.charAt(i))) { |
| 244 | output.insertBeforeLastWhitespace(','); |
| 245 | return stack.update(Caret.beforeKey); |
| 246 | } |
| 247 | |
| 248 | // repair missing closing brace |
| 249 | output.insertBeforeLastWhitespace('}'); |
| 250 | return stack.pop(); |
| 251 | } |
| 252 | function parseArrayComma() { |
| 253 | if (parseCharacter(codeComma)) { |
| 254 | return stack.update(Caret.beforeValue); |
| 255 | } |
| 256 | return false; |
| 257 | } |
| 258 | function parseArrayEnd() { |
| 259 | if (parseCharacter(codeClosingBracket)) { |
| 260 | return stack.pop(); |
| 261 | } |
| 262 | return false; |
| 263 | } |
| 264 | function parseRepairMissingComma() { |
| 265 | // repair missing comma |
| 266 | if (!input.isEnd(i) && isStartOfValue(input.charAt(i))) { |
| 267 | output.insertBeforeLastWhitespace(','); |
| 268 | return stack.update(Caret.beforeValue); |
| 269 | } |
| 270 | return false; |
| 271 | } |
| 272 | function parseRepairArrayEnd() { |
| 273 | // repair missing closing bracket |
| 274 | output.insertBeforeLastWhitespace(']'); |
| 275 | return stack.pop(); |
| 276 | } |
| 277 | function parseRepairNdJsonEnd() { |
| 278 | if (input.isEnd(i)) { |
| 279 | output.push('\n]'); |
| 280 | return stack.pop(); |
| 281 | } else { |
| 282 | throwUnexpectedEnd(); |
| 283 | return false; // just to make TS happy |
| 284 | } |
| 285 | } |
| 286 | function parseFunctionCallEnd() { |
| 287 | if (skipCharacter(codeCloseParenthesis)) { |
| 288 | skipCharacter(codeSemicolon); |
| 289 | } |
| 290 | return stack.pop(); |
| 291 | } |
| 292 | function parseRootEnd() { |
| 293 | const parsedComma = parseCharacter(codeComma); |
| 294 | parseWhitespaceAndSkipComments(); |
| 295 | if (isStartOfValue(input.charAt(i)) && (output.endsWithIgnoringWhitespace(',') || output.endsWithIgnoringWhitespace('\n'))) { |
| 296 | // start of a new value after end of the root level object: looks like |
| 297 | // newline delimited JSON -> turn into a root level array |
| 298 | if (!parsedComma) { |
| 299 | // repair missing comma |
| 300 | output.insertBeforeLastWhitespace(','); |
| 301 | } |
| 302 | output.unshift('[\n'); |
| 303 | return stack.push(StackType.ndJson, Caret.beforeValue); |
| 304 | } |
| 305 | if (parsedComma) { |
| 306 | // repair: remove trailing comma |
| 307 | output.stripLastOccurrence(','); |
| 308 | return stack.update(Caret.afterValue); |
| 309 | } |
| 310 | |
| 311 | // repair redundant end braces and brackets |
| 312 | while (input.charCodeAt(i) === codeClosingBrace || input.charCodeAt(i) === codeClosingBracket) { |
| 313 | i++; |
| 314 | parseWhitespaceAndSkipComments(); |
| 315 | } |
| 316 | if (!input.isEnd(i)) { |
| 317 | throwUnexpectedCharacter(); |
| 318 | } |
| 319 | return false; |
| 320 | } |
| 321 | function parseWhitespaceAndSkipComments() { |
| 322 | const start = i; |
| 323 | let changed = parseWhitespace(); |
| 324 | do { |
| 325 | changed = parseComment(); |
| 326 | if (changed) { |
| 327 | changed = parseWhitespace(); |
| 328 | } |
| 329 | } while (changed); |
| 330 | return i > start; |
| 331 | } |
| 332 | function parseWhitespace() { |
| 333 | let whitespace = ''; |
| 334 | let normal; |
| 335 | while ((normal = isWhitespace(input.charCodeAt(i))) || isSpecialWhitespace(input.charCodeAt(i))) { |
| 336 | if (normal) { |
| 337 | whitespace += input.charAt(i); |
| 338 | } else { |
| 339 | // repair special whitespace |
| 340 | whitespace += ' '; |
| 341 | } |
| 342 | i++; |
| 343 | } |
| 344 | if (whitespace.length > 0) { |
| 345 | output.push(whitespace); |
| 346 | return true; |
| 347 | } |
| 348 | return false; |
| 349 | } |
| 350 | function parseComment() { |
| 351 | // find a block comment '/* ... */' |
| 352 | if (input.charCodeAt(i) === codeSlash && input.charCodeAt(i + 1) === codeAsterisk) { |
| 353 | // repair block comment by skipping it |
| 354 | while (!input.isEnd(i) && !atEndOfBlockComment(i)) { |
| 355 | i++; |
| 356 | } |
| 357 | i += 2; |
| 358 | return true; |
| 359 | } |
| 360 | |
| 361 | // find a line comment '// ...' |
| 362 | if (input.charCodeAt(i) === codeSlash && input.charCodeAt(i + 1) === codeSlash) { |
| 363 | // repair line comment by skipping it |
| 364 | while (!input.isEnd(i) && input.charCodeAt(i) !== codeNewline) { |
| 365 | i++; |
| 366 | } |
| 367 | return true; |
| 368 | } |
| 369 | return false; |
| 370 | } |
| 371 | function parseCharacter(code) { |
| 372 | if (input.charCodeAt(i) === code) { |
| 373 | output.push(input.charAt(i)); |
| 374 | i++; |
| 375 | return true; |
| 376 | } |
| 377 | return false; |
| 378 | } |
| 379 | function skipCharacter(code) { |
| 380 | if (input.charCodeAt(i) === code) { |
| 381 | i++; |
| 382 | return true; |
| 383 | } |
| 384 | return false; |
| 385 | } |
| 386 | function skipEscapeCharacter() { |
| 387 | return skipCharacter(codeBackslash); |
| 388 | } |
| 389 | |
| 390 | /** |
| 391 | * Skip ellipsis like "[1,2,3,...]" or "[1,2,3,...,9]" or "[...,7,8,9]" |
| 392 | * or a similar construct in objects. |
| 393 | */ |
| 394 | function skipEllipsis() { |
| 395 | parseWhitespaceAndSkipComments(); |
| 396 | if (input.charCodeAt(i) === codeDot && input.charCodeAt(i + 1) === codeDot && input.charCodeAt(i + 2) === codeDot) { |
| 397 | // repair: remove the ellipsis (three dots) and optionally a comma |
| 398 | i += 3; |
| 399 | parseWhitespaceAndSkipComments(); |
| 400 | skipCharacter(codeComma); |
| 401 | return true; |
| 402 | } |
| 403 | return false; |
| 404 | } |
| 405 | |
| 406 | /** |
| 407 | * Parse a string enclosed by double quotes "...". Can contain escaped quotes |
| 408 | * Repair strings enclosed in single quotes or special quotes |
| 409 | * Repair an escaped string |
| 410 | * |
| 411 | * The function can run in two stages: |
| 412 | * - First, it assumes the string has a valid end quote |
| 413 | * - If it turns out that the string does not have a valid end quote followed |
| 414 | * by a delimiter (which should be the case), the function runs again in a |
| 415 | * more conservative way, stopping the string at the first next delimiter |
| 416 | * and fixing the string by inserting a quote there. |
| 417 | */ |
| 418 | function parseString() { |
| 419 | let stopAtDelimiter = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; |
| 420 | let skipEscapeChars = input.charCodeAt(i) === codeBackslash; |
| 421 | if (skipEscapeChars) { |
| 422 | // repair: remove the first escape character |
| 423 | i++; |
| 424 | skipEscapeChars = true; |
| 425 | } |
| 426 | if (isQuote(input.charCodeAt(i))) { |
| 427 | // double quotes are correct JSON, |
| 428 | // single quotes come from JavaScript for example, we assume it will have a correct single end quote too |
| 429 | // otherwise, we will match any double-quote-like start with a double-quote-like end, |
| 430 | // or any single-quote-like start with a single-quote-like end |
| 431 | const isEndQuote = isDoubleQuote(input.charCodeAt(i)) ? isDoubleQuote : isSingleQuote(input.charCodeAt(i)) ? isSingleQuote : isSingleQuoteLike(input.charCodeAt(i)) ? isSingleQuoteLike : isDoubleQuoteLike; |
| 432 | const iBefore = i; |
| 433 | const oBefore = output.length(); |
| 434 | output.push('"'); |
| 435 | i++; |
| 436 | while (true) { |
| 437 | if (input.isEnd(i)) { |
| 438 | // end of text, we have a missing quote somewhere |
| 439 | |
| 440 | const iPrev = prevNonWhitespaceIndex(i - 1); |
| 441 | if (!stopAtDelimiter && isDelimiter(input.charAt(iPrev))) { |
| 442 | // if the text ends with a delimiter, like ["hello], |
| 443 | // so the missing end quote should be inserted before this delimiter |
| 444 | // retry parsing the string, stopping at the first next delimiter |
| 445 | i = iBefore; |
| 446 | output.remove(oBefore); |
| 447 | return parseString(true); |
| 448 | } |
| 449 | |
| 450 | // repair missing quote |
| 451 | output.insertBeforeLastWhitespace('"'); |
| 452 | return stack.update(Caret.afterValue); |
| 453 | } else if (isEndQuote(input.charCodeAt(i))) { |
| 454 | // end quote |
| 455 | // let us check what is before and after the quote to verify whether this is a legit end quote |
| 456 | const iQuote = i; |
| 457 | const oQuote = output.length(); |
| 458 | output.push('"'); |
| 459 | i++; |
| 460 | parseWhitespaceAndSkipComments(); |
| 461 | if (stopAtDelimiter || input.isEnd(i) || isDelimiter(input.charAt(i)) || isQuote(input.charCodeAt(i)) || isDigit(input.charCodeAt(i))) { |
| 462 | // The quote is followed by the end of the text, a delimiter, or a next value |
| 463 | // so the quote is indeed the end of the string |
| 464 | parseConcatenatedString(); |
| 465 | return stack.update(Caret.afterValue); |
| 466 | } |
| 467 | if (isDelimiter(input.charAt(prevNonWhitespaceIndex(iQuote - 1)))) { |
| 468 | // This is not the right end quote: it is preceded by a delimiter, |
| 469 | // and NOT followed by a delimiter. So, there is an end quote missing |
| 470 | // parse the string again and then stop at the first next delimiter |
| 471 | i = iBefore; |
| 472 | output.remove(oBefore); |
| 473 | return parseString(true); |
| 474 | } |
| 475 | |
| 476 | // revert to right after the quote but before any whitespace, and continue parsing the string |
| 477 | output.remove(oQuote + 1); |
| 478 | i = iQuote + 1; |
| 479 | |
| 480 | // repair unescaped quote |
| 481 | output.insertAt(oQuote, '\\'); |
| 482 | } else if (stopAtDelimiter && isDelimiter(input.charAt(i))) { |
| 483 | // we're in the mode to stop the string at the first delimiter |
| 484 | // because there is an end quote missing |
| 485 | |
| 486 | // repair missing quote |
| 487 | output.insertBeforeLastWhitespace('"'); |
| 488 | parseConcatenatedString(); |
| 489 | return stack.update(Caret.afterValue); |
| 490 | } else if (input.charCodeAt(i) === codeBackslash) { |
| 491 | // handle escaped content like \n or \u2605 |
| 492 | const char = input.charAt(i + 1); |
| 493 | const escapeChar = escapeCharacters[char]; |
| 494 | if (escapeChar !== undefined) { |
| 495 | output.push(input.substring(i, i + 2)); |
| 496 | i += 2; |
| 497 | } else if (char === 'u') { |
| 498 | let j = 2; |
| 499 | while (j < 6 && isHex(input.charCodeAt(i + j))) { |
| 500 | j++; |
| 501 | } |
| 502 | if (j === 6) { |
| 503 | output.push(input.substring(i, i + 6)); |
| 504 | i += 6; |
| 505 | } else if (input.isEnd(i + j)) { |
| 506 | // repair invalid or truncated unicode char at the end of the text |
| 507 | // by removing the unicode char and ending the string here |
| 508 | i += j; |
| 509 | } else { |
| 510 | throwInvalidUnicodeCharacter(); |
| 511 | } |
| 512 | } else { |
| 513 | // repair invalid escape character: remove it |
| 514 | output.push(char); |
| 515 | i += 2; |
| 516 | } |
| 517 | } else { |
| 518 | // handle regular characters |
| 519 | const char = input.charAt(i); |
| 520 | const code = char.charCodeAt(0); |
| 521 | if (code === codeDoubleQuote && input.charCodeAt(i - 1) !== codeBackslash) { |
| 522 | // repair unescaped double quote |
| 523 | output.push('\\' + char); |
| 524 | i++; |
| 525 | } else if (isControlCharacter(code)) { |
| 526 | // unescaped control character |
| 527 | output.push(controlCharacters[char]); |
| 528 | i++; |
| 529 | } else { |
| 530 | if (!isValidStringCharacter(code)) { |
| 531 | throwInvalidCharacter(char); |
| 532 | } |
| 533 | output.push(char); |
| 534 | i++; |
| 535 | } |
| 536 | } |
| 537 | if (skipEscapeChars) { |
| 538 | // repair: skipped escape character (nothing to do) |
| 539 | skipEscapeCharacter(); |
| 540 | } |
| 541 | } |
| 542 | } |
| 543 | return false; |
| 544 | } |
| 545 | |
| 546 | /** |
| 547 | * Repair concatenated strings like "hello" + "world", change this into "helloworld" |
| 548 | */ |
| 549 | function parseConcatenatedString() { |
| 550 | let parsed = false; |
| 551 | parseWhitespaceAndSkipComments(); |
| 552 | while (input.charCodeAt(i) === codePlus) { |
| 553 | parsed = true; |
| 554 | i++; |
| 555 | parseWhitespaceAndSkipComments(); |
| 556 | |
| 557 | // repair: remove the end quote of the first string |
| 558 | output.stripLastOccurrence('"', true); |
| 559 | const start = output.length(); |
| 560 | const parsedStr = parseString(); |
| 561 | if (parsedStr) { |
| 562 | // repair: remove the start quote of the second string |
| 563 | output.remove(start, start + 1); |
| 564 | } else { |
| 565 | // repair: remove the + because it is not followed by a string |
| 566 | output.insertBeforeLastWhitespace('"'); |
| 567 | } |
| 568 | } |
| 569 | return parsed; |
| 570 | } |
| 571 | |
| 572 | /** |
| 573 | * Parse a number like 2.4 or 2.4e6 |
| 574 | */ |
| 575 | function parseNumber() { |
| 576 | const start = i; |
| 577 | if (input.charCodeAt(i) === codeMinus) { |
| 578 | i++; |
| 579 | if (atEndOfNumber()) { |
| 580 | repairNumberEndingWithNumericSymbol(start); |
| 581 | return stack.update(Caret.afterValue); |
| 582 | } |
| 583 | if (!isDigit(input.charCodeAt(i))) { |
| 584 | i = start; |
| 585 | return false; |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | // Note that in JSON leading zeros like "00789" are not allowed. |
| 590 | // We will allow all leading zeros here though and at the end of parseNumber |
| 591 | // check against trailing zeros and repair that if needed. |
| 592 | // Leading zeros can have meaning, so we should not clear them. |
| 593 | while (isDigit(input.charCodeAt(i))) { |
| 594 | i++; |
| 595 | } |
| 596 | if (input.charCodeAt(i) === codeDot) { |
| 597 | i++; |
| 598 | if (atEndOfNumber()) { |
| 599 | repairNumberEndingWithNumericSymbol(start); |
| 600 | return stack.update(Caret.afterValue); |
| 601 | } |
| 602 | if (!isDigit(input.charCodeAt(i))) { |
| 603 | i = start; |
| 604 | return false; |
| 605 | } |
| 606 | while (isDigit(input.charCodeAt(i))) { |
| 607 | i++; |
| 608 | } |
| 609 | } |
| 610 | if (input.charCodeAt(i) === codeLowercaseE || input.charCodeAt(i) === codeUppercaseE) { |
| 611 | i++; |
| 612 | if (input.charCodeAt(i) === codeMinus || input.charCodeAt(i) === codePlus) { |
| 613 | i++; |
| 614 | } |
| 615 | if (atEndOfNumber()) { |
| 616 | repairNumberEndingWithNumericSymbol(start); |
| 617 | return stack.update(Caret.afterValue); |
| 618 | } |
| 619 | if (!isDigit(input.charCodeAt(i))) { |
| 620 | i = start; |
| 621 | return false; |
| 622 | } |
| 623 | while (isDigit(input.charCodeAt(i))) { |
| 624 | i++; |
| 625 | } |
| 626 | } |
| 627 | |
| 628 | // if we're not at the end of the number by this point, allow this to be parsed as another type |
| 629 | if (!atEndOfNumber()) { |
| 630 | i = start; |
| 631 | return false; |
| 632 | } |
| 633 | if (i > start) { |
| 634 | // repair a number with leading zeros like "00789" |
| 635 | const num = input.substring(start, i); |
| 636 | const hasInvalidLeadingZero = /^0\d/.test(num); |
| 637 | output.push(hasInvalidLeadingZero ? "\"".concat(num, "\"") : num); |
| 638 | return stack.update(Caret.afterValue); |
| 639 | } |
| 640 | return false; |
| 641 | } |
| 642 | |
| 643 | /** |
| 644 | * Parse keywords true, false, null |
| 645 | * Repair Python keywords True, False, None |
| 646 | */ |
| 647 | function parseKeywords() { |
| 648 | return parseKeyword('true', 'true') || parseKeyword('false', 'false') || parseKeyword('null', 'null') || |
| 649 | // repair Python keywords True, False, None |
| 650 | parseKeyword('True', 'true') || parseKeyword('False', 'false') || parseKeyword('None', 'null'); |
| 651 | } |
| 652 | function parseKeyword(name, value) { |
| 653 | if (input.substring(i, i + name.length) === name) { |
| 654 | output.push(value); |
| 655 | i += name.length; |
| 656 | return stack.update(Caret.afterValue); |
| 657 | } |
| 658 | return false; |
| 659 | } |
| 660 | function parseUnquotedKey() { |
| 661 | let end = findNextDelimiter(); |
| 662 | if (end !== null) { |
| 663 | // first, go back to prevent getting trailing whitespaces in the string |
| 664 | while (isWhitespace(input.charCodeAt(end - 1)) && end > i) { |
| 665 | end--; |
| 666 | } |
| 667 | const symbol = input.substring(i, end); |
| 668 | output.push(JSON.stringify(symbol)); |
| 669 | i = end; |
| 670 | if (input.charCodeAt(i) === codeDoubleQuote) { |
| 671 | // we had a missing start quote, but now we encountered the end quote, so we can skip that one |
| 672 | i++; |
| 673 | } |
| 674 | return stack.update(Caret.afterValue); // we do not have a state Caret.afterKey, therefore we use afterValue here |
| 675 | } |
| 676 | return false; |
| 677 | } |
| 678 | function findNextDelimiter() { |
| 679 | // note that the symbol can end with whitespaces: we stop at the next delimiter |
| 680 | // also, note that we allow strings to contain a slash / in order to support repairing regular expressions |
| 681 | let j = i; |
| 682 | while (!input.isEnd(j) && !isDelimiterExceptSlash(input.charAt(j)) && !isQuote(input.charCodeAt(j))) { |
| 683 | j++; |
| 684 | } |
| 685 | return j > i ? j : null; |
| 686 | } |
| 687 | function prevNonWhitespaceIndex(start) { |
| 688 | let prev = start; |
| 689 | while (prev > 0 && isWhitespace(input.charCodeAt(prev))) { |
| 690 | prev--; |
| 691 | } |
| 692 | return prev; |
| 693 | } |
| 694 | function atEndOfNumber() { |
| 695 | return input.isEnd(i) || isDelimiter(input.charAt(i)) || isWhitespace(input.charCodeAt(i)); |
| 696 | } |
| 697 | function repairNumberEndingWithNumericSymbol(start) { |
| 698 | // repair numbers cut off at the end |
| 699 | // this will only be called when we end after a '.', '-', or 'e' and does not |
| 700 | // change the number more than it needs to make it valid JSON |
| 701 | output.push(input.substring(start, i) + '0'); |
| 702 | } |
| 703 | function throwInvalidCharacter(char) { |
| 704 | throw new JSONRepairError('Invalid character ' + JSON.stringify(char), i); |
| 705 | } |
| 706 | function throwUnexpectedCharacter() { |
| 707 | throw new JSONRepairError('Unexpected character ' + JSON.stringify(input.charAt(i)), i); |
| 708 | } |
| 709 | function throwUnexpectedEnd() { |
| 710 | throw new JSONRepairError('Unexpected end of json string', i); |
| 711 | } |
| 712 | function throwObjectKeyExpected() { |
| 713 | throw new JSONRepairError('Object key expected', i); |
| 714 | } |
| 715 | function throwColonExpected() { |
| 716 | throw new JSONRepairError('Colon expected', i); |
| 717 | } |
| 718 | function throwInvalidUnicodeCharacter() { |
| 719 | const chars = input.substring(i, i + 6); |
| 720 | throw new JSONRepairError("Invalid unicode character \"".concat(chars, "\""), i); |
| 721 | } |
| 722 | function atEndOfBlockComment(i) { |
| 723 | return input.charAt(i) === '*' && input.charAt(i + 1) === '/'; |
| 724 | } |
| 725 | return { |
| 726 | transform, |
| 727 | flush |
| 728 | }; |
| 729 | } |
| 730 | //# sourceMappingURL=core.js.map |