diff --git a/src/options.ts b/src/options.ts index 2b8a0585..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'`. diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 533f5ff7..f7ae04e6 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -55,6 +55,7 @@ export function createStringifyContext( nullStr: 'null', simpleKeys: false, singleQuote: null, + trailingComma: false, trueStr: 'true', verifyAliasOrder: true }, diff --git a/src/stringify/stringifyCollection.ts b/src/stringify/stringifyCollection.ts index a920fe34..bb72c6c3 100644 --- a/src/stringify/stringifyCollection.ts +++ b/src/stringify/stringifyCollection.ts @@ -134,10 +134,21 @@ function stringifyFlowCollection( if (comment) reqNewline = true let str = stringify(item, itemCtx, () => (comment = null)) - if (i < items.length - 1) str += ',' + 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 + } + if (reqNewline) { + 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 } diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 24e86610..22cd33b5 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -543,6 +543,186 @@ z: expect(String(doc)).toBe(src) }) }) + + describe('trailing comma (maps)', () => { + 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, 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` + { + a: aaa, + b: bbb, + c: ccc, + d: dddddd + } + `) + 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` + { + a: aaa, + b: bbb, + c: ccc, + d: ddddddd + } + `) + 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` + { + a: aaa, # my cool comment + b: bbb, + c: ccc + } + `) + 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` + { + a: aaa, + + b: bbb + } + `) + 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` + { + 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' + ) + }) + }) + + describe('trailing comma (arrays)', () => { + 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, 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` + [ + aaaaaa, + bbbbbb, + cccccc, + ddddddddd + ] + `) + 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` + [ + aaaaaa, + bbbbbb, + cccccc, + dddddddddd + ] + `) + 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( + '[ aaa, bbb, ccc ]\n' + ) + }) + test('a trailing comma is added when a comment is present in the flow array', () => { + const doc = YAML.parseDocument(source` + [ + aaa, # my cool comment + bbb, + ccc + ] + `) + 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` + [ + aaa, + + bbb + ] + `) + 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` + [ + { + a: a # a + }, + bbb + ] + `) + expect(doc.toString({ trailingComma: true })).toBe( + '[\n {\n a: a, # a\n },\n bbb,\n]\n' + ) + }) + }) }) test('Quoting colons (#43)', () => {