oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2@MainActor enum TextEditMapping {
3 static func marks(in text: NSAttributedString, at position: Int, preferFollowing: Bool = false) -> [String: JSONValue] {
4 guard position >= 0, position <= text.length, text.length > 0 else { return [:] }
5 func read(_ index: Int) -> [String: JSONValue] {
6 guard index >= 0, index < text.length,
7 let data = text.attribute(.docsMarks, at: index, effectiveRange: nil) as? Data else { return [:] }
8 return (try? JSONDecoder().decode([String: JSONValue].self, from: data)) ?? [:]
9 }
10 let previousIsText = position > 0 && text.attribute(.docsMarks, at: position - 1, effectiveRange: nil) is Data
11 var result = read(preferFollowing || !previousIsText ? position : position - 1)
12 // Links are non-inclusive in the web schema: typing at either boundary
13 // must not extend a link, while typing inside one preserves it.
14 if !preferFollowing, let link = result["link"],
15 read(position - 1)["link"] != link || read(position)["link"] != link {
16 result.removeValue(forKey: "link")
17 }
18 return result
19 }
20 static func displayCommand(in text: NSAttributedString, range: NSRange, replacement: String) -> [String: JSONValue]? {
21 guard range.location >= 0, range.length >= 0, range.location <= text.length, range.length <= text.length - range.location else { return nil }
22 let marks = marks(in: text, at: range.location, preferFollowing: range.length > 0)
23 return ["op": .string("display_edit"), "at": .number(Double(range.location)), "length": .number(Double(range.length)), "text": .string(replacement), "marks": .object(marks)]
24 }
25 static func command(in text: NSAttributedString, range: NSRange, replacement: String) -> [String: JSONValue]? {
26 guard text.length > 0, range.location <= text.length, NSMaxRange(range) <= text.length else { return nil }
27 let index = min(range.location, text.length - 1)
28 var run = NSRange()
29 let attributes = text.attributes(at: index, effectiveRange: &run)
30 guard let node = attributes[.docsNode] as? String else { return nil }
31 if range.length > 0 {
32 var valid = true
33 text.enumerateAttribute(.docsNode, in: range) { value, _, stop in
34 if value as? String != node { valid = false; stop.pointee = true }
35 }
36 guard valid else { return nil }
37 }
38 var nodeStart = index
39 while nodeStart > 0 && text.attribute(.docsNode, at: nodeStart - 1, effectiveRange: nil) as? String == node { nodeStart -= 1 }
40 let offset = index - nodeStart
41 if replacement == "\n" && range.length == 0 {
42 return ["op":.string("split"), "node":.string(node), "at":.number(Double(offset))]
43 }
44 let marks = (attributes[.docsMarks] as? Data).flatMap { try? JSONDecoder().decode([String: JSONValue].self, from: $0) } ?? [:]
45 return ["op":.string("replace_text"), "node":.string(node), "at":.number(Double(offset)), "length":.number(Double(range.length)), "text":.string(replacement), "marks":.object(marks)]
46 }
47}
48