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 🧪 é½

  1. First
  2. 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 textLarge

" 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) }