From aec5f4ffb19889d1733438408cd0ee1ef76cd931 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 27 Feb 2026 17:54:05 -0500 Subject: [PATCH 1/8] Option to add trailing comma to multiline flow for `toString` Disabled by default. When enabled, the last entry in a flow map or flow sequence will have a `,` after it if it's split across multiple lines. eg. ```yaml { a: aaa, # comment b: bbb } ``` When parsed then converted back to string will become: ```yaml { a: aaa, # comment b: bbb, } ``` Closes #669 Signed-off-by: David Thompson --- src/options.ts | 7 ++ src/stringify/stringify.ts | 3 +- src/stringify/stringifyCollection.ts | 13 ++- tests/doc/stringify.ts | 161 +++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 2 deletions(-) diff --git a/src/options.ts b/src/options.ts index 2b8a0585..5836642d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -387,4 +387,11 @@ export type ToStringOptions = { * Default: `'true'` */ verifyAliasOrder?: boolean + + /** + * Add a trailing comma after the last entry in a flow map or flow sequence that's split across multiple lines. + * + * Default: `'false'` + */ + trailingComma?: boolean } diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 533f5ff7..1aff1311 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -56,7 +56,8 @@ export function createStringifyContext( simpleKeys: false, singleQuote: null, trueStr: 'true', - verifyAliasOrder: true + verifyAliasOrder: true, + trailingComma: false, }, doc.schema.toStringOptions, options diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index a920fe34..dafc5e4a 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -134,7 +134,18 @@ function stringifyFlowCollection( if (comment) reqNewline = true let str = stringify(item, itemCtx, () => (comment = null)) - if (i < items.length - 1) str += ',' + if (i < items.length - 1) { + str += ',' + } else if (ctx.options.trailingComma) { + // 'look forwards' to see if entries will be connected with newlines + const willReqNewline = reqNewline + || lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) + (comment ? lineComment(str, itemIndent, commentString(comment)).length : 0) > ctx.options.lineWidth + || lines.length > linesAtValue + || str.includes('\n') + if (willReqNewline) { + str += ',' + } + } if (comment) str += lineComment(str, itemIndent, commentString(comment)) if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) reqNewline = true diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 24e86610..d939f0f9 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -543,6 +543,167 @@ z: expect(String(doc)).toBe(src) }) }) + + describe('trailing comma (maps)', () => { + test('single line due to under 80 characters', () => { + const doc = new YAML.Document, false>({ + a: 'aaaaaaaaa', + b: 'bbbbbbbbb', + c: 'ccccccccc', + }) + doc.contents.flow = true + expect(doc.toString({ trailingComma: true })).toBe('{ a: aaaaaaaaa, b: bbbbbbbbb, c: ccccccccc }\n') + }) + test('single line due to exactly 80 characters', () => { + const doc = YAML.parseDocument(source` +{ + a: aaaaaaa, + b: bbbbbbb, + c: ccccccc, + d: ddddddd, + e: eeeeeee, + f: ffffffff +} + `) + expect(doc.toString({ trailingComma: true })).toBe('{ a: aaaaaaa, b: bbbbbbb, c: ccccccc, d: ddddddd, e: eeeeeee, f: ffffffff }\n') + }) + test('multi line due to exactly 81 characters', () => { + const doc = YAML.parseDocument(source` +{ + a: aaaaaaa, + b: bbbbbbb, + c: ccccccc, + d: ddddddd, + e: eeeeeee, + f: fffffffff +} + `) + expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaa,\n b: bbbbbbb,\n c: ccccccc,\n d: ddddddd,\n e: eeeeeee,\n f: fffffffff,\n}\n') + }) + test('multiline due to existing comment', () => { + const doc = YAML.parseDocument(source` + { + a: aaaaaaaaa, # my cool comment + b: bbbbbbbbb + } + `) + expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaaaa, # my cool comment\n b: bbbbbbbbb,\n}\n') + }) + test('multiline due to preserving a newline', () => { + const doc = YAML.parseDocument(source` + { + a: aaaaaaaaa, + + b: bbbbbbbbb + } + `) + expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaaaa,\n\n b: bbbbbbbbb,\n}\n') + }) + test('multiline due to entry includes a newline', () => { + const doc = YAML.parseDocument(source` +{ + a: { + a: a # a + }, + b: bbb +} + `) + expect(doc.toString({ trailingComma: true })).toBe('{\n a:\n {\n a: a, # a\n },\n b: bbb,\n}\n') + }) + test('multiline due to over 80 characters', () => { + const doc = new YAML.Document, false>({ + a: 'aaaaaaaaa', + b: 'bbbbbbbbb', + c: 'ccccccccc', + d: 'ddddddddd', + e: 'eeeeeeeee', + f: 'fffffffff' + }) + doc.contents.flow = true + expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaaaa,\n b: bbbbbbbbb,\n c: ccccccccc,\n d: ddddddddd,\n e: eeeeeeeee,\n f: fffffffff,\n}\n') + }) + }) + + describe('trailing comma (arrays)', () => { + test('single line due to under 80 characters', () => { + const doc = new YAML.Document, false>([ + 'aaaaaaaaa', + 'bbbbbbbbb', + 'ccccccccc', + ]) + doc.contents.flow = true + expect(doc.toString({ trailingComma: true })).toBe('[ aaaaaaaaa, bbbbbbbbb, ccccccccc ]\n') + }) + test('single line due to exactly 80 characters', () => { + const doc = YAML.parseDocument(source` +[ + aaaaaaaaaa, + bbbbbbbbbb, + cccccccccc, + dddddddddd, + eeeeeeeeee, + fffffffffff +] + `) + expect(doc.toString({ trailingComma: true })).toBe('[ aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, fffffffffff ]\n') + }) + test('multi line due to exactly 81 characters', () => { + const doc = YAML.parseDocument(source` +[ + aaaaaaaaaa, + bbbbbbbbbb, + cccccccccc, + dddddddddd, + eeeeeeeeee, + ffffffffffff +] + `) + expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaaa,\n bbbbbbbbbb,\n cccccccccc,\n dddddddddd,\n eeeeeeeeee,\n ffffffffffff,\n]\n') + }) + test('multiline due to existing comment', () => { + const doc = YAML.parseDocument(source` + [ + aaaaaaaaa, # my cool comment + bbbbbbbbb + ] + `) + expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaa, # my cool comment\n bbbbbbbbb,\n]\n') + }) + test('multiline due to preserving a newline', () => { + const doc = YAML.parseDocument(source` + [ + aaaaaaaaa, + + bbbbbbbbb + ] + `) + expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaa,\n\n bbbbbbbbb,\n]\n') + }) + test('multiline due to entry includes a newline', () => { + const doc = YAML.parseDocument(source` +[ + { + a: a # a + }, + bbb +] + `) + expect(doc.toString({ trailingComma: true })).toBe('[\n {\n a: a, # a\n },\n bbb,\n]\n') + }) + test('multiline due to over 80 characters', () => { + const doc = new YAML.Document, false>([ + 'aaaaaaaaa', + 'bbbbbbbbb', + 'ccccccccc', + 'ddddddddd', + 'eeeeeeeee', + 'fffffffff', + 'ggggggggg' + ]) + doc.contents.flow = true + expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaa,\n bbbbbbbbb,\n ccccccccc,\n ddddddddd,\n eeeeeeeee,\n fffffffff,\n ggggggggg,\n]\n') + }) + }) }) test('Quoting colons (#43)', () => { From 1c7a6d498a2ff5e0ba27e96fbfeff994fd33c911 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 2 Mar 2026 15:53:17 -0500 Subject: [PATCH 2/8] Place new option in alphabetical order Signed-off-by: David Thompson --- src/options.ts | 14 +++++++------- src/stringify/stringify.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/options.ts b/src/options.ts index 5836642d..17a46a8f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -371,6 +371,13 @@ export type ToStringOptions = { */ singleQuote?: boolean | null + /** + * Add a trailing comma after the last entry in a flow map or flow sequence that's split across multiple lines. + * + * Default: `'false'` + */ + trailingComma?: boolean + /** * String representation for `true`. * With the core schema, use `'true'`, `'True'`, or `'TRUE'`. @@ -387,11 +394,4 @@ export type ToStringOptions = { * Default: `'true'` */ verifyAliasOrder?: boolean - - /** - * Add a trailing comma after the last entry in a flow map or flow sequence that's split across multiple lines. - * - * Default: `'false'` - */ - trailingComma?: boolean } diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 1aff1311..70708ab1 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -55,9 +55,9 @@ export function createStringifyContext( nullStr: 'null', simpleKeys: false, singleQuote: null, + trailingComma: false, trueStr: 'true', verifyAliasOrder: true, - trailingComma: false, }, doc.schema.toStringOptions, options From cdb6a392d5d65978cb1bdb4e43136d7c0961f97c Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 2 Mar 2026 16:37:39 -0500 Subject: [PATCH 3/8] Clean up tests - Use "source\`\`" consistently - Indent the test source code consistently - Use line length 40 to make the tests shorter Signed-off-by: David Thompson --- tests/doc/stringify.ts | 193 +++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 111 deletions(-) diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index d939f0f9..f4354b7b 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -545,164 +545,135 @@ z: }) describe('trailing comma (maps)', () => { - test('single line due to under 80 characters', () => { - const doc = new YAML.Document, false>({ - a: 'aaaaaaaaa', - b: 'bbbbbbbbb', - c: 'ccccccccc', - }) - doc.contents.flow = true - expect(doc.toString({ trailingComma: true })).toBe('{ a: aaaaaaaaa, b: bbbbbbbbb, c: ccccccccc }\n') + test('no trailing comma is added when the flow map is shorter than the word wrap length', () => { + const doc = YAML.parseDocument(source` + { + a: aaa, + b: bbb, + c: ccc + } + `) + expect(doc.toString({ trailingComma: true })).toBe('{ a: aaa, b: bbb, c: ccc }\n') }) - test('single line due to exactly 80 characters', () => { + test('no trailing comma is added when the flow map is exactly the word wrap length', () => { const doc = YAML.parseDocument(source` -{ - a: aaaaaaa, - b: bbbbbbb, - c: ccccccc, - d: ddddddd, - e: eeeeeee, - f: ffffffff -} + { + a: aaa, + b: bbb, + c: ccc, + d: dddddd + } `) - expect(doc.toString({ trailingComma: true })).toBe('{ a: aaaaaaa, b: bbbbbbb, c: ccccccc, d: ddddddd, e: eeeeeee, f: ffffffff }\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{ a: aaa, b: bbb, c: ccc, d: dddddd }\n') }) - test('multi line due to exactly 81 characters', () => { + test('a trailing comma is added when the flow map is exactly one more than the word wrap length', () => { const doc = YAML.parseDocument(source` -{ - a: aaaaaaa, - b: bbbbbbb, - c: ccccccc, - d: ddddddd, - e: eeeeeee, - f: fffffffff -} + { + a: aaa, + b: bbb, + c: ccc, + d: ddddddd + } `) - expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaa,\n b: bbbbbbb,\n c: ccccccc,\n d: ddddddd,\n e: eeeeeee,\n f: fffffffff,\n}\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{\n a: aaa,\n b: bbb,\n c: ccc,\n d: ddddddd,\n}\n') }) - test('multiline due to existing comment', () => { + test('a trailing comma is added when a comment is present in the flow map', () => { const doc = YAML.parseDocument(source` { - a: aaaaaaaaa, # my cool comment - b: bbbbbbbbb + a: aaa, # my cool comment + b: bbb, + c: ccc } `) - expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaaaa, # my cool comment\n b: bbbbbbbbb,\n}\n') + expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaa, # my cool comment\n b: bbb,\n c: ccc,\n}\n') }) - test('multiline due to preserving a newline', () => { + test('a trailing comma is added when a newline is present in the flow map', () => { const doc = YAML.parseDocument(source` { - a: aaaaaaaaa, + a: aaa, - b: bbbbbbbbb + b: bbb } `) - expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaaaa,\n\n b: bbbbbbbbb,\n}\n') + expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaa,\n\n b: bbb,\n}\n') }) - test('multiline due to entry includes a newline', () => { + test('a trailing comma is added when one of the entries includes a newline', () => { const doc = YAML.parseDocument(source` -{ - a: { - a: a # a - }, - b: bbb -} + { + a: { + a: a # a + }, + b: bbb + } `) expect(doc.toString({ trailingComma: true })).toBe('{\n a:\n {\n a: a, # a\n },\n b: bbb,\n}\n') }) - test('multiline due to over 80 characters', () => { - const doc = new YAML.Document, false>({ - a: 'aaaaaaaaa', - b: 'bbbbbbbbb', - c: 'ccccccccc', - d: 'ddddddddd', - e: 'eeeeeeeee', - f: 'fffffffff' - }) - doc.contents.flow = true - expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaaaaaaaa,\n b: bbbbbbbbb,\n c: ccccccccc,\n d: ddddddddd,\n e: eeeeeeeee,\n f: fffffffff,\n}\n') - }) }) describe('trailing comma (arrays)', () => { - test('single line due to under 80 characters', () => { - const doc = new YAML.Document, false>([ - 'aaaaaaaaa', - 'bbbbbbbbb', - 'ccccccccc', - ]) - doc.contents.flow = true - expect(doc.toString({ trailingComma: true })).toBe('[ aaaaaaaaa, bbbbbbbbb, ccccccccc ]\n') + test('no trailing comma is added when the flow array is shorter than the word wrap length', () => { + const doc = YAML.parseDocument(source` + [ + aaa, + bbb, + ccc + ] + `) + expect(doc.toString({ trailingComma: true })).toBe('[ aaa, bbb, ccc ]\n') }) - test('single line due to exactly 80 characters', () => { + test('no trailing comma is added when the flow array is exactly the word wrap length', () => { const doc = YAML.parseDocument(source` -[ - aaaaaaaaaa, - bbbbbbbbbb, - cccccccccc, - dddddddddd, - eeeeeeeeee, - fffffffffff -] + [ + aaaaaa, + bbbbbb, + cccccc, + ddddddddd + ] `) - expect(doc.toString({ trailingComma: true })).toBe('[ aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, fffffffffff ]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[ aaaaaa, bbbbbb, cccccc, ddddddddd ]\n') }) - test('multi line due to exactly 81 characters', () => { + test('a trailing comma is added when the flow array is exactly one more than the word wrap length', () => { const doc = YAML.parseDocument(source` -[ - aaaaaaaaaa, - bbbbbbbbbb, - cccccccccc, - dddddddddd, - eeeeeeeeee, - ffffffffffff -] + [ + aaaaaa, + bbbbbb, + cccccc, + dddddddddd + ] `) - expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaaa,\n bbbbbbbbbb,\n cccccccccc,\n dddddddddd,\n eeeeeeeeee,\n ffffffffffff,\n]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[\n aaaaaa,\n bbbbbb,\n cccccc,\n dddddddddd,\n]\n') }) - test('multiline due to existing comment', () => { + test('a trailing comma is added when a comment is present in the flow array', () => { const doc = YAML.parseDocument(source` [ - aaaaaaaaa, # my cool comment - bbbbbbbbb + aaa, # my cool comment + bbb, + ccc ] `) - expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaa, # my cool comment\n bbbbbbbbb,\n]\n') + expect(doc.toString({ trailingComma: true })).toBe('[\n aaa, # my cool comment\n bbb,\n ccc,\n]\n') }) - test('multiline due to preserving a newline', () => { + test('a trailing comma is added when a newline is present in the flow array', () => { const doc = YAML.parseDocument(source` [ - aaaaaaaaa, + aaa, - bbbbbbbbb + bbb ] `) - expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaa,\n\n bbbbbbbbb,\n]\n') + expect(doc.toString({ trailingComma: true })).toBe('[\n aaa,\n\n bbb,\n]\n') }) - test('multiline due to entry includes a newline', () => { + test('a trailing comma is added when one of the entries includes a newline', () => { const doc = YAML.parseDocument(source` -[ - { - a: a # a - }, - bbb -] + [ + { + a: a # a + }, + bbb + ] `) expect(doc.toString({ trailingComma: true })).toBe('[\n {\n a: a, # a\n },\n bbb,\n]\n') }) - test('multiline due to over 80 characters', () => { - const doc = new YAML.Document, false>([ - 'aaaaaaaaa', - 'bbbbbbbbb', - 'ccccccccc', - 'ddddddddd', - 'eeeeeeeee', - 'fffffffff', - 'ggggggggg' - ]) - doc.contents.flow = true - expect(doc.toString({ trailingComma: true })).toBe('[\n aaaaaaaaa,\n bbbbbbbbb,\n ccccccccc,\n ddddddddd,\n eeeeeeeee,\n fffffffff,\n ggggggggg,\n]\n') - }) }) }) From 535fccfee5e87037aaed286824de1e87d662d1bb Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 2 Mar 2026 16:56:26 -0500 Subject: [PATCH 4/8] Add test for line length == 0 Signed-off-by: David Thompson --- tests/doc/stringify.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index f4354b7b..1e953898 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -553,7 +553,7 @@ z: c: ccc } `) - expect(doc.toString({ trailingComma: true })).toBe('{ a: aaa, b: bbb, c: ccc }\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{ a: aaa, b: bbb, c: ccc }\n') }) test('no trailing comma is added when the flow map is exactly the word wrap length', () => { const doc = YAML.parseDocument(source` @@ -577,6 +577,16 @@ z: `) expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{\n a: aaa,\n b: bbb,\n c: ccc,\n d: ddddddd,\n}\n') }) + test('no trailing comma is added when the word wrap length is 0', () => { + const doc = YAML.parseDocument(source` + { + a: aaa, + b: bbb, + c: ccc + } + `) + expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe('{ a: aaa, b: bbb, c: ccc }\n') + }) test('a trailing comma is added when a comment is present in the flow map', () => { const doc = YAML.parseDocument(source` { @@ -619,7 +629,7 @@ z: ccc ] `) - expect(doc.toString({ trailingComma: true })).toBe('[ aaa, bbb, ccc ]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[ aaa, bbb, ccc ]\n') }) test('no trailing comma is added when the flow array is exactly the word wrap length', () => { const doc = YAML.parseDocument(source` @@ -643,6 +653,16 @@ z: `) expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[\n aaaaaa,\n bbbbbb,\n cccccc,\n dddddddddd,\n]\n') }) + test('no trailing comma is added when the word wrap length is 0', () => { + const doc = YAML.parseDocument(source` + [ + aaa, + bbb, + ccc + ] + `) + expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe('{ a: aaa, b: bbb, c: ccc }\n') + }) test('a trailing comma is added when a comment is present in the flow array', () => { const doc = YAML.parseDocument(source` [ From d248db9ec837b7415e46fbe8626a8a2dc894c4df Mon Sep 17 00:00:00 2001 From: David Thompson Date: Thu, 5 Mar 2026 14:47:00 -0500 Subject: [PATCH 5/8] Fix for lineWidth == 0, don't recompute values Don't recompute the comment string and, if the line width calculation is done while figuring out if a trailing comma is needed, reuse that value. Signed-off-by: David Thompson --- src/stringify/stringifyCollection.ts | 48 ++++++++++++++++++++-------- tests/doc/stringify.ts | 2 +- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index dafc5e4a..4cf916da 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -108,6 +108,7 @@ function stringifyFlowCollection( let reqNewline = false let linesAtValue = 0 const lines: string[] = [] + let precomputedLineLength = -1 for (let i = 0; i < items.length; ++i) { const item = items[i] let comment: string | null = null @@ -136,21 +137,40 @@ function stringifyFlowCollection( let str = stringify(item, itemCtx, () => (comment = null)) if (i < items.length - 1) { str += ',' - } else if (ctx.options.trailingComma) { - // 'look forwards' to see if entries will be connected with newlines - const willReqNewline = reqNewline - || lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) + (comment ? lineComment(str, itemIndent, commentString(comment)).length : 0) > ctx.options.lineWidth - || lines.length > linesAtValue - || str.includes('\n') - if (willReqNewline) { - str += ',' + if (comment) str += lineComment(str, itemIndent, commentString(comment)) + if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) + reqNewline = true + lines.push(str) + linesAtValue = lines.length + } else { + let renderedComment = '' + if (comment) { + renderedComment = lineComment(str, itemIndent, commentString(comment)) } + + if (ctx.options.trailingComma) { + // only should happen when lines are connected with newlines; + // figure out if that'll happen + let newlineDueToLength = false + if (ctx.options.lineWidth > 0) { + precomputedLineLength = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) + renderedComment.length + newlineDueToLength = precomputedLineLength > ctx.options.lineWidth + } + + const willReqNewline = reqNewline + || newlineDueToLength + || lines.length > linesAtValue + || str.includes('\n') + if (willReqNewline) { + str += ',' + } + } + str += renderedComment + if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) + reqNewline = true + lines.push(str) + linesAtValue = lines.length } - if (comment) str += lineComment(str, itemIndent, commentString(comment)) - if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) - reqNewline = true - lines.push(str) - linesAtValue = lines.length } const { start, end } = flowChars @@ -158,7 +178,7 @@ function stringifyFlowCollection( return start + end } else { if (!reqNewline) { - const len = lines.reduce((sum, line) => sum + line.length + 2, 2) + const len = precomputedLineLength >= 0 ? precomputedLineLength : lines.reduce((sum, line) => sum + line.length + 2, 2) reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth } if (reqNewline) { diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 1e953898..24bf28c8 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -661,7 +661,7 @@ z: ccc ] `) - expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe('{ a: aaa, b: bbb, c: ccc }\n') + expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe('[ aaa, bbb, ccc ]\n') }) test('a trailing comma is added when a comment is present in the flow array', () => { const doc = YAML.parseDocument(source` From c89a2efa178fba0b1983f0d30deb3b7f89990375 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 10 Mar 2026 15:34:22 -0400 Subject: [PATCH 6/8] Further cleanup of logic thanks to Eemeli's comments Signed-off-by: David Thompson --- src/stringify/stringifyCollection.ts | 44 +++++++--------------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index 4cf916da..e91f3760 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -108,7 +108,6 @@ function stringifyFlowCollection( let reqNewline = false let linesAtValue = 0 const lines: string[] = [] - let precomputedLineLength = -1 for (let i = 0; i < items.length; ++i) { const item = items[i] let comment: string | null = null @@ -137,40 +136,19 @@ function stringifyFlowCollection( let str = stringify(item, itemCtx, () => (comment = null)) if (i < items.length - 1) { str += ',' - if (comment) str += lineComment(str, itemIndent, commentString(comment)) - if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) - reqNewline = true - lines.push(str) - linesAtValue = lines.length - } else { - let renderedComment = '' - if (comment) { - renderedComment = lineComment(str, itemIndent, commentString(comment)) + } else if (ctx.options.trailingComma) { + if (ctx.options.lineWidth > 0) { + reqNewline ||= lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth } - - if (ctx.options.trailingComma) { - // only should happen when lines are connected with newlines; - // figure out if that'll happen - let newlineDueToLength = false - if (ctx.options.lineWidth > 0) { - precomputedLineLength = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) + renderedComment.length - newlineDueToLength = precomputedLineLength > ctx.options.lineWidth - } - - const willReqNewline = reqNewline - || newlineDueToLength - || lines.length > linesAtValue - || str.includes('\n') - if (willReqNewline) { - str += ',' - } + reqNewline ||= lines.length > linesAtValue || str.includes('\n') + if (reqNewline) { + str += ',' } - str += renderedComment - if (!reqNewline && (lines.length > linesAtValue || str.includes('\n'))) - reqNewline = true - lines.push(str) - linesAtValue = lines.length } + if (comment) str += lineComment(str, itemIndent, commentString(comment)) + reqNewline ||= lines.length > linesAtValue || str.includes('\n') + lines.push(str) + linesAtValue = lines.length } const { start, end } = flowChars @@ -178,7 +156,7 @@ function stringifyFlowCollection( return start + end } else { if (!reqNewline) { - const len = precomputedLineLength >= 0 ? precomputedLineLength : lines.reduce((sum, line) => sum + line.length + 2, 2) + const len = lines.reduce((sum, line) => sum + line.length + 2, 2) reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth } if (reqNewline) { From 6d64af6dfbe9cfa1929bc1cbd340c2912fe156c8 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 13 Mar 2026 09:06:01 -0400 Subject: [PATCH 7/8] Move newline check right after `str` declaration Signed-off-by: David Thompson --- src/stringify/stringifyCollection.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index e91f3760..1dfa373c 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -134,19 +134,18 @@ function stringifyFlowCollection( if (comment) reqNewline = true let str = stringify(item, itemCtx, () => (comment = null)) + reqNewline ||= lines.length > linesAtValue || str.includes('\n') if (i < items.length - 1) { str += ',' } else if (ctx.options.trailingComma) { if (ctx.options.lineWidth > 0) { reqNewline ||= lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth } - reqNewline ||= lines.length > linesAtValue || str.includes('\n') if (reqNewline) { str += ',' } } if (comment) str += lineComment(str, itemIndent, commentString(comment)) - reqNewline ||= lines.length > linesAtValue || str.includes('\n') lines.push(str) linesAtValue = lines.length } From 620e66855732b25033760cdf8cebe59982671d35 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 13 Mar 2026 09:07:13 -0400 Subject: [PATCH 8/8] Run prettier Signed-off-by: David Thompson --- src/stringify/stringify.ts | 2 +- src/stringify/stringifyCollection.ts | 5 ++- tests/doc/stringify.ts | 56 +++++++++++++++++++++------- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 70708ab1..f7ae04e6 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -57,7 +57,7 @@ export function createStringifyContext( singleQuote: null, trailingComma: false, trueStr: 'true', - verifyAliasOrder: true, + verifyAliasOrder: true }, doc.schema.toStringOptions, options diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index 1dfa373c..bb72c6c3 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -139,7 +139,10 @@ function stringifyFlowCollection( str += ',' } else if (ctx.options.trailingComma) { if (ctx.options.lineWidth > 0) { - reqNewline ||= lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth + reqNewline ||= + lines.reduce((sum, line) => sum + line.length + 2, 2) + + (str.length + 2) > + ctx.options.lineWidth } if (reqNewline) { str += ',' diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 24bf28c8..22cd33b5 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -553,7 +553,9 @@ z: c: ccc } `) - expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{ a: aaa, b: bbb, c: ccc }\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe( + '{ a: aaa, b: bbb, c: ccc }\n' + ) }) test('no trailing comma is added when the flow map is exactly the word wrap length', () => { const doc = YAML.parseDocument(source` @@ -564,7 +566,9 @@ z: d: dddddd } `) - expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{ a: aaa, b: bbb, c: ccc, d: dddddd }\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe( + '{ a: aaa, b: bbb, c: ccc, d: dddddd }\n' + ) }) test('a trailing comma is added when the flow map is exactly one more than the word wrap length', () => { const doc = YAML.parseDocument(source` @@ -575,7 +579,9 @@ z: d: ddddddd } `) - expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('{\n a: aaa,\n b: bbb,\n c: ccc,\n d: ddddddd,\n}\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe( + '{\n a: aaa,\n b: bbb,\n c: ccc,\n d: ddddddd,\n}\n' + ) }) test('no trailing comma is added when the word wrap length is 0', () => { const doc = YAML.parseDocument(source` @@ -585,7 +591,9 @@ z: c: ccc } `) - expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe('{ a: aaa, b: bbb, c: ccc }\n') + expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe( + '{ a: aaa, b: bbb, c: ccc }\n' + ) }) test('a trailing comma is added when a comment is present in the flow map', () => { const doc = YAML.parseDocument(source` @@ -595,7 +603,9 @@ z: c: ccc } `) - expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaa, # my cool comment\n b: bbb,\n c: ccc,\n}\n') + expect(doc.toString({ trailingComma: true })).toBe( + '{\n a: aaa, # my cool comment\n b: bbb,\n c: ccc,\n}\n' + ) }) test('a trailing comma is added when a newline is present in the flow map', () => { const doc = YAML.parseDocument(source` @@ -605,7 +615,9 @@ z: b: bbb } `) - expect(doc.toString({ trailingComma: true })).toBe('{\n a: aaa,\n\n b: bbb,\n}\n') + expect(doc.toString({ trailingComma: true })).toBe( + '{\n a: aaa,\n\n b: bbb,\n}\n' + ) }) test('a trailing comma is added when one of the entries includes a newline', () => { const doc = YAML.parseDocument(source` @@ -616,7 +628,9 @@ z: b: bbb } `) - expect(doc.toString({ trailingComma: true })).toBe('{\n a:\n {\n a: a, # a\n },\n b: bbb,\n}\n') + expect(doc.toString({ trailingComma: true })).toBe( + '{\n a:\n {\n a: a, # a\n },\n b: bbb,\n}\n' + ) }) }) @@ -629,7 +643,9 @@ z: ccc ] `) - expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[ aaa, bbb, ccc ]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe( + '[ aaa, bbb, ccc ]\n' + ) }) test('no trailing comma is added when the flow array is exactly the word wrap length', () => { const doc = YAML.parseDocument(source` @@ -640,7 +656,9 @@ z: ddddddddd ] `) - expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[ aaaaaa, bbbbbb, cccccc, ddddddddd ]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe( + '[ aaaaaa, bbbbbb, cccccc, ddddddddd ]\n' + ) }) test('a trailing comma is added when the flow array is exactly one more than the word wrap length', () => { const doc = YAML.parseDocument(source` @@ -651,7 +669,9 @@ z: dddddddddd ] `) - expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe('[\n aaaaaa,\n bbbbbb,\n cccccc,\n dddddddddd,\n]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 40 })).toBe( + '[\n aaaaaa,\n bbbbbb,\n cccccc,\n dddddddddd,\n]\n' + ) }) test('no trailing comma is added when the word wrap length is 0', () => { const doc = YAML.parseDocument(source` @@ -661,7 +681,9 @@ z: ccc ] `) - expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe('[ aaa, bbb, ccc ]\n') + expect(doc.toString({ trailingComma: true, lineWidth: 0 })).toBe( + '[ aaa, bbb, ccc ]\n' + ) }) test('a trailing comma is added when a comment is present in the flow array', () => { const doc = YAML.parseDocument(source` @@ -671,7 +693,9 @@ z: ccc ] `) - expect(doc.toString({ trailingComma: true })).toBe('[\n aaa, # my cool comment\n bbb,\n ccc,\n]\n') + expect(doc.toString({ trailingComma: true })).toBe( + '[\n aaa, # my cool comment\n bbb,\n ccc,\n]\n' + ) }) test('a trailing comma is added when a newline is present in the flow array', () => { const doc = YAML.parseDocument(source` @@ -681,7 +705,9 @@ z: bbb ] `) - expect(doc.toString({ trailingComma: true })).toBe('[\n aaa,\n\n bbb,\n]\n') + expect(doc.toString({ trailingComma: true })).toBe( + '[\n aaa,\n\n bbb,\n]\n' + ) }) test('a trailing comma is added when one of the entries includes a newline', () => { const doc = YAML.parseDocument(source` @@ -692,7 +718,9 @@ z: bbb ] `) - expect(doc.toString({ trailingComma: true })).toBe('[\n {\n a: a, # a\n },\n bbb,\n]\n') + expect(doc.toString({ trailingComma: true })).toBe( + '[\n {\n a: a, # a\n },\n bbb,\n]\n' + ) }) }) })