Skip to content

Commit

Permalink
Merge pull request #248 from eemeli/lazy-anchors
Browse files Browse the repository at this point in the history
Make anchor & alias resolution lazier
  • Loading branch information
eemeli committed Mar 27, 2021
2 parents 2b8451a + f12ff89 commit 96b5f95
Show file tree
Hide file tree
Showing 37 changed files with 686 additions and 598 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -67,7 +67,7 @@ const YAML = require('yaml')
- [`new Scalar(value)`](https://eemeli.org/yaml/#scalar-values)
- [`new YAMLMap()`](https://eemeli.org/yaml/#collections)
- [`new YAMLSeq()`](https://eemeli.org/yaml/#collections)
- [`doc.anchors.createAlias(node, name?): Alias`](https://eemeli.org/yaml/#working-with-anchors)
- [`doc.createAlias(node, name?): Alias`](https://eemeli.org/yaml/#working-with-anchors)
- [`doc.createNode(value, options?): Node`](https://eemeli.org/yaml/#creating-nodes)
- [`doc.createPair(key, value): Pair`](https://eemeli.org/yaml/#creating-nodes)
- [`visit(node, visitor)`](https://eemeli.org/yaml/#modifying-nodes)
Expand Down
2 changes: 1 addition & 1 deletion docs/01_intro.md
Expand Up @@ -85,7 +85,7 @@ import {
- [`new Scalar(value)`](#scalar-values)
- [`new YAMLMap()`](#collections)
- [`new YAMLSeq()`](#collections)
- [`doc.anchors.createAlias(node, name?): Alias`](#working-with-anchors)
- [`doc.createAlias(node, name?): Alias`](#working-with-anchors)
- [`doc.createNode(value, options?): Node`](#creating-nodes)
- [`doc.createPair(key, value): Pair`](#creating-nodes)
- [`visit(node, visitor)`](#modifying-nodes)
Expand Down
21 changes: 15 additions & 6 deletions docs/03_options.md
Expand Up @@ -37,12 +37,10 @@ Document options are relevant for operations on the `Document` object, which mak

Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `stringify()`, `new Composer()`, and `new Document()`

| Name | Type | Default | Description |
| ------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| anchorPrefix | `string` | `'a'` | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default. |
| keepUndefined | `boolean` | `false` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. |
| logLevel | `'warn' ⎮ 'error' ⎮` `'silent'` | `'warn'` | Control the verbosity of `parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors (not recommended). |
| version | `'1.1' ⎮ '1.2'` | `'1.2'` | The YAML version used by documents without a `%YAML` directive. |
| Name | Type | Default | Description |
| -------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| logLevel | `'warn' ⎮ 'error' ⎮` `'silent'` | `'warn'` | Control the verbosity of `parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors (not recommended). |
| version | `'1.1' ⎮ '1.2'` | `'1.2'` | The YAML version used by documents without a `%YAML` directive. |

By default, the library will emit warnings as required by the YAML spec during parsing.
If you'd like to silence these, set the `logLevel` option to `'error'`.
Expand Down Expand Up @@ -95,6 +93,17 @@ mergeResult.target
**Merge** keys are a [YAML 1.1 feature](http://yaml.org/type/merge.html) that is not a part of the 1.2 spec.
To use a merge key, assign an alias node or an array of alias nodes as the value of a `<<` key in a mapping.

## CreateNode Options

Used by: `stringify()`, `new Document()`, `doc.createNode()`, and `doc.createPair()`

| Name | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| anchorPrefix | `string` | `'a'` | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default. |
| flow | `boolean` | `false` | Force the top-level collection node to use flow style. |
| keepUndefined | `boolean` | `false` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. |
| tag | `string` | | Specify the top-level collection type, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available in this document's schema. |

## ToJS Options

```js
Expand Down
72 changes: 1 addition & 71 deletions docs/04_documents.md
Expand Up @@ -75,7 +75,6 @@ See [Options](#options) for more information on the last argument.

| Member | Type | Description |
| ------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| anchors | [`Anchors`](#anchors) | Anchors associated with the document's nodes; also provides alias & merge node creators. |
| commentBefore | `string?` | A comment at the very beginning of the document. If not empty, separated from the rest of the document by a blank line or the doc-start indicator when stringified. |
| comment | `string?` | A comment at the end of the document. If not empty, separated from the rest of the document by a blank line when stringified. |
| contents | [`Node`](#content-nodes) `⎮ any` | The document contents. |
Expand Down Expand Up @@ -106,6 +105,7 @@ Although `parseDocument()` and `parseAllDocuments()` will leave it with `YAMLMap

| Method | Returns | Description |
| ------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| createAlias(node: Node, name?: string) | `Alias` | Create a new `Alias` node, adding the required anchor for `node`. If `name` is empty, a new anchor name will be generated. |
| createNode(value,&nbsp;options?) | `Node` | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information. |
| createPair(key,&nbsp;value,&nbsp;options?) | `Pair` | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information. |
| setSchema(version,&nbsp;options?) | `void` | Change the YAML version and schema used by the document. `version` must be either `'1.1'` or `'1.2'`; accepts all Schema options. |
Expand Down Expand Up @@ -176,73 +176,3 @@ See the section on [custom tags](#writing-custom-tags) for more on this topic.

`doc.contents.yaml` determines if an explicit `%YAML` directive should be included in the output, and what version it should use.
If changing the version after the document's creation, you'll probably want to use `doc.setSchema()` as it will also update the schema accordingly.

## Working with Anchors

A description of [alias and merge nodes](#alias-nodes) is included in the next section.

<br/>

#### `Document#anchors`

| Method | Returns | Description |
| -------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
| createAlias(node: Node, name?: string) | `Alias` | Create a new `Alias` node, adding the required anchor for `node`. If `name` is empty, a new anchor name will be generated. |
| createMergePair(...Node) | `Pair` | Create a new merge pair with the given source nodes. Non-`Alias` sources will be automatically wrapped. |
| getName(node: Node) | `string?` | The anchor name associated with `node`, if set. |
| getNames() | `string[]` | List of all defined anchor names. |
| getNode(name: string) | `Node?` | The node associated with the anchor `name`, if set. |
| newName(prefix: string) | `string` | Find an available anchor name with the given `prefix` and a numerical suffix. |
| setAnchor(node?: Node, name?: string) | `string?` | Associate an anchor with `node`. If `name` is empty, a new name will be generated. |

```js
const src = '[{ a: A }, { b: B }]'
const doc = parseDocument(src)
doc.anchors.setAnchor(doc.getIn([0, 'a'], true)) // 'a1'
doc.anchors.setAnchor(doc.getIn([1, 'b'], true)) // 'a2'
doc.anchors.setAnchor(null, 'a1') // 'a1'
doc.anchors.getNode('a2')
// { value: 'B', range: [ 16, 18 ], type: 'PLAIN' }
String(doc)
// [ { a: A }, { b: &a2 B } ]

const alias = doc.anchors.createAlias(doc.get(0, true), 'AA')
// Alias { source: YAMLMap { items: [ [Pair] ] } }
doc.add(alias)
doc.toJS()
// [ { a: 'A' }, { b: 'B' }, { a: 'A' } ]
String(doc)
// [ &AA { a: A }, { b: &a2 B }, *AA ]

doc.setSchema('1.2', { merge: true })
const merge = doc.anchors.createMergePair(alias)
// Pair {
// key: Scalar { value: '<<' },
// value: Alias { source: YAMLMap { ... } } }
doc.addIn([1], merge)
doc.toJS()
// [ { a: 'A' }, { b: 'B', a: 'A' }, { a: 'A' } ]
String(doc)
// [ &AA { a: A }, { b: &a2 B, <<: *AA }, *AA ]

// This creates a circular reference
merge.value = doc.anchors.createAlias(doc.get(1, true))
doc.toJS() // [RangeError: Maximum call stack size exceeded]
String(doc)
// [
// &AA { a: A },
// &a1 { b: &a2 B, <<: *a1 },
// *AA
// ]
```

You should make sure to only add alias and merge nodes to the document after the nodes to which they refer, or the document's YAML stringification will fail.

It is valid to have an anchor associated with a node even if it has no aliases.
`yaml` will not allow you to associate the same name with more than one node, even though this is allowed by the YAML spec (all but the last instance will have numerical suffixes added).
To add or reassign an anchor, use **`setAnchor(node, name)`**.
The second parameter is optional, and if left out either the pre-existing anchor name of the node will be used, or a new one generated.
To remove an anchor, use `setAnchor(null, name)`.
The function will return the new anchor's name, or `null` if both of its arguments are `null`.

While the `merge` option needs to be true to parse merge pairs as such, this is not required during stringification.
65 changes: 45 additions & 20 deletions docs/05_content_nodes.md
Expand Up @@ -2,32 +2,37 @@

After parsing, the `contents` value of each `YAML.Document` is the root of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) of nodes representing the document (or `null` for an empty document).

Both scalar and collection values may have an `anchor` associated with them; this is rendered in the string representation with a `&` prefix, so e.g. in `foo: &aa bar`, the value `bar` has the anchor `aa`.
Anchors are used by [Alias nodes](#alias-nodes) to allow for the same value to be used in multiple places in the document.
It is valid to have an anchor associated with a node even if it has no aliases.

## Scalar Values

```js
class NodeBase {
comment?: string, // a comment on or immediately after this
commentBefore?: string, // a comment before this
range?: [number, number],
comment?: string // a comment on or immediately after this
commentBefore?: string // a comment before this
range?: [number, number]
// the [start, end] range of characters of the source parsed
// into this node (undefined for pairs or if not parsed)
spaceBefore?: boolean,
spaceBefore?: boolean
// a blank line before this node and its commentBefore
tag?: string, // a fully qualified tag, if required
toJSON(): any // a plain JS or JSON representation of this node
tag?: string // a fully qualified tag, if required
toJSON(): any // a plain JS or JSON representation of this node
}
```

For scalar values, the `tag` will not be set unless it was explicitly defined in the source document; this also applies for unsupported tags that have been resolved using a fallback tag (string, `YAMLMap`, or `YAMLSeq`).

```js
class Scalar<T = unknown> extends NodeBase {
format?: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined,
anchor?: string // an anchor associated with this node
format?: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined
// By default (undefined), numbers use decimal notation.
// The YAML 1.2 core schema only supports 'HEX' and 'OCT'.
type?:
'BLOCK_FOLDED' | 'BLOCK_LITERAL' | 'PLAIN' |
'QUOTE_DOUBLE' | 'QUOTE_SINGLE' | undefined,
'QUOTE_DOUBLE' | 'QUOTE_SINGLE' | undefined
value: T
}
```
Expand All @@ -42,12 +47,13 @@ On the other hand, `!!int` and `!!float` stringifiers will take `format` into ac

```js
class Pair<K = unknown, V = unknown> extends NodeBase {
key: K, // When parsed, key and value are always
key: K // When parsed, key and value are always
value: V // Node or null, but can be set to anything
}

class Collection extends NodeBase {
flow?: boolean // use flow style when stringifying this
anchor?: string // an anchor associated with this node
flow?: boolean // use flow style when stringifying this
schema?: Schema
addIn(path: Iterable<unknown>, value: unknown): void
deleteIn(path: Iterable<unknown>): boolean
Expand Down Expand Up @@ -131,7 +137,8 @@ Note that for `addIn` the path argument points to the collection rather than the
<!-- prettier-ignore -->
```js
class Alias extends NodeBase {
source: Scalar | YAMLMap | YAMLSeq
source: string
resolve(doc: Document): Scalar | YAMLMap | YAMLSeq | undefined
}

const obj = YAML.parse('[ &x { X: 42 }, Y, *x ]')
Expand All @@ -146,7 +153,9 @@ YAML.stringify(obj)
// - *a1
```

`Alias` nodes provide a way to include a single node in multiple places in a document; the `source` of an alias node must be a preceding node in the document. Circular references are fully supported, and where possible the JS representation of alias nodes will be the actual source object.
`Alias` nodes provide a way to include a single node in multiple places in a document; the `source` of an alias node must be a preceding anchor in the document.
Circular references are fully supported, and where possible the JS representation of alias nodes will be the actual source object.
For ease of use, alias nodes also provide a `resolve(doc)` method to dreference its source node.

When nodes are constructed from JS structures (e.g. during `YAML.stringify()`), multiple references to the same object will result in including an autogenerated anchor at its first instance, and alias nodes to that anchor at later references.

Expand Down Expand Up @@ -176,27 +185,43 @@ String(doc)
// - balloons: 99
```

#### `YAML.Document#createNode(value, options?): Node`
#### `doc.createNode(value, replacer?, options?): Node`

To create a new node, use the `createNode(value, options?)` document method.
This will recursively wrap any input with appropriate `Node` containers.
Generic JS `Object` values as well as `Map` and its descendants become mappings, while arrays and other iterable objects result in sequences.
With `Object`, entries that have an `undefined` value are dropped.

To force flow styling on a collection, use `options.flow = true`
Use `options.replacer` to apply a replacer array or function, following the [JSON implementation][replacer].
To specify the collection type, set `options.tag` to its identifying string, e.g. `"!!omap"`.
Note that this requires the corresponding tag to be available in the document's schema.
Use a `replacer` to apply a replacer array or function, following the [JSON implementation][replacer].
To force flow styling on a collection, use the `flow: true` option.
For all available options, see the [CreateNode Options](#createnode-options) section.

[replacer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter

As a possible side effect, this method may add entries to the document's [`anchors`](#working-with-anchors).

The primary purpose of this method is to enable attaching comments or other metadata to a value, or to otherwise exert more fine-grained control over the stringified output.
To that end, you'll need to assign its return value to the `contents` of a document (or somewhere within said contents), as the document's schema is required for YAML string output.
If you're not interested in working with such metadata, document `contents` may also include non-`Node` values at any level.

<h4 style="clear:both"><code>new YAMLMap(), new YAMLSeq(), doc.createPair(key, value)</code></h4>
<h4 style="clear:both"><code>doc.createAlias(node, name?): Alias</code></h4>

```js
const alias = doc.createAlias(doc.get(1, true), 'foo')
doc.add(alias)
String(doc)
// - some # A commented item
// - &foo values
// - balloons: 99
// - *foo
```

Create a new `Alias` node, ensuring that the target `node` has the required anchor.
If `node` already has an anchor, `name` is ignored.
Otherwise, the `node.anchor` value will be set to `name`, or if an anchor with that name is already present in the document, `name` will be used as a prefix for a new unique anchor.
If `name` is undefined, the generated anchor will use 'a' as a prefix.

You should make sure to only add alias nodes to the document after the nodes to which they refer, or the document's YAML stringification will fail.

<h4 style="clear:both"><code>new YAMLMap(), new YAMLSeq(), doc.createPair(key, value): Pair</code></h4>

```js
import { Document, YAMLSeq } from 'yaml'
Expand Down

0 comments on commit 96b5f95

Please sign in to comment.