# elide:yaml

> The elide:yaml module: parse, stream and write YAML as plain JavaScript data.

`elide:yaml` parses, streams and writes YAML 1.2. Parsed documents are plain data: mappings are objects,
sequences are arrays and scalars are primitives. There is nothing to install.

```js
import { parse, stringify } from "elide:yaml";

const config = parse(`
port: 8080
hosts: [a, b]
tls:
  cert: ./cert.pem
`);
config.port; // 8080
config.hosts; // ["a", "b"]
stringify(config);
// "port: 8080\nhosts:\n  - a\n  - b\ntls:\n  cert: ./cert.pem\n"
```

Import it as `elide:yaml`. A bare `"yaml"` specifier resolves to the npm package of that name.

## Read a file

```yaml
# team.yaml
team: platform
users:
  - id: 1
    name: ada
  - id: 2
    name: lin
```

```js
// team.mjs
import { parseFile } from "elide:yaml";

const { team, users } = parseFile("team.yaml");
for (const user of users) console.log(team, user.id, user.name);
```

`elide run team.mjs` prints:

```text
platform 1 ada
platform 2 lin
```

## Parsing

- `parse(input, options?)` reads a string, `Uint8Array`, `ArrayBuffer` or `DataView` (UTF-8, or a
  BOM-marked encoding) holding one document and returns its value. Empty input is `null`; input
  holding several documents throws.
- `parseAll(input, options?)` returns every document of a stream as an array.
- `parseDocument(input, options?)` and `parseAllDocuments(input, options?)` return
  [documents](#documents), which carry their directives.
- `parseFile(path, options?)` reads one document from a file path or `file:` URL.
- `stream(source, options?)` reads documents one at a time from a path, `file:` URL, bytes or a
  `ReadableStream` of bytes. See [Streaming](#streaming).

Values follow the YAML 1.2 core schema: `true`/`false` are booleans, `null`/`~`/empty are `null`,
decimal, `0x` and `0o` integers and floats (including `.inf` and `.nan`) are numbers, and everything
else is a string. Integers outside ±(2<sup>53</sup>−1) are `bigint`s. Keys are always strings, so
`1: one` gives `{ "1": "one" }`.

```js
parse("a: 1\nb: 1.5\nc: yes\nd: 2024-01-01\n");
// { a: 1, b: 1.5, c: "yes", d: "2024-01-01" }
parse("big: 18446744073709551616"); // { big: 18446744073709551616n }
```

### Anchors, aliases and merge keys

Aliases refer to the value built for their anchor, so `*ref` is the same object as `&ref`. An alias
to a node that contains it is rejected with `ERR_YAML_SYNTAX`. Merge keys (`<<`) are folded into their
mapping with `merge: true`, or by default when reading YAML 1.1; otherwise `<<` is an ordinary key.

```js
const v = parse("base: &b\n  x: 1\nref: *b\n");
v.ref === v.base; // true

parse("base: &b\n  a: 1\n  b: 2\nchild:\n  <<: *b\n  b: 3\n", { merge: true }).child;
// { a: 1, b: 3 }
```

### Tags

`!!binary` reads as a `Uint8Array` and `!!timestamp` as a `Date`. Other tags leave the value as the
schema parses it; with `unknownTags: "error"` they throw instead. `customTags` maps a tag to a
function that receives the parsed value and the tag and returns what to use:

```js
parse("db: !secret DATABASE_URL", {
  customTags: { "!secret": (name) => process.env[name] },
});
```

Tag URIs are given in full for the standard tags (`"tag:yaml.org,2002:timestamp"`) and as written
for local tags (`"!secret"`).

### Options

| Option | Default | Meaning |
| --- | --- | --- |
| `version` | `"1.2"` | `"1.1"` selects the YAML 1.1 schema and merge keys |
| `schema` | `"core"` | `"json"` (JSON forms only), `"failsafe"` (strings only) or `"yaml-1.1"` (`yes`/`no`, `010`, `1_000`) |
| `merge` | `false` | Fold `<<` merge keys |
| `uniqueKeys` | `true` | Reject a mapping that repeats a key; `false` keeps the last value |
| `aliases` | `true` | Allow anchors and aliases |
| `unknownTags` | `"keep"` | `"error"` rejects tags the schema does not know |
| `customTags` | none | Tag handlers, by tag |
| `limits` | see below | Resource bounds |

`limits` bounds `maxBytes` (input size, 64 MiB), `maxNodes` (one million), `maxDepth` (128),
`maxAliases` (10 000) and `maxAliasExpansion` (nodes aliases may expand to, one million).

## Documents

`parseDocument()` returns the document's `contents`, the `%YAML` directive as `version` (or `null`)
and `%TAG` prefixes by handle as `tags`, with `toJS()` and `toString(options?)`:

```js
import { parseDocument } from "elide:yaml";

const doc = parseDocument("%YAML 1.2\n---\nname: elide\n");
doc.contents; // { name: "elide" }
doc.version; // "1.2"
doc.toString(); // "name: elide\n"
```

## Streaming

`stream()` returns an async iterator that yields one document per step:

```js
import { stream } from "elide:yaml";

for await (const record of stream("records.yaml")) {
  console.log(record.id);
}
```

Documents are cut at `---` and `...` lines and parsed as the loop reaches them, so a file of many
documents never needs to be in memory at once. `maxDocumentBytes` (default 1 MiB) bounds one
document. Breaking out of the loop, `await reader.close()` and `await using reader = stream(...)`
all stop reading and release the source. Bytes passed to `stream()` are copied first.

## Writing

`stringify(value, options?)` writes block-style YAML ending in a newline. Objects serialize as
`JSON.stringify` would (own enumerable keys, `toJSON()`, `undefined` and functions skipped in
objects and written as `null` in arrays), plus `Map` as a mapping, `Set` as a sequence, `Date` as a
timestamp and byte views as `!!binary`. Strings that would read back as something else (`"123"`,
`"true"`, `"a: b"`) are quoted, and multi-line strings become block scalars. Repeated objects become
anchors and aliases. Cycles throw a `TypeError` with code `ERR_INVALID_ARG_VALUE`, regardless of
`noRefs`. `stringifyAll(documents, options?)`
separates documents with `---` lines.

```js
import { stringify } from "elide:yaml";

stringify({ name: "ada", tags: ["x", "y"], note: "line one\nline two" });
// "name: ada\ntags:\n  - x\n  - y\nnote: |-\n  line one\n  line two\n"

stringify({ a: [1, 2] }, { flowLevel: 0 }); // "{ a: [1, 2] }\n"
```

| Option | Default | Meaning |
| --- | --- | --- |
| `indent` | `2` | Spaces per level, 1 to 8 |
| `flowLevel` | `-1` | Depth from which collections use flow style; `0` for all, `-1` for none |
| `sortKeys` | `false` | Sort mapping keys |
| `noRefs` | `false` | Write repeated objects inline |
| `quotingType` | `'"'` | `"'"` quotes with single quotes |
| `forceQuotes` | `false` | Quote every string |

## Errors

Failures throw errors with a `code`; parse errors also carry 1-based `line` and `column` properties
and name the position in their message.

| Code | Error | Cause |
| --- | --- | --- |
| `ERR_YAML_SYNTAX` | `SyntaxError` | Malformed YAML, a duplicate key, a rejected alias or tag, or several documents where one was expected |
| `ERR_YAML_LIMIT` | `Error` | A `limits` bound or `maxDocumentBytes` was exceeded |
| `ERR_YAML_CLOSED` | `Error` | A closed stream was read |
| `ERR_YAML_IO` | `Error` | The source failed while reading |
| `ERR_INVALID_ARG_TYPE`, `ERR_INVALID_ARG_VALUE` | `TypeError` | An invalid argument, option or value to write |
| `ERR_OUT_OF_RANGE` | `RangeError` | A numeric option out of range |

File errors (`ENOENT`, `EACCES`, `EISDIR`) match `node:fs`.

## TypeScript

`@elide-dev/types` declares the module. Name the shape you expect with a type argument:

```ts
import { parse, stream } from "elide:yaml";

interface Config {
  port: number;
  hosts: string[];
}

const config = parse<Config>(text);
for await (const record of stream<{ id: string }>("records.yaml")) {
  record.id;
}
```

## With `elide:csv`

Parsed documents are plain data, so [`elide:csv`](https://elide.help/docs/api-csv) writes a list of mappings directly:

```js
// to-csv.mjs
import { parseFile } from "elide:yaml";
import { stringify } from "elide:csv";

const { users } = parseFile("team.yaml");
process.stdout.write(stringify(users, { recordSeparator: "\n" }));
```

`elide run to-csv.mjs` prints:

```text
id,name
1,ada
2,lin
```
