@toon-format/toon
Advanced tools
+131
-25
@@ -65,5 +65,76 @@ //#region src/constants.d.ts | ||
| type ResolvedDecodeOptions = Readonly<Required<DecodeOptions>>; | ||
| /** | ||
| * Options for streaming decode operations. | ||
| * | ||
| * @remarks | ||
| * Path expansion is not supported in streaming mode. | ||
| */ | ||
| interface DecodeStreamOptions extends Omit<DecodeOptions, "expandPaths"> { | ||
| /** | ||
| * Path expansion is not supported in streaming decode. | ||
| * This option is explicitly omitted. | ||
| */ | ||
| expandPaths?: never; | ||
| } | ||
| type JsonStreamEvent = { | ||
| type: "startObject"; | ||
| } | { | ||
| type: "endObject"; | ||
| } | { | ||
| type: "startArray"; | ||
| length: number; | ||
| } | { | ||
| type: "endArray"; | ||
| } | { | ||
| type: "key"; | ||
| key: string; | ||
| wasQuoted?: boolean; | ||
| } | { | ||
| type: "primitive"; | ||
| value: JsonPrimitive; | ||
| }; | ||
| //#endregion | ||
| //#region src/index.d.ts | ||
| /** | ||
| * Encodes a JavaScript value into TOON format string. | ||
| * | ||
| * @param input - Any JavaScript value (objects, arrays, primitives) | ||
| * @param options - Optional encoding configuration | ||
| * @returns TOON formatted string | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * encode({ name: 'Alice', age: 30 }) | ||
| * // name: Alice | ||
| * // age: 30 | ||
| * | ||
| * encode({ users: [{ id: 1 }, { id: 2 }] }) | ||
| * // users[]: | ||
| * // - id: 1 | ||
| * // - id: 2 | ||
| * | ||
| * encode(data, { indent: 4, keyFolding: 'safe' }) | ||
| * ``` | ||
| */ | ||
| declare function encode(input: unknown, options?: EncodeOptions): string; | ||
| /** | ||
| * Decodes a TOON format string into a JavaScript value. | ||
| * | ||
| * @param input - TOON formatted string | ||
| * @param options - Optional decoding configuration | ||
| * @returns Parsed JavaScript value (object, array, or primitive) | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * decode('name: Alice\nage: 30') | ||
| * // { name: 'Alice', age: 30 } | ||
| * | ||
| * decode('users[]:\n - id: 1\n - id: 2') | ||
| * // { users: [{ id: 1 }, { id: 2 }] } | ||
| * | ||
| * decode(toonString, { strict: false, expandPaths: 'safe' }) | ||
| * ``` | ||
| */ | ||
| declare function decode(input: string, options?: DecodeOptions): JsonValue; | ||
| /** | ||
| * Encodes a JavaScript value into TOON format as a sequence of lines. | ||
@@ -94,43 +165,78 @@ * | ||
| /** | ||
| * Encodes a JavaScript value into TOON format string. | ||
| * Decodes TOON format from pre-split lines into a JavaScript value. | ||
| * | ||
| * @param input - Any JavaScript value (objects, arrays, primitives) | ||
| * @param options - Optional encoding configuration | ||
| * @returns TOON formatted string | ||
| * This is a convenience wrapper around the streaming decoder that builds | ||
| * the full value in memory. Useful when you already have lines as an array | ||
| * or iterable and want the standard decode behavior with path expansion support. | ||
| * | ||
| * @param lines - Iterable of TOON lines (without newlines) | ||
| * @param options - Optional decoding configuration (supports expandPaths) | ||
| * @returns Parsed JavaScript value (object, array, or primitive) | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * encode({ name: 'Alice', age: 30 }) | ||
| * // name: Alice | ||
| * // age: 30 | ||
| * const lines = ['name: Alice', 'age: 30'] | ||
| * decodeFromLines(lines) | ||
| * // { name: 'Alice', age: 30 } | ||
| * ``` | ||
| */ | ||
| declare function decodeFromLines(lines: Iterable<string>, options?: DecodeOptions): JsonValue; | ||
| /** | ||
| * Synchronously decodes TOON lines into a stream of JSON events. | ||
| * | ||
| * encode({ users: [{ id: 1 }, { id: 2 }] }) | ||
| * // users[]: | ||
| * // - id: 1 | ||
| * // - id: 2 | ||
| * This function yields structured events (startObject, endObject, startArray, endArray, | ||
| * key, primitive) that represent the JSON data model without building the full value tree. | ||
| * Useful for streaming processing, custom transformations, or memory-efficient parsing. | ||
| * | ||
| * encode(data, { indent: 4, keyFolding: 'safe' }) | ||
| * @remarks | ||
| * Path expansion (`expandPaths: 'safe'`) is not supported in streaming mode. | ||
| * | ||
| * @param lines - Iterable of TOON lines (without newlines) | ||
| * @param options - Optional decoding configuration (expandPaths not supported) | ||
| * @returns Iterable of JSON stream events | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const lines = ['name: Alice', 'age: 30'] | ||
| * for (const event of decodeStreamSync(lines)) { | ||
| * console.log(event) | ||
| * // { type: 'startObject' } | ||
| * // { type: 'key', key: 'name' } | ||
| * // { type: 'primitive', value: 'Alice' } | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare function encode(input: unknown, options?: EncodeOptions): string; | ||
| declare function decodeStreamSync(lines: Iterable<string>, options?: DecodeStreamOptions): Iterable<JsonStreamEvent>; | ||
| /** | ||
| * Decodes a TOON format string into a JavaScript value. | ||
| * Asynchronously decodes TOON lines into a stream of JSON events. | ||
| * | ||
| * @param input - TOON formatted string | ||
| * @param options - Optional decoding configuration | ||
| * @returns Parsed JavaScript value (object, array, or primitive) | ||
| * This function yields structured events (startObject, endObject, startArray, endArray, | ||
| * key, primitive) that represent the JSON data model without building the full value tree. | ||
| * Supports both sync and async iterables for maximum flexibility with file streams, | ||
| * network responses, or other async sources. | ||
| * | ||
| * @remarks | ||
| * Path expansion (`expandPaths: 'safe'`) is not supported in streaming mode. | ||
| * | ||
| * @param source - Async or sync iterable of TOON lines (without newlines) | ||
| * @param options - Optional decoding configuration (expandPaths not supported) | ||
| * @returns Async iterable of JSON stream events | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * decode('name: Alice\nage: 30') | ||
| * // { name: 'Alice', age: 30 } | ||
| * const fileStream = createReadStream('data.toon', 'utf-8') | ||
| * const lines = splitLines(fileStream) // Async iterable of lines | ||
| * | ||
| * decode('users[]:\n - id: 1\n - id: 2') | ||
| * // { users: [{ id: 1 }, { id: 2 }] } | ||
| * | ||
| * decode(toonString, { strict: false, expandPaths: 'safe' }) | ||
| * for await (const event of decodeStream(lines)) { | ||
| * console.log(event) | ||
| * // { type: 'startObject' } | ||
| * // { type: 'key', key: 'name' } | ||
| * // { type: 'primitive', value: 'Alice' } | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare function decode(input: string, options?: DecodeOptions): JsonValue; | ||
| declare function decodeStream(source: AsyncIterable<string> | Iterable<string>, options?: DecodeStreamOptions): AsyncIterable<JsonStreamEvent>; | ||
| //#endregion | ||
| export { DEFAULT_DELIMITER, DELIMITERS, type DecodeOptions, type Delimiter, type DelimiterKey, type EncodeOptions, type JsonArray, type JsonObject, type JsonPrimitive, type JsonValue, type ResolvedDecodeOptions, type ResolvedEncodeOptions, decode, encode, encodeLines }; | ||
| export { DEFAULT_DELIMITER, DELIMITERS, type DecodeOptions, type DecodeStreamOptions, type Delimiter, type DelimiterKey, type EncodeOptions, type JsonArray, type JsonObject, type JsonPrimitive, type JsonStreamEvent, type JsonValue, type ResolvedDecodeOptions, type ResolvedEncodeOptions, decode, decodeFromLines, decodeStream, decodeStreamSync, encode, encodeLines }; |
+938
-461
@@ -122,54 +122,2 @@ //#region src/constants.ts | ||
| //#endregion | ||
| //#region src/encode/normalize.ts | ||
| function normalizeValue(value) { | ||
| if (value === null) return null; | ||
| if (typeof value === "string" || typeof value === "boolean") return value; | ||
| if (typeof value === "number") { | ||
| if (Object.is(value, -0)) return 0; | ||
| if (!Number.isFinite(value)) return null; | ||
| return value; | ||
| } | ||
| if (typeof value === "bigint") { | ||
| if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) return Number(value); | ||
| return value.toString(); | ||
| } | ||
| if (value instanceof Date) return value.toISOString(); | ||
| if (Array.isArray(value)) return value.map(normalizeValue); | ||
| if (value instanceof Set) return Array.from(value).map(normalizeValue); | ||
| if (value instanceof Map) return Object.fromEntries(Array.from(value, ([k, v]) => [String(k), normalizeValue(v)])); | ||
| if (isPlainObject(value)) { | ||
| const normalized = {}; | ||
| for (const key in value) if (Object.prototype.hasOwnProperty.call(value, key)) normalized[key] = normalizeValue(value[key]); | ||
| return normalized; | ||
| } | ||
| return null; | ||
| } | ||
| function isJsonPrimitive(value) { | ||
| return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean"; | ||
| } | ||
| function isJsonArray(value) { | ||
| return Array.isArray(value); | ||
| } | ||
| function isJsonObject(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| function isEmptyObject(value) { | ||
| return Object.keys(value).length === 0; | ||
| } | ||
| function isPlainObject(value) { | ||
| if (value === null || typeof value !== "object") return false; | ||
| const prototype = Object.getPrototypeOf(value); | ||
| return prototype === null || prototype === Object.prototype; | ||
| } | ||
| function isArrayOfPrimitives(value) { | ||
| return value.length === 0 || value.every((item) => isJsonPrimitive(item)); | ||
| } | ||
| function isArrayOfArrays(value) { | ||
| return value.length === 0 || value.every((item) => isJsonArray(item)); | ||
| } | ||
| function isArrayOfObjects(value) { | ||
| return value.length === 0 || value.every((item) => isJsonObject(item)); | ||
| } | ||
| //#endregion | ||
| //#region src/shared/literal-utils.ts | ||
@@ -193,191 +141,2 @@ function isBooleanOrNullLiteral(token) { | ||
| //#endregion | ||
| //#region src/shared/validation.ts | ||
| /** | ||
| * Checks if a key can be used without quotes. | ||
| * | ||
| * @remarks | ||
| * Valid unquoted keys must start with a letter or underscore, | ||
| * followed by letters, digits, underscores, or dots. | ||
| */ | ||
| function isValidUnquotedKey(key) { | ||
| return /^[A-Z_][\w.]*$/i.test(key); | ||
| } | ||
| /** | ||
| * Checks if a key segment is a valid identifier for safe folding/expansion. | ||
| * | ||
| * @remarks | ||
| * Identifier segments are more restrictive than unquoted keys: | ||
| * - Must start with a letter or underscore | ||
| * - Followed only by letters, digits, or underscores (no dots) | ||
| * - Used for safe key folding and path expansion | ||
| */ | ||
| function isIdentifierSegment(key) { | ||
| return /^[A-Z_]\w*$/i.test(key); | ||
| } | ||
| /** | ||
| * Determines if a string value can be safely encoded without quotes. | ||
| * | ||
| * @remarks | ||
| * A string needs quoting if it: | ||
| * - Is empty | ||
| * - Has leading or trailing whitespace | ||
| * - Could be confused with a literal (boolean, null, number) | ||
| * - Contains structural characters (colons, brackets, braces) | ||
| * - Contains quotes or backslashes (need escaping) | ||
| * - Contains control characters (newlines, tabs, etc.) | ||
| * - Contains the active delimiter | ||
| * - Starts with a list marker (hyphen) | ||
| */ | ||
| function isSafeUnquoted(value, delimiter = DEFAULT_DELIMITER) { | ||
| if (!value) return false; | ||
| if (value !== value.trim()) return false; | ||
| if (isBooleanOrNullLiteral(value) || isNumericLike(value)) return false; | ||
| if (value.includes(":")) return false; | ||
| if (value.includes("\"") || value.includes("\\")) return false; | ||
| if (/[[\]{}]/.test(value)) return false; | ||
| if (/[\n\r\t]/.test(value)) return false; | ||
| if (value.includes(delimiter)) return false; | ||
| if (value.startsWith(LIST_ITEM_MARKER)) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Checks if a string looks like a number. | ||
| * | ||
| * @remarks | ||
| * Match numbers like `42`, `-3.14`, `1e-6`, `05`, etc. | ||
| */ | ||
| function isNumericLike(value) { | ||
| return /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value) || /^0\d+$/.test(value); | ||
| } | ||
| //#endregion | ||
| //#region src/decode/expand.ts | ||
| /** | ||
| * Symbol used to mark object keys that were originally quoted in the TOON source. | ||
| * Quoted dotted keys should not be expanded, even if they meet expansion criteria. | ||
| */ | ||
| const QUOTED_KEY_MARKER = Symbol("quotedKey"); | ||
| /** | ||
| * Expands dotted keys into nested objects in safe mode. | ||
| * | ||
| * @remarks | ||
| * This function recursively traverses a decoded TOON value and expands any keys | ||
| * containing dots (`.`) into nested object structures, provided all segments | ||
| * are valid identifiers. | ||
| * | ||
| * Expansion rules: | ||
| * - Keys containing dots are split into segments | ||
| * - All segments must pass `isIdentifierSegment` validation | ||
| * - Non-eligible keys (with special characters) are left as literal dotted keys | ||
| * - Deep merge: When multiple dotted keys expand to the same path, their values are merged if both are objects | ||
| * - Conflict handling: | ||
| * - `strict=true`: Throws TypeError on conflicts (non-object collision) | ||
| * - `strict=false`: LWW (silent overwrite) | ||
| * | ||
| * @param value - The decoded value to expand | ||
| * @param strict - Whether to throw errors on conflicts | ||
| * @returns The expanded value with dotted keys reconstructed as nested objects | ||
| * @throws TypeError if conflicts occur in strict mode | ||
| */ | ||
| function expandPathsSafe(value, strict) { | ||
| if (Array.isArray(value)) return value.map((item) => expandPathsSafe(item, strict)); | ||
| if (isJsonObject(value)) { | ||
| const expandedObject = {}; | ||
| const quotedKeys = value[QUOTED_KEY_MARKER]; | ||
| for (const [key, keyValue] of Object.entries(value)) { | ||
| const isQuoted = quotedKeys?.has(key); | ||
| if (key.includes(DOT) && !isQuoted) { | ||
| const segments = key.split(DOT); | ||
| if (segments.every((seg) => isIdentifierSegment(seg))) { | ||
| insertPathSafe(expandedObject, segments, expandPathsSafe(keyValue, strict), strict); | ||
| continue; | ||
| } | ||
| } | ||
| const expandedValue = expandPathsSafe(keyValue, strict); | ||
| if (key in expandedObject) { | ||
| const conflictingValue = expandedObject[key]; | ||
| if (canMerge(conflictingValue, expandedValue)) mergeObjects(conflictingValue, expandedValue, strict); | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at key "${key}": cannot merge ${typeof conflictingValue} with ${typeof expandedValue}`); | ||
| expandedObject[key] = expandedValue; | ||
| } | ||
| } else expandedObject[key] = expandedValue; | ||
| } | ||
| return expandedObject; | ||
| } | ||
| return value; | ||
| } | ||
| /** | ||
| * Inserts a value at a nested path, creating intermediate objects as needed. | ||
| * | ||
| * @remarks | ||
| * This function walks the segment path, creating nested objects as needed. | ||
| * When an existing value is encountered: | ||
| * - If both are objects: deep merge (continue insertion) | ||
| * - If values differ: conflict | ||
| * - strict=true: throw TypeError | ||
| * - strict=false: overwrite with new value (LWW) | ||
| * | ||
| * @param target - The object to insert into | ||
| * @param segments - Array of path segments (e.g., ['data', 'metadata', 'items']) | ||
| * @param value - The value to insert at the end of the path | ||
| * @param strict - Whether to throw on conflicts | ||
| * @throws TypeError if a conflict occurs in strict mode | ||
| */ | ||
| function insertPathSafe(target, segments, value, strict) { | ||
| let currentNode = target; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const currentSegment = segments[i]; | ||
| const segmentValue = currentNode[currentSegment]; | ||
| if (segmentValue === void 0) { | ||
| const newObj = {}; | ||
| currentNode[currentSegment] = newObj; | ||
| currentNode = newObj; | ||
| } else if (isJsonObject(segmentValue)) currentNode = segmentValue; | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at segment "${currentSegment}": expected object but found ${typeof segmentValue}`); | ||
| const newObj = {}; | ||
| currentNode[currentSegment] = newObj; | ||
| currentNode = newObj; | ||
| } | ||
| } | ||
| const lastSeg = segments[segments.length - 1]; | ||
| const destinationValue = currentNode[lastSeg]; | ||
| if (destinationValue === void 0) currentNode[lastSeg] = value; | ||
| else if (canMerge(destinationValue, value)) mergeObjects(destinationValue, value, strict); | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at key "${lastSeg}": cannot merge ${typeof destinationValue} with ${typeof value}`); | ||
| currentNode[lastSeg] = value; | ||
| } | ||
| } | ||
| /** | ||
| * Deep merges properties from source into target. | ||
| * | ||
| * @remarks | ||
| * For each key in source: | ||
| * - If key doesn't exist in target: copy it | ||
| * - If both values are objects: recursively merge | ||
| * - Otherwise: conflict (strict throws, non-strict overwrites) | ||
| * | ||
| * @param target - The target object to merge into | ||
| * @param source - The source object to merge from | ||
| * @param strict - Whether to throw on conflicts | ||
| * @throws TypeError if a conflict occurs in strict mode | ||
| */ | ||
| function mergeObjects(target, source, strict) { | ||
| for (const [key, sourceValue] of Object.entries(source)) { | ||
| const targetValue = target[key]; | ||
| if (targetValue === void 0) target[key] = sourceValue; | ||
| else if (canMerge(targetValue, sourceValue)) mergeObjects(targetValue, sourceValue, strict); | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at key "${key}": cannot merge ${typeof targetValue} with ${typeof sourceValue}`); | ||
| target[key] = sourceValue; | ||
| } | ||
| } | ||
| } | ||
| function canMerge(a, b) { | ||
| return isJsonObject(a) && isJsonObject(b); | ||
| } | ||
| //#endregion | ||
| //#region src/decode/parser.ts | ||
@@ -549,6 +308,6 @@ function parseArrayHeaderLine(content, defaultDelimiter) { | ||
| } | ||
| function isArrayHeaderAfterHyphen(content) { | ||
| function isArrayHeaderContent(content) { | ||
| return content.trim().startsWith(OPEN_BRACKET) && findUnquotedChar(content, COLON) !== -1; | ||
| } | ||
| function isObjectFirstFieldAfterHyphen(content) { | ||
| function isKeyValueContent(content) { | ||
| return findUnquotedChar(content, COLON) !== -1; | ||
@@ -558,2 +317,56 @@ } | ||
| //#endregion | ||
| //#region src/decode/scanner.ts | ||
| function createScanState() { | ||
| return { | ||
| lineNumber: 0, | ||
| blankLines: [] | ||
| }; | ||
| } | ||
| function parseLineIncremental(raw, state, indentSize, strict) { | ||
| state.lineNumber++; | ||
| const lineNumber = state.lineNumber; | ||
| let indent = 0; | ||
| while (indent < raw.length && raw[indent] === SPACE) indent++; | ||
| const content = raw.slice(indent); | ||
| if (!content.trim()) { | ||
| const depth$1 = computeDepthFromIndent(indent, indentSize); | ||
| state.blankLines.push({ | ||
| lineNumber, | ||
| indent, | ||
| depth: depth$1 | ||
| }); | ||
| return; | ||
| } | ||
| const depth = computeDepthFromIndent(indent, indentSize); | ||
| if (strict) { | ||
| let whitespaceEndIndex = 0; | ||
| while (whitespaceEndIndex < raw.length && (raw[whitespaceEndIndex] === SPACE || raw[whitespaceEndIndex] === TAB)) whitespaceEndIndex++; | ||
| if (raw.slice(0, whitespaceEndIndex).includes(TAB)) throw new SyntaxError(`Line ${lineNumber}: Tabs are not allowed in indentation in strict mode`); | ||
| if (indent > 0 && indent % indentSize !== 0) throw new SyntaxError(`Line ${lineNumber}: Indentation must be exact multiple of ${indentSize}, but found ${indent} spaces`); | ||
| } | ||
| return { | ||
| raw, | ||
| indent, | ||
| content, | ||
| depth, | ||
| lineNumber | ||
| }; | ||
| } | ||
| function* parseLinesSync(source, indentSize, strict, state) { | ||
| for (const raw of source) { | ||
| const parsedLine = parseLineIncremental(raw, state, indentSize, strict); | ||
| if (parsedLine !== void 0) yield parsedLine; | ||
| } | ||
| } | ||
| async function* parseLinesAsync(source, indentSize, strict, state) { | ||
| for await (const raw of source) { | ||
| const parsedLine = parseLineIncremental(raw, state, indentSize, strict); | ||
| if (parsedLine !== void 0) yield parsedLine; | ||
| } | ||
| } | ||
| function computeDepthFromIndent(indentSpaces, indentSize) { | ||
| return Math.floor(indentSpaces / indentSize); | ||
| } | ||
| //#endregion | ||
| //#region src/decode/validation.ts | ||
@@ -569,4 +382,3 @@ /** | ||
| */ | ||
| function validateNoExtraListItems(cursor, itemDepth, expectedCount) { | ||
| const nextLine = cursor.peek(); | ||
| function validateNoExtraListItems(nextLine, itemDepth, expectedCount) { | ||
| if (nextLine?.depth === itemDepth && nextLine.content.startsWith(LIST_ITEM_PREFIX)) throw new RangeError(`Expected ${expectedCount} list array items, but found more`); | ||
@@ -577,4 +389,3 @@ } | ||
| */ | ||
| function validateNoExtraTabularRows(cursor, rowDepth, header) { | ||
| const nextLine = cursor.peek(); | ||
| function validateNoExtraTabularRows(nextLine, rowDepth, header) { | ||
| if (nextLine?.depth === rowDepth && !nextLine.content.startsWith(LIST_ITEM_PREFIX) && isDataRow(nextLine.content, header.delimiter)) throw new RangeError(`Expected ${header.length} tabular rows, but found more`); | ||
@@ -603,120 +414,412 @@ } | ||
| //#region src/decode/decoders.ts | ||
| function decodeValueFromLines(cursor, options) { | ||
| const first = cursor.peek(); | ||
| if (!first) throw new ReferenceError("No content to decode"); | ||
| if (isArrayHeaderAfterHyphen(first.content)) { | ||
| var StreamingLineCursor = class { | ||
| buffer = []; | ||
| generator; | ||
| done = false; | ||
| lastLine; | ||
| scanState; | ||
| constructor(generator, scanState) { | ||
| this.generator = generator; | ||
| this.scanState = scanState; | ||
| } | ||
| getBlankLines() { | ||
| return this.scanState.blankLines; | ||
| } | ||
| async peek() { | ||
| if (this.buffer.length > 0) return this.buffer[0]; | ||
| if (this.done) return; | ||
| const result = await this.generator.next(); | ||
| if (result.done) { | ||
| this.done = true; | ||
| return; | ||
| } | ||
| this.buffer.push(result.value); | ||
| return result.value; | ||
| } | ||
| async next() { | ||
| const line = await this.peek(); | ||
| if (line !== void 0) { | ||
| this.buffer.shift(); | ||
| this.lastLine = line; | ||
| } | ||
| return line; | ||
| } | ||
| async advance() { | ||
| await this.next(); | ||
| } | ||
| current() { | ||
| return this.lastLine; | ||
| } | ||
| async atEnd() { | ||
| return await this.peek() === void 0; | ||
| } | ||
| peekSync() { | ||
| if (this.buffer.length > 0) return this.buffer[0]; | ||
| if (this.done) return; | ||
| const result = this.generator.next(); | ||
| if (result.done) { | ||
| this.done = true; | ||
| return; | ||
| } | ||
| this.buffer.push(result.value); | ||
| return result.value; | ||
| } | ||
| nextSync() { | ||
| const line = this.peekSync(); | ||
| if (line !== void 0) { | ||
| this.buffer.shift(); | ||
| this.lastLine = line; | ||
| } | ||
| return line; | ||
| } | ||
| advanceSync() { | ||
| this.nextSync(); | ||
| } | ||
| atEndSync() { | ||
| return this.peekSync() === void 0; | ||
| } | ||
| }; | ||
| function* decodeStreamSync$1(source, options) { | ||
| if (options?.expandPaths !== void 0) throw new Error("expandPaths is not supported in streaming decode"); | ||
| const resolvedOptions = { | ||
| indent: options?.indent ?? 2, | ||
| strict: options?.strict ?? true | ||
| }; | ||
| const scanState = createScanState(); | ||
| const cursor = new StreamingLineCursor(parseLinesSync(source, resolvedOptions.indent, resolvedOptions.strict, scanState), scanState); | ||
| const first = cursor.peekSync(); | ||
| if (!first) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (isArrayHeaderContent(first.content)) { | ||
| const headerInfo = parseArrayHeaderLine(first.content, DEFAULT_DELIMITER); | ||
| if (headerInfo) { | ||
| cursor.advance(); | ||
| return decodeArrayFromHeader(headerInfo.header, headerInfo.inlineValues, cursor, 0, options); | ||
| cursor.advanceSync(); | ||
| yield* decodeArrayFromHeaderSync(headerInfo.header, headerInfo.inlineValues, cursor, 0, resolvedOptions); | ||
| return; | ||
| } | ||
| } | ||
| if (cursor.length === 1 && !isKeyValueLine(first)) return parsePrimitiveToken(first.content.trim()); | ||
| return decodeObject(cursor, 0, options); | ||
| cursor.advanceSync(); | ||
| if (!!cursor.atEndSync() && !isKeyValueLineSync(first)) { | ||
| yield { | ||
| type: "primitive", | ||
| value: parsePrimitiveToken(first.content.trim()) | ||
| }; | ||
| return; | ||
| } | ||
| yield { type: "startObject" }; | ||
| yield* decodeKeyValueSync(first.content, cursor, 0, resolvedOptions); | ||
| while (!cursor.atEndSync()) { | ||
| const line = cursor.peekSync(); | ||
| if (!line || line.depth !== 0) break; | ||
| cursor.advanceSync(); | ||
| yield* decodeKeyValueSync(line.content, cursor, 0, resolvedOptions); | ||
| } | ||
| yield { type: "endObject" }; | ||
| } | ||
| function isKeyValueLine(line) { | ||
| const content = line.content; | ||
| if (content.startsWith("\"")) { | ||
| const closingQuoteIndex = findClosingQuote(content, 0); | ||
| if (closingQuoteIndex === -1) return false; | ||
| return content.slice(closingQuoteIndex + 1).includes(COLON); | ||
| } else return content.includes(COLON); | ||
| function* decodeKeyValueSync(content, cursor, baseDepth, options) { | ||
| const arrayHeader = parseArrayHeaderLine(content, DEFAULT_DELIMITER); | ||
| if (arrayHeader && arrayHeader.header.key) { | ||
| yield { | ||
| type: "key", | ||
| key: arrayHeader.header.key | ||
| }; | ||
| yield* decodeArrayFromHeaderSync(arrayHeader.header, arrayHeader.inlineValues, cursor, baseDepth, options); | ||
| return; | ||
| } | ||
| const { key, isQuoted } = parseKeyToken(content, 0); | ||
| const colonIndex = content.indexOf(COLON, key.length); | ||
| const rest = colonIndex >= 0 ? content.slice(colonIndex + 1).trim() : ""; | ||
| yield isQuoted ? { | ||
| type: "key", | ||
| key, | ||
| wasQuoted: true | ||
| } : { | ||
| type: "key", | ||
| key | ||
| }; | ||
| if (!rest) { | ||
| const nextLine = cursor.peekSync(); | ||
| if (nextLine && nextLine.depth > baseDepth) { | ||
| yield { type: "startObject" }; | ||
| yield* decodeObjectFieldsSync(cursor, baseDepth + 1, options); | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| yield { | ||
| type: "primitive", | ||
| value: parsePrimitiveToken(rest) | ||
| }; | ||
| } | ||
| function decodeObject(cursor, baseDepth, options) { | ||
| const obj = {}; | ||
| const quotedKeys = /* @__PURE__ */ new Set(); | ||
| function* decodeObjectFieldsSync(cursor, baseDepth, options) { | ||
| let computedDepth; | ||
| while (!cursor.atEnd()) { | ||
| const line = cursor.peek(); | ||
| while (!cursor.atEndSync()) { | ||
| const line = cursor.peekSync(); | ||
| if (!line || line.depth < baseDepth) break; | ||
| if (computedDepth === void 0 && line.depth >= baseDepth) computedDepth = line.depth; | ||
| if (line.depth === computedDepth) { | ||
| cursor.advance(); | ||
| const { key, value, isQuoted } = decodeKeyValue(line.content, cursor, computedDepth, options); | ||
| obj[key] = value; | ||
| if (isQuoted && key.includes(DOT)) quotedKeys.add(key); | ||
| cursor.advanceSync(); | ||
| yield* decodeKeyValueSync(line.content, cursor, computedDepth, options); | ||
| } else break; | ||
| } | ||
| if (quotedKeys.size > 0) obj[QUOTED_KEY_MARKER] = quotedKeys; | ||
| return obj; | ||
| } | ||
| function decodeKeyValue(content, cursor, baseDepth, options) { | ||
| const arrayHeader = parseArrayHeaderLine(content, DEFAULT_DELIMITER); | ||
| if (arrayHeader && arrayHeader.header.key) { | ||
| const decodedValue = decodeArrayFromHeader(arrayHeader.header, arrayHeader.inlineValues, cursor, baseDepth, options); | ||
| return { | ||
| key: arrayHeader.header.key, | ||
| value: decodedValue, | ||
| followDepth: baseDepth + 1, | ||
| isQuoted: false | ||
| }; | ||
| function* decodeArrayFromHeaderSync(header, inlineValues, cursor, baseDepth, options) { | ||
| yield { | ||
| type: "startArray", | ||
| length: header.length | ||
| }; | ||
| if (inlineValues) { | ||
| yield* decodeInlinePrimitiveArraySync(header, inlineValues, options); | ||
| yield { type: "endArray" }; | ||
| return; | ||
| } | ||
| const { key, end, isQuoted } = parseKeyToken(content, 0); | ||
| const rest = content.slice(end).trim(); | ||
| if (!rest) { | ||
| const nextLine = cursor.peek(); | ||
| if (nextLine && nextLine.depth > baseDepth) return { | ||
| key, | ||
| value: decodeObject(cursor, baseDepth + 1, options), | ||
| followDepth: baseDepth + 1, | ||
| isQuoted | ||
| }; | ||
| return { | ||
| key, | ||
| value: {}, | ||
| followDepth: baseDepth + 1, | ||
| isQuoted | ||
| }; | ||
| if (header.fields && header.fields.length > 0) { | ||
| yield* decodeTabularArraySync(header, cursor, baseDepth, options); | ||
| yield { type: "endArray" }; | ||
| return; | ||
| } | ||
| return { | ||
| key, | ||
| value: parsePrimitiveToken(rest), | ||
| followDepth: baseDepth + 1, | ||
| isQuoted | ||
| }; | ||
| yield* decodeListArraySync(header, cursor, baseDepth, options); | ||
| yield { type: "endArray" }; | ||
| } | ||
| function decodeArrayFromHeader(header, inlineValues, cursor, baseDepth, options) { | ||
| if (inlineValues) return decodeInlinePrimitiveArray(header, inlineValues, options); | ||
| if (header.fields && header.fields.length > 0) return decodeTabularArray(header, cursor, baseDepth, options); | ||
| return decodeListArray(header, cursor, baseDepth, options); | ||
| } | ||
| function decodeInlinePrimitiveArray(header, inlineValues, options) { | ||
| function* decodeInlinePrimitiveArraySync(header, inlineValues, options) { | ||
| if (!inlineValues.trim()) { | ||
| assertExpectedCount(0, header.length, "inline array items", options); | ||
| return []; | ||
| return; | ||
| } | ||
| const primitives = mapRowValuesToPrimitives(parseDelimitedValues(inlineValues, header.delimiter)); | ||
| assertExpectedCount(primitives.length, header.length, "inline array items", options); | ||
| return primitives; | ||
| for (const primitive of primitives) yield { | ||
| type: "primitive", | ||
| value: primitive | ||
| }; | ||
| } | ||
| function decodeListArray(header, cursor, baseDepth, options) { | ||
| const items = []; | ||
| function* decodeTabularArraySync(header, cursor, baseDepth, options) { | ||
| const rowDepth = baseDepth + 1; | ||
| let rowCount = 0; | ||
| let startLine; | ||
| let endLine; | ||
| while (!cursor.atEndSync() && rowCount < header.length) { | ||
| const line = cursor.peekSync(); | ||
| if (!line || line.depth < rowDepth) break; | ||
| if (line.depth === rowDepth) { | ||
| if (startLine === void 0) startLine = line.lineNumber; | ||
| endLine = line.lineNumber; | ||
| cursor.advanceSync(); | ||
| const values = parseDelimitedValues(line.content, header.delimiter); | ||
| assertExpectedCount(values.length, header.fields.length, "tabular row values", options); | ||
| const primitives = mapRowValuesToPrimitives(values); | ||
| yield { type: "startObject" }; | ||
| for (let i = 0; i < header.fields.length; i++) { | ||
| yield { | ||
| type: "key", | ||
| key: header.fields[i] | ||
| }; | ||
| yield { | ||
| type: "primitive", | ||
| value: primitives[i] | ||
| }; | ||
| } | ||
| yield { type: "endObject" }; | ||
| rowCount++; | ||
| } else break; | ||
| } | ||
| assertExpectedCount(rowCount, header.length, "tabular rows", options); | ||
| if (options.strict && startLine !== void 0 && endLine !== void 0) validateNoBlankLinesInRange(startLine, endLine, cursor.getBlankLines(), options.strict, "tabular array"); | ||
| if (options.strict) validateNoExtraTabularRows(cursor.peekSync(), rowDepth, header); | ||
| } | ||
| function* decodeListArraySync(header, cursor, baseDepth, options) { | ||
| const itemDepth = baseDepth + 1; | ||
| let itemCount = 0; | ||
| let startLine; | ||
| let endLine; | ||
| while (!cursor.atEnd() && items.length < header.length) { | ||
| const line = cursor.peek(); | ||
| while (!cursor.atEndSync() && itemCount < header.length) { | ||
| const line = cursor.peekSync(); | ||
| if (!line || line.depth < itemDepth) break; | ||
| const isListItem = line.content.startsWith(LIST_ITEM_PREFIX) || line.content === "-"; | ||
| const isListItem = line.content.startsWith(LIST_ITEM_PREFIX) || line.content === LIST_ITEM_MARKER; | ||
| if (line.depth === itemDepth && isListItem) { | ||
| if (startLine === void 0) startLine = line.lineNumber; | ||
| endLine = line.lineNumber; | ||
| const item = decodeListItem(cursor, itemDepth, options); | ||
| items.push(item); | ||
| yield* decodeListItemSync(cursor, itemDepth, options); | ||
| const currentLine = cursor.current(); | ||
| if (currentLine) endLine = currentLine.lineNumber; | ||
| itemCount++; | ||
| } else break; | ||
| } | ||
| assertExpectedCount(items.length, header.length, "list array items", options); | ||
| assertExpectedCount(itemCount, header.length, "list array items", options); | ||
| if (options.strict && startLine !== void 0 && endLine !== void 0) validateNoBlankLinesInRange(startLine, endLine, cursor.getBlankLines(), options.strict, "list array"); | ||
| if (options.strict) validateNoExtraListItems(cursor, itemDepth, header.length); | ||
| return items; | ||
| if (options.strict) validateNoExtraListItems(cursor.peekSync(), itemDepth, header.length); | ||
| } | ||
| function decodeTabularArray(header, cursor, baseDepth, options) { | ||
| const objects = []; | ||
| function* decodeListItemSync(cursor, baseDepth, options) { | ||
| const line = cursor.nextSync(); | ||
| if (!line) throw new ReferenceError("Expected list item"); | ||
| let afterHyphen; | ||
| if (line.content === LIST_ITEM_MARKER) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } else if (line.content.startsWith(LIST_ITEM_PREFIX)) afterHyphen = line.content.slice(LIST_ITEM_PREFIX.length); | ||
| else throw new SyntaxError(`Expected list item to start with "${LIST_ITEM_PREFIX}"`); | ||
| if (!afterHyphen.trim()) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (isArrayHeaderContent(afterHyphen)) { | ||
| const arrayHeader = parseArrayHeaderLine(afterHyphen, DEFAULT_DELIMITER); | ||
| if (arrayHeader) { | ||
| yield* decodeArrayFromHeaderSync(arrayHeader.header, arrayHeader.inlineValues, cursor, baseDepth, options); | ||
| return; | ||
| } | ||
| } | ||
| if (isKeyValueContent(afterHyphen)) { | ||
| yield { type: "startObject" }; | ||
| yield* decodeKeyValueSync(afterHyphen, cursor, baseDepth, options); | ||
| const followDepth = baseDepth + 1; | ||
| while (!cursor.atEndSync()) { | ||
| const nextLine = cursor.peekSync(); | ||
| if (!nextLine || nextLine.depth < followDepth) break; | ||
| if (nextLine.depth === followDepth && !nextLine.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| cursor.advanceSync(); | ||
| yield* decodeKeyValueSync(nextLine.content, cursor, followDepth, options); | ||
| } else break; | ||
| } | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| yield { | ||
| type: "primitive", | ||
| value: parsePrimitiveToken(afterHyphen) | ||
| }; | ||
| } | ||
| function isKeyValueLineSync(line) { | ||
| const content = line.content; | ||
| if (content.startsWith("\"")) { | ||
| const closingQuoteIndex = findClosingQuote(content, 0); | ||
| if (closingQuoteIndex === -1) return false; | ||
| return content.slice(closingQuoteIndex + 1).includes(COLON); | ||
| } else return content.includes(COLON); | ||
| } | ||
| async function* decodeStream$1(source, options) { | ||
| if (options?.expandPaths !== void 0) throw new Error("expandPaths is not supported in streaming decode"); | ||
| const resolvedOptions = { | ||
| indent: options?.indent ?? 2, | ||
| strict: options?.strict ?? true | ||
| }; | ||
| const scanState = createScanState(); | ||
| if (Symbol.asyncIterator in source) { | ||
| const cursor = new StreamingLineCursor(parseLinesAsync(source, resolvedOptions.indent, resolvedOptions.strict, scanState), scanState); | ||
| const first = await cursor.peek(); | ||
| if (!first) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (isArrayHeaderContent(first.content)) { | ||
| const headerInfo = parseArrayHeaderLine(first.content, DEFAULT_DELIMITER); | ||
| if (headerInfo) { | ||
| await cursor.advance(); | ||
| yield* decodeArrayFromHeaderAsync(headerInfo.header, headerInfo.inlineValues, cursor, 0, resolvedOptions); | ||
| return; | ||
| } | ||
| } | ||
| await cursor.advance(); | ||
| if (!!await cursor.atEnd() && !isKeyValueLineSync(first)) { | ||
| yield { | ||
| type: "primitive", | ||
| value: parsePrimitiveToken(first.content.trim()) | ||
| }; | ||
| return; | ||
| } | ||
| yield { type: "startObject" }; | ||
| yield* decodeKeyValueAsync(first.content, cursor, 0, resolvedOptions); | ||
| while (!await cursor.atEnd()) { | ||
| const line = await cursor.peek(); | ||
| if (!line || line.depth !== 0) break; | ||
| await cursor.advance(); | ||
| yield* decodeKeyValueAsync(line.content, cursor, 0, resolvedOptions); | ||
| } | ||
| yield { type: "endObject" }; | ||
| } else yield* decodeStreamSync$1(source, options); | ||
| } | ||
| async function* decodeKeyValueAsync(content, cursor, baseDepth, options) { | ||
| const arrayHeader = parseArrayHeaderLine(content, DEFAULT_DELIMITER); | ||
| if (arrayHeader && arrayHeader.header.key) { | ||
| yield { | ||
| type: "key", | ||
| key: arrayHeader.header.key | ||
| }; | ||
| yield* decodeArrayFromHeaderAsync(arrayHeader.header, arrayHeader.inlineValues, cursor, baseDepth, options); | ||
| return; | ||
| } | ||
| const { key, isQuoted } = parseKeyToken(content, 0); | ||
| const colonIndex = content.indexOf(COLON, key.length); | ||
| const rest = colonIndex >= 0 ? content.slice(colonIndex + 1).trim() : ""; | ||
| yield isQuoted ? { | ||
| type: "key", | ||
| key, | ||
| wasQuoted: true | ||
| } : { | ||
| type: "key", | ||
| key | ||
| }; | ||
| if (!rest) { | ||
| const nextLine = await cursor.peek(); | ||
| if (nextLine && nextLine.depth > baseDepth) { | ||
| yield { type: "startObject" }; | ||
| yield* decodeObjectFieldsAsync(cursor, baseDepth + 1, options); | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| yield { | ||
| type: "primitive", | ||
| value: parsePrimitiveToken(rest) | ||
| }; | ||
| } | ||
| async function* decodeObjectFieldsAsync(cursor, baseDepth, options) { | ||
| let computedDepth; | ||
| while (!await cursor.atEnd()) { | ||
| const line = await cursor.peek(); | ||
| if (!line || line.depth < baseDepth) break; | ||
| if (computedDepth === void 0 && line.depth >= baseDepth) computedDepth = line.depth; | ||
| if (line.depth === computedDepth) { | ||
| await cursor.advance(); | ||
| yield* decodeKeyValueAsync(line.content, cursor, computedDepth, options); | ||
| } else break; | ||
| } | ||
| } | ||
| async function* decodeArrayFromHeaderAsync(header, inlineValues, cursor, baseDepth, options) { | ||
| yield { | ||
| type: "startArray", | ||
| length: header.length | ||
| }; | ||
| if (inlineValues) { | ||
| yield* decodeInlinePrimitiveArraySync(header, inlineValues, options); | ||
| yield { type: "endArray" }; | ||
| return; | ||
| } | ||
| if (header.fields && header.fields.length > 0) { | ||
| yield* decodeTabularArrayAsync(header, cursor, baseDepth, options); | ||
| yield { type: "endArray" }; | ||
| return; | ||
| } | ||
| yield* decodeListArrayAsync(header, cursor, baseDepth, options); | ||
| yield { type: "endArray" }; | ||
| } | ||
| async function* decodeTabularArrayAsync(header, cursor, baseDepth, options) { | ||
| const rowDepth = baseDepth + 1; | ||
| let rowCount = 0; | ||
| let startLine; | ||
| let endLine; | ||
| while (!cursor.atEnd() && objects.length < header.length) { | ||
| const line = cursor.peek(); | ||
| while (!await cursor.atEnd() && rowCount < header.length) { | ||
| const line = await cursor.peek(); | ||
| if (!line || line.depth < rowDepth) break; | ||
@@ -726,133 +829,424 @@ if (line.depth === rowDepth) { | ||
| endLine = line.lineNumber; | ||
| cursor.advance(); | ||
| await cursor.advance(); | ||
| const values = parseDelimitedValues(line.content, header.delimiter); | ||
| assertExpectedCount(values.length, header.fields.length, "tabular row values", options); | ||
| const primitives = mapRowValuesToPrimitives(values); | ||
| const obj = {}; | ||
| for (let i = 0; i < header.fields.length; i++) obj[header.fields[i]] = primitives[i]; | ||
| objects.push(obj); | ||
| yield { type: "startObject" }; | ||
| for (let i = 0; i < header.fields.length; i++) { | ||
| yield { | ||
| type: "key", | ||
| key: header.fields[i] | ||
| }; | ||
| yield { | ||
| type: "primitive", | ||
| value: primitives[i] | ||
| }; | ||
| } | ||
| yield { type: "endObject" }; | ||
| rowCount++; | ||
| } else break; | ||
| } | ||
| assertExpectedCount(objects.length, header.length, "tabular rows", options); | ||
| assertExpectedCount(rowCount, header.length, "tabular rows", options); | ||
| if (options.strict && startLine !== void 0 && endLine !== void 0) validateNoBlankLinesInRange(startLine, endLine, cursor.getBlankLines(), options.strict, "tabular array"); | ||
| if (options.strict) validateNoExtraTabularRows(cursor, rowDepth, header); | ||
| return objects; | ||
| if (options.strict) validateNoExtraTabularRows(await cursor.peek(), rowDepth, header); | ||
| } | ||
| function decodeListItem(cursor, baseDepth, options) { | ||
| const line = cursor.next(); | ||
| async function* decodeListArrayAsync(header, cursor, baseDepth, options) { | ||
| const itemDepth = baseDepth + 1; | ||
| let itemCount = 0; | ||
| let startLine; | ||
| let endLine; | ||
| while (!await cursor.atEnd() && itemCount < header.length) { | ||
| const line = await cursor.peek(); | ||
| if (!line || line.depth < itemDepth) break; | ||
| const isListItem = line.content.startsWith(LIST_ITEM_PREFIX) || line.content === LIST_ITEM_MARKER; | ||
| if (line.depth === itemDepth && isListItem) { | ||
| if (startLine === void 0) startLine = line.lineNumber; | ||
| endLine = line.lineNumber; | ||
| yield* decodeListItemAsync(cursor, itemDepth, options); | ||
| const currentLine = cursor.current(); | ||
| if (currentLine) endLine = currentLine.lineNumber; | ||
| itemCount++; | ||
| } else break; | ||
| } | ||
| assertExpectedCount(itemCount, header.length, "list array items", options); | ||
| if (options.strict && startLine !== void 0 && endLine !== void 0) validateNoBlankLinesInRange(startLine, endLine, cursor.getBlankLines(), options.strict, "list array"); | ||
| if (options.strict) validateNoExtraListItems(await cursor.peek(), itemDepth, header.length); | ||
| } | ||
| async function* decodeListItemAsync(cursor, baseDepth, options) { | ||
| const line = await cursor.next(); | ||
| if (!line) throw new ReferenceError("Expected list item"); | ||
| let afterHyphen; | ||
| if (line.content === "-") return {}; | ||
| else if (line.content.startsWith(LIST_ITEM_PREFIX)) afterHyphen = line.content.slice(LIST_ITEM_PREFIX.length); | ||
| if (line.content === LIST_ITEM_MARKER) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } else if (line.content.startsWith(LIST_ITEM_PREFIX)) afterHyphen = line.content.slice(LIST_ITEM_PREFIX.length); | ||
| else throw new SyntaxError(`Expected list item to start with "${LIST_ITEM_PREFIX}"`); | ||
| if (!afterHyphen.trim()) return {}; | ||
| if (isArrayHeaderAfterHyphen(afterHyphen)) { | ||
| if (!afterHyphen.trim()) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (isArrayHeaderContent(afterHyphen)) { | ||
| const arrayHeader = parseArrayHeaderLine(afterHyphen, DEFAULT_DELIMITER); | ||
| if (arrayHeader) return decodeArrayFromHeader(arrayHeader.header, arrayHeader.inlineValues, cursor, baseDepth, options); | ||
| if (arrayHeader) { | ||
| yield* decodeArrayFromHeaderAsync(arrayHeader.header, arrayHeader.inlineValues, cursor, baseDepth, options); | ||
| return; | ||
| } | ||
| } | ||
| if (isObjectFirstFieldAfterHyphen(afterHyphen)) return decodeObjectFromListItem(line, cursor, baseDepth, options); | ||
| return parsePrimitiveToken(afterHyphen); | ||
| } | ||
| function decodeObjectFromListItem(firstLine, cursor, baseDepth, options) { | ||
| const { key, value, followDepth, isQuoted } = decodeKeyValue(firstLine.content.slice(LIST_ITEM_PREFIX.length), cursor, baseDepth, options); | ||
| const obj = { [key]: value }; | ||
| const quotedKeys = /* @__PURE__ */ new Set(); | ||
| if (isQuoted && key.includes(DOT)) quotedKeys.add(key); | ||
| while (!cursor.atEnd()) { | ||
| const line = cursor.peek(); | ||
| if (!line || line.depth < followDepth) break; | ||
| if (line.depth === followDepth && !line.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| cursor.advance(); | ||
| const { key: k, value: v, isQuoted: kIsQuoted } = decodeKeyValue(line.content, cursor, followDepth, options); | ||
| obj[k] = v; | ||
| if (kIsQuoted && k.includes(DOT)) quotedKeys.add(k); | ||
| } else break; | ||
| if (isKeyValueContent(afterHyphen)) { | ||
| yield { type: "startObject" }; | ||
| yield* decodeKeyValueAsync(afterHyphen, cursor, baseDepth, options); | ||
| const followDepth = baseDepth + 1; | ||
| while (!await cursor.atEnd()) { | ||
| const nextLine = await cursor.peek(); | ||
| if (!nextLine || nextLine.depth < followDepth) break; | ||
| if (nextLine.depth === followDepth && !nextLine.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| await cursor.advance(); | ||
| yield* decodeKeyValueAsync(nextLine.content, cursor, followDepth, options); | ||
| } else break; | ||
| } | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (quotedKeys.size > 0) obj[QUOTED_KEY_MARKER] = quotedKeys; | ||
| return obj; | ||
| yield { | ||
| type: "primitive", | ||
| value: parsePrimitiveToken(afterHyphen) | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/decode/scanner.ts | ||
| var LineCursor = class { | ||
| lines; | ||
| index; | ||
| blankLines; | ||
| constructor(lines, blankLines = []) { | ||
| this.lines = lines; | ||
| this.index = 0; | ||
| this.blankLines = blankLines; | ||
| //#region src/encode/normalize.ts | ||
| function normalizeValue(value) { | ||
| if (value === null) return null; | ||
| if (typeof value === "string" || typeof value === "boolean") return value; | ||
| if (typeof value === "number") { | ||
| if (Object.is(value, -0)) return 0; | ||
| if (!Number.isFinite(value)) return null; | ||
| return value; | ||
| } | ||
| getBlankLines() { | ||
| return this.blankLines; | ||
| if (typeof value === "bigint") { | ||
| if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) return Number(value); | ||
| return value.toString(); | ||
| } | ||
| peek() { | ||
| return this.lines[this.index]; | ||
| if (value instanceof Date) return value.toISOString(); | ||
| if (Array.isArray(value)) return value.map(normalizeValue); | ||
| if (value instanceof Set) return Array.from(value).map(normalizeValue); | ||
| if (value instanceof Map) return Object.fromEntries(Array.from(value, ([k, v]) => [String(k), normalizeValue(v)])); | ||
| if (isPlainObject(value)) { | ||
| const normalized = {}; | ||
| for (const key in value) if (Object.prototype.hasOwnProperty.call(value, key)) normalized[key] = normalizeValue(value[key]); | ||
| return normalized; | ||
| } | ||
| next() { | ||
| return this.lines[this.index++]; | ||
| return null; | ||
| } | ||
| function isJsonPrimitive(value) { | ||
| return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean"; | ||
| } | ||
| function isJsonArray(value) { | ||
| return Array.isArray(value); | ||
| } | ||
| function isJsonObject(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| function isEmptyObject(value) { | ||
| return Object.keys(value).length === 0; | ||
| } | ||
| function isPlainObject(value) { | ||
| if (value === null || typeof value !== "object") return false; | ||
| const prototype = Object.getPrototypeOf(value); | ||
| return prototype === null || prototype === Object.prototype; | ||
| } | ||
| function isArrayOfPrimitives(value) { | ||
| return value.length === 0 || value.every((item) => isJsonPrimitive(item)); | ||
| } | ||
| function isArrayOfArrays(value) { | ||
| return value.length === 0 || value.every((item) => isJsonArray(item)); | ||
| } | ||
| function isArrayOfObjects(value) { | ||
| return value.length === 0 || value.every((item) => isJsonObject(item)); | ||
| } | ||
| //#endregion | ||
| //#region src/shared/validation.ts | ||
| /** | ||
| * Checks if a key can be used without quotes. | ||
| * | ||
| * @remarks | ||
| * Valid unquoted keys must start with a letter or underscore, | ||
| * followed by letters, digits, underscores, or dots. | ||
| */ | ||
| function isValidUnquotedKey(key) { | ||
| return /^[A-Z_][\w.]*$/i.test(key); | ||
| } | ||
| /** | ||
| * Checks if a key segment is a valid identifier for safe folding/expansion. | ||
| * | ||
| * @remarks | ||
| * Identifier segments are more restrictive than unquoted keys: | ||
| * - Must start with a letter or underscore | ||
| * - Followed only by letters, digits, or underscores (no dots) | ||
| * - Used for safe key folding and path expansion | ||
| */ | ||
| function isIdentifierSegment(key) { | ||
| return /^[A-Z_]\w*$/i.test(key); | ||
| } | ||
| /** | ||
| * Determines if a string value can be safely encoded without quotes. | ||
| * | ||
| * @remarks | ||
| * A string needs quoting if it: | ||
| * - Is empty | ||
| * - Has leading or trailing whitespace | ||
| * - Could be confused with a literal (boolean, null, number) | ||
| * - Contains structural characters (colons, brackets, braces) | ||
| * - Contains quotes or backslashes (need escaping) | ||
| * - Contains control characters (newlines, tabs, etc.) | ||
| * - Contains the active delimiter | ||
| * - Starts with a list marker (hyphen) | ||
| */ | ||
| function isSafeUnquoted(value, delimiter = DEFAULT_DELIMITER) { | ||
| if (!value) return false; | ||
| if (value !== value.trim()) return false; | ||
| if (isBooleanOrNullLiteral(value) || isNumericLike(value)) return false; | ||
| if (value.includes(":")) return false; | ||
| if (value.includes("\"") || value.includes("\\")) return false; | ||
| if (/[[\]{}]/.test(value)) return false; | ||
| if (/[\n\r\t]/.test(value)) return false; | ||
| if (value.includes(delimiter)) return false; | ||
| if (value.startsWith(LIST_ITEM_MARKER)) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Checks if a string looks like a number. | ||
| * | ||
| * @remarks | ||
| * Match numbers like `42`, `-3.14`, `1e-6`, `05`, etc. | ||
| */ | ||
| function isNumericLike(value) { | ||
| return /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value) || /^0\d+$/.test(value); | ||
| } | ||
| //#endregion | ||
| //#region src/decode/expand.ts | ||
| /** | ||
| * Symbol used to mark object keys that were originally quoted in the TOON source. | ||
| * Quoted dotted keys should not be expanded, even if they meet expansion criteria. | ||
| */ | ||
| const QUOTED_KEY_MARKER = Symbol("quotedKey"); | ||
| /** | ||
| * Expands dotted keys into nested objects in safe mode. | ||
| * | ||
| * @remarks | ||
| * This function recursively traverses a decoded TOON value and expands any keys | ||
| * containing dots (`.`) into nested object structures, provided all segments | ||
| * are valid identifiers. | ||
| * | ||
| * Expansion rules: | ||
| * - Keys containing dots are split into segments | ||
| * - All segments must pass `isIdentifierSegment` validation | ||
| * - Non-eligible keys (with special characters) are left as literal dotted keys | ||
| * - Deep merge: When multiple dotted keys expand to the same path, their values are merged if both are objects | ||
| * - Conflict handling: | ||
| * - `strict=true`: Throws TypeError on conflicts (non-object collision) | ||
| * - `strict=false`: LWW (silent overwrite) | ||
| * | ||
| * @param value - The decoded value to expand | ||
| * @param strict - Whether to throw errors on conflicts | ||
| * @returns The expanded value with dotted keys reconstructed as nested objects | ||
| * @throws TypeError if conflicts occur in strict mode | ||
| */ | ||
| function expandPathsSafe(value, strict) { | ||
| if (Array.isArray(value)) return value.map((item) => expandPathsSafe(item, strict)); | ||
| if (isJsonObject(value)) { | ||
| const expandedObject = {}; | ||
| const quotedKeys = value[QUOTED_KEY_MARKER]; | ||
| for (const [key, keyValue] of Object.entries(value)) { | ||
| const isQuoted = quotedKeys?.has(key); | ||
| if (key.includes(DOT) && !isQuoted) { | ||
| const segments = key.split(DOT); | ||
| if (segments.every((seg) => isIdentifierSegment(seg))) { | ||
| insertPathSafe(expandedObject, segments, expandPathsSafe(keyValue, strict), strict); | ||
| continue; | ||
| } | ||
| } | ||
| const expandedValue = expandPathsSafe(keyValue, strict); | ||
| if (key in expandedObject) { | ||
| const conflictingValue = expandedObject[key]; | ||
| if (canMerge(conflictingValue, expandedValue)) mergeObjects(conflictingValue, expandedValue, strict); | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at key "${key}": cannot merge ${typeof conflictingValue} with ${typeof expandedValue}`); | ||
| expandedObject[key] = expandedValue; | ||
| } | ||
| } else expandedObject[key] = expandedValue; | ||
| } | ||
| return expandedObject; | ||
| } | ||
| current() { | ||
| return this.index > 0 ? this.lines[this.index - 1] : void 0; | ||
| return value; | ||
| } | ||
| /** | ||
| * Inserts a value at a nested path, creating intermediate objects as needed. | ||
| * | ||
| * @remarks | ||
| * This function walks the segment path, creating nested objects as needed. | ||
| * When an existing value is encountered: | ||
| * - If both are objects: deep merge (continue insertion) | ||
| * - If values differ: conflict | ||
| * - strict=true: throw TypeError | ||
| * - strict=false: overwrite with new value (LWW) | ||
| * | ||
| * @param target - The object to insert into | ||
| * @param segments - Array of path segments (e.g., ['data', 'metadata', 'items']) | ||
| * @param value - The value to insert at the end of the path | ||
| * @param strict - Whether to throw on conflicts | ||
| * @throws TypeError if a conflict occurs in strict mode | ||
| */ | ||
| function insertPathSafe(target, segments, value, strict) { | ||
| let currentNode = target; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const currentSegment = segments[i]; | ||
| const segmentValue = currentNode[currentSegment]; | ||
| if (segmentValue === void 0) { | ||
| const newObj = {}; | ||
| currentNode[currentSegment] = newObj; | ||
| currentNode = newObj; | ||
| } else if (isJsonObject(segmentValue)) currentNode = segmentValue; | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at segment "${currentSegment}": expected object but found ${typeof segmentValue}`); | ||
| const newObj = {}; | ||
| currentNode[currentSegment] = newObj; | ||
| currentNode = newObj; | ||
| } | ||
| } | ||
| advance() { | ||
| this.index++; | ||
| const lastSeg = segments[segments.length - 1]; | ||
| const destinationValue = currentNode[lastSeg]; | ||
| if (destinationValue === void 0) currentNode[lastSeg] = value; | ||
| else if (canMerge(destinationValue, value)) mergeObjects(destinationValue, value, strict); | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at key "${lastSeg}": cannot merge ${typeof destinationValue} with ${typeof value}`); | ||
| currentNode[lastSeg] = value; | ||
| } | ||
| atEnd() { | ||
| return this.index >= this.lines.length; | ||
| } | ||
| /** | ||
| * Deep merges properties from source into target. | ||
| * | ||
| * @remarks | ||
| * For each key in source: | ||
| * - If key doesn't exist in target: copy it | ||
| * - If both values are objects: recursively merge | ||
| * - Otherwise: conflict (strict throws, non-strict overwrites) | ||
| * | ||
| * @param target - The target object to merge into | ||
| * @param source - The source object to merge from | ||
| * @param strict - Whether to throw on conflicts | ||
| * @throws TypeError if a conflict occurs in strict mode | ||
| */ | ||
| function mergeObjects(target, source, strict) { | ||
| for (const [key, sourceValue] of Object.entries(source)) { | ||
| const targetValue = target[key]; | ||
| if (targetValue === void 0) target[key] = sourceValue; | ||
| else if (canMerge(targetValue, sourceValue)) mergeObjects(targetValue, sourceValue, strict); | ||
| else { | ||
| if (strict) throw new TypeError(`Path expansion conflict at key "${key}": cannot merge ${typeof targetValue} with ${typeof sourceValue}`); | ||
| target[key] = sourceValue; | ||
| } | ||
| } | ||
| get length() { | ||
| return this.lines.length; | ||
| } | ||
| peekAtDepth(targetDepth) { | ||
| const line = this.peek(); | ||
| return line?.depth === targetDepth ? line : void 0; | ||
| } | ||
| }; | ||
| function toParsedLines(source, indentSize, strict) { | ||
| if (!source.trim()) return { | ||
| lines: [], | ||
| blankLines: [] | ||
| }; | ||
| const lines = source.split("\n"); | ||
| const parsed = []; | ||
| const blankLines = []; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const raw = lines[i]; | ||
| const lineNumber = i + 1; | ||
| let indent = 0; | ||
| while (indent < raw.length && raw[indent] === SPACE) indent++; | ||
| const content = raw.slice(indent); | ||
| if (!content.trim()) { | ||
| const depth$1 = computeDepthFromIndent(indent, indentSize); | ||
| blankLines.push({ | ||
| lineNumber, | ||
| indent, | ||
| depth: depth$1 | ||
| } | ||
| function canMerge(a, b) { | ||
| return isJsonObject(a) && isJsonObject(b); | ||
| } | ||
| //#endregion | ||
| //#region src/decode/event-builder.ts | ||
| function buildValueFromEvents(events) { | ||
| const stack = []; | ||
| let root; | ||
| for (const event of events) switch (event.type) { | ||
| case "startObject": { | ||
| const obj = {}; | ||
| const quotedKeys = /* @__PURE__ */ new Set(); | ||
| if (stack.length === 0) stack.push({ | ||
| type: "object", | ||
| obj, | ||
| quotedKeys | ||
| }); | ||
| continue; | ||
| else { | ||
| const parent = stack[stack.length - 1]; | ||
| if (parent.type === "object") { | ||
| if (parent.currentKey === void 0) throw new Error("Object startObject event without preceding key"); | ||
| parent.obj[parent.currentKey] = obj; | ||
| parent.currentKey = void 0; | ||
| } else if (parent.type === "array") parent.arr.push(obj); | ||
| stack.push({ | ||
| type: "object", | ||
| obj, | ||
| quotedKeys | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| const depth = computeDepthFromIndent(indent, indentSize); | ||
| if (strict) { | ||
| let whitespaceEndIndex = 0; | ||
| while (whitespaceEndIndex < raw.length && (raw[whitespaceEndIndex] === SPACE || raw[whitespaceEndIndex] === TAB)) whitespaceEndIndex++; | ||
| if (raw.slice(0, whitespaceEndIndex).includes(TAB)) throw new SyntaxError(`Line ${lineNumber}: Tabs are not allowed in indentation in strict mode`); | ||
| if (indent > 0 && indent % indentSize !== 0) throw new SyntaxError(`Line ${lineNumber}: Indentation must be exact multiple of ${indentSize}, but found ${indent} spaces`); | ||
| case "endObject": { | ||
| if (stack.length === 0) throw new Error("Unexpected endObject event"); | ||
| const context = stack.pop(); | ||
| if (context.type !== "object") throw new Error("Mismatched endObject event"); | ||
| if (context.quotedKeys.size > 0) Object.defineProperty(context.obj, QUOTED_KEY_MARKER, { | ||
| value: context.quotedKeys, | ||
| enumerable: false, | ||
| writable: false, | ||
| configurable: false | ||
| }); | ||
| if (stack.length === 0) root = context.obj; | ||
| break; | ||
| } | ||
| parsed.push({ | ||
| raw, | ||
| indent, | ||
| content, | ||
| depth, | ||
| lineNumber | ||
| }); | ||
| case "startArray": { | ||
| const arr = []; | ||
| if (stack.length === 0) stack.push({ | ||
| type: "array", | ||
| arr | ||
| }); | ||
| else { | ||
| const parent = stack[stack.length - 1]; | ||
| if (parent.type === "object") { | ||
| if (parent.currentKey === void 0) throw new Error("Array startArray event without preceding key"); | ||
| parent.obj[parent.currentKey] = arr; | ||
| parent.currentKey = void 0; | ||
| } else if (parent.type === "array") parent.arr.push(arr); | ||
| stack.push({ | ||
| type: "array", | ||
| arr | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| case "endArray": { | ||
| if (stack.length === 0) throw new Error("Unexpected endArray event"); | ||
| const context = stack.pop(); | ||
| if (context.type !== "array") throw new Error("Mismatched endArray event"); | ||
| if (stack.length === 0) root = context.arr; | ||
| break; | ||
| } | ||
| case "key": { | ||
| if (stack.length === 0) throw new Error("Key event outside of object context"); | ||
| const parent = stack[stack.length - 1]; | ||
| if (parent.type !== "object") throw new Error("Key event in non-object context"); | ||
| parent.currentKey = event.key; | ||
| if (event.wasQuoted) parent.quotedKeys.add(event.key); | ||
| break; | ||
| } | ||
| case "primitive": | ||
| if (stack.length === 0) root = event.value; | ||
| else { | ||
| const parent = stack[stack.length - 1]; | ||
| if (parent.type === "object") { | ||
| if (parent.currentKey === void 0) throw new Error("Primitive event without preceding key in object"); | ||
| parent.obj[parent.currentKey] = event.value; | ||
| parent.currentKey = void 0; | ||
| } else if (parent.type === "array") parent.arr.push(event.value); | ||
| } | ||
| break; | ||
| } | ||
| return { | ||
| lines: parsed, | ||
| blankLines | ||
| }; | ||
| if (stack.length !== 0) throw new Error("Incomplete event stream: stack not empty at end"); | ||
| if (root === void 0) throw new Error("No root value built from events"); | ||
| return root; | ||
| } | ||
| function computeDepthFromIndent(indentSpaces, indentSize) { | ||
| return Math.floor(indentSpaces / indentSize); | ||
| } | ||
@@ -1168,29 +1562,2 @@ //#endregion | ||
| /** | ||
| * Encodes a JavaScript value into TOON format as a sequence of lines. | ||
| * | ||
| * This function yields TOON lines one at a time without building the full string, | ||
| * making it suitable for streaming large outputs to files, HTTP responses, or process stdout. | ||
| * | ||
| * @param input - Any JavaScript value (objects, arrays, primitives) | ||
| * @param options - Optional encoding configuration | ||
| * @returns Iterable of TOON lines (without trailing newlines) | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Stream to stdout | ||
| * for (const line of encodeLines({ name: 'Alice', age: 30 })) { | ||
| * console.log(line) | ||
| * } | ||
| * | ||
| * // Collect to array | ||
| * const lines = Array.from(encodeLines(data)) | ||
| * | ||
| * // Equivalent to encode() | ||
| * const toonString = Array.from(encodeLines(data, options)).join('\n') | ||
| * ``` | ||
| */ | ||
| function encodeLines(input, options) { | ||
| return encodeJsonValue(normalizeValue(input), resolveOptions(options), 0); | ||
| } | ||
| /** | ||
| * Encodes a JavaScript value into TOON format string. | ||
@@ -1238,9 +1605,119 @@ * | ||
| function decode(input, options) { | ||
| return decodeFromLines(input.split("\n"), options); | ||
| } | ||
| /** | ||
| * Encodes a JavaScript value into TOON format as a sequence of lines. | ||
| * | ||
| * This function yields TOON lines one at a time without building the full string, | ||
| * making it suitable for streaming large outputs to files, HTTP responses, or process stdout. | ||
| * | ||
| * @param input - Any JavaScript value (objects, arrays, primitives) | ||
| * @param options - Optional encoding configuration | ||
| * @returns Iterable of TOON lines (without trailing newlines) | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Stream to stdout | ||
| * for (const line of encodeLines({ name: 'Alice', age: 30 })) { | ||
| * console.log(line) | ||
| * } | ||
| * | ||
| * // Collect to array | ||
| * const lines = Array.from(encodeLines(data)) | ||
| * | ||
| * // Equivalent to encode() | ||
| * const toonString = Array.from(encodeLines(data, options)).join('\n') | ||
| * ``` | ||
| */ | ||
| function encodeLines(input, options) { | ||
| return encodeJsonValue(normalizeValue(input), resolveOptions(options), 0); | ||
| } | ||
| /** | ||
| * Decodes TOON format from pre-split lines into a JavaScript value. | ||
| * | ||
| * This is a convenience wrapper around the streaming decoder that builds | ||
| * the full value in memory. Useful when you already have lines as an array | ||
| * or iterable and want the standard decode behavior with path expansion support. | ||
| * | ||
| * @param lines - Iterable of TOON lines (without newlines) | ||
| * @param options - Optional decoding configuration (supports expandPaths) | ||
| * @returns Parsed JavaScript value (object, array, or primitive) | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const lines = ['name: Alice', 'age: 30'] | ||
| * decodeFromLines(lines) | ||
| * // { name: 'Alice', age: 30 } | ||
| * ``` | ||
| */ | ||
| function decodeFromLines(lines, options) { | ||
| const resolvedOptions = resolveDecodeOptions(options); | ||
| const scanResult = toParsedLines(input, resolvedOptions.indent, resolvedOptions.strict); | ||
| if (scanResult.lines.length === 0) return {}; | ||
| const decodedValue = decodeValueFromLines(new LineCursor(scanResult.lines, scanResult.blankLines), resolvedOptions); | ||
| const decodedValue = buildValueFromEvents(decodeStreamSync$1(lines, { | ||
| indent: resolvedOptions.indent, | ||
| strict: resolvedOptions.strict | ||
| })); | ||
| if (resolvedOptions.expandPaths === "safe") return expandPathsSafe(decodedValue, resolvedOptions.strict); | ||
| return decodedValue; | ||
| } | ||
| /** | ||
| * Synchronously decodes TOON lines into a stream of JSON events. | ||
| * | ||
| * This function yields structured events (startObject, endObject, startArray, endArray, | ||
| * key, primitive) that represent the JSON data model without building the full value tree. | ||
| * Useful for streaming processing, custom transformations, or memory-efficient parsing. | ||
| * | ||
| * @remarks | ||
| * Path expansion (`expandPaths: 'safe'`) is not supported in streaming mode. | ||
| * | ||
| * @param lines - Iterable of TOON lines (without newlines) | ||
| * @param options - Optional decoding configuration (expandPaths not supported) | ||
| * @returns Iterable of JSON stream events | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const lines = ['name: Alice', 'age: 30'] | ||
| * for (const event of decodeStreamSync(lines)) { | ||
| * console.log(event) | ||
| * // { type: 'startObject' } | ||
| * // { type: 'key', key: 'name' } | ||
| * // { type: 'primitive', value: 'Alice' } | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| function decodeStreamSync(lines, options) { | ||
| return decodeStreamSync$1(lines, options); | ||
| } | ||
| /** | ||
| * Asynchronously decodes TOON lines into a stream of JSON events. | ||
| * | ||
| * This function yields structured events (startObject, endObject, startArray, endArray, | ||
| * key, primitive) that represent the JSON data model without building the full value tree. | ||
| * Supports both sync and async iterables for maximum flexibility with file streams, | ||
| * network responses, or other async sources. | ||
| * | ||
| * @remarks | ||
| * Path expansion (`expandPaths: 'safe'`) is not supported in streaming mode. | ||
| * | ||
| * @param source - Async or sync iterable of TOON lines (without newlines) | ||
| * @param options - Optional decoding configuration (expandPaths not supported) | ||
| * @returns Async iterable of JSON stream events | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const fileStream = createReadStream('data.toon', 'utf-8') | ||
| * const lines = splitLines(fileStream) // Async iterable of lines | ||
| * | ||
| * for await (const event of decodeStream(lines)) { | ||
| * console.log(event) | ||
| * // { type: 'startObject' } | ||
| * // { type: 'key', key: 'name' } | ||
| * // { type: 'primitive', value: 'Alice' } | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| function decodeStream(source, options) { | ||
| return decodeStream$1(source, options); | ||
| } | ||
| function resolveOptions(options) { | ||
@@ -1263,2 +1740,2 @@ return { | ||
| //#endregion | ||
| export { DEFAULT_DELIMITER, DELIMITERS, decode, encode, encodeLines }; | ||
| export { DEFAULT_DELIMITER, DELIMITERS, decode, decodeFromLines, decodeStream, decodeStreamSync, encode, encodeLines }; |
+1
-1
| { | ||
| "name": "@toon-format/toon", | ||
| "type": "module", | ||
| "version": "1.2.0", | ||
| "version": "1.3.0", | ||
| "description": "Token-Oriented Object Notation (TOON) – Compact, human-readable, schema-aware encoding of JSON for LLM prompts", | ||
@@ -6,0 +6,0 @@ "author": "Johann Schopplich <hello@johannschopplich.com>", |
+40
-0
@@ -780,2 +780,42 @@  | ||
| **Streaming decode:** | ||
| ```ts | ||
| import { decodeFromLines, decodeStreamSync } from '@toon-format/toon' | ||
| // 1. Lines → value (build full JSON value) | ||
| const value = decodeFromLines([ | ||
| 'users[2]{id,name}:', | ||
| ' 1,Alice', | ||
| ' 2,Bob', | ||
| ]) | ||
| // { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] } | ||
| // 2. Lines → events (for custom streaming consumers) | ||
| const lines = [ | ||
| 'users[2]{id,name}:', | ||
| ' 1,Alice', | ||
| ' 2,Bob', | ||
| ] | ||
| for (const event of decodeStreamSync(lines)) { | ||
| // { type: 'startObject' }, { type: 'key', key: 'users' }, ... | ||
| } | ||
| ``` | ||
| **Async streaming decode:** | ||
| ```ts | ||
| // 3. Async streaming from files or network | ||
| import { createReadStream } from 'node:fs' | ||
| import { createInterface } from 'node:readline' | ||
| import { decodeStream } from '@toon-format/toon' | ||
| const fileStream = createReadStream('data.toon', 'utf-8') | ||
| const rl = createInterface({ input: fileStream }) | ||
| for await (const event of decodeStream(rl)) { | ||
| // Process events as they arrive | ||
| } | ||
| ``` | ||
| ## Playgrounds | ||
@@ -782,0 +822,0 @@ |
108064
22.09%1714
38.45%951
4.39%