import AppKit
import ConjureKit
import Testing
@Test @MainActor
func markdownImportProducesVisibleTextAndSemanticFormatting() throws {
let markdown = "# Title 🦊\n**bold** *italic* ***both*** under ~~gone~~ `let x = 1` [site](https://example.com)\n\n> quote\n\n- [ ] todo\n- [x] done\n - nested\n\n3. third\n4. fourth\n\n```swift\n# literal **code**\n```\n"
let document = NoteDocument.importMarkdown(NSAttributedString(string: markdown))
#expect(document.string == "Title 🦊\nbold italic both under gone let x = 1 site\n\nquote\n\ntodo\ndone\nnested\n\nthird\nfourth\n\n# literal **code**\n")
func attribute(_ key: NSAttributedString.Key, _ text: String) throws -> Any? {
let range = (document.string as NSString).range(of: text)
#expect(range.location != NSNotFound)
return document.attribute(key, at: range.location, effectiveRange: nil)
}
#expect(try attribute(NoteAttributes.block, "Title") as? String == "heading")
#expect(try attribute(NoteAttributes.headingLevel, "Title") as? Int == 1)
#expect(try attribute(NoteAttributes.bold, "bold") as? Bool == true)
#expect(try attribute(NoteAttributes.italic, "italic") as? Bool == true)
#expect(try attribute(NoteAttributes.bold, "both") as? Bool == true)
#expect(try attribute(NoteAttributes.italic, "both") as? Bool == true)
#expect(try attribute(.underlineStyle, "under") as? Int == NSUnderlineStyle.single.rawValue)
#expect(try attribute(.strikethroughStyle, "gone") as? Int == NSUnderlineStyle.single.rawValue)
#expect(try attribute(NoteAttributes.inlineCode, "let x") as? Bool == true)
#expect(try attribute(.link, "site") as? String == "https://example.com")
#expect(try attribute(NoteAttributes.block, "quote") as? String == "quote")
#expect(try attribute(NoteAttributes.checked, "todo") as? Bool == false)
#expect(try attribute(NoteAttributes.checked, "done") as? Bool == true)
#expect(try attribute(NoteAttributes.depth, "nested") as? Int == 1)
#expect(try attribute(NoteAttributes.number, "third") as? Int == 3)
#expect(try attribute(NoteAttributes.number, "fourth") as? Int == 4)
#expect(try attribute(NoteAttributes.block, "# literal") as? String == "code")
#expect(try attribute(NoteAttributes.language, "# literal") as? String == "swift")
#expect(try attribute(NoteAttributes.bold, "**code**") == nil)
let restored = NoteDocument.importMarkdown(NoteDocument.markdown(document))
#expect(restored.string == document.string)
}
@Test @MainActor
func markdownImportHandlesEveryHeadingEscapesAndUnsupportedLiteralContent() {
for level in 1...6 {
let document = NoteDocument.importMarkdown(NSAttributedString(string: String(repeating: "#", count: level) + " Heading"))
#expect(document.string == "Heading")
#expect(document.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == level)
}
let escaped = NoteDocument.importMarkdown(NSAttributedString(string: "\\*literal\\* & café 🦊"))
#expect(escaped.string == "*literal* & café 🦊")
#expect(escaped.attribute(NoteAttributes.italic, at: 0, effectiveRange: nil) == nil)
for literal in ["", "
literal **content**
", "| A | B |\n| - | - |\n| x | y |", "---"] {
#expect(NoteDocument.importMarkdown(NSAttributedString(string: literal)).string == literal)
}
}
@Test @MainActor
func markdownConversionPreservesNativeAttachmentsAndLiteralCode() throws {
let attachment = NSTextAttachment()
let source = NSMutableAttributedString(string: "**fox ")
source.append(NSAttributedString(attachment: attachment))
source.append(NSAttributedString(string: "**\n```text\n`literal` **not bold**\n```"))
let document = NoteDocument.importMarkdown(source)
#expect(document.string == "fox \u{fffc}\n`literal` **not bold**")
#expect(document.attribute(.attachment, at: 4, effectiveRange: nil) as? NSTextAttachment === attachment)
let markdown = NoteDocument.markdown(document)
#expect(markdown.string.contains("```text\n`literal` **not bold**\n```"))
let location = (markdown.string as NSString).range(of: "\u{fffc}").location
#expect(markdown.attribute(.attachment, at: location, effectiveRange: nil) as? NSTextAttachment === attachment)
}
@Test @MainActor
func markdownExportRecognizesNativeMarksAndEscapesLiteralSyntax() {
let native = NSMutableAttributedString(string: "bold italic under strike link literal *stars* #tag")
let text = native.string as NSString
native.addAttribute(.font, value: NSFont.boldSystemFont(ofSize: 14), range: text.range(of: "bold"))
native.addAttribute(.font, value: NSFontManager.shared.convert(NSFont.systemFont(ofSize: 14), toHaveTrait: .italicFontMask), range: text.range(of: "italic"))
native.addAttribute(.underlineStyle, value: 1, range: text.range(of: "under"))
native.addAttribute(.strikethroughStyle, value: 1, range: text.range(of: "strike"))
native.addAttribute(.link, value: URL(string: "https://example.com")!, range: text.range(of: "link"))
let output = NoteDocument.markdown(native)
#expect(output.string == "**bold** *italic* under ~~strike~~ [link]() literal \\*stars\\* \\#tag")
#expect(NoteDocument.importMarkdown(output).string == native.string)
native.addAttribute(NoteAttributes.bold, value: false, range: text.range(of: "bold"))
#expect(NoteDocument.markdown(native).string.hasPrefix("bold "))
}
@Test @MainActor
func structuredPackageRoundTripsAllSemanticAttributesIncludingFalseAndZero() throws {
let document = NSMutableAttributedString(attributedString: NoteDocument.importMarkdown(NSAttributedString(string: "###### Heading\n- [ ] todo\n 0. nested\n```swift\nlet x = 1\n```\n**bold** *italic* under ~~strike~~ `code` [link](https://example.com)")))
document.addAttributes([NoteAttributes.bold: false, NoteAttributes.italic: false, NoteAttributes.inlineCode: false, NoteAttributes.depth: 0], range: NSRange(location: 0, length: 1))
let wrapper = try NoteDocument.fileWrapper(document)
let serialized = try #require(wrapper.serializedRepresentation)
let received = try #require(FileWrapper(serializedRepresentation: serialized))
let restored = try NoteDocument.contents(from: received)
#expect(restored.string == document.string)
let keys: [NSAttributedString.Key] = [NoteAttributes.block, NoteAttributes.headingLevel, NoteAttributes.depth, NoteAttributes.number, NoteAttributes.checked, NoteAttributes.language, NoteAttributes.inlineCode, NoteAttributes.bold, NoteAttributes.italic, .underlineStyle, .strikethroughStyle, .link]
for index in 0.. quote\n[link](https://example.com)\nnext", "quote\nlink\nnext"),
("- item\n***both*** under\nlast", "item\nboth under\nlast"),
] {
let contents = NoteDocument.importMarkdown(NSAttributedString(string: source))
#expect(contents.string == expected)
}
let attachment = NSTextAttachment()
let source = NSMutableAttributedString(string: "# Heading\n- [ ] task\n**bold** and `code`\n")
source.append(NSAttributedString(attachment: attachment))
let contents = NoteDocument.importMarkdown(source)
#expect(contents.string == "Heading\ntask\nbold and code\n\u{fffc}")
#expect(contents.attribute(.attachment, at: contents.length - 1, effectiveRange: nil) as? NSTextAttachment === attachment)
let bold = (contents.string as NSString).range(of: "bold")
#expect(contents.attribute(NoteAttributes.bold, at: bold.location, effectiveRange: nil) as? Bool == true)
let code = (contents.string as NSString).range(of: "code")
#expect(contents.attribute(NoteAttributes.inlineCode, at: code.location, effectiveRange: nil) as? Bool == true)
}
@Test @MainActor
func markdownExportGroupsSemanticMarksAcrossDisplayRunsAndKeepsWhitespace() {
let contents = NSMutableAttributedString(string: "fox 🦊 \n")
let marked = NSRange(location: 0, length: (contents.string as NSString).range(of: " \n").location)
contents.addAttributes([.underlineStyle: 1, NoteAttributes.bold: false, NoteAttributes.italic: false], range: NSRange(location: 0, length: contents.length))
contents.addAttribute(.font, value: NSFont.systemFont(ofSize: 14), range: NSRange(location: 0, length: 4))
contents.addAttributes([.font: NSFont.systemFont(ofSize: 18), .foregroundColor: NSColor.red], range: NSRange(location: 4, length: marked.length - 4))
#expect(NoteDocument.markdown(contents).string == "fox 🦊 \n")
contents.addAttribute(NoteAttributes.bold, value: true, range: marked)
#expect(NoteDocument.markdown(contents).string == "**fox 🦊** \n")
for source in ["- [ ] todo \n", "first \nsecond \t\n", "trailing "] {
let imported = NoteDocument.importMarkdown(NSAttributedString(string: source))
let expected = source.hasPrefix("- [ ] ") ? String(source.dropFirst(6)) : source
#expect(imported.string == expected)
}
}