241 lines
No EOL
8.1 KiB
JavaScript
241 lines
No EOL
8.1 KiB
JavaScript
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
|