From ba8413a3cea6940a548ef300865c2b5e3edb4248 Mon Sep 17 00:00:00 2001 From: isaacs Date: Fri, 21 Apr 2023 16:32:31 -0700 Subject: [PATCH] add collection examples to custom tag docs --- docs/06_custom_tags.md | 90 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/docs/06_custom_tags.md b/docs/06_custom_tags.md index c7c6425c..15bbe6a8 100644 --- a/docs/06_custom_tags.md +++ b/docs/06_custom_tags.md @@ -65,7 +65,7 @@ These tags are a part of the YAML 1.1 [language-independent types](https://yaml. ## Writing Custom Tags ```js -import { stringify } from 'yaml' +import { YAMLMap, stringify } from 'yaml' import { stringifyString } from 'yaml/util' const regexp = { @@ -89,18 +89,102 @@ const sharedSymbol = { } } +class YAMLNullObject extends YAMLMap { + tag = '!nullobject' + toJSON(_, ctx) { + const obj = super.toJSON(_, { ...ctx, mapAsMap: false }, Object) + return Object.assign(Object.create(null), obj) + } +} + +const nullObject = { + tag: '!nullobject', + collection: 'map', + nodeClass: YAMLNullObject, + identify: v => !!( + typeof v === 'object' && + v && + !Object.getPrototypeOf(v) + ) +} + +// slightly more complicated object type +class YAMLError extends YAMLMap { + tag = '!error' + toJSON(_, ctx) { + const { name, message, stack, ...rest } = super.toJSON(_, { + ...ctx, + mapAsMap: false, + }, Object) + // craft the appropriate error type + const Cls = + name === 'EvalError' ? EvalError + : name === 'RangeError' ? RangeError + : name === 'ReferenceError' ? ReferenceError + : name === 'SyntaxError' ? SyntaxError + : name === 'TypeError' ? TypeError + : name === 'URIError' ? URIError + : Error + if (Cls.name !== name) { + Object.defineProperty(er, 'name', { + value: name, + enumerable: false, + configurable: true, + }) + } + Object.defineProperty(er, 'stack', { + value: stack, + enumerable: false, + configurable: true, + }) + 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, + identify: v => !!( + typeof v === 'object' && + v && + v instanceof Error + ) +} + stringify( - { regexp: /foo/gi, symbol: Symbol.for('bar') }, - { customTags: [regexp, sharedSymbol] } + { + regexp: /foo/gi, + symbol: Symbol.for('bar'), + nullobj: Object.assign(Object.create(null), { a: 1, b: 2 }), + error: new Error('This was an error'), + }, + { customTags: [regexp, sharedSymbol, nullObject, error] } ) // regexp: !re /foo/gi // symbol: !symbol/shared bar +// nullobj: !nullobject +// a: 1 +// b: 2 +// error: !error +// name: Error +// message: 'This was an error' +// stack: | +// at some-file.js:1:3 ``` In YAML-speak, a custom data type is represented by a _tag_. To define your own tag, you need to account for the ways that your data is both parsed and stringified. Furthermore, both of those processes are split into two stages by the intermediate AST node structure. If you wish to implement your own custom tags, the [`!!binary`](https://github.com/eemeli/yaml/blob/main/src/schema/yaml-1.1/binary.ts) and [`!!set`](https://github.com/eemeli/yaml/blob/main/src/schema/yaml-1.1/set.ts) tags provide relatively cohesive examples to study in addition to the simple examples in the sidebar here. +Custom collection types (ie, Maps, Sets, objects, and arrays; anything with child properties that may not be propertly serialized to a scalar value) may provide a `nodeClass` property that extends the [`YAMLMap`](https://github.com/eemeli/yaml/blob/main/src/nodes/YAMLMap.ts) and [`YAMLSeq`](https://github.com/eemeli/yaml/blob/main/src/nodes/YAMLSeq.ts) classes, which will be used for parsing and stringifying objects with the specified tag. + ### Parsing Custom Data At the lowest level, the [`Lexer`](#lexer) and [`Parser`](#parser) will take care of turning string input into a concrete syntax tree (CST).