src/numbers/lossless.ts
v0.1.0 · 1.9 KB
const JSON_NUMBER_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
const QUOTE = 0x22;
const BACKSLASH = 0x5c;
const MINUS = 0x2d;
const isDigit = (c: number): boolean => c >= 0x30 && c <= 0x39;
const isNumberChar = (c: number): boolean =>
isDigit(c) || c === MINUS || c === 0x2b || c === 0x2e || c === 0x65 || c === 0x45;
export function parseLossless(text: string): unknown {
if (typeof text !== 'string') throw new TypeError('Lossless JSON input must be a string');
const out: string[] = [];
const stack: number[] = [];
let expectKey = false;
let from = 0;
let i = 0;
const n = text.length;
while (i < n) {
const c = text.charCodeAt(i);
if (c === QUOTE) {
i++;
while (i < n) {
const d = text.charCodeAt(i);
if (d === BACKSLASH) i += 2;
else if (d === QUOTE) {
i++;
break;
} else i++;
}
expectKey = false;
continue;
}
if (c === MINUS || isDigit(c)) {
let j = i + 1;
while (j < n && isNumberChar(text.charCodeAt(j))) j++;
const lit = text.slice(i, j);
if (!JSON_NUMBER_RE.test(lit)) throw new SyntaxError(`Invalid JSON number ${lit.slice(0, 40)} at offset ${i}`);
if (expectKey) throw new SyntaxError(`JSON object key must be quoted; found ${lit.slice(0, 40)} at offset ${i}`);
out.push(text.slice(from, i), '"', lit, '"');
from = j;
i = j;
continue;
}
switch (c) {
case 0x7b:
stack.push(c);
expectKey = true;
break;
case 0x5b:
stack.push(c);
expectKey = false;
break;
case 0x7d:
case 0x5d:
stack.pop();
expectKey = false;
break;
case 0x2c:
expectKey = stack[stack.length - 1] === 0x7b;
break;
case 0x3a:
expectKey = false;
break;
default:
break;
}
i++;
}
out.push(text.slice(from));
return JSON.parse(out.join(''));
}