fix(parsing): fold root block scalars with column-0 content
A block scalar that is the document's own root node (`>`, `|`, or after `---` on the same line) may begin its content at column 0: the document node's indentation is -1, so column-0 content is still more indented than its parent. The scanner read the base level as 0 and required content strictly deeper than it, so the lines fell through indentation handling and were mis-scanned as a stray plain scalar after the document, raising "expected document boundary". A root block scalar and an under-indented nested one (`- |` then unindented content) present identical indentation state at the first content line, so the two cannot be told apart there. Capture the distinction at header-scan time instead: a root block scalar has the base indentation and no mapping value, explicit key, block-entry implicit indent, or flow context introducing it on its line. Only then is its parent the document; a sequence item or mapping value keeps its parent's indent as the floor, so its under-indented content is still rejected, unchanged. Raises yaml-test-suite parse-level to 291/333 (87.4%) and value-level to 173/191 (90.6%). A regression suite covers the root cases (including a leading '#' as literal content) and the nested under-indented cases that must error. 410 tests pass.
This commit is contained in:
parent
8f87a1ecd0
commit
a5b8986d5e
10
CHANGELOG.md
10
CHANGELOG.md
@ -84,6 +84,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A block scalar that is the document's own root node now folds or keeps content that begins at
|
||||
column 0, e.g. `>` or `--- |` followed by unindented lines. The document node's indentation is -1,
|
||||
so its content at column 0 is still more indented than its parent, but the scanner read the base
|
||||
level as 0 and required content to be strictly deeper, leaving the lines to be mis-scanned as a
|
||||
stray scalar after the document. The scanner now recognizes a root block scalar (one with no
|
||||
enclosing mapping value, explicit key, block-entry indent, or flow context on its header line) and
|
||||
establishes its content indentation at column 0. A non-root block scalar keeps its parent's indent
|
||||
as the floor, so an under-indented sequence item or mapping value is still rejected. Raises
|
||||
yaml-test-suite parse-level conformance to 291/333 (87.4%) and value-level to 173/191 (90.6%).
|
||||
|
||||
- Content after a document-end marker (`...`) now begins a new bare document. A stream such as
|
||||
`--- scalar / ... / key: value` or `a / ... / b` is two documents, but the scanner rejected the
|
||||
content and the parser expected the document to end at the marker. The scanner now reopens the
|
||||
|
||||
@ -35,6 +35,14 @@ extension PureYAML.Parsing.Scanner {
|
||||
)
|
||||
} else if width > current {
|
||||
try applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
|
||||
} else if state.pendingBlockScalarIndentation, state.pendingBlockScalarIsRootNode, width == current {
|
||||
// A root block scalar (`>`/`|` as the whole document) has the document node as its
|
||||
// parent, whose indentation is -1, so content at column 0 (width == current == 0) is
|
||||
// still more indented than its parent and must establish the block scalar's content
|
||||
// indentation here, where `width > current` (with the base level read as 0) cannot fire.
|
||||
// A non-root block scalar (a sequence item or mapping value) keeps its parent's indent as
|
||||
// the floor, so its under-indented content still falls through to an error, unchanged.
|
||||
try applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
|
||||
} else if width < current {
|
||||
try applyDedentation(width: width, start: start, end: end, state: &state)
|
||||
}
|
||||
@ -224,6 +232,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
state.pendingBlockScalarIndentationIndicator = nil
|
||||
state.pendingBlockScalarLeadingBlankLines = 0
|
||||
state.pendingBlockScalarMaxBlankIndent = 0
|
||||
state.pendingBlockScalarIsRootNode = false
|
||||
}
|
||||
|
||||
func closeIndentation(_ state: inout State) {
|
||||
|
||||
@ -88,6 +88,16 @@ extension PureYAML.Parsing.Scanner {
|
||||
)
|
||||
state.pendingBlockScalarIndentation = true
|
||||
state.pendingBlockScalarIndentationIndicator = indentationIndicator
|
||||
// A root block scalar (the document's own node) has no enclosing collection: it sits at the
|
||||
// base indentation with no mapping value, explicit key, block-entry implicit indent, or flow
|
||||
// context introducing it on this line. Only then is its parent the document (indentation -1),
|
||||
// letting its content begin at column 0. A sequence item or mapping value keeps its parent's
|
||||
// indent as the floor instead.
|
||||
state.pendingBlockScalarIsRootNode = state.indentation == [0]
|
||||
&& !state.sawMappingValueOnLine
|
||||
&& !state.pendingExplicitKey
|
||||
&& state.validImplicitIndentation.isEmpty
|
||||
&& state.flowDepth == 0
|
||||
}
|
||||
|
||||
func scanBlockScalarContent(_ state: inout State) {
|
||||
|
||||
@ -61,6 +61,9 @@ extension PureYAML.Parsing.Scanner {
|
||||
var validImplicitIndentation: Set<Int> = []
|
||||
var pendingBlockScalarIndentation = false
|
||||
var pendingBlockScalarIndentationIndicator: Int?
|
||||
/// 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
|
||||
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
|
||||
|
||||
46
Tests/RootBlockScalarIndentationTests.swift
Normal file
46
Tests/RootBlockScalarIndentationTests.swift
Normal file
@ -0,0 +1,46 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
/// A block scalar that is the document's own root node has the document as its parent, whose
|
||||
/// indentation is -1, so its content may begin at column 0. A block scalar nested in a collection
|
||||
/// keeps its parent's indentation as the floor, so column-0 content there is under-indented and stays
|
||||
/// an error. These two sit on the same scanner state at the first content line, distinguished only by
|
||||
/// whether a mapping value, explicit key, block entry, or flow context introduced the scalar.
|
||||
@Suite("Root block scalar indentation")
|
||||
struct RootBlockScalarIndentationTests {
|
||||
@Test("A root block scalar folds content that begins at column 0")
|
||||
func test_rootFoldedAtColumnZero() throws {
|
||||
#expect(try PureYAML.parse(">\nline1\nline2\n") == .string("line1 line2\n"))
|
||||
#expect(try PureYAML.parse("--- >\nline1\nline2\n") == .string("line1 line2\n"))
|
||||
}
|
||||
|
||||
@Test("A root literal block keeps line breaks for content at column 0")
|
||||
func test_rootLiteralAtColumnZero() throws {
|
||||
#expect(try PureYAML.parse("|\nline1\nline2\n") == .string("line1\nline2\n"))
|
||||
#expect(try PureYAML.parse("--- |\nline1\nline2\n") == .string("line1\nline2\n"))
|
||||
}
|
||||
|
||||
@Test("In a block scalar at column 0, a leading '#' is literal content, not a comment")
|
||||
func test_rootBlockScalarHashIsLiteral() throws {
|
||||
#expect(try PureYAML.parse(">\nline1\n# no comment\nline3\n") == .string("line1 # no comment line3\n"))
|
||||
}
|
||||
|
||||
@Test("A non-root block scalar with content at column 0 is still under-indented and rejected")
|
||||
func test_nestedUnderIndentedStillRejected() {
|
||||
// A sequence item value: its parent is the sequence at indent 0, so content must be indented.
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse("- |\ncontent\n")
|
||||
}
|
||||
// A mapping value: same floor, so unindented content is not the block scalar's body.
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse("key: |\ncontent\n")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A nested block scalar with properly indented content is unaffected")
|
||||
func test_nestedIndentedStillWorks() throws {
|
||||
#expect(try PureYAML.parse("key: |\n content\n") == .mapping(.init([
|
||||
.init(key: "key", value: .string("content\n")),
|
||||
])))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user