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