Last active
April 26, 2026 18:22
-
-
Save conartist6/5a46f57b741de72b2b88854a7700032d to your computer and use it in GitHub Desktop.
JSON stream parser for guthib-of-dan
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { StreamGenerator, wait } from '@bablr/stream-iterator'; | |
| import { decodeUTF8, readFile } from '@bablr/fs'; | |
| let { isArray } = Array; | |
| let arrayLast = (arr) => arr[arr.length - 1]; | |
| let maybeWait = (maybePromise, callback) => { | |
| let isPromise = maybePromise instanceof Promise; | |
| return isPromise ? maybePromise.then(callback) : callback(maybePromise); | |
| }; | |
| let isPlainObject = (val) => val != null && typeof val === 'object' && !isArray(val); | |
| let isHexDigit = (chr) => | |
| (chr >= '0' && chr <= '9') || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F'); | |
| let escapables = { | |
| b: '\b', | |
| f: '\f', | |
| n: '\n', | |
| r: '\r', | |
| t: '\t', | |
| }; | |
| let keywords = { | |
| t: 'true', | |
| f: 'false', | |
| n: 'null', | |
| }; | |
| let Bare = Symbol('Bare'); | |
| let Keyword = Symbol('Keyword'); | |
| let Escape = Symbol('Escape'); | |
| let EscapeCode = Symbol('EscapeCode'); | |
| let String_ = Symbol('String'); | |
| function* __streamParseJSON(bytes) { | |
| let iter = decodeUTF8(bytes)[Symbol.for('@@streamIterator')](); | |
| let step = iter.next(); | |
| let keyword = null; | |
| let key = null; | |
| let value = undefined; | |
| let path = []; | |
| let state = Bare; | |
| for (;;) { | |
| if (step instanceof Promise) step = yield wait(step); | |
| if (step.done) break; | |
| let chr = step.value; | |
| switch (state) { | |
| case Bare: { | |
| switch (chr) { | |
| case ' ': | |
| case '\t': | |
| case '\r': | |
| case '\n': | |
| break; | |
| case ',': | |
| key = isPlainObject(value) ? null : value.length; | |
| break; | |
| case ':': | |
| if (!key) throw new Error(); | |
| break; | |
| case '{': | |
| if (value !== undefined) path.push([key, value]); | |
| value = {}; | |
| key = null; | |
| break; | |
| case '[': | |
| if (value !== undefined) path.push([key, value]); | |
| value = []; | |
| key = 0; | |
| break; | |
| case ']': | |
| case '}': { | |
| let parent = arrayLast(path)?.[1]; | |
| if (parent) { | |
| if (isPlainObject(value) && chr === ']') throw new Error(); | |
| let newValue = value; | |
| ({ 0: key, 1: value } = path.pop()); | |
| value[key] = newValue; | |
| } | |
| break; | |
| } | |
| case '"': | |
| case "'": | |
| if (value !== undefined) path.push([key, value]); | |
| state = String_; | |
| value = ''; | |
| break; | |
| case 't': | |
| case 'f': | |
| case 'n': | |
| if (value !== undefined) path.push([key, value]); | |
| keyword = keywords[chr]; | |
| state = Keyword; | |
| value = ''; | |
| continue; | |
| default: | |
| throw new Error(); | |
| } | |
| break; | |
| } | |
| case Keyword: { | |
| if (keyword[value.length] === chr) { | |
| value += chr; | |
| } else throw new Error(); | |
| if (value.length === keyword.length) { | |
| switch (keyword) { | |
| case 'true': | |
| value = true; | |
| break; | |
| case 'false': | |
| value = false; | |
| break; | |
| case 'null': | |
| value = null; | |
| break; | |
| default: | |
| throw new Error(); | |
| } | |
| let parent = arrayLast(path)[1]; | |
| parent[key] = value; | |
| ({ 0: key, 1: value } = path.pop()); | |
| state = Bare; | |
| } | |
| break; | |
| } | |
| case String_: { | |
| switch (chr) { | |
| case '"': | |
| case "'": | |
| let parent = arrayLast(path)?.[1]; | |
| if (parent) { | |
| if (isPlainObject(parent) && !key) { | |
| key = value; | |
| ({ 1: value } = path.pop()); | |
| } else { | |
| parent[key] = value; | |
| ({ 0: key, 1: value } = path.pop()); | |
| } | |
| } | |
| state = Bare; | |
| break; | |
| case '\\': | |
| state = Escape; | |
| break; | |
| default: | |
| value = value + chr; // TODO, optimize this | |
| break; | |
| } | |
| break; | |
| } | |
| case Escape: { | |
| switch (chr) { | |
| case '"': | |
| case '\\': | |
| case '//': | |
| case 'b': | |
| case 'f': | |
| case 'n': | |
| case 'r': | |
| case 't': | |
| value = value + escapables[chr] || chr; | |
| let parent = arrayLast(path)?.[1]; | |
| if (parent) { | |
| parent[key] = value; | |
| } | |
| state = String_; | |
| break; | |
| case 'u': | |
| path.push([key, value]); | |
| value = []; | |
| state = EscapeCode; | |
| break; | |
| } | |
| break; | |
| } | |
| case EscapeCode: { | |
| if (isHexDigit(chr)) { | |
| value += chr; | |
| } else throw new Error(); | |
| if (value.length === 4) { | |
| let code = parseInt(value.join(''), 16); | |
| let parent = arrayLast(path)[1]; | |
| state = String_; | |
| ({ 0: key, 1: value } = path.pop()); | |
| value += String.fromCharCode(code); | |
| } | |
| break; | |
| } | |
| } | |
| step = iter.next(); | |
| } | |
| return value; | |
| } | |
| const streamParseJSON = (input) => { | |
| let iter = new StreamGenerator(__streamParseJSON(input)); | |
| return maybeWait(iter.next(), (step) => step.value); | |
| }; | |
| let obj = await streamParseJSON(readFile('fixture.json')); | |
| debugger; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment