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. 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 scalar.value < 0x20 { out += "\\u" + hex4(scalar.value) } else { out.unicodeScalars.append(scalar) } } } out += "\"" return out } 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 } } }