From 76447eca2116b7bc7d9c906e2d70cc6396b9ea40 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 18 Jan 2026 17:49:48 +0800 Subject: [PATCH 1/7] refactor: Replace CreateNodeContext with NodeCreator --- src/doc/Document.ts | 54 +++-------- src/doc/NodeCreator.ts | 177 +++++++++++++++++++++++++++++++++++ src/doc/anchors.ts | 51 ---------- src/doc/createNode.ts | 110 ---------------------- src/nodes/Collection.ts | 12 +-- src/nodes/Pair.ts | 13 --- src/nodes/YAMLMap.ts | 18 ++-- src/nodes/YAMLSeq.ts | 14 ++- src/schema/common/map.ts | 2 +- src/schema/common/seq.ts | 2 +- src/schema/types.ts | 6 +- src/schema/yaml-1.1/omap.ts | 13 +-- src/schema/yaml-1.1/pairs.ts | 20 ++-- src/schema/yaml-1.1/set.ts | 20 ++-- src/util.ts | 4 +- 15 files changed, 233 insertions(+), 283 deletions(-) create mode 100644 src/doc/NodeCreator.ts delete mode 100644 src/doc/createNode.ts diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 492fe506..072daf33 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -9,7 +9,7 @@ import { NODE_TYPE } from '../nodes/identity.ts' import type { Node, NodeType, ParsedNode, Range } from '../nodes/Node.ts' -import { Pair } from '../nodes/Pair.ts' +import type { Pair } from '../nodes/Pair.ts' import type { Scalar } from '../nodes/Scalar.ts' import type { ToJSContext } from '../nodes/toJS.ts' import { toJS } from '../nodes/toJS.ts' @@ -25,10 +25,9 @@ import type { } from '../options.ts' import { Schema } from '../schema/Schema.ts' import { stringifyDocument } from '../stringify/stringifyDocument.ts' -import { anchorNames, createNodeAnchors, findNewAnchor } from './anchors.ts' +import { anchorNames, findNewAnchor } from './anchors.ts' import { applyReviver } from './applyReviver.ts' -import type { CreateNodeContext } from './createNode.ts' -import { createNode } from './createNode.ts' +import { NodeCreator } from './NodeCreator.ts' import { Directives } from './directives.ts' export type Replacer = any[] | ((key: any, value: any) => unknown) @@ -215,46 +214,22 @@ export class Document< replacer?: Replacer | CreateNodeOptions | null, options?: CreateNodeOptions ): Node { - let _replacer: Replacer | undefined = undefined + let nc: NodeCreator if (typeof replacer === 'function') { value = replacer.call({ '': value }, '', value) - _replacer = replacer + nc = new NodeCreator(this, options, replacer) } else if (Array.isArray(replacer)) { const keyToStr = (v: unknown) => typeof v === 'number' || v instanceof String || v instanceof Number const asStr = replacer.filter(keyToStr).map(String) if (asStr.length > 0) replacer = replacer.concat(asStr) - _replacer = replacer - } else if (options === undefined && replacer) { - options = replacer - replacer = undefined - } - - const { - aliasDuplicateObjects, - anchorPrefix, - flow, - keepUndefined, - onTagObj, - tag - } = options ?? {} - const { onAnchor, setAnchors, sourceObjects } = createNodeAnchors( - this, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - anchorPrefix || 'a' - ) - const ctx: CreateNodeContext = { - aliasDuplicateObjects: aliasDuplicateObjects ?? true, - keepUndefined: keepUndefined ?? false, - onAnchor, - onTagObj, - replacer: _replacer, - schema: this.schema, - sourceObjects + nc = new NodeCreator(this, options, replacer) + } else { + options ??= replacer ?? undefined + nc = new NodeCreator(this, options) } - const node = createNode(value, tag, ctx) - if (flow && isCollection(node)) node.flow = true - setAnchors() + const node = nc.create(value, options?.tag) + nc.setAnchors() return node } @@ -267,9 +242,10 @@ export class Document< value: unknown, options: CreateNodeOptions = {} ): Pair { - const k = this.createNode(key, null, options) as K - const v = this.createNode(value, null, options) as V - return new Pair(k, v) + const nc = new NodeCreator(this, options) + const pair = nc.createPair(key, value) as Pair + nc.setAnchors() + return pair } /** diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts new file mode 100644 index 00000000..529ce24a --- /dev/null +++ b/src/doc/NodeCreator.ts @@ -0,0 +1,177 @@ +import { Alias } from '../nodes/Alias.ts' +import { + isCollection, + isDocument, + isNode, + isPair, + isScalar, + MAP, + SEQ +} from '../nodes/identity.ts' +import type { Node } from '../nodes/Node.ts' +import { Pair } from '../nodes/Pair.ts' +import { Scalar } from '../nodes/Scalar.ts' +import type { YAMLMap } from '../nodes/YAMLMap.ts' +import type { CreateNodeOptions } from '../options.ts' +import type { Schema } from '../schema/Schema.ts' +import type { CollectionTag, ScalarTag } from '../schema/types.ts' +import { anchorNames, findNewAnchor } from './anchors.ts' +import type { Document, Replacer } from './Document.ts' + +const defaultTagPrefix = 'tag:yaml.org,2002:' + +export class NodeCreator { + keepUndefined: boolean + replacer?: Replacer + schema: Schema + + #aliasDuplicateObjects: boolean + #anchorPrefix: string + #aliasObjects: unknown[] = [] + #doc?: Document + #flow: boolean + #onTagObj?: (tagObj: ScalarTag | CollectionTag) => void + #prevAnchors: Set | null = null + #sourceObjects: Map = + new Map() + + constructor( + doc: Document, + options?: CreateNodeOptions, + replacer?: Replacer + ) + constructor( + schema: Schema, + options: CreateNodeOptions & { aliasDuplicateObjects: false } + ) + constructor( + docOrSchema: Document | Schema, + options: CreateNodeOptions = {}, + replacer?: Replacer + ) { + this.keepUndefined = options.keepUndefined ?? false + this.replacer = replacer + + this.#flow = options.flow ?? false + this.#onTagObj = options.onTagObj + + this.#aliasDuplicateObjects = options.aliasDuplicateObjects ?? true + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.#anchorPrefix = options.anchorPrefix || 'a' + if (isDocument(docOrSchema)) { + this.#doc = docOrSchema + this.schema = docOrSchema.schema + } else { + this.schema = docOrSchema + } + } + + create(value: unknown, tagName?: string): Node { + if (isDocument(value)) value = value.contents + if (isNode(value)) return value + if (isPair(value)) { + const map = this.schema[MAP].createNode?.(this, null) as YAMLMap + map.items.push(value) + return map + } + if ( + value instanceof String || + value instanceof Number || + value instanceof Boolean || + (typeof BigInt !== 'undefined' && value instanceof BigInt) // not supported everywhere + ) { + // https://tc39.es/ecma262/#sec-serializejsonproperty + value = value.valueOf() + } + + // Detect duplicate references to the same object & use Alias nodes for all + // after first. The `ref` wrapper allows for circular references to resolve. + let ref: { anchor: string | null; node: Node | null } | undefined = + undefined + if (this.#aliasDuplicateObjects && value && typeof value === 'object') { + ref = this.#sourceObjects.get(value) + if (ref) { + if (!ref.anchor) { + this.#aliasObjects.push(value) + this.#prevAnchors ??= anchorNames(this.#doc!) + ref.anchor = findNewAnchor(this.#anchorPrefix, this.#prevAnchors) + this.#prevAnchors.add(ref.anchor) + } + return new Alias(ref.anchor) + } else { + ref = { anchor: null, node: null } + this.#sourceObjects.set(value, ref) + } + } + + let tagObj: ScalarTag | CollectionTag | undefined + if (tagName) { + if (tagName.startsWith('!!')) + tagName = defaultTagPrefix + tagName.slice(2) + const match = this.schema.tags.filter(t => t.tag === tagName) + tagObj = match.find(t => !t.format) ?? match[0] + if (!tagObj) throw new Error(`Tag ${tagName} not found`) + } else { + tagObj = this.schema.tags.find(t => t.identify?.(value) && !t.format) + } + if (!tagObj) { + if (value && typeof (value as any).toJSON === 'function') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + value = (value as any).toJSON() + } + if (!value || typeof value !== 'object') { + const node = new Scalar(value) + if (ref) ref.node = node + return node + } + tagObj = + value instanceof Map + ? this.schema[MAP] + : Symbol.iterator in Object(value) + ? this.schema[SEQ] + : this.schema[MAP] + } + if (this.#onTagObj) { + this.#onTagObj(tagObj) + this.#onTagObj = undefined + } + + const node = + tagObj?.createNode?.(this, value) ?? + tagObj?.nodeClass?.from?.(this, value) ?? + new Scalar(value) + if (tagName) node.tag = tagName + else if (!tagObj.default) node.tag = tagObj.tag + + if (ref) ref.node = node + if (this.#flow && isCollection(node)) node.flow = true + return node + } + + createPair(key: unknown, value: unknown): Pair { + const k = this.create(key) + const v = this.create(value) + return new Pair(k, v) + } + + /** + * 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. + */ + setAnchors(): void { + for (const source of this.#aliasObjects) { + const ref = this.#sourceObjects.get(source) + if ( + typeof ref === 'object' && + ref.anchor && + (isScalar(ref.node) || isCollection(ref.node)) + ) { + ref.node.anchor = ref.anchor + } else { + const msg = 'Failed to resolve repeated object (this should not happen)' + throw Object.assign(new Error(msg), { source }) + } + } + } +} diff --git a/src/doc/anchors.ts b/src/doc/anchors.ts index dce9481f..487584ac 100644 --- a/src/doc/anchors.ts +++ b/src/doc/anchors.ts @@ -1,10 +1,8 @@ -import { isCollection, isScalar } from '../nodes/identity.ts' import type { Node } from '../nodes/Node.ts' import type { Scalar } from '../nodes/Scalar.ts' import type { YAMLMap } from '../nodes/YAMLMap.ts' import type { YAMLSeq } from '../nodes/YAMLSeq.ts' import { visit } from '../visit.ts' -import type { CreateNodeContext } from './createNode.ts' import type { Document } from './Document.ts' /** @@ -38,52 +36,3 @@ export function findNewAnchor(prefix: string, exclude: Set): string { if (!exclude.has(name)) return name } } - -export function createNodeAnchors( - doc: Document, - prefix: string -): { - onAnchor: (source: unknown) => string - setAnchors: () => void - sourceObjects: CreateNodeContext['sourceObjects'] -} { - const aliasObjects: unknown[] = [] - const sourceObjects: CreateNodeContext['sourceObjects'] = new Map() - let prevAnchors: Set | null = null - - return { - onAnchor: (source: unknown) => { - aliasObjects.push(source) - prevAnchors ??= anchorNames(doc) - const anchor = findNewAnchor(prefix, prevAnchors) - prevAnchors.add(anchor) - return anchor - }, - - /** - * 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. - */ - setAnchors: () => { - for (const source of aliasObjects) { - const ref = sourceObjects.get(source) - if ( - typeof ref === 'object' && - ref.anchor && - (isScalar(ref.node) || isCollection(ref.node)) - ) { - ref.node.anchor = ref.anchor - } else { - const error = new Error( - 'Failed to resolve repeated object (this should not happen)' - ) as Error & { source: unknown } - error.source = source - throw error - } - } - }, - - sourceObjects - } -} diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts deleted file mode 100644 index 995a04ba..00000000 --- a/src/doc/createNode.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Alias } from '../nodes/Alias.ts' -import { isDocument, isNode, isPair, MAP, SEQ } from '../nodes/identity.ts' -import type { Node } from '../nodes/Node.ts' -import { Scalar } from '../nodes/Scalar.ts' -import type { YAMLMap } from '../nodes/YAMLMap.ts' -import type { Schema } from '../schema/Schema.ts' -import type { CollectionTag, ScalarTag } from '../schema/types.ts' -import type { Replacer } from './Document.ts' - -const defaultTagPrefix = 'tag:yaml.org,2002:' - -export interface CreateNodeContext { - aliasDuplicateObjects: boolean - keepUndefined: boolean - onAnchor: (source: unknown) => string - onTagObj?: (tagObj: ScalarTag | CollectionTag) => void - sourceObjects: Map - replacer?: Replacer - schema: Schema -} - -function findTagObject( - value: unknown, - tagName: string | undefined, - tags: Array -) { - if (tagName) { - const match = tags.filter(t => t.tag === tagName) - const tagObj = match.find(t => !t.format) ?? match[0] - if (!tagObj) throw new Error(`Tag ${tagName} not found`) - return tagObj - } - return tags.find(t => t.identify?.(value) && !t.format) -} - -export function createNode( - value: unknown, - tagName: string | undefined, - ctx: CreateNodeContext -): Node { - if (isDocument(value)) value = value.contents - if (isNode(value)) return value - if (isPair(value)) { - const map = ctx.schema[MAP].createNode?.(ctx.schema, null, ctx) as YAMLMap - map.items.push(value) - return map - } - if ( - value instanceof String || - value instanceof Number || - value instanceof Boolean || - (typeof BigInt !== 'undefined' && value instanceof BigInt) // not supported everywhere - ) { - // https://tc39.es/ecma262/#sec-serializejsonproperty - value = value.valueOf() - } - - const { aliasDuplicateObjects, onAnchor, onTagObj, schema, sourceObjects } = - ctx - - // Detect duplicate references to the same object & use Alias nodes for all - // after first. The `ref` wrapper allows for circular references to resolve. - let ref: { anchor: string | null; node: Node | null } | undefined = undefined - if (aliasDuplicateObjects && value && typeof value === 'object') { - ref = sourceObjects.get(value) - if (ref) { - ref.anchor ??= onAnchor(value) - return new Alias(ref.anchor) - } else { - ref = { anchor: null, node: null } - sourceObjects.set(value, ref) - } - } - - if (tagName?.startsWith('!!')) tagName = defaultTagPrefix + tagName.slice(2) - - let tagObj = findTagObject(value, tagName, schema.tags) - if (!tagObj) { - if (value && typeof (value as any).toJSON === 'function') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - value = (value as any).toJSON() - } - if (!value || typeof value !== 'object') { - const node = new Scalar(value) - if (ref) ref.node = node - return node - } - tagObj = - value instanceof Map - ? schema[MAP] - : Symbol.iterator in Object(value) - ? schema[SEQ] - : schema[MAP] - } - if (onTagObj) { - onTagObj(tagObj) - delete ctx.onTagObj - } - - const node = tagObj?.createNode - ? tagObj.createNode(ctx.schema, value, ctx) - : typeof tagObj?.nodeClass?.from === 'function' - ? tagObj.nodeClass.from(ctx.schema, value, ctx) - : new Scalar(value) - if (tagName) node.tag = tagName - else if (!tagObj.default) node.tag = tagObj.tag - - if (ref) ref.node = node - return node -} diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index a73bece7..05a7c90a 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,4 +1,4 @@ -import { createNode } from '../doc/createNode.ts' +import { NodeCreator } from '../doc/NodeCreator.ts' import type { Schema } from '../schema/Schema.ts' import { isCollection, @@ -25,15 +25,7 @@ export function collectionFromPath( v = new Map([[k, v]]) } } - return createNode(v, undefined, { - aliasDuplicateObjects: false, - keepUndefined: false, - onAnchor: () => { - throw new Error('This should not happen, please report a bug.') - }, - schema, - sourceObjects: new Map() - }) + return new NodeCreator(schema, { aliasDuplicateObjects: false }).create(v) } // Type guard is intentionally a little wrong so as to be more useful, diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 4b34f84f..1b573c8b 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -1,24 +1,11 @@ -import type { CreateNodeContext } from '../doc/createNode.ts' -import { createNode } from '../doc/createNode.ts' import type { CollectionItem } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyPair } from '../stringify/stringifyPair.ts' import { addPairToJSMap } from './addPairToJSMap.ts' import { isNode, NODE_TYPE, PAIR } from './identity.ts' -import type { Node } from './Node.ts' import type { ToJSContext } from './toJS.ts' -export function createPair( - key: unknown, - value: unknown, - ctx: CreateNodeContext -): Pair { - const k = createNode(key, undefined, ctx) - const v = createNode(value, undefined, ctx) - return new Pair(k, v) -} - export class Pair { /** @internal */ declare readonly [NODE_TYPE]: symbol diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index 6169e14f..9efb0707 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -2,12 +2,12 @@ import type { BlockMap, FlowCollection } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' -import type { CreateNodeContext } from '../util.ts' +import type { NodeCreator } from '../util.ts' import { addPairToJSMap } from './addPairToJSMap.ts' import { Collection } from './Collection.ts' import { isPair, isScalar, MAP } from './identity.ts' import type { ParsedNode, Range } from './Node.ts' -import { createPair, Pair } from './Pair.ts' +import { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' import { isScalarValue } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' @@ -57,22 +57,22 @@ export class YAMLMap extends Collection { * A generic collection parsing method that can be extended * to other node classes that inherit from YAMLMap */ - static from(schema: Schema, obj: unknown, ctx: CreateNodeContext): YAMLMap { - const { keepUndefined, replacer } = ctx - const map = new this(schema) + static from(nc: NodeCreator, obj: unknown): YAMLMap { + const { replacer } = nc + const map = new this(nc.schema) const add = (key: unknown, value: unknown) => { if (typeof replacer === 'function') value = replacer.call(obj, key, value) else if (Array.isArray(replacer) && !replacer.includes(key)) return - if (value !== undefined || keepUndefined) - map.items.push(createPair(key, value, ctx)) + if (value !== undefined || nc.keepUndefined) + map.items.push(nc.createPair(key, value)) } if (obj instanceof Map) { for (const [key, value] of obj) add(key, value) } else if (obj && typeof obj === 'object') { for (const key of Object.keys(obj)) add(key, (obj as any)[key]) } - if (typeof schema.sortMapEntries === 'function') { - map.items.sort(schema.sortMapEntries) + if (typeof nc.schema.sortMapEntries === 'function') { + map.items.sort(nc.schema.sortMapEntries) } return map } diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index c3f96e99..95d9973e 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -1,5 +1,4 @@ -import type { CreateNodeContext } from '../doc/createNode.ts' -import { createNode } from '../doc/createNode.ts' +import type { NodeCreator } from '../doc/NodeCreator.ts' import type { BlockSequence, FlowCollection } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' @@ -121,17 +120,16 @@ export class YAMLSeq extends Collection { }) } - static from(schema: Schema, obj: unknown, ctx: CreateNodeContext): YAMLSeq { - const { replacer } = ctx - const seq = new this(schema) + static from(nc: NodeCreator, obj: unknown): YAMLSeq { + const seq = new this(nc.schema) if (obj && Symbol.iterator in Object(obj)) { let i = 0 for (let it of obj as Iterable) { - if (typeof replacer === 'function') { + if (typeof nc.replacer === 'function') { const key = obj instanceof Set ? it : String(i++) - it = replacer.call(obj, key, it) + it = nc.replacer.call(obj, key, it) } - seq.items.push(createNode(it, undefined, ctx)) + seq.items.push(nc.create(it)) } } return seq diff --git a/src/schema/common/map.ts b/src/schema/common/map.ts index 3f9a22e0..468d9cd3 100644 --- a/src/schema/common/map.ts +++ b/src/schema/common/map.ts @@ -11,5 +11,5 @@ export const map: CollectionTag = { if (!isMap(map)) onError('Expected a mapping for this tag') return map }, - createNode: (schema, obj, ctx) => YAMLMap.from(schema, obj, ctx) + createNode: (nc, obj) => YAMLMap.from(nc, obj) } diff --git a/src/schema/common/seq.ts b/src/schema/common/seq.ts index 401e9c74..31e1d7e1 100644 --- a/src/schema/common/seq.ts +++ b/src/schema/common/seq.ts @@ -11,5 +11,5 @@ export const seq: CollectionTag = { if (!isSeq(seq)) onError('Expected a sequence for this tag') return seq }, - createNode: (schema, obj, ctx) => YAMLSeq.from(schema, obj, ctx) + createNode: (nc, obj) => YAMLSeq.from(nc, obj) } diff --git a/src/schema/types.ts b/src/schema/types.ts index 0583ffc3..456fd87b 100644 --- a/src/schema/types.ts +++ b/src/schema/types.ts @@ -1,4 +1,4 @@ -import type { CreateNodeContext } from '../doc/createNode.ts' +import type { NodeCreator } from '../doc/NodeCreator.ts' import type { Node } from '../nodes/Node.ts' import type { Scalar } from '../nodes/Scalar.ts' import type { YAMLMap } from '../nodes/YAMLMap.ts' @@ -11,7 +11,7 @@ interface TagBase { /** * An optional factory function, used e.g. by collections when wrapping JS objects as AST nodes. */ - createNode?: (schema: Schema, value: unknown, ctx: CreateNodeContext) => Node + createNode?: (nc: NodeCreator, value: unknown) => Node /** * If `true`, allows for values to be stringified without @@ -100,7 +100,7 @@ export interface CollectionTag extends TagBase { */ nodeClass?: { new (schema?: Schema): Node - from?: (schema: Schema, obj: unknown, ctx: CreateNodeContext) => Node + from?: (nc: NodeCreator, obj: unknown) => Node } /** diff --git a/src/schema/yaml-1.1/omap.ts b/src/schema/yaml-1.1/omap.ts index cd14c3e4..d9d9b8f2 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -3,8 +3,7 @@ import type { ToJSContext } from '../../nodes/toJS.ts' import { toJS } from '../../nodes/toJS.ts' import { YAMLMap } from '../../nodes/YAMLMap.ts' import { YAMLSeq } from '../../nodes/YAMLSeq.ts' -import type { CreateNodeContext } from '../../util.ts' -import type { Schema } from '../Schema.ts' +import type { NodeCreator } from '../../util.ts' import type { CollectionTag } from '../types.ts' import { createPairs, resolvePairs } from './pairs.ts' @@ -45,12 +44,8 @@ export class YAMLOMap extends YAMLSeq { return map as unknown as unknown[] } - static from( - schema: Schema, - iterable: unknown, - ctx: CreateNodeContext - ): YAMLOMap { - const pairs = createPairs(schema, iterable, ctx) + static from(nc: NodeCreator, iterable: unknown): YAMLOMap { + const pairs = createPairs(nc, iterable) const omap = new this() omap.items = pairs.items return omap @@ -78,5 +73,5 @@ export const omap: CollectionTag = { } return Object.assign(new YAMLOMap(), pairs) }, - createNode: (schema, iterable, ctx) => YAMLOMap.from(schema, iterable, ctx) + createNode: (nc, iterable) => YAMLOMap.from(nc, iterable) } diff --git a/src/schema/yaml-1.1/pairs.ts b/src/schema/yaml-1.1/pairs.ts index 3e98c8e8..e899b572 100644 --- a/src/schema/yaml-1.1/pairs.ts +++ b/src/schema/yaml-1.1/pairs.ts @@ -1,11 +1,10 @@ -import type { CreateNodeContext } from '../../doc/createNode.ts' +import type { NodeCreator } from '../../doc/NodeCreator.ts' import { isMap, isPair, isSeq } from '../../nodes/identity.ts' import type { ParsedNode } from '../../nodes/Node.ts' -import { createPair, Pair } from '../../nodes/Pair.ts' +import { Pair } from '../../nodes/Pair.ts' import { Scalar } from '../../nodes/Scalar.ts' import type { YAMLMap } from '../../nodes/YAMLMap.ts' import { YAMLSeq } from '../../nodes/YAMLSeq.ts' -import type { Schema } from '../../schema/Schema.ts' import type { CollectionTag } from '../types.ts' export function resolvePairs( @@ -41,19 +40,14 @@ export function resolvePairs( return seq as YAMLSeq.Parsed> } -export function createPairs( - schema: Schema, - iterable: unknown, - ctx: CreateNodeContext -): YAMLSeq { - const { replacer } = ctx - const pairs = new YAMLSeq(schema) +export function createPairs(nc: NodeCreator, iterable: unknown): YAMLSeq { + const pairs = new YAMLSeq(nc.schema) pairs.tag = 'tag:yaml.org,2002:pairs' let i = 0 if (iterable && Symbol.iterator in Object(iterable)) for (let it of iterable as Iterable) { - if (typeof replacer === 'function') - it = replacer.call(iterable, String(i++), it) + if (typeof nc.replacer === 'function') + it = nc.replacer.call(iterable, String(i++), it) let key: unknown, value: unknown if (Array.isArray(it)) { if (it.length === 2) { @@ -73,7 +67,7 @@ export function createPairs( } else { key = it } - pairs.items.push(createPair(key, value, ctx)) + pairs.items.push(nc.createPair(key, value)) } return pairs } diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index 11916935..0a128046 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -5,8 +5,7 @@ import type { ToJSContext } from '../../nodes/toJS.ts' import { findPair, YAMLMap } from '../../nodes/YAMLMap.ts' import type { Schema } from '../../schema/Schema.ts' import type { StringifyContext } from '../../stringify/stringify.ts' -import type { CreateNodeContext } from '../../util.ts' -import { createPair } from '../../util.ts' +import type { NodeCreator } from '../../util.ts' import type { CollectionTag } from '../types.ts' export class YAMLSet extends YAMLMap | null> { @@ -87,19 +86,14 @@ export class YAMLSet extends YAMLMap | null> { else throw new Error('Set items must all have null values') } - static from( - schema: Schema, - iterable: unknown, - ctx: CreateNodeContext - ): YAMLSet { - const { replacer } = ctx - const set = new this(schema) + static from(nc: NodeCreator, iterable: unknown): YAMLSet { + const set = new this(nc.schema) if (iterable && Symbol.iterator in Object(iterable)) for (let value of iterable as Iterable) { - if (typeof replacer === 'function') - value = replacer.call(iterable, value, value) + if (typeof nc.replacer === 'function') + value = nc.replacer.call(iterable, value, value) set.items.push( - createPair(value, null, ctx) as Pair> + nc.createPair(value, null) as Pair> ) } return set @@ -112,7 +106,7 @@ export const set: CollectionTag = { nodeClass: YAMLSet, default: false, tag: 'tag:yaml.org,2002:set', - createNode: (schema, iterable, ctx) => YAMLSet.from(schema, iterable, ctx), + createNode: (nc, iterable) => YAMLSet.from(nc, iterable), resolve(map, onError) { if (isMap(map)) { if (map.hasAllNullValues(true)) return Object.assign(new YAMLSet(), map) diff --git a/src/util.ts b/src/util.ts index d1b9aa83..09d1f915 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,8 +1,6 @@ -export { createNode } from './doc/createNode.ts' -export type { CreateNodeContext } from './doc/createNode.ts' +export type { NodeCreator } from './doc/NodeCreator.ts' export { debug, warn } from './log.ts' export type { LogLevelId } from './log.ts' -export { createPair } from './nodes/Pair.ts' export { toJS } from './nodes/toJS.ts' export type { ToJSContext } from './nodes/toJS.ts' export { findPair } from './nodes/YAMLMap.ts' From ae17e6cc8a6f53d6dec7f422c66c95bd9d742f83 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 21 Jan 2026 13:12:07 +0800 Subject: [PATCH 2/7] feat!: Be more strict about collection values --- src/compose/compose-collection.ts | 4 +- src/compose/compose-doc.ts | 2 +- src/compose/compose-node.ts | 6 +- src/compose/compose-scalar.ts | 4 +- src/compose/resolve-block-map.ts | 17 ++-- src/compose/resolve-block-seq.ts | 6 +- src/compose/resolve-flow-collection.ts | 27 +++--- src/compose/util-map-includes.ts | 8 +- src/doc/Document.ts | 37 +++++--- src/doc/NodeCreator.ts | 8 +- src/nodes/Alias.ts | 10 +-- src/nodes/Collection.ts | 48 +++------- src/nodes/Node.ts | 15 +--- src/nodes/Pair.ts | 23 ++--- src/nodes/Scalar.ts | 12 +-- src/nodes/YAMLMap.ts | 101 ++++++++++----------- src/nodes/YAMLSeq.ts | 117 +++++++++++++----------- src/nodes/identity.ts | 26 ++---- src/options.ts | 6 +- src/schema/common/map.ts | 3 +- src/schema/common/seq.ts | 3 +- src/schema/types.ts | 2 +- src/schema/yaml-1.1/omap.ts | 18 ++-- src/schema/yaml-1.1/pairs.ts | 24 +++-- src/schema/yaml-1.1/set.ts | 118 ++++++++++++++---------- src/stringify/stringify.ts | 2 +- src/stringify/stringifyPair.ts | 33 ++++--- src/test-events.ts | 16 ++-- src/visit.ts | 14 +-- tests/collection-access.ts | 119 ++++++++++--------------- tests/doc/comments.ts | 6 +- tests/doc/createNode.ts | 2 +- tests/doc/parse.ts | 6 +- tests/doc/stringify.ts | 23 +++-- tests/doc/types.ts | 12 +-- tests/is-node.ts | 6 +- 36 files changed, 436 insertions(+), 448 deletions(-) diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index c789ab58..21c2b1ea 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -136,9 +136,7 @@ export function composeCollection( ctx.options ) ?? coll - const node = isNode(res) - ? (res as ParsedNode) - : (new Scalar(res) as Scalar.Parsed) + const node = isNode(res) ? (res as ParsedNode) : new Scalar(res) node.range = coll.range node.tag = tagName if (tag?.format) (node as Scalar).format = tag.format diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index cc304c48..e0beb731 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -60,7 +60,7 @@ export function composeDoc< ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, start, null, props, onError) - const contentEnd = doc.contents.range[2] + const contentEnd = doc.contents.range![2] const re = resolveEnd(end, contentEnd, false, onError) if (re.comment) doc.comment = re.comment doc.range = [offset, contentEnd, re.offset] diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 7c160263..b1978bc4 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -114,7 +114,7 @@ export function composeEmptyNode( pos: number | null, { spaceBefore, comment, anchor, tag, end }: Props, onError: ComposeErrorHandler -): Scalar.Parsed { +): Scalar { const token: FlowScalar = { type: 'scalar', offset: emptyScalarPosition(offset, before, pos), @@ -130,7 +130,7 @@ export function composeEmptyNode( if (spaceBefore) node.spaceBefore = true if (comment) { node.comment = comment - node.range[2] = end + node.range![2] = end } return node } @@ -154,5 +154,5 @@ function composeAlias( const re = resolveEnd(end, valueEnd, options.strict, onError) alias.range = [offset, valueEnd, re.offset] if (re.comment) alias.comment = re.comment - return alias as Alias.Parsed + return alias } diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index e994ec0f..cfa1d95b 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -13,7 +13,7 @@ export function composeScalar( token: FlowScalar | BlockScalar, tagToken: SourceToken | null, onError: ComposeErrorHandler -) { +): Scalar { const { value, type, comment, range } = token.type === 'block-scalar' ? resolveBlockScalar(ctx, token, onError) @@ -54,7 +54,7 @@ export function composeScalar( if (tag.format) scalar.format = tag.format if (comment) scalar.comment = comment - return scalar as Scalar.Parsed + return scalar } function findScalarTagByName( diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index d01c7a66..8cdb03ff 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -1,4 +1,3 @@ -import type { ParsedNode } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' import type { BlockMap } from '../parse/cst.ts' @@ -18,9 +17,9 @@ export function resolveBlockMap( bm: BlockMap, onError: ComposeErrorHandler, tag?: CollectionTag -) { +): YAMLMap { const NodeClass = tag?.nodeClass ?? YAMLMap - const map = new NodeClass(ctx.schema) as YAMLMap + const map = new NodeClass(ctx.schema) as YAMLMap if (ctx.atRoot) ctx.atRoot = false let offset = bm.offset @@ -84,7 +83,7 @@ export function resolveBlockMap( const valueProps = resolveProps(sep ?? [], { indicator: 'map-value-ind', next: value, - offset: keyNode.range[2], + offset: keyNode.range![2], onError, parentIndent: bm.indent, startOnNewline: !key || key.type === 'block-scalar' @@ -104,7 +103,7 @@ export function resolveBlockMap( keyProps.start < valueProps.found.offset - 1024 ) onError( - keyNode.range, + keyNode.range!, 'KEY_OVER_1024_CHARS', 'The : indicator must be at most 1024 chars after the start of an implicit block mapping key' ) @@ -114,7 +113,7 @@ export function resolveBlockMap( ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep, null, valueProps, onError) if (ctx.schema.compat) flowIndentCheck(bm.indent, value, onError) - offset = valueNode.range[2] + offset = valueNode.range![2] const pair = new Pair(keyNode, valueNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem map.items.push(pair) @@ -122,7 +121,7 @@ export function resolveBlockMap( // key with no value if (implicitKey) onError( - keyNode.range, + keyNode.range!, 'MISSING_CHAR', 'Implicit map keys need to be followed by map values' ) @@ -130,7 +129,7 @@ export function resolveBlockMap( if (keyNode.comment) keyNode.comment += '\n' + valueProps.comment else keyNode.comment = valueProps.comment } - const pair: Pair = new Pair(keyNode) + const pair = new Pair(keyNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem map.items.push(pair) } @@ -139,5 +138,5 @@ export function resolveBlockMap( if (commentEnd && commentEnd < offset) onError(commentEnd, 'IMPOSSIBLE', 'Map comment with trailing content') map.range = [bm.offset, offset, commentEnd ?? offset] - return map as YAMLMap.Parsed + return map } diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts index 7339da16..a9617073 100644 --- a/src/compose/resolve-block-seq.ts +++ b/src/compose/resolve-block-seq.ts @@ -12,7 +12,7 @@ export function resolveBlockSeq( bs: BlockSequence, onError: ComposeErrorHandler, tag?: CollectionTag -) { +): YAMLSeq { const NodeClass = tag?.nodeClass ?? YAMLSeq const seq = new NodeClass(ctx.schema) as YAMLSeq @@ -49,9 +49,9 @@ export function resolveBlockSeq( ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, start, null, props, onError) if (ctx.schema.compat) flowIndentCheck(bs.indent, value, onError) - offset = node.range[2] + offset = node.range![2] seq.items.push(node) } seq.range = [bs.offset, offset, commentEnd ?? offset] - return seq as YAMLSeq.Parsed + return seq } diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index a8efd6ab..42cce319 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -3,7 +3,6 @@ import { Pair } from '../nodes/Pair.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' import { YAMLSeq } from '../nodes/YAMLSeq.ts' import type { FlowCollection, Token } from '../parse/cst.ts' -import type { Schema } from '../schema/Schema.ts' import type { CollectionTag } from '../schema/types.ts' import type { ComposeContext, ComposeNode } from './compose-node.ts' import type { ComposeErrorHandler } from './composer.ts' @@ -22,13 +21,11 @@ export function resolveFlowCollection( fc: FlowCollection, onError: ComposeErrorHandler, tag?: CollectionTag -): YAMLMap.Parsed | YAMLSeq.Parsed { +): YAMLMap | YAMLSeq { const isMap = fc.start.source === '{' const fcName = isMap ? 'flow map' : 'flow sequence' - const NodeClass = (tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq)) as { - new (schema: Schema): YAMLMap.Parsed | YAMLSeq.Parsed - } - const coll = new NodeClass(ctx.schema) + const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq) + const coll = new NodeClass(ctx.schema) as YAMLMap | YAMLSeq coll.flow = true const atRoot = ctx.atRoot if (atRoot) ctx.atRoot = false @@ -113,8 +110,8 @@ export function resolveFlowCollection( ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError) ;(coll as YAMLSeq).items.push(valueNode) - offset = valueNode.range[2] - if (isBlock(value)) onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg) + offset = valueNode.range![2] + if (isBlock(value)) onError(valueNode.range!, 'BLOCK_IN_FLOW', blockMsg) } else { // item is a key+value pair @@ -124,7 +121,7 @@ export function resolveFlowCollection( const keyNode = key ? composeNode(ctx, key, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError) - if (isBlock(key)) onError(keyNode.range, 'BLOCK_IN_FLOW', blockMsg) + if (isBlock(key)) onError(keyNode.range!, 'BLOCK_IN_FLOW', blockMsg) ctx.atKey = false // value properties @@ -132,7 +129,7 @@ export function resolveFlowCollection( flow: fcName, indicator: 'map-value-ind', next: value, - offset: keyNode.range[2], + offset: keyNode.range![2], onError, parentIndent: fc.indent, startOnNewline: false @@ -184,7 +181,7 @@ export function resolveFlowCollection( ) : null if (valueNode) { - if (isBlock(value)) onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg) + if (isBlock(value)) onError(valueNode.range!, 'BLOCK_IN_FLOW', blockMsg) } else if (valueProps.comment) { if (keyNode.comment) keyNode.comment += '\n' + valueProps.comment else keyNode.comment = valueProps.comment @@ -193,7 +190,7 @@ export function resolveFlowCollection( const pair = new Pair(keyNode, valueNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem if (isMap) { - const map = coll as YAMLMap.Parsed + const map = coll as YAMLMap if (mapIncludes(ctx, map.items, keyNode)) onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique') map.items.push(pair) @@ -201,11 +198,11 @@ export function resolveFlowCollection( const map = new YAMLMap(ctx.schema) map.flow = true map.items.push(pair) - const endRange = (valueNode ?? keyNode).range - map.range = [keyNode.range[0], endRange[1], endRange[2]] + const endRange = (valueNode ?? keyNode).range! + map.range = [keyNode.range![0], endRange[1], endRange[2]] ;(coll as YAMLSeq).items.push(map) } - offset = valueNode ? valueNode.range[2] : valueProps.end + offset = valueNode ? valueNode.range![2] : valueProps.end } } diff --git a/src/compose/util-map-includes.ts b/src/compose/util-map-includes.ts index d51a82bf..38ba56b8 100644 --- a/src/compose/util-map-includes.ts +++ b/src/compose/util-map-includes.ts @@ -1,19 +1,19 @@ import { isScalar } from '../nodes/identity.ts' -import type { ParsedNode } from '../nodes/Node.ts' +import type { NodeBase } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import type { ComposeContext } from './compose-node.ts' export function mapIncludes( ctx: ComposeContext, - items: Pair[], - search: ParsedNode + items: Pair[], + search: NodeBase ): boolean { const { uniqueKeys } = ctx.options if (uniqueKeys === false) return false const isEqual = typeof uniqueKeys === 'function' ? uniqueKeys - : (a: ParsedNode, b: ParsedNode) => + : (a: NodeBase, b: NodeBase) => a === b || (isScalar(a) && isScalar(b) && a.value === b.value) return items.some(pair => isEqual(pair.key, search)) } diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 072daf33..05a308c6 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,6 +1,10 @@ import type { YAMLError, YAMLWarning } from '../errors.ts' import { Alias } from '../nodes/Alias.ts' -import { collectionFromPath, isEmptyPath } from '../nodes/Collection.ts' +import { + collectionFromPath, + isEmptyPath, + type Primitive +} from '../nodes/Collection.ts' import { DOC, isCollection, @@ -8,7 +12,13 @@ import { isScalar, NODE_TYPE } from '../nodes/identity.ts' -import type { Node, NodeType, ParsedNode, Range } from '../nodes/Node.ts' +import type { + Node, + NodeBase, + NodeType, + ParsedNode, + Range +} from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import type { Scalar } from '../nodes/Scalar.ts' import type { ToJSContext } from '../nodes/toJS.ts' @@ -237,22 +247,25 @@ export class Document< * Convert a key and a value into a `Pair` using the current schema, * recursively wrapping all values as `Scalar` or `Collection` nodes. */ - createPair( - key: unknown, - value: unknown, + createPair( + key: K, + value: V, options: CreateNodeOptions = {} - ): Pair { + ) { const nc = new NodeCreator(this, options) - const pair = nc.createPair(key, value) as Pair + const pair = nc.createPair(key, value) nc.setAnchors() - return pair + return pair as Pair< + K extends Primitive | NodeBase ? K : NodeBase, + V extends Primitive | NodeBase ? V : NodeBase + > } /** * Removes a value from the document. * @returns `true` if the item was found and removed. */ - delete(key: unknown): boolean { + delete(key: any): boolean { return assertCollection(this.contents) ? this.contents.delete(key) : false } @@ -277,7 +290,7 @@ export class Document< * scalar values from their surrounding node; to disable set `keepScalar` to * `true` (collections are always returned intact). */ - get(key: unknown, keepScalar?: boolean): Strict extends true ? unknown : any { + get(key: any, keepScalar?: boolean): Strict extends true ? unknown : any { return isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined @@ -304,7 +317,7 @@ export class Document< /** * Checks if the document includes a value with the key `key`. */ - has(key: unknown): boolean { + has(key: any): boolean { return isCollection(this.contents) ? this.contents.has(key) : false } @@ -320,7 +333,7 @@ export class Document< * Sets a value in this document. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. */ - set(key: any, value: unknown): void { + set(key: any, value: any): void { if (this.contents == null) { // @ts-expect-error We can't really know that this matches Contents. this.contents = collectionFromPath(this.schema, [key], value) diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index 529ce24a..7a4ff53a 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -8,7 +8,7 @@ import { MAP, SEQ } from '../nodes/identity.ts' -import type { Node } from '../nodes/Node.ts' +import type { Node, NodeBase } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' import type { YAMLMap } from '../nodes/YAMLMap.ts' @@ -148,10 +148,10 @@ export class NodeCreator { return node } - createPair(key: unknown, value: unknown): Pair { + createPair(key: unknown, value: unknown): Pair { const k = this.create(key) - const v = this.create(value) - return new Pair(k, v) + const v = value == null ? null : this.create(value) + return new Pair(k, v) } /** diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts index dfc8583e..c5c437f6 100644 --- a/src/nodes/Alias.ts +++ b/src/nodes/Alias.ts @@ -4,7 +4,7 @@ import type { FlowScalar } from '../parse/cst.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { visit } from '../visit.ts' import { ALIAS, hasAnchor, isAlias, isCollection, isPair } from './identity.ts' -import type { Node, Range } from './Node.ts' +import type { Node } from './Node.ts' import { NodeBase } from './Node.ts' import type { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' @@ -12,17 +12,11 @@ import { toJS } from './toJS.ts' import type { YAMLMap } from './YAMLMap.ts' import type { YAMLSeq } from './YAMLSeq.ts' -export declare namespace Alias { - interface Parsed extends Alias { - range: Range - srcToken?: FlowScalar & { type: 'alias' } - } -} - export class Alias extends NodeBase { source: string declare anchor?: never + declare srcToken?: FlowScalar & { type: 'alias' } constructor(source: string) { super(ALIAS) diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index 05a7c90a..55054ba8 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,13 +1,12 @@ import { NodeCreator } from '../doc/NodeCreator.ts' import type { Schema } from '../schema/Schema.ts' -import { - isCollection, - isNode, - isPair, - isScalar, - NODE_TYPE -} from './identity.ts' +import { isCollection, isScalar, NODE_TYPE } from './identity.ts' import { type Node, NodeBase } from './Node.ts' +import type { Pair } from './Pair.ts' +import type { Scalar } from './Scalar.ts' + +export type Primitive = boolean | number | bigint | string | null +export type NodeOf = T extends Primitive ? Scalar : T export function collectionFromPath( schema: Schema, @@ -42,7 +41,7 @@ export abstract class Collection extends NodeBase { /** @internal */ declare [NODE_TYPE]: symbol - declare items: unknown[] + declare items: (NodeBase | Pair)[] /** An optional anchor on this node. Used by alias nodes. */ declare anchor?: string @@ -74,9 +73,7 @@ export abstract class Collection extends NodeBase { Object.getOwnPropertyDescriptors(this) ) if (schema) copy.schema = schema - copy.items = copy.items.map(it => - isNode(it) || isPair(it) ? it.clone(schema) : it - ) + copy.items = copy.items.map(it => it.clone(schema)) if (this.range) copy.range = this.range.slice() as NodeBase['range'] return copy } @@ -103,15 +100,14 @@ export abstract class Collection extends NodeBase { abstract has(key: unknown): boolean /** - * Sets a value in this collection. For `!!set`, `value` needs to be a - * boolean to add/remove the item from the set. + * Sets a value in this collection. */ abstract set(key: unknown, value: unknown): 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. + * Adds a value to the collection. + * + * For `!!map` and `!!omap` the value must be a Pair instance. */ addIn(path: Iterable, value: unknown): void { if (isEmptyPath(path)) this.add(value) @@ -130,6 +126,7 @@ export abstract class Collection extends NodeBase { /** * Removes a value from the collection. + * * @returns `true` if the item was found and removed. */ deleteIn(path: Iterable): boolean { @@ -156,22 +153,6 @@ export abstract class Collection extends NodeBase { else return isCollection(node) ? node.getIn(rest, keepScalar) : undefined } - hasAllNullValues(allowScalar?: boolean): boolean { - return this.items.every(node => { - if (!isPair(node)) return false - const n = node.value - return ( - n == null || - (allowScalar && - isScalar(n) && - n.value == null && - !n.commentBefore && - !n.comment && - !n.tag) - ) - }) - } - /** * Checks if the collection includes a value with the key `key`. */ @@ -183,8 +164,7 @@ export abstract class Collection extends NodeBase { } /** - * Sets a value in this collection. For `!!set`, `value` needs to be a - * boolean to add/remove the item from the set. + * Sets a value in this collection. */ setIn(path: Iterable, value: unknown): void { const [key, ...rest] = path diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index 403c157c..52014e07 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -2,6 +2,7 @@ import { applyReviver } from '../doc/applyReviver.ts' import type { Document } from '../doc/Document.ts' import type { ToJSOptions } from '../options.ts' import type { Token } from '../parse/cst.ts' +import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import type { Alias } from './Alias.ts' import { isDocument, NODE_TYPE } from './identity.ts' @@ -11,11 +12,7 @@ import { toJS } from './toJS.ts' import type { MapLike, YAMLMap } from './YAMLMap.ts' import type { YAMLSeq } from './YAMLSeq.ts' -export type Node = - | Alias - | Scalar - | YAMLMap - | YAMLSeq +export type Node = Alias | Scalar | YAMLSeq | YAMLMap /** Utility type mapper */ export type NodeType = T extends @@ -36,11 +33,7 @@ export type NodeType = T extends ? YAMLMap, NodeType> : Node -export type ParsedNode = - | Alias.Parsed - | Scalar.Parsed - | YAMLMap.Parsed - | YAMLSeq.Parsed +export type ParsedNode = Alias | Scalar | YAMLMap | YAMLSeq /** `[start, value-end, node-end]` */ export type Range = [number, number, number] @@ -96,7 +89,7 @@ export abstract class NodeBase { } /** Create a copy of this node. */ - clone(): NodeBase { + clone(_schema?: Schema): NodeBase { const copy: NodeBase = Object.create( Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this) diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 1b573c8b..b11e5efb 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -3,32 +3,33 @@ import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyPair } from '../stringify/stringifyPair.ts' import { addPairToJSMap } from './addPairToJSMap.ts' -import { isNode, NODE_TYPE, PAIR } from './identity.ts' +import type { NodeOf, Primitive } from './Collection.ts' +import { NODE_TYPE, PAIR } from './identity.ts' +import type { NodeBase } from './Node.ts' import type { ToJSContext } from './toJS.ts' -export class Pair { +export class Pair< + K extends Primitive | NodeBase = Primitive | NodeBase, + V extends Primitive | NodeBase = Primitive | NodeBase +> { /** @internal */ declare readonly [NODE_TYPE]: symbol - /** Always Node or null when parsed, but can be set to anything. */ - key: K - - /** Always Node or null when parsed, but can be set to anything. */ - value: V | null + key: NodeOf + value: NodeOf | null /** The CST token that was composed into this pair. */ declare srcToken?: CollectionItem - constructor(key: K, value: V | null = null) { + constructor(key: NodeOf, value: NodeOf | null = null) { Object.defineProperty(this, NODE_TYPE, { value: PAIR }) this.key = key this.value = value } clone(schema?: Schema): Pair { - let { key, value } = this - if (isNode(key)) key = key.clone(schema) as unknown as K - if (isNode(value)) value = value.clone(schema) as unknown as V + const key = this.key.clone(schema) as NodeOf + const value = (this.value?.clone(schema) ?? null) as NodeOf return new Pair(key, value) } diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index 00eabfe6..6b82f155 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -1,20 +1,10 @@ import type { BlockScalar, FlowScalar } from '../parse/cst.ts' import { SCALAR } from './identity.ts' -import type { Range } from './Node.ts' import { NodeBase } from './Node.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' -export const isScalarValue = (value: unknown): boolean => - !value || (typeof value !== 'function' && typeof value !== 'object') - export declare namespace Scalar { - interface Parsed extends Scalar { - range: Range - source: string - srcToken?: FlowScalar | BlockScalar - } - type BLOCK_FOLDED = 'BLOCK_FOLDED' type BLOCK_LITERAL = 'BLOCK_LITERAL' type PLAIN = 'PLAIN' @@ -49,6 +39,8 @@ export class Scalar extends NodeBase { /** Set during parsing to the source string value */ declare source?: string + declare srcToken?: FlowScalar | BlockScalar + /** The scalar style used for the node's string representation */ declare type?: Scalar.Type diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index 9efb0707..ee80be99 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -1,15 +1,15 @@ +import type { CreateNodeOptions } from '../options.ts' import type { BlockMap, FlowCollection } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' -import type { NodeCreator } from '../util.ts' +import { NodeCreator } from '../doc/NodeCreator.ts' import { addPairToJSMap } from './addPairToJSMap.ts' -import { Collection } from './Collection.ts' -import { isPair, isScalar, MAP } from './identity.ts' -import type { ParsedNode, Range } from './Node.ts' +import { Collection, type Primitive } from './Collection.ts' +import { isNode, isPair, isScalar, MAP } from './identity.ts' +import type { NodeBase } from './Node.ts' import { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' -import { isScalarValue } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' export type MapLike = @@ -17,37 +17,28 @@ export type MapLike = | Set | Record -export function findPair( - items: Iterable>, - key: unknown -): Pair | undefined { +export function findPair< + K extends Primitive | NodeBase = Primitive | NodeBase, + V extends Primitive | NodeBase = Primitive | NodeBase +>(items: Iterable>, key: unknown): Pair | undefined { const k = isScalar(key) ? key.value : key for (const it of items) { - if (isPair(it)) { - if (it.key === key || it.key === k) return it - if (isScalar(it.key) && it.key.value === k) return it - } + if (it.key === key || it.key === k) return it + if (isScalar(it.key) && it.key.value === k) return it } return undefined } -export declare namespace YAMLMap { - interface Parsed< - K extends ParsedNode = ParsedNode, - V extends ParsedNode | null = ParsedNode | null - > extends YAMLMap { - items: Pair[] - range: Range - srcToken?: BlockMap | FlowCollection - } -} - -export class YAMLMap extends Collection { +export class YAMLMap< + K extends Primitive | NodeBase = Primitive | NodeBase, + V extends Primitive | NodeBase = Primitive | NodeBase +> extends Collection { static get tagName(): 'tag:yaml.org,2002:map' { return 'tag:yaml.org,2002:map' } items: Pair[] = [] + declare srcToken?: BlockMap | FlowCollection constructor(schema?: Schema) { super(MAP, schema) @@ -57,7 +48,7 @@ export class YAMLMap extends Collection { * A generic collection parsing method that can be extended * to other node classes that inherit from YAMLMap */ - static from(nc: NodeCreator, obj: unknown): YAMLMap { + static from(nc: NodeCreator, obj: unknown): YAMLMap { const { replacer } = nc const map = new this(nc.schema) const add = (key: unknown, value: unknown) => { @@ -78,33 +69,26 @@ export class YAMLMap extends Collection { } /** - * Adds a value to the collection. + * Adds a key-value pair to the map. * - * @param overwrite - If not set `true`, using a key that is already in the - * collection will throw. Otherwise, overwrites the previous value. + * Using a key that is already in the collection overwrites the previous value. */ - add(pair: Pair | { key: K; value: V }, overwrite?: boolean): void { - let _pair: Pair - if (isPair(pair)) _pair = pair - else if (!pair || typeof pair !== 'object' || !('key' in pair)) { - // In TypeScript, this never happens. - _pair = new Pair(pair as any, (pair as any)?.value) - } else _pair = new Pair(pair.key, pair.value) + add(pair: Pair): void { + if (!isPair(pair)) throw new TypeError('Expected a Pair') - const prev = findPair(this.items, _pair.key) + const prev = findPair(this.items, pair.key) const sortEntries = this.schema?.sortMapEntries if (prev) { - if (!overwrite) throw new Error(`Key ${_pair.key} already set`) // For scalars, keep the old node & its comments and anchors - if (isScalar(prev.value) && isScalarValue(_pair.value)) - prev.value.value = _pair.value - else prev.value = _pair.value + if (isScalar(prev.value) && isScalar(pair.value)) + prev.value.value = pair.value.value + else prev.value = pair.value } else if (sortEntries) { - const i = this.items.findIndex(item => sortEntries(_pair, item) < 0) - if (i === -1) this.items.push(_pair) - else this.items.splice(i, 0, _pair) + const i = this.items.findIndex(item => sortEntries(pair, item) < 0) + if (i === -1) this.items.push(pair) + else this.items.splice(i, 0, pair) } else { - this.items.push(_pair) + this.items.push(pair) } } @@ -121,15 +105,34 @@ export class YAMLMap extends Collection { get(key: unknown, keepScalar?: boolean): V | Scalar | undefined { const it = findPair(this.items, key) const node = it?.value - return (!keepScalar && isScalar(node) ? node.value : node) ?? undefined + return ( + (!keepScalar && isScalar(node) ? (node.value as V) : node) ?? undefined + ) } has(key: unknown): boolean { return !!findPair(this.items, key) } - set(key: K, value: V): void { - this.add(new Pair(key, value), true) + set( + key: unknown, + value: unknown, + options?: Omit + ): void { + let pair: Pair + if (isNode(key) && (isNode(value) || value === null)) { + pair = new Pair(key, value) + } else if (!this.schema) { + throw new Error('Schema is required') + } else { + const nc = new NodeCreator(this.schema, { + ...options, + aliasDuplicateObjects: false + }) + pair = nc.createPair(key, value) + nc.setAnchors() + } + this.add(pair as Pair) } /** @@ -160,8 +163,6 @@ export class YAMLMap extends Collection { `Map items must all be pairs; found ${JSON.stringify(item)} instead` ) } - if (!ctx.allNullValues && this.hasAllNullValues(false)) - ctx = Object.assign({}, ctx, { allNullValues: true }) return stringifyCollection(this, ctx, { blockItemPrefix: '', flowChars: { start: '{', end: '}' }, diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index 95d9973e..f96c0b8b 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -1,53 +1,61 @@ -import type { NodeCreator } from '../doc/NodeCreator.ts' +import { NodeCreator } from '../doc/NodeCreator.ts' +import type { CreateNodeOptions } from '../options.ts' import type { BlockSequence, FlowCollection } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' -import { Collection } from './Collection.ts' -import { isScalar, SEQ } from './identity.ts' -import type { ParsedNode, Range } from './Node.ts' +import { Collection, type NodeOf, type Primitive } from './Collection.ts' +import { isNode, isScalar, SEQ } from './identity.ts' +import type { NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' -import { isScalarValue } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' -export declare namespace YAMLSeq { - interface Parsed< - T extends ParsedNode | Pair = ParsedNode - > extends YAMLSeq { - items: T[] - range: Range - srcToken?: BlockSequence | FlowCollection - } -} +const isScalarValue = (value: unknown): boolean => + !value || (typeof value !== 'function' && typeof value !== 'object') -export class YAMLSeq extends Collection { +export class YAMLSeq< + T extends Primitive | NodeBase | Pair = Primitive | NodeBase | Pair +> extends Collection { static get tagName(): 'tag:yaml.org,2002:seq' { return 'tag:yaml.org,2002:seq' } - items: T[] = [] + items: NodeOf[] = [] + declare srcToken?: BlockSequence | FlowCollection constructor(schema?: Schema) { super(SEQ, schema) } - add(value: T): void { - this.items.push(value) + add( + value: T, + options?: Omit + ): void { + if (isNode(value)) this.items.push(value as NodeOf) + else if (!this.schema) throw new Error('Schema is required') + else { + const nc = new NodeCreator(this.schema, { + ...options, + aliasDuplicateObjects: false + }) + this.items.push(nc.create(value) as NodeOf) + nc.setAnchors() + } } /** * Removes a value from the collection. * - * `key` must contain a representation of an integer for this to succeed. - * It may be wrapped in a `Scalar`. + * Throws if `idx` is not a non-negative integer. * * @returns `true` if the item was found and removed. */ - delete(key: unknown): boolean { - const idx = asItemIndex(key) - if (typeof idx !== 'number') return false + delete(idx: number): boolean { + if (!Number.isInteger(idx)) + throw new TypeError(`Expected an integer, not ${idx}.`) + if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) const del = this.items.splice(idx, 1) return del.length > 0 } @@ -57,44 +65,57 @@ export class YAMLSeq extends Collection { * scalar values from their surrounding node; to disable set `keepScalar` to * `true` (collections are always returned intact). * - * `key` must contain a representation of an integer for this to succeed. - * It may be wrapped in a `Scalar`. + * Throws if `idx` is not a non-negative integer. */ - get(key: unknown, keepScalar: true): Scalar | undefined - get(key: unknown, keepScalar?: false): T | undefined - get(key: unknown, keepScalar?: boolean): T | Scalar | undefined - get(key: unknown, keepScalar?: boolean): T | Scalar | undefined { - const idx = asItemIndex(key) - if (typeof idx !== 'number') return undefined + get(idx: number, keepScalar: true): Scalar | undefined + get(idx: number, keepScalar?: false): T | undefined + get(idx: number, keepScalar?: boolean): T | Scalar | undefined + get(idx: number, keepScalar?: boolean): T | Scalar | undefined { + if (!Number.isInteger(idx)) + throw new TypeError(`Expected an integer, not ${JSON.stringify(idx)}.`) + if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) const it = this.items[idx] - return !keepScalar && isScalar(it) ? it.value : it + return !keepScalar && isScalar(it) ? (it.value as T) : it } /** * Checks if the collection includes a value with the key `key`. * - * `key` must contain a representation of an integer for this to succeed. - * It may be wrapped in a `Scalar`. + * Throws if `idx` is not a non-negative integer. */ - has(key: unknown): boolean { - const idx = asItemIndex(key) - return typeof idx === 'number' && idx < this.items.length + has(idx: number): boolean { + if (!Number.isInteger(idx)) + throw new TypeError(`Expected an integer, not ${JSON.stringify(idx)}.`) + if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) + return idx < this.items.length } /** * Sets a value in this collection. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. * - * If `key` does not contain a representation of an integer, this will throw. - * It may be wrapped in a `Scalar`. + * Throws if `idx` is not a non-negative integer. */ - set(key: unknown, value: T): void { - const idx = asItemIndex(key) - if (typeof idx !== 'number') - throw new Error(`Expected a valid index, not ${key}.`) + set( + idx: number, + value: T, + options?: Omit + ): void { + if (!Number.isInteger(idx)) + throw new TypeError(`Expected an integer, not ${JSON.stringify(idx)}.`) + if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) const prev = this.items[idx] if (isScalar(prev) && isScalarValue(value)) prev.value = value - else this.items[idx] = value + else if (isNode(value)) this.items[idx] = value as NodeOf + else if (!this.schema) throw new Error('Schema is required') + else { + const nc = new NodeCreator(this.schema, { + ...options, + aliasDuplicateObjects: false + }) + this.items[idx] = nc.create(value) as NodeOf + nc.setAnchors() + } } toJSON(_?: unknown, ctx?: ToJSContext): unknown[] { @@ -135,11 +156,3 @@ export class YAMLSeq extends Collection { return seq } } - -function asItemIndex(key: unknown): number | null { - let idx = isScalar(key) ? key.value : key - if (idx && typeof idx === 'string') idx = Number(idx) - return typeof idx === 'number' && Number.isInteger(idx) && idx >= 0 - ? idx - : null -} diff --git a/src/nodes/identity.ts b/src/nodes/identity.ts index 9f1fba2f..638a8ec8 100644 --- a/src/nodes/identity.ts +++ b/src/nodes/identity.ts @@ -17,30 +17,22 @@ export const NODE_TYPE: unique symbol = Symbol.for('yaml.node.type') export const isAlias = (node: any): node is Alias => !!node && typeof node === 'object' && node[NODE_TYPE] === ALIAS -export const isDocument = ( - node: any -): node is Document => +export const isDocument = (node: any): node is Document => !!node && typeof node === 'object' && node[NODE_TYPE] === DOC -export const isMap = ( - node: any -): node is YAMLMap => +export const isMap = (node: any): node is YAMLMap => !!node && typeof node === 'object' && node[NODE_TYPE] === MAP -export const isPair = ( - node: any -): node is Pair => +export const isPair = (node: any): node is Pair => !!node && typeof node === 'object' && node[NODE_TYPE] === PAIR -export const isScalar = (node: any): node is Scalar => +export const isScalar = (node: any): node is Scalar => !!node && typeof node === 'object' && node[NODE_TYPE] === SCALAR -export const isSeq = (node: any): node is YAMLSeq => +export const isSeq = (node: any): node is YAMLSeq => !!node && typeof node === 'object' && node[NODE_TYPE] === SEQ -export function isCollection( - node: any -): node is YAMLMap | YAMLSeq { +export function isCollection(node: any): node is YAMLMap | YAMLSeq { if (node && typeof node === 'object') switch (node[NODE_TYPE]) { case MAP: @@ -50,7 +42,7 @@ export function isCollection( return false } -export function isNode(node: any): node is Node { +export function isNode(node: any): node is Node { if (node && typeof node === 'object') switch (node[NODE_TYPE]) { case ALIAS: @@ -62,7 +54,5 @@ export function isNode(node: any): node is Node { return false } -export const hasAnchor = ( - node: unknown -): node is Scalar | YAMLMap | YAMLSeq => +export const hasAnchor = (node: unknown): node is Scalar | YAMLMap | YAMLSeq => (isScalar(node) || isCollection(node)) && !!node.anchor diff --git a/src/options.ts b/src/options.ts index 2b8a0585..ef8a0aaf 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,7 +1,7 @@ import type { Reviver } from './doc/applyReviver.ts' import type { Directives } from './doc/directives.ts' import type { LogLevelId } from './log.ts' -import type { ParsedNode } from './nodes/Node.ts' +import type { NodeBase } from './nodes/Node.ts' import type { Pair } from './nodes/Pair.ts' import type { Scalar } from './nodes/Scalar.ts' import type { LineCounter } from './parse/line-counter.ts' @@ -62,12 +62,12 @@ export type ParseOptions = { * multiple `<<` keys are allowed. * * Set `false` to disable, or provide your own comparator function to - * customise. The comparator will be passed two `ParsedNode` values, and + * customise. The comparator will be passed two node values, and * is expected to return a `boolean` indicating their equality. * * Default: `true` */ - uniqueKeys?: boolean | ((a: ParsedNode, b: ParsedNode) => boolean) + uniqueKeys?: boolean | ((a: NodeBase, b: NodeBase) => boolean) } export type DocumentOptions = { diff --git a/src/schema/common/map.ts b/src/schema/common/map.ts index 468d9cd3..68bd75fc 100644 --- a/src/schema/common/map.ts +++ b/src/schema/common/map.ts @@ -10,6 +10,5 @@ export const map: CollectionTag = { resolve(map, onError) { if (!isMap(map)) onError('Expected a mapping for this tag') return map - }, - createNode: (nc, obj) => YAMLMap.from(nc, obj) + } } diff --git a/src/schema/common/seq.ts b/src/schema/common/seq.ts index 31e1d7e1..012e8c5e 100644 --- a/src/schema/common/seq.ts +++ b/src/schema/common/seq.ts @@ -10,6 +10,5 @@ export const seq: CollectionTag = { resolve(seq, onError) { if (!isSeq(seq)) onError('Expected a sequence for this tag') return seq - }, - createNode: (nc, obj) => YAMLSeq.from(nc, obj) + } } diff --git a/src/schema/types.ts b/src/schema/types.ts index 456fd87b..042584df 100644 --- a/src/schema/types.ts +++ b/src/schema/types.ts @@ -110,7 +110,7 @@ export interface CollectionTag extends TagBase { * Note: this is required if nodeClass is not provided. */ resolve?: ( - value: YAMLMap.Parsed | YAMLSeq.Parsed, + value: YAMLMap | YAMLSeq, onError: (message: string) => void, options: ParseOptions ) => unknown diff --git a/src/schema/yaml-1.1/omap.ts b/src/schema/yaml-1.1/omap.ts index d9d9b8f2..0ae311d6 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -1,17 +1,24 @@ +import type { Primitive } from '../../nodes/Collection.ts' import { isPair, isScalar } from '../../nodes/identity.ts' +import type { NodeBase } from '../../nodes/Node.ts' +import type { Pair } from '../../nodes/Pair.ts' import type { ToJSContext } from '../../nodes/toJS.ts' import { toJS } from '../../nodes/toJS.ts' import { YAMLMap } from '../../nodes/YAMLMap.ts' import { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { NodeCreator } from '../../util.ts' +import type { Schema } from '../Schema.ts' import type { CollectionTag } from '../types.ts' import { createPairs, resolvePairs } from './pairs.ts' -export class YAMLOMap extends YAMLSeq { +export class YAMLOMap< + K extends Primitive | NodeBase = Primitive | NodeBase, + V extends Primitive | NodeBase = Primitive | NodeBase +> extends YAMLSeq> { static tag = 'tag:yaml.org,2002:omap' - constructor() { - super() + constructor(schema?: Schema) { + super(schema) this.tag = YAMLOMap.tag } @@ -46,7 +53,7 @@ export class YAMLOMap extends YAMLSeq { static from(nc: NodeCreator, iterable: unknown): YAMLOMap { const pairs = createPairs(nc, iterable) - const omap = new this() + const omap = new this(nc.schema) omap.items = pairs.items return omap } @@ -72,6 +79,5 @@ export const omap: CollectionTag = { } } return Object.assign(new YAMLOMap(), pairs) - }, - createNode: (nc, iterable) => YAMLOMap.from(nc, iterable) + } } diff --git a/src/schema/yaml-1.1/pairs.ts b/src/schema/yaml-1.1/pairs.ts index e899b572..92311f72 100644 --- a/src/schema/yaml-1.1/pairs.ts +++ b/src/schema/yaml-1.1/pairs.ts @@ -1,6 +1,6 @@ import type { NodeCreator } from '../../doc/NodeCreator.ts' import { isMap, isPair, isSeq } from '../../nodes/identity.ts' -import type { ParsedNode } from '../../nodes/Node.ts' +import type { NodeBase } from '../../nodes/Node.ts' import { Pair } from '../../nodes/Pair.ts' import { Scalar } from '../../nodes/Scalar.ts' import type { YAMLMap } from '../../nodes/YAMLMap.ts' @@ -8,20 +8,17 @@ import { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { CollectionTag } from '../types.ts' export function resolvePairs( - seq: - | YAMLSeq.Parsed> - | YAMLMap.Parsed, + seq: YAMLSeq | YAMLMap, onError: (message: string) => void -) { +): YAMLSeq { if (isSeq(seq)) { for (let i = 0; i < seq.items.length; ++i) { - let item = seq.items[i] + const item = seq.items[i] if (isPair(item)) continue else if (isMap(item)) { if (item.items.length > 1) onError('Each pair must have its own sequence indicator') - const pair = - item.items[0] || new Pair(new Scalar(null) as Scalar.Parsed) + const pair = item.items[0] || new Pair(new Scalar(null)) if (item.commentBefore) pair.key.commentBefore = pair.key.commentBefore ? `${item.commentBefore}\n${pair.key.commentBefore}` @@ -32,16 +29,17 @@ export function resolvePairs( ? `${item.comment}\n${cn.comment}` : item.comment } - item = pair + seq.items[i] = pair + } else { + seq.items[i] = new Pair(item, null) } - seq.items[i] = isPair(item) ? item : new Pair(item) } } else onError('Expected a sequence for this tag') - return seq as YAMLSeq.Parsed> + return seq as YAMLSeq } -export function createPairs(nc: NodeCreator, iterable: unknown): YAMLSeq { - const pairs = new YAMLSeq(nc.schema) +export function createPairs(nc: NodeCreator, iterable: unknown): YAMLSeq { + const pairs = new YAMLSeq(nc.schema) pairs.tag = 'tag:yaml.org,2002:pairs' let i = 0 if (iterable && Symbol.iterator in Object(iterable)) diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index 0a128046..d72c3627 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -1,14 +1,18 @@ -import { isMap, isPair, isScalar } from '../../nodes/identity.ts' +import { NodeCreator } from '../../doc/NodeCreator.ts' +import type { NodeOf, Primitive } from '../../nodes/Collection.ts' +import { isMap, isNode, isPair, isScalar } from '../../nodes/identity.ts' +import type { NodeBase } from '../../nodes/Node.ts' import { Pair } from '../../nodes/Pair.ts' -import type { Scalar } from '../../nodes/Scalar.ts' import type { ToJSContext } from '../../nodes/toJS.ts' import { findPair, YAMLMap } from '../../nodes/YAMLMap.ts' +import type { CreateNodeOptions } from '../../options.ts' import type { Schema } from '../../schema/Schema.ts' import type { StringifyContext } from '../../stringify/stringify.ts' -import type { NodeCreator } from '../../util.ts' import type { CollectionTag } from '../types.ts' -export class YAMLSet extends YAMLMap | null> { +export class YAMLSet< + T extends Primitive | NodeBase = Primitive | NodeBase +> extends YAMLMap { static tag = 'tag:yaml.org,2002:set' constructor(schema?: Schema) { @@ -16,25 +20,25 @@ export class YAMLSet extends YAMLMap | null> { this.tag = YAMLSet.tag } + /** + * Add a value to the set. + * + * If `value` is a Pair, its `.value` must be null and `options` is ignored. + * + * If the set already includes a matching value, no value is added. + */ add( - key: - | T - | Pair | null> - | { key: T; value: Scalar | null } + value: unknown, + options?: Omit ): void { - let pair: Pair | null> - if (isPair(key)) pair = key - else if ( - key && - typeof key === 'object' && - 'key' in key && - 'value' in key && - key.value === null - ) - pair = new Pair(key.key, null) - else pair = new Pair(key as T, null) - const prev = findPair(this.items, pair.key) - if (!prev) this.items.push(pair) + if (!isPair(value)) { + this.set(value, true, options) + } else if (value.value !== null) { + throw new TypeError('set pair values must be null') + } else { + const prev = findPair(this.items, value.key) + if (!prev) this.items.push(value as Pair) + } } /** @@ -50,20 +54,34 @@ export class YAMLSet extends YAMLMap | null> { : pair } - set(key: T, value: boolean): void - - /** @deprecated Will throw; `value` must be boolean */ - set(key: T, value: null): void - set(key: T, value: boolean | null) { + /** + * `value` needs to be true/false to add/remove the item from the set. + */ + set( + key: unknown, + value: boolean, + options?: Omit + ): void { if (typeof value !== 'boolean') - throw new Error( - `Expected boolean value for set(key, value) in a YAML set, not ${typeof value}` - ) + throw new Error(`Expected a boolean value, not ${typeof value}`) const prev = findPair(this.items, key) if (prev && !value) { this.items.splice(this.items.indexOf(prev), 1) } else if (!prev && value) { - this.items.push(new Pair(key)) + let node: NodeBase + if (isNode(key)) { + node = key + } else if (!this.schema) { + throw new Error('Schema is required') + } else { + const nc = new NodeCreator(this.schema, { + ...options, + aliasDuplicateObjects: false + }) + node = nc.create(key) + nc.setAnchors() + } + this.items.push(new Pair(node as NodeOf)) } } @@ -77,13 +95,7 @@ export class YAMLSet extends YAMLMap | null> { onChompKeep?: () => void ): string { if (!ctx) return JSON.stringify(this) - if (this.hasAllNullValues(true)) - return super.toString( - Object.assign({}, ctx, { allNullValues: true }), - onComment, - onChompKeep - ) - else throw new Error('Set items must all have null values') + return super.toString({ ...ctx, noValues: true }, onComment, onChompKeep) } static from(nc: NodeCreator, iterable: unknown): YAMLSet { @@ -92,26 +104,40 @@ export class YAMLSet extends YAMLMap | null> { for (let value of iterable as Iterable) { if (typeof nc.replacer === 'function') value = nc.replacer.call(iterable, value, value) - set.items.push( - nc.createPair(value, null) as Pair> - ) + set.items.push(nc.createPair(value, null) as Pair) } return set } } +const hasAllNullValues = (map: YAMLMap): boolean => + map.items.every( + ({ value }) => + value == null || + (isScalar(value) && + value.value == null && + !value.commentBefore && + !value.comment && + !value.tag) + ) + export const set: CollectionTag = { collection: 'map', identify: value => value instanceof Set, nodeClass: YAMLSet, default: false, tag: 'tag:yaml.org,2002:set', - createNode: (nc, iterable) => YAMLSet.from(nc, iterable), resolve(map, onError) { - if (isMap(map)) { - if (map.hasAllNullValues(true)) return Object.assign(new YAMLSet(), map) - else onError('Set items must all have null values') - } else onError('Expected a mapping for this tag') - return map + if (!isMap(map)) { + onError('Expected a mapping for this tag') + return map + } else if (!hasAllNullValues(map)) { + onError('Set items must all have null values') + return map + } else { + const set = Object.assign(new YAMLSet(), map) + for (const pair of map.items) pair.value &&= null + return set + } } } diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 533f5ff7..9ae56135 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -17,7 +17,6 @@ import { stringifyString } from './stringifyString.ts' export type StringifyContext = { actualString?: boolean - allNullValues?: boolean anchors: Set doc: Document forceBlockIndent?: boolean @@ -28,6 +27,7 @@ export type StringifyContext = { inFlow: boolean | null inStringifyKey?: boolean flowCollectionPadding: string + noValues?: boolean options: Readonly< Required> > diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts index 5de8e874..e3561192 100644 --- a/src/stringify/stringifyPair.ts +++ b/src/stringify/stringifyPair.ts @@ -12,10 +12,10 @@ export function stringifyPair( onChompKeep?: () => void ): string { const { - allNullValues, doc, indent, indentStep, + noValues, options: { commentString, indentSeq, simpleKeys } } = ctx let keyComment = (isNode(key) && key.comment) || null @@ -31,17 +31,16 @@ export function stringifyPair( let explicitKey = !simpleKeys && (!key || - (keyComment && value == null && !ctx.inFlow) || - isCollection(key) || - (isScalar(key) - ? key.type === Scalar.BLOCK_FOLDED || key.type === Scalar.BLOCK_LITERAL - : typeof key === 'object')) + !isScalar(key) || + key.type === Scalar.BLOCK_FOLDED || + key.type === Scalar.BLOCK_LITERAL) - ctx = Object.assign({}, ctx, { - allNullValues: false, - implicitKey: !explicitKey && (simpleKeys || !allNullValues), - indent: indent + indentStep - }) + ctx = { + ...ctx, + implicitKey: !explicitKey && (simpleKeys || !noValues), + indent: indent + indentStep, + noValues: false + } let keyCommentDone = false let chompKeep = false let str = stringify( @@ -60,11 +59,17 @@ export function stringifyPair( } if (ctx.inFlow) { - if (allNullValues || value == null) { + if (noValues || value == null) { if (keyCommentDone && onComment) onComment() - return str === '' ? '?' : explicitKey ? `? ${str}` : str + return str === '' + ? '?' + : explicitKey + ? `? ${str}` + : noValues + ? str + : `${str}:` } - } else if ((allNullValues && !simpleKeys) || (value == null && explicitKey)) { + } else if ((noValues && !simpleKeys) || (value == null && explicitKey)) { str = `? ${str}` if (keyComment && !keyCommentDone) { str += lineComment(str, ctx.indent, commentString(keyComment)) diff --git a/src/test-events.ts b/src/test-events.ts index 97b987fc..6b869de6 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -8,7 +8,7 @@ import { isScalar, isSeq } from './nodes/identity.ts' -import type { Node, ParsedNode } from './nodes/Node.ts' +import type { Node, NodeBase } from './nodes/Node.ts' import type { Pair } from './nodes/Pair.ts' import { parseAllDocuments } from './public-api.ts' import { visit } from './visit.ts' @@ -55,7 +55,7 @@ export function testEvents(src: string): { if (doc.directives.docStart) docStart += ' ---' else if ( doc.contents && - doc.contents.range[2] === doc.contents.range[0] && + doc.contents.range![2] === doc.contents.range![0] && !doc.contents.anchor && !doc.contents.tag ) @@ -78,13 +78,13 @@ function addEvents( events: string[], doc: Document, errPos: number, - node: ParsedNode | Pair | null + node: NodeBase | Pair | null ) { if (!node) { events.push('=VAL :') return } - if (errPos !== -1 && isNode(node) && node.range[0] >= errPos) + if (errPos !== -1 && isNode(node) && node.range![0] >= errPos) throw new Error() let props = '' let anchor = isScalar(node) || isCollection(node) ? node.anchor : undefined @@ -124,11 +124,11 @@ function addEvents( if (anchorExists(doc, alt)) alias = alt } events.push(`=ALI${props} *${alias}`) - } else { + } else if (isScalar(node)) { const scalar = scalarChar[String(node.type)] if (!scalar) throw new Error(`Unexpected node type ${node.type}`) - const value = node.source - .replace(/\\/g, '\\\\') + const value = node + .source!.replace(/\\/g, '\\\\') .replace(/\0/g, '\\0') .replace(/\x07/g, '\\a') .replace(/\x08/g, '\\b') @@ -139,5 +139,7 @@ function addEvents( .replace(/\r/g, '\\r') .replace(/\x1b/g, '\\e') events.push(`=VAL${props} ${scalar}${value}`) + } else { + throw new Error(`Unexpected node ${node}`) } } diff --git a/src/visit.ts b/src/visit.ts index 7ac9c1b9..50380aa9 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -12,7 +12,7 @@ import { } from './nodes/identity.ts' import type { Node } from './nodes/Node.ts' import type { Pair } from './nodes/Pair.ts' -import type { Scalar } from './nodes/Scalar.ts' +import { Scalar } from './nodes/Scalar.ts' import type { YAMLMap } from './nodes/YAMLMap.ts' import type { YAMLSeq } from './nodes/YAMLSeq.ts' @@ -146,7 +146,7 @@ function visit_( path = Object.freeze(path.concat(node)) const ck = visit_('key', node.key, visitor, path) if (ck === BREAK) return BREAK - else if (ck === REMOVE) node.key = null + else if (ck === REMOVE) node.key = new Scalar(null) const cv = visit_('value', node.value, visitor, path) if (cv === BREAK) return BREAK else if (cv === REMOVE) node.value = null @@ -245,7 +245,7 @@ async function visitAsync_( path = Object.freeze(path.concat(node)) const ck = await visitAsync_('key', node.key, visitor, path) if (ck === BREAK) return BREAK - else if (ck === REMOVE) node.key = null + else if (ck === REMOVE) node.key = new Scalar(null) const cv = await visitAsync_('value', node.value, visitor, path) if (cv === BREAK) return BREAK else if (cv === REMOVE) node.value = null @@ -319,8 +319,12 @@ function replaceNode( if (isCollection(parent)) { parent.items[key as number] = node } else if (isPair(parent)) { - if (key === 'key') parent.key = node - else parent.value = node + if (isNode(node)) { + if (key === 'key') parent.key = node + else parent.value = node + } else { + throw new Error(`Cannot replace pair ${key} with non-node value`) + } } else if (isDocument(parent)) { parent.contents = node as Node } else { diff --git a/tests/collection-access.ts b/tests/collection-access.ts index ec0ed50b..374c56a3 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -1,7 +1,7 @@ import { Document, Pair, - type Scalar, + Scalar, type YAMLMap, type YAMLOMap, type YAMLSeq, @@ -13,13 +13,7 @@ import { describe('Map', () => { let doc: Document - let map: YAMLMap< - string | Scalar, - | number - | string - | Scalar - | YAMLMap, number | Scalar> - > + let map: YAMLMap> beforeEach(() => { doc = new Document({ a: 1, b: { c: 3, d: 4 } }) map = doc.contents as any @@ -38,11 +32,10 @@ describe('Map', () => { }) test('add', () => { - map.add({ key: 'c', value: 'x' }) + map.add(doc.createPair('c', 'x')) expect(map.get('c')).toBe('x') - // @ts-expect-error TS should complain here - expect(() => map.add('a')).toThrow(/already set/) - expect(() => map.add(new Pair('c', 'y'))).toThrow(/already set/) + map.add(doc.createPair('c', 'y')) + expect(map.get('c')).toBe('y') expect(map.items).toHaveLength(3) }) @@ -58,7 +51,7 @@ describe('Map', () => { expect(map.get('a')).toBe(1) expect(map.get('a', true)).toMatchObject({ value: 1 }) const subMap = map.get('b') - if (isMap(subMap)) { + if (isMap(subMap)) { expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 @@ -73,7 +66,7 @@ describe('Map', () => { expect(map.get(doc.createNode('a'))).toBe(1) expect(map.get(doc.createNode('a'), true)).toMatchObject({ value: 1 }) const subMap = map.get(doc.createNode('b')) - if (isMap(subMap)) { + if (isMap(subMap)) { expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 }) expect(map.get(doc.createNode('c'))).toBeUndefined() } else { @@ -150,17 +143,16 @@ describe('Seq', () => { test('delete', () => { expect(seq.delete(0)).toBe(true) expect(seq.delete(2)).toBe(false) - expect(seq.delete('a')).toBe(false) + expect(() => seq.delete('a' as any)).toThrow(TypeError) expect(seq.get(0)).toMatchObject({ items: [{ value: 2 }, { value: 3 }] }) expect(seq.items).toHaveLength(1) }) - test('get with value', () => { + test('get with integer', () => { expect(seq.get(0)).toBe(1) - expect(seq.get('0')).toBe(1) expect(seq.get(0, true)).toMatchObject({ value: 1 }) const subSeq = seq.get(1) - if (isSeq(subSeq)) { + if (isSeq(subSeq)) { expect(subSeq.toJSON()).toMatchObject([2, 3]) expect(seq.get(2)).toBeUndefined() } else { @@ -168,72 +160,57 @@ describe('Seq', () => { } }) - test('get with node', () => { - expect(seq.get(doc.createNode(0))).toBe(1) - expect(seq.get(doc.createNode('0'))).toBe(1) - expect(seq.get(doc.createNode(0), true)).toMatchObject({ value: 1 }) - const subSeq = seq.get(doc.createNode(1)) - if (isSeq(subSeq)) { - expect(subSeq.toJSON()).toMatchObject([2, 3]) - expect(seq.get(doc.createNode(2))).toBeUndefined() - } else { - throw new Error('not a seq') - } + test('get with non-integer', () => { + expect(() => seq.get(-1)).toThrow(RangeError) + expect(() => seq.get(0.5)).toThrow(TypeError) + expect(() => seq.get('0' as any)).toThrow(TypeError) + expect(() => seq.get(doc.createNode(0) as any)).toThrow(TypeError) }) - test('has with value', () => { + test('has with integer', () => { expect(seq.has(0)).toBe(true) expect(seq.has(1)).toBe(true) expect(seq.has(2)).toBe(false) - expect(seq.has('0')).toBe(true) - expect(seq.has('')).toBe(false) - // @ts-expect-error TS should complain here - expect(seq.has()).toBe(false) }) - test('has with node', () => { - expect(seq.has(doc.createNode(0))).toBe(true) - expect(seq.has(doc.createNode('0'))).toBe(true) - expect(seq.has(doc.createNode(2))).toBe(false) - expect(seq.has(doc.createNode(''))).toBe(false) - // @ts-expect-error TS should complain here - expect(seq.has(doc.createNode())).toBe(false) + test('has with non-integer', () => { + expect(() => seq.has(-1)).toThrow(RangeError) + expect(() => seq.has(0.5)).toThrow(TypeError) + expect(() => seq.has('0' as any)).toThrow(TypeError) + // @ts-expect-error TS complains, as it should. + expect(() => seq.has()).toThrow(TypeError) + expect(() => seq.has(doc.createNode(0) as any)).toThrow(TypeError) }) - test('set with value', () => { + test('set with integer', () => { seq.set(0, 2) expect(seq.get(0)).toBe(2) expect(seq.get(0, true)).toMatchObject({ value: 2 }) - seq.set('1', 5) + seq.set(1, 5) expect(seq.get(1)).toBe(5) seq.set(2, 6) expect(seq.get(2)).toBe(6) expect(seq.items).toHaveLength(3) }) - test('set with node', () => { - seq.set(doc.createNode(0), 2) - expect(seq.get(0)).toBe(2) - expect(seq.get(0, true)).toMatchObject({ value: 2 }) - seq.set(doc.createNode('1'), 5) - expect(seq.get(1)).toBe(5) - seq.set(doc.createNode(2), 6) - expect(seq.get(2)).toBe(6) - expect(seq.items).toHaveLength(3) + test('set with non-integer', () => { + expect(() => seq.set(-1, 2)).toThrow(RangeError) + expect(() => seq.set(0.5, 2)).toThrow(TypeError) + expect(() => seq.set(doc.createNode(0) as any, 2)).toThrow(TypeError) }) }) describe('Set', () => { let doc: Document - let set: YAMLSet | Pair> + let set: YAMLSet beforeEach(() => { doc = new Document(null, { version: '1.1' }) set = doc.createNode([1, 2, 3], { tag: '!!set' }) as any doc.contents = set expect(set.items).toMatchObject([ - { key: { value: 1 }, value: { value: null } }, - { key: { value: 2 }, value: { value: null } }, - { key: { value: 3 }, value: { value: null } } + { key: { value: 1 }, value: null }, + { key: { value: 2 }, value: null }, + { key: { value: 3 }, value: null } ]) }) @@ -241,9 +218,9 @@ describe('Set', () => { set.add('x') expect(set.get('x')).toBe('x') set.add('x') - const y0 = new Pair('y') + const y0 = new Pair(new Scalar('y')) set.add(y0) - set.add(new Pair('y')) + set.add(new Pair(new Scalar('y'))) expect(set.get('y', true)).toBe(y0) expect(set.items).toHaveLength(5) }) @@ -252,7 +229,7 @@ describe('Set', () => { expect(set.get(1)).toBe(1) expect(set.get(1, true)).toMatchObject({ key: { value: 1 }, - value: { value: null } + value: null }) expect(set.get(0)).toBeUndefined() expect(set.get('1')).toBeUndefined() @@ -267,7 +244,7 @@ describe('Set', () => { expect(set.get(4)).toBeUndefined() set.set(4, true) expect(set.get(4)).toBe(4) - expect(set.get(4, true)).toMatchObject({ key: 4, value: null }) + expect(set.get(4, true)).toMatchObject(new Pair(new Scalar(4), null)) expect(set.items).toHaveLength(3) }) }) @@ -296,11 +273,9 @@ describe('OMap', () => { }) test('add', () => { - omap.add({ key: 'c', value: 'x' }) + omap.add(doc.createPair('c', 'x')) expect(omap.get('c')).toBe('x') - // @ts-expect-error TS should complain here - expect(() => omap.add('a')).toThrow(/already set/) - expect(() => omap.add(new Pair('c', 'y'))).toThrow(/already set/) + omap.add(doc.createPair('c', 'y')) expect(omap.items).toHaveLength(3) }) @@ -356,7 +331,7 @@ describe('Collection', () => { test('addIn', () => { map.addIn(['b'], 4) expect(map.getIn(['b', 2])).toBe(4) - map.addIn([], new Pair('c', 5)) + map.addIn([], doc.createPair('c', 5)) expect(map.get('c')).toBe(5) expect(() => map.addIn(['a'], -1)).toThrow(/Expected YAML collection/) map.addIn(['b', 3], 6) @@ -390,7 +365,7 @@ describe('Collection', () => { expect(map.getIn(['a'])).toBe(1) expect(map.getIn(['a'], true)).toMatchObject({ value: 1 }) expect(map.getIn(['b', 1])).toBe(3) - expect(map.getIn(['b', '1'])).toBe(3) + expect(() => map.getIn(['b', '1'])).toThrow(TypeError) expect(map.getIn(['b', 1], true)).toMatchObject({ value: 3 }) expect(map.getIn(['b', 2])).toBeUndefined() expect(map.getIn(['c', 'e'])).toBeUndefined() @@ -400,7 +375,7 @@ describe('Collection', () => { test('hasIn', () => { expect(map.hasIn(['a'])).toBe(true) expect(map.hasIn(['b', 1])).toBe(true) - expect(map.hasIn(['b', '1'])).toBe(true) + expect(() => map.hasIn(['b', '1'])).toThrow(TypeError) expect(map.hasIn(['b', 2])).toBe(false) expect(map.hasIn(['c', 'e'])).toBe(false) expect(map.hasIn(['a', 'e'])).toBe(false) @@ -443,17 +418,15 @@ describe('Document', () => { }) test('add', () => { - doc.add({ key: 'c', value: 'x' }) + doc.add(doc.createPair('c', 'x')) expect(doc.get('c')).toBe('x') - expect(() => doc.add('a')).toThrow(/already set/) - expect(() => doc.add(new Pair('c', 'y'))).toThrow(/already set/) expect(doc.contents?.items).toHaveLength(3) }) test('addIn', () => { doc.addIn(['b'], 4) expect(doc.getIn(['b', 2])).toBe(4) - doc.addIn([], new Pair('c', 5)) + doc.addIn([], doc.createPair('c', 5)) expect(doc.get('c')).toBe(5) expect(() => doc.addIn(['a'], -1)).toThrow(/Expected YAML collection/) doc.addIn(['b', 3], 6) @@ -503,7 +476,7 @@ describe('Document', () => { expect(doc.getIn(['a'], true)).toMatchObject({ value: 1 }) expect(doc.getIn(['b', 1])).toBe(3) expect(doc.getIn(['b', 1], true)).toMatchObject({ value: 3 }) - expect(doc.getIn(['b', 'e'])).toBeUndefined() + expect(() => doc.getIn(['b', 'e'])).toThrow(TypeError) expect(doc.getIn(['c', 'e'])).toBeUndefined() expect(doc.getIn(['a', 'e'])).toBeUndefined() }) @@ -528,7 +501,7 @@ describe('Document', () => { test('hasIn', () => { expect(doc.hasIn(['a'])).toBe(true) expect(doc.hasIn(['b', 1])).toBe(true) - expect(doc.hasIn(['b', 'e'])).toBe(false) + expect(() => doc.hasIn(['b', 'e'])).toThrow(TypeError) expect(doc.hasIn(['c', 'e'])).toBe(false) expect(doc.hasIn(['a', 'e'])).toBe(false) }) diff --git a/tests/doc/comments.ts b/tests/doc/comments.ts index 630afbdc..72c056ae 100644 --- a/tests/doc/comments.ts +++ b/tests/doc/comments.ts @@ -575,9 +575,9 @@ describe('stringify comments', () => { }`) expect(String(doc)).toBe(source` { - a, #c0 + a:, #c0 b: c, #c1 - d #c2 + d: #c2 } `) }) @@ -592,7 +592,7 @@ describe('stringify comments', () => { { ? { a: 1 }, #c0 b: { c: 2 }, #c1 - d #c2 + d: #c2 } `) }) diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 5b070fd8..755a3e79 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -138,7 +138,7 @@ describe('objects', () => { expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toMatchObject([ { key: { value: 'x' }, value: { value: true } }, - { key: { value: 'y' }, value: { value: null } } + { key: { value: 'y' }, value: null } ]) }) diff --git a/tests/doc/parse.ts b/tests/doc/parse.ts index 627b9d91..ec0f9478 100644 --- a/tests/doc/parse.ts +++ b/tests/doc/parse.ts @@ -384,7 +384,7 @@ describe('maps with no values', () => { test('block map', () => { const src = `a: null\n? b #c` const doc = YAML.parseDocument(src) - expect(String(doc)).toBe(`a: null\n? b #c\n`) + expect(String(doc)).toBe(`a: null\nb: #c\n null\n`) doc.set('b', 'x') expect(String(doc)).toBe(`a: null\nb: #c\n x\n`) }) @@ -392,9 +392,9 @@ describe('maps with no values', () => { test('flow map', () => { const src = `{\na: null,\n? b\n}` const doc = YAML.parseDocument(src) - expect(String(doc)).toBe(`{ a: null, b }\n`) + expect(String(doc)).toBe(`{ a: null, b: }\n`) doc.contents.items[1].key.comment = 'c' - expect(String(doc)).toBe(`{\n a: null,\n b #c\n}\n`) + expect(String(doc)).toBe(`{\n a: null,\n b: #c\n}\n`) doc.set('b', 'x') expect(String(doc)).toBe(`{\n a: null,\n b: #c\n x\n}\n`) }) diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 4aba165f..6dc1576a 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -1,6 +1,6 @@ import { source } from '../_utils.ts' import * as YAML from 'yaml' -import { Pair, Scalar } from 'yaml' +import { Scalar } from 'yaml' for (const [name, version] of [ ['YAML 1.1', '1.1'], @@ -345,7 +345,7 @@ z: test('Document as key', () => { const doc = new YAML.Document({ a: 1 }) - doc.add(new YAML.Document({ b: 2, c: 3 })) + doc.set(new YAML.Document({ b: 2, c: 3 }), null) expect(String(doc)).toBe('a: 1\n? b: 2\n c: 3\n') }) @@ -356,7 +356,7 @@ z: describe('No extra whitespace for empty values', () => { const getDoc = () => - new YAML.Document, false>({ + new YAML.Document({ a: null, b: null }) @@ -367,12 +367,14 @@ z: test('Block map, with key.comment', () => { const doc = getDoc() + doc.set('a', new Scalar(null)) doc.contents.items[0].key.comment = 'c' expect(doc.toString({ nullStr: '' })).toBe('a: #c\nb:\n') }) test('Block map, with value.commentBefore', () => { const doc = getDoc() + doc.set('a', new Scalar(null)) doc.get('a', true).commentBefore = 'c' expect(doc.toString({ nullStr: '' })).toBe('a:\n #c\nb:\n') }) @@ -387,12 +389,13 @@ z: const doc = getDoc() doc.contents.flow = true doc.contents.items[0].key.comment = 'c' - expect(doc.toString({ nullStr: '' })).toBe('{\n a: #c\n ,\n b:\n}\n') + expect(doc.toString({ nullStr: '' })).toBe('{\n a:, #c\n b:\n}\n') }) test('Flow map, with value.commentBefore', () => { const doc = getDoc() doc.contents.flow = true + doc.set('a', new Scalar(null)) doc.get('a', true).commentBefore = 'c' expect(doc.toString({ nullStr: '' })).toBe( '{\n a:\n #c\n ,\n b:\n}\n' @@ -721,7 +724,7 @@ describe('scalar styles', () => { describe('simple keys', () => { test('key with no value', () => { const doc = YAML.parseDocument('? ~') - expect(doc.toString()).toBe('? ~\n') + expect(doc.toString()).toBe('~: null\n') expect(doc.toString({ simpleKeys: true })).toBe('~: null\n') }) @@ -815,7 +818,7 @@ describe('sortMapEntries', () => { }) test('doc.add', () => { const doc = new YAML.Document(obj, { sortMapEntries: true }) - doc.add(new Pair('bb', 4)) + doc.add(doc.createPair('bb', 4)) expect(String(doc)).toBe('a: 1\nb: 2\nbb: 4\nc: 3\n') }) test('doc.set', () => { @@ -1050,13 +1053,15 @@ describe('Scalar options', () => { expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('"foo": "bar"\n') }) - test('Use defaultType for explicit keys', () => { + test('Use defaultStringType for explicit keys', () => { const opt = { + blockQuote: false, defaultStringType: Scalar.QUOTE_DOUBLE, defaultKeyType: Scalar.QUOTE_SINGLE } as const const doc = new YAML.Document({ foo: null }) - doc.contents.items[0].value = null + const key = doc.contents.items[0].key as Scalar + key.type = Scalar.BLOCK_LITERAL expect(doc.toString(opt)).toBe('? "foo"\n') }) }) @@ -1442,7 +1447,7 @@ describe('YAML.stringify on ast Document', () => { describe('flow collection padding', () => { const doc = new YAML.Document() doc.contents = new YAML.YAMLSeq() - doc.contents.items = [1, 2] + doc.contents.items = [new Scalar(1), new Scalar(2)] doc.contents.flow = true test('default', () => { diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 4f91a23e..9eecdbd1 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -472,11 +472,11 @@ one: 1 '{ 3: 4 }': 'many' }) expect(doc.errors).toHaveLength(0) - doc.contents.items[2].key = { 3: 4 } + doc.contents.items[2].key = doc.createNode({ 3: 4 }) expect(doc.toJS()).toMatchObject({ one: 1, 2: 'two', - '{"3":4}': 'many' + '{ "3": 4 }': 'many' }) }) @@ -494,12 +494,12 @@ one: 1 ]) ) expect(doc.errors).toHaveLength(0) - doc.contents.items[2].key = { 3: 4 } + doc.contents.items[2].key = doc.createNode({ 5: 6 }) expect(doc.toJS({ mapAsMap: true })).toMatchObject( new Map([ ['one', 1], [2, 'two'], - [{ 3: 4 }, 'many'] + [new Map([[5, 6]]), 'many'] ]) ) }) @@ -1033,12 +1033,12 @@ describe('custom tags', () => { } } - class YAMLNullObject extends YAMLMap { + class YAMLNullObject extends YAMLMap { tag: string = '!nullobject' toJSON(_?: unknown, ctx?: any): any { const obj = super.toJSON>( _, - { ...(ctx || {}), mapAsMap: false }, + { ...ctx, mapAsMap: false }, Object ) return Object.assign(Object.create(null), obj) diff --git a/tests/is-node.ts b/tests/is-node.ts index ed26183f..e79a9ca8 100644 --- a/tests/is-node.ts +++ b/tests/is-node.ts @@ -128,12 +128,12 @@ for (const { fn, exp } of [ }) test('parsed alias', () => { - const doc = parseDocument('[ &a foo, *a ]') + const doc = parseDocument('[ &a foo, *a ]') expect(fn(doc.contents?.items[1])).toBe(exp.alias) }) test('parsed pair', () => { - const doc = parseDocument('foo: bar') + const doc = parseDocument('foo: bar') expect(fn(doc.contents?.items[0])).toBe(exp.pair) }) @@ -158,7 +158,7 @@ for (const { fn, exp } of [ }) test('created pair', () => { - expect(fn(new Pair(null))).toBe(exp.pair) + expect(fn(new Pair(new Scalar(null)))).toBe(exp.pair) }) test('null', () => { From 2fb85c55f6d91b6c8cc5bf206b786eae73ac4315 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 21 Jan 2026 17:13:12 +0800 Subject: [PATCH 3/7] feat!: Always return nodes from document & collection getters --- src/doc/Document.ts | 36 +++-------- src/nodes/Collection.ts | 29 ++++----- src/nodes/YAMLMap.ts | 13 +--- src/nodes/YAMLSeq.ts | 13 +--- src/schema/yaml-1.1/set.ts | 15 ++--- tests/clone.ts | 6 +- tests/collection-access.ts | 125 +++++++++++++++---------------------- tests/directives.ts | 4 +- tests/doc/anchors.ts | 8 +-- tests/doc/parse.ts | 6 +- tests/doc/stringify.ts | 4 +- tests/doc/types.ts | 2 +- tests/node-to-js.ts | 2 +- 13 files changed, 100 insertions(+), 163 deletions(-) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 05a308c6..1cf2b65d 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -5,13 +5,7 @@ import { isEmptyPath, type Primitive } from '../nodes/Collection.ts' -import { - DOC, - isCollection, - isNode, - isScalar, - NODE_TYPE -} from '../nodes/identity.ts' +import { DOC, isCollection, isNode, NODE_TYPE } from '../nodes/identity.ts' import type { Node, NodeBase, @@ -286,32 +280,20 @@ export class Document< } /** - * Returns item at `key`, or `undefined` if not found. By default unwraps - * scalar values from their surrounding node; to disable set `keepScalar` to - * `true` (collections are always returned intact). + * Returns item at `key`, or `undefined` if not found. */ - get(key: any, keepScalar?: boolean): Strict extends true ? unknown : any { - return isCollection(this.contents) - ? this.contents.get(key, keepScalar) - : undefined + get(key: any): Strict extends true ? NodeBase | Pair | undefined : any { + return isCollection(this.contents) ? this.contents.get(key) : undefined } /** - * Returns item at `path`, or `undefined` if not found. By default unwraps - * scalar values from their surrounding node; to disable set `keepScalar` to - * `true` (collections are always returned intact). + * Returns item at `path`, or `undefined` if not found. */ getIn( - path: Iterable | null, - keepScalar?: boolean - ): Strict extends true ? unknown : any { - if (isEmptyPath(path)) - return !keepScalar && isScalar(this.contents) - ? this.contents.value - : this.contents - return isCollection(this.contents) - ? this.contents.getIn(path, keepScalar) - : undefined + path: Iterable | null + ): Strict extends true ? NodeBase | Pair | null | undefined : any { + if (isEmptyPath(path)) return this.contents + return isCollection(this.contents) ? this.contents.getIn(path) : undefined } /** diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index 55054ba8..fda8e916 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,6 +1,6 @@ import { NodeCreator } from '../doc/NodeCreator.ts' import type { Schema } from '../schema/Schema.ts' -import { isCollection, isScalar, NODE_TYPE } from './identity.ts' +import { isCollection, NODE_TYPE } from './identity.ts' import { type Node, NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' @@ -88,11 +88,9 @@ export abstract class Collection extends NodeBase { abstract delete(key: unknown): boolean /** - * Returns item at `key`, or `undefined` if not found. By default unwraps - * scalar values from their surrounding node; to disable set `keepScalar` to - * `true` (collections are always returned intact). + * Returns item at `key`, or `undefined` if not found. */ - abstract get(key: unknown, keepScalar?: boolean): unknown + abstract get(key: unknown): NodeBase | Pair | undefined /** * Checks if the collection includes a value with the key `key`. @@ -113,7 +111,7 @@ export abstract class Collection extends NodeBase { if (isEmptyPath(path)) this.add(value) else { const [key, ...rest] = path - const node = this.get(key, true) + const node = this.get(key) if (isCollection(node)) node.addIn(rest, value) else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value)) @@ -132,7 +130,7 @@ export abstract class Collection extends NodeBase { deleteIn(path: Iterable): boolean { const [key, ...rest] = path if (rest.length === 0) return this.delete(key) - const node = this.get(key, true) + const node = this.get(key) if (isCollection(node)) return node.deleteIn(rest) else throw new Error( @@ -141,16 +139,13 @@ export abstract class Collection extends NodeBase { } /** - * Returns item at `key`, or `undefined` if not found. By default unwraps - * scalar values from their surrounding node; to disable set `keepScalar` to - * `true` (collections are always returned intact). + * Returns item at `key`, or `undefined` if not found. */ - getIn(path: Iterable, keepScalar?: boolean): unknown { + getIn(path: Iterable): NodeBase | Pair | undefined { const [key, ...rest] = path - const node = this.get(key, true) - if (rest.length === 0) - return !keepScalar && isScalar(node) ? node.value : node - else return isCollection(node) ? node.getIn(rest, keepScalar) : undefined + const node = this.get(key) + if (rest.length === 0) return node + else return isCollection(node) ? node.getIn(rest) : undefined } /** @@ -159,7 +154,7 @@ export abstract class Collection extends NodeBase { hasIn(path: Iterable): boolean { const [key, ...rest] = path if (rest.length === 0) return this.has(key) - const node = this.get(key, true) + const node = this.get(key) return isCollection(node) ? node.hasIn(rest) : false } @@ -171,7 +166,7 @@ export abstract class Collection extends NodeBase { if (rest.length === 0) { this.set(key, value) } else { - const node = this.get(key, true) + const node = this.get(key) if (isCollection(node)) node.setIn(rest, value) else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value)) diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index ee80be99..5e54c442 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -5,11 +5,10 @@ import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' import { NodeCreator } from '../doc/NodeCreator.ts' import { addPairToJSMap } from './addPairToJSMap.ts' -import { Collection, type Primitive } from './Collection.ts' +import { Collection, type NodeOf, type Primitive } from './Collection.ts' import { isNode, isPair, isScalar, MAP } from './identity.ts' import type { NodeBase } from './Node.ts' import { Pair } from './Pair.ts' -import type { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' export type MapLike = @@ -99,15 +98,9 @@ export class YAMLMap< return del.length > 0 } - get(key: unknown, keepScalar: true): Scalar | undefined - get(key: unknown, keepScalar?: false): V | undefined - get(key: unknown, keepScalar?: boolean): V | Scalar | undefined - get(key: unknown, keepScalar?: boolean): V | Scalar | undefined { + get(key: unknown): NodeOf | undefined { const it = findPair(this.items, key) - const node = it?.value - return ( - (!keepScalar && isScalar(node) ? (node.value as V) : node) ?? undefined - ) + return it?.value ?? undefined } has(key: unknown): boolean { diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index f96c0b8b..a93dcc3c 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -8,7 +8,6 @@ import { Collection, type NodeOf, type Primitive } from './Collection.ts' import { isNode, isScalar, SEQ } from './identity.ts' import type { NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' -import type { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' @@ -61,21 +60,15 @@ export class YAMLSeq< } /** - * Returns item at `key`, or `undefined` if not found. By default unwraps - * scalar values from their surrounding node; to disable set `keepScalar` to - * `true` (collections are always returned intact). + * Returns item at `key`, or `undefined` if not found. * * Throws if `idx` is not a non-negative integer. */ - get(idx: number, keepScalar: true): Scalar | undefined - get(idx: number, keepScalar?: false): T | undefined - get(idx: number, keepScalar?: boolean): T | Scalar | undefined - get(idx: number, keepScalar?: boolean): T | Scalar | undefined { + get(idx: number): NodeOf | undefined { if (!Number.isInteger(idx)) throw new TypeError(`Expected an integer, not ${JSON.stringify(idx)}.`) if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) - const it = this.items[idx] - return !keepScalar && isScalar(it) ? (it.value as T) : it + return this.items[idx] } /** diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index d72c3627..1140b1db 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -12,7 +12,7 @@ import type { CollectionTag } from '../types.ts' export class YAMLSet< T extends Primitive | NodeBase = Primitive | NodeBase -> extends YAMLMap { +> extends YAMLMap { static tag = 'tag:yaml.org,2002:set' constructor(schema?: Schema) { @@ -37,21 +37,16 @@ export class YAMLSet< throw new TypeError('set pair values must be null') } else { const prev = findPair(this.items, value.key) - if (!prev) this.items.push(value as Pair) + if (!prev) this.items.push(value as Pair) } } /** - * If `keepPair` is `true`, returns the Pair matching `key`. - * Otherwise, returns the value of that Pair's key. + * Returns the value matching `key`. */ - get(key: unknown, keepPair?: boolean): any { + get(key: unknown): NodeOf | undefined { const pair = findPair(this.items, key) - return !keepPair && isPair(pair) - ? isScalar(pair.key) - ? pair.key.value - : pair.key - : pair + return pair?.key } /** diff --git a/tests/clone.ts b/tests/clone.ts index 63dfb11a..720e2476 100644 --- a/tests/clone.ts +++ b/tests/clone.ts @@ -1,4 +1,4 @@ -import type { Scalar, YAMLMap } from 'yaml' +import { Scalar, type YAMLMap } from 'yaml' import { isAlias, isScalar, parseDocument, visit } from 'yaml' import { source } from './_utils.ts' @@ -33,8 +33,8 @@ describe('doc.clone()', () => { const doc = parseDocument('foo: bar') const copy = doc.clone() copy.set('foo', 'fizz') - expect(doc.get('foo')).toBe('bar') - expect(copy.get('foo')).toBe('fizz') + expect(doc.get('foo')).toMatchObject(new Scalar('bar')) + expect(copy.get('foo')).toMatchObject(new Scalar('fizz')) }) test('has separate directives from original', () => { diff --git a/tests/collection-access.ts b/tests/collection-access.ts index 374c56a3..9b338527 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -33,9 +33,9 @@ describe('Map', () => { test('add', () => { map.add(doc.createPair('c', 'x')) - expect(map.get('c')).toBe('x') + expect(map.get('c')).toMatchObject({ value: 'x' }) map.add(doc.createPair('c', 'y')) - expect(map.get('c')).toBe('y') + expect(map.get('c')).toMatchObject({ value: 'y' }) expect(map.items).toHaveLength(3) }) @@ -48,8 +48,7 @@ describe('Map', () => { }) test('get with value', () => { - expect(map.get('a')).toBe(1) - expect(map.get('a', true)).toMatchObject({ value: 1 }) + expect(map.get('a')).toMatchObject({ value: 1 }) const subMap = map.get('b') if (isMap(subMap)) { expect(subMap.toJSON()).toMatchObject({ @@ -63,8 +62,7 @@ describe('Map', () => { }) test('get with node', () => { - expect(map.get(doc.createNode('a'))).toBe(1) - expect(map.get(doc.createNode('a'), true)).toMatchObject({ value: 1 }) + expect(map.get(doc.createNode('a'))).toMatchObject({ value: 1 }) const subMap = map.get(doc.createNode('b')) if (isMap(subMap)) { expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 }) @@ -93,30 +91,28 @@ describe('Map', () => { test('set with value', () => { map.set('a', 2) - expect(map.get('a')).toBe(2) - expect(map.get('a', true)).toMatchObject({ value: 2 }) + expect(map.get('a')).toMatchObject({ value: 2 }) map.set('b', 5) - expect(map.get('b')).toBe(5) + expect(map.get('b')).toMatchObject({ value: 5 }) map.set('c', 6) - expect(map.get('c')).toBe(6) + expect(map.get('c')).toMatchObject({ value: 6 }) expect(map.items).toHaveLength(3) }) test('set with node', () => { map.set(doc.createNode('a'), 2) - expect(map.get('a')).toBe(2) - expect(map.get('a', true)).toMatchObject({ value: 2 }) + expect(map.get('a')).toMatchObject({ value: 2 }) map.set(doc.createNode('b'), 5) - expect(map.get('b')).toBe(5) + expect(map.get('b')).toMatchObject({ value: 5 }) map.set(doc.createNode('c'), 6) - expect(map.get('c')).toBe(6) + expect(map.get('c')).toMatchObject({ value: 6 }) expect(map.items).toHaveLength(3) }) test('set scalar node with anchor', () => { doc = parseDocument('a: &A value\nb: *A\n') doc.set('a', 'foo') - expect(doc.get('a', true)).toMatchObject({ value: 'foo' }) + expect(doc.get('a')).toMatchObject({ value: 'foo' }) expect(String(doc)).toBe('a: &A foo\nb: *A\n') }) }) @@ -135,7 +131,7 @@ describe('Seq', () => { test('add', () => { seq.add(9) - expect(seq.get(2)).toBe(9) + expect(seq.get(2)).toMatchObject({ value: 9 }) seq.add(1) expect(seq.items).toHaveLength(4) }) @@ -149,8 +145,7 @@ describe('Seq', () => { }) test('get with integer', () => { - expect(seq.get(0)).toBe(1) - expect(seq.get(0, true)).toMatchObject({ value: 1 }) + expect(seq.get(0)).toMatchObject({ value: 1 }) const subSeq = seq.get(1) if (isSeq(subSeq)) { expect(subSeq.toJSON()).toMatchObject([2, 3]) @@ -184,12 +179,11 @@ describe('Seq', () => { test('set with integer', () => { seq.set(0, 2) - expect(seq.get(0)).toBe(2) - expect(seq.get(0, true)).toMatchObject({ value: 2 }) + expect(seq.get(0)).toMatchObject({ value: 2 }) seq.set(1, 5) - expect(seq.get(1)).toBe(5) + expect(seq.get(1)).toMatchObject({ value: 5 }) seq.set(2, 6) - expect(seq.get(2)).toBe(6) + expect(seq.get(2)).toMatchObject({ value: 6 }) expect(seq.items).toHaveLength(3) }) @@ -216,35 +210,30 @@ describe('Set', () => { test('add', () => { set.add('x') - expect(set.get('x')).toBe('x') + expect(set.get('x')).toMatchObject({ value: 'x' }) set.add('x') - const y0 = new Pair(new Scalar('y')) - set.add(y0) + const y0 = new Scalar('y') + set.add(new Pair(y0)) set.add(new Pair(new Scalar('y'))) - expect(set.get('y', true)).toBe(y0) + expect(set.get('y')).toBe(y0) expect(set.items).toHaveLength(5) }) test('get', () => { - expect(set.get(1)).toBe(1) - expect(set.get(1, true)).toMatchObject({ - key: { value: 1 }, - value: null - }) + expect(set.get(1)).toMatchObject({ value: 1 }) expect(set.get(0)).toBeUndefined() expect(set.get('1')).toBeUndefined() }) test('set', () => { set.set(1, true) - expect(set.get(1)).toBe(1) + expect(set.get(1)).toMatchObject({ value: 1 }) set.set(1, false) expect(set.get(1)).toBeUndefined() set.set(4, false) expect(set.get(4)).toBeUndefined() set.set(4, true) - expect(set.get(4)).toBe(4) - expect(set.get(4, true)).toMatchObject(new Pair(new Scalar(4), null)) + expect(set.get(4)).toMatchObject(new Scalar(4)) expect(set.items).toHaveLength(3) }) }) @@ -274,7 +263,7 @@ describe('OMap', () => { test('add', () => { omap.add(doc.createPair('c', 'x')) - expect(omap.get('c')).toBe('x') + expect(omap.get('c')).toMatchObject({ value: 'x' }) omap.add(doc.createPair('c', 'y')) expect(omap.items).toHaveLength(3) }) @@ -288,8 +277,7 @@ describe('OMap', () => { }) test('get', () => { - expect(omap.get('a')).toBe(1) - expect(omap.get('a', true)).toMatchObject({ value: 1 }) + expect(omap.get('a')).toMatchObject({ value: 1 }) const subMap = omap.get('b') if (isMap(subMap)) { expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 }) @@ -310,12 +298,11 @@ describe('OMap', () => { test('set', () => { omap.set('a', 2) - expect(omap.get('a')).toBe(2) - expect(omap.get('a', true)).toMatchObject({ value: 2 }) + expect(omap.get('a')).toMatchObject({ value: 2 }) omap.set('b', 5) - expect(omap.get('b')).toBe(5) + expect(omap.get('b')).toMatchObject({ value: 5 }) omap.set('c', 6) - expect(omap.get('c')).toBe(6) + expect(omap.get('c')).toMatchObject({ value: 6 }) expect(omap.items).toHaveLength(3) }) }) @@ -330,9 +317,9 @@ describe('Collection', () => { test('addIn', () => { map.addIn(['b'], 4) - expect(map.getIn(['b', 2])).toBe(4) + expect(map.getIn(['b', 2])).toMatchObject({ value: 4 }) map.addIn([], doc.createPair('c', 5)) - expect(map.get('c')).toBe(5) + expect(map.get('c')).toMatchObject({ value: 5 }) expect(() => map.addIn(['a'], -1)).toThrow(/Expected YAML collection/) map.addIn(['b', 3], 6) expect(map.items).toHaveLength(3) @@ -362,11 +349,9 @@ describe('Collection', () => { }) test('getIn', () => { - expect(map.getIn(['a'])).toBe(1) - expect(map.getIn(['a'], true)).toMatchObject({ value: 1 }) - expect(map.getIn(['b', 1])).toBe(3) + expect(map.getIn(['a'])).toMatchObject({ value: 1 }) + expect(map.getIn(['b', 1])).toMatchObject({ value: 3 }) expect(() => map.getIn(['b', '1'])).toThrow(TypeError) - expect(map.getIn(['b', 1], true)).toMatchObject({ value: 3 }) expect(map.getIn(['b', 2])).toBeUndefined() expect(map.getIn(['c', 'e'])).toBeUndefined() expect(map.getIn(['a', 'e'])).toBeUndefined() @@ -383,16 +368,15 @@ describe('Collection', () => { test('setIn', () => { map.setIn(['a'], 2) - expect(map.get('a')).toBe(2) - expect(map.get('a', true)).toMatchObject({ value: 2 }) + expect(map.get('a')).toMatchObject({ value: 2 }) map.setIn(['b', 1], 5) - expect(map.getIn(['b', 1])).toBe(5) + expect(map.getIn(['b', 1])).toMatchObject({ value: 5 }) map.setIn([1], 6) - expect(map.get(1)).toBe(6) + expect(map.get(1)).toMatchObject({ value: 6 }) map.setIn(['b', 2], 6) - expect(map.getIn(['b', 2])).toBe(6) + expect(map.getIn(['b', 2])).toMatchObject({ value: 6 }) map.setIn(['e', 'e'], 7) - expect(map.getIn(['e', 'e'])).toBe(7) + expect(map.getIn(['e', 'e'])).toMatchObject({ value: 7 }) expect(() => map.setIn(['a', 'e'], 8)).toThrow(/Expected YAML collection/) expect(map.items).toHaveLength(4) const subSeq = map.getIn(['b']) @@ -419,15 +403,15 @@ describe('Document', () => { test('add', () => { doc.add(doc.createPair('c', 'x')) - expect(doc.get('c')).toBe('x') + expect(doc.get('c')).toMatchObject({ value: 'x' }) expect(doc.contents?.items).toHaveLength(3) }) test('addIn', () => { doc.addIn(['b'], 4) - expect(doc.getIn(['b', 2])).toBe(4) + expect(doc.getIn(['b', 2])).toMatchObject({ value: 4 }) doc.addIn([], doc.createPair('c', 5)) - expect(doc.get('c')).toBe(5) + expect(doc.get('c')).toMatchObject({ value: 5 }) expect(() => doc.addIn(['a'], -1)).toThrow(/Expected YAML collection/) doc.addIn(['b', 3], 6) expect(doc.contents?.items).toHaveLength(3) @@ -461,8 +445,7 @@ describe('Document', () => { }) test('get', () => { - expect(doc.get('a')).toBe(1) - expect(doc.get('a', true)).toMatchObject({ value: 1 }) + expect(doc.get('a')).toMatchObject({ value: 1 }) expect(doc.get('c')).toBeUndefined() }) @@ -472,10 +455,8 @@ describe('Document', () => { }) test('getIn collection', () => { - expect(doc.getIn(['a'])).toBe(1) - expect(doc.getIn(['a'], true)).toMatchObject({ value: 1 }) - expect(doc.getIn(['b', 1])).toBe(3) - expect(doc.getIn(['b', 1], true)).toMatchObject({ value: 3 }) + expect(doc.getIn(['a'])).toMatchObject({ value: 1 }) + expect(doc.getIn(['b', 1])).toMatchObject({ value: 3 }) expect(() => doc.getIn(['b', 'e'])).toThrow(TypeError) expect(doc.getIn(['c', 'e'])).toBeUndefined() expect(doc.getIn(['a', 'e'])).toBeUndefined() @@ -483,8 +464,8 @@ describe('Document', () => { test('getIn scalar', () => { const doc = new Document('s') - expect(doc.getIn([])).toBe('s') - expect(doc.getIn(null, true)).toMatchObject({ value: 's' }) + expect(doc.getIn([])).toMatchObject({ value: 's' }) + expect(doc.getIn(null)).toMatchObject({ value: 's' }) expect(doc.getIn([0])).toBeUndefined() }) @@ -508,10 +489,9 @@ describe('Document', () => { test('set', () => { doc.set('a', 2) - expect(doc.get('a')).toBe(2) - expect(doc.get('a', true)).toMatchObject({ value: 2 }) + expect(doc.get('a')).toMatchObject({ value: 2 }) doc.set('c', 6) - expect(doc.get('c')).toBe(6) + expect(doc.get('c')).toMatchObject({ value: 6 }) expect(doc.contents?.items).toHaveLength(3) }) @@ -523,19 +503,18 @@ describe('Document', () => { test('set on empty document', () => { doc.contents = null doc.set('a', 1) - expect(doc.get('a')).toBe(1) + expect(doc.get('a')).toMatchObject({ value: 1 }) }) test('setIn', () => { doc.setIn(['a'], 2) - expect(doc.getIn(['a'])).toBe(2) - expect(doc.getIn(['a'], true)).toMatchObject({ value: 2 }) + expect(doc.getIn(['a'])).toMatchObject({ value: 2 }) doc.setIn(['b', 1], 5) - expect(doc.getIn(['b', 1])).toBe(5) + expect(doc.getIn(['b', 1])).toMatchObject({ value: 5 }) doc.setIn(['c'], 6) - expect(doc.getIn(['c'])).toBe(6) + expect(doc.getIn(['c'])).toMatchObject({ value: 6 }) doc.setIn(['e', 1, 'e'], 7) - expect(doc.getIn(['e', 1, 'e'])).toBe(7) + expect(doc.getIn(['e', 1, 'e'])).toMatchObject({ value: 7 }) expect(() => doc.setIn(['a', 'e'], 8)).toThrow(/Expected YAML collection/) expect(doc.contents?.items).toHaveLength(4) expect((doc.get('b') as any).items).toHaveLength(2) diff --git a/tests/directives.ts b/tests/directives.ts index 058875ec..e4bcc944 100644 --- a/tests/directives.ts +++ b/tests/directives.ts @@ -49,8 +49,8 @@ describe('%TAG', () => { test('create & stringify', () => { const doc = parseDocument('[ v1, v2 ]\n') - ;(doc.get(0, true) as Scalar).tag = '!foo:foo' - ;(doc.get(1, true) as Scalar).tag = '!bar:bar' + ;(doc.get(0) as Scalar).tag = '!foo:foo' + ;(doc.get(1) as Scalar).tag = '!bar:bar' doc.directives.tags['!'] = '!foo:' doc.directives.tags['!bar!'] = '!bar:' expect(String(doc)).toBe(source` diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index 20bd259d..da783ec7 100644 --- a/tests/doc/anchors.ts +++ b/tests/doc/anchors.ts @@ -60,9 +60,9 @@ describe('create', () => { test('node.anchor', () => { const doc = parseDocument('[{ a: A }, { b: B }]') doc.get(0).anchor = 'AA' - doc.getIn([0, 'a'], true).anchor = 'a' - doc.getIn([0, 'a'], true).anchor = '' - doc.getIn([1, 'b'], true).anchor = 'BB' + doc.getIn([0, 'a']).anchor = 'a' + doc.getIn([0, 'a']).anchor = '' + doc.getIn([1, 'b']).anchor = 'BB' expect(String(doc)).toBe('[ &AA { a: A }, { b: &BB B } ]\n') }) @@ -340,7 +340,7 @@ describe('merge <<', () => { const doc = parseDocument('[{ a: A }, { b: B }]', { merge: true }) - const alias = doc.createAlias(doc.getIn([0, 'a'], true)) + const alias = doc.createAlias(doc.getIn([0, 'a'])) doc.addIn([1], doc.createPair('<<', alias)) expect(String(doc)).toBe('[ { a: &a1 A }, { b: B, <<: *a1 } ]\n') expect(() => doc.toJS()).toThrow( diff --git a/tests/doc/parse.ts b/tests/doc/parse.ts index ec0f9478..a2b602b7 100644 --- a/tests/doc/parse.ts +++ b/tests/doc/parse.ts @@ -693,7 +693,7 @@ describe('keepSourceTokens', () => { const doc = YAML.parseDocument(src) expect(doc.contents).not.toHaveProperty('srcToken') expect(doc.contents.items[0]).not.toHaveProperty('srcToken') - expect(doc.get('foo', true)).not.toHaveProperty('srcToken') + expect(doc.get('foo')).not.toHaveProperty('srcToken') }) test(`${type}: included when set`, () => { @@ -705,7 +705,7 @@ describe('keepSourceTokens', () => { key: { type: 'scalar' }, value: { type: 'scalar' } }) - expect(doc.get('foo', true).srcToken).toMatchObject({ type: 'scalar' }) + expect(doc.get('foo').srcToken).toMatchObject({ type: 'scalar' }) }) } @@ -716,7 +716,7 @@ describe('keepSourceTokens', () => { keepSourceTokens: true }).compose(tokens) const doc = Array.from(docs)[0] - const node = doc.get('foo', true) + const node = doc.get('foo') YAML.CST.setScalarValue(node.srcToken, 'eek') const res = tokens.map(YAML.CST.stringify).join('') expect(res).toBe('foo:\n eek') diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 6dc1576a..3cc56047 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -375,7 +375,7 @@ z: test('Block map, with value.commentBefore', () => { const doc = getDoc() doc.set('a', new Scalar(null)) - doc.get('a', true).commentBefore = 'c' + doc.get('a').commentBefore = 'c' expect(doc.toString({ nullStr: '' })).toBe('a:\n #c\nb:\n') }) @@ -396,7 +396,7 @@ z: const doc = getDoc() doc.contents.flow = true doc.set('a', new Scalar(null)) - doc.get('a', true).commentBefore = 'c' + doc.get('a').commentBefore = 'c' expect(doc.toString({ nullStr: '' })).toBe( '{\n a:\n #c\n ,\n b:\n}\n' ) diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 9eecdbd1..3da9cb8d 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -268,7 +268,7 @@ describe('failsafe schema', () => { test('empty scalars (#616)', () => { const doc = parseDocument('empty:\n', { schema: 'failsafe' }) expect(doc.errors).toMatchObject([]) - expect(doc.get('empty', true)).toMatchObject({ value: '' }) + expect(doc.get('empty')).toMatchObject({ value: '' }) expect(doc.toJS()).toMatchObject({ empty: '' }) expect(doc.toString()).toEqual('empty:\n') }) diff --git a/tests/node-to-js.ts b/tests/node-to-js.ts index 93ff7ad7..29ab1915 100644 --- a/tests/node-to-js.ts +++ b/tests/node-to-js.ts @@ -10,7 +10,7 @@ describe('scalars', () => { test('plain in map', () => { const doc = parseDocument('key: 42') - expect(doc.get('key', true).toJS(doc)).toBe(42) + expect(doc.get('key').toJS(doc)).toBe(42) }) }) From 4037aa8c26ff972f2e23314877920c8d7d83ae7b Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 28 Jan 2026 16:00:33 +0800 Subject: [PATCH 4/7] feat!: Drop custom node type structure, prefer instanceof checks --- README.md | 17 +-- docs/01_intro.md | 10 +- docs/05_content_nodes.md | 45 ------- src/compose/compose-collection.ts | 5 +- src/compose/compose-node.ts | 5 +- src/compose/compose-scalar.ts | 15 +-- src/compose/composer.ts | 7 +- src/compose/resolve-flow-collection.ts | 3 +- src/compose/util-map-includes.ts | 5 +- src/doc/Document.ts | 44 +++---- src/doc/NodeCreator.ts | 37 +++--- src/doc/directives.ts | 6 +- src/index.ts | 10 -- src/nodes/Alias.ts | 15 ++- src/nodes/Collection.ts | 20 ++- src/nodes/Node.ts | 10 +- src/nodes/Pair.ts | 5 - src/nodes/Scalar.ts | 3 +- src/nodes/YAMLMap.ts | 40 +++--- src/nodes/YAMLSeq.ts | 15 +-- src/nodes/addPairToJSMap.ts | 6 +- src/nodes/identity.ts | 58 -------- src/nodes/toJS.ts | 14 +- src/public-api.ts | 3 +- src/schema/Schema.ts | 13 +- src/schema/common/map.ts | 3 +- src/schema/common/seq.ts | 3 +- src/schema/yaml-1.1/merge.ts | 18 +-- src/schema/yaml-1.1/omap.ts | 8 +- src/schema/yaml-1.1/pairs.ts | 9 +- src/schema/yaml-1.1/set.ts | 12 +- src/stringify/stringify.ts | 35 +++-- src/stringify/stringifyCollection.ts | 17 +-- src/stringify/stringifyDocument.ts | 5 +- src/stringify/stringifyPair.ts | 21 +-- src/test-events.ts | 37 +++--- src/visit.ts | 58 ++++---- tests/clone.ts | 8 +- tests/collection-access.ts | 69 ++++------ tests/doc/createNode.ts | 9 ++ tests/is-node.ts | 176 ------------------------- tests/node-to-js.ts | 5 - tests/visit.ts | 14 +- 43 files changed, 286 insertions(+), 632 deletions(-) delete mode 100644 src/nodes/identity.ts delete mode 100644 tests/is-node.ts diff --git a/README.md b/README.md index 5914c993..647fddbc 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,6 @@ parse(file) - [`#directives`](https://eemeli.org/yaml/#stream-directives) - [`#errors`](https://eemeli.org/yaml/#errors) - [`#warnings`](https://eemeli.org/yaml/#errors) -- [`isDocument(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) - [`parseAllDocuments(str, options?): Document[]`](https://eemeli.org/yaml/#parsing-documents) - [`parseDocument(str, options?): Document`](https://eemeli.org/yaml/#parsing-documents) @@ -109,7 +108,6 @@ parse(file) ```js import { Document, - isDocument, parseAllDocuments, parseDocument } from 'yaml' @@ -117,13 +115,6 @@ import { ### Content Nodes -- [`isAlias(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) -- [`isCollection(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) -- [`isMap(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) -- [`isNode(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) -- [`isPair(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) -- [`isScalar(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) -- [`isSeq(foo): boolean`](https://eemeli.org/yaml/#identifying-node-types) - [`new Scalar(value)`](https://eemeli.org/yaml/#scalar-values) - [`new YAMLMap()`](https://eemeli.org/yaml/#collections) - [`new YAMLSeq()`](https://eemeli.org/yaml/#collections) @@ -136,9 +127,11 @@ import { ```js import { - isAlias, isCollection, isMap, isNode, - isPair, isScalar, isSeq, Scalar, - visit, visitAsync, YAMLMap, YAMLSeq + Scalar, + visit, + visitAsync, + YAMLMap, + YAMLSeq } from 'yaml' ``` diff --git a/docs/01_intro.md b/docs/01_intro.md index 319468b4..371fa501 100644 --- a/docs/01_intro.md +++ b/docs/01_intro.md @@ -62,7 +62,6 @@ import { parse, stringify } from 'yaml' ```js import { Document, - isDocument, parseAllDocuments, parseDocument } from 'yaml' @@ -82,13 +81,14 @@ import { ```js import { - isAlias, isCollection, isMap, isNode, - isPair, isScalar, isSeq, Scalar, - visit, visitAsync, YAMLMap, YAMLSeq + Scalar, + visit, + visitAsync, + YAMLMap, + YAMLSeq } from 'yaml' ``` -- [`is*(foo): boolean`](#identifying-node-types) - [`new Scalar(value)`](#scalar-values) - [`new YAMLMap()`](#collections) - [`new YAMLSeq()`](#collections) diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index a1d9c749..b559c04a 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -332,51 +332,6 @@ The same as `visit()`, but allows for visitor functions that return a promise which resolves to one of the above-defined control values. -## Identifying Node Types - -```js -import { - isAlias, - isCollection, // map or seq - isDocument, - isMap, - isNode, // alias, scalar, map or seq - isPair, - isScalar, - isSeq -} from 'yaml' - -const doc = new Document({ foo: [13, 42] }) -isDocument(doc) === true -isNode(doc) === false -isMap(doc.contents) === true -isNode(doc.contents) === true -isPair(doc.contents.items[0]) === true -isCollection(doc.get('foo')) === true -isScalar(doc.getIn(['foo', 1])) === true -``` - -#### `isAlias(x: unknown): boolean` - -#### `isCollection(x: unknown): boolean` - -#### `isDocument(x: unknown): boolean` - -#### `isMap(x: unknown): boolean` - -#### `isNode(x: unknown): boolean` - -#### `isPair(x: unknown): boolean` - -#### `isScalar(x: unknown): boolean` - -#### `isSeq(x: unknown): boolean` - -To find out what you've got, a family of custom type guard functions is provided. -These should be preferred over other methods such as `instanceof` checks, as they'll work even if the nodes have been created by a different instance of the library. - -Internally, node identification uses property symbols that are set on instances during their construction. - ## Comments and Blank Lines ```js diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index 21c2b1ea..f71619e5 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -1,5 +1,4 @@ -import { isNode } from '../nodes/identity.ts' -import type { ParsedNode } from '../nodes/Node.ts' +import { NodeBase, type ParsedNode } from '../nodes/Node.ts' import { Scalar } from '../nodes/Scalar.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' import { YAMLSeq } from '../nodes/YAMLSeq.ts' @@ -136,7 +135,7 @@ export function composeCollection( ctx.options ) ?? coll - const node = isNode(res) ? (res as ParsedNode) : new Scalar(res) + const node = res instanceof NodeBase ? (res as ParsedNode) : new Scalar(res) node.range = coll.range node.tag = tagName if (tag?.format) (node as Scalar).format = tag.format diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index b1978bc4..3b0850be 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -1,8 +1,7 @@ import type { Directives } from '../doc/directives.ts' import { Alias } from '../nodes/Alias.ts' -import { isScalar } from '../nodes/identity.ts' import type { ParsedNode } from '../nodes/Node.ts' -import type { Scalar } from '../nodes/Scalar.ts' +import { Scalar } from '../nodes/Scalar.ts' import type { ParseOptions } from '../options.ts' import type { FlowScalar, SourceToken, Token } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' @@ -90,7 +89,7 @@ export function composeNode( if ( atKey && ctx.options.stringKeys && - (!isScalar(node) || + (!(node instanceof Scalar) || typeof node.value !== 'string' || (node.tag && node.tag !== 'tag:yaml.org,2002:str')) ) { diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index cfa1d95b..1d44f7b2 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -1,4 +1,3 @@ -import { isScalar, SCALAR } from '../nodes/identity.ts' import { Scalar } from '../nodes/Scalar.ts' import type { BlockScalar, FlowScalar, SourceToken } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' @@ -27,12 +26,12 @@ export function composeScalar( let tag: ScalarTag if (ctx.options.stringKeys && ctx.atKey) { - tag = ctx.schema[SCALAR] + tag = ctx.schema.scalar } else if (tagName) tag = findScalarTagByName(ctx.schema, value, tagName, tagToken!, onError) else if (token.type === 'scalar') tag = findScalarTagByTest(ctx, value, token, onError) - else tag = ctx.schema[SCALAR] + else tag = ctx.schema.scalar let scalar: Scalar try { @@ -41,7 +40,7 @@ export function composeScalar( msg => onError(tagToken ?? token, 'TAG_RESOLVE_FAILED', msg), ctx.options ) - scalar = isScalar(res) ? res : new Scalar(res) + scalar = res instanceof Scalar ? res : new Scalar(res) } catch (error) { const msg = error instanceof Error ? error.message : String(error) onError(tagToken ?? token, 'TAG_RESOLVE_FAILED', msg) @@ -64,7 +63,7 @@ function findScalarTagByName( tagToken: SourceToken, onError: ComposeErrorHandler ) { - if (tagName === '!') return schema[SCALAR] // non-specific tag + if (tagName === '!') return schema.scalar // non-specific tag const matchWithTest: ScalarTag[] = [] for (const tag of schema.tags) { if (!tag.collection && tag.tag === tagName) { @@ -86,7 +85,7 @@ function findScalarTagByName( `Unresolved tag: ${tagName}`, tagName !== 'tag:yaml.org,2002:str' ) - return schema[SCALAR] + return schema.scalar } function findScalarTagByTest( @@ -100,12 +99,12 @@ function findScalarTagByTest( tag => (tag.default === true || (atKey && tag.default === 'key')) && tag.test?.test(value) - ) as ScalarTag) || schema[SCALAR] + ) as ScalarTag) || schema.scalar if (schema.compat) { const compat = schema.compat.find(tag => tag.default && tag.test?.test(value)) ?? - schema[SCALAR] + schema.scalar if (tag.tag !== compat.tag) { const ts = directives.tagString(tag.tag) const cs = directives.tagString(compat.tag) diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 5d3c7062..a19437c2 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -2,8 +2,9 @@ import { Directives } from '../doc/directives.ts' import { Document } from '../doc/Document.ts' import type { ErrorCode } from '../errors.ts' import { YAMLParseError, YAMLWarning } from '../errors.ts' -import { isCollection, isPair } from '../nodes/identity.ts' +import { Collection } from '../nodes/Collection.ts' import type { ParsedNode, Range } from '../nodes/Node.ts' +import { Pair } from '../nodes/Pair.ts' import type { DocumentOptions, ParseOptions, @@ -110,9 +111,9 @@ export class Composer< doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment } else if (afterEmptyLine || doc.directives.docStart || !dc) { doc.commentBefore = comment - } else if (isCollection(dc) && !dc.flow && dc.items.length > 0) { + } else if (dc instanceof Collection && !dc.flow && dc.items.length > 0) { let it = dc.items[0] - if (isPair(it)) it = it.key + if (it instanceof Pair) it = it.key const cb = it.commentBefore it.commentBefore = cb ? `${comment}\n${cb}` : comment } else { diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index 42cce319..cecdc280 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -1,4 +1,3 @@ -import { isPair } from '../nodes/identity.ts' import { Pair } from '../nodes/Pair.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' import { YAMLSeq } from '../nodes/YAMLSeq.ts' @@ -95,7 +94,7 @@ export function resolveFlowCollection( } if (prevItemComment) { let prev = coll.items[coll.items.length - 1] - if (isPair(prev)) prev = prev.value ?? prev.key + if (prev instanceof Pair) prev = prev.value ?? prev.key if (prev.comment) prev.comment += '\n' + prevItemComment else prev.comment = prevItemComment props.comment = props.comment.substring(prevItemComment.length + 1) diff --git a/src/compose/util-map-includes.ts b/src/compose/util-map-includes.ts index 38ba56b8..8d4e9b3a 100644 --- a/src/compose/util-map-includes.ts +++ b/src/compose/util-map-includes.ts @@ -1,6 +1,6 @@ -import { isScalar } from '../nodes/identity.ts' import type { NodeBase } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' +import { Scalar } from '../nodes/Scalar.ts' import type { ComposeContext } from './compose-node.ts' export function mapIncludes( @@ -14,6 +14,7 @@ export function mapIncludes( typeof uniqueKeys === 'function' ? uniqueKeys : (a: NodeBase, b: NodeBase) => - a === b || (isScalar(a) && isScalar(b) && a.value === b.value) + a === b || + (a instanceof Scalar && b instanceof Scalar && a.value === b.value) return items.some(pair => isEqual(pair.key, search)) } diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 1cf2b65d..295b6034 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,18 +1,13 @@ import type { YAMLError, YAMLWarning } from '../errors.ts' import { Alias } from '../nodes/Alias.ts' import { + Collection, collectionFromPath, isEmptyPath, type Primitive } from '../nodes/Collection.ts' -import { DOC, isCollection, isNode, NODE_TYPE } from '../nodes/identity.ts' -import type { - Node, - NodeBase, - NodeType, - ParsedNode, - Range -} from '../nodes/Node.ts' +import type { Node, NodeType, ParsedNode, Range } from '../nodes/Node.ts' +import { NodeBase } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import type { Scalar } from '../nodes/Scalar.ts' import type { ToJSContext } from '../nodes/toJS.ts' @@ -31,8 +26,8 @@ import { Schema } from '../schema/Schema.ts' import { stringifyDocument } from '../stringify/stringifyDocument.ts' import { anchorNames, findNewAnchor } from './anchors.ts' import { applyReviver } from './applyReviver.ts' -import { NodeCreator } from './NodeCreator.ts' import { Directives } from './directives.ts' +import { NodeCreator } from './NodeCreator.ts' export type Replacer = any[] | ((key: any, value: any) => unknown) @@ -52,9 +47,6 @@ export class Document< Contents extends Node = Node, Strict extends boolean = true > { - /** @internal */ - declare readonly [NODE_TYPE]: symbol - /** A comment before this Document */ commentBefore: string | null = null @@ -112,7 +104,6 @@ export class Document< | null, options?: DocumentOptions & SchemaOptions & ParseOptions & CreateNodeOptions ) { - Object.defineProperty(this, NODE_TYPE, { value: DOC }) let _replacer: Replacer | null = null if (typeof replacer === 'function' || Array.isArray(replacer)) { _replacer = replacer @@ -153,9 +144,7 @@ export class Document< * Custom Node values that inherit from `Object` still refer to their original instances. */ clone(): Document { - const copy: Document = Object.create(Document.prototype, { - [NODE_TYPE]: { value: DOC } - }) + const copy: Document = Object.create(Document.prototype) copy.commentBefore = this.commentBefore copy.comment = this.comment copy.errors = this.errors.slice() @@ -164,9 +153,10 @@ export class Document< if (this.directives) copy.directives = this.directives.clone() copy.schema = this.schema.clone() // @ts-expect-error We can't really know that this matches Contents. - copy.contents = isNode(this.contents) - ? this.contents.clone(copy.schema) - : this.contents + copy.contents = + this.contents instanceof NodeBase + ? this.contents.clone(copy.schema) + : this.contents if (this.range) copy.range = this.range.slice() as Document['range'] return copy } @@ -283,7 +273,9 @@ export class Document< * Returns item at `key`, or `undefined` if not found. */ get(key: any): Strict extends true ? NodeBase | Pair | undefined : any { - return isCollection(this.contents) ? this.contents.get(key) : undefined + return this.contents instanceof Collection + ? this.contents.get(key) + : undefined } /** @@ -293,14 +285,16 @@ export class Document< path: Iterable | null ): Strict extends true ? NodeBase | Pair | null | undefined : any { if (isEmptyPath(path)) return this.contents - return isCollection(this.contents) ? this.contents.getIn(path) : undefined + return this.contents instanceof Collection + ? this.contents.getIn(path) + : undefined } /** * Checks if the document includes a value with the key `key`. */ has(key: any): boolean { - return isCollection(this.contents) ? this.contents.has(key) : false + return this.contents instanceof Collection ? this.contents.has(key) : false } /** @@ -308,7 +302,9 @@ export class Document< */ hasIn(path: Iterable | null): boolean { if (isEmptyPath(path)) return this.contents !== undefined - return isCollection(this.contents) ? this.contents.hasIn(path) : false + return this.contents instanceof Collection + ? this.contents.hasIn(path) + : false } /** @@ -441,6 +437,6 @@ export class Document< } function assertCollection(contents: unknown): contents is YAMLMap | YAMLSeq { - if (isCollection(contents)) return true + if (contents instanceof Collection) return true throw new Error('Expected a YAML collection as document contents') } diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index 7a4ff53a..d257e68b 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -1,14 +1,6 @@ import { Alias } from '../nodes/Alias.ts' -import { - isCollection, - isDocument, - isNode, - isPair, - isScalar, - MAP, - SEQ -} from '../nodes/identity.ts' -import type { Node, NodeBase } from '../nodes/Node.ts' +import { Collection } from '../nodes/Collection.ts' +import { type Node, NodeBase } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' import type { YAMLMap } from '../nodes/YAMLMap.ts' @@ -16,7 +8,7 @@ import type { CreateNodeOptions } from '../options.ts' import type { Schema } from '../schema/Schema.ts' import type { CollectionTag, ScalarTag } from '../schema/types.ts' import { anchorNames, findNewAnchor } from './anchors.ts' -import type { Document, Replacer } from './Document.ts' +import { Document, type Replacer } from './Document.ts' const defaultTagPrefix = 'tag:yaml.org,2002:' @@ -58,7 +50,7 @@ export class NodeCreator { this.#aliasDuplicateObjects = options.aliasDuplicateObjects ?? true // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing this.#anchorPrefix = options.anchorPrefix || 'a' - if (isDocument(docOrSchema)) { + if (docOrSchema instanceof Document) { this.#doc = docOrSchema this.schema = docOrSchema.schema } else { @@ -67,10 +59,13 @@ export class NodeCreator { } create(value: unknown, tagName?: string): Node { - if (isDocument(value)) value = value.contents - if (isNode(value)) return value - if (isPair(value)) { - const map = this.schema[MAP].createNode?.(this, null) as YAMLMap + if (value instanceof Document) value = value.contents + if (value instanceof NodeBase) return value as Node + if (value instanceof Pair) { + const map = (this.schema.map.nodeClass! as typeof YAMLMap).from( + this, + null + ) map.items.push(value) return map } @@ -126,10 +121,10 @@ export class NodeCreator { } tagObj = value instanceof Map - ? this.schema[MAP] + ? this.schema.map : Symbol.iterator in Object(value) - ? this.schema[SEQ] - : this.schema[MAP] + ? this.schema.seq + : this.schema.map } if (this.#onTagObj) { this.#onTagObj(tagObj) @@ -144,7 +139,7 @@ export class NodeCreator { else if (!tagObj.default) node.tag = tagObj.tag if (ref) ref.node = node - if (this.#flow && isCollection(node)) node.flow = true + if (this.#flow && node instanceof Collection) node.flow = true return node } @@ -165,7 +160,7 @@ export class NodeCreator { if ( typeof ref === 'object' && ref.anchor && - (isScalar(ref.node) || isCollection(ref.node)) + (ref.node instanceof Scalar || ref.node instanceof Collection) ) { ref.node.anchor = ref.anchor } else { diff --git a/src/doc/directives.ts b/src/doc/directives.ts index 55227a86..eaecf058 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -1,4 +1,4 @@ -import { isNode } from '../nodes/identity.ts' +import { NodeBase } from '../nodes/Node.ts' import { visit } from '../visit.ts' import type { Document } from './Document.ts' @@ -180,10 +180,10 @@ export class Directives { const tagEntries = Object.entries(this.tags) let tagNames: string[] - if (doc && tagEntries.length > 0 && isNode(doc.contents)) { + if (doc && tagEntries.length > 0 && doc.contents instanceof NodeBase) { const tags: Record = {} visit(doc.contents, (_key, node) => { - if (isNode(node) && node.tag) tags[node.tag] = true + if (node instanceof NodeBase && node.tag) tags[node.tag] = true }) tagNames = Object.keys(tags) } else tagNames = [] diff --git a/src/index.ts b/src/index.ts index e4082837..99e193b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,16 +7,6 @@ export type { ErrorCode } from './errors.ts' export { YAMLError, YAMLParseError, YAMLWarning } from './errors.ts' export { Alias } from './nodes/Alias.ts' -export { - isAlias, - isCollection, - isDocument, - isMap, - isNode, - isPair, - isScalar, - isSeq -} from './nodes/identity.ts' export type { Node, ParsedNode, Range } from './nodes/Node.ts' export { Pair } from './nodes/Pair.ts' export { Scalar } from './nodes/Scalar.ts' diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts index c5c437f6..8f81e9e0 100644 --- a/src/nodes/Alias.ts +++ b/src/nodes/Alias.ts @@ -3,9 +3,9 @@ import type { Document } from '../doc/Document.ts' import type { FlowScalar } from '../parse/cst.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { visit } from '../visit.ts' -import { ALIAS, hasAnchor, isAlias, isCollection, isPair } from './identity.ts' import type { Node } from './Node.ts' import { NodeBase } from './Node.ts' +import { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' @@ -19,7 +19,7 @@ export class Alias extends NodeBase { declare srcToken?: FlowScalar & { type: 'alias' } constructor(source: string) { - super(ALIAS) + super() this.source = source Object.defineProperty(this, 'tag', { set() { @@ -43,7 +43,7 @@ export class Alias extends NodeBase { nodes = [] visit(doc, { Node: (_key: unknown, node: Node) => { - if (isAlias(node) || hasAnchor(node)) nodes.push(node) + if (node instanceof Alias || node.anchor) nodes.push(node) } }) if (ctx) ctx.aliasResolveCache = nodes @@ -113,18 +113,19 @@ function getAliasCount( node: unknown, anchors: ToJSContext['anchors'] ): number { - if (isAlias(node)) { + if (node instanceof Alias) { const source = node.resolve(doc) const anchor = anchors && source && anchors.get(source) return anchor ? anchor.count * anchor.aliasCount : 0 - } else if (isCollection(node)) { + } else if (node instanceof NodeBase && 'items' in node) { + const coll = node as YAMLMap | YAMLSeq let count = 0 - for (const item of node.items) { + for (const item of coll.items) { const c = getAliasCount(doc, item, anchors) if (c > count) count = c } return count - } else if (isPair(node)) { + } else if (node instanceof Pair) { const kc = getAliasCount(doc, node.key, anchors) const vc = getAliasCount(doc, node.value, anchors) return Math.max(kc, vc) diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index fda8e916..10989c36 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,6 +1,5 @@ import { NodeCreator } from '../doc/NodeCreator.ts' import type { Schema } from '../schema/Schema.ts' -import { isCollection, NODE_TYPE } from './identity.ts' import { type Node, NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' @@ -36,10 +35,7 @@ export const isEmptyPath = ( (typeof path === 'object' && !!path[Symbol.iterator]().next().done) export abstract class Collection extends NodeBase { - schema: Schema | undefined; - - /** @internal */ - declare [NODE_TYPE]: symbol + schema: Schema | undefined declare items: (NodeBase | Pair)[] @@ -52,8 +48,8 @@ export abstract class Collection extends NodeBase { */ declare flow?: boolean - constructor(type: symbol, schema?: Schema) { - super(type) + constructor(schema?: Schema) { + super() Object.defineProperty(this, 'schema', { value: schema, configurable: true, @@ -112,7 +108,7 @@ export abstract class Collection extends NodeBase { else { const [key, ...rest] = path const node = this.get(key) - if (isCollection(node)) node.addIn(rest, value) + if (node instanceof Collection) node.addIn(rest, value) else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value)) else @@ -131,7 +127,7 @@ export abstract class Collection extends NodeBase { const [key, ...rest] = path if (rest.length === 0) return this.delete(key) const node = this.get(key) - if (isCollection(node)) return node.deleteIn(rest) + if (node instanceof Collection) return node.deleteIn(rest) else throw new Error( `Expected YAML collection at ${key}. Remaining path: ${rest}` @@ -145,7 +141,7 @@ export abstract class Collection extends NodeBase { const [key, ...rest] = path const node = this.get(key) if (rest.length === 0) return node - else return isCollection(node) ? node.getIn(rest) : undefined + else return node instanceof Collection ? node.getIn(rest) : undefined } /** @@ -155,7 +151,7 @@ export abstract class Collection extends NodeBase { const [key, ...rest] = path if (rest.length === 0) return this.has(key) const node = this.get(key) - return isCollection(node) ? node.hasIn(rest) : false + return node instanceof Collection ? node.hasIn(rest) : false } /** @@ -167,7 +163,7 @@ export abstract class Collection extends NodeBase { this.set(key, value) } else { const node = this.get(key) - if (isCollection(node)) node.setIn(rest, value) + if (node instanceof Collection) node.setIn(rest, value) else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value)) else diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index 52014e07..fc1b5bb0 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -5,7 +5,6 @@ import type { Token } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import type { Alias } from './Alias.ts' -import { isDocument, NODE_TYPE } from './identity.ts' import type { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' @@ -39,9 +38,6 @@ export type ParsedNode = Alias | Scalar | YAMLMap | YAMLSeq export type Range = [number, number, number] export abstract class NodeBase { - /** @internal */ - declare readonly [NODE_TYPE]: symbol - /** A comment on or immediately after this */ declare comment?: string | null @@ -84,10 +80,6 @@ export abstract class NodeBase { onChompKeep?: () => void ): string - constructor(type: symbol) { - Object.defineProperty(this, NODE_TYPE, { value: type }) - } - /** Create a copy of this node. */ clone(_schema?: Schema): NodeBase { const copy: NodeBase = Object.create( @@ -103,7 +95,7 @@ export abstract class NodeBase { doc: Document, { mapAsMap, maxAliasCount, onAnchor, reviver }: ToJSOptions = {} ): any { - if (!isDocument(doc)) throw new TypeError('A document argument is required') + if (!doc?.schema) throw new TypeError('A document argument is required') const ctx: ToJSContext = { anchors: new Map(), doc, diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index b11e5efb..321696e1 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -4,7 +4,6 @@ import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyPair } from '../stringify/stringifyPair.ts' import { addPairToJSMap } from './addPairToJSMap.ts' import type { NodeOf, Primitive } from './Collection.ts' -import { NODE_TYPE, PAIR } from './identity.ts' import type { NodeBase } from './Node.ts' import type { ToJSContext } from './toJS.ts' @@ -12,9 +11,6 @@ export class Pair< K extends Primitive | NodeBase = Primitive | NodeBase, V extends Primitive | NodeBase = Primitive | NodeBase > { - /** @internal */ - declare readonly [NODE_TYPE]: symbol - key: NodeOf value: NodeOf | null @@ -22,7 +18,6 @@ export class Pair< declare srcToken?: CollectionItem constructor(key: NodeOf, value: NodeOf | null = null) { - Object.defineProperty(this, NODE_TYPE, { value: PAIR }) this.key = key this.value = value } diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index 6b82f155..58a69abf 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -1,5 +1,4 @@ import type { BlockScalar, FlowScalar } from '../parse/cst.ts' -import { SCALAR } from './identity.ts' import { NodeBase } from './Node.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' @@ -45,7 +44,7 @@ export class Scalar extends NodeBase { declare type?: Scalar.Type constructor(value: T) { - super(SCALAR) + super() this.value = value } diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index 5e54c442..4df98059 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -1,29 +1,28 @@ import type { CreateNodeOptions } from '../options.ts' import type { BlockMap, FlowCollection } from '../parse/cst.ts' -import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' import { NodeCreator } from '../doc/NodeCreator.ts' import { addPairToJSMap } from './addPairToJSMap.ts' import { Collection, type NodeOf, type Primitive } from './Collection.ts' -import { isNode, isPair, isScalar, MAP } from './identity.ts' -import type { NodeBase } from './Node.ts' +import { NodeBase } from './Node.ts' import { Pair } from './Pair.ts' import type { ToJSContext } from './toJS.ts' +import { Scalar } from './Scalar.ts' export type MapLike = - | Map - | Set - | Record + | Map + | Set + | Record export function findPair< K extends Primitive | NodeBase = Primitive | NodeBase, V extends Primitive | NodeBase = Primitive | NodeBase >(items: Iterable>, key: unknown): Pair | undefined { - const k = isScalar(key) ? key.value : key + const k = key instanceof Scalar ? key.value : key for (const it of items) { if (it.key === key || it.key === k) return it - if (isScalar(it.key) && it.key.value === k) return it + if (it.key instanceof Scalar && it.key.value === k) return it } return undefined } @@ -39,10 +38,6 @@ export class YAMLMap< items: Pair[] = [] declare srcToken?: BlockMap | FlowCollection - constructor(schema?: Schema) { - super(MAP, schema) - } - /** * A generic collection parsing method that can be extended * to other node classes that inherit from YAMLMap @@ -73,13 +68,13 @@ export class YAMLMap< * Using a key that is already in the collection overwrites the previous value. */ add(pair: Pair): void { - if (!isPair(pair)) throw new TypeError('Expected a Pair') + if (!(pair instanceof Pair)) throw new TypeError('Expected a Pair') const prev = findPair(this.items, pair.key) const sortEntries = this.schema?.sortMapEntries if (prev) { // For scalars, keep the old node & its comments and anchors - if (isScalar(prev.value) && isScalar(pair.value)) + if (prev.value instanceof Scalar && pair.value instanceof Scalar) prev.value.value = pair.value.value else prev.value = pair.value } else if (sortEntries) { @@ -113,7 +108,10 @@ export class YAMLMap< options?: Omit ): void { let pair: Pair - if (isNode(key) && (isNode(value) || value === null)) { + if ( + key instanceof NodeBase && + (value instanceof NodeBase || value === null) + ) { pair = new Pair(key, value) } else if (!this.schema) { throw new Error('Schema is required') @@ -133,11 +131,17 @@ export class YAMLMap< * @param {Class} Type - If set, forces the returned collection type * @returns Instance of Type, Map, or Object */ - toJSON>( + toJSON>( + _: unknown, + ctx: ToJSContext | undefined, + Type: { new (): T } + ): T + toJSON(_?: unknown, ctx?: ToJSContext): any + toJSON( _?: unknown, ctx?: ToJSContext, Type?: { new (): T } - ): any { + ) { const map = Type ? new Type() : ctx?.mapAsMap ? new Map() : {} if (ctx?.onCreate) ctx.onCreate(map) for (const item of this.items) addPairToJSMap(ctx, map, item) @@ -151,7 +155,7 @@ export class YAMLMap< ): string { if (!ctx) return JSON.stringify(this) for (const item of this.items) { - if (!isPair(item)) + if (!(item instanceof Pair)) throw new Error( `Map items must all be pairs; found ${JSON.stringify(item)} instead` ) diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index a93dcc3c..d7073135 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -1,13 +1,12 @@ import { NodeCreator } from '../doc/NodeCreator.ts' import type { CreateNodeOptions } from '../options.ts' import type { BlockSequence, FlowCollection } from '../parse/cst.ts' -import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' import { Collection, type NodeOf, type Primitive } from './Collection.ts' -import { isNode, isScalar, SEQ } from './identity.ts' -import type { NodeBase } from './Node.ts' +import { NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' +import { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' @@ -24,15 +23,11 @@ export class YAMLSeq< items: NodeOf[] = [] declare srcToken?: BlockSequence | FlowCollection - constructor(schema?: Schema) { - super(SEQ, schema) - } - add( value: T, options?: Omit ): void { - if (isNode(value)) this.items.push(value as NodeOf) + if (value instanceof NodeBase) this.items.push(value as NodeOf) else if (!this.schema) throw new Error('Schema is required') else { const nc = new NodeCreator(this.schema, { @@ -98,8 +93,8 @@ export class YAMLSeq< throw new TypeError(`Expected an integer, not ${JSON.stringify(idx)}.`) if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) const prev = this.items[idx] - if (isScalar(prev) && isScalarValue(value)) prev.value = value - else if (isNode(value)) this.items[idx] = value as NodeOf + if (prev instanceof Scalar && isScalarValue(value)) prev.value = value + else if (value instanceof NodeBase) this.items[idx] = value as NodeOf else if (!this.schema) throw new Error('Schema is required') else { const nc = new NodeCreator(this.schema, { diff --git a/src/nodes/addPairToJSMap.ts b/src/nodes/addPairToJSMap.ts index d429a35d..64049d8c 100644 --- a/src/nodes/addPairToJSMap.ts +++ b/src/nodes/addPairToJSMap.ts @@ -1,7 +1,7 @@ import { warn } from '../log.ts' import { addMergeToJSMap, isMergeKey } from '../schema/yaml-1.1/merge.ts' import { createStringifyContext } from '../stringify/stringify.ts' -import { isNode } from './identity.ts' +import { NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' import type { ToJSContext } from './toJS.ts' import { toJS } from './toJS.ts' @@ -12,7 +12,7 @@ export function addPairToJSMap( map: MapLike, { key, value }: Pair ): MapLike { - if (isNode(key) && key.addToJSMap) key.addToJSMap(ctx, map, value) + if (key instanceof NodeBase && key.addToJSMap) key.addToJSMap(ctx, map, value) // TODO: Should drop this special case for bare << handling else if (isMergeKey(ctx, key)) addMergeToJSMap(ctx, map, value) else { @@ -45,7 +45,7 @@ function stringifyKey( if (jsKey === null) return '' // eslint-disable-next-line @typescript-eslint/no-base-to-string if (typeof jsKey !== 'object') return String(jsKey) - if (isNode(key) && ctx?.doc) { + if (key instanceof NodeBase && ctx?.doc) { const strCtx = createStringifyContext(ctx.doc, {}) strCtx.anchors = new Set() for (const node of ctx.anchors.keys()) diff --git a/src/nodes/identity.ts b/src/nodes/identity.ts deleted file mode 100644 index 638a8ec8..00000000 --- a/src/nodes/identity.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Document } from '../doc/Document.ts' -import type { Alias } from './Alias.ts' -import type { Node } from './Node.ts' -import type { Pair } from './Pair.ts' -import type { Scalar } from './Scalar.ts' -import type { YAMLMap } from './YAMLMap.ts' -import type { YAMLSeq } from './YAMLSeq.ts' - -export const ALIAS: unique symbol = Symbol.for('yaml.alias') -export const DOC: unique symbol = Symbol.for('yaml.document') -export const MAP: unique symbol = Symbol.for('yaml.map') -export const PAIR: unique symbol = Symbol.for('yaml.pair') -export const SCALAR: unique symbol = Symbol.for('yaml.scalar') -export const SEQ: unique symbol = Symbol.for('yaml.seq') -export const NODE_TYPE: unique symbol = Symbol.for('yaml.node.type') - -export const isAlias = (node: any): node is Alias => - !!node && typeof node === 'object' && node[NODE_TYPE] === ALIAS - -export const isDocument = (node: any): node is Document => - !!node && typeof node === 'object' && node[NODE_TYPE] === DOC - -export const isMap = (node: any): node is YAMLMap => - !!node && typeof node === 'object' && node[NODE_TYPE] === MAP - -export const isPair = (node: any): node is Pair => - !!node && typeof node === 'object' && node[NODE_TYPE] === PAIR - -export const isScalar = (node: any): node is Scalar => - !!node && typeof node === 'object' && node[NODE_TYPE] === SCALAR - -export const isSeq = (node: any): node is YAMLSeq => - !!node && typeof node === 'object' && node[NODE_TYPE] === SEQ - -export function isCollection(node: any): node is YAMLMap | YAMLSeq { - if (node && typeof node === 'object') - switch (node[NODE_TYPE]) { - case MAP: - case SEQ: - return true - } - return false -} - -export function isNode(node: any): node is Node { - if (node && typeof node === 'object') - switch (node[NODE_TYPE]) { - case ALIAS: - case MAP: - case SCALAR: - case SEQ: - return true - } - return false -} - -export const hasAnchor = (node: unknown): node is Scalar | YAMLMap | YAMLSeq => - (isScalar(node) || isCollection(node)) && !!node.anchor diff --git a/src/nodes/toJS.ts b/src/nodes/toJS.ts index 2b9c6c14..93b37d78 100644 --- a/src/nodes/toJS.ts +++ b/src/nodes/toJS.ts @@ -1,6 +1,5 @@ import type { Document } from '../doc/Document.ts' -import { hasAnchor } from './identity.ts' -import type { Node } from './Node.ts' +import { NodeBase, type Node } from './Node.ts' export interface AnchorData { aliasCount: number @@ -34,15 +33,18 @@ export function toJS(value: any, arg: string | null, ctx?: ToJSContext): any { // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (Array.isArray(value)) return value.map((v, i) => toJS(v, String(i), ctx)) if (value && typeof value.toJSON === 'function') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (!ctx || !hasAnchor(value)) return value.toJSON(arg, ctx) + const node = value as Node + if (!ctx || !(node instanceof NodeBase) || !node.anchor) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + return value.toJSON(arg, ctx) + } const data: AnchorData = { aliasCount: 0, count: 1, res: undefined } - ctx.anchors.set(value, data) + ctx.anchors.set(node, data) ctx.onCreate = res => { data.res = res delete ctx.onCreate } - const res = value.toJSON(arg, ctx) + const res = node.toJSON(arg, ctx) if (ctx.onCreate) ctx.onCreate(res) return res } diff --git a/src/public-api.ts b/src/public-api.ts index 1f8b8655..042ca99a 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -4,7 +4,6 @@ import type { Replacer } from './doc/Document.ts' import { Document } from './doc/Document.ts' import { prettifyError, YAMLParseError } from './errors.ts' import { warn } from './log.ts' -import { isDocument } from './nodes/identity.ts' import type { Node, ParsedNode } from './nodes/Node.ts' import type { CreateNodeOptions, @@ -226,6 +225,6 @@ export function stringify( const { keepUndefined } = options ?? (replacer as CreateNodeOptions) ?? {} if (!keepUndefined) return undefined } - if (isDocument(value) && !_replacer) return value.toString(options) + if (value instanceof Document && !_replacer) return value.toString(options) return new Document(value, _replacer, options).toString(options) } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index c99468f4..47b400fa 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -1,4 +1,3 @@ -import { MAP, SCALAR, SEQ } from '../nodes/identity.ts' import type { Pair } from '../nodes/Pair.ts' import type { SchemaOptions, ToStringOptions } from '../options.ts' import { map } from './common/map.ts' @@ -20,11 +19,11 @@ export class Schema { // These are used by createNode() and composeScalar() /** @internal */ - declare readonly [MAP]: CollectionTag + declare readonly map: CollectionTag /** @internal */ - declare readonly [SCALAR]: ScalarTag + declare readonly scalar: ScalarTag /** @internal */ - declare readonly [SEQ]: CollectionTag + declare readonly seq: CollectionTag constructor({ compat, @@ -45,9 +44,9 @@ export class Schema { this.tags = getTags(customTags, this.name, merge) this.toStringOptions = toStringDefaults ?? null - Object.defineProperty(this, MAP, { value: map }) - Object.defineProperty(this, SCALAR, { value: string }) - Object.defineProperty(this, SEQ, { value: seq }) + Object.defineProperty(this, 'map', { value: map }) + Object.defineProperty(this, 'scalar', { value: string }) + Object.defineProperty(this, 'seq', { value: seq }) // Used by createMap() this.sortMapEntries = diff --git a/src/schema/common/map.ts b/src/schema/common/map.ts index 68bd75fc..73c97d6c 100644 --- a/src/schema/common/map.ts +++ b/src/schema/common/map.ts @@ -1,4 +1,3 @@ -import { isMap } from '../../nodes/identity.ts' import { YAMLMap } from '../../nodes/YAMLMap.ts' import type { CollectionTag } from '../types.ts' @@ -8,7 +7,7 @@ export const map: CollectionTag = { nodeClass: YAMLMap, tag: 'tag:yaml.org,2002:map', resolve(map, onError) { - if (!isMap(map)) onError('Expected a mapping for this tag') + if (!(map instanceof YAMLMap)) onError('Expected a mapping for this tag') return map } } diff --git a/src/schema/common/seq.ts b/src/schema/common/seq.ts index 012e8c5e..5a01192f 100644 --- a/src/schema/common/seq.ts +++ b/src/schema/common/seq.ts @@ -1,4 +1,3 @@ -import { isSeq } from '../../nodes/identity.ts' import { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { CollectionTag } from '../types.ts' @@ -8,7 +7,7 @@ export const seq: CollectionTag = { nodeClass: YAMLSeq, tag: 'tag:yaml.org,2002:seq', resolve(seq, onError) { - if (!isSeq(seq)) onError('Expected a sequence for this tag') + if (!(seq instanceof YAMLSeq)) onError('Expected a sequence for this tag') return seq } } diff --git a/src/schema/yaml-1.1/merge.ts b/src/schema/yaml-1.1/merge.ts index e8f449ac..de1d2e1c 100644 --- a/src/schema/yaml-1.1/merge.ts +++ b/src/schema/yaml-1.1/merge.ts @@ -1,7 +1,8 @@ -import { isAlias, isMap, isScalar, isSeq } from '../../nodes/identity.ts' +import { Alias } from '../../nodes/Alias.ts' import { Scalar } from '../../nodes/Scalar.ts' import type { ToJSContext } from '../../nodes/toJS.ts' -import type { MapLike } from '../../nodes/YAMLMap.ts' +import type { MapLike, YAMLMap } from '../../nodes/YAMLMap.ts' +import { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { ScalarTag } from '../types.ts' // If the value associated with a merge key is a single mapping node, each of @@ -36,7 +37,7 @@ export const isMergeKey = ( key: unknown ): boolean => (merge.identify(key) || - (isScalar(key) && + (key instanceof Scalar && (!key.type || key.type === Scalar.PLAIN) && merge.identify(key.value))) && Boolean( @@ -48,8 +49,9 @@ export function addMergeToJSMap( map: MapLike, value: unknown ): void { - value = ctx && isAlias(value) ? value.resolve(ctx.doc) : value - if (isSeq(value)) for (const it of value.items) mergeValue(ctx, map, it) + value = ctx && value instanceof Alias ? value.resolve(ctx.doc) : value + if (value instanceof YAMLSeq) + for (const it of value.items) mergeValue(ctx, map, it) else if (Array.isArray(value)) for (const it of value) mergeValue(ctx, map, it) else mergeValue(ctx, map, value) @@ -60,10 +62,10 @@ function mergeValue( map: MapLike, value: unknown ) { - const source = ctx && isAlias(value) ? value.resolve(ctx.doc) : value - if (!isMap(source)) + const source = ctx && value instanceof Alias ? value.resolve(ctx.doc) : value + const srcMap = (source as YAMLMap).toJSON(null, ctx, Map) + if (!(srcMap instanceof Map)) throw new Error('Merge sources must be maps or map aliases') - const srcMap = source.toJSON(null, ctx, Map) for (const [key, value] of srcMap) { if (map instanceof Map) { if (!map.has(key)) map.set(key, value) diff --git a/src/schema/yaml-1.1/omap.ts b/src/schema/yaml-1.1/omap.ts index 0ae311d6..7d2846bc 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -1,7 +1,7 @@ import type { Primitive } from '../../nodes/Collection.ts' -import { isPair, isScalar } from '../../nodes/identity.ts' import type { NodeBase } from '../../nodes/Node.ts' -import type { Pair } from '../../nodes/Pair.ts' +import { Pair } from '../../nodes/Pair.ts' +import { Scalar } from '../../nodes/Scalar.ts' import type { ToJSContext } from '../../nodes/toJS.ts' import { toJS } from '../../nodes/toJS.ts' import { YAMLMap } from '../../nodes/YAMLMap.ts' @@ -38,7 +38,7 @@ export class YAMLOMap< if (ctx?.onCreate) ctx.onCreate(map) for (const pair of this.items) { let key, value - if (isPair(pair)) { + if (pair instanceof Pair) { key = toJS(pair.key, '', ctx) value = toJS(pair.value, key, ctx) } else { @@ -70,7 +70,7 @@ export const omap: CollectionTag = { const pairs = resolvePairs(seq, onError) const seenKeys: unknown[] = [] for (const { key } of pairs.items) { - if (isScalar(key)) { + if (key instanceof Scalar) { if (seenKeys.includes(key.value)) { onError(`Ordered maps must not include duplicate keys: ${key.value}`) } else { diff --git a/src/schema/yaml-1.1/pairs.ts b/src/schema/yaml-1.1/pairs.ts index 92311f72..6890e1be 100644 --- a/src/schema/yaml-1.1/pairs.ts +++ b/src/schema/yaml-1.1/pairs.ts @@ -1,9 +1,8 @@ import type { NodeCreator } from '../../doc/NodeCreator.ts' -import { isMap, isPair, isSeq } from '../../nodes/identity.ts' import type { NodeBase } from '../../nodes/Node.ts' import { Pair } from '../../nodes/Pair.ts' import { Scalar } from '../../nodes/Scalar.ts' -import type { YAMLMap } from '../../nodes/YAMLMap.ts' +import { YAMLMap } from '../../nodes/YAMLMap.ts' import { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { CollectionTag } from '../types.ts' @@ -11,11 +10,11 @@ export function resolvePairs( seq: YAMLSeq | YAMLMap, onError: (message: string) => void ): YAMLSeq { - if (isSeq(seq)) { + if (seq instanceof YAMLSeq) { for (let i = 0; i < seq.items.length; ++i) { const item = seq.items[i] - if (isPair(item)) continue - else if (isMap(item)) { + if (item instanceof Pair) continue + else if (item instanceof YAMLMap) { if (item.items.length > 1) onError('Each pair must have its own sequence indicator') const pair = item.items[0] || new Pair(new Scalar(null)) diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index 1140b1db..1af04d9b 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -1,8 +1,8 @@ import { NodeCreator } from '../../doc/NodeCreator.ts' import type { NodeOf, Primitive } from '../../nodes/Collection.ts' -import { isMap, isNode, isPair, isScalar } from '../../nodes/identity.ts' -import type { NodeBase } from '../../nodes/Node.ts' +import { NodeBase } from '../../nodes/Node.ts' import { Pair } from '../../nodes/Pair.ts' +import { Scalar } from '../../nodes/Scalar.ts' import type { ToJSContext } from '../../nodes/toJS.ts' import { findPair, YAMLMap } from '../../nodes/YAMLMap.ts' import type { CreateNodeOptions } from '../../options.ts' @@ -31,7 +31,7 @@ export class YAMLSet< value: unknown, options?: Omit ): void { - if (!isPair(value)) { + if (!(value instanceof Pair)) { this.set(value, true, options) } else if (value.value !== null) { throw new TypeError('set pair values must be null') @@ -64,7 +64,7 @@ export class YAMLSet< this.items.splice(this.items.indexOf(prev), 1) } else if (!prev && value) { let node: NodeBase - if (isNode(key)) { + if (key instanceof NodeBase) { node = key } else if (!this.schema) { throw new Error('Schema is required') @@ -109,7 +109,7 @@ const hasAllNullValues = (map: YAMLMap): boolean => map.items.every( ({ value }) => value == null || - (isScalar(value) && + (value instanceof Scalar && value.value == null && !value.commentBefore && !value.comment && @@ -123,7 +123,7 @@ export const set: CollectionTag = { default: false, tag: 'tag:yaml.org,2002:set', resolve(map, onError) { - if (!isMap(map)) { + if (!(map instanceof YAMLMap)) { onError('Expected a mapping for this tag') return map } else if (!hasAllNullValues(map)) { diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 9ae56135..c44e0c47 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,15 +1,10 @@ import { anchorIsValid } from '../doc/anchors.ts' import type { Document } from '../doc/Document.ts' -import type { Alias } from '../nodes/Alias.ts' -import { - isAlias, - isCollection, - isNode, - isPair, - isScalar -} from '../nodes/identity.ts' -import type { Node } from '../nodes/Node.ts' -import type { Scalar } from '../nodes/Scalar.ts' +import { Alias } from '../nodes/Alias.ts' +import { Collection } from '../nodes/Collection.ts' +import { NodeBase, type Node } from '../nodes/Node.ts' +import { Pair } from '../nodes/Pair.ts' +import { Scalar } from '../nodes/Scalar.ts' import type { ToStringOptions } from '../options.ts' import type { CollectionTag, ScalarTag } from '../schema/types.ts' import { stringifyComment } from './stringifyComment.ts' @@ -94,7 +89,7 @@ function getTagObject(tags: Array, item: Node) { let tagObj: ScalarTag | CollectionTag | undefined = undefined let obj: unknown - if (isScalar(item)) { + if (item instanceof Scalar) { obj = item.value let match = tags.filter(t => t.identify?.(obj)) if (match.length > 1) { @@ -124,7 +119,8 @@ function stringifyProps( ) { if (!doc.directives) return '' const props = [] - const anchor = (isScalar(node) || isCollection(node)) && node.anchor + const anchor = + (node instanceof Scalar || node instanceof Collection) && node.anchor if (anchor && anchorIsValid(anchor)) { anchors.add(anchor) props.push(`&${anchor}`) @@ -140,8 +136,8 @@ export function stringify( onComment?: () => void, onChompKeep?: () => void ): string { - if (isPair(item)) return item.toString(ctx, onComment, onChompKeep) - if (isAlias(item)) { + if (item instanceof Pair) return item.toString(ctx, onComment, onChompKeep) + if (item instanceof Alias) { if (ctx.doc.directives) return item.toString(ctx) if (ctx.resolvedAliases?.has(item)) { @@ -156,9 +152,10 @@ export function stringify( } let tagObj: ScalarTag | CollectionTag | undefined = undefined - const node = isNode(item) - ? item - : ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) }) + const node = + item instanceof NodeBase + ? (item as Node) + : ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) }) tagObj ??= getTagObject(ctx.doc.schema.tags, node) const props = stringifyProps(node, tagObj, ctx) @@ -168,11 +165,11 @@ export function stringify( const str = typeof tagObj.stringify === 'function' ? tagObj.stringify(node as Scalar, ctx, onComment, onChompKeep) - : isScalar(node) + : node instanceof Scalar ? stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep) if (!props) return str - return isScalar(node) || str[0] === '{' || str[0] === '[' + return node instanceof Scalar || str[0] === '{' || str[0] === '[' ? `${props} ${str}` : `${props}\n${ctx.indent}${str}` } diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index b7de321c..895b4c34 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -1,5 +1,6 @@ import type { Collection } from '../nodes/Collection.ts' -import { isNode, isPair } from '../nodes/identity.ts' +import { NodeBase } from '../nodes/Node.ts' +import { Pair } from '../nodes/Pair.ts' import type { StringifyContext } from './stringify.ts' import { stringify } from './stringify.ts' import { indentComment, lineComment } from './stringifyComment.ts' @@ -44,12 +45,12 @@ function stringifyBlockCollection( for (let i = 0; i < items.length; ++i) { const item = items[i] let comment: string | null = null - if (isNode(item)) { + if (item instanceof NodeBase) { if (!chompKeep && item.spaceBefore) lines.push('') addCommentBefore(ctx, lines, item.commentBefore, chompKeep) if (item.comment) comment = item.comment - } else if (isPair(item)) { - const ik = isNode(item.key) ? item.key : null + } else if (item instanceof Pair) { + const ik = item.key instanceof NodeBase ? item.key : null if (ik) { if (!chompKeep && ik.spaceBefore) lines.push('') addCommentBefore(ctx, lines, ik.commentBefore, chompKeep) @@ -111,19 +112,19 @@ function stringifyFlowCollection( for (let i = 0; i < items.length; ++i) { const item = items[i] let comment: string | null = null - if (isNode(item)) { + if (item instanceof NodeBase) { if (item.spaceBefore) lines.push('') addCommentBefore(ctx, lines, item.commentBefore, false) if (item.comment) comment = item.comment - } else if (isPair(item)) { - const ik = isNode(item.key) ? item.key : null + } else if (item instanceof Pair) { + const ik = item.key instanceof NodeBase ? item.key : null if (ik) { if (ik.spaceBefore) lines.push('') addCommentBefore(ctx, lines, ik.commentBefore, false) if (ik.comment) reqNewline = true } - const iv = isNode(item.value) ? item.value : null + const iv = item.value instanceof NodeBase ? item.value : null if (iv) { if (iv.comment) comment = iv.comment if (iv.commentBefore) reqNewline = true diff --git a/src/stringify/stringifyDocument.ts b/src/stringify/stringifyDocument.ts index 6d11bcea..3bdfa34d 100644 --- a/src/stringify/stringifyDocument.ts +++ b/src/stringify/stringifyDocument.ts @@ -1,6 +1,5 @@ import type { Document } from '../doc/Document.ts' -import { isNode } from '../nodes/identity.ts' -import type { Node } from '../nodes/Node.ts' +import { NodeBase, type Node } from '../nodes/Node.ts' import type { ToStringOptions } from '../options.ts' import { createStringifyContext, @@ -36,7 +35,7 @@ export function stringifyDocument( let chompKeep = false let contentComment = null if (doc.contents) { - if (isNode(doc.contents)) { + if (doc.contents instanceof NodeBase) { if (doc.contents.spaceBefore && hasDirectives) lines.push('') if (doc.contents.commentBefore) { const cs = commentString(doc.contents.commentBefore) diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts index e3561192..b47ce38b 100644 --- a/src/stringify/stringifyPair.ts +++ b/src/stringify/stringifyPair.ts @@ -1,6 +1,8 @@ -import { isCollection, isNode, isScalar, isSeq } from '../nodes/identity.ts' +import { Collection } from '../nodes/Collection.ts' +import { NodeBase } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' +import { YAMLSeq } from '../nodes/YAMLSeq.ts' import type { StringifyContext } from './stringify.ts' import { stringify } from './stringify.ts' import { indentComment, lineComment } from './stringifyComment.ts' @@ -18,12 +20,15 @@ export function stringifyPair( noValues, options: { commentString, indentSeq, simpleKeys } } = ctx - let keyComment = (isNode(key) && key.comment) || null + let keyComment = (key instanceof NodeBase && key.comment) || null if (simpleKeys) { if (keyComment) { throw new Error('With simple keys, key nodes cannot have comments') } - if (isCollection(key) || (!isNode(key) && typeof key === 'object')) { + if ( + key instanceof Collection || + (!(key instanceof NodeBase) && typeof key === 'object') + ) { const msg = 'With simple keys, collection cannot be used as a key value' throw new Error(msg) } @@ -31,7 +36,7 @@ export function stringifyPair( let explicitKey = !simpleKeys && (!key || - !isScalar(key) || + !(key instanceof Scalar) || key.type === Scalar.BLOCK_FOLDED || key.type === Scalar.BLOCK_LITERAL) @@ -89,7 +94,7 @@ export function stringifyPair( } let vsb, vcb, valueComment - if (isNode(value)) { + if (value instanceof NodeBase) { vsb = !!value.spaceBefore vcb = value.commentBefore valueComment = value.comment @@ -100,7 +105,7 @@ export function stringifyPair( if (value && typeof value === 'object') value = doc.createNode(value) } ctx.implicitKey = false - if (!explicitKey && !keyComment && isScalar(value)) + if (!explicitKey && !keyComment && value instanceof Scalar) ctx.indentAtStart = str.length + 1 chompKeep = false if ( @@ -108,7 +113,7 @@ export function stringifyPair( indentStep.length >= 2 && !ctx.inFlow && !explicitKey && - isSeq(value) && + value instanceof YAMLSeq && !value.flow && !value.tag && !value.anchor @@ -136,7 +141,7 @@ export function stringifyPair( } else { ws += `\n${ctx.indent}` } - } else if (!explicitKey && isCollection(value)) { + } else if (!explicitKey && value instanceof Collection) { const vs0 = valueStr[0] const nl0 = valueStr.indexOf('\n') const hasNewline = nl0 !== -1 diff --git a/src/test-events.ts b/src/test-events.ts index 6b869de6..66709ce5 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -1,15 +1,11 @@ import type { Document } from './doc/Document.ts' -import { - isAlias, - isCollection, - isMap, - isNode, - isPair, - isScalar, - isSeq -} from './nodes/identity.ts' -import type { Node, NodeBase } from './nodes/Node.ts' -import type { Pair } from './nodes/Pair.ts' +import { Alias } from './nodes/Alias.ts' +import { Collection } from './nodes/Collection.ts' +import { type Node, NodeBase } from './nodes/Node.ts' +import { Pair } from './nodes/Pair.ts' +import { Scalar } from './nodes/Scalar.ts' +import { YAMLMap } from './nodes/YAMLMap.ts' +import { YAMLSeq } from './nodes/YAMLSeq.ts' import { parseAllDocuments } from './public-api.ts' import { visit } from './visit.ts' @@ -84,10 +80,13 @@ function addEvents( events.push('=VAL :') return } - if (errPos !== -1 && isNode(node) && node.range![0] >= errPos) + if (errPos !== -1 && node instanceof NodeBase && node.range![0] >= errPos) throw new Error() let props = '' - let anchor = isScalar(node) || isCollection(node) ? node.anchor : undefined + let anchor = + node instanceof Scalar || node instanceof Collection + ? node.anchor + : undefined if (anchor) { if (/\d$/.test(anchor)) { const alt = anchor.replace(/\d$/, '') @@ -95,9 +94,9 @@ function addEvents( } props = ` &${anchor}` } - if (isNode(node) && node.tag) props += ` <${node.tag}>` + if (node instanceof NodeBase && node.tag) props += ` <${node.tag}>` - if (isMap(node)) { + if (node instanceof YAMLMap) { const ev = node.flow ? '+MAP {}' : '+MAP' events.push(`${ev}${props}`) node.items.forEach(({ key, value }) => { @@ -105,26 +104,26 @@ function addEvents( addEvents(events, doc, errPos, value) }) events.push('-MAP') - } else if (isSeq(node)) { + } else if (node instanceof YAMLSeq) { const ev = node.flow ? '+SEQ []' : '+SEQ' events.push(`${ev}${props}`) node.items.forEach(item => { addEvents(events, doc, errPos, item) }) events.push('-SEQ') - } else if (isPair(node)) { + } else if (node instanceof Pair) { events.push(`+MAP${props}`) addEvents(events, doc, errPos, node.key) addEvents(events, doc, errPos, node.value) events.push('-MAP') - } else if (isAlias(node)) { + } else if (node instanceof Alias) { let alias = node.source if (alias && /\d$/.test(alias)) { const alt = alias.replace(/\d$/, '') if (anchorExists(doc, alt)) alias = alt } events.push(`=ALI${props} *${alias}`) - } else if (isScalar(node)) { + } else if (node instanceof Scalar) { const scalar = scalarChar[String(node.type)] if (!scalar) throw new Error(`Unexpected node type ${node.type}`) const value = node diff --git a/src/visit.ts b/src/visit.ts index 50380aa9..a2b66372 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -1,20 +1,10 @@ -import type { Document } from './doc/Document.ts' -import type { Alias } from './nodes/Alias.ts' -import { - isAlias, - isCollection, - isDocument, - isMap, - isNode, - isPair, - isScalar, - isSeq -} from './nodes/identity.ts' -import type { Node } from './nodes/Node.ts' -import type { Pair } from './nodes/Pair.ts' +import { Document } from './doc/Document.ts' +import { Alias } from './nodes/Alias.ts' +import { NodeBase, type Node } from './nodes/Node.ts' +import { Pair } from './nodes/Pair.ts' import { Scalar } from './nodes/Scalar.ts' -import type { YAMLMap } from './nodes/YAMLMap.ts' -import type { YAMLSeq } from './nodes/YAMLSeq.ts' +import { YAMLMap } from './nodes/YAMLMap.ts' +import { YAMLSeq } from './nodes/YAMLSeq.ts' const BREAK = Symbol('break visit') const SKIP = Symbol('skip children') @@ -107,7 +97,7 @@ export const visit: { REMOVE: symbol } = function visit(node, visitor) { const visitor_ = initVisitor(visitor) - if (isDocument(node)) { + if (node instanceof Document) { const cd = visit_(null, node.contents, visitor_, Object.freeze([node])) if (cd === REMOVE) node.contents = null } else visit_(null, node, visitor_, Object.freeze([])) @@ -125,13 +115,13 @@ function visit_( ): number | symbol | void { const ctrl = callVisitor(key, node, visitor, path) - if (isNode(ctrl) || isPair(ctrl)) { + if (ctrl instanceof NodeBase || ctrl instanceof Pair) { replaceNode(key, path, ctrl) return visit_(key, ctrl, visitor, path) } if (typeof ctrl !== 'symbol') { - if (isCollection(node)) { + if (node instanceof YAMLMap || node instanceof YAMLSeq) { path = Object.freeze(path.concat(node)) for (let i = 0; i < node.items.length; ++i) { const ci = visit_(i, node.items[i], visitor, path) @@ -142,7 +132,7 @@ function visit_( i -= 1 } } - } else if (isPair(node)) { + } else if (node instanceof Pair) { path = Object.freeze(path.concat(node)) const ck = visit_('key', node.key, visitor, path) if (ck === BREAK) return BREAK @@ -201,7 +191,7 @@ export const visitAsync: { REMOVE: symbol } = async function visitAsync(node, visitor) { const visitor_ = initVisitor(visitor) - if (isDocument(node)) { + if (node instanceof Document) { const cd = await visitAsync_( null, node.contents, @@ -224,13 +214,13 @@ async function visitAsync_( ): Promise { const ctrl = await callVisitor(key, node, visitor, path) - if (isNode(ctrl) || isPair(ctrl)) { + if (ctrl instanceof NodeBase || ctrl instanceof Pair) { replaceNode(key, path, ctrl) return visitAsync_(key, ctrl, visitor, path) } if (typeof ctrl !== 'symbol') { - if (isCollection(node)) { + if (node instanceof YAMLMap || node instanceof YAMLSeq) { path = Object.freeze(path.concat(node)) for (let i = 0; i < node.items.length; ++i) { const ci = await visitAsync_(i, node.items[i], visitor, path) @@ -241,7 +231,7 @@ async function visitAsync_( i -= 1 } } - } else if (isPair(node)) { + } else if (node instanceof Pair) { path = Object.freeze(path.concat(node)) const ck = await visitAsync_('key', node.key, visitor, path) if (ck === BREAK) return BREAK @@ -302,11 +292,11 @@ function callVisitor( path: readonly (Document | Node | Pair)[] ): ReturnType> | ReturnType> { if (typeof visitor === 'function') return visitor(key, node, path) - if (isMap(node)) return visitor.Map?.(key, node, path) - if (isSeq(node)) return visitor.Seq?.(key, node, path) - if (isPair(node)) return visitor.Pair?.(key, node, path) - if (isScalar(node)) return visitor.Scalar?.(key, node, path) - if (isAlias(node)) return visitor.Alias?.(key, node, path) + if (node instanceof YAMLMap) return visitor.Map?.(key, node, path) + if (node instanceof YAMLSeq) return visitor.Seq?.(key, node, path) + if (node instanceof Pair) return visitor.Pair?.(key, node, path) + if (node instanceof Scalar) return visitor.Scalar?.(key, node, path) + if (node instanceof Alias) return visitor.Alias?.(key, node, path) return undefined } @@ -316,19 +306,19 @@ function replaceNode( node: Node | Pair ): number | symbol | void { const parent = path[path.length - 1] - if (isCollection(parent)) { + if (parent instanceof YAMLMap || parent instanceof YAMLSeq) { parent.items[key as number] = node - } else if (isPair(parent)) { - if (isNode(node)) { + } else if (parent instanceof Pair) { + if (node instanceof NodeBase) { if (key === 'key') parent.key = node else parent.value = node } else { throw new Error(`Cannot replace pair ${key} with non-node value`) } - } else if (isDocument(parent)) { + } else if (parent instanceof Document) { parent.contents = node as Node } else { - const pt = isAlias(parent) ? 'alias' : 'scalar' + const pt = parent instanceof Alias ? 'alias' : 'scalar' throw new Error(`Cannot replace node with ${pt} parent`) } } diff --git a/tests/clone.ts b/tests/clone.ts index 720e2476..de279719 100644 --- a/tests/clone.ts +++ b/tests/clone.ts @@ -1,5 +1,5 @@ -import { Scalar, type YAMLMap } from 'yaml' -import { isAlias, isScalar, parseDocument, visit } from 'yaml' +import { Alias, Scalar, type YAMLMap } from 'yaml' +import { parseDocument, visit } from 'yaml' import { source } from './_utils.ts' describe('doc.clone()', () => { @@ -21,11 +21,11 @@ describe('doc.clone()', () => { expect(copy.toJS()).toEqual({ foo: 'bar' }) const node = copy.createNode(42) - expect(isScalar(node)).toBe(true) + expect(node).toBeInstanceOf(Scalar) expect(node).toMatchObject({ value: 42 }) const alias = copy.createAlias(node as Scalar, 'foo') - expect(isAlias(alias)).toBe(true) + expect(alias).toBeInstanceOf(Alias) expect(alias).toMatchObject({ source: 'foo' }) }) diff --git a/tests/collection-access.ts b/tests/collection-access.ts index 9b338527..fc3b1b19 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -2,12 +2,10 @@ import { Document, Pair, Scalar, - type YAMLMap, + YAMLMap, type YAMLOMap, - type YAMLSeq, + YAMLSeq, type YAMLSet, - isMap, - isSeq, parseDocument } from 'yaml' @@ -50,26 +48,20 @@ describe('Map', () => { test('get with value', () => { expect(map.get('a')).toMatchObject({ value: 1 }) const subMap = map.get('b') - if (isMap(subMap)) { - expect(subMap.toJSON()).toMatchObject({ - c: 3, - d: 4 - }) - expect(map.get('c')).toBeUndefined() - } else { - throw new Error('Expected subMap to be a Map') - } + expect(subMap).toBeInstanceOf(YAMLMap) + expect(subMap!.toJSON()).toMatchObject({ + c: 3, + d: 4 + }) + expect(map.get('c')).toBeUndefined() }) test('get with node', () => { expect(map.get(doc.createNode('a'))).toMatchObject({ value: 1 }) const subMap = map.get(doc.createNode('b')) - if (isMap(subMap)) { - expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 }) - expect(map.get(doc.createNode('c'))).toBeUndefined() - } else { - throw new Error('Expected subMap to be a Map') - } + expect(subMap).toBeInstanceOf(YAMLMap) + expect(subMap!.toJSON()).toMatchObject({ c: 3, d: 4 }) + expect(map.get(doc.createNode('c'))).toBeUndefined() }) test('has with value', () => { @@ -147,12 +139,9 @@ describe('Seq', () => { test('get with integer', () => { expect(seq.get(0)).toMatchObject({ value: 1 }) const subSeq = seq.get(1) - if (isSeq(subSeq)) { - expect(subSeq.toJSON()).toMatchObject([2, 3]) - expect(seq.get(2)).toBeUndefined() - } else { - throw new Error('not a seq') - } + expect(subSeq).toBeInstanceOf(YAMLSeq) + expect(subSeq!.toJSON()).toMatchObject([2, 3]) + expect(seq.get(2)).toBeUndefined() }) test('get with non-integer', () => { @@ -279,12 +268,9 @@ describe('OMap', () => { test('get', () => { expect(omap.get('a')).toMatchObject({ value: 1 }) const subMap = omap.get('b') - if (isMap(subMap)) { - expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 }) - expect(omap.get('c')).toBeUndefined() - } else { - throw new Error('Expected subMap to be a map') - } + expect(subMap).toBeInstanceOf(YAMLMap) + expect(subMap.toJSON()).toMatchObject({ c: 3, d: 4 }) + expect(omap.get('c')).toBeUndefined() }) test('has', () => { @@ -324,11 +310,8 @@ describe('Collection', () => { map.addIn(['b', 3], 6) expect(map.items).toHaveLength(3) const seq = map.getIn(['b']) - if (isSeq(seq)) { - expect(seq.items).toHaveLength(4) - } else { - throw new Error('Expected seq to be a seq') - } + expect(seq).toBeInstanceOf(YAMLSeq) + expect((seq as any).items).toHaveLength(4) }) test('deleteIn', () => { @@ -341,11 +324,8 @@ describe('Collection', () => { expect(() => map.deleteIn(['a', 'e'])).toThrow(/Expected YAML collection/) expect(map.items).toHaveLength(1) const subSeq = map.getIn(['b']) - if (isSeq(subSeq)) { - expect(subSeq.items).toHaveLength(1) - } else { - throw new Error('Expected subSeq to be a seq') - } + expect(subSeq).toBeInstanceOf(YAMLSeq) + expect((subSeq as any).items).toHaveLength(1) }) test('getIn', () => { @@ -380,11 +360,8 @@ describe('Collection', () => { expect(() => map.setIn(['a', 'e'], 8)).toThrow(/Expected YAML collection/) expect(map.items).toHaveLength(4) const subSeq = map.getIn(['b']) - if (isSeq(subSeq)) { - expect(subSeq.items).toHaveLength(3) - } else { - throw new Error('Expected subSeq to be a seq') - } + expect(subSeq).toBeInstanceOf(YAMLSeq) + expect((subSeq as any).items).toHaveLength(3) }) }) diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 755a3e79..11abc12b 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -142,6 +142,15 @@ describe('objects', () => { ]) }) + test('createNode(pair)', () => { + const pair = new Document().createPair('x', true) + const s = new Document().createNode(pair) + expect(s).toBeInstanceOf(YAMLMap) + expect(s.items).toMatchObject([ + { key: { value: 'x' }, value: { value: true } } + ]) + }) + describe('{ x: 3, y: [4], z: { w: "five", v: 6 } }', () => { const object = { x: 3, y: [4], z: { w: 'five', v: 6 } } test('createNode(value)', () => { diff --git a/tests/is-node.ts b/tests/is-node.ts deleted file mode 100644 index e79a9ca8..00000000 --- a/tests/is-node.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { - Alias, - Document, - isAlias, - isCollection, - isDocument, - isMap, - isNode, - isPair, - isScalar, - isSeq, - Pair, - parseDocument, - Scalar, - YAMLMap, - YAMLSeq -} from 'yaml' - -for (const { fn, exp } of [ - { - fn: isAlias, - exp: { - doc: false, - scalar: false, - map: false, - seq: false, - alias: true, - pair: false - } - }, - { - fn: isCollection, - exp: { - doc: false, - scalar: false, - map: true, - seq: true, - alias: false, - pair: false - } - }, - { - fn: isDocument, - exp: { - doc: true, - scalar: false, - map: false, - seq: false, - alias: false, - pair: false - } - }, - { - fn: isMap, - exp: { - doc: false, - scalar: false, - map: true, - seq: false, - alias: false, - pair: false - } - }, - { - fn: isNode, - exp: { - doc: false, - scalar: true, - map: true, - seq: true, - alias: true, - pair: false - } - }, - { - fn: isPair, - exp: { - doc: false, - scalar: false, - map: false, - seq: false, - alias: false, - pair: true - } - }, - { - fn: isScalar, - exp: { - doc: false, - scalar: true, - map: false, - seq: false, - alias: false, - pair: false - } - }, - { - fn: isSeq, - exp: { - doc: false, - scalar: false, - map: false, - seq: true, - alias: false, - pair: false - } - } -] as { fn: (x: unknown) => boolean; exp: Record }[]) { - describe(fn.name, () => { - test('parsed doc', () => { - const doc = parseDocument('foo') - expect(fn(doc)).toBe(exp.doc) - }) - - test('parsed scalar', () => { - const doc = parseDocument('foo') - expect(fn(doc.contents)).toBe(exp.scalar) - }) - - test('parsed map', () => { - const doc = parseDocument('foo: bar') - expect(fn(doc.contents)).toBe(exp.map) - }) - - test('parsed seq', () => { - const doc = parseDocument('[foo, bar]') - expect(fn(doc.contents)).toBe(exp.seq) - }) - - test('parsed alias', () => { - const doc = parseDocument('[ &a foo, *a ]') - expect(fn(doc.contents?.items[1])).toBe(exp.alias) - }) - - test('parsed pair', () => { - const doc = parseDocument('foo: bar') - expect(fn(doc.contents?.items[0])).toBe(exp.pair) - }) - - test('created doc', () => { - expect(fn(new Document())).toBe(exp.doc) - }) - - test('created scalar', () => { - expect(fn(new Scalar(42))).toBe(exp.scalar) - }) - - test('created map', () => { - expect(fn(new YAMLMap())).toBe(exp.map) - }) - - test('created seq', () => { - expect(fn(new YAMLSeq())).toBe(exp.seq) - }) - - test('created alias', () => { - expect(fn(new Alias('42'))).toBe(exp.alias) - }) - - test('created pair', () => { - expect(fn(new Pair(new Scalar(null)))).toBe(exp.pair) - }) - - test('null', () => { - expect(fn(null)).toBe(false) - }) - - test('string', () => { - expect(fn('foo')).toBe(false) - }) - - test('object', () => { - expect(fn({ type: 'SCALAR', value: 42 })).toBe(false) - }) - }) -} diff --git a/tests/node-to-js.ts b/tests/node-to-js.ts index 29ab1915..7b3d31cb 100644 --- a/tests/node-to-js.ts +++ b/tests/node-to-js.ts @@ -64,11 +64,6 @@ describe('alias', () => { }) describe('options', () => { - test('doc is required', () => { - const doc = parseDocument('key: 42') - expect(() => doc.contents?.toJS({} as any)).toThrow(TypeError) - }) - test('mapAsMap', () => { const doc = parseDocument('key: 42') expect(doc.contents?.toJS(doc, { mapAsMap: true })).toMatchObject( diff --git a/tests/visit.ts b/tests/visit.ts index 197f8576..da0e6d51 100644 --- a/tests/visit.ts +++ b/tests/visit.ts @@ -1,5 +1,11 @@ -import type { Scalar } from 'yaml' -import { Document, isSeq, parseDocument, visit, visitAsync } from 'yaml' +import { + Document, + parseDocument, + visit, + visitAsync, + YAMLSeq, + type Scalar +} from 'yaml' const coll = { items: {} } @@ -147,7 +153,9 @@ for (const [visit_, title] of [ test('Do not visit block seq items', async () => { const doc = parseDocument('foo:\n - one\n - two\nbar:\n') - const fn = vi.fn((_, node) => (isSeq(node) ? visit_.SKIP : undefined)) + const fn = vi.fn((_, node) => + node instanceof YAMLSeq ? visit_.SKIP : undefined + ) await visit_(doc, { Map: fn, Pair: fn, Seq: fn, Scalar: fn }) expect(fn.mock.calls).toMatchObject([ [null, coll, [{}]], From 44944ef3ce6a45adfa35e8cce6514c0b5e4db1a7 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 28 Jan 2026 20:15:21 +0800 Subject: [PATCH 5/7] feat!: Disallow empty doc.contents, require array arg for *in() methods --- docs/05_content_nodes.md | 10 +++--- src/compose/compose-doc.ts | 12 +++++-- src/compose/composer.ts | 2 +- src/doc/Document.ts | 58 ++++++++++-------------------- src/doc/directives.ts | 2 +- src/nodes/Collection.ts | 20 ++++------- src/stringify/stringifyDocument.ts | 56 +++++++++++++---------------- src/test-events.ts | 10 ++---- src/visit.ts | 4 +-- tests/collection-access.ts | 43 +++++++--------------- tests/doc/YAML-1.2.spec.ts | 2 +- tests/doc/anchors.ts | 8 ++--- tests/node-to-js.ts | 6 ++-- 13 files changed, 90 insertions(+), 143 deletions(-) diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index b559c04a..bfd9401b 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -59,12 +59,12 @@ class Collection extends NodeBase { anchor?: string // an anchor associated with this node flow?: boolean // use flow style when stringifying this schema?: Schema - addIn(path: Iterable, value: unknown): void + addIn(path: unknown[], value: unknown): void clone(schema?: Schema): NodeBase // a deep copy of this collection - deleteIn(path: Iterable): boolean - getIn(path: Iterable, keepScalar?: boolean): unknown - hasIn(path: Iterable): boolean - setIn(path: Iterable, value: unknown): void + deleteIn(path: unknown[]): boolean + getIn(path: unknown[], keepScalar?: boolean): unknown + hasIn(path: unknown[]): boolean + setIn(path: unknown[], value: unknown): void } class YAMLMap extends Collection { diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index e0beb731..22e09f3e 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -55,10 +55,16 @@ export function composeDoc< 'Block collection cannot start on same line with directives-end marker' ) } - // @ts-expect-error If Contents is set, let's trust the user doc.contents = value - ? composeNode(ctx, value, props, onError) - : composeEmptyNode(ctx, props.end, start, null, props, onError) + ? (composeNode(ctx, value, props, onError) as Contents) + : (composeEmptyNode( + ctx, + props.end, + start, + null, + props, + onError + ) as Contents) const contentEnd = doc.contents.range![2] const re = resolveEnd(end, contentEnd, false, onError) diff --git a/src/compose/composer.ts b/src/compose/composer.ts index a19437c2..0ae3ed05 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -109,7 +109,7 @@ export class Composer< const dc = doc.contents if (afterDoc) { doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment - } else if (afterEmptyLine || doc.directives.docStart || !dc) { + } 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] diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 295b6034..9110620b 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,15 +1,10 @@ import type { YAMLError, YAMLWarning } from '../errors.ts' import { Alias } from '../nodes/Alias.ts' -import { - Collection, - collectionFromPath, - isEmptyPath, - type Primitive -} from '../nodes/Collection.ts' +import { Collection, type Primitive } from '../nodes/Collection.ts' import type { Node, NodeType, ParsedNode, Range } from '../nodes/Node.ts' -import { NodeBase } from '../nodes/Node.ts' +import type { NodeBase } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' -import type { Scalar } from '../nodes/Scalar.ts' +import { Scalar } from '../nodes/Scalar.ts' import type { ToJSContext } from '../nodes/toJS.ts' import { toJS } from '../nodes/toJS.ts' import type { YAMLMap } from '../nodes/YAMLMap.ts' @@ -54,7 +49,7 @@ export class Document< comment: string | null = null /** The document contents. */ - contents: Strict extends true ? Contents | null : Contents + contents: Contents directives: Strict extends true ? Directives | undefined : Directives @@ -133,9 +128,7 @@ export class Document< } else this.directives = new Directives({ version }) this.setSchema(version, options) - // @ts-expect-error We can't really know that this matches Contents. - this.contents = - value === undefined ? null : this.createNode(value, _replacer, options) + this.contents = this.createNode(value, _replacer, options) as Contents } /** @@ -152,11 +145,7 @@ export class Document< copy.options = Object.assign({}, this.options) if (this.directives) copy.directives = this.directives.clone() copy.schema = this.schema.clone() - // @ts-expect-error We can't really know that this matches Contents. - copy.contents = - this.contents instanceof NodeBase - ? this.contents.clone(copy.schema) - : this.contents + copy.contents = this.contents.clone(copy.schema) as Contents if (this.range) copy.range = this.range.slice() as Document['range'] return copy } @@ -167,7 +156,7 @@ export class Document< } /** Adds a value to the document. */ - addIn(path: Iterable, value: unknown): void { + addIn(path: unknown[], value: unknown): void { if (assertCollection(this.contents)) this.contents.addIn(path, value) } @@ -257,11 +246,9 @@ export class Document< * Removes a value from the document. * @returns `true` if the item was found and removed. */ - deleteIn(path: Iterable | null): boolean { - if (isEmptyPath(path)) { - if (this.contents == null) return false - // @ts-expect-error Presumed impossible if Strict extends false - this.contents = null + deleteIn(path: unknown[]): boolean { + if (!path.length) { + this.contents = new Scalar(null) as Contents return true } return assertCollection(this.contents) @@ -282,9 +269,9 @@ export class Document< * Returns item at `path`, or `undefined` if not found. */ getIn( - path: Iterable | null + path: unknown[] ): Strict extends true ? NodeBase | Pair | null | undefined : any { - if (isEmptyPath(path)) return this.contents + if (!path.length) return this.contents return this.contents instanceof Collection ? this.contents.getIn(path) : undefined @@ -300,8 +287,8 @@ export class Document< /** * Checks if the document includes a value at `path`. */ - hasIn(path: Iterable | null): boolean { - if (isEmptyPath(path)) return this.contents !== undefined + hasIn(path: unknown[]): boolean { + if (!path.length) return true return this.contents instanceof Collection ? this.contents.hasIn(path) : false @@ -312,25 +299,16 @@ export class Document< * boolean to add/remove the item from the set. */ set(key: any, value: any): void { - if (this.contents == null) { - // @ts-expect-error We can't really know that this matches Contents. - this.contents = collectionFromPath(this.schema, [key], value) - } else if (assertCollection(this.contents)) { - this.contents.set(key, value) - } + if (assertCollection(this.contents)) this.contents.set(key, value) } /** * Sets a value in this document. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. */ - setIn(path: Iterable | null, value: unknown): void { - if (isEmptyPath(path)) { - // @ts-expect-error We can't really know that this matches Contents. - this.contents = value - } else if (this.contents == null) { - // @ts-expect-error We can't really know that this matches Contents. - this.contents = collectionFromPath(this.schema, Array.from(path), value) + setIn(path: unknown[], value: unknown): void { + if (!path.length) { + this.contents = value as Contents } else if (assertCollection(this.contents)) { this.contents.setIn(path, value) } diff --git a/src/doc/directives.ts b/src/doc/directives.ts index eaecf058..8c555246 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -180,7 +180,7 @@ export class Directives { const tagEntries = Object.entries(this.tags) let tagNames: string[] - if (doc && tagEntries.length > 0 && doc.contents instanceof NodeBase) { + if (doc && tagEntries.length > 0) { const tags: Record = {} visit(doc.contents, (_key, node) => { if (node instanceof NodeBase && node.tag) tags[node.tag] = true diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index 10989c36..a78a8edc 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -26,14 +26,6 @@ export function collectionFromPath( return new NodeCreator(schema, { aliasDuplicateObjects: false }).create(v) } -// Type guard is intentionally a little wrong so as to be more useful, -// as it does not cover untypable empty non-string iterables (e.g. []). -export const isEmptyPath = ( - path: Iterable | null | undefined -): path is null | undefined => - path == null || - (typeof path === 'object' && !!path[Symbol.iterator]().next().done) - export abstract class Collection extends NodeBase { schema: Schema | undefined @@ -103,8 +95,8 @@ export abstract class Collection extends NodeBase { * * For `!!map` and `!!omap` the value must be a Pair instance. */ - addIn(path: Iterable, value: unknown): void { - if (isEmptyPath(path)) this.add(value) + addIn(path: unknown[], value: unknown): void { + if (!path.length) this.add(value) else { const [key, ...rest] = path const node = this.get(key) @@ -123,7 +115,7 @@ export abstract class Collection extends NodeBase { * * @returns `true` if the item was found and removed. */ - deleteIn(path: Iterable): boolean { + deleteIn(path: unknown[]): boolean { const [key, ...rest] = path if (rest.length === 0) return this.delete(key) const node = this.get(key) @@ -137,7 +129,7 @@ export abstract class Collection extends NodeBase { /** * Returns item at `key`, or `undefined` if not found. */ - getIn(path: Iterable): NodeBase | Pair | undefined { + getIn(path: unknown[]): NodeBase | Pair | undefined { const [key, ...rest] = path const node = this.get(key) if (rest.length === 0) return node @@ -147,7 +139,7 @@ export abstract class Collection extends NodeBase { /** * Checks if the collection includes a value with the key `key`. */ - hasIn(path: Iterable): boolean { + hasIn(path: unknown[]): boolean { const [key, ...rest] = path if (rest.length === 0) return this.has(key) const node = this.get(key) @@ -157,7 +149,7 @@ export abstract class Collection extends NodeBase { /** * Sets a value in this collection. */ - setIn(path: Iterable, value: unknown): void { + setIn(path: unknown[], value: unknown): void { const [key, ...rest] = path if (rest.length === 0) { this.set(key, value) diff --git a/src/stringify/stringifyDocument.ts b/src/stringify/stringifyDocument.ts index 3bdfa34d..8f21b537 100644 --- a/src/stringify/stringifyDocument.ts +++ b/src/stringify/stringifyDocument.ts @@ -1,5 +1,5 @@ import type { Document } from '../doc/Document.ts' -import { NodeBase, type Node } from '../nodes/Node.ts' +import type { Node } from '../nodes/Node.ts' import type { ToStringOptions } from '../options.ts' import { createStringifyContext, @@ -34,37 +34,31 @@ export function stringifyDocument( let chompKeep = false let contentComment = null - if (doc.contents) { - if (doc.contents instanceof NodeBase) { - if (doc.contents.spaceBefore && hasDirectives) lines.push('') - if (doc.contents.commentBefore) { - const cs = commentString(doc.contents.commentBefore) - lines.push(indentComment(cs, '')) - } - // top-level block scalars need to be indented if followed by a comment - ctx.forceBlockIndent = !!doc.comment - contentComment = doc.contents.comment - } - const onChompKeep = contentComment ? undefined : () => (chompKeep = true) - let body = stringify( - doc.contents, - ctx, - () => (contentComment = null), - onChompKeep - ) - if (contentComment) - body += lineComment(body, '', commentString(contentComment)) - if ( - (body[0] === '|' || body[0] === '>') && - lines[lines.length - 1] === '---' - ) { - // Top-level block scalars with a preceding doc marker ought to use the - // same line for their header. - lines[lines.length - 1] = `--- ${body}` - } else lines.push(body) - } else { - lines.push(stringify(doc.contents, ctx)) + if (doc.contents.spaceBefore && hasDirectives) lines.push('') + if (doc.contents.commentBefore) { + const cs = commentString(doc.contents.commentBefore) + lines.push(indentComment(cs, '')) } + // top-level block scalars need to be indented if followed by a comment + ctx.forceBlockIndent = !!doc.comment + contentComment = doc.contents.comment + const onChompKeep = contentComment ? undefined : () => (chompKeep = true) + let body = stringify( + doc.contents, + ctx, + () => (contentComment = null), + onChompKeep + ) + if (contentComment) + body += lineComment(body, '', commentString(contentComment)) + if ( + (body[0] === '|' || body[0] === '>') && + lines[lines.length - 1] === '---' + ) { + // Top-level block scalars with a preceding doc marker ought to use the + // same line for their header. + lines[lines.length - 1] = `--- ${body}` + } else lines.push(body) if (doc.directives?.docEnd) { if (doc.comment) { const cs = commentString(doc.comment) diff --git a/src/test-events.ts b/src/test-events.ts index 66709ce5..829c577a 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -42,19 +42,13 @@ export function testEvents(src: string): { try { for (let i = 0; i < docs.length; ++i) { const doc = docs[i] - let root = doc.contents - if (Array.isArray(root)) root = root[0] + const root = doc.contents const [rootStart] = doc.range || [0] const error = doc.errors[0] if (error && (!error.pos || error.pos[0] < rootStart)) throw new Error() let docStart = '+DOC' if (doc.directives.docStart) docStart += ' ---' - else if ( - doc.contents && - doc.contents.range![2] === doc.contents.range![0] && - !doc.contents.anchor && - !doc.contents.tag - ) + else if (root.range![2] === root.range![0] && !root.anchor && !root.tag) continue events.push(docStart) addEvents(events, doc, error?.pos[0] ?? -1, root) diff --git a/src/visit.ts b/src/visit.ts index a2b66372..0a678f8c 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -99,7 +99,7 @@ export const visit: { const visitor_ = initVisitor(visitor) if (node instanceof Document) { const cd = visit_(null, node.contents, visitor_, Object.freeze([node])) - if (cd === REMOVE) node.contents = null + if (cd === REMOVE) node.contents = new Scalar(null) } else visit_(null, node, visitor_, Object.freeze([])) } @@ -198,7 +198,7 @@ export const visitAsync: { visitor_, Object.freeze([node]) ) - if (cd === REMOVE) node.contents = null + if (cd === REMOVE) node.contents = new Scalar(null) } else await visitAsync_(null, node, visitor_, Object.freeze([])) } diff --git a/tests/collection-access.ts b/tests/collection-access.ts index fc3b1b19..fa43e27f 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -369,7 +369,7 @@ describe('Document', () => { let doc: Document | YAMLSeq>>> beforeEach(() => { doc = new Document({ a: 1, b: [2, 3] }) - expect(doc.contents?.items).toMatchObject([ + expect(doc.contents.items).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, @@ -381,7 +381,7 @@ describe('Document', () => { test('add', () => { doc.add(doc.createPair('c', 'x')) expect(doc.get('c')).toMatchObject({ value: 'x' }) - expect(doc.contents?.items).toHaveLength(3) + expect(doc.contents.items).toHaveLength(3) }) test('addIn', () => { @@ -391,7 +391,7 @@ describe('Document', () => { expect(doc.get('c')).toMatchObject({ value: 5 }) expect(() => doc.addIn(['a'], -1)).toThrow(/Expected YAML collection/) doc.addIn(['b', 3], 6) - expect(doc.contents?.items).toHaveLength(3) + expect(doc.contents.items).toHaveLength(3) expect((doc.get('b') as any).items).toHaveLength(4) }) @@ -399,7 +399,7 @@ describe('Document', () => { expect(doc.delete('a')).toBe(true) expect(doc.delete('a')).toBe(false) expect(doc.get('a')).toBeUndefined() - expect(doc.contents?.items).toHaveLength(1) + expect(doc.contents.items).toHaveLength(1) }) test('delete on scalar contents', () => { @@ -415,10 +415,9 @@ describe('Document', () => { expect(doc.deleteIn([1])).toBe(false) expect(doc.deleteIn(['b', 2])).toBe(false) expect(() => doc.deleteIn(['a', 'e'])).toThrow(/Expected/) - expect(doc.contents?.items).toHaveLength(1) + expect(doc.contents.items).toHaveLength(1) expect((doc.get('b') as any).items).toHaveLength(1) - expect(doc.deleteIn(null)).toBe(true) - expect(doc.deleteIn(null)).toBe(false) + expect(() => doc.deleteIn(null as any)).toThrow() }) test('get', () => { @@ -442,7 +441,7 @@ describe('Document', () => { test('getIn scalar', () => { const doc = new Document('s') expect(doc.getIn([])).toMatchObject({ value: 's' }) - expect(doc.getIn(null)).toMatchObject({ value: 's' }) + expect(() => doc.getIn(null as any)).toThrow() expect(doc.getIn([0])).toBeUndefined() }) @@ -469,7 +468,7 @@ describe('Document', () => { expect(doc.get('a')).toMatchObject({ value: 2 }) doc.set('c', 6) expect(doc.get('c')).toMatchObject({ value: 6 }) - expect(doc.contents?.items).toHaveLength(3) + expect(doc.contents.items).toHaveLength(3) }) test('set on scalar contents', () => { @@ -477,12 +476,6 @@ describe('Document', () => { expect(() => doc.set('a', 1)).toThrow(/document contents/) }) - test('set on empty document', () => { - doc.contents = null - doc.set('a', 1) - expect(doc.get('a')).toMatchObject({ value: 1 }) - }) - test('setIn', () => { doc.setIn(['a'], 2) expect(doc.getIn(['a'])).toMatchObject({ value: 2 }) @@ -493,7 +486,7 @@ describe('Document', () => { doc.setIn(['e', 1, 'e'], 7) expect(doc.getIn(['e', 1, 'e'])).toMatchObject({ value: 7 }) expect(() => doc.setIn(['a', 'e'], 8)).toThrow(/Expected YAML collection/) - expect(doc.contents?.items).toHaveLength(4) + expect(doc.contents.items).toHaveLength(4) expect((doc.get('b') as any).items).toHaveLength(2) expect(String(doc)).toBe( 'a: 2\nb:\n - 2\n - 5\nc: 6\ne:\n - null\n - e: 7\n' @@ -505,14 +498,6 @@ describe('Document', () => { expect(() => doc.setIn(['a'], 1)).toThrow(/document contents/) }) - test('setIn on empty document', () => { - doc.contents = null - doc.setIn(['a', 2], 1) - expect(doc.get('a')).toMatchObject({ - items: [{ value: null }, { value: null }, { value: 1 }] - }) - }) - test('setIn on parsed document', () => { const doc = parseDocument('{ a: 1, b: [2, 3] }') doc.setIn(['c', 1], 9) @@ -525,11 +510,10 @@ describe('Document', () => { }) test('setIn with object key', () => { - doc.contents = null + doc.contents = doc.createNode({}) const foo = { foo: 'FOO' } doc.setIn([foo], 'BAR') - // @ts-expect-error - Doesn't see that setIn() changes contents - expect(doc.contents?.items).toMatchObject([ + expect(doc.contents.items).toMatchObject([ { key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, value: { value: 'BAR' } @@ -538,11 +522,10 @@ describe('Document', () => { }) test('setIn with repeated object key', () => { - doc.contents = null + doc.contents = doc.createNode({}) const foo = { foo: 'FOO' } doc.setIn([foo, foo], 'BAR') - // @ts-expect-error - Doesn't see that setIn() changes contents - expect(doc.contents?.items).toMatchObject([ + expect(doc.contents.items).toMatchObject([ { key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, value: { diff --git a/tests/doc/YAML-1.2.spec.ts b/tests/doc/YAML-1.2.spec.ts index 03f3b240..629f4d26 100644 --- a/tests/doc/YAML-1.2.spec.ts +++ b/tests/doc/YAML-1.2.spec.ts @@ -673,7 +673,7 @@ mapping: { sky: blue, sea: green }`, tgt: [], special(src) { const doc = YAML.parseDocument(src) - expect(doc.commentBefore).toBe(' Comment only.') + expect(doc.contents.commentBefore).toBe(' Comment only.') } }, diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index da783ec7..40a71ad7 100644 --- a/tests/doc/anchors.ts +++ b/tests/doc/anchors.ts @@ -5,7 +5,7 @@ test('basic', () => { const src = `- &a 1\n- *a\n` const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents?.items).toMatchObject([ + expect(doc.contents.items).toMatchObject([ { anchor: 'a', value: 1 }, { source: 'a' } ]) @@ -16,7 +16,7 @@ test('re-defined anchor', () => { const src = '- &a 1\n- &a 2\n- *a\n' const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents?.items).toMatchObject([ + expect(doc.contents.items).toMatchObject([ { anchor: 'a', value: 1 }, { anchor: 'a', value: 2 }, { source: 'a' } @@ -69,7 +69,7 @@ describe('create', () => { test('doc.createAlias', () => { const doc = parseDocument('[{ a: A }, { b: B }]') const alias = doc.createAlias(doc.get(0), 'AA') - doc.contents?.items.push(alias) + doc.contents.items.push(alias) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }]) const alias2 = doc.createAlias(doc.get(1), 'AA') expect(doc.get(1).anchor).toBe('AA1') @@ -167,7 +167,7 @@ describe('__proto__ as anchor name', () => { const src = `- &__proto__ 1\n- *__proto__\n` const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents?.items).toMatchObject([ + expect(doc.contents.items).toMatchObject([ { anchor: '__proto__', value: 1 }, { source: '__proto__' } ]) diff --git a/tests/node-to-js.ts b/tests/node-to-js.ts index 7b3d31cb..052e47a6 100644 --- a/tests/node-to-js.ts +++ b/tests/node-to-js.ts @@ -5,7 +5,7 @@ import { source } from './_utils.ts' describe('scalars', () => { test('plain', () => { const doc = parseDocument('42') - expect(doc.contents?.toJS(doc)).toBe(42) + expect(doc.contents.toJS(doc)).toBe(42) }) test('plain in map', () => { @@ -17,7 +17,7 @@ describe('scalars', () => { describe('collections', () => { test('map', () => { const doc = parseDocument('key: 42') - expect(doc.contents?.toJS(doc)).toMatchObject({ key: 42 }) + expect(doc.contents.toJS(doc)).toMatchObject({ key: 42 }) }) test('map in seq', () => { @@ -66,7 +66,7 @@ describe('alias', () => { describe('options', () => { test('mapAsMap', () => { const doc = parseDocument('key: 42') - expect(doc.contents?.toJS(doc, { mapAsMap: true })).toMatchObject( + expect(doc.contents.toJS(doc, { mapAsMap: true })).toMatchObject( new Map([['key', 42]]) ) }) From b4fe89a1f08a0dd3eb1229bea43ae13bd9822fe9 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 28 Jan 2026 20:46:04 +0800 Subject: [PATCH 6/7] feat!: Rename doc.contents as doc.value --- README.md | 2 +- docs/01_intro.md | 2 +- docs/04_documents.md | 41 ++++---- docs/05_content_nodes.md | 26 +++-- docs/06_custom_tags.md | 2 +- src/compose/compose-collection.ts | 6 +- src/compose/compose-doc.ts | 24 ++--- src/compose/compose-node.ts | 6 +- src/compose/composer.ts | 22 ++--- src/doc/Document.ts | 74 +++++++------- src/doc/NodeCreator.ts | 10 +- src/doc/anchors.ts | 11 +-- src/doc/directives.ts | 2 +- src/index.ts | 4 +- src/nodes/Node.ts | 4 +- src/nodes/toJS.ts | 4 +- src/public-api.ts | 27 ++--- src/stringify/stringifyDocument.ts | 15 ++- src/test-events.ts | 2 +- src/visit.ts | 12 +-- tests/cli.ts | 6 +- tests/clone.ts | 2 +- tests/collection-access.ts | 48 ++++----- tests/directives.ts | 6 +- tests/doc/YAML-1.2.spec.ts | 12 +-- tests/doc/anchors.ts | 40 ++++---- tests/doc/comments.ts | 152 ++++++++++++++--------------- tests/doc/createNode.ts | 36 +++---- tests/doc/errors.ts | 8 +- tests/doc/foldFlowLines.ts | 10 +- tests/doc/parse.ts | 38 ++++---- tests/doc/stringify.ts | 74 +++++++------- tests/doc/types.ts | 129 ++++++++++++------------ tests/node-to-js.ts | 6 +- tests/visit.ts | 10 +- 35 files changed, 419 insertions(+), 454 deletions(-) diff --git a/README.md b/README.md index 647fddbc..c7dcb2e8 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ parse(file) - [`Document`](https://eemeli.org/yaml/#documents) - [`constructor(value, replacer?, options?)`](https://eemeli.org/yaml/#creating-documents) - - [`#contents`](https://eemeli.org/yaml/#content-nodes) + - [`#value`](https://eemeli.org/yaml/#content-nodes) - [`#directives`](https://eemeli.org/yaml/#stream-directives) - [`#errors`](https://eemeli.org/yaml/#errors) - [`#warnings`](https://eemeli.org/yaml/#errors) diff --git a/docs/01_intro.md b/docs/01_intro.md index 371fa501..87f0f084 100644 --- a/docs/01_intro.md +++ b/docs/01_intro.md @@ -69,7 +69,7 @@ import { - [`Document`](#documents) - [`constructor(value, replacer?, options?)`](#creating-documents) - - [`#contents`](#content-nodes) + - [`#value`](#content-nodes) - [`#directives`](#stream-directives) - [`#errors`](#errors) - [`#warnings`](#errors) diff --git a/docs/04_documents.md b/docs/04_documents.md index 17f15fd0..2332e41c 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -10,7 +10,7 @@ import { parseAllDocuments, parseDocument } from 'yaml' const file = fs.readFileSync('./file.yml', 'utf8') const doc = parseDocument(file) -doc.contents +doc.value // YAMLMap { // items: // [ Pair { @@ -44,9 +44,9 @@ These functions should never throw, provided that `str` is a string and the `options` are valid. Errors and warnings are included in the documents' `errors` and `warnings` arrays. In particular, if `errors` is not empty -it's likely that the document's parsed `contents` are not entirely correct. +it's likely that the document's parsed `value` are not entirely correct. -The `contents` of a parsed document will always consist of `Scalar`, `Map`, `Seq` or `null` values. +The `value` of a parsed document will always consist of `Scalar`, `Map`, or `Seq` values. #### `parseDocument(str, options = {}): Document` @@ -69,16 +69,15 @@ See [Options](#options) for more information on the second parameter. #### `new Document(value, replacer?, options = {})` Creates a new document. -If `value` is defined, the document `contents` are initialised with that value, wrapped recursively in appropriate [content nodes](#content-nodes). -If `value` is `undefined`, the document's `contents` is initialised as `null`. -If defined, a `replacer` may filter or modify the initial document contents, following the same algorithm as the [JSON implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). +The document `value` is initialised with `value`, wrapped recursively in appropriate [content nodes](#content-nodes). +If defined, a `replacer` may filter or modify the initial document value, following the same algorithm as the [JSON implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). See [Options](#options) for more information on the last argument. | Member | Type | Description | | ------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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. | +| value | [`Node`](#content-nodes) `⎮ any` | The document value. | | directives | [`Directives`](#stream-directives) | Controls for the `%YAML` and `%TAG` directives, as well as the doc-start marker `---`. | | errors | [`Error[]`](#errors) | Errors encountered during parsing. | | schema | `Schema` | The schema used with the document. | @@ -99,21 +98,21 @@ String(doc) ``` The Document members are all modifiable, though it's unlikely that you'll have reason to change `errors`, `schema` or `warnings`. -In particular you may be interested in both reading and writing **`contents`**. -Although `parseDocument()` and `parseAllDocuments()` will leave it with `YAMLMap`, `YAMLSeq`, `Scalar` or `null` contents, it can be set to anything. +In particular you may be interested in both reading and writing **`value`**, +which is expected to always contain a `YAMLMap`, `YAMLSeq`, or `Scalar` value. ## Document Methods -| Method | Returns | Description | -| ------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| clone() | `Document` | Create a deep copy of this Document and its contents. Custom Node values that inherit from `Object` still refer to their original instances. | -| 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, options?) | `Node` | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information. | -| createPair(key, value, options?) | `Pair` | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information. | -| setSchema(version, 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. | -| toJS(options?) | `any` | A plain JavaScript representation of the document `contents`. | -| toJSON() | `any` | A JSON representation of the document `contents`. | -| toString(options?) | `string` | A YAML representation of the document. | +| Method | Returns | Description | +| ------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| clone() | `Document` | Create a deep copy of this Document and its value. Custom Node values that inherit from `Object` still refer to their original instances. | +| 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, options?) | `Node` | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information. | +| createPair(key, value, options?) | `Pair` | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information. | +| setSchema(version, 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. | +| toJS(options?) | `any` | A plain JavaScript representation of the document `value`. | +| toJSON() | `any` | A JSON representation of the document `value`. | +| toString(options?) | `string` | A YAML representation of the document. | ```js const doc = parseDocument('a: 1\nb: [2, 3]\n') @@ -127,7 +126,7 @@ doc.getIn(['b', 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. `null`, `undefined`, or `[]`) will refer to the document's top-level `contents`. +For the `*In` methods using an empty `path` value (i.e. `[]`) will refer to the document's top-level `value`. #### `Document#toJS()`, `Document#toJSON()` and `Document#toString()` @@ -177,5 +176,5 @@ The contents of `doc.directives.tags` are used both for the `%TAG` directives an Each of the handles must start and end with a `!` character; `!` is by default the local tag and `!!` is used for default tags. 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. +`doc.directives.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. diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index bfd9401b..8782137e 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -1,6 +1,6 @@ # Content Nodes -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). +After parsing, the `value` 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. 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. @@ -41,7 +41,7 @@ class Scalar extends NodeBase { } ``` -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. +A parsed document's value 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. @@ -50,9 +50,9 @@ On the other hand, `!!int` and `!!float` stringifiers will take `format` into ac ## Collections ```js -class Pair { - key: K // When parsed, key and value are always - value: V // Node or null, but can be set to anything +class Pair { + key: Node + value: Node | null } class Collection extends NodeBase { @@ -87,8 +87,8 @@ class YAMLSeq extends Collection { ``` 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. +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 node type, or `null` for `value`. +The `items` array of a `YAMLSeq` object may contain any node values. 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. @@ -169,7 +169,7 @@ When nodes are constructed from JS structures (e.g. during `YAML.stringify()`), ```js const doc = new YAML.Document(['some', 'values']) // Document { -// contents: +// value: // YAMLSeq { // items: // [ Scalar { value: 'some' }, @@ -209,8 +209,7 @@ For all available options, see the [CreateNode Options](#createnode-options) sec [replacer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter 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. +To that end, you'll need to assign its return value to the `value` of a document (or somewhere within said value), as the document's schema is required for YAML string output.

doc.createAlias(node, name?): Alias

@@ -236,12 +235,11 @@ You should make sure to only add alias nodes to the document after the nodes to ```js import { Document, YAMLSeq } from 'yaml' -const doc = new Document(new YAMLSeq()) -doc.contents.items = [ +const doc = new Document([ 'some values', 42, { including: 'objects', 3: 'a string' } -] +]) doc.add(doc.createPair(1, 'a number')) doc.toString() @@ -320,7 +318,7 @@ The return value of the visitor may be used to control the traversal: - `number`: While iterating the items of a sequence or map, set the index of the next step. This is useful especially if the index of the current node has changed. -If `visitor` is a single function, it will be called with all values encountered in the tree, including e.g. `null` values. +If `visitor` is a single function, it will be called with all values encountered in the tree, including `null` values. Alternatively, separate visitor functions may be defined for each `Map`, `Pair`, `Seq`, `Alias` and `Scalar` node. To define the same visitor function for more than one node type, use the `Collection` (map and seq), `Value` (map, seq & scalar) and `Node` (alias, map, seq & scalar) targets. diff --git a/docs/06_custom_tags.md b/docs/06_custom_tags.md index afa6fb58..d9c4ae9b 100644 --- a/docs/06_custom_tags.md +++ b/docs/06_custom_tags.md @@ -10,7 +10,7 @@ parse('!!timestamp 2001-12-15 2:59:43') // 2001-12-15T02:59:43.000Z (Date instance) const doc = parseDocument('2001-12-15 2:59:43', { customTags: ['timestamp'] }) -doc.contents.value.toDateString() +doc.value.value.toDateString() // 'Sat Dec 15 2001' ``` diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index f71619e5..4162f81e 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -1,4 +1,4 @@ -import { NodeBase, type ParsedNode } from '../nodes/Node.ts' +import { NodeBase, type Node } from '../nodes/Node.ts' import { Scalar } from '../nodes/Scalar.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' import { YAMLSeq } from '../nodes/YAMLSeq.ts' @@ -54,7 +54,7 @@ export function composeCollection( token: BlockMap | BlockSequence | FlowCollection, props: Props, onError: ComposeErrorHandler -): ParsedNode { +): Node { const tagToken = props.tag const tagName: string | null = !tagToken ? null @@ -135,7 +135,7 @@ export function composeCollection( ctx.options ) ?? coll - const node = res instanceof NodeBase ? (res as ParsedNode) : new Scalar(res) + const node = res instanceof NodeBase ? (res as Node) : new Scalar(res) node.range = coll.range node.tag = tagName if (tag?.format) (node as Scalar).format = tag.format diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index 22e09f3e..5df947b7 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -1,6 +1,5 @@ import type { Directives } from '../doc/directives.ts' -import { Document } from '../doc/Document.ts' -import type { ParsedNode } from '../nodes/Node.ts' +import { Document, type DocValue } from '../doc/Document.ts' import type { DocumentOptions, ParseOptions, @@ -17,16 +16,16 @@ import { resolveEnd } from './resolve-end.ts' import { resolveProps } from './resolve-props.ts' export function composeDoc< - Contents extends ParsedNode = ParsedNode, + Value extends DocValue = DocValue, Strict extends boolean = true >( options: ParseOptions & DocumentOptions & SchemaOptions, directives: Directives, { offset, start, value, end }: CST.Document, onError: ComposeErrorHandler -): Document.Parsed { +): Document.Parsed { const opts = Object.assign({ _directives: directives }, options) - const doc = new Document(undefined, opts) as Document.Parsed + const doc = new Document(undefined, opts) as Document.Parsed const ctx: ComposeContext = { atKey: false, atRoot: true, @@ -55,18 +54,11 @@ export function composeDoc< 'Block collection cannot start on same line with directives-end marker' ) } - doc.contents = value - ? (composeNode(ctx, value, props, onError) as Contents) - : (composeEmptyNode( - ctx, - props.end, - start, - null, - props, - onError - ) as Contents) + doc.value = value + ? (composeNode(ctx, value, props, onError) as Value) + : (composeEmptyNode(ctx, props.end, start, null, props, onError) as Value) - const contentEnd = doc.contents.range![2] + const contentEnd = doc.value.range![2] const re = resolveEnd(end, contentEnd, false, onError) if (re.comment) doc.comment = re.comment doc.range = [offset, contentEnd, re.offset] diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 3b0850be..9ff3327b 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -1,6 +1,6 @@ import type { Directives } from '../doc/directives.ts' import { Alias } from '../nodes/Alias.ts' -import type { ParsedNode } from '../nodes/Node.ts' +import type { Node } from '../nodes/Node.ts' import { Scalar } from '../nodes/Scalar.ts' import type { ParseOptions } from '../options.ts' import type { FlowScalar, SourceToken, Token } from '../parse/cst.ts' @@ -39,10 +39,10 @@ export function composeNode( token: Token, props: Props, onError: ComposeErrorHandler -): ParsedNode { +): Node { const atKey = ctx.atKey const { spaceBefore, comment, anchor, tag } = props - let node: ParsedNode + let node: Node let isSrcToken = true switch (token.type) { case 'alias': diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 0ae3ed05..990a4846 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -1,9 +1,9 @@ import { Directives } from '../doc/directives.ts' -import { Document } from '../doc/Document.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 type { ParsedNode, Range } from '../nodes/Node.ts' +import type { Range } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import type { DocumentOptions, @@ -78,12 +78,12 @@ function parsePrelude(prelude: string[]) { * ``` */ export class Composer< - Contents extends ParsedNode = ParsedNode, + Value extends DocValue = DocValue, Strict extends boolean = true > { private directives: Directives - private doc: Document.Parsed | null = null - private docs: Document.Parsed[] = [] + private doc: Document.Parsed | null = null + private docs: Document.Parsed[] = [] private options: ParseOptions & DocumentOptions & SchemaOptions private atDirectives = false private prelude: string[] = [] @@ -102,11 +102,11 @@ export class Composer< else this.errors.push(new YAMLParseError(pos, code, message)) } - private decorate(doc: Document.Parsed, afterDoc: boolean) { + private decorate(doc: Document.Parsed, afterDoc: boolean) { const { comment, afterEmptyLine } = parsePrelude(this.prelude) //console.log({ dc: doc.comment, prelude, comment }) if (comment) { - const dc = doc.contents + const dc = doc.value if (afterDoc) { doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment } else if (afterEmptyLine || doc.directives.docStart) { @@ -164,7 +164,7 @@ export class Composer< tokens: Iterable, forceDoc = false, endOffset = -1 - ): Document.Parsed[] { + ): Document.Parsed[] { this.docs = [] for (const token of tokens) this.next(token) return this.end(forceDoc, endOffset) @@ -184,7 +184,7 @@ export class Composer< this.atDirectives = true break case 'document': { - const doc = composeDoc( + const doc = composeDoc( this.options, this.directives, token, @@ -262,7 +262,7 @@ export class Composer< * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. */ - end(forceDoc = false, endOffset = -1): Document.Parsed[] { + end(forceDoc = false, endOffset = -1): Document.Parsed[] { if (this.doc) { this.decorate(this.doc, true) this.docs.push(this.doc) @@ -270,7 +270,7 @@ export class Composer< } else if (forceDoc) { const opts = Object.assign({ _directives: this.directives }, this.options) const doc = new Document(undefined, opts) as Document.Parsed< - Contents, + Value, Strict > if (this.atDirectives) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 9110620b..8abc5002 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,7 +1,7 @@ import type { YAMLError, YAMLWarning } from '../errors.ts' import { Alias } from '../nodes/Alias.ts' import { Collection, type Primitive } from '../nodes/Collection.ts' -import type { Node, NodeType, ParsedNode, Range } from '../nodes/Node.ts' +import type { Node, NodeType, Range } from '../nodes/Node.ts' import type { NodeBase } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' @@ -24,22 +24,24 @@ import { applyReviver } from './applyReviver.ts' import { Directives } from './directives.ts' import { NodeCreator } from './NodeCreator.ts' +export type DocValue = Scalar | YAMLSeq | YAMLMap + export type Replacer = any[] | ((key: any, value: any) => unknown) export declare namespace Document { // eslint-disable-next-line @typescript-eslint/ban-ts-comment /** @ts-ignore The typing of directives fails in TS <= 4.2 */ interface Parsed< - Contents extends ParsedNode = ParsedNode, + Value extends DocValue = DocValue, Strict extends boolean = true - > extends Document { + > extends Document { directives: Directives range: Range } } export class Document< - Contents extends Node = Node, + Value extends DocValue = DocValue, Strict extends boolean = true > { /** A comment before this Document */ @@ -48,8 +50,8 @@ export class Document< /** A comment immediately after this Document */ comment: string | null = null - /** The document contents. */ - contents: Contents + /** The document value. */ + value: Value directives: Strict extends true ? Directives | undefined : Directives @@ -128,16 +130,16 @@ export class Document< } else this.directives = new Directives({ version }) this.setSchema(version, options) - this.contents = this.createNode(value, _replacer, options) as Contents + this.value = this.createNode(value, _replacer, options) as Value } /** - * Create a deep copy of this Document and its contents. + * Create a deep copy of this Document and its value. * * Custom Node values that inherit from `Object` still refer to their original instances. */ - clone(): Document { - const copy: Document = Object.create(Document.prototype) + clone(): Document { + const copy: Document = Object.create(Document.prototype) copy.commentBefore = this.commentBefore copy.comment = this.comment copy.errors = this.errors.slice() @@ -145,19 +147,19 @@ export class Document< copy.options = Object.assign({}, this.options) if (this.directives) copy.directives = this.directives.clone() copy.schema = this.schema.clone() - copy.contents = this.contents.clone(copy.schema) as Contents + copy.value = this.value.clone(copy.schema) as Value if (this.range) copy.range = this.range.slice() as Document['range'] return copy } /** Adds a value to the document. */ add(value: any): void { - if (assertCollection(this.contents)) this.contents.add(value) + assertCollection(this.value).add(value) } /** Adds a value to the document. */ addIn(path: unknown[], value: unknown): void { - if (assertCollection(this.contents)) this.contents.addIn(path, value) + assertCollection(this.value).addIn(path, value) } /** @@ -170,7 +172,7 @@ export class Document< * If `name` is undefined, the generated anchor will use 'a' as a prefix. */ createAlias( - node: Strict extends true ? Scalar | YAMLMap | YAMLSeq : Node, + node: Strict extends true ? DocValue : Node, name?: string ): Alias { if (!node.anchor) { @@ -239,7 +241,7 @@ export class Document< * @returns `true` if the item was found and removed. */ delete(key: any): boolean { - return assertCollection(this.contents) ? this.contents.delete(key) : false + return assertCollection(this.value).delete(key) } /** @@ -248,21 +250,17 @@ export class Document< */ deleteIn(path: unknown[]): boolean { if (!path.length) { - this.contents = new Scalar(null) as Contents + this.value = new Scalar(null) as Value return true } - return assertCollection(this.contents) - ? this.contents.deleteIn(path) - : false + return assertCollection(this.value).deleteIn(path) } /** * Returns item at `key`, or `undefined` if not found. */ get(key: any): Strict extends true ? NodeBase | Pair | undefined : any { - return this.contents instanceof Collection - ? this.contents.get(key) - : undefined + return this.value instanceof Collection ? this.value.get(key) : undefined } /** @@ -271,17 +269,15 @@ export class Document< getIn( path: unknown[] ): Strict extends true ? NodeBase | Pair | null | undefined : any { - if (!path.length) return this.contents - return this.contents instanceof Collection - ? this.contents.getIn(path) - : undefined + if (!path.length) return this.value + return this.value instanceof Collection ? this.value.getIn(path) : undefined } /** * Checks if the document includes a value with the key `key`. */ has(key: any): boolean { - return this.contents instanceof Collection ? this.contents.has(key) : false + return this.value instanceof Collection ? this.value.has(key) : false } /** @@ -289,9 +285,7 @@ export class Document< */ hasIn(path: unknown[]): boolean { if (!path.length) return true - return this.contents instanceof Collection - ? this.contents.hasIn(path) - : false + return this.value instanceof Collection ? this.value.hasIn(path) : false } /** @@ -299,7 +293,7 @@ export class Document< * boolean to add/remove the item from the set. */ set(key: any, value: any): void { - if (assertCollection(this.contents)) this.contents.set(key, value) + assertCollection(this.value).set(key, value) } /** @@ -308,9 +302,9 @@ export class Document< */ setIn(path: unknown[], value: unknown): void { if (!path.length) { - this.contents = value as Contents - } else if (assertCollection(this.contents)) { - this.contents.setIn(path, value) + this.value = value as Value + } else { + assertCollection(this.value).setIn(path, value) } } @@ -361,7 +355,7 @@ export class Document< ) } - /** A plain JavaScript representation of the document `contents`. */ + /** A plain JavaScript representation of the document `value`. */ toJS(opt?: ToJSOptions & { [ignored: string]: unknown }): any // json & jsonArg are only used from toJSON() @@ -381,7 +375,7 @@ export class Document< mapKeyWarned: false, maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100 } - const res = toJS(this.contents, jsonArg ?? '', ctx) + const res = toJS(this.value, jsonArg ?? '', ctx) if (typeof onAnchor === 'function') for (const { count, res } of ctx.anchors.values()) onAnchor(res, count) return typeof reviver === 'function' @@ -390,7 +384,7 @@ export class Document< } /** - * A JSON representation of the document `contents`. + * A JSON representation of the document `value`. * * @param jsonArg Used by `JSON.stringify` to indicate the array index or * property name. @@ -414,7 +408,7 @@ export class Document< } } -function assertCollection(contents: unknown): contents is YAMLMap | YAMLSeq { - if (contents instanceof Collection) return true - throw new Error('Expected a YAML collection as document contents') +function assertCollection(value: unknown) { + if (value instanceof Collection) return value as YAMLMap | YAMLSeq + throw new Error('Expected a YAML collection as document value') } diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index d257e68b..57be20ee 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -8,7 +8,7 @@ import type { CreateNodeOptions } from '../options.ts' import type { Schema } from '../schema/Schema.ts' import type { CollectionTag, ScalarTag } from '../schema/types.ts' import { anchorNames, findNewAnchor } from './anchors.ts' -import { Document, type Replacer } from './Document.ts' +import { Document, type DocValue, type Replacer } from './Document.ts' const defaultTagPrefix = 'tag:yaml.org,2002:' @@ -20,7 +20,7 @@ export class NodeCreator { #aliasDuplicateObjects: boolean #anchorPrefix: string #aliasObjects: unknown[] = [] - #doc?: Document + #doc?: Document #flow: boolean #onTagObj?: (tagObj: ScalarTag | CollectionTag) => void #prevAnchors: Set | null = null @@ -28,7 +28,7 @@ export class NodeCreator { new Map() constructor( - doc: Document, + doc: Document, options?: CreateNodeOptions, replacer?: Replacer ) @@ -37,7 +37,7 @@ export class NodeCreator { options: CreateNodeOptions & { aliasDuplicateObjects: false } ) constructor( - docOrSchema: Document | Schema, + docOrSchema: Document | Schema, options: CreateNodeOptions = {}, replacer?: Replacer ) { @@ -59,7 +59,7 @@ export class NodeCreator { } create(value: unknown, tagName?: string): Node { - if (value instanceof Document) value = value.contents + if (value instanceof Document) value = value.value if (value instanceof NodeBase) return value as Node if (value instanceof Pair) { const map = (this.schema.map.nodeClass! as typeof YAMLMap).from( diff --git a/src/doc/anchors.ts b/src/doc/anchors.ts index 487584ac..8bbe6926 100644 --- a/src/doc/anchors.ts +++ b/src/doc/anchors.ts @@ -1,9 +1,6 @@ import type { Node } from '../nodes/Node.ts' -import type { Scalar } from '../nodes/Scalar.ts' -import type { YAMLMap } from '../nodes/YAMLMap.ts' -import type { YAMLSeq } from '../nodes/YAMLSeq.ts' import { visit } from '../visit.ts' -import type { Document } from './Document.ts' +import type { Document, DocValue } from './Document.ts' /** * Verify that the input string is a valid anchor. @@ -19,10 +16,12 @@ export function anchorIsValid(anchor: string): true { return true } -export function anchorNames(root: Document | Node): Set { +export function anchorNames( + root: Document | Node +): Set { const anchors = new Set() visit(root, { - Value(_key: unknown, node: Scalar | YAMLMap | YAMLSeq) { + Value(_key, node) { if (node.anchor) anchors.add(node.anchor) } }) diff --git a/src/doc/directives.ts b/src/doc/directives.ts index 8c555246..aef96d06 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -182,7 +182,7 @@ export class Directives { let tagNames: string[] if (doc && tagEntries.length > 0) { const tags: Record = {} - visit(doc.contents, (_key, node) => { + visit(doc.value, (_key, node) => { if (node instanceof NodeBase && node.tag) tags[node.tag] = true }) tagNames = Object.keys(tags) diff --git a/src/index.ts b/src/index.ts index 99e193b8..e363e3a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ export { Composer } from './compose/composer.ts' -export { Document } from './doc/Document.ts' +export { Document, type DocValue } from './doc/Document.ts' export { Schema } from './schema/Schema.ts' export type { ErrorCode } from './errors.ts' export { YAMLError, YAMLParseError, YAMLWarning } from './errors.ts' export { Alias } from './nodes/Alias.ts' -export type { Node, ParsedNode, Range } from './nodes/Node.ts' +export type { Node, Range } from './nodes/Node.ts' export { Pair } from './nodes/Pair.ts' export { Scalar } from './nodes/Scalar.ts' export { YAMLMap } from './nodes/YAMLMap.ts' diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index fc1b5bb0..078b3a79 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -32,8 +32,6 @@ export type NodeType = T extends ? YAMLMap, NodeType> : Node -export type ParsedNode = Alias | Scalar | YAMLMap | YAMLSeq - /** `[start, value-end, node-end]` */ export type Range = [number, number, number] @@ -92,7 +90,7 @@ export abstract class NodeBase { /** A plain JavaScript representation of this node. */ toJS( - doc: Document, + doc: Document, { mapAsMap, maxAliasCount, onAnchor, reviver }: ToJSOptions = {} ): any { if (!doc?.schema) throw new TypeError('A document argument is required') diff --git a/src/nodes/toJS.ts b/src/nodes/toJS.ts index 93b37d78..b34fdb51 100644 --- a/src/nodes/toJS.ts +++ b/src/nodes/toJS.ts @@ -1,4 +1,4 @@ -import type { Document } from '../doc/Document.ts' +import type { Document, DocValue } from '../doc/Document.ts' import { NodeBase, type Node } from './Node.ts' export interface AnchorData { @@ -11,7 +11,7 @@ export interface ToJSContext { anchors: Map /** Cached anchor and alias nodes in the order they occur in the document */ aliasResolveCache?: Node[] - doc: Document + doc: Document keep: boolean mapAsMap: boolean mapKeyWarned: boolean diff --git a/src/public-api.ts b/src/public-api.ts index 042ca99a..aee8e9c4 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -1,10 +1,9 @@ import { Composer } from './compose/composer.ts' import type { Reviver } from './doc/applyReviver.ts' -import type { Replacer } from './doc/Document.ts' +import type { DocValue, Replacer } from './doc/Document.ts' import { Document } from './doc/Document.ts' import { prettifyError, YAMLParseError } from './errors.ts' import { warn } from './log.ts' -import type { Node, ParsedNode } from './nodes/Node.ts' import type { CreateNodeOptions, DocumentOptions, @@ -38,18 +37,12 @@ function parseOptions(options: ParseOptions) { * TypeScript, you should use `'empty' in docs` as a type guard for it. */ export function parseAllDocuments< - Contents extends Node = ParsedNode, + Value extends DocValue = DocValue, Strict extends boolean = true >( source: string, options: ParseOptions & DocumentOptions & SchemaOptions = {} -): - | Array< - Contents extends ParsedNode - ? Document.Parsed - : Document - > - | EmptyStream { +): Document.Parsed[] | EmptyStream { const { lineCounter, prettyErrors } = parseOptions(options) const parser = new Parser(lineCounter?.addNewLine) const composer = new Composer(options) @@ -61,9 +54,7 @@ export function parseAllDocuments< doc.warnings.forEach(prettifyError(source, lineCounter)) } - type DocType = Contents extends ParsedNode - ? Document.Parsed - : Document + type DocType = Document.Parsed if (docs.length > 0) return docs as DocType[] return Object.assign< DocType[], @@ -74,21 +65,17 @@ export function parseAllDocuments< /** Parse an input string into a single YAML.Document */ export function parseDocument< - Contents extends Node = ParsedNode, + Value extends DocValue = DocValue, Strict extends boolean = true >( source: string, options: ParseOptions & DocumentOptions & SchemaOptions = {} -): Contents extends ParsedNode - ? Document.Parsed - : Document { +): Document.Parsed { const { lineCounter, prettyErrors } = parseOptions(options) const parser = new Parser(lineCounter?.addNewLine) const composer = new Composer(options) - type DocType = Contents extends ParsedNode - ? Document.Parsed - : Document + type DocType = Document.Parsed // `doc` is always set by compose.end(true) at the very latest let doc: DocType = null as any for (const _doc of composer.compose( diff --git a/src/stringify/stringifyDocument.ts b/src/stringify/stringifyDocument.ts index 8f21b537..655c82d2 100644 --- a/src/stringify/stringifyDocument.ts +++ b/src/stringify/stringifyDocument.ts @@ -1,5 +1,4 @@ -import type { Document } from '../doc/Document.ts' -import type { Node } from '../nodes/Node.ts' +import type { Document, DocValue } from '../doc/Document.ts' import type { ToStringOptions } from '../options.ts' import { createStringifyContext, @@ -9,7 +8,7 @@ import { import { indentComment, lineComment } from './stringifyComment.ts' export function stringifyDocument( - doc: Readonly>, + doc: Readonly>, options: ToStringOptions ): string { const lines: string[] = [] @@ -34,17 +33,17 @@ export function stringifyDocument( let chompKeep = false let contentComment = null - if (doc.contents.spaceBefore && hasDirectives) lines.push('') - if (doc.contents.commentBefore) { - const cs = commentString(doc.contents.commentBefore) + if (doc.value.spaceBefore && hasDirectives) lines.push('') + if (doc.value.commentBefore) { + const cs = commentString(doc.value.commentBefore) lines.push(indentComment(cs, '')) } // top-level block scalars need to be indented if followed by a comment ctx.forceBlockIndent = !!doc.comment - contentComment = doc.contents.comment + contentComment = doc.value.comment const onChompKeep = contentComment ? undefined : () => (chompKeep = true) let body = stringify( - doc.contents, + doc.value, ctx, () => (contentComment = null), onChompKeep diff --git a/src/test-events.ts b/src/test-events.ts index 829c577a..60770462 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -42,7 +42,7 @@ export function testEvents(src: string): { try { for (let i = 0; i < docs.length; ++i) { const doc = docs[i] - const root = doc.contents + const root = doc.value const [rootStart] = doc.range || [0] const error = doc.errors[0] if (error && (!error.pos || error.pos[0] < rootStart)) throw new Error() diff --git a/src/visit.ts b/src/visit.ts index 0a678f8c..2721022f 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -1,4 +1,4 @@ -import { Document } from './doc/Document.ts' +import { Document, type DocValue } from './doc/Document.ts' import { Alias } from './nodes/Alias.ts' import { NodeBase, type Node } from './nodes/Node.ts' import { Pair } from './nodes/Pair.ts' @@ -98,8 +98,8 @@ export const visit: { } = function visit(node, visitor) { const visitor_ = initVisitor(visitor) if (node instanceof Document) { - const cd = visit_(null, node.contents, visitor_, Object.freeze([node])) - if (cd === REMOVE) node.contents = new Scalar(null) + const cd = visit_(null, node.value, visitor_, Object.freeze([node])) + if (cd === REMOVE) node.value = new Scalar(null) } else visit_(null, node, visitor_, Object.freeze([])) } @@ -194,11 +194,11 @@ export const visitAsync: { if (node instanceof Document) { const cd = await visitAsync_( null, - node.contents, + node.value, visitor_, Object.freeze([node]) ) - if (cd === REMOVE) node.contents = new Scalar(null) + if (cd === REMOVE) node.value = new Scalar(null) } else await visitAsync_(null, node, visitor_, Object.freeze([])) } @@ -316,7 +316,7 @@ function replaceNode( throw new Error(`Cannot replace pair ${key} with non-node value`) } } else if (parent instanceof Document) { - parent.contents = node as Node + parent.value = node as DocValue } else { const pt = parent instanceof Alias ? 'alias' : 'scalar' throw new Error(`Cannot replace node with ${pt} parent`) diff --git a/tests/cli.ts b/tests/cli.ts index e4f33238..177e14eb 100644 --- a/tests/cli.ts +++ b/tests/cli.ts @@ -243,18 +243,18 @@ const skip = Number(major) < 20 ) }) describe('--doc', () => { - ok('basic', 'hello: world', ['--doc'], [{ contents: { items: [{}] } }]) + ok('basic', 'hello: world', ['--doc'], [{ value: { items: [{}] } }]) ok( 'multiple', 'hello: world\n---\n42', ['--doc'], - [{ contents: { items: [{}] } }, { contents: { value: 42 } }] + [{ value: { items: [{}] } }, { value: { value: 42 } }] ) ok( 'error', 'hello: world: 2', ['--doc'], - [{ contents: { items: [{}] } }], + [{ value: { items: [{}] } }], [{ name: 'YAMLParseError' }] ) }) diff --git a/tests/clone.ts b/tests/clone.ts index de279719..7fb3f11e 100644 --- a/tests/clone.ts +++ b/tests/clone.ts @@ -29,7 +29,7 @@ describe('doc.clone()', () => { expect(alias).toMatchObject({ source: 'foo' }) }) - test('has separate contents from original', () => { + test('has separate value from original', () => { const doc = parseDocument('foo: bar') const copy = doc.clone() copy.set('foo', 'fizz') diff --git a/tests/collection-access.ts b/tests/collection-access.ts index fa43e27f..65ed2666 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -14,7 +14,7 @@ describe('Map', () => { let map: YAMLMap> beforeEach(() => { doc = new Document({ a: 1, b: { c: 3, d: 4 } }) - map = doc.contents as any + map = doc.value as any expect(map.items).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { @@ -114,7 +114,7 @@ describe('Seq', () => { let seq: YAMLSeq | YAMLSeq>> beforeEach(() => { doc = new Document([1, [2, 3]]) - seq = doc.contents as any + seq = doc.value as any expect(seq.items).toMatchObject([ { value: 1 }, { items: [{ value: 2 }, { value: 3 }] } @@ -189,7 +189,7 @@ describe('Set', () => { beforeEach(() => { doc = new Document(null, { version: '1.1' }) set = doc.createNode([1, 2, 3], { tag: '!!set' }) as any - doc.contents = set + doc.value = set expect(set.items).toMatchObject([ { key: { value: 1 }, value: null }, { key: { value: 2 }, value: null }, @@ -235,7 +235,7 @@ describe('OMap', () => { omap = doc.createNode([{ a: 1 }, { b: { c: 3, d: 4 } }], { tag: '!!omap' }) as any - doc.contents = omap + doc.value = omap expect(omap.items).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { @@ -298,7 +298,7 @@ describe('Collection', () => { let map: YAMLMap | YAMLSeq>> beforeEach(() => { doc = new Document({ a: 1, b: [2, 3] }) - map = doc.contents as any + map = doc.value as any }) test('addIn', () => { @@ -369,7 +369,7 @@ describe('Document', () => { let doc: Document | YAMLSeq>>> beforeEach(() => { doc = new Document({ a: 1, b: [2, 3] }) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, @@ -381,7 +381,7 @@ describe('Document', () => { test('add', () => { doc.add(doc.createPair('c', 'x')) expect(doc.get('c')).toMatchObject({ value: 'x' }) - expect(doc.contents.items).toHaveLength(3) + expect(doc.value.items).toHaveLength(3) }) test('addIn', () => { @@ -391,7 +391,7 @@ describe('Document', () => { expect(doc.get('c')).toMatchObject({ value: 5 }) expect(() => doc.addIn(['a'], -1)).toThrow(/Expected YAML collection/) doc.addIn(['b', 3], 6) - expect(doc.contents.items).toHaveLength(3) + expect(doc.value.items).toHaveLength(3) expect((doc.get('b') as any).items).toHaveLength(4) }) @@ -399,12 +399,12 @@ describe('Document', () => { expect(doc.delete('a')).toBe(true) expect(doc.delete('a')).toBe(false) expect(doc.get('a')).toBeUndefined() - expect(doc.contents.items).toHaveLength(1) + expect(doc.value.items).toHaveLength(1) }) - test('delete on scalar contents', () => { + test('delete on scalar value', () => { const doc = new Document('s') - expect(() => doc.set('a', 1)).toThrow(/document contents/) + expect(() => doc.set('a', 1)).toThrow(/document value/) }) test('deleteIn', () => { @@ -415,7 +415,7 @@ describe('Document', () => { expect(doc.deleteIn([1])).toBe(false) expect(doc.deleteIn(['b', 2])).toBe(false) expect(() => doc.deleteIn(['a', 'e'])).toThrow(/Expected/) - expect(doc.contents.items).toHaveLength(1) + expect(doc.value.items).toHaveLength(1) expect((doc.get('b') as any).items).toHaveLength(1) expect(() => doc.deleteIn(null as any)).toThrow() }) @@ -425,7 +425,7 @@ describe('Document', () => { expect(doc.get('c')).toBeUndefined() }) - test('get on scalar contents', () => { + test('get on scalar value', () => { const doc = new Document('s') expect(doc.get('a')).toBeUndefined() }) @@ -450,7 +450,7 @@ describe('Document', () => { expect(doc.has('c')).toBe(false) }) - test('has on scalar contents', () => { + test('has on scalar value', () => { const doc = new Document('s') expect(doc.has('a')).toBe(false) }) @@ -468,12 +468,12 @@ describe('Document', () => { expect(doc.get('a')).toMatchObject({ value: 2 }) doc.set('c', 6) expect(doc.get('c')).toMatchObject({ value: 6 }) - expect(doc.contents.items).toHaveLength(3) + expect(doc.value.items).toHaveLength(3) }) - test('set on scalar contents', () => { + test('set on scalar value', () => { const doc = new Document('s') - expect(() => doc.set('a', 1)).toThrow(/document contents/) + expect(() => doc.set('a', 1)).toThrow(/document value/) }) test('setIn', () => { @@ -486,16 +486,16 @@ describe('Document', () => { doc.setIn(['e', 1, 'e'], 7) expect(doc.getIn(['e', 1, 'e'])).toMatchObject({ value: 7 }) expect(() => doc.setIn(['a', 'e'], 8)).toThrow(/Expected YAML collection/) - expect(doc.contents.items).toHaveLength(4) + expect(doc.value.items).toHaveLength(4) expect((doc.get('b') as any).items).toHaveLength(2) expect(String(doc)).toBe( 'a: 2\nb:\n - 2\n - 5\nc: 6\ne:\n - null\n - e: 7\n' ) }) - test('setIn on scalar contents', () => { + test('setIn on scalar value', () => { const doc = new Document('s') - expect(() => doc.setIn(['a'], 1)).toThrow(/document contents/) + expect(() => doc.setIn(['a'], 1)).toThrow(/document value/) }) test('setIn on parsed document', () => { @@ -510,10 +510,10 @@ describe('Document', () => { }) test('setIn with object key', () => { - doc.contents = doc.createNode({}) + doc.value = doc.createNode({}) const foo = { foo: 'FOO' } doc.setIn([foo], 'BAR') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, value: { value: 'BAR' } @@ -522,10 +522,10 @@ describe('Document', () => { }) test('setIn with repeated object key', () => { - doc.contents = doc.createNode({}) + doc.value = doc.createNode({}) const foo = { foo: 'FOO' } doc.setIn([foo, foo], 'BAR') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, value: { diff --git a/tests/directives.ts b/tests/directives.ts index e4bcc944..ed197f03 100644 --- a/tests/directives.ts +++ b/tests/directives.ts @@ -17,7 +17,7 @@ describe('%TAG', () => { '!': '!foo:', '!bar!': '!bar:' }) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { value: 'v1', tag: '!foo:bar' }, { value: 'v2', tag: '!bar:foo' } @@ -39,7 +39,7 @@ describe('%TAG', () => { '!': 'foo:', '!bar!': 'bar:bar#bar?' }) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { value: 'v1', tag: 'foo:bar' }, { value: 'v2', tag: 'bar:bar#bar?foo' } @@ -74,7 +74,7 @@ describe('broken directives', () => { expect(doc.errors).toMatchObject([{ pos: [10, 11] }]) }) - test('missing separator before doc contents', () => { + test('missing separator before doc value', () => { const doc = parseDocument(`%YAML 1.2\nfoo\n`) expect(doc.errors).toMatchObject([{ pos: [10, 11] }]) }) diff --git a/tests/doc/YAML-1.2.spec.ts b/tests/doc/YAML-1.2.spec.ts index 629f4d26..fcd0d6d1 100644 --- a/tests/doc/YAML-1.2.spec.ts +++ b/tests/doc/YAML-1.2.spec.ts @@ -436,7 +436,7 @@ application specific tag: !something | warnings: [['Unresolved tag: !something']], special(src) { const doc = YAML.parseDocument(src, { schema: 'yaml-1.1' }) - const data = doc.contents.items[1].value.value + const data = doc.value.items[1].value.value expect(data).toBeInstanceOf(Uint8Array) expect(data.byteLength).toBe(65) } @@ -673,7 +673,7 @@ mapping: { sky: blue, sea: green }`, tgt: [], special(src) { const doc = YAML.parseDocument(src) - expect(doc.contents.commentBefore).toBe(' Comment only.') + expect(doc.value.commentBefore).toBe(' Comment only.') } }, @@ -1717,12 +1717,12 @@ mapping: !!map ], special(src) { const doc = YAML.parseDocument, false>(src) - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.items[0].value.tag).toBe('tag:yaml.org,2002:seq') - expect(doc.contents.items[0].value.items[1].tag).toBe( + expect(doc.value.tag).toBeUndefined() + expect(doc.value.items[0].value.tag).toBe('tag:yaml.org,2002:seq') + expect(doc.value.items[0].value.items[1].tag).toBe( 'tag:yaml.org,2002:seq' ) - expect(doc.contents.items[1].value.tag).toBe('tag:yaml.org,2002:map') + expect(doc.value.items[1].value.tag).toBe('tag:yaml.org,2002:map') } } }, diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index 40a71ad7..f16338d9 100644 --- a/tests/doc/anchors.ts +++ b/tests/doc/anchors.ts @@ -5,7 +5,7 @@ test('basic', () => { const src = `- &a 1\n- *a\n` const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { anchor: 'a', value: 1 }, { source: 'a' } ]) @@ -16,7 +16,7 @@ test('re-defined anchor', () => { const src = '- &a 1\n- &a 2\n- *a\n' const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { anchor: 'a', value: 1 }, { anchor: 'a', value: 2 }, { source: 'a' } @@ -30,7 +30,7 @@ test('circular reference', () => { const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) expect(doc.warnings).toHaveLength(0) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ anchor: 'a', items: [{ value: 1 }, { source: 'a' }] }) @@ -69,7 +69,7 @@ describe('create', () => { test('doc.createAlias', () => { const doc = parseDocument('[{ a: A }, { b: B }]') const alias = doc.createAlias(doc.get(0), 'AA') - doc.contents.items.push(alias) + doc.value.items.push(alias) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }]) const alias2 = doc.createAlias(doc.get(1), 'AA') expect(doc.get(1).anchor).toBe('AA1') @@ -114,7 +114,7 @@ describe('errors', () => { test('set tag on alias', () => { const doc = parseDocument, false>('[{ a: A }, { b: B }]') - const node = doc.contents.items[0] + const node = doc.value.items[0] const alias = doc.createAlias(node, 'AA') expect(() => { alias.tag = 'tag:yaml.org,2002:alias' @@ -126,7 +126,7 @@ describe('errors', () => { '[{ a: A }, { b: B }]' ) const alias = doc.createAlias(doc.get(0), 'AA') - doc.contents.items.unshift(alias) + doc.value.items.unshift(alias) expect(() => String(doc)).toThrow( 'Unresolved alias (the anchor must be set before the alias): AA' ) @@ -167,7 +167,7 @@ describe('__proto__ as anchor name', () => { const src = `- &__proto__ 1\n- *__proto__\n` const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { anchor: '__proto__', value: 1 }, { source: '__proto__' } ]) @@ -179,8 +179,8 @@ describe('__proto__ as anchor name', () => { const doc = parseDocument, false>( '[{ a: A }, { b: B }]' ) - const alias = doc.createAlias(doc.contents.items[0], '__proto__') - doc.contents.items.push(alias) + const alias = doc.createAlias(doc.value.items[0], '__proto__') + doc.value.items.push(alias) expect(doc.toJSON()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }]) expect(String(doc)).toMatch( '[ &__proto__ { a: A }, { b: B }, *__proto__ ]\n' @@ -249,13 +249,13 @@ describe('merge <<', () => { test('YAML.parseDocument', () => { const doc = parseDocument, false>(src, { merge: true }) - expect( - doc.contents.items.slice(5).map(it => it.items[0].value) - ).toMatchObject([ - { source: 'CENTER' }, - { items: [{ source: 'CENTER' }, { source: 'BIG' }] }, - { items: [{ source: 'BIG' }, { source: 'LEFT' }, { source: 'SMALL' }] } - ]) + expect(doc.value.items.slice(5).map(it => it.items[0].value)).toMatchObject( + [ + { source: 'CENTER' }, + { items: [{ source: 'CENTER' }, { source: 'BIG' }] }, + { items: [{ source: 'BIG' }, { source: 'LEFT' }, { source: 'SMALL' }] } + ] + ) }) test('alias is associated with a sequence', () => { @@ -292,7 +292,7 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { merge: true } ) - const [a, b] = doc.contents.items + const [a, b] = doc.value.items const merge = doc.createPair('<<', doc.createAlias(a)) b.items.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) @@ -304,7 +304,7 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { customTags: ['merge'] } ) - const [a, b] = doc.contents.items + const [a, b] = doc.value.items const merge = doc.createPair('<<', doc.createAlias(a)) b.items.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) @@ -316,7 +316,7 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { merge: true } ) - const [a, b] = doc.contents.items + const [a, b] = doc.value.items const merge = doc.createPair(Symbol('<<'), doc.createAlias(a)) b.items.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) @@ -328,7 +328,7 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { merge: true } ) - const [a, b] = doc.contents.items + const [a, b] = doc.value.items const alias = doc.createAlias(a, 'AA') const merge = doc.createPair('<<', alias) b.items.push(merge) diff --git a/tests/doc/comments.ts b/tests/doc/comments.ts index 72c056ae..224d40fb 100644 --- a/tests/doc/comments.ts +++ b/tests/doc/comments.ts @@ -18,7 +18,7 @@ describe('parse comments', () => { string ` const doc = YAML.parseDocument(src) - expect(doc.contents.commentBefore).toBe('comment\ncomment') + expect(doc.value.commentBefore).toBe('comment\ncomment') expect(String(doc)).toBe(src) }) @@ -31,7 +31,7 @@ describe('parse comments', () => { string ` const doc = YAML.parseDocument(src) - expect(doc.contents.commentBefore).toBe('comment\n \ncomment') + expect(doc.value.commentBefore).toBe('comment\n \ncomment') expect(String(doc)).toBe(source` --- #comment @@ -53,31 +53,31 @@ describe('parse comments', () => { test('plain', () => { const src = '#c0\nvalue #c1\n#c2' const doc = YAML.parseDocument(src) - expect(doc.contents.commentBefore).toBe('c0') - expect(doc.contents.comment).toBe('c1') + expect(doc.value.commentBefore).toBe('c0') + expect(doc.value.comment).toBe('c1') expect(doc.comment).toBe('c2') - expect(doc.contents.value).toBe('value') - expect(doc.contents.range).toMatchObject([4, 9, 14]) + expect(doc.value.value).toBe('value') + expect(doc.value.range).toMatchObject([4, 9, 14]) }) test('"quoted"', () => { const src = '#c0\n"value" #c1\n#c2' const doc = YAML.parseDocument(src) - expect(doc.contents.commentBefore).toBe('c0') - expect(doc.contents.comment).toBe('c1') + expect(doc.value.commentBefore).toBe('c0') + expect(doc.value.comment).toBe('c1') expect(doc.comment).toBe('c2') - expect(doc.contents.value).toBe('value') - expect(doc.contents.range).toMatchObject([4, 11, 16]) + expect(doc.value.value).toBe('value') + expect(doc.value.range).toMatchObject([4, 11, 16]) }) test('block', () => { const src = '#c0\n>- #c1\n value\n#c2\n' const doc = YAML.parseDocument(src) - expect(doc.contents.commentBefore).toBe('c0') - expect(doc.contents.comment).toBe('c1') + expect(doc.value.commentBefore).toBe('c0') + expect(doc.value.comment).toBe('c1') expect(doc.comment).toBe('c2') - expect(doc.contents.value).toBe('value') - expect(doc.contents.range).toMatchObject([4, 18, 18]) + expect(doc.value.value).toBe('value') + expect(doc.value.range).toMatchObject([4, 18, 18]) }) }) @@ -94,7 +94,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - contents: { + value: { items: [ { commentBefore: 'c0', value: 'value 1', comment: 'c1' }, { value: 'value 2' } @@ -119,7 +119,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - contents: { + value: { items: [{ comment: 'c0' }, { commentBefore: 'c1\n\nc2' }] }, comment: 'c3\nc4' @@ -140,7 +140,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - contents: { + value: { items: [ { key: { commentBefore: 'c0' }, value: { comment: 'c1' } }, { key: {}, value: {} } @@ -164,7 +164,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - contents: { + value: { items: [ { value: { comment: 'c0' } }, { key: { commentBefore: 'c1\n\nc2' } } @@ -189,7 +189,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - contents: { + value: { items: [ { commentBefore: 'c0\nc1', @@ -232,7 +232,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - contents: { + value: { items: [ { key: { commentBefore: 'c0', value: 'k1' }, @@ -272,7 +272,7 @@ describe('parse comments', () => { [ a, #c0 b #c1 ]`) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { value: 'a', comment: 'c0' }, { value: 'b', comment: 'c1' } ]) @@ -284,7 +284,7 @@ describe('parse comments', () => { b: c, #c1 d #c2 }`) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: 'a', comment: 'c0' } }, { key: { value: 'b' }, value: { value: 'c', comment: 'c1' } }, { key: { value: 'd', comment: 'c2' } } @@ -293,7 +293,7 @@ describe('parse comments', () => { test('multi-line comments', () => { const doc = YAML.parseDocument('{ a,\n#c0\n#c1\nb }') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: 'a' } }, { key: { commentBefore: 'c0\nc1', value: 'b' } } ]) @@ -320,21 +320,21 @@ describe('stringify comments', () => { test('plain', () => { const src = 'string' const doc = YAML.parseDocument(src) - doc.contents.comment = 'comment' + doc.value.comment = 'comment' expect(String(doc)).toBe('string #comment\n') }) test('"quoted"', () => { const src = '"string\\u0000"' const doc = YAML.parseDocument(src) - doc.contents.comment = 'comment' + doc.value.comment = 'comment' expect(String(doc)).toBe('"string\\0" #comment\n') }) test('block', () => { const src = '>\nstring\n' const doc = YAML.parseDocument(src) - doc.contents.comment = 'comment' + doc.value.comment = 'comment' expect(String(doc)).toBe('> #comment\nstring\n') }) }) @@ -343,21 +343,21 @@ describe('stringify comments', () => { test('plain', () => { const src = 'string' const doc = YAML.parseDocument(src) - doc.contents.comment = 'comment\nlines' + doc.value.comment = 'comment\nlines' expect(String(doc)).toBe('string\n#comment\n#lines\n') }) test('"quoted"', () => { const src = '"string\\u0000"' const doc = YAML.parseDocument(src) - doc.contents.comment = 'comment\nlines' + doc.value.comment = 'comment\nlines' expect(String(doc)).toBe('"string\\0"\n#comment\n#lines\n') }) test('block', () => { const src = '>\nstring\n' const doc = YAML.parseDocument(src) - doc.contents.comment = 'comment\nlines' + doc.value.comment = 'comment\nlines' expect(String(doc)).toBe('> #comment lines\nstring\n') }) }) @@ -385,10 +385,10 @@ describe('stringify comments', () => { test('plain', () => { const src = '- value 1\n- value 2\n' const doc = YAML.parseDocument, false>(src) - doc.contents.commentBefore = 'c0' - doc.contents.items[0].commentBefore = 'c1' - doc.contents.items[1].commentBefore = 'c2' - doc.contents.comment = 'c3' + doc.value.commentBefore = 'c0' + doc.value.items[0].commentBefore = 'c1' + doc.value.items[1].commentBefore = 'c2' + doc.value.comment = 'c3' expect(String(doc)).toBe(source` #c0 #c1 @@ -402,9 +402,9 @@ describe('stringify comments', () => { test('multiline', () => { const src = '- value 1\n- value 2\n' const doc = YAML.parseDocument, false>(src) - doc.contents.items[0].commentBefore = 'c0\nc1' - doc.contents.items[1].commentBefore = ' \nc2\n\nc3' - doc.contents.comment = 'c4\nc5' + doc.value.items[0].commentBefore = 'c0\nc1' + doc.value.items[1].commentBefore = ' \nc2\n\nc3' + doc.value.comment = 'c4\nc5' expect(String(doc)).toBe(source` #c0 #c1 @@ -425,9 +425,9 @@ describe('stringify comments', () => { YAML.YAMLMap>, false >(src) - doc.contents.items[0].key.commentBefore = 'c0' - doc.contents.items[0].key.comment = 'c1' - const seq = doc.contents.items[0].value! + doc.value.items[0].key.commentBefore = 'c0' + doc.value.items[0].key.comment = 'c1' + const seq = doc.value.items[0].value! seq.commentBefore = 'c2' seq.items[0].commentBefore = 'c3' seq.items[1].commentBefore = 'c4' @@ -448,9 +448,9 @@ describe('stringify comments', () => { const doc = YAML.parseDocument, false>( '- a\n- b\n' ) - doc.contents.commentBefore = 'c0' - doc.contents.items[0].commentBefore = 'c1' - doc.contents.items[1].commentBefore = 'c2\nc3' + doc.value.commentBefore = 'c0' + doc.value.items[0].commentBefore = 'c1' + doc.value.items[1].commentBefore = 'c2\nc3' expect(doc.toString({ commentString: str => str.replace(/^/gm, '// ') })) .toBe(source` // c0 @@ -470,11 +470,11 @@ describe('stringify comments', () => { YAML.YAMLMap, false >(src) - doc.contents.items[0].key.commentBefore = 'c0' - doc.contents.items[1].key.commentBefore = 'c1' - doc.contents.items[1].key.comment = 'c2' - doc.contents.items[1].value!.spaceBefore = true - doc.contents.comment = 'c3' + doc.value.items[0].key.commentBefore = 'c0' + doc.value.items[1].key.commentBefore = 'c1' + doc.value.items[1].key.comment = 'c2' + doc.value.items[1].value!.spaceBefore = true + doc.value.comment = 'c3' expect(String(doc)).toBe(source` #c0 key1: value 1 @@ -492,12 +492,12 @@ describe('stringify comments', () => { YAML.YAMLMap, false >(src) - doc.contents.items[0].key.commentBefore = 'c0\nc1' - doc.contents.items[1].key.commentBefore = ' \nc2\n\nc3' - doc.contents.items[1].key.comment = 'c4\nc5' - doc.contents.items[1].value!.spaceBefore = true - doc.contents.items[1].value!.commentBefore = 'c6' - doc.contents.comment = 'c7\nc8' + doc.value.items[0].key.commentBefore = 'c0\nc1' + doc.value.items[1].key.commentBefore = ' \nc2\n\nc3' + doc.value.items[1].key.comment = 'c4\nc5' + doc.value.items[1].value!.spaceBefore = true + doc.value.items[1].value!.commentBefore = 'c6' + doc.value.comment = 'c7\nc8' expect(String(doc)).toBe(source` #c0 #c1 @@ -636,25 +636,25 @@ describe('blank lines', () => { }) describe('drop trailing blank lines', () => { - test('empty contents', () => { + test('empty value', () => { const src = '\n\n\n' const doc = YAML.parseDocument(src) expect(String(doc)).toBe('null\n') }) - test('scalar contents', () => { + test('scalar value', () => { const src = 'str\n\n\n' const doc = YAML.parseDocument(src) expect(String(doc)).toBe('str\n') }) - test('seq contents', () => { + test('seq value', () => { const src = '- a\n- b\n\n\n' const doc = YAML.parseDocument(src) expect(String(doc)).toBe('- a\n- b\n') }) - test('empty/comment contents', () => { + test('empty/comment value', () => { const src = '#cc\n\n\n' const doc = YAML.parseDocument(src) expect(String(doc)).toBe('#cc\n\nnull\n') @@ -675,7 +675,7 @@ describe('blank lines', () => { test('before first node in document with directives', () => { const doc = YAML.parseDocument('str\n') - doc.contents.spaceBefore = true + doc.value.spaceBefore = true expect(doc.toString({ directives: true })).toBe('---\n\nstr\n') }) @@ -745,7 +745,7 @@ describe('blank lines', () => { test(name, () => { const doc = YAML.parseDocument(src) expect(String(doc)).toBe(src) - let it = doc.contents.items[1] + let it = doc.value.items[1] if (it.key) it = it.key expect(it).not.toHaveProperty('spaceBefore', true) it.spaceBefore = true @@ -755,12 +755,12 @@ describe('blank lines', () => { }) } - test('as contents', () => { + test('as value', () => { const src = '|+\n a\n\n#c\n' const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ comment: 'c', - contents: { value: 'a\n\n' } + value: { value: 'a\n\n' } }) expect(String(doc)).toBe(src) doc.comment = '\n\nc' @@ -771,7 +771,7 @@ describe('blank lines', () => { test('before block map values', () => { const src = 'a:\n\n 1\nb:\n\n #c\n 2\n' const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a' }, @@ -806,7 +806,7 @@ describe('blank lines', () => { test('flow seq', () => { const src = '[1,\n\n2,\n3,\n\n4\n\n]' const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { value: 1 }, { value: 2, spaceBefore: true }, @@ -820,7 +820,7 @@ describe('blank lines', () => { test('flow map', () => { const src = '{\n\na: 1,\n\nb: 2 }' const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a', spaceBefore: true }, value: { value: 1 } }, { key: { value: 'b', spaceBefore: true }, value: { value: 2 } } @@ -831,7 +831,7 @@ describe('blank lines', () => { test('flow map value comments & spaces', () => { const src = '{\n a:\n #c\n 1,\n b:\n\n #d\n 2\n}\n' const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a' }, @@ -931,7 +931,7 @@ map: # c4 ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { value: { commentBefore: ' c1\n \n c2\n', comment: ' c3\n \n c4' } } ] @@ -963,7 +963,7 @@ map: # c4 ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { value: { commentBefore: ' c1\n\n c2\n', comment: ' c3\n\n c4' } } ] @@ -993,7 +993,7 @@ map: - v2 ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { commentBefore: ' c1\n \n c2', comment: ' c3\n ' }, { commentBefore: ' c4' } @@ -1023,7 +1023,7 @@ map: - v2 ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { commentBefore: ' c1\n\n c2', comment: ' c3' }, { commentBefore: ' c4', spaceBefore: true } @@ -1046,7 +1046,7 @@ map: const doc = YAML.parseDocument, false>( '- v1\n- v2\n' ) - const [v1, v2] = doc.contents.items + const [v1, v2] = doc.value.items v1.commentBefore = '\n' v1.comment = '\n' v2.commentBefore = '\n' @@ -1066,7 +1066,7 @@ map: YAML.YAMLMap, false >('k1: v1\nk2: v2') - const [p1, p2] = doc.contents.items + const [p1, p2] = doc.value.items p1.key.commentBefore = '\n' p1.value!.commentBefore = '\n' p1.value!.comment = '\n' @@ -1189,7 +1189,7 @@ describe('collection end comments', () => { - d ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { items: [{ value: 'a' }, { value: 'b' }], comment: '1\n\n2' }, { value: 'd' } @@ -1217,7 +1217,7 @@ describe('collection end comments', () => { - d ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { items: [ @@ -1252,7 +1252,7 @@ describe('collection end comments', () => { d: 1 ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a' }, @@ -1285,7 +1285,7 @@ describe('collection end comments', () => { d: 1 ` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a' }, @@ -1322,7 +1322,7 @@ a: #2 - e\n` const doc = YAML.parseDocument(src) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a' }, diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 11abc12b..9635e11e 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -56,7 +56,7 @@ describe('arrays', () => { const s = doc.createNode([true]) expect(s).toBeInstanceOf(YAMLSeq) expect(s.items).toMatchObject([{ value: true }]) - doc.contents = s + doc.value = s expect(String(doc)).toBe('- true\n') }) @@ -65,7 +65,7 @@ describe('arrays', () => { const s = doc.createNode([true], { flow: true }) expect(s).toBeInstanceOf(YAMLSeq) expect(s.items).toMatchObject([{ value: true }]) - doc.contents = s + doc.value = s expect(String(doc)).toBe('[ true ]\n') }) @@ -81,13 +81,13 @@ describe('arrays', () => { expect(s.items[1].items[0].value).toBe('four') expect(s.items[1].items[1].value).toBe(5) }) - test('set doc contents', () => { + test('set doc value', () => { const res = '- 3\n- - four\n - 5\n' const doc = new Document(array) expect(String(doc)).toBe(res) - doc.contents = array as any + doc.value = array as any expect(String(doc)).toBe(res) - doc.contents = doc.createNode(array) + doc.value = doc.createNode(array) expect(String(doc)).toBe(res) }) }) @@ -107,7 +107,7 @@ describe('objects', () => { expect(s.items).toMatchObject([ { key: { value: 'x' }, value: { value: true } } ]) - doc.contents = s + doc.value = s expect(String(doc)).toBe('x: true\n') }) @@ -118,7 +118,7 @@ describe('objects', () => { expect(s.items).toMatchObject([ { key: { value: 'x' }, value: { value: true } } ]) - doc.contents = s + doc.value = s expect(String(doc)).toBe('{ x: true }\n') }) @@ -171,7 +171,7 @@ describe('objects', () => { } ]) }) - test('set doc contents', () => { + test('set doc value', () => { const res = `x: 3 y: - 4 @@ -180,9 +180,9 @@ z: v: 6\n` const doc = new Document(object) expect(String(doc)).toBe(res) - doc.contents = object as any + doc.value = object as any expect(String(doc)).toBe(res) - doc.contents = doc.createNode(object) + doc.value = doc.createNode(object) expect(String(doc)).toBe(res) }) }) @@ -211,13 +211,13 @@ describe('Set', () => { expect(s.items[1].items[0].value).toBe('four') expect(s.items[1].items[1].value).toBe(5) }) - test('set doc contents', () => { + test('set doc value', () => { const res = '- 3\n- - four\n - 5\n' const doc = new Document(set) expect(String(doc)).toBe(res) - doc.contents = set as any + doc.value = set as any expect(String(doc)).toBe(res) - doc.contents = doc.createNode(set) + doc.value = doc.createNode(set) expect(String(doc)).toBe(res) }) test('Schema#createNode() - YAML 1.2', () => { @@ -284,7 +284,7 @@ describe('Map', () => { } ]) }) - test('set doc contents', () => { + test('set doc value', () => { const res = `x: 3 y: - 4 @@ -293,9 +293,9 @@ y: : z\n` const doc = new Document(map) expect(String(doc)).toBe(res) - doc.contents = map as any + doc.value = map as any expect(String(doc)).toBe(res) - doc.contents = doc.createNode(map) + doc.value = doc.createNode(map) expect(String(doc)).toBe(res) }) }) @@ -341,7 +341,7 @@ describe('circular references', () => { const map: any = { foo: 'bar' } map.map = map const doc = new Document(map) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ anchor: 'a1', items: [ { key: { value: 'foo' }, value: { value: 'bar' } }, @@ -380,7 +380,7 @@ describe('circular references', () => { const two = ['two'] const seq = [one, two, one, one, two] const doc = new Document(seq) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { items: [{ value: 'one' }] }, { items: [{ value: 'two' }] }, diff --git a/tests/doc/errors.ts b/tests/doc/errors.ts index 9691218f..a518d50c 100644 --- a/tests/doc/errors.ts +++ b/tests/doc/errors.ts @@ -66,7 +66,7 @@ describe('block collections', () => { expect(doc.errors[0].message).toMatch( 'All mapping items must start at the same column' ) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'foo' }, value: { value: '1' } }, { key: { value: 'bar' }, value: { value: 2 } } @@ -81,7 +81,7 @@ describe('block collections', () => { expect(doc.errors[0].message).toMatch( 'All sequence items must start at the same column' ) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [{ value: 'foo' }, { items: [{ value: 'bar' }] }] }) }) @@ -94,7 +94,7 @@ describe('block collections', () => { { code: 'UNEXPECTED_TOKEN' }, { code: 'MISSING_CHAR' } ]) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'foo' }, value: { value: '1' } }, { key: { value: null }, value: null } @@ -430,7 +430,7 @@ test('broken document with comment before first node', () => { test('multiple tags on one node', () => { const doc = YAML.parseDocument('!foo !bar baz\n') - expect(doc.contents).toMatchObject({ value: 'baz', type: 'PLAIN' }) + expect(doc.value).toMatchObject({ value: 'baz', type: 'PLAIN' }) expect(doc.errors).toHaveLength(1) expect(doc.warnings).toHaveLength(1) }) diff --git a/tests/doc/foldFlowLines.ts b/tests/doc/foldFlowLines.ts index 5636a296..c95bb1c9 100644 --- a/tests/doc/foldFlowLines.ts +++ b/tests/doc/foldFlowLines.ts @@ -160,7 +160,7 @@ describe('double-quoted', () => { const str = YAML.stringify({ x }) const doc = YAML.parseDocument(str) expect(doc.errors).toHaveLength(0) - expect(doc.contents.items[0].value.value).toBe(x) + expect(doc.value.items[0].value.value).toBe(x) }) }) @@ -188,7 +188,7 @@ describe('double-quoted', () => { const str = YAML.stringify({ key: [[value]] }) const doc = YAML.parseDocument(str) expect(doc.errors).toHaveLength(0) - expect(doc.contents.items[0].value.items[0].items[0].value).toBe(value) + expect(doc.value.items[0].value.items[0].items[0].value).toBe(value) }) }) @@ -304,7 +304,7 @@ describe('end-to-end', () => { Unfolded paragraph. ` const doc = YAML.parseDocument(src) - expect(doc.contents.value).toBe(source` + expect(doc.value.value).toBe(source` Text on a line that should get folded with a line width of 20 characters. Indented text @@ -332,10 +332,10 @@ describe('end-to-end', () => { - plain with comment # that won't get folded ` const doc = YAML.parseDocument, false>(src) - expect(doc.contents.items[0].value).toBe( + expect(doc.value.items[0].value).toBe( 'plain value with enough length to fold twice' ) - expect(doc.contents.items[1].value).toBe('plain with comment') + expect(doc.value.items[1].value).toBe('plain with comment') expect(doc.toString(foldOptions)).toBe(src) }) diff --git a/tests/doc/parse.ts b/tests/doc/parse.ts index a2b602b7..28edc02f 100644 --- a/tests/doc/parse.ts +++ b/tests/doc/parse.ts @@ -175,7 +175,7 @@ describe('flow collection keys', () => { test('block map with flow collection key as explicit key', () => { const doc = YAML.parseDocument(`? []: x`) expect(doc.errors).toHaveLength(0) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { @@ -194,7 +194,7 @@ describe('flow collection keys', () => { c: d `) expect(doc.errors).toHaveLength(0) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'a' }, @@ -217,7 +217,7 @@ describe('flow collection keys', () => { c: d `) expect(doc.errors).toHaveLength(0) - expect(doc.contents).toMatchObject({ + expect(doc.value).toMatchObject({ items: [ { key: { value: 'x' }, value: { value: 'y' } }, { @@ -235,14 +235,14 @@ describe('flow collection keys', () => { test('empty scalar as last flow collection value (#550)', () => { const doc = YAML.parseDocument('{c:}') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: 'c' }, value: { value: null } } ]) }) test('plain key with no space before flow collection value (#550)', () => { const doc = YAML.parseDocument('{c:[]}') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: 'c' }, value: { items: [] } } ]) }) @@ -321,8 +321,8 @@ describe('empty(ish) nodes', () => { const src = '{ ? : 123 }' const doc = YAML.parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.contents.items[0].key.value).toBeNull() - expect(doc.contents.items[0].value.value).toBe(123) + expect(doc.value.items[0].key.value).toBeNull() + expect(doc.value.items[0].value.value).toBe(123) }) describe('comment on empty pair value (#19)', () => { @@ -358,7 +358,7 @@ describe('empty(ish) nodes', () => { test('empty node position', () => { const doc = YAML.parseDocument('\r\na: # 123\r\n') - const empty = doc.contents.items[0].value + const empty = doc.value.items[0].value expect(empty.range).toEqual([5, 5, 12]) }) @@ -393,7 +393,7 @@ describe('maps with no values', () => { const src = `{\na: null,\n? b\n}` const doc = YAML.parseDocument(src) expect(String(doc)).toBe(`{ a: null, b: }\n`) - doc.contents.items[1].key.comment = 'c' + doc.value.items[1].key.comment = 'c' expect(String(doc)).toBe(`{\n a: null,\n b: #c\n}\n`) doc.set('b', 'x') expect(String(doc)).toBe(`{\n a: null,\n b: #c\n x\n}\n`) @@ -412,7 +412,7 @@ describe('maps with no values', () => { test('implicit scalar key after explicit key with no value', () => { const doc = YAML.parseDocument('? - 1\nx:\n') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { items: [{ value: 1 }] }, value: null }, { key: { value: 'x' }, value: { value: null } } ]) @@ -420,7 +420,7 @@ describe('maps with no values', () => { test('implicit flow collection key after explicit key with no value', () => { const doc = YAML.parseDocument('? - 1\n[x]: y\n') - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { items: [{ value: 1 }] }, value: null }, { key: { items: [{ value: 'x' }] }, value: { value: 'y' } } ]) @@ -431,7 +431,7 @@ describe('odd indentations', () => { test('Block map with empty explicit key (#551)', () => { const doc = YAML.parseDocument('?\n? a') expect(doc.errors).toHaveLength(0) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: null }, value: null }, { key: { value: 'a' }, value: null } ]) @@ -455,7 +455,7 @@ describe('odd indentations', () => { test('comment after top-level block scalar with indentation indicator (#547)', () => { const doc = YAML.parseDocument('|1\n x\n#c') expect(doc.errors).toHaveLength(0) - expect(doc.contents).toMatchObject({ value: 'x\n' }) + expect(doc.value).toMatchObject({ value: 'x\n' }) }) test('tab after indent spaces for flow-in-block (#604)', () => { @@ -691,8 +691,8 @@ describe('keepSourceTokens', () => { ]) { test(`${type}: default false`, () => { const doc = YAML.parseDocument(src) - expect(doc.contents).not.toHaveProperty('srcToken') - expect(doc.contents.items[0]).not.toHaveProperty('srcToken') + expect(doc.value).not.toHaveProperty('srcToken') + expect(doc.value.items[0]).not.toHaveProperty('srcToken') expect(doc.get('foo')).not.toHaveProperty('srcToken') }) @@ -700,8 +700,8 @@ describe('keepSourceTokens', () => { const doc = YAML.parseDocument(src, { keepSourceTokens: true }) - expect(doc.contents.srcToken).toMatchObject({ type }) - expect(doc.contents.items[0].srcToken).toMatchObject({ + expect(doc.value.srcToken).toMatchObject({ type }) + expect(doc.value.items[0].srcToken).toMatchObject({ key: { type: 'scalar' }, value: { type: 'scalar' } }) @@ -712,7 +712,7 @@ describe('keepSourceTokens', () => { test('allow for CST modifications (#903)', () => { const src = 'foo:\n [ 42 ]' const tokens = Array.from(new YAML.Parser().parse(src)) - const docs = new YAML.Composer({ + const docs = new YAML.Composer({ keepSourceTokens: true }).compose(tokens) const doc = Array.from(docs)[0] @@ -923,7 +923,7 @@ describe('stringKeys', () => { `, { stringKeys: true } ) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { key: { value: 'x' }, value: { value: 'x' } }, { key: { value: 'y' }, value: { value: 'y' } }, { key: { value: '42' }, value: { value: 42 } }, diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 3cc56047..f91e565d 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -49,29 +49,29 @@ for (const [name, version] of [ test('float with trailing zeros', () => { const doc = new YAML.Document(3, { version }) - doc.contents.minFractionDigits = 2 + doc.value.minFractionDigits = 2 expect(String(doc)).toBe('3.00\n') }) test('scientific float ignores minFractionDigits', () => { const doc = new YAML.Document(3, { version }) - doc.contents.format = 'EXP' - doc.contents.minFractionDigits = 2 + doc.value.format = 'EXP' + doc.value.minFractionDigits = 2 expect(String(doc)).toBe('3e+0\n') }) test('integer with HEX format', () => { const doc = new YAML.Document(42, { version }) - doc.contents.format = 'HEX' + doc.value.format = 'HEX' expect(String(doc)).toBe('0x2a\n') }) test('float with HEX format', () => { const doc = new YAML.Document(4.2, { version }) - doc.contents.format = 'HEX' + doc.value.format = 'HEX' expect(String(doc)).toBe('4.2\n') }) test('negative integer with HEX format', () => { const doc = new YAML.Document(-42, { version }) - doc.contents.format = 'HEX' + doc.value.format = 'HEX' const exp = version === '1.2' ? '-42\n' : '-0x2a\n' expect(String(doc)).toBe(exp) }) @@ -86,14 +86,14 @@ for (const [name, version] of [ const doc = new YAML.Document(BigInt('42'), { version }) - doc.contents.format = 'HEX' + doc.value.format = 'HEX' expect(String(doc)).toBe('0x2a\n') }) test('BigInt with OCT format', () => { const doc = new YAML.Document(BigInt('42'), { version }) - doc.contents.format = 'OCT' + doc.value.format = 'OCT' const exp = version === '1.2' ? '0o52\n' : '052\n' expect(String(doc)).toBe(exp) }) @@ -101,7 +101,7 @@ for (const [name, version] of [ const doc = new YAML.Document(BigInt('-42'), { version }) - doc.contents.format = 'OCT' + doc.value.format = 'OCT' const exp = version === '1.2' ? '-42\n' : '-052\n' expect(String(doc)).toBe(exp) }) @@ -150,7 +150,7 @@ blah blah\n`) YAML.YAMLMap, false >({ foo }, { version }) - for (const node of doc.contents.items) + for (const node of doc.value.items) node.value!.type = Scalar.QUOTE_DOUBLE expect( doc @@ -165,7 +165,7 @@ blah blah\n`) const doc = new YAML.Document, false>([foo], { version }) - for (const node of doc.contents.items) node.type = Scalar.QUOTE_DOUBLE + for (const node of doc.value.items) node.type = Scalar.QUOTE_DOUBLE expect( doc .toString(opt) @@ -180,7 +180,7 @@ blah blah\n`) YAML.YAMLMap>, false >({ foo: [foo] }, { version }) - const seq = doc.contents.items[0].value! + const seq = doc.value.items[0].value! for (const node of seq.items) node.type = Scalar.QUOTE_DOUBLE expect( doc @@ -334,7 +334,7 @@ z: const doc = new YAML.Document({ x: 3, y: 4 }) expect(String(doc)).toBe('x: 3\ny: 4\n') // @ts-expect-error This should fail. - doc.contents.items.push('TEST') + doc.value.items.push('TEST') expect(() => String(doc)).toThrow(/^Map items must all be pairs.*TEST/) }) @@ -368,7 +368,7 @@ z: test('Block map, with key.comment', () => { const doc = getDoc() doc.set('a', new Scalar(null)) - doc.contents.items[0].key.comment = 'c' + doc.value.items[0].key.comment = 'c' expect(doc.toString({ nullStr: '' })).toBe('a: #c\nb:\n') }) @@ -381,20 +381,20 @@ z: test('Flow map, no comments', () => { const doc = getDoc() - doc.contents.flow = true + doc.value.flow = true expect(doc.toString({ nullStr: '' })).toBe('{ a:, b: }\n') }) test('Flow map, with key.comment', () => { const doc = getDoc() - doc.contents.flow = true - doc.contents.items[0].key.comment = 'c' + doc.value.flow = true + doc.value.items[0].key.comment = 'c' expect(doc.toString({ nullStr: '' })).toBe('{\n a:, #c\n b:\n}\n') }) test('Flow map, with value.commentBefore', () => { const doc = getDoc() - doc.contents.flow = true + doc.value.flow = true doc.set('a', new Scalar(null)) doc.get('a').commentBefore = 'c' expect(doc.toString({ nullStr: '' })).toBe( @@ -500,7 +500,7 @@ z: expect(String(doc)).toBe(src) }) test('explicit tag on empty mapping', () => { - const doc = new YAML.Document({ key: {} }) + const doc = new YAML.Document({ key: {} }) doc.get('key').tag = '!tag' expect(String(doc)).toBe(source` key: !tag {} @@ -560,7 +560,7 @@ test('Quoting item markers (#52)', () => { const str = String(doc) expect(() => YAML.parse(str)).not.toThrow() expect(str).toBe('key: "-"\n') - doc.contents = doc.createNode({ key: '?' }) + doc.value = doc.createNode({ key: '?' }) const str2 = String(doc) expect(() => YAML.parse(str2)).not.toThrow() expect(str2).toBe('key: "?"\n') @@ -730,14 +730,14 @@ describe('simple keys', () => { test('key with block scalar value', () => { const doc = YAML.parseDocument('foo: bar') - doc.contents.items[0].key.type = 'BLOCK_LITERAL' + doc.value.items[0].key.type = 'BLOCK_LITERAL' expect(doc.toString()).toBe('? |-\n foo\n: bar\n') expect(doc.toString({ simpleKeys: true })).toBe('"foo": bar\n') }) test('key with comment', () => { const doc = YAML.parseDocument('foo: bar') - doc.contents.items[0].key.comment = 'FOO' + doc.value.items[0].key.comment = 'FOO' expect(doc.toString()).toBe('foo: #FOO\n bar\n') expect(() => doc.toString({ simpleKeys: true })).toThrow( /With simple keys, key nodes cannot have comments/ @@ -754,7 +754,7 @@ describe('simple keys', () => { test('key with JS object value', () => { const doc = YAML.parseDocument('[foo]: bar') - doc.contents.items[0].key = { foo: 42 } + doc.value.items[0].key = { foo: 42 } expect(doc.toString()).toBe('? foo: 42\n: bar\n') expect(() => doc.toString({ simpleKeys: true })).toThrow( /With simple keys, collection cannot be used as a key value/ @@ -763,7 +763,7 @@ describe('simple keys', () => { test('key with JS null value', () => { const doc = YAML.parseDocument('[foo]: bar') - doc.contents.items[0].key = null + doc.value.items[0].key = null expect(doc.toString()).toBe('? null\n: bar\n') expect(() => doc.toString({ simpleKeys: true })).toThrow( /With simple keys, collection cannot be used as a key value/ @@ -963,11 +963,11 @@ describe('collectionStyle', () => { const doc = new YAML.Document({ foo: ['bar'] }) expect(doc.toString()).toBe('foo:\n - bar\n') - doc.contents.flow = false + doc.value.flow = false doc.get('foo').flow = true expect(doc.toString()).toBe('foo: [ bar ]\n') - doc.contents.flow = true + doc.value.flow = true doc.get('foo').flow = false expect(doc.toString()).toBe('{ foo: [ bar ] }\n') }) @@ -976,11 +976,11 @@ describe('collectionStyle', () => { const doc = new YAML.Document({ foo: ['bar'] }) expect(doc.toString({ collectionStyle: 'any' })).toBe('foo:\n - bar\n') - doc.contents.flow = false + doc.value.flow = false doc.get('foo').flow = true expect(doc.toString({ collectionStyle: 'any' })).toBe('foo: [ bar ]\n') - doc.contents.flow = true + doc.value.flow = true doc.get('foo').flow = false expect(doc.toString({ collectionStyle: 'any' })).toBe('{ foo: [ bar ] }\n') }) @@ -989,11 +989,11 @@ describe('collectionStyle', () => { const doc = new YAML.Document({ foo: ['bar'] }) expect(doc.toString({ collectionStyle: 'block' })).toBe('foo:\n - bar\n') - doc.contents.flow = false + doc.value.flow = false doc.get('foo').flow = true expect(doc.toString({ collectionStyle: 'block' })).toBe('foo:\n - bar\n') - doc.contents.flow = true + doc.value.flow = true doc.get('foo').flow = false expect(doc.toString({ collectionStyle: 'block' })).toBe('foo:\n - bar\n') }) @@ -1005,7 +1005,7 @@ describe('collectionStyle', () => { doc.get('foo').flow = true expect(doc.toString({ collectionStyle: 'flow' })).toBe('{ foo: [ bar ] }\n') - doc.contents.flow = true + doc.value.flow = true doc.get('foo').flow = false expect(doc.toString({ collectionStyle: 'flow' })).toBe('{ foo: [ bar ] }\n') }) @@ -1060,7 +1060,7 @@ describe('Scalar options', () => { defaultKeyType: Scalar.QUOTE_SINGLE } as const const doc = new YAML.Document({ foo: null }) - const key = doc.contents.items[0].key as Scalar + const key = doc.value.items[0].key as Scalar key.type = Scalar.BLOCK_LITERAL expect(doc.toString(opt)).toBe('? "foo"\n') }) @@ -1202,7 +1202,7 @@ describe('Document markers in top-level scalars', () => { test("'foo\\n...'", () => { const doc = new YAML.Document('foo\n...') - doc.contents.type = Scalar.QUOTE_SINGLE + doc.value.type = Scalar.QUOTE_SINGLE const str = String(doc) expect(str).toBe("'foo\n\n ...'\n") expect(YAML.parse(str)).toBe('foo\n...') @@ -1210,7 +1210,7 @@ describe('Document markers in top-level scalars', () => { test('"foo\\n..."', () => { const doc = new YAML.Document('foo\n...') - doc.contents.type = Scalar.QUOTE_DOUBLE + doc.value.type = Scalar.QUOTE_DOUBLE const str = doc.toString({ doubleQuotedMinMultiLineLength: 0 }) expect(str).toBe('"foo\n\n ..."\n') expect(YAML.parse(str)).toBe('foo\n...') @@ -1446,9 +1446,9 @@ describe('YAML.stringify on ast Document', () => { describe('flow collection padding', () => { const doc = new YAML.Document() - doc.contents = new YAML.YAMLSeq() - doc.contents.items = [new Scalar(1), new Scalar(2)] - doc.contents.flow = true + doc.value = new YAML.YAMLSeq() + doc.value.items = [new Scalar(1), new Scalar(2)] + doc.value.flow = true test('default', () => { expect(doc.toString()).toBe('[ 1, 2 ]\n') diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 3da9cb8d..e46eaf60 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -2,10 +2,9 @@ import { type CollectionTag, Document, type DocumentOptions, - type Node, + type DocValue, parseDocument as origParseDocument, parse, - type ParsedNode, type ParseOptions, Scalar, type ScalarTag, @@ -18,7 +17,7 @@ import { import { seqTag, stringifyString, stringTag } from 'yaml/util' import { source } from '../_utils.ts' -const parseDocument = ( +const parseDocument = ( source: string, options?: ParseOptions & DocumentOptions & SchemaOptions ) => origParseDocument(source, options) @@ -27,66 +26,66 @@ describe('tags', () => { describe('implicit tags', () => { test('plain string', () => { const doc = parseDocument('foo') - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.value).toBe('foo') + expect(doc.value.tag).toBeUndefined() + expect(doc.value.value).toBe('foo') }) test('quoted string', () => { const doc = parseDocument('"foo"') - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.value).toBe('foo') + expect(doc.value.tag).toBeUndefined() + expect(doc.value.value).toBe('foo') }) test('flow map', () => { const doc = parseDocument('{ foo }') - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.toJSON()).toMatchObject({ foo: null }) + expect(doc.value.tag).toBeUndefined() + expect(doc.value.toJSON()).toMatchObject({ foo: null }) }) test('flow seq', () => { const doc = parseDocument('[ foo ]') - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.toJSON()).toMatchObject(['foo']) + expect(doc.value.tag).toBeUndefined() + expect(doc.value.toJSON()).toMatchObject(['foo']) }) test('block map', () => { const doc = parseDocument('foo:\n') - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.toJSON()).toMatchObject({ foo: null }) + expect(doc.value.tag).toBeUndefined() + expect(doc.value.toJSON()).toMatchObject({ foo: null }) }) test('block seq', () => { const doc = parseDocument('- foo') - expect(doc.contents.tag).toBeUndefined() - expect(doc.contents.toJSON()).toMatchObject(['foo']) + expect(doc.value.tag).toBeUndefined() + expect(doc.value.toJSON()).toMatchObject(['foo']) }) }) describe('explicit tags', () => { test('plain string', () => { const doc = parseDocument('!!str foo') - expect(doc.contents.tag).toBe('tag:yaml.org,2002:str') - expect(doc.contents.value).toBe('foo') + expect(doc.value.tag).toBe('tag:yaml.org,2002:str') + expect(doc.value.value).toBe('foo') }) test('quoted string', () => { const doc = parseDocument('!!str "foo"') - expect(doc.contents.tag).toBe('tag:yaml.org,2002:str') - expect(doc.contents.value).toBe('foo') + expect(doc.value.tag).toBe('tag:yaml.org,2002:str') + expect(doc.value.value).toBe('foo') }) test('flow map', () => { const doc = parseDocument('!!map { foo }') - expect(doc.contents.tag).toBe('tag:yaml.org,2002:map') - expect(doc.contents.toJSON()).toMatchObject({ foo: null }) + expect(doc.value.tag).toBe('tag:yaml.org,2002:map') + expect(doc.value.toJSON()).toMatchObject({ foo: null }) }) test('flow seq', () => { const doc = parseDocument('!!seq [ foo ]') - expect(doc.contents.tag).toBe('tag:yaml.org,2002:seq') - expect(doc.contents.toJSON()).toMatchObject(['foo']) + expect(doc.value.tag).toBe('tag:yaml.org,2002:seq') + expect(doc.value.toJSON()).toMatchObject(['foo']) }) test('block map', () => { const doc = parseDocument('!!map\nfoo:\n') - expect(doc.contents.tag).toBe('tag:yaml.org,2002:map') - expect(doc.contents.toJSON()).toMatchObject({ foo: null }) + expect(doc.value.tag).toBe('tag:yaml.org,2002:map') + expect(doc.value.toJSON()).toMatchObject({ foo: null }) }) test('block seq', () => { const doc = parseDocument('!!seq\n- foo') - expect(doc.contents.tag).toBe('tag:yaml.org,2002:seq') - expect(doc.contents.toJSON()).toMatchObject(['foo']) + expect(doc.value.tag).toBe('tag:yaml.org,2002:seq') + expect(doc.value.toJSON()).toMatchObject(['foo']) }) }) @@ -144,7 +143,7 @@ describe('number types', () => { intAsBigInt: false, version: '1.1' }) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { value: 10, format: 'BIN' }, { value: 83, format: 'OCT' }, { value: -0, format: 'OCT' }, @@ -156,10 +155,10 @@ describe('number types', () => { { value: 0.42 }, { value: 0.4 } ]) - expect(doc.contents.items[3]).not.toHaveProperty('format') - expect(doc.contents.items[6]).not.toHaveProperty('format') - expect(doc.contents.items[6]).not.toHaveProperty('minFractionDigits') - expect(doc.contents.items[7]).not.toHaveProperty('format') + expect(doc.value.items[3]).not.toHaveProperty('format') + expect(doc.value.items[6]).not.toHaveProperty('format') + expect(doc.value.items[6]).not.toHaveProperty('minFractionDigits') + expect(doc.value.items[7]).not.toHaveProperty('format') }) test('Version 1.2', () => { @@ -177,7 +176,7 @@ describe('number types', () => { intAsBigInt: false, version: '1.2' }) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { value: 83, format: 'OCT' }, { value: 0, format: 'OCT' }, { value: 123456 }, @@ -188,10 +187,10 @@ describe('number types', () => { { value: 0.42 }, { value: 0.4 } ]) - expect(doc.contents.items[2]).not.toHaveProperty('format') - expect(doc.contents.items[5]).not.toHaveProperty('format') - expect(doc.contents.items[5]).not.toHaveProperty('minFractionDigits') - expect(doc.contents.items[6]).not.toHaveProperty('format') + expect(doc.value.items[2]).not.toHaveProperty('format') + expect(doc.value.items[5]).not.toHaveProperty('format') + expect(doc.value.items[5]).not.toHaveProperty('minFractionDigits') + expect(doc.value.items[6]).not.toHaveProperty('format') }) }) @@ -209,7 +208,7 @@ describe('number types', () => { intAsBigInt: true, version: '1.1' }) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { value: 10n, format: 'BIN' }, { value: 83n, format: 'OCT' }, { value: 0n, format: 'OCT' }, @@ -218,9 +217,9 @@ describe('number types', () => { { value: 0.5123, format: 'EXP' }, { value: 4.02 } ]) - expect(doc.contents.items[3]).not.toHaveProperty('format') - expect(doc.contents.items[6]).not.toHaveProperty('format') - expect(doc.contents.items[6]).not.toHaveProperty('minFractionDigits') + expect(doc.value.items[3]).not.toHaveProperty('format') + expect(doc.value.items[6]).not.toHaveProperty('format') + expect(doc.value.items[6]).not.toHaveProperty('minFractionDigits') }) test('Version 1.2', () => { @@ -235,7 +234,7 @@ describe('number types', () => { intAsBigInt: true, version: '1.2' }) - expect(doc.contents.items).toMatchObject([ + expect(doc.value.items).toMatchObject([ { value: 83n, format: 'OCT' }, { value: 0n, format: 'OCT' }, { value: 123456n }, @@ -243,9 +242,9 @@ describe('number types', () => { { value: 0.5123, format: 'EXP' }, { value: 4.02 } ]) - expect(doc.contents.items[2]).not.toHaveProperty('format') - expect(doc.contents.items[5]).not.toHaveProperty('format') - expect(doc.contents.items[5]).not.toHaveProperty('minFractionDigits') + expect(doc.value.items[2]).not.toHaveProperty('format') + expect(doc.value.items[5]).not.toHaveProperty('format') + expect(doc.value.items[5]).not.toHaveProperty('minFractionDigits') }) }) }) @@ -330,7 +329,7 @@ describe('json schema', () => { }) expect(doc.errors).toHaveLength(2) doc.errors = [] - doc.contents.items[1].value!.tag = 'tag:yaml.org,2002:float' + doc.value.items[1].value!.tag = 'tag:yaml.org,2002:float' expect(String(doc)).toBe( '"canonical": 685230.15\n"fixed": !!float 685230.15\n"negative infinity": "-.inf"\n"not a number": ".NaN"\n' ) @@ -472,7 +471,7 @@ one: 1 '{ 3: 4 }': 'many' }) expect(doc.errors).toHaveLength(0) - doc.contents.items[2].key = doc.createNode({ 3: 4 }) + doc.value.items[2].key = doc.createNode({ 3: 4 }) expect(doc.toJS()).toMatchObject({ one: 1, 2: 'two', @@ -494,7 +493,7 @@ one: 1 ]) ) expect(doc.errors).toHaveLength(0) - doc.contents.items[2].key = doc.createNode({ 5: 6 }) + doc.value.items[2].key = doc.createNode({ 5: 6 }) expect(doc.toJS({ mapAsMap: true })).toMatchObject( new Map([ ['one', 1], @@ -524,8 +523,8 @@ description: const doc = parseDocument>>(src, { schema: 'yaml-1.1' }) - const canonical = doc.contents.items[0].value!.value - const generic = doc.contents.items[1].value!.value + const canonical = doc.value.items[0].value!.value + const generic = doc.value.items[1].value!.value expect(canonical).toBeInstanceOf(Uint8Array) expect(generic).toBeInstanceOf(Uint8Array) expect(canonical).toHaveLength(185) @@ -696,7 +695,7 @@ no time zone (Z): 2001-12-15 2:59:43.10 date (00:00:00Z): 2002-12-14` const doc = parseDocument>(src) - doc.contents.items.forEach(item => { + doc.value.items.forEach(item => { expect(item.value!.value).toBeInstanceOf(Date) }) expect(doc.toJSON()).toMatchObject({ @@ -733,8 +732,8 @@ date (00:00:00Z): 2002-12-14\n`) ]) test(name, () => { const doc = parseDocument(src, { version: '1.1' }) - expect(doc.contents).toBeInstanceOf(YAMLSeq) - expect(doc.contents.items).toMatchObject([ + expect(doc.value).toBeInstanceOf(YAMLSeq) + expect(doc.value.items).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, value: { value: 2 } }, { key: { value: 'a' }, value: { value: 3 } } @@ -746,7 +745,7 @@ date (00:00:00Z): 2002-12-14\n`) test('stringify', () => { const doc = new Document(null, { version: '1.1' }) - doc.contents = doc.createNode( + doc.value = doc.createNode( [ ['a', 1], ['b', 2], @@ -754,7 +753,7 @@ date (00:00:00Z): 2002-12-14\n`) ], { tag: '!!pairs' } ) - expect(doc.contents.tag).toBe('tag:yaml.org,2002:pairs') + expect(doc.value.tag).toBe('tag:yaml.org,2002:pairs') expect(String(doc)).toBe(`!!pairs\n- a: 1\n- b: 2\n- a: 3\n`) }) }) @@ -766,7 +765,7 @@ date (00:00:00Z): 2002-12-14\n`) ]) test(name, () => { const doc = parseDocument(src, { version: '1.1' }) - expect(doc.contents.constructor.tag).toBe('tag:yaml.org,2002:omap') + expect(doc.value.constructor.tag).toBe('tag:yaml.org,2002:omap') expect(doc.toJS()).toBeInstanceOf(Map) expect(doc.toJS()).toMatchObject( new Map([ @@ -806,7 +805,7 @@ date (00:00:00Z): 2002-12-14\n`) test('stringify Array', () => { const doc = new Document(null, { version: '1.1' }) - doc.contents = doc.createNode( + doc.value = doc.createNode( [ ['a', 1], ['b', 2], @@ -814,7 +813,7 @@ date (00:00:00Z): 2002-12-14\n`) ], { tag: '!!omap' } ) - expect(doc.contents.constructor.tag).toBe('tag:yaml.org,2002:omap') + expect(doc.value.constructor.tag).toBe('tag:yaml.org,2002:omap') expect(String(doc)).toBe(`!!omap\n- a: 1\n- b: 2\n- a: 3\n`) }) }) @@ -826,7 +825,7 @@ date (00:00:00Z): 2002-12-14\n`) ]) test(name, () => { const doc = parseDocument(src, { version: '1.1' }) - expect(doc.contents.constructor.tag).toBe('tag:yaml.org,2002:set') + expect(doc.value.constructor.tag).toBe('tag:yaml.org,2002:set') expect(doc.toJS()).toBeInstanceOf(Set) expect(doc.toJS()).toMatchObject(new Set(['a', 'b', 'c'])) expect(String(doc)).toBe(src) @@ -929,9 +928,9 @@ describe('custom tags', () => { test('parse', () => { const doc = parseDocument>(src) - expect(doc.contents).toBeInstanceOf(YAMLSeq) - expect(doc.contents.tag).toBe('tag:example.com,2000:test/x') - const { items } = doc.contents + expect(doc.value).toBeInstanceOf(YAMLSeq) + expect(doc.value.tag).toBe('tag:example.com,2000:test/x') + const { items } = doc.value expect(items).toHaveLength(4) items.forEach(item => expect(typeof item.value).toBe('string')) expect(items[0].tag).toBe('!y') @@ -962,12 +961,12 @@ describe('custom tags', () => { '!f!': prefix }) - doc.contents.commentBefore = 'c' - doc.contents.items[3].comment = 'cc' + doc.value.commentBefore = 'c' + doc.value.items[3].comment = 'cc' const s = new Scalar(6) s.tag = '!g' // @ts-expect-error TS should complain here - doc.contents.items.splice(1, 1, s, '7') + doc.value.items.splice(1, 1, s, '7') expect(String(doc)).toBe(source` %TAG !e! tag:example.com,2000:test/ %TAG !f! tag:example.com,2000:other/ diff --git a/tests/node-to-js.ts b/tests/node-to-js.ts index 052e47a6..2728a653 100644 --- a/tests/node-to-js.ts +++ b/tests/node-to-js.ts @@ -5,7 +5,7 @@ import { source } from './_utils.ts' describe('scalars', () => { test('plain', () => { const doc = parseDocument('42') - expect(doc.contents.toJS(doc)).toBe(42) + expect(doc.value.toJS(doc)).toBe(42) }) test('plain in map', () => { @@ -17,7 +17,7 @@ describe('scalars', () => { describe('collections', () => { test('map', () => { const doc = parseDocument('key: 42') - expect(doc.contents.toJS(doc)).toMatchObject({ key: 42 }) + expect(doc.value.toJS(doc)).toMatchObject({ key: 42 }) }) test('map in seq', () => { @@ -66,7 +66,7 @@ describe('alias', () => { describe('options', () => { test('mapAsMap', () => { const doc = parseDocument('key: 42') - expect(doc.contents.toJS(doc, { mapAsMap: true })).toMatchObject( + expect(doc.value.toJS(doc, { mapAsMap: true })).toMatchObject( new Map([['key', 42]]) ) }) diff --git a/tests/visit.ts b/tests/visit.ts index da0e6d51..5b669627 100644 --- a/tests/visit.ts +++ b/tests/visit.ts @@ -33,9 +33,9 @@ for (const [visit_, title] of [ const fn = vi.fn() await visit_(doc, { Map: fn, Pair: fn, Seq: fn, Alias: fn, Scalar: fn }) expect(fn.mock.calls).toMatchObject([ - [null, coll, [{ contents: {} }]], - [0, { type: 'PLAIN', value: 1 }, [{ contents: {} }, coll]], - [1, { type: 'PLAIN', value: 'two' }, [{ contents: {} }, coll]] + [null, coll, [{ value: {} }]], + [0, { type: 'PLAIN', value: 1 }, [{ value: {} }, coll]], + [1, { type: 'PLAIN', value: 'two' }, [{ value: {} }, coll]] ]) }) @@ -238,11 +238,11 @@ for (const [visit_, title] of [ const doc = parseDocument('- one\n- two\n- three\n') const Seq = vi.fn(() => doc.createNode(42)) if (visit_ === visit) { - expect(() => visit_(doc.contents, { Seq })).toThrow( + expect(() => visit_(doc.value, { Seq })).toThrow( 'Cannot replace node with scalar parent' ) } else { - await expect(visit_(doc.contents, { Seq })).rejects.toMatchObject({ + await expect(visit_(doc.value, { Seq })).rejects.toMatchObject({ message: 'Cannot replace node with scalar parent' }) } From 92f132b4f566dd6c4aca50f676b0d3b88be1aaae Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Thu, 29 Jan 2026 07:56:21 +0800 Subject: [PATCH 7/7] fix: Simplify NodeType (reverts 4ff5051b) --- src/nodes/Node.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index 078b3a79..9988c7e1 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -26,11 +26,9 @@ export type NodeType = T extends ? Scalar : T extends Array ? YAMLSeq> - : T extends { [key: string]: any } + : T extends { [key: string | number]: any } ? YAMLMap, NodeType> - : T extends { [key: number]: any } // Merge with previous once supported in all TS versions - ? YAMLMap, NodeType> - : Node + : Node /** `[start, value-end, node-end]` */ export type Range = [number, number, number]