@jswalden/streaming-json
Version:
Streaming JSON parsing and stringification for JavaScript/TypeScript
264 lines • 10.4 kB
JavaScript
import { ArrayFind, IsArray, Pop, Push } from "../stdlib/array.js";
import { ExtractBigIntData, ExtractBooleanData, HasBigIntDataSlot, HasBooleanDataSlot, HasNumberDataSlot, HasStringDataSlot, } from "../stdlib/boxed.js";
import { ThrowError, ThrowTypeError } from "../stdlib/error.js";
import { JSONStringify } from "../stdlib/json-stringify.js";
import { LengthOfArrayLike } from "../stdlib/length.js";
import { Min, Truncate } from "../stdlib/math.js";
import { ToNumber } from "../stdlib/number.js";
import { EnumerableOwnPropertyKeys } from "../stdlib/object.js";
import { ReflectApply } from "../stdlib/reflect.js";
import { SetHas, SetAdd } from "../stdlib/set.js";
import { StringRepeat, StringSlice, ToString } from "../stdlib/string.js";
export const Quantum = 1024;
function toPropertyList(array) {
const propertyList = [];
const set = new Set();
const len = LengthOfArrayLike(array);
for (let k = 0; k < len; k++) {
let v = array[k];
v = typeof v === "number" ? ToString(v) : v;
if (!SetHas(set, v)) {
SetAdd(set, v);
Push(propertyList, v);
}
}
return propertyList;
}
;
function BUG(msg) {
ThrowError(`LOGIC ERROR: ${msg}`);
}
class StringifyGenerator {
constructor(replacer, space) {
this.stack = [];
this.indent = "";
if (typeof replacer === "function")
this.replacer = replacer;
else if (replacer)
this.propertyList = toPropertyList(replacer);
this.gap = typeof space === "string"
? StringSlice(space, 0, 10)
: space >= 1
? StringRepeat(" ", Truncate(Min(10, space)))
: "";
}
*lengthyFragment(frag) {
for (let i = 0; i < frag.length; i += Quantum)
yield StringSlice(frag, i, i + Quantum);
}
checkAcyclic(obj) {
if (ArrayFind(this.stack, (entry) => entry.object === obj))
ThrowTypeError("Attempting to stringify a cyclic object");
}
enterArray(array, length) {
const stepBack = this.indent;
this.indent += this.gap;
const [begin, separator, end] = this.gap === "" ? ["[", ",", "]"] : [`[\n${this.indent}`, `,\n${this.indent}`, `\n${stepBack}]`];
Push(this.stack, {
state: 1,
object: array,
index: 0,
length,
priorIndent: stepBack,
separator,
end,
});
return begin;
}
enterObject(object, props) {
Push(this.stack, {
state: 2,
object: object,
index: 0,
props,
});
}
stackTop() {
return this.stack[this.stack.length - 1];
}
exitObject() {
Pop(this.stack);
}
*run(value) {
let sval;
{
const v = this.preprocessValue(value, this.replacer ? { "": value } : null, "");
if (typeof v === "undefined")
return;
sval = v;
}
let state = 0;
processingElementOrPropertyValue: do {
toFinishValue: switch (state) {
case 1: {
const arrayState = this.stackTop();
const index = ++arrayState.index;
const length = arrayState.length;
if (index >= length) {
if (index > length)
BUG("iterated past end of array elements");
this.exitObject();
this.indent = arrayState.priorIndent;
yield arrayState.end;
break toFinishValue;
}
const { object: array, separator } = arrayState;
yield separator;
sval = this.preprocessValue(array[index], array, ToString(index)) ?? null;
state = 0;
}
case 0: {
do {
if (sval === null) {
yield "null";
break toFinishValue;
}
if (!IsArray(sval))
break;
this.checkAcyclic(sval);
const length = LengthOfArrayLike(sval);
if (length === 0) {
yield "[]";
break toFinishValue;
}
yield this.enterArray(sval, length);
sval = this.preprocessValue(sval[0], sval, "0") ?? null;
} while (true);
if (typeof sval === "object") {
this.checkAcyclic(sval);
const props = this.propertyList ?? EnumerableOwnPropertyKeys(sval);
this.enterObject(sval, props);
state = 2;
continue processingElementOrPropertyValue;
}
const frag = JSONStringify(sval);
if (typeof sval === "string")
yield* this.lengthyFragment(frag);
else
yield frag;
break toFinishValue;
}
case 2: {
const objectState = this.stackTop();
const { object, props: keys } = objectState;
let v;
let index = objectState.index;
let key;
foundUnfilteredProperty: do {
if (index >= keys.length) {
if (index > keys.length)
BUG("iterated past keys end looking for first stringifiable");
this.exitObject();
yield "{}";
break toFinishValue;
}
key = keys[index];
v = this.preprocessValue(object[key], object, key);
if (typeof v !== "undefined")
break foundUnfilteredProperty;
index++;
} while (true);
const stepBack = this.indent;
this.indent += this.gap;
const [opening, colon, comma, closing] = this.gap === ""
? ["{", ":", ",", "}"]
: [`{\n${this.indent}`, ": ", `,\n${this.indent}`, `\n${stepBack}}`];
yield* this.lengthyFragment(`${opening}${JSONStringify(key)}${colon}`);
this.stack[this.stack.length - 1] = {
state: 3,
object,
index: index + 1,
props: keys,
priorIndent: stepBack,
colon,
comma,
closing,
};
sval = v;
state = 0;
continue processingElementOrPropertyValue;
}
case 3: {
const objectState = this.stackTop();
const { object, props: keys } = objectState;
let v;
let index = objectState.index;
let key;
foundUnfilteredProperty: do {
if (index >= keys.length) {
if (index > keys.length)
BUG("iterated past keys end looking for subsequent stringifiable");
this.exitObject();
this.indent = objectState.priorIndent;
yield objectState.closing;
break toFinishValue;
}
key = keys[index];
v = this.preprocessValue(object[key], object, key);
if (typeof v !== "undefined")
break foundUnfilteredProperty;
index++;
} while (true);
const { comma, colon } = objectState;
yield* this.lengthyFragment(`${comma}${JSONStringify(key)}${colon}`);
objectState.index = index + 1;
sval = v;
state = 0;
continue processingElementOrPropertyValue;
}
default: {
}
}
if (this.stack.length === 0)
break;
state = this.stackTop().state;
} while (true);
}
preprocessValue(value, holder, key) {
switch (typeof value) {
case "object":
if (value === null)
break;
case "function":
case "bigint":
const toJSON = value.toJSON;
if (typeof toJSON === "function")
value = ReflectApply(toJSON, value, [key]);
break;
default:
break;
}
if (this.replacer)
value = ReflectApply(this.replacer, holder, [key, value]);
if (value === null)
return value;
if (typeof value === "object") {
if (HasNumberDataSlot(value))
value = ToNumber(value);
else if (HasStringDataSlot(value))
value = ToString(value);
else if (HasBooleanDataSlot(value))
value = ExtractBooleanData(value);
else if (HasBigIntDataSlot(value))
value = ExtractBigIntData(value);
}
switch (typeof value) {
case "string":
case "number":
case "boolean":
case "object":
return value;
case "symbol":
case "undefined":
case "function":
return undefined;
case "bigint":
ThrowTypeError("Can't serialize bigint");
}
}
}
;
export function stringify(value, replacer, space = "") {
return new StringifyGenerator(replacer, space).run(value);
}
//# sourceMappingURL=stringify.js.map