From 37ac4d862db17e131c9bf02eebe4ee802ecf5e8f Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sun, 28 Jun 2026 21:14:14 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 7 ++++ Sources/Parsing/TokenEventParser.Block.swift | 10 ++++-- Tests/FoldedScalarBlankLineTests.swift | 35 ++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 Tests/FoldedScalarBlankLineTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 765117c..084b6ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/TokenEventParser.Block.swift b/Sources/Parsing/TokenEventParser.Block.swift index 52616d1..5992038 100644 --- a/Sources/Parsing/TokenEventParser.Block.swift +++ b/Sources/Parsing/TokenEventParser.Block.swift @@ -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 diff --git a/Tests/FoldedScalarBlankLineTests.swift b/Tests/FoldedScalarBlankLineTests.swift new file mode 100644 index 0000000..da4f857 --- /dev/null +++ b/Tests/FoldedScalarBlankLineTests.swift @@ -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")) + } +}