fix(parsing): base an inline-mapping indent indicator on the mapping

An explicit block scalar indentation indicator is the content indentation
relative to the block scalar's parent node. For an inline mapping value
(`- aaa: |2`) the parent is the mapping, but a compact inline mapping never
pushes its indentation onto the main stack, so the indicator was added to the
stack level (0) rather than the mapping's (2). The content indentation then
came out too small and the next mapping key was pulled into the scalar.

Capture at the header whether the block scalar is an inline mapping value
(a mapping value was seen on its line). When it is, fold the deepest
applicable implicit indentation into the indicator's base; a direct sequence
item (`- |1`), which has no mapping value on its line, keeps the sequence's
own level, so the explicit indicators on sequence items stay correct.

Raises yaml-test-suite value-level conformance to 191/194 (98.5%) with the
parse-level and should-error rejection rates unchanged. 463 tests pass.
This commit is contained in:
Mihaela M 2026-06-28 23:24:21 +02:00
parent a33be7c37a
commit 8bc855d7a0
5 changed files with 60 additions and 1 deletions

View File

@ -84,6 +84,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- An explicit block scalar indentation indicator (`|2`, `>1`) on an inline mapping value now measures
from the mapping's indentation. A value like `- aaa: |2` has the inline mapping as its parent, but
that mapping sits at an implicit indentation off the main stack, so the indicator was added to the
stack's level (0) instead of the mapping's (2), making the content indentation too small and pulling
the next key into the scalar. The deepest applicable implicit indentation is now folded in for an
inline mapping value, while a direct sequence item (`- |1`) keeps the sequence's own level. Raises
yaml-test-suite value-level conformance to 191/194 (98.5%), with the parse and should-error rates
unchanged.
- Compact nested block sequences (`- - x`) now separate their outer items correctly. Such a sequence
carries no indent/dedent tokens, only block entries at their columns, but the sibling-entry loop
collected every following block entry regardless of column, so `- - x` then `- - y` became one

View File

@ -170,7 +170,15 @@ extension PureYAML.Parsing.Scanner {
marks: (start: PureYAML.Parsing.Mark, end: PureYAML.Parsing.Mark),
state: inout State,
) throws {
let blockScalarIndentation = current + indicator
// The indicator is the content indentation relative to the block scalar's parent node. For an
// inline mapping value (`- aaa: |2`), the parent mapping sits at an implicit indentation that
// never reached the main stack, so take the deepest applicable implicit level as the base:
// `|2` under a mapping at column 2 makes the content indentation 4. A direct sequence item
// (`- |2`) has no such parent and keeps the stack's current level.
let parentIndentation = state.pendingBlockScalarInlineMappingValue
? max(current, state.validImplicitIndentation.filter { $0 <= width }.max() ?? current)
: current
let blockScalarIndentation = parentIndentation + indicator
guard width >= blockScalarIndentation else {
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: marks.end.line)
}
@ -252,6 +260,7 @@ extension PureYAML.Parsing.Scanner {
state.pendingBlockScalarLeadingBlankLines = 0
state.pendingBlockScalarMaxBlankIndent = 0
state.pendingBlockScalarIsRootNode = false
state.pendingBlockScalarInlineMappingValue = false
}
func closeIndentation(_ state: inout State) {

View File

@ -98,6 +98,11 @@ extension PureYAML.Parsing.Scanner {
&& !state.pendingExplicitKey
&& state.validImplicitIndentation.isEmpty
&& state.flowDepth == 0
// Whether this block scalar is an inline mapping value (`- key: |2`). Such a value's parent is
// the mapping, which sits at an implicit indentation off the main stack, so an explicit
// indicator is measured relative to it. A direct sequence item (`- |2`) has no mapping value
// on its line and keeps the sequence's own indentation as the base.
state.pendingBlockScalarInlineMappingValue = state.sawMappingValueOnLine && state.flowDepth == 0
}
func scanBlockScalarContent(_ state: inout State) {

View File

@ -64,6 +64,10 @@ extension PureYAML.Parsing.Scanner {
/// Whether the pending block scalar is the document's own root node (no enclosing collection),
/// so its content may begin at column 0 (the document parent's indentation being -1).
var pendingBlockScalarIsRootNode = false
/// Whether the pending block scalar is an inline mapping value (`- key: |2`), whose parent
/// mapping sits at an implicit indentation off the main stack, so an explicit indentation
/// indicator is measured relative to that level rather than the stack's.
var pendingBlockScalarInlineMappingValue = false
var pendingBlockScalarLeadingBlankLines = 0
/// The widest indentation seen on a block scalar's leading blank lines (before its first
/// content line). The spec forbids a leading empty line from being more indented than the

View File

@ -0,0 +1,32 @@
@testable import PureYAML
import Testing
/// An explicit block scalar indentation indicator (`|2`, `>1`) is the content indentation relative to
/// the block scalar's parent node. For an inline mapping value (`- aaa: |2`) the parent is the
/// mapping, which sits at an implicit indentation off the main stack, so the indicator is measured
/// from there. A direct sequence item (`- |1`) has no such parent and keeps the sequence's own level.
@Suite("Block scalar indent indicator")
struct BlockScalarIndentIndicatorTests {
@Test("An indicator on an inline mapping value is relative to the mapping indent")
func test_inlineMappingValue() throws {
#expect(try PureYAML.parse("- aaa: |2\n xxx\n bbb: |\n xxx\n") == .sequence([
.mapping(.init([
.init(key: "aaa", value: .string("xxx\n")),
.init(key: "bbb", value: .string("xxx\n")),
])),
]))
}
@Test("An indicator on a root mapping value is relative to the root")
func test_rootMappingValue() throws {
#expect(try PureYAML.parse("aaa: |2\n xxx\n") == .mapping(.init([
.init(key: "aaa", value: .string("xxx\n")),
])))
}
@Test("An indicator on a direct sequence item keeps the sequence base")
func test_sequenceItem() throws {
// `|1` over ` explicit` (indent 2) strips one space at the sequence's own base, not three.
#expect(try PureYAML.parse("- |1\n explicit\n") == .sequence([.string(" explicit\n")]))
}
}