diff --git a/src/features/file/helpers.ts b/src/features/file/helpers.ts index a4b68bd..be0f77c 100644 --- a/src/features/file/helpers.ts +++ b/src/features/file/helpers.ts @@ -12,7 +12,7 @@ interface Contents extends Map> { } export const read = (file: File): Promise | undefined> => { switch (file.type) { - case 'application/json': return json.load(file.stream()); + case 'application/json': return json.load(file.text()); default: return Promise.resolve(undefined); } diff --git a/src/features/file/parser/json.ts b/src/features/file/parser/json.ts index 0db2a2d..2bd5317 100644 --- a/src/features/file/parser/json.ts +++ b/src/features/file/parser/json.ts @@ -1,200 +1,20 @@ import { decode } from "~/utilities"; -export async function load(stream: ReadableStream): Promise> { - return new Map(await Array.fromAsync(parse(stream), ({ key, value }) => [key, value])); -} +export async function load(text: Promise): Promise> { + const source = JSON.parse(await text); + const result = new Map(); + const candidates = Object.entries(source); -interface Entry { - key: string; - value: string; -} + while (candidates.length !== 0) { + const [ key, value ] = candidates.shift()!; -interface State { - (token: Token): State; - entry?: Entry -} - -const states = { - none(): State { - return (token: Token) => { - if (token.kind === 'braceOpen') { - return states.object(); - } - - return states.none; - }; - }, - object({ path = [], expect = 'key' }: Partial<{ path: string[], expect: 'key' | 'colon' | 'value' }> = {}): State { - return (token: Token) => { - switch (expect) { - case 'key': { - if (token.kind === 'braceClose') { - return states.object({ - path: path.slice(0, -1), - expect: 'key', - }); - } - else if (token.kind === 'string') { - return states.object({ - path: [...path, token.value], - expect: 'colon' - }); - } - - return states.error(`Expected a key, got ${token.kind} instead`); - } - - case 'colon': { - if (token.kind !== 'colon') { - return states.error(`Expected a ':', got ${token.kind} instead`); - } - - return states.object({ - path, - expect: 'value' - }); - } - - case 'value': { - if (token.kind === 'braceOpen') { - return states.object({ - path, - expect: 'key', - }); - } - else if (token.kind === 'string') { - const next = states.object({ - path: path.slice(0, -1), - expect: 'key', - }); - - next.entry = { key: path.join('.'), value: decode(token.value) }; - - return next - } - - return states.error(`Invalid value type found '${token.kind}'`); - } - } - - return states.none(); + if (typeof value !== 'object' || value === null || value === undefined) { + result.set(key, decode(value as string)); } - }, - error(message: string): State { - throw new Error(message); - - return states.none(); - }, -} as const; - -async function* parse(stream: ReadableStream): AsyncGenerator { - let state = states.none(); - - for await (const token of tokenize(read(toGenerator(stream)))) { - try { - state = state(token); - } - catch (e) { - console.error(e); - - break; - } - - if (state.entry) { - yield state.entry; + else { + candidates.unshift(...Object.entries(value).map<[string, any]>(([ k, v ]) => [`${key}.${k}`, v])); } } -} -async function* take(iterable: AsyncIterable, numberToTake: number): AsyncGenerator { - let i = 0; - for await (const entry of iterable) { - yield entry; - - i++; - - if (i === numberToTake) { - break; - } - } -} - -type Token = { start: number, length: number } & ( - | { kind: 'braceOpen' } - | { kind: 'braceClose' } - | { kind: 'colon' } - | { kind: 'string', value: string } -); - -async function* tokenize(characters: AsyncIterable): AsyncGenerator { - let buffer: string = ''; - let clearBuffer = false; - let start = 0; - let i = 0; - - for await (const character of characters) { - if (buffer.length === 0) { - start = i; - } - - buffer += String.fromCharCode(character); - const length = buffer.length; - - if (buffer === '{') { - yield { kind: 'braceOpen', start, length }; - clearBuffer = true; - } - else if (buffer === '}') { - yield { kind: 'braceClose', start, length }; - clearBuffer = true; - } - else if (buffer === ':') { - yield { kind: 'colon', start, length }; - clearBuffer = true; - } - else if (buffer.length > 1 && buffer.startsWith('"') && buffer.endsWith('"') && buffer.at(-2) !== '\\') { - yield { kind: 'string', start, length, value: buffer.slice(1, buffer.length - 1) }; - clearBuffer = true; - } - else if (buffer === ',') { - clearBuffer = true; - } - else if (buffer.trim() === '') { - clearBuffer = true; - } - - if (clearBuffer) { - buffer = ''; - clearBuffer = false; - } - - i++; - } -} - -async function* read(chunks: AsyncIterable): AsyncGenerator { - for await (const chunk of chunks) { - for (const character of chunk) { - yield character; - } - } -} - -async function* toGenerator(stream: ReadableStream): AsyncGenerator { - const reader = stream.getReader(); - - try { - while (true) { - const { done, value } = await reader.read(); - - if (done) { - break; - } - - yield value; - } - } - finally { - reader.releaseLock(); - } + return result; } \ No newline at end of file