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/03_options.md b/docs/03_options.md index aadf1635..3c1cb65a 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -1,119 +1,149 @@ # Options ```js -YAML.defaultOptions -// { indent: 2, -// keepNodeTypes: true, -// mapAsMap: false, -// 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' } } +import { parse, stringify } from 'yaml' + +parse('number: 999') +// { number: 999 } + +parse('number: 999', { intAsBigInt: true }) +// { number: 999n } + +parse('number: 999', { schema: 'failsafe' }) +// { number: '999' } ``` -#### `YAML.defaultOptions` - -#### `YAML.Document.defaults` - -`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. - -The `version` option value (`'1.2'` by default) may be overridden by any document-specific `%YAML` directive. - -| 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 | -| 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`. | -| 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`. | -| 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'`. | +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. -[exponential entity expansion attacks]: https://en.wikipedia.org/wiki/Billion_laughs_attack -[yaml 1.1 tags]: https://yaml.org/type/ +## Parse Options -## Data Schemas +Parse options affect the parsing and composition of a YAML Document from it source. -```js -YAML.parse('3') // 3 -YAML.parse('3', { schema: 'failsafe' }) // '3' +Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `new Composer()`, and `new Document()` -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 +| 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. | -YAML.parse('{[1, 2]: many}') // { '[1,2]': 'many' } -YAML.parse('{[1, 2]: many}', { mapAsMap: true }) // Map { [ 1, 2 ] => 'many' } -``` +[bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt -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. +## Document Options -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. +Document options are relevant for operations on the `Document` object, which makes them relevant for both conversion directions. + +Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `stringify()`, `new Composer()`, and `new Document()` + +| 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. | + +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'`. + +## Schema Options ```js -YAML.defaultOptions.merge = true +parse('3') // 3 (Using YAML 1.2 core schema by default) +parse('3', { schema: 'failsafe' }) // '3' + +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 +``` + +Schema options determine the types of values that the document is expected and able to support. + +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. + +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. -const mergeResult = YAML.parse(` -source: &base { a: 1, b: 2 } -target: - <<: *base - b: base -`) +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. | + +[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.scalarOptions.null.nullStr = '~' -YAML.scalarOptions.str.defaultType = 'QUOTE_SINGLE' -YAML.stringify({ this: null, that: 'value' }) -// 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()` -| 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 | +| 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`. | -[bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt +[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 -## Silencing Warnings +## ToString Options -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'`. +```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. | +| 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 2ffc926f..97812c16 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). @@ -69,21 +73,23 @@ 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 -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,25 +102,26 @@ 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. ## 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') +const doc = parseDocument('a: 1\nb: [2, 3]\n') doc.get('a') // 1 doc.getIn([]) // YAMLMap { items: [Pair, Pair], ... } doc.hasIn(['b', 0]) // true @@ -123,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()`, `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 } @@ -143,11 +153,16 @@ 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. +See [Options](#options) for more information on the optional parameter. -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. +For a representation consisting only of JSON values, use **`toJSON()`**. -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. +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. +See [Options](#options) for more information on the optional parameter. ## Working with Anchors @@ -155,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 | | -------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- | @@ -169,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' 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/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/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-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/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/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 ec12a8ff..2f127d9d 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 @@ -16,49 +18,28 @@ 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, Options, - defaultOptions, - documentOptions + ParseOptions, + 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 { + createStringifyContext, + stringify, + StringifyContext +} from '../stringify/stringify.js' import { Anchors } from './Anchors.js' -import { Schema, SchemaName, SchemaOptions } from './Schema.js' -import { Reviver, applyReviver } from './applyReviver.js' +import { Schema } 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 - mapAsMap?: boolean - onAnchor?: (value: unknown, count: number) => void - reviver?: Reviver -} export declare namespace Document { interface Parsed extends Document { @@ -74,8 +55,6 @@ export declare namespace Document { } export class Document { - static defaults = documentOptions; - readonly [NODE_TYPE]: symbol /** @@ -95,15 +74,19 @@ export class Document { directives: Directives - directivesEndMarker = false - /** 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 @@ -113,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[] = [] @@ -142,16 +119,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 @@ -175,7 +151,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) @@ -201,6 +177,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 @@ -213,6 +190,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 } @@ -253,14 +235,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 @@ -336,29 +310,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?: (TagId | TagObj)[] - ) { - 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. */ @@ -374,16 +352,18 @@ 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 - */ - toJS({ json, jsonArg, mapAsMap, onAnchor, reviver }: ToJSOptions = {}) { + /** A plain JavaScript representation of the document `contents`. */ + toJS(opt?: ToJSOptions & { [ignored: string]: unknown }): any + + // json & jsonArg are only used from toJSON() + toJS({ + json, + jsonArg, + mapAsMap, + maxAliasCount, + onAnchor, + reviver + }: ToJSOptions & { json?: boolean; jsonArg?: string | null } = {}) { const anchorNodes = Object.values(this.anchors.map).map( node => [node, { alias: [], aliasCount: 0, count: 1 }] as [ @@ -395,12 +375,10 @@ export class Document { const ctx: ToJSContext = { anchors, doc: this, - indentStep: ' ', keep: !json, - mapAsMap: - typeof mapAsMap === 'boolean' ? mapAsMap : !!this.options.mapAsMap, + mapAsMap: mapAsMap === true, mapKeyWarned: false, - maxAliasCount: this.options.maxAliasCount, + maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100, stringify } const res = toJS(this.contents, jsonArg || '', ctx) @@ -422,42 +400,38 @@ export class Document { } /** A YAML representation of the document. */ - toString() { + toString(options: ToStringOptions = {}) { if (this.errors.length > 0) throw new Error('Document with errors cannot be stringified') - const indentSize = this.options.indent - 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}`) } + 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, '#')) } - const ctx: StringifyContext = { - 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) { 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/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/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/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..5195dc0f 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,14 @@ 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, + SchemaOptions, + ToJSOptions, + ToStringOptions +} from './options.js' export { Lexer } from './parse/lexer.js' export { LineCounter } from './parse/line-counter.js' 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 d38978cb..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' @@ -116,7 +120,13 @@ 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, + indentStep, + options: { indentSeq, simpleKeys } + } = ctx let { key, value }: { key: K; value: V | Node | null } = this let keyComment = (isNode(key) && key.comment) || null if (simpleKeys) { @@ -136,7 +146,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 +159,7 @@ export class Pair extends NodeBase { () => (keyComment = null), () => (chompKeep = true) ) + if (!explicitKey && !ctx.inFlow && str.length > 1024) { if (simpleKeys) throw new Error( @@ -199,7 +210,7 @@ export class Pair extends NodeBase { chompKeep = false if ( !indentSeq && - indentSize >= 2 && + indentStep.length >= 2 && !ctx.inFlow && !explicitKey && isSeq(value) && @@ -236,15 +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, - indent: '', - indentStep: ctx.indentStep, - inFlow: true, - inStringifyKey: true, - stringify: ctx.stringify - }) + 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/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/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/options.ts b/src/options.ts index 50476df3..70aff82a 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,45 +1,59 @@ -import { LogLevelId, defaultTagPrefix } from './constants.js' -import type { SchemaOptions } from './doc/Schema.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 { - binaryOptions, - boolOptions, - intOptions, - nullOptions, - strOptions -} from './tags/options.js' +import type { CollectionTag, ScalarTag, TagValue } from './tags/types.js' -export interface DocumentOptions { +export type ParseOptions = { /** - * Default prefix for anchors. + * Whether integers should be parsed into BigInt rather than number values. * - * Default: `'a'`, resulting in anchors `a1`, `a2`, etc. + * Default: `false` + * + * https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt */ - anchorPrefix?: string + intAsBigInt?: boolean + /** - * The number of spaces to use when indenting code. - * - * Default: `2` + * If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` + * to provide the `{ line, col }` positions within the input. */ - indent?: number + lineCounter?: LineCounter + /** - * Whether block sequences should be indented. + * Include line/col position & node type directly in parse errors. * * Default: `true` */ - indentSeq?: boolean + prettyErrors?: boolean + /** - * Include references in the AST to each node's corresponding CST node. + * Detect and report errors that are required by the YAML 1.2 spec, + * but are caused by unambiguous content. * - * Default: `false` + * Default: `true` */ - keepCstNodes?: boolean + strict?: boolean +} + +export type DocumentOptions = { /** - * Store the original node type when parsing documents. + * Default prefix for anchors. * - * Default: `true` + * Default: `'a'`, resulting in anchors `a1`, `a2`, etc. + */ + anchorPrefix?: string + + /** + * Used internally by Composer. If set and includes an explicit version, + * that overrides the `version` option. */ - keepNodeTypes?: boolean + directives?: Directives + /** * Keep `undefined` object values when creating mappings and return a Scalar * node when calling `YAML.stringify(undefined)`, rather than `undefined`. @@ -48,25 +62,97 @@ 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 * * Default: `'warn'` */ logLevel?: LogLevelId + + /** + * The YAML version used by documents without a `%YAML` directive. + * + * Default: `"1.2"` + */ + version?: '1.1' | '1.2' +} + +export type 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 + + /** + * 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 adding to or stringifying a map, sort the entries. + * If `true`, sort by comparing key values with `<`. + * + * Default: `false` + */ + sortMapEntries?: boolean | ((a: Pair, b: Pair) => number) +} + +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 + + /** + * 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 type ToJSOptions = { /** - * When outputting JS, use Map rather than Object to represent mappings. + * 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. @@ -74,33 +160,143 @@ export interface DocumentOptions { * Default: `100` */ maxAliasCount?: number + + /** + * 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 +} + +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?: Scalar.Type | null + + /** + * 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?: 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. + * + * 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'`. + * + * Default: `'false'` + */ + falseStr?: string + /** - * Include line position & node type directly in errors; drop their verbose source and context. + * The number of spaces to use when indenting code. + * + * Default: `2` + */ + indent?: number + + /** + * Whether block sequences should be indented. * * Default: `true` */ - prettyErrors?: boolean + 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 + * string `''`. + * + * Default: `'null'` + */ + nullStr?: string + /** - * When stringifying, require keys to be scalars and to use implicit rather than explicit notation. + * 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. + * Prefer 'single quote' rather than "double quote" where applicable. * - * Default: `true` + * Default: `false` */ - strict?: boolean + singleQuote?: boolean + /** - * The YAML version used by documents without a `%YAML` directive. + * String representation for `true`. + * With the core schema, use `'true'`, `'True'`, or `'TRUE'`. * - * Default: `"1.2"` + * Default: `'true'` */ - version?: '1.1' | '1.2' + trueStr?: string } -export type Options = DocumentOptions & SchemaOptions +export type Options = ParseOptions & DocumentOptions & SchemaOptions /** * `yaml` defines document-specific options in three places: as an argument of @@ -109,84 +305,14 @@ export type Options = 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', - indent: 2, - indentSeq: true, - keepCstNodes: false, - keepNodeTypes: true, + intAsBigInt: false, keepUndefined: false, - lineCounter: null, logLevel: 'warn', - mapAsMap: false, - maxAliasCount: 100, prettyErrors: true, - simpleKeys: false, 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 = { - get binary() { - return binaryOptions - }, - set binary(opt) { - Object.assign(binaryOptions, opt) - }, - get bool() { - return boolOptions - }, - set bool(opt) { - Object.assign(boolOptions, opt) - }, - get int() { - return intOptions - }, - set int(opt) { - Object.assign(intOptions, opt) - }, - get null() { - return nullOptions - }, - set null(opt) { - Object.assign(nullOptions, opt) - }, - get str() { - return strOptions - }, - set str(opt) { - 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/src/public-api.ts b/src/public-api.ts index 543d09f7..a33beabc 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, ToJSOptions, ToStringOptions } from './options.js' import { Parser } from './parse/parser.js' export interface EmptyStream @@ -77,13 +78,17 @@ export function parseDocument( * 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') { @@ -100,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)) } /** @@ -109,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)) { @@ -133,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/src/stringify/stringify.ts b/src/stringify/stringify.ts index 8aebfd09..8dedf50d 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,10 +1,14 @@ +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' +import type { ToStringOptions } from '../options.js' import type { TagObj } from '../tags/types.js' import { stringifyString } from './stringifyString.js' -export interface StringifyContext { +export type StringifyContext = { + actualString?: boolean + allNullValues?: boolean anchors: Record doc: Document forceBlockIndent?: boolean @@ -13,10 +17,39 @@ export interface StringifyContext { 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( + { + defaultKeyType: null, + defaultStringType: Type.PLAIN, + directives: null, + doubleQuotedAsJSON: false, + doubleQuotedMinMultiLineLength: 40, + falseStr: 'false', + indentSeq: true, + lineWidth: 80, + minContentWidth: 20, + nullStr: 'null', + simpleKeys: false, + singleQuote: 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/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index 87007774..6b0a8476 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -1,9 +1,9 @@ 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, + FoldOptions, FOLD_BLOCK, FOLD_FLOW, FOLD_QUOTED @@ -14,10 +14,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. @@ -39,10 +40,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 @@ -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}` } @@ -243,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 @@ -305,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' @@ -339,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/core.ts b/src/tags/core.ts index ee5d5fbb..59559791 100644 --- a/src/tags/core.ts +++ b/src/tags/core.ts @@ -1,14 +1,18 @@ 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 { 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 @@ -23,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 }, ctx) => + source && nullObj.test.test(source) ? source : ctx.options.nullStr } export const boolObj: ScalarTag & { test: RegExp } = { @@ -34,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 }, ctx) { 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 ? ctx.options.trueStr : ctx.options.falseStr } } @@ -50,8 +52,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 +61,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 +71,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/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/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 deleted file mode 100644 index b686ff18..00000000 --- a/src/tags/options.ts +++ /dev/null @@ -1,114 +0,0 @@ -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 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 intOptions = { - /** - * Whether integers should be parsed into BigInt values. - * - * Default: `false` - */ - asBigInt: 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 - * - * 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, - - 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 - }, - - 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/src/tags/types.ts b/src/tags/types.ts index e6b7ea0d..128d3104 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' @@ -50,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 @@ -71,7 +67,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 +118,13 @@ 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 + +export type TagValue = TagId | ScalarTag | CollectionTag 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/src/tags/yaml-1.1/index.ts b/src/tags/yaml-1.1/index.ts index d0093d97..31f2b8fd 100644 --- a/src/tags/yaml-1.1/index.ts +++ b/src/tags/yaml-1.1/index.ts @@ -1,8 +1,9 @@ import { Scalar } from '../../nodes/Scalar.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, intOptions, 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' @@ -16,15 +17,14 @@ 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 }, ctx) => + source && nullObj.test.test(source) ? source : ctx.options.nullStr } -const boolStringify = ({ value, source }: Scalar) => { +function boolStringify({ value, source }: Scalar, ctx: StringifyContext) { const boolObj = value ? trueObj : falseObj if (source && boolObj.test.test(source)) return source - return value ? boolOptions.trueStr : boolOptions.falseStr + return value ? ctx.options.trueStr : ctx.options.falseStr } const trueObj: ScalarTag & { test: RegExp } = { @@ -33,7 +33,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 } @@ -43,18 +42,22 @@ 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 } 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 +96,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 +106,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 +115,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 +125,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/src/test-events.ts b/src/test-events.ts index 607c8e80..2ee2684d 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 @@ -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/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 ed023816..8cb29533 100644 --- a/tests/doc/YAML-1.2.spec.js +++ b/tests/doc/YAML-1.2.spec.js @@ -1851,26 +1851,9 @@ matches %: 20`, } } -let origFoldOptions, 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 -}) - beforeEach(() => mockWarn.mockClear()) - -afterAll(() => { - YAML.scalarOptions.str.fold = origFoldOptions - YAML.defaultOptions.prettyErrors = origPrettyErrors - mockWarn.mockRestore() -}) +afterAll(() => mockWarn.mockRestore()) for (const section in spec) { describe(section, () => { @@ -1879,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) => { @@ -1900,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/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/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)', () => { 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/parse.js b/tests/doc/parse.js index b86caf0c..35807a50 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -89,24 +89,22 @@ 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 }) }) }) describe('number types', () => { - describe('asBigInt: false', () => { + describe('intAsBigInt: false', () => { test('Version 1.1', () => { const src = ` - 0b10_10 @@ -119,7 +117,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 +150,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 +172,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 +182,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 +205,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' }, @@ -378,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') }) }) @@ -633,8 +628,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/stringify.js b/tests/doc/stringify.js index 147b4dfc..e757f811 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,39 +93,30 @@ 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 opt = { lineWidth: 20, minContentWidth: 0, version } test('plain', () => { - expect(YAML.stringify('STR')).toBe('STR\n') + expect(YAML.stringify('STR', opt)).toBe('STR\n') }) test('double-quoted', () => { - expect(YAML.stringify('"x"')).toBe('\'"x"\'\n') + expect(YAML.stringify('"x"', opt)).toBe('\'"x"\'\n') }) test('single-quoted', () => { - expect(YAML.stringify("'x'")).toBe('"\'x\'"\n') + expect(YAML.stringify("'x'", opt)).toBe('"\'x\'"\n') }) test('escaped', () => { - expect(YAML.stringify('null: \u0000')).toBe('"null: \\0"\n') + expect(YAML.stringify('null: \u0000', opt)).toBe('"null: \\0"\n') }) test('short multiline', () => { - expect(YAML.stringify('blah\nblah\nblah')).toBe( + expect(YAML.stringify('blah\nblah\nblah', opt)).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', + opt ) ).toBe(`> blah blah @@ -146,11 +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( - String(doc) + doc + .toString(opt) .split('\n') .map(line => line.length) ).toMatchObject([20, 20, 20, 20, 0]) @@ -158,10 +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( - String(doc) + doc + .toString(opt) .split('\n') .map(line => line.length) ).toMatchObject([20, 20, 20, 17, 0]) @@ -169,11 +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( - String(doc) + doc + .toString(opt) .split('\n') .map(line => line.length) ).toMatchObject([4, 20, 20, 20, 20, 10, 0]) @@ -371,36 +356,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 +385,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) }) }) @@ -452,47 +429,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]) }) @@ -595,34 +555,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 +592,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/ ) }) @@ -790,120 +750,80 @@ 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', () => { - 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') @@ -933,7 +853,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...') }) @@ -946,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') }) }) diff --git a/tests/doc/types.js b/tests/doc/types.js index b614a0c7..bdcf3e0e 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -4,23 +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 origFoldOptions, 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 -}) - describe('json schema', () => { test('!!bool', () => { const src = `"canonical": true @@ -209,8 +192,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 +202,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'], @@ -260,19 +243,17 @@ 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/\\ ++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`) - YAML.scalarOptions.str.fold.lineWidth = 20 }) test('!!bool', () => { @@ -363,18 +344,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 +361,6 @@ octal: 02472256 hexadecimal: 0xa74ae binary: 0b10100111010010101110 sexagesimal: 190:20:30\n`) - } finally { - YAML.scalarOptions.int.asBigInt = false - } }) test('!!null', () => { @@ -685,9 +661,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 +671,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 +680,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 +688,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 +696,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 +704,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') 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' diff --git a/tests/yaml-test-suite.js b/tests/yaml-test-suite.js index 69eeaec6..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() @@ -107,10 +93,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) }) }