Stage 3 of the faithful libyaml scanner port, continued. Translates fetch_block_scalar, scan_block_scalar, and scan_block_scalar_breaks: literal (|) and folded (>) block scalars with the chomping (+/-) and indentation indicators in either order, indentation auto-detection from the first non-empty line (clamped to parser indent + 1), folded joining of equally-indented non-empty lines into a space, and clip/strip/keep chomping of the trailing breaks. Unit-tested for literal clip, folded join, and strip chomping. Only tags and directives remain unported in the scanner. 314 tests, no regression.
166 lines
6.5 KiB
Swift
166 lines
6.5 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
|
|
var indent = 0
|
|
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
|
|
while true {
|
|
while indent == 0 || reader.column < indent, reader.peek() == " " {
|
|
reader.skip()
|
|
}
|
|
if reader.column > maxIndent {
|
|
maxIndent = reader.column
|
|
}
|
|
if indent == 0 || reader.column < indent, 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 {
|
|
indent = max(maxIndent, indentEngine.indent + 1)
|
|
if indent < 1 {
|
|
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)
|
|
}
|
|
}
|