From 8bc855d7a0677277099fc2df4b46f5f91ab51c43 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sun, 28 Jun 2026 23:24:21 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 9 ++++++ Sources/Parsing/Scanner.Indentation.swift | 11 ++++++- Sources/Parsing/Scanner.Scalars.swift | 5 ++++ Sources/Parsing/Scanner.swift | 4 +++ Tests/BlockScalarIndentIndicatorTests.swift | 32 +++++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 Tests/BlockScalarIndentIndicatorTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 9403d12..0618f16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/Scanner.Indentation.swift b/Sources/Parsing/Scanner.Indentation.swift index a135451..a90b55d 100644 --- a/Sources/Parsing/Scanner.Indentation.swift +++ b/Sources/Parsing/Scanner.Indentation.swift @@ -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) { diff --git a/Sources/Parsing/Scanner.Scalars.swift b/Sources/Parsing/Scanner.Scalars.swift index 8520de7..2ebf278 100644 --- a/Sources/Parsing/Scanner.Scalars.swift +++ b/Sources/Parsing/Scanner.Scalars.swift @@ -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) { diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index 844d81a..5c87eaa 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -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 diff --git a/Tests/BlockScalarIndentIndicatorTests.swift b/Tests/BlockScalarIndentIndicatorTests.swift new file mode 100644 index 0000000..4d16d3e --- /dev/null +++ b/Tests/BlockScalarIndentIndicatorTests.swift @@ -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")])) + } +}