From 059d7219b1ee11ee8ee28e7aea4df26e6b5aa040 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 14 Feb 2026 23:55:59 +0200 Subject: [PATCH 1/5] feat!: Drop addIn(), deleteIn(), getIn(), hasIn(), and setIn() doc & collection methods --- docs/04_documents.md | 13 ++- docs/05_content_nodes.md | 54 ++++------- src/doc/Document.ts | 52 +---------- src/nodes/Collection.ts | 95 ------------------- tests/collection-access.ts | 185 ------------------------------------- tests/doc/anchors.ts | 10 +- tests/doc/createNode.ts | 10 +- tests/doc/types.ts | 4 +- 8 files changed, 40 insertions(+), 383 deletions(-) diff --git a/docs/04_documents.md b/docs/04_documents.md index 2332e41c..d44034c1 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -117,16 +117,15 @@ which is expected to always contain a `YAMLMap`, `YAMLSeq`, or `Scalar` value. ```js const doc = parseDocument('a: 1\nb: [2, 3]\n') doc.get('a') // 1 -doc.getIn([]) // YAMLMap { items: [Pair, Pair], ... } -doc.hasIn(['b', 0]) // true -doc.addIn(['b'], 4) // -> doc.get('b').items.length === 3 -doc.deleteIn(['b', 1]) // true -doc.getIn(['b', 1]) // 4 +doc.get() // YAMLMap { items: [Pair, Pair], ... } +doc.get('b').has(0) // true +doc.get('b').add(4) // -> doc.get('b').items.length === 3 +doc.get('b').delete(1) // true +doc.get('b').get(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. `[]`) will refer to the document's top-level `value`. +`add`, `delete`, `get`, `has`, and `set`. #### `Document#toJS()`, `Document#toJSON()` and `Document#toString()` diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index ddcb8db2..584491db 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -58,12 +58,7 @@ 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): this // a deep copy of this collection - deleteIn(path: unknown[]): boolean - getIn(path: unknown[], keepScalar?: boolean): unknown - hasIn(path: unknown[]): boolean - setIn(path: unknown[], value: unknown): void } class YAMLMap extends Collection { @@ -96,45 +91,34 @@ The `yaml-1.1` schema includes [additional collections](https://yaml.org/type/in All of the collections provide the following accessor methods: -| Method | Returns | Description | -| ----------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| add(value), addIn(path, value) | `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. | -| delete(key), deleteIn(path) | `boolean` | Removes a value from the collection. Returns `true` if the item was found and removed. | -| get(key, [keep]), getIn(path, [keep]) | `any` | Returns value at `key`, or `undefined` if not found. By default unwraps scalar values from their surrounding node; to disable set `keep` to `true` (collections are always returned intact). | -| has(key), hasIn(path) | `boolean` | Checks if the collection includes a value with the key `key`. | -| set(key, value), setIn(path, value) | `any` | Sets a value in this collection. For `!!set`, `value` needs to be a boolean to add/remove the item from the set. When overwriting a `Scalar` value with a scalar, the original node is retained. | +| Method | Returns | Description | +| --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| add(value) | `void` | Adds a value to the collection. For `!!map` and `!!omap` the value must be a Pair instance, which must not have a key that already exists in the map. | +| delete(key) | `boolean` | Removes a value from the collection. Returns `true` if the item was found and removed. | +| get(key) | `Node` | Returns value at `key`, or `undefined` if not found. | +| has(key) | `boolean` | Checks if the collection includes a value with the key `key`. | +| set(key, value) | `any` | Sets a value in this collection. For `!!set`, `value` needs to be a boolean to add/remove the item from the set. When overwriting a `Scalar` value with a scalar, the original node is retained. | ```js const doc = new YAML.Document({ a: 1, b: [2, 3] }) // { a: 1, b: [ 2, 3 ] } -doc.add({ key: 'c', value: 4 }) // { a: 1, b: [ 2, 3 ], c: 4 } -doc.addIn(['b'], 5) // { a: 1, b: [ 2, 3, 5 ], c: 4 } +doc.add(doc.createPair('c', 4)) // { a: 1, b: [ 2, 3 ], c: 4 } +doc.get('b').add(5) // { a: 1, b: [ 2, 3, 5 ], c: 4 } doc.set('c', 42) // { a: 1, b: [ 2, 3, 5 ], c: 42 } -doc.setIn(['c', 'x']) // Error: Expected YAML collection at c. Remaining path: x +doc.get('c').set('x') // TypeError: doc.get(...).set is not a function doc.delete('c') // { a: 1, b: [ 2, 3, 5 ] } -doc.deleteIn(['b', 1]) // { a: 1, b: [ 2, 5 ] } +doc.get('b').delete(1) // { a: 1, b: [ 2, 5 ] } -doc.get('a') // 1 -doc.get('a', true) // Scalar { value: 1 } -doc.getIn(['b', 1]) // 5 +doc.get('a') // Scalar { value: 1 } +doc.get('b').get(1) // Scalar { value: 5 } doc.has(doc.createNode('a')) // true doc.has('c') // false -doc.hasIn(['b', '0']) // true +doc.get('b').has(0) // true ``` For all of these methods, the keys may be nodes or their wrapped scalar values (i.e. `42` will match `Scalar { value: 42 }`). -Keys for `!!seq` should be positive integers, or their string representations. -`add()` and `set()` do not automatically call `doc.createNode()` to wrap the value. - -Each of the methods also has a variant that requires an iterable as the first parameter, and allows fetching or modifying deeper collections. -If any intermediate node in `path` is a scalar rather than a collection, an error will be thrown. -If any of the intermediate collections is not found: - -- `getIn` and `hasIn` will return `undefined` or `false` (respectively) -- `addIn` and `setIn` will create missing collections; non-negative integer keys will create sequences, all other keys create maps -- `deleteIn` will throw an error - -Note that for `addIn` the path argument points to the collection rather than the item; for maps its `value` should be a `Pair` or an object with `{ key, value }` fields. +Keys for `!!seq` should be non-negative integers, or their string representations. +`add()` and `set()` will internally call `doc.createNode()` to wrap the value. ## Alias Nodes @@ -266,12 +250,12 @@ const doc = YAML.parseDocument(` - 1: a number `) -const obs = doc.getIn([2, 'including'], true) +const obs = doc.get(2).get('including') obs.type = 'QUOTE_DOUBLE' YAML.visit(doc, { Pair(_, pair) { - if (pair.key && pair.key.value === '3') return YAML.visit.REMOVE + if (pair.key?.value === '3') return YAML.visit.REMOVE }, Scalar(key, node) { if ( @@ -292,7 +276,7 @@ String(doc) ``` In general, it's safe to modify nodes manually, e.g. splicing the `items` array of a `YAMLMap` or setting its `flow` value to `true`. -For operations on nodes at a known location in the tree, it's probably easiest to use `doc.getIn(path, true)` to access them. +For operations on nodes at a known location in the tree, it's probably easiest to use `doc.get(...)` to access them. For more complex or general operations, a visitor API is provided: #### `YAML.visit(node, visitor): void` diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 2fb90a6f..96face54 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -3,7 +3,7 @@ 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 { Pair } from '../nodes/Pair.ts' -import { Scalar } from '../nodes/Scalar.ts' +import type { Scalar } from '../nodes/Scalar.ts' import { ToJSContext } from '../nodes/toJS.ts' import type { YAMLMap } from '../nodes/YAMLMap.ts' import type { YAMLSeq } from '../nodes/YAMLSeq.ts' @@ -155,11 +155,6 @@ export class Document< assertCollection(this.value).add(value) } - /** Adds a value to the document. */ - addIn(path: unknown[], value: unknown): void { - assertCollection(this.value).addIn(path, value) - } - /** * Create a new `Alias` node, ensuring that the target `node` has the required anchor. * @@ -245,35 +240,14 @@ export class Document< return assertCollection(this.value).delete(key) } - /** - * Removes a value from the document. - * @returns `true` if the item was found and removed. - */ - deleteIn(path: unknown[]): boolean { - if (!path.length) { - this.value = new Scalar(null) as Value - return true - } - return assertCollection(this.value).deleteIn(path) - } - /** * Returns item at `key`, or `undefined` if not found. */ - get(key: any): Strict extends true ? Node | Pair | undefined : any { + get(key?: any): Strict extends true ? Node | Pair | undefined : any { + if (key === undefined) return this.value return this.value instanceof Collection ? this.value.get(key) : undefined } - /** - * Returns item at `path`, or `undefined` if not found. - */ - getIn( - path: unknown[] - ): Strict extends true ? Node | Pair | null | undefined : any { - 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`. */ @@ -281,14 +255,6 @@ export class Document< return this.value instanceof Collection ? this.value.has(key) : false } - /** - * Checks if the document includes a value at `path`. - */ - hasIn(path: unknown[]): boolean { - if (!path.length) return true - return this.value instanceof Collection ? this.value.hasIn(path) : false - } - /** * Sets a value in this document. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. @@ -297,18 +263,6 @@ export class Document< assertCollection(this.value).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: unknown[], value: unknown): void { - if (!path.length) { - this.value = value as Value - } else { - assertCollection(this.value).setIn(path, value) - } - } - /** * Change the YAML version and schema used by the document. * A `null` version disables support for directives, explicit tags, anchors, and aliases. diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index c60244b7..e0316052 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,4 +1,3 @@ -import { NodeCreator } from '../doc/NodeCreator.ts' import type { Token } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' import type { Node, Range } from './Node.ts' @@ -8,25 +7,6 @@ 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, - path: unknown[], - value: unknown -): Node { - let v = value - for (let i = path.length - 1; i >= 0; --i) { - const k = path[i] - if (typeof k === 'number' && Number.isInteger(k) && k >= 0) { - const a: unknown[] = [] - a[k] = v - v = a - } else { - v = new Map([[k, v]]) - } - } - return new NodeCreator(schema, { aliasDuplicateObjects: false }).create(v) -} - export abstract class Collection { schema: Schema | undefined @@ -111,79 +91,4 @@ export abstract class Collection { * 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. - */ - addIn(path: unknown[], value: unknown): void { - if (!path.length) this.add(value) - else { - const [key, ...rest] = path - const node = this.get(key) - if (node instanceof Collection) node.addIn(rest, value) - else if (node === undefined && this.schema) - this.set(key, collectionFromPath(this.schema, rest, value)) - else - throw new Error( - `Expected YAML collection at ${key}. Remaining path: ${rest}` - ) - } - } - - /** - * Removes a value from the collection. - * - * @returns `true` if the item was found and removed. - */ - deleteIn(path: unknown[]): boolean { - const [key, ...rest] = path - if (rest.length === 0) return this.delete(key) - const node = this.get(key) - if (node instanceof Collection) return node.deleteIn(rest) - else - throw new Error( - `Expected YAML collection at ${key}. Remaining path: ${rest}` - ) - } - - /** - * Returns item at `key`, or `undefined` if not found. - */ - getIn(path: unknown[]): Node | Pair | undefined { - const [key, ...rest] = path - const node = this.get(key) - if (rest.length === 0) return node - else return node instanceof Collection ? node.getIn(rest) : undefined - } - - /** - * Checks if the collection includes a value with the key `key`. - */ - hasIn(path: unknown[]): boolean { - const [key, ...rest] = path - if (rest.length === 0) return this.has(key) - const node = this.get(key) - return node instanceof Collection ? node.hasIn(rest) : false - } - - /** - * Sets a value in this collection. - */ - setIn(path: unknown[], value: unknown): void { - const [key, ...rest] = path - if (rest.length === 0) { - this.set(key, value) - } else { - const node = this.get(key) - if (node instanceof Collection) node.setIn(rest, value) - else if (node === undefined && this.schema) - this.set(key, collectionFromPath(this.schema, rest, value)) - else - throw new Error( - `Expected YAML collection at ${key}. Remaining path: ${rest}` - ) - } - } } diff --git a/tests/collection-access.ts b/tests/collection-access.ts index 3d0badeb..096c7bb2 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -293,78 +293,6 @@ describe('OMap', () => { }) }) -describe('Collection', () => { - let doc: Document - let map: YAMLMap | YAMLSeq>> - beforeEach(() => { - doc = new Document({ a: 1, b: [2, 3] }) - map = doc.value as any - }) - - test('addIn', () => { - map.addIn(['b'], 4) - expect(map.getIn(['b', 2])).toMatchObject({ value: 4 }) - map.addIn([], doc.createPair('c', 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) - const seq = map.getIn(['b']) - expect(seq).toBeInstanceOf(YAMLSeq) - expect((seq as any).items).toHaveLength(4) - }) - - test('deleteIn', () => { - expect(map.deleteIn(['a'])).toBe(true) - expect(map.get('a')).toBeUndefined() - expect(map.deleteIn(['b', 1])).toBe(true) - expect(map.getIn(['b', 1])).toBeUndefined() - expect(map.deleteIn([1])).toBe(false) - expect(map.deleteIn(['b', 2])).toBe(false) - expect(() => map.deleteIn(['a', 'e'])).toThrow(/Expected YAML collection/) - expect(map.items).toHaveLength(1) - const subSeq = map.getIn(['b']) - expect(subSeq).toBeInstanceOf(YAMLSeq) - expect((subSeq as any).items).toHaveLength(1) - }) - - test('getIn', () => { - 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', 2])).toBeUndefined() - expect(map.getIn(['c', 'e'])).toBeUndefined() - expect(map.getIn(['a', 'e'])).toBeUndefined() - }) - - test('hasIn', () => { - expect(map.hasIn(['a'])).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) - }) - - test('setIn', () => { - map.setIn(['a'], 2) - expect(map.get('a')).toMatchObject({ value: 2 }) - map.setIn(['b', 1], 5) - expect(map.getIn(['b', 1])).toMatchObject({ value: 5 }) - map.setIn([1], 6) - expect(map.get(1)).toMatchObject({ value: 6 }) - map.setIn(['b', 2], 6) - expect(map.getIn(['b', 2])).toMatchObject({ value: 6 }) - map.setIn(['e', 'e'], 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']) - expect(subSeq).toBeInstanceOf(YAMLSeq) - expect((subSeq as any).items).toHaveLength(3) - }) -}) - describe('Document', () => { let doc: Document | YAMLSeq>>> beforeEach(() => { @@ -384,17 +312,6 @@ describe('Document', () => { expect(doc.value.items).toHaveLength(3) }) - test('addIn', () => { - doc.addIn(['b'], 4) - expect(doc.getIn(['b', 2])).toMatchObject({ value: 4 }) - doc.addIn([], doc.createPair('c', 5)) - expect(doc.get('c')).toMatchObject({ value: 5 }) - expect(() => doc.addIn(['a'], -1)).toThrow(/Expected YAML collection/) - doc.addIn(['b', 3], 6) - expect(doc.value.items).toHaveLength(3) - expect((doc.get('b') as any).items).toHaveLength(4) - }) - test('delete', () => { expect(doc.delete('a')).toBe(true) expect(doc.delete('a')).toBe(false) @@ -407,19 +324,6 @@ describe('Document', () => { expect(() => doc.set('a', 1)).toThrow(/document value/) }) - test('deleteIn', () => { - expect(doc.deleteIn(['a'])).toBe(true) - expect(doc.get('a')).toBeUndefined() - expect(doc.deleteIn(['b', 1])).toBe(true) - expect(doc.getIn(['b', 1])).toBeUndefined() - expect(doc.deleteIn([1])).toBe(false) - expect(doc.deleteIn(['b', 2])).toBe(false) - expect(() => doc.deleteIn(['a', 'e'])).toThrow(/Expected/) - expect(doc.value.items).toHaveLength(1) - expect((doc.get('b') as any).items).toHaveLength(1) - expect(() => doc.deleteIn(null as any)).toThrow() - }) - test('get', () => { expect(doc.get('a')).toMatchObject({ value: 1 }) expect(doc.get('c')).toBeUndefined() @@ -430,21 +334,6 @@ describe('Document', () => { expect(doc.get('a')).toBeUndefined() }) - test('getIn collection', () => { - 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() - }) - - test('getIn scalar', () => { - const doc = new Document('s') - expect(doc.getIn([])).toMatchObject({ value: 's' }) - expect(() => doc.getIn(null as any)).toThrow() - expect(doc.getIn([0])).toBeUndefined() - }) - test('has', () => { expect(doc.has('a')).toBe(true) expect(doc.has('c')).toBe(false) @@ -455,14 +344,6 @@ describe('Document', () => { expect(doc.has('a')).toBe(false) }) - test('hasIn', () => { - expect(doc.hasIn(['a'])).toBe(true) - expect(doc.hasIn(['b', 1])).toBe(true) - expect(() => doc.hasIn(['b', 'e'])).toThrow(TypeError) - expect(doc.hasIn(['c', 'e'])).toBe(false) - expect(doc.hasIn(['a', 'e'])).toBe(false) - }) - test('set', () => { doc.set('a', 2) expect(doc.get('a')).toMatchObject({ value: 2 }) @@ -475,70 +356,4 @@ describe('Document', () => { const doc = new Document('s') expect(() => doc.set('a', 1)).toThrow(/document value/) }) - - test('setIn', () => { - doc.setIn(['a'], 2) - expect(doc.getIn(['a'])).toMatchObject({ value: 2 }) - doc.setIn(['b', 1], 5) - expect(doc.getIn(['b', 1])).toMatchObject({ value: 5 }) - doc.setIn(['c'], 6) - expect(doc.getIn(['c'])).toMatchObject({ value: 6 }) - 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.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 value', () => { - const doc = new Document('s') - expect(() => doc.setIn(['a'], 1)).toThrow(/document value/) - }) - - test('setIn on parsed document', () => { - const doc = parseDocument('{ a: 1, b: [2, 3] }') - doc.setIn(['c', 1], 9) - expect(String(doc)).toBe('{ a: 1, b: [ 2, 3 ], c: [ null, 9 ] }\n') - }) - - test('setIn with __proto__ as key', () => { - doc.setIn(['c', '__proto__'], 9) - expect(String(doc)).toBe('a: 1\nb:\n - 2\n - 3\nc:\n __proto__: 9\n') - }) - - test('setIn with object key', () => { - doc.value = doc.createNode({}) - const foo = { foo: 'FOO' } - doc.setIn([foo], 'BAR') - expect(doc.value.items).toMatchObject([ - { - key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, - value: { value: 'BAR' } - } - ]) - }) - - test('setIn with repeated object key', () => { - doc.value = doc.createNode({}) - const foo = { foo: 'FOO' } - doc.setIn([foo, foo], 'BAR') - expect(doc.value.items).toMatchObject([ - { - key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, - value: { - items: [ - { - key: { - items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] - }, - value: { value: 'BAR' } - } - ] - } - } - ]) - }) }) diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index b1386700..16cfd968 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']).anchor = 'a' - doc.getIn([0, 'a']).anchor = '' - doc.getIn([1, 'b']).anchor = 'BB' + doc.get(0).get('a').anchor = 'a' + doc.get(0).get('a').anchor = '' + doc.get(1).get('b').anchor = 'BB' expect(String(doc)).toBe('[ &AA { a: A }, { b: &BB B } ]\n') }) @@ -341,8 +341,8 @@ describe('merge <<', () => { const doc = parseDocument('[{ a: A }, { b: B }]', { merge: true }) - const alias = doc.createAlias(doc.getIn([0, 'a'])) - doc.addIn([1], doc.createPair('<<', alias)) + const alias = doc.createAlias(doc.get(0).get('a')) + doc.get(1).add(doc.createPair('<<', alias)) expect(String(doc)).toBe('[ { a: &a1 A }, { b: B, <<: *a1 } ]\n') expect(() => doc.toJS()).toThrow( 'Merge sources must be maps or map aliases' diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 842c64ce..9af3ceaf 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -354,8 +354,8 @@ describe('circular references', () => { const baz: any = {} const map = { foo: { bar: { baz } } } baz.map = map - const doc = new Document(map) - expect(doc.getIn(['foo', 'bar', 'baz', 'map'])).toMatchObject({ + const doc = new Document(map) + expect(doc.get('foo').get('bar').get('baz').get('map')).toMatchObject({ source: 'a1' }) expect(doc.toString()).toBe(source` @@ -396,9 +396,9 @@ describe('circular references', () => { const baz = { a: 1 } const seq = [{ foo: { bar: { baz } } }, { fe: { fi: { fo: { baz } } } }] const doc = new Document(null) - const node = doc.createNode(seq) - const source = node.getIn([0, 'foo', 'bar', 'baz']) as Node - const alias = node.getIn([1, 'fe', 'fi', 'fo', 'baz']) as Alias + const node = doc.createNode(seq) as any + const source = node.get(0).get('foo').get('bar').get('baz') as Node + const alias = node.get(1).get('fe').get('fi').get('fo').get('baz') as Alias expect(source).toMatchObject({ items: [{ key: { value: 'a' }, value: { value: 1 } }] }) diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 31a3cf28..91b0449d 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -907,7 +907,7 @@ date (00:00:00Z): 2002-12-14\n`) const src = '- { a: A, b: B }\n- { b: X }\n' const doc = parseDocument(src, { version: '1.1' }) const alias = doc.createAlias(doc.get(0), 'a') - doc.addIn([1], doc.createPair('<<', alias)) + doc.get(1).add(doc.createPair('<<', alias)) expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n') expect(doc.toJS()).toMatchObject([ { a: 'A', b: 'B' }, @@ -919,7 +919,7 @@ date (00:00:00Z): 2002-12-14\n`) const src = '- { a: A, b: B }\n- { b: X }\n' const doc = parseDocument(src, { version: '1.1' }) const alias = doc.createAlias(doc.get(0), 'a') - doc.addIn([1], doc.createPair('<<', alias)) + doc.get(1).add(doc.createPair('<<', alias)) expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n') expect(doc.toJS()).toMatchObject([ { a: 'A', b: 'B' }, From a2fe9cc939a50eba552b62cb116da1b2bb8a9d45 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 15 Feb 2026 00:34:40 +0200 Subject: [PATCH 2/5] feat!: Turn Collection into an interface, add isCollection() --- docs/05_content_nodes.md | 6 +-- src/compose/composer.ts | 4 +- src/doc/Document.ts | 9 ++-- src/doc/NodeCreator.ts | 7 ++- src/index.ts | 2 +- src/nodes/Collection.ts | 80 ++++++++-------------------------- src/nodes/Node.ts | 8 +++- src/nodes/YAMLMap.ts | 73 +++++++++++++++++++++++++++---- src/nodes/YAMLSeq.ts | 67 +++++++++++++++++++++++++--- src/nodes/identity.ts | 12 ++++- src/stringify/stringify.ts | 4 +- src/stringify/stringifyPair.ts | 6 +-- src/test-events.ts | 6 +-- 13 files changed, 178 insertions(+), 106 deletions(-) diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index 584491db..a1fa1499 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -54,14 +54,14 @@ class Pair { value: Node | null } -class Collection implements NodeBase { +interface CollectionBase extends NodeBase { anchor?: string // an anchor associated with this node flow?: boolean // use flow style when stringifying this schema?: Schema clone(schema?: Schema): this // a deep copy of this collection } -class YAMLMap extends Collection { +class YAMLMap implements CollectionBase { items: Pair[] add(pair: Pair | { key: K; value: V }, overwrite?: boolean): void delete(key: K): boolean @@ -70,7 +70,7 @@ class YAMLMap extends Collection { set(key: K, value: V): void } -class YAMLSeq extends Collection { +class YAMLSeq implements CollectionBase { items: T[] add(value: T): void delete(key: number | Scalar): boolean diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 990a4846..528a8359 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -2,7 +2,7 @@ import { Directives } from '../doc/directives.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 { isCollection } from '../nodes/identity.ts' import type { Range } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import type { @@ -111,7 +111,7 @@ export class Composer< doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment } else if (afterEmptyLine || doc.directives.docStart) { doc.commentBefore = comment - } else if (dc instanceof Collection && !dc.flow && dc.items.length > 0) { + } else if (isCollection(dc) && !dc.flow && dc.items.length > 0) { let it = dc.items[0] if (it instanceof Pair) it = it.key const cb = it.commentBefore diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 96face54..f4975c4f 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,6 +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 { Primitive } from '../nodes/Collection.ts' +import { isCollection } from '../nodes/identity.ts' import type { Node, NodeType, Range } from '../nodes/Node.ts' import type { Pair } from '../nodes/Pair.ts' import type { Scalar } from '../nodes/Scalar.ts' @@ -245,14 +246,14 @@ export class Document< */ get(key?: any): Strict extends true ? Node | Pair | undefined : any { if (key === undefined) return this.value - return this.value instanceof Collection ? this.value.get(key) : undefined + return isCollection(this.value) ? this.value.get(key) : undefined } /** * Checks if the document includes a value with the key `key`. */ has(key: any): boolean { - return this.value instanceof Collection ? this.value.has(key) : false + return isCollection(this.value) ? this.value.has(key) : false } /** @@ -343,6 +344,6 @@ export class Document< } function assertCollection(value: unknown) { - if (value instanceof Collection) return value as YAMLMap | YAMLSeq + if (isCollection(value)) return value throw new Error('Expected a YAML collection as document value') } diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index 7cf5035f..c88cef67 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -1,6 +1,5 @@ import { Alias } from '../nodes/Alias.ts' -import { Collection } from '../nodes/Collection.ts' -import { isNode } from '../nodes/identity.ts' +import { isCollection, isNode } from '../nodes/identity.ts' import { type Node } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' @@ -140,7 +139,7 @@ export class NodeCreator { else if (!tagObj.default) node.tag = tagObj.tag if (ref) ref.node = node - if (this.#flow && node instanceof Collection) node.flow = true + if (this.#flow && isCollection(node)) node.flow = true return node } @@ -161,7 +160,7 @@ export class NodeCreator { if ( typeof ref === 'object' && ref.anchor && - (ref.node instanceof Scalar || ref.node instanceof Collection) + (ref.node instanceof Scalar || isCollection(ref.node)) ) { ref.node.anchor = ref.anchor } else { diff --git a/src/index.ts b/src/index.ts index ad063f60..f38c0fe3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ export type { ErrorCode } from './errors.ts' export { YAMLError, YAMLParseError, YAMLWarning } from './errors.ts' export { Alias } from './nodes/Alias.ts' -export { isNode } from './nodes/identity.ts' +export { isCollection, 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' diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index e0316052..fa4b2cc7 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -1,94 +1,50 @@ -import type { Token } from '../parse/cst.ts' import type { Schema } from '../schema/Schema.ts' -import type { Node, Range } from './Node.ts' +import type { Node, NodeBase } 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 type Collection = YAMLMap | YAMLSeq export type Primitive = boolean | number | bigint | string | null + export type NodeOf = T extends Primitive ? Scalar : T -export abstract class Collection { +export interface CollectionBase extends NodeBase { schema: Schema | undefined - declare items: (Node | Pair)[] - /** An optional anchor on this collection. Used by alias nodes. */ - declare anchor?: string + anchor?: string /** * If true, stringify this and all child nodes using flow rather than * block styles. */ - 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) { - Object.defineProperty(this, 'schema', { - value: schema, - configurable: true, - enumerable: false, - writable: true - }) - } + flow?: boolean /** * Create a copy of this collection. * * @param schema - If defined, overwrites the original's schema */ - 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] - return copy - } + clone(schema?: Schema): this /** Adds a value to the collection. */ - abstract add(value: unknown): void + add(value: unknown): void /** * Removes a value from the collection. * @returns `true` if the item was found and removed. */ - abstract delete(key: unknown): boolean + delete(key: unknown): boolean - /** - * Returns item at `key`, or `undefined` if not found. - */ - abstract get(key: unknown): Node | Pair | undefined + /** Returns item at `key`, or `undefined` if not found. */ + get(key: unknown): Node | Pair | undefined - /** - * Checks if the collection includes a value with the key `key`. - */ - abstract has(key: unknown): boolean + /** Checks if the collection includes a value with the key `key`. */ + has(key: unknown): boolean - /** - * Sets a value in this collection. - */ - abstract set(key: unknown, value: unknown): void + /** Sets a value in this collection. */ + set(key: unknown, value: unknown): void } diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index d656e111..d049c810 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -53,8 +53,12 @@ export interface NodeBase { /** A fully qualified tag, if required */ tag?: string - /** Create a copy of this node. */ - clone(_schema?: Schema): this + /** + * Create a copy of this node. + * + * @param schema - If defined, overwrites the original's schema for cloned collections. + */ + clone(schema?: Schema): this /** A plain JavaScript representation of this node. */ toJS(doc: Document, opt?: ToJSContext): any diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index 84bb77ea..45e066d8 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -2,12 +2,13 @@ import type { Document, DocValue } from '../doc/Document.ts' import { NodeCreator } from '../doc/NodeCreator.ts' 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 { addPairToJSMap } from './addPairToJSMap.ts' -import { Collection, type NodeOf, type Primitive } from './Collection.ts' +import type { CollectionBase, NodeOf, Primitive } from './Collection.ts' import { isNode } from './identity.ts' -import type { Node, NodeBase } from './Node.ts' +import type { Node, Range } from './Node.ts' import { Pair } from './Pair.ts' import { Scalar } from './Scalar.ts' import { ToJSContext } from './toJS.ts' @@ -32,17 +33,46 @@ export function findPair< export class YAMLMap< K extends Primitive | Node = Primitive | Node, V extends Primitive | Node = Primitive | Node -> - extends Collection - implements NodeBase -{ +> implements CollectionBase { + items: Pair[] = [] + + schema: Schema | undefined + + /** An optional anchor on this collection. Used by alias nodes. */ + declare anchor?: string + + /** + * If true, stringify this and all child nodes using flow rather than + * block styles. + */ + 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?: BlockMap | FlowCollection + + /** A fully qualified tag, if required */ + declare tag?: string + static get tagName(): 'tag:yaml.org,2002:map' { return 'tag:yaml.org,2002:map' } - items: Pair[] = [] - declare srcToken?: BlockMap | FlowCollection - /** * A generic collection parsing method that can be extended * to other node classes that inherit from YAMLMap @@ -67,6 +97,31 @@ export class YAMLMap< return map } + constructor(schema?: Schema) { + Object.defineProperty(this, 'schema', { + value: schema, + configurable: true, + enumerable: false, + writable: true + }) + } + + /** + * Create a copy of this collection. + * + * @param schema - If defined, overwrites the original's schema + */ + 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] + return copy + } + /** * Adds a key-value pair to the map. * diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index d083ac88..abb846c5 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -2,11 +2,12 @@ import type { Document, DocValue } from '../doc/Document.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, type NodeOf, type Primitive } from './Collection.ts' +import type { CollectionBase, NodeOf, Primitive } from './Collection.ts' import { isNode } from './identity.ts' -import type { Node, NodeBase } from './Node.ts' +import type { Node, Range } from './Node.ts' import type { Pair } from './Pair.ts' import { Scalar } from './Scalar.ts' import { ToJSContext } from './toJS.ts' @@ -16,17 +17,71 @@ const isScalarValue = (value: unknown): boolean => export class YAMLSeq< T extends Primitive | Node | Pair = Primitive | Node | Pair -> - extends Collection - implements NodeBase -{ +> implements CollectionBase { static get tagName(): 'tag:yaml.org,2002:seq' { return 'tag:yaml.org,2002:seq' } items: NodeOf[] = [] + + schema: Schema | undefined + + /** An optional anchor on this collection. Used by alias nodes. */ + declare anchor?: string + + /** + * If true, stringify this and all child nodes using flow rather than + * block styles. + */ + 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?: BlockSequence | FlowCollection + /** A fully qualified tag, if required */ + declare tag?: string + + constructor(schema?: Schema) { + Object.defineProperty(this, 'schema', { + value: schema, + configurable: true, + enumerable: false, + writable: true + }) + } + + /** + * Create a copy of this collection. + * + * @param schema - If defined, overwrites the original's schema + */ + 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) as NodeOf) + if (this.range) copy.range = [...this.range] + return copy + } + add( value: T, options?: Omit diff --git a/src/nodes/identity.ts b/src/nodes/identity.ts index a3bb0314..01d0c363 100644 --- a/src/nodes/identity.ts +++ b/src/nodes/identity.ts @@ -1,8 +1,16 @@ import { Alias } from './Alias.ts' -import { Collection } from './Collection.ts' import type { Node } from './Node.ts' import { Scalar } from './Scalar.ts' +import { YAMLMap } from './YAMLMap.ts' +import { YAMLSeq } from './YAMLSeq.ts' + +/** Type predicate for collections */ +export const isCollection = (node: unknown): node is YAMLMap | YAMLSeq => + node instanceof YAMLMap || node instanceof YAMLSeq /** Type predicate for `Node` values */ export const isNode = (node: unknown): node is Node => - node instanceof Scalar || node instanceof Alias || node instanceof Collection + node instanceof Scalar || + node instanceof Alias || + node instanceof YAMLMap || + node instanceof YAMLSeq diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 6983ae19..fe2cbd1b 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,7 +1,6 @@ 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 type { Node } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' @@ -119,8 +118,7 @@ function stringifyProps( ) { if (!doc.directives) return '' const props = [] - const anchor = - (node instanceof Scalar || node instanceof Collection) && node.anchor + const anchor = node.anchor if (anchor && anchorIsValid(anchor)) { anchors.add(anchor) props.push(`&${anchor}`) diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts index 109cabbf..0e98f8e3 100644 --- a/src/stringify/stringifyPair.ts +++ b/src/stringify/stringifyPair.ts @@ -1,4 +1,4 @@ -import { Collection } from '../nodes/Collection.ts' +import { isCollection } from '../nodes/identity.ts' import type { Pair } from '../nodes/Pair.ts' import { Scalar } from '../nodes/Scalar.ts' import { YAMLSeq } from '../nodes/YAMLSeq.ts' @@ -22,7 +22,7 @@ export function stringifyPair( if (key.comment) { throw new Error('With simple keys, key nodes cannot have comments') } - if (key instanceof Collection) { + if (isCollection(key)) { const msg = 'With simple keys, collection cannot be used as a key value' throw new Error(msg) } @@ -134,7 +134,7 @@ export function stringifyPair( } else { ws += `\n${ctx.indent}` } - } else if (!explicitKey && value instanceof Collection) { + } else if (!explicitKey && isCollection(value)) { 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 41645f27..d5132476 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -1,6 +1,5 @@ import type { Document } from './doc/Document.ts' import { Alias } from './nodes/Alias.ts' -import { Collection } from './nodes/Collection.ts' import { isNode } from './nodes/identity.ts' import type { Node } from './nodes/Node.ts' import { Pair } from './nodes/Pair.ts' @@ -78,10 +77,7 @@ function addEvents( if (errPos !== -1 && isNode(node) && node.range![0] >= errPos) throw new Error() let props = '' - let anchor = - node instanceof Scalar || node instanceof Collection - ? node.anchor - : undefined + let anchor = isNode(node) ? node.anchor : undefined if (anchor) { if (/\d$/.test(anchor)) { const alt = anchor.replace(/\d$/, '') From c7de1b15c5289549d538f4cba34587235e034db7 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 15 Feb 2026 01:39:49 +0200 Subject: [PATCH 3/5] feat!: Require createNode() for collection tags, rename static .from() methods as .create() --- docs/06_custom_tags.md | 16 +++++++++------- src/doc/NodeCreator.ts | 11 ++--------- src/nodes/YAMLMap.ts | 4 ++-- src/nodes/YAMLSeq.ts | 34 +++++++++++++++++++--------------- src/schema/Schema.ts | 6 +++--- src/schema/common/map.ts | 25 +++++++++++++++++++++---- src/schema/common/seq.ts | 22 ++++++++++++++++++---- src/schema/types.ts | 6 ++---- src/schema/yaml-1.1/omap.ts | 14 +++++++------- src/schema/yaml-1.1/set.ts | 23 ++++++++++++----------- tests/doc/types.ts | 3 +++ 11 files changed, 98 insertions(+), 66 deletions(-) diff --git a/docs/06_custom_tags.md b/docs/06_custom_tags.md index 6b047f69..54f89485 100644 --- a/docs/06_custom_tags.md +++ b/docs/06_custom_tags.md @@ -110,6 +110,9 @@ const nullObject = { tag: '!nullobject', collection: 'map', nodeClass: YAMLNullObject, + createNode(nc, obj) { + return YAMLNullObject.create(nc, obj) + }, identify: v => !!(typeof v === 'object' && v && !Object.getPrototypeOf(v)) } @@ -147,18 +150,17 @@ class YAMLError extends YAMLMap { }) return Object.assign(er, rest) } - - static from(schema, obj, ctx) { - const { name, message, stack } = obj - // ensure these props remain, even if not enumerable - return super.from(schema, { ...obj, name, message, stack }, ctx) - } } const error = { tag: '!error', collection: 'map', nodeClass: YAMLError, + createNode(nc, obj) { + const { name, message, stack } = obj + // ensure these props remain, even if not enumerable + return YAMLNullObject.create(nc, { ...obj, name, message, stack }) + }, identify: v => !!(typeof v === 'object' && v && v instanceof Error) } @@ -240,7 +242,7 @@ import { To define your own tag, you'll need to define an object comprising of some of the following fields. Those in bold are required: -- `createNode(schema, value, ctx): Node` is an optional factory function, used e.g. by collections when wrapping JS objects as AST nodes. +- `createNode(nodeCreator, value): Node` is a factory function, required by collection tags for wrapping JS objects as AST nodes. - `format: string` If a tag has multiple forms that should be parsed and/or stringified differently, use `format` to identify them. Used by `!!int` and `!!float`. - **`identify(value): boolean`** is used by `doc.createNode()` to detect your data type, e.g. using `typeof` or `instanceof`. Required. - `nodeClass: Node` is the `Node` child class that implements this tag. Required for collections and tags that have overlapping JS representations. diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index c88cef67..69ab9cd2 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -3,7 +3,6 @@ import { isCollection, 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' import type { CreateNodeOptions } from '../options.ts' import type { Schema } from '../schema/Schema.ts' import type { CollectionTag, ScalarTag } from '../schema/types.ts' @@ -62,10 +61,7 @@ export class NodeCreator { if (value instanceof Document) value = value.value if (isNode(value)) return value if (value instanceof Pair) { - const map = (this.schema.map.nodeClass! as typeof YAMLMap).from( - this, - null - ) + const map = this.schema.map.createNode(this, null) map.items.push(value) return map } @@ -131,10 +127,7 @@ export class NodeCreator { this.#onTagObj = undefined } - const node = - tagObj?.createNode?.(this, value) ?? - tagObj?.nodeClass?.from?.(this, value) ?? - new Scalar(value) + const node = tagObj?.createNode?.(this, value) ?? new Scalar(value) if (tagName) node.tag = tagName else if (!tagObj.default) node.tag = tagObj.tag diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index 45e066d8..be4ffc1e 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -74,10 +74,10 @@ export class YAMLMap< } /** - * A generic collection parsing method that can be extended + * A generic collection factory method that can be extended * to other node classes that inherit from YAMLMap */ - static from(nc: NodeCreator, obj: unknown): YAMLMap { + static create(nc: NodeCreator, obj: unknown): YAMLMap { const { replacer } = nc const map = new this(nc.schema) const add = (key: unknown, value: unknown) => { diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index abb846c5..75b16c87 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -57,6 +57,25 @@ export class YAMLSeq< /** A fully qualified tag, if required */ declare tag?: string + /** + * A generic collection factory method that can be extended + * to other node classes that inherit from YAMLSeq + */ + static create(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 nc.replacer === 'function') { + const key = obj instanceof Set ? it : String(i++) + it = nc.replacer.call(obj, key, it) + } + seq.items.push(nc.create(it)) + } + } + return seq + } + constructor(schema?: Schema) { Object.defineProperty(this, 'schema', { value: schema, @@ -188,19 +207,4 @@ export class YAMLSeq< onComment }) } - - 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 nc.replacer === 'function') { - const key = obj instanceof Set ? it : String(i++) - it = nc.replacer.call(obj, key, it) - } - seq.items.push(nc.create(it)) - } - } - return seq - } } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index 47b400fa..41bb2e90 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -19,11 +19,11 @@ export class Schema { // These are used by createNode() and composeScalar() /** @internal */ - declare readonly map: CollectionTag + declare readonly map: typeof map /** @internal */ - declare readonly scalar: ScalarTag + declare readonly scalar: typeof string /** @internal */ - declare readonly seq: CollectionTag + declare readonly seq: typeof seq constructor({ compat, diff --git a/src/schema/common/map.ts b/src/schema/common/map.ts index 73c97d6c..3256a2a3 100644 --- a/src/schema/common/map.ts +++ b/src/schema/common/map.ts @@ -1,13 +1,30 @@ +import type { NodeCreator } from '../../doc/NodeCreator.ts' import { YAMLMap } from '../../nodes/YAMLMap.ts' +import type { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { CollectionTag } from '../types.ts' -export const map: CollectionTag = { +export const map: { + collection: 'map' + default: true + nodeClass: typeof YAMLMap + tag: string + createNode(nc: NodeCreator, obj: unknown): YAMLMap + resolve( + map: YAMLMap | YAMLSeq, + onError: (message: string) => void + ): YAMLMap +} = { collection: 'map', default: true, nodeClass: YAMLMap, tag: 'tag:yaml.org,2002:map', - resolve(map, onError) { + + createNode(nc: NodeCreator, obj: unknown): YAMLMap { + return YAMLMap.create(nc, obj) + }, + + resolve(map: YAMLMap | YAMLSeq, onError: (message: string) => void) { if (!(map instanceof YAMLMap)) onError('Expected a mapping for this tag') - return map + return map as YAMLMap } -} +} satisfies CollectionTag diff --git a/src/schema/common/seq.ts b/src/schema/common/seq.ts index 5a01192f..e4bfdce5 100644 --- a/src/schema/common/seq.ts +++ b/src/schema/common/seq.ts @@ -1,13 +1,27 @@ +import type { NodeCreator } from '../../doc/NodeCreator.ts' +import type { YAMLMap } from '../../nodes/YAMLMap.ts' import { YAMLSeq } from '../../nodes/YAMLSeq.ts' import type { CollectionTag } from '../types.ts' -export const seq: CollectionTag = { +export const seq: { + collection: 'seq' + default: true + nodeClass: typeof YAMLSeq + tag: string + createNode(nc: NodeCreator, obj: unknown): YAMLSeq + resolve(seq: YAMLMap | YAMLSeq, onError: (message: string) => void): YAMLSeq +} = { collection: 'seq', default: true, nodeClass: YAMLSeq, tag: 'tag:yaml.org,2002:seq', - resolve(seq, onError) { + + createNode(nc: NodeCreator, obj: unknown): YAMLSeq { + return YAMLSeq.create(nc, obj) + }, + + resolve(seq: YAMLMap | YAMLSeq, onError: (message: string) => void): YAMLSeq { if (!(seq instanceof YAMLSeq)) onError('Expected a sequence for this tag') - return seq + return seq as YAMLSeq } -} +} satisfies CollectionTag diff --git a/src/schema/types.ts b/src/schema/types.ts index 042584df..a0088a8e 100644 --- a/src/schema/types.ts +++ b/src/schema/types.ts @@ -91,16 +91,14 @@ export interface CollectionTag extends TagBase { /** The source collection type supported by this tag. */ collection: 'map' | 'seq' + createNode: (nc: NodeCreator, value: unknown) => Node + /** * The `Node` child class that implements this tag. * If set, used to select this tag when stringifying. - * - * If the class provides a static `from` method, then that - * will be used if the tag object doesn't have a `createNode` method. */ nodeClass?: { new (schema?: Schema): 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 e65dc1aa..d37ff6aa 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -47,13 +47,6 @@ export class YAMLOMap< } return map as unknown as unknown[] } - - static from(nc: NodeCreator, iterable: unknown): YAMLOMap { - const pairs = createPairs(nc, iterable) - const omap = new this(nc.schema) - omap.items = pairs.items - return omap - } } export const omap: CollectionTag = { @@ -63,6 +56,13 @@ export const omap: CollectionTag = { default: false, tag: 'tag:yaml.org,2002:omap', + createNode(nc: NodeCreator, iterable: unknown): YAMLOMap { + const pairs = createPairs(nc, iterable) + const omap = new YAMLOMap(nc.schema) + omap.items = pairs.items + return omap + }, + resolve(seq, onError) { const pairs = resolvePairs(seq, onError) const seenKeys: unknown[] = [] diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index 2e47a55c..651bd114 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -94,17 +94,6 @@ export class YAMLSet< if (!ctx) return JSON.stringify(this) return super.toString({ ...ctx, noValues: true }, onComment, onChompKeep) } - - 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 nc.replacer === 'function') - value = nc.replacer.call(iterable, value, value) - set.items.push(nc.createPair(value, null) as Pair) - } - return set - } } const hasAllNullValues = (map: YAMLMap): boolean => @@ -124,6 +113,18 @@ export const set: CollectionTag = { nodeClass: YAMLSet, default: false, tag: 'tag:yaml.org,2002:set', + + createNode(nc: NodeCreator, iterable: unknown): YAMLSet { + const set = new YAMLSet(nc.schema) + if (iterable && Symbol.iterator in Object(iterable)) + 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) + } + return set + }, + resolve(map, onError) { if (!(map instanceof YAMLMap)) { onError('Expected a mapping for this tag') diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 91b0449d..02a050d0 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -1060,6 +1060,9 @@ describe('custom tags', () => { const nullObject: CollectionTag = { tag: '!nullobject', collection: 'map', + createNode(nc, value) { + return YAMLNullObject.create(nc, value) + }, identify: (value: any) => !!value && typeof value === 'object' && !Object.getPrototypeOf(value), nodeClass: YAMLNullObject From b60f14ada5328acda6191e661cde0951f738b84b Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 15 Feb 2026 12:13:38 +0200 Subject: [PATCH 4/5] feat!: Have YAMLMap & YAMLSeq extend Array, rather than containing an .items Array --- .github/vitest.browserstack.config.js | 1 + src/compose/composer.ts | 4 +- src/compose/resolve-block-map.ts | 6 +- src/compose/resolve-block-seq.ts | 2 +- src/compose/resolve-flow-collection.ts | 17 +- src/doc/NodeCreator.ts | 2 +- src/nodes/Alias.ts | 4 +- src/nodes/Collection.ts | 16 ++ src/nodes/Pair.ts | 4 + src/nodes/YAMLMap.ts | 52 ++-- src/nodes/YAMLSeq.ts | 41 ++- src/schema/yaml-1.1/merge.ts | 11 +- src/schema/yaml-1.1/omap.ts | 12 +- src/schema/yaml-1.1/pairs.ts | 14 +- src/schema/yaml-1.1/set.ts | 18 +- src/stringify/stringifyCollection.ts | 18 +- src/stringify/stringifyPair.ts | 2 +- src/test-events.ts | 4 +- src/visit.ts | 14 +- tests/_setup.ts | 12 + tests/cli.ts | 6 +- tests/collection-access.ts | 71 +++-- tests/directives.ts | 20 +- tests/doc/YAML-1.2.spec.ts | 10 +- tests/doc/anchors.ts | 88 +++--- tests/doc/comments.ts | 359 +++++++++++-------------- tests/doc/createNode.ts | 153 +++++------ tests/doc/errors.ts | 24 +- tests/doc/foldFlowLines.ts | 8 +- tests/doc/parse.ts | 97 +++---- tests/doc/stringify.ts | 24 +- tests/doc/types.ts | 65 +++-- tests/visit.ts | 2 +- vitest.config.js | 1 + 34 files changed, 545 insertions(+), 637 deletions(-) create mode 100644 tests/_setup.ts diff --git a/.github/vitest.browserstack.config.js b/.github/vitest.browserstack.config.js index 8c35ad4c..f3c69255 100644 --- a/.github/vitest.browserstack.config.js +++ b/.github/vitest.browserstack.config.js @@ -42,6 +42,7 @@ export default defineConfig({ instances }, globals: true, + setupFiles: ['tests/_setup.ts'], include: ['tests/**/*.{js,ts}'], exclude: [ 'tests/_*', diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 528a8359..5900e51b 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -111,8 +111,8 @@ export class Composer< doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment } else if (afterEmptyLine || doc.directives.docStart) { doc.commentBefore = comment - } else if (isCollection(dc) && !dc.flow && dc.items.length > 0) { - let it = dc.items[0] + } else if (isCollection(dc) && !dc.flow && dc.length > 0) { + let it = dc[0] if (it instanceof Pair) it = it.key const cb = it.commentBefore it.commentBefore = cb ? `${comment}\n${cb}` : comment diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index 8cdb03ff..7fa1a2b5 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -76,7 +76,7 @@ export function resolveBlockMap( if (ctx.schema.compat) flowIndentCheck(bm.indent, key, onError) ctx.atKey = false - if (mapIncludes(ctx, map.items, keyNode)) + if (mapIncludes(ctx, map, keyNode)) onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique') // value properties @@ -116,7 +116,7 @@ export function resolveBlockMap( offset = valueNode.range![2] const pair = new Pair(keyNode, valueNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem - map.items.push(pair) + map.push(pair) } else { // key with no value if (implicitKey) @@ -131,7 +131,7 @@ export function resolveBlockMap( } const pair = new Pair(keyNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem - map.items.push(pair) + map.push(pair) } } diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts index a9617073..e086787b 100644 --- a/src/compose/resolve-block-seq.ts +++ b/src/compose/resolve-block-seq.ts @@ -50,7 +50,7 @@ export function resolveBlockSeq( : composeEmptyNode(ctx, props.end, start, null, props, onError) if (ctx.schema.compat) flowIndentCheck(bs.indent, value, onError) offset = node.range![2] - seq.items.push(node) + seq.push(node) } seq.range = [bs.offset, offset, commentEnd ?? offset] return seq diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index cecdc280..d34ca5f8 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -23,8 +23,9 @@ export function resolveFlowCollection( ): YAMLMap | YAMLSeq { const isMap = fc.start.source === '{' const fcName = isMap ? 'flow map' : 'flow sequence' - const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq) - const coll = new NodeClass(ctx.schema) as YAMLMap | YAMLSeq + let coll + if (tag?.nodeClass) coll = new tag.nodeClass(ctx.schema) as YAMLMap | YAMLSeq + else coll = isMap ? new YAMLMap(ctx.schema) : new YAMLSeq(ctx.schema) coll.flow = true const atRoot = ctx.atRoot if (atRoot) ctx.atRoot = false @@ -93,7 +94,7 @@ export function resolveFlowCollection( } } if (prevItemComment) { - let prev = coll.items[coll.items.length - 1] + let prev = coll[coll.length - 1] if (prev instanceof Pair) prev = prev.value ?? prev.key if (prev.comment) prev.comment += '\n' + prevItemComment else prev.comment = prevItemComment @@ -108,7 +109,7 @@ export function resolveFlowCollection( const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError) - ;(coll as YAMLSeq).items.push(valueNode) + ;(coll as YAMLSeq).push(valueNode) offset = valueNode.range![2] if (isBlock(value)) onError(valueNode.range!, 'BLOCK_IN_FLOW', blockMsg) } else { @@ -190,16 +191,16 @@ export function resolveFlowCollection( if (ctx.options.keepSourceTokens) pair.srcToken = collItem if (isMap) { const map = coll as YAMLMap - if (mapIncludes(ctx, map.items, keyNode)) + if (mapIncludes(ctx, map, keyNode)) onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique') - map.items.push(pair) + map.push(pair) } else { const map = new YAMLMap(ctx.schema) map.flow = true - map.items.push(pair) + map.push(pair) const endRange = (valueNode ?? keyNode).range! map.range = [keyNode.range![0], endRange[1], endRange[2]] - ;(coll as YAMLSeq).items.push(map) + ;(coll as YAMLSeq).push(map) } offset = valueNode ? valueNode.range![2] : valueProps.end } diff --git a/src/doc/NodeCreator.ts b/src/doc/NodeCreator.ts index 69ab9cd2..a266bf66 100644 --- a/src/doc/NodeCreator.ts +++ b/src/doc/NodeCreator.ts @@ -62,7 +62,7 @@ export class NodeCreator { if (isNode(value)) return value if (value instanceof Pair) { const map = this.schema.map.createNode(this, null) - map.items.push(value) + map.push(value) return map } if ( diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts index eee15eac..25182232 100644 --- a/src/nodes/Alias.ts +++ b/src/nodes/Alias.ts @@ -153,9 +153,9 @@ function getAliasCount( const kc = getAliasCount(doc, ctx, node.key, anchors) const vc = getAliasCount(doc, ctx, node.value, anchors) return Math.max(kc, vc) - } else if (node && 'items' in node) { + } else if (Array.isArray(node)) { let count = 0 - for (const item of node.items) { + for (const item of node) { const c = getAliasCount(doc, ctx, item, anchors) if (c > count) count = c } diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index fa4b2cc7..8cb742bf 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -48,3 +48,19 @@ export interface CollectionBase extends NodeBase { /** Sets a value in this collection. */ set(key: unknown, value: unknown): void } + +export function copyCollection( + orig: T, + schema: Schema | undefined +): T { + const copy = (orig.constructor as typeof YAMLMap).from(orig, it => + it.clone(schema) + ) as typeof orig + if (orig.range) copy.range = [...orig.range] + const propDesc = Object.getOwnPropertyDescriptors(orig) + for (const [name, prop] of Object.entries(propDesc)) { + if (!(name in copy)) Object.defineProperty(copy, name, prop) + } + if (schema) copy.schema = schema + return copy +} diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 9296543a..d499cff7 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -15,6 +15,10 @@ export class Pair< key: NodeOf value: NodeOf | null + declare comment?: never + declare commentBefore?: never + declare spaceBefore?: never + /** The CST token that was composed into this pair. */ declare srcToken?: CollectionItem diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index be4ffc1e..a57f1482 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -6,7 +6,12 @@ import type { Schema } from '../schema/Schema.ts' import type { StringifyContext } from '../stringify/stringify.ts' import { stringifyCollection } from '../stringify/stringifyCollection.ts' import { addPairToJSMap } from './addPairToJSMap.ts' -import type { CollectionBase, NodeOf, Primitive } from './Collection.ts' +import { + copyCollection, + type CollectionBase, + type NodeOf, + type Primitive +} from './Collection.ts' import { isNode } from './identity.ts' import type { Node, Range } from './Node.ts' import { Pair } from './Pair.ts' @@ -33,9 +38,10 @@ export function findPair< export class YAMLMap< K extends Primitive | Node = Primitive | Node, V extends Primitive | Node = Primitive | Node -> implements CollectionBase { - items: Pair[] = [] - +> + extends Array> + implements CollectionBase +{ schema: Schema | undefined /** An optional anchor on this collection. Used by alias nodes. */ @@ -84,7 +90,7 @@ export class YAMLMap< if (typeof replacer === 'function') value = replacer.call(obj, key, value) else if (Array.isArray(replacer) && !replacer.includes(key)) return if (value !== undefined || nc.keepUndefined) - map.items.push(nc.createPair(key, value)) + map.push(nc.createPair(key, value)) } if (obj instanceof Map) { for (const [key, value] of obj) add(key, value) @@ -92,12 +98,13 @@ export class YAMLMap< for (const key of Object.keys(obj)) add(key, (obj as any)[key]) } if (typeof nc.schema.sortMapEntries === 'function') { - map.items.sort(nc.schema.sortMapEntries) + map.sort(nc.schema.sortMapEntries) } return map } - constructor(schema?: Schema) { + constructor(schema?: Schema, elements: Array> = []) { + super(...elements) Object.defineProperty(this, 'schema', { value: schema, configurable: true, @@ -112,14 +119,7 @@ export class YAMLMap< * @param schema - If defined, overwrites the original's schema */ 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] - return copy + return copyCollection(this, schema) } /** @@ -130,7 +130,7 @@ export class YAMLMap< add(pair: Pair): void { if (!(pair instanceof Pair)) throw new TypeError('Expected a Pair') - const prev = findPair(this.items, pair.key) + const prev = findPair(this, pair.key) const sortEntries = this.schema?.sortMapEntries if (prev) { // For scalars, keep the old node & its comments and anchors @@ -138,28 +138,28 @@ export class YAMLMap< 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.findIndex(item => sortEntries(pair, item) < 0) + if (i === -1) this.push(pair) + else this.splice(i, 0, pair) } else { - this.items.push(pair) + this.push(pair) } } delete(key: unknown): boolean { - const it = findPair(this.items, key) + const it = findPair(this, key) if (!it) return false - const del = this.items.splice(this.items.indexOf(it), 1) + const del = this.splice(this.indexOf(it), 1) return del.length > 0 } get(key: unknown): NodeOf | undefined { - const it = findPair(this.items, key) + const it = findPair(this, key) return it?.value ?? undefined } has(key: unknown): boolean { - return !!findPair(this.items, key) + return !!findPair(this, key) } set( @@ -203,7 +203,7 @@ export class YAMLMap< ctx ??= new ToJSContext() const map = Type ? new Type() : ctx?.mapAsMap ? new Map() : {} if (this.anchor) ctx.setAnchor(this, map) - for (const item of this.items) addPairToJSMap(doc, ctx, map, item) + for (const item of this) addPairToJSMap(doc, ctx, map, item) return map } @@ -213,7 +213,7 @@ export class YAMLMap< onChompKeep?: () => void ): string { if (!ctx) return JSON.stringify(this) - for (const item of this.items) { + for (const item of this) { 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 75b16c87..3b668de2 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -5,7 +5,7 @@ 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 type { CollectionBase, NodeOf, Primitive } from './Collection.ts' +import { copyCollection, type CollectionBase, type NodeOf, type Primitive } from './Collection.ts' import { isNode } from './identity.ts' import type { Node, Range } from './Node.ts' import type { Pair } from './Pair.ts' @@ -17,13 +17,14 @@ const isScalarValue = (value: unknown): boolean => export class YAMLSeq< T extends Primitive | Node | Pair = Primitive | Node | Pair -> implements CollectionBase { +> + extends Array> + implements CollectionBase +{ static get tagName(): 'tag:yaml.org,2002:seq' { return 'tag:yaml.org,2002:seq' } - items: NodeOf[] = [] - schema: Schema | undefined /** An optional anchor on this collection. Used by alias nodes. */ @@ -70,13 +71,14 @@ export class YAMLSeq< const key = obj instanceof Set ? it : String(i++) it = nc.replacer.call(obj, key, it) } - seq.items.push(nc.create(it)) + seq.push(nc.create(it)) } } return seq } - constructor(schema?: Schema) { + constructor(schema?: Schema, elements: Array> = []) { + super(...elements) Object.defineProperty(this, 'schema', { value: schema, configurable: true, @@ -91,28 +93,21 @@ export class YAMLSeq< * @param schema - If defined, overwrites the original's schema */ 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) as NodeOf) - if (this.range) copy.range = [...this.range] - return copy + return copyCollection(this, schema) } add( value: T, options?: Omit ): void { - if (isNode(value)) this.items.push(value as NodeOf) + if (isNode(value)) this.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) + this.push(nc.create(value) as NodeOf) nc.setAnchors() } } @@ -128,7 +123,7 @@ export class YAMLSeq< 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) + const del = this.splice(idx, 1) return del.length > 0 } @@ -141,7 +136,7 @@ export class YAMLSeq< 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 this.items[idx] + return this[idx] } /** @@ -153,7 +148,7 @@ export class YAMLSeq< 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 + return idx < this.length } /** @@ -170,16 +165,16 @@ export class YAMLSeq< 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] + const prev = this[idx] if (prev instanceof Scalar && isScalarValue(value)) prev.value = value - else if (isNode(value)) this.items[idx] = value as NodeOf + else if (isNode(value)) this[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 + this[idx] = nc.create(value) as NodeOf nc.setAnchors() } } @@ -189,7 +184,7 @@ export class YAMLSeq< ctx ??= new ToJSContext() const res: unknown[] = [] if (this.anchor) ctx.setAnchor(this, res) - for (const item of this.items) res.push(item.toJS(doc, ctx)) + for (const item of this) res.push(item.toJS(doc, ctx)) return res } diff --git a/src/schema/yaml-1.1/merge.ts b/src/schema/yaml-1.1/merge.ts index f6bca109..7c33b59f 100644 --- a/src/schema/yaml-1.1/merge.ts +++ b/src/schema/yaml-1.1/merge.ts @@ -2,8 +2,7 @@ import type { Document, DocValue } from '../../doc/Document.ts' import { Alias } from '../../nodes/Alias.ts' import { Scalar } from '../../nodes/Scalar.ts' import type { ToJSContext } from '../../nodes/toJS.ts' -import type { MapLike, YAMLMap } from '../../nodes/YAMLMap.ts' -import { YAMLSeq } from '../../nodes/YAMLSeq.ts' +import { type MapLike, YAMLMap } from '../../nodes/YAMLMap.ts' import type { ScalarTag } from '../types.ts' // If the value associated with a merge key is a single mapping node, each of @@ -50,11 +49,11 @@ export function addMergeToJSMap( value: unknown ): void { value = ctx && value instanceof Alias ? value.resolve(doc, ctx) : value - if (value instanceof YAMLSeq) - for (const it of value.items) mergeValue(doc, ctx, map, it) - else if (Array.isArray(value)) + if (Array.isArray(value) && !(value instanceof YAMLMap)) { for (const it of value) mergeValue(doc, ctx, map, it) - else mergeValue(doc, ctx, map, value) + } else { + mergeValue(doc, ctx, map, value) + } } function mergeValue( diff --git a/src/schema/yaml-1.1/omap.ts b/src/schema/yaml-1.1/omap.ts index d37ff6aa..5b2b83a6 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -17,8 +17,8 @@ export class YAMLOMap< > extends YAMLSeq> { static tag = 'tag:yaml.org,2002:omap' - constructor(schema?: Schema) { - super(schema) + constructor(schema?: Schema, elements?: Array>) { + super(schema, elements) this.tag = YAMLOMap.tag } @@ -38,7 +38,7 @@ export class YAMLOMap< if (this.anchor) { ctx.anchors.set(this, { aliasCount: 0, count: 1, res: map }) } - for (const pair of this.items) { + for (const pair of this) { const key = pair.key.toJS(doc, ctx) const value = pair.value ? pair.value.toJS(doc, ctx) : pair.value if (map.has(key)) @@ -58,15 +58,13 @@ export const omap: CollectionTag = { createNode(nc: NodeCreator, iterable: unknown): YAMLOMap { const pairs = createPairs(nc, iterable) - const omap = new YAMLOMap(nc.schema) - omap.items = pairs.items - return omap + return new YAMLOMap(nc.schema, pairs) }, resolve(seq, onError) { const pairs = resolvePairs(seq, onError) const seenKeys: unknown[] = [] - for (const { key } of pairs.items) { + for (const { key } of pairs) { if (key instanceof Scalar) { if (seenKeys.includes(key.value)) { onError(`Ordered maps must not include duplicate keys: ${key.value}`) diff --git a/src/schema/yaml-1.1/pairs.ts b/src/schema/yaml-1.1/pairs.ts index 293ca388..86e0dbfd 100644 --- a/src/schema/yaml-1.1/pairs.ts +++ b/src/schema/yaml-1.1/pairs.ts @@ -11,13 +11,13 @@ export function resolvePairs( onError: (message: string) => void ): YAMLSeq { if (seq instanceof YAMLSeq) { - for (let i = 0; i < seq.items.length; ++i) { - const item = seq.items[i] + for (let i = 0; i < seq.length; ++i) { + const item = seq[i] if (item instanceof Pair) continue else if (item instanceof YAMLMap) { - if (item.items.length > 1) + if (item.length > 1) onError('Each pair must have its own sequence indicator') - const pair = item.items[0] || new Pair(new Scalar(null)) + const pair = item[0] || new Pair(new Scalar(null)) if (item.commentBefore) pair.key.commentBefore = pair.key.commentBefore ? `${item.commentBefore}\n${pair.key.commentBefore}` @@ -28,9 +28,9 @@ export function resolvePairs( ? `${item.comment}\n${cn.comment}` : item.comment } - seq.items[i] = pair + seq[i] = pair } else { - seq.items[i] = new Pair(item, null) + seq[i] = new Pair(item, null) } } } else onError('Expected a sequence for this tag') @@ -64,7 +64,7 @@ export function createPairs(nc: NodeCreator, iterable: unknown): YAMLSeq { } else { key = it } - pairs.items.push(nc.createPair(key, value)) + pairs.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 651bd114..d7c1feee 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -38,8 +38,8 @@ export class YAMLSet< } 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) + const prev = findPair(this, value.key) + if (!prev) this.push(value as Pair) } } @@ -47,7 +47,7 @@ export class YAMLSet< * Returns the value matching `key`. */ get(key: unknown): NodeOf | undefined { - const pair = findPair(this.items, key) + const pair = findPair(this, key) return pair?.key } @@ -61,9 +61,9 @@ export class YAMLSet< ): void { if (typeof value !== 'boolean') throw new Error(`Expected a boolean value, not ${typeof value}`) - const prev = findPair(this.items, key) + const prev = findPair(this, key) if (prev && !value) { - this.items.splice(this.items.indexOf(prev), 1) + this.splice(this.indexOf(prev), 1) } else if (!prev && value) { let node: Node if (isNode(key)) { @@ -78,7 +78,7 @@ export class YAMLSet< node = nc.create(key) nc.setAnchors() } - this.items.push(new Pair(node as NodeOf)) + this.push(new Pair(node as NodeOf)) } } @@ -97,7 +97,7 @@ export class YAMLSet< } const hasAllNullValues = (map: YAMLMap): boolean => - map.items.every( + map.every( ({ value }) => value == null || (value instanceof Scalar && @@ -120,7 +120,7 @@ export const set: CollectionTag = { 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.push(nc.createPair(value, null) as Pair) } return set }, @@ -134,7 +134,7 @@ export const set: CollectionTag = { return map } else { const set = Object.assign(new YAMLSet(), map) - for (const pair of map.items) pair.value &&= null + for (const pair of map) pair.value &&= null return set } } diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index 133d5572..c17b7254 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -23,7 +23,7 @@ export function stringifyCollection( } function stringifyBlockCollection( - { comment, items }: Readonly, + coll: Readonly, ctx: StringifyContext, { blockItemPrefix, @@ -41,8 +41,8 @@ function stringifyBlockCollection( let chompKeep = false // flag for the preceding node's status const lines: string[] = [] - for (let i = 0; i < items.length; ++i) { - const item = items[i] + for (let i = 0; i < coll.length; ++i) { + const item = coll[i] let comment: string | null = null if (item instanceof Pair) { if (!chompKeep && item.key.spaceBefore) lines.push('') @@ -76,8 +76,8 @@ function stringifyBlockCollection( } } - if (comment) { - str += '\n' + indentComment(commentString(comment), indent) + if (coll.comment) { + str += '\n' + indentComment(commentString(coll.comment), indent) if (onComment) onComment() } else if (chompKeep && onChompKeep) onChompKeep() @@ -85,7 +85,7 @@ function stringifyBlockCollection( } function stringifyFlowCollection( - { items }: Readonly, + coll: Readonly, ctx: StringifyContext, { flowChars, itemIndent }: StringifyCollectionOptions ) { @@ -105,8 +105,8 @@ function stringifyFlowCollection( let reqNewline = false let linesAtValue = 0 const lines: string[] = [] - for (let i = 0; i < items.length; ++i) { - const item = items[i] + for (let i = 0; i < coll.length; ++i) { + const item = coll[i] let comment: string | null = null if (item instanceof Pair) { const ik = item.key @@ -129,7 +129,7 @@ function stringifyFlowCollection( if (comment) reqNewline = true let str = stringify(item, itemCtx, () => (comment = null)) - if (i < items.length - 1) str += ',' + if (i < coll.length - 1) str += ',' if (comment) str += lineComment(str, itemIndent, commentString(comment)) if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) reqNewline = true diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts index 0e98f8e3..ce2a67b9 100644 --- a/src/stringify/stringifyPair.ts +++ b/src/stringify/stringifyPair.ts @@ -138,7 +138,7 @@ export function stringifyPair( const vs0 = valueStr[0] const nl0 = valueStr.indexOf('\n') const hasNewline = nl0 !== -1 - const flow = ctx.inFlow ?? value.flow ?? value.items.length === 0 + const flow = ctx.inFlow ?? value.flow ?? value.length === 0 if (hasNewline || !flow) { let hasPropsLine = false if (hasNewline && (vs0 === '&' || vs0 === '!')) { diff --git a/src/test-events.ts b/src/test-events.ts index d5132476..f00ea762 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -90,7 +90,7 @@ function addEvents( if (node instanceof YAMLMap) { const ev = node.flow ? '+MAP {}' : '+MAP' events.push(`${ev}${props}`) - node.items.forEach(({ key, value }) => { + node.forEach(({ key, value }) => { addEvents(events, doc, errPos, key) addEvents(events, doc, errPos, value) }) @@ -98,7 +98,7 @@ function addEvents( } else if (node instanceof YAMLSeq) { const ev = node.flow ? '+SEQ []' : '+SEQ' events.push(`${ev}${props}`) - node.items.forEach(item => { + node.forEach(item => { addEvents(events, doc, errPos, item) }) events.push('-SEQ') diff --git a/src/visit.ts b/src/visit.ts index 67e1d70f..7ac4a200 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -124,12 +124,12 @@ function visit_( if (typeof ctrl !== 'symbol') { if (node instanceof YAMLMap || node instanceof YAMLSeq) { path = [...path, node] - for (let i = 0; i < node.items.length; ++i) { - const ci = visit_(i, node.items[i], visitor, path) + for (let i = 0; i < node.length; ++i) { + const ci = visit_(i, node[i], visitor, path) if (typeof ci === 'number') i = ci - 1 else if (ci === BREAK) return BREAK else if (ci === REMOVE) { - node.items.splice(i, 1) + node.splice(i, 1) i -= 1 } } @@ -218,12 +218,12 @@ async function visitAsync_( if (typeof ctrl !== 'symbol') { if (node instanceof YAMLMap || node instanceof YAMLSeq) { path = [...path, node] - for (let i = 0; i < node.items.length; ++i) { - const ci = await visitAsync_(i, node.items[i], visitor, path) + for (let i = 0; i < node.length; ++i) { + const ci = await visitAsync_(i, node[i], visitor, path) if (typeof ci === 'number') i = ci - 1 else if (ci === BREAK) return BREAK else if (ci === REMOVE) { - node.items.splice(i, 1) + node.splice(i, 1) i -= 1 } } @@ -303,7 +303,7 @@ function replaceNode( ): number | symbol | void { const parent = path[path.length - 1] if (parent instanceof YAMLMap || parent instanceof YAMLSeq) { - parent.items[key as number] = node + parent[key as number] = node } else if (parent instanceof Pair) { if (isNode(node)) { if (key === 'key') parent.key = node diff --git a/tests/_setup.ts b/tests/_setup.ts new file mode 100644 index 00000000..486e9225 --- /dev/null +++ b/tests/_setup.ts @@ -0,0 +1,12 @@ +expect.addEqualityTesters([ + function arrayishEquals(a, b, customTesters) { + if ( + Array.isArray(a) && + Array.isArray(b) && + a.constructor !== b.constructor + ) { + return this.equals(Array.from(a), Array.from(b), customTesters) + } + return undefined + } +]) diff --git a/tests/cli.ts b/tests/cli.ts index f6844f45..7c9b29ae 100644 --- a/tests/cli.ts +++ b/tests/cli.ts @@ -245,18 +245,18 @@ const skip = Number(major) < 20 ) }) describe('--doc', () => { - ok('basic', 'hello: world', ['--doc'], [{ value: { items: [{}] } }]) + ok('basic', 'hello: world', ['--doc'], [{ value: [{}] }]) ok( 'multiple', 'hello: world\n---\n42', ['--doc'], - [{ value: { items: [{}] } }, { value: { value: 42 } }] + [{ value: [{}] }, { value: { value: 42 } }] ) ok( 'error', 'hello: world: 2', ['--doc'], - [{ value: { items: [{}] } }], + [{ value: [{}] }], [{ name: 'YAMLParseError' }] ) }) diff --git a/tests/collection-access.ts b/tests/collection-access.ts index 096c7bb2..374dd3c9 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -15,16 +15,14 @@ describe('Map', () => { beforeEach(() => { doc = new Document({ a: 1, b: { c: 3, d: 4 } }) map = doc.value as any - expect(map.items).toMatchObject([ + expect(map).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, - value: { - items: [ - { key: { value: 'c' }, value: { value: 3 } }, - { key: { value: 'd' }, value: { value: 4 } } - ] - } + value: [ + { key: { value: 'c' }, value: { value: 3 } }, + { key: { value: 'd' }, value: { value: 4 } } + ] } ]) }) @@ -34,15 +32,15 @@ describe('Map', () => { expect(map.get('c')).toMatchObject({ value: 'x' }) map.add(doc.createPair('c', 'y')) expect(map.get('c')).toMatchObject({ value: 'y' }) - expect(map.items).toHaveLength(3) + expect(map).toHaveLength(3) }) test('delete', () => { expect(map.delete('a')).toBe(true) expect(map.get('a')).toBeUndefined() expect(map.delete('c')).toBe(false) - expect(map.get('b')).toMatchObject({ items: [{}, {}] }) - expect(map.items).toHaveLength(1) + expect(map.get('b')).toMatchObject([{}, {}]) + expect(map).toHaveLength(1) }) test('get with value', () => { @@ -88,7 +86,7 @@ describe('Map', () => { expect(map.get('b')).toMatchObject({ value: 5 }) map.set('c', 6) expect(map.get('c')).toMatchObject({ value: 6 }) - expect(map.items).toHaveLength(3) + expect(map).toHaveLength(3) }) test('set with node', () => { @@ -98,7 +96,7 @@ describe('Map', () => { expect(map.get('b')).toMatchObject({ value: 5 }) map.set(doc.createNode('c'), 6) expect(map.get('c')).toMatchObject({ value: 6 }) - expect(map.items).toHaveLength(3) + expect(map).toHaveLength(3) }) test('set scalar node with anchor', () => { @@ -115,25 +113,22 @@ describe('Seq', () => { beforeEach(() => { doc = new Document([1, [2, 3]]) seq = doc.value as any - expect(seq.items).toMatchObject([ - { value: 1 }, - { items: [{ value: 2 }, { value: 3 }] } - ]) + expect(seq).toMatchObject([{ value: 1 }, [{ value: 2 }, { value: 3 }]]) }) test('add', () => { seq.add(9) expect(seq.get(2)).toMatchObject({ value: 9 }) seq.add(1) - expect(seq.items).toHaveLength(4) + expect(seq).toHaveLength(4) }) test('delete', () => { expect(seq.delete(0)).toBe(true) expect(seq.delete(2)).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) + expect(seq.get(0)).toMatchObject([{ value: 2 }, { value: 3 }]) + expect(seq).toHaveLength(1) }) test('get with integer', () => { @@ -173,7 +168,7 @@ describe('Seq', () => { expect(seq.get(1)).toMatchObject({ value: 5 }) seq.set(2, 6) expect(seq.get(2)).toMatchObject({ value: 6 }) - expect(seq.items).toHaveLength(3) + expect(seq).toHaveLength(3) }) test('set with non-integer', () => { @@ -190,7 +185,7 @@ describe('Set', () => { doc = new Document(null, { version: '1.1' }) set = doc.createNode([1, 2, 3], { tag: '!!set' }) as any doc.value = set - expect(set.items).toMatchObject([ + expect(set).toMatchObject([ { key: { value: 1 }, value: null }, { key: { value: 2 }, value: null }, { key: { value: 3 }, value: null } @@ -205,7 +200,7 @@ describe('Set', () => { set.add(new Pair(y0)) set.add(new Pair(new Scalar('y'))) expect(set.get('y')).toBe(y0) - expect(set.items).toHaveLength(5) + expect(set).toHaveLength(5) }) test('get', () => { @@ -223,7 +218,7 @@ describe('Set', () => { expect(set.get(4)).toBeUndefined() set.set(4, true) expect(set.get(4)).toMatchObject(new Scalar(4)) - expect(set.items).toHaveLength(3) + expect(set).toHaveLength(3) }) }) @@ -236,16 +231,14 @@ describe('OMap', () => { tag: '!!omap' }) as any doc.value = omap - expect(omap.items).toMatchObject([ + expect(omap).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, - value: { - items: [ - { key: { value: 'c' }, value: { value: 3 } }, - { key: { value: 'd' }, value: { value: 4 } } - ] - } + value: [ + { key: { value: 'c' }, value: { value: 3 } }, + { key: { value: 'd' }, value: { value: 4 } } + ] } ]) }) @@ -254,15 +247,15 @@ describe('OMap', () => { omap.add(doc.createPair('c', 'x')) expect(omap.get('c')).toMatchObject({ value: 'x' }) omap.add(doc.createPair('c', 'y')) - expect(omap.items).toHaveLength(3) + expect(omap).toHaveLength(3) }) test('delete', () => { expect(omap.delete('a')).toBe(true) expect(omap.get('a')).toBeUndefined() expect(omap.delete('c')).toBe(false) - expect(omap.get('b')).toMatchObject({ items: [{}, {}] }) - expect(omap.items).toHaveLength(1) + expect(omap.get('b')).toMatchObject([{}, {}]) + expect(omap).toHaveLength(1) }) test('get', () => { @@ -289,7 +282,7 @@ describe('OMap', () => { expect(omap.get('b')).toMatchObject({ value: 5 }) omap.set('c', 6) expect(omap.get('c')).toMatchObject({ value: 6 }) - expect(omap.items).toHaveLength(3) + expect(omap).toHaveLength(3) }) }) @@ -297,11 +290,11 @@ describe('Document', () => { let doc: Document | YAMLSeq>>> beforeEach(() => { doc = new Document({ a: 1, b: [2, 3] }) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, - value: { items: [{ value: 2 }, { value: 3 }] } + value: [{ value: 2 }, { value: 3 }] } ]) }) @@ -309,14 +302,14 @@ describe('Document', () => { test('add', () => { doc.add(doc.createPair('c', 'x')) expect(doc.get('c')).toMatchObject({ value: 'x' }) - expect(doc.value.items).toHaveLength(3) + expect(doc.value).toHaveLength(3) }) test('delete', () => { expect(doc.delete('a')).toBe(true) expect(doc.delete('a')).toBe(false) expect(doc.get('a')).toBeUndefined() - expect(doc.value.items).toHaveLength(1) + expect(doc.value).toHaveLength(1) }) test('delete on scalar value', () => { @@ -349,7 +342,7 @@ describe('Document', () => { expect(doc.get('a')).toMatchObject({ value: 2 }) doc.set('c', 6) expect(doc.get('c')).toMatchObject({ value: 6 }) - expect(doc.value.items).toHaveLength(3) + expect(doc.value).toHaveLength(3) }) test('set on scalar value', () => { diff --git a/tests/directives.ts b/tests/directives.ts index ed197f03..c27afd1c 100644 --- a/tests/directives.ts +++ b/tests/directives.ts @@ -17,12 +17,10 @@ describe('%TAG', () => { '!': '!foo:', '!bar!': '!bar:' }) - expect(doc.value).toMatchObject({ - items: [ - { value: 'v1', tag: '!foo:bar' }, - { value: 'v2', tag: '!bar:foo' } - ] - }) + expect(doc.value).toMatchObject([ + { value: 'v1', tag: '!foo:bar' }, + { value: 'v2', tag: '!bar:foo' } + ]) }) test('parse global tags', () => { @@ -39,12 +37,10 @@ describe('%TAG', () => { '!': 'foo:', '!bar!': 'bar:bar#bar?' }) - expect(doc.value).toMatchObject({ - items: [ - { value: 'v1', tag: 'foo:bar' }, - { value: 'v2', tag: 'bar:bar#bar?foo' } - ] - }) + expect(doc.value).toMatchObject([ + { value: 'v1', tag: 'foo:bar' }, + { value: 'v2', tag: 'bar:bar#bar?foo' } + ]) }) test('create & stringify', () => { diff --git a/tests/doc/YAML-1.2.spec.ts b/tests/doc/YAML-1.2.spec.ts index fcd0d6d1..cd2216da 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.value.items[1].value.value + const data = doc.value[1].value.value expect(data).toBeInstanceOf(Uint8Array) expect(data.byteLength).toBe(65) } @@ -1718,11 +1718,9 @@ mapping: !!map special(src) { const doc = YAML.parseDocument, false>(src) 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.value.items[1].value.tag).toBe('tag:yaml.org,2002:map') + expect(doc.value[0].value.tag).toBe('tag:yaml.org,2002:seq') + expect(doc.value[0].value[1].tag).toBe('tag:yaml.org,2002:seq') + expect(doc.value[1].value.tag).toBe('tag:yaml.org,2002:map') } } }, diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index 16cfd968..d5d11da0 100644 --- a/tests/doc/anchors.ts +++ b/tests/doc/anchors.ts @@ -5,10 +5,7 @@ test('basic', () => { const src = `- &a 1\n- *a\n` const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.value.items).toMatchObject([ - { anchor: 'a', value: 1 }, - { source: 'a' } - ]) + expect(doc.value).toMatchObject([{ anchor: 'a', value: 1 }, { source: 'a' }]) expect(String(doc)).toBe(src) }) @@ -16,7 +13,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.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { anchor: 'a', value: 1 }, { anchor: 'a', value: 2 }, { source: 'a' } @@ -30,10 +27,8 @@ test('circular reference', () => { const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) expect(doc.warnings).toHaveLength(0) - expect(doc.value).toMatchObject({ - anchor: 'a', - items: [{ value: 1 }, { source: 'a' }] - }) + expect(doc.value).toMatchObject([{ value: 1 }, { source: 'a' }]) + expect(doc.value.anchor).toBe('a') const res = doc.toJS() expect(res[1]).toBe(res) expect(String(doc)).toBe(src) @@ -42,16 +37,18 @@ test('circular reference', () => { describe('anchor on tagged collection', () => { test('!!set', () => { const res = parse('- &a !!set { 1, 2 }\n- *a\n') - expect(Array.from(res[0])).toMatchObject([1, 2]) + expect(res[0]).toMatchObject(new Set([1, 2])) expect(res[1]).toBe(res[0]) }) test('!!omap', () => { const res = parse('- &a !!omap [ 1: 1, 2: 2 ]\n- *a\n') - expect(Array.from(res[0])).toMatchObject([ - [1, 1], - [2, 2] - ]) + expect(res[0]).toMatchObject( + new Map([ + [1, 1], + [2, 2] + ]) + ) expect(res[1]).toBe(res[0]) }) }) @@ -69,7 +66,7 @@ describe('create', () => { test('doc.createAlias', () => { const doc = parseDocument('[{ a: A }, { b: B }]') const alias = doc.createAlias(doc.get(0), 'AA') - doc.value.items.push(alias) + doc.value.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') @@ -81,12 +78,11 @@ describe('create', () => { const doc = new Document() const ref: unknown[] = [] const node = doc.createNode({ src: ref, ref }) - expect(node).toMatchObject({ - items: [ - { key: { value: 'src' }, value: { items: [], anchor: 'a1' } }, - { key: { value: 'ref' }, value: { source: 'a1' } } - ] - }) + expect(node).toMatchObject([ + { key: { value: 'src' }, value: [] }, + { key: { value: 'ref' }, value: { source: 'a1' } } + ]) + expect(node[0].value?.anchor).toBe('a1') }) test('repeated Date in createNode', () => { @@ -94,12 +90,10 @@ describe('create', () => { const date = new Date() const value = date.toJSON() const node = doc.createNode({ src: date, ref: date }) - expect(node).toMatchObject({ - items: [ - { key: { value: 'src' }, value: { value, anchor: 'a1' } }, - { key: { value: 'ref' }, value: { source: 'a1' } } - ] - }) + expect(node).toMatchObject([ + { key: { value: 'src' }, value: { value, anchor: 'a1' } }, + { key: { value: 'ref' }, value: { source: 'a1' } } + ]) }) }) @@ -114,7 +108,7 @@ describe('errors', () => { test('set tag on alias', () => { const doc = parseDocument, false>('[{ a: A }, { b: B }]') - const node = doc.value.items[0] + const node = doc.value[0] const alias = doc.createAlias(node, 'AA') expect(() => { // @ts-expect-error This is intentionally wrong. @@ -127,7 +121,7 @@ describe('errors', () => { '[{ a: A }, { b: B }]' ) const alias = doc.createAlias(doc.get(0), 'AA') - doc.value.items.unshift(alias) + doc.value.unshift(alias) expect(() => String(doc)).toThrow( 'Unresolved alias (the anchor must be set before the alias): AA' ) @@ -168,7 +162,7 @@ describe('__proto__ as anchor name', () => { const src = `- &__proto__ 1\n- *__proto__\n` const doc = parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { anchor: '__proto__', value: 1 }, { source: '__proto__' } ]) @@ -180,8 +174,8 @@ describe('__proto__ as anchor name', () => { const doc = parseDocument, false>( '[{ a: A }, { b: B }]' ) - const alias = doc.createAlias(doc.value.items[0], '__proto__') - doc.value.items.push(alias) + const alias = doc.createAlias(doc.value[0], '__proto__') + doc.value.push(alias) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }]) expect(String(doc)).toMatch( '[ &__proto__ { a: A }, { b: B }, *__proto__ ]\n' @@ -218,7 +212,7 @@ describe('merge <<', () => { x: 1 label: center/big` - test('YAML.parse with merge:true', () => { + test.only('YAML.parse with merge:true', () => { const res = parse(src, { merge: true }) expect(res).toHaveLength(8) for (let i = 4; i < res.length; ++i) { @@ -250,13 +244,11 @@ describe('merge <<', () => { test('YAML.parseDocument', () => { const doc = parseDocument, false>(src, { merge: true }) - 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' }] } - ] - ) + expect(doc.value.slice(5).map(it => it[0].value)).toMatchObject([ + { source: 'CENTER' }, + [{ source: 'CENTER' }, { source: 'BIG' }], + [{ source: 'BIG' }, { source: 'LEFT' }, { source: 'SMALL' }] + ]) }) test('alias is associated with a sequence', () => { @@ -293,9 +285,9 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { merge: true } ) - const [a, b] = doc.value.items + const [a, b] = doc.value const merge = doc.createPair('<<', doc.createAlias(a)) - b.items.push(merge) + b.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) expect(String(doc)).toBe('[ &a1 { a: A }, { b: B, <<: *a1 } ]\n') }) @@ -305,9 +297,9 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { customTags: ['merge'] } ) - const [a, b] = doc.value.items + const [a, b] = doc.value const merge = doc.createPair('<<', doc.createAlias(a)) - b.items.push(merge) + b.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) expect(String(doc)).toBe('[ &a1 { a: A }, { b: B, <<: *a1 } ]\n') }) @@ -317,9 +309,9 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { merge: true } ) - const [a, b] = doc.value.items + const [a, b] = doc.value const merge = doc.createPair(Symbol('<<'), doc.createAlias(a)) - b.items.push(merge) + b.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) expect(String(doc)).toBe('[ &a1 { a: A }, { b: B, <<: *a1 } ]\n') }) @@ -329,10 +321,10 @@ describe('merge <<', () => { '[{ a: A }, { b: B }]', { merge: true } ) - const [a, b] = doc.value.items + const [a, b] = doc.value const alias = doc.createAlias(a, 'AA') const merge = doc.createPair('<<', alias) - b.items.push(merge) + b.push(merge) expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }]) expect(String(doc)).toBe('[ &AA { a: A }, { b: B, <<: *AA } ]\n') }) diff --git a/tests/doc/comments.ts b/tests/doc/comments.ts index 224d40fb..aa1099d6 100644 --- a/tests/doc/comments.ts +++ b/tests/doc/comments.ts @@ -94,15 +94,13 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - value: { - items: [ - { commentBefore: 'c0', value: 'value 1', comment: 'c1' }, - { value: 'value 2' } - ], - range: [4, 31, 31] - }, + value: [ + { commentBefore: 'c0', value: 'value 1', comment: 'c1' }, + { value: 'value 2' } + ], comment: 'c2' }) + expect(doc.value.range).toMatchObject([4, 31, 31]) }) test('multiline', () => { @@ -119,9 +117,7 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - value: { - items: [{ comment: 'c0' }, { commentBefore: 'c1\n\nc2' }] - }, + value: [{ comment: 'c0' }, { commentBefore: 'c1\n\nc2' }], comment: 'c3\nc4' }) }) @@ -140,12 +136,10 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - value: { - items: [ - { key: { commentBefore: 'c0' }, value: { comment: 'c1' } }, - { key: {}, value: {} } - ] - }, + value: [ + { key: { commentBefore: 'c0' }, value: { comment: 'c1' } }, + { key: {}, value: {} } + ], comment: 'c2' }) }) @@ -164,12 +158,10 @@ describe('parse comments', () => { ` const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - value: { - items: [ - { value: { comment: 'c0' } }, - { key: { commentBefore: 'c1\n\nc2' } } - ] - }, + value: [ + { value: { comment: 'c0' } }, + { key: { commentBefore: 'c1\n\nc2' } } + ], comment: 'c3\nc4' }) }) @@ -187,22 +179,18 @@ describe('parse comments', () => { k3: v3 #c5 ` - const doc = YAML.parseDocument(src) + const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - value: { - items: [ - { - commentBefore: 'c0\nc1', - items: [ - {}, - { key: { commentBefore: 'c2' }, value: { comment: 'c3' } }, - { key: { commentBefore: 'c4' } } - ] - } + value: [ + [ + {}, + { key: { commentBefore: 'c2' }, value: { comment: 'c3' } }, + { key: { commentBefore: 'c4' } } ] - }, + ], comment: 'c5' }) + expect(doc.value[0].commentBefore).toBe('c0\nc1') expect(String(doc)).toBe(source` #c0 #c1 @@ -230,26 +218,24 @@ describe('parse comments', () => { - v3 #c4 #c5 ` - const doc = YAML.parseDocument(src) + const doc = YAML.parseDocument(src) expect(doc).toMatchObject({ - value: { - items: [ - { - key: { commentBefore: 'c0', value: 'k1' }, - value: { - commentBefore: 'c1', - items: [{ value: 'v1' }, { commentBefore: 'c2', value: 'v2' }], - comment: 'c3' - } - }, - { - key: { value: 'k2' }, - value: { items: [{ value: 'v3', comment: 'c4' }] } - } - ] - }, + value: [ + { + key: { commentBefore: 'c0', value: 'k1' }, + value: [{ value: 'v1' }, { commentBefore: 'c2', value: 'v2' }] + }, + { + key: { value: 'k2' }, + value: [{ value: 'v3', comment: 'c4' }] + } + ], comment: 'c5' }) + expect(doc.value[0].value).toMatchObject({ + commentBefore: 'c1', + comment: 'c3' + }) expect(String(doc)).toBe(source` #c0 k1: @@ -272,7 +258,7 @@ describe('parse comments', () => { [ a, #c0 b #c1 ]`) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { value: 'a', comment: 'c0' }, { value: 'b', comment: 'c1' } ]) @@ -284,7 +270,7 @@ describe('parse comments', () => { b: c, #c1 d #c2 }`) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { key: { value: 'a', comment: 'c0' } }, { key: { value: 'b' }, value: { value: 'c', comment: 'c1' } }, { key: { value: 'd', comment: 'c2' } } @@ -293,7 +279,7 @@ describe('parse comments', () => { test('multi-line comments', () => { const doc = YAML.parseDocument('{ a,\n#c0\n#c1\nb }') - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { key: { value: 'a' } }, { key: { commentBefore: 'c0\nc1', value: 'b' } } ]) @@ -386,8 +372,8 @@ describe('stringify comments', () => { const src = '- value 1\n- value 2\n' const doc = YAML.parseDocument, false>(src) doc.value.commentBefore = 'c0' - doc.value.items[0].commentBefore = 'c1' - doc.value.items[1].commentBefore = 'c2' + doc.value[0].commentBefore = 'c1' + doc.value[1].commentBefore = 'c2' doc.value.comment = 'c3' expect(String(doc)).toBe(source` #c0 @@ -402,8 +388,8 @@ describe('stringify comments', () => { test('multiline', () => { const src = '- value 1\n- value 2\n' const doc = YAML.parseDocument, false>(src) - doc.value.items[0].commentBefore = 'c0\nc1' - doc.value.items[1].commentBefore = ' \nc2\n\nc3' + doc.value[0].commentBefore = 'c0\nc1' + doc.value[1].commentBefore = ' \nc2\n\nc3' doc.value.comment = 'c4\nc5' expect(String(doc)).toBe(source` #c0 @@ -425,12 +411,12 @@ describe('stringify comments', () => { YAML.YAMLMap>, false >(src) - doc.value.items[0].key.commentBefore = 'c0' - doc.value.items[0].key.comment = 'c1' - const seq = doc.value.items[0].value! + doc.value[0].key.commentBefore = 'c0' + doc.value[0].key.comment = 'c1' + const seq = doc.value[0].value! seq.commentBefore = 'c2' - seq.items[0].commentBefore = 'c3' - seq.items[1].commentBefore = 'c4' + seq[0].commentBefore = 'c3' + seq[1].commentBefore = 'c4' seq.comment = 'c5' expect(String(doc)).toBe(source` #c0 @@ -449,8 +435,8 @@ describe('stringify comments', () => { '- a\n- b\n' ) doc.value.commentBefore = 'c0' - doc.value.items[0].commentBefore = 'c1' - doc.value.items[1].commentBefore = 'c2\nc3' + doc.value[0].commentBefore = 'c1' + doc.value[1].commentBefore = 'c2\nc3' expect(doc.toString({ commentString: str => str.replace(/^/gm, '// ') })) .toBe(source` // c0 @@ -470,10 +456,10 @@ describe('stringify comments', () => { YAML.YAMLMap, false >(src) - 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[0].key.commentBefore = 'c0' + doc.value[1].key.commentBefore = 'c1' + doc.value[1].key.comment = 'c2' + doc.value[1].value!.spaceBefore = true doc.value.comment = 'c3' expect(String(doc)).toBe(source` #c0 @@ -492,11 +478,11 @@ describe('stringify comments', () => { YAML.YAMLMap, false >(src) - 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[0].key.commentBefore = 'c0\nc1' + doc.value[1].key.commentBefore = ' \nc2\n\nc3' + doc.value[1].key.comment = 'c4\nc5' + doc.value[1].value!.spaceBefore = true + doc.value[1].value!.commentBefore = 'c6' doc.value.comment = 'c7\nc8' expect(String(doc)).toBe(source` #c0 @@ -745,7 +731,7 @@ describe('blank lines', () => { test(name, () => { const doc = YAML.parseDocument(src) expect(String(doc)).toBe(src) - let it = doc.value.items[1] + let it = doc.value[1] if (it.key) it = it.key expect(it).not.toHaveProperty('spaceBefore', true) it.spaceBefore = true @@ -771,18 +757,16 @@ 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.value).toMatchObject({ - items: [ - { - key: { value: 'a' }, - value: { value: 1, spaceBefore: true } - }, - { - key: { value: 'b' }, - value: { value: 2, commentBefore: 'c', spaceBefore: true } - } - ] - }) + expect(doc.value).toMatchObject([ + { + key: { value: 'a' }, + value: { value: 1, spaceBefore: true } + }, + { + key: { value: 'b' }, + value: { value: 2, commentBefore: 'c', spaceBefore: true } + } + ]) expect(String(doc)).toBe(src) }) @@ -806,43 +790,37 @@ describe('blank lines', () => { test('flow seq', () => { const src = '[1,\n\n2,\n3,\n\n4\n\n]' const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { value: 1 }, - { value: 2, spaceBefore: true }, - { value: 3 }, - { value: 4, spaceBefore: true } - ] - }) + expect(doc.value).toMatchObject([ + { value: 1 }, + { value: 2, spaceBefore: true }, + { value: 3 }, + { value: 4, spaceBefore: true } + ]) expect(String(doc)).toBe('[\n 1,\n\n 2,\n 3,\n\n 4\n]\n') }) test('flow map', () => { const src = '{\n\na: 1,\n\nb: 2 }' const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { key: { value: 'a', spaceBefore: true }, value: { value: 1 } }, - { key: { value: 'b', spaceBefore: true }, value: { value: 2 } } - ] - }) + expect(doc.value).toMatchObject([ + { key: { value: 'a', spaceBefore: true }, value: { value: 1 } }, + { key: { value: 'b', spaceBefore: true }, value: { value: 2 } } + ]) }) 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.value).toMatchObject({ - items: [ - { - key: { value: 'a' }, - value: { value: 1, commentBefore: 'c' } - }, - { - key: { value: 'b' }, - value: { value: 2, commentBefore: 'd', spaceBefore: true } - } - ] - }) + expect(doc.value).toMatchObject([ + { + key: { value: 'a' }, + value: { value: 1, commentBefore: 'c' } + }, + { + key: { value: 'b' }, + value: { value: 2, commentBefore: 'd', spaceBefore: true } + } + ]) expect(String(doc)).toBe(src) }) }) @@ -931,11 +909,9 @@ map: # c4 ` const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { value: { commentBefore: ' c1\n \n c2\n', comment: ' c3\n \n c4' } } - ] - }) + expect(doc.value).toMatchObject([ + { value: { commentBefore: ' c1\n \n c2\n', comment: ' c3\n \n c4' } } + ]) expect(doc.toString()).toBe(source` key: # c1 @@ -963,11 +939,9 @@ map: # c4 ` const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { value: { commentBefore: ' c1\n\n c2\n', comment: ' c3\n\n c4' } } - ] - }) + expect(doc.value).toMatchObject([ + { value: { commentBefore: ' c1\n\n c2\n', comment: ' c3\n\n c4' } } + ]) expect(doc.toString()).toBe(source` key: # c1 @@ -993,12 +967,10 @@ map: - v2 ` const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { commentBefore: ' c1\n \n c2', comment: ' c3\n ' }, - { commentBefore: ' c4' } - ] - }) + expect(doc.value).toMatchObject([ + { commentBefore: ' c1\n \n c2', comment: ' c3\n ' }, + { commentBefore: ' c4' } + ]) expect(doc.toString()).toBe(source` # c1 # @@ -1023,12 +995,10 @@ map: - v2 ` const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { commentBefore: ' c1\n\n c2', comment: ' c3' }, - { commentBefore: ' c4', spaceBefore: true } - ] - }) + expect(doc.value).toMatchObject([ + { commentBefore: ' c1\n\n c2', comment: ' c3' }, + { commentBefore: ' c4', spaceBefore: true } + ]) expect(doc.toString()).toBe(source` # c1 @@ -1046,7 +1016,7 @@ map: const doc = YAML.parseDocument, false>( '- v1\n- v2\n' ) - const [v1, v2] = doc.value.items + const [v1, v2] = doc.value v1.commentBefore = '\n' v1.comment = '\n' v2.commentBefore = '\n' @@ -1066,7 +1036,7 @@ map: YAML.YAMLMap, false >('k1: v1\nk2: v2') - const [p1, p2] = doc.value.items + const [p1, p2] = doc.value p1.key.commentBefore = '\n' p1.value!.commentBefore = '\n' p1.value!.comment = '\n' @@ -1188,13 +1158,12 @@ describe('collection end comments', () => { #2 - d ` - const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { items: [{ value: 'a' }, { value: 'b' }], comment: '1\n\n2' }, - { value: 'd' } - ] - }) + const doc = YAML.parseDocument(src) + expect(doc.value).toMatchObject([ + [{ value: 'a' }, { value: 'b' }], + { value: 'd' } + ]) + expect(doc.value[0].comment).toBe('1\n\n2') expect(String(doc)).toBe(source` #0 - - a @@ -1216,19 +1185,15 @@ describe('collection end comments', () => { #2 - d ` - const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { - items: [ - { key: { value: 'a' }, value: { value: 1 } }, - { key: { value: 'b' }, value: { value: 2 } } - ], - comment: '1\n\n2' - }, - { value: 'd' } - ] - }) + const doc = YAML.parseDocument(src) + expect(doc.value).toMatchObject([ + [ + { key: { value: 'a' }, value: { value: 1 } }, + { key: { value: 'b' }, value: { value: 2 } } + ], + { value: 'd' } + ]) + expect(doc.value[0].comment).toBe('1\n\n2') expect(String(doc)).toBe(source` #0 - a: 1 @@ -1251,16 +1216,15 @@ describe('collection end comments', () => { #2 d: 1 ` - const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { - key: { value: 'a' }, - value: { items: [{ value: 'b' }, { value: 'c' }], comment: '1\n\n2' } - }, - { key: { value: 'd' }, value: { value: 1 } } - ] - }) + const doc = YAML.parseDocument(src) + expect(doc.value).toMatchObject([ + { + key: { value: 'a' }, + value: [{ value: 'b' }, { value: 'c' }] + }, + { key: { value: 'd' }, value: { value: 1 } } + ]) + expect(doc.value[0]).toMatchObject({ value: { comment: '1\n\n2' } }) expect(String(doc)).toBe(source` #0 a: @@ -1284,22 +1248,18 @@ describe('collection end comments', () => { #2 d: 1 ` - const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { - key: { value: 'a' }, - value: { - items: [ - { key: { value: 'b' }, value: { value: 1 } }, - { key: { value: 'c' }, value: { value: 2 } } - ], - comment: '1\n\n2' - } - }, - { key: { value: 'd' }, value: { value: 1 } } - ] - }) + const doc = YAML.parseDocument(src) + expect(doc.value).toMatchObject([ + { + key: { value: 'a' }, + value: [ + { key: { value: 'b' }, value: { value: 1 } }, + { key: { value: 'c' }, value: { value: 2 } } + ] + }, + { key: { value: 'd' }, value: { value: 1 } } + ]) + expect(doc.value[0]).toMatchObject({ value: { comment: '1\n\n2' } }) expect(String(doc)).toBe(source` #0 a: @@ -1321,28 +1281,17 @@ a: #2 - e\n` - const doc = YAML.parseDocument(src) - expect(doc.value).toMatchObject({ - items: [ - { - key: { value: 'a' }, - value: { - commentBefore: '1', - items: [ - { - items: [ - { - key: { value: 'b' }, - value: { items: [{ value: 'c' }] } - } - ] - }, - { spaceBefore: true, commentBefore: '2', value: 'e' } - ] - } - } - ] - }) + const doc = YAML.parseDocument(src) + expect(doc.value).toMatchObject([ + { + key: { value: 'a' }, + value: [ + [{ key: { value: 'b' }, value: [{ value: 'c' }] }], + { spaceBefore: true, commentBefore: '2', value: 'e' } + ] + } + ]) + expect(doc.value[0]).toMatchObject({ value: { commentBefore: '1' } }) expect(String(doc)).toBe(src) }) }) diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 9af3ceaf..71ded54e 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -48,14 +48,14 @@ describe('arrays', () => { test('createNode([])', () => { const s = new Document().createNode([]) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toHaveLength(0) + expect(s).toHaveLength(0) }) test('createNode([true])', () => { const doc = new Document() const s = doc.createNode([true]) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([{ value: true }]) + expect(s).toMatchObject([{ value: true }]) doc.value = s expect(String(doc)).toBe('- true\n') }) @@ -64,7 +64,7 @@ describe('arrays', () => { const doc = new Document() const s = doc.createNode([true], { flow: true }) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([{ value: true }]) + expect(s).toMatchObject([{ value: true }]) doc.value = s expect(String(doc)).toBe('[ true ]\n') }) @@ -74,12 +74,12 @@ describe('arrays', () => { test('createNode(value)', () => { const s = new Document().createNode(array) as any expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toHaveLength(2) - expect(s.items[0].value).toBe(3) - expect(s.items[1]).toBeInstanceOf(YAMLSeq) - expect(s.items[1].items).toHaveLength(2) - expect(s.items[1].items[0].value).toBe('four') - expect(s.items[1].items[1].value).toBe(5) + expect(s).toHaveLength(2) + expect(s[0].value).toBe(3) + expect(s[1]).toBeInstanceOf(YAMLSeq) + expect(s[1]).toHaveLength(2) + expect(s[1][0].value).toBe('four') + expect(s[1][1].value).toBe(5) }) test('set doc value', () => { const res = '- 3\n- - four\n - 5\n' @@ -95,16 +95,14 @@ describe('objects', () => { test('createNode({})', () => { const s = new Document().createNode({}) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(0) + expect(s).toHaveLength(0) }) test('createNode({ x: true })', () => { const doc = new Document() const s = doc.createNode({ x: true }) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toMatchObject([ - { key: { value: 'x' }, value: { value: true } } - ]) + expect(s).toMatchObject([{ key: { value: 'x' }, value: { value: true } }]) doc.value = s expect(String(doc)).toBe('x: true\n') }) @@ -113,9 +111,7 @@ describe('objects', () => { const doc = new Document() const s = doc.createNode({ x: true }, { flow: true }) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toMatchObject([ - { key: { value: 'x' }, value: { value: true } } - ]) + expect(s).toMatchObject([{ key: { value: 'x' }, value: { value: true } }]) doc.value = s expect(String(doc)).toBe('{ x: true }\n') }) @@ -123,9 +119,7 @@ describe('objects', () => { test('createNode({ x: true, y: undefined })', () => { const s = new Document().createNode({ x: true, y: undefined }) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toMatchObject([ - { key: { value: 'x' }, value: { value: true } } - ]) + expect(s).toMatchObject([{ key: { value: 'x' }, value: { value: true } }]) }) test('createNode({ x: true, y: undefined }, { keepUndefined: true })', () => { @@ -134,7 +128,7 @@ describe('objects', () => { { keepUndefined: true } ) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toMatchObject([ + expect(s).toMatchObject([ { key: { value: 'x' }, value: { value: true } }, { key: { value: 'y' }, value: null } ]) @@ -144,9 +138,7 @@ describe('objects', () => { 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 } } - ]) + expect(s).toMatchObject([{ key: { value: 'x' }, value: { value: true } }]) }) describe('{ x: 3, y: [4], z: { w: "five", v: 6 } }', () => { @@ -154,18 +146,16 @@ describe('objects', () => { test('createNode(value)', () => { const s = new Document().createNode(object) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(3) - expect(s.items).toMatchObject([ + expect(s).toHaveLength(3) + expect(s).toMatchObject([ { key: { value: 'x' }, value: { value: 3 } }, - { key: { value: 'y' }, value: { items: [{ value: 4 }] } }, + { key: { value: 'y' }, value: [{ value: 4 }] }, { key: { value: 'z' }, - value: { - items: [ - { key: { value: 'w' }, value: { value: 'five' } }, - { key: { value: 'v' }, value: { value: 6 } } - ] - } + value: [ + { key: { value: 'w' }, value: { value: 'five' } }, + { key: { value: 'v' }, value: { value: 6 } } + ] } ]) }) @@ -188,24 +178,24 @@ describe('Set', () => { test('createNode(new Set)', () => { const s = new Document().createNode(new Set()) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toHaveLength(0) + expect(s).toHaveLength(0) }) test('createNode(new Set([true]))', () => { const s = new Document().createNode(new Set([true])) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([{ value: true }]) + expect(s).toMatchObject([{ value: true }]) }) describe("Set { 3, Set { 'four', 5 } }", () => { const set = new Set([3, new Set(['four', 5])]) test('createNode(set)', () => { const s = new Document().createNode(set) as any expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toHaveLength(2) - expect(s.items[0].value).toBe(3) - expect(s.items[1]).toBeInstanceOf(YAMLSeq) - expect(s.items[1].items).toHaveLength(2) - expect(s.items[1].items[0].value).toBe('four') - expect(s.items[1].items[1].value).toBe(5) + expect(s).toHaveLength(2) + expect(s[0].value).toBe(3) + expect(s[1]).toBeInstanceOf(YAMLSeq) + expect(s[1]).toHaveLength(2) + expect(s[1][0].value).toBe('four') + expect(s[1][1].value).toBe(5) }) test('set doc value', () => { const res = '- 3\n- - four\n - 5\n' @@ -218,18 +208,15 @@ describe('Set', () => { const doc = new Document(null) const s = doc.createNode(set) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([ - { value: 3 }, - { items: [{ value: 'four' }, { value: 5 }] } - ]) + expect(s).toMatchObject([{ value: 3 }, [{ value: 'four' }, { value: 5 }]]) }) test('Schema#createNode() - YAML 1.1', () => { const doc = new Document(null, { version: '1.1' }) const s = doc.createNode(set) as any expect(s.constructor.tag).toBe('tag:yaml.org,2002:set') - expect(s.items).toMatchObject([ + expect(s).toMatchObject([ { key: { value: 3 } }, - { key: { items: [{ key: { value: 'four' } }, { key: { value: 5 } }] } } + { key: [{ key: { value: 'four' } }, { key: { value: 5 } }] } ]) }) }) @@ -239,14 +226,12 @@ describe('Map', () => { test('createNode(new Map)', () => { const s = new Document().createNode(new Map()) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(0) + expect(s).toHaveLength(0) }) test('createNode(new Map([["x", true]]))', () => { const s = new Document().createNode(new Map([['x', true]])) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toMatchObject([ - { key: { value: 'x' }, value: { value: true } } - ]) + expect(s).toMatchObject([{ key: { value: 'x' }, value: { value: true } }]) }) describe("Map { 'x' => 3, 'y' => Set { 4 }, Map { 'w' => 'five', 'v' => 6 } => 'z' }", () => { const map = new Map([ @@ -263,17 +248,15 @@ describe('Map', () => { test('createNode(map)', () => { const s = new Document().createNode(map) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(3) - expect(s.items).toMatchObject([ + expect(s).toHaveLength(3) + expect(s).toMatchObject([ { key: { value: 'x' }, value: { value: 3 } }, - { key: { value: 'y' }, value: { items: [{ value: 4 }] } }, + { key: { value: 'y' }, value: [{ value: 4 }] }, { - key: { - items: [ - { key: { value: 'w' }, value: { value: 'five' } }, - { key: { value: 'v' }, value: { value: 6 } } - ] - }, + key: [ + { key: { value: 'w' }, value: { value: 'five' } }, + { key: { value: 'v' }, value: { value: 6 } } + ], value: { value: 'z' } } ]) @@ -306,13 +289,11 @@ describe('strictly equal objects', () => { const foo = { foo: 'FOO' } const s = new Document().createNode([foo, foo]) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([ - { - anchor: 'a1', - items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] - }, + expect(s).toMatchObject([ + [{ key: { value: 'foo' }, value: { value: 'FOO' } }], { source: 'a1' } ]) + expect(s[0].anchor).toBe('a1') }) test('createNode([foo, foo], { aliasDuplicateObjects: false })', () => { @@ -321,9 +302,9 @@ describe('strictly equal objects', () => { aliasDuplicateObjects: false }) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([ - { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }, - { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] } + expect(s).toMatchObject([ + [{ key: { value: 'foo' }, value: { value: 'FOO' } }], + [{ key: { value: 'foo' }, value: { value: 'FOO' } }] ]) }) }) @@ -333,16 +314,14 @@ describe('circular references', () => { const map: any = { foo: 'bar' } map.map = map const doc = new Document(map) - expect(doc.value).toMatchObject({ - anchor: 'a1', - items: [ - { key: { value: 'foo' }, value: { value: 'bar' } }, - { - key: { value: 'map' }, - value: { source: 'a1' } - } - ] - }) + expect(doc.value).toMatchObject([ + { key: { value: 'foo' }, value: { value: 'bar' } }, + { + key: { value: 'map' }, + value: { source: 'a1' } + } + ]) + expect(doc.value.anchor).toBe('a1') expect(doc.toString()).toBe(source` &a1 foo: bar @@ -372,15 +351,13 @@ describe('circular references', () => { const two = ['two'] const seq = [one, two, one, one, two] const doc = new Document(seq) - expect(doc.value).toMatchObject({ - items: [ - { items: [{ value: 'one' }] }, - { items: [{ value: 'two' }] }, - { source: 'a1' }, - { source: 'a1' }, - { source: 'a2' } - ] - }) + expect(doc.value).toMatchObject([ + [{ value: 'one' }], + [{ value: 'two' }], + { source: 'a1' }, + { source: 'a1' }, + { source: 'a2' } + ]) expect(doc.toString()).toBe(source` - &a1 - one @@ -399,9 +376,7 @@ describe('circular references', () => { const node = doc.createNode(seq) as any const source = node.get(0).get('foo').get('bar').get('baz') as Node const alias = node.get(1).get('fe').get('fi').get('fo').get('baz') as Alias - expect(source).toMatchObject({ - items: [{ key: { value: 'a' }, value: { value: 1 } }] - }) + expect(source).toMatchObject([{ key: { value: 'a' }, value: { value: 1 } }]) expect(alias.source).toBe(source.anchor) }) }) diff --git a/tests/doc/errors.ts b/tests/doc/errors.ts index a518d50c..7836a39e 100644 --- a/tests/doc/errors.ts +++ b/tests/doc/errors.ts @@ -66,12 +66,10 @@ describe('block collections', () => { expect(doc.errors[0].message).toMatch( 'All mapping items must start at the same column' ) - expect(doc.value).toMatchObject({ - items: [ - { key: { value: 'foo' }, value: { value: '1' } }, - { key: { value: 'bar' }, value: { value: 2 } } - ] - }) + expect(doc.value).toMatchObject([ + { key: { value: 'foo' }, value: { value: '1' } }, + { key: { value: 'bar' }, value: { value: 2 } } + ]) }) test('sequence with bad indentation', () => { @@ -81,9 +79,7 @@ describe('block collections', () => { expect(doc.errors[0].message).toMatch( 'All sequence items must start at the same column' ) - expect(doc.value).toMatchObject({ - items: [{ value: 'foo' }, { items: [{ value: 'bar' }] }] - }) + expect(doc.value).toMatchObject([{ value: 'foo' }, [{ value: 'bar' }]]) }) test('seq item in mapping', () => { @@ -94,12 +90,10 @@ describe('block collections', () => { { code: 'UNEXPECTED_TOKEN' }, { code: 'MISSING_CHAR' } ]) - expect(doc.value).toMatchObject({ - items: [ - { key: { value: 'foo' }, value: { value: '1' } }, - { key: { value: null }, value: null } - ] - }) + expect(doc.value).toMatchObject([ + { key: { value: 'foo' }, value: { value: '1' } }, + { key: { value: null }, value: null } + ]) }) test('doubled value indicator', () => { diff --git a/tests/doc/foldFlowLines.ts b/tests/doc/foldFlowLines.ts index c95bb1c9..3a9359d6 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.value.items[0].value.value).toBe(x) + expect(doc.value[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.value.items[0].value.items[0].items[0].value).toBe(value) + expect(doc.value[0].value[0][0].value).toBe(value) }) }) @@ -332,10 +332,10 @@ describe('end-to-end', () => { - plain with comment # that won't get folded ` const doc = YAML.parseDocument, false>(src) - expect(doc.value.items[0].value).toBe( + expect(doc.value[0].value).toBe( 'plain value with enough length to fold twice' ) - expect(doc.value.items[1].value).toBe('plain with comment') + expect(doc.value[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 28edc02f..2a6ba33d 100644 --- a/tests/doc/parse.ts +++ b/tests/doc/parse.ts @@ -173,18 +173,15 @@ test('long scalar value in flow map (#36)', () => { describe('flow collection keys', () => { test('block map with flow collection key as explicit key', () => { - const doc = YAML.parseDocument(`? []: x`) + const doc = YAML.parseDocument(`? []: x`) expect(doc.errors).toHaveLength(0) - expect(doc.value).toMatchObject({ - items: [ - { - key: { - items: [{ key: { items: [], flow: true }, value: { value: 'x' } }] - }, - value: null - } - ] - }) + expect(doc.value).toMatchObject([ + { + key: [{ key: [], value: { value: 'x' } }], + value: null + } + ]) + expect(doc.value[0].key[0].key).toMatchObject({ flow: true }) }) test('flow collection as first block map key (redhat-developer/vscode-yaml#712)', () => { @@ -194,19 +191,15 @@ describe('flow collection keys', () => { c: d `) expect(doc.errors).toHaveLength(0) - expect(doc.value).toMatchObject({ - items: [ - { - key: { value: 'a' }, - value: { - items: [ - { key: { items: [] }, value: { value: 'b' } }, - { key: { value: 'c' }, value: { value: 'd' } } - ] - } - } - ] - }) + expect(doc.value).toMatchObject([ + { + key: { value: 'a' }, + value: [ + { key: [], value: { value: 'b' } }, + { key: { value: 'c' }, value: { value: 'd' } } + ] + } + ]) }) test('flow collection as second block map key (redhat-developer/vscode-yaml#712)', () => { @@ -217,34 +210,28 @@ describe('flow collection keys', () => { c: d `) expect(doc.errors).toHaveLength(0) - expect(doc.value).toMatchObject({ - items: [ - { key: { value: 'x' }, value: { value: 'y' } }, - { - key: { value: 'a' }, - value: { - items: [ - { key: { items: [] }, value: { value: 'b' } }, - { key: { value: 'c' }, value: { value: 'd' } } - ] - } - } - ] - }) + expect(doc.value).toMatchObject([ + { key: { value: 'x' }, value: { value: 'y' } }, + { + key: { value: 'a' }, + value: [ + { key: [], value: { value: 'b' } }, + { key: { value: 'c' }, value: { value: 'd' } } + ] + } + ]) }) test('empty scalar as last flow collection value (#550)', () => { const doc = YAML.parseDocument('{c:}') - expect(doc.value.items).toMatchObject([ + expect(doc.value).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.value.items).toMatchObject([ - { key: { value: 'c' }, value: { items: [] } } - ]) + expect(doc.value).toMatchObject([{ key: { value: 'c' }, value: [] }]) }) }) @@ -321,8 +308,8 @@ describe('empty(ish) nodes', () => { const src = '{ ? : 123 }' const doc = YAML.parseDocument(src) expect(doc.errors).toHaveLength(0) - expect(doc.value.items[0].key.value).toBeNull() - expect(doc.value.items[0].value.value).toBe(123) + expect(doc.value[0].key.value).toBeNull() + expect(doc.value[0].value.value).toBe(123) }) describe('comment on empty pair value (#19)', () => { @@ -358,7 +345,7 @@ describe('empty(ish) nodes', () => { test('empty node position', () => { const doc = YAML.parseDocument('\r\na: # 123\r\n') - const empty = doc.value.items[0].value + const empty = doc.value[0].value expect(empty.range).toEqual([5, 5, 12]) }) @@ -393,7 +380,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.value.items[1].key.comment = 'c' + doc.value[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,17 +399,17 @@ describe('maps with no values', () => { test('implicit scalar key after explicit key with no value', () => { const doc = YAML.parseDocument('? - 1\nx:\n') - expect(doc.value.items).toMatchObject([ - { key: { items: [{ value: 1 }] }, value: null }, + expect(doc.value).toMatchObject([ + { key: [{ value: 1 }], value: null }, { key: { value: 'x' }, value: { value: null } } ]) }) test('implicit flow collection key after explicit key with no value', () => { const doc = YAML.parseDocument('? - 1\n[x]: y\n') - expect(doc.value.items).toMatchObject([ - { key: { items: [{ value: 1 }] }, value: null }, - { key: { items: [{ value: 'x' }] }, value: { value: 'y' } } + expect(doc.value).toMatchObject([ + { key: [{ value: 1 }], value: null }, + { key: [{ value: 'x' }], value: { value: 'y' } } ]) }) }) @@ -431,7 +418,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.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { key: { value: null }, value: null }, { key: { value: 'a' }, value: null } ]) @@ -692,7 +679,7 @@ describe('keepSourceTokens', () => { test(`${type}: default false`, () => { const doc = YAML.parseDocument(src) expect(doc.value).not.toHaveProperty('srcToken') - expect(doc.value.items[0]).not.toHaveProperty('srcToken') + expect(doc.value[0]).not.toHaveProperty('srcToken') expect(doc.get('foo')).not.toHaveProperty('srcToken') }) @@ -701,7 +688,7 @@ describe('keepSourceTokens', () => { keepSourceTokens: true }) expect(doc.value.srcToken).toMatchObject({ type }) - expect(doc.value.items[0].srcToken).toMatchObject({ + expect(doc.value[0].srcToken).toMatchObject({ key: { type: 'scalar' }, value: { type: 'scalar' } }) @@ -923,7 +910,7 @@ describe('stringKeys', () => { `, { stringKeys: true } ) - expect(doc.value.items).toMatchObject([ + expect(doc.value).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 1c791440..dc4ce71a 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -150,8 +150,7 @@ blah blah\n`) YAML.YAMLMap, false >({ foo }, { version }) - for (const node of doc.value.items) - node.value!.type = Scalar.QUOTE_DOUBLE + for (const node of doc.value) node.value!.type = Scalar.QUOTE_DOUBLE expect( doc .toString(opt) @@ -165,7 +164,7 @@ blah blah\n`) const doc = new YAML.Document, false>([foo], { version }) - for (const node of doc.value.items) node.type = Scalar.QUOTE_DOUBLE + for (const node of doc.value) node.type = Scalar.QUOTE_DOUBLE expect( doc .toString(opt) @@ -180,8 +179,8 @@ blah blah\n`) YAML.YAMLMap>, false >({ foo: [foo] }, { version }) - const seq = doc.value.items[0].value! - for (const node of seq.items) node.type = Scalar.QUOTE_DOUBLE + const seq = doc.value[0].value! + for (const node of seq) node.type = Scalar.QUOTE_DOUBLE expect( doc .toString(opt) @@ -334,7 +333,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.value.items.push('TEST') + doc.value.push('TEST') expect(() => String(doc)).toThrow(/^Map items must all be pairs.*TEST/) }) @@ -368,7 +367,7 @@ z: test('Block map, with key.comment', () => { const doc = getDoc() doc.set('a', new Scalar(null)) - doc.value.items[0].key.comment = 'c' + doc.value[0].key.comment = 'c' expect(doc.toString({ nullStr: '' })).toBe('a: #c\nb:\n') }) @@ -388,7 +387,7 @@ z: test('Flow map, with key.comment', () => { const doc = getDoc() doc.value.flow = true - doc.value.items[0].key.comment = 'c' + doc.value[0].key.comment = 'c' expect(doc.toString({ nullStr: '' })).toBe('{\n a:, #c\n b:\n}\n') }) @@ -730,14 +729,14 @@ describe('simple keys', () => { test('key with block scalar value', () => { const doc = YAML.parseDocument('foo: bar') - doc.value.items[0].key.type = 'BLOCK_LITERAL' + doc.value[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.value.items[0].key.comment = 'FOO' + doc.value[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/ @@ -1042,7 +1041,7 @@ describe('Scalar options', () => { defaultKeyType: Scalar.QUOTE_SINGLE } as const const doc = new YAML.Document({ foo: null }) - const key = doc.value.items[0].key as Scalar + const key = doc.value[0].key as Scalar key.type = Scalar.BLOCK_LITERAL expect(doc.toString(opt)).toBe('? "foo"\n') }) @@ -1428,8 +1427,7 @@ describe('YAML.stringify on ast Document', () => { describe('flow collection padding', () => { const doc = new YAML.Document() - doc.value = new YAML.YAMLSeq() - doc.value.items = [new Scalar(1), new Scalar(2)] + doc.value = new YAML.YAMLSeq(undefined, [new Scalar(1), new Scalar(2)]) doc.value.flow = true test('default', () => { diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 02a050d0..9a098d5c 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -147,7 +147,7 @@ describe('number types', () => { intAsBigInt: false, version: '1.1' }) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { value: 10, format: 'BIN' }, { value: 83, format: 'OCT' }, { value: -0, format: 'OCT' }, @@ -159,10 +159,10 @@ describe('number types', () => { { value: 0.42 }, { value: 0.4 } ]) - 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') + expect(doc.value[3]).not.toHaveProperty('format') + expect(doc.value[6]).not.toHaveProperty('format') + expect(doc.value[6]).not.toHaveProperty('minFractionDigits') + expect(doc.value[7]).not.toHaveProperty('format') }) test('Version 1.2', () => { @@ -180,7 +180,7 @@ describe('number types', () => { intAsBigInt: false, version: '1.2' }) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { value: 83, format: 'OCT' }, { value: 0, format: 'OCT' }, { value: 123456 }, @@ -191,10 +191,10 @@ describe('number types', () => { { value: 0.42 }, { value: 0.4 } ]) - 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') + expect(doc.value[2]).not.toHaveProperty('format') + expect(doc.value[5]).not.toHaveProperty('format') + expect(doc.value[5]).not.toHaveProperty('minFractionDigits') + expect(doc.value[6]).not.toHaveProperty('format') }) }) @@ -212,7 +212,7 @@ describe('number types', () => { intAsBigInt: true, version: '1.1' }) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { value: 10n, format: 'BIN' }, { value: 83n, format: 'OCT' }, { value: 0n, format: 'OCT' }, @@ -221,9 +221,9 @@ describe('number types', () => { { value: 0.5123, format: 'EXP' }, { value: 4.02 } ]) - 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[3]).not.toHaveProperty('format') + expect(doc.value[6]).not.toHaveProperty('format') + expect(doc.value[6]).not.toHaveProperty('minFractionDigits') expect(doc.toJS()).toEqual([10n, 83n, 0n, 123456n, 310, 0.5123, 4.02]) expect(doc.value.toJS(doc)).toEqual([ 10n, @@ -249,7 +249,7 @@ describe('number types', () => { intAsBigInt: true, version: '1.2' }) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { value: 83n, format: 'OCT' }, { value: 0n, format: 'OCT' }, { value: 123456n }, @@ -257,9 +257,9 @@ describe('number types', () => { { value: 0.5123, format: 'EXP' }, { value: 4.02 } ]) - 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[2]).not.toHaveProperty('format') + expect(doc.value[5]).not.toHaveProperty('format') + expect(doc.value[5]).not.toHaveProperty('minFractionDigits') }) }) }) @@ -344,7 +344,7 @@ describe('json schema', () => { }) expect(doc.errors).toHaveLength(2) doc.errors = [] - doc.value.items[1].value!.tag = 'tag:yaml.org,2002:float' + doc.value[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' ) @@ -486,7 +486,7 @@ one: 1 '{ 3: 4 }': 'many' }) expect(doc.errors).toHaveLength(0) - doc.value.items[2].key = doc.createNode({ 3: 4 }) + doc.value[2].key = doc.createNode({ 3: 4 }) expect(doc.toJS()).toMatchObject({ one: 1, 2: 'two', @@ -508,7 +508,7 @@ one: 1 ]) ) expect(doc.errors).toHaveLength(0) - doc.value.items[2].key = doc.createNode({ 5: 6 }) + doc.value[2].key = doc.createNode({ 5: 6 }) expect(doc.toJS({ mapAsMap: true })).toMatchObject( new Map([ ['one', 1], @@ -538,8 +538,8 @@ description: const doc = parseDocument>>(src, { schema: 'yaml-1.1' }) - const canonical = doc.value.items[0].value!.value - const generic = doc.value.items[1].value!.value + const canonical = doc.value[0].value!.value + const generic = doc.value[1].value!.value expect(canonical).toBeInstanceOf(Uint8Array) expect(generic).toBeInstanceOf(Uint8Array) expect(canonical).toHaveLength(185) @@ -710,7 +710,7 @@ no time zone (Z): 2001-12-15 2:59:43.10 date (00:00:00Z): 2002-12-14` const doc = parseDocument>(src) - doc.value.items.forEach(item => { + doc.value.forEach(item => { expect(item.value!.value).toBeInstanceOf(Date) }) expect(doc.toJS()).toMatchObject({ @@ -748,7 +748,7 @@ date (00:00:00Z): 2002-12-14\n`) test(name, () => { const doc = parseDocument(src, { version: '1.1' }) expect(doc.value).toBeInstanceOf(YAMLSeq) - expect(doc.value.items).toMatchObject([ + expect(doc.value).toMatchObject([ { key: { value: 'a' }, value: { value: 1 } }, { key: { value: 'b' }, value: { value: 2 } }, { key: { value: 'a' }, value: { value: 3 } } @@ -945,12 +945,11 @@ describe('custom tags', () => { const doc = parseDocument>(src) 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') - expect(items[1].tag).toBe('tag:example.com,2000:test/z') - expect(items[2].tag).toBe('tag:example.com,2000:other/w') + expect(doc.value).toHaveLength(4) + doc.value.forEach(item => expect(typeof item.value).toBe('string')) + expect(doc.value[0].tag).toBe('!y') + expect(doc.value[1].tag).toBe('tag:example.com,2000:test/z') + expect(doc.value[2].tag).toBe('tag:example.com,2000:other/w') }) test('stringify', () => { @@ -977,10 +976,10 @@ describe('custom tags', () => { }) doc.value.commentBefore = 'c' - doc.value.items[3].comment = 'cc' + doc.value[3].comment = 'cc' const s = new Scalar(6) s.tag = '!g' - doc.value.items.splice(1, 1, s, new Scalar('7')) + doc.value.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/ diff --git a/tests/visit.ts b/tests/visit.ts index 1344cd4e..495f58c7 100644 --- a/tests/visit.ts +++ b/tests/visit.ts @@ -144,7 +144,7 @@ for (const [visit_, title] of [ const Scalar = vi.fn() await visit_(doc, { Seq(_, seq) { - seq.items.push(doc.createNode('three')) + seq.push(doc.createNode('three')) }, Scalar }) diff --git a/vitest.config.js b/vitest.config.js index bc445dc8..8ae49874 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -36,6 +36,7 @@ export default defineConfig({ test: { alias, globals: true, + setupFiles: ['tests/_setup.ts'], include: ['tests/**/*.{js,ts}'], exclude: ['tests/_*', 'tests/artifacts/', 'tests/json-test-suite/'] } From d9c1921ebf7e5fe48cd0e00d664280b8e9f64660 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 15 Feb 2026 14:52:29 +0200 Subject: [PATCH 5/5] feat!: Drop the map & seq .add() methods, override .push() instead --- docs/04_documents.md | 5 +- docs/05_content_nodes.md | 36 ++++++----- src/compose/resolve-block-map.ts | 4 +- src/compose/resolve-block-seq.ts | 2 +- src/compose/resolve-flow-collection.ts | 8 +-- src/doc/Document.ts | 18 ++---- src/nodes/Collection.ts | 3 - src/nodes/YAMLMap.ts | 69 +++++++++++++++------ src/nodes/YAMLSeq.ts | 85 +++++++++++++++++--------- src/schema/yaml-1.1/omap.ts | 2 +- src/schema/yaml-1.1/set.ts | 32 +++++----- tests/collection-access.ts | 77 ++++++++++++----------- tests/doc/anchors.ts | 4 +- tests/doc/createNode.ts | 4 +- tests/doc/stringify.ts | 11 ++-- tests/doc/types.ts | 4 +- 16 files changed, 201 insertions(+), 163 deletions(-) diff --git a/docs/04_documents.md b/docs/04_documents.md index d44034c1..70a17b7f 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -117,15 +117,14 @@ which is expected to always contain a `YAMLMap`, `YAMLSeq`, or `Scalar` value. ```js const doc = parseDocument('a: 1\nb: [2, 3]\n') doc.get('a') // 1 -doc.get() // YAMLMap { items: [Pair, Pair], ... } doc.get('b').has(0) // true -doc.get('b').add(4) // -> doc.get('b').items.length === 3 +doc.get('b').push(4) // -> doc.get('b').items.length === 3 doc.get('b').delete(1) // true doc.get('b').get(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`. +`delete`, `get`, `has`, and `set`. #### `Document#toJS()`, `Document#toJSON()` and `Document#toString()` diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index a1fa1499..531bc5ff 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -61,21 +61,19 @@ interface CollectionBase extends NodeBase { clone(schema?: Schema): this // a deep copy of this collection } -class YAMLMap implements CollectionBase { - items: Pair[] - add(pair: Pair | { key: K; value: V }, overwrite?: boolean): void +class YAMLMap extends Array> implements CollectionBase { delete(key: K): boolean get(key: K, keepScalar?: boolean): unknown has(key: K): boolean + push(...pairs: Pair[]): number set(key: K, value: V): void } -class YAMLSeq implements CollectionBase { - items: T[] - add(value: T): void +class YAMLSeq extends Array> implements CollectionBase { delete(key: number | Scalar): boolean get(key: number | Scalar, keepScalar?: boolean): unknown has(key: number | Scalar): boolean + push(...items: Array>): number set(key: number | Scalar, value: T): void } ``` @@ -93,21 +91,21 @@ All of the collections provide the following accessor methods: | Method | Returns | Description | | --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| add(value) | `void` | Adds a value to the collection. For `!!map` and `!!omap` the value must be a Pair instance, which must not have a key that already exists in the map. | | delete(key) | `boolean` | Removes a value from the collection. Returns `true` if the item was found and removed. | | get(key) | `Node` | Returns value at `key`, or `undefined` if not found. | | has(key) | `boolean` | Checks if the collection includes a value with the key `key`. | +| push(...values) | `number` | Adds values to the collection. For `!!map` and `!!omap` the value must be a Pair instance, which must not have a key that already exists in the map. | | set(key, value) | `any` | Sets a value in this collection. For `!!set`, `value` needs to be a boolean to add/remove the item from the set. When overwriting a `Scalar` value with a scalar, the original node is retained. | ```js const doc = new YAML.Document({ a: 1, b: [2, 3] }) // { a: 1, b: [ 2, 3 ] } -doc.add(doc.createPair('c', 4)) // { a: 1, b: [ 2, 3 ], c: 4 } -doc.get('b').add(5) // { a: 1, b: [ 2, 3, 5 ], c: 4 } -doc.set('c', 42) // { a: 1, b: [ 2, 3, 5 ], c: 42 } -doc.get('c').set('x') // TypeError: doc.get(...).set is not a function -doc.delete('c') // { a: 1, b: [ 2, 3, 5 ] } -doc.get('b').delete(1) // { a: 1, b: [ 2, 5 ] } +doc.set('c', 4) // { a: 1, b: [ 2, 3 ], c: 4 } +doc.get('b').push(5) // { a: 1, b: [ 2, 3, 5 ], c: 4 } +doc.set('c', 42) // { a: 1, b: [ 2, 3, 5 ], c: 42 } +doc.get('c').set('x') // TypeError: doc.get(...).set is not a function +doc.delete('c') // { a: 1, b: [ 2, 3, 5 ] } +doc.get('b').delete(1) // { a: 1, b: [ 2, 5 ] } doc.get('a') // Scalar { value: 1 } doc.get('b').get(1) // Scalar { value: 5 } @@ -118,7 +116,7 @@ doc.get('b').has(0) // true For all of these methods, the keys may be nodes or their wrapped scalar values (i.e. `42` will match `Scalar { value: 42 }`). Keys for `!!seq` should be non-negative integers, or their string representations. -`add()` and `set()` will internally call `doc.createNode()` to wrap the value. +`set()` will internally call `doc.createNode()` to wrap the value. ## Alias Nodes @@ -165,8 +163,8 @@ const map = doc.createNode({ balloons: 99 }) // key: Scalar { value: 'balloons' }, // value: Scalar { value: 99 } } ] } -doc.add(map) -doc.get(0, true).comment = ' A commented item' +doc.value.push(map) +doc.get(0).comment = ' A commented item' String(doc) // - some # A commented item // - values @@ -197,8 +195,8 @@ To that end, you'll need to assign its return value to the `value` of a document

doc.createAlias(node, name?): Alias

```js -const alias = doc.createAlias(doc.get(1, true), 'foo') -doc.add(alias) +const alias = doc.createAlias(doc.get(1), 'foo') +doc.value.push(alias) String(doc) // - some # A commented item // - &foo values @@ -223,7 +221,7 @@ const doc = new Document([ 42, { including: 'objects', 3: 'a string' } ]) -doc.add(doc.createPair(1, 'a number')) +doc.value.push(doc.createPair(1, 'a number')) doc.toString() // - some values diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index 7fa1a2b5..a4848b08 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -116,7 +116,7 @@ export function resolveBlockMap( offset = valueNode.range![2] const pair = new Pair(keyNode, valueNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem - map.push(pair) + map._push(pair) } else { // key with no value if (implicitKey) @@ -131,7 +131,7 @@ export function resolveBlockMap( } const pair = new Pair(keyNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem - map.push(pair) + map._push(pair) } } diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts index e086787b..9717d1c4 100644 --- a/src/compose/resolve-block-seq.ts +++ b/src/compose/resolve-block-seq.ts @@ -50,7 +50,7 @@ export function resolveBlockSeq( : composeEmptyNode(ctx, props.end, start, null, props, onError) if (ctx.schema.compat) flowIndentCheck(bs.indent, value, onError) offset = node.range![2] - seq.push(node) + seq._push(node) } seq.range = [bs.offset, offset, commentEnd ?? offset] return seq diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index d34ca5f8..848c7918 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -109,7 +109,7 @@ export function resolveFlowCollection( const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError) - ;(coll as YAMLSeq).push(valueNode) + ;(coll as YAMLSeq)._push(valueNode) offset = valueNode.range![2] if (isBlock(value)) onError(valueNode.range!, 'BLOCK_IN_FLOW', blockMsg) } else { @@ -193,14 +193,14 @@ export function resolveFlowCollection( const map = coll as YAMLMap if (mapIncludes(ctx, map, keyNode)) onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique') - map.push(pair) + map._push(pair) } else { const map = new YAMLMap(ctx.schema) map.flow = true - map.push(pair) + map._push(pair) const endRange = (valueNode ?? keyNode).range! map.range = [keyNode.range![0], endRange[1], endRange[2]] - ;(coll as YAMLSeq).push(map) + ;(coll as YAMLSeq)._push(map) } offset = valueNode ? valueNode.range![2] : valueProps.end } diff --git a/src/doc/Document.ts b/src/doc/Document.ts index f4975c4f..ae096cfa 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -151,11 +151,6 @@ export class Document< return copy } - /** Adds a value to the document. */ - add(value: any): void { - assertCollection(this.value).add(value) - } - /** * Create a new `Alias` node, ensuring that the target `node` has the required anchor. * @@ -238,14 +233,13 @@ export class Document< * @returns `true` if the item was found and removed. */ delete(key: any): boolean { - return assertCollection(this.value).delete(key) + return isCollection(this.value) ? this.value.delete(key) : false } /** * Returns item at `key`, or `undefined` if not found. */ - get(key?: any): Strict extends true ? Node | Pair | undefined : any { - if (key === undefined) return this.value + get(key: any): Strict extends true ? Node | Pair | undefined : any { return isCollection(this.value) ? this.value.get(key) : undefined } @@ -261,7 +255,8 @@ export class Document< * boolean to add/remove the item from the set. */ set(key: any, value: any): void { - assertCollection(this.value).set(key, value) + if (isCollection(this.value)) this.value.set(key, value) + else throw new Error('Expected a YAML collection as document value') } /** @@ -342,8 +337,3 @@ export class Document< return stringifyDocument(this, options) } } - -function assertCollection(value: unknown) { - if (isCollection(value)) return value - throw new Error('Expected a YAML collection as document value') -} diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index 8cb742bf..141a0ff8 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -30,9 +30,6 @@ export interface CollectionBase extends NodeBase { */ clone(schema?: Schema): this - /** Adds a value to the collection. */ - add(value: unknown): void - /** * Removes a value from the collection. * @returns `true` if the item was found and removed. diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index a57f1482..4d948ddd 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -122,30 +122,43 @@ export class YAMLMap< return copyCollection(this, schema) } + /** @private */ + _push(pair: Pair): void { + super.push(pair) + } + /** - * Adds a key-value pair to the map. + * Adds new key-value pairs to the mapping, and returns its new length. * - * Using a key that is already in the collection overwrites the previous value. + * Added pairs must not have the same keys as ones previously set in the map. */ - add(pair: Pair): void { - if (!(pair instanceof Pair)) throw new TypeError('Expected a Pair') + push(...pairs: Pair[]): number { + for (const pair of pairs) { + if (!(pair instanceof Pair)) { + const msg = `Expected a Pair, but found ${(pair as any).constructor?.name ?? pair}` + throw new TypeError(msg) + } + if (findPair(this, pair.key)) { + const msg = `Maps must not include duplicate keys: ${String(pair.key)}` + throw new Error(msg) + } - const prev = findPair(this, pair.key) - const sortEntries = this.schema?.sortMapEntries - if (prev) { - // For scalars, keep the old node & its comments and anchors - if (prev.value instanceof Scalar && pair.value instanceof Scalar) - prev.value.value = pair.value.value - else prev.value = pair.value - } else if (sortEntries) { - const i = this.findIndex(item => sortEntries(pair, item) < 0) - if (i === -1) this.push(pair) - else this.splice(i, 0, pair) - } else { - this.push(pair) + if (this.schema?.sortMapEntries) { + const sortEntries = this.schema.sortMapEntries + const i = this.findIndex(item => sortEntries(pair, item) < 0) + if (i === -1) super.push(pair) + else this.splice(i, 0, pair) + } else { + super.push(pair) + } } + return this.length } + /** + * Removes a value from the mapping. + * @returns `true` if the item was found and removed. + */ delete(key: unknown): boolean { const it = findPair(this, key) if (!it) return false @@ -153,11 +166,13 @@ export class YAMLMap< return del.length > 0 } + /** Returns item at `key`, or `undefined` if not found. */ get(key: unknown): NodeOf | undefined { const it = findPair(this, key) return it?.value ?? undefined } + /** Checks if the mapping includes a value with the key `key`. */ has(key: unknown): boolean { return !!findPair(this, key) } @@ -170,9 +185,8 @@ export class YAMLMap< let pair: Pair if (isNode(key) && (value === null || isNode(value))) { pair = new Pair(key, value) - } else if (!this.schema) { - throw new Error('Schema is required') } else { + if (!this.schema) throw new Error('Schema is required') const nc = new NodeCreator(this.schema, { ...options, aliasDuplicateObjects: false @@ -180,7 +194,22 @@ export class YAMLMap< pair = nc.createPair(key, value) nc.setAnchors() } - this.add(pair as Pair) + + const prev = findPair(this, pair.key) + if (prev) { + const pv = pair.value as NodeOf + // For scalars, keep the old node & its comments and anchors + if (prev.value instanceof Scalar && pv instanceof Scalar) { + Object.assign(prev.value, pv) + } else prev.value = pv + } else if (this.schema?.sortMapEntries) { + const sortEntries = this.schema.sortMapEntries + const i = this.findIndex(item => sortEntries(pair, item) < 0) + if (i === -1) super.push(pair as Pair) + else this.splice(i, 0, pair as Pair) + } else { + super.push(pair as Pair) + } } /** diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index 3b668de2..37437806 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -5,10 +5,15 @@ 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 { copyCollection, type CollectionBase, type NodeOf, type Primitive } from './Collection.ts' +import { + copyCollection, + type CollectionBase, + type NodeOf, + type Primitive +} from './Collection.ts' import { isNode } from './identity.ts' import type { Node, Range } from './Node.ts' -import type { Pair } from './Pair.ts' +import { Pair } from './Pair.ts' import { Scalar } from './Scalar.ts' import { ToJSContext } from './toJS.ts' @@ -96,20 +101,29 @@ export class YAMLSeq< return copyCollection(this, schema) } - add( - value: T, - options?: Omit - ): void { - if (isNode(value)) this.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.push(nc.create(value) as NodeOf) - nc.setAnchors() + /** @private */ + _push(item: NodeOf): void { + super.push(item) + } + + /** + * Appends new elements to the sequence, and returns its new length. + * + * Non-node values are converted to Node values. + */ + push(...items: Array>): number { + let nc: NodeCreator | undefined + for (const value of items) { + if (isNode(value) || value instanceof Pair) { + super.push(value as NodeOf) + } else { + if (!this.schema) throw new Error('Schema is required') + nc ??= new NodeCreator(this.schema, { aliasDuplicateObjects: false }) + super.push(nc.create(value) as NodeOf) + nc.setAnchors() + } } + return this.length } /** @@ -155,7 +169,7 @@ export class YAMLSeq< * Sets a value in this collection. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. * - * Throws if `idx` is not a non-negative integer. + * Throws if `idx` is not an integer. */ set( idx: number, @@ -164,28 +178,39 @@ export class YAMLSeq< ): 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[idx] + const prev = this.at(idx) if (prev instanceof Scalar && isScalarValue(value)) prev.value = value - else if (isNode(value)) this[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[idx] = nc.create(value) as NodeOf - nc.setAnchors() + let nv + if (isNode(value)) nv = value as NodeOf + else { + if (!this.schema) throw new Error('Schema is required') + const nc = new NodeCreator(this.schema, { + ...options, + aliasDuplicateObjects: false + }) + nv = nc.create(value) as NodeOf + nc.setAnchors() + } + if (idx < 0) { + if (idx < -this.length) throw new RangeError(`Invalid index ${idx}`) + idx += this.length + } + this[idx] = nv } } /** A plain JavaScript representation of this node. */ toJS(doc: Document, ctx?: ToJSContext): any[] { ctx ??= new ToJSContext() - const res: unknown[] = [] - if (this.anchor) ctx.setAnchor(this, res) - for (const item of this) res.push(item.toJS(doc, ctx)) - return res + if (this.anchor) { + const res: unknown[] = [] + if (this.anchor) ctx.setAnchor(this, res) + for (const item of this) res.push(item.toJS(doc, ctx)) + return res + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Array.from(this, item => item.toJS(doc, ctx)) } toString( diff --git a/src/schema/yaml-1.1/omap.ts b/src/schema/yaml-1.1/omap.ts index 5b2b83a6..389d8a74 100644 --- a/src/schema/yaml-1.1/omap.ts +++ b/src/schema/yaml-1.1/omap.ts @@ -22,10 +22,10 @@ export class YAMLOMap< this.tag = YAMLOMap.tag } - add: typeof YAMLMap.prototype.add = YAMLMap.prototype.add.bind(this) delete: typeof YAMLMap.prototype.delete = YAMLMap.prototype.delete.bind(this) get: typeof YAMLMap.prototype.get = YAMLMap.prototype.get.bind(this) has: typeof YAMLMap.prototype.has = YAMLMap.prototype.has.bind(this) + push: typeof YAMLMap.prototype.push = YAMLMap.prototype.push.bind(this) set: typeof YAMLMap.prototype.set = YAMLMap.prototype.set.bind(this) /** diff --git a/src/schema/yaml-1.1/set.ts b/src/schema/yaml-1.1/set.ts index d7c1feee..34431e0b 100644 --- a/src/schema/yaml-1.1/set.ts +++ b/src/schema/yaml-1.1/set.ts @@ -25,22 +25,22 @@ export class YAMLSet< /** * Add a value to the set. * - * If `value` is a Pair, its `.value` must be null and `options` is ignored. + * If a value of `items` is a Pair, its `.value` must be null. * * If the set already includes a matching value, no value is added. */ - add( - value: unknown, - options?: Omit - ): void { - if (!(value instanceof Pair)) { - this.set(value, true, options) - } else if (value.value !== null) { - throw new TypeError('set pair values must be null') - } else { - const prev = findPair(this, value.key) - if (!prev) this.push(value as Pair) + push(...items: unknown[]): number { + for (const value of items) { + if (value instanceof Pair) { + if (value.value !== null) + throw new TypeError('set pair values must be null') + const prev = findPair(this, value.key) + if (!prev) super.push(value as Pair) + } else { + this.set(value, true) + } } + return this.length } /** @@ -66,11 +66,9 @@ export class YAMLSet< this.splice(this.indexOf(prev), 1) } else if (!prev && value) { let node: Node - if (isNode(key)) { - node = key - } else if (!this.schema) { - throw new Error('Schema is required') - } else { + if (isNode(key)) node = key + else { + if (!this.schema) throw new Error('Schema is required') const nc = new NodeCreator(this.schema, { ...options, aliasDuplicateObjects: false diff --git a/tests/collection-access.ts b/tests/collection-access.ts index 374dd3c9..9625462a 100644 --- a/tests/collection-access.ts +++ b/tests/collection-access.ts @@ -27,10 +27,11 @@ describe('Map', () => { ]) }) - test('add', () => { - map.add(doc.createPair('c', 'x')) + test('push', () => { + map.push(doc.createPair('c', 'x')) expect(map.get('c')).toMatchObject({ value: 'x' }) - map.add(doc.createPair('c', 'y')) + expect(() => map.push(doc.createPair('c', 'y'))).toThrow() + map.set('c', 'y') expect(map.get('c')).toMatchObject({ value: 'y' }) expect(map).toHaveLength(3) }) @@ -116,10 +117,10 @@ describe('Seq', () => { expect(seq).toMatchObject([{ value: 1 }, [{ value: 2 }, { value: 3 }]]) }) - test('add', () => { - seq.add(9) - expect(seq.get(2)).toMatchObject({ value: 9 }) - seq.add(1) + test('push', () => { + seq.push(9) + expect(seq[2]).toMatchObject({ value: 9 }) + seq.push(1) expect(seq).toHaveLength(4) }) @@ -140,7 +141,6 @@ describe('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) @@ -164,7 +164,7 @@ describe('Seq', () => { test('set with integer', () => { seq.set(0, 2) expect(seq.get(0)).toMatchObject({ value: 2 }) - seq.set(1, 5) + seq.set(-1, 5) expect(seq.get(1)).toMatchObject({ value: 5 }) seq.set(2, 6) expect(seq.get(2)).toMatchObject({ value: 6 }) @@ -172,7 +172,6 @@ describe('Seq', () => { }) 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) }) @@ -192,13 +191,13 @@ describe('Set', () => { ]) }) - test('add', () => { - set.add('x') + test('push', () => { + set.push('x') expect(set.get('x')).toMatchObject({ value: 'x' }) - set.add('x') + set.push('x') const y0 = new Scalar('y') - set.add(new Pair(y0)) - set.add(new Pair(new Scalar('y'))) + set.push(new Pair(y0)) + set.push(new Pair(new Scalar('y'))) expect(set.get('y')).toBe(y0) expect(set).toHaveLength(5) }) @@ -243,10 +242,11 @@ describe('OMap', () => { ]) }) - test('add', () => { - omap.add(doc.createPair('c', 'x')) + test('push', () => { + omap.push(doc.createPair('c', 'x')) expect(omap.get('c')).toMatchObject({ value: 'x' }) - omap.add(doc.createPair('c', 'y')) + expect(() => omap.push(doc.createPair('c', 'y'))).toThrow() + omap.set('c', 'y') expect(omap).toHaveLength(3) }) @@ -299,12 +299,6 @@ describe('Document', () => { ]) }) - test('add', () => { - doc.add(doc.createPair('c', 'x')) - expect(doc.get('c')).toMatchObject({ value: 'x' }) - expect(doc.value).toHaveLength(3) - }) - test('delete', () => { expect(doc.delete('a')).toBe(true) expect(doc.delete('a')).toBe(false) @@ -317,27 +311,26 @@ describe('Document', () => { expect(() => doc.set('a', 1)).toThrow(/document value/) }) - test('get', () => { + test('get with map value', () => { + const doc = new Document({ a: 1, b: [2, 3] }) expect(doc.get('a')).toMatchObject({ value: 1 }) expect(doc.get('c')).toBeUndefined() }) - test('get on scalar value', () => { - const doc = new Document('s') - expect(doc.get('a')).toBeUndefined() + test('get with seq value', () => { + const doc = new Document([2, 3]) + expect(doc.get(0)).toMatchObject({ value: 2 }) + expect(() => doc.get(-1)).toThrow() + expect(() => doc.get('a')).toThrow() }) - test('has', () => { - expect(doc.has('a')).toBe(true) - expect(doc.has('c')).toBe(false) - }) - - test('has on scalar value', () => { + test('get with scalar value', () => { const doc = new Document('s') - expect(doc.has('a')).toBe(false) + expect(doc.get('a')).toBeUndefined() }) - test('set', () => { + test('set with map value', () => { + const doc = new Document({ a: 1, b: [2, 3] }) doc.set('a', 2) expect(doc.get('a')).toMatchObject({ value: 2 }) doc.set('c', 6) @@ -345,7 +338,17 @@ describe('Document', () => { expect(doc.value).toHaveLength(3) }) - test('set on scalar value', () => { + test('set with seq value', () => { + const doc = new Document([2, 3]) + doc.set(0, 4) + expect(doc.get(0)).toMatchObject({ value: 4 }) + doc.set(3, 6) + expect(doc.get(3)).toMatchObject({ value: 6 }) + expect(() => doc.set('a', 1)).toThrow(TypeError) + expect(doc.value).toHaveLength(4) + }) + + test('set with scalar value', () => { const doc = new Document('s') expect(() => doc.set('a', 1)).toThrow(/document value/) }) diff --git a/tests/doc/anchors.ts b/tests/doc/anchors.ts index d5d11da0..eedaf0fe 100644 --- a/tests/doc/anchors.ts +++ b/tests/doc/anchors.ts @@ -212,7 +212,7 @@ describe('merge <<', () => { x: 1 label: center/big` - test.only('YAML.parse with merge:true', () => { + test('YAML.parse with merge:true', () => { const res = parse(src, { merge: true }) expect(res).toHaveLength(8) for (let i = 4; i < res.length; ++i) { @@ -334,7 +334,7 @@ describe('merge <<', () => { merge: true }) const alias = doc.createAlias(doc.get(0).get('a')) - doc.get(1).add(doc.createPair('<<', alias)) + doc.get(1).push(doc.createPair('<<', alias)) expect(String(doc)).toBe('[ { a: &a1 A }, { b: B, <<: *a1 } ]\n') expect(() => doc.toJS()).toThrow( 'Merge sources must be maps or map aliases' diff --git a/tests/doc/createNode.ts b/tests/doc/createNode.ts index 71ded54e..1ae41088 100644 --- a/tests/doc/createNode.ts +++ b/tests/doc/createNode.ts @@ -374,8 +374,8 @@ describe('circular references', () => { const seq = [{ foo: { bar: { baz } } }, { fe: { fi: { fo: { baz } } } }] const doc = new Document(null) const node = doc.createNode(seq) as any - const source = node.get(0).get('foo').get('bar').get('baz') as Node - const alias = node.get(1).get('fe').get('fi').get('fo').get('baz') as Alias + const source = node[0].get('foo').get('bar').get('baz') as Node + const alias = node[1].get('fe').get('fi').get('fo').get('baz') as Alias expect(source).toMatchObject([{ key: { value: 'a' }, value: { value: 1 } }]) expect(alias.source).toBe(source.anchor) }) diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index dc4ce71a..2ee4da02 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -329,12 +329,11 @@ z: ) }) - test('Map with non-Pair item', () => { + test('pushing non-Pair item', () => { 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.value.push('TEST') - expect(() => String(doc)).toThrow(/^Map items must all be pairs.*TEST/) + expect(() => doc.value.push('TEST')).toThrow(TypeError) }) test('Keep block scalar types for keys', () => { @@ -797,9 +796,9 @@ describe('sortMapEntries', () => { a.key < b.key ? 1 : a.key > b.key ? -1 : 0 expect(YAML.stringify(obj, { sortMapEntries })).toBe('c: 3\nb: 2\na: 1\n') }) - test('doc.add', () => { - const doc = new YAML.Document(obj, { sortMapEntries: true }) - doc.add(doc.createPair('bb', 4)) + test('map.push', () => { + const doc = new YAML.Document(obj, { sortMapEntries: true }) + doc.value.push(doc.createPair('bb', 4)) expect(String(doc)).toBe('a: 1\nb: 2\nbb: 4\nc: 3\n') }) test('doc.set', () => { diff --git a/tests/doc/types.ts b/tests/doc/types.ts index 9a098d5c..15fbc5c1 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -907,7 +907,7 @@ date (00:00:00Z): 2002-12-14\n`) const src = '- { a: A, b: B }\n- { b: X }\n' const doc = parseDocument(src, { version: '1.1' }) const alias = doc.createAlias(doc.get(0), 'a') - doc.get(1).add(doc.createPair('<<', alias)) + doc.get(1).push(doc.createPair('<<', alias)) expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n') expect(doc.toJS()).toMatchObject([ { a: 'A', b: 'B' }, @@ -919,7 +919,7 @@ date (00:00:00Z): 2002-12-14\n`) const src = '- { a: A, b: B }\n- { b: X }\n' const doc = parseDocument(src, { version: '1.1' }) const alias = doc.createAlias(doc.get(0), 'a') - doc.get(1).add(doc.createPair('<<', alias)) + doc.get(1).push(doc.createPair('<<', alias)) expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n') expect(doc.toJS()).toMatchObject([ { a: 'A', b: 'B' },