fix(parsing): fold line breaks in multi-line single-quoted scalars

Single-quoted scalars share the same flow line folding as double-quoted,
which the previous commit added: a single break folds to a space, a blank
line to one newline, whitespace adjacent to a break is stripped, and the
first-line leading and last-line trailing whitespace are kept. The
single-quoted decoder kept its raw breaks; it now reuses the shared
foldFlowScalarLineBreaks helper, while doubled quotes ('') still escape to
one quote.

Measured against the official YAML test-suite, value-level conformance
rises from 143/169 (84.6%) to 147/169 (87.0%) with no regression.
Parse-level is unchanged: these documents already parsed, just to the
wrong tree.
This commit is contained in:
Mihaela M 2026-06-26 21:18:55 +02:00
parent 08e1979085
commit 27e8defac6
3 changed files with 45 additions and 15 deletions

View File

@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A multi-line single-quoted scalar now folds its line breaks, like the double-quoted fix
below; both share the same flow line folding, so the fold logic is now one helper. Lifts
official test-suite value-level conformance from 143/169 (84.6%) to 147/169 (87.0%), with
no regression.
- A multi-line double-quoted scalar now folds its line breaks instead of keeping them raw.
The decoder appended real line breaks and continuation-line indentation verbatim, so
`"a\n b"` came back as `"a\n b"` rather than `"a b"`. It now applies flow folding: a

View File

@ -112,17 +112,22 @@ extension PureYAML.Parsing.Parser {
}
let inner = text.dropFirst().dropLast()
var output = ""
var iterator = inner.makeIterator()
while let character = iterator.next() {
if character == "'", let next = iterator.next() {
if next == "'" {
output.append("'")
} else {
output.append(character)
output.append(next)
var index = inner.startIndex
while index < inner.endIndex {
let character = inner[index]
if character == "'" {
// A doubled quote escapes to a single quote; a lone quote (not normally present
// inside the stripped body) is kept as-is.
inner.formIndex(after: &index)
output.append("'")
if index < inner.endIndex, inner[index] == "'" {
inner.formIndex(after: &index)
}
} else if character == "\n" || character == "\r" {
foldFlowScalarLineBreaks(in: inner, index: &index, to: &output)
} else {
output.append(character)
inner.formIndex(after: &index)
}
}
return output
@ -148,7 +153,7 @@ extension PureYAML.Parsing.Parser {
}
appendDoubleQuotedEscape(in: inner, index: &index, to: &output)
} else if character == "\n" || character == "\r" {
foldDoubleQuotedLineBreaks(in: inner, index: &index, to: &output)
foldFlowScalarLineBreaks(in: inner, index: &index, to: &output)
} else {
output.append(character)
inner.formIndex(after: &index)
@ -157,12 +162,12 @@ extension PureYAML.Parsing.Parser {
return output
}
/// Flow line folding for a double-quoted scalar at an unescaped line break. Whitespace adjacent
/// to the break is stripped (the current line's trailing, each following line's leading), then a
/// single break folds to one space and a run of breaks (blank lines) folds to one fewer newline.
/// The scalar's first-line leading whitespace and last-line trailing whitespace are untouched,
/// since they sit next to a quote, not a break.
func foldDoubleQuotedLineBreaks(
/// Flow line folding for a quoted (single- or double-) scalar at a line break. Whitespace
/// adjacent to the break is stripped (the current line's trailing, each following line's
/// leading), then a single break folds to one space and a run of breaks (blank lines) folds to
/// one fewer newline. The scalar's first-line leading whitespace and last-line trailing
/// whitespace are untouched, since they sit next to a quote, not a break.
func foldFlowScalarLineBreaks(
in text: Substring,
index: inout Substring.Index,
to output: inout String,

View File

@ -0,0 +1,21 @@
@testable import PureYAML
import Testing
@Suite("Single-quoted scalar folding")
struct SingleQuotedFoldingTests {
@Test("A multi-line single-quoted scalar folds breaks like a double-quoted one")
func test_singleQuotedLineFolding() throws {
let parsed = try PureYAML.parse("' 1st non-empty\n\n 2nd non-empty \n\t3rd non-empty '")
#expect(parsed == .string(" 1st non-empty\n2nd non-empty 3rd non-empty "))
}
@Test("A doubled quote escapes to one while real breaks fold")
func test_singleQuotedEscapeAndFold() throws {
#expect(try PureYAML.parse("'it''s a test\n folded line'") == .string("it's a test folded line"))
}
@Test("A single-line single-quoted scalar is unchanged")
func test_singleQuotedSingleLine() throws {
#expect(try PureYAML.parse("'single'") == .string("single"))
}
}