| 1 | import Foundation |
| 2 | |
| 3 | /// The optimistic string mirrors the engine's block splice, while the next |
| 4 | /// acknowledged projection supplies fonts, list markers and image attachments. |
| 5 | @MainActor enum DocumentClipboardInsertion { |
| 6 | static func text(blocks: [JSONValue], in content: NSAttributedString, range: NSRange) -> String? { |
| 7 | guard !blocks.isEmpty, range.location >= 0, range.length >= 0, |
| 8 | range.location <= content.length, range.length <= content.length - range.location else { return nil } |
| 9 | var budget = 100_000 |
| 10 | guard let text = flatten(.object(["type": .string("fragment"), "children": .array(blocks)]), depth: 0, budget: &budget) else { return nil } |
| 11 | let firstIsText = ["paragraph", "heading", "code_block"].contains(type(blocks[0])) |
| 12 | let lastHasText = containsTextBlock(blocks[blocks.count - 1]) |
| 13 | let raw = content.string as NSString |
| 14 | let paragraph = raw.paragraphRange(for: NSRange(location: min(range.location, max(0, raw.length - 1)), length: 0)) |
| 15 | let prefixLength = range.location - paragraph.location |
| 16 | let prefix = !firstIsText && prefixLength > 0 ? "\n" : "" |
| 17 | return prefix + (lastHasText && text.hasSuffix("\n") ? String(text.dropLast()) : text) |
| 18 | } |
| 19 | static func attributed(blocks: [JSONValue], in content: NSAttributedString, range: NSRange) -> NSAttributedString? { |
| 20 | guard let expected = text(blocks: blocks, in: content, range: range) else { return nil } |
| 21 | var counter = 0 |
| 22 | func identified(_ value: JSONValue) -> JSONValue { |
| 23 | guard case .object(var node) = value else { return value } |
| 24 | counter += 1; node["id"] = .string("clipboard-\(counter)") |
| 25 | if case .array(let children)? = node["children"] { node["children"] = .array(children.map(identified)) } |
| 26 | return .object(node) |
| 27 | } |
| 28 | let value: [String: JSONValue] = ["children": .array(blocks.map(identified)), "canUndo": .bool(false), "canRedo": .bool(false)] |
| 29 | guard let data = try? JSONEncoder().encode(value), let projection = try? JSONDecoder().decode(CRDTProjection.self, from: data) else { return nil } |
| 30 | let rendered = NSMutableAttributedString(attributedString: NativeTextProjection.render(projection)) |
| 31 | if containsTextBlock(blocks[blocks.count - 1]), rendered.string.hasSuffix("\n") { rendered.deleteCharacters(in: NSRange(location: rendered.length - 1, length: 1)) } |
| 32 | if expected.hasPrefix("\n"), !rendered.string.hasPrefix("\n"), expected == "\n" + rendered.string { rendered.insert(NSAttributedString(string: "\n"), at: 0) } |
| 33 | guard rendered.string == expected else { return nil } |
| 34 | return rendered |
| 35 | } |
| 36 | private static func type(_ value: JSONValue) -> String { |
| 37 | guard case .object(let node) = value, case .string(let type)? = node["type"] else { return "" }; return type |
| 38 | } |
| 39 | private static func containsTextBlock(_ value: JSONValue) -> Bool { |
| 40 | guard case .object(let node) = value else { return false } |
| 41 | if ["paragraph", "heading", "code_block"].contains(type(value)) { return true } |
| 42 | guard case .array(let children)? = node["children"], let last = children.last else { return false } |
| 43 | return containsTextBlock(last) |
| 44 | } |
| 45 | private static func flatten(_ value: JSONValue, depth: Int, budget: inout Int) -> String? { |
| 46 | guard depth <= 64, budget > 0, case .object(let node) = value else { return nil } |
| 47 | budget -= 1 |
| 48 | let kind = type(value) |
| 49 | if kind == "text" { |
| 50 | guard case .array(let runs)? = node["runs"] else { return nil } |
| 51 | var text = "" |
| 52 | for run in runs { |
| 53 | guard case .object(let run) = run, case .string(let insert)? = run["insert"] else { return nil } |
| 54 | text += insert |
| 55 | } |
| 56 | return text |
| 57 | } |
| 58 | if kind == "hard_break" { return "\u{2028}" } |
| 59 | if kind == "image" { return "\u{fffc}" } |
| 60 | if kind == "horizontal_rule" { return "\u{fffc}\n" } |
| 61 | guard ["fragment", "paragraph", "heading", "code_block", "blockquote", "bullet_list", "ordered_list", "list_item"].contains(kind) else { return nil } |
| 62 | var result = "" |
| 63 | if case .array(let children)? = node["children"] { |
| 64 | for child in children { guard let text = flatten(child, depth: depth + 1, budget: &budget) else { return nil }; result += text } |
| 65 | } |
| 66 | if ["paragraph", "heading", "code_block"].contains(kind) { result += "\n" } |
| 67 | return result |
| 68 | } |
| 69 | } |
| 70 | |