PureYAML/Sources/Parsing/LibYAML.Scanner.FlowScalar.swift
Mihaela Mihaljevic 0588996506 feat(parsing): run the whole library on the ported engine
Routes the value and stream APIs through the ported scanner+parser (the
event API already was), so PureYAML now parses entirely on the libyaml
port. Adds the features needed to match and exceed the previous engine:
JSON UTF-16 surrogate-pair \u escapes, bare documents after an explicit
`...`, reserved-directive handling that still requires a following `---`,
and a 64-level nesting bound enforced on the event stream so deeply nested
input fails with nestingTooDeep instead of overflowing a recursive
consumer. Parser errors are translated to the diagnostic-aware ParseError,
recovering precise codes where identifiable and a readable malformed
fallback otherwise; error-reporting fixtures are rebased onto the port.

Canonical event conformance: 268 -> 271 of 308 (7Z25, HWV9, QT73).
393 tests pass.
2026-06-24 18:02:22 +02:00

204 lines
8.3 KiB
Swift

extension PureYAML.Parsing.LibYAML.Scanner {
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
/// The folding state carried through `scan_flow_scalar`, the same shape as the
/// plain-scalar state.
private struct FlowState {
var string = ""
var leadingBreak = ""
var trailingBreaks = ""
var whitespaces = ""
var leadingBlanks = false
}
/// Decoded single-character escapes for double-quoted scalars (libyaml's escape
/// switch). libyaml emits UTF-8 bytes; here each maps to its Unicode scalar.
private static let doubleQuotedEscapes: [Unicode.Scalar: Unicode.Scalar] = [
"0": "\u{0}", "a": "\u{07}", "b": "\u{08}", "t": "\u{09}", "\t": "\u{09}",
"n": "\u{0A}", "v": "\u{0B}", "f": "\u{0C}", "r": "\u{0D}", "e": "\u{1B}",
" ": "\u{20}", "\"": "\"", "/": "/", "\\": "\\",
"N": "\u{85}", "_": "\u{A0}", "L": "\u{2028}", "P": "\u{2029}",
]
/// Port of `yaml_parser_scan_flow_scalar`. Scans a single- or double-quoted
/// scalar, folding line breaks like a plain scalar and, for double quotes,
/// decoding escape sequences.
mutating func scanFlowScalar(single: Bool) throws -> PureYAML.Parsing.LibYAML.Token {
let startMark = reader.mark
reader.skip()
var state = FlowState()
let quote: Unicode.Scalar = single ? "'" : "\""
while true {
if reader.column == 0, documentMarkerAhead() {
throw flowError("found unexpected document indicator", startMark)
}
if reader.peek() == nil {
throw flowError("found unexpected end of stream", startMark)
}
try consumeFlowContent(single: single, &state, startMark: startMark)
if reader.peek() == quote {
break
}
consumeFlowBreaks(&state)
foldFlow(&state)
}
reader.skip()
return PureYAML.Parsing.LibYAML.Token(
type: .scalar(value: state.string, style: single ? .singleQuoted : .doubleQuoted),
startMark: startMark,
endMark: reader.mark,
)
}
/// Consume the run of non-blank characters, decoding escapes and stopping at
/// the closing quote or an escaped line break.
private mutating func consumeFlowContent(single: Bool, _ state: inout FlowState, startMark: PureYAML.Parsing.Mark) throws {
state.leadingBlanks = false
let quote: Unicode.Scalar = single ? "'" : "\""
while let character = reader.peek(), !Classify.isBlankZ(character) {
if single, character == "'", reader.peek(1) == "'" {
state.string.unicodeScalars.append("'")
reader.skip()
reader.skip()
} else if character == quote {
return
} else if !single, character == "\\", Classify.isBreak(reader.peek(1)) {
reader.skip()
reader.skipLine()
state.leadingBlanks = true
return
} else if !single, character == "\\" {
try appendEscape(&state, startMark: startMark)
} else {
state.string.unicodeScalars.append(character)
reader.skip()
}
}
}
/// Decode a double-quoted escape sequence (`\` already at the reader).
private mutating func appendEscape(_ state: inout FlowState, startMark: PureYAML.Parsing.Mark) throws {
let escape = reader.peek(1)
if let escape, let decoded = Self.doubleQuotedEscapes[escape] {
state.string.unicodeScalars.append(decoded)
reader.skip()
reader.skip()
return
}
let codeLength = try hexEscapeLength(escape, startMark: startMark)
reader.skip()
reader.skip()
var value = try readHexEscape(count: codeLength, startMark: startMark)
if codeLength == 4 {
value = try combineSurrogateIfNeeded(high: value, startMark: startMark)
}
guard let scalar = Unicode.Scalar(value) else {
throw flowError("found invalid Unicode character escape code", startMark)
}
state.string.unicodeScalars.append(scalar)
}
/// The hex digit count for `\x` (2), `\u` (4), and `\U` (8) escapes.
private func hexEscapeLength(_ escape: Unicode.Scalar?, startMark: PureYAML.Parsing.Mark) throws -> Int {
switch escape {
case "x": 2
case "u": 4
case "U": 8
default: throw flowError("found unknown escape character", startMark)
}
}
/// Read and consume `count` hexadecimal digits, returning their value.
private mutating func readHexEscape(count: Int, startMark: PureYAML.Parsing.Mark) throws -> UInt32 {
var value: UInt32 = 0
for _ in 0 ..< count {
guard let digit = reader.peek(), Classify.isHex(digit) else {
throw flowError("did not find expected hexdecimal number", startMark)
}
value = (value << 4) + hexValue(digit)
reader.skip()
}
return value
}
/// Combine a UTF-16 surrogate pair (a high surrogate `\u` escape immediately
/// followed by a low surrogate `\u` escape) into a single astral code point,
/// for JSON compatibility. A lone high surrogate is left for the scalar check.
private mutating func combineSurrogateIfNeeded(high: UInt32, startMark: PureYAML.Parsing.Mark) throws -> UInt32 {
guard (0xD800 ... 0xDBFF).contains(high), reader.peek() == "\\", reader.peek(1) == "u" else {
return high
}
reader.skip()
reader.skip()
let low = try readHexEscape(count: 4, startMark: startMark)
guard (0xDC00 ... 0xDFFF).contains(low) else {
throw flowError("found invalid Unicode character escape code", startMark)
}
return 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00)
}
/// Consume blanks and line breaks between content, recording pending
/// whitespace and folded breaks.
private mutating func consumeFlowBreaks(_ state: inout FlowState) {
while Classify.isBlank(reader.peek()) || Classify.isBreak(reader.peek()) {
if Classify.isBlank(reader.peek()) {
if !state.leadingBlanks, let blank = reader.peek() {
state.whitespaces.unicodeScalars.append(blank)
}
reader.skip()
} else if !state.leadingBlanks {
state.whitespaces = ""
reader.skipLine()
state.leadingBreak = "\n"
state.leadingBlanks = true
} else {
reader.skipLine()
state.trailingBreaks += "\n"
}
}
}
/// Fold the pending breaks or whitespace into the value.
private func foldFlow(_ state: inout FlowState) {
guard state.leadingBlanks else {
state.string += state.whitespaces
state.whitespaces = ""
return
}
if state.leadingBreak.first == "\n" {
if state.trailingBreaks.isEmpty {
state.string += " "
} else {
state.string += state.trailingBreaks
state.trailingBreaks = ""
}
state.leadingBreak = ""
} else {
state.string += state.leadingBreak
state.string += state.trailingBreaks
state.leadingBreak = ""
state.trailingBreaks = ""
}
}
private func hexValue(_ scalar: Unicode.Scalar) -> UInt32 {
switch scalar {
case "0" ... "9": scalar.value - Unicode.Scalar("0").value
case "A" ... "F": scalar.value - Unicode.Scalar("A").value + 10
default: scalar.value - Unicode.Scalar("a").value + 10
}
}
private func flowError(_ problem: String, _ mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.LibYAML.ScannerError {
PureYAML.Parsing.LibYAML.ScannerError(context: "while scanning a quoted scalar", problem: problem, mark: mark)
}
private func documentMarkerAhead() -> Bool {
let first = reader.peek()
guard first == "-" || first == "." else {
return false
}
return reader.peek(1) == first && reader.peek(2) == first && Classify.isBlankZ(reader.peek(3))
}
}