fix(parsing): preserve leading blank lines in block scalars
Blank lines between a block scalar header and its first content line come before the indent token, so the earlier blank-line capture skipped them. Buffer leading blanks while the block indentation is still pending and flush them as empty content once the indent is established, in the shared indentation path used by both scan loops. Also fix folded bodies to emit a break for a leading blank rather than folding it into a leading space. This closes the last block-scalar blank-line edge from #57.
This commit is contained in:
parent
234e0382e6
commit
ceb12295a4
@ -42,6 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
scalar without blank lines is unchanged.
|
||||
- Keep a more-indented line inside a folded `>` block literal, with its line
|
||||
breaks preserved, rather than folding it into the surrounding text.
|
||||
- Preserve leading blank lines between a block scalar header and its first
|
||||
content line. They precede the indent token, so they are buffered and emitted
|
||||
as empty content once the content indentation is established.
|
||||
|
||||
### Documentation
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ extension PureYAML.Parsing.Scanner {
|
||||
/// chomping and folding can apply to it. The guard keeps blank lines outside
|
||||
/// block scalars unaffected; `isAtLineStart` is true here only when the
|
||||
/// previous line already ended, so a content line's own terminating break
|
||||
/// does not emit a blank.
|
||||
/// does not emit a blank. Blank lines between the header and the first
|
||||
/// content line precede the indent token, so they are buffered and emitted
|
||||
/// once the content indentation is established.
|
||||
func captureBlockScalarBlankLine(_ state: inout State) {
|
||||
guard state.isAtLineStart, state.blockScalarIndentation != nil else {
|
||||
guard state.isAtLineStart else {
|
||||
return
|
||||
}
|
||||
state.append(.scalar(value: "", style: .plain))
|
||||
if state.blockScalarIndentation != nil {
|
||||
state.append(.scalar(value: "", style: .plain))
|
||||
} else if state.pendingBlockScalarIndentation {
|
||||
state.pendingBlockScalarLeadingBlankLines += 1
|
||||
}
|
||||
}
|
||||
|
||||
func startsWith(
|
||||
|
||||
@ -98,9 +98,20 @@ extension PureYAML.Parsing.Scanner {
|
||||
state.append(.indent(width: blockScalarIndentation), mark: marks.start, endMark: marks.end)
|
||||
state.blockScalarIndentation = blockScalarIndentation
|
||||
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
||||
emitBufferedBlockScalarLeadingBlanks(&state)
|
||||
clearPendingBlockScalarIndentation(&state)
|
||||
}
|
||||
|
||||
/// Leading blank lines (between the header and the first content line) are
|
||||
/// buffered until the indent token is emitted, then flushed as empty content
|
||||
/// so they land after the indent and before the first content line.
|
||||
func emitBufferedBlockScalarLeadingBlanks(_ state: inout State) {
|
||||
for _ in 0 ..< state.pendingBlockScalarLeadingBlankLines {
|
||||
state.append(.scalar(value: "", style: .plain))
|
||||
}
|
||||
state.pendingBlockScalarLeadingBlankLines = 0
|
||||
}
|
||||
|
||||
func applyIncreasedIndentation(
|
||||
width: Int,
|
||||
start: PureYAML.Parsing.Mark,
|
||||
@ -114,6 +125,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
}
|
||||
state.blockScalarIndentation = width
|
||||
state.blockScalarContentPrefix = ""
|
||||
emitBufferedBlockScalarLeadingBlanks(&state)
|
||||
clearPendingBlockScalarIndentation(&state)
|
||||
}
|
||||
|
||||
@ -153,6 +165,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
func clearPendingBlockScalarIndentation(_ state: inout State) {
|
||||
state.pendingBlockScalarIndentation = false
|
||||
state.pendingBlockScalarIndentationIndicator = nil
|
||||
state.pendingBlockScalarLeadingBlankLines = 0
|
||||
}
|
||||
|
||||
func closeIndentation(_ state: inout State) {
|
||||
|
||||
@ -52,6 +52,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
var validImplicitIndentation: Set<Int> = []
|
||||
var pendingBlockScalarIndentation = false
|
||||
var pendingBlockScalarIndentationIndicator: Int?
|
||||
var pendingBlockScalarLeadingBlankLines = 0
|
||||
var blockScalarIndentation: Int?
|
||||
var blockScalarContentPrefix = ""
|
||||
var tagHandles = [
|
||||
|
||||
@ -255,11 +255,13 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
}
|
||||
|
||||
func foldedBlockBody(_ lines: [String]) -> String {
|
||||
guard let first = lines.first else {
|
||||
return ""
|
||||
guard let firstContent = lines.firstIndex(where: { !$0.isEmpty }) else {
|
||||
// The block is entirely blank lines: one break each, no folding.
|
||||
return String(repeating: "\n", count: lines.count)
|
||||
}
|
||||
var result = first
|
||||
var index = 1
|
||||
// Leading blank lines each contribute a line break before the content.
|
||||
var result = String(repeating: "\n", count: firstContent) + lines[firstContent]
|
||||
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
|
||||
|
||||
@ -42,6 +42,13 @@ struct BlockScalarChompingTests {
|
||||
#expect(try value("k: >\n a\n code\nnext: 1\n") == .string("a\n code\n"))
|
||||
}
|
||||
|
||||
@Test("Leading blank lines before the first content line are preserved")
|
||||
func test_leadingBlankLinesPreserved() throws {
|
||||
#expect(try value("k: |\n\n a\n b\nnext: 1\n") == .string("\na\nb\n"))
|
||||
#expect(try value("k: |\n\n\n a\nnext: 1\n") == .string("\n\na\n"))
|
||||
#expect(try value("k: >\n\n a\n b\nnext: 1\n") == .string("\na b\n"))
|
||||
}
|
||||
|
||||
@Test("A block scalar with no blank lines is unaffected")
|
||||
func test_blockScalarWithoutBlankLinesUnchanged() throws {
|
||||
#expect(try value("k: |\n a\n b\nnext: 1\n") == .string("a\nb\n"))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user