Model libyaml's parser->indent (the enclosing block-collection column, -1 at the bare root) separately from per-line indentation, via a blockIndent stack that rolls on block-sequence entries and mapping keys, unrolls at each block-line start, and resets at document boundaries. Plain-scalar continuation folding now compares against blockIndent, so a root scalar folds its column-0 continuations (single break to space, blank line to newline, per-line whitespace stripped) while a scalar inside a collection still ends at a sibling line. The ':' value indicator deliberately does not roll the block indent.
A '---' document-start marker may now be followed by the document's root node on the same line ('--- foo', '--- [1, 2]', '--- |', '--- >'), and a block scalar whose body sits at the document root's column 0 is captured and ends at the next '---'/'...' marker.
Flips 33 official YAML test suite cases (338/402): 18 false-rejects and 12 value mismatches now match, and 3 invalid inputs (9KBC, ZVH3, 9MQT/01) are now correctly rejected. M7A3 and NB6Z now parse but with mismatched values (separate multi-doc and tab-folding gaps) and stay tracked. Adds root multi-line plain scalar regression tests; updates manifest, fixtures, counts, and roadmap.
143 lines
5.5 KiB
Swift
143 lines
5.5 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
func scanIndentation(_ state: inout State) throws {
|
|
let start = state.reader.mark
|
|
let width = try scanIndentationWidth(&state)
|
|
guard shouldProcessIndentation(state) else {
|
|
return
|
|
}
|
|
|
|
// Mirror libyaml unrolling the block indent to the line's column before any
|
|
// token on the line is scanned, so a dedented line restores the enclosing
|
|
// collection's indent. A continuation of a plain scalar never reaches here
|
|
// (it is consumed inside the scalar scanner), so this only fires at genuine
|
|
// block-line starts.
|
|
unrollBlockIndent(toColumn: width, state: &state)
|
|
|
|
let end = state.reader.mark
|
|
let current = state.indentation.last ?? 0
|
|
|
|
if width > current {
|
|
applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
|
|
} else if width < current {
|
|
try applyDedentation(width: width, start: start, end: end, state: &state)
|
|
}
|
|
|
|
pruneImplicitIndentation(&state)
|
|
}
|
|
|
|
func scanIndentationWidth(_ state: inout State) throws -> Int {
|
|
var width = 0
|
|
while let character = state.reader.peek() {
|
|
if character == " " {
|
|
width += 1
|
|
state.reader.advance()
|
|
} else if character == "\t" {
|
|
// A tab may not provide block indentation. At the start of a block
|
|
// line a simple key is allowed, so libyaml does not consume the tab
|
|
// as whitespace and the line is malformed (scanner.c:1948-1953). A
|
|
// tab is only legal here once it follows content on the same line.
|
|
if state.flowDepth == 0, state.simpleKeyAllowed {
|
|
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
|
|
}
|
|
width = nextTabStop(after: width)
|
|
state.reader.advance()
|
|
} else {
|
|
return width
|
|
}
|
|
}
|
|
return width
|
|
}
|
|
|
|
func nextTabStop(after width: Int) -> Int {
|
|
((width / 4) + 1) * 4
|
|
}
|
|
|
|
func shouldProcessIndentation(_ state: State) -> Bool {
|
|
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
|
|
return false
|
|
}
|
|
// A leading '#' starts a comment line, which must not drive indent/dedent
|
|
// processing. Block scalar content (including lines that begin with '#') is
|
|
// consumed in full by the block scalar scanner before control returns here,
|
|
// so this check no longer needs to special-case block scalars.
|
|
if state.reader.peek() == "#" {
|
|
return false
|
|
}
|
|
return state.flowDepth == 0
|
|
}
|
|
|
|
func applyIncreasedIndentation(
|
|
width: Int,
|
|
start: PureYAML.Parsing.Mark,
|
|
end: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) {
|
|
state.indentation.append(width)
|
|
state.append(.indent(width: width), mark: start, endMark: end)
|
|
}
|
|
|
|
func applyDedentation(
|
|
width: Int,
|
|
start: PureYAML.Parsing.Mark,
|
|
end: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) throws {
|
|
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
|
|
state.indentation.removeLast()
|
|
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
|
|
}
|
|
if canUseImplicitIndentation(width: width, state: state) {
|
|
state.indentation.append(width)
|
|
state.append(.indent(width: width), mark: start, endMark: end)
|
|
}
|
|
guard width == state.indentation.last else {
|
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
|
|
}
|
|
}
|
|
|
|
func canUseImplicitIndentation(
|
|
width: Int,
|
|
state: State,
|
|
) -> Bool {
|
|
width != state.indentation.last
|
|
&& width > (state.indentation.last ?? 0)
|
|
&& state.validImplicitIndentation.contains(width)
|
|
}
|
|
|
|
func pruneImplicitIndentation(_ state: inout State) {
|
|
let current = state.indentation.last ?? 0
|
|
state.validImplicitIndentation = state.validImplicitIndentation.filter { $0 <= current }
|
|
}
|
|
|
|
/// Mirrors libyaml's `yaml_parser_roll_indent`: when a block collection opens at
|
|
/// `column` (0-based) deeper than the current block indent, push the old indent
|
|
/// and adopt the new one. No-op in flow context, matching the C scanner.
|
|
func rollBlockIndent(toColumn column: Int, state: inout State) {
|
|
guard state.flowDepth == 0, state.blockIndent < column else {
|
|
return
|
|
}
|
|
state.blockIndents.append(state.blockIndent)
|
|
state.blockIndent = column
|
|
}
|
|
|
|
/// Mirrors libyaml's `yaml_parser_unroll_indent`: pop block indents until the
|
|
/// current block indent is at or below `column`. Called at each block-context
|
|
/// line start so the block indent tracks the line's actual nesting.
|
|
func unrollBlockIndent(toColumn column: Int, state: inout State) {
|
|
guard state.flowDepth == 0 else {
|
|
return
|
|
}
|
|
while state.blockIndent > column {
|
|
state.blockIndent = state.blockIndents.popLast() ?? -1
|
|
}
|
|
}
|
|
|
|
func closeIndentation(_ state: inout State) {
|
|
let mark = state.reader.mark
|
|
while state.indentation.count > 1 {
|
|
state.indentation.removeLast()
|
|
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
|
|
}
|
|
}
|
|
}
|