// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // @testable import ElementX import SwiftUI import Testing struct AttributedStringBuilderTests { private let attributedStringBuilder: AttributedStringBuilder private let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) init() async throws { attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder()) } @Test func renderHTMLStringWithHeaders() throws { let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.headers.rawValue), "Could not build the attributed string") #expect(String(attributedString.characters) == "H1 Header\nH2 Header\nH3 Header\nH4 Header\nH5 Header\nH6 Header") #expect(attributedString.runs.count == 4) // newlines hold no attributes let pointSizes = attributedString.runs.compactMap(\.uiKit.font?.pointSize) #expect(pointSizes == [23, 21, 19, 17]) } @Test func renderHTMLStringWithPreCode() throws { let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.code.rawValue), "Could not build the attributed string") #expect(attributedString.runs.first?.uiKit.font?.fontName == ".AppleSystemUIFontMonospaced-Regular") let string = String(attributedString.characters) let regex = try #require(try? NSRegularExpression(pattern: "\\R", options: []), "Could not build the regex for the test.") #expect(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)) == 23) } @Test func renderHTMLStringWithLink() throws { let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.links.rawValue), "Could not build the attributed string") #expect(String(attributedString.characters) == "Links too:\nMatrix rules! 🤘, beta.org, www.gamma.org, http://delta.org") let link = attributedString.runs.first { $0.link != nil }?.link #expect(link?.host == "www.alpha.org") } @Test func renderPlainStringWithLink() throws { let plainString = "This text contains a https://www.matrix.org link." let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string") #expect(String(attributedString.characters) == plainString) #expect(attributedString.runs.count == 3) let link = attributedString.runs.first { $0.link != nil }?.link #expect(link?.host == "www.matrix.org") } @Test func punctuationAtTheEndOfPlainStringLinks() throws { let plainString = "Most punctuation marks are removed https://www.matrix.org:;., but closing brackets are kept https://example.com/(test)" let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string") #expect(String(attributedString.characters) == plainString) #expect(attributedString.runs.count == 4) let firstLink = attributedString.runs.first { $0.link != nil }?.link #expect(firstLink == "https://www.matrix.org") let secondLink = attributedString.runs.last { $0.link != nil }?.link #expect(secondLink == "https://example.com/(test)") } @Test func linkDefaultScheme() throws { let plainString = "This text contains a matrix.org link." let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string") #expect(String(attributedString.characters) == plainString) #expect(attributedString.runs.count == 3) let link = attributedString.runs.first { $0.link != nil }?.link #expect(link == "https://matrix.org") } @Test func mailToLinks() throws { let plainString = "Linking to email addresses like stefan@matrix.org should work as well" let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string") let link = attributedString.runs.first { $0.link != nil }?.link #expect(link == "mailto:stefan@matrix.org") } @Test func renderHTMLStringWithLinkInHeader() throws { let h1HTMLString = "

Matrix.org

" let h2HTMLString = "

Matrix.org

" let h3HTMLString = "

Matrix.org

" let h1AttributedString = try #require(attributedStringBuilder.fromHTML(h1HTMLString), "Could not build the attributed string") let h2AttributedString = try #require(attributedStringBuilder.fromHTML(h2HTMLString), "Could not build the attributed string") let h3AttributedString = try #require(attributedStringBuilder.fromHTML(h3HTMLString), "Could not build the attributed string") let h1Font = try #require(h1AttributedString.runs.first?.uiKit.font, "Could not extract a font from the strings.") let h2Font = try #require(h2AttributedString.runs.first?.uiKit.font, "Could not extract a font from the strings.") let h3Font = try #require(h3AttributedString.runs.first?.uiKit.font, "Could not extract a font from the strings.") #expect(String(h1AttributedString.characters) == "Matrix.org") #expect(String(h2AttributedString.characters) == "Matrix.org") #expect(String(h3AttributedString.characters) == "Matrix.org") #expect(h1AttributedString.runs.count == 1) #expect(h2AttributedString.runs.count == 1) #expect(h3AttributedString.runs.count == 1) #expect(h1Font == h2Font) #expect(h2Font == h3Font) #expect(h1Font.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize) #expect(h1Font.pointSize <= 23) #expect(h1AttributedString.runs.first?.link?.host == "matrix.org") #expect(h2AttributedString.runs.first?.link?.host == "matrix.org") #expect(h3AttributedString.runs.first?.link?.host == "matrix.org") } @Test func renderHTMLStringWithIFrame() throws { let htmlString = "" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.uiKit.attachment == nil, "iFrame attachments should be removed as they're not included in the allowedHTMLTags array.") } @Test func linkWithFragment() throws { var string = "https://example.com/#/" try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: "https://example.com", expectedRuns: 1) try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: "https://example.com", expectedRuns: 1) string = "https://example.com/#/some_fragment/" try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: "https://example.com/#/some_fragment", expectedRuns: 1) try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: "https://example.com/#/some_fragment", expectedRuns: 1) } @Test func permalink() throws { let string = "https://matrix.to/#/!hello:matrix.org/$world?via=matrix.org" try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: string, expectedRuns: 1) try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: string, expectedRuns: 1) } @Test func matrixURI() throws { let string = "matrix:roomid/hello:matrix.org/e/world?via=matrix.org" try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: string, expectedRuns: 1) try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: string, expectedRuns: 1) } @Test func userIDLink() throws { let userID = "@user:matrix.org" let string = "The user is \(userID)." let expectedLink = "https://matrix.to/#/\(userID)" try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink, expectedRuns: 3) try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink, expectedRuns: 3) } @Test func roomAliasLink() throws { let roomAlias = "#room:matrix.org" let string = "The room is \(roomAlias)." let expectedLink = try #require(URL(string: "https://matrix.to/#/\(roomAlias)"), "The expected link should be valid.") try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3) try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3) } @Test func defaultFont() throws { let htmlString = "Test string " let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 3) } @Test func defaultForegroundColor() throws { let htmlString = "Test string link link" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 7) for run in attributedString.runs { #expect(run.uiKit.foregroundColor == nil) } } @Test func customForegroundColor() throws { // swiftlint:disable:next line_length let htmlString = "Rain www.matrix.org bow" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 3) var foundLink = false // Foreground colors should be completely stripped from the attributed string // letting UI components chose the defaults (e.g. tintColor) for run in attributedString.runs { if run.link != nil { #expect(run.link?.host == "www.matrix.org") #expect(run.uiKit.foregroundColor == nil) foundLink = true } else { #expect(run.uiKit.foregroundColor == nil) } } #expect(foundLink) } @Test func singleBlockquote() throws { let htmlString = "
Blockquote

Another paragraph

" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 2) #expect(attributedString.formattedComponents.count == 2) for run in attributedString.runs where run.elementX.blockquote ?? false { return } Issue.record("Couldn't find blockquote") #expect(String(attributedString.characters) == "Blockquote\nAnother paragraph") } // swiftlint:disable line_length @Test func blockquoteWithinText() throws { let htmlString = """ The text before the blockquote
For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally.
The text after the blockquote """ let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 3) #expect(attributedString.formattedComponents.count == 3) for run in attributedString.runs where run.elementX.blockquote ?? false { return } Issue.record("Couldn't find blockquote") } // swiftlint:enable line_length @Test func blockquoteWithLink() throws { let htmlString = "
Blockquote with a link in it
" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 3) let coalescedComponents = attributedString.formattedComponents #expect(coalescedComponents.count == 1) #expect(coalescedComponents.first?.attributedString.runs.count == 3, "Link not present in the component") var foundBlockquoteAndLink = false for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { foundBlockquoteAndLink = true } #expect(foundBlockquoteAndLink != nil, "Couldn't find blockquote or link") } @Test func replyBlockquote() throws { let htmlString = "
In reply to @user:matrix.org
The future is swift run tools 😎
" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") let coalescedComponents = attributedString.formattedComponents #expect(coalescedComponents.count == 1) let component = try #require(coalescedComponents.first, "Could not get the first component") #expect(component.type == .blockquote, "The reply quote should be a blockquote.") } @Test func multipleGroupedBlockquotes() throws { let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.groupedBlockQuotes.rawValue), "Could not build the attributed string") #expect(attributedString.runs.count == 11) #expect(attributedString.formattedComponents.count == 5) var numberOfBlockquotes = 0 for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { numberOfBlockquotes += 1 } #expect(numberOfBlockquotes == 3, "Couldn't find all the blockquotes") } @Test func multipleSeparatedBlockquotes() throws { let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.separatedBlockQuotes.rawValue), "Could not build the attributed string") let coalescedComponents = attributedString.formattedComponents #expect(attributedString.runs.count == 5) #expect(coalescedComponents.count == 5) var numberOfBlockquotes = 0 for run in attributedString.runs where run.elementX.blockquote ?? false { numberOfBlockquotes += 1 } #expect(numberOfBlockquotes == 2, "Couldn't find all the blockquotes") } @Test func userPermalinkMentionAtachment() { let string = "https://matrix.to/#/@test:matrix.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.userID == "@test:matrix.org") #expect(attributedStringFromHTML?.link?.absoluteString == string) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromPlain?.userID == "@test:matrix.org") #expect(attributedStringFromPlain?.link?.absoluteString == string) } @Test func userIDMentionAtachment() { let string = "@test:matrix.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.userID == "@test:matrix.org") #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/@test:matrix.org") let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromPlain?.userID == "@test:matrix.org") #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/@test:matrix.org") } @Test func roomIDPermalinkMentionAttachment() { let string = "https://matrix.to/#/!test:matrix.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.roomID == "!test:matrix.org") #expect(attributedStringFromHTML?.link?.absoluteString == string) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromHTML?.roomID == "!test:matrix.org") #expect(attributedStringFromPlain?.link?.absoluteString == string) } @Test func roomAliasPermalinkMentionAttachment() { let string = "https://matrix.to/#/#test:matrix.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org") #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org") let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org") #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org") } @Test func roomAliasMentionAttachment() { let string = "#test:matrix.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org") #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org") let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org") #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org") } @Test func eventRoomIDPermalinkMentionAttachment() { let string = "https://matrix.to/#/!test:matrix.org/$test" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.eventOnRoomID == .some(.init(roomID: "!test:matrix.org", eventID: "$test"))) #expect(attributedStringFromHTML?.link?.absoluteString == string) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromPlain?.eventOnRoomID == .some(.init(roomID: "!test:matrix.org", eventID: "$test"))) #expect(attributedStringFromPlain?.link?.absoluteString == string) } @Test func eventRoomAliasPermalinkMentionAttachment() { let string = "https://matrix.to/#/#test:matrix.org/$test" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) #expect(attributedStringFromHTML?.attachment != nil) #expect(attributedStringFromHTML?.eventOnRoomAlias == .some(.init(alias: "#test:matrix.org", eventID: "$test"))) #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org/$test") let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) #expect(attributedStringFromPlain?.attachment != nil) #expect(attributedStringFromPlain?.eventOnRoomAlias == .some(.init(alias: "#test:matrix.org", eventID: "$test"))) #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org/$test") } @Test func userMentionAtachmentInBlockQuotes() throws { let link = "https://matrix.to/#/@test:matrix.org" let string = "
hello \(link) how are you?
" let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil") for run in attributedStringFromHTML.runs { #expect(run.blockquote != nil) } try checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3) try checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: link, expectedRuns: 3) } @Test func allUsersMentionAtachmentInBlockQuotes() throws { let string = "
hello @room how are you?
" let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil") for run in attributedStringFromHTML.runs { #expect(run.blockquote != nil) } try checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3) } @Test func allUsersMentionAttachment() throws { let string = "@room" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) try checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) try checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1) let string2 = "Hello @room" let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2) try checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2) let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2) try checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 2) let string3 = "Hello @room how are you doing?" let attributedStringFromHTML3 = attributedStringBuilder.fromHTML(string3) try checkAttachment(attributedString: attributedStringFromHTML3, expectedRuns: 3) let attributedStringFromPlain3 = attributedStringBuilder.fromPlain(string3) try checkAttachment(attributedString: attributedStringFromPlain3, expectedRuns: 3) } @Test func linksHavePriorityOverAllUserMention() throws { let string = "https://test@room.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) try checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: string, expectedRuns: 1) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) try checkLinkIn(attributedString: attributedStringFromPlain, expectedLink: string, expectedRuns: 1) let string2 = "https://matrix.to/#/@roomusername:matrix.org" let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2) try checkLinkIn(attributedString: attributedStringFromHTML2, expectedLink: string2, expectedRuns: 1) try checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 1) let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2) try checkLinkIn(attributedString: attributedStringFromPlain2, expectedLink: string2, expectedRuns: 1) try checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 1) } @Test func uRLsAreIgnoredInCode() { var htmlString = "
test https://matrix.org test
" var attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString) #expect(attributedStringFromHTML?.runs.count == 1) #expect(attributedStringFromHTML?.link == nil) htmlString = "
matrix.org
" attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString) #expect(attributedStringFromHTML?.runs.count == 1) #expect(attributedStringFromHTML?.link == nil) } @Test func hyperlinksAreIgnoredInCode() { let htmlString = "
test matrix test
" let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString) #expect(attributedStringFromHTML?.link == nil) } @Test func userMentionIsIgnoredInCode() { let htmlString = "
test https://matrix.org/#/@test:matrix.org test
" let attributedString = attributedStringBuilder.fromHTML(htmlString) #expect(attributedString?.runs.count == 1) #expect(attributedString?.attachment == nil) } @Test func plainTextUserMentionIsIgnoredInCode() { let htmlString = "
Hey @some.user.ceriu:matrix.org
" let attributedString = attributedStringBuilder.fromHTML(htmlString) #expect(attributedString?.runs.count == 1) #expect(attributedString?.attachment == nil) } @Test func allUsersIsIgnoredInCode() { let htmlString = "
test @room test
" let attributedString = attributedStringBuilder.fromHTML(htmlString) #expect(attributedString?.runs.count == 1) #expect(attributedString?.attachment == nil) } @Test func multipleMentions() throws { let url = try #require(URL(string: "https://matrix.to/#/@test:matrix.org"), "Invalid url") let string = "Hello @room, but especially hello to you \(url)" let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil") var foundAttachments = 0 var foundLink: URL? for run in attributedStringFromHTML.runs { if run.attachment != nil { foundAttachments += 1 } if let link = run.link { foundLink = link } } #expect(foundLink == url) #expect(foundAttachments == 2) let attributedStringFromPlain = try #require(attributedStringBuilder.fromPlain(string), "Attributed string is nil") foundAttachments = 0 foundLink = nil for run in attributedStringFromPlain.runs { if run.attachment != nil { foundAttachments += 1 } if let link = run.link { foundLink = link } } #expect(foundLink == url) #expect(foundAttachments == 2) } @Test func multipleMentions2() throws { let url = try #require(URL(string: "https://matrix.to/#/@test:matrix.org"), "Invalid url") let string = "\(url) @room" let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil") var foundAttachments = 0 var foundLink: URL? for run in attributedStringFromHTML.runs { if run.attachment != nil { foundAttachments += 1 } if let link = run.link { foundLink = link } } #expect(foundLink == url) #expect(foundAttachments == 2) let attributedStringFromPlain = try #require(attributedStringBuilder.fromPlain(string), "Attributed string is nil") foundAttachments = 0 foundLink = nil for run in attributedStringFromPlain.runs { if run.attachment != nil { foundAttachments += 1 } if let link = run.link { foundLink = link } } #expect(foundLink == url) #expect(foundAttachments == 2) } @Test func imageTags() throws { let htmlString = "Hey \"Smiley! How's work?" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey [img: Smiley face]! How's work[img]?") } @Test func listTags() throws { let htmlString = "

like

\n\n" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "like\n • this\ntest") } @Test func unorderedList() throws { let htmlString = "" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == " • 1\n • 2\n • 3") } @Test func nestedUnorderedList() throws { let htmlString = "" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == " • A\n • A1\n • A2\n • A3\n • B\n • C") } @Test func orderedList() throws { let htmlString = "
  1. 1
  2. 2
  3. 3
" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == " 1. 1\n 2. 2\n 3. 3") } @Test func nestedOrderedList() throws { let htmlString = "
  1. A
    1. A1
    2. A2
    3. A3
  2. B
  3. C
" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == " 1. A\n 1. A1\n 2. A2\n 3. A3\n 2. B\n 3. C") } @Test func outOfOrderListNubmering() throws { let htmlString = "
    \n
  1. this is a two
  2. \n
" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == " 2. this is a two") } @Test func nestedHeterogeneousLists() throws { let htmlString = "
  1. A
  2. B
  3. C
" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == " 1. A\n • A1\n • A2\n • A3\n 2. B\n 3. C") } /// https://github.com/element-hq/element-x-ios/issues/4856 @Test func normalisedWhitespaces() throws { let html = """ Stefan pushed 2 commits to main: """ let attributedString = try #require(attributedStringBuilder.fromHTML(html), "Could not build the attributed string") #expect(String(attributedString.characters) == "Stefan pushed 2 commits to main:\n • Some update \n • Some other update") } // MARK: - Phishing prevention @Test func phishingLink() throws { let htmlString = "Hey check the following link https://element.io" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following link https://element.io") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "https://element.io") } @Test func validLink() throws { let htmlString = "Hey check the following link" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(!link.requiresConfirmation) #expect(link.absoluteString == "https://matrix.org") } @Test func validLinkWithRTLOverride() throws { let htmlString = "\u{202E}https://matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(!link.requiresConfirmation) #expect(link.absoluteString == "https://matrix.org") } @Test func phishingUserID() throws { let htmlString = "Hey check the following user @alice:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following user @alice:matrix.org") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "@alice:matrix.org") } @Test func validUserIDLink() throws { let htmlString = "Hey check the following user @alice:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") try checkAttachment(attributedString: attributedString, expectedRuns: 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(!link.requiresConfirmation) #expect(link.absoluteString == "https://matrix.to/#/@alice:matrix.org") } @Test func phishingUserIDWithAnotherUserIDPermalink() throws { let htmlString = "Hey check the following user @alice:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following user @alice:matrix.org") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.to/#/@bob:matrix.org") #expect(link.confirmationParameters?.displayString == "@alice:matrix.org") } @Test func phishingUserIDWithDistractingCharacters() throws { let htmlString = "Hey check the following user 👉️ @alice:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following user 👉️ @alice:matrix.org") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "👉️ @alice:matrix.org") } @Test func phishingLinkWithDistractingCharacters() throws { let htmlString = "Hey check the following link 👉️ https://element.io" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following link 👉️ https://element.io") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "👉️ https://element.io") } @Test func validLinkWithDistractingCharacters() throws { let htmlString = "Hey check the following link 👉️ https://element.io" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following link 👉️ https://element.io") let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(!link.requiresConfirmation) #expect(link.absoluteString == "https://element.io") } @Test func phishingLinkWithFakeDotCharacter() throws { let htmlString = "Hey check the following link https://element﹒io" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following link https://element﹒io") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "https://element﹒io") } @Test func phishingMatrixPermalinks() throws { let htmlString = "Hey check the following room https://matrix.to/#/#beautiful-room:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.to/#/%23offensive-room:matrix.org") #expect(link.confirmationParameters?.displayString == "https://matrix.to/#/#beautiful-room:matrix.org") } @Test func validMatrixPermalinks() throws { let htmlString = "Hey check the following room https://matrix.to/#/#beautiful-room:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") try checkAttachment(attributedString: attributedString, expectedRuns: 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(!link.requiresConfirmation) #expect(link.absoluteString == "https://matrix.to/#/%23beautiful-room:matrix.org") } @Test func phishingRoomAlias() throws { let htmlString = "Hey check the following room #room:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following room #room:matrix.org") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "#room:matrix.org") } @Test func validRoomAliasLink() throws { let htmlString = "Hey check the following user #room:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") try checkAttachment(attributedString: attributedString, expectedRuns: 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(!link.requiresConfirmation) #expect(link.absoluteString == "https://matrix.to/#/%23room:matrix.org") } @Test func phishingRoomAliasWithAnotherRoomAliasPermalink() throws { let htmlString = "Hey check the following room #room:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following room #room:matrix.org") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.to/#/%23another-room:matrix.org") #expect(link.confirmationParameters?.displayString == "#room:matrix.org") } @Test func roomAliasWithDistractingCharacters() throws { let htmlString = "Hey check the following user 👉️ #room:matrix.org" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "Hey check the following user 👉️ #room:matrix.org") #expect(attributedString.runs.count == 2) let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link") #expect(link.requiresConfirmation) #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org") #expect(link.confirmationParameters?.displayString == "👉️ #room:matrix.org") } @Test func mxExternalPaymentDetailsRemoved() throws { var htmlString = "This is visible. But this is hidden and this link too" let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString.characters) == "This is visible.") for run in attributedString.runs where run.link != nil { Issue.record("No link expected, but found one") return } htmlString = "This is visible. And this text and link are visible too." let attributedString2 = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string") #expect(String(attributedString2.characters) == "This is visible. And this text and link are visible too.") try #require(attributedString2.runs.first { $0.link != nil }?.link, "Couldn't find the link") } // MARK: - Private private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) throws { let attributedString = try #require(attributedString, "Could not build the attributed string") #expect(attributedString.runs.count == expectedRuns) for run in attributedString.runs where run.link != nil { #expect(run.link?.absoluteString == expectedLink) return } Issue.record("Couldn't find expected value.") } private func checkAttachment(attributedString: AttributedString?, expectedRuns: Int) throws { let attributedString = try #require(attributedString, "Could not build the attributed string") #expect(attributedString.runs.count == expectedRuns) for run in attributedString.runs where run.attachment != nil { return } Issue.record("Couldn't find expected value.") } }