diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index ee40b938..ddcb8db2 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -9,7 +9,7 @@ It is valid to have an anchor associated with a node even if it has no aliases. ## Scalar Values ```js -class NodeBase { +interface NodeBase { comment?: string // a comment on or immediately after this commentBefore?: string // a comment before this range?: [number, number, number] @@ -19,16 +19,16 @@ class NodeBase { // included in their respective ranges. spaceBefore?: boolean // a blank line before this node and its commentBefore - tag?: string // a fully qualified tag, if required - clone(): NodeBase // a copy of this node - toJS(doc, context?): any // a plain JS representation of this node + tag?: string // a fully qualified tag, if required + clone(): this // a copy of this node + toJS(doc): any // a plain JS representation of this node } ``` For scalar values, the `tag` will not be set unless it was explicitly defined in the source document; this also applies for unsupported tags that have been resolved using a fallback tag (string, `YAMLMap`, or `YAMLSeq`). ```js -class Scalar extends NodeBase { +class Scalar implements NodeBase { anchor?: string // an anchor associated with this node format?: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined // By default (undefined), numbers use decimal notation. @@ -54,12 +54,12 @@ class Pair { value: Node | null } -class Collection extends NodeBase { +class Collection implements NodeBase { anchor?: string // an anchor associated with this node flow?: boolean // use flow style when stringifying this schema?: Schema addIn(path: unknown[], value: unknown): void - clone(schema?: Schema): NodeBase // a deep copy of this collection + clone(schema?: Schema): this // a deep copy of this collection deleteIn(path: unknown[]): boolean getIn(path: unknown[], keepScalar?: boolean): unknown hasIn(path: unknown[]): boolean @@ -140,7 +140,7 @@ Note that for `addIn` the path argument points to the collection rather than the ```js -class Alias extends NodeBase { +class Alias implements NodeBase { source: string resolve(doc: Document): Scalar | YAMLMap | YAMLSeq | undefined } diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index 4162f81e..eb1e7f83 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -1,4 +1,5 @@ -import { NodeBase, type Node } from '../nodes/Node.ts' +import { isNode } from '../nodes/identity.ts' +import type { Node } from '../nodes/Node.ts' import { Scalar } from '../nodes/Scalar.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' import { YAMLSeq } from '../nodes/YAMLSeq.ts' @@ -135,7 +136,7 @@ export function composeCollection( ctx.options ) ?? coll - const node = res instanceof NodeBase ? (res as Node) : new Scalar(res) + const node = isNode(res) ? res : new Scalar(res) node.range = coll.range node.tag = tagName if (tag?.format) (node as Scalar).format = tag.format diff --git a/src/compose/util-map-includes.ts b/src/compose/util-map-includes.ts index 8d4e9b3a..f853de94 100644 --- a/src/compose/util-map-includes.ts +++ b/src/compose/util-map-includes.ts @@ -1,4 +1,4 @@ -import type { NodeBase } from '../nodes/Node.ts' +import type { Node } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' import type { ComposeContext } from './compose-node.ts' @@ -6,14 +6,14 @@ import type { ComposeContext } from './compose-node.ts' export function mapIncludes( ctx: ComposeContext, items: Pair[], - search: NodeBase + search: Node ): boolean { const { uniqueKeys } = ctx.options if (uniqueKeys === false) return false const isEqual = typeof uniqueKeys === 'function' ? uniqueKeys - : (a: NodeBase, b: NodeBase) => + : (a: Node, b: Node) => 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 d4b95610..de666740 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -2,7 +2,6 @@ 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, 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' import { ToJSContext } from '../nodes/toJS.ts' @@ -226,13 +225,13 @@ export class Document< value: V, options: CreateNodeOptions = {} ): Pair< - K extends Primitive | NodeBase ? K : NodeBase, - V extends Primitive | NodeBase ? V : NodeBase + K extends Primitive | Node ? K : Node, + V extends Primitive | Node ? V : Node > { const nc = new NodeCreator(this, options) const pair = nc.createPair(key, value) as Pair< - K extends Primitive | NodeBase ? K : NodeBase, - V extends Primitive | NodeBase ? V : NodeBase + K extends Primitive | Node ? K : Node, + V extends Primitive | Node ? V : Node > nc.setAnchors() return pair @@ -261,7 +260,7 @@ export class Document< /** * Returns item at `key`, or `undefined` if not found. */ - get(key: any): Strict extends true ? NodeBase | Pair | undefined : any { + get(key: any): Strict extends true ? Node | Pair | undefined : any { return this.value instanceof Collection ? this.value.get(key) : undefined } @@ -270,7 +269,7 @@ export class Document< */ getIn( path: unknown[] - ): Strict extends true ? NodeBase | Pair | null | undefined : any { + ): Strict extends true ? Node | Pair | null | undefined : any { if (!path.length) return this.value return this.value instanceof Collection ? this.value.getIn(path) : undefined } diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index 57be20ee..7cf5035f 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -1,6 +1,7 @@ import { Alias } from '../nodes/Alias.ts' import { Collection } from '../nodes/Collection.ts' -import { type Node, NodeBase } from '../nodes/Node.ts' +import { isNode } 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' @@ -60,7 +61,7 @@ export class NodeCreator { create(value: unknown, tagName?: string): Node { if (value instanceof Document) value = value.value - if (value instanceof NodeBase) return value as Node + if (isNode(value)) return value if (value instanceof Pair) { const map = (this.schema.map.nodeClass! as typeof YAMLMap).from( this, @@ -143,10 +144,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 = value == null ? null : this.create(value) - return new Pair(k, v) + return new Pair(k, v) } /** diff --git a/src/doc/directives.ts b/src/doc/directives.ts index aef96d06..38f84457 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -1,4 +1,3 @@ -import { NodeBase } from '../nodes/Node.ts' import { visit } from '../visit.ts' import type { Document } from './Document.ts' @@ -182,8 +181,8 @@ export class Directives { let tagNames: string[] if (doc && tagEntries.length > 0) { const tags: Record = {} - visit(doc.value, (_key, node) => { - if (node instanceof NodeBase && node.tag) tags[node.tag] = true + visit(doc.value, (_key, node: any) => { + if (node?.tag) tags[node.tag] = true }) tagNames = Object.keys(tags) } else tagNames = [] diff --git a/src/index.ts b/src/index.ts index 006750bd..ad063f60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,8 @@ export type { ErrorCode } from './errors.ts' export { YAMLError, YAMLParseError, YAMLWarning } from './errors.ts' export { Alias } from './nodes/Alias.ts' -export type { Node, Range } from './nodes/Node.ts' +export { isNode } from './nodes/identity.ts' +export type { Node, NodeBase, 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/Alias.ts b/src/nodes/Alias.ts index a38a341e..25922ab1 100644 --- a/src/nodes/Alias.ts +++ b/src/nodes/Alias.ts @@ -3,22 +3,40 @@ import type { Document, DocValue } from '../doc/Document.ts' import type { FlowScalar } from '../parse/cst.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { visit } from '../visit.ts' -import type { Node } from './Node.ts' -import { NodeBase } from './Node.ts' +import type { Node, NodeBase, Range } from './Node.ts' import { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' import { ToJSContext } from './toJS.ts' import type { YAMLMap } from './YAMLMap.ts' import type { YAMLSeq } from './YAMLSeq.ts' -export class Alias extends NodeBase { +export class Alias implements NodeBase { source: string declare anchor?: never + + /** A comment on or immediately after this node. */ + declare comment?: string | null + + /** A comment before this node. */ + declare commentBefore?: string | null + + /** + * The `[start, value-end, node-end]` character offsets for + * the part of the source parsed into this node (undefined if not parsed). + * The `value-end` and `node-end` positions are themselves not included in their respective ranges. + */ + declare range?: Range | null + + /** A blank line before this node and its commentBefore */ + declare spaceBefore?: boolean + + /** The CST token that was composed into this node. */ declare srcToken?: FlowScalar & { type: 'alias' } + declare tag?: never + constructor(source: string) { - super() this.source = source Object.defineProperty(this, 'tag', { set() { @@ -27,6 +45,16 @@ export class Alias extends NodeBase { }) } + /** Create a copy of this node. */ + clone(): this { + const copy: this = Object.create( + Object.getPrototypeOf(this), + Object.getOwnPropertyDescriptors(this) + ) + if (this.range) copy.range = [...this.range] + return copy + } + /** * Resolve the value of this alias within `doc`, finding the last * instance of the `source` anchor before this node. @@ -114,25 +142,24 @@ export class Alias extends NodeBase { function getAliasCount( doc: Document, - node: unknown, + node: Node | Pair | null, anchors: ToJSContext['anchors'] ): number { 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 (node instanceof NodeBase && 'items' in node) { - const coll = node as YAMLMap | YAMLSeq + } else if (node instanceof Pair) { + const kc = getAliasCount(doc, node.key, anchors) + const vc = getAliasCount(doc, node.value, anchors) + return Math.max(kc, vc) + } else if (node && 'items' in node) { let count = 0 - for (const item of coll.items) { + for (const item of node.items) { const c = getAliasCount(doc, item, anchors) if (c > count) count = c } return count - } else if (node instanceof Pair) { - const kc = getAliasCount(doc, node.key, anchors) - const vc = getAliasCount(doc, node.value, anchors) - return Math.max(kc, vc) } return 1 } diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index a78a8edc..c60244b7 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,6 +1,7 @@ import { NodeCreator } from '../doc/NodeCreator.ts' +import type { Token } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' -import { type Node, NodeBase } from './Node.ts' +import type { Node, Range } from './Node.ts' import type { Pair } from './Pair.ts' import type { Scalar } from './Scalar.ts' @@ -26,12 +27,12 @@ export function collectionFromPath( return new NodeCreator(schema, { aliasDuplicateObjects: false }).create(v) } -export abstract class Collection extends NodeBase { +export abstract class Collection { schema: Schema | undefined - declare items: (NodeBase | Pair)[] + declare items: (Node | Pair)[] - /** An optional anchor on this node. Used by alias nodes. */ + /** An optional anchor on this collection. Used by alias nodes. */ declare anchor?: string /** @@ -40,8 +41,29 @@ export abstract class Collection extends NodeBase { */ declare flow?: boolean + /** A comment on or immediately after this collection. */ + declare comment?: string | null + + /** A comment before this collection. */ + declare commentBefore?: string | null + + /** + * The `[start, value-end, node-end]` character offsets for + * the part of the source parsed into this collection (undefined if not parsed). + * The `value-end` and `node-end` positions are themselves not included in their respective ranges. + */ + declare range?: Range | null + + /** A blank line before this collection and its commentBefore */ + declare spaceBefore?: boolean + + /** The CST token that was composed into this collection. */ + declare srcToken?: Token + + /** A fully qualified tag, if required */ + declare tag?: string + constructor(schema?: Schema) { - super() Object.defineProperty(this, 'schema', { value: schema, configurable: true, @@ -55,14 +77,14 @@ export abstract class Collection extends NodeBase { * * @param schema - If defined, overwrites the original's schema */ - clone(schema?: Schema): Collection { - const copy: Collection = Object.create( + clone(schema?: Schema): this { + const copy: this = Object.create( Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this) ) if (schema) copy.schema = schema copy.items = copy.items.map(it => it.clone(schema)) - if (this.range) copy.range = this.range.slice() as NodeBase['range'] + if (this.range) copy.range = [...this.range] return copy } @@ -78,7 +100,7 @@ export abstract class Collection extends NodeBase { /** * Returns item at `key`, or `undefined` if not found. */ - abstract get(key: unknown): NodeBase | Pair | undefined + abstract get(key: unknown): Node | Pair | undefined /** * Checks if the collection includes a value with the key `key`. @@ -129,7 +151,7 @@ export abstract class Collection extends NodeBase { /** * Returns item at `key`, or `undefined` if not found. */ - getIn(path: unknown[]): NodeBase | Pair | undefined { + getIn(path: unknown[]): Node | Pair | undefined { const [key, ...rest] = path const node = this.get(key) if (rest.length === 0) return node diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index bf7747fd..d656e111 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -1,11 +1,11 @@ import type { Document, DocValue } from '../doc/Document.ts' import type { Token } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' -import type { StringifyContext } from '../stringify/stringify.ts' +import type { StringifyContext } from '../util.ts' import type { Alias } from './Alias.ts' import type { Scalar } from './Scalar.ts' import type { ToJSContext } from './toJS.ts' -import type { MapLike, YAMLMap } from './YAMLMap.ts' +import type { YAMLMap } from './YAMLMap.ts' import type { YAMLSeq } from './YAMLSeq.ts' export type Node = Alias | Scalar | YAMLSeq | YAMLMap @@ -29,12 +29,12 @@ export type NodeType = T extends export type Range = [start: number, valueEnd: number, nodeEnd: number] -export abstract class NodeBase { +export interface NodeBase { /** A comment on or immediately after this */ - declare comment?: string | null + comment?: string | null /** A comment before this */ - declare commentBefore?: string | null + commentBefore?: string | null /** * The `[start, value-end, node-end]` character offsets for the part of the @@ -42,44 +42,26 @@ export abstract class NodeBase { * and `node-end` positions are themselves not included in their respective * ranges. */ - declare range?: Range | null + range?: Range | null /** A blank line before this node and its commentBefore */ - declare spaceBefore?: boolean + spaceBefore?: boolean /** The CST token that was composed into this node. */ - declare srcToken?: Token + srcToken?: Token /** A fully qualified tag, if required */ - declare tag?: string + tag?: string - /** - * Customize the way that a key-value pair is resolved. - * Used for YAML 1.1 !!merge << handling. - */ - declare addToJSMap?: ( - doc: Document, - ctx: ToJSContext | undefined, - map: MapLike, - value: unknown - ) => void + /** Create a copy of this node. */ + clone(_schema?: Schema): this /** A plain JavaScript representation of this node. */ - abstract toJS(doc: Document, opt?: ToJSContext): any + toJS(doc: Document, opt?: ToJSContext): any - abstract toString( + toString( ctx?: StringifyContext, onComment?: () => void, onChompKeep?: () => void ): string - - /** Create a copy of this node. */ - clone(_schema?: Schema): NodeBase { - const copy: NodeBase = Object.create( - Object.getPrototypeOf(this), - Object.getOwnPropertyDescriptors(this) - ) - if (this.range) copy.range = this.range.slice() as NodeBase['range'] - return copy - } } diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 07131aec..9296543a 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -5,12 +5,12 @@ 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 type { NodeBase } from './Node.ts' +import type { Node } from './Node.ts' import type { ToJSContext } from './toJS.ts' export class Pair< - K extends Primitive | NodeBase = Primitive | NodeBase, - V extends Primitive | NodeBase = Primitive | NodeBase + K extends Primitive | Node = Primitive | Node, + V extends Primitive | Node = Primitive | Node > { key: NodeOf value: NodeOf | null diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index 20da8531..c82f91d5 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -1,6 +1,8 @@ +import type { Document, DocValue } from '../doc/Document.ts' import type { BlockScalar, FlowScalar } from '../parse/cst.ts' -import { NodeBase } from './Node.ts' +import type { NodeBase, Range } from './Node.ts' import type { ToJSContext } from './toJS.ts' +import type { MapLike } from './YAMLMap.ts' export declare namespace Scalar { type BLOCK_FOLDED = 'BLOCK_FOLDED' @@ -12,7 +14,7 @@ export declare namespace Scalar { type Type = BLOCK_FOLDED | BLOCK_LITERAL | PLAIN | QUOTE_DOUBLE | QUOTE_SINGLE } -export class Scalar extends NodeBase { +export class Scalar implements NodeBase { static readonly BLOCK_FOLDED = 'BLOCK_FOLDED' static readonly BLOCK_LITERAL = 'BLOCK_LITERAL' static readonly PLAIN = 'PLAIN' @@ -24,6 +26,12 @@ export class Scalar extends NodeBase { /** An optional anchor on this node. Used by alias nodes. */ declare anchor?: string + /** A comment on or immediately after this node. */ + declare comment?: string | null + + /** A comment before this node. */ + declare commentBefore?: string | null + /** * By default (undefined), numbers use decimal notation. * The YAML 1.2 core schema only supports 'HEX' and 'OCT'. @@ -34,19 +42,53 @@ export class Scalar extends NodeBase { /** If `value` is a number, use this value when stringifying this node. */ declare minFractionDigits?: number + /** + * The `[start, value-end, node-end]` character offsets for + * the part of the source parsed into this node (undefined if not parsed). + * The `value-end` and `node-end` positions are themselves not included in their respective ranges. + */ + declare range?: Range | null + + /** A blank line before this node and its commentBefore */ + declare spaceBefore?: boolean + + /** The CST token that was composed into this node. */ + declare srcToken?: FlowScalar | BlockScalar + /** Set during parsing to the source string value */ declare source?: string - declare srcToken?: FlowScalar | BlockScalar + /** A fully qualified tag, if required */ + declare tag?: string /** The scalar style used for the node's string representation */ declare type?: Scalar.Type + /** + * Customize the way that a key-value pair is resolved. + * Used for YAML 1.1 !!merge << handling. + */ + declare addToJSMap?: ( + doc: Document, + ctx: ToJSContext | undefined, + map: MapLike, + value: unknown + ) => void + constructor(value: T) { - super() this.value = value } + /** Create a copy of this node. */ + clone(): this { + const copy: this = Object.create( + Object.getPrototypeOf(this), + Object.getOwnPropertyDescriptors(this) + ) + if (this.range) copy.range = [...this.range] + return copy + } + /** A plain JavaScript representation of this node. */ toJS(_doc?: unknown, ctx?: ToJSContext): T { if (ctx && this.anchor) { diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index a14aac93..84bb77ea 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -6,7 +6,8 @@ import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' import { addPairToJSMap } from './addPairToJSMap.ts' import { Collection, type NodeOf, type Primitive } from './Collection.ts' -import { NodeBase } from './Node.ts' +import { isNode } from './identity.ts' +import type { Node, NodeBase } from './Node.ts' import { Pair } from './Pair.ts' import { Scalar } from './Scalar.ts' import { ToJSContext } from './toJS.ts' @@ -17,8 +18,8 @@ export type MapLike = | Record export function findPair< - K extends Primitive | NodeBase = Primitive | NodeBase, - V extends Primitive | NodeBase = Primitive | NodeBase + K extends Primitive | Node = Primitive | Node, + V extends Primitive | Node = Primitive | Node >(items: Iterable>, key: unknown): Pair | undefined { const k = key instanceof Scalar ? key.value : key for (const it of items) { @@ -29,9 +30,12 @@ export function findPair< } export class YAMLMap< - K extends Primitive | NodeBase = Primitive | NodeBase, - V extends Primitive | NodeBase = Primitive | NodeBase -> extends Collection { + K extends Primitive | Node = Primitive | Node, + V extends Primitive | Node = Primitive | Node +> + extends Collection + implements NodeBase +{ static get tagName(): 'tag:yaml.org,2002:map' { return 'tag:yaml.org,2002:map' } @@ -109,10 +113,7 @@ export class YAMLMap< options?: Omit ): void { let pair: Pair - if ( - key instanceof NodeBase && - (value instanceof NodeBase || value === null) - ) { + if (isNode(key) && (value === null || isNode(value))) { pair = new Pair(key, value) } else if (!this.schema) { throw new Error('Schema is required') diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index f122edfa..d083ac88 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -5,7 +5,8 @@ import type { BlockSequence, FlowCollection } from '../parse/cst.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' import { Collection, type NodeOf, type Primitive } from './Collection.ts' -import { NodeBase } from './Node.ts' +import { isNode } from './identity.ts' +import type { Node, NodeBase } from './Node.ts' import type { Pair } from './Pair.ts' import { Scalar } from './Scalar.ts' import { ToJSContext } from './toJS.ts' @@ -14,8 +15,11 @@ const isScalarValue = (value: unknown): boolean => !value || (typeof value !== 'function' && typeof value !== 'object') export class YAMLSeq< - T extends Primitive | NodeBase | Pair = Primitive | NodeBase | Pair -> extends Collection { + T extends Primitive | Node | Pair = Primitive | Node | Pair +> + extends Collection + implements NodeBase +{ static get tagName(): 'tag:yaml.org,2002:seq' { return 'tag:yaml.org,2002:seq' } @@ -27,7 +31,7 @@ export class YAMLSeq< value: T, options?: Omit ): void { - if (value instanceof NodeBase) this.items.push(value as NodeOf) + 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, { @@ -94,7 +98,7 @@ export class YAMLSeq< if (idx < 0) throw new RangeError(`Invalid negative index ${idx}`) const prev = this.items[idx] if (prev instanceof Scalar && isScalarValue(value)) prev.value = value - else if (value instanceof NodeBase) this.items[idx] = value as NodeOf + 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, { diff --git a/src/nodes/addPairToJSMap.ts b/src/nodes/addPairToJSMap.ts index 18dc0460..fd676b64 100644 --- a/src/nodes/addPairToJSMap.ts +++ b/src/nodes/addPairToJSMap.ts @@ -2,7 +2,7 @@ import type { Document, DocValue } from '../doc/Document.ts' import { warn } from '../log.ts' import { addMergeToJSMap, isMergeKey } from '../schema/yaml-1.1/merge.ts' import { createStringifyContext } from '../stringify/stringify.ts' -import type { NodeBase } from './Node.ts' +import type { Node } from './Node.ts' import type { Pair } from './Pair.ts' import type { ToJSContext } from './toJS.ts' import type { MapLike } from './YAMLMap.ts' @@ -13,7 +13,7 @@ export function addPairToJSMap( map: MapLike, { key, value }: Pair ): MapLike { - if (key.addToJSMap) key.addToJSMap(doc, ctx, map, value) + if ('addToJSMap' in key) key.addToJSMap?.(doc, ctx, map, value) // TODO: Should drop this special case for bare << handling else if (isMergeKey(doc, key)) addMergeToJSMap(doc, ctx, map, value) else { @@ -41,7 +41,7 @@ export function addPairToJSMap( function stringifyKey( doc: Document, ctx: ToJSContext, - key: NodeBase, + key: Node, jsKey: unknown ) { if (jsKey === null) return '' diff --git a/src/nodes/identity.ts b/src/nodes/identity.ts new file mode 100644 index 00000000..a3bb0314 --- /dev/null +++ b/src/nodes/identity.ts @@ -0,0 +1,8 @@ +import { Alias } from './Alias.ts' +import { Collection } from './Collection.ts' +import type { Node } from './Node.ts' +import { Scalar } from './Scalar.ts' + +/** Type predicate for `Node` values */ +export const isNode = (node: unknown): node is Node => + node instanceof Scalar || node instanceof Alias || node instanceof Collection diff --git a/src/options.ts b/src/options.ts index ef8a0aaf..1d20f556 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 { NodeBase } from './nodes/Node.ts' +import type { Node } 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' @@ -67,7 +67,7 @@ export type ParseOptions = { * * Default: `true` */ - uniqueKeys?: boolean | ((a: NodeBase, b: NodeBase) => boolean) + uniqueKeys?: boolean | ((a: Node, b: Node) => boolean) } export type DocumentOptions = { diff --git a/src/schema/yaml-1.1/omap.ts b/src/schema/yaml-1.1/omap.ts index d6656d6f..e65dc1aa 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -1,6 +1,6 @@ import type { Document, DocValue } from '../../doc/Document.ts' import type { Primitive } from '../../nodes/Collection.ts' -import type { NodeBase } from '../../nodes/Node.ts' +import type { Node } from '../../nodes/Node.ts' import type { Pair } from '../../nodes/Pair.ts' import { Scalar } from '../../nodes/Scalar.ts' import type { ToJSContext } from '../../nodes/toJS.ts' @@ -12,8 +12,8 @@ import type { CollectionTag } from '../types.ts' import { createPairs, resolvePairs } from './pairs.ts' export class YAMLOMap< - K extends Primitive | NodeBase = Primitive | NodeBase, - V extends Primitive | NodeBase = Primitive | NodeBase + K extends Primitive | Node = Primitive | Node, + V extends Primitive | Node = Primitive | Node > extends YAMLSeq> { static tag = 'tag:yaml.org,2002:omap' diff --git a/src/schema/yaml-1.1/pairs.ts b/src/schema/yaml-1.1/pairs.ts index 6890e1be..293ca388 100644 --- a/src/schema/yaml-1.1/pairs.ts +++ b/src/schema/yaml-1.1/pairs.ts @@ -1,5 +1,5 @@ import type { NodeCreator } from '../../doc/NodeCreator.ts' -import type { NodeBase } from '../../nodes/Node.ts' +import type { Node } from '../../nodes/Node.ts' import { Pair } from '../../nodes/Pair.ts' import { Scalar } from '../../nodes/Scalar.ts' import { YAMLMap } from '../../nodes/YAMLMap.ts' @@ -30,7 +30,7 @@ export function resolvePairs( } seq.items[i] = pair } else { - seq.items[i] = new Pair(item, null) + seq.items[i] = new Pair(item, null) } } } else onError('Expected a sequence for this tag') diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index 1a9bec73..2e47a55c 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -1,7 +1,8 @@ import type { Document, DocValue } from '../../doc/Document.ts' import { NodeCreator } from '../../doc/NodeCreator.ts' import type { NodeOf, Primitive } from '../../nodes/Collection.ts' -import { NodeBase } from '../../nodes/Node.ts' +import { isNode } 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 { ToJSContext } from '../../nodes/toJS.ts' @@ -12,7 +13,7 @@ import type { StringifyContext } from '../../stringify/stringify.ts' import type { CollectionTag } from '../types.ts' export class YAMLSet< - T extends Primitive | NodeBase = Primitive | NodeBase + T extends Primitive | Node = Primitive | Node > extends YAMLMap { static tag = 'tag:yaml.org,2002:set' @@ -64,8 +65,8 @@ export class YAMLSet< if (prev && !value) { this.items.splice(this.items.indexOf(prev), 1) } else if (!prev && value) { - let node: NodeBase - if (key instanceof NodeBase) { + let node: Node + if (isNode(key)) { node = key } else if (!this.schema) { throw new Error('Schema is required') @@ -100,7 +101,7 @@ export class YAMLSet< 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 } diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index c44e0c47..6983ae19 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -2,7 +2,7 @@ import { anchorIsValid } from '../doc/anchors.ts' import type { Document } from '../doc/Document.ts' import { Alias } from '../nodes/Alias.ts' import { Collection } from '../nodes/Collection.ts' -import { NodeBase, type Node } from '../nodes/Node.ts' +import type { Node } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' import type { ToStringOptions } from '../options.ts' @@ -131,31 +131,26 @@ function stringifyProps( } export function stringify( - item: unknown, + node: Node | Pair | null, ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ): string { - 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)) { - throw new TypeError( - `Cannot stringify circular structure without alias nodes` - ) - } else { - if (ctx.resolvedAliases) ctx.resolvedAliases.add(item) - else ctx.resolvedAliases = new Set([item]) - item = item.resolve(ctx.doc) + if (node instanceof Pair) return node.toString(ctx, onComment, onChompKeep) + if (node instanceof Alias) { + if (ctx.doc.directives) return node.toString(ctx) + if (ctx.resolvedAliases?.has(node)) { + const msg = 'Cannot stringify circular structure without alias nodes' + throw new TypeError(msg) } + + if (ctx.resolvedAliases) ctx.resolvedAliases.add(node) + else ctx.resolvedAliases = new Set([node]) + node = node.resolve(ctx.doc) ?? null } let tagObj: ScalarTag | CollectionTag | undefined = undefined - const node = - item instanceof NodeBase - ? (item as Node) - : ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) }) + node ??= ctx.doc.createNode(null, { onTagObj: o => (tagObj = o) }) tagObj ??= getTagObject(ctx.doc.schema.tags, node) const props = stringifyProps(node, tagObj, ctx) diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index 895b4c34..133d5572 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -1,5 +1,4 @@ import type { Collection } from '../nodes/Collection.ts' -import { NodeBase } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import type { StringifyContext } from './stringify.ts' import { stringify } from './stringify.ts' @@ -45,16 +44,13 @@ function stringifyBlockCollection( for (let i = 0; i < items.length; ++i) { const item = items[i] let comment: string | null = null - if (item instanceof NodeBase) { + if (item instanceof Pair) { + if (!chompKeep && item.key.spaceBefore) lines.push('') + addCommentBefore(ctx, lines, item.key.commentBefore, chompKeep) + } else if (item) { if (!chompKeep && item.spaceBefore) lines.push('') addCommentBefore(ctx, lines, item.commentBefore, chompKeep) if (item.comment) comment = item.comment - } 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) - } } chompKeep = false @@ -112,25 +108,23 @@ function stringifyFlowCollection( for (let i = 0; i < items.length; ++i) { const item = items[i] let comment: string | null = null - if (item instanceof NodeBase) { - if (item.spaceBefore) lines.push('') - addCommentBefore(ctx, lines, item.commentBefore, false) - if (item.comment) comment = item.comment - } 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 - } + if (item instanceof Pair) { + const ik = item.key + if (ik.spaceBefore) lines.push('') + addCommentBefore(ctx, lines, ik.commentBefore, false) + if (ik.comment) reqNewline = true - const iv = item.value instanceof NodeBase ? item.value : null + const iv = item.value if (iv) { if (iv.comment) comment = iv.comment if (iv.commentBefore) reqNewline = true - } else if (item.value == null && ik?.comment) { + } else if (ik?.comment) { comment = ik.comment } + } else if (item) { + if (item.spaceBefore) lines.push('') + addCommentBefore(ctx, lines, item.commentBefore, false) + if (item.comment) comment = item.comment } if (comment) reqNewline = true diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts index b47ce38b..109cabbf 100644 --- a/src/stringify/stringifyPair.ts +++ b/src/stringify/stringifyPair.ts @@ -1,5 +1,4 @@ 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' @@ -14,29 +13,23 @@ export function stringifyPair( onChompKeep?: () => void ): string { const { - doc, indent, indentStep, noValues, options: { commentString, indentSeq, simpleKeys } } = ctx - let keyComment = (key instanceof NodeBase && key.comment) || null if (simpleKeys) { - if (keyComment) { + if (key.comment) { throw new Error('With simple keys, key nodes cannot have comments') } - if ( - key instanceof Collection || - (!(key instanceof NodeBase) && typeof key === 'object') - ) { + if (key instanceof Collection) { const msg = 'With simple keys, collection cannot be used as a key value' throw new Error(msg) } } let explicitKey = !simpleKeys && - (!key || - !(key instanceof Scalar) || + (!(key instanceof Scalar) || key.type === Scalar.BLOCK_FOLDED || key.type === Scalar.BLOCK_LITERAL) @@ -46,6 +39,7 @@ export function stringifyPair( indent: indent + indentStep, noValues: false } + let keyComment = key.comment let keyCommentDone = false let chompKeep = false let str = stringify( @@ -94,7 +88,7 @@ export function stringifyPair( } let vsb, vcb, valueComment - if (value instanceof NodeBase) { + if (value) { vsb = !!value.spaceBefore vcb = value.commentBefore valueComment = value.comment @@ -102,7 +96,6 @@ export function stringifyPair( vsb = false vcb = null valueComment = null - if (value && typeof value === 'object') value = doc.createNode(value) } ctx.implicitKey = false if (!explicitKey && !keyComment && value instanceof Scalar) diff --git a/src/test-events.ts b/src/test-events.ts index 60770462..41645f27 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -1,7 +1,8 @@ import type { Document } from './doc/Document.ts' import { Alias } from './nodes/Alias.ts' import { Collection } from './nodes/Collection.ts' -import { type Node, NodeBase } from './nodes/Node.ts' +import { isNode } from './nodes/identity.ts' +import type { Node } from './nodes/Node.ts' import { Pair } from './nodes/Pair.ts' import { Scalar } from './nodes/Scalar.ts' import { YAMLMap } from './nodes/YAMLMap.ts' @@ -68,13 +69,13 @@ function addEvents( events: string[], doc: Document, errPos: number, - node: NodeBase | Pair | null + node: Node | Pair | null ) { if (!node) { events.push('=VAL :') return } - if (errPos !== -1 && node instanceof NodeBase && node.range![0] >= errPos) + if (errPos !== -1 && isNode(node) && node.range![0] >= errPos) throw new Error() let props = '' let anchor = @@ -88,7 +89,7 @@ function addEvents( } props = ` &${anchor}` } - if (node instanceof NodeBase && node.tag) props += ` <${node.tag}>` + if (isNode(node) && node.tag) props += ` <${node.tag}>` if (node instanceof YAMLMap) { const ev = node.flow ? '+MAP {}' : '+MAP' diff --git a/src/visit.ts b/src/visit.ts index 2721022f..c5014747 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -1,6 +1,7 @@ import { Document, type DocValue } from './doc/Document.ts' import { Alias } from './nodes/Alias.ts' -import { NodeBase, type Node } from './nodes/Node.ts' +import { isNode } from './nodes/identity.ts' +import type { Node } from './nodes/Node.ts' import { Pair } from './nodes/Pair.ts' import { Scalar } from './nodes/Scalar.ts' import { YAMLMap } from './nodes/YAMLMap.ts' @@ -17,7 +18,7 @@ export type visitorFn = ( ) => void | symbol | number | Node | Pair export type visitor = - | visitorFn + | visitorFn | { Alias?: visitorFn Collection?: visitorFn @@ -42,7 +43,7 @@ export type asyncVisitorFn = ( | Promise export type asyncVisitor = - | asyncVisitorFn + | asyncVisitorFn | { Alias?: asyncVisitorFn Collection?: asyncVisitorFn @@ -109,13 +110,13 @@ visit.REMOVE = REMOVE function visit_( key: number | 'key' | 'value' | null, - node: unknown, + node: Node | Pair | null, visitor: visitor, path: readonly (Document | Node | Pair)[] ): number | symbol | void { const ctrl = callVisitor(key, node, visitor, path) - if (ctrl instanceof NodeBase || ctrl instanceof Pair) { + if (isNode(ctrl) || ctrl instanceof Pair) { replaceNode(key, path, ctrl) return visit_(key, ctrl, visitor, path) } @@ -208,13 +209,13 @@ visitAsync.REMOVE = REMOVE async function visitAsync_( key: number | 'key' | 'value' | null, - node: unknown, + node: Node | Pair | null, visitor: asyncVisitor, path: readonly (Document | Node | Pair)[] ): Promise { const ctrl = await callVisitor(key, node, visitor, path) - if (ctrl instanceof NodeBase || ctrl instanceof Pair) { + if (isNode(ctrl) || ctrl instanceof Pair) { replaceNode(key, path, ctrl) return visitAsync_(key, ctrl, visitor, path) } @@ -275,19 +276,19 @@ function initVisitor(visitor: V) { function callVisitor( key: number | 'key' | 'value' | null, - node: unknown, + node: Node | Pair | null, visitor: visitor, path: readonly (Document | Node | Pair)[] ): ReturnType> function callVisitor( key: number | 'key' | 'value' | null, - node: unknown, + node: Node | Pair | null, visitor: asyncVisitor, path: readonly (Document | Node | Pair)[] ): ReturnType> function callVisitor( key: number | 'key' | 'value' | null, - node: unknown, + node: Node | Pair | null, visitor: visitor | asyncVisitor, path: readonly (Document | Node | Pair)[] ): ReturnType> | ReturnType> { @@ -309,7 +310,7 @@ function replaceNode( if (parent instanceof YAMLMap || parent instanceof YAMLSeq) { parent.items[key as number] = node } else if (parent instanceof Pair) { - if (node instanceof NodeBase) { + if (isNode(node)) { if (key === 'key') parent.key = node else parent.value = node } else { diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index 956dbd8d..b1386700 100644 --- a/tests/doc/anchors.ts +++ b/tests/doc/anchors.ts @@ -117,6 +117,7 @@ describe('errors', () => { const node = doc.value.items[0] const alias = doc.createAlias(node, 'AA') expect(() => { + // @ts-expect-error This is intentionally wrong. alias.tag = 'tag:yaml.org,2002:alias' }).toThrow('Alias nodes cannot have tags') }) diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 9635e11e..842c64ce 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -85,8 +85,6 @@ describe('arrays', () => { const res = '- 3\n- - four\n - 5\n' const doc = new Document(array) expect(String(doc)).toBe(res) - doc.value = array as any - expect(String(doc)).toBe(res) doc.value = doc.createNode(array) expect(String(doc)).toBe(res) }) @@ -180,8 +178,6 @@ z: v: 6\n` const doc = new Document(object) expect(String(doc)).toBe(res) - doc.value = object as any - expect(String(doc)).toBe(res) doc.value = doc.createNode(object) expect(String(doc)).toBe(res) }) @@ -215,8 +211,6 @@ describe('Set', () => { const res = '- 3\n- - four\n - 5\n' const doc = new Document(set) expect(String(doc)).toBe(res) - doc.value = set as any - expect(String(doc)).toBe(res) doc.value = doc.createNode(set) expect(String(doc)).toBe(res) }) @@ -293,8 +287,6 @@ y: : z\n` const doc = new Document(map) expect(String(doc)).toBe(res) - doc.value = map as any - expect(String(doc)).toBe(res) doc.value = doc.createNode(map) expect(String(doc)).toBe(res) }) diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index f91e565d..1c791440 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -752,25 +752,7 @@ describe('simple keys', () => { ) }) - test('key with JS object value', () => { - const doc = YAML.parseDocument('[foo]: bar') - 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/ - ) - }) - - test('key with JS null value', () => { - const doc = YAML.parseDocument('[foo]: bar') - 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/ - ) - }) - - test('key value lingth > 1024', () => { + test('key value length > 1024', () => { const str = ` ? ${new Array(1026).join('a')} : longkey` diff --git a/tests/doc/types.ts b/tests/doc/types.ts index c16aeab1..31a3cf28 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -980,8 +980,7 @@ describe('custom tags', () => { doc.value.items[3].comment = 'cc' const s = new Scalar(6) s.tag = '!g' - // @ts-expect-error TS should complain here - doc.value.items.splice(1, 1, s, '7') + doc.value.items.splice(1, 1, s, new Scalar('7')) expect(String(doc)).toBe(source` %TAG !e! tag:example.com,2000:test/ %TAG !f! tag:example.com,2000:other/