Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/vitest.browserstack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default defineConfig({
instances
},
globals: true,
setupFiles: ['tests/_setup.ts'],
include: ['tests/**/*.{js,ts}'],
exclude: [
'tests/_*',
Expand Down
12 changes: 5 additions & 7 deletions docs/04_documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,14 @@ which is expected to always contain a `YAMLMap`, `YAMLSeq`, or `Scalar` value.
```js
const doc = parseDocument('a: 1\nb: [2, 3]\n')
doc.get('a') // 1
doc.getIn([]) // YAMLMap { items: [Pair, Pair], ... }
doc.hasIn(['b', 0]) // true
doc.addIn(['b'], 4) // -> doc.get('b').items.length === 3
doc.deleteIn(['b', 1]) // true
doc.getIn(['b', 1]) // 4
doc.get('b').has(0) // true
doc.get('b').push(4) // -> doc.get('b').items.length === 3
doc.get('b').delete(1) // true
doc.get('b').get(1) // 4
```

In addition to the above, the document object also provides the same **accessor methods** as [collections](#collections), based on the top-level collection:
`add`, `delete`, `get`, `has`, and `set`, along with their deeper variants `addIn`, `deleteIn`, `getIn`, `hasIn`, and `setIn`.
For the `*In` methods using an empty `path` value (i.e. `[]`) will refer to the document's top-level `value`.
`delete`, `get`, `has`, and `set`.

#### `Document#toJS()`, `Document#toJSON()` and `Document#toString()`

Expand Down
82 changes: 32 additions & 50 deletions docs/05_content_nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,33 +54,26 @@ class Pair {
value: Node | null
}

class Collection implements NodeBase {
interface CollectionBase extends NodeBase {
anchor?: string // an anchor associated with this node
flow?: boolean // use flow style when stringifying this
schema?: Schema
addIn(path: unknown[], value: unknown): void
clone(schema?: Schema): this // a deep copy of this collection
deleteIn(path: unknown[]): boolean
getIn(path: unknown[], keepScalar?: boolean): unknown
hasIn(path: unknown[]): boolean
setIn(path: unknown[], value: unknown): void
}

class YAMLMap<K = unknown, V = unknown> extends Collection {
items: Pair<K, V>[]
add(pair: Pair<K, V> | { key: K; value: V }, overwrite?: boolean): void
class YAMLMap<K = unknown, V = unknown> extends Array<Pair<K, V>> implements CollectionBase {
delete(key: K): boolean
get(key: K, keepScalar?: boolean): unknown
has(key: K): boolean
push(...pairs: Pair<K, V>[]): number
set(key: K, value: V): void
}

class YAMLSeq<T = unknown> extends Collection {
items: T[]
add(value: T): void
class YAMLSeq<T = unknown> extends Array<NodeOf<T>> implements CollectionBase {
delete(key: number | Scalar<number>): boolean
get(key: number | Scalar<number>, keepScalar?: boolean): unknown
has(key: number | Scalar<number>): boolean
push(...items: Array<T | NodeOf<T>>): number
set(key: number | Scalar<number>, value: T): void
}
```
Expand All @@ -96,45 +89,34 @@ The `yaml-1.1` schema includes [additional collections](https://yaml.org/type/in

All of the collections provide the following accessor methods:

| Method | Returns | Description |
| ----------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| add(value), addIn(path, value) | `void` | Adds a value to the collection. For `!!map` and `!!omap` the value must be a Pair instance or a `{ key, value }` object, which may not have a key that already exists in the map. |
| delete(key), deleteIn(path) | `boolean` | Removes a value from the collection. Returns `true` if the item was found and removed. |
| get(key,&nbsp;[keep]), getIn(path,&nbsp;[keep]) | `any` | Returns value at `key`, or `undefined` if not found. By default unwraps scalar values from their surrounding node; to disable set `keep` to `true` (collections are always returned intact). |
| has(key), hasIn(path) | `boolean` | Checks if the collection includes a value with the key `key`. |
| set(key, value), setIn(path, value) | `any` | Sets a value in this collection. For `!!set`, `value` needs to be a boolean to add/remove the item from the set. When overwriting a `Scalar` value with a scalar, the original node is retained. |
| Method | Returns | Description |
| --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| delete(key) | `boolean` | Removes a value from the collection. Returns `true` if the item was found and removed. |
| get(key) | `Node` | Returns value at `key`, or `undefined` if not found. |
| has(key) | `boolean` | Checks if the collection includes a value with the key `key`. |
| push(...values) | `number` | Adds values to the collection. For `!!map` and `!!omap` the value must be a Pair instance, which must not have a key that already exists in the map. |
| set(key, value) | `any` | Sets a value in this collection. For `!!set`, `value` needs to be a boolean to add/remove the item from the set. When overwriting a `Scalar` value with a scalar, the original node is retained. |

<!-- prettier-ignore -->
```js
const doc = new YAML.Document({ a: 1, b: [2, 3] }) // { a: 1, b: [ 2, 3 ] }
doc.add({ key: 'c', value: 4 }) // { a: 1, b: [ 2, 3 ], c: 4 }
doc.addIn(['b'], 5) // { a: 1, b: [ 2, 3, 5 ], c: 4 }
doc.set('c', 42) // { a: 1, b: [ 2, 3, 5 ], c: 42 }
doc.setIn(['c', 'x']) // Error: Expected YAML collection at c. Remaining path: x
doc.delete('c') // { a: 1, b: [ 2, 3, 5 ] }
doc.deleteIn(['b', 1]) // { a: 1, b: [ 2, 5 ] }

doc.get('a') // 1
doc.get('a', true) // Scalar { value: 1 }
doc.getIn(['b', 1]) // 5
doc.set('c', 4) // { a: 1, b: [ 2, 3 ], c: 4 }
doc.get('b').push(5) // { a: 1, b: [ 2, 3, 5 ], c: 4 }
doc.set('c', 42) // { a: 1, b: [ 2, 3, 5 ], c: 42 }
doc.get('c').set('x') // TypeError: doc.get(...).set is not a function
doc.delete('c') // { a: 1, b: [ 2, 3, 5 ] }
doc.get('b').delete(1) // { a: 1, b: [ 2, 5 ] }

doc.get('a') // Scalar { value: 1 }
doc.get('b').get(1) // Scalar { value: 5 }
doc.has(doc.createNode('a')) // true
doc.has('c') // false
doc.hasIn(['b', '0']) // true
doc.get('b').has(0) // true
```

For all of these methods, the keys may be nodes or their wrapped scalar values (i.e. `42` will match `Scalar { value: 42 }`).
Keys for `!!seq` should be positive integers, or their string representations.
`add()` and `set()` do not automatically call `doc.createNode()` to wrap the value.

Each of the methods also has a variant that requires an iterable as the first parameter, and allows fetching or modifying deeper collections.
If any intermediate node in `path` is a scalar rather than a collection, an error will be thrown.
If any of the intermediate collections is not found:

- `getIn` and `hasIn` will return `undefined` or `false` (respectively)
- `addIn` and `setIn` will create missing collections; non-negative integer keys will create sequences, all other keys create maps
- `deleteIn` will throw an error

Note that for `addIn` the path argument points to the collection rather than the item; for maps its `value` should be a `Pair` or an object with `{ key, value }` fields.
Keys for `!!seq` should be non-negative integers, or their string representations.
`set()` will internally call `doc.createNode()` to wrap the value.

## Alias Nodes

Expand Down Expand Up @@ -181,8 +163,8 @@ const map = doc.createNode({ balloons: 99 })
// key: Scalar { value: 'balloons' },
// value: Scalar { value: 99 } } ] }

doc.add(map)
doc.get(0, true).comment = ' A commented item'
doc.value.push(map)
doc.get(0).comment = ' A commented item'
String(doc)
// - some # A commented item
// - values
Expand Down Expand Up @@ -213,8 +195,8 @@ To that end, you'll need to assign its return value to the `value` of a document
<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)
const alias = doc.createAlias(doc.get(1), 'foo')
doc.value.push(alias)
String(doc)
// - some # A commented item
// - &foo values
Expand All @@ -239,7 +221,7 @@ const doc = new Document([
42,
{ including: 'objects', 3: 'a string' }
])
doc.add(doc.createPair(1, 'a number'))
doc.value.push(doc.createPair(1, 'a number'))

doc.toString()
// - some values
Expand All @@ -266,12 +248,12 @@ const doc = YAML.parseDocument(`
- 1: a number
`)

const obs = doc.getIn([2, 'including'], true)
const obs = doc.get(2).get('including')
obs.type = 'QUOTE_DOUBLE'

YAML.visit(doc, {
Pair(_, pair) {
if (pair.key && pair.key.value === '3') return YAML.visit.REMOVE
if (pair.key?.value === '3') return YAML.visit.REMOVE
},
Scalar(key, node) {
if (
Expand All @@ -292,7 +274,7 @@ String(doc)
```

In general, it's safe to modify nodes manually, e.g. splicing the `items` array of a `YAMLMap` or setting its `flow` value to `true`.
For operations on nodes at a known location in the tree, it's probably easiest to use `doc.getIn(path, true)` to access them.
For operations on nodes at a known location in the tree, it's probably easiest to use `doc.get(...)` to access them.
For more complex or general operations, a visitor API is provided:

#### `YAML.visit(node, visitor): void`
Expand Down
16 changes: 9 additions & 7 deletions docs/06_custom_tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ const nullObject = {
tag: '!nullobject',
collection: 'map',
nodeClass: YAMLNullObject,
createNode(nc, obj) {
return YAMLNullObject.create(nc, obj)
},
identify: v => !!(typeof v === 'object' && v && !Object.getPrototypeOf(v))
}

Expand Down Expand Up @@ -147,18 +150,17 @@ class YAMLError extends YAMLMap {
})
return Object.assign(er, rest)
}

static from(schema, obj, ctx) {
const { name, message, stack } = obj
// ensure these props remain, even if not enumerable
return super.from(schema, { ...obj, name, message, stack }, ctx)
}
}

const error = {
tag: '!error',
collection: 'map',
nodeClass: YAMLError,
createNode(nc, obj) {
const { name, message, stack } = obj
// ensure these props remain, even if not enumerable
return YAMLNullObject.create(nc, { ...obj, name, message, stack })
},
identify: v => !!(typeof v === 'object' && v && v instanceof Error)
}

Expand Down Expand Up @@ -240,7 +242,7 @@ import {

To define your own tag, you'll need to define an object comprising of some of the following fields. Those in bold are required:

- `createNode(schema, value, ctx): Node` is an optional factory function, used e.g. by collections when wrapping JS objects as AST nodes.
- `createNode(nodeCreator, value): Node` is a factory function, required by collection tags for wrapping JS objects as AST nodes.
- `format: string` If a tag has multiple forms that should be parsed and/or stringified differently, use `format` to identify them. Used by `!!int` and `!!float`.
- **`identify(value): boolean`** is used by `doc.createNode()` to detect your data type, e.g. using `typeof` or `instanceof`. Required.
- `nodeClass: Node` is the `Node` child class that implements this tag. Required for collections and tags that have overlapping JS representations.
Expand Down
6 changes: 3 additions & 3 deletions src/compose/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Directives } from '../doc/directives.ts'
import { Document, type DocValue } from '../doc/Document.ts'
import type { ErrorCode } from '../errors.ts'
import { YAMLParseError, YAMLWarning } from '../errors.ts'
import { Collection } from '../nodes/Collection.ts'
import { isCollection } from '../nodes/identity.ts'
import type { Range } from '../nodes/Node.ts'
import { Pair } from '../nodes/Pair.ts'
import type {
Expand Down Expand Up @@ -111,8 +111,8 @@ export class Composer<
doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment
} else if (afterEmptyLine || doc.directives.docStart) {
doc.commentBefore = comment
} else if (dc instanceof Collection && !dc.flow && dc.items.length > 0) {
let it = dc.items[0]
} else if (isCollection(dc) && !dc.flow && dc.length > 0) {
let it = dc[0]
if (it instanceof Pair) it = it.key
const cb = it.commentBefore
it.commentBefore = cb ? `${comment}\n${cb}` : comment
Expand Down
6 changes: 3 additions & 3 deletions src/compose/resolve-block-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function resolveBlockMap(
if (ctx.schema.compat) flowIndentCheck(bm.indent, key, onError)
ctx.atKey = false

if (mapIncludes(ctx, map.items, keyNode))
if (mapIncludes(ctx, map, keyNode))
onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique')

// value properties
Expand Down Expand Up @@ -116,7 +116,7 @@ export function resolveBlockMap(
offset = valueNode.range![2]
const pair = new Pair(keyNode, valueNode)
if (ctx.options.keepSourceTokens) pair.srcToken = collItem
map.items.push(pair)
map._push(pair)
} else {
// key with no value
if (implicitKey)
Expand All @@ -131,7 +131,7 @@ export function resolveBlockMap(
}
const pair = new Pair(keyNode)
if (ctx.options.keepSourceTokens) pair.srcToken = collItem
map.items.push(pair)
map._push(pair)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/compose/resolve-block-seq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function resolveBlockSeq(
: composeEmptyNode(ctx, props.end, start, null, props, onError)
if (ctx.schema.compat) flowIndentCheck(bs.indent, value, onError)
offset = node.range![2]
seq.items.push(node)
seq._push(node)
}
seq.range = [bs.offset, offset, commentEnd ?? offset]
return seq
Expand Down
17 changes: 9 additions & 8 deletions src/compose/resolve-flow-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export function resolveFlowCollection(
): YAMLMap | YAMLSeq {
const isMap = fc.start.source === '{'
const fcName = isMap ? 'flow map' : 'flow sequence'
const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq)
const coll = new NodeClass(ctx.schema) as YAMLMap | YAMLSeq
let coll
if (tag?.nodeClass) coll = new tag.nodeClass(ctx.schema) as YAMLMap | YAMLSeq
else coll = isMap ? new YAMLMap(ctx.schema) : new YAMLSeq(ctx.schema)
coll.flow = true
const atRoot = ctx.atRoot
if (atRoot) ctx.atRoot = false
Expand Down Expand Up @@ -93,7 +94,7 @@ export function resolveFlowCollection(
}
}
if (prevItemComment) {
let prev = coll.items[coll.items.length - 1]
let prev = coll[coll.length - 1]
if (prev instanceof Pair) prev = prev.value ?? prev.key
if (prev.comment) prev.comment += '\n' + prevItemComment
else prev.comment = prevItemComment
Expand All @@ -108,7 +109,7 @@ export function resolveFlowCollection(
const valueNode = value
? composeNode(ctx, value, props, onError)
: composeEmptyNode(ctx, props.end, sep, null, props, onError)
;(coll as YAMLSeq).items.push(valueNode)
;(coll as YAMLSeq)._push(valueNode)
offset = valueNode.range![2]
if (isBlock(value)) onError(valueNode.range!, 'BLOCK_IN_FLOW', blockMsg)
} else {
Expand Down Expand Up @@ -190,16 +191,16 @@ export function resolveFlowCollection(
if (ctx.options.keepSourceTokens) pair.srcToken = collItem
if (isMap) {
const map = coll as YAMLMap
if (mapIncludes(ctx, map.items, keyNode))
if (mapIncludes(ctx, map, keyNode))
onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique')
map.items.push(pair)
map._push(pair)
} else {
const map = new YAMLMap(ctx.schema)
map.flow = true
map.items.push(pair)
map._push(pair)
const endRange = (valueNode ?? keyNode).range!
map.range = [keyNode.range![0], endRange[1], endRange[2]]
;(coll as YAMLSeq).items.push(map)
;(coll as YAMLSeq)._push(map)
}
offset = valueNode ? valueNode.range![2] : valueProps.end
}
Expand Down
Loading
Loading