From cfc42ad926ca23b5036200f06858eade44a2ba3d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 21:45:11 +0200 Subject: [PATCH 01/20] Add options argument to doc.toString() --- src/doc/Document.ts | 62 ++++++++++++++++++++++++++++++-------- src/nodes/Pair.ts | 17 +++++++++-- src/options.ts | 25 +++------------ src/stringify/stringify.ts | 2 ++ 4 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index ec12a8ff..47138c3d 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -55,9 +55,53 @@ export interface CreateNodeOptions { export interface ToJSOptions { json?: boolean jsonArg?: string | null + + /** + * Use Map rather than Object to represent mappings. + * + * Default: `false` + */ mapAsMap?: boolean + + /** + * If defined, called with the resolved `value` and reference `count` for + * each anchor in the document. + */ onAnchor?: (value: unknown, count: number) => void + + /** + * Optional function that may filter or modify the output JS value + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter + */ reviver?: Reviver + + [key: string]: unknown +} + +export interface ToStringOptions { + /** + * The number of spaces to use when indenting code. + * + * Default: `2` + */ + indent?: number + + /** + * Whether block sequences should be indented. + * + * Default: `true` + */ + indentSeq?: boolean + + /** + * Require keys to be scalars and to use implicit rather than explicit notation. + * + * Default: `false` + */ + simpleKeys?: boolean + + [key: string]: unknown } export declare namespace Document { @@ -100,7 +144,7 @@ export class Document { /** Errors encountered during parsing. */ errors: YAMLError[] = [] - options: Required & SchemaOptions + options: Required & SchemaOptions /** The schema used with the document. Use `setSchema()` to change. */ schema: Schema @@ -374,15 +418,7 @@ export class Document { } } - /** - * A plain JavaScript representation of the document `contents`. - * - * @param mapAsMap - Use Map rather than Object to represent mappings. - * Overrides values set in Document or global options. - * @param onAnchor - If defined, called with the resolved `value` and - * reference `count` for each anchor in the document. - * @param reviver - A function that may filter or modify the output JS value - */ + /** A plain JavaScript representation of the document `contents`. */ toJS({ json, jsonArg, mapAsMap, onAnchor, reviver }: ToJSOptions = {}) { const anchorNodes = Object.values(this.anchors.map).map( node => @@ -422,10 +458,10 @@ export class Document { } /** A YAML representation of the document. */ - toString() { + toString({ indent, indentSeq, simpleKeys }: ToStringOptions = {}) { if (this.errors.length > 0) throw new Error('Document with errors cannot be stringified') - const indentSize = this.options.indent + const indentSize = indent ?? this.options.indent if (!Number.isInteger(indentSize) || indentSize <= 0) { const s = JSON.stringify(indentSize) throw new Error(`"indent" option must be a positive integer, not ${s}`) @@ -446,7 +482,9 @@ export class Document { anchors: Object.create(null), doc: this, indent: '', + indentSeq: indentSeq ?? this.options.indentSeq, indentStep: ' '.repeat(indentSize), + simpleKeys: simpleKeys ?? this.options.simpleKeys, stringify // Requiring directly in nodes would create circular dependencies } let chompKeep = false diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index d38978cb..9f619764 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -116,7 +116,15 @@ export class Pair extends NodeBase { onChompKeep?: () => void ) { if (!ctx || !ctx.doc) return JSON.stringify(this) - const { indent: indentSize, indentSeq, simpleKeys } = ctx.doc.options + const { + allNullValues, + doc, + indent, + indentSeq, + indentStep, + simpleKeys, + stringify + } = ctx let { key, value }: { key: K; value: V | Node | null } = this let keyComment = (isNode(key) && key.comment) || null if (simpleKeys) { @@ -136,7 +144,7 @@ export class Pair extends NodeBase { (isScalar(key) ? key.type === Type.BLOCK_FOLDED || key.type === Type.BLOCK_LITERAL : typeof key === 'object')) - const { allNullValues, doc, indent, indentStep, stringify } = ctx + ctx = Object.assign({}, ctx, { allNullValues: false, implicitKey: !explicitKey && (simpleKeys || !allNullValues), @@ -149,6 +157,7 @@ export class Pair extends NodeBase { () => (keyComment = null), () => (chompKeep = true) ) + if (!explicitKey && !ctx.inFlow && str.length > 1024) { if (simpleKeys) throw new Error( @@ -199,7 +208,7 @@ export class Pair extends NodeBase { chompKeep = false if ( !indentSeq && - indentSize >= 2 && + indentStep.length >= 2 && !ctx.inFlow && !explicitKey && isSeq(value) && @@ -240,9 +249,11 @@ function stringifyKey( anchors: Object.create(null), doc: ctx.doc, indent: '', + indentSeq: false, indentStep: ctx.indentStep, inFlow: true, inStringifyKey: true, + simpleKeys: false, stringify: ctx.stringify }) if (!ctx.mapKeyWarned) { diff --git a/src/options.ts b/src/options.ts index 50476df3..8bfe56d0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,4 +1,5 @@ import { LogLevelId, defaultTagPrefix } from './constants.js' +import { ToStringOptions } from './doc/Document.js' import type { SchemaOptions } from './doc/Schema.js' import type { LineCounter } from './parse/line-counter.js' import { @@ -16,18 +17,7 @@ export interface DocumentOptions { * Default: `'a'`, resulting in anchors `a1`, `a2`, etc. */ anchorPrefix?: string - /** - * The number of spaces to use when indenting code. - * - * Default: `2` - */ - indent?: number - /** - * Whether block sequences should be indented. - * - * Default: `true` - */ - indentSeq?: boolean + /** * Include references in the AST to each node's corresponding CST node. * @@ -80,12 +70,7 @@ export interface DocumentOptions { * Default: `true` */ prettyErrors?: boolean - /** - * When stringifying, require keys to be scalars and to use implicit rather than explicit notation. - * - * Default: `false` - */ - simpleKeys?: boolean + /** * When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. * @@ -100,7 +85,7 @@ export interface DocumentOptions { version?: '1.1' | '1.2' } -export type Options = DocumentOptions & SchemaOptions +export type Options = DocumentOptions & SchemaOptions & ToStringOptions /** * `yaml` defines document-specific options in three places: as an argument of @@ -109,7 +94,7 @@ export type Options = DocumentOptions & SchemaOptions * `YAML.defaultOptions` override version-dependent defaults, and argument * options override both. */ -export const defaultOptions: Required = { +export const defaultOptions: Required = { anchorPrefix: 'a', indent: 2, indentSeq: true, diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 8aebfd09..2a348b26 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -10,9 +10,11 @@ export interface StringifyContext { forceBlockIndent?: boolean implicitKey?: boolean indent: string + indentSeq: boolean indentStep: string indentAtStart?: number inFlow?: boolean + simpleKeys: boolean stringify: typeof stringify [key: string]: unknown } From ebd65feb1d11e86f982beff331e9863b75a04042 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 11:07:58 +0200 Subject: [PATCH 02/20] Collect external option interfaces in src/options.ts: SchemaOptions, CreateNodeOptions, ToJSOptions --- src/doc/Document.ts | 99 +++++++------------------------- src/doc/Schema.ts | 49 +--------------- src/doc/getSchemaTags.ts | 8 +-- src/index.ts | 12 +++- src/options.ts | 119 ++++++++++++++++++++++++++++++++++++++- src/public-api.ts | 7 ++- src/tags/types.ts | 2 + 7 files changed, 160 insertions(+), 136 deletions(-) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 47138c3d..46c018eb 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -16,93 +16,25 @@ import { toJS, ToJSAnchorValue, ToJSContext } from '../nodes/toJS.js' import type { YAMLMap } from '../nodes/YAMLMap.js' import type { YAMLSeq } from '../nodes/YAMLSeq.js' import { + CreateNodeOptions, + defaultOptions, + documentOptions, DocumentOptions, Options, - defaultOptions, - documentOptions + SchemaOptions, + ToJSOptions, + ToStringOptions } from '../options.js' import { addComment } from '../stringify/addComment.js' import { stringify, StringifyContext } from '../stringify/stringify.js' -import type { TagId, TagObj } from '../tags/types.js' +import type { TagValue } from '../tags/types.js' import { Anchors } from './Anchors.js' -import { Schema, SchemaName, SchemaOptions } from './Schema.js' -import { Reviver, applyReviver } from './applyReviver.js' +import { Schema, SchemaName } from './Schema.js' +import { applyReviver } from './applyReviver.js' import { createNode, CreateNodeContext } from './createNode.js' import { Directives } from './directives.js' export type Replacer = any[] | ((key: any, value: any) => unknown) -export type { Anchors, Reviver } - -export interface CreateNodeOptions { - keepUndefined?: boolean | null - - onTagObj?: (tagObj: TagObj) => void - - /** - * Filter or modify values while creating a node. - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter - */ - replacer?: Replacer - - /** - * Specify the collection type, e.g. `"!!omap"`. Note that this requires the - * corresponding tag to be available in this document's schema. - */ - tag?: string -} - -export interface ToJSOptions { - json?: boolean - jsonArg?: string | null - - /** - * Use Map rather than Object to represent mappings. - * - * Default: `false` - */ - mapAsMap?: boolean - - /** - * If defined, called with the resolved `value` and reference `count` for - * each anchor in the document. - */ - onAnchor?: (value: unknown, count: number) => void - - /** - * Optional function that may filter or modify the output JS value - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter - */ - reviver?: Reviver - - [key: string]: unknown -} - -export interface ToStringOptions { - /** - * The number of spaces to use when indenting code. - * - * Default: `2` - */ - indent?: number - - /** - * Whether block sequences should be indented. - * - * Default: `true` - */ - indentSeq?: boolean - - /** - * Require keys to be scalars and to use implicit rather than explicit notation. - * - * Default: `false` - */ - simpleKeys?: boolean - - [key: string]: unknown -} export declare namespace Document { interface Parsed extends Document { @@ -387,7 +319,7 @@ export class Document { */ setSchema( id: Options['version'] | SchemaName | null, - customTags?: (TagId | TagObj)[] + customTags?: TagValue[] ) { if (!id && !customTags) return @@ -419,7 +351,16 @@ export class Document { } /** A plain JavaScript representation of the document `contents`. */ - toJS({ json, jsonArg, mapAsMap, onAnchor, reviver }: ToJSOptions = {}) { + toJS(opt?: ToJSOptions): any + + // json & jsonArg are only used from toJSON() + toJS({ + json, + jsonArg, + mapAsMap, + onAnchor, + reviver + }: ToJSOptions & { json?: boolean; jsonArg?: string | null } = {}) { const anchorNodes = Object.values(this.anchors.map).map( node => [node, { alias: [], aliasCount: 0, count: 1 }] as [ diff --git a/src/doc/Schema.ts b/src/doc/Schema.ts index d4f22e79..d9386dd1 100644 --- a/src/doc/Schema.ts +++ b/src/doc/Schema.ts @@ -1,54 +1,11 @@ import type { Pair } from '../nodes/Pair.js' +import type { SchemaOptions } from '../options.js' import { schemas, tags } from '../tags/index.js' -import type { CollectionTag, ScalarTag, TagId, TagObj } from '../tags/types.js' -import { Directives } from './directives.js' +import type { CollectionTag, ScalarTag } from '../tags/types.js' import { getSchemaTags } from './getSchemaTags.js' export type SchemaName = 'core' | 'failsafe' | 'json' | 'yaml-1.1' -type TagValue = TagId | ScalarTag | CollectionTag - -export interface SchemaOptions { - /** - * Array of additional tags to include in the schema, or a function that may - * modify the schema's base tag array. - */ - customTags?: TagValue[] | ((tags: TagValue[]) => TagValue[]) | null - - directives?: Directives - - /** - * Enable support for `<<` merge keys. - * - * Default: `false` for YAML 1.2, `true` for earlier versions - */ - merge?: boolean - - /** - * When using the `'core'` schema, support parsing values with these - * explicit YAML 1.1 tags: - * - * `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. - * - * Default `true` - */ - resolveKnownTags?: boolean - - /** - * The base schema to use. - * - * Default: `"core"` for YAML 1.2, `"yaml-1.1"` for earlier versions - */ - schema?: SchemaName - - /** - * When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. - * - * Default: `false` - */ - sortMapEntries?: boolean | ((a: Pair, b: Pair) => number) -} - const sortMapEntriesByKey = (a: Pair, b: Pair) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0 @@ -61,7 +18,7 @@ const coreKnownTags = { } export class Schema { - knownTags: Record + knownTags: Record merge: boolean name: SchemaName sortMapEntries: ((a: Pair, b: Pair) => number) | null diff --git a/src/doc/getSchemaTags.ts b/src/doc/getSchemaTags.ts index 93d65d06..6dde5ad9 100644 --- a/src/doc/getSchemaTags.ts +++ b/src/doc/getSchemaTags.ts @@ -1,18 +1,18 @@ -import type { SchemaId, TagId, TagObj } from '../tags/types.js' +import type { SchemaId, TagId, TagObj, TagValue } from '../tags/types.js' import type { SchemaName } from './Schema.js' export function getSchemaTags( schemas: Record, knownTags: Record, customTags: - | Array - | ((tags: Array) => Array) + | TagValue[] + | ((tags: TagValue[]) => TagValue[]) | null | undefined, schemaName: SchemaName ) { const schemaId = schemaName.replace(/\W/g, '') as SchemaId // 'yaml-1.1' -> 'yaml11' - let tags: Array = schemas[schemaId] + let tags: TagValue[] = schemas[schemaId] if (!tags) { const keys = Object.keys(schemas) .map(key => JSON.stringify(key)) diff --git a/src/index.ts b/src/index.ts index d20de809..6a54c1e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export { Composer } from './compose/composer.js' export { Type } from './constants.js' -export { CreateNodeOptions, Document } from './doc/Document.js' +export { Document } from './doc/Document.js' export { Schema } from './doc/Schema.js' export { YAMLError, YAMLParseError, YAMLWarning } from './errors.js' @@ -25,7 +25,15 @@ export { Scalar } from './nodes/Scalar.js' export { YAMLMap } from './nodes/YAMLMap.js' export { YAMLSeq } from './nodes/YAMLSeq.js' -export { Options, defaultOptions, scalarOptions } from './options.js' +export { + CreateNodeOptions, + defaultOptions, + Options, + scalarOptions, + SchemaOptions, + ToJSOptions, + ToStringOptions +} from './options.js' export { Lexer } from './parse/lexer.js' export { LineCounter } from './parse/line-counter.js' diff --git a/src/options.ts b/src/options.ts index 8bfe56d0..e6bb36c0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,6 +1,9 @@ import { LogLevelId, defaultTagPrefix } from './constants.js' -import { ToStringOptions } from './doc/Document.js' -import type { SchemaOptions } from './doc/Schema.js' +import type { Reviver } from './doc/applyReviver.js' +import type { Directives } from './doc/directives.js' +import type { Replacer } from './doc/Document.js' +import type { SchemaName } from './doc/Schema.js' +import type { Pair } from './nodes/Pair.js' import type { LineCounter } from './parse/line-counter.js' import { binaryOptions, @@ -9,6 +12,7 @@ import { nullOptions, strOptions } from './tags/options.js' +import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' export interface DocumentOptions { /** @@ -85,6 +89,117 @@ export interface DocumentOptions { version?: '1.1' | '1.2' } +export interface SchemaOptions { + /** + * Array of additional tags to include in the schema, or a function that may + * modify the schema's base tag array. + */ + customTags?: TagValue[] | ((tags: TagValue[]) => TagValue[]) | null + + directives?: Directives + + /** + * Enable support for `<<` merge keys. + * + * Default: `false` for YAML 1.2, `true` for earlier versions + */ + merge?: boolean + + /** + * When using the `'core'` schema, support parsing values with these + * explicit YAML 1.1 tags: + * + * `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. + * + * Default `true` + */ + resolveKnownTags?: boolean + + /** + * The base schema to use. + * + * Default: `"core"` for YAML 1.2, `"yaml-1.1"` for earlier versions + */ + schema?: SchemaName + + /** + * When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. + * + * Default: `false` + */ + sortMapEntries?: boolean | ((a: Pair, b: Pair) => number) +} + +export interface CreateNodeOptions { + keepUndefined?: boolean | null + + onTagObj?: (tagObj: ScalarTag | CollectionTag) => void + + /** + * Filter or modify values while creating a node. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter + */ + replacer?: Replacer + + /** + * Specify the collection type, e.g. `"!!omap"`. Note that this requires the + * corresponding tag to be available in this document's schema. + */ + tag?: string + + [key: string]: unknown +} + +export interface ToJSOptions { + /** + * Use Map rather than Object to represent mappings. + * + * Default: `false` + */ + mapAsMap?: boolean + + /** + * If defined, called with the resolved `value` and reference `count` for + * each anchor in the document. + */ + onAnchor?: (value: unknown, count: number) => void + + /** + * Optional function that may filter or modify the output JS value + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter + */ + reviver?: Reviver + + [key: string]: unknown +} + +export interface ToStringOptions { + /** + * The number of spaces to use when indenting code. + * + * Default: `2` + */ + indent?: number + + /** + * Whether block sequences should be indented. + * + * Default: `true` + */ + indentSeq?: boolean + + /** + * Require keys to be scalars and to use implicit rather than explicit notation. + * + * Default: `false` + */ + simpleKeys?: boolean + + [key: string]: unknown +} + export type Options = DocumentOptions & SchemaOptions & ToStringOptions /** diff --git a/src/public-api.ts b/src/public-api.ts index 543d09f7..f87c9264 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -1,10 +1,11 @@ import { Composer } from './compose/composer.js' import { LogLevel } from './constants.js' -import { Document, Replacer, Reviver } from './doc/Document.js' +import type { Reviver } from './doc/applyReviver.js' +import { Document, Replacer } from './doc/Document.js' import { YAMLParseError } from './errors.js' import { warn } from './log.js' -import { ParsedNode } from './nodes/Node.js' -import { Options } from './options.js' +import type { ParsedNode } from './nodes/Node.js' +import type { Options } from './options.js' import { Parser } from './parse/parser.js' export interface EmptyStream diff --git a/src/tags/types.ts b/src/tags/types.ts index e6b7ea0d..6fe83873 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -122,3 +122,5 @@ export interface CollectionTag extends TagBase { } export type TagObj = ScalarTag | CollectionTag + +export type TagValue = TagId | ScalarTag | CollectionTag From 3a984dedb2e5e9f6c33e1c94c696e71ade55d8ec Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 11:14:37 +0200 Subject: [PATCH 03/20] Drop the keepCstNodes and keepNodeTypes options BREAKING CHANGE: The earlier parser change has meant that information is retained differently during composition. Effectively, the values of these options are no longer used, so they should be explicitly dropped from the options interfaces to make that clear. The behaviour now is as if `{ keepCstNodes: false, keepNodeTypes: true }` is always set. This may require some API changes for CST users, as errors also no longer provide a reference to the source CST node, just the original line position. To match an error to a node, you should search for a node that includes the error position in its range. To discard node type information from the tree, use a visitor: import { isNode, parseDocument, visit } from 'yaml' const doc = parseDocument('{ foo: "bar" }') visit(doc, (key, node) => { if (isNode(node)) delete node.type }) String(doc) === 'foo: bar\n' --- docs/03_options.md | 3 --- src/options.ts | 14 -------------- src/test-events.ts | 2 +- tests/properties.ts | 2 -- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index aadf1635..4c15615f 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -3,7 +3,6 @@ ```js YAML.defaultOptions // { indent: 2, -// keepNodeTypes: true, // mapAsMap: false, // version: '1.2' } @@ -27,8 +26,6 @@ The `version` option value (`'1.2'` by default) may be overridden by any documen | customTags | `Tag[] ⎮ function` | Array of [additional tags](#custom-data-types) to include in the schema | | indent | `number` | The number of spaces to use when indenting code. By default `2`. | | indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | -| keepCstNodes | `boolean` | Include references in the AST to each node's corresponding CST node. By default `false`. | -| keepNodeTypes | `boolean` | Store the original node type when parsing documents. By default `true`. | | keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | | logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | | mapAsMap | `boolean` | When outputting JS, use Map rather than Object to represent mappings. By default `false`. | diff --git a/src/options.ts b/src/options.ts index e6bb36c0..c08b43c1 100644 --- a/src/options.ts +++ b/src/options.ts @@ -22,18 +22,6 @@ export interface DocumentOptions { */ anchorPrefix?: string - /** - * Include references in the AST to each node's corresponding CST node. - * - * Default: `false` - */ - keepCstNodes?: boolean - /** - * Store the original node type when parsing documents. - * - * Default: `true` - */ - keepNodeTypes?: boolean /** * Keep `undefined` object values when creating mappings and return a Scalar * node when calling `YAML.stringify(undefined)`, rather than `undefined`. @@ -213,8 +201,6 @@ export const defaultOptions: Required = { anchorPrefix: 'a', indent: 2, indentSeq: true, - keepCstNodes: false, - keepNodeTypes: true, keepUndefined: false, lineCounter: null, logLevel: 'warn', diff --git a/src/test-events.ts b/src/test-events.ts index 607c8e80..5e837a0f 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -7,7 +7,7 @@ import { parseAllDocuments } from './public-api.js' // test harness for yaml-test-suite event tests export function testEvents(src: string, options?: Options) { - const opt = Object.assign({ keepNodeTypes: true, version: '1.2' }, options) + const opt = Object.assign({ version: '1.2' }, options) const docs = parseAllDocuments(src, opt) const errDoc = docs.find(doc => doc.errors.length > 0) const error = errDoc ? errDoc.errors[0].message : null diff --git a/tests/properties.ts b/tests/properties.ts index b25e072c..9950f964 100644 --- a/tests/properties.ts +++ b/tests/properties.ts @@ -16,8 +16,6 @@ describe('properties', () => { const yamlArbitrary = fc.anything({ key: key, values: values }) const optionsArbitrary = fc.record( { - keepCstNodes: fc.boolean(), - keepNodeTypes: fc.boolean(), mapAsMap: fc.constant(false), merge: fc.boolean(), schema: fc.constantFrom<('core' | 'yaml-1.1')[]>('core', 'yaml-1.1') // ignore 'failsafe', 'json' From 44d35939c7d8c980a40c172f3d0826259b01ee52 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 14:32:49 +0200 Subject: [PATCH 04/20] Split ParseOptions from DocumentOptions; drop { [key: string]: unknown } from interfaces --- src/doc/Document.ts | 6 ++++-- src/options.ts | 52 ++++++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 46c018eb..dc3bcf8e 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -21,6 +21,7 @@ import { documentOptions, DocumentOptions, Options, + ParseOptions, SchemaOptions, ToJSOptions, ToStringOptions @@ -76,7 +77,8 @@ export class Document { /** Errors encountered during parsing. */ errors: YAMLError[] = [] - options: Required & SchemaOptions + options: Required & + SchemaOptions /** The schema used with the document. Use `setSchema()` to change. */ schema: Schema @@ -351,7 +353,7 @@ export class Document { } /** A plain JavaScript representation of the document `contents`. */ - toJS(opt?: ToJSOptions): any + toJS(opt?: ToJSOptions & { [ignored: string]: unknown }): any // json & jsonArg are only used from toJSON() toJS({ diff --git a/src/options.ts b/src/options.ts index c08b43c1..122278af 100644 --- a/src/options.ts +++ b/src/options.ts @@ -14,6 +14,29 @@ import { } from './tags/options.js' import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' +export interface ParseOptions { + /** + * If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` + * to provide the `{ line, col }` positions within the input. + */ + lineCounter?: LineCounter | null + + /** + * Include line/col position & node type directly in parse errors. + * + * Default: `true` + */ + prettyErrors?: boolean + + /** + * Detect and report errors that are required by the YAML 1.2 spec, + * but are caused by unambiguous content. + * + * Default: `true` + */ + strict?: boolean +} + export interface DocumentOptions { /** * Default prefix for anchors. @@ -30,13 +53,6 @@ export interface DocumentOptions { */ keepUndefined?: boolean - /** - * If set, newlines will be tracked while parsing, to allow for - * `lineCounter.linePos(offset)` to provide the `{ line, col }` positions - * within the input. - */ - lineCounter?: LineCounter | null - /** * Control the logging level during parsing * @@ -56,19 +72,7 @@ export interface DocumentOptions { * Default: `100` */ maxAliasCount?: number - /** - * Include line position & node type directly in errors; drop their verbose source and context. - * - * Default: `true` - */ - prettyErrors?: boolean - /** - * When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. - * - * Default: `true` - */ - strict?: boolean /** * The YAML version used by documents without a `%YAML` directive. * @@ -135,8 +139,6 @@ export interface CreateNodeOptions { * corresponding tag to be available in this document's schema. */ tag?: string - - [key: string]: unknown } export interface ToJSOptions { @@ -159,8 +161,6 @@ export interface ToJSOptions { * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter */ reviver?: Reviver - - [key: string]: unknown } export interface ToStringOptions { @@ -184,11 +184,9 @@ export interface ToStringOptions { * Default: `false` */ simpleKeys?: boolean - - [key: string]: unknown } -export type Options = DocumentOptions & SchemaOptions & ToStringOptions +export type Options = ParseOptions & DocumentOptions & SchemaOptions & ToStringOptions /** * `yaml` defines document-specific options in three places: as an argument of @@ -197,7 +195,7 @@ export type Options = DocumentOptions & SchemaOptions & ToStringOptions * `YAML.defaultOptions` override version-dependent defaults, and argument * options override both. */ -export const defaultOptions: Required = { +export const defaultOptions: Required = { anchorPrefix: 'a', indent: 2, indentSeq: true, From b64bc4d51a7f70a07ea749f23386e6e121f38d14 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 17:20:37 +0200 Subject: [PATCH 05/20] Use type rather than interface for options This is a workaround for https://github.com/microsoft/TypeScript/issues/15300 --- src/options.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/options.ts b/src/options.ts index 122278af..f2d6ec1d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -14,7 +14,7 @@ import { } from './tags/options.js' import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' -export interface ParseOptions { +export type ParseOptions = { /** * If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` * to provide the `{ line, col }` positions within the input. @@ -37,7 +37,7 @@ export interface ParseOptions { strict?: boolean } -export interface DocumentOptions { +export type DocumentOptions = { /** * Default prefix for anchors. * @@ -81,7 +81,7 @@ export interface DocumentOptions { version?: '1.1' | '1.2' } -export interface SchemaOptions { +export type SchemaOptions = { /** * Array of additional tags to include in the schema, or a function that may * modify the schema's base tag array. @@ -122,7 +122,7 @@ export interface SchemaOptions { sortMapEntries?: boolean | ((a: Pair, b: Pair) => number) } -export interface CreateNodeOptions { +export type CreateNodeOptions = { keepUndefined?: boolean | null onTagObj?: (tagObj: ScalarTag | CollectionTag) => void @@ -141,7 +141,7 @@ export interface CreateNodeOptions { tag?: string } -export interface ToJSOptions { +export type ToJSOptions = { /** * Use Map rather than Object to represent mappings. * @@ -163,7 +163,7 @@ export interface ToJSOptions { reviver?: Reviver } -export interface ToStringOptions { +export type ToStringOptions = { /** * The number of spaces to use when indenting code. * From dfcc35bc8d7a8385d676b80bccc7e3c85fafbc7c Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 17:37:55 +0200 Subject: [PATCH 06/20] Drop mapAsMap from DocumentOptions BREAKING CHANGE: The option now needs to be given to the document's toJS() method, rather than being also available on the document constructor and in YAML.defaultOptions. The behaviour of parse() is unaffected. ```diff import { parseDocument } from 'yaml' -const doc = parseDocument('foo: bar', { mapAsMap: true }) -const js = doc.toJS() +const doc = parseDocument('foo: bar') +const js = doc.toJS({ mapAsMap: true }) ``` --- docs/03_options.md | 13 ++++++++++--- docs/04_documents.md | 2 +- src/doc/Document.ts | 3 +-- src/options.ts | 8 +------- src/public-api.ts | 16 ++++++++++------ tests/doc/parse.js | 4 ++-- tests/doc/types.js | 6 +++--- tests/yaml-test-suite.js | 8 ++++---- 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 4c15615f..9fc0d214 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -2,8 +2,16 @@ ```js YAML.defaultOptions -// { indent: 2, -// mapAsMap: false, +// { anchorPrefix: 'a', +// indent: 2, +// indentSeq: true, +// keepUndefined: false, +// lineCounter: null, +// logLevel: 'warn', +// maxAliasCount: 100, +// prettyErrors: true, +// simpleKeys: false, +// strict: true, // version: '1.2' } YAML.Document.defaults @@ -28,7 +36,6 @@ The `version` option value (`'1.2'` by default) may be overridden by any documen | indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | | keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | | logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | -| mapAsMap | `boolean` | When outputting JS, use Map rather than Object to represent mappings. By default `false`. | | maxAliasCount | `number` | Prevent [exponential entity expansion attacks] by limiting data aliasing count; set to `-1` to disable checks; `0` disallows all alias nodes. By default `100`. | | merge | `boolean` | Enable support for `<<` merge keys. By default `false` for YAML 1.2 and `true` for earlier versions. | | prettyErrors | `boolean` | Include line position & node type directly in errors; drop their verbose source and context. By default `false`. | diff --git a/docs/04_documents.md b/docs/04_documents.md index 2ffc926f..d2ed1f44 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -145,7 +145,7 @@ String(doc) For a plain JavaScript representation of the document, **`toJS()`** is your friend. Its output may include `Map` and `Set` collections (e.g. if the `mapAsMap` option is true) and complex scalar values like `Date` for `!!timestamp`, but all YAML nodes will be resolved. For a representation consisting only of JSON values, use **`toJSON()`**. -Use `toJS({ mapAsMap, onAnchor, reviver })` to explicitly set the `mapAsMap` option, define an `onAnchor` callback `(value: any, count: number) => void` for each aliased anchor in the document, or to apply a [reviver function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) to the output. +Use `toJS({ mapAsMap, onAnchor, reviver })` to set the `mapAsMap` option, define an `onAnchor` callback `(value: any, count: number) => void` for each aliased anchor in the document, or to apply a [reviver function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) to the output. Conversely, to stringify a document as YAML, use **`toString()`**. This will also be called by `String(doc)`. This method will throw if the `errors` array is not empty. diff --git a/src/doc/Document.ts b/src/doc/Document.ts index dc3bcf8e..0ca9545f 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -376,8 +376,7 @@ export class Document { doc: this, indentStep: ' ', keep: !json, - mapAsMap: - typeof mapAsMap === 'boolean' ? mapAsMap : !!this.options.mapAsMap, + mapAsMap: mapAsMap === true, mapKeyWarned: false, maxAliasCount: this.options.maxAliasCount, stringify diff --git a/src/options.ts b/src/options.ts index f2d6ec1d..cfd06e68 100644 --- a/src/options.ts +++ b/src/options.ts @@ -59,12 +59,7 @@ export type DocumentOptions = { * Default: `'warn'` */ logLevel?: LogLevelId - /** - * When outputting JS, use Map rather than Object to represent mappings. - * - * Default: `false` - */ - mapAsMap?: boolean + /** * Prevent exponential entity expansion attacks by limiting data aliasing count; * set to `-1` to disable checks; `0` disallows all alias nodes. @@ -202,7 +197,6 @@ export const defaultOptions: Required( * document, so Maps become objects, Sequences arrays, and scalars result in * nulls, booleans, numbers and strings. */ -export function parse(src: string, options?: Options): any -export function parse(src: string, reviver: Reviver, options?: Options): any +export function parse(src: string, options?: Options & ToJSOptions): any +export function parse( + src: string, + reviver: Reviver, + options?: Options & ToJSOptions +): any export function parse( src: string, - reviver?: Reviver | Options, - options?: Options + reviver?: Reviver | (Options & ToJSOptions), + options?: Options & ToJSOptions ) { let _reviver: Reviver | undefined = undefined if (typeof reviver === 'function') { @@ -101,7 +105,7 @@ export function parse( throw doc.errors[0] else doc.errors = [] } - return doc.toJS({ reviver: _reviver }) + return doc.toJS(Object.assign({ reviver: _reviver }, options)) } /** diff --git a/tests/doc/parse.js b/tests/doc/parse.js index b86caf0c..091a2832 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -633,8 +633,8 @@ describe('handling complex keys', () => { test('do not add warning when using mapIsMap: true', () => { process.emitWarning = jest.fn() - const doc = YAML.parseDocument('[foo]: bar', { mapAsMap: true }) - doc.toJS() + const doc = YAML.parseDocument('[foo]: bar') + doc.toJS({ mapAsMap: true }) expect(doc.warnings).toMatchObject([]) expect(process.emitWarning).not.toHaveBeenCalled() }) diff --git a/tests/doc/types.js b/tests/doc/types.js index b614a0c7..29205bb4 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -209,8 +209,8 @@ one: 1 one: 1 2: two { 3: 4 }: many\n` - const doc = YAML.parseDocument(src, { mapAsMap: true }) - expect(doc.toJS()).toMatchObject( + const doc = YAML.parseDocument(src) + expect(doc.toJS({ mapAsMap: true })).toMatchObject( new Map([ ['one', 1], [2, 'two'], @@ -219,7 +219,7 @@ one: 1 ) expect(doc.errors).toHaveLength(0) doc.contents.items[2].key = { 3: 4 } - expect(doc.toJS()).toMatchObject( + expect(doc.toJS({ mapAsMap: true })).toMatchObject( new Map([ ['one', 1], [2, 'two'], diff --git a/tests/yaml-test-suite.js b/tests/yaml-test-suite.js index 69eeaec6..2d17e20c 100644 --- a/tests/yaml-test-suite.js +++ b/tests/yaml-test-suite.js @@ -107,10 +107,10 @@ testDirs.forEach(dir => { if (outYaml) { _test('out.yaml', () => { - const resDocs = YAML.parseAllDocuments(yaml, { mapAsMap: true }) - const resJson = resDocs.map(doc => doc.toJS()) - const expDocs = YAML.parseAllDocuments(outYaml, { mapAsMap: true }) - const expJson = expDocs.map(doc => doc.toJS()) + const resDocs = YAML.parseAllDocuments(yaml) + const resJson = resDocs.map(doc => doc.toJS({ mapAsMap: true })) + const expDocs = YAML.parseAllDocuments(outYaml) + const expJson = expDocs.map(doc => doc.toJS({ mapAsMap: true })) expect(resJson).toMatchObject(expJson) }) } From d16d17060c67b82b57b7a2453f1902ba246bbafe Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 17:37:55 +0200 Subject: [PATCH 07/20] Move maxAliasCount from DocumentOptions to ToJSOptions BREAKING CHANGE: The option now needs to be given to the document's toJS() method, rather than being also available on the document constructor and in YAML.defaultOptions. The behaviour of parse() is unaffected. ```diff import { parseDocument } from 'yaml' -const doc = parseDocument('foo: bar', { maxAliasCount: -1 }) -const js = doc.toJS() +const doc = parseDocument('foo: bar') +const js = doc.toJS({ maxAliasCount: -1 }) ``` --- docs/03_options.md | 3 --- docs/04_documents.md | 18 +++++++++++++++--- src/doc/Document.ts | 3 ++- src/options.ts | 17 ++++++++--------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 9fc0d214..59b96b0a 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -8,7 +8,6 @@ YAML.defaultOptions // keepUndefined: false, // lineCounter: null, // logLevel: 'warn', -// maxAliasCount: 100, // prettyErrors: true, // simpleKeys: false, // strict: true, @@ -36,7 +35,6 @@ The `version` option value (`'1.2'` by default) may be overridden by any documen | indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | | keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | | logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | -| maxAliasCount | `number` | Prevent [exponential entity expansion attacks] by limiting data aliasing count; set to `-1` to disable checks; `0` disallows all alias nodes. By default `100`. | | merge | `boolean` | Enable support for `<<` merge keys. By default `false` for YAML 1.2 and `true` for earlier versions. | | prettyErrors | `boolean` | Include line position & node type directly in errors; drop their verbose source and context. By default `false`. | | resolveKnownTags | `boolean` | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. | @@ -46,7 +44,6 @@ The `version` option value (`'1.2'` by default) may be overridden by any documen | strict | `boolean` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. By default `true`. | | version | `'1.0' ⎮ '1.1' ⎮ '1.2'` | The YAML version used by documents without a `%YAML` directive. By default `'1.2'`. | -[exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack [yaml 1.1 tags]: https://yaml.org/type/ ## Data Schemas diff --git a/docs/04_documents.md b/docs/04_documents.md index d2ed1f44..81507a8d 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -143,11 +143,23 @@ String(doc) // '1969-07-21T02:56:15\n' ``` -For a plain JavaScript representation of the document, **`toJS()`** is your friend. Its output may include `Map` and `Set` collections (e.g. if the `mapAsMap` option is true) and complex scalar values like `Date` for `!!timestamp`, but all YAML nodes will be resolved. For a representation consisting only of JSON values, use **`toJSON()`**. +For a plain JavaScript representation of the document, **`toJS(options = {})`** is your friend. +Its output may include `Map` and `Set` collections (e.g. if the `mapAsMap` option is true) and complex scalar values like `Date` for `!!timestamp`, but all YAML nodes will be resolved. +For a representation consisting only of JSON values, use **`toJSON()`**. -Use `toJS({ mapAsMap, onAnchor, reviver })` to set the `mapAsMap` option, define an `onAnchor` callback `(value: any, count: number) => void` for each aliased anchor in the document, or to apply a [reviver function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) to the output. +The following options are also available when calling `YAML.parse()`: -Conversely, to stringify a document as YAML, use **`toString()`**. This will also be called by `String(doc)`. This method will throw if the `errors` array is not empty. +| `toJS()` Option | Type | Description | +| --------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mapAsMap | `boolean` | Use Map rather than Object to represent mappings. By default `false`. | +| maxAliasCount | `number` | Prevent [exponential entity expansion attacks] by limiting data aliasing; set to `-1` to disable checks; `0` disallows all alias nodes. By default `100`. | +| onAnchor | `(value: any, count: number) => void` | Optional callback for each aliased anchor in the document. | +| reviver | `(key: any, value: any) => any` | Optionally apply a [reviver function] to the output, following the JSON specification but with appropriate extensions for handling `Map` and `Set`. | + +[exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack +[reviver function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter + +To stringify a document as YAML, use **`toString()`**. This will also be called by `String(doc)`. This method will throw if the `errors` array is not empty. ## Working with Anchors diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 0ca9545f..9245c16d 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -360,6 +360,7 @@ export class Document { json, jsonArg, mapAsMap, + maxAliasCount, onAnchor, reviver }: ToJSOptions & { json?: boolean; jsonArg?: string | null } = {}) { @@ -378,7 +379,7 @@ export class Document { keep: !json, mapAsMap: mapAsMap === true, mapKeyWarned: false, - maxAliasCount: this.options.maxAliasCount, + maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100, stringify } const res = toJS(this.contents, jsonArg || '', ctx) diff --git a/src/options.ts b/src/options.ts index cfd06e68..e374e3dd 100644 --- a/src/options.ts +++ b/src/options.ts @@ -60,14 +60,6 @@ export type DocumentOptions = { */ logLevel?: LogLevelId - /** - * Prevent exponential entity expansion attacks by limiting data aliasing count; - * set to `-1` to disable checks; `0` disallows all alias nodes. - * - * Default: `100` - */ - maxAliasCount?: number - /** * The YAML version used by documents without a `%YAML` directive. * @@ -144,6 +136,14 @@ export type ToJSOptions = { */ mapAsMap?: boolean + /** + * Prevent exponential entity expansion attacks by limiting data aliasing count; + * set to `-1` to disable checks; `0` disallows all alias nodes. + * + * Default: `100` + */ + maxAliasCount?: number + /** * If defined, called with the resolved `value` and reference `count` for * each anchor in the document. @@ -197,7 +197,6 @@ export const defaultOptions: Required Date: Sun, 28 Feb 2021 17:37:55 +0200 Subject: [PATCH 08/20] Drop indent, indentSeq & simpleKeys from DocumentOptions BREAKING CHANGE: These options now need to be given to the document's toString() method, rather than being also available on the document constructor and in YAML.defaultOptions. The behaviour of stringify() is unaffected. ```diff import { parseDocument } from 'yaml' -const doc = parseDocument('foo: bar', { indent: 4, indentSeq: false }) -const str = doc.toString() +const doc = parseDocument('foo: bar') +const str = doc.toString({ indent: 4, indentSeq: false }) ``` --- docs/03_options.md | 6 ------ docs/04_documents.md | 16 ++++++++++++++-- src/doc/Document.ts | 11 ++++++----- src/options.ts | 7 ++----- src/public-api.ts | 14 +++++++------- tests/doc/stringify.js | 20 ++++++++++---------- 6 files changed, 39 insertions(+), 35 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 59b96b0a..1fa84f2c 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -3,13 +3,10 @@ ```js YAML.defaultOptions // { anchorPrefix: 'a', -// indent: 2, -// indentSeq: true, // keepUndefined: false, // lineCounter: null, // logLevel: 'warn', // prettyErrors: true, -// simpleKeys: false, // strict: true, // version: '1.2' } @@ -31,15 +28,12 @@ The `version` option value (`'1.2'` by default) may be overridden by any documen | ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | anchorPrefix | `string` | Default prefix for anchors. By default `'a'`, resulting in anchors `a1`, `a2`, etc. | | customTags | `Tag[] ⎮ function` | Array of [additional tags](#custom-data-types) to include in the schema | -| indent | `number` | The number of spaces to use when indenting code. By default `2`. | -| indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | | keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | | logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | | merge | `boolean` | Enable support for `<<` merge keys. By default `false` for YAML 1.2 and `true` for earlier versions. | | prettyErrors | `boolean` | Include line position & node type directly in errors; drop their verbose source and context. By default `false`. | | resolveKnownTags | `boolean` | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. | | schema | `'core' ⎮ 'failsafe' ⎮` `'json' ⎮ 'yaml-1.1'` | The base schema to use. By default `'core'` for YAML 1.2 and `'yaml-1.1'` for earlier versions. | -| simpleKeys | `boolean` | When stringifying, require keys to be scalars and to use implicit rather than explicit notation. By default `false`. | | sortMapEntries | `boolean ⎮` `(a, b: Pair) => number` | When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. By default `false`. | | strict | `boolean` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. By default `true`. | | version | `'1.0' ⎮ '1.1' ⎮ '1.2'` | The YAML version used by documents without a `%YAML` directive. By default `'1.2'`. | diff --git a/docs/04_documents.md b/docs/04_documents.md index 81507a8d..310cb717 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -127,7 +127,7 @@ In addition to the above, the document object also provides the same **accessor To define a tag prefix to use when stringifying, use **`setTagPrefix(handle, prefix)`** rather than setting a value directly in `tagPrefixes`. This will guarantee that the `handle` is valid (by throwing an error), and will overwrite any previous definition for the `handle`. Use an empty `prefix` value to remove a prefix. -#### `Document#toJS()`, `Document#toJSON()` and `Document#toString()` +#### `Document#toJS()` and `Document#toJSON()` ```js const src = '1969-07-21T02:56:15Z' @@ -159,7 +159,19 @@ The following options are also available when calling `YAML.parse()`: [exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack [reviver function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter -To stringify a document as YAML, use **`toString()`**. This will also be called by `String(doc)`. This method will throw if the `errors` array is not empty. +#### `Document#toString()` + +To stringify a document as YAML, use **`toString(options = {})`**. +This will also be called by `String(doc)` (with no options). +This method will throw if the `errors` array is not empty. + +The following options are also available when calling `YAML.stringify()`: + +| `toString()` Option | Type | Description | +| ------------------- | --------- | ----------------------------------------------------------------------------------------------------- | +| indent | `number` | The number of spaces to use when indenting code. By default `2`. | +| indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | +| simpleKeys | `boolean` | Require keys to be scalars and always use implicit rather than explicit notation. By default `false`. | ## Working with Anchors diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 9245c16d..ff07c68e 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -77,8 +77,7 @@ export class Document { /** Errors encountered during parsing. */ errors: YAMLError[] = [] - options: Required & - SchemaOptions + options: Required & SchemaOptions /** The schema used with the document. Use `setSchema()` to change. */ schema: Schema @@ -404,11 +403,13 @@ export class Document { toString({ indent, indentSeq, simpleKeys }: ToStringOptions = {}) { if (this.errors.length > 0) throw new Error('Document with errors cannot be stringified') - const indentSize = indent ?? this.options.indent + + const indentSize = typeof indent === 'number' ? indent : 2 if (!Number.isInteger(indentSize) || indentSize <= 0) { const s = JSON.stringify(indentSize) throw new Error(`"indent" option must be a positive integer, not ${s}`) } + const lines = [] let hasDirectives = false const dir = this.directives.toString(this) @@ -425,9 +426,9 @@ export class Document { anchors: Object.create(null), doc: this, indent: '', - indentSeq: indentSeq ?? this.options.indentSeq, + indentSeq: indentSeq !== false, // default true indentStep: ' '.repeat(indentSize), - simpleKeys: simpleKeys ?? this.options.simpleKeys, + simpleKeys: simpleKeys === true, // default false stringify // Requiring directly in nodes would create circular dependencies } let chompKeep = false diff --git a/src/options.ts b/src/options.ts index e374e3dd..6f156f62 100644 --- a/src/options.ts +++ b/src/options.ts @@ -181,7 +181,7 @@ export type ToStringOptions = { simpleKeys?: boolean } -export type Options = ParseOptions & DocumentOptions & SchemaOptions & ToStringOptions +export type Options = ParseOptions & DocumentOptions & SchemaOptions /** * `yaml` defines document-specific options in three places: as an argument of @@ -190,15 +190,12 @@ export type Options = ParseOptions & DocumentOptions & SchemaOptions & ToStringO * `YAML.defaultOptions` override version-dependent defaults, and argument * options override both. */ -export const defaultOptions: Required = { +export const defaultOptions: Required = { anchorPrefix: 'a', - indent: 2, - indentSeq: true, keepUndefined: false, lineCounter: null, logLevel: 'warn', prettyErrors: true, - simpleKeys: false, strict: true, version: '1.2' } diff --git a/src/public-api.ts b/src/public-api.ts index 7f7cc86c..a33beabc 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -5,7 +5,7 @@ import { Document, Replacer } from './doc/Document.js' import { YAMLParseError } from './errors.js' import { warn } from './log.js' import type { ParsedNode } from './nodes/Node.js' -import type { Options, ToJSOptions } from './options.js' +import type { Options, ToJSOptions, ToStringOptions } from './options.js' import { Parser } from './parse/parser.js' export interface EmptyStream @@ -114,16 +114,16 @@ export function parse( * @param replacer - A replacer array or function, as in `JSON.stringify()` * @returns Will always include `\n` as the last character, as is expected of YAML documents. */ -export function stringify(value: any, options?: Options): string +export function stringify(value: any, options?: Options & ToStringOptions): string export function stringify( value: any, replacer?: Replacer | null, - options?: string | number | Options + options?: string | number | Options & ToStringOptions ): string export function stringify( value: any, - replacer?: Replacer | Options | null, - options?: string | number | Options + replacer?: Replacer | Options & ToStringOptions | null, + options?: string | number | Options & ToStringOptions ) { let _replacer: Replacer | null = null if (typeof replacer === 'function' || Array.isArray(replacer)) { @@ -138,8 +138,8 @@ export function stringify( options = indent < 1 ? undefined : indent > 8 ? { indent: 8 } : { indent } } if (value === undefined) { - const { keepUndefined } = options || (replacer as Options) || {} + const { keepUndefined } = options || (replacer as Options & ToStringOptions) || {} if (!keepUndefined) return undefined } - return new Document(value, _replacer, options).toString() + return new Document(value, _replacer, options).toString(options) } diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index 147b4dfc..84056a59 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -595,34 +595,34 @@ describe('scalar styles', () => { describe('simple keys', () => { test('key with no value', () => { const doc = YAML.parseDocument('? ~') - expect(String(doc)).toBe('? ~\n') + expect(doc.toString()).toBe('? ~\n') doc.options.simpleKeys = true - expect(String(doc)).toBe('~: null\n') + expect(doc.toString({ simpleKeys: true })).toBe('~: null\n') }) test('key with block scalar value', () => { const doc = YAML.parseDocument('foo: bar') doc.contents.items[0].key.type = 'BLOCK_LITERAL' - expect(String(doc)).toBe('? |-\n foo\n: bar\n') + expect(doc.toString()).toBe('? |-\n foo\n: bar\n') doc.options.simpleKeys = true - expect(String(doc)).toBe('"foo": bar\n') + expect(doc.toString({ simpleKeys: true })).toBe('"foo": bar\n') }) test('key with comment', () => { const doc = YAML.parseDocument('foo: bar') doc.contents.items[0].key.comment = 'FOO' - expect(String(doc)).toBe('foo: #FOO\n bar\n') + expect(doc.toString()).toBe('foo: #FOO\n bar\n') doc.options.simpleKeys = true - expect(() => String(doc)).toThrow( + expect(() => doc.toString({ simpleKeys: true })).toThrow( /With simple keys, key nodes cannot have comments/ ) }) test('key with collection value', () => { const doc = YAML.parseDocument('[foo]: bar') - expect(String(doc)).toBe('? [ foo ]\n: bar\n') + expect(doc.toString()).toBe('? [ foo ]\n: bar\n') doc.options.simpleKeys = true - expect(() => String(doc)).toThrow( + expect(() => doc.toString({ simpleKeys: true })).toThrow( /With simple keys, collection cannot be used as a key value/ ) }) @@ -632,9 +632,9 @@ describe('simple keys', () => { ? ${new Array(1026).join('a')} : longkey` const doc = YAML.parseDocument(str) - expect(String(doc)).toBe(`? ${new Array(1026).join('a')}\n: longkey\n`) + expect(doc.toString()).toBe(`? ${new Array(1026).join('a')}\n: longkey\n`) doc.options.simpleKeys = true - expect(() => String(doc)).toThrow( + expect(() => doc.toString({ simpleKeys: true })).toThrow( /With simple keys, single line scalar must not span more than 1024 characters/ ) }) From b76e6204343bfb07e84303521dabc73fce7594a7 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 28 Feb 2021 22:24:15 +0200 Subject: [PATCH 09/20] Refactor doc.setSchema(); drop Document.defaults BREAKING CHANGE: The new API of doc.setSchema(), now requiring an explicit version as well as supporting all Schema options. This is required to reduce the dependency on a mutating doc.options object. -doc.setSchema('json') +doc.setSchema('1.2', { schema: 'json' }) -doc.options.merge = false -doc.setSchema('1.1') +doc.setSchema('1.1', { merge: false }) As a side effect of these changes, the Document.defaults object is removed, along with the doc.getDefaults() method. The version-specific schema options now need to be set explicitly in function arguments. --- docs/03_options.md | 35 +++++++++--------- docs/04_documents.md | 19 +++++----- src/doc/Document.ts | 83 ++++++++++++++++++++---------------------- src/options.ts | 48 +++++++----------------- tests/doc/stringify.js | 28 +++++--------- tests/doc/types.js | 16 ++++---- 6 files changed, 97 insertions(+), 132 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 1fa84f2c..0f9ce37a 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -4,39 +4,40 @@ YAML.defaultOptions // { anchorPrefix: 'a', // keepUndefined: false, -// lineCounter: null, // logLevel: 'warn', // prettyErrors: true, // strict: true, // version: '1.2' } - -YAML.Document.defaults -// { '1.0': { merge: true, schema: 'yaml-1.1' }, -// '1.1': { merge: true, schema: 'yaml-1.1' }, -// '1.2': { merge: false, schema: 'core' } } ``` -#### `YAML.defaultOptions` +In addition to the `options` arguments of functions, the values of `YAML.defaultOptions` are used as default values. + +Parse options affect the parsing and composition of a YAML Document from it source. + +| Parse Option | Type | Description | +| ------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| lineCounter | `LineCounter` | If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` to provide the `{ line, col }` positions within the input. | +| prettyErrors | `boolean` | Include line position & node type directly in errors. By default `false`. | +| strict | `boolean` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. By default `true`. | -#### `YAML.Document.defaults` +Document options are relevant for operations on the `Document` object. -`yaml` defines document-specific options in three places: as an argument of parse, create and stringify calls, in the values of `YAML.defaultOptions`, and in the version-dependent `YAML.Document.defaults` object. Values set in `YAML.defaultOptions` override version-dependent defaults, and argument options override both. +| Document Option | Type | Description | +| --------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| anchorPrefix | `string` | Default prefix for anchors. By default `'a'`, resulting in anchors `a1`, `a2`, etc. | +| keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | +| logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | +| version | `'1.1' ⎮ '1.2'` | The YAML version used by documents without a `%YAML` directive. By default `'1.2'`. | -The `version` option value (`'1.2'` by default) may be overridden by any document-specific `%YAML` directive. +Schema options determine the types of values that the document is expected and able to support. -| Option | Type | Description | +| Schema Option | Type | Description | | ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| anchorPrefix | `string` | Default prefix for anchors. By default `'a'`, resulting in anchors `a1`, `a2`, etc. | | customTags | `Tag[] ⎮ function` | Array of [additional tags](#custom-data-types) to include in the schema | -| keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | -| logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | | merge | `boolean` | Enable support for `<<` merge keys. By default `false` for YAML 1.2 and `true` for earlier versions. | -| prettyErrors | `boolean` | Include line position & node type directly in errors; drop their verbose source and context. By default `false`. | | resolveKnownTags | `boolean` | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. | | schema | `'core' ⎮ 'failsafe' ⎮` `'json' ⎮ 'yaml-1.1'` | The base schema to use. By default `'core'` for YAML 1.2 and `'yaml-1.1'` for earlier versions. | | sortMapEntries | `boolean ⎮` `(a, b: Pair) => number` | When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. By default `false`. | -| strict | `boolean` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. By default `true`. | -| version | `'1.0' ⎮ '1.1' ⎮ '1.2'` | The YAML version used by documents without a `%YAML` directive. By default `'1.2'`. | [yaml 1.1 tags]: https://yaml.org/type/ diff --git a/docs/04_documents.md b/docs/04_documents.md index 310cb717..76ad3a53 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -102,16 +102,15 @@ During stringification, a document with a true-ish `version` value will include ## Document Methods -| Method | Returns | Description | -| ------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| createNode(value, options?) | `Node` | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information. | -| createPair(key, value, options?) | `Pair` | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information. | -| parse(cst) | `Document` | Parse a CST into this document. Mostly an internal method, modifying the document according to the contents of the parsed `cst`. Calling this multiple times on a Document is not recommended. | -| setSchema(id?, customTags?) | `void` | Set the schema used by the document. `id` may either be a YAML version, or the identifier of a YAML 1.2 schema; if set, `customTags` should have the same shape as the similarly-named option. | -| setTagPrefix(handle, prefix) | `void` | Set `handle` as a shorthand string for the `prefix` tag namespace. | -| toJS(options?) | `any` | A plain JavaScript representation of the document `contents`. | -| toJSON() | `any` | A JSON representation of the document `contents`. | -| toString() | `string` | A YAML representation of the document. | +| Method | Returns | Description | +| ------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- | +| createNode(value, options?) | `Node` | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information. | +| createPair(key, value, options?) | `Pair` | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information. | +| setSchema(version, options?) | `void` | Change the YAML version and schema used by the document. `version` must be either `'1.1'` or `'1.2'`; accepts all Schema options. | +| setTagPrefix(handle, prefix) | `void` | Set `handle` as a shorthand string for the `prefix` tag namespace. | +| toJS(options?) | `any` | A plain JavaScript representation of the document `contents`. | +| toJSON() | `any` | A JSON representation of the document `contents`. | +| toString(options?) | `string` | A YAML representation of the document. | ```js const doc = YAML.parseDocument('a: 1\nb: [2, 3]\n') diff --git a/src/doc/Document.ts b/src/doc/Document.ts index ff07c68e..d2ae31a2 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -18,7 +18,6 @@ import type { YAMLSeq } from '../nodes/YAMLSeq.js' import { CreateNodeOptions, defaultOptions, - documentOptions, DocumentOptions, Options, ParseOptions, @@ -28,9 +27,8 @@ import { } from '../options.js' import { addComment } from '../stringify/addComment.js' import { stringify, StringifyContext } from '../stringify/stringify.js' -import type { TagValue } from '../tags/types.js' import { Anchors } from './Anchors.js' -import { Schema, SchemaName } from './Schema.js' +import { Schema } from './Schema.js' import { applyReviver } from './applyReviver.js' import { createNode, CreateNodeContext } from './createNode.js' import { Directives } from './directives.js' @@ -51,8 +49,6 @@ export declare namespace Document { } export class Document { - static defaults = documentOptions; - readonly [NODE_TYPE]: symbol /** @@ -77,10 +73,16 @@ export class Document { /** Errors encountered during parsing. */ errors: YAMLError[] = [] - options: Required & SchemaOptions + options: Required< + Omit< + ParseOptions & DocumentOptions, + 'lineCounter' | 'directives' | 'version' + > + > + // TS can't figure out that setSchema() will set this, or throw /** The schema used with the document. Use `setSchema()` to change. */ - schema: Schema + declare schema: Schema /** * Array of prefixes; each will have a string `handle` that @@ -119,16 +121,15 @@ export class Document { replacer = undefined } - this.options = Object.assign({}, defaultOptions, options) + const opt = Object.assign({}, defaultOptions, options) + this.options = opt this.anchors = new Anchors(this.options.anchorPrefix) + let { version } = opt if (options?.directives) { this.directives = options.directives.atDocument() - if (options.version && !this.directives.yaml.explicit) - this.directives.yaml.version = options.version - } else this.directives = new Directives({ version: this.options.version }) - - const schemaOpts = Object.assign({}, this.getDefaults(), this.options) - this.schema = new Schema(schemaOpts) + if (this.directives.yaml.explicit) version = this.directives.yaml.version + } else this.directives = new Directives({ version }) + this.setSchema(version, options) this.contents = value === undefined @@ -230,14 +231,6 @@ export class Document { : false } - getDefaults() { - return ( - Document.defaults[this.directives.yaml.version] || - Document.defaults[this.options.version] || - {} - ) - } - /** * Returns item at `key`, or `undefined` if not found. By default unwraps * scalar values from their surrounding node; to disable set `keepScalar` to @@ -313,29 +306,33 @@ export class Document { } /** - * When a document is created with `new YAML.Document()`, the schema object is - * not set as it may be influenced by parsed directives; call this with no - * arguments to set it manually, or with arguments to change the schema used - * by the document. + * Change the YAML version and schema used by the document. + * + * Overrides all previously set schema options */ - setSchema( - id: Options['version'] | SchemaName | null, - customTags?: TagValue[] - ) { - if (!id && !customTags) return - - // @ts-ignore Never happens in TypeScript - if (typeof id === 'number') id = id.toFixed(1) - - if (id === '1.1' || id === '1.2') { - this.directives.yaml.version = id - delete this.options.schema - } else if (id && typeof id === 'string') { - this.options.schema = id + setSchema(version: '1.1' | '1.2', options?: SchemaOptions) { + let _options: SchemaOptions + switch (String(version)) { + case '1.1': + this.directives.yaml.version = '1.1' + _options = Object.assign( + { merge: true, resolveKnownTags: false, schema: 'yaml-1.1' }, + options + ) + break + case '1.2': + this.directives.yaml.version = '1.2' + _options = Object.assign( + { merge: false, resolveKnownTags: true, schema: 'core' }, + options + ) + break + default: { + const sv = JSON.stringify(version) + throw new Error(`Expected '1.1' or '1.2' as version, but found: ${sv}`) + } } - if (Array.isArray(customTags)) this.options.customTags = customTags - const schemaOpts = Object.assign({}, this.getDefaults(), this.options) - this.schema = new Schema(schemaOpts) + this.schema = new Schema(_options) } /** Set `handle` as a shorthand string for the `prefix` tag namespace. */ diff --git a/src/options.ts b/src/options.ts index 6f156f62..69ea76c3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,4 +1,4 @@ -import { LogLevelId, defaultTagPrefix } from './constants.js' +import { LogLevelId } from './constants.js' import type { Reviver } from './doc/applyReviver.js' import type { Directives } from './doc/directives.js' import type { Replacer } from './doc/Document.js' @@ -19,7 +19,7 @@ export type ParseOptions = { * If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` * to provide the `{ line, col }` positions within the input. */ - lineCounter?: LineCounter | null + lineCounter?: LineCounter /** * Include line/col position & node type directly in parse errors. @@ -45,6 +45,12 @@ export type DocumentOptions = { */ anchorPrefix?: string + /** + * Used internally by Composer. If set and includes an explicit version, + * that overrides the `version` option. + */ + directives?: Directives + /** * Keep `undefined` object values when creating mappings and return a Scalar * node when calling `YAML.stringify(undefined)`, rather than `undefined`. @@ -75,8 +81,6 @@ export type SchemaOptions = { */ customTags?: TagValue[] | ((tags: TagValue[]) => TagValue[]) | null - directives?: Directives - /** * Enable support for `<<` merge keys. * @@ -102,7 +106,8 @@ export type SchemaOptions = { schema?: SchemaName /** - * When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. + * When adding to or stringifying a map, sort the entries. + * If `true`, sort by comparing key values with `<`. * * Default: `false` */ @@ -190,10 +195,11 @@ export type Options = ParseOptions & DocumentOptions & SchemaOptions * `YAML.defaultOptions` override version-dependent defaults, and argument * options override both. */ -export const defaultOptions: Required = { +export const defaultOptions: Required< + Omit & Omit +> = { anchorPrefix: 'a', keepUndefined: false, - lineCounter: null, logLevel: 'warn', prettyErrors: true, strict: true, @@ -236,31 +242,3 @@ export const scalarOptions = { Object.assign(strOptions, opt) } } - -export const documentOptions = { - '1.0': { - schema: 'yaml-1.1', - merge: true, - tagPrefixes: [ - { handle: '!', prefix: defaultTagPrefix }, - { handle: '!!', prefix: 'tag:private.yaml.org,2002:' } - ] - }, - 1.1: { - schema: 'yaml-1.1', - merge: true, - tagPrefixes: [ - { handle: '!', prefix: '!' }, - { handle: '!!', prefix: defaultTagPrefix } - ] - }, - 1.2: { - schema: 'core', - merge: false, - resolveKnownTags: true, - tagPrefixes: [ - { handle: '!', prefix: '!' }, - { handle: '!!', prefix: defaultTagPrefix } - ] - } -} diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index 84056a59..8e5ad9a1 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -371,36 +371,28 @@ describe('eemeli/yaml#80: custom tags', () => { } } - beforeAll(() => { - YAML.defaultOptions.customTags = [regexp, sharedSymbol] - }) - - afterAll(() => { - YAML.defaultOptions.customTags = [] - }) - describe('RegExp', () => { test('stringify as plain scalar', () => { - const str = YAML.stringify(/re/g) + const str = YAML.stringify(/re/g, { customTags: [regexp] }) expect(str).toBe('!re /re/g\n') - const res = YAML.parse(str) + const res = YAML.parse(str, { customTags: [regexp] }) expect(res).toBeInstanceOf(RegExp) }) test('stringify as quoted scalar', () => { - const str = YAML.stringify(/re: /g) + const str = YAML.stringify(/re: /g, { customTags: [regexp] }) expect(str).toBe('!re "/re: /g"\n') - const res = YAML.parse(str) + const res = YAML.parse(str, { customTags: [regexp] }) expect(res).toBeInstanceOf(RegExp) }) test('parse plain string as string', () => { - const res = YAML.parse('/re/g') + const res = YAML.parse('/re/g', { customTags: [regexp] }) expect(res).toBe('/re/g') }) test('parse quoted string as string', () => { - const res = YAML.parse('"/re/g"') + const res = YAML.parse('"/re/g"', { customTags: [regexp] }) expect(res).toBe('/re/g') }) }) @@ -408,17 +400,17 @@ describe('eemeli/yaml#80: custom tags', () => { describe('Symbol', () => { test('stringify as plain scalar', () => { const symbol = Symbol.for('foo') - const str = YAML.stringify(symbol) + const str = YAML.stringify(symbol, { customTags: [sharedSymbol] }) expect(str).toBe('!symbol/shared foo\n') - const res = YAML.parse(str) + const res = YAML.parse(str, { customTags: [sharedSymbol] }) expect(res).toBe(symbol) }) test('stringify as block scalar', () => { const symbol = Symbol.for('foo\nbar') - const str = YAML.stringify(symbol) + const str = YAML.stringify(symbol, { customTags: [sharedSymbol] }) expect(str).toBe('!symbol/shared |-\nfoo\nbar\n') - const res = YAML.parse(str) + const res = YAML.parse(str, { customTags: [sharedSymbol] }) expect(res).toBe(symbol) }) }) diff --git a/tests/doc/types.js b/tests/doc/types.js index 29205bb4..67c4816a 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -685,9 +685,9 @@ describe('custom tags', () => { describe('schema changes', () => { test('write as json', () => { const doc = YAML.parseDocument('foo: bar', { schema: 'core' }) - expect(doc.options.schema).toBe('core') - doc.setSchema('json') - expect(doc.options.schema).toBe('json') + expect(doc.schema.name).toBe('core') + doc.setSchema('1.2', { schema: 'json' }) + expect(doc.schema.name).toBe('json') expect(String(doc)).toBe('"foo": "bar"\n') }) @@ -695,7 +695,6 @@ describe('schema changes', () => { const doc = YAML.parseDocument('foo: 1971-02-03T12:13:14', { version: '1.1' }) - expect(doc.options.version).toBe('1.1') expect(doc.directives.yaml).toMatchObject({ version: '1.1', explicit: false @@ -705,8 +704,7 @@ describe('schema changes', () => { version: '1.2', explicit: false }) - expect(doc.options.version).toBe('1.1') - expect(doc.options.schema).toBeUndefined() + expect(doc.schema.name).toBe('core') expect(() => String(doc)).toThrow(/Tag not resolved for Date value/) }) @@ -714,7 +712,7 @@ describe('schema changes', () => { const doc = YAML.parseDocument('foo: 1971-02-03T12:13:14', { version: '1.1' }) - doc.setSchema('json', ['timestamp']) + doc.setSchema('1.1', { customTags: ['timestamp'], schema: 'json' }) expect(String(doc)).toBe('"foo": 1971-02-03T12:13:14\n') }) @@ -722,7 +720,7 @@ describe('schema changes', () => { const doc = YAML.parseDocument('foo: 1971-02-03T12:13:14', { version: '1.1' }) - doc.setSchema(1.2, ['timestamp']) + doc.setSchema(1.2, { customTags: ['timestamp'] }) expect(String(doc)).toBe('foo: 1971-02-03T12:13:14\n') }) @@ -730,7 +728,7 @@ describe('schema changes', () => { const doc = YAML.parseDocument('[y, yes, on, n, no, off]', { version: '1.1' }) - doc.setSchema('core') + doc.setSchema('1.1', { schema: 'core' }) expect(String(doc)).toBe('[ true, true, true, false, false, false ]\n') doc.setSchema('1.1') expect(String(doc)).toBe('[ y, yes, on, n, no, off ]\n') From fbe193e78f367473fb08e0c00dfaa735df2ca895 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 00:55:56 +0200 Subject: [PATCH 10/20] Move BigInt option from scalarOptions to ParseOptions BREAKING CHANGE: Rather than setting `YAML.scalarOptions.int.asBigInt`, users should use the `intAsBigInt` option given to parse functions and constructors. --- docs/03_options.md | 28 ++++++++++++++-------------- src/compose/compose-collection.ts | 2 +- src/compose/compose-scalar.ts | 4 +++- src/options.ts | 17 ++++++++++------- src/tags/core.ts | 20 +++++++++++--------- src/tags/json.ts | 6 +++--- src/tags/options.ts | 9 --------- src/tags/types.ts | 13 +++++++++++-- src/tags/yaml-1.1/index.ts | 24 +++++++++++++++++------- src/tags/yaml-1.1/timestamp.ts | 10 ++++------ tests/doc/parse.js | 27 ++++++++++++--------------- tests/doc/types.js | 25 ++++++++++--------------- 12 files changed, 96 insertions(+), 89 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 0f9ce37a..a3e70ee8 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -16,10 +16,13 @@ Parse options affect the parsing and composition of a YAML Document from it sour | Parse Option | Type | Description | | ------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| intAsBigInt | `boolean` | Whether integers should be parsed into [BigInt] rather than `number` values. By default `false`. | | lineCounter | `LineCounter` | If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` to provide the `{ line, col }` positions within the input. | | prettyErrors | `boolean` | Include line position & node type directly in errors. By default `false`. | | strict | `boolean` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. By default `true`. | +[bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt + Document options are relevant for operations on the `Document` object. | Document Option | Type | Description | @@ -94,20 +97,17 @@ YAML.stringify({ this: null, that: 'value' }) Some customization options are availabe to control the parsing and stringification of scalars. Note that these values are used by all documents. -| Option | Type | Default value | Description | -| ------------------ | --------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| binary.defaultType | `Type` | `'BLOCK_LITERAL'` | The type of string literal used to stringify `!!binary` values | -| binary.lineWidth | `number` | `76` | Maximum line width for `!!binary` values | -| bool.trueStr | `string` | `'true'` | String representation for `true` values | -| bool.falseStr | `string` | `'false'` | String representation for `false` values | -| int.asBigInt | `boolean` | `false` | Whether integers should be parsed into [BigInt] values | -| null.nullStr | `string` | `'null'` | String representation for `null` values | -| str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | -| str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | -| str.doubleQuoted | `object` | `{ jsonEncoding: false,` `minMultiLineLength: 40 }` | `jsonEncoding`: Whether to restrict double-quoted strings to use JSON-compatible syntax; `minMultiLineLength`: Minimum length to use multiple lines to represent the value | -| str.fold | `object` | `{ lineWidth: 80,` `minContentWidth: 20 }` | `lineWidth`: Maximum line width (set to `0` to disable folding); `minContentWidth`: Minimum width for highly-indented content | - -[bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt +| Option | Type | Default value | Description | +| ------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| binary.defaultType | `Type` | `'BLOCK_LITERAL'` | The type of string literal used to stringify `!!binary` values | +| binary.lineWidth | `number` | `76` | Maximum line width for `!!binary` values | +| bool.trueStr | `string` | `'true'` | String representation for `true` values | +| bool.falseStr | `string` | `'false'` | String representation for `false` values | +| null.nullStr | `string` | `'null'` | String representation for `null` values | +| str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | +| str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | +| str.doubleQuoted | `object` | `{ jsonEncoding: false,` `minMultiLineLength: 40 }` | `jsonEncoding`: Whether to restrict double-quoted strings to use JSON-compatible syntax; `minMultiLineLength`: Minimum length to use multiple lines to represent the value | +| str.fold | `object` | `{ lineWidth: 80,` `minContentWidth: 20 }` | `lineWidth`: Maximum line width (set to `0` to disable folding); `minContentWidth`: Minimum width for highly-indented content | ## Silencing Warnings diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index c003b394..8e12230e 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -79,7 +79,7 @@ export function composeCollection( } } - const res = tag.resolve(coll, msg => onError(coll.range[0], msg)) + const res = tag.resolve(coll, msg => onError(coll.range[0], msg), doc.options) const node = isNode(res) ? (res as ParsedNode) : (new Scalar(res) as Scalar.Parsed) diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index 909c661c..6730fa54 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -26,7 +26,9 @@ export function composeScalar( let scalar: Scalar try { - const res = tag ? tag.resolve(value, msg => onError(offset, msg)) : value + const res = tag + ? tag.resolve(value, msg => onError(offset, msg), doc.options) + : value scalar = isScalar(res) ? res : new Scalar(res) } catch (error) { onError(offset, error.message) diff --git a/src/options.ts b/src/options.ts index 69ea76c3..3a4331cf 100644 --- a/src/options.ts +++ b/src/options.ts @@ -8,13 +8,21 @@ import type { LineCounter } from './parse/line-counter.js' import { binaryOptions, boolOptions, - intOptions, nullOptions, strOptions } from './tags/options.js' import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' export type ParseOptions = { + /** + * Whether integers should be parsed into BigInt rather than number values. + * + * Default: `false` + * + * https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt + */ + intAsBigInt?: boolean + /** * If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` * to provide the `{ line, col }` positions within the input. @@ -199,6 +207,7 @@ export const defaultOptions: Required< Omit & Omit > = { anchorPrefix: 'a', + intAsBigInt: false, keepUndefined: false, logLevel: 'warn', prettyErrors: true, @@ -223,12 +232,6 @@ export const scalarOptions = { set bool(opt) { Object.assign(boolOptions, opt) }, - get int() { - return intOptions - }, - set int(opt) { - Object.assign(intOptions, opt) - }, get null() { return nullOptions }, diff --git a/src/tags/core.ts b/src/tags/core.ts index ee5d5fbb..7a7e0a64 100644 --- a/src/tags/core.ts +++ b/src/tags/core.ts @@ -1,14 +1,19 @@ import { Scalar } from '../nodes/Scalar.js' +import { ParseOptions } from '../options.js' import { stringifyNumber } from '../stringify/stringifyNumber.js' import { failsafe } from './failsafe/index.js' -import { boolOptions, intOptions, nullOptions } from './options.js' +import { boolOptions, nullOptions } from './options.js' import { ScalarTag } from './types.js' const intIdentify = (value: unknown): value is number | bigint => typeof value === 'bigint' || Number.isInteger(value) -const intResolve = (str: string, offset: number, radix: number) => - intOptions.asBigInt ? BigInt(str) : parseInt(str.substring(offset), radix) +const intResolve = ( + str: string, + offset: number, + radix: number, + { intAsBigInt }: ParseOptions +) => (intAsBigInt ? BigInt(str) : parseInt(str.substring(offset), radix)) function intStringify(node: Scalar, radix: number, prefix: string) { const { value } = node @@ -50,8 +55,7 @@ export const octObj: ScalarTag = { tag: 'tag:yaml.org,2002:int', format: 'OCT', test: /^0o[0-7]+$/, - resolve: str => intResolve(str, 2, 8), - options: intOptions, + resolve: (str, _onError, opt) => intResolve(str, 2, 8, opt), stringify: node => intStringify(node, 8, '0o') } @@ -60,8 +64,7 @@ export const intObj: ScalarTag = { default: true, tag: 'tag:yaml.org,2002:int', test: /^[-+]?[0-9]+$/, - resolve: str => intResolve(str, 0, 10), - options: intOptions, + resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), stringify: stringifyNumber } @@ -71,8 +74,7 @@ export const hexObj: ScalarTag = { tag: 'tag:yaml.org,2002:int', format: 'HEX', test: /^0x[0-9a-fA-F]+$/, - resolve: str => intResolve(str, 2, 16), - options: intOptions, + resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt), stringify: node => intStringify(node, 16, '0x') } diff --git a/src/tags/json.ts b/src/tags/json.ts index bba74495..90827435 100644 --- a/src/tags/json.ts +++ b/src/tags/json.ts @@ -3,10 +3,9 @@ import { Scalar } from '../nodes/Scalar.js' import { map } from './failsafe/map.js' import { seq } from './failsafe/seq.js' -import { intOptions } from './options.js' import { CollectionTag, ScalarTag } from './types.js' -function intIdentify(value: unknown): value is number | BigInt { +function intIdentify(value: unknown): value is number | bigint { return typeof value === 'bigint' || Number.isInteger(value) } @@ -42,7 +41,8 @@ const jsonScalars: ScalarTag[] = [ default: true, tag: 'tag:yaml.org,2002:int', test: /^-?(?:0|[1-9][0-9]*)$/, - resolve: str => (intOptions.asBigInt ? BigInt(str) : parseInt(str, 10)), + resolve: (str, _onError, { intAsBigInt }) => + intAsBigInt ? BigInt(str) : parseInt(str, 10), stringify: ({ value }) => intIdentify(value) ? value.toString() : JSON.stringify(value) }, diff --git a/src/tags/options.ts b/src/tags/options.ts index b686ff18..bc6fa67e 100644 --- a/src/tags/options.ts +++ b/src/tags/options.ts @@ -39,15 +39,6 @@ export const boolOptions = { falseStr: 'false' } -export const intOptions = { - /** - * Whether integers should be parsed into BigInt values. - * - * Default: `false` - */ - asBigInt: false -} - export const nullOptions = { /** * String representation for `null`. diff --git a/src/tags/types.ts b/src/tags/types.ts index 6fe83873..1752c140 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -4,6 +4,7 @@ import type { Node } from '../nodes/Node.js' import type { Scalar } from '../nodes/Scalar.js' import type { YAMLMap } from '../nodes/YAMLMap.js' import type { YAMLSeq } from '../nodes/YAMLSeq.js' +import { ParseOptions } from '../options.js' import type { StringifyContext } from '../stringify/stringify.js' export type SchemaId = 'core' | 'failsafe' | 'json' | 'yaml11' @@ -71,7 +72,11 @@ export interface ScalarTag extends TagBase { * Turns a value into an AST node. * If returning a non-`Node` value, the output will be wrapped as a `Scalar`. */ - resolve(value: string, onError: (message: string) => void): unknown + resolve( + value: string, + onError: (message: string) => void, + options: ParseOptions + ): unknown /** * Optional function stringifying a Scalar node. If your data includes a @@ -118,7 +123,11 @@ export interface CollectionTag extends TagBase { * Turns a value into an AST node. * If returning a non-`Node` value, the output will be wrapped as a `Scalar`. */ - resolve(value: YAMLMap | YAMLSeq, onError: (message: string) => void): unknown + resolve( + value: YAMLMap | YAMLSeq, + onError: (message: string) => void, + options: ParseOptions + ): unknown } export type TagObj = ScalarTag | CollectionTag diff --git a/src/tags/yaml-1.1/index.ts b/src/tags/yaml-1.1/index.ts index d0093d97..59ff314d 100644 --- a/src/tags/yaml-1.1/index.ts +++ b/src/tags/yaml-1.1/index.ts @@ -1,7 +1,8 @@ import { Scalar } from '../../nodes/Scalar.js' +import { ParseOptions } from '../../options.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' import { failsafe } from '../failsafe/index.js' -import { boolOptions, intOptions, nullOptions } from '../options.js' +import { boolOptions, nullOptions } from '../options.js' import { ScalarTag } from '../types.js' import { binary } from './binary.js' import { omap } from './omap.js' @@ -50,11 +51,16 @@ const falseObj: ScalarTag & { test: RegExp } = { const intIdentify = (value: unknown): value is number | bigint => typeof value === 'bigint' || Number.isInteger(value) -function intResolve(str: string, offset: number, radix: number) { +function intResolve( + str: string, + offset: number, + radix: number, + { intAsBigInt }: ParseOptions +) { const sign = str[0] if (sign === '-' || sign === '+') offset += 1 str = str.substring(offset).replace(/_/g, '') - if (intOptions.asBigInt) { + if (intAsBigInt) { switch (radix) { case 2: str = `0b${str}` @@ -93,7 +99,8 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:int', format: 'BIN', test: /^[-+]?0b[0-1_]+$/, - resolve: (str: string) => intResolve(str, 2, 2), + resolve: (str: string, _onError: unknown, opt: ParseOptions) => + intResolve(str, 2, 2, opt), stringify: node => intStringify(node, 2, '0b') }, { @@ -102,7 +109,8 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:int', format: 'OCT', test: /^[-+]?0[0-7_]+$/, - resolve: (str: string) => intResolve(str, 1, 8), + resolve: (str: string, _onError: unknown, opt: ParseOptions) => + intResolve(str, 1, 8, opt), stringify: node => intStringify(node, 8, '0') }, { @@ -110,7 +118,8 @@ export const yaml11 = failsafe.concat( default: true, tag: 'tag:yaml.org,2002:int', test: /^[-+]?[0-9][0-9_]*$/, - resolve: (str: string) => intResolve(str, 0, 10), + resolve: (str: string, _onError: unknown, opt: ParseOptions) => + intResolve(str, 0, 10, opt), stringify: stringifyNumber }, { @@ -119,7 +128,8 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:int', format: 'HEX', test: /^[-+]?0x[0-9a-fA-F_]+$/, - resolve: (str: string) => intResolve(str, 2, 16), + resolve: (str: string, _onError: unknown, opt: ParseOptions) => + intResolve(str, 2, 16, opt), stringify: node => intStringify(node, 16, '0x') }, { diff --git a/src/tags/yaml-1.1/timestamp.ts b/src/tags/yaml-1.1/timestamp.ts index 3e9ff0d7..999ad9f3 100644 --- a/src/tags/yaml-1.1/timestamp.ts +++ b/src/tags/yaml-1.1/timestamp.ts @@ -1,16 +1,13 @@ import type { Scalar } from '../../nodes/Scalar.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' -import { intOptions } from '../options.js' import type { ScalarTag } from '../types.js' /** Internal types handle bigint as number, because TS can't figure it out. */ -function parseSexagesimal(str: string, isInt: B) { +function parseSexagesimal(str: string, asBigInt?: B) { const sign = str[0] const parts = sign === '-' || sign === '+' ? str.substring(1) : str const num = (n: unknown) => - isInt && intOptions.asBigInt - ? ((BigInt(n) as unknown) as number) - : Number(n) + asBigInt ? ((BigInt(n) as unknown) as number) : Number(n) const res = parts .replace(/_/g, '') .split(':') @@ -62,7 +59,8 @@ export const intTime: ScalarTag = { tag: 'tag:yaml.org,2002:int', format: 'TIME', test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/, - resolve: str => parseSexagesimal(str, true), + resolve: (str, _onError, { intAsBigInt }) => + parseSexagesimal(str, intAsBigInt), stringify: stringifySexagesimal } diff --git a/tests/doc/parse.js b/tests/doc/parse.js index 091a2832..ff18486d 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -106,7 +106,7 @@ describe('custom string on node', () => { }) describe('number types', () => { - describe('asBigInt: false', () => { + describe('intAsBigInt: false', () => { test('Version 1.1', () => { const src = ` - 0b10_10 @@ -119,7 +119,10 @@ describe('number types', () => { - 4.20 - .42 - 00.4` - const doc = YAML.parseDocument(src, { version: '1.1' }) + const doc = YAML.parseDocument(src, { + intAsBigInt: false, + version: '1.1' + }) expect(doc.contents.items).toMatchObject([ { value: 10, format: 'BIN' }, { value: 83, format: 'OCT' }, @@ -149,7 +152,10 @@ describe('number types', () => { - 4.20 - .42 - 00.4` - const doc = YAML.parseDocument(src, { version: '1.2' }) + const doc = YAML.parseDocument(src, { + intAsBigInt: false, + version: '1.2' + }) expect(doc.contents.items).toMatchObject([ { value: 83, format: 'OCT' }, { value: 0, format: 'OCT' }, @@ -168,16 +174,7 @@ describe('number types', () => { }) }) - describe('asBigInt: true', () => { - let prevAsBigInt - beforeAll(() => { - prevAsBigInt = YAML.scalarOptions.int.asBigInt - YAML.scalarOptions.int.asBigInt = true - }) - afterAll(() => { - YAML.scalarOptions.int.asBigInt = prevAsBigInt - }) - + describe('intAsBigInt: true', () => { test('Version 1.1', () => { const src = ` - 0b10_10 @@ -187,7 +184,7 @@ describe('number types', () => { - 3.1e+2 - 5.1_2_3E-1 - 4.02` - const doc = YAML.parseDocument(src, { version: '1.1' }) + const doc = YAML.parseDocument(src, { intAsBigInt: true, version: '1.1' }) expect(doc.contents.items).toMatchObject([ { value: 10n, format: 'BIN' }, { value: 83n, format: 'OCT' }, @@ -210,7 +207,7 @@ describe('number types', () => { - 3.1e+2 - 5.123E-1 - 4.02` - const doc = YAML.parseDocument(src, { version: '1.2' }) + const doc = YAML.parseDocument(src, { intAsBigInt: true, version: '1.2' }) expect(doc.contents.items).toMatchObject([ { value: 83n, format: 'OCT' }, { value: 0n, format: 'OCT' }, diff --git a/tests/doc/types.js b/tests/doc/types.js index 67c4816a..69b24474 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -363,18 +363,16 @@ hexadecimal: 0x_0A_74_AE binary: 0b1010_0111_0100_1010_1110 sexagesimal: 190:20:30` - try { - YAML.scalarOptions.int.asBigInt = true - const doc = YAML.parseDocument(src) - expect(doc.toJS()).toMatchObject({ - canonical: 685230n, - decimal: 685230n, - octal: 685230n, - hexadecimal: 685230n, - binary: 685230n, - sexagesimal: 685230n - }) - expect(String(doc)).toBe(`%YAML 1.1 + const doc = YAML.parseDocument(src, { intAsBigInt: true }) + expect(doc.toJS()).toMatchObject({ + canonical: 685230n, + decimal: 685230n, + octal: 685230n, + hexadecimal: 685230n, + binary: 685230n, + sexagesimal: 685230n + }) + expect(String(doc)).toBe(`%YAML 1.1 --- canonical: 685230 decimal: 685230 @@ -382,9 +380,6 @@ octal: 02472256 hexadecimal: 0xa74ae binary: 0b10100111010010101110 sexagesimal: 190:20:30\n`) - } finally { - YAML.scalarOptions.int.asBigInt = false - } }) test('!!null', () => { From 37e66887afa0c517372962cef992aabae48a17ed Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 00:55:56 +0200 Subject: [PATCH 11/20] Move falseStr, nullStr & trueStr options from scalarOptions to ParseOptions BREAKING CHANGE: Rather than setting these via `scalarOptions.bool` and `scalarOptions.null`, users should pass them as options given to the stringify() function and the doc.toString() method. --- docs/03_options.md | 6 +----- docs/04_documents.md | 25 ++++++++++++---------- src/doc/Document.ts | 31 +++++++++++++++++---------- src/nodes/Pair.ts | 5 ++++- src/options.ts | 44 ++++++++++++++++++++++---------------- src/stringify/stringify.ts | 5 ++--- src/tags/core.ts | 11 ++++------ src/tags/options.ts | 28 ------------------------ src/tags/yaml-1.1/index.ts | 20 ++++++++--------- tests/doc/parse.js | 12 +++++------ tests/doc/stringify.js | 25 ++++------------------ 11 files changed, 90 insertions(+), 122 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index a3e70ee8..382c9020 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -86,9 +86,8 @@ mergeResult.target YAML.stringify({ 'this is': null }, { simpleKeys: true }) // this is: null -YAML.scalarOptions.null.nullStr = '~' YAML.scalarOptions.str.defaultType = 'QUOTE_SINGLE' -YAML.stringify({ this: null, that: 'value' }) +YAML.stringify({ this: null, that: 'value' }, { nullStr: '~' }) // this: ~ // that: 'value' ``` @@ -101,9 +100,6 @@ Some customization options are availabe to control the parsing and stringificati | ------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | binary.defaultType | `Type` | `'BLOCK_LITERAL'` | The type of string literal used to stringify `!!binary` values | | binary.lineWidth | `number` | `76` | Maximum line width for `!!binary` values | -| bool.trueStr | `string` | `'true'` | String representation for `true` values | -| bool.falseStr | `string` | `'false'` | String representation for `false` values | -| null.nullStr | `string` | `'null'` | String representation for `null` values | | str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | | str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | | str.doubleQuoted | `object` | `{ jsonEncoding: false,` `minMultiLineLength: 40 }` | `jsonEncoding`: Whether to restrict double-quoted strings to use JSON-compatible syntax; `minMultiLineLength`: Minimum length to use multiple lines to represent the value | diff --git a/docs/04_documents.md b/docs/04_documents.md index 76ad3a53..196741aa 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -148,12 +148,12 @@ For a representation consisting only of JSON values, use **`toJSON()`**. The following options are also available when calling `YAML.parse()`: -| `toJS()` Option | Type | Description | -| --------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| mapAsMap | `boolean` | Use Map rather than Object to represent mappings. By default `false`. | -| maxAliasCount | `number` | Prevent [exponential entity expansion attacks] by limiting data aliasing; set to `-1` to disable checks; `0` disallows all alias nodes. By default `100`. | -| onAnchor | `(value: any, count: number) => void` | Optional callback for each aliased anchor in the document. | -| reviver | `(key: any, value: any) => any` | Optionally apply a [reviver function] to the output, following the JSON specification but with appropriate extensions for handling `Map` and `Set`. | +| `toJS()` Option | Type | Default value | Description | +| --------------- | ------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| mapAsMap | `boolean` | `false` | Use Map rather than Object to represent mappings. | +| maxAliasCount | `number` | `100` | Prevent [exponential entity expansion attacks] by limiting data aliasing; set to `-1` to disable checks; `0` disallows all alias nodes. | +| onAnchor | `(value: any, count: number) => void` | | Optional callback for each aliased anchor in the document. | +| reviver | `(key: any, value: any) => any` | | Optionally apply a [reviver function] to the output, following the JSON specification but with appropriate extensions for handling `Map` and `Set`. | [exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack [reviver function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter @@ -166,11 +166,14 @@ This method will throw if the `errors` array is not empty. The following options are also available when calling `YAML.stringify()`: -| `toString()` Option | Type | Description | -| ------------------- | --------- | ----------------------------------------------------------------------------------------------------- | -| indent | `number` | The number of spaces to use when indenting code. By default `2`. | -| indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | -| simpleKeys | `boolean` | Require keys to be scalars and always use implicit rather than explicit notation. By default `false`. | +| `toString()` Option | Type | Default value | Description | +| ------------------- | --------- | ------------- | --------------------------------------------------------------------------------- | +| falseStr | `string` | `'false'` | String representation for `false` values. | +| indent | `number` | `2` | The number of spaces to use when indenting code. Should be at least 2. | +| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | +| nullStr | `string` | `'null'` | String representation for `null` values. | +| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | +| trueStr | `string` | `'true'` | String representation for `true` values. | ## Working with Anchors diff --git a/src/doc/Document.ts b/src/doc/Document.ts index d2ae31a2..ef865ab4 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -397,11 +397,11 @@ export class Document { } /** A YAML representation of the document. */ - toString({ indent, indentSeq, simpleKeys }: ToStringOptions = {}) { + toString(options: ToStringOptions = {}) { if (this.errors.length > 0) throw new Error('Document with errors cannot be stringified') - const indentSize = typeof indent === 'number' ? indent : 2 + const indentSize = typeof options.indent === 'number' ? options.indent : 2 if (!Number.isInteger(indentSize) || indentSize <= 0) { const s = JSON.stringify(indentSize) throw new Error(`"indent" option must be a positive integer, not ${s}`) @@ -419,15 +419,24 @@ export class Document { if (hasDirectives || !this.directivesEndMarker) lines.unshift('') lines.unshift(this.commentBefore.replace(/^/gm, '#')) } - const ctx: StringifyContext = { - anchors: Object.create(null), - doc: this, - indent: '', - indentSeq: indentSeq !== false, // default true - indentStep: ' '.repeat(indentSize), - simpleKeys: simpleKeys === true, // default false - stringify // Requiring directly in nodes would create circular dependencies - } + + const ctx: StringifyContext = Object.assign( + { + falseStr: 'false', + indentSeq: true, + nullStr: 'null', + simpleKeys: false, + trueStr: 'true' + }, + options, + { + anchors: Object.create(null), + doc: this, + indent: '', + indentStep: ' '.repeat(indentSize), + stringify // Requiring directly in nodes would create circular dependencies + } + ) let chompKeep = false let contentComment = null if (this.contents) { diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 9f619764..74c1aa62 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -248,13 +248,16 @@ function stringifyKey( const strKey = key.toString({ anchors: Object.create(null), doc: ctx.doc, + falseStr: 'false', indent: '', indentSeq: false, indentStep: ctx.indentStep, inFlow: true, inStringifyKey: true, + nullStr: 'null', simpleKeys: false, - stringify: ctx.stringify + stringify: ctx.stringify, + trueStr: 'true' }) if (!ctx.mapKeyWarned) { let jsonStr = JSON.stringify(strKey) diff --git a/src/options.ts b/src/options.ts index 3a4331cf..47807b9a 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,12 +5,7 @@ import type { Replacer } from './doc/Document.js' import type { SchemaName } from './doc/Schema.js' import type { Pair } from './nodes/Pair.js' import type { LineCounter } from './parse/line-counter.js' -import { - binaryOptions, - boolOptions, - nullOptions, - strOptions -} from './tags/options.js' +import { binaryOptions, strOptions } from './tags/options.js' import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' export type ParseOptions = { @@ -172,6 +167,14 @@ export type ToJSOptions = { } export type ToStringOptions = { + /** + * String representation for `false`. + * With the core schema, use `'false'`, `'False'`, or `'FALSE'`. + * + * Default: `'false'` + */ + falseStr?: string + /** * The number of spaces to use when indenting code. * @@ -186,12 +189,29 @@ export type ToStringOptions = { */ indentSeq?: boolean + /** + * String representation for `null`. + * With the core schema, use `'null'`, `'Null'`, `'NULL'`, `'~'`, or an empty + * string `''`. + * + * Default: `'null'` + */ + nullStr?: string + /** * Require keys to be scalars and to use implicit rather than explicit notation. * * Default: `false` */ simpleKeys?: boolean + + /** + * String representation for `true`. + * With the core schema, use `'true'`, `'True'`, or `'TRUE'`. + * + * Default: `'true'` + */ + trueStr?: string } export type Options = ParseOptions & DocumentOptions & SchemaOptions @@ -226,18 +246,6 @@ export const scalarOptions = { set binary(opt) { Object.assign(binaryOptions, opt) }, - get bool() { - return boolOptions - }, - set bool(opt) { - Object.assign(boolOptions, opt) - }, - get null() { - return nullOptions - }, - set null(opt) { - Object.assign(nullOptions, opt) - }, get str() { return strOptions }, diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 2a348b26..360d14a0 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,20 +1,19 @@ import type { Document } from '../doc/Document.js' import { isAlias, isNode, isPair, isScalar, Node } from '../nodes/Node.js' import type { Scalar } from '../nodes/Scalar.js' +import type { ToStringOptions } from '../options.js' import type { TagObj } from '../tags/types.js' import { stringifyString } from './stringifyString.js' -export interface StringifyContext { +export type StringifyContext = Required> & { anchors: Record doc: Document forceBlockIndent?: boolean implicitKey?: boolean indent: string - indentSeq: boolean indentStep: string indentAtStart?: number inFlow?: boolean - simpleKeys: boolean stringify: typeof stringify [key: string]: unknown } diff --git a/src/tags/core.ts b/src/tags/core.ts index 7a7e0a64..2495583a 100644 --- a/src/tags/core.ts +++ b/src/tags/core.ts @@ -2,7 +2,6 @@ import { Scalar } from '../nodes/Scalar.js' import { ParseOptions } from '../options.js' import { stringifyNumber } from '../stringify/stringifyNumber.js' import { failsafe } from './failsafe/index.js' -import { boolOptions, nullOptions } from './options.js' import { ScalarTag } from './types.js' const intIdentify = (value: unknown): value is number | bigint => @@ -28,9 +27,8 @@ export const nullObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:null', test: /^(?:~|[Nn]ull|NULL)?$/, resolve: () => new Scalar(null), - options: nullOptions, - stringify: ({ source }) => - source && nullObj.test.test(source) ? source : nullOptions.nullStr + stringify: ({ source }, { nullStr }) => + source && nullObj.test.test(source) ? source : nullStr } export const boolObj: ScalarTag & { test: RegExp } = { @@ -39,13 +37,12 @@ export const boolObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:bool', test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/, resolve: str => new Scalar(str[0] === 't' || str[0] === 'T'), - options: boolOptions, - stringify({ source, value }) { + stringify({ source, value }, { falseStr, trueStr }) { if (source && boolObj.test.test(source)) { const sv = source[0] === 't' || source[0] === 'T' if (value === sv) return source } - return value ? boolOptions.trueStr : boolOptions.falseStr + return value ? trueStr : falseStr } } diff --git a/src/tags/options.ts b/src/tags/options.ts index bc6fa67e..85928284 100644 --- a/src/tags/options.ts +++ b/src/tags/options.ts @@ -21,34 +21,6 @@ export const binaryOptions = { lineWidth: 76 } -export const boolOptions = { - /** - * String representation for `true`. - * With the core schema, use `true`, `True`, or `TRUE`. - * - * Default: `'true'` - */ - trueStr: 'true', - - /** - * String representation for `false`. - * With the core schema, use `false`, `False`, or `FALSE`. - * - * Default: `'false'` - */ - falseStr: 'false' -} - -export const nullOptions = { - /** - * String representation for `null`. - * With the core schema, use `null`, `Null`, `NULL`, `~`, or an empty string. - * - * Default: `'null'` - */ - nullStr: 'null' -} - export const strOptions = { /** * The default type of string literal used to stringify values in general diff --git a/src/tags/yaml-1.1/index.ts b/src/tags/yaml-1.1/index.ts index 59ff314d..80913e02 100644 --- a/src/tags/yaml-1.1/index.ts +++ b/src/tags/yaml-1.1/index.ts @@ -1,9 +1,9 @@ import { Scalar } from '../../nodes/Scalar.js' -import { ParseOptions } from '../../options.js' +import type { ParseOptions } from '../../options.js' +import type { StringifyContext } from '../../stringify/stringify.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' import { failsafe } from '../failsafe/index.js' -import { boolOptions, nullOptions } from '../options.js' -import { ScalarTag } from '../types.js' +import type { ScalarTag } from '../types.js' import { binary } from './binary.js' import { omap } from './omap.js' import { pairs } from './pairs.js' @@ -17,15 +17,17 @@ const nullObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:null', test: /^(?:~|[Nn]ull|NULL)?$/, resolve: () => new Scalar(null), - options: nullOptions, - stringify: ({ source }) => - source && nullObj.test.test(source) ? source : nullOptions.nullStr + stringify: ({ source }, { nullStr }) => + source && nullObj.test.test(source) ? source : nullStr } -const boolStringify = ({ value, source }: Scalar) => { +function boolStringify( + { value, source }: Scalar, + { falseStr, trueStr }: StringifyContext +) { const boolObj = value ? trueObj : falseObj if (source && boolObj.test.test(source)) return source - return value ? boolOptions.trueStr : boolOptions.falseStr + return value ? trueStr : falseStr } const trueObj: ScalarTag & { test: RegExp } = { @@ -34,7 +36,6 @@ const trueObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:bool', test: /^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/, resolve: () => new Scalar(true), - options: boolOptions, stringify: boolStringify } @@ -44,7 +45,6 @@ const falseObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:bool', test: /^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/i, resolve: () => new Scalar(false), - options: boolOptions, stringify: boolStringify } diff --git a/tests/doc/parse.js b/tests/doc/parse.js index ff18486d..35807a50 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -89,17 +89,15 @@ describe('tags', () => { describe('custom string on node', () => { test('tiled null', () => { - YAML.scalarOptions.null.nullStr = '~' const doc = YAML.parse('a: null') - const str = YAML.stringify(doc, { simpleKeys: true }) + const str = YAML.stringify(doc, { nullStr: '~', simpleKeys: true }) expect(str).toBe('a: ~\n') expect(YAML.parse(str)).toEqual({ a: null }) }) test('empty string null', () => { - YAML.scalarOptions.null.nullStr = '' const doc = YAML.parse('a: null') - const str = YAML.stringify(doc, { simpleKeys: true }) + const str = YAML.stringify(doc, { nullStr: '', simpleKeys: true }) expect(str).toBe('a: \n') expect(YAML.parse(str)).toEqual({ a: null }) }) @@ -375,17 +373,17 @@ aliases: }) }) -describe('eemeli/yaml#l19', () => { +describe('eemeli/yaml#19', () => { test('map', () => { const src = 'a:\n # 123' const doc = YAML.parseDocument(src) - expect(String(doc)).toBe('a: # 123\n') + expect(String(doc)).toBe('a: null # 123\n') }) test('seq', () => { const src = '- a: # 123' const doc = YAML.parseDocument(src) - expect(String(doc)).toBe('- a: # 123\n') + expect(String(doc)).toBe('- a: null # 123\n') }) }) diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index 8e5ad9a1..efcb31ed 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -444,47 +444,30 @@ test('eemeli/yaml#87', () => { }) describe('emitter custom null/bool string', () => { - let origNullOptions - let origBoolOptions - beforeAll(() => { - origNullOptions = YAML.scalarOptions.null.nullStr - origBoolOptions = YAML.scalarOptions.bool - }) - afterAll(() => { - YAML.scalarOptions.null.nullStr = origNullOptions - YAML.scalarOptions.bool = origBoolOptions - }) - test('tiled null', () => { - YAML.scalarOptions.null.nullStr = '~' const doc = YAML.parse('a: null') - const str = YAML.stringify(doc, { simpleKeys: true }) + const str = YAML.stringify(doc, { nullStr: '~', simpleKeys: true }) expect(str).toBe('a: ~\n') expect(YAML.parse(str)).toEqual({ a: null }) }) test('empty string null', () => { - YAML.scalarOptions.null.nullStr = '' const doc = YAML.parse('a: null') - const str = YAML.stringify(doc, { simpleKeys: true }) + const str = YAML.stringify(doc, { nullStr: '', simpleKeys: true }) expect(str).toBe('a: \n') expect(YAML.parse(str)).toEqual({ a: null }) }) test('empty string camelBool', () => { - YAML.scalarOptions.bool.trueStr = 'True' - YAML.scalarOptions.bool.falseStr = 'False' const doc = YAML.parse('[true, false]') - const str = YAML.stringify(doc) + const str = YAML.stringify(doc, { trueStr: 'True', falseStr: 'False' }) expect(str).toBe('- True\n- False\n') expect(YAML.parse(str)).toEqual([true, false]) }) test('empty string upperBool', () => { - YAML.scalarOptions.bool.trueStr = 'TRUE' - YAML.scalarOptions.bool.falseStr = 'FALSE' const doc = YAML.parse('[true, false]') - const str = YAML.stringify(doc) + const str = YAML.stringify(doc, { trueStr: 'TRUE', falseStr: 'FALSE' }) expect(str).toBe('- TRUE\n- FALSE\n') expect(YAML.parse(str)).toEqual([true, false]) }) From bd3bec5fe3e0d17edf768897aa38f199e74bf2ed Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 10:34:21 +0200 Subject: [PATCH 12/20] Simplify StringifyContext creation --- src/doc/Document.ts | 34 +++++++++++----------------------- src/nodes/Collection.ts | 4 ++-- src/nodes/Pair.ts | 28 ++++++++++------------------ src/nodes/toJS.ts | 1 - src/stringify/stringify.ts | 29 ++++++++++++++++++++++++++--- src/tags/core.ts | 8 ++++---- src/tags/yaml-1.1/index.ts | 11 ++++------- 7 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index ef865ab4..d667bd74 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -26,7 +26,11 @@ import { ToStringOptions } from '../options.js' import { addComment } from '../stringify/addComment.js' -import { stringify, StringifyContext } from '../stringify/stringify.js' +import { + createStringifyContext, + stringify, + StringifyContext +} from '../stringify/stringify.js' import { Anchors } from './Anchors.js' import { Schema } from './Schema.js' import { applyReviver } from './applyReviver.js' @@ -371,7 +375,6 @@ export class Document { const ctx: ToJSContext = { anchors, doc: this, - indentStep: ' ', keep: !json, mapAsMap: mapAsMap === true, mapKeyWarned: false, @@ -400,10 +403,11 @@ export class Document { toString(options: ToStringOptions = {}) { if (this.errors.length > 0) throw new Error('Document with errors cannot be stringified') - - const indentSize = typeof options.indent === 'number' ? options.indent : 2 - if (!Number.isInteger(indentSize) || indentSize <= 0) { - const s = JSON.stringify(indentSize) + if ( + 'indent' in options && + (!Number.isInteger(options.indent) || Number(options.indent) <= 0) + ) { + const s = JSON.stringify(options.indent) throw new Error(`"indent" option must be a positive integer, not ${s}`) } @@ -420,23 +424,7 @@ export class Document { lines.unshift(this.commentBefore.replace(/^/gm, '#')) } - const ctx: StringifyContext = Object.assign( - { - falseStr: 'false', - indentSeq: true, - nullStr: 'null', - simpleKeys: false, - trueStr: 'true' - }, - options, - { - anchors: Object.create(null), - doc: this, - indent: '', - indentStep: ' '.repeat(indentSize), - stringify // Requiring directly in nodes would create circular dependencies - } - ) + const ctx: StringifyContext = createStringifyContext(this, options) let chompKeep = false let contentComment = null if (this.contents) { diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index 87fe7bfe..b91e1ade 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -2,7 +2,7 @@ import { Type } from '../constants.js' import { createNode } from '../doc/createNode.js' import type { Schema } from '../doc/Schema.js' import { addComment } from '../stringify/addComment.js' -import type { StringifyContext } from '../stringify/stringify.js' +import { stringify, StringifyContext } from '../stringify/stringify.js' import { isCollection, isNode, isPair, isScalar, NodeBase, NODE_TYPE } from './Node.js' import type { Pair } from './Pair.js' @@ -204,7 +204,7 @@ export abstract class Collection extends NodeBase { onComment?: () => void, onChompKeep?: () => void ) { - const { indent, indentStep, stringify } = ctx + const { indent, indentStep } = ctx const inFlow = this.type === Type.FLOW_MAP || this.type === Type.FLOW_SEQ || ctx.inFlow if (inFlow) itemIndent += indentStep diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 74c1aa62..6857d9a2 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -2,7 +2,11 @@ import { Type } from '../constants.js' import { createNode, CreateNodeContext } from '../doc/createNode.js' import { warn } from '../log.js' import { addComment } from '../stringify/addComment.js' -import { StringifyContext } from '../stringify/stringify.js' +import { + createStringifyContext, + stringify, + StringifyContext +} from '../stringify/stringify.js' import { Scalar } from './Scalar.js' import { toJS, ToJSContext } from './toJS.js' @@ -120,10 +124,8 @@ export class Pair extends NodeBase { allNullValues, doc, indent, - indentSeq, indentStep, - simpleKeys, - stringify + options: { indentSeq, simpleKeys } } = ctx let { key, value }: { key: K; value: V | Node | null } = this let keyComment = (isNode(key) && key.comment) || null @@ -245,20 +247,10 @@ function stringifyKey( if (jsKey === null) return '' if (typeof jsKey !== 'object') return String(jsKey) if (isNode(key) && ctx && ctx.doc) { - const strKey = key.toString({ - anchors: Object.create(null), - doc: ctx.doc, - falseStr: 'false', - indent: '', - indentSeq: false, - indentStep: ctx.indentStep, - inFlow: true, - inStringifyKey: true, - nullStr: 'null', - simpleKeys: false, - stringify: ctx.stringify, - trueStr: 'true' - }) + const strCtx = createStringifyContext(ctx.doc, {}) + strCtx.inFlow = true + strCtx.inStringifyKey = true + const strKey = key.toString(strCtx) if (!ctx.mapKeyWarned) { let jsonStr = JSON.stringify(strKey) if (jsonStr.length > 40) jsonStr = jsonStr.substring(0, 36) + '..."' diff --git a/src/nodes/toJS.ts b/src/nodes/toJS.ts index 9d8e2970..ce368d1d 100644 --- a/src/nodes/toJS.ts +++ b/src/nodes/toJS.ts @@ -12,7 +12,6 @@ export interface ToJSAnchorValue { export interface ToJSContext { anchors: Map | null doc: Document - indentStep: string keep: boolean mapAsMap: boolean mapKeyWarned: boolean diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 360d14a0..6c13df4a 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -5,7 +5,9 @@ import type { ToStringOptions } from '../options.js' import type { TagObj } from '../tags/types.js' import { stringifyString } from './stringifyString.js' -export type StringifyContext = Required> & { +export type StringifyContext = { + actualString?: boolean + allNullValues?: boolean anchors: Record doc: Document forceBlockIndent?: boolean @@ -14,10 +16,31 @@ export type StringifyContext = Required> & { indentStep: string indentAtStart?: number inFlow?: boolean - stringify: typeof stringify - [key: string]: unknown + inStringifyKey?: boolean + options: Readonly>> } +export const createStringifyContext = ( + doc: Document, + options: ToStringOptions +): StringifyContext => ({ + anchors: Object.create(null), + doc, + indent: '', + indentStep: + typeof options.indent === 'number' ? ' '.repeat(options.indent) : ' ', + options: Object.assign( + { + falseStr: 'false', + indentSeq: true, + nullStr: 'null', + simpleKeys: false, + trueStr: 'true' + }, + options + ) +}) + function getTagObject(tags: TagObj[], item: Node) { if (item.tag) { const match = tags.filter(t => t.tag === item.tag) diff --git a/src/tags/core.ts b/src/tags/core.ts index 2495583a..59559791 100644 --- a/src/tags/core.ts +++ b/src/tags/core.ts @@ -27,8 +27,8 @@ export const nullObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:null', test: /^(?:~|[Nn]ull|NULL)?$/, resolve: () => new Scalar(null), - stringify: ({ source }, { nullStr }) => - source && nullObj.test.test(source) ? source : nullStr + stringify: ({ source }, ctx) => + source && nullObj.test.test(source) ? source : ctx.options.nullStr } export const boolObj: ScalarTag & { test: RegExp } = { @@ -37,12 +37,12 @@ export const boolObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:bool', test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/, resolve: str => new Scalar(str[0] === 't' || str[0] === 'T'), - stringify({ source, value }, { falseStr, trueStr }) { + stringify({ source, value }, ctx) { if (source && boolObj.test.test(source)) { const sv = source[0] === 't' || source[0] === 'T' if (value === sv) return source } - return value ? trueStr : falseStr + return value ? ctx.options.trueStr : ctx.options.falseStr } } diff --git a/src/tags/yaml-1.1/index.ts b/src/tags/yaml-1.1/index.ts index 80913e02..31f2b8fd 100644 --- a/src/tags/yaml-1.1/index.ts +++ b/src/tags/yaml-1.1/index.ts @@ -17,17 +17,14 @@ const nullObj: ScalarTag & { test: RegExp } = { tag: 'tag:yaml.org,2002:null', test: /^(?:~|[Nn]ull|NULL)?$/, resolve: () => new Scalar(null), - stringify: ({ source }, { nullStr }) => - source && nullObj.test.test(source) ? source : nullStr + stringify: ({ source }, ctx) => + source && nullObj.test.test(source) ? source : ctx.options.nullStr } -function boolStringify( - { value, source }: Scalar, - { falseStr, trueStr }: StringifyContext -) { +function boolStringify({ value, source }: Scalar, ctx: StringifyContext) { const boolObj = value ? trueObj : falseObj if (source && boolObj.test.test(source)) return source - return value ? trueStr : falseStr + return value ? ctx.options.trueStr : ctx.options.falseStr } const trueObj: ScalarTag & { test: RegExp } = { From 3ccdb134c8b6e5f3910d669206acc6ae50312b8b Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 00:55:56 +0200 Subject: [PATCH 13/20] Move lineWidth & minContentWidth options from scalarOptions to ParseOptions BREAKING CHANGE: Rather than setting these via `scalarOptions.str.fold`, users should pass them as options given to the stringify() function and the doc.toString() method. --- docs/03_options.md | 1 - docs/04_documents.md | 18 ++++++++------ src/options.ts | 18 ++++++++++++++ src/stringify/stringify.ts | 2 ++ src/stringify/stringifyString.ts | 14 ++++++----- src/tags/options.ts | 16 ------------ tests/doc/YAML-1.2.spec.js | 8 +----- tests/doc/foldFlowLines.js | 42 +++++++++----------------------- tests/doc/stringify.js | 36 ++++++++++++--------------- tests/doc/types.js | 10 +------- tests/yaml-test-suite.js | 14 ----------- 11 files changed, 67 insertions(+), 112 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 382c9020..c8fcf646 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -103,7 +103,6 @@ Some customization options are availabe to control the parsing and stringificati | str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | | str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | | str.doubleQuoted | `object` | `{ jsonEncoding: false,` `minMultiLineLength: 40 }` | `jsonEncoding`: Whether to restrict double-quoted strings to use JSON-compatible syntax; `minMultiLineLength`: Minimum length to use multiple lines to represent the value | -| str.fold | `object` | `{ lineWidth: 80,` `minContentWidth: 20 }` | `lineWidth`: Maximum line width (set to `0` to disable folding); `minContentWidth`: Minimum width for highly-indented content | ## Silencing Warnings diff --git a/docs/04_documents.md b/docs/04_documents.md index 196741aa..2d8bbc1d 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -166,14 +166,16 @@ This method will throw if the `errors` array is not empty. The following options are also available when calling `YAML.stringify()`: -| `toString()` Option | Type | Default value | Description | -| ------------------- | --------- | ------------- | --------------------------------------------------------------------------------- | -| falseStr | `string` | `'false'` | String representation for `false` values. | -| indent | `number` | `2` | The number of spaces to use when indenting code. Should be at least 2. | -| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | -| nullStr | `string` | `'null'` | String representation for `null` values. | -| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | -| trueStr | `string` | `'true'` | String representation for `true` values. | +| `toString()` Option | Type | Default value | Description | +| ------------------- | --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| falseStr | `string` | `'false'` | String representation for `false` values. | +| indent | `number` | `2` | The number of spaces to use when indenting code. Should be at least 2. | +| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | +| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | +| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | +| nullStr | `string` | `'null'` | String representation for `null` values. | +| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | +| trueStr | `string` | `'true'` | String representation for `true` values. | ## Working with Anchors diff --git a/src/options.ts b/src/options.ts index 47807b9a..53115c93 100644 --- a/src/options.ts +++ b/src/options.ts @@ -189,6 +189,24 @@ export type ToStringOptions = { */ indentSeq?: boolean + /** + * Maximum line width (set to `0` to disable folding). + * + * This is a soft limit, as only double-quoted semantics allow for inserting + * a line break in the middle of a word, as well as being influenced by the + * `minContentWidth` option. + * + * Default: `80` + */ + lineWidth?: number + + /** + * Minimum line width for highly-indented content (set to `0` to disable). + * + * Default: `20` + */ + minContentWidth?: number + /** * String representation for `null`. * With the core schema, use `'null'`, `'Null'`, `'NULL'`, `'~'`, or an empty diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 6c13df4a..931c20ab 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -33,6 +33,8 @@ export const createStringifyContext = ( { falseStr: 'false', indentSeq: true, + lineWidth: 80, + minContentWidth: 20, nullStr: 'null', simpleKeys: false, trueStr: 'true' diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index 87007774..bf0492a5 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -4,6 +4,7 @@ import { strOptions } from '../tags/options.js' import { addCommentBefore } from './addComment.js' import { foldFlowLines, + FoldOptions, FOLD_BLOCK, FOLD_FLOW, FOLD_QUOTED @@ -14,10 +15,11 @@ interface StringifyScalar extends Scalar { value: string } -const getFoldOptions = ({ indentAtStart }: StringifyContext) => - indentAtStart - ? Object.assign({ indentAtStart }, strOptions.fold) - : strOptions.fold +const getFoldOptions = (ctx: StringifyContext): FoldOptions => ({ + indentAtStart: ctx.indentAtStart, + lineWidth: ctx.options.lineWidth, + minContentWidth: ctx.options.minContentWidth +}) // Also checks for lines starting with %, as parsing the output as YAML 1.1 will // presume that's starting a new document. @@ -163,7 +165,7 @@ function blockString( ? false : type === Type.BLOCK_LITERAL ? true - : !lineLengthOverLimit(value, strOptions.fold.lineWidth, indent.length) + : !lineLengthOverLimit(value, ctx.options.lineWidth, indent.length) let header = literal ? '|' : '>' if (!value) return header + '\n' let wsStart = '' @@ -211,7 +213,7 @@ function blockString( `${wsStart}${value}${wsEnd}`, indent, FOLD_BLOCK, - strOptions.fold + getFoldOptions(ctx) ) return `${header}\n${indent}${body}` } diff --git a/src/tags/options.ts b/src/tags/options.ts index 85928284..a06fe940 100644 --- a/src/tags/options.ts +++ b/src/tags/options.ts @@ -57,21 +57,5 @@ export const strOptions = { * Default: `40` */ minMultiLineLength: 40 - }, - - fold: { - /** - * Maximum line width (set to `0` to disable folding). - * - * Default: `80` - */ - lineWidth: 80, - - /** - * Minimum width for highly-indented content. - * - * Default: `20` - */ - minContentWidth: 20 } } diff --git a/tests/doc/YAML-1.2.spec.js b/tests/doc/YAML-1.2.spec.js index ed023816..39146ea3 100644 --- a/tests/doc/YAML-1.2.spec.js +++ b/tests/doc/YAML-1.2.spec.js @@ -1851,15 +1851,10 @@ matches %: 20`, } } -let origFoldOptions, origPrettyErrors +let origPrettyErrors const mockWarn = jest.spyOn(global.process, 'emitWarning').mockImplementation() beforeAll(() => { - origFoldOptions = YAML.scalarOptions.str.fold - YAML.scalarOptions.str.fold = { - lineWidth: 20, - minContentWidth: 0 - } origPrettyErrors = YAML.defaultOptions.prettyErrors YAML.defaultOptions.prettyErrors = false }) @@ -1867,7 +1862,6 @@ beforeAll(() => { beforeEach(() => mockWarn.mockClear()) afterAll(() => { - YAML.scalarOptions.str.fold = origFoldOptions YAML.defaultOptions.prettyErrors = origPrettyErrors mockWarn.mockRestore() }) diff --git a/tests/doc/foldFlowLines.js b/tests/doc/foldFlowLines.js index 478c8df7..a70160f6 100644 --- a/tests/doc/foldFlowLines.js +++ b/tests/doc/foldFlowLines.js @@ -251,19 +251,7 @@ describe('double-quoted', () => { }) describe('end-to-end', () => { - let origFoldOptions - - beforeAll(() => { - origFoldOptions = YAML.scalarOptions.str.fold - YAML.scalarOptions.str.fold = { - lineWidth: 20, - minContentWidth: 0 - } - }) - - afterAll(() => { - YAML.scalarOptions.str.fold = origFoldOptions - }) + const foldOptions = { lineWidth: 20, minContentWidth: 0 } test('more-indented folded block', () => { const src = `> # comment with an excessive length that won't get folded @@ -291,12 +279,12 @@ folded but is not. Unfolded paragraph.\n` ) - expect(String(doc)).toBe(src) + expect(doc.toString(foldOptions)).toBe(src) }) test('eemeli/yaml#55', () => { const str = ' first more-indented line\nnext line\n' - const ys = YAML.stringify(str) + const ys = YAML.stringify(str, foldOptions) expect(ys).toBe('>1\n first more-indented line\nnext line\n') }) @@ -310,28 +298,20 @@ Unfolded paragraph.\n` 'plain value with enough length to fold twice' ) expect(doc.contents.items[1].value).toBe('plain with comment') - expect(String(doc)).toBe(src) + expect(doc.toString(foldOptions)).toBe(src) }) test('long line width', () => { - const src = { - lorem: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In laoreet massa eros, dignissim aliquam nunc elementum sit amet. Mauris pulvinar nunc eget ante sodales viverra. Vivamus quis convallis sapien, ut auctor magna. Cras volutpat erat eu lacus luctus facilisis. Aenean sapien leo, auctor sed tincidunt at, scelerisque a urna. Nunc ullamcorper, libero non mollis aliquet, nulla diam lobortis neque, ac rutrum dui nibh iaculis lectus. Aenean lobortis interdum arcu eget sollicitudin.\n\nDuis quam enim, ultricies a enim non, tincidunt lobortis ipsum. Mauris condimentum ultrices eros rutrum euismod. Fusce et mi eget quam dapibus blandit. Maecenas sodales tempor euismod. Phasellus vulputate purus felis, eleifend ullamcorper tortor semper sit amet. Sed nunc quam, iaculis et neque sit amet, consequat egestas lectus. Aenean dapibus lorem sed accumsan porttitor. Curabitur eu magna congue, mattis urna ac, lacinia eros. In in iaculis justo, nec faucibus enim. Fusce id viverra purus, nec ultricies mi. Aliquam eu risus risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. \n' - } + const src = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. In laoreet massa eros, dignissim aliquam nunc elementum sit amet. Mauris pulvinar nunc eget ante sodales viverra. Vivamus quis convallis sapien, ut auctor magna. Cras volutpat erat eu lacus luctus facilisis. Aenean sapien leo, auctor sed tincidunt at, scelerisque a urna. Nunc ullamcorper, libero non mollis aliquet, nulla diam lobortis neque, ac rutrum dui nibh iaculis lectus. Aenean lobortis interdum arcu eget sollicitudin. - YAML.scalarOptions.str.fold.lineWidth = 1000 - const ysWithLineWidthGreater = YAML.stringify(src) +Duis quam enim, ultricies a enim non, tincidunt lobortis ipsum. Mauris condimentum ultrices eros rutrum euismod. Fusce et mi eget quam dapibus blandit. Maecenas sodales tempor euismod. Phasellus vulputate purus felis, eleifend ullamcorper tortor semper sit amet. Sed nunc quam, iaculis et neque sit amet, consequat egestas lectus. Aenean dapibus lorem sed accumsan porttitor. Curabitur eu magna congue, mattis urna ac, lacinia eros. In in iaculis justo, nec faucibus enim. Fusce id viverra purus, nec ultricies mi. Aliquam eu risus risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. \n` - YAML.scalarOptions.str.fold.lineWidth = 0 - const ysWithUnlimitedLength = YAML.stringify(src) + const exp = `| +Lorem ipsum dolor sit amet, consectetur adipiscing elit. In laoreet massa eros, dignissim aliquam nunc elementum sit amet. Mauris pulvinar nunc eget ante sodales viverra. Vivamus quis convallis sapien, ut auctor magna. Cras volutpat erat eu lacus luctus facilisis. Aenean sapien leo, auctor sed tincidunt at, scelerisque a urna. Nunc ullamcorper, libero non mollis aliquet, nulla diam lobortis neque, ac rutrum dui nibh iaculis lectus. Aenean lobortis interdum arcu eget sollicitudin. - YAML.scalarOptions.str.fold.lineWidth = -1 - const ysWithUnlimitedLength2 = YAML.stringify(src) +Duis quam enim, ultricies a enim non, tincidunt lobortis ipsum. Mauris condimentum ultrices eros rutrum euismod. Fusce et mi eget quam dapibus blandit. Maecenas sodales tempor euismod. Phasellus vulputate purus felis, eleifend ullamcorper tortor semper sit amet. Sed nunc quam, iaculis et neque sit amet, consequat egestas lectus. Aenean dapibus lorem sed accumsan porttitor. Curabitur eu magna congue, mattis urna ac, lacinia eros. In in iaculis justo, nec faucibus enim. Fusce id viverra purus, nec ultricies mi. Aliquam eu risus risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. \n` - expect(ysWithLineWidthGreater).toBe('lorem: |\n' + - ' Lorem ipsum dolor sit amet, consectetur adipiscing elit. In laoreet massa eros, dignissim aliquam nunc elementum sit amet. Mauris pulvinar nunc eget ante sodales viverra. Vivamus quis convallis sapien, ut auctor magna. Cras volutpat erat eu lacus luctus facilisis. Aenean sapien leo, auctor sed tincidunt at, scelerisque a urna. Nunc ullamcorper, libero non mollis aliquet, nulla diam lobortis neque, ac rutrum dui nibh iaculis lectus. Aenean lobortis interdum arcu eget sollicitudin.\n' + - '\n' + - ' Duis quam enim, ultricies a enim non, tincidunt lobortis ipsum. Mauris condimentum ultrices eros rutrum euismod. Fusce et mi eget quam dapibus blandit. Maecenas sodales tempor euismod. Phasellus vulputate purus felis, eleifend ullamcorper tortor semper sit amet. Sed nunc quam, iaculis et neque sit amet, consequat egestas lectus. Aenean dapibus lorem sed accumsan porttitor. Curabitur eu magna congue, mattis urna ac, lacinia eros. In in iaculis justo, nec faucibus enim. Fusce id viverra purus, nec ultricies mi. Aliquam eu risus risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. \n') - expect(ysWithUnlimitedLength).toBe(ysWithLineWidthGreater) - expect(ysWithUnlimitedLength2).toBe(ysWithLineWidthGreater) + for (const lineWidth of [1000, 0, -1]) + expect(YAML.stringify(src, { lineWidth })).toBe(exp) }) }) diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index efcb31ed..cf9f2072 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -102,39 +102,32 @@ for (const [name, version] of [ }) describe('string', () => { - let origFoldOptions - beforeAll(() => { - origFoldOptions = YAML.scalarOptions.str.fold - YAML.scalarOptions.str.fold = { - lineWidth: 20, - minContentWidth: 0 - } - }) - afterAll(() => { - YAML.scalarOptions.str.fold = origFoldOptions - }) + const foldOptions = { lineWidth: 20, minContentWidth: 0 } test('plain', () => { - expect(YAML.stringify('STR')).toBe('STR\n') + expect(YAML.stringify('STR', foldOptions)).toBe('STR\n') }) test('double-quoted', () => { - expect(YAML.stringify('"x"')).toBe('\'"x"\'\n') + expect(YAML.stringify('"x"', foldOptions)).toBe('\'"x"\'\n') }) test('single-quoted', () => { - expect(YAML.stringify("'x'")).toBe('"\'x\'"\n') + expect(YAML.stringify("'x'", foldOptions)).toBe('"\'x\'"\n') }) test('escaped', () => { - expect(YAML.stringify('null: \u0000')).toBe('"null: \\0"\n') + expect(YAML.stringify('null: \u0000', foldOptions)).toBe( + '"null: \\0"\n' + ) }) test('short multiline', () => { - expect(YAML.stringify('blah\nblah\nblah')).toBe( + expect(YAML.stringify('blah\nblah\nblah', foldOptions)).toBe( '|-\nblah\nblah\nblah\n' ) }) test('long multiline', () => { expect( YAML.stringify( - 'blah blah\nblah blah blah blah blah blah blah blah blah blah\n' + 'blah blah\nblah blah blah blah blah blah blah blah blah blah\n', + foldOptions ) ).toBe(`> blah blah @@ -150,7 +143,8 @@ blah blah\n`) for (const node of doc.contents.items) node.value.type = Type.QUOTE_DOUBLE expect( - String(doc) + doc + .toString(foldOptions) .split('\n') .map(line => line.length) ).toMatchObject([20, 20, 20, 20, 0]) @@ -161,7 +155,8 @@ blah blah\n`) const doc = new YAML.Document([foo]) for (const node of doc.contents.items) node.type = Type.QUOTE_DOUBLE expect( - String(doc) + doc + .toString(foldOptions) .split('\n') .map(line => line.length) ).toMatchObject([20, 20, 20, 17, 0]) @@ -173,7 +168,8 @@ blah blah\n`) const seq = doc.contents.items[0].value for (const node of seq.items) node.type = Type.QUOTE_DOUBLE expect( - String(doc) + doc + .toString(foldOptions) .split('\n') .map(line => line.length) ).toMatchObject([4, 20, 20, 20, 20, 10, 0]) diff --git a/tests/doc/types.js b/tests/doc/types.js index 69b24474..b76449a8 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -4,20 +4,14 @@ import { binary } from '../../src/tags/yaml-1.1/binary.js' import { YAMLOMap } from '../../src/tags/yaml-1.1/omap.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' -let origFoldOptions, origPrettyErrors +let origPrettyErrors beforeAll(() => { - origFoldOptions = YAML.scalarOptions.str.fold - YAML.scalarOptions.str.fold = { - lineWidth: 20, - minContentWidth: 0 - } origPrettyErrors = YAML.defaultOptions.prettyErrors YAML.defaultOptions.prettyErrors = false }) afterAll(() => { - YAML.scalarOptions.str.fold = origFoldOptions YAML.defaultOptions.prettyErrors = origPrettyErrors }) @@ -260,7 +254,6 @@ description: genericStr += String.fromCharCode(generic[i]) expect(canonicalStr).toBe(genericStr) expect(canonicalStr.substr(0, 5)).toBe('GIF89') - YAML.scalarOptions.str.fold.lineWidth = 80 expect(String(doc)) .toBe(`canonical: !!binary "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J\\ +fn5OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/\\ @@ -272,7 +265,6 @@ generic: !!binary |- ZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYN G84BwwEeECcgggoBADs= description: The binary value above is a tiny arrow encoded as a gif image.\n`) - YAML.scalarOptions.str.fold.lineWidth = 20 }) test('!!bool', () => { diff --git a/tests/yaml-test-suite.js b/tests/yaml-test-suite.js index 2d17e20c..dfe23edb 100644 --- a/tests/yaml-test-suite.js +++ b/tests/yaml-test-suite.js @@ -30,20 +30,6 @@ const matchJson = (docs, json) => { } } -let origFoldOptions - -beforeAll(() => { - origFoldOptions = YAML.scalarOptions.str.fold - YAML.scalarOptions.str.fold = { - lineWidth: 20, - minContentWidth: 0 - } -}) - -afterAll(() => { - YAML.scalarOptions.str.fold = origFoldOptions -}) - testDirs.forEach(dir => { const root = path.resolve(__dirname, 'yaml-test-suite', dir) const name = fs.readFileSync(path.resolve(root, '==='), 'utf8').trim() From 5f1df4979117bdb72161d6559ef06f45e476c914 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 11:54:03 +0200 Subject: [PATCH 14/20] Drop special options for !!binary BREAKING CHANGE: The line width now follows the general `lineWidth` value and takes indent level into account. `!!binary` nodes that do not explicitly declare a `type` will use `BLOCK_LITERAL`. To set the type of such nodes, use a visitor: import { visit } from 'yaml' visit(doc, { Scalar(key, value) { if (value.tag === 'tag:yaml.org,2002:binary') value.type = 'QUOTE_DOUBLE' } }) --- docs/03_options.md | 2 -- src/options.ts | 8 +------- src/tags/options.ts | 21 --------------------- src/tags/yaml-1.1/binary.ts | 9 +++++---- tests/doc/types.js | 8 ++++---- 5 files changed, 10 insertions(+), 38 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index c8fcf646..d4381c8b 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -98,8 +98,6 @@ Some customization options are availabe to control the parsing and stringificati | Option | Type | Default value | Description | | ------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| binary.defaultType | `Type` | `'BLOCK_LITERAL'` | The type of string literal used to stringify `!!binary` values | -| binary.lineWidth | `number` | `76` | Maximum line width for `!!binary` values | | str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | | str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | | str.doubleQuoted | `object` | `{ jsonEncoding: false,` `minMultiLineLength: 40 }` | `jsonEncoding`: Whether to restrict double-quoted strings to use JSON-compatible syntax; `minMultiLineLength`: Minimum length to use multiple lines to represent the value | diff --git a/src/options.ts b/src/options.ts index 53115c93..a6e65731 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,7 +5,7 @@ import type { Replacer } from './doc/Document.js' import type { SchemaName } from './doc/Schema.js' import type { Pair } from './nodes/Pair.js' import type { LineCounter } from './parse/line-counter.js' -import { binaryOptions, strOptions } from './tags/options.js' +import { strOptions } from './tags/options.js' import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' export type ParseOptions = { @@ -258,12 +258,6 @@ export const defaultOptions: Required< * stringification of scalars. Note that these values are used by all documents. */ export const scalarOptions = { - get binary() { - return binaryOptions - }, - set binary(opt) { - Object.assign(binaryOptions, opt) - }, get str() { return strOptions }, diff --git a/src/tags/options.ts b/src/tags/options.ts index a06fe940..eece80bd 100644 --- a/src/tags/options.ts +++ b/src/tags/options.ts @@ -1,26 +1,5 @@ import { Type } from '../constants.js' -export const binaryOptions = { - /** - * The type of string literal used to stringify `!!binary` values. - * - * Default: `'BLOCK_LITERAL'` - */ - defaultType: Type.BLOCK_LITERAL as - | Type.BLOCK_FOLDED - | Type.BLOCK_LITERAL - | Type.PLAIN - | Type.QUOTE_DOUBLE - | Type.QUOTE_SINGLE, - - /** - * Maximum line width for `!!binary`. - * - * Default: `76` - */ - lineWidth: 76 -} - export const strOptions = { /** * The default type of string literal used to stringify values in general diff --git a/src/tags/yaml-1.1/binary.ts b/src/tags/yaml-1.1/binary.ts index f0ff90b9..53d6c887 100644 --- a/src/tags/yaml-1.1/binary.ts +++ b/src/tags/yaml-1.1/binary.ts @@ -1,14 +1,12 @@ import { Type } from '../../constants.js' import type { Scalar } from '../../nodes/Scalar.js' import { stringifyString } from '../../stringify/stringifyString.js' -import { binaryOptions as options } from '../options.js' import type { ScalarTag } from '../types.js' export const binary: ScalarTag = { identify: value => value instanceof Uint8Array, // Buffer inherits from Uint8Array default: false, tag: 'tag:yaml.org,2002:binary', - options, /** * Returns a Buffer in node and an Uint8Array in browsers @@ -53,9 +51,12 @@ export const binary: ScalarTag = { ) } - if (!type) type = options.defaultType + if (!type) type = Type.BLOCK_LITERAL if (type !== Type.QUOTE_DOUBLE) { - const { lineWidth } = options + const lineWidth = Math.max( + ctx.options.lineWidth - ctx.indent.length, + ctx.options.minContentWidth + ) const n = Math.ceil(str.length / lineWidth) const lines = new Array(n) for (let i = 0, o = 0; i < n; ++i, o += lineWidth) { diff --git a/tests/doc/types.js b/tests/doc/types.js index b76449a8..18df69ed 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -260,10 +260,10 @@ description: ++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BN\\ JHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=" generic: !!binary |- - R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5OTk6enp56enmlp - aWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++SH+Dk1h - ZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYN - G84BwwEeECcgggoBADs= + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5OTk6enp56enmlpaW + NjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++SH+Dk1hZGUg + d2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84Bww + EeECcgggoBADs= description: The binary value above is a tiny arrow encoded as a gif image.\n`) }) From 21d4060f76946b2060ff893802a8edd85f218f6c Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 00:55:56 +0200 Subject: [PATCH 15/20] Move double-quoted string options from scalarOptions to ParseOptions The options are also renamed: - doubleQuoted.jsonEncoding -> doubleQuotedAsJSON - doubleQuoted.minMultiLineLength -> doubleQuotedMinMultiLineLength BREAKING CHANGE: Rather than setting these via `scalarOptions.str.doubleQuoted`, users should pass them as options given to the stringify() function and the doc.toString() method. --- docs/03_options.md | 1 - docs/04_documents.md | 22 ++++++++++++---------- src/options.ts | 15 +++++++++++++++ src/stringify/stringify.ts | 2 ++ src/stringify/stringifyString.ts | 7 ++++--- src/tags/options.ts | 16 ---------------- tests/doc/stringify.js | 14 +------------- 7 files changed, 34 insertions(+), 43 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index d4381c8b..b0d609fa 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -100,7 +100,6 @@ Some customization options are availabe to control the parsing and stringificati | ------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | | str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | -| str.doubleQuoted | `object` | `{ jsonEncoding: false,` `minMultiLineLength: 40 }` | `jsonEncoding`: Whether to restrict double-quoted strings to use JSON-compatible syntax; `minMultiLineLength`: Minimum length to use multiple lines to represent the value | ## Silencing Warnings diff --git a/docs/04_documents.md b/docs/04_documents.md index 2d8bbc1d..6bf55e72 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -166,16 +166,18 @@ This method will throw if the `errors` array is not empty. The following options are also available when calling `YAML.stringify()`: -| `toString()` Option | Type | Default value | Description | -| ------------------- | --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| falseStr | `string` | `'false'` | String representation for `false` values. | -| indent | `number` | `2` | The number of spaces to use when indenting code. Should be at least 2. | -| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | -| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | -| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | -| nullStr | `string` | `'null'` | String representation for `null` values. | -| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | -| trueStr | `string` | `'true'` | String representation for `true` values. | +| `toString()` Option | Type | Default value | Description | +| ------------------------------ | --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | +| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | +| falseStr | `string` | `'false'` | String representation for `false` values. | +| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | +| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | +| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | +| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | +| nullStr | `string` | `'null'` | String representation for `null` values. | +| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | +| trueStr | `string` | `'true'` | String representation for `true` values. | ## Working with Anchors diff --git a/src/options.ts b/src/options.ts index a6e65731..9b92a8d0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -167,6 +167,21 @@ export type ToJSOptions = { } export type ToStringOptions = { + /** + * Restrict double-quoted strings to use JSON-compatible syntax. + * + * Default: `false` + */ + doubleQuotedAsJSON?: boolean + + /** + * Minimum length for double-quoted strings to use multiple lines to + * represent the value. Ignored if `doubleQuotedAsJSON` is set. + * + * Default: `40` + */ + doubleQuotedMinMultiLineLength?: number + /** * String representation for `false`. * With the core schema, use `'false'`, `'False'`, or `'FALSE'`. diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 931c20ab..bc572293 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -31,6 +31,8 @@ export const createStringifyContext = ( typeof options.indent === 'number' ? ' '.repeat(options.indent) : ' ', options: Object.assign( { + doubleQuotedAsJSON: false, + doubleQuotedMinMultiLineLength: 40, falseStr: 'false', indentSeq: true, lineWidth: 80, diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index bf0492a5..a604dc61 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -41,10 +41,11 @@ function lineLengthOverLimit(str: string, lineWidth: number, indentLength: numbe } function doubleQuotedString(value: string, ctx: StringifyContext) { - const { implicitKey } = ctx - const { jsonEncoding, minMultiLineLength } = strOptions.doubleQuoted const json = JSON.stringify(value) - if (jsonEncoding) return json + if (ctx.options.doubleQuotedAsJSON) return json + + const { implicitKey } = ctx + const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '') let str = '' let start = 0 diff --git a/src/tags/options.ts b/src/tags/options.ts index eece80bd..795f0dc2 100644 --- a/src/tags/options.ts +++ b/src/tags/options.ts @@ -21,20 +21,4 @@ export const strOptions = { * Default: `false` */ defaultQuoteSingle: false, - - doubleQuoted: { - /** - * Whether to restrict double-quoted strings to use JSON-compatible syntax. - * - * Default: `false` - */ - jsonEncoding: false, - - /** - * Minimum length to use multiple lines to represent the value. - * - * Default: `40` - */ - minMultiLineLength: 40 - } } diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index cf9f2072..0c06cc27 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -863,18 +863,6 @@ describe('Scalar options', () => { }) describe('Document markers in top-level scalars', () => { - let origDoubleQuotedOptions - beforeAll(() => { - origDoubleQuotedOptions = YAML.scalarOptions.str.doubleQuoted - YAML.scalarOptions.str.doubleQuoted = { - jsonEncoding: false, - minMultiLineLength: 0 - } - }) - afterAll(() => { - YAML.scalarOptions.str.doubleQuoted = origDoubleQuotedOptions - }) - test('---', () => { const str = YAML.stringify('---') expect(str).toBe('|-\n ---\n') @@ -904,7 +892,7 @@ describe('Document markers in top-level scalars', () => { test('"foo\\n..."', () => { const doc = new YAML.Document('foo\n...') doc.contents.type = Type.QUOTE_DOUBLE - const str = String(doc) + const str = doc.toString({ doubleQuotedMinMultiLineLength: 0 }) expect(str).toBe('"foo\n\n ..."\n') expect(YAML.parse(str)).toBe('foo\n...') }) From 67de2e1d333b50ae1f77762b719b87611598237f Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 00:55:56 +0200 Subject: [PATCH 16/20] Move remaining string options from scalarOptions to ParseOptions The `defaultKeyType` option now accepts (and defaults to) `null`, with which value the `defaultStringType` also controls implicit key types. Some of the options are renamed: - defaultType -> defaultStringType - defaultKeyType -> defaultKeyType (unchanged) - defaultQuoteSingle -> singleQuote BREAKING CHANGE: Rather than setting these via `scalarOptions.str`, users should pass them as options given to the stringify() function and the doc.toString() method. --- docs/03_options.md | 12 +-- docs/04_documents.md | 27 ++++--- src/options.ts | 45 +++++++++-- src/stringify/stringify.ts | 4 + src/stringify/stringifyString.ts | 7 +- src/tags/failsafe/string.ts | 4 +- src/tags/options.ts | 24 ------ tests/doc/stringify.js | 126 ++++++++++++------------------- 8 files changed, 112 insertions(+), 137 deletions(-) delete mode 100644 src/tags/options.ts diff --git a/docs/03_options.md b/docs/03_options.md index b0d609fa..771bda9f 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -86,21 +86,15 @@ mergeResult.target YAML.stringify({ 'this is': null }, { simpleKeys: true }) // this is: null -YAML.scalarOptions.str.defaultType = 'QUOTE_SINGLE' -YAML.stringify({ this: null, that: 'value' }, { nullStr: '~' }) -// this: ~ -// that: 'value' +YAML.stringify({ this: null, that: 'value' }, { defaultStringType: 'QUOTE_SINGLE', nullStr: '~' }) +// 'this': ~ +// 'that': 'value' ``` #### `YAML.scalarOptions` Some customization options are availabe to control the parsing and stringification of scalars. Note that these values are used by all documents. -| Option | Type | Default value | Description | -| ------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| str.defaultType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values in general | -| str.defaultKeyType | `Type` | `'PLAIN'` | The default type of string literal used to stringify implicit key values | - ## Silencing Warnings By default, the library will emit warnings as required by the YAML spec during parsing. diff --git a/docs/04_documents.md b/docs/04_documents.md index 6bf55e72..a57749f6 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -166,18 +166,21 @@ This method will throw if the `errors` array is not empty. The following options are also available when calling `YAML.stringify()`: -| `toString()` Option | Type | Default value | Description | -| ------------------------------ | --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | -| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | -| falseStr | `string` | `'false'` | String representation for `false` values. | -| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | -| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | -| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | -| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | -| nullStr | `string` | `'null'` | String representation for `null` values. | -| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | -| trueStr | `string` | `'true'` | String representation for `true` values. | +| `toString()` Option | Type | Default value | Description | +| ------------------------------ | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| defaultKeyType | `Type ⎮ null` | `null` | If not `null`, overrides `defaultStringType` for implicit key values. | +| defaultStringType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values. | +| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | +| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | +| falseStr | `string` | `'false'` | String representation for `false` values. | +| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | +| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | +| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | +| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | +| nullStr | `string` | `'null'` | String representation for `null` values. | +| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | +| singleQuote | `boolean` | `false` | Prefer 'single quote' rather than "double quote" where applicable. | +| trueStr | `string` | `'true'` | String representation for `true` values. | ## Working with Anchors diff --git a/src/options.ts b/src/options.ts index 9b92a8d0..c01b7178 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,11 +1,10 @@ -import { LogLevelId } from './constants.js' +import { LogLevelId, Type } from './constants.js' import type { Reviver } from './doc/applyReviver.js' import type { Directives } from './doc/directives.js' import type { Replacer } from './doc/Document.js' import type { SchemaName } from './doc/Schema.js' import type { Pair } from './nodes/Pair.js' import type { LineCounter } from './parse/line-counter.js' -import { strOptions } from './tags/options.js' import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' export type ParseOptions = { @@ -167,6 +166,35 @@ export type ToJSOptions = { } export type ToStringOptions = { + /** + * The default type of string literal used to stringify implicit key values. + * Output may use other types if required to fully represent the value. + * + * If `null`, the value of `defaultStringType` is used. + * + * Default: `null` + */ + defaultKeyType?: + | null + | Type.BLOCK_FOLDED + | Type.BLOCK_LITERAL + | Type.PLAIN + | Type.QUOTE_DOUBLE + | Type.QUOTE_SINGLE + + /** + * The default type of string literal used to stringify values in general. + * Output may use other types if required to fully represent the value. + * + * Default: `'PLAIN'` + */ + defaultStringType?: + | Type.BLOCK_FOLDED + | Type.BLOCK_LITERAL + | Type.PLAIN + | Type.QUOTE_DOUBLE + | Type.QUOTE_SINGLE + /** * Restrict double-quoted strings to use JSON-compatible syntax. * @@ -238,6 +266,13 @@ export type ToStringOptions = { */ simpleKeys?: boolean + /** + * Prefer 'single quote' rather than "double quote" where applicable. + * + * Default: `false` + */ + singleQuote?: boolean + /** * String representation for `true`. * With the core schema, use `'true'`, `'True'`, or `'TRUE'`. @@ -273,10 +308,4 @@ export const defaultOptions: Required< * stringification of scalars. Note that these values are used by all documents. */ export const scalarOptions = { - get str() { - return strOptions - }, - set str(opt) { - Object.assign(strOptions, opt) - } } diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index bc572293..ba224fb6 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,3 +1,4 @@ +import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' import { isAlias, isNode, isPair, isScalar, Node } from '../nodes/Node.js' import type { Scalar } from '../nodes/Scalar.js' @@ -31,6 +32,8 @@ export const createStringifyContext = ( typeof options.indent === 'number' ? ' '.repeat(options.indent) : ' ', options: Object.assign( { + defaultKeyType: null, + defaultStringType: Type.PLAIN, doubleQuotedAsJSON: false, doubleQuotedMinMultiLineLength: 40, falseStr: 'false', @@ -39,6 +42,7 @@ export const createStringifyContext = ( minContentWidth: 20, nullStr: 'null', simpleKeys: false, + singleQuote: false, trueStr: 'true' }, options diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index a604dc61..6b0a8476 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -1,6 +1,5 @@ import { Type } from '../constants.js' import type { Scalar } from '../nodes/Scalar.js' -import { strOptions } from '../tags/options.js' import { addCommentBefore } from './addComment.js' import { foldFlowLines, @@ -246,7 +245,7 @@ function plainString( quotedString = singleQuotedString } else if (hasSingle && !hasDouble) { quotedString = doubleQuotedString - } else if (strOptions.defaultQuoteSingle) { + } else if (ctx.options.singleQuote) { quotedString = singleQuotedString } else { quotedString = doubleQuotedString @@ -308,7 +307,6 @@ export function stringifyString( onComment?: () => void, onChompKeep?: () => void ) { - const { defaultKeyType, defaultType } = strOptions const { implicitKey, inFlow } = ctx const ss: Scalar = typeof item.value === 'string' @@ -342,7 +340,8 @@ export function stringifyString( let res = _stringify(type) if (res === null) { - const t = implicitKey ? defaultKeyType : defaultType + const { defaultKeyType, defaultStringType } = ctx.options + const t = (implicitKey && defaultKeyType) || defaultStringType res = _stringify(t) if (res === null) throw new Error(`Unsupported default string type ${t}`) } diff --git a/src/tags/failsafe/string.ts b/src/tags/failsafe/string.ts index 4a7d70c0..2d4a091e 100644 --- a/src/tags/failsafe/string.ts +++ b/src/tags/failsafe/string.ts @@ -1,5 +1,4 @@ import { stringifyString } from '../../stringify/stringifyString.js' -import { strOptions } from '../options.js' import type { ScalarTag } from '../types.js' export const string: ScalarTag = { @@ -10,6 +9,5 @@ export const string: ScalarTag = { stringify(item, ctx, onComment, onChompKeep) { ctx = Object.assign({ actualString: true }, ctx) return stringifyString(item, ctx, onComment, onChompKeep) - }, - options: strOptions + } } diff --git a/src/tags/options.ts b/src/tags/options.ts deleted file mode 100644 index 795f0dc2..00000000 --- a/src/tags/options.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Type } from '../constants.js' - -export const strOptions = { - /** - * The default type of string literal used to stringify values in general - * - * Default: `'PLAIN'` - */ - defaultType: Type.PLAIN, - - /** - * The default type of string literal used to stringify implicit key values - * - * Default: `'PLAIN'` - */ - defaultKeyType: Type.PLAIN, - - /** - * Use 'single quote' rather than "double quote" by default - * - * Default: `false` - */ - defaultQuoteSingle: false, -} diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index 0c06cc27..196c5f87 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -761,105 +761,77 @@ describe('indentSeq: false', () => { }) describe('Scalar options', () => { - describe('str.defaultType & str.defaultKeyType', () => { - let origDefaultType, origDefaultKeyType - beforeAll(() => { - origDefaultType = YAML.scalarOptions.str.defaultType - origDefaultKeyType = YAML.scalarOptions.str.defaultKeyType - }) - afterAll(() => { - YAML.scalarOptions.str.defaultType = origDefaultType - YAML.scalarOptions.str.defaultKeyType = origDefaultKeyType - }) - + describe('defaultStringType & defaultKeyType', () => { test('PLAIN, PLAIN', () => { - YAML.scalarOptions.str.defaultType = Type.PLAIN - YAML.scalarOptions.str.defaultKeyType = Type.PLAIN - expect(YAML.stringify({ foo: 'bar' })).toBe('foo: bar\n') + const opt = { defaultStringType: Type.PLAIN, defaultKeyType: Type.PLAIN } + expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('foo: bar\n') }) test('BLOCK_FOLDED, BLOCK_FOLDED', () => { - YAML.scalarOptions.str.defaultType = Type.BLOCK_FOLDED - YAML.scalarOptions.str.defaultKeyType = Type.BLOCK_FOLDED - expect(YAML.stringify({ foo: 'bar' })).toBe('"foo": |-\n bar\n') + const opt = { + defaultStringType: Type.BLOCK_FOLDED, + defaultKeyType: Type.BLOCK_FOLDED + } + expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('"foo": |-\n bar\n') }) test('QUOTE_DOUBLE, PLAIN', () => { - YAML.scalarOptions.str.defaultType = Type.QUOTE_DOUBLE - YAML.scalarOptions.str.defaultKeyType = Type.PLAIN - expect(YAML.stringify({ foo: 'bar' })).toBe('foo: "bar"\n') + const opt = { + defaultStringType: Type.QUOTE_DOUBLE, + defaultKeyType: Type.PLAIN + } + expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('foo: "bar"\n') }) test('QUOTE_DOUBLE, QUOTE_SINGLE', () => { - YAML.scalarOptions.str.defaultType = Type.QUOTE_DOUBLE - YAML.scalarOptions.str.defaultKeyType = Type.QUOTE_SINGLE - expect(YAML.stringify({ foo: 'bar' })).toBe('\'foo\': "bar"\n') + const opt = { + defaultStringType: Type.QUOTE_DOUBLE, + defaultKeyType: Type.QUOTE_SINGLE + } + expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('\'foo\': "bar"\n') + }) + + test('QUOTE_DOUBLE, null', () => { + const opt = { defaultStringType: Type.QUOTE_DOUBLE, defaultKeyType: null } + expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('"foo": "bar"\n') }) test('Use defaultType for explicit keys', () => { - YAML.scalarOptions.str.defaultType = Type.QUOTE_DOUBLE - YAML.scalarOptions.str.defaultKeyType = Type.QUOTE_SINGLE + const opt = { + defaultStringType: Type.QUOTE_DOUBLE, + defaultKeyType: Type.QUOTE_SINGLE + } const doc = new YAML.Document({ foo: null }) doc.contents.items[0].value = null - expect(String(doc)).toBe('? "foo"\n') + expect(doc.toString(opt)).toBe('? "foo"\n') }) }) - describe('str.defaultQuoteSingle', () => { - let origDefaultQuoteOption - beforeAll(() => { - origDefaultQuoteOption = YAML.scalarOptions.str.defaultQuoteSingle - }) - afterAll(() => { - YAML.scalarOptions.str.defaultQuoteSingle = origDefaultQuoteOption - }) + for (const { bool, exp } of [ + { bool: false, exp: '"foo #bar"\n' }, + { bool: true, exp: "'foo #bar'\n" } + ]) { + describe(`singleQuote: ${bool}`, () => { + const opt = { singleQuote: bool } - const testSingleQuote = str => { - const expected = `'${str}'\n` - const actual = YAML.stringify(str) - expect(actual).toBe(expected) - expect(YAML.parse(actual)).toBe(str) - } - const testDoubleQuote = str => { - const expected = `"${str}"\n` - const actual = YAML.stringify(str) - expect(actual).toBe(expected) - expect(YAML.parse(actual)).toBe(str) - } + test('plain', () => { + expect(YAML.stringify('foo bar', opt)).toBe('foo bar\n') + }) - const testPlainStyle = () => { - const str = YAML.stringify('foo bar') - expect(str).toBe('foo bar\n') - } - const testForcedQuotes = () => { - let str = YAML.stringify('foo: "bar"') - expect(str).toBe(`'foo: "bar"'\n`) - str = YAML.stringify("foo: 'bar'") - expect(str).toBe(`"foo: 'bar'"\n`) - } + test('forced', () => { + expect(YAML.stringify('foo: "bar"', opt)).toBe(`'foo: "bar"'\n`) + expect(YAML.stringify("foo: 'bar'", opt)).toBe(`"foo: 'bar'"\n`) + }) - test('default', () => { - YAML.scalarOptions.str.defaultQuoteSingle = origDefaultQuoteOption - testPlainStyle() - testForcedQuotes() - testDoubleQuote('123') - testDoubleQuote('foo #bar') - }) - test("'", () => { - YAML.scalarOptions.str.defaultQuoteSingle = true - testPlainStyle() - testForcedQuotes() - testDoubleQuote('123') // number-as-string is double-quoted - testSingleQuote('foo #bar') - }) - test('"', () => { - YAML.scalarOptions.str.defaultQuoteSingle = false - testPlainStyle() - testForcedQuotes() - testDoubleQuote('123') - testDoubleQuote('foo #bar') + test('numerical string', () => { + expect(YAML.stringify('123', opt)).toBe('"123"\n') + }) + + test('upgrade from plain', () => { + expect(YAML.stringify('foo #bar', opt)).toBe(exp) + }) }) - }) + } }) describe('Document markers in top-level scalars', () => { From f281157f5995858f5d368cb440e00afaa3e0e9db Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 15:29:22 +0200 Subject: [PATCH 17/20] Rewrite options section of documentation --- docs/03_options.md | 175 +++++++++++++++++++++++++++---------------- docs/04_documents.md | 80 ++++++++------------ 2 files changed, 142 insertions(+), 113 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 771bda9f..664fa972 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -1,101 +1,148 @@ # Options ```js -YAML.defaultOptions -// { anchorPrefix: 'a', -// keepUndefined: false, -// logLevel: 'warn', -// prettyErrors: true, -// strict: true, -// version: '1.2' } +import { parse, stringify } from 'yaml' + +parse('number: 999') +// { number: 999 } + +parse('number: 999', { intAsBigInt: true }) +// { number: 999n } + +parse('number: 999', { schema: 'failsafe' }) +// { number: '999' } ``` -In addition to the `options` arguments of functions, the values of `YAML.defaultOptions` are used as default values. +The options supported by various `yaml` are split into various categories, depending on how and where they are used. +Options in various categories do not overlap, so it's fine to use a single "bag" of options and pass it to each function or method. + +## Parse Options Parse options affect the parsing and composition of a YAML Document from it source. -| Parse Option | Type | Description | -| ------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| intAsBigInt | `boolean` | Whether integers should be parsed into [BigInt] rather than `number` values. By default `false`. | -| lineCounter | `LineCounter` | If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` to provide the `{ line, col }` positions within the input. | -| prettyErrors | `boolean` | Include line position & node type directly in errors. By default `false`. | -| strict | `boolean` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. By default `true`. | +Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `new Composer()`, and `new Document()` + +| Name | Type | Default | Description | +| ------------ | ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| intAsBigInt | `boolean` | `false` | Whether integers should be parsed into [BigInt] rather than `number` values. | +| lineCounter | `LineCounter` | | If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` to provide the `{ line, col }` positions within the input. | +| prettyErrors | `boolean` | `false` | Include line position & node type directly in errors. | +| strict | `boolean` | `true` | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content. | [bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt -Document options are relevant for operations on the `Document` object. +## Document Options -| Document Option | Type | Description | -| --------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| anchorPrefix | `string` | Default prefix for anchors. By default `'a'`, resulting in anchors `a1`, `a2`, etc. | -| keepUndefined | `boolean` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. By default `false`. | -| logLevel | `'warn' ⎮ 'error' ⎮ 'silent'` | Control the verbosity of `YAML.parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors. By default `'warn'`. | -| version | `'1.1' ⎮ '1.2'` | The YAML version used by documents without a `%YAML` directive. By default `'1.2'`. | +Document options are relevant for operations on the `Document` object, which makes them relevant for both conversion directions. -Schema options determine the types of values that the document is expected and able to support. +Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `stringify()`, `new Composer()`, and `new Document()` -| Schema Option | Type | Description | -| ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| customTags | `Tag[] ⎮ function` | Array of [additional tags](#custom-data-types) to include in the schema | -| merge | `boolean` | Enable support for `<<` merge keys. By default `false` for YAML 1.2 and `true` for earlier versions. | -| resolveKnownTags | `boolean` | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. | -| schema | `'core' ⎮ 'failsafe' ⎮` `'json' ⎮ 'yaml-1.1'` | The base schema to use. By default `'core'` for YAML 1.2 and `'yaml-1.1'` for earlier versions. | -| sortMapEntries | `boolean ⎮` `(a, b: Pair) => number` | When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. By default `false`. | +| Name | Type | Default | Description | +| ------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| anchorPrefix | `string` | `'a'` | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default. | +| keepUndefined | `boolean` | `false` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. | +| logLevel | `'warn' ⎮ 'error' ⎮` `'silent'` | `'warn'` | Control the verbosity of `parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors (not recommended). | +| version | `'1.1' ⎮ '1.2'` | `'1.2'` | The YAML version used by documents without a `%YAML` directive. | -[yaml 1.1 tags]: https://yaml.org/type/ +By default, the library will emit warnings as required by the YAML spec during parsing. +If you'd like to silence these, set the `logLevel` option to `'error'`. -## Data Schemas +## Schema Options ```js -YAML.parse('3') // 3 -YAML.parse('3', { schema: 'failsafe' }) // '3' +parse('3') // 3 (Using YAML 1.2 core schema by default) +parse('3', { schema: 'failsafe' }) // '3' -YAML.parse('No') // 'No' -YAML.parse('No', { schema: 'json' }) // SyntaxError: Unresolved plain scalar "No" -YAML.parse('No', { schema: 'yaml-1.1' }) // false -YAML.parse('No', { version: '1.1' }) // false - -YAML.parse('{[1, 2]: many}') // { '[1,2]': 'many' } -YAML.parse('{[1, 2]: many}', { mapAsMap: true }) // Map { [ 1, 2 ] => 'many' } +parse('No') // 'No' +parse('No', { schema: 'json' }) // SyntaxError: Unresolved plain scalar "No" +parse('No', { schema: 'yaml-1.1' }) // false +parse('No', { version: '1.1' }) // false ``` -Aside from defining the language structure, the YAML 1.2 spec defines a number of different _schemas_ that may be used. The default is the [`core`](http://yaml.org/spec/1.2/spec.html#id2804923) schema, which is the most common one. The [`json`](http://yaml.org/spec/1.2/spec.html#id2803231) schema is effectively the minimum schema required to parse JSON; both it and the core schema are supersets of the minimal [`failsafe`](http://yaml.org/spec/1.2/spec.html#id2802346) schema. +Schema options determine the types of values that the document is expected and able to support. -The `yaml-1.1` schema matches the more liberal [YAML 1.1 types](http://yaml.org/type/) (also used by YAML 1.0), including binary data and timestamps as distinct tags as well as accepting greater variance in scalar values (with e.g. `'No'` being parsed as `false` rather than a string value). The `!!value` and `!!yaml` types are not supported. +Aside from defining the language structure, the YAML 1.2 spec defines a number of different _schemas_ that may be used. +The default is the [`core`](http://yaml.org/spec/1.2/spec.html#id2804923) schema, which is the most common one. +The [`json`](http://yaml.org/spec/1.2/spec.html#id2803231) schema is effectively the minimum schema required to parse JSON; both it and the core schema are supersets of the minimal [`failsafe`](http://yaml.org/spec/1.2/spec.html#id2802346) schema. -```js -YAML.defaultOptions.merge = true +The `yaml-1.1` schema matches the more liberal [YAML 1.1 types](http://yaml.org/type/) (also used by YAML 1.0), including binary data and timestamps as distinct tags. +This schema accepts a greater variance in scalar values (with e.g. `'No'` being parsed as `false` rather than a string value). +The `!!value` and `!!yaml` types are not supported. + +Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `stringify()`, `new Composer()`, `new Document()`, and `doc.setSchema()` + +| Name | Type | Default | Description | +| ---------------- | --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| customTags | `Tag[] ⎮ function` | | Array of [additional tags](#custom-data-types) to include in the schema | +| merge | `boolean` | 1.1: `true` 1.2: `false` | Enable support for `<<` merge keys. Default value depends on YAML version. | +| resolveKnownTags | `boolean` | `true` | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. | +| schema | `'core' ⎮ 'failsafe' ⎮` `'json' ⎮ 'yaml-1.1'` | 1.1: `'yaml-1.1` 1.2: `'core'` | The base schema to use. Default value depends on YAML version. | +| sortMapEntries | `boolean ⎮` `(a, b: Pair) => number` | `false` | When stringifying, sort map entries. If `true`, sort by comparing key values using the native less-than `<` operator. | -const mergeResult = YAML.parse(` -source: &base { a: 1, b: 2 } -target: - <<: *base - b: base -`) +[yaml 1.1 tags]: https://yaml.org/type/ +```js +const src = ` + source: &base { a: 1, b: 2 } + target: + <<: *base + b: base` +const mergeResult = parse(src, { marge: true }) mergeResult.target // { a: 1, b: 'base' } ``` -**Merge** keys are a [YAML 1.1 feature](http://yaml.org/type/merge.html) that is not a part of the 1.2 spec. To use a merge key, assign an alias node or an array of alias nodes as the value of a `<<` key in a mapping. +**Merge** keys are a [YAML 1.1 feature](http://yaml.org/type/merge.html) that is not a part of the 1.2 spec. +To use a merge key, assign an alias node or an array of alias nodes as the value of a `<<` key in a mapping. -## Scalar Options +## ToJS Options ```js -// Without simpleKeys, an all-null-values object uses explicit keys & no values -YAML.stringify({ 'this is': null }, { simpleKeys: true }) -// this is: null - -YAML.stringify({ this: null, that: 'value' }, { defaultStringType: 'QUOTE_SINGLE', nullStr: '~' }) -// 'this': ~ -// 'that': 'value' +parse('{[1, 2]: many}') // { '[1,2]': 'many' } +parse('{[1, 2]: many}', { mapAsMap: true }) // Map { [ 1, 2 ] => 'many' } ``` -#### `YAML.scalarOptions` +These options influence how the document is transformed into "native" JavaScript representation. -Some customization options are availabe to control the parsing and stringification of scalars. Note that these values are used by all documents. +Used by: `parse()` and `doc.toJS()` -## Silencing Warnings +| Name | Type | Default | Description | +| ------------- | ------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| mapAsMap | `boolean` | `false` | Use Map rather than Object to represent mappings. | +| maxAliasCount | `number` | `100` | Prevent [exponential entity expansion attacks] by limiting data aliasing; set to `-1` to disable checks; `0` disallows all alias nodes. | +| onAnchor | `(value: any, count: number) => void` | | Optional callback for each aliased anchor in the document. | +| reviver | `(key: any, value: any) => any` | | Optionally apply a [reviver function] to the output, following the JSON specification but with appropriate extensions for handling `Map` and `Set`. | -By default, the library will emit warnings as required by the YAML spec during parsing. -If you'd like to silence these, set the `logLevel` option to `'error'`. +[exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack +[reviver function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter + +## ToString Options + +```js +stringify( + { this: null, that: 'value' }, + { defaultStringType: 'QUOTE_SINGLE', nullStr: '~' } +) +// 'this': ~ +// 'that': 'value' +``` + +The `doc.toString()` method may be called with additional options to control the resulting YAML string representation of the document. + +Used by: `stringify()` and `doc.toString()` + +| Name | Type | Default | Description | +| ------------------------------ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| defaultKeyType | `Type ⎮ null` | `null` | If not `null`, overrides `defaultStringType` for implicit key values. | +| defaultStringType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values. | +| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | +| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | +| falseStr | `string` | `'false'` | String representation for `false` values. | +| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | +| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | +| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | +| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | +| nullStr | `string` | `'null'` | String representation for `null` values. | +| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | +| singleQuote | `boolean` | `false` | Prefer 'single quote' rather than "double quote" where applicable. | +| trueStr | `string` | `'true'` | String representation for `true` values. | diff --git a/docs/04_documents.md b/docs/04_documents.md index a57749f6..8eaa0278 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -1,15 +1,15 @@ # Documents -In order to work with YAML features not directly supported by native JavaScript data types, such as comments, anchors and aliases, `yaml` provides the `YAML.Document` API. +In order to work with YAML features not directly supported by native JavaScript data types, such as comments, anchors and aliases, `yaml` provides the `Document` API. ## Parsing Documents ```js import fs from 'fs' -import YAML from 'yaml' +import { parseAllDocuments, parseDocument } from 'yaml' const file = fs.readFileSync('./file.yml', 'utf8') -const doc = YAML.parseDocument(file) +const doc = parseDocument(file) doc.contents // YAMLMap { // items: @@ -43,15 +43,19 @@ doc.contents // range: [ 0, 180 ] } ``` -#### `YAML.parseDocument(str, options = {}): YAML.Document` +#### `parseDocument(str, options = {}): Document` -Parses a single `YAML.Document` from the input `str`; used internally by `YAML.parse`. Will include an error if `str` contains more than one document. See [Options](#options) for more information on the second parameter. +Parses a single `Document` from the input `str`; used internally by `parse`. +Will include an error if `str` contains more than one document. +See [Options](#options) for more information on the second parameter.
-#### `YAML.parseAllDocuments(str, options = {}): YAML.Document[]` +#### `parseAllDocuments(str, options = {}): Document[]` -When parsing YAML, the input string `str` may consist of a stream of documents separated from each other by `...` document end marker lines. `YAML.parseAllDocuments` will return an array of `Document` objects that allow these documents to be parsed and manipulated with more control. See [Options](#options) for more information on the second parameter. +When parsing YAML, the input string `str` may consist of a stream of documents separated from each other by `...` document end marker lines. +`parseAllDocuments` will return an array of `Document` objects that allow these documents to be parsed and manipulated with more control. +See [Options](#options) for more information on the second parameter.
@@ -61,7 +65,7 @@ The `contents` of a parsed document will always consist of `Scalar`, `Map`, `Seq ## Creating Documents -#### `new YAML.Document(value, replacer?, options = {})` +#### `new Document(value, replacer?, options = {})` Creates a new document. If `value` is defined, the document `contents` are initialised with that value, wrapped recursively in appropriate [content nodes](#content-nodes). @@ -83,7 +87,9 @@ See [Options](#options) for more information on the last argument. | warnings | `Error[]` | Warnings encountered during parsing. | ```js -const doc = new YAML.Document(['some', 'values', { balloons: 99 }]) +import { Document } from 'yaml' + +const doc = new Document(['some', 'values', { balloons: 99 }]) doc.version = true doc.commentBefore = ' A commented document' @@ -96,7 +102,9 @@ String(doc) // - balloons: 99 ``` -The Document members are all modifiable, though it's unlikely that you'll have reason to change `errors`, `schema` or `warnings`. In particular you may be interested in both reading and writing **`contents`**. Although `YAML.parseDocument()` and `YAML.parseAllDocuments()` will leave it with `Map`, `Seq`, `Scalar` or `null` contents, it can be set to anything. +The Document members are all modifiable, though it's unlikely that you'll have reason to change `errors`, `schema` or `warnings`. +In particular you may be interested in both reading and writing **`contents`**. +Although `parseDocument()` and `parseAllDocuments()` will leave it with `Map`, `Seq`, `Scalar` or `null` contents, it can be set to anything. During stringification, a document with a true-ish `version` value will include a `%YAML` directive; the version number will be set to `1.2` unless the `yaml-1.1` schema is in use. @@ -113,7 +121,7 @@ During stringification, a document with a true-ish `version` value will include | toString(options?) | `string` | A YAML representation of the document. | ```js -const doc = YAML.parseDocument('a: 1\nb: [2, 3]\n') +const doc = parseDocument('a: 1\nb: [2, 3]\n') doc.get('a') // 1 doc.getIn([]) // YAMLMap { items: [Pair, Pair], ... } doc.hasIn(['b', 0]) // true @@ -122,15 +130,18 @@ doc.deleteIn(['b', 1]) // true doc.getIn(['b', 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. `null`, `undefined`, or `[]`) will refer to the document's top-level `contents`. +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. `null`, `undefined`, or `[]`) will refer to the document's top-level `contents`. -To define a tag prefix to use when stringifying, use **`setTagPrefix(handle, prefix)`** rather than setting a value directly in `tagPrefixes`. This will guarantee that the `handle` is valid (by throwing an error), and will overwrite any previous definition for the `handle`. Use an empty `prefix` value to remove a prefix. +To define a tag prefix to use when stringifying, use **`setTagPrefix(handle, prefix)`** rather than setting a value directly in `tagPrefixes`. +This will guarantee that the `handle` is valid (by throwing an error), and will overwrite any previous definition for the `handle`. +Use an empty `prefix` value to remove a prefix. -#### `Document#toJS()` and `Document#toJSON()` +#### `Document#toJS()`, `Document#toJSON()` and `Document#toString()` ```js const src = '1969-07-21T02:56:15Z' -const doc = YAML.parseDocument(src, { customTags: ['timestamp'] }) +const doc = parseDocument(src, { customTags: ['timestamp'] }) doc.toJS() // Date { 1969-07-21T02:56:15.000Z } @@ -144,43 +155,14 @@ String(doc) For a plain JavaScript representation of the document, **`toJS(options = {})`** is your friend. Its output may include `Map` and `Set` collections (e.g. if the `mapAsMap` option is true) and complex scalar values like `Date` for `!!timestamp`, but all YAML nodes will be resolved. -For a representation consisting only of JSON values, use **`toJSON()`**. - -The following options are also available when calling `YAML.parse()`: - -| `toJS()` Option | Type | Default value | Description | -| --------------- | ------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| mapAsMap | `boolean` | `false` | Use Map rather than Object to represent mappings. | -| maxAliasCount | `number` | `100` | Prevent [exponential entity expansion attacks] by limiting data aliasing; set to `-1` to disable checks; `0` disallows all alias nodes. | -| onAnchor | `(value: any, count: number) => void` | | Optional callback for each aliased anchor in the document. | -| reviver | `(key: any, value: any) => any` | | Optionally apply a [reviver function] to the output, following the JSON specification but with appropriate extensions for handling `Map` and `Set`. | +See [Options](#options) for more information on the optional parameter. -[exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack -[reviver function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter - -#### `Document#toString()` +For a representation consisting only of JSON values, use **`toJSON()`**. To stringify a document as YAML, use **`toString(options = {})`**. This will also be called by `String(doc)` (with no options). This method will throw if the `errors` array is not empty. - -The following options are also available when calling `YAML.stringify()`: - -| `toString()` Option | Type | Default value | Description | -| ------------------------------ | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| defaultKeyType | `Type ⎮ null` | `null` | If not `null`, overrides `defaultStringType` for implicit key values. | -| defaultStringType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values. | -| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | -| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | -| falseStr | `string` | `'false'` | String representation for `false` values. | -| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | -| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | -| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | -| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | -| nullStr | `string` | `'null'` | String representation for `null` values. | -| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | -| singleQuote | `boolean` | `false` | Prefer 'single quote' rather than "double quote" where applicable. | -| trueStr | `string` | `'true'` | String representation for `true` values. | +See [Options](#options) for more information on the optional parameter. ## Working with Anchors @@ -188,7 +170,7 @@ A description of [alias and merge nodes](#alias-nodes) is included in the next s
-#### `YAML.Document#anchors` +#### `Document#anchors` | Method | Returns | Description | | -------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- | @@ -202,7 +184,7 @@ A description of [alias and merge nodes](#alias-nodes) is included in the next s ```js const src = '[{ a: A }, { b: B }]' -const doc = YAML.parseDocument(src) +const doc = parseDocument(src) doc.anchors.setAnchor(doc.getIn([0, 'a'], true)) // 'a1' doc.anchors.setAnchor(doc.getIn([1, 'b'], true)) // 'a2' doc.anchors.setAnchor(null, 'a1') // 'a1' From 24383fcec5fa9ba69f35ae85766aeaf27ac7f26a Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 16:10:46 +0200 Subject: [PATCH 18/20] Cleanup: Drop scalarOptions (now empty) & options from tag interface Additionally, un-document the YAML.defaultOptions export. BREAKING CHANGE: These are no longer used, as their functionality has been moved to ParseOptions and ToStringOptions. --- README.md | 1 - docs/01_intro.md | 1 - docs/06_custom_tags.md | 41 +++++++++------------- src/index.ts | 1 - src/options.ts | 7 ---- src/tags/types.ts | 5 --- tests/doc/YAML-1.1.spec.js | 16 ++------- tests/doc/YAML-1.2.spec.js | 19 +++------- tests/doc/stringify.js | 71 ++++++++++++++++---------------------- tests/doc/types.js | 11 ------ 10 files changed, 54 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 23e905ee..5e9eac3f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ const YAML = require('yaml') ### YAML Documents -- [`YAML.defaultOptions`](https://eemeli.org/yaml/#options) - [`YAML.Document`](https://eemeli.org/yaml/#yaml-documents) - [`constructor(value, replacer?, options?)`](https://eemeli.org/yaml/#creating-documents) - [`defaults`](https://eemeli.org/yaml/#options) diff --git a/docs/01_intro.md b/docs/01_intro.md index 39886a5e..599a67bb 100644 --- a/docs/01_intro.md +++ b/docs/01_intro.md @@ -39,7 +39,6 @@ const YAML = require('yaml')

Documents

-- [`YAML.defaultOptions`](#options) - [`YAML.Document`](#documents) - [`constructor(value, replacer?, options?)`](#creating-documents) - [`defaults`](#options) diff --git a/docs/06_custom_tags.md b/docs/06_custom_tags.md index dbbe4b8e..3b602813 100644 --- a/docs/06_custom_tags.md +++ b/docs/06_custom_tags.md @@ -1,18 +1,15 @@ # Custom Data Types ```js -YAML.parse('!!timestamp 2001-12-15 2:59:43') -// YAMLWarning: -// The tag tag:yaml.org,2002:timestamp is unavailable, -// falling back to tag:yaml.org,2002:str -// '2001-12-15 2:59:43' +import { parse, parseDocument } from 'yaml' -YAML.defaultOptions.customTags = ['timestamp'] +parse('2001-12-15 2:59:43') +// '2001-12-15 2:59:43' -YAML.parse('2001-12-15 2:59:43') // returns a Date instance -// 2001-12-15T02:59:43.000Z +parse('!!timestamp 2001-12-15 2:59:43') +// 2001-12-15T02:59:43.000Z (Date instance) -const doc = YAML.parseDocument('2001-12-15 2:59:43') +const doc = parseDocument('2001-12-15 2:59:43', { customTags: ['timestamp'] }) doc.contents.value.toDateString() // 'Sat Dec 15 2001' ``` @@ -24,16 +21,14 @@ For further customisation, `customTags` may also be a function `(Tag[]) => (Tag[ ## Built-in Custom Tags ```js -YAML.parse('[ one, true, 42 ]').map(v => typeof v) -// [ 'string', 'boolean', 'number' ] +parse('[ one, true, 42 ]') +// [ 'one', true, 42 ] -let opt = { schema: 'failsafe' } -YAML.parse('[ one, true, 42 ]', opt).map(v => typeof v) -// [ 'string', 'string', 'string' ] +parse('[ one, true, 42 ]', { schema: 'failsafe' }) +// [ 'one', 'true', '42' ] -opt = { schema: 'failsafe', customTags: ['int'] } -YAML.parse('[ one, true, 42 ]', opt).map(v => typeof v) -// [ 'string', 'string', 'number' ] +parse('[ one, true, 42 ]', { schema: 'failsafe', customTags: ['int'] }) +// [ 'one', 'true', 42 ] ``` ### YAML 1.2 Core Schema @@ -70,6 +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 { stringifyString } from 'yaml/util' const regexp = { @@ -92,12 +88,10 @@ const sharedSymbol = { } } -YAML.defaultOptions.customTags = [regexp, sharedSymbol] - -YAML.stringify({ - regexp: /foo/gi, - symbol: Symbol.for('bar') -}) +stringify( + { regexp: /foo/gi, symbol: Symbol.for('bar') }, + { customTags: [regexp, sharedSymbol] } +) // regexp: !re /foo/gi // symbol: !symbol/shared bar ``` @@ -143,7 +137,6 @@ To define your own tag, you'll need to define an object comprising of some of th - `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. -- `options: Object` is used by some tags to configure their stringification. - **`resolve(value, onError): Node | any`** turns a parsed value into an AST node; `value` is either a `string`, a `YAMLMap` or a `YAMLSeq`. `onError(msg)` should be called with an error message string when encountering errors, as it'll allow you to still return some value for the node. If returning a non-`Node` value, the output will be wrapped as a `Scalar`. Required. - `stringify(item, ctx, onComment, onChompKeep): string` is an optional function stringifying the `item` AST node in the current context `ctx`. `onComment` and `onChompKeep` are callback functions for a couple of special cases. If your data includes a suitable `.toString()` method, you can probably leave this undefined and use the default stringifier. - **`tag: string`** is the identifier for your data type, with which its stringified form will be prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified `tag:domain,date:foo`. Required. diff --git a/src/index.ts b/src/index.ts index 6a54c1e6..5195dc0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,6 @@ export { CreateNodeOptions, defaultOptions, Options, - scalarOptions, SchemaOptions, ToJSOptions, ToStringOptions diff --git a/src/options.ts b/src/options.ts index c01b7178..0732fb30 100644 --- a/src/options.ts +++ b/src/options.ts @@ -302,10 +302,3 @@ export const defaultOptions: Required< strict: true, version: '1.2' } - -/** - * Some customization options are availabe to control the parsing and - * stringification of scalars. Note that these values are used by all documents. - */ -export const scalarOptions = { -} diff --git a/src/tags/types.ts b/src/tags/types.ts index 1752c140..128d3104 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -51,11 +51,6 @@ interface TagBase { */ identify?: (value: unknown) => boolean - /** - * Used by some tags to configure their stringification, where applicable. - */ - options?: Record - /** * The identifier for your data type, with which its stringified form will be * prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified diff --git a/tests/doc/YAML-1.1.spec.js b/tests/doc/YAML-1.1.spec.js index 5032af8a..4e58795e 100644 --- a/tests/doc/YAML-1.1.spec.js +++ b/tests/doc/YAML-1.1.spec.js @@ -1,17 +1,5 @@ import { source } from 'common-tags' -import * as YAML from 'yaml' - -const orig = {} -beforeAll(() => { - orig.prettyErrors = YAML.defaultOptions.prettyErrors - orig.version = YAML.defaultOptions.version - YAML.defaultOptions.prettyErrors = true - YAML.defaultOptions.version = '1.1' -}) -afterAll(() => { - YAML.defaultOptions.prettyErrors = orig.prettyErrors - YAML.defaultOptions.version = orig.version -}) +import { parseAllDocuments } from 'yaml' test('Use preceding directives if none defined', () => { const src = source` @@ -29,7 +17,7 @@ test('Use preceding directives if none defined', () => { --- !bar "Using previous YAML directive" ` - const docs = YAML.parseAllDocuments(src, { prettyErrors: false }) + const docs = parseAllDocuments(src, { prettyErrors: false, version: '1.1' }) const warn = tag => ({ message: `Unresolved tag: ${tag}` }) expect(docs).toMatchObject([ { diff --git a/tests/doc/YAML-1.2.spec.js b/tests/doc/YAML-1.2.spec.js index 39146ea3..8cb29533 100644 --- a/tests/doc/YAML-1.2.spec.js +++ b/tests/doc/YAML-1.2.spec.js @@ -1851,20 +1851,9 @@ matches %: 20`, } } -let origPrettyErrors const mockWarn = jest.spyOn(global.process, 'emitWarning').mockImplementation() - -beforeAll(() => { - origPrettyErrors = YAML.defaultOptions.prettyErrors - YAML.defaultOptions.prettyErrors = false -}) - beforeEach(() => mockWarn.mockClear()) - -afterAll(() => { - YAML.defaultOptions.prettyErrors = origPrettyErrors - mockWarn.mockRestore() -}) +afterAll(() => mockWarn.mockRestore()) for (const section in spec) { describe(section, () => { @@ -1873,7 +1862,7 @@ for (const section in spec) { const { src, tgt, errors, special, jsWarnings, warnings } = spec[ section ][name] - const documents = YAML.parseAllDocuments(src) + const documents = YAML.parseAllDocuments(src, { prettyErrors: false }) const json = documents.map(doc => doc.toJS()) expect(json).toMatchObject(tgt) documents.forEach((doc, i) => { @@ -1894,7 +1883,9 @@ for (const section in spec) { if (special) special(src) if (!errors) { const src2 = documents.map(doc => String(doc)).join('\n...\n') - const documents2 = YAML.parseAllDocuments(src2) + const documents2 = YAML.parseAllDocuments(src2, { + prettyErrors: false + }) const json2 = documents2.map(doc => doc.toJS()) expect(json2).toMatchObject(tgt) } diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index 196c5f87..f472ef2f 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -10,91 +10,82 @@ for (const [name, version] of [ ['YAML 1.2', '1.2'] ]) { describe(name, () => { - let origVersion - beforeAll(() => { - origVersion = YAML.defaultOptions.version - YAML.defaultOptions.version = version - }) - afterAll(() => { - YAML.defaultOptions.version = origVersion - }) - test('undefined', () => { - expect(YAML.stringify()).toBeUndefined() + expect(YAML.stringify(undefined, { version })).toBeUndefined() }) test('null', () => { - expect(YAML.stringify(null)).toBe('null\n') + expect(YAML.stringify(null, { version })).toBe('null\n') }) describe('boolean', () => { test('true', () => { - expect(YAML.stringify(true)).toBe('true\n') + expect(YAML.stringify(true, { version })).toBe('true\n') }) test('false', () => { - expect(YAML.stringify(false)).toBe('false\n') + expect(YAML.stringify(false, { version })).toBe('false\n') }) }) describe('number', () => { test('integer', () => { - expect(YAML.stringify(3)).toBe('3\n') + expect(YAML.stringify(3, { version })).toBe('3\n') }) test('float', () => { - expect(YAML.stringify(3.141)).toBe('3.141\n') + expect(YAML.stringify(3.141, { version })).toBe('3.141\n') }) test('zero', () => { - expect(YAML.stringify(0)).toBe('0\n') + expect(YAML.stringify(0, { version })).toBe('0\n') }) test('NaN', () => { - expect(YAML.stringify(NaN)).toBe('.nan\n') + expect(YAML.stringify(NaN, { version })).toBe('.nan\n') }) test('float with trailing zeros', () => { - const doc = new YAML.Document(3) + const doc = new YAML.Document(3, { version }) doc.contents.minFractionDigits = 2 expect(String(doc)).toBe('3.00\n') }) test('scientific float ignores minFractionDigits', () => { - const doc = new YAML.Document(3) + const doc = new YAML.Document(3, { version }) doc.contents.format = 'EXP' doc.contents.minFractionDigits = 2 expect(String(doc)).toBe('3e+0\n') }) test('integer with HEX format', () => { - const doc = new YAML.Document(42) + const doc = new YAML.Document(42, { version }) doc.contents.format = 'HEX' expect(String(doc)).toBe('0x2a\n') }) test('float with HEX format', () => { - const doc = new YAML.Document(4.2) + const doc = new YAML.Document(4.2, { version }) doc.contents.format = 'HEX' expect(String(doc)).toBe('4.2\n') }) test('negative integer with HEX format', () => { - const doc = new YAML.Document(-42) + const doc = new YAML.Document(-42, { version }) doc.contents.format = 'HEX' const exp = version === '1.2' ? '-42\n' : '-0x2a\n' expect(String(doc)).toBe(exp) }) test('BigInt', () => { - expect(YAML.stringify(BigInt('-42'))).toBe('-42\n') + expect(YAML.stringify(BigInt('-42'), { version })).toBe('-42\n') }) test('BigInt with HEX format', () => { - const doc = new YAML.Document(BigInt('42')) + const doc = new YAML.Document(BigInt('42'), { version }) doc.contents.format = 'HEX' expect(String(doc)).toBe('0x2a\n') }) test('BigInt with OCT format', () => { - const doc = new YAML.Document(BigInt('42')) + const doc = new YAML.Document(BigInt('42'), { version }) doc.contents.format = 'OCT' const exp = version === '1.2' ? '0o52\n' : '052\n' expect(String(doc)).toBe(exp) }) test('negative BigInt with OCT format', () => { - const doc = new YAML.Document(BigInt('-42')) + const doc = new YAML.Document(BigInt('-42'), { version }) doc.contents.format = 'OCT' const exp = version === '1.2' ? '-42\n' : '-052\n' expect(String(doc)).toBe(exp) @@ -102,24 +93,22 @@ for (const [name, version] of [ }) describe('string', () => { - const foldOptions = { lineWidth: 20, minContentWidth: 0 } + const opt = { lineWidth: 20, minContentWidth: 0, version } test('plain', () => { - expect(YAML.stringify('STR', foldOptions)).toBe('STR\n') + expect(YAML.stringify('STR', opt)).toBe('STR\n') }) test('double-quoted', () => { - expect(YAML.stringify('"x"', foldOptions)).toBe('\'"x"\'\n') + expect(YAML.stringify('"x"', opt)).toBe('\'"x"\'\n') }) test('single-quoted', () => { - expect(YAML.stringify("'x'", foldOptions)).toBe('"\'x\'"\n') + expect(YAML.stringify("'x'", opt)).toBe('"\'x\'"\n') }) test('escaped', () => { - expect(YAML.stringify('null: \u0000', foldOptions)).toBe( - '"null: \\0"\n' - ) + expect(YAML.stringify('null: \u0000', opt)).toBe('"null: \\0"\n') }) test('short multiline', () => { - expect(YAML.stringify('blah\nblah\nblah', foldOptions)).toBe( + expect(YAML.stringify('blah\nblah\nblah', opt)).toBe( '|-\nblah\nblah\nblah\n' ) }) @@ -127,7 +116,7 @@ for (const [name, version] of [ expect( YAML.stringify( 'blah blah\nblah blah blah blah blah blah blah blah blah blah\n', - foldOptions + opt ) ).toBe(`> blah blah @@ -139,12 +128,12 @@ blah blah\n`) test('long line in map', () => { const foo = 'fuzz'.repeat(16) - const doc = new YAML.Document({ foo }) + const doc = new YAML.Document({ foo }, version) for (const node of doc.contents.items) node.value.type = Type.QUOTE_DOUBLE expect( doc - .toString(foldOptions) + .toString(opt) .split('\n') .map(line => line.length) ).toMatchObject([20, 20, 20, 20, 0]) @@ -152,11 +141,11 @@ blah blah\n`) test('long line in sequence', () => { const foo = 'fuzz'.repeat(16) - const doc = new YAML.Document([foo]) + const doc = new YAML.Document([foo], version) for (const node of doc.contents.items) node.type = Type.QUOTE_DOUBLE expect( doc - .toString(foldOptions) + .toString(opt) .split('\n') .map(line => line.length) ).toMatchObject([20, 20, 20, 17, 0]) @@ -164,12 +153,12 @@ blah blah\n`) test('long line in sequence in map', () => { const foo = 'fuzz'.repeat(16) - const doc = new YAML.Document({ foo: [foo] }) + const doc = new YAML.Document({ foo: [foo] }, version) const seq = doc.contents.items[0].value for (const node of seq.items) node.type = Type.QUOTE_DOUBLE expect( doc - .toString(foldOptions) + .toString(opt) .split('\n') .map(line => line.length) ).toMatchObject([4, 20, 20, 20, 20, 10, 0]) diff --git a/tests/doc/types.js b/tests/doc/types.js index 18df69ed..bdcf3e0e 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -4,17 +4,6 @@ import { binary } from '../../src/tags/yaml-1.1/binary.js' import { YAMLOMap } from '../../src/tags/yaml-1.1/omap.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' -let origPrettyErrors - -beforeAll(() => { - origPrettyErrors = YAML.defaultOptions.prettyErrors - YAML.defaultOptions.prettyErrors = false -}) - -afterAll(() => { - YAML.defaultOptions.prettyErrors = origPrettyErrors -}) - describe('json schema', () => { test('!!bool', () => { const src = `"canonical": true From 4e8d437951c707359ee4e6ee0b3f1cdc2854278d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Mar 2021 21:04:27 +0200 Subject: [PATCH 19/20] Add flow: boolean option to CreateNodeOptions --- docs/05_content_nodes.md | 18 ++++++++++++++---- src/doc/Document.ts | 10 +++++++++- src/nodes/Scalar.ts | 1 + src/options.ts | 27 +++++++++++++-------------- tests/doc/createNode.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 19 deletions(-) diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index 7fedeb5c..636ad674 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -164,13 +164,23 @@ String(doc) #### `YAML.Document#createNode(value, options?): Node` -To create a new node, use the `createNode(value, options?)` document method. This will recursively wrap any input with appropriate `Node` containers. Generic JS `Object` values as well as `Map` and its descendants become mappings, while arrays and other iterable objects result in sequences. With `Object`, entries that have an `undefined` value are dropped. +To create a new node, use the `createNode(value, options?)` document method. +This will recursively wrap any input with appropriate `Node` containers. +Generic JS `Object` values as well as `Map` and its descendants become mappings, while arrays and other iterable objects result in sequences. +With `Object`, entries that have an `undefined` value are dropped. -Use `options.replacer` to apply a replacer array or function, following the [JSON implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). To specify the collection type, set `options.tag` to its identifying string, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available in the document's schema. If `options.wrapScalars` is undefined or `true`, plain values are wrapped in `Scalar` objects. +To force flow styling on a collection, use `options.flow = true` +Use `options.replacer` to apply a replacer array or function, following the [JSON implementation][replacer]. +To specify the collection type, set `options.tag` to its identifying string, e.g. `"!!omap"`. +Note that this requires the corresponding tag to be available in the document's schema. -As a possible side effect, this method may add entries to the document's [`anchors`](#working-with-anchors) +[replacer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter -The primary purpose of this method is to enable attaching comments or other metadata to a value, or to otherwise exert more fine-grained control over the stringified output. To that end, you'll need to assign its return value to the `contents` of a document (or somewhere within said contents), as the document's schema is required for YAML string output. If you're not interested in working with such metadata, document `contents` may also include non-`Node` values at any level. +As a possible side effect, this method may add entries to the document's [`anchors`](#working-with-anchors). + +The primary purpose of this method is to enable attaching comments or other metadata to a value, or to otherwise exert more fine-grained control over the stringified output. +To that end, you'll need to assign its return value to the `contents` of a document (or somewhere within said contents), as the document's schema is required for YAML string output. +If you're not interested in working with such metadata, document `contents` may also include non-`Node` values at any level.

new YAMLMap(), new YAMLSeq(), doc.createPair(key, value)

diff --git a/src/doc/Document.ts b/src/doc/Document.ts index d667bd74..3007d522 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -5,8 +5,10 @@ import { collectionFromPath, isEmptyPath } from '../nodes/Collection.js' import { DOC, isCollection, + isMap, isNode, isScalar, + isSeq, Node, NODE_TYPE, ParsedNode @@ -157,7 +159,7 @@ export class Document { */ createNode( value: unknown, - { keepUndefined, onTagObj, replacer, tag }: CreateNodeOptions = {} + { flow, keepUndefined, onTagObj, replacer, tag }: CreateNodeOptions = {} ): Node { if (typeof replacer === 'function') value = replacer.call({ '': value }, '', value) @@ -183,6 +185,7 @@ export class Document { replacer, schema: this.schema } + const node = createNode(value, tag, ctx) for (const alias of aliasNodes) { // With circular references, the source node is only resolved after all of @@ -195,6 +198,11 @@ export class Document { this.anchors.map[name] = alias.source } } + if (flow) { + if (isMap(node)) node.type = Type.FLOW_MAP + else if (isSeq(node)) node.type = Type.FLOW_SEQ + } + return node } diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index f8737274..6c7c66cb 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -31,6 +31,7 @@ export class Scalar extends NodeBase { */ declare format?: string + /** If `value` is a number, use this value when stringifying this node. */ declare minFractionDigits?: number /** Set during parsing to the source string value */ diff --git a/src/options.ts b/src/options.ts index 0732fb30..fab3e969 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,9 +1,10 @@ -import { LogLevelId, Type } from './constants.js' +import type { LogLevelId } from './constants.js' import type { Reviver } from './doc/applyReviver.js' import type { Directives } from './doc/directives.js' import type { Replacer } from './doc/Document.js' import type { SchemaName } from './doc/Schema.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 { CollectionTag, ScalarTag, TagValue } from './tags/types.js' @@ -117,6 +118,15 @@ export type SchemaOptions = { } export type CreateNodeOptions = { + /** Force the top-level collection node to use flow style. */ + flow?: boolean + + /** + * Keep `undefined` object values when creating mappings, rather than + * discarding them. + * + * Default: `false` + */ keepUndefined?: boolean | null onTagObj?: (tagObj: ScalarTag | CollectionTag) => void @@ -174,13 +184,7 @@ export type ToStringOptions = { * * Default: `null` */ - defaultKeyType?: - | null - | Type.BLOCK_FOLDED - | Type.BLOCK_LITERAL - | Type.PLAIN - | Type.QUOTE_DOUBLE - | Type.QUOTE_SINGLE + defaultKeyType?: Scalar.Type | null /** * The default type of string literal used to stringify values in general. @@ -188,12 +192,7 @@ export type ToStringOptions = { * * Default: `'PLAIN'` */ - defaultStringType?: - | Type.BLOCK_FOLDED - | Type.BLOCK_LITERAL - | Type.PLAIN - | Type.QUOTE_DOUBLE - | Type.QUOTE_SINGLE + defaultStringType?: Scalar.Type /** * Restrict double-quoted strings to use JSON-compatible syntax. diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js index b309adfa..f11bf22f 100644 --- a/tests/doc/createNode.js +++ b/tests/doc/createNode.js @@ -54,11 +54,23 @@ describe('arrays', () => { expect(s).toBeInstanceOf(YAMLSeq) expect(s.items).toHaveLength(0) }) + test('createNode([true])', () => { const s = doc.createNode([true]) expect(s).toBeInstanceOf(YAMLSeq) expect(s.items).toMatchObject([{ value: true }]) + doc.contents = s + expect(String(doc)).toBe('- true\n') + }) + + test('flow: true', () => { + const s = doc.createNode([true], { flow: true }) + expect(s).toBeInstanceOf(YAMLSeq) + expect(s.items).toMatchObject([{ value: true }]) + doc.contents = s + expect(String(doc)).toBe('[ true ]\n') }) + describe('[3, ["four", 5]]', () => { const array = [3, ['four', 5]] test('createNode(value)', () => { @@ -89,13 +101,27 @@ describe('objects', () => { expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toHaveLength(0) }) + test('createNode({ x: true })', () => { const s = doc.createNode({ x: true }) expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toMatchObject([ { key: { value: 'x' }, value: { value: true } } ]) + doc.contents = s + expect(String(doc)).toBe('x: true\n') + }) + + test('flow: true', () => { + const s = doc.createNode({ x: true }, { flow: true }) + expect(s).toBeInstanceOf(YAMLMap) + expect(s.items).toMatchObject([ + { key: { value: 'x' }, value: { value: true } } + ]) + doc.contents = s + expect(String(doc)).toBe('{ x: true }\n') }) + test('createNode({ x: true, y: undefined })', () => { const s = doc.createNode({ x: true, y: undefined }) expect(s).toBeInstanceOf(YAMLMap) @@ -103,6 +129,7 @@ describe('objects', () => { { type: PairType.PAIR, key: { value: 'x' }, value: { value: true } } ]) }) + test('createNode({ x: true, y: undefined }, { keepUndefined: true })', () => { const s = doc.createNode({ x: true, y: undefined }, { keepUndefined: true }) expect(s).toBeInstanceOf(YAMLMap) @@ -111,6 +138,7 @@ describe('objects', () => { { type: PairType.PAIR, key: { value: 'y' }, value: { value: null } } ]) }) + describe('{ x: 3, y: [4], z: { w: "five", v: 6 } }', () => { const object = { x: 3, y: [4], z: { w: 'five', v: 6 } } test('createNode(value)', () => { From 7f762124b77302651bc242a2542d0b2c9c9fe310 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 7 Mar 2021 19:35:52 +0200 Subject: [PATCH 20/20] Replace doc.directivesEndMarker with doc.directives.marker & directives option As cleanup, also remove the now-obsolete doc.version value BREAKING CHANGE: To now detect if a parsed document included an explicit doc-start marker, use `doc.directives.marker === true`. To include the marker when stringifying, either set that value to true, or use `doc.toString({ directives: true )`. To explicitly leave out directives and the doc-start marker from the output, use `doc.toString({ directives: false )`. --- docs/03_options.md | 31 ++++++++++++++++--------------- docs/04_documents.md | 24 ++++++++++++------------ src/compose/compose-doc.ts | 4 +--- src/compose/composer.ts | 2 +- src/doc/Document.ts | 30 ++++++++++-------------------- src/doc/directives.ts | 6 ++++++ src/options.ts | 15 +++++++++++++++ src/stringify/stringify.ts | 1 + src/test-events.ts | 2 +- tests/doc/comments.js | 3 +-- tests/doc/stringify.js | 3 +-- 11 files changed, 65 insertions(+), 56 deletions(-) diff --git a/docs/03_options.md b/docs/03_options.md index 664fa972..3c1cb65a 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -131,18 +131,19 @@ The `doc.toString()` method may be called with additional options to control the Used by: `stringify()` and `doc.toString()` -| Name | Type | Default | Description | -| ------------------------------ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| defaultKeyType | `Type ⎮ null` | `null` | If not `null`, overrides `defaultStringType` for implicit key values. | -| defaultStringType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values. | -| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | -| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | -| falseStr | `string` | `'false'` | String representation for `false` values. | -| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | -| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | -| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | -| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | -| nullStr | `string` | `'null'` | String representation for `null` values. | -| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | -| singleQuote | `boolean` | `false` | Prefer 'single quote' rather than "double quote" where applicable. | -| trueStr | `string` | `'true'` | String representation for `true` values. | +| Name | Type | Default | Description | +| ------------------------------ | ---------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| defaultKeyType | `Type ⎮ null` | `null` | If not `null`, overrides `defaultStringType` for implicit key values. | +| defaultStringType | `Type` | `'PLAIN'` | The default type of string literal used to stringify values. | +| directives | `boolean ⎮ null` | `null` | Include directives in the output. If `true`, at least the document-start marker `---` is always included. If `false`, no directives or marker is ever included. If `null`, directives and marker may be included if required. | +| doubleQuotedAsJSON | `boolean` | `false` | Restrict double-quoted strings to use JSON-compatible syntax. | +| doubleQuotedMinMultiLineLength | `number` | `40` | Minimum length for double-quoted strings to use multiple lines to represent the value. | +| falseStr | `string` | `'false'` | String representation for `false` values. | +| indent | `number` | `2` | The number of spaces to use when indenting code. Should be a strictly positive integer. | +| indentSeq | `boolean` | `true` | Whether block sequences should be indented. | +| lineWidth | `number` | `80` | Maximum line width (set to `0` to disable folding). This is a soft limit, as only double-quoted semantics allow for inserting a line break in the middle of a word. | +| minContentWidth | `number` | `20` | Minimum line width for highly-indented content (set to `0` to disable). | +| nullStr | `string` | `'null'` | String representation for `null` values. | +| simpleKeys | `boolean` | `false` | Require keys to be scalars and always use implicit rather than explicit notation. | +| singleQuote | `boolean` | `false` | Prefer 'single quote' rather than "double quote" where applicable. | +| trueStr | `string` | `'true'` | String representation for `true` values. | diff --git a/docs/04_documents.md b/docs/04_documents.md index 8eaa0278..97812c16 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -73,18 +73,18 @@ If `value` is `undefined`, the document's `contents` is initialised as `null`. If defined, a `replacer` may filter or modify the initial document contents, following the same algorithm as the [JSON implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). See [Options](#options) for more information on the last argument. -| Member | Type | Description | -| ------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| anchors | [`Anchors`](#anchors) | Anchors associated with the document's nodes; also provides alias & merge node creators. | -| commentBefore | `string?` | A comment at the very beginning of the document. If not empty, separated from the rest of the document by a blank line or the directives-end indicator when stringified. | -| comment | `string?` | A comment at the end of the document. If not empty, separated from the rest of the document by a blank line when stringified. | -| contents | [`Node`](#content-nodes)|`any` | The document contents. | -| directivesEndMarker | `boolean?` | Whether the document should always include a directives-end marker `---` at its start, even if it includes no directives. | -| errors | `Error[]` | Errors encountered during parsing. | -| schema | `Schema` | The schema used with the document. | -| tagPrefixes | `Prefix[]` | Array of prefixes; each will have a string `handle` that starts and ends with `!` and a string `prefix` that the handle will be replaced by. | -| version | `string?` | The parsed version of the source document; if true-ish, stringified output will include a `%YAML` directive. | -| warnings | `Error[]` | Warnings encountered during parsing. | +| Member | Type | Description | +| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| anchors | [`Anchors`](#anchors) | Anchors associated with the document's nodes; also provides alias & merge node creators. | +| commentBefore | `string?` | A comment at the very beginning of the document. If not empty, separated from the rest of the document by a blank line or the doc-start indicator when stringified. | +| comment | `string?` | A comment at the end of the document. If not empty, separated from the rest of the document by a blank line when stringified. | +| contents | [`Node`](#content-nodes) `⎮ any` | The document contents. | +| directives | `Directives` | Document directives `%YAML` and `%TAG`, as well as the doc-start marker `---`. | +| errors | `Error[]` | Errors encountered during parsing. | +| schema | `Schema` | The schema used with the document. | +| tagPrefixes | `Prefix[]` | Array of prefixes; each will have a string `handle` that starts and ends with `!` and a string `prefix` that the handle will be replaced by. | +| version | `string?` | The parsed version of the source document; if true-ish, stringified output will include a `%YAML` directive. | +| warnings | `Error[]` | Warnings encountered during parsing. | ```js import { Document } from 'yaml' diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index 63e8b090..bbc48a07 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -14,10 +14,8 @@ export function composeDoc( ) { const opts = Object.assign({ directives }, options) const doc = new Document(undefined, opts) as Document.Parsed - const props = resolveProps(doc, start, true, 'doc-start', offset, onError) - if (props.found) doc.directivesEndMarker = true - + if (props.found) doc.directives.marker = true doc.contents = value ? composeNode(doc, value, props, onError) : composeEmptyNode(doc, offset + props.length, start, null, props, onError) diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 8d25394f..ab5b53af 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -76,7 +76,7 @@ export class Composer { const dc = doc.contents if (afterDoc) { doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment - } else if (afterEmptyLine || doc.directivesEndMarker || !dc) { + } else if (afterEmptyLine || doc.directives.marker || !dc) { doc.commentBefore = comment } else if ( isCollection(dc) && diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 3007d522..2f127d9d 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -74,8 +74,6 @@ export class Document { directives: Directives - directivesEndMarker = false - /** Errors encountered during parsing. */ errors: YAMLError[] = [] @@ -98,12 +96,6 @@ export class Document { type: Type.DOCUMENT = Type.DOCUMENT - /** - * The parsed version of the source document; - * if true-ish, stringified output will include a `%YAML` directive. - */ - version?: string - /** Warnings encountered during parsing. */ warnings: YAMLWarning[] = [] @@ -420,15 +412,17 @@ export class Document { } const lines = [] - let hasDirectives = false - const dir = this.directives.toString(this) - if (dir) { - lines.push(dir) - hasDirectives = true + let hasDirectives = options.directives === true + if (options.directives !== false) { + const dir = this.directives.toString(this) + if (dir) { + lines.push(dir) + hasDirectives = true + } else if (this.directives.marker) hasDirectives = true } - if (hasDirectives || this.directivesEndMarker) lines.push('---') + if (hasDirectives) lines.push('---') if (this.commentBefore) { - if (hasDirectives || !this.directivesEndMarker) lines.unshift('') + if (lines.length !== 1) lines.unshift('') lines.unshift(this.commentBefore.replace(/^/gm, '#')) } @@ -437,11 +431,7 @@ export class Document { let contentComment = null if (this.contents) { if (isNode(this.contents)) { - if ( - this.contents.spaceBefore && - (hasDirectives || this.directivesEndMarker) - ) - lines.push('') + if (this.contents.spaceBefore && hasDirectives) lines.push('') if (this.contents.commentBefore) lines.push(this.contents.commentBefore.replace(/^/gm, '#')) // top-level block scalars need to be indented if followed by a comment diff --git a/src/doc/directives.ts b/src/doc/directives.ts index 8c24628a..c5744386 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -21,6 +21,12 @@ export class Directives { yaml: { version: '1.1' | '1.2'; explicit?: boolean } tags: Record + /** + * The directives-end/doc-start marker `---`. If `null`, a marker may still be + * included in the document's stringified representation. + */ + marker: true | null = null + /** * Used when parsing YAML 1.1, where: * > If the document specifies no directives, it is parsed using the same diff --git a/src/options.ts b/src/options.ts index fab3e969..70aff82a 100644 --- a/src/options.ts +++ b/src/options.ts @@ -194,6 +194,21 @@ export type ToStringOptions = { */ defaultStringType?: Scalar.Type + /** + * Include directives in the output. + * + * - If `true`, at least the document-start marker `---` is always included. + * This does not force the `%YAML` directive to be included. To do that, + * set `doc.directives.yaml.explicit = true`. + * - If `false`, no directives or marker is ever included. If using the `%TAG` + * directive, you are expected to include it manually in the stream before + * its use. + * - If `null`, directives and marker may be included if required. + * + * Default: `null` + */ + directives?: boolean | null + /** * Restrict double-quoted strings to use JSON-compatible syntax. * diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index ba224fb6..8dedf50d 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -34,6 +34,7 @@ export const createStringifyContext = ( { defaultKeyType: null, defaultStringType: Type.PLAIN, + directives: null, doubleQuotedAsJSON: false, doubleQuotedMinMultiLineLength: 40, falseStr: 'false', diff --git a/src/test-events.ts b/src/test-events.ts index 5e837a0f..2ee2684d 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -22,7 +22,7 @@ export function testEvents(src: string, options?: Options) { if (error && (!error.offset || error.offset < rootStart)) throw new Error() let docStart = '+DOC' - if (doc.directivesEndMarker) docStart += ' ---' + if (doc.directives.marker) docStart += ' ---' else if (doc.contents && doc.contents.range[1] === doc.contents.range[0]) continue events.push(docStart) diff --git a/tests/doc/comments.js b/tests/doc/comments.js index e0a899a1..e333b639 100644 --- a/tests/doc/comments.js +++ b/tests/doc/comments.js @@ -503,9 +503,8 @@ describe('blank lines', () => { test('before first node in document with directives', () => { const doc = YAML.parseDocument('str\n') - doc.directivesEndMarker = true doc.contents.spaceBefore = true - expect(String(doc)).toBe('---\n\nstr\n') + expect(doc.toString({ directives: true })).toBe('---\n\nstr\n') }) test('between seq items', () => { diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index f472ef2f..e757f811 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -866,8 +866,7 @@ describe('Document markers in top-level scalars', () => { test('use marker line for block scalar header', () => { const doc = YAML.parseDocument('|\nfoo\n') - doc.directivesEndMarker = true - expect(String(doc)).toBe('--- |\nfoo\n') + expect(doc.toString({ directives: true })).toBe('--- |\nfoo\n') }) })