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")])) + } +}