import Foundation
import UniformTypeIdentifiers
#if os(macOS)
import AppKit
#else
import UIKit
#endif
@MainActor extension DocsRichClipboard {
private static let nativeType = "net.amadan.oficina.docs.fragment"
static func readNative() -> [JSONValue]? {
#if os(macOS)
let data = NSPasteboard.general.data(forType: NSPasteboard.PasteboardType(nativeType))
#else
let data = UIPasteboard.general.data(forPasteboardType: nativeType)
#endif
guard let data, data.count <= 16 << 20,
let values = try? JSONDecoder().decode([JSONValue].self, from: data), !values.isEmpty else { return nil }
// This is still untrusted input. The replica validates the same node
// vocabulary and transaction limits as every other paste command.
return values
}
@discardableResult static func write(_ source: NSAttributedString) -> Bool {
guard source.length > 0 else { return false }
let values = blocks(source)
guard !values.isEmpty, let data = try? JSONEncoder().encode(values), data.count <= 16 << 20 else { return false }
let html = html(values)
let rtf = try? source.data(from: NSRange(location: 0, length: source.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
#if os(macOS)
let board = NSPasteboard.general
board.clearContents()
guard board.setData(data, forType: NSPasteboard.PasteboardType(nativeType)) else { return false }
_ = board.setString(source.string, forType: .string)
_ = board.setString(html, forType: .html)
if let rtf { _ = board.setData(rtf, forType: .rtf) }
#else
var item: [String: Any] = [nativeType: data, UTType.utf8PlainText.identifier: source.string, UTType.html.identifier: Data(html.utf8)]
if let rtf { item[UTType.rtf.identifier] = rtf }
UIPasteboard.general.setItems([item])
#endif
return true
}
static func html(_ values: [JSONValue]) -> String { values.map(htmlNode).joined() }
private static func escape(_ value: String) -> String {
value.replacingOccurrences(of: "&", with: "&").replacingOccurrences(of: "<", with: "<")
.replacingOccurrences(of: ">", with: ">").replacingOccurrences(of: "\"", with: """)
}
private static func string(_ value: JSONValue?) -> String {
if case .string(let result) = value { return result }; return ""
}
private static func htmlNode(_ node: JSONValue) -> String {
guard case .object(let object) = node else { return "" }
let type = string(object["type"])
let attrs: [String: JSONValue]
if case .object(let value)? = object["attrs"] { attrs = value } else { attrs = [:] }
if type == "text", case .array(let runs)? = object["runs"] { return runs.map(htmlRun).joined() }
let children: [JSONValue]
if case .array(let value)? = object["children"] { children = value } else { children = [] }
let body = html(children)
if type == "hard_break" { return "
" }
if type == "horizontal_rule" { return "
\(output)" }
else if key == "link", case .object(let link)? = marks[key], safeLink(string(link["href"])) {
let title = link["title"].map { " title=\"\(escape(string($0)))\"" } ?? ""
output = "\(output)"
}
else if ["font_sans", "font_serif", "font_mono"].contains(key) {
let family = key == "font_serif" ? "serif" : key == "font_mono" ? "monospace" : "sans-serif"
output = "\(output)"
} else if key.hasPrefix("size_") {
let suffix = String(key.dropFirst(5))
let size = ["sm": 10, "lg": 14, "xl": 18][suffix] ?? Int(suffix)
if let size, sizes.contains(size), size != 12 { output = "\(output)" }
}
}
return output
}
}