Repository: eemeli/yaml
Pull request #235 (2021-03-01 16:14:01)

Refactor options
Some prior discussion is found in issue #190.

This does a bunch of stuff that changes (and breaks!) the options used by `yaml` by moving them closer to where they're actually used:

<details>
<summary><b>Drop the keepCstNodes and keepNodeTypes options completely</b></summary>

The earlier parser change has meant that information is retained differently during composition. Effectively, the values of these options are no longer used, so they should be explicitly dropped from the options interfaces to make that clear. The behaviour now is as if `{ keepCstNodes: false, keepNodeTypes: true }` is always set.

This may require some API changes for CST users, as errors also no longer provide a reference to the source CST node, just the original line position. To match an error to a node, you should search for a node that includes the error position in its range.

To discard node type information from the tree, use a visitor:

```js
import { isNode, parseDocument, visit } from 'yaml'

const doc = parseDocument('{ foo: "bar" }')
visit(doc, (key, node) => {
  if (isNode(node)) delete node.type
})

String(doc) === 'foo: bar\n'
```
</details>

<details>
<summary><b>Move mapAsMap and maxAliasCount from DocumentOptions to ToJSOptions</b></summary>

The option now needs to be given to the document's `toJS()` method, rather than being also available on the document constructor and in `YAML.defaultOptions`. The behaviour of `parse()` is unaffected.

```diff
import { parseDocument } from 'yaml'

-const doc = parseDocument('foo: bar', { mapAsMap: true, maxAliasCount: -1 })
-const js = doc.toJS()
+const doc = parseDocument('foo: bar')
+const js = doc.toJS({ mapAsMap: true, maxAliasCount: -1 })
```
</details>

<details>
<summary><b>Move indent, indentSeq & simpleKeys from DocumentOptions to ToStringOptions</b></summary>

These options now need to be given to the document's `toString()` method, rather than being also available on the document constructor and in `YAML.defaultOptions`. The behaviour of `stringify()` is unaffected.

```diff
import { parseDocument } from 'yaml'

-const doc = parseDocument('foo: bar', { indent: 4, indentSeq: false })
-const str = doc.toString()
+const doc = parseDocument('foo: bar')
+const str = doc.toString({ indent: 4, indentSeq: false })
```
</details>

<details>
<summary><b>Move BigInt option from scalarOptions to ParseOptions</b></summary>

Rather than setting `YAML.scalarOptions.int.asBigInt`, users should use the `intAsBigInt` option given to parse functions and constructors.
</details>

<details>
<summary><b>Drop special options for !!binary</b></summary>

The line width now follows the general `lineWidth` value and takes indent level into account. `!!binary` nodes that do not explicitly declare a `type` will use `BLOCK_LITERAL`. To set the type of such nodes, use a visitor:

```js
import { visit } from 'yaml'

visit(doc, {
  Scalar(key, value) {
    if (value.tag === 'tag:yaml.org,2002:binary')
      value.type = 'QUOTE_DOUBLE'
  }
})
```
</details>

<details>
<summary><b>Move all other scalar options to ToStringOptions</b></summary>

Rather than setting these via `YAML.scalarOptions`, users should pass them as options given to the `stringify()` function and the `doc.toString()` method.

The `defaultKeyType` option now accepts (and defaults to) `null`, with which value the `defaultStringType` also controls implicit key types.

Some of the options are renamed and their structure is flattened:
- `bool.falseStr` -> `falseStr`
- `bool.trueStr` -> `trueStr`
- `null.nullStr` -> `nullStr`
- `str.defaultType` -> `defaultStringType`
- `str.defaultKeyType` -> `defaultKeyType`
- `str.defaultQuoteSingle` -> `singleQuote`
- `str.doubleQuoted.jsonEncoding` -> `doubleQuotedAsJSON`
- `str.doubleQuoted.minMultiLineLength` -> `doubleQuotedMinMultiLineLength`
- `str.fold.lineWidth` -> `lineWidth`
- `str.fold.minContentWidth` -> `minContentWidth`
</details>

<details>
<summary><b>Refactor doc.setSchema()</b></summary>

The API of `doc.setSchema()` now requires an explicit `version` as well as supporting all Schema options. This is required to reduce the dependency on a mutating `doc.options` object.

```diff
-doc.setSchema('json')
+doc.setSchema('1.2', { schema: 'json' })

-doc.options.merge = false
-doc.setSchema('1.1')
+doc.setSchema('1.1', { merge: false })
```
</details>

<details>
<summary><b>Drop Document.defaults and doc.getDefaults()</b></summary>

This was effectively a side effect of the `doc.setSchema()` refactor. The version-specific schema options now need to be set explicitly in function arguments.
</details>

<details>
<summary><b>Drop scalarOptions</b></summary>

All of the options under this object are now elsewhere.
</details>

<details>
<summary><b>Drop options from the tag interface</b></summary>

Tag-specific options are now handled for the built-in tags via ParseOptions and ToJSOptions; custom tags should provide their own configuration separately, if any is needed.
</details>

<details>
<summary><b>Un-document the YAML.defaultOptions export</b></summary>

I'm reeeeally tempted to remove this entirely, as that would remove the last explicitly mutating global state from the library.
</details>

<details>
<summary><b>Replace doc.directivesEndMarker with doc.directives.marker & directives option</b></summary>

As cleanup, also remove the now-obsolete `doc.version` value

To now detect if a parsed document included an explicit doc-start marker, use `doc.directives.marker === true`. To include the marker when stringifying, either set that value to true, or use `doc.toString({ directives: true )`. To explicitly leave out directives and the doc-start marker from the output, use `doc.toString({ directives: false )`.
</details>


Patch:
diff --git a/README.md b/README.md
index 23e905e..5e9eac3 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 39886a5..599a67b 100644
--- a/docs/01_intro.md
+++ b/docs/01_intro.md
@@ -39,7 +39,6 @@ const YAML = require('yaml')
 
 <h3>Documents</h3>
 
-- [`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 aadf163..3c1cb65 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:&nbsp;`true` 1.2:&nbsp;`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:&nbsp;`'yaml-1.1` 1.2:&nbsp;`'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 2ffc926..97812c1 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.
 
 <br/>
 
-#### `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.
 
 <br/>
 
@@ -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)&vert;`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,&nbsp;options?)           | `Node`     | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information.                                                                     |
-| createPair(key,&nbsp;value,&nbsp;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,&nbsp;options?)           | `Node`   | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information.        |
+| createPair(key,&nbsp;value,&nbsp;options?) | `Pair`   | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information.              |
+| setSchema(version,&nbsp;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,&nbsp;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
 
 <br/>
 
-#### `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 7fedeb5..636ad67 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.
 
 <h4 style="clear:both"><code>new YAMLMap(), new YAMLSeq(), doc.createPair(key, value)</code></h4>
 
diff --git a/docs/06_custom_tags.md b/docs/06_custom_tags.md
index dbbe4b8..3b60281 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 c003b39..8e12230 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 63e8b09..bbc48a0 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 909c661..6730fa5 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 8d25394..ab5b53a 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 ec12a8f..2f127d9 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<T extends ParsedNode = ParsedNode> extends Document<T> {
@@ -74,8 +55,6 @@ export declare namespace Document {
 }
 
 export class Document<T = unknown> {
-  static defaults = documentOptions;
-
   readonly [NODE_TYPE]: symbol
 
   /**
@@ -95,15 +74,19 @@ export class Document<T = unknown> {
 
   directives: Directives
 
-  directivesEndMarker = false
-
   /** Errors encountered during parsing. */
   errors: YAMLError[] = []
 
-  options: Required<DocumentOptions> & 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<T = unknown> {
 
   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<T = unknown> {
       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<T = unknown> {
    */
   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<T = unknown> {
       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<T = unknown> {
         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<T = unknown> {
       : 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<T = unknown> {
   }
 
   /**
-   * 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<T = unknown> {
     }
   }
 
-  /**
-   * 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<T = unknown> {
     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<T = unknown> {
   }
 
   /** 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 d4f22e7..d9386dd 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<any>, b: Pair<any>) =>
   a.key < b.key ? -1 : a.key > b.key ? 1 : 0
 
@@ -61,7 +18,7 @@ const coreKnownTags = {
 }
 
 export class Schema {
-  knownTags: Record<string, TagObj>
+  knownTags: Record<string, CollectionTag | ScalarTag>
   merge: boolean
   name: SchemaName
   sortMapEntries: ((a: Pair, b: Pair) => number) | null
diff --git a/src/doc/directives.ts b/src/doc/directives.ts
index 8c24628..c574438 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<string, string>
 
+  /**
+   * 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 93d65d0..6dde5ad 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<SchemaId, TagObj[]>,
   knownTags: Record<TagId, TagObj>,
   customTags:
-    | Array<TagId | TagObj>
-    | ((tags: Array<TagId | TagObj>) => Array<TagId | TagObj>)
+    | TagValue[]
+    | ((tags: TagValue[]) => TagValue[])
     | null
     | undefined,
   schemaName: SchemaName
 ) {
   const schemaId = schemaName.replace(/\W/g, '') as SchemaId // 'yaml-1.1' -> 'yaml11'
-  let tags: Array<TagId | TagObj> = 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 d20de80..5195dc0 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 87fe7bf..b91e1ad 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 d38978c..6857d9a 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<K = unknown, V = unknown> 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<K = unknown, V = unknown> 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<K = unknown, V = unknown> 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<K = unknown, V = unknown> 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 f873727..6c7c66c 100644
--- a/src/nodes/Scalar.ts
+++ b/src/nodes/Scalar.ts
@@ -31,6 +31,7 @@ export class Scalar<T = unknown> 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 9d8e297..ce368d1 100644
--- a/src/nodes/toJS.ts
+++ b/src/nodes/toJS.ts
@@ -12,7 +12,6 @@ export interface ToJSAnchorValue {
 export interface ToJSContext {
   anchors: Map<Node, ToJSAnchorValue> | null
   doc: Document
-  indentStep: string
   keep: boolean
   mapAsMap: boolean
   mapKeyWarned: boolean
diff --git a/src/options.ts b/src/options.ts
index 50476df..70aff82 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<DocumentOptions> = {
+export const defaultOptions: Required<
+  Omit<ParseOptions, 'lineCounter'> & Omit<DocumentOptions, 'directives'>
+> = {
   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 543d09f..a33beab 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<T extends ParsedNode = ParsedNode>(
  *   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 8aebfd0..8dedf50 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<string, Node>
   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<Required<Omit<ToStringOptions, 'indent'>>>
 }
 
+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 8700777..6b0a847 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<string> =
     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 ee5d5fb..5955979 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 4a7d70c..2d4a091 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 bba7449..9082743 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 b686ff1..0000000
--- 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 e6b7ea0..128d310 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<string, unknown>
-
   /**
    * 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 f0ff90b..53d6c88 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 d0093d9..31f2b8f 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 3e9ff0d..999ad9f 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<B extends boolean>(str: string, isInt: B) {
+function parseSexagesimal<B extends boolean>(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
 }
 


Test patch:
diff --git a/src/test-events.ts b/src/test-events.ts
index 607c8e8..2ee2684 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 5032af8..4e58795 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 ed02381..8cb2953 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 e0a899a..e333b63 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 b309adf..f11bf22 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 478c8df..a70160f 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 b86caf0..35807a5 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 147b4df..e757f81 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 b614a0c..bdcf3e0 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 b25e072..9950f96 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 69eeaec..dfe23ed 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)
         })
       }