The block-scalar break scanner raised 'found a tab character where an indentation space is expected' for every tab seen while auto-detecting the content indentation. But a tab at or beyond the content column is content, not indentation: only a tab below the content column is illegal. Gate the tab check on a content-column floor (the resolved indent, or the parent indent + 1 while auto-detecting) instead of firing on every tab in auto-detect mode. A tab-only line shallower than the content is still rejected, so tab-as-indentation errors (Y79Y/000 and friends) are preserved. Surpasses libyaml on 96NN/00, 96NN/01, R4YG, Y79Y/001. Port score 286 -> 290.
173 lines
7.2 KiB
Swift
173 lines
7.2 KiB
Swift
extension PureYAML.Parsing.LibYAML.Scanner {
|
|
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
|
|
|
|
/// The accumulation state for `scan_block_scalar`.
|
|
private struct BlockState {
|
|
var string = ""
|
|
var leadingBreak = ""
|
|
var trailingBreaks = ""
|
|
var leadingBlank = false
|
|
}
|
|
|
|
/// Port of `yaml_parser_scan_block_scalar`. Scans a literal (`|`) or folded
|
|
/// (`>`) block scalar: parses the chomping (`+`/`-`) and indentation indicators
|
|
/// from the header, auto-detects the indentation if not given, then folds the
|
|
/// content (literal keeps breaks, folded joins equally-indented non-empty lines
|
|
/// with a space) and applies clip/strip/keep chomping to the trailing breaks.
|
|
mutating func scanBlockScalar(literal: Bool) throws -> PureYAML.Parsing.LibYAML.Token {
|
|
let startMark = reader.mark
|
|
reader.skip()
|
|
let header = try readBlockScalarHeader(startMark: startMark)
|
|
try consumeBlockHeaderTail(startMark: startMark)
|
|
|
|
var endMark = reader.mark
|
|
// -1 is the "indentation not yet auto-detected" sentinel. libyaml uses 0,
|
|
// but a root-level block scalar legitimately detects indentation 0, so 0
|
|
// must be a representable detected value rather than the sentinel.
|
|
var indent = -1
|
|
if header.increment != 0 {
|
|
indent = indentEngine.indent >= 0 ? indentEngine.indent + header.increment : header.increment
|
|
}
|
|
var state = BlockState()
|
|
try scanBlockScalarBreaks(indent: &indent, state: &state, startMark: startMark, endMark: &endMark)
|
|
|
|
while reader.column == indent, reader.peek() != nil {
|
|
foldBlockLine(&state, literal: literal)
|
|
while let character = reader.peek(), !Classify.isBreakZ(character) {
|
|
state.string.unicodeScalars.append(character)
|
|
reader.skip()
|
|
}
|
|
state.leadingBreak += readBlockLine()
|
|
try scanBlockScalarBreaks(indent: &indent, state: &state, startMark: startMark, endMark: &endMark)
|
|
}
|
|
|
|
if header.chomping != -1 {
|
|
state.string += state.leadingBreak
|
|
}
|
|
if header.chomping == 1 {
|
|
state.string += state.trailingBreaks
|
|
}
|
|
return PureYAML.Parsing.LibYAML.Token(
|
|
type: .scalar(value: state.string, style: literal ? .literal : .folded),
|
|
startMark: startMark,
|
|
endMark: endMark,
|
|
)
|
|
}
|
|
|
|
/// Parse the chomping and indentation indicators in either order.
|
|
private mutating func readBlockScalarHeader(startMark: PureYAML.Parsing.Mark) throws -> (chomping: Int, increment: Int) {
|
|
var chomping = 0
|
|
var increment = 0
|
|
if reader.peek() == "+" || reader.peek() == "-" {
|
|
chomping = reader.peek() == "+" ? 1 : -1
|
|
reader.skip()
|
|
if Classify.isDigit(reader.peek()) {
|
|
increment = try blockIncrement(startMark: startMark)
|
|
}
|
|
} else if Classify.isDigit(reader.peek()) {
|
|
increment = try blockIncrement(startMark: startMark)
|
|
if reader.peek() == "+" || reader.peek() == "-" {
|
|
chomping = reader.peek() == "+" ? 1 : -1
|
|
reader.skip()
|
|
}
|
|
}
|
|
return (chomping, increment)
|
|
}
|
|
|
|
private mutating func blockIncrement(startMark: PureYAML.Parsing.Mark) throws -> Int {
|
|
if reader.peek() == "0" {
|
|
throw blockError("found an indentation indicator equal to 0", startMark)
|
|
}
|
|
let value = digitValue(reader.peek())
|
|
reader.skip()
|
|
return value
|
|
}
|
|
|
|
/// Eat trailing blanks and a comment to the end of the header line, then the
|
|
/// line break.
|
|
private mutating func consumeBlockHeaderTail(startMark: PureYAML.Parsing.Mark) throws {
|
|
while reader.peek() == " " {
|
|
reader.skip()
|
|
}
|
|
if reader.peek() == "#" {
|
|
while !Classify.isBreakZ(reader.peek()) {
|
|
reader.skip()
|
|
}
|
|
}
|
|
guard Classify.isBreakZ(reader.peek()) else {
|
|
throw blockError("did not find expected comment or line break", startMark)
|
|
}
|
|
if Classify.isBreak(reader.peek()) {
|
|
reader.skipLine()
|
|
}
|
|
}
|
|
|
|
/// Port of `yaml_parser_scan_block_scalar_breaks`: eat indentation spaces and
|
|
/// blank lines, auto-detecting the indentation level when none was given.
|
|
private mutating func scanBlockScalarBreaks(indent: inout Int, state: inout BlockState, startMark: PureYAML.Parsing.Mark, endMark: inout PureYAML.Parsing.Mark) throws {
|
|
var maxIndent = 0
|
|
endMark = reader.mark
|
|
// A tab is content (or separation) once the column reaches the content
|
|
// indentation; it is illegal only while still inside the required
|
|
// indentation. During auto-detection the floor is the parent indent + 1.
|
|
let tabFloor = indent >= 0 ? indent : max(0, indentEngine.indent + 1)
|
|
while true {
|
|
while indent < 0 || reader.column < indent, reader.peek() == " " {
|
|
reader.skip()
|
|
}
|
|
if reader.column > maxIndent {
|
|
maxIndent = reader.column
|
|
}
|
|
if reader.column < tabFloor, Classify.isTab(reader.peek()) {
|
|
throw blockError("found a tab character where an indentation space is expected", startMark)
|
|
}
|
|
if !Classify.isBreak(reader.peek()) {
|
|
break
|
|
}
|
|
state.trailingBreaks += readBlockLine()
|
|
endMark = reader.mark
|
|
}
|
|
if indent < 0 {
|
|
// Auto-detected indentation. A root-level block scalar has a parent
|
|
// indent of -1, so the floor is 0 (allowing column-0 content), not
|
|
// libyaml's 1. maxIndent >= 0 keeps the result non-negative.
|
|
indent = max(maxIndent, indentEngine.indent + 1)
|
|
}
|
|
}
|
|
|
|
/// Fold the pending breaks at the start of a content line (folded style joins a
|
|
/// single break between non-blank lines into a space; otherwise breaks are
|
|
/// kept).
|
|
private func foldBlockLine(_ state: inout BlockState, literal: Bool) {
|
|
let trailingBlank = Classify.isBlank(reader.peek())
|
|
if !literal, state.leadingBreak.first == "\n", !state.leadingBlank, !trailingBlank {
|
|
if state.trailingBreaks.isEmpty {
|
|
state.string += " "
|
|
}
|
|
state.leadingBreak = ""
|
|
} else {
|
|
state.string += state.leadingBreak
|
|
state.leadingBreak = ""
|
|
}
|
|
state.string += state.trailingBreaks
|
|
state.trailingBreaks = ""
|
|
state.leadingBlank = Classify.isBlank(reader.peek())
|
|
}
|
|
|
|
private mutating func readBlockLine() -> String {
|
|
reader.skipLine()
|
|
return "\n"
|
|
}
|
|
|
|
private func digitValue(_ scalar: Unicode.Scalar?) -> Int {
|
|
guard let scalar, scalar >= "0", scalar <= "9" else {
|
|
return 0
|
|
}
|
|
return Int(scalar.value - Unicode.Scalar("0").value)
|
|
}
|
|
|
|
private func blockError(_ problem: String, _ mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.LibYAML.ScannerError {
|
|
PureYAML.Parsing.LibYAML.ScannerError(context: "while scanning a block scalar", problem: problem, mark: mark)
|
|
}
|
|
}
|