Skip to content

Commit

Permalink
Merge pull request #240 from eemeli/less-type
Browse files Browse the repository at this point in the history
Drop `type` property from all but Scalar nodes
  • Loading branch information
eemeli committed Mar 12, 2021
2 parents 48c8ebc + 9c5ba3b commit 4c93af0
Show file tree
Hide file tree
Showing 37 changed files with 383 additions and 466 deletions.
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

0 comments on commit 4c93af0

Please sign in to comment.