A block scalar's leading empty lines must not be more indented than its
first non-empty line, so a blank line wider than the content (a 5-space
blank above 2-space content, or 2,3-space blanks above 1-space content) is
malformed. The scanner buffered the leading blanks without comparing their
indentation to the content, folding such input into the value.
The scanner now records the widest leading blank and rejects the scalar
when it exceeds the content indentation, established for both auto-detected
and explicit indicators. Indentation is counted in leading spaces, not
tab-expanded width, so a tab on a blank line ('␣\t') is one space rather
than a jump to the next tab stop, keeping such valid block scalars intact.
Measured against the official YAML test-suite, parse-level conformance
rises from 270/333 (81.1%) to 273/333 (82.0%) with no regression.
237 lines
10 KiB
Swift
237 lines
10 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 {
|
|
preserveBlockScalarSurplusOnBlankLine(width: width, state: &state)
|
|
recordPendingBlockScalarBlankIndent(width: width, state: &state)
|
|
return
|
|
}
|
|
|
|
let end = state.reader.mark
|
|
let current = state.indentation.last ?? 0
|
|
let wasInBlockScalar = state.blockScalarIndentation != nil
|
|
if consumeBlockScalarContentIndentation(width: width, state: &state) {
|
|
return
|
|
}
|
|
|
|
// An active block scalar that just ended (its content was less indented this line) onto a
|
|
// line beginning with '#' is leaving an ordinary comment. A comment's own indentation does
|
|
// not participate in block structure, so leave it for the main loop to scan rather than
|
|
// forcing its column to match an indentation level, which would dedent onto a width no stack
|
|
// level holds and raise a spurious error. The guard on `wasInBlockScalar` keeps a block
|
|
// scalar's own first content line that begins with '#' (e.g. `|-` then `# text`) as content.
|
|
if wasInBlockScalar, state.reader.peek() == "#" {
|
|
return
|
|
}
|
|
|
|
if let indicator = state.pendingBlockScalarIndentationIndicator {
|
|
try applyExplicitBlockScalarIndentation(
|
|
indicator: indicator,
|
|
width: width,
|
|
current: current,
|
|
marks: (start: start, end: end),
|
|
state: &state,
|
|
)
|
|
} else if width > current {
|
|
try 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)
|
|
clearPendingBlockScalarIndentation(&state)
|
|
}
|
|
|
|
/// Records a leading blank line's indentation while a block scalar's content indentation is
|
|
/// still pending (before its first content line), so an over-indented leading line can be
|
|
/// caught once the content indentation is established.
|
|
func recordPendingBlockScalarBlankIndent(width _: Int, state: inout State) {
|
|
guard state.pendingBlockScalarIndentation, state.blockScalarIndentation == nil else {
|
|
return
|
|
}
|
|
// Compare leading spaces, not the tab-expanded width: a tab on a blank line is not
|
|
// indentation, so `␣\t` counts as one space, not a jump to the next tab stop.
|
|
state.pendingBlockScalarMaxBlankIndent = max(state.pendingBlockScalarMaxBlankIndent, state.lastLeadingSpaceCount)
|
|
}
|
|
|
|
/// A block scalar's leading empty lines must not be more indented than its content, so reject
|
|
/// the scalar if the widest leading blank exceeds the content indentation.
|
|
func enforceLeadingBlankIndentation(contentIndent: Int, line: Int, state: State) throws {
|
|
guard state.pendingBlockScalarMaxBlankIndent > contentIndent else {
|
|
return
|
|
}
|
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: line)
|
|
}
|
|
|
|
/// A whitespace-only line inside an active block scalar that is indented past the block's
|
|
/// content indentation carries the surplus spaces as content (e.g. a 3-space line in a 2-space
|
|
/// block contributes `" "`). The width is consumed before the line is recognized as blank, so
|
|
/// stash the surplus in the content prefix for the blank-line emission to pick up; otherwise the
|
|
/// line collapses to empty and a space is dropped from the scalar's value.
|
|
func preserveBlockScalarSurplusOnBlankLine(width: Int, state: inout State) {
|
|
guard let blockScalarIndentation = state.blockScalarIndentation, width > blockScalarIndentation else {
|
|
return
|
|
}
|
|
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
|
}
|
|
|
|
func scanIndentationWidth(_ state: inout State) throws -> Int {
|
|
var width = 0
|
|
var leadingSpaces = 0
|
|
var sawTab = false
|
|
while let character = state.reader.peek() {
|
|
if character == " " {
|
|
width += 1
|
|
if !sawTab {
|
|
leadingSpaces += 1
|
|
}
|
|
state.reader.advance()
|
|
} else if character == "\t" {
|
|
sawTab = true
|
|
width = nextTabStop(after: width)
|
|
state.reader.advance()
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
state.lastLeadingSpaceCount = leadingSpaces
|
|
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 '#' normally starts a comment line, which must not drive
|
|
// indent/dedent processing. Inside a block scalar it is different: when
|
|
// the header is still pending or the content is active, a leading '#' is
|
|
// literal scalar content, so indentation must be processed so the block
|
|
// scalar's indentation resolves and its content lines are consumed.
|
|
// Bailing here would leave `blockScalarIndentation` unset and the main
|
|
// loop would mis-scan the line as a comment, dropping the content.
|
|
if state.reader.peek() == "#", !state.pendingBlockScalarIndentation, state.blockScalarIndentation == nil {
|
|
return false
|
|
}
|
|
return state.flowDepth == 0
|
|
}
|
|
|
|
func consumeBlockScalarContentIndentation(
|
|
width: Int,
|
|
state: inout State,
|
|
) -> Bool {
|
|
guard let blockScalarIndentation = state.blockScalarIndentation else {
|
|
return false
|
|
}
|
|
guard width < blockScalarIndentation else {
|
|
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
|
return true
|
|
}
|
|
state.blockScalarIndentation = nil
|
|
state.blockScalarContentPrefix = ""
|
|
return false
|
|
}
|
|
|
|
func applyExplicitBlockScalarIndentation(
|
|
indicator: Int,
|
|
width: Int,
|
|
current: Int,
|
|
marks: (start: PureYAML.Parsing.Mark, end: PureYAML.Parsing.Mark),
|
|
state: inout State,
|
|
) throws {
|
|
let blockScalarIndentation = current + indicator
|
|
guard width >= blockScalarIndentation else {
|
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: marks.end.line)
|
|
}
|
|
try enforceLeadingBlankIndentation(contentIndent: blockScalarIndentation, line: marks.end.line, state: state)
|
|
state.indentation.append(blockScalarIndentation)
|
|
state.append(.indent(width: blockScalarIndentation), mark: marks.start, endMark: marks.end)
|
|
state.blockScalarIndentation = blockScalarIndentation
|
|
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
|
emitBufferedBlockScalarLeadingBlanks(&state)
|
|
clearPendingBlockScalarIndentation(&state)
|
|
}
|
|
|
|
/// Leading blank lines (between the header and the first content line) are
|
|
/// buffered until the indent token is emitted, then flushed as empty content
|
|
/// so they land after the indent and before the first content line.
|
|
func emitBufferedBlockScalarLeadingBlanks(_ state: inout State) {
|
|
for _ in 0 ..< state.pendingBlockScalarLeadingBlankLines {
|
|
state.append(.scalar(value: "", style: .plain))
|
|
}
|
|
state.pendingBlockScalarLeadingBlankLines = 0
|
|
}
|
|
|
|
func applyIncreasedIndentation(
|
|
width: Int,
|
|
start: PureYAML.Parsing.Mark,
|
|
end: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) throws {
|
|
guard state.pendingBlockScalarIndentation else {
|
|
state.indentation.append(width)
|
|
state.append(.indent(width: width), mark: start, endMark: end)
|
|
return
|
|
}
|
|
try enforceLeadingBlankIndentation(contentIndent: width, line: end.line, state: state)
|
|
state.indentation.append(width)
|
|
state.append(.indent(width: width), mark: start, endMark: end)
|
|
state.blockScalarIndentation = width
|
|
state.blockScalarContentPrefix = ""
|
|
emitBufferedBlockScalarLeadingBlanks(&state)
|
|
clearPendingBlockScalarIndentation(&state)
|
|
}
|
|
|
|
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 }
|
|
}
|
|
|
|
func clearPendingBlockScalarIndentation(_ state: inout State) {
|
|
state.pendingBlockScalarIndentation = false
|
|
state.pendingBlockScalarIndentationIndicator = nil
|
|
state.pendingBlockScalarLeadingBlankLines = 0
|
|
state.pendingBlockScalarMaxBlankIndent = 0
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|