diff --git a/CHANGELOG.md b/CHANGELOG.md index 53377d8..a029d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/Scalar.swift b/Sources/Parsing/Scalar.swift index d4ac470..8cb55a8 100644 --- a/Sources/Parsing/Scalar.swift +++ b/Sources/Parsing/Scalar.swift @@ -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, diff --git a/Tests/SingleQuotedFoldingTests.swift b/Tests/SingleQuotedFoldingTests.swift new file mode 100644 index 0000000..94abc4e --- /dev/null +++ b/Tests/SingleQuotedFoldingTests.swift @@ -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")) + } +}