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#if os(macOS)
3import AppKit
4private typealias PlatformFont = NSFont
5#else
6import UIKit
7private typealias PlatformFont = UIFont
8#endif
9
10extension NSAttributedString.Key {
11 static let docsNode = NSAttributedString.Key("OficinaDocsNode")
12 static let docsBlock = NSAttributedString.Key("OficinaDocsBlock")
13 static let docsOffset = NSAttributedString.Key("OficinaDocsOffset")
14 static let docsQuoteBorders = NSAttributedString.Key("OficinaDocsQuoteBorders")
15 static let docsMarks = NSAttributedString.Key("OficinaDocsMarks")
16}
17@MainActor enum NativeTextProjection {
18 static func render(_ projection: CRDTProjection, images: [String: DocsPlatformImage] = [:]) -> NSAttributedString {
19 let output = NSMutableAttributedString(string: "")
20 for node in projection.children {
21 let start = output.length
22 append(node, to: output, depth: 0, lists: [], images: images)
23 #if os(macOS)
24 let margins = DocumentBlockSpacing.margins(node)
25 output.addAttributes([.docsTopBlock: node.id, .docsMarginBefore: margins.0, .docsMarginAfter: margins.1],
26 range: NSRange(location: start, length: output.length - start))
27 #endif
28 }
29 DocumentBlockSpacing.apply(projection.children, to: output)
30 DocumentInlineCode.apply(to: output)
31 return output
32 }
33 private static func append(_ node: CRDTNode, to output: NSMutableAttributedString, depth: Int, lists: [NSTextList], images: [String: DocsPlatformImage], containers: [DocsClipboardContainer] = []) {
34 if ["bullet_list", "ordered_list", "list_item", "blockquote"].contains(node.type) {
35 var nested = lists
36 if node.type == "bullet_list" { nested.append(NSTextList(markerFormat: .disc, options: 0)) }
37 if node.type == "ordered_list" {
38 let list = NSTextList(markerFormat: .decimal, options: 0)
39 if let order = Int(node.attrs?["order"] ?? ""), (Int(Int32.min)...Int(Int32.max)).contains(order) { list.startingItemNumber = order }
40 nested.append(list)
41 }
42 let ancestry = containers + [DocsClipboardContainer(id: node.id, type: node.type, attrs: node.attrs)]
43 for child in node.children ?? [] { append(child, to: output, depth: depth + (node.type == "list_item" || node.type == "blockquote" ? 1 : 0), lists: nested, images: images, containers: ancestry) }
44 return
45 }
46 let start = output.length
47 let style = NSMutableParagraphStyle()
48 style.paragraphSpacing = 0
49 style.textLists = lists
50 let indent = max(0, min(8, Int(node.attrs?["indent"] ?? "0") ?? 0))
51 style.headIndent = containers.reduce(CGFloat(indent * 36)) { offset, container in
52 offset + (container.type == "list_item" ? 16.8 : container.type == "blockquote" ? 14.25 : 0)
53 }
54 style.firstLineHeadIndent = style.headIndent
55 switch node.attrs?["align"] { case "center": style.alignment = .center; case "right": style.alignment = .right; default: style.alignment = .left }
56 let heading = node.type == "heading" ? max(1, min(6, Int(node.attrs?["level"] ?? "1") ?? 1)) : 0
57 #if os(macOS)
58 style.headerLevel = heading
59 #endif
60 let base: CGFloat = heading == 1 ? 19.2 : heading == 2 ? 15.6 : heading == 3 ? 13.2 : 12
61 let leading: CGFloat = heading > 0 ? 1.3 : 1.4
62 var inheritedMarks: [String: JSONValue] = [:]
63 if heading >= 4 || containers.contains(where: { $0.type == "blockquote" }) { inheritedMarks["em"] = .object([:]) }
64 if node.type == "code_block" { inheritedMarks["code"] = .object([:]) }
65 let baseFont = font(marks: inheritedMarks, size: base, heading: heading > 0)
66 #if os(macOS)
67 let naturalHeight = NSLayoutManager().defaultLineHeight(for: baseFont)
68 #else
69 let naturalHeight = baseFont.lineHeight
70 #endif
71 style.lineHeightMultiple = base * leading / naturalHeight
72 style.minimumLineHeight = base * leading
73 if heading > 0 {
74 style.paragraphSpacingBefore = base * 1.5
75 style.paragraphSpacing = base * 0.5
76 }
77 var baseAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: style, .font: baseFont, .docsBlock: node.id, .docsBlockType: node.type, .docsHeading: heading]
78 #if os(macOS)
79 baseAttributes[.docsBaseFont] = baseFont
80 baseAttributes[.docsLeading] = leading
81 #endif
82 var quoteBorders: [CGFloat] = []
83 var containerIndent: CGFloat = 0
84 for container in containers {
85 if container.type == "blockquote" { quoteBorders.append(containerIndent); containerIndent += 14.25 }
86 if container.type == "list_item" { containerIndent += 16.8 }
87 }
88 if !quoteBorders.isEmpty {
89 baseAttributes[.docsQuoteBorders] = quoteBorders
90 #if os(macOS)
91 baseAttributes[.foregroundColor] = NSColor(white: 74.0 / 255, alpha: 1)
92 #else
93 baseAttributes[.foregroundColor] = UIColor(white: 74.0 / 255, alpha: 1)
94 #endif
95 }
96 baseAttributes[.docsBlockAttributes] = try? JSONEncoder().encode(node.attrs ?? [:])
97 baseAttributes[.docsClipboardContainers] = try? JSONEncoder().encode(containers)
98 if node.type == "horizontal_rule" {
99 let rule = NSMutableAttributedString(attributedString: DocsAttachments.rule(width: 468))
100 rule.addAttributes(baseAttributes, range: NSRange(location: 0, length: rule.length))
101 output.append(rule); output.append(NSAttributedString(string: "\n", attributes: baseAttributes)); return
102 }
103 for child in node.children ?? [] {
104 if child.type == "text" {
105 var offset = 0
106 for run in child.runs ?? [] {
107 var attributes = baseAttributes
108 let marks = run.marks ?? [:]
109 var visualMarks = inheritedMarks.merging(marks, uniquingKeysWith: { _, authored in authored })
110 if node.type == "code_block" { visualMarks["code"] = .object([:]) }
111 attributes[.font] = font(marks: visualMarks, size: base, heading: heading > 0)
112 attributes[.docsNode] = child.id
113 attributes[.docsOffset] = offset
114 attributes[.docsMarks] = try? JSONEncoder().encode(marks)
115 if case .object(let link)? = marks["link"], case .string(let href)? = link["href"], let url = URL(string: href), ["https", "http", "mailto"].contains(url.scheme ?? "") { attributes[.link] = url }
116 output.append(NSAttributedString(string: run.insert, attributes: attributes))
117 offset += run.insert.utf16.count
118 }
119 } else if child.type == "hard_break" { output.append(NSAttributedString(string: "\u{2028}", attributes: baseAttributes)) }
120 else if child.type == "image" { let image = NSMutableAttributedString(attributedString: DocsAttachments.image(images[child.attrs?["src"] ?? ""], alt: child.attrs?["alt"] ?? "", maxWidth: 468))
121 image.addAttributes(baseAttributes.merging([.docsNode: child.id], uniquingKeysWith: { _, new in new }), range: NSRange(location: 0, length: image.length))
122 if let attrs = try? JSONEncoder().encode(child.attrs ?? [:]) { image.addAttribute(.docsImageAttributes, value: attrs, range: NSRange(location: 0, length: image.length)) }
123 output.append(image) }
124 else { append(child, to: output, depth: depth, lists: lists, images: images, containers: containers) }
125 }
126 var ending = baseAttributes
127 if output.length == start { ending[.docsNode] = node.id; ending[.docsOffset] = 0 }
128 output.append(NSAttributedString(string: "\n", attributes: ending))
129 }
130 private static func font(marks: [String: JSONValue], size: CGFloat, heading: Bool) -> PlatformFont {
131 DocsFonts.font(marks: marks, size: size, heading: heading)
132 }
133}
134