fix(parsing): comments and over-indented blanks ending block scalars

A block scalar terminated by a less-indented comment line failed with a
spurious 'unexpected indentation': once the smaller indentation ended the
scalar, the comment's own column was processed as a dedent, and because no
indentation level held that width the dedent raised an error. A comment's
indentation does not participate in block structure, so a comment that ends
a block scalar is now left for the main loop to scan, with the block
scalar's own first content line (which may itself begin with '#') still
treated as content.

Separately, a whitespace-only line indented past a block scalar's content
level lost its surplus spaces because the width was consumed before the
line was recognized as blank; the surplus is now preserved as the line's
content, so an over-indented blank contributes the correct value.

Measured against the official YAML test-suite, parse-level conformance
rises from 240/333 (72.1%) to 243/333 (73.0%) with matching value-level
gains, and no regression.
This commit is contained in:
Mihaela M 2026-06-27 15:34:13 +02:00
parent ceec6cca84
commit b80e9c1c63
4 changed files with 62 additions and 1 deletions

View File

@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A block scalar followed by a less-indented comment line no longer fails with a spurious
"unexpected indentation". When a comment ends a block scalar, its own column was processed as
a dedent and, landing on a width no indentation level held, raised an error; a comment's
indentation is now left out of block structure. Relatedly, a whitespace-only line indented
past a block scalar's content level now keeps its surplus spaces as content instead of
collapsing to empty, so such a line contributes the correct value. Lifts official test-suite
parse-level conformance from 240/333 (72.1%) to 243/333 (73.0%) and value-level matches.
- A quoted mapping key may now have a space before its `:` (`"key" : value`). A plain key
consumes the space before the colon into the scalar, but a quoted key stops at its closing
quote, leaving the space, and the implicit-key check looked only at the immediate next

View File

@ -11,7 +11,10 @@ extension PureYAML.Parsing.Scanner {
return
}
if state.blockScalarIndentation != nil {
state.append(.scalar(value: "", style: .plain))
// The prefix holds any surplus indentation past the block's content level for an
// otherwise-empty line (see `preserveBlockScalarSurplusOnBlankLine`); empty otherwise.
state.append(.scalar(value: state.blockScalarContentPrefix, style: .plain))
state.blockScalarContentPrefix = ""
} else if state.pendingBlockScalarIndentation {
state.pendingBlockScalarLeadingBlankLines += 1
}

View File

@ -3,15 +3,27 @@ extension PureYAML.Parsing.Scanner {
let start = state.reader.mark
let width = try scanIndentationWidth(&state)
guard shouldProcessIndentation(state) else {
preserveBlockScalarSurplusOnBlankLine(width: width, state: &state)
return
}
let end = state.reader.mark
let current = state.indentation.last ?? 0
let wasInBlockScalar = state.blockScalarIndentation != nil
if consumeBlockScalarContentIndentation(width: width, state: &state) {
return
}
// An active block scalar that just ended (its content was less indented this line) onto a
// line beginning with '#' is leaving an ordinary comment. A comment's own indentation does
// not participate in block structure, so leave it for the main loop to scan rather than
// forcing its column to match an indentation level, which would dedent onto a width no stack
// level holds and raise a spurious error. The guard on `wasInBlockScalar` keeps a block
// scalar's own first content line that begins with '#' (e.g. `|-` then `# text`) as content.
if wasInBlockScalar, state.reader.peek() == "#" {
return
}
if let indicator = state.pendingBlockScalarIndentationIndicator {
try applyExplicitBlockScalarIndentation(
indicator: indicator,
@ -30,6 +42,18 @@ extension PureYAML.Parsing.Scanner {
clearPendingBlockScalarIndentation(&state)
}
/// A whitespace-only line inside an active block scalar that is indented past the block's
/// content indentation carries the surplus spaces as content (e.g. a 3-space line in a 2-space
/// block contributes `" "`). The width is consumed before the line is recognized as blank, so
/// stash the surplus in the content prefix for the blank-line emission to pick up; otherwise the
/// line collapses to empty and a space is dropped from the scalar's value.
func preserveBlockScalarSurplusOnBlankLine(width: Int, state: inout State) {
guard let blockScalarIndentation = state.blockScalarIndentation, width > blockScalarIndentation else {
return
}
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
}
func scanIndentationWidth(_ state: inout State) throws -> Int {
var width = 0
while let character = state.reader.peek() {

View File

@ -0,0 +1,27 @@
@testable import PureYAML
import Testing
@Suite("Block scalar indentation edges")
struct BlockScalarTrailingCommentTests {
@Test("A comment less indented than block scalar content ends it without error")
func test_lessIndentedTrailingCommentEndsBlockScalar() throws {
// The `# comment` sits at column 2, below the content indent (column 3), so it ends the
// block scalar and is an ordinary comment rather than driving a dedent onto an invalid
// indentation level.
#expect(try PureYAML.parse("|\n text\n # comment\n") == .string("text\n"))
}
@Test("An over-indented blank line keeps its surplus spaces as content")
func test_overIndentedBlankLineKeepsSurplusSpaces() throws {
// The middle line carries three spaces against a two-space content indent, so one surplus
// space is preserved as the line's content instead of collapsing to empty.
#expect(try PureYAML.parse("|\n a\n \n b\n") == .string("a\n \nb\n"))
}
@Test("A block scalar whose own content starts with '#' keeps it as literal text")
func test_blockScalarContentStartingWithHash() throws {
#expect(try PureYAML.parse("strip: |-\n # text\n") == .mapping(.init([
.init(key: "strip", value: .string("# text")),
])))
}
}