fix(parsing): end an inline item value at a sibling block entry

A block entry with an inline node recorded only the node's indent (one past
the dash) as a valid implicit level, not the dash's own indent. So a plain
scalar that is an inline sequence item value could not tell a sibling entry
that returns to the sequence indent from its own content, and a compact nested
sequence as an explicit key's value folded into one scalar: `? a` / `: - b` /
`  - - c` / `   - d` became `{a: [b -  - c - d]}` instead of `{a: [b, [c, d]]}`.

Record the entry's own indent as a sibling level too, so the continuation
stops there. Skip it under a pending explicit key, whose own sequence key
drives the implicit indentation differently (an explicit key whose key is a
sequence, `? - x`, must keep parsing). A block entry more indented than its
container is still plain content, so a wrapped value beginning a line with
`- ` folds in. The block-entry recording moved to a helper to keep the file
within its length limit.

Brings yaml-test-suite value-level conformance to 197/197 (100.0%) with the
parse-level and should-error rejection rates unchanged. 485 tests pass.
This commit is contained in:
Mihaela M 2026-06-29 02:09:57 +02:00
parent 3b504a9c3a
commit 4b06f6e5fc
4 changed files with 61 additions and 2 deletions

View File

@ -84,6 +84,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A plain scalar that is an inline block sequence item value (`: - b`) now stops folding at a later
block entry that returns to the sequence's own indent, so the dashes form the rest of the sequence
rather than folding into the first item. A block entry only recorded its inline node's indent as a
valid implicit level, not the dash's own indent, so a continuation could not tell a sibling entry
from content, and `? a` / `: - b` / ` - - c` / ` - d` folded into one scalar. The entry's indent
is now recorded as a sibling level too (except under an explicit key, whose sequence key drives
implicit indentation itself), while a block entry more indented than its container stays plain
content. Brings yaml-test-suite value-level conformance to 197/197 (100.0%), with the parse and
should-error rates unchanged.
- A leading blank line of a block scalar now keeps any whitespace past the content indentation as
content, as a blank line within the scalar already did. The leading blank lines (those before the
first content line) were only counted, so an otherwise-blank `␣\t` line under a one-space content

View File

@ -349,6 +349,21 @@ extension PureYAML.Parsing.Scanner {
/// further key would form a forbidden inline nested mapping; in flow style several pairs per
/// line are allowed, so that restriction does not apply. In flow a scalar that is the content of
/// an open explicit key (`?`) is the key itself, not a new implicit key.
/// Records the implicit indentation levels a block entry with an inline node introduces: the
/// node's own indent (one past the dash), and the dash's indent as a sibling level so an inline
/// item's plain value stops folding at a later entry that returns to it (`: -␣b` then ` - - c`).
/// The sibling level is skipped under a pending explicit key (`? - x`), whose sequence key drives
/// implicit indentation itself.
func recordBlockEntryImplicitIndentation(start: PureYAML.Parsing.Mark, state: inout State) {
guard hasNodeOnSameLine(afterCurrentPositionIn: state.reader) else {
return
}
state.validImplicitIndentation.insert(start.column + 1)
if !state.pendingExplicitKey {
state.validImplicitIndentation.insert(start.column - 1)
}
}
func canSynthesizeImplicitBlockKey(_ state: State) -> Bool {
if state.flowDepth > 0 {
return state.flowContexts.last?.hasExplicitKey != true

View File

@ -369,8 +369,8 @@ extension PureYAML.Parsing.Scanner {
let start = state.reader.mark
state.reader.advance()
state.append(kind, mark: start, endMark: state.reader.mark)
if case .blockEntry = kind, hasNodeOnSameLine(afterCurrentPositionIn: state.reader) {
state.validImplicitIndentation.insert(start.column + 1)
if case .blockEntry = kind {
recordBlockEntryImplicitIndentation(start: start, state: &state)
}
if case .mappingValue = kind, state.flowContexts.last?.kind == .mapping {
state.flowContexts[state.flowContexts.count - 1].isExpectingKey = false

View File

@ -0,0 +1,34 @@
@testable import PureYAML
import Testing
/// A plain scalar that is an inline block sequence item value (`: - b`) stops folding when a later
/// block entry returns to the sequence's own indent: those dashes form the rest of the sequence, not
/// content of the first item. A block entry that is more indented than the sequence is still plain
/// content of the scalar, so a wrapped value that happens to begin a line with `- ` folds in.
@Suite("Inline sequence item continuation")
struct InlineSequenceItemContinuationTests {
@Test("An inline item scalar ends at a sibling block entry")
func test_siblingEntryEndsItem() throws {
// `: - b` then ` - c` is the two-item sequence `[b, c]`, not `[b - c]`.
#expect(try PureYAML.parse(": - b\n - c\n") == .mapping(.init([
.init(keyNode: .string(""), value: .sequence([.string("b"), .string("c")])),
])))
}
@Test("A nested compact sequence as an explicit-key value parses with tabs")
func test_explicitKeyNestedSequenceWithTabs() throws {
#expect(try PureYAML.parse("? a\n: -\tb\n - -\tc\n - d\n") == .mapping(.init([
.init(key: "a", value: .sequence([
.string("b"),
.sequence([.string("c"), .string("d")]),
])),
])))
}
@Test("A more-indented dash inside a wrapped value stays content")
func test_moreIndentedDashIsContent() throws {
#expect(try PureYAML.parse("- description: a query\n and 3\n - Other.\n") == .sequence([
.mapping(.init([.init(key: "description", value: .string("a query and 3 - Other."))])),
]))
}
}