oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import Testing
3@testable import DocsKit
4#if os(macOS)
5import AppKit
6#else
7import UIKit
8#endif
9
10private func field(_ value: JSONValue, _ key: String) -> JSONValue? {
11 if case .object(let values) = value { return values[key] }
12 return nil
13}
14private func array(_ value: JSONValue?) -> [JSONValue] {
15 if case .array(let values) = value { return values }
16 return []
17}
18private func flattened(_ values: [JSONValue]) -> [JSONValue] {
19 values.flatMap { [$0] + flattened(array(field($0, "children"))) }
20}
21private func runs(_ values: [JSONValue]) -> [JSONValue] {
22 flattened(values).flatMap { array(field($0, "runs")) }
23}
24
25@MainActor @Test func nativeRichClipboardPreservesTypographyAndParagraphStyle() {
26 let style = NSMutableParagraphStyle()
27 #if os(macOS)
28 style.headerLevel = 2
29 #endif
30 style.alignment = .center
31 let source = NSAttributedString(string: "Résumé 🧪\n", attributes: [
32 .font: DocsFonts.font(marks: ["strong": .object([:]), "em": .object([:]), "font_serif": .object([:]), "size_18": .object([:])], size: 12, heading: false),
33 .paragraphStyle: style,
34 .docsHeading: 2,
35 .link: URL(string: "https://example.com/doc")!,
36 ])
37 let blocks = DocsRichClipboard.blocks(source)
38 #expect(blocks.count == 1)
39 #expect(field(blocks[0], "type") == .string("heading"))
40 #expect(field(field(blocks[0], "attrs")!, "level") == .string("2"))
41 #expect(field(field(blocks[0], "attrs")!, "align") == .string("center"))
42 let run = runs(blocks)[0]
43 #expect(field(run, "insert") == .string("Résumé 🧪"))
44 let marks = field(run, "marks")!
45 for name in ["strong", "em", "font_serif", "size_18"] { #expect(field(marks, name) == .object([:])) }
46 #expect(field(marks, "link") == .object(["href": .string("https://example.com/doc")]))
47}
48
49@MainActor @Test func htmlClipboardKeepsListsLinksHardBreaksAndKnownEntities() {
50 let html = "<h2>Heading</h2><p><strong>Bold</strong> <a href='https://example.com/?x=1&amp;y=2'>link</a><br>Next &#x1F9EA; &eacute;&frac12;</p><ol start='3'><li>First<ul><li>Nested</li></ul></li><li><em>Second</em></li></ol>"
51 let blocks = DocsHTMLClipboard.blocks(html)
52 #expect(blocks.count == 3)
53 #expect(field(blocks[0], "type") == .string("heading"))
54 #expect(field(blocks[2], "type") == .string("ordered_list"))
55 #expect(field(field(blocks[2], "attrs")!, "order") == .string("3"))
56 let nodes = flattened(blocks)
57 #expect(nodes.contains { field($0, "type") == .string("bullet_list") })
58 #expect(nodes.contains { field($0, "type") == .string("hard_break") })
59 let text = runs(blocks)
60 #expect(text.contains { field($0, "insert") == .string("Next 🧪 é½") })
61 #expect(text.contains { field(field($0, "marks")!, "strong") == .object([:]) })
62 #expect(text.contains { field(field($0, "marks")!, "link") == .object(["href": .string("https://example.com/?x=1&y=2")]) })
63}
64
65@MainActor @Test func htmlClipboardNeverImportsExecutableContentOrUnsafeLinks() {
66 let html = "<html><head><style>@import url(https://example.com/style);</style></head><body><script>secret()</script><p><a href='javascript:secret()'>Safe text</a><img src='file:///private/secret'><span style='font-family: Georgia; font-size:24px; font-weight:bold'>Large</span></p></body></html>"
67 let values = DocsHTMLClipboard.blocks(html)
68 let text = runs(values)
69 #expect(text.count == 2)
70 #expect(text.allSatisfy { field(field($0, "marks")!, "link") == nil })
71 #expect(!flattened(values).contains { field($0, "type") == .string("image") })
72 #expect(field(field(text[1], "marks")!, "font_serif") == .object([:]))
73 #expect(field(field(text[1], "marks")!, "size_18") == .object([:]))
74}
75
76@MainActor @Test func htmlClipboardPreservesBlankParagraphsAndExplicitFontResets() {
77 let values = DocsHTMLClipboard.blocks("<p>Before</p><p></p><p><strong>Bold<span style='font-weight:normal'>normal</span></strong></p>")
78 #expect(values.count == 3)
79 let text = runs(values)
80 let normal = text.first { field($0, "insert") == .string("normal") }
81 #expect(normal != nil)
82 #expect(field(field(normal!, "marks")!, "strong") == nil)
83}
84
85@MainActor @Test func webClipboardClassesAndGoogleBlankLinesRemainSemantic() {
86 let html = "<b style='font-weight:normal'><p class='docs-align-center docs-indent-2'><span class='docs-font-serif docs-size-18'>One</span></p><p>Two</p><br><p>Four</p></b><br class='Apple-interchange-newline'><br class='Apple-interchange-newline'>"
87 let values = DocsHTMLClipboard.blocks(html)
88 #expect(values.count == 4)
89 #expect(field(field(values[0], "attrs")!, "align") == .string("center"))
90 #expect(field(field(values[0], "attrs")!, "indent") == .string("2"))
91 let mark = field(runs(values)[0], "marks")!
92 #expect(field(mark, "font_serif") == .object([:]))
93 #expect(field(mark, "size_18") == .object([:]))
94 #expect(field(mark, "strong") == nil)
95 #expect(!flattened([values[2]]).contains { field($0, "type") == .string("hard_break") })
96}
97
98@MainActor @Test func nativeClipboardMetadataPreservesCodeImagesAndNestedContainers() throws {
99 let data = Data("""
100 {"children":[{"id":"quote","type":"blockquote","children":[{"id":"list","type":"ordered_list","attrs":{"order":"3"},"children":[{"id":"item","type":"list_item","children":[{"id":"heading","type":"heading","attrs":{"level":"2"},"children":[{"id":"title","type":"text","runs":[{"insert":"Title"}]}]},{"id":"code","type":"code_block","children":[{"id":"code-text","type":"text","runs":[{"insert":"let x = 1\\nprint(x)"}]}]},{"id":"image-paragraph","type":"paragraph","children":[{"id":"image","type":"image","attrs":{"src":"https://example.com/image.png","alt":"A diagram"}}]}]}]}]}],"canUndo":false,"canRedo":false}
101 """.utf8)
102 let projection = try JSONDecoder().decode(CRDTProjection.self, from: data)
103 let content = NativeTextProjection.render(projection)
104 let values = DocsRichClipboard.blocks(content)
105 #expect(values.count == 1)
106 #expect(field(values[0], "type") == .string("blockquote"))
107 let nodes = flattened(values)
108 let lists = nodes.filter { field($0, "type") == .string("ordered_list") }
109 #expect(lists.count == 1)
110 #expect(field(field(lists[0], "attrs")!, "order") == .string("3"))
111 let code = nodes.filter { field($0, "type") == .string("code_block") }
112 #expect(code.count == 1)
113 #expect(field(runs(code)[0], "insert") == .string("let x = 1\nprint(x)"))
114 let images = nodes.filter { field($0, "type") == .string("image") }
115 #expect(images.count == 1)
116 #expect(field(field(images[0], "attrs")!, "src") == .string("https://example.com/image.png"))
117 #expect(runs(values).allSatisfy { field(field($0, "marks")!, "size_12") == nil })
118 let roundTrip = DocsHTMLClipboard.blocks(DocsRichClipboard.html(values))
119 #expect(flattened(roundTrip).contains { field($0, "type") == .string("code_block") })
120 #expect(flattened(roundTrip).contains { field($0, "type") == .string("image") })
121}
122
123@MainActor @Test func richClipboardLinkTitleSurvivesHTMLRoundTrip() {
124 let first = DocsHTMLClipboard.blocks("<p><a href='https://example.com' title='A &quot;quoted&quot; title'>Link</a></p>")
125 let again = DocsHTMLClipboard.blocks(DocsRichClipboard.html(first))
126 let link = field(field(runs(again)[0], "marks")!, "link")!
127 #expect(field(link, "title") == .string("A \"quoted\" title"))
128}
129
130@MainActor @Test func nativeNumberedListUsesItsStoredStartingNumber() throws {
131 let data = Data("""
132 {"children":[{"id":"list","type":"ordered_list","attrs":{"order":"7"},"children":[{"id":"item","type":"list_item","children":[{"id":"p","type":"paragraph","children":[{"id":"text","type":"text","runs":[{"insert":"Seventh"}]}]}]}]}],"canUndo":false,"canRedo":false}
133 """.utf8)
134 let content = NativeTextProjection.render(try JSONDecoder().decode(CRDTProjection.self, from: data))
135 let style = content.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
136 #expect(style?.textLists.first?.startingItemNumber == 7)
137}
138
139@MainActor @Test func projectionClampsUntrustedHeadingAndIndentValues() throws {
140 let data = Data("""
141 {"children":[{"id":"p","type":"heading","attrs":{"level":"9223372036854775807","indent":"9223372036854775807"},"children":[{"id":"text","type":"text","runs":[{"insert":"Bounded"}]}]}],"canUndo":false,"canRedo":false}
142 """.utf8)
143 let content = NativeTextProjection.render(try JSONDecoder().decode(CRDTProjection.self, from: data))
144 let style = content.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
145 #expect(style?.headIndent == 288)
146 #expect(content.attribute(.docsHeading, at: 0, effectiveRange: nil) as? Int == 6)
147}
148