|
const { CharStream } = require('./char-stream'); |
|
const { unescapeChar } = require('./utils'); |
|
|
|
/** |
|
* Parse a JSON string. |
|
* @param {string} str The JSON string to parse |
|
* @returns {any} The parsed JSON value |
|
* @throws In case of error parsing the JSON |
|
*/ |
|
function parseJson(str) { |
|
const input = new CharStream(str); |
|
const value = readValue(input); |
|
|
|
const nextChar = input.next(); |
|
if (nextChar !== undefined) |
|
throw new Error(nextChar); |
|
|
|
return value; |
|
} |
|
|
|
/** |
|
* Read the next value from the given input. |
|
* @param {CharStream} input The input to read from. |
|
* @returns {any} The value read |
|
* @throws In case of error reading the value |
|
*/ |
|
function readValue(input) { |
|
const nextChar = input.skipEmpty().get(); |
|
|
|
switch (nextChar) { |
|
case '{': |
|
return readObject(input); |
|
case '[': |
|
return readArray(input); |
|
case '"': |
|
return readString(input); |
|
case 't': |
|
return readExact(input, true); |
|
case 'f': |
|
return readExact(input, false); |
|
case 'n': |
|
return readExact(input, null); |
|
} |
|
|
|
if (!Number.isNaN(Number.parseInt(nextChar)) || nextChar === '-') |
|
return readNumber(input); |
|
|
|
// Unparsable value |
|
// throw new Error(`Unexpected token: '${nextChar}'`); |
|
input.expectCurrent(''); |
|
} |
|
|
|
/** |
|
* Read exactly the given value from the input. |
|
* The value is stringified and each char of the string representation is expected. |
|
* @param {CharStream} input The input to read from. |
|
* @param {any} value The expected value |
|
* @returns {any} The value read |
|
* @throws If the read value doesn't match the given one. |
|
*/ |
|
function readExact(input, value) { |
|
const chars = String(value); |
|
for (const c of chars) { |
|
input.expectNext(c); |
|
} |
|
return value; |
|
} |
|
|
|
/** |
|
* Read the next number from the given input. |
|
* Returns when the number in the input stream is ended. |
|
* @param {CharStream} input The input to read from. |
|
* @returns {number} The number read |
|
* @throws If the stream doesn't point to a number |
|
*/ |
|
function readNumber(input) { |
|
let num = 0; |
|
let negative = false; |
|
let c = input.skipEmpty().next(); |
|
|
|
if (c === '-') { |
|
negative = true; |
|
c = input.next(); |
|
} |
|
|
|
// First char must be a digit |
|
if (Number.isNaN(num = Number.parseInt(c))) |
|
throw new Error(`Unexpected ${c}, expected a digit.`); |
|
|
|
let digit; |
|
while (!Number.isNaN(digit = Number.parseInt(input.get()))) { |
|
num = num * 10 + digit; |
|
input.next(); |
|
} |
|
|
|
// Check for decimal values |
|
if (input.get() === '.') { |
|
// Consume the dot and continue parsing as a number |
|
input.next(); |
|
|
|
let floatFactor = 0.1; |
|
while (!Number.isNaN(digit = Number.parseInt(input.get()))) { |
|
num += digit * floatFactor; |
|
floatFactor *= 0.1; |
|
input.next(); |
|
} |
|
} |
|
|
|
return negative ? -num : num; |
|
} |
|
|
|
/** |
|
* Read the next string from the given input. |
|
* @param {CharStream} input The input to read from. |
|
* @returns {string} The string read |
|
* @throws If the stream doesn't point to a string |
|
*/ |
|
function readString(input) { |
|
const chars = []; |
|
input.skipEmpty().expectNext('"'); |
|
|
|
let c, escaped = false; |
|
while ((c = input.get()) != '"' || escaped) { |
|
if (c === '\n') |
|
throw new Error('Unexpected newline while parsing a string'); |
|
|
|
if (c === '\\' && !escaped) { |
|
escaped = true; |
|
input.next(); |
|
continue; |
|
} |
|
|
|
if (escaped) { |
|
c = unescapeChar(c); |
|
escaped = false; |
|
} |
|
|
|
chars.push(c); |
|
input.next(); |
|
} |
|
|
|
input.expectCurrent('"'); |
|
|
|
return chars.join(''); |
|
} |
|
|
|
/** |
|
* Read the next object from the given input. |
|
* @param {CharStream} input The input to read from. |
|
* @returns {object} The object read |
|
* @throws If the stream doesn't point to a valid object |
|
*/ |
|
function readObject(input) { |
|
input.skipEmpty().expectNext('{'); |
|
|
|
const obj = {}; |
|
let objectHasItems = false; |
|
|
|
do { |
|
if (objectHasItems) { |
|
// Consume the comma |
|
input.next(); |
|
} |
|
|
|
const key = readString(input); |
|
input.skipEmpty().expectNext(':'); |
|
obj[key] = readValue(input); |
|
objectHasItems = true; |
|
} while (input.skipEmpty().get() === ','); |
|
|
|
input.expectCurrent('}'); |
|
|
|
return obj; |
|
} |
|
|
|
/** |
|
* Read the next array from the given input. |
|
* @param {CharStream} input The input to read from. |
|
* @returns {Array<any>} The array read |
|
* @throws If the stream doesn't point to a valid array |
|
*/ |
|
function readArray(input) { |
|
input.skipEmpty().expectNext('['); |
|
|
|
const values = []; |
|
|
|
do { |
|
if (values.length > 0) { |
|
// Consume the previous the comma |
|
input.next(); |
|
} |
|
|
|
values.push(readValue(input)); |
|
} while (input.skipEmpty().get() === ','); |
|
|
|
input.expectCurrent(']'); |
|
|
|
return values; |
|
} |
|
|
|
module.exports = { parseJson }; |