Some checks failed
rules-freshness / rules (push) Failing after 12m24s
The JSON writer escapes the characters a YAML reader mangles. PureYAML.JSON.parse and parseLocated read JSON with those characters held back and given back, so no position moves, and Convert.convert takes the format of its source.
136 lines
5.4 KiB
Swift
136 lines
5.4 KiB
Swift
import PureYAML
|
|
|
|
public extension PureYAML.JSON {
|
|
/// A pure-Swift JSON serializer for the YAML/JSON value tree. Converting a
|
|
/// parsed document to strict JSON is the core "emit as JSON" operation; since
|
|
/// JSON is a subset of YAML 1.2, any value PureYAML parses can be emitted as
|
|
/// JSON unless it uses a YAML-only feature with no JSON representation (a
|
|
/// non-string mapping key, or a non-finite number).
|
|
enum Writer {
|
|
/// Serialize a value tree to a JSON string.
|
|
public static func string(from value: PureYAML.Model.Value, options: Options = .pretty) throws -> String {
|
|
var out = ""
|
|
try write(value, into: &out, depth: 0, options: options)
|
|
return out
|
|
}
|
|
|
|
private static func write(
|
|
_ value: PureYAML.Model.Value,
|
|
into out: inout String,
|
|
depth: Int,
|
|
options: Options,
|
|
) throws {
|
|
switch value {
|
|
case .null:
|
|
out += "null"
|
|
case let .bool(flag):
|
|
out += flag ? "true" : "false"
|
|
case let .int(number):
|
|
out += String(number)
|
|
case let .double(number):
|
|
guard number.isFinite else { throw Failure.nonFiniteNumber(number) }
|
|
out += String(number)
|
|
case let .string(text):
|
|
out += escaped(text)
|
|
case let .sequence(items):
|
|
try writeArray(items, into: &out, depth: depth, options: options)
|
|
case let .mapping(mapping):
|
|
try writeObject(mapping, into: &out, depth: depth, options: options)
|
|
}
|
|
}
|
|
|
|
private static func writeArray(
|
|
_ items: [PureYAML.Model.Value],
|
|
into out: inout String,
|
|
depth: Int,
|
|
options: Options,
|
|
) throws {
|
|
guard !items.isEmpty else {
|
|
out += "[]"
|
|
return
|
|
}
|
|
out += "["
|
|
for (index, item) in items.enumerated() {
|
|
out += index == 0 ? "" : ","
|
|
newline(&out, depth: depth + 1, options: options)
|
|
try write(item, into: &out, depth: depth + 1, options: options)
|
|
}
|
|
newline(&out, depth: depth, options: options)
|
|
out += "]"
|
|
}
|
|
|
|
private static func writeObject(
|
|
_ mapping: PureYAML.Model.Mapping,
|
|
into out: inout String,
|
|
depth: Int,
|
|
options: Options,
|
|
) throws {
|
|
guard !mapping.pairs.isEmpty else {
|
|
out += "{}"
|
|
return
|
|
}
|
|
out += "{"
|
|
for (index, pair) in mapping.pairs.enumerated() {
|
|
guard let key = pair.keyNode.stringValue else {
|
|
throw Failure.nonStringKey(pair.keyNode.description)
|
|
}
|
|
out += index == 0 ? "" : ","
|
|
newline(&out, depth: depth + 1, options: options)
|
|
out += escaped(key)
|
|
out += options.pretty ? ": " : ":"
|
|
try write(pair.value, into: &out, depth: depth + 1, options: options)
|
|
}
|
|
newline(&out, depth: depth, options: options)
|
|
out += "}"
|
|
}
|
|
|
|
private static func newline(_ out: inout String, depth: Int, options: Options) {
|
|
guard options.pretty else { return }
|
|
out += "\n"
|
|
out += String(repeating: " ", count: depth * options.indent)
|
|
}
|
|
|
|
/// Quote and escape a string per RFC 8259: `"` and `\` are escaped, the
|
|
/// named short escapes are used where they apply, and any remaining control
|
|
/// character below U+0020 becomes a `\u00XX` sequence. So that a YAML reader gives the same string back from the text (JSON may hold them as they are,
|
|
/// and YAML does not: it folds U+0085, U+2028 and U+2029 as line breaks, refuses DEL and the C1 controls, and drops a byte order mark that starts a stream),
|
|
/// U+007F to U+009F, U+2028, U+2029, U+FEFF, U+FFFE and U+FFFF are escaped too.
|
|
private static func escaped(_ text: String) -> String {
|
|
var out = "\""
|
|
for scalar in text.unicodeScalars {
|
|
switch scalar {
|
|
case "\"": out += "\\\""
|
|
case "\\": out += "\\\\"
|
|
case "\u{08}": out += "\\b"
|
|
case "\u{0C}": out += "\\f"
|
|
case "\n": out += "\\n"
|
|
case "\r": out += "\\r"
|
|
case "\t": out += "\\t"
|
|
default:
|
|
if mustEscape(scalar) {
|
|
out += "\\u" + hex4(scalar.value)
|
|
} else {
|
|
out.unicodeScalars.append(scalar)
|
|
}
|
|
}
|
|
}
|
|
out += "\""
|
|
return out
|
|
}
|
|
|
|
private static func mustEscape(_ scalar: Unicode.Scalar) -> Bool {
|
|
scalar.value < 0x20 || (0x7F ... 0x9F).contains(scalar.value) || [0x2028, 0x2029, 0xFEFF, 0xFFFE, 0xFFFF].contains(scalar.value)
|
|
}
|
|
|
|
private static func hex4(_ value: UInt32) -> String {
|
|
let digits = "0123456789abcdef"
|
|
var result = ""
|
|
for shift in stride(from: 12, through: 0, by: -4) {
|
|
let index = Int((value >> UInt32(shift)) & 0xF)
|
|
result.append(digits[digits.index(digits.startIndex, offsetBy: index)])
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|