Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [5.1.0] - 2026-06-23
### Added
- Collection tags can finalize an incrementally populated carrier into a
different result value.

### Changed
- [breaking] `quoteStyle` now selects the preferred quote style; use the
restored `forceQuotes` option to force quoting non-key strings.


## [5.0.0] - 2026-06-20
### Added
- Added named exports for schemas, tags, parser events and AST utilities.
Expand Down Expand Up @@ -635,6 +645,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- First public release


[5.1.0]: https://github.com/nodeca/js-yaml/compare/5.0.0...5.1.0
[5.0.0]: https://github.com/nodeca/js-yaml/compare/4.2.0...5.0.0
[4.2.0]: https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0
[4.1.1]: https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ options:
width.
- `noRefs` _(default: `false`)_ - if `true`, don't convert duplicate objects into
references; inline them instead.
- `quoteStyle` _(`auto`, `single`, or `double`, default: `auto`)_ - force quotes
to single/double, or select the most suitable.
- `quoteStyle` _(`single` or `double`, default: `single`)_ - quoting style to use
when a string needs quotes.
- `forceQuotes` _(default: `false`)_ - if `true`, quote all non-key strings,
using `quoteStyle`.
- `flowBracketPadding` _(default: `false`)_ - add spaces inside flow collection
brackets, `{a: 1}` => `{ a: 1 }`.
- `flowSkipCommaSpace` _(default: `false`)_ - omit the space after commas in
Expand Down
24 changes: 21 additions & 3 deletions docs/migrate_v4_to_v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ options.
| `replacer` | removed — patch the data before `dump()` |
| `noCompatMode` | select the schema whose scalar rules apply |
| `condenseFlow` | `flowSkipCommaSpace`, `flowSkipColonSpace`, `quoteFlowKeys` |
| `quotingType`, `forceQuotes` | `quoteStyle: 'single'` or `quoteStyle: 'double'` |
| `quotingType` | `quoteStyle: 'single'` or `quoteStyle: 'double'` |
| `noArrayIndent` | `seqNoIndent` |

### Replacing `styles`
Expand Down Expand Up @@ -195,5 +195,23 @@ const pointTag = defineSequenceTag('!point', {
})
```

This only sketches the shape. For the full method set (mapping `addPair`, scalar
`resolve`, styling, etc.) see [examples/custom_tags.mjs](../examples/custom_tags.mjs).
If the result cannot be populated incrementally, collect its contents in a
temporary carrier and add `finalize`:

```js
const pointTag = defineSequenceTag('!point', {
create: () => [],
addItem: (items, value) => { items.push(value) },
finalize: items => new ImmutablePoint(...items),
identify: value => value instanceof ImmutablePoint,
represent: point => [point.x, point.y]
})
```

An anchored tag with a temporary carrier cannot recursively alias itself,
because its result does not exist until `finalize` returns. Such input throws.

This only sketches the shape. See
[examples/custom_tags.mjs](../examples/custom_tags.mjs) for the full method set
and [examples/custom_tags_immutable.mjs](../examples/custom_tags_immutable.mjs)
for carrier finalization and its recursive-alias limitation.
44 changes: 44 additions & 0 deletions examples/custom_tags_immutable.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict'
import { CORE_SCHEMA, defineSequenceTag, dump, load } from 'js-yaml'

// Immutable values cannot be populated item by item. Build a mutable carrier
// first, then turn the completed carrier into the final value.
class ImmutablePoint {
constructor (coordinates) {
this.coordinates = Object.freeze([...coordinates])
Object.freeze(this)
}
}

const schema = CORE_SCHEMA.withTags(defineSequenceTag('!point', {
create: () => [],
addItem: (carrier, item) => { carrier.push(item) },
finalize: carrier => {
if (carrier.length !== 2) throw new Error('!point expects exactly 2 coordinates')
return new ImmutablePoint(carrier)
},
identify: value => value instanceof ImmutablePoint,
represent: point => point.coordinates
}))

const source = `
point: &point !point [10, 20]
samePoint: *point
`

const value = load(source, { schema })

assert.deepStrictEqual(value.point, new ImmutablePoint([10, 20]))
assert.strictEqual(value.samePoint, value.point)
assert.equal(dump(value.point, { schema }), '!point\n- 10\n- 20\n')
assert.throws(
() => load('!point [10]', { schema }),
/!point expects exactly 2 coordinates/
)

// A recursive alias needs the final object before finalize() can create it, so
// recursive aliases are intentionally rejected for tags that use finalize().
assert.throws(
() => load('&point !point [*point]', { schema }),
/recursive alias "point" is not supported for tag !point because it uses finalize\(\)/
)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "js-yaml",
"version": "5.0.0",
"version": "5.1.0",
"description": "YAML 1.2 parser and serializer",
"keywords": [
"yaml",
Expand Down
32 changes: 16 additions & 16 deletions src/ast/presenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ interface PresenterOptions {
flowSkipCommaSpace?: boolean
flowSkipColonSpace?: boolean
quoteFlowKeys?: boolean
quoteStyle?: 'auto' | 'single' | 'double'
quoteStyle?: 'single' | 'double'
forceQuotes?: boolean
tagBeforeAnchor?: boolean
}

Expand All @@ -82,7 +83,8 @@ const DEFAULT_PRESENTER_OPTIONS: Required<Omit<PresenterOptions, 'schema'>> = {
flowSkipCommaSpace: false,
flowSkipColonSpace: false,
quoteFlowKeys: false,
quoteStyle: 'auto',
quoteStyle: 'single',
forceQuotes: false,
tagBeforeAnchor: false
}

Expand Down Expand Up @@ -360,10 +362,8 @@ type ScalarStyleId =
// STYLE_LITERAL => no lines are suitable for folding (or lineWidth is -1).
// STYLE_FOLDED => a line > lineWidth and can be folded (and lineWidth != -1).
function chooseScalarStyle (state: PresenterState, string: string, layout: ReturnType<typeof scalarLayout>,
singleLineOnly: boolean, inblock: boolean): ScalarStyleId {
singleLineOnly: boolean, forceQuote: boolean, inblock: boolean): ScalarStyleId {
const { blockIndent, lineWidth } = layout
// quoteStyle !== 'auto' forces quoting: suppress plain and block styles.
const forceQuote = state.quoteStyle !== 'auto'
let i
let char = 0
let prevChar = -1 // -1 = no previous character yet (see isPlainSafe)
Expand Down Expand Up @@ -477,23 +477,23 @@ function resolveScalarStyle (state: PresenterState, node: ScalarNode,
const string = node.value

if (string.length === 0) {
// An empty scalar carries no text to resolve, so a bare plain one reads
// back as the implicit tag. That round-trips only if an explicit tag is
// printed (disambiguating it) or the implicit tag already matches; else
// quote it. Without a property a plain empty would vanish entirely.
if (state.quoteStyle === 'auto' &&
(node.style.tagged || resolveImplicitTag(state, string) === node.tag)) return STYLE_PLAIN
// An empty scalar is safe when its tag is explicit or resolves back to the
// node tag (notably, the default null representation). A real empty string
// does neither and therefore remains quoted.
if (node.style.tagged || resolveImplicitTag(state, string) === node.tag) return STYLE_PLAIN
return state.quoteStyle === 'double' ? STYLE_DOUBLE : STYLE_SINGLE
}

const style = chooseScalarStyle(state, string, layout, singleLineOnly, inblock)
// v4's forceQuotes deliberately excluded keys. Keys are still quoted when
// syntax or tag resolution requires it, using quoteStyle as the preference.
const style = chooseScalarStyle(
state, string, layout, singleLineOnly, state.forceQuotes && !iskey, inblock)

// Plain writes no tag, so it round-trips only if the bare text resolves back
// to the node's tag (or the tag gets printed explicitly). Else downgrade.
// `chooseScalarStyle` can return plain only in auto mode, so downgrade to
// the default quote style here.
// Downgrade to the preferred quote style here.
if (style === STYLE_PLAIN && !node.style.tagged && resolveImplicitTag(state, string) !== node.tag) {
return STYLE_SINGLE
return state.quoteStyle === 'double' ? STYLE_DOUBLE : STYLE_SINGLE
}
return style
}
Expand Down Expand Up @@ -695,7 +695,7 @@ function writeFlowMapping (state: PresenterState, level: number, node: MappingNo
let pairBuffer = ''
if (result !== '') pairBuffer += `,${!state.flowSkipCommaSpace ? ' ' : ''}`

const keyText = writeNode(state, level, key, {})
const keyText = writeNode(state, level, key, { iskey: true })
const explicitPair = keyText.length > 1024

if (explicitPair) {
Expand Down
Loading
Loading