Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop type property from all but Scalar nodes #240

Merged
merged 6 commits into from Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 61 additions & 35 deletions docs/05_content_nodes.md
Expand Up @@ -5,62 +5,84 @@ After parsing, the `contents` value of each `YAML.Document` is the root of an [A
## Scalar Values

```js
class Node {
comment: ?string, // a comment on or immediately after this
commentBefore: ?string, // a comment before this
range: ?[number, number],
class NodeBase {
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
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, `Map`, or `Seq`).
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 extends Node {
format: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined,
class Scalar<T = unknown> extends NodeBase {
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:
type?:
'BLOCK_FOLDED' | 'BLOCK_LITERAL' | 'PLAIN' |
'QUOTE_DOUBLE' | 'QUOTE_SINGLE' | undefined,
value: any
value: T
}
```

A parsed document's contents will have all of its non-object values wrapped in `Scalar` objects, which themselves may be in some hierarchy of `Map` and `Seq` collections. However, this is not a requirement for the document's stringification, which is rather tolerant regarding its input values, and will use [`doc.createNode()`](#creating-nodes) when encountering an unwrapped value.
A parsed document's contents will have all of its non-object values wrapped in `Scalar` objects, which themselves may be in some hierarchy of `YAMLMap` and `YAMLSeq` collections.
However, this is not a requirement for the document's stringification, which is rather tolerant regarding its input values, and will use [`doc.createNode()`](#creating-nodes) when encountering an unwrapped value.

When stringifying, the node `type` will be taken into account by `!!str` and `!!binary` values, and ignored by other scalars. On the other hand, `!!int` and `!!float` stringifiers will take `format` into account.
When stringifying, the node `type` will be taken into account by `!!str` and `!!binary` values, and ignored by other scalars.
On the other hand, `!!int` and `!!float` stringifiers will take `format` into account.

## Collections

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

class Map extends Node {
items: Array<Pair>,
type: 'FLOW_MAP' | 'MAP' | undefined
class Collection extends NodeBase {
flow?: boolean // use flow style when stringifying this
schema?: Schema
addIn(path: Iterable<unknown>, value: unknown): void
deleteIn(path: Iterable<unknown>): boolean
getIn(path: Iterable<unknown>, keepScalar?: boolean): unknown
hasIn(path: Iterable<unknown>): boolean
setIn(path: Iterable<unknown>, value: unknown): void
}

class Seq extends Node {
items: Array<Node | any>,
type: 'FLOW_SEQ' | 'SEQ' | undefined
class YAMLMap<K = unknown, V = unknown> extends Collection {
items: Pair<K, V>[]
add(pair: Pair<K, V> | { key: K; value: V }, overwrite?: boolean): void
delete(key: K): boolean
get(key: K, keepScalar?: boolean): unknown
has(key: K): boolean
set(key: K, value: V): void
}

class YAMLSeq<T = unknown> extends Collection {
items: T[]
add(value: T): void
delete(key: number | Scalar<number>): boolean
get(key: number | Scalar<number>, keepScalar?: boolean): unknown
has(key: number | Scalar<number>): boolean
set(key: number | Scalar<number>, value: T): void
}
```

Within all YAML documents, two forms of collections are supported: sequential `Seq` collections and key-value `Map` collections. The JavaScript representations of these collections both have an `items` array, which may (`Seq`) or must (`Map`) consist of `Pair` objects that contain a `key` and a `value` of any type, including `null`. The `items` array of a `Seq` object may contain values of any type.
Within all YAML documents, two forms of collections are supported: sequential `YAMLSeq` collections and key-value `YAMLMap` collections.
The JavaScript representations of these collections both have an `items` array, which may (`YAMLSeq`) or must (`YAMLMap`) consist of `Pair` objects that contain a `key` and a `value` of any type, including `null`.
The `items` array of a `YAMLSeq` object may contain values of any type.

When stringifying collections, by default block notation will be used. Flow notation will be selected if `type` is `FLOW_MAP` or `FLOW_SEQ`, the collection is within a surrounding flow collection, or if the collection is in an implicit key.
When stringifying collections, by default block notation will be used.
Flow notation will be selected if `flow` is `true`, the collection is within a surrounding flow collection, or if the collection is in an implicit key.

The `yaml-1.1` schema includes [additional collections](https://yaml.org/type/index.html) that are based on `Map` and `Seq`: `OMap` and `Pairs` are sequences of `Pair` objects (`OMap` requires unique keys & corresponds to the JS Map object), and `Set` is a map of keys with null values that corresponds to the JS Set object.
The `yaml-1.1` schema includes [additional collections](https://yaml.org/type/index.html) that are based on `YAMLMap` and `YAMLSeq`: `OMap` and `Pairs` are sequences of `Pair` objects (`OMap` requires unique keys & corresponds to the JS Map object), and `Set` is a map of keys with null values that corresponds to the JS Set object.

All of the collections provide the following accessor methods:

Expand Down Expand Up @@ -90,9 +112,13 @@ doc.has('c') // false
doc.hasIn(['b', '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.
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:
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
Expand All @@ -103,9 +129,8 @@ Note that for `addIn` the path argument points to the collection rather than the
## Alias Nodes

```js
class Alias extends Node {
source: Scalar | Map | Seq,
type: 'ALIAS'
class Alias extends NodeBase {
source: Scalar | YAMLMap | YAMLSeq
}

const obj = YAML.parse('[ &x { X: 42 }, Y, *x ]')
Expand All @@ -127,8 +152,7 @@ When nodes are constructed from JS structures (e.g. during `YAML.stringify()`),
```js
class Merge extends Pair {
key: Scalar('<<'), // defined by the type specification
value: Seq<Alias(Map)>, // stringified as *A if length = 1
type: 'MERGE_PAIR'
value: Seq<Alias(Map)> // stringified as *A if length = 1
}
```

Expand Down Expand Up @@ -205,7 +229,9 @@ doc.toString()

To construct a `YAMLSeq` or `YAMLMap`, use `new Document()` or `doc.createNode()` with array, object or iterable input, or create the collections directly by importing the classes from `yaml`.

Once created, normal array operations may be used to modify the `items` array. New `Pair` objects may created either by importing the class from `yaml` and using its `new Pair(key, value)` constructor, or by using the `doc.createPair(key, value, options?)` method. The latter will recursively wrap the `key` and `value` as nodes, and accepts the same options as `doc.createNode()`
Once created, normal array operations may be used to modify the `items` array.
New `Pair` objects may created either by importing the class from `yaml` and using its `new Pair(key, value)` constructor, or by using the `doc.createPair(key, value, options?)` method.
The latter will recursively wrap the `key` and `value` as nodes, and accepts the same options as `doc.createNode()`

## Modifying Nodes

Expand Down Expand Up @@ -243,7 +269,7 @@ String(doc)
// - 1: 'a number'
```

In general, it's safe to modify nodes manually, e.g. splicing the `items` array of a `YAMLMap` or changing its `type` from `'MAP'` to `'FLOW_MAP'`.
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 more complex or general operations, a visitor API is provided:

Expand Down
20 changes: 2 additions & 18 deletions src/compose/compose-collection.ts
@@ -1,6 +1,5 @@
import { Type } from '../constants.js'
import type { Document } from '../doc/Document.js'
import { isNode, ParsedNode } from '../nodes/Node.js'
import { isMap, isNode, ParsedNode } from '../nodes/Node.js'
import { Scalar } from '../nodes/Scalar.js'
import type { YAMLMap } from '../nodes/YAMLMap.js'
import type { YAMLSeq } from '../nodes/YAMLSeq.js'
Expand Down Expand Up @@ -48,22 +47,7 @@ export function composeCollection(
return coll
}

let expType: 'map' | 'seq' // | null = null
switch (coll.type) {
case Type.FLOW_MAP:
case Type.MAP:
expType = 'map'
break
case Type.FLOW_SEQ:
case Type.SEQ:
expType = 'seq'
break
default:
onError(coll.range[0], `Unexpected collection type: ${coll.type}`)
coll.tag = tagName
return coll
}

const expType = isMap(coll) ? 'map' : 'seq'
let tag = doc.schema.tags.find(
t => t.collection === expType && t.tag === tagName
) as CollectionTag | undefined
Expand Down
6 changes: 1 addition & 5 deletions src/compose/composer.ts
Expand Up @@ -78,11 +78,7 @@ export class Composer {
doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment
} else if (afterEmptyLine || doc.directives.marker || !dc) {
doc.commentBefore = comment
} else if (
isCollection(dc) &&
(dc.type === 'MAP' || dc.type === 'SEQ') &&
dc.items.length > 0
) {
} else if (isCollection(dc) && !dc.flow && dc.items.length > 0) {
const it = dc.items[0]
const cb = it.commentBefore
it.commentBefore = cb ? `${comment}\n${cb}` : comment
Expand Down
6 changes: 1 addition & 5 deletions src/compose/resolve-block-map.ts
@@ -1,10 +1,8 @@
import { Type } from '../constants.js'
import type { Document } from '../doc/Document.js'
import { Pair } from '../nodes/Pair.js'
import { YAMLMap } from '../nodes/YAMLMap.js'
import type { BlockMap } from '../parse/tokens.js'
import type { ComposeNode } from './compose-node.js'
import { resolveMergePair } from './resolve-merge-pair.js'
import { resolveProps } from './resolve-props.js'
import { containsNewline } from './util-contains-newline.js'

Expand All @@ -19,7 +17,6 @@ export function resolveBlockMap(
) {
const start = offset
const map = new YAMLMap(doc.schema)
map.type = Type.MAP
if (anchor) doc.anchors.setAnchor(map, anchor)

for (const { start, key, sep, value } of items) {
Expand Down Expand Up @@ -92,8 +89,7 @@ export function resolveBlockMap(
? composeNode(doc, value, valueProps, onError)
: composeEmptyNode(doc, offset, sep, null, valueProps, onError)
offset = valueNode.range[1]
const pair = new Pair(keyNode, valueNode)
map.items.push(doc.schema.merge ? resolveMergePair(pair, onError) : pair)
map.items.push(new Pair(keyNode, valueNode))
} else {
// key with no value
if (implicitKey)
Expand Down
8 changes: 4 additions & 4 deletions src/compose/resolve-block-scalar.ts
@@ -1,4 +1,4 @@
import { Type } from '../constants.js'
import { Scalar } from '../nodes/Scalar.js'
import type { BlockScalar } from '../parse/tokens.js'

export function resolveBlockScalar(
Expand All @@ -7,13 +7,13 @@ export function resolveBlockScalar(
onError: (offset: number, message: string) => void
): {
value: string
type: Type.BLOCK_FOLDED | Type.BLOCK_LITERAL | null
type: Scalar.BLOCK_FOLDED | Scalar.BLOCK_LITERAL | null
comment: string
length: number
} {
const header = parseBlockScalarHeader(scalar, strict, onError)
if (!header) return { value: '', type: null, comment: '', length: 0 }
const type = header.mode === '>' ? Type.BLOCK_FOLDED : Type.BLOCK_LITERAL
const type = header.mode === '>' ? Scalar.BLOCK_FOLDED : Scalar.BLOCK_LITERAL
const lines = scalar.source ? splitLines(scalar.source) : []

// determine the end of content & start of chomping
Expand Down Expand Up @@ -79,7 +79,7 @@ export function resolveBlockScalar(
indent = ''
}

if (type === Type.BLOCK_LITERAL) {
if (type === Scalar.BLOCK_LITERAL) {
value += sep + indent.slice(trimIndent) + content
sep = '\n'
} else if (indent.length > trimIndent || content[0] === '\t') {
Expand Down
2 changes: 0 additions & 2 deletions src/compose/resolve-block-seq.ts
@@ -1,4 +1,3 @@
import { Type } from '../constants.js'
import type { Document } from '../doc/Document.js'
import { YAMLSeq } from '../nodes/YAMLSeq.js'
import type { BlockSequence } from '../parse/tokens.js'
Expand All @@ -14,7 +13,6 @@ export function resolveBlockSeq(
) {
const start = offset
const seq = new YAMLSeq(doc.schema)
seq.type = Type.SEQ
if (anchor) doc.anchors.setAnchor(seq, anchor)
for (const { start, value } of items) {
const props = resolveProps(
Expand Down
11 changes: 4 additions & 7 deletions src/compose/resolve-flow-collection.ts
@@ -1,4 +1,3 @@
import { Type } from '../constants.js'
import type { Document } from '../doc/Document.js'
import { isNode, isPair, ParsedNode } from '../nodes/Node.js'
import { Pair } from '../nodes/Pair.js'
Expand All @@ -7,7 +6,6 @@ import { YAMLSeq } from '../nodes/YAMLSeq.js'
import type { FlowCollection, SourceToken, Token } from '../parse/tokens.js'
import type { ComposeNode } from './compose-node.js'
import { resolveEnd } from './resolve-end.js'
import { resolveMergePair } from './resolve-merge-pair.js'
import { containsNewline } from './util-contains-newline.js'

export function resolveFlowCollection(
Expand All @@ -19,7 +17,7 @@ export function resolveFlowCollection(
) {
const isMap = fc.start.source === '{'
const coll = isMap ? new YAMLMap(doc.schema) : new YAMLSeq(doc.schema)
coll.type = isMap ? Type.FLOW_MAP : Type.FLOW_SEQ
coll.flow = true
if (_anchor) doc.anchors.setAnchor(coll, _anchor)

let key: ParsedNode | null = null
Expand Down Expand Up @@ -60,13 +58,12 @@ export function resolveFlowCollection(
value = composeEmptyNode(doc, offset, fc.items, pos, getProps(), onError)
}
if (isMap || atExplicitKey) {
const pair = key ? new Pair(key, value) : new Pair(value)
coll.items.push(doc.schema.merge ? resolveMergePair(pair, onError) : pair)
coll.items.push(key ? new Pair(key, value) : new Pair(value))
} else {
const seq = coll as YAMLSeq
if (key) {
const map = new YAMLMap(doc.schema)
map.type = Type.FLOW_MAP
map.flow = true
map.items.push(new Pair(key, value))
seq.items.push(map)
} else seq.items.push(value)
Expand Down Expand Up @@ -143,7 +140,7 @@ export function resolveFlowCollection(
if (value) {
onError(offset, 'Missing {} around pair used as mapping key')
const map = new YAMLMap(doc.schema)
map.type = Type.FLOW_MAP
map.flow = true
map.items.push(new Pair(key, value))
map.range = [key.range[0], value.range[1]]
key = map as YAMLMap.Parsed
Expand Down
12 changes: 6 additions & 6 deletions src/compose/resolve-flow-scalar.ts
@@ -1,4 +1,4 @@
import { Type } from '../constants.js'
import { Scalar } from '../nodes/Scalar.js'
import type { FlowScalar } from '../parse/tokens.js'
import { resolveEnd } from './resolve-end.js'

Expand All @@ -8,26 +8,26 @@ export function resolveFlowScalar(
onError: (offset: number, message: string) => void
): {
value: string
type: Type.PLAIN | Type.QUOTE_DOUBLE | Type.QUOTE_SINGLE | null
type: Scalar.PLAIN | Scalar.QUOTE_DOUBLE | Scalar.QUOTE_SINGLE | null
comment: string
length: number
} {
let _type: Type.PLAIN | Type.QUOTE_DOUBLE | Type.QUOTE_SINGLE
let _type: Scalar.PLAIN | Scalar.QUOTE_DOUBLE | Scalar.QUOTE_SINGLE
let value: string
const _onError = (rel: number, msg: string) => onError(offset + rel, msg)
switch (type) {
case 'scalar':
_type = Type.PLAIN
_type = Scalar.PLAIN
value = plainValue(source, _onError)
break

case 'single-quoted-scalar':
_type = Type.QUOTE_SINGLE
_type = Scalar.QUOTE_SINGLE
value = singleQuotedValue(source, _onError)
break

case 'double-quoted-scalar':
_type = Type.QUOTE_DOUBLE
_type = Scalar.QUOTE_DOUBLE
value = doubleQuotedValue(source, _onError)
break

Expand Down