From b24a51188bf4519099f005fc1a07d3741c53128f Mon Sep 17 00:00:00 2001 From: UlikGames Date: Sun, 2 Aug 2026 12:26:05 +0300 Subject: [PATCH] Fix round-trip of block scalars whose content starts with a space --- src/stringify/foldFlowLines.ts | 8 ++++++++ src/stringify/stringifyString.ts | 16 +++++++++------- tests/doc/foldFlowLines.ts | 3 ++- tests/doc/stringify.ts | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/stringify/foldFlowLines.ts b/src/stringify/foldFlowLines.ts index b1734645..386499fb 100644 --- a/src/stringify/foldFlowLines.ts +++ b/src/stringify/foldFlowLines.ts @@ -68,6 +68,14 @@ export function foldFlowLines( let escStart = -1 let escEnd = -1 if (mode === FOLD_BLOCK) { + if (text[0] === ' ' || text[0] === '\t') { + // Unlike the following ones, the first line does not include the block + // indent, which the caller adds. Any white space here is therefore + // content of a more-indented line, which must not be folded. + const nl = text.indexOf('\n') + if (nl === -1) return text + i = nl + } i = consumeMoreIndentedLines(text, i, indent.length) if (i !== -1) end = i + endStep } diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index ffaae708..33dd34a2 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -190,9 +190,14 @@ function blockString( return quotedString(value, ctx) } + // Content starting with a space requires an explicit indentation indicator, + // which is only unambiguous if the block itself is indented as well. + const startsWithSpace = /^[\n ]*[ ]/.test(value) const indent = ctx.indent || - (ctx.forceBlockIndent || containsDocumentMarker(value) ? ' ' : '') + (ctx.forceBlockIndent || startsWithSpace || containsDocumentMarker(value) + ? ' ' + : '') const literal = blockQuote === 'literal' ? true @@ -227,14 +232,12 @@ function blockString( } // determine indent indicator from whitespace at value start - let startWithSpace = false let startEnd: number let startNlPos = -1 for (startEnd = 0; startEnd < value.length; ++startEnd) { const ch = value[startEnd] - if (ch === ' ') startWithSpace = true - else if (ch === '\n') startNlPos = startEnd - else break + if (ch === '\n') startNlPos = startEnd + else if (ch !== ' ') break } let start = value.substring( 0, @@ -245,9 +248,8 @@ function blockString( start = start.replace(/\n+/g, `$&${indent}`) } - const indentSize = indent ? '2' : '1' // root is at -1 // Leading | or > is added later - let header = (startWithSpace ? indentSize : '') + chomp + let header = (startsWithSpace ? '2' : '') + chomp if (comment) { header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' ')) if (onComment) onComment() diff --git a/tests/doc/foldFlowLines.ts b/tests/doc/foldFlowLines.ts index 20a3c291..72261381 100644 --- a/tests/doc/foldFlowLines.ts +++ b/tests/doc/foldFlowLines.ts @@ -321,7 +321,8 @@ describe('end-to-end', () => { test('More-indented first line (#55)', () => { const str = ' first more-indented line\nnext line\n' const ys = YAML.stringify(str, foldOptions) - expect(ys).toBe('>1\n first more-indented line\nnext line\n') + expect(ys).toBe('>2\n first more-indented line\n next line\n') + expect(YAML.parse(ys)).toBe(str) }) test('plain string', () => { diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 4aea7796..265688f3 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -1407,6 +1407,38 @@ describe('Document markers in top-level scalars', () => { }) }) +describe('Block scalars with leading spaces (#692)', () => { + for (const [value, exp] of [ + [' a\n b', '|2-\n a\n b\n'], + [' indented\nlines', '|2-\n indented\n lines\n'], + [' a\nb', '|2-\n a\n b\n'], + [' x\n y\nz', '|2-\n x\n y\n z\n'], + [' deep\nshallow', '|2-\n deep\n shallow\n'], + ['\n a', '|2-\n \n a\n'], + [' \nabc', '|2-\n \n abc\n'], + [' a\n', '|2\n a\n'], + [' a\n\n', '|2+\n a\n\n'] + ]) { + test(JSON.stringify(value), () => { + const str = YAML.stringify(value) + expect(str).toBe(exp) + expect(YAML.parse(str)).toBe(value) + }) + } + + test('as a map value', () => { + const str = YAML.stringify({ key: ' a\n b' }) + expect(str).toBe('key: |2-\n a\n b\n') + expect(YAML.parse(str)).toEqual({ key: ' a\n b' }) + }) + + test('as a seq item', () => { + const str = YAML.stringify([' a\n b']) + expect(str).toBe('- |2-\n a\n b\n') + expect(YAML.parse(str)).toEqual([' a\n b']) + }) +}) + describe('Document markers in top-level map keys (#431)', () => { test('---', () => { const str = YAML.stringify({ '--- x': 42 })