Skip to content

Commit 7a434f0

Browse files
authored
feat: Use a proper tag for !!merge << keys (#580)
1 parent 5adbb60 commit 7a434f0

File tree

18 files changed

+196
-92
lines changed

18 files changed

+196
-92
lines changed

docs/06_custom_tags.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,16 @@ If including more than one custom tag from this set, make sure that the `'float'
6060

6161
These tags are a part of the YAML 1.1 [language-independent types](https://yaml.org/type/), but are not a part of any default YAML 1.2 schema.
6262

63-
| Identifier | YAML Type | JS Type | Description |
64-
| ------------- | ----------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65-
| `'binary'` | [`!!binary`](https://yaml.org/type/binary.html) | `Uint8Array` | Binary data, represented in YAML as base64 encoded characters. |
66-
| `'floatTime'` | [`!!float`](https://yaml.org/type/float.html) | `Number` | Sexagesimal floating-point number format, e.g. `190:20:30.15`. To stringify with this tag, the node `format` must be `'TIME'`. |
67-
| `'intTime'` | [`!!int`](https://yaml.org/type/int.html) | `Number` | Sexagesimal integer number format, e.g. `190:20:30`. To stringify with this tag, the node `format` must be `'TIME'`. |
68-
| `'omap'` | [`!!omap`](https://yaml.org/type/omap.html) | `Map` | Ordered sequence of key: value pairs without duplicates. Using `mapAsMap: true` together with this tag is not recommended, as it makes the parse → stringify loop non-idempotent. |
69-
| `'pairs'` | [`!!pairs`](https://yaml.org/type/pairs.html) | `Array` | Ordered sequence of key: value pairs allowing duplicates. To create from JS, use `doc.createNode(array, { tag: '!!pairs' })`. |
70-
| `'set'` | [`!!set`](https://yaml.org/type/set.html) | `Set` | Unordered set of non-equal values. |
71-
| `'timestamp'` | [`!!timestamp`](https://yaml.org/type/timestamp.html) | `Date` | A point in time, e.g. `2001-12-15T02:59:43`. |
63+
| Identifier | YAML Type | JS Type | Description |
64+
| ------------- | ----------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65+
| `'binary'` | [`!!binary`](https://yaml.org/type/binary.html) | `Uint8Array` | Binary data, represented in YAML as base64 encoded characters. |
66+
| `'floatTime'` | [`!!float`](https://yaml.org/type/float.html) | `Number` | Sexagesimal floating-point number format, e.g. `190:20:30.15`. To stringify with this tag, the node `format` must be `'TIME'`. |
67+
| `'intTime'` | [`!!int`](https://yaml.org/type/int.html) | `Number` | Sexagesimal integer number format, e.g. `190:20:30`. To stringify with this tag, the node `format` must be `'TIME'`. |
68+
| `'merge'` | [`!!merge`](https://yaml.org/type/merge.html) | `Symbol('<<')` | A `<<` merge key which allows one or more mappings to be merged with the current one. |
69+
| `'omap'` | [`!!omap`](https://yaml.org/type/omap.html) | `Map` | Ordered sequence of key: value pairs without duplicates. Using `mapAsMap: true` together with this tag is not recommended, as it makes the parse → stringify loop non-idempotent. |
70+
| `'pairs'` | [`!!pairs`](https://yaml.org/type/pairs.html) | `Array` | Ordered sequence of key: value pairs allowing duplicates. To create from JS, use `doc.createNode(array, { tag: '!!pairs' })`. |
71+
| `'set'` | [`!!set`](https://yaml.org/type/set.html) | `Set` | Unordered set of non-equal values. |
72+
| `'timestamp'` | [`!!timestamp`](https://yaml.org/type/timestamp.html) | `Date` | A point in time, e.g. `2001-12-15T02:59:43`. |
7273

7374
## Writing Custom Tags
7475

src/compose/compose-doc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function composeDoc<
2828
const opts = Object.assign({ _directives: directives }, options)
2929
const doc = new Document(undefined, opts) as Document.Parsed<Contents, Strict>
3030
const ctx: ComposeContext = {
31+
atKey: false,
3132
atRoot: true,
3233
directives: doc.directives,
3334
options: doc.options,

src/compose/compose-node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { resolveEnd } from './resolve-end.js'
1111
import { emptyScalarPosition } from './util-empty-scalar-position.js'
1212

1313
export interface ComposeContext {
14+
atKey: boolean
1415
atRoot: boolean
1516
directives: Directives
1617
options: Readonly<Required<Omit<ParseOptions, 'lineCounter'>>>

src/compose/compose-scalar.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,16 @@ function findScalarTagByName(
8787
}
8888

8989
function findScalarTagByTest(
90-
{ directives, schema }: ComposeContext,
90+
{ atKey, directives, schema }: ComposeContext,
9191
value: string,
9292
token: FlowScalar,
9393
onError: ComposeErrorHandler
9494
) {
9595
const tag =
9696
(schema.tags.find(
97-
tag => tag.default && tag.test?.test(value)
97+
tag =>
98+
(tag.default === true || (atKey && tag.default === 'key')) &&
99+
tag.test?.test(value)
98100
) as ScalarTag) || schema[SCALAR]
99101

100102
if (schema.compat) {

src/compose/resolve-block-map.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ export function resolveBlockMap(
6969
}
7070

7171
// key value
72+
ctx.atKey = true
7273
const keyStart = keyProps.end
7374
const keyNode = key
7475
? composeNode(ctx, key, keyProps, onError)
7576
: composeEmptyNode(ctx, keyStart, start, null, keyProps, onError)
7677
if (ctx.schema.compat) flowIndentCheck(bm.indent, key, onError)
78+
ctx.atKey = false
7779

7880
if (mapIncludes(ctx, map.items, keyNode))
7981
onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique')

src/compose/resolve-block-seq.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function resolveBlockSeq(
1717
const seq = new NodeClass(ctx.schema) as YAMLSeq
1818

1919
if (ctx.atRoot) ctx.atRoot = false
20+
if (ctx.atKey) ctx.atKey = false
2021
let offset = bs.offset
2122
let commentEnd: number | null = null
2223
for (const { start, value } of bs.items) {

src/compose/resolve-flow-collection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function resolveFlowCollection(
3232
coll.flow = true
3333
const atRoot = ctx.atRoot
3434
if (atRoot) ctx.atRoot = false
35+
if (ctx.atKey) ctx.atKey = false
3536

3637
let offset = fc.offset + fc.start.source.length
3738
for (let i = 0; i < fc.items.length; ++i) {
@@ -118,11 +119,13 @@ export function resolveFlowCollection(
118119
// item is a key+value pair
119120

120121
// key value
122+
ctx.atKey = true
121123
const keyStart = props.end
122124
const keyNode = key
123125
? composeNode(ctx, key, props, onError)
124126
: composeEmptyNode(ctx, keyStart, start, null, props, onError)
125127
if (isBlock(key)) onError(keyNode.range, 'BLOCK_IN_FLOW', blockMsg)
128+
ctx.atKey = false
126129

127130
// value properties
128131
const valueProps = resolveProps(sep ?? [], {

src/compose/util-map-includes.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ export function mapIncludes(
1414
typeof uniqueKeys === 'function'
1515
? uniqueKeys
1616
: (a: ParsedNode, b: ParsedNode) =>
17-
a === b ||
18-
(isScalar(a) &&
19-
isScalar(b) &&
20-
a.value === b.value &&
21-
!(a.value === '<<' && ctx.schema.merge))
17+
a === b || (isScalar(a) && isScalar(b) && a.value === b.value)
2218
return items.some(pair => isEqual(pair.key, search))
2319
}

src/doc/Document.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,13 @@ export class Document<
383383
case '1.1':
384384
if (this.directives) this.directives.yaml.version = '1.1'
385385
else this.directives = new Directives({ version: '1.1' })
386-
opt = { merge: true, resolveKnownTags: false, schema: 'yaml-1.1' }
386+
opt = { resolveKnownTags: false, schema: 'yaml-1.1' }
387387
break
388388
case '1.2':
389389
case 'next':
390390
if (this.directives) this.directives.yaml.version = version
391391
else this.directives = new Directives({ version })
392-
opt = { merge: false, resolveKnownTags: true, schema: 'core' }
392+
opt = { resolveKnownTags: true, schema: 'core' }
393393
break
394394
case null:
395395
if (this.directives) delete this.directives

src/nodes/Node.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Alias } from './Alias.js'
77
import { isDocument, NODE_TYPE } from './identity.js'
88
import type { Scalar } from './Scalar.js'
99
import { toJS, ToJSContext } from './toJS.js'
10-
import type { YAMLMap } from './YAMLMap.js'
10+
import type { MapLike, YAMLMap } from './YAMLMap.js'
1111
import type { YAMLSeq } from './YAMLSeq.js'
1212

1313
export type Node<T = unknown> =
@@ -70,6 +70,16 @@ export abstract class NodeBase {
7070
/** A fully qualified tag, if required */
7171
declare tag?: string
7272

73+
/**
74+
* Customize the way that a key-value pair is resolved.
75+
* Used for YAML 1.1 !!merge << handling.
76+
*/
77+
declare addToJSMap?: (
78+
ctx: ToJSContext | undefined,
79+
map: MapLike,
80+
value: unknown
81+
) => void
82+
7383
/** A plain JS representation of this node */
7484
abstract toJSON(): any
7585

0 commit comments

Comments
 (0)