From 8112954f4c74a22072c310aa7fd2b7ab74ebdbd5 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 08:57:39 +0200 Subject: [PATCH] fix(parsing): end a root block scalar at a document marker A `---` or `...` at column 0 ends an active block scalar rather than being consumed as its content. The scanner now closes the scalar's indentation at the marker, dedenting back to the document level, so the token stream stays balanced: the dedent precedes the documentEnd. Previously the scalar's indent lingered past the marker and surfaced as a stray late dedent plus a phantom empty document, and a following directive was misread as a scalar. Fixes suite case W4TN (parse-level conformance 315 -> 316); adds tests. --- Sources/Parsing/Scanner.Indentation.swift | 16 ++++++++++++ Tests/BlockScalarDocumentBoundaryTests.swift | 27 ++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 Tests/BlockScalarDocumentBoundaryTests.swift diff --git a/Sources/Parsing/Scanner.Indentation.swift b/Sources/Parsing/Scanner.Indentation.swift index 259fa7d..fc101c6 100644 --- a/Sources/Parsing/Scanner.Indentation.swift +++ b/Sources/Parsing/Scanner.Indentation.swift @@ -11,6 +11,22 @@ extension PureYAML.Parsing.Scanner { let end = state.reader.mark let current = state.indentation.last ?? 0 let wasInBlockScalar = state.blockScalarIndentation != nil + + // A document marker (`---` / `...`) at column 0 ends an active block scalar rather than + // being its content. Close the scalar's indentation here, dedenting back to the document + // level, so the marker is then scanned balanced at the document level. Without this the + // scalar's indent lingers past the marker and surfaces later as a stray dedent plus a + // phantom empty node (the marker still ends the document, but the structure is unbalanced). + if state.blockScalarIndentation != nil, width == 0, isAtDocumentMarker(state.reader) { + state.blockScalarIndentation = nil + state.blockScalarContentPrefix = "" + while state.indentation.count > 1 { + state.indentation.removeLast() + state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end) + } + return + } + if consumeBlockScalarContentIndentation(width: width, state: &state) { return } diff --git a/Tests/BlockScalarDocumentBoundaryTests.swift b/Tests/BlockScalarDocumentBoundaryTests.swift new file mode 100644 index 0000000..452f5cf --- /dev/null +++ b/Tests/BlockScalarDocumentBoundaryTests.swift @@ -0,0 +1,27 @@ +@testable import PureYAML +import Testing + +/// A root block scalar whose content runs to column 0 is ended by a document marker (`...` or +/// `---`), not continued into it. The scalar's indentation is closed at the marker, so the stream +/// stays balanced: no stray dedent and no phantom empty document afterward. +@Suite("Block scalar ended by document boundary") +struct BlockScalarDocumentBoundaryTests { + @Test("A `...` ends a root block scalar, yielding a single document") + func test_terminatedByDocumentEnd() throws { + let documents = try PureYAML.parseStream("--- |\n%!PS\n...\n").map(\.value) + #expect(documents == [.string("%!PS\n")]) + } + + @Test("A `---` ends a root block scalar and starts the next document") + func test_terminatedByNextDocumentStart() throws { + let documents = try PureYAML.parseStream("--- |\n%!PS\n--- next\n").map(\.value) + #expect(documents == [.string("%!PS\n"), .string("next")]) + } + + @Test("Suite case W4TN: a directive, a root literal block scalar, then a comment-only document") + func test_w4tnShape() throws { + let yaml = "%YAML 1.2\n--- |\n%!PS-Adobe-2.0\n...\n%YAML 1.2\n---\n# Empty\n...\n" + let documents = try PureYAML.parseStream(yaml).map(\.value) + #expect(documents == [.string("%!PS-Adobe-2.0\n"), .null]) + } +}