@toon-format/toon
Advanced tools
+63
-33
@@ -656,5 +656,26 @@ //#region src/constants.ts | ||
| if (line.content === LIST_ITEM_MARKER) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| const followDepth = baseDepth + 1; | ||
| const nextLine = cursor.peekSync(); | ||
| if (!nextLine || nextLine.depth < followDepth) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (nextLine.depth === followDepth && !nextLine.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| yield { type: "startObject" }; | ||
| while (!cursor.atEndSync()) { | ||
| const fieldLine = cursor.peekSync(); | ||
| if (!fieldLine || fieldLine.depth < followDepth) break; | ||
| if (fieldLine.depth === followDepth && !fieldLine.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| cursor.advanceSync(); | ||
| yield* decodeKeyValueSync(fieldLine.content, cursor, followDepth, options); | ||
| } else break; | ||
| } | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } else { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| } else if (line.content.startsWith(LIST_ITEM_PREFIX)) afterHyphen = line.content.slice(LIST_ITEM_PREFIX.length); | ||
@@ -873,5 +894,26 @@ else throw new SyntaxError(`Expected list item to start with "${LIST_ITEM_PREFIX}"`); | ||
| if (line.content === LIST_ITEM_MARKER) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| const followDepth = baseDepth + 1; | ||
| const nextLine = await cursor.peek(); | ||
| if (!nextLine || nextLine.depth < followDepth) { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| if (nextLine.depth === followDepth && !nextLine.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| yield { type: "startObject" }; | ||
| while (!cursor.atEnd()) { | ||
| const fieldLine = await cursor.peek(); | ||
| if (!fieldLine || fieldLine.depth < followDepth) break; | ||
| if (fieldLine.depth === followDepth && !fieldLine.content.startsWith(LIST_ITEM_PREFIX)) { | ||
| await cursor.advance(); | ||
| yield* decodeKeyValueAsync(fieldLine.content, cursor, followDepth, options); | ||
| } else break; | ||
| } | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } else { | ||
| yield { type: "startObject" }; | ||
| yield { type: "endObject" }; | ||
| return; | ||
| } | ||
| } else if (line.content.startsWith(LIST_ITEM_PREFIX)) afterHyphen = line.content.slice(LIST_ITEM_PREFIX.length); | ||
@@ -1507,31 +1549,19 @@ else throw new SyntaxError(`Expected list item to start with "${LIST_ITEM_PREFIX}"`); | ||
| const entries = Object.entries(obj); | ||
| const [firstKey, firstValue] = entries[0]; | ||
| const encodedKey = encodeKey(firstKey); | ||
| if (isJsonPrimitive(firstValue)) yield indentedListItem(depth, `${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`, options.indent); | ||
| else if (isJsonArray(firstValue)) if (isArrayOfPrimitives(firstValue)) yield indentedListItem(depth, encodeInlineArrayLine(firstValue, options.delimiter, firstKey), options.indent); | ||
| else if (isArrayOfObjects(firstValue)) { | ||
| const header = extractTabularHeader(firstValue); | ||
| if (header) { | ||
| yield indentedListItem(depth, formatHeader(firstValue.length, { | ||
| key: firstKey, | ||
| fields: header, | ||
| delimiter: options.delimiter | ||
| }), options.indent); | ||
| yield* writeTabularRowsLines(firstValue, header, depth + 1, options); | ||
| } else { | ||
| yield indentedListItem(depth, `${encodedKey}[${firstValue.length}]:`, options.indent); | ||
| for (const item of firstValue) yield* encodeObjectAsListItemLines(item, depth + 1, options); | ||
| if (entries.length === 1) { | ||
| const [key, value] = entries[0]; | ||
| if (isJsonArray(value) && isArrayOfObjects(value)) { | ||
| const header = extractTabularHeader(value); | ||
| if (header) { | ||
| yield indentedListItem(depth, formatHeader(value.length, { | ||
| key, | ||
| fields: header, | ||
| delimiter: options.delimiter | ||
| }), options.indent); | ||
| yield* writeTabularRowsLines(value, header, depth + 1, options); | ||
| return; | ||
| } | ||
| } | ||
| } else { | ||
| yield indentedListItem(depth, `${encodedKey}[${firstValue.length}]:`, options.indent); | ||
| for (const item of firstValue) yield* encodeListItemValueLines(item, depth + 1, options); | ||
| } | ||
| else if (isJsonObject(firstValue)) { | ||
| yield indentedListItem(depth, `${encodedKey}:`, options.indent); | ||
| if (!isEmptyObject(firstValue)) yield* encodeObjectLines(firstValue, depth + 2, options); | ||
| } | ||
| for (let i = 1; i < entries.length; i++) { | ||
| const [key, value] = entries[i]; | ||
| yield* encodeKeyValuePairLines(key, value, depth + 1, options); | ||
| } | ||
| yield indentedLine(depth, LIST_ITEM_MARKER, options.indent); | ||
| yield* encodeObjectLines(obj, depth + 1, options); | ||
| } | ||
@@ -1538,0 +1568,0 @@ function* encodeListItemValueLines(value, depth, options) { |
+2
-2
| { | ||
| "name": "@toon-format/toon", | ||
| "type": "module", | ||
| "version": "1.3.0", | ||
| "version": "1.4.0", | ||
| "description": "Token-Oriented Object Notation (TOON) – Compact, human-readable, schema-aware encoding of JSON for LLM prompts", | ||
@@ -36,3 +36,3 @@ "author": "Johann Schopplich <hello@johannschopplich.com>", | ||
| "devDependencies": { | ||
| "@toon-format/spec": "^2.0.1" | ||
| "@toon-format/spec": "^2.1.0" | ||
| }, | ||
@@ -39,0 +39,0 @@ "scripts": { |
+15
-45
@@ -7,3 +7,3 @@  | ||
| [](https://www.npmjs.com/package/@toon-format/toon) | ||
| [](https://github.com/toon-format/spec) | ||
| [](https://github.com/toon-format/spec) | ||
| [](https://www.npmjs.com/package/@toon-format/toon) | ||
@@ -140,2 +140,6 @@ [](./LICENSE) | ||
| ## Media Type & File Extension | ||
| By convention, TOON files use the `.toon` extension and the provisional media type `text/toon` for HTTP and content-type–aware contexts. TOON documents are always UTF-8 encoded; the `charset=utf-8` parameter may be specified but defaults to UTF-8 when omitted. See [SPEC.md §18.2](https://github.com/toon-format/spec/blob/main/SPEC.md#182-provisional-media-type) for normative details. | ||
| ## When Not to Use TOON | ||
@@ -782,42 +786,5 @@ | ||
| **Streaming decode:** | ||
| > [!TIP] | ||
| > For streaming decode APIs, see [`decodeFromLines()`](/reference/api#decodeFromLines-lines-options) and [`decodeStream()`](/reference/api#decodeStream-source-options). | ||
| ```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 | ||
@@ -895,3 +862,4 @@ | ||
| **Getting Started** | ||
| ### Getting Started | ||
| - [Introduction & Installation](https://toonformat.dev/guide/getting-started) – What TOON is, when to use it, first steps | ||
@@ -901,3 +869,4 @@ - [Format Overview](https://toonformat.dev/guide/format-overview) – Complete syntax with examples | ||
| **Tools & Integration** | ||
| ### Tools & Integration | ||
| - [CLI](https://toonformat.dev/cli/) – Command-line tool for JSON↔TOON conversions | ||
@@ -907,6 +876,7 @@ - [Using TOON with LLMs](https://toonformat.dev/guide/llm-prompts) – Prompting strategies & validation | ||
| **Reference** | ||
| ### References | ||
| - [API Reference](https://toonformat.dev/reference/api) – TypeScript/JavaScript encode/decode API | ||
| - [Syntax Cheatsheet](https://toonformat.dev/reference/syntax-cheatsheet) – Quick format lookup | ||
| - [Specification v2.0](https://github.com/toon-format/spec/blob/main/SPEC.md) – Normative rules for implementers | ||
| - [Specification v2.1](https://github.com/toon-format/spec/blob/main/SPEC.md) – Normative rules for implementers | ||
@@ -916,3 +886,3 @@ ## Other Implementations | ||
| > [!NOTE] | ||
| > When implementing TOON in other languages, please follow the [specification](https://github.com/toon-format/spec/blob/main/SPEC.md) (currently v2.0) to ensure compatibility across implementations. The [conformance tests](https://github.com/toon-format/spec/tree/main/tests) provide language-agnostic test fixtures that validate your implementations. | ||
| > When implementing TOON in other languages, please follow the [Specification](https://github.com/toon-format/spec/blob/main/SPEC.md) (currently v2.1) to ensure compatibility across implementations. The [conformance tests](https://github.com/toon-format/spec/tree/main/tests) provide language-agnostic test fixtures that validate your implementations. | ||
@@ -919,0 +889,0 @@ ### Official Implementations |
108251
0.17%1744
1.75%921
-3.15%