UNPKG

@jswalden/streaming-json

Version:

Streaming JSON parsing and stringification for JavaScript/TypeScript

264 lines 10.4 kB
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