UNPKG

lossless-json

Version:

Parse JSON without risk of losing numeric information

328 lines (325 loc) 10.2 kB
import { parseLosslessNumber } from './numberParsers.js'; import { revive } from './revive.js'; /** * The LosslessJSON.parse() method parses a string as JSON, optionally transforming * the value produced by parsing. * * The parser is based on the parser of Tan Li Hou shared in * https://lihautan.com/json-parser-with-javascript/ * * @param text * The string to parse as JSON. See the JSON object for a description of JSON syntax. * * @param [reviver] * If a function, prescribes how the value originally produced by parsing is * transformed, before being returned. * * @param [parseNumber=parseLosslessNumber] * Pass a custom number parser. Input is a string, and the output can be unknown * numeric value: number, bigint, LosslessNumber, or a custom BigNumber library. * * @returns Returns the Object corresponding to the given JSON text. * * @throws Throws a SyntaxError exception if the string to parse is not valid JSON. */ export function parse(text, reviver) { let parseNumber = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : parseLosslessNumber; let i = 0; const value = parseValue(); expectValue(value); expectEndOfInput(); return reviver ? revive(value, reviver) : value; function parseObject() { if (text.charCodeAt(i) === codeOpeningBrace) { i++; skipWhitespace(); const object = {}; let initial = true; while (i < text.length && text.charCodeAt(i) !== codeClosingBrace) { if (!initial) { eatComma(); skipWhitespace(); } else { initial = false; } const start = i; const key = parseString(); if (key === undefined) { throwObjectKeyExpected(); return; // To make TS happy } skipWhitespace(); eatColon(); const value = parseValue(); if (value === undefined) { throwObjectValueExpected(); return; // To make TS happy } // TODO: test deep equal instead of strict equal if (Object.prototype.hasOwnProperty.call(object, key) && !isDeepEqual(value, object[key])) { // Note that we could also test `if(key in object) {...}` // or `if (object[key] !== 'undefined') {...}`, but that is slower. throwDuplicateKey(key, start + 1); } object[key] = value; } if (text.charCodeAt(i) !== codeClosingBrace) { throwObjectKeyOrEndExpected(); } i++; return object; } } function parseArray() { if (text.charCodeAt(i) === codeOpeningBracket) { i++; skipWhitespace(); const array = []; let initial = true; while (i < text.length && text.charCodeAt(i) !== codeClosingBracket) { if (!initial) { eatComma(); } else { initial = false; } const value = parseValue(); expectArrayItem(value); array.push(value); } if (text.charCodeAt(i) !== codeClosingBracket) { throwArrayItemOrEndExpected(); } i++; return array; } } function parseValue() { skipWhitespace(); const value = parseString() ?? parseNumeric() ?? parseObject() ?? parseArray() ?? parseKeyword('true', true) ?? parseKeyword('false', false) ?? parseKeyword('null', null); skipWhitespace(); return value; } function parseKeyword(name, value) { if (text.slice(i, i + name.length) === name) { i += name.length; return value; } } function skipWhitespace() { while (isWhitespace(text.charCodeAt(i))) { i++; } } function parseString() { if (text.charCodeAt(i) === codeDoubleQuote) { i++; let result = ''; while (i < text.length && text.charCodeAt(i) !== codeDoubleQuote) { if (text.charCodeAt(i) === codeBackslash) { const char = text[i + 1]; const escapeChar = escapeCharacters[char]; if (escapeChar !== undefined) { result += escapeChar; i++; } else if (char === 'u') { if (isHex(text.charCodeAt(i + 2)) && isHex(text.charCodeAt(i + 3)) && isHex(text.charCodeAt(i + 4)) && isHex(text.charCodeAt(i + 5))) { result += String.fromCharCode(Number.parseInt(text.slice(i + 2, i + 6), 16)); i += 5; } else { throwInvalidUnicodeCharacter(i); } } else { throwInvalidEscapeCharacter(i); } } else { if (isValidStringCharacter(text.charCodeAt(i))) { result += text[i]; } else { throwInvalidCharacter(text[i]); } } i++; } expectEndOfString(); i++; return result; } } function parseNumeric() { const start = i; if (text.charCodeAt(i) === codeMinus) { i++; expectDigit(start); } if (text.charCodeAt(i) === codeZero) { i++; } else if (isNonZeroDigit(text.charCodeAt(i))) { i++; while (isDigit(text.charCodeAt(i))) { i++; } } if (text.charCodeAt(i) === codeDot) { i++; expectDigit(start); while (isDigit(text.charCodeAt(i))) { i++; } } if (text.charCodeAt(i) === codeLowercaseE || text.charCodeAt(i) === codeUppercaseE) { i++; if (text.charCodeAt(i) === codeMinus || text.charCodeAt(i) === codePlus) { i++; } expectDigit(start); while (isDigit(text.charCodeAt(i))) { i++; } } if (i > start) { return parseNumber(text.slice(start, i)); } } function eatComma() { if (text.charCodeAt(i) !== codeComma) { throw new SyntaxError(`Comma ',' expected after value ${gotAt()}`); } i++; } function eatColon() { if (text.charCodeAt(i) !== codeColon) { throw new SyntaxError(`Colon ':' expected after property name ${gotAt()}`); } i++; } function expectValue(value) { if (value === undefined) { throw new SyntaxError(`JSON value expected ${gotAt()}`); } } function expectArrayItem(value) { if (value === undefined) { throw new SyntaxError(`Array item expected ${gotAt()}`); } } function expectEndOfInput() { if (i < text.length) { throw new SyntaxError(`Expected end of input ${gotAt()}`); } } function expectDigit(start) { if (!isDigit(text.charCodeAt(i))) { const numSoFar = text.slice(start, i); throw new SyntaxError(`Invalid number '${numSoFar}', expecting a digit ${gotAt()}`); } } function expectEndOfString() { if (text.charCodeAt(i) !== codeDoubleQuote) { throw new SyntaxError(`End of string '"' expected ${gotAt()}`); } } function throwObjectKeyExpected() { throw new SyntaxError(`Quoted object key expected ${gotAt()}`); } function throwDuplicateKey(key, pos) { throw new SyntaxError(`Duplicate key '${key}' encountered at position ${pos}`); } function throwObjectKeyOrEndExpected() { throw new SyntaxError(`Quoted object key or end of object '}' expected ${gotAt()}`); } function throwArrayItemOrEndExpected() { throw new SyntaxError(`Array item or end of array ']' expected ${gotAt()}`); } function throwInvalidCharacter(char) { throw new SyntaxError(`Invalid character '${char}' ${pos()}`); } function throwInvalidEscapeCharacter(start) { const chars = text.slice(start, start + 2); throw new SyntaxError(`Invalid escape character '${chars}' ${pos()}`); } function throwObjectValueExpected() { throw new SyntaxError(`Object value expected after ':' ${pos()}`); } function throwInvalidUnicodeCharacter(start) { const chars = text.slice(start, start + 6); throw new SyntaxError(`Invalid unicode character '${chars}' ${pos()}`); } // zero based character position function pos() { return `at position ${i}`; } function got() { return i < text.length ? `but got '${text[i]}'` : 'but reached end of input'; } function gotAt() { return `${got()} ${pos()}`; } } function isWhitespace(code) { return code === codeSpace || code === codeNewline || code === codeTab || code === codeReturn; } function isHex(code) { return code >= codeZero && code <= codeNine || code >= codeUppercaseA && code <= codeUppercaseF || code >= codeLowercaseA && code <= codeLowercaseF; } function isDigit(code) { return code >= codeZero && code <= codeNine; } function isNonZeroDigit(code) { return code >= codeOne && code <= codeNine; } export function isValidStringCharacter(code) { return code >= 0x20 && code <= 0x10ffff; } export function isDeepEqual(a, b) { if (a === b) { return true; } if (Array.isArray(a) && Array.isArray(b)) { return a.length === b.length && a.every((item, index) => isDeepEqual(item, b[index])); } if (isObject(a) && isObject(b)) { const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])]; return keys.every(key => isDeepEqual(a[key], b[key])); } return false; } function isObject(value) { return typeof value === 'object' && value !== null; } // map with all escape characters const escapeCharacters = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' // note that \u is handled separately in parseString() }; const codeBackslash = 0x5c; // "\" const codeOpeningBrace = 0x7b; // "{" const codeClosingBrace = 0x7d; // "}" const codeOpeningBracket = 0x5b; // "[" const codeClosingBracket = 0x5d; // "]" const codeSpace = 0x20; // " " const codeNewline = 0xa; // "\n" const codeTab = 0x9; // "\t" const codeReturn = 0xd; // "\r" const codeDoubleQuote = 0x0022; // " const codePlus = 0x2b; // "+" const codeMinus = 0x2d; // "-" const codeZero = 0x30; const codeOne = 0x31; const codeNine = 0x39; const codeComma = 0x2c; // "," const codeDot = 0x2e; // "." (dot, period) const codeColon = 0x3a; // ":" export const codeUppercaseA = 0x41; // "A" export const codeLowercaseA = 0x61; // "a" export const codeUppercaseE = 0x45; // "E" export const codeLowercaseE = 0x65; // "e" export const codeUppercaseF = 0x46; // "F" export const codeLowercaseF = 0x66; // "f" //# sourceMappingURL=parse.js.map