fix(parsing): keep a root node that begins on the document-marker line

A document-start marker scan skipped the rest of the '---' line
unconditionally, so a root node written on that same line was discarded:
a block-scalar header ('--- >' / '--- |') was dropped and its body
mis-read as a plain scalar (folded to spaces, no trailing newline), and a
same-line plain or flow node was lost entirely. The document start now
skips only trailing spaces and leaves any real same-line content for the
parser; an empty rest-of-line or a comment is still consumed, and a node
on the line below the marker is unaffected. Document end ('...') still
takes no same-line node.

Measured against the official YAML test-suite, value-level conformance
rises from 130/170 (76.5%) to 135/169 (79.9%) with no regression.
Parse-level is unchanged: these documents already parsed, just to the
wrong tree, which only the value-level metric exposes.
This commit is contained in:
Mihaela M 2026-06-26 20:50:24 +02:00
parent c888a113f2
commit 455cd07f4a
3 changed files with 43 additions and 0 deletions

View File

@ -23,6 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A root node that begins on the document-marker line (`--- >`, `--- |`, `--- foo`,
`--- {a: 1}`) is no longer discarded. The marker scanner skipped the rest of the
`---` line unconditionally, so a block-scalar header was dropped and its body was
mis-read as a plain scalar (folded to spaces, no trailing newline) and a same-line
plain or flow node was lost entirely. The document start now keeps any real
same-line content for the parser, skipping only trailing spaces or a comment.
Lifts official test-suite value-level conformance from 130/170 (76.5%) to
135/169 (79.9%), with no regression; parse-level is unchanged because these
documents already parsed, just to the wrong tree, which only the value-level
metric could see.
- A multi-line plain scalar whose value begins on its own line under a mapping key
now folds its continuation lines instead of stopping after the first. The
continuation check compared the next line's indent against the value's own pushed

View File

@ -38,6 +38,19 @@ extension PureYAML.Parsing.Scanner {
state.reader.advance()
state.reader.advance()
state.append(kind, mark: start, endMark: state.reader.mark)
// The document's root node may begin on the marker's own line (e.g. `--- >`, `--- foo`,
// `--- {a: 1}`, `--- !!str x`). After a document start, skip only the spaces and leave any
// real content for the main loop to scan; an empty rest-of-line or a comment is consumed.
// A document end (`...`) takes no same-line node, so its line is always skipped.
if case .documentStart = kind {
while state.reader.peek() == " " {
state.reader.advance()
}
if let next = state.reader.peek(), !isLineBreak(next), next != "#" {
state.isAtLineStart = false
return
}
}
skipLine(&state)
}

View File

@ -0,0 +1,20 @@
@testable import PureYAML
import Testing
@Suite("Document marker root nodes")
struct DocumentMarkerNodeTests {
@Test("A block scalar header on the document-marker line is recognized, not discarded")
func test_blockScalarHeaderOnDocumentMarkerLine() throws {
// `--- >` / `--- |`: the marker scanner used to skip the rest of the line, dropping the
// block header so the body was mis-read as a plain scalar (folded, no trailing newline).
#expect(try PureYAML.parse("--- >\n a\n b\n") == .string("a b\n"))
#expect(try PureYAML.parse("--- |\n a\n b\n") == .string("a\nb\n"))
}
@Test("A scalar root node may begin on the document-marker line")
func test_scalarRootNodeOnDocumentMarkerLine() throws {
#expect(try PureYAML.parse("--- folded text\n") == .string("folded text"))
// A node still on its own line below the marker keeps working.
#expect(try PureYAML.parse("---\nplain text\n") == .string("plain text"))
}
}