fix(parsing): keep both breaks at a folded blank by a more-indented line

In a folded block scalar, a blank run of N lines between two non-more-indented
lines folds to N line breaks: the fold absorbs the first break. Next to a
more-indented line nothing folds, so that break survives and the run yields
N+1 breaks. The folder used N in every case, so paragraph spacing around an
indented block collapsed: a blank line before a more-indented block came out
as one break instead of two.

Track whether the line before the blank run, or the one after it, is
more-indented and add the extra break in that case. Folding between plain
lines and runs with no blank lines are unchanged.

Raises yaml-test-suite value-level conformance to 186/194 (95.9%) with the
parse-level and should-error rejection rates unchanged. 444 tests pass.
This commit is contained in:
Mihaela M 2026-06-28 21:14:14 +02:00
parent 246e6b1891
commit 37ac4d862d
3 changed files with 49 additions and 3 deletions

View File

@ -84,6 +84,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A blank line next to a more-indented line in a folded block scalar (`>`) now keeps both line
breaks. Between two non-more-indented lines a blank run of N folds to N breaks (the fold is
absorbed into the first), but next to a more-indented line nothing folds, so the absorbed break
survives and the run yields N+1 breaks. The folder counted N breaks in every case, collapsing the
paragraph spacing around indented blocks. Raises yaml-test-suite value-level conformance to 186/194
(95.9%), with the parse and should-error rates unchanged.
- Plain scalar line folding now strips tabs, not only spaces, around continuation lines. A tab is not
indentation, so a continuation line's leading tab (`\t3rd`) is folding white space and folds away
rather than entering the value, and a line of only white space that holds a tab (`␣␣\t`) is an empty

View File

@ -281,14 +281,18 @@ extension PureYAML.Parsing.TokenEventParser {
var index = firstContent + 1
while index < lines.count {
if lines[index].isEmpty {
// A run of N blank lines folds to N line breaks: the first break
// is absorbed, the rest are literal.
// Between two non-more-indented lines a blank run of N folds to N line breaks: the
// first break is the absorbed fold, the rest are literal. Next to a more-indented line
// nothing folds, so the absorbed break survives too and the run yields N+1 breaks.
let beforeMoreIndented = isMoreIndentedBlockLine(lines[index - 1])
var blankCount = 0
while index < lines.count, lines[index].isEmpty {
blankCount += 1
index += 1
}
result += String(repeating: "\n", count: blankCount)
let afterMoreIndented = index < lines.count && isMoreIndentedBlockLine(lines[index])
let breaks = beforeMoreIndented || afterMoreIndented ? blankCount + 1 : blankCount
result += String(repeating: "\n", count: breaks)
if index < lines.count {
result += lines[index]
index += 1

View File

@ -0,0 +1,35 @@
@testable import PureYAML
import Testing
/// In a folded block scalar (`>`), a single line break between two non-more-indented lines folds to a
/// space, and a blank run of N lines between them yields N line breaks (the fold is absorbed). Next to
/// a more-indented line nothing folds, so the absorbed break survives and the run yields N+1 breaks;
/// more-indented lines keep their own line breaks.
@Suite("Folded scalar blank lines")
struct FoldedScalarBlankLineTests {
@Test("A blank line next to a more-indented line keeps both breaks")
func test_blankNextToMoreIndented() throws {
let input = ">\n Sammy Sosa completed another\n fine season with great stats.\n\n"
+ " 63 Home Runs\n 0.288 Batting Average\n\n What a year!\n"
#expect(try PureYAML.parse(input) == .string(
"Sammy Sosa completed another fine season with great stats.\n\n"
+ " 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n",
))
}
@Test("A blank line between non-more-indented lines folds one break away")
func test_blankBetweenPlainLines() throws {
// `folded`+`line` fold to a space; the blank yields one break; the more-indented bullets keep
// their breaks and the blank around them keeps both.
let input = ">\n\n folded\n line\n\n next\n line\n * bullet\n\n"
+ " * list\n * lines\n\n last\n line\n\n# Comment\n"
#expect(try PureYAML.parse(input) == .string(
"\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n",
))
}
@Test("A plain folded scalar with no blank lines is unchanged")
func test_noBlankLines() throws {
#expect(try PureYAML.parse(">\n folded\n text\n") == .string("folded text\n"))
}
}