diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 06778380..7b18fb99 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -37,6 +37,7 @@ export type Replacer = any[] | ((key: any, value: any) => unknown) export declare namespace Document { interface Parsed extends Document { + directives: Directives range: Range } } @@ -53,7 +54,7 @@ export class Document { /** The document contents. */ contents: T | null - directives: Directives + directives?: Directives /** Errors encountered during parsing. */ errors: YAMLError[] = [] @@ -139,7 +140,7 @@ export class Document { copy.errors = this.errors.slice() copy.warnings = this.warnings.slice() copy.options = Object.assign({}, this.options) - copy.directives = this.directives.clone() + if (this.directives) copy.directives = this.directives.clone() copy.schema = this.schema.clone() copy.contents = isNode(this.contents) ? (this.contents.clone(copy.schema) as unknown as T) @@ -346,32 +347,45 @@ export class Document { /** * Change the YAML version and schema used by the document. + * A `null` version disables support for directives, explicit tags, anchors, and aliases. + * It also requires the `schema` option to be given as a `Schema` instance value. * - * Overrides all previously set schema options + * Overrides all previously set schema options. */ - setSchema(version: '1.1' | '1.2', options?: SchemaOptions) { - let _options: SchemaOptions - switch (String(version)) { + setSchema(version: '1.1' | '1.2' | null, options: SchemaOptions = {}) { + if (typeof version === 'number') version = String(version) as '1.1' | '1.2' + + let opt: (SchemaOptions & { schema: string }) | null + switch (version) { case '1.1': - this.directives.yaml.version = '1.1' - _options = Object.assign( - { merge: true, resolveKnownTags: false, schema: 'yaml-1.1' }, - options - ) + if (this.directives) this.directives.yaml.version = '1.1' + else this.directives = new Directives({ version: '1.1' }) + opt = { merge: true, resolveKnownTags: false, schema: 'yaml-1.1' } break case '1.2': - this.directives.yaml.version = '1.2' - _options = Object.assign( - { merge: false, resolveKnownTags: true, schema: 'core' }, - options - ) + if (this.directives) this.directives.yaml.version = '1.2' + else this.directives = new Directives({ version: '1.2' }) + opt = { merge: false, resolveKnownTags: true, schema: 'core' } + break + case null: + if (this.directives) delete this.directives + opt = null break default: { const sv = JSON.stringify(version) - throw new Error(`Expected '1.1' or '1.2' as version, but found: ${sv}`) + throw new Error( + `Expected '1.1', '1.2' or null as first argument, but found: ${sv}` + ) } } - this.schema = new Schema(_options) + + // Not using `instanceof Schema` to allow for duck typing + if (options.schema instanceof Object) this.schema = options.schema + else if (opt) this.schema = new Schema(Object.assign(opt, options)) + else + throw new Error( + `With a null YAML version, the { schema: Schema } option is required` + ) } /** A plain JavaScript representation of the document `contents`. */ diff --git a/src/options.ts b/src/options.ts index 16eb6389..9242c09d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,6 +5,7 @@ import type { ParsedNode } from './nodes/Node.js' import type { Pair } from './nodes/Pair.js' import type { Scalar } from './nodes/Scalar.js' import type { LineCounter } from './parse/line-counter.js' +import type { Schema } from './schema/Schema.js' import type { Tags } from './schema/tags.js' import type { CollectionTag, ScalarTag } from './schema/types.js' @@ -131,7 +132,7 @@ export type SchemaOptions = { * * Default: `'core'` for YAML 1.2, `'yaml-1.1'` for earlier versions */ - schema?: string + schema?: string | Schema /** * When adding to or stringifying a map, sort the entries. diff --git a/src/public-api.ts b/src/public-api.ts index e6e2206d..48a85c4f 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -21,12 +21,10 @@ export interface EmptyStream empty: true } -function parseOptions(options: ParseOptions | undefined) { - const prettyErrors = !options || options.prettyErrors !== false +function parseOptions(options: ParseOptions) { + const prettyErrors = options.prettyErrors !== false const lineCounter = - (options && options.lineCounter) || - (prettyErrors && new LineCounter()) || - null + options.lineCounter || (prettyErrors && new LineCounter()) || null return { lineCounter, prettyErrors } } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index 26bba256..85df993f 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -39,7 +39,7 @@ export class Schema { ? getTags(null, compat) : null this.merge = !!merge - this.name = schema || 'core' + this.name = (typeof schema === 'string' && schema) || 'core' this.knownTags = resolveKnownTags ? coreKnownTags : {} this.tags = getTags(customTags, this.name) this.toStringOptions = toStringDefaults || null diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 0f204b52..7417d8e3 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,5 +1,6 @@ import { anchorIsValid } from '../doc/anchors.js' import type { Document } from '../doc/Document.js' +import type { Alias } from '../nodes/Alias.js' import { isAlias, isCollection, @@ -29,6 +30,7 @@ export type StringifyContext = { options: Readonly< Required> > + resolvedAliases?: Set } export function createStringifyContext( @@ -113,17 +115,15 @@ function stringifyProps( tagObj: ScalarTag | CollectionTag, { anchors, doc }: StringifyContext ) { + if (!doc.directives) return '' const props = [] const anchor = (isScalar(node) || isCollection(node)) && node.anchor if (anchor && anchorIsValid(anchor)) { anchors.add(anchor) props.push(`&${anchor}`) } - if (node.tag) { - props.push(doc.directives.tagString(node.tag)) - } else if (!tagObj.default) { - props.push(doc.directives.tagString(tagObj.tag)) - } + const tag = node.tag || (tagObj.default ? null : tagObj.tag) + if (tag) props.push(doc.directives.tagString(tag)) return props.join(' ') } @@ -134,7 +134,19 @@ export function stringify( onChompKeep?: () => void ): string { if (isPair(item)) return item.toString(ctx, onComment, onChompKeep) - if (isAlias(item)) return item.toString(ctx) + if (isAlias(item)) { + if (ctx.doc.directives) return item.toString(ctx) + + if (ctx.resolvedAliases?.has(item)) { + throw new TypeError( + `Cannot stringify circular structure without alias nodes` + ) + } else { + if (ctx.resolvedAliases) ctx.resolvedAliases.add(item) + else ctx.resolvedAliases = new Set([item]) + item = item.resolve(ctx.doc) + } + } let tagObj: ScalarTag | CollectionTag | undefined = undefined const node = isNode(item) diff --git a/src/stringify/stringifyDocument.ts b/src/stringify/stringifyDocument.ts index ebcf4e4a..abe0358b 100644 --- a/src/stringify/stringifyDocument.ts +++ b/src/stringify/stringifyDocument.ts @@ -14,7 +14,7 @@ export function stringifyDocument( ) { const lines: string[] = [] let hasDirectives = options.directives === true - if (options.directives !== false) { + if (options.directives !== false && doc.directives) { const dir = doc.directives.toString(doc) if (dir) { lines.push(dir) diff --git a/tests/doc/types.js b/tests/doc/types.js index ffb6eb1b..69cf9efa 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -775,7 +775,7 @@ describe('custom tags', () => { expect(str).toBe('re: !re /re/g\nsymbol: !symbol/shared foo\n') }) - describe('completely custom schema', () => { + describe('schema from custom tags', () => { test('customTags is required', () => { expect(() => YAML.parseDocument('foo', { schema: 'custom-test' }) @@ -836,6 +836,23 @@ describe('custom tags', () => { }) expect(() => String(doc)).toThrow(/Tag not resolved for String value/) }) + + test('setSchema', () => { + const src = '- foo\n' + const doc = YAML.parseDocument(src) + + doc.setSchema('1.2', { + customTags: [seqTag, stringTag], + schema: 'custom-test-1' + }) + expect(doc.toString()).toBe(src) + + doc.setSchema('1.2', { + customTags: [stringTag], + schema: 'custom-test-2' + }) + expect(() => String(doc)).toThrow(/Tag not resolved for YAMLSeq value/) + }) }) }) @@ -904,4 +921,30 @@ describe('schema changes', () => { doc.set('a', false) expect(String(doc)).toBe('a: false\n') }) + + test('custom schema instance', () => { + const src = '[ !!bool yes, &foo no, *foo ]' + const doc = YAML.parseDocument(src, { version: '1.1' }) + + doc.setSchema('1.2', new YAML.Schema({ schema: 'core' })) + expect(String(doc)).toBe('[ !!bool true, &foo false, *foo ]\n') + + const yaml11 = new YAML.Schema({ schema: 'yaml-1.1' }) + doc.setSchema('1.1', Object.assign({}, yaml11)) + expect(String(doc)).toBe('[ !!bool yes, &foo no, *foo ]\n') + + const schema = new YAML.Schema({ schema: 'core' }) + doc.setSchema(null, { schema }) + expect(String(doc)).toBe('[ true, false, false ]\n') + }) + + test('null version requires Schema instance', () => { + const doc = YAML.parseDocument('foo: bar') + const msg = + 'With a null YAML version, the { schema: Schema } option is required' + expect(() => doc.setSchema(null)).toThrow(msg) + expect(() => doc.setSchema(null, { schema: 'core' })).toThrow(msg) + doc.setSchema(null, { schema: doc.schema }) + expect(doc.toString()).toBe('foo: bar\n') + }) })