import PureYAML extension PureYAML.JSON { /// JSON text with the characters the YAML reader cannot be given held back. /// /// Each U+007F to U+009F, U+2028, U+2029, U+FFFE and U+FFFF that is inside a string is replaced, one scalar for one scalar, by a private-use character that is not /// in the text, so that the reader sees a plain character and every position of the text is the position it had. The replacement is undone in the value that /// comes out of the reader. A private-use character that the text itself holds is never used as a replacement, so what comes back is never mistaken for it. struct Source { let masked: String /// What each replacement stands for. private let originals: [Unicode.Scalar: Unicode.Scalar] init(_ text: String) throws { let present = Set(text.unicodeScalars.filter { Self.privateUse.contains($0.value) }) var free = Self.privateUse.lazy.compactMap(Unicode.Scalar.init).filter { !present.contains($0) }.makeIterator() var replacement: [Unicode.Scalar: Unicode.Scalar] = [:] var originals: [Unicode.Scalar: Unicode.Scalar] = [:] var output = String.UnicodeScalarView() var inString = false var escaped = false for scalar in text.unicodeScalars { guard inString else { inString = scalar == "\"" output.append(scalar) continue } if escaped { escaped = false } else if scalar == "\\" { escaped = true } else if scalar == "\"" { inString = false } else if Self.isHeldBack(scalar) { if replacement[scalar] == nil { guard let next = free.next() else { throw PureYAML.JSON.Convert.Failure.sourceInvalid("the text holds too many private-use characters to read it as JSON") } replacement[scalar] = next originals[next] = scalar } output.append(replacement[scalar] ?? scalar) continue } output.append(scalar) } masked = String(output) self.originals = originals } private static let privateUse: ClosedRange = 0xE000 ... 0xF8FF /// The characters RFC 8259 lets a string hold as they are and the YAML reader folds or refuses. static func isHeldBack(_ scalar: Unicode.Scalar) -> Bool { (0x7F ... 0x9F).contains(scalar.value) || [0x2028, 0x2029, 0xFFFE, 0xFFFF].contains(scalar.value) } func restoring(_ text: String) -> String { guard !originals.isEmpty else { return text } var output = String.UnicodeScalarView() for scalar in text.unicodeScalars { output.append(originals[scalar] ?? scalar) } return String(output) } func restoring(_ value: PureYAML.Model.Value) -> PureYAML.Model.Value { guard !originals.isEmpty else { return value } switch value { case .null, .bool, .int, .double: return value case let .string(text): return .string(restoring(text)) case let .sequence(items): return .sequence(items.map(restoring)) case let .mapping(mapping): return .mapping(restoring(mapping)) } } func restoring(_ located: PureYAML.Model.LocatedValue) -> PureYAML.Model.LocatedValue { guard !originals.isEmpty else { return located } switch located { case let .scalar(value, range): return .scalar(restoring(value), range) case let .sequence(items, range): return .sequence(items.map(restoring), range) case let .mapping(pairs, range): return .mapping(pairs.map { .init(keyNode: restoring($0.keyNode), keyRange: $0.keyRange, value: restoring($0.value)) }, range) } } private func restoring(_ mapping: PureYAML.Model.Mapping) -> PureYAML.Model.Mapping { .init(mapping.pairs.map { .init(keyNode: restoring($0.keyNode), value: restoring($0.value)) }) } private func restoring(_ key: PureYAML.Model.Key) -> PureYAML.Model.Key { switch key { case let .string(text): .string(restoring(text)) case let .sequence(items): .sequence(items.map(restoring)) case let .mapping(mapping): .mapping(restoring(mapping)) } } } }