const STR = 0b000000001; const NUM = 0b000000010; const ARR = 0b000000100; const OBJ = 0b000001000; const NULL = 0b000010000; const BOOL = 0b000100000; const NAN = 0b001000000; const INFINITY = 0b010000000; const MINUS_INFINITY = 0b100000000; const INF = INFINITY | MINUS_INFINITY; const SPECIAL = NULL | BOOL | INF | NAN; const ATOM = STR | NUM | SPECIAL; const COLLECTION = ARR | OBJ; const ALL = ATOM | COLLECTION; const Allow = { STR, NUM, ARR, OBJ, NULL, BOOL, NAN, INFINITY, MINUS_INFINITY, INF, SPECIAL, ATOM, COLLECTION, ALL, }; // The JSON string segment was unable to be parsed completely class PartialJSON extends Error { } class MalformedJSON extends Error { } /** * Parse incomplete JSON * @param {string} jsonString Partial JSON to be parsed * @param {number} allowPartial Specify what types are allowed to be partial, see {@link Allow} for details * @returns The parsed JSON * @throws {PartialJSON} If the JSON is incomplete (related to the `allow` parameter) * @throws {MalformedJSON} If the JSON is malformed */ function parseJSON(jsonString, allowPartial = Allow.ALL) { if (typeof jsonString !== 'string') { throw new TypeError(`expecting str, got ${typeof jsonString}`); } if (!jsonString.trim()) { throw new Error(`${jsonString} is empty`); } return _parseJSON(jsonString.trim(), allowPartial); } const _parseJSON = (jsonString, allow) => { const length = jsonString.length; let index = 0; const markPartialJSON = (msg) => { throw new PartialJSON(`${msg} at position ${index}`); }; const throwMalformedError = (msg) => { throw new MalformedJSON(`${msg} at position ${index}`); }; const parseAny = () => { skipBlank(); if (index >= length) markPartialJSON('Unexpected end of input'); if (jsonString[index] === '"') return parseStr(); if (jsonString[index] === '{') return parseObj(); if (jsonString[index] === '[') return parseArr(); if (jsonString.substring(index, index + 4) === 'null' || (Allow.NULL & allow && length - index < 4 && 'null'.startsWith(jsonString.substring(index)))) { index += 4; return null; } if (jsonString.substring(index, index + 4) === 'true' || (Allow.BOOL & allow && length - index < 4 && 'true'.startsWith(jsonString.substring(index)))) { index += 4; return true; } if (jsonString.substring(index, index + 5) === 'false' || (Allow.BOOL & allow && length - index < 5 && 'false'.startsWith(jsonString.substring(index)))) { index += 5; return false; } if (jsonString.substring(index, index + 8) === 'Infinity' || (Allow.INFINITY & allow && length - index < 8 && 'Infinity'.startsWith(jsonString.substring(index)))) { index += 8; return Infinity; } if (jsonString.substring(index, index + 9) === '-Infinity' || (Allow.MINUS_INFINITY & allow && 1 < length - index && length - index < 9 && '-Infinity'.startsWith(jsonString.substring(index)))) { index += 9; return -Infinity; } if (jsonString.substring(index, index + 3) === 'NaN' || (Allow.NAN & allow && length - index < 3 && 'NaN'.startsWith(jsonString.substring(index)))) { index += 3; return NaN; } return parseNum(); }; const parseStr = () => { const start = index; let escape = false; index++; // skip initial quote while (index < length && (jsonString[index] !== '"' || (escape && jsonString[index - 1] === '\\'))) { escape = jsonString[index] === '\\' ? !escape : false; index++; } if (jsonString.charAt(index) == '"') { try { return JSON.parse(jsonString.substring(start, ++index - Number(escape))); } catch (e) { throwMalformedError(String(e)); } } else if (Allow.STR & allow) { try { return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"'); } catch (e) { // SyntaxError: Invalid escape sequence return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf('\\')) + '"'); } } markPartialJSON('Unterminated string literal'); }; const parseObj = () => { index++; // skip initial brace skipBlank(); const obj = {}; try { while (jsonString[index] !== '}') { skipBlank(); if (index >= length && Allow.OBJ & allow) return obj; const key = parseStr(); skipBlank(); index++; // skip colon try { const value = parseAny(); Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true }); } catch (e) { if (Allow.OBJ & allow) return obj; else throw e; } skipBlank(); if (jsonString[index] === ',') index++; // skip comma } } catch (e) { if (Allow.OBJ & allow) return obj; else markPartialJSON("Expected '}' at end of object"); } index++; // skip final brace return obj; }; const parseArr = () => { index++; // skip initial bracket const arr = []; try { while (jsonString[index] !== ']') { arr.push(parseAny()); skipBlank(); if (jsonString[index] === ',') { index++; // skip comma } } } catch (e) { if (Allow.ARR & allow) { return arr; } markPartialJSON("Expected ']' at end of array"); } index++; // skip final bracket return arr; }; const parseNum = () => { if (index === 0) { if (jsonString === '-' && Allow.NUM & allow) markPartialJSON("Not sure what '-' is"); try { return JSON.parse(jsonString); } catch (e) { if (Allow.NUM & allow) { try { if ('.' === jsonString[jsonString.length - 1]) return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf('.'))); return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf('e'))); } catch (e) { } } throwMalformedError(String(e)); } } const start = index; if (jsonString[index] === '-') index++; while (jsonString[index] && !',]}'.includes(jsonString[index])) index++; if (index == length && !(Allow.NUM & allow)) markPartialJSON('Unterminated number literal'); try { return JSON.parse(jsonString.substring(start, index)); } catch (e) { if (jsonString.substring(start, index) === '-' && Allow.NUM & allow) markPartialJSON("Not sure what '-' is"); try { return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf('e'))); } catch (e) { throwMalformedError(String(e)); } } }; const skipBlank = () => { while (index < length && ' \n\r\t'.includes(jsonString[index])) { index++; } }; return parseAny(); }; // using this function with malformed JSON is undefined behavior const partialParse = (input) => parseJSON(input, Allow.ALL ^ Allow.NUM); export { partialParse, PartialJSON, MalformedJSON }; //# sourceMappingURL=parser.mjs.map