@loaders.gl/json
Version:
Framework-independent loader for JSON and streaming JSON formats
537 lines • 21.4 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT AND BSD
// Copyright (c) vis.gl contributors
// This is a fork of the clarinet library, originally BSD license (see LICENSE file)
// loaders.gl changes:
// - typescript port
// Removes the MAX_BUFFER_LENGTH, originally set to 64 * 1024
const MAX_BUFFER_LENGTH = Number.MAX_SAFE_INTEGER;
// const DEBUG = false;
var STATE;
(function (STATE) {
STATE[STATE["BEGIN"] = 0] = "BEGIN";
STATE[STATE["VALUE"] = 1] = "VALUE";
STATE[STATE["OPEN_OBJECT"] = 2] = "OPEN_OBJECT";
STATE[STATE["CLOSE_OBJECT"] = 3] = "CLOSE_OBJECT";
STATE[STATE["OPEN_ARRAY"] = 4] = "OPEN_ARRAY";
STATE[STATE["CLOSE_ARRAY"] = 5] = "CLOSE_ARRAY";
STATE[STATE["TEXT_ESCAPE"] = 6] = "TEXT_ESCAPE";
STATE[STATE["STRING"] = 7] = "STRING";
STATE[STATE["BACKSLASH"] = 8] = "BACKSLASH";
STATE[STATE["END"] = 9] = "END";
STATE[STATE["OPEN_KEY"] = 10] = "OPEN_KEY";
STATE[STATE["CLOSE_KEY"] = 11] = "CLOSE_KEY";
STATE[STATE["TRUE"] = 12] = "TRUE";
STATE[STATE["TRUE2"] = 13] = "TRUE2";
STATE[STATE["TRUE3"] = 14] = "TRUE3";
STATE[STATE["FALSE"] = 15] = "FALSE";
STATE[STATE["FALSE2"] = 16] = "FALSE2";
STATE[STATE["FALSE3"] = 17] = "FALSE3";
STATE[STATE["FALSE4"] = 18] = "FALSE4";
STATE[STATE["NULL"] = 19] = "NULL";
STATE[STATE["NULL2"] = 20] = "NULL2";
STATE[STATE["NULL3"] = 21] = "NULL3";
STATE[STATE["NUMBER_DECIMAL_POINT"] = 22] = "NUMBER_DECIMAL_POINT";
STATE[STATE["NUMBER_DIGIT"] = 23] = "NUMBER_DIGIT"; // [0-9]
})(STATE || (STATE = {}));
const Char = {
tab: 0x09, // \t
lineFeed: 0x0a, // \n
carriageReturn: 0x0d, // \r
space: 0x20, // " "
doubleQuote: 0x22, // "
plus: 0x2b, // +
comma: 0x2c, // ,
minus: 0x2d, // -
period: 0x2e, // .
_0: 0x30, // 0
_9: 0x39, // 9
colon: 0x3a, // :
E: 0x45, // E
openBracket: 0x5b, // [
backslash: 0x5c, // \
closeBracket: 0x5d, // ]
a: 0x61, // a
b: 0x62, // b
e: 0x65, // e
f: 0x66, // f
l: 0x6c, // l
n: 0x6e, // n
r: 0x72, // r
s: 0x73, // s
t: 0x74, // t
u: 0x75, // u
openBrace: 0x7b, // {
closeBrace: 0x7d // }
};
const stringTokenPattern = /[\\"\n]/g;
const DEFAULT_OPTIONS = {
onready: () => { },
onopenobject: () => { },
onkey: () => { },
oncloseobject: () => { },
onopenarray: () => { },
onclosearray: () => { },
onvalue: () => { },
onerror: () => { },
onend: () => { },
onchunkparsed: () => { }
};
export default class ClarinetParser {
options = DEFAULT_OPTIONS;
bufferCheckPosition = MAX_BUFFER_LENGTH;
q = '';
c = '';
p = '';
closed = false;
closedRoot = false;
sawRoot = false;
// tag = null;
error = null;
state = STATE.BEGIN;
stack = [];
// mostly just for error reporting
position = 0;
column = 0;
line = 1;
slashed = false;
unicodeI = 0;
unicodeS = null;
depth = 0;
textNode;
numberNode;
constructor(options = {}) {
this.options = { ...DEFAULT_OPTIONS, ...options };
this.textNode = undefined;
this.numberNode = '';
this.emit('onready');
}
end() {
if (this.state !== STATE.VALUE || this.depth !== 0)
this._error('Unexpected end');
this._closeValue();
this.c = '';
this.closed = true;
this.emit('onend');
return this;
}
resume() {
this.error = null;
return this;
}
close() {
return this.write(null);
}
// protected
emit(event, data) {
// if (DEBUG) console.log('-- emit', event, data);
this.options[event]?.(data, this);
}
emitNode(event, data) {
this._closeValue();
this.emit(event, data);
}
/* eslint-disable no-continue */
// eslint-disable-next-line complexity, max-statements
write(chunk) {
if (this.error) {
throw this.error;
}
if (this.closed) {
return this._error('Cannot write after close. Assign an onready handler.');
}
if (chunk === null) {
return this.end();
}
let i = 0;
let c = chunk.charCodeAt(0);
let p = this.p;
// if (DEBUG) console.log(`write -> [${ chunk }]`);
while (c) {
p = c;
this.c = c = chunk.charCodeAt(i++);
// if chunk doesnt have next, like streaming char by char
// this way we need to check if previous is really previous
// if not we need to reset to what the this says is the previous
// from buffer
if (p !== c) {
this.p = p;
}
else {
p = this.p;
}
if (!c)
break;
// if (DEBUG) console.log(i, c, STATE[this.state]);
this.position++;
if (c === Char.lineFeed) {
this.line++;
this.column = 0;
}
else
this.column++;
switch (this.state) {
case STATE.BEGIN:
if (c === Char.openBrace)
this.state = STATE.OPEN_OBJECT;
else if (c === Char.openBracket)
this.state = STATE.OPEN_ARRAY;
else if (!isWhitespace(c)) {
this._error('Non-whitespace before {[.');
}
continue;
case STATE.OPEN_KEY:
case STATE.OPEN_OBJECT:
if (isWhitespace(c))
continue;
if (this.state === STATE.OPEN_KEY)
this.stack.push(STATE.CLOSE_KEY);
else if (c === Char.closeBrace) {
this.emit('onopenobject');
this.depth++;
this.emit('oncloseobject');
this.depth--;
this.state = this.stack.pop() || STATE.VALUE;
continue;
}
else
this.stack.push(STATE.CLOSE_OBJECT);
if (c === Char.doubleQuote)
this.state = STATE.STRING;
else
this._error('Malformed object key should start with "');
continue;
case STATE.CLOSE_KEY:
case STATE.CLOSE_OBJECT:
if (isWhitespace(c))
continue;
// let event = this.state === STATE.CLOSE_KEY ? 'key' : 'object';
if (c === Char.colon) {
if (this.state === STATE.CLOSE_OBJECT) {
this.stack.push(STATE.CLOSE_OBJECT);
this._closeValue('onopenobject');
this.depth++;
}
else
this._closeValue('onkey');
this.state = STATE.VALUE;
}
else if (c === Char.closeBrace) {
this.emitNode('oncloseobject');
this.depth--;
this.state = this.stack.pop() || STATE.VALUE;
}
else if (c === Char.comma) {
if (this.state === STATE.CLOSE_OBJECT)
this.stack.push(STATE.CLOSE_OBJECT);
this._closeValue();
this.state = STATE.OPEN_KEY;
}
else
this._error('Bad object');
continue;
case STATE.OPEN_ARRAY: // after an array there always a value
case STATE.VALUE:
if (isWhitespace(c))
continue;
if (this.state === STATE.OPEN_ARRAY) {
this.emit('onopenarray');
this.depth++;
this.state = STATE.VALUE;
if (c === Char.closeBracket) {
this.emit('onclosearray');
this.depth--;
this.state = this.stack.pop() || STATE.VALUE;
continue;
}
else {
this.stack.push(STATE.CLOSE_ARRAY);
}
}
if (c === Char.doubleQuote)
this.state = STATE.STRING;
else if (c === Char.openBrace)
this.state = STATE.OPEN_OBJECT;
else if (c === Char.openBracket)
this.state = STATE.OPEN_ARRAY;
else if (c === Char.t)
this.state = STATE.TRUE;
else if (c === Char.f)
this.state = STATE.FALSE;
else if (c === Char.n)
this.state = STATE.NULL;
else if (c === Char.minus) {
// keep and continue
this.numberNode += '-';
}
else if (Char._0 <= c && c <= Char._9) {
this.numberNode += String.fromCharCode(c);
this.state = STATE.NUMBER_DIGIT;
}
else
this._error('Bad value');
continue;
case STATE.CLOSE_ARRAY:
if (c === Char.comma) {
this.stack.push(STATE.CLOSE_ARRAY);
this._closeValue('onvalue');
this.state = STATE.VALUE;
}
else if (c === Char.closeBracket) {
this.emitNode('onclosearray');
this.depth--;
this.state = this.stack.pop() || STATE.VALUE;
}
else if (isWhitespace(c))
continue;
else
this._error('Bad array');
continue;
case STATE.STRING:
if (this.textNode === undefined) {
this.textNode = '';
}
// thanks thejh, this is an about 50% performance improvement.
let starti = i - 1;
let slashed = this.slashed;
let unicodeI = this.unicodeI;
// eslint-disable-next-line no-constant-condition, no-labels
STRING_BIGLOOP: while (true) {
// if (DEBUG) console.log(i, c, STATE[this.state], slashed);
// zero means "no unicode active". 1-4 mean "parse some more". end after 4.
while (unicodeI > 0) {
this.unicodeS += String.fromCharCode(c);
c = chunk.charCodeAt(i++);
this.position++;
if (unicodeI === 4) {
// TODO this might be slow? well, probably not used too often anyway
this.textNode += String.fromCharCode(parseInt(this.unicodeS, 16));
unicodeI = 0;
starti = i - 1;
}
else {
unicodeI++;
}
// we can just break here: no stuff we skipped that still has to be sliced out or so
// eslint-disable-next-line no-labels
if (!c)
break STRING_BIGLOOP;
}
if (c === Char.doubleQuote && !slashed) {
this.state = this.stack.pop() || STATE.VALUE;
this.textNode += chunk.substring(starti, i - 1);
this.position += i - 1 - starti;
break;
}
if (c === Char.backslash && !slashed) {
slashed = true;
this.textNode += chunk.substring(starti, i - 1);
this.position += i - 1 - starti;
c = chunk.charCodeAt(i++);
this.position++;
if (!c)
break;
}
if (slashed) {
slashed = false;
if (c === Char.n) {
this.textNode += '\n';
}
else if (c === Char.r) {
this.textNode += '\r';
}
else if (c === Char.t) {
this.textNode += '\t';
}
else if (c === Char.f) {
this.textNode += '\f';
}
else if (c === Char.b) {
this.textNode += '\b';
}
else if (c === Char.u) {
// \uxxxx. meh!
unicodeI = 1;
this.unicodeS = '';
}
else {
this.textNode += String.fromCharCode(c);
}
c = chunk.charCodeAt(i++);
this.position++;
starti = i - 1;
if (!c)
break;
else
continue;
}
stringTokenPattern.lastIndex = i;
const reResult = stringTokenPattern.exec(chunk);
if (reResult === null) {
i = chunk.length + 1;
this.textNode += chunk.substring(starti, i - 1);
this.position += i - 1 - starti;
break;
}
i = reResult.index + 1;
c = chunk.charCodeAt(reResult.index);
if (!c) {
this.textNode += chunk.substring(starti, i - 1);
this.position += i - 1 - starti;
break;
}
}
this.slashed = slashed;
this.unicodeI = unicodeI;
continue;
case STATE.TRUE:
if (c === Char.r)
this.state = STATE.TRUE2;
else
this._error(`Invalid true started with t${c}`);
continue;
case STATE.TRUE2:
if (c === Char.u)
this.state = STATE.TRUE3;
else
this._error(`Invalid true started with tr${c}`);
continue;
case STATE.TRUE3:
if (c === Char.e) {
this.emit('onvalue', true);
this.state = this.stack.pop() || STATE.VALUE;
}
else
this._error(`Invalid true started with tru${c}`);
continue;
case STATE.FALSE:
if (c === Char.a)
this.state = STATE.FALSE2;
else
this._error(`Invalid false started with f${c}`);
continue;
case STATE.FALSE2:
if (c === Char.l)
this.state = STATE.FALSE3;
else
this._error(`Invalid false started with fa${c}`);
continue;
case STATE.FALSE3:
if (c === Char.s)
this.state = STATE.FALSE4;
else
this._error(`Invalid false started with fal${c}`);
continue;
case STATE.FALSE4:
if (c === Char.e) {
this.emit('onvalue', false);
this.state = this.stack.pop() || STATE.VALUE;
}
else
this._error(`Invalid false started with fals${c}`);
continue;
case STATE.NULL:
if (c === Char.u)
this.state = STATE.NULL2;
else
this._error(`Invalid null started with n${c}`);
continue;
case STATE.NULL2:
if (c === Char.l)
this.state = STATE.NULL3;
else
this._error(`Invalid null started with nu${c}`);
continue;
case STATE.NULL3:
if (c === Char.l) {
this.emit('onvalue', null);
this.state = this.stack.pop() || STATE.VALUE;
}
else
this._error(`Invalid null started with nul${c}`);
continue;
case STATE.NUMBER_DECIMAL_POINT:
if (c === Char.period) {
this.numberNode += '.';
this.state = STATE.NUMBER_DIGIT;
}
else
this._error('Leading zero not followed by .');
continue;
case STATE.NUMBER_DIGIT:
if (Char._0 <= c && c <= Char._9)
this.numberNode += String.fromCharCode(c);
else if (c === Char.period) {
if (this.numberNode.indexOf('.') !== -1)
this._error('Invalid number has two dots');
this.numberNode += '.';
}
else if (c === Char.e || c === Char.E) {
if (this.numberNode.indexOf('e') !== -1 || this.numberNode.indexOf('E') !== -1)
this._error('Invalid number has two exponential');
this.numberNode += 'e';
}
else if (c === Char.plus || c === Char.minus) {
// @ts-expect-error
if (!(p === Char.e || p === Char.E))
this._error('Invalid symbol in number');
this.numberNode += String.fromCharCode(c);
}
else {
this._closeNumber();
i--; // go back one
this.state = this.stack.pop() || STATE.VALUE;
}
continue;
default:
this._error(`Unknown state: ${this.state}`);
}
}
if (this.position >= this.bufferCheckPosition) {
checkBufferLength(this);
}
this.emit('onchunkparsed');
return this;
}
_closeValue(event = 'onvalue') {
if (this.textNode !== undefined) {
this.emit(event, this.textNode);
}
this.textNode = undefined;
}
_closeNumber() {
if (this.numberNode)
this.emit('onvalue', parseFloat(this.numberNode));
this.numberNode = '';
}
_error(message = '') {
this._closeValue();
message += `\nLine: ${this.line}\nColumn: ${this.column}\nChar: ${this.c}`;
const error = new Error(message);
this.error = error;
this.emit('onerror', error);
}
}
function isWhitespace(c) {
return c === Char.carriageReturn || c === Char.lineFeed || c === Char.space || c === Char.tab;
}
function checkBufferLength(parser) {
const maxAllowed = Math.max(MAX_BUFFER_LENGTH, 10);
let maxActual = 0;
for (const buffer of ['textNode', 'numberNode']) {
const len = parser[buffer] === undefined ? 0 : parser[buffer].length;
if (len > maxAllowed) {
switch (buffer) {
case 'text':
// TODO - should this be closeValue?
// closeText(parser);
break;
default:
parser._error(`Max buffer length exceeded: ${buffer}`);
}
}
maxActual = Math.max(maxActual, len);
}
parser.bufferCheckPosition = MAX_BUFFER_LENGTH - maxActual + parser.position;
}
//# sourceMappingURL=clarinet.js.map