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) } }