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 ? "'" : "\"" let indent = indentEngine.indent + 1 // A continuation indented below `indent` is an error, but it is recorded // and only raised once the quote is known to close; an unterminated quote // reaches end of input first and reports that instead. var indentError: PureYAML.Parsing.LibYAML.ScannerError? 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, indent: indent, quote: quote, startMark: startMark, error: &indentError) foldFlow(&state) } reader.skip() if let indentError { throw indentError } 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, indent: Int, quote: Unicode.Scalar, startMark: PureYAML.Parsing.Mark, error: inout PureYAML.Parsing.LibYAML.ScannerError?, ) { while Classify.isBlank(reader.peek()) || Classify.isBreak(reader.peek()) { if Classify.isBlank(reader.peek()) { if error == nil, state.leadingBlanks, reader.column < indent, Classify.isTab(reader.peek()), !blankLineAhead() { error = flowError("found a tab character that violates indentation", startMark) } 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" } } // The continuation content must be indented at least `indent`; a less // indented (non-closing, non-empty) line is malformed. if error == nil, state.leadingBlanks, let next = reader.peek(), next != quote, !Classify.isBreakZ(next), reader.column < indent { error = flowError("found wrong indentation in a multi-line quoted scalar", startMark) } } /// 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 mutating 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)) } }