diff --git a/packages/toon/src/decode/expand.ts b/packages/toon/src/decode/expand.ts index eb87069..0f90447 100644 --- a/packages/toon/src/decode/expand.ts +++ b/packages/toon/src/decode/expand.ts @@ -18,9 +18,6 @@ export interface ObjectWithQuotedKeys extends JsonObject { [QUOTED_KEY_MARKER]?: Set } -/** - * Checks if two values can be merged (both are plain objects). - */ function canMerge(a: JsonValue, b: JsonValue): a is JsonObject { return isJsonObject(a) && isJsonObject(b) } @@ -140,13 +137,13 @@ function insertPathSafe( // Walk to the penultimate segment, creating objects as needed for (let i = 0; i < segments.length - 1; i++) { - const seg = segments[i]! - const segmentValue = currentNode[seg] + const currentSegment = segments[i]! + const segmentValue = currentNode[currentSegment] if (segmentValue === undefined) { // Create new intermediate object const newObj: JsonObject = {} - currentNode[seg] = newObj + currentNode[currentSegment] = newObj currentNode = newObj } else if (isJsonObject(segmentValue)) { @@ -157,12 +154,12 @@ function insertPathSafe( // Conflict: existing value is not an object if (strict) { throw new TypeError( - `Path expansion conflict at segment "${seg}": expected object but found ${typeof segmentValue}`, + `Path expansion conflict at segment "${currentSegment}": expected object but found ${typeof segmentValue}`, ) } // Non-strict: overwrite with new object const newObj: JsonObject = {} - currentNode[seg] = newObj + currentNode[currentSegment] = newObj currentNode = newObj } } diff --git a/packages/toon/src/decode/parser.ts b/packages/toon/src/decode/parser.ts index e7c647f..a317cf0 100644 --- a/packages/toon/src/decode/parser.ts +++ b/packages/toon/src/decode/parser.ts @@ -144,6 +144,15 @@ export function parseBracketSegment( // #region Delimited value parsing +/** + * Parses a delimited string into values, respecting quoted strings and escape sequences. + * + * @remarks + * Uses a state machine that tracks: + * - `inQuotes`: Whether we're inside a quoted string (to ignore delimiters) + * - `valueBuffer`: Accumulates characters for the current value + * - Escape sequences: Handled within quoted strings + */ export function parseDelimitedValues(input: string, delimiter: Delimiter): string[] { const values: string[] = [] let valueBuffer = '' @@ -252,22 +261,22 @@ export function parseStringLiteral(token: string): string { } export function parseUnquotedKey(content: string, start: number): { key: string, end: number } { - let end = start - while (end < content.length && content[end] !== COLON) { - end++ + let parsePosition = start + while (parsePosition < content.length && content[parsePosition] !== COLON) { + parsePosition++ } // Validate that a colon was found - if (end >= content.length || content[end] !== COLON) { + if (parsePosition >= content.length || content[parsePosition] !== COLON) { throw new SyntaxError('Missing colon after key') } - const key = content.slice(start, end).trim() + const key = content.slice(start, parsePosition).trim() // Skip the colon - end++ + parsePosition++ - return { key, end } + return { key, end: parsePosition } } export function parseQuotedKey(content: string, start: number): { key: string, end: number } { @@ -281,15 +290,15 @@ export function parseQuotedKey(content: string, start: number): { key: string, e // Extract and unescape the key content const keyContent = content.slice(start + 1, closingQuoteIndex) const key = unescapeString(keyContent) - let end = closingQuoteIndex + 1 + let parsePosition = closingQuoteIndex + 1 // Validate and skip colon after quoted key - if (end >= content.length || content[end] !== COLON) { + if (parsePosition >= content.length || content[parsePosition] !== COLON) { throw new SyntaxError('Missing colon after key') } - end++ + parsePosition++ - return { key, end } + return { key, end: parsePosition } } export function parseKeyToken(content: string, start: number): { key: string, end: number, isQuoted: boolean } { diff --git a/packages/toon/src/decode/scanner.ts b/packages/toon/src/decode/scanner.ts index f8fcbe4..8dfb96d 100644 --- a/packages/toon/src/decode/scanner.ts +++ b/packages/toon/src/decode/scanner.ts @@ -92,13 +92,13 @@ export function toParsedLines(source: string, indentSize: number, strict: boolea // Strict mode validation if (strict) { // Find the full leading whitespace region (spaces and tabs) - let wsEnd = 0 - while (wsEnd < raw.length && (raw[wsEnd] === SPACE || raw[wsEnd] === TAB)) { - wsEnd++ + let whitespaceEndIndex = 0 + while (whitespaceEndIndex < raw.length && (raw[whitespaceEndIndex] === SPACE || raw[whitespaceEndIndex] === TAB)) { + whitespaceEndIndex++ } // Check for tabs in leading whitespace (before actual content) - if (raw.slice(0, wsEnd).includes(TAB)) { + if (raw.slice(0, whitespaceEndIndex).includes(TAB)) { throw new SyntaxError(`Line ${lineNumber}: Tabs are not allowed in indentation in strict mode`) } diff --git a/packages/toon/src/decode/validation.ts b/packages/toon/src/decode/validation.ts index 233ee4e..f05ebb6 100644 --- a/packages/toon/src/decode/validation.ts +++ b/packages/toon/src/decode/validation.ts @@ -4,12 +4,6 @@ import { COLON, LIST_ITEM_PREFIX } from '../constants' /** * Asserts that the actual count matches the expected count in strict mode. - * - * @param actual The actual count - * @param expected The expected count - * @param itemType The type of items being counted (e.g., `list array items`, `tabular rows`) - * @param options Decode options - * @throws RangeError if counts don't match in strict mode */ export function assertExpectedCount( actual: number, @@ -24,11 +18,6 @@ export function assertExpectedCount( /** * Validates that there are no extra list items beyond the expected count. - * - * @param cursor The line cursor - * @param itemDepth The expected depth of items - * @param expectedCount The expected number of items - * @throws RangeError if extra items are found */ export function validateNoExtraListItems( cursor: LineCursor, @@ -46,11 +35,6 @@ export function validateNoExtraListItems( /** * Validates that there are no extra tabular rows beyond the expected count. - * - * @param cursor The line cursor - * @param rowDepth The expected depth of rows - * @param header The array header info containing length and delimiter - * @throws RangeError if extra rows are found */ export function validateNoExtraTabularRows( cursor: LineCursor, @@ -72,17 +56,7 @@ export function validateNoExtraTabularRows( } /** - * Validates that there are no blank lines within a specific line range and depth. - * - * @remarks - * In strict mode, blank lines inside arrays/tabular rows are not allowed. - * - * @param startLine The starting line number (inclusive) - * @param endLine The ending line number (inclusive) - * @param blankLines Array of blank line information - * @param strict Whether strict mode is enabled - * @param context Description of the context (e.g., "list array", "tabular array") - * @throws SyntaxError if blank lines are found in strict mode + * Validates that there are no blank lines within a specific line range in strict mode. */ export function validateNoBlankLinesInRange( startLine: number, @@ -110,11 +84,7 @@ export function validateNoBlankLinesInRange( } /** - * Checks if a line represents a data row (as opposed to a key-value pair) in a tabular array. - * - * @param content The line content - * @param delimiter The delimiter used in the table - * @returns true if the line is a data row, false if it's a key-value pair + * Checks if a line is a data row (vs a key-value pair) in a tabular array. */ function isDataRow(content: string, delimiter: Delimiter): boolean { const colonPos = content.indexOf(COLON) diff --git a/packages/toon/src/encode/folding.ts b/packages/toon/src/encode/folding.ts index 26aabf0..56e7d73 100644 --- a/packages/toon/src/encode/folding.ts +++ b/packages/toon/src/encode/folding.ts @@ -138,6 +138,8 @@ function collectSingleKeyChain( const segments: string[] = [startKey] let currentValue = startValue + // Traverse nested single-key objects, collecting each key into segments array + // Stop when we encounter: multi-key object, array, primitive, or depth limit while (segments.length < maxDepth) { // Must be an object to continue if (!isJsonObject(currentValue)) { @@ -180,12 +182,6 @@ function collectSingleKeyChain( return { segments, tail: currentValue, leafValue: currentValue } } -/** - * Builds a folded key from segments. - * - * @param segments - Array of key segments - * @returns Dot-separated key string - */ function buildFoldedKey(segments: readonly string[]): string { return segments.join(DOT) } diff --git a/packages/toon/src/index.ts b/packages/toon/src/index.ts index f234787..3a8d9c1 100644 --- a/packages/toon/src/index.ts +++ b/packages/toon/src/index.ts @@ -20,12 +20,51 @@ export type { ResolvedEncodeOptions, } from './types' +/** + * 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' }) + * ``` + */ export function encode(input: unknown, options?: EncodeOptions): string { const normalizedValue = normalizeValue(input) const resolvedOptions = resolveOptions(options) return encodeValue(normalizedValue, resolvedOptions) } +/** + * 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' }) + * ``` + */ export function decode(input: string, options?: DecodeOptions): JsonValue { const resolvedOptions = resolveDecodeOptions(options) const scanResult = toParsedLines(input, resolvedOptions.indent, resolvedOptions.strict) diff --git a/packages/toon/src/shared/literal-utils.ts b/packages/toon/src/shared/literal-utils.ts index 9682917..b296b97 100644 --- a/packages/toon/src/shared/literal-utils.ts +++ b/packages/toon/src/shared/literal-utils.ts @@ -1,8 +1,5 @@ import { FALSE_LITERAL, NULL_LITERAL, TRUE_LITERAL } from '../constants' -/** - * Checks if a token is a boolean or null literal (`true`, `false`, `null`). - */ export function isBooleanOrNullLiteral(token: string): boolean { return token === TRUE_LITERAL || token === FALSE_LITERAL || token === NULL_LITERAL } diff --git a/packages/toon/src/shared/string-utils.ts b/packages/toon/src/shared/string-utils.ts index 167403b..ffecc0c 100644 --- a/packages/toon/src/shared/string-utils.ts +++ b/packages/toon/src/shared/string-utils.ts @@ -69,11 +69,7 @@ export function unescapeString(value: string): string { } /** - * Finds the index of the closing double quote in a string, accounting for escape sequences. - * - * @param content The string to search in - * @param start The index of the opening quote - * @returns The index of the closing quote, or -1 if not found + * Finds the index of the closing double quote, accounting for escape sequences. */ export function findClosingQuote(content: string, start: number): number { let i = start + 1 @@ -92,12 +88,7 @@ export function findClosingQuote(content: string, start: number): number { } /** - * Finds the index of a specific character outside of quoted sections. - * - * @param content The string to search in - * @param char The character to look for - * @param start Optional starting index (defaults to 0) - * @returns The index of the character, or -1 if not found outside quotes + * Finds the index of a character outside of quoted sections. */ export function findUnquotedChar(content: string, char: string, start = 0): number { let inQuotes = false