@amritk/yaml
Advanced tools
+45
| # @amritk/yaml — notes for AI coding agents | ||
| A fast, zero-dependency YAML parser for OpenAPI tooling that records exact | ||
| `[start, end)` source offsets on every node. Full reference is | ||
| [README.md](./README.md). | ||
| > Pre-alpha: APIs change in **minor** versions. This is a YAML **subset** for | ||
| > tooling, not full YAML 1.2 conformance. | ||
| ## Minimal example | ||
| ```ts | ||
| import { parseDocument, nodeAtPath, lineCounter } from '@amritk/yaml' | ||
| const source = 'info:\n title: My API\n version: 1.0.0\n' | ||
| const doc = parseDocument(source) | ||
| const node = nodeAtPath(doc.contents, ['info', 'version']) | ||
| const lc = lineCounter(source) | ||
| lc.linePos(node!.start) // → { line: 3, col: 12 } (1-based) | ||
| for (const error of doc.errors) { | ||
| const { line, col } = lc.linePos(error.start) | ||
| console.error(`${line}:${col} ${error.message}`) | ||
| } | ||
| ``` | ||
| ## Gotchas — where agents fail | ||
| 1. **`version: 1.0.0` stays the STRING `"1.0.0"`** (YAML 1.2 core schema), not a | ||
| number — intentional for OpenAPI round-trip safety. | ||
| 2. **Nodes carry `start` / `end` inline** (not a `range` tuple). `end` is | ||
| **exclusive**, and offsets are char offsets — convert with `lineCounter` | ||
| (1-based line/col). | ||
| 3. **`parseDocument` reads only the FIRST document** of a `---` stream. Use | ||
| `parseAllDocuments` for multi-doc; each doc has its own anchor scope. | ||
| 4. **Values materialize lazily via `toJS()`** (resolves aliases + merge keys); | ||
| `parse()` === `parseDocument().toJS()`. `pair.value` can be `null` (e.g. | ||
| `paths:` with no value). | ||
| 5. **Errors are collected on `doc.errors` / `doc.warnings`, not thrown.** Tab | ||
| indentation is a `TAB_INDENT` error. | ||
| Exports: `parse`, `parseDocument`, `parseAllDocuments`, `nodeAtPath`, | ||
| `lineCounter`, the guards `isScalar`/`isMap`/`isSeq`/`isPair`/`isAlias`, + node | ||
| types. Only the `.` entry. Install: `bun add @amritk/yaml`. |
@@ -1017,6 +1017,10 @@ import { resolveDoubleQuoted, resolvePlainValue, resolveSingleQuoted } from "./resolve-scalar.js"; | ||
| }); | ||
| const toJsValue = (node, merge, budget) => { | ||
| const MAX_PROJECT_DEPTH = MAX_PARSE_DEPTH * 2; | ||
| const toJsValue = (node, merge, budget, depth) => { | ||
| if (budget.left-- <= 0) { | ||
| throw new Error("Excessive alias expansion \u2014 the document may be a resource-exhaustion attack"); | ||
| } | ||
| if (depth > MAX_PROJECT_DEPTH) { | ||
| throw new Error("Excessive nesting depth \u2014 the document may be a resource-exhaustion attack"); | ||
| } | ||
| if (node === null) | ||
@@ -1027,3 +1031,3 @@ return null; | ||
| if (node.kind === "alias") { | ||
| return node.target ? toJsValue(node.target, merge, budget) : void 0; | ||
| return node.target ? toJsValue(node.target, merge, budget, depth + 1) : void 0; | ||
| } | ||
@@ -1034,3 +1038,3 @@ if (node.kind === "seq") { | ||
| for (let i = 0; i < items2.length; i++) | ||
| out[i] = toJsValue(items2[i] ?? null, merge, budget); | ||
| out[i] = toJsValue(items2[i] ?? null, merge, budget, depth + 1); | ||
| if (node.tag === "omap") | ||
@@ -1048,6 +1052,6 @@ return toOmap(out); | ||
| if (merge && key.kind === "scalar" && key.source === "<<") { | ||
| applyMerge(obj, toJsValue(pair.value, merge, budget)); | ||
| applyMerge(obj, toJsValue(pair.value, merge, budget, depth + 1)); | ||
| continue; | ||
| } | ||
| setMapKey(obj, keyText(key), pair.value ? toJsValue(pair.value, merge, budget) : null); | ||
| setMapKey(obj, keyText(key), pair.value ? toJsValue(pair.value, merge, budget, depth + 1) : null); | ||
| } | ||
@@ -1059,3 +1063,3 @@ if (node.tag === "set") { | ||
| if (pair) | ||
| set.add(toJsValue(pair.key, merge, budget)); | ||
| set.add(toJsValue(pair.key, merge, budget, depth + 1)); | ||
| } | ||
@@ -1104,3 +1108,3 @@ return set; | ||
| const { errors, warnings, merge, len } = state; | ||
| return { contents, errors, warnings, toJS: () => toJsValue(contents, merge, newExpansionBudget(len)) }; | ||
| return { contents, errors, warnings, toJS: () => toJsValue(contents, merge, newExpansionBudget(len), 0) }; | ||
| }; | ||
@@ -1107,0 +1111,0 @@ const parseDocument = (source, options = {}) => { |
+7
-0
@@ -6,3 +6,10 @@ import type { ParseOptions } from './types.js'; | ||
| * list of problems for diagnostics. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const value = parse('info:\n title: My API\n version: 1.0.0\n') | ||
| * // value.info.version is the STRING "1.0.0" (YAML 1.2 core schema), not a number. | ||
| * // Errors are collected, not thrown — use parseDocument() and read doc.errors. | ||
| * ``` | ||
| */ | ||
| export declare const parse: (source: string, options?: ParseOptions) => unknown; |
+3
-2
| { | ||
| "name": "@amritk/yaml", | ||
| "version": "0.3.3", | ||
| "version": "0.3.4", | ||
| "description": "A fast, featherweight, zero-dependency YAML parser for OpenAPI tooling — with exact source positions (line:column) on every node.", | ||
@@ -31,3 +31,4 @@ "module": "./dist/index.js", | ||
| "files": [ | ||
| "dist" | ||
| "dist", | ||
| "AI.md" | ||
| ], | ||
@@ -34,0 +35,0 @@ "publishConfig": { |
67129
3.65%20
5.26%1648
0.67%