Skip to content

Commit

Permalink
Move createNode & createPair from Schema to Document
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The createNode & createPair methods are moved from the
Schema instance to the Document instance, and their options arguments
are now collected into an object. So where you may have previously had:
    doc.schema.createNode(value, false, '!!omap')
you should now use:
    doc.createNode(value, { tag: '!!omap', wrapScalars: false })
and similarly for createPair(key, value).
  • Loading branch information
eemeli committed Aug 5, 2020
1 parent 538cb7a commit fae1614
Show file tree
Hide file tree
Showing 19 changed files with 141 additions and 132 deletions.
18 changes: 10 additions & 8 deletions docs/04_documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,16 @@ During stringification, a document with a true-ish `version` value will include

## Document Methods

| Method | Returns | Description |
| ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| listNonDefaultTags() | `string[]` | List the tags used in the document that are not in the default `tag:yaml.org,2002:` namespace. |
| parse(cst) | `Document` | Parse a CST into this document. Mostly an internal method, modifying the document according to the contents of the parsed `cst`. Calling this multiple times on a Document is not recommended. |
| setSchema(id, customTags) | `void` | When a document is created with `new YAML.Document()`, the schema object is not set as it may be influenced by parsed directives; call this with no arguments to set it manually, or with arguments to change the schema used by the document. `id` may either be a YAML version, or the identifier of a YAML 1.2 schema; if set, `customTags` should have the same shape as the similarly-named option. |
| setTagPrefix(handle, prefix) | `void` | Set `handle` as a shorthand string for the `prefix` tag namespace. |
| toJSON() | `any` | A plain JavaScript representation of the document `contents`. |
| toString() | `string` | A YAML representation of the document. |
| Method | Returns | Description |
| ------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| createNode(value, options?) | `Node` | Recursively turn objects into [collections](#collections). Generic objects as well as `Map` and its descendants become mappings, while arrays and other iterable objects result in sequences. If `options.wrapScalars` is undefined or `true`, it also wraps plain values in `Scalar` objects. To specify the collection type, set `options.tag` to its identifying string, e.g. `"!!omap"`. |
| createPair(key, value, options?) | `Pair` | Recursively wrap `key` and `value` into a `Pair` object. See `createNode()` for the available options. |
| listNonDefaultTags() | `string[]` | List the tags used in the document that are not in the default `tag:yaml.org,2002:` namespace. |
| parse(cst) | `Document` | Parse a CST into this document. Mostly an internal method, modifying the document according to the contents of the parsed `cst`. Calling this multiple times on a Document is not recommended. |
| setSchema(id?, customTags?) | `void` | Set the schema used by the document. `id` may either be a YAML version, or the identifier of a YAML 1.2 schema; if set, `customTags` should have the same shape as the similarly-named option. |
| setTagPrefix(handle, prefix) | `void` | Set `handle` as a shorthand string for the `prefix` tag namespace. |
| toJSON() | `any` | A plain JavaScript representation of the document `contents`. |
| toString() | `string` | A YAML representation of the document. |

```js
const doc = YAML.parseDocument('a: 1\nb: [2, 3]\n')
Expand Down
4 changes: 2 additions & 2 deletions docs/05_content_nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ String(doc)

`YAML.createNode` recursively turns objects into [collections](#collections). Generic objects as well as `Map` and its descendants become mappings, while arrays and other iterable objects result in sequences. If `wrapScalars` is undefined or `true`, it also wraps plain values in `Scalar` objects; if it is false and `value` is not an object, it will be returned directly.

To specify the collection type, set `tag` to its identifying string, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available based on the default options. To use a specific document's schema, use the wrapped method `doc.schema.createNode(value, wrapScalars, tag)`.
To specify the collection type, set `tag` to its identifying string, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available based on the default options. To use a specific document's schema, use the Document method `createNode(value, options?)`.

The primary purpose of this function 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.

Expand Down Expand Up @@ -194,7 +194,7 @@ doc.toString()

To construct a `YAMLSeq` or `YAMLMap`, use [`YAML.createNode()`](#yaml-createnode) with array, object or iterable input, or create the collections directly by importing the classes from `yaml/types`.

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/types` and using its `new Pair(key, value)` constructor, or by using the `doc.schema.createPair(key, value)` method. The latter will recursively wrap the `key` and `value` as nodes.
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/types` and using its `new Pair(key, value)` constructor, or by using the `doc.createPair(key, value)` method. The latter will recursively wrap the `key` and `value` as nodes.

## Comments

Expand Down
25 changes: 24 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
Collection,
Merge,
Node,
Pair,
Scalar,
Schema,
YAMLMap,
YAMLSeq
YAMLSeq,
} from './types'
import { Type, YAMLError, YAMLWarning } from './util'

Expand Down Expand Up @@ -231,6 +232,28 @@ export class Document extends Collection {
version?: string
/** Warnings encountered during parsing. */
warnings: YAMLWarning[]

/**
* Convert any value into a `Node` using the current schema, recursively
* turning objects into collections.
*
* @param options Use `tag` to specify the collection type, e.g. `"!!omap"`.
* Note that this requires the corresponding tag to be available in this
* schema. If `wrapScalars` is not `false`, also wraps plain values in
* `Scalar` objects.
*/
createNode(
value: any,
options?: { tag?: string; wrapScalars?: boolean }
): Node
/**
* Convert a key and a value into a `Pair` using the current schema,
* recursively wrapping all values as `Scalar` or `Collection` nodes.
*
* @param options If `wrapScalars` is not `false`, wraps plain values in
* `Scalar` objects.
*/
createPair(key: any, value: any, options?: { wrapScalars?: boolean }): Pair
/**
* List the tags used in the document that are not in the default
* `tag:yaml.org,2002:` namespace.
Expand Down
7 changes: 6 additions & 1 deletion src/ast/Collection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { addComment } from '../stringify/addComment.js'
import { Type } from '../constants.js'
import { createNode } from '../doc/createNode.js'
import { Node } from './Node.js'
import { Scalar } from './Scalar.js'

Expand All @@ -11,7 +12,11 @@ function collectionFromPath(schema, path, value) {
o[k] = v
v = o
}
return schema.createNode(v, false)
return createNode(v, null, {
prevObjects: new Map(),
schema,
wrapScalars: false
})
}

// null, undefined, or an empty non-string iterable (e.g. [])
Expand Down
9 changes: 8 additions & 1 deletion src/ast/Pair.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Type } from '../constants.js'
import { createNode } from '../doc/createNode.js'
import { addComment } from '../stringify/addComment.js'
import { Collection } from './Collection.js'
import { Node } from './Node.js'
Expand All @@ -22,6 +23,12 @@ const stringifyKey = (key, jsKey, ctx) => {
return JSON.stringify(jsKey)
}

export function createPair(key, value, ctx) {
const k = createNode(key, null, ctx)
const v = createNode(value, null, ctx)
return new Pair(k, v)
}

export class Pair extends Node {
static Type = {
PAIR: 'PAIR',
Expand Down Expand Up @@ -125,7 +132,7 @@ export class Pair extends Node {
}
valueComment = value.comment
} else if (value && typeof value === 'object') {
value = doc.schema.createNode(value, true)
value = doc.createNode(value)
}
ctx.implicitKey = false
if (!explicitKey && !this.comment && value instanceof Scalar)
Expand Down
56 changes: 47 additions & 9 deletions src/doc/Document.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Collection, Node, Scalar, isEmptyPath, toJSON } from '../ast/index.js'
import {
Collection,
Node,
Pair,
Scalar,
isEmptyPath,
toJSON
} from '../ast/index.js'
import { Document as CSTDocument } from '../cst/Document'
import { defaultTagPrefix } from '../constants.js'
import { YAMLError } from '../errors.js'
Expand All @@ -8,6 +15,7 @@ import { stringify } from '../stringify/stringify.js'

import { Anchors } from './Anchors.js'
import { Schema } from './Schema.js'
import { createNode } from './createNode.js'
import { listTagNames } from './listTagNames.js'
import { parseContents } from './parseContents.js'
import { parseDirectives } from './parseDirectives.js'
Expand All @@ -20,25 +28,25 @@ function assertCollection(contents) {
export class Document {
static defaults = documentOptions

constructor(contents, options) {
constructor(value, options) {
this.anchors = new Anchors(options.anchorPrefix)
this.commentBefore = null
this.comment = null
this.directivesEndMarker = null
this.errors = []
this.options = options
this.schema = null
this.tagPrefixes = []
this.version = null
this.warnings = []

if (contents === undefined) {
this.schema = null
if (value === undefined) {
// note that this.schema is left as null here
this.contents = null
} else if (contents instanceof CSTDocument) {
this.parse(contents)
} else if (value instanceof CSTDocument) {
this.parse(value)
} else {
this.setSchema()
this.contents = this.schema.createNode(contents, true)
this.contents = this.createNode(value)
}
}

Expand All @@ -52,6 +60,36 @@ export class Document {
this.contents.addIn(path, value)
}

createNode(value, { onTagObj, tag, wrapScalars } = {}) {
this.setSchema()
const ctx = {
aliasNodes: [],
onTagObj,
prevObjects: new Map(),
schema: this.schema,
wrapScalars: wrapScalars !== false
}
const node = createNode(value, tag, ctx)
for (const alias of ctx.aliasNodes) {
// With circular references, the source node is only resolved after all of
// its child nodes are. This is why anchors are set only after all of the
// nodes have been created.
alias.source = alias.source.node
let name = this.anchors.getName(alias.source)
if (!name) {
name = this.anchors.newName()
this.anchors.map[name] = alias.source
}
}
return node
}

createPair(key, value, options = {}) {
const k = this.createNode(key, options)
const v = this.createNode(value, options)
return new Pair(k, v)
}

delete(key) {
assertCollection(this.contents)
return this.contents.delete(key)
Expand Down Expand Up @@ -127,7 +165,7 @@ export class Document {
}
if (Array.isArray(customTags)) this.options.customTags = customTags
const opt = Object.assign({}, this.getDefaults(), this.options)
this.schema = new Schema(opt, this.anchors)
this.schema = new Schema(opt)
}

parse(node, prevDoc) {
Expand Down
46 changes: 1 addition & 45 deletions src/doc/Schema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Pair } from '../ast/Pair.js'
import { schemas, tags } from '../tags/index.js'
import { createNode } from './createNode.js'
import { getSchemaTags } from './getSchemaTags.js'

const sortMapEntriesByKey = (a, b) =>
Expand All @@ -15,54 +13,12 @@ const coreKnownTags = {
}

export class Schema {
constructor({ customTags, merge, resolveKnownTags, schema, sortMapEntries }, anchors) {
constructor({ customTags, merge, resolveKnownTags, schema, sortMapEntries }) {
this.merge = !!merge
this.name = schema
this.sortMapEntries =
sortMapEntries === true ? sortMapEntriesByKey : sortMapEntries || null
this.tags = getSchemaTags(schemas, tags, customTags, schema)
this.knownTags = resolveKnownTags ? coreKnownTags : {}
this.anchors = anchors
}

createNode(value, wrapScalars, tagName, ctx) {
const aliasNodes = []
if (!ctx || typeof ctx === 'function') {
ctx = {
aliasNodes,
onTagObj: ctx,
prevObjects: new Map(),
schema: this,
wrapScalars
}
} else if (typeof wrapScalars === 'boolean') ctx.wrapScalars = wrapScalars
const node = createNode(value, tagName, ctx)
for (const alias of aliasNodes) {
if (!this.anchors)
throw new ReferenceError(
'Circular references require a document; use doc.schema.createNode()'
)
// With circular references, the source node is only resolved after all of
// its child nodes are. This is why anchors are set only after all of the
// nodes have been created.
alias.source = alias.source.node
let name = this.anchors.getName(alias.source)
if (!name) {
name = this.anchors.newName()
this.anchors.map[name] = alias.source
}
}
return node
}

createPair(key, value, ctx) {
let wrapScalars = null
if (typeof ctx === 'boolean') {
wrapScalars = ctx
ctx = undefined
}
const k = this.createNode(key, wrapScalars, null, ctx)
const v = this.createNode(value, wrapScalars, null, ctx)
return new Pair(k, v)
}
}
6 changes: 4 additions & 2 deletions src/doc/createNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function findTagObject(value, tagName, tags) {

export function createNode(value, tagName, ctx) {
if (value instanceof Node) return value
const { onTagObj, prevObjects, schema, wrapScalars } = ctx
const { aliasNodes, onTagObj, prevObjects, schema, wrapScalars } = ctx
if (tagName && tagName.startsWith('!!'))
tagName = defaultTagPrefix + tagName.slice(2)

Expand All @@ -39,8 +39,10 @@ export function createNode(value, tagName, ctx) {
if (value && typeof value === 'object' && prevObjects) {
const prev = prevObjects.get(value)
if (prev) {
if (!aliasNodes)
throw new Error('Circular references are not supported here')
const alias = new Alias(prev) // leaves source dirty; must be cleaned by caller
ctx.aliasNodes.push(alias) // defined along with prevObjects
aliasNodes.push(alias)
return alias
}
obj.value = value
Expand Down
9 changes: 7 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parse as parseCST } from './cst/parse.js'
import { createNode as _createNode } from './doc/createNode.js'
import { Document as YAMLDocument } from './doc/Document.js'
import { Schema } from './doc/Schema.js'
import { YAMLSemanticError } from './errors.js'
Expand All @@ -15,8 +16,12 @@ function createNode(value, wrapScalars = true, tag) {
YAMLDocument.defaults[defaultOptions.version],
defaultOptions
)
const schema = new Schema(options, null)
return schema.createNode(value, wrapScalars, tag)
const ctx = {
prevObjects: new Map(),
schema: new Schema(options),
wrapScalars
}
return _createNode(value, tag, ctx)
}

class Document extends YAMLDocument {
Expand Down

0 comments on commit fae1614

Please sign in to comment.