The emitter kept its own list of reserved words and number shapes, apart from the parser's, and the two disagreed: the string yes was written plain and read back as a boolean, as were 0x1F, 1_000 and .inf. The parser's reading of a plain scalar is now one type that both use, and the emitter quotes what does not read back as a string. Also quoted: the merge key, the document end marker, and a string with a CR+LF break, which was written raw.
324 lines
10 KiB
Swift
324 lines
10 KiB
Swift
extension PureYAML.Parsing.Parser {
|
|
func scalarStyle(_ text: String) -> PureYAML.Parsing.ScalarStyle {
|
|
if text.hasPrefix("\"") {
|
|
return .doubleQuoted
|
|
}
|
|
if text.hasPrefix("'") {
|
|
return .singleQuoted
|
|
}
|
|
return .plain
|
|
}
|
|
|
|
func parseScalar(
|
|
_ text: String,
|
|
line: Int,
|
|
) throws -> PureYAML.Model.Value {
|
|
let value = trim(text)
|
|
if value.hasPrefix("\"") {
|
|
return try .string(parseDoubleQuoted(value, line: line))
|
|
}
|
|
if value.hasPrefix("'") {
|
|
return try .string(parseSingleQuoted(value, line: line))
|
|
}
|
|
return PureYAML.Parsing.PlainScalar.resolve(value)
|
|
}
|
|
|
|
func parseBool(_ text: String) -> Bool? {
|
|
PureYAML.Parsing.PlainScalar.bool(text)
|
|
}
|
|
|
|
func parseInteger(_ text: String) -> Int? {
|
|
PureYAML.Parsing.PlainScalar.integer(text)
|
|
}
|
|
|
|
func parseDouble(_ text: String) -> Double? {
|
|
PureYAML.Parsing.PlainScalar.double(text)
|
|
}
|
|
|
|
func parseSingleQuoted(
|
|
_ text: String,
|
|
line: Int,
|
|
) throws -> String {
|
|
guard text.count >= 2, text.last == "'" else {
|
|
throw PureYAML.Parsing.ParseError.unterminatedQuotedString(line: line)
|
|
}
|
|
let inner = Array(text.dropFirst().dropLast())
|
|
return foldFlowScalar(inner, single: true)
|
|
}
|
|
|
|
func parseDoubleQuoted(
|
|
_ text: String,
|
|
line: Int,
|
|
) throws -> String {
|
|
guard text.count >= 2, text.last == "\"" else {
|
|
throw PureYAML.Parsing.ParseError.unterminatedQuotedString(line: line)
|
|
}
|
|
let inner = Array(text.dropFirst().dropLast())
|
|
return foldFlowScalar(inner, single: false)
|
|
}
|
|
|
|
/// Flow (quoted) scalar line folding, mirroring libyaml's
|
|
/// yaml_parser_scan_flow_scalar (single and double quoted). Trailing
|
|
/// whitespace before a break is dropped, leading whitespace on a
|
|
/// continuation line is dropped, a single break between non-empty lines
|
|
/// folds to one space, and N consecutive breaks yield N-1 line feeds.
|
|
/// For double quoted scalars a backslash at end of line suppresses the
|
|
/// break entirely.
|
|
func foldFlowScalar(
|
|
_ inner: [Character],
|
|
single: Bool,
|
|
) -> String {
|
|
var state = FlowFoldState()
|
|
let count = inner.count
|
|
while state.index < count {
|
|
state.leadingBlanks = false
|
|
consumeFlowContent(inner, single: single, state: &state)
|
|
consumeFlowBlanks(inner, state: &state)
|
|
joinFlowFold(state: &state)
|
|
}
|
|
return state.output
|
|
}
|
|
|
|
/// Mutable accumulators for flow scalar folding, matching the C reference's
|
|
/// string / leading_break / trailing_breaks / whitespaces buffers.
|
|
struct FlowFoldState {
|
|
var output = ""
|
|
var whitespaces = ""
|
|
var leadingBreak = ""
|
|
var trailingBreaks = ""
|
|
var leadingBlanks = false
|
|
var index = 0
|
|
}
|
|
|
|
/// Consume the run of non-blank characters at `state.index`, decoding
|
|
/// escapes. A double quoted backslash at end of line sets `leadingBlanks`
|
|
/// and stops, so the following break is suppressed during folding.
|
|
func consumeFlowContent(
|
|
_ inner: [Character],
|
|
single: Bool,
|
|
state: inout FlowFoldState,
|
|
) {
|
|
let count = inner.count
|
|
while state.index < count, !isFlowBlank(inner[state.index]), !isFlowBreak(inner[state.index]) {
|
|
if single, inner[state.index] == "'" {
|
|
state.output.append("'")
|
|
let isEscapedQuote = state.index + 1 < count && inner[state.index + 1] == "'"
|
|
state.index += isEscapedQuote ? 2 : 1
|
|
continue
|
|
}
|
|
if !single, inner[state.index] == "\\" {
|
|
if state.index + 1 < count, isFlowBreak(inner[state.index + 1]) {
|
|
state.index += 2
|
|
state.leadingBlanks = true
|
|
break
|
|
}
|
|
state.index += 1
|
|
appendDoubleQuotedEscape(inner, index: &state.index, to: &state.output)
|
|
continue
|
|
}
|
|
state.output.append(inner[state.index])
|
|
state.index += 1
|
|
}
|
|
}
|
|
|
|
/// Consume the run of blanks and line breaks at `state.index`, sorting them
|
|
/// into the whitespace / leading-break / trailing-break buffers.
|
|
func consumeFlowBlanks(
|
|
_ inner: [Character],
|
|
state: inout FlowFoldState,
|
|
) {
|
|
let count = inner.count
|
|
while state.index < count, isFlowBlank(inner[state.index]) || isFlowBreak(inner[state.index]) {
|
|
if isFlowBlank(inner[state.index]) {
|
|
if !state.leadingBlanks {
|
|
state.whitespaces.append(inner[state.index])
|
|
}
|
|
} else if !state.leadingBlanks {
|
|
state.whitespaces = ""
|
|
state.leadingBreak = "\n"
|
|
state.leadingBlanks = true
|
|
} else {
|
|
state.trailingBreaks.append("\n")
|
|
}
|
|
state.index += 1
|
|
}
|
|
}
|
|
|
|
/// Join the captured whitespace or fold the captured line breaks into the
|
|
/// output, per YAML 1.2 flow folding.
|
|
func joinFlowFold(state: inout FlowFoldState) {
|
|
guard state.leadingBlanks else {
|
|
state.output.append(state.whitespaces)
|
|
state.whitespaces = ""
|
|
return
|
|
}
|
|
if state.leadingBreak.first == "\n" {
|
|
if state.trailingBreaks.isEmpty {
|
|
state.output.append(" ")
|
|
} else {
|
|
state.output.append(state.trailingBreaks)
|
|
state.trailingBreaks = ""
|
|
}
|
|
state.leadingBreak = ""
|
|
} else {
|
|
state.output.append(state.leadingBreak)
|
|
state.output.append(state.trailingBreaks)
|
|
state.leadingBreak = ""
|
|
state.trailingBreaks = ""
|
|
}
|
|
}
|
|
|
|
func isFlowBlank(_ character: Character) -> Bool {
|
|
character == " " || character == "\t"
|
|
}
|
|
|
|
func isFlowBreak(_ character: Character) -> Bool {
|
|
character == "\n" || character == "\r" || character == "\r\n"
|
|
}
|
|
|
|
/// Decode a double quoted escape. On entry index points at the escape
|
|
/// character (the position just after the backslash). On return index has
|
|
/// advanced past every character the escape consumed. The escape set
|
|
/// mirrors libyaml's yaml_parser_scan_flow_scalar.
|
|
func appendDoubleQuotedEscape(
|
|
_ text: [Character],
|
|
index: inout Int,
|
|
to output: inout String,
|
|
) {
|
|
guard index < text.count else {
|
|
output.append("\\")
|
|
return
|
|
}
|
|
let character = text[index]
|
|
if let simple = Self.simpleDoubleQuotedEscapes[character] {
|
|
output.append(simple)
|
|
index += 1
|
|
return
|
|
}
|
|
switch character {
|
|
case "x":
|
|
index += 1
|
|
appendHexEscape(text, index: &index, digits: 2, marker: "x", to: &output)
|
|
case "u":
|
|
index += 1
|
|
appendUnicodeEscape(text, index: &index, to: &output)
|
|
case "U":
|
|
index += 1
|
|
appendHexEscape(text, index: &index, digits: 8, marker: "U", to: &output)
|
|
default:
|
|
output.append(character)
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
/// Single character double quoted escapes, matching libyaml's escape table.
|
|
static let simpleDoubleQuotedEscapes: [Character: Character] = [
|
|
"0": "\u{00}",
|
|
"a": "\u{07}",
|
|
"b": "\u{08}",
|
|
"t": "\t",
|
|
"\t": "\t",
|
|
"n": "\n",
|
|
"v": "\u{0B}",
|
|
"f": "\u{0C}",
|
|
"r": "\r",
|
|
"e": "\u{1B}",
|
|
" ": " ",
|
|
"\"": "\"",
|
|
"/": "/",
|
|
"\\": "\\",
|
|
"N": "\u{85}",
|
|
"_": "\u{A0}",
|
|
"L": "\u{2028}",
|
|
"P": "\u{2029}",
|
|
]
|
|
|
|
/// Decode \xXX or \UXXXXXXXX. On entry index points at the first hex digit.
|
|
func appendHexEscape(
|
|
_ text: [Character],
|
|
index: inout Int,
|
|
digits: Int,
|
|
marker: Character,
|
|
to output: inout String,
|
|
) {
|
|
guard let value = readHex(text, index: &index, digits: digits) else {
|
|
output.append(marker)
|
|
return
|
|
}
|
|
if let scalar = UnicodeScalar(value) {
|
|
output.unicodeScalars.append(scalar)
|
|
} else {
|
|
output.append(marker)
|
|
}
|
|
}
|
|
|
|
/// Decode \uXXXX, including surrogate pairs. On entry index points at the
|
|
/// first hex digit.
|
|
func appendUnicodeEscape(
|
|
_ text: [Character],
|
|
index: inout Int,
|
|
to output: inout String,
|
|
) {
|
|
let start = index
|
|
guard let codeUnit = readHex(text, index: &index, digits: 4) else {
|
|
output.append("u")
|
|
index = start
|
|
return
|
|
}
|
|
if isHighSurrogate(codeUnit), let scalar = readSurrogatePair(high: codeUnit, text: text, index: &index) {
|
|
output.unicodeScalars.append(scalar)
|
|
} else if let scalar = UnicodeScalar(codeUnit) {
|
|
output.unicodeScalars.append(scalar)
|
|
} else {
|
|
output.append("u")
|
|
}
|
|
}
|
|
|
|
func readSurrogatePair(
|
|
high: UInt32,
|
|
text: [Character],
|
|
index: inout Int,
|
|
) -> UnicodeScalar? {
|
|
var probe = index
|
|
guard probe + 1 < text.count, text[probe] == "\\", text[probe + 1] == "u" else {
|
|
return nil
|
|
}
|
|
probe += 2
|
|
guard let low = readHex(text, index: &probe, digits: 4), isLowSurrogate(low) else {
|
|
return nil
|
|
}
|
|
index = probe
|
|
let value = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00)
|
|
return UnicodeScalar(value)
|
|
}
|
|
|
|
/// Read exactly `digits` hex characters starting at index. On success index
|
|
/// advances past them, otherwise index is left unchanged.
|
|
func readHex(
|
|
_ text: [Character],
|
|
index: inout Int,
|
|
digits: Int,
|
|
) -> UInt32? {
|
|
guard index + digits <= text.count else {
|
|
return nil
|
|
}
|
|
var value: UInt32 = 0
|
|
for offset in 0 ..< digits {
|
|
guard let digit = text[index + offset].hexDigitValue else {
|
|
return nil
|
|
}
|
|
value = (value << 4) + UInt32(digit)
|
|
}
|
|
index += digits
|
|
return value
|
|
}
|
|
|
|
func isHighSurrogate(_ value: UInt32) -> Bool {
|
|
(0xD800 ... 0xDBFF).contains(value)
|
|
}
|
|
|
|
func isLowSurrogate(_ value: UInt32) -> Bool {
|
|
(0xDC00 ... 0xDFFF).contains(value)
|
|
}
|
|
}
|