import Foundation
import Testing
@testable import DocsKit
#if os(macOS)
import AppKit
#else
import UIKit
#endif
private func field(_ value: JSONValue, _ key: String) -> JSONValue? {
if case .object(let values) = value { return values[key] }
return nil
}
private func array(_ value: JSONValue?) -> [JSONValue] {
if case .array(let values) = value { return values }
return []
}
private func flattened(_ values: [JSONValue]) -> [JSONValue] {
values.flatMap { [$0] + flattened(array(field($0, "children"))) }
}
private func runs(_ values: [JSONValue]) -> [JSONValue] {
flattened(values).flatMap { array(field($0, "runs")) }
}
@MainActor @Test func nativeRichClipboardPreservesTypographyAndParagraphStyle() {
let style = NSMutableParagraphStyle()
#if os(macOS)
style.headerLevel = 2
#endif
style.alignment = .center
let source = NSAttributedString(string: "Résumé 🧪\n", attributes: [
.font: DocsFonts.font(marks: ["strong": .object([:]), "em": .object([:]), "font_serif": .object([:]), "size_18": .object([:])], size: 12, heading: false),
.paragraphStyle: style,
.docsHeading: 2,
.link: URL(string: "https://example.com/doc")!,
])
let blocks = DocsRichClipboard.blocks(source)
#expect(blocks.count == 1)
#expect(field(blocks[0], "type") == .string("heading"))
#expect(field(field(blocks[0], "attrs")!, "level") == .string("2"))
#expect(field(field(blocks[0], "attrs")!, "align") == .string("center"))
let run = runs(blocks)[0]
#expect(field(run, "insert") == .string("Résumé 🧪"))
let marks = field(run, "marks")!
for name in ["strong", "em", "font_serif", "size_18"] { #expect(field(marks, name) == .object([:])) }
#expect(field(marks, "link") == .object(["href": .string("https://example.com/doc")]))
}
@MainActor @Test func htmlClipboardKeepsListsLinksHardBreaksAndKnownEntities() {
let html = "
Heading
Bold link
Next 🧪 é½
- First
- Second
"
let blocks = DocsHTMLClipboard.blocks(html)
#expect(blocks.count == 3)
#expect(field(blocks[0], "type") == .string("heading"))
#expect(field(blocks[2], "type") == .string("ordered_list"))
#expect(field(field(blocks[2], "attrs")!, "order") == .string("3"))
let nodes = flattened(blocks)
#expect(nodes.contains { field($0, "type") == .string("bullet_list") })
#expect(nodes.contains { field($0, "type") == .string("hard_break") })
let text = runs(blocks)
#expect(text.contains { field($0, "insert") == .string("Next 🧪 é½") })
#expect(text.contains { field(field($0, "marks")!, "strong") == .object([:]) })
#expect(text.contains { field(field($0, "marks")!, "link") == .object(["href": .string("https://example.com/?x=1&y=2")]) })
}
@MainActor @Test func htmlClipboardNeverImportsExecutableContentOrUnsafeLinks() {
let html = "Safe text
Large
"
let values = DocsHTMLClipboard.blocks(html)
let text = runs(values)
#expect(text.count == 2)
#expect(text.allSatisfy { field(field($0, "marks")!, "link") == nil })
#expect(!flattened(values).contains { field($0, "type") == .string("image") })
#expect(field(field(text[1], "marks")!, "font_serif") == .object([:]))
#expect(field(field(text[1], "marks")!, "size_18") == .object([:]))
}
@MainActor @Test func htmlClipboardPreservesBlankParagraphsAndExplicitFontResets() {
let values = DocsHTMLClipboard.blocks("Before
Boldnormal
")
#expect(values.count == 3)
let text = runs(values)
let normal = text.first { field($0, "insert") == .string("normal") }
#expect(normal != nil)
#expect(field(field(normal!, "marks")!, "strong") == nil)
}
@MainActor @Test func webClipboardClassesAndGoogleBlankLinesRemainSemantic() {
let html = "One
Two
Four
"
let values = DocsHTMLClipboard.blocks(html)
#expect(values.count == 4)
#expect(field(field(values[0], "attrs")!, "align") == .string("center"))
#expect(field(field(values[0], "attrs")!, "indent") == .string("2"))
let mark = field(runs(values)[0], "marks")!
#expect(field(mark, "font_serif") == .object([:]))
#expect(field(mark, "size_18") == .object([:]))
#expect(field(mark, "strong") == nil)
#expect(!flattened([values[2]]).contains { field($0, "type") == .string("hard_break") })
}
@MainActor @Test func nativeClipboardMetadataPreservesCodeImagesAndNestedContainers() throws {
let data = Data("""
{"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}
""".utf8)
let projection = try JSONDecoder().decode(CRDTProjection.self, from: data)
let content = NativeTextProjection.render(projection)
let values = DocsRichClipboard.blocks(content)
#expect(values.count == 1)
#expect(field(values[0], "type") == .string("blockquote"))
let nodes = flattened(values)
let lists = nodes.filter { field($0, "type") == .string("ordered_list") }
#expect(lists.count == 1)
#expect(field(field(lists[0], "attrs")!, "order") == .string("3"))
let code = nodes.filter { field($0, "type") == .string("code_block") }
#expect(code.count == 1)
#expect(field(runs(code)[0], "insert") == .string("let x = 1\nprint(x)"))
let images = nodes.filter { field($0, "type") == .string("image") }
#expect(images.count == 1)
#expect(field(field(images[0], "attrs")!, "src") == .string("https://example.com/image.png"))
#expect(runs(values).allSatisfy { field(field($0, "marks")!, "size_12") == nil })
let roundTrip = DocsHTMLClipboard.blocks(DocsRichClipboard.html(values))
#expect(flattened(roundTrip).contains { field($0, "type") == .string("code_block") })
#expect(flattened(roundTrip).contains { field($0, "type") == .string("image") })
}
@MainActor @Test func richClipboardLinkTitleSurvivesHTMLRoundTrip() {
let first = DocsHTMLClipboard.blocks("Link
")
let again = DocsHTMLClipboard.blocks(DocsRichClipboard.html(first))
let link = field(field(runs(again)[0], "marks")!, "link")!
#expect(field(link, "title") == .string("A \"quoted\" title"))
}
@MainActor @Test func nativeNumberedListUsesItsStoredStartingNumber() throws {
let data = Data("""
{"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}
""".utf8)
let content = NativeTextProjection.render(try JSONDecoder().decode(CRDTProjection.self, from: data))
let style = content.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
#expect(style?.textLists.first?.startingItemNumber == 7)
}
@MainActor @Test func projectionClampsUntrustedHeadingAndIndentValues() throws {
let data = Data("""
{"children":[{"id":"p","type":"heading","attrs":{"level":"9223372036854775807","indent":"9223372036854775807"},"children":[{"id":"text","type":"text","runs":[{"insert":"Bounded"}]}]}],"canUndo":false,"canRedo":false}
""".utf8)
let content = NativeTextProjection.render(try JSONDecoder().decode(CRDTProjection.self, from: data))
let style = content.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
#expect(style?.headIndent == 288)
#expect(content.attribute(.docsHeading, at: 0, effectiveRange: nil) as? Int == 6)
}