diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 66e83e8ea..1d8350cdf 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -313,7 +313,6 @@ 36CD6E11B37396E14F032CB6 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 36DE961B784087D5E18EF9BA /* LogViewerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A07692536D66E3DA32C4964 /* LogViewerScreen.swift */; }; 370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */; }; - 37276FF5C194F87EDB785DF1 /* AttributedStringBuilderV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37201D4C9718D398A7D3A020 /* AttributedStringBuilderV1.swift */; }; 377980ABF16525114E72DDE2 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D2330417C6BE2F6872FFF96B /* SwiftSoup */; }; 37906355E207DB5703754675 /* AppLockSetupBiometricsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F893F4A111CB7BA5C96949 /* AppLockSetupBiometricsScreenViewModel.swift */; }; 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; }; @@ -355,7 +354,6 @@ 3E7B65C2C97748D5D65AAA8B /* NotificationPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */; }; 3EC5A41F9FB7DD63A4DC6144 /* RoomChangeRolesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14B1DE3E2D5D26732C49036 /* RoomChangeRolesScreenViewModel.swift */; }; 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893777A4997BBDB68079D4F5 /* ArrayTests.swift */; }; - 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; 3F2148F11164C7C5609984EB /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 2B788C81F6369D164ADEB917 /* GZIP */; }; 3F327A62D233933F54F0F33A /* SwiftOGG in Frameworks */ = {isa = PBXBuildFile; productRef = 3FE40E79C36E7903121E6E3B /* SwiftOGG */; }; 3F55721B5C08E8D9F1295592 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; }; @@ -1011,7 +1009,6 @@ B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */; }; B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; }; - B5618E3C948584E5C1F67033 /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; B5899F18AD6C56CE08FE532B /* RoomSummaryProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */; }; B5BCE012F9E7C45D1C76108E /* RoomMembersListScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */; }; B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; }; @@ -1049,7 +1046,6 @@ BB9B800C6094E34860E89DC5 /* AppLockSetupBiometricsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */; }; BC1222EDFF0C240F14259315 /* BloomModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D152423EE6CF0ECCC84091A /* BloomModifier.swift */; }; BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */; }; - BC859868970A38FF72DFD461 /* AttributedStringBuilderV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37201D4C9718D398A7D3A020 /* AttributedStringBuilderV1.swift */; }; BCA5E2157CE27AB6F1D043D3 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 5A8EF1A5F9629FCA309D4B2A /* AsyncAlgorithms */; }; BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1DDB2293A51EA4C2739351 /* RoomListFiltersEmptyStateView.swift */; }; @@ -1096,7 +1092,6 @@ C49FCC766673006B6D299F1C /* RoomDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */; }; C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */; }; C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */; }; - C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */; }; @@ -1147,7 +1142,6 @@ CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E97E9615A158C76B2AB77 /* DateTests.swift */; }; CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; }; CDAE3A37D4DF136F9D07DB61 /* RoomChangeRolesScreenSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF710CB1C31F8938EAA3A7D /* RoomChangeRolesScreenSection.swift */; }; - CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE3B7FC34FB2C279AAA5EA01 /* AVMetadataMachineReadableCodeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */; }; CE4B342F9DD747CF4BEDB5AB /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43F773904F87FF5ADFE4DD1 /* TestablePreview.swift */; }; @@ -1672,7 +1666,6 @@ 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = ""; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = ""; }; - 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = ""; }; 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleReactionLayout.swift; sourceTree = ""; }; 1F8C01DEEA83903D45069BBD /* CharacterSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterSet.swift; sourceTree = ""; }; @@ -1741,7 +1734,6 @@ 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenCoordinator.swift; sourceTree = ""; }; 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = ""; }; - 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; 2A95C9B8299A36A6495DECA6 /* TracingHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingHook.swift; sourceTree = ""; }; 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = ""; }; 2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelTests.swift; sourceTree = ""; }; @@ -1803,7 +1795,6 @@ 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; - 37201D4C9718D398A7D3A020 /* AttributedStringBuilderV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderV1.swift; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = ""; }; @@ -5121,11 +5112,8 @@ 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */ = { isa = PBXGroup; children = ( - 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */, 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */, - 37201D4C9718D398A7D3A020 /* AttributedStringBuilderV1.swift */, F254AD274D6973B840FEAF0B /* AttributedStringBuilderV2.swift */, - 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */, C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */, 27A5FBB74981BF8EFFDAE90D /* HTMLFixtures.swift */, AB07F03461023BC39C730922 /* PhishingDetector.swift */, @@ -7238,9 +7226,7 @@ E1DE8A5422643F759385290F /* AppSettings.swift in Sources */, F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */, 484202C5D50983442D24D061 /* AttributedString.swift in Sources */, - CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */, BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */, - 37276FF5C194F87EDB785DF1 /* AttributedStringBuilderV1.swift in Sources */, AB8F31096ECDD966C4CF0F14 /* AttributedStringBuilderV2.swift in Sources */, F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */, 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, @@ -7248,7 +7234,6 @@ 238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */, 0BAF83521871E69D222EE8E4 /* ClientBuilderHook.swift in Sources */, 211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */, - B5618E3C948584E5C1F67033 /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, 7CD05B18A432060E4770FBD8 /* DataProtectionManager.swift in Sources */, 24A75F72EEB7561B82D726FD /* Date.swift in Sources */, FE9A5D4715C7AB68682C030C /* Dictionary.swift in Sources */, @@ -7607,9 +7592,7 @@ 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */, 273AB64B9A26B61C51858867 /* AsyncSequence.swift in Sources */, D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */, - 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */, A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */, - BC859868970A38FF72DFD461 /* AttributedStringBuilderV1.swift in Sources */, 16148F9B450F4357CE5E7CEE /* AttributedStringBuilderV2.swift in Sources */, 2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */, F3F38062C6CA21CF403C5C90 /* AudioConverterProtocol.swift in Sources */, @@ -7719,7 +7702,6 @@ 565868808A1DA565707394ED /* CurrentValuePublisher.swift in Sources */, A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */, 12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */, - C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, 9905C1B1C6EFE38F3A6533F3 /* Data.swift in Sources */, C6C06DDA8881260303FBA3A0 /* Date.swift in Sources */, 99C463F12F89C99C77D02077 /* DeactivateAccountScreen.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index bc8a9ecd4..20bc87597 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -160,12 +160,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } .store(in: &cancellables) - appSettings.$nextGenHTMLParserEnabled - .sink { value in - AttributedStringBuilder.useNextGenHTMLParser = value - } - .store(in: &cancellables) - elementCallService.actions .receive(on: DispatchQueue.main) .sink { [weak self] action in diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 348ebe0ec..e9b3bf146 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -63,7 +63,6 @@ final class AppSettings { case knockingEnabled case threadsEnabled case developerOptionsEnabled - case nextGenHTMLParserEnabled case linkPreviewsEnabled case latestEventSorterEnabled @@ -389,10 +388,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store)) var threadsEnabled - - @UserPreference(key: UserDefaultsKeys.nextGenHTMLParserEnabled, defaultValue: true, storageType: .userDefaults(store)) - var nextGenHTMLParserEnabled - + @UserPreference(key: UserDefaultsKeys.linkPreviewsEnabled, defaultValue: false, storageType: .userDefaults(store)) var linkPreviewsEnabled diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift deleted file mode 100644 index 332c82d74..000000000 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// 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. -// - -import Compound -import DTCoreText -import Foundation -import LRUCache -import MatrixRustSDK - -protocol MentionBuilderProtocol { - func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) - func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String) - func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String, roomDisplayName: String?) - func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String) - func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String) - func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) -} - -extension NSAttributedString.Key { - static let DTTextBlocks: NSAttributedString.Key = .init(rawValue: DTTextBlocksAttribute) - static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name) - static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name) - static let MatrixUserDisplayName: NSAttributedString.Key = .init(rawValue: UserDisplayNameAttribute.name) - static let MatrixRoomDisplayName: NSAttributedString.Key = .init(rawValue: RoomDisplayNameAttribute.name) - static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name) - static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name) - static let MatrixEventOnRoomID: NSAttributedString.Key = .init(rawValue: EventOnRoomIDAttribute.name) - static let MatrixEventOnRoomAlias: NSAttributedString.Key = .init(rawValue: EventOnRoomAliasAttribute.name) - static let MatrixAllUsersMention: NSAttributedString.Key = .init(rawValue: AllUsersMentionAttribute.name) - static let CodeBlock: NSAttributedString.Key = .init(rawValue: CodeBlockAttribute.name) -} - -struct AttributedStringBuilder: AttributedStringBuilderProtocol { - private static let defaultKey = "default" - - private let builder: AttributedStringBuilderProtocol - - static var useNextGenHTMLParser = false - - static func invalidateCaches() { - AttributedStringBuilderV1.invalidateCaches() - AttributedStringBuilderV2.invalidateCaches() - } - - init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) { - if Self.useNextGenHTMLParser { - builder = AttributedStringBuilderV2(cacheKey: cacheKey, mentionBuilder: mentionBuilder) - } else { - builder = AttributedStringBuilderV1(cacheKey: cacheKey, mentionBuilder: mentionBuilder) - } - } - - func fromPlain(_ string: String?) -> AttributedString? { - builder.fromPlain(string) - } - - func fromHTML(_ htmlString: String?) -> AttributedString? { - builder.fromHTML(htmlString) - } - - func addMatrixEntityPermalinkAttributesTo(_ attributedString: NSMutableAttributedString) { - builder.addMatrixEntityPermalinkAttributesTo(attributedString) - } -} diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV1.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV1.swift deleted file mode 100644 index 403a381bf..000000000 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV1.swift +++ /dev/null @@ -1,399 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 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. -// - -import Compound -import DTCoreText -import LRUCache -import MatrixRustSDK -import UIKit - -struct AttributedStringBuilderV1: AttributedStringBuilderProtocol { - private let cacheKey: String - private let temporaryBlockquoteMarkingColor = UIColor.magenta - private let temporaryCodeBlockMarkingColor = UIColor.cyan - private let mentionBuilder: MentionBuilderProtocol - - private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.attributed_string_builder_cache") - private static var caches: [String: LRUCache] = [:] - - static func invalidateCaches() { - caches.removeAll() - } - - init(cacheKey: String, mentionBuilder: MentionBuilderProtocol) { - self.cacheKey = cacheKey - self.mentionBuilder = mentionBuilder - } - - func fromPlain(_ string: String?) -> AttributedString? { - guard let string else { - return nil - } - - if let cached = Self.cachedValue(forKey: string, cacheKey: cacheKey) { - return cached - } - - let mutableAttributedString = NSMutableAttributedString(string: string) - removeDefaultForegroundColors(mutableAttributedString) - addLinksAndMentions(mutableAttributedString) - addMatrixEntityPermalinkAttributesTo(mutableAttributedString) - - let result = try? AttributedString(mutableAttributedString, including: \.elementX) - Self.cacheValue(result, forKey: string, cacheKey: cacheKey) - - return result - } - - // Do not use the default HTML renderer of NSAttributedString because this method - // runs on the UI thread which we want to avoid because renderHTMLString is called - // most of the time from a background thread. - // Use DTCoreText HTML renderer instead. - // Using DTCoreText, which renders static string, helps to avoid code injection attacks - // that could happen with the default HTML renderer of NSAttributedString which is a - // webview. - func fromHTML(_ htmlString: String?) -> AttributedString? { - guard let originalHTMLString = htmlString else { - return nil - } - - if let cached = Self.cachedValue(forKey: originalHTMLString, cacheKey: cacheKey) { - return cached - } - - let htmlString = originalHTMLString.replacingHtmlBreaksOccurrences() - - guard let data = htmlString.data(using: .utf8) else { - return nil - } - - let defaultFont = UIFont.preferredFont(forTextStyle: .body) - - let parsingOptions: [String: Any] = [ - DTUseiOS6Attributes: true, - DTDefaultFontFamily: defaultFont.familyName, - DTDefaultFontName: defaultFont.fontName, - DTDefaultFontSize: defaultFont.pointSize, - DTDefaultStyleSheet: DTCSSStylesheet(styleBlock: defaultCSS) as Any, - DTDefaultLinkDecoration: false - ] - - guard let builder = DTHTMLAttributedStringBuilder(html: data, options: parsingOptions, documentAttributes: nil) else { - return nil - } - - builder.willFlushCallback = { element in - element?.sanitize(font: defaultFont) - } - - guard let attributedString = builder.generatedAttributedString() else { - return nil - } - - let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) - removeDefaultForegroundColors(mutableAttributedString) - detectPhishingAttempts(mutableAttributedString) - addLinksAndMentions(mutableAttributedString) - replaceMarkedBlockquotes(mutableAttributedString) - replaceMarkedCodeBlocks(mutableAttributedString) - addMatrixEntityPermalinkAttributesTo(mutableAttributedString) - removeDTCoreTextArtifacts(mutableAttributedString) - - let result = try? AttributedString(mutableAttributedString, including: \.elementX) - Self.cacheValue(result, forKey: originalHTMLString, cacheKey: cacheKey) - - return result - } - - // MARK: - Private - - private static func cacheValue(_ value: AttributedString?, forKey key: String, cacheKey: String) { - cacheDispatchQueue.sync { - if caches[cacheKey] == nil { - caches[cacheKey] = LRUCache(countLimit: 1000) - } - - caches[cacheKey]?.setValue(value, forKey: key) - } - } - - private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? { - var result: AttributedString? - cacheDispatchQueue.sync { - result = caches[cacheKey]?.value(forKey: key) - } - - return result - } - - private func removeDefaultForegroundColors(_ attributedString: NSMutableAttributedString) { - attributedString.removeAttribute(.foregroundColor, range: .init(location: 0, length: attributedString.length)) - } - - // swiftlint:disable:next cyclomatic_complexity - private func addLinksAndMentions(_ attributedString: NSMutableAttributedString) { - let string = attributedString.string - - // Event identifiers and room aliases and identifiers detected in plain text are techincally incomplete - // without via parameters and we won't bother detecting them - - var matches: [TextParsingMatch] = MatrixEntityRegex.userIdentifierRegex.matches(in: string).compactMap { match in - guard let matchRange = Range(match.range, in: string) else { - return nil - } - - let identifier = String(string[matchRange]) - - return TextParsingMatch(type: .userID(identifier: identifier), range: match.range) - } - - matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string).compactMap { match in - guard let matchRange = Range(match.range, in: string) else { - return nil - } - - let alias = String(string[matchRange]) - - return TextParsingMatch(type: .roomAlias(alias: alias), range: match.range) - }) - - matches.append(contentsOf: MatrixEntityRegex.uriRegex.matches(in: string).compactMap { match in - guard let matchRange = Range(match.range, in: string) else { - return nil - } - - let uri = String(string[matchRange]) - - return TextParsingMatch(type: .matrixURI(uri: uri), range: match.range) - }) - - matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string).compactMap { match in - guard let matchRange = Range(match.range, in: string) else { - return nil - } - - let link = String(string[matchRange]).asSanitizedLink - return TextParsingMatch(type: .link(urlString: link), range: match.range) - }) - - matches.append(contentsOf: MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string).map { match in - TextParsingMatch(type: .atRoom, range: match.range) - }) - - guard matches.count > 0 else { - return - } - - // Sort the links by length so the longest one always takes priority - matches.sorted { $0.range.length > $1.range.length }.forEach { [attributedString] match in - var hasLink = false - attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in - if value != nil { - hasLink = true - stop.pointee = true - } - } - - if hasLink { - return - } - - // Don't add any extra attributes within codeblocks - if attributedString.attribute(.backgroundColor, at: match.range.location, effectiveRange: nil) as? UIColor == temporaryCodeBlockMarkingColor { - return - } - - switch match.type { - case .atRoom: - attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range) - case .roomAlias(let alias): - if let urlString = try? matrixToRoomAliasPermalink(roomAlias: alias), - let url = URL(string: urlString) { - attributedString.addAttribute(.link, value: url, range: match.range) - } - case .matrixURI(let uri): - if let url = URL(string: uri) { - attributedString.addAttribute(.link, value: url, range: match.range) - } - case .userID, .link: - if let url = match.link { - attributedString.addAttribute(.link, value: url, range: match.range) - } - } - } - } - - private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) { - // According to blockquotes in the string, DTCoreText can apply 2 policies: - // - define a `DTTextBlocksAttribute` attribute on a
block - // - or, just define a `NSBackgroundColorAttributeName` attribute - attributedString.enumerateAttribute(.DTTextBlocks, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in - guard let value = value as? NSArray, - let dtTextBlock = value.firstObject as? DTTextBlock, - dtTextBlock.backgroundColor == temporaryBlockquoteMarkingColor else { - return - } - - attributedString.addAttribute(.MatrixBlockquote, value: true, range: range) - } - - attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in - guard let value = value as? UIColor, - value == temporaryBlockquoteMarkingColor else { - return - } - - attributedString.removeAttribute(.backgroundColor, range: range) - attributedString.addAttribute(.MatrixBlockquote, value: true, range: range) - } - } - - private func replaceMarkedCodeBlocks(_ attributedString: NSMutableAttributedString) { - attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in - if let value = value as? UIColor, - value == temporaryCodeBlockMarkingColor { - attributedString.addAttribute(.backgroundColor, value: UIColor.compound._bgCodeBlock as Any, range: range) - attributedString.removeAttribute(.link, range: range) - } - } - } - - func addMatrixEntityPermalinkAttributesTo(_ attributedString: NSMutableAttributedString) { - attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in - if value != nil { - if let url = value as? URL, - let matrixEntity = parseMatrixEntityFrom(uri: url.absoluteString) { - switch matrixEntity.id { - case .user(let userID): - mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: userID, userDisplayName: nil) - case .room(let roomID): - mentionBuilder.handleRoomIDMention(for: attributedString, in: range, url: url, roomID: roomID) - case .roomAlias(let alias): - mentionBuilder.handleRoomAliasMention(for: attributedString, in: range, url: url, roomAlias: alias, roomDisplayName: nil) - case .eventOnRoomId(let roomID, let eventID): - mentionBuilder.handleEventOnRoomIDMention(for: attributedString, in: range, url: url, eventID: eventID, roomID: roomID) - case .eventOnRoomAlias(let alias, let eventID): - mentionBuilder.handleEventOnRoomAliasMention(for: attributedString, in: range, url: url, eventID: eventID, roomAlias: alias) - } - } - } - } - - attributedString.enumerateAttribute(.MatrixAllUsersMention, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in - if let value = value as? Bool, - value { - mentionBuilder.handleAllUsersMention(for: attributedString, in: range) - } - } - } - - private func detectPhishingAttempts(_ attributedString: NSMutableAttributedString) { - attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in - guard value != nil, let internalURL = value as? URL else { - return - } - let displayString = attributedString.attributedSubstring(from: range).string - - guard PhishingDetector.isPhishingAttempt(displayString: displayString, internalURL: internalURL) else { - return - } - handlePhishingAttempt(for: attributedString, in: range, internalURL: internalURL, displayString: displayString) - } - } - - private func handlePhishingAttempt(for attributedString: NSMutableAttributedString, - in range: NSRange, - internalURL: URL, - displayString: String) { - // Let's remove the existing link attribute - attributedString.removeAttribute(.link, range: range) - - var urlComponents = URLComponents() - urlComponents.scheme = URL.confirmationScheme - urlComponents.host = "" - let parameters = ConfirmURLParameters(internalURL: internalURL, displayString: displayString) - urlComponents.queryItems = parameters.urlQueryItems - - guard let finalURL = urlComponents.url else { - return - } - - attributedString.addAttribute(.link, value: finalURL, range: range) - } - - private func removeDTCoreTextArtifacts(_ attributedString: NSMutableAttributedString) { - guard attributedString.length > 0 else { - return - } - - // DTCoreText adds a newline at the end of plain text ( https://github.com/Cocoanetics/DTCoreText/issues/779 ) - // or after a blockquote section. - // Trim trailing whitespace and newlines in the string content - while (attributedString.string as NSString).hasSuffixCharacter(from: .whitespacesAndNewlines) { - attributedString.deleteCharacters(in: .init(location: attributedString.length - 1, length: 1)) - } - } - - private var defaultCSS: String { - """ - blockquote { - background: \(temporaryBlockquoteMarkingColor.toHexString()); - display: block; - } - pre,code { - background-color: \(temporaryCodeBlockMarkingColor.toHexString()); - display: inline; - white-space: pre; - font-size: 0.9em; - -coretext-fontname: .AppleSystemUIFontMonospaced-Regular; - } - h1,h2,h3 { - font-size: 1.2em; - } - """ - } -} - -private extension UIColor { - func toHexString() -> String { - var red: CGFloat = 0.0 - var green: CGFloat = 0.0 - var blue: CGFloat = 0.0 - var alpha: CGFloat = 0.0 - - getRed(&red, green: &green, blue: &blue, alpha: &alpha) - - let rgb = Int(red * 255) << 16 | Int(green * 255) << 8 | Int(blue * 255) << 0 - - return NSString(format: "#%06x", rgb) as String - } -} - -private struct TextParsingMatch { - enum MatchType { - case userID(identifier: String) - case roomAlias(alias: String) - case matrixURI(uri: String) - case link(urlString: String) - case atRoom - } - - let type: MatchType - let range: NSRange - - var link: URL? { - switch type { - case .userID(let identifier): - return try? URL(string: matrixToUserPermalink(userId: identifier)) - case .link(let urlString): - return URL(string: urlString) - default: - return nil - } - } -} diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV2.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV2.swift index 567ca43dc..80fb8419c 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV2.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderV2.swift @@ -12,7 +12,31 @@ import MatrixRustSDK import SwiftSoup import UIKit -struct AttributedStringBuilderV2: AttributedStringBuilderProtocol { +protocol MentionBuilderProtocol { + func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) + func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String) + func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String, roomDisplayName: String?) + func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String) + func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String) + func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) +} + +extension NSAttributedString.Key { + static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name) + static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name) + static let MatrixUserDisplayName: NSAttributedString.Key = .init(rawValue: UserDisplayNameAttribute.name) + static let MatrixRoomDisplayName: NSAttributedString.Key = .init(rawValue: RoomDisplayNameAttribute.name) + static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name) + static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name) + static let MatrixEventOnRoomID: NSAttributedString.Key = .init(rawValue: EventOnRoomIDAttribute.name) + static let MatrixEventOnRoomAlias: NSAttributedString.Key = .init(rawValue: EventOnRoomAliasAttribute.name) + static let MatrixAllUsersMention: NSAttributedString.Key = .init(rawValue: AllUsersMentionAttribute.name) + static let CodeBlock: NSAttributedString.Key = .init(rawValue: CodeBlockAttribute.name) +} + +struct AttributedStringBuilder: AttributedStringBuilderProtocol { + private static let defaultKey = "default" + private let cacheKey: String private let mentionBuilder: MentionBuilderProtocol @@ -24,7 +48,7 @@ struct AttributedStringBuilderV2: AttributedStringBuilderProtocol { caches.removeAll() } - init(cacheKey: String, mentionBuilder: MentionBuilderProtocol) { + init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) { self.cacheKey = cacheKey self.mentionBuilder = mentionBuilder } @@ -475,3 +499,15 @@ private extension NSMutableAttributedString { } } } + +private extension NSString { + func hasSuffixCharacter(from characterSet: CharacterSet) -> Bool { + if length == 0 { + return false + } + + let lastChar = character(at: length - 1) + + return (characterSet as NSCharacterSet).characterIsMember(lastChar) + } +} diff --git a/ElementX/Sources/Other/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift deleted file mode 100644 index 1f6e767b1..000000000 --- a/ElementX/Sources/Other/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// 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. -// - -import DTCoreText -import Foundation - -public extension DTHTMLElement { - /// Sanitize the element using the given parameters. - /// - Parameters: - /// - font: The default font to use when resetting the content of any unsupported tags. - @objc func sanitize(font: UIFont) { - if let name, !Self.allowedHTMLTags.contains(name) { - // This is an unsupported tag. - // Remove any attachments to fix rendering. - textAttachment = nil - - // Handle special case for span with data-mx-external-payment-details - // This could be based on Storefront.current.countryCode to show the link - // content in unrestricted countries. e.g. currently USA - if name == "span", - let attributes = attributes as? [String: String], - attributes["data-msc4286-external-payment-details"] != nil { - parent.removeChildNode(self) - return - } - - // If the element has plain text content show that, - // otherwise prevent the tag from displaying. - if let stringContent = attributedString()?.string, - !stringContent.isEmpty, - let element = DTTextHTMLElement(name: nil, attributes: nil) { - element.setText(stringContent) - removeAllChildNodes() - addChildNode(element) - - if let parent = parent() { - element.inheritAttributes(from: parent) - } else { - fontDescriptor = DTCoreTextFontDescriptor() - fontDescriptor.fontFamily = font.familyName - fontDescriptor.fontName = font.fontName - fontDescriptor.pointSize = font.pointSize - paragraphStyle = DTCoreTextParagraphStyle.default() - - element.inheritAttributes(from: self) - } - element.interpretAttributes() - - } else if let parent = parent() { - parent.removeChildNode(self) - } else { - didOutput = true - } - - } else { - // This element is a supported tag, but it may contain children that aren't, - // so santize all child nodes to ensure correct tags. - if let childNodes = childNodes as? [DTHTMLElement] { - childNodes.forEach { $0.sanitize(font: font) } - } - } - } - - private static let allowedHTMLTags = ["font", // custom to matrix for IRC-style font coloring - "del", // for markdown - "body", // added internally by DTCoreText - "mx-reply", - "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", - "nl", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div", - "table", "thead", "caption", "tbody", "tr", "th", "td", "pre"] -} diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index f2d424c31..bf4ce9bb3 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -53,7 +53,6 @@ protocol DeveloperOptionsProtocol: AnyObject { var knockingEnabled: Bool { get set } var latestEventSorterEnabled: Bool { get set } - var nextGenHTMLParserEnabled: Bool { get set } var linkPreviewsEnabled: Bool { get set } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 23853876b..5b6c9d9ed 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -33,12 +33,6 @@ struct DeveloperOptionsScreen: View { } } - Section("General") { - Toggle(isOn: $context.nextGenHTMLParserEnabled) { - Text("Next gen HTML parsing") - } - } - Section("Room List") { Toggle(isOn: $context.publicSearchEnabled) { Text("Public search") diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift index c0ee0a971..5ebafb3c3 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift @@ -135,13 +135,8 @@ struct FormattedBodyText: View { struct FormattedBodyText_Previews: PreviewProvider, TestablePreview { static var previews: some View { - body(AttributedStringBuilderV1(cacheKey: "v1", mentionBuilder: MentionBuilder())) + body(AttributedStringBuilder(cacheKey: "FormattedBodyText", mentionBuilder: MentionBuilder())) .previewLayout(.sizeThatFits) - .previewDisplayName("v1") - - body(AttributedStringBuilderV2(cacheKey: "v2", mentionBuilder: MentionBuilder())) - .previewLayout(.sizeThatFits) - .previewDisplayName("v2") } @ViewBuilder diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index 64eaf1a0e..d561b721a 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -9,19 +9,7 @@ @testable import ElementX import XCTest -class AttributedStringBuilderV2Tests: AttributedStringBuilderV1Tests { - override func setUp() async throws { - AttributedStringBuilder.useNextGenHTMLParser = true - try await super.setUp() - } - - override func tearDown() async throws { - AttributedStringBuilder.useNextGenHTMLParser = false - try await super.tearDown() - } -} - -class AttributedStringBuilderV1Tests: XCTestCase { +class AttributedStringBuilderTests: XCTestCase { private var attributedStringBuilder: AttributedStringBuilder! private let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) @@ -35,33 +23,12 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(String(attributedString.characters), "H1 Header\n\nH2 Header\n\nH3 Header\n\nH4 Header\n\nH5 Header\n\nH6 Header") - - XCTAssertEqual(attributedString.runs.count, 11) // newlines hold no attributes - - let pointSizes = attributedString.runs.compactMap(\.uiKit.font?.pointSize) - XCTAssertEqual(pointSizes, [23, 23, 23, 21, 19, 17]) - } else { - XCTAssertEqual(String(attributedString.characters), "H1 Header\nH2 Header\nH3 Header\nH4 Header\nH5 Header\nH6 Header") - - XCTAssert(attributedString.runs.count == 6) - - let firstRun = attributedString.runs[attributedString.runs.startIndex] - let secondRun = attributedString.runs[attributedString.runs.index(attributedString.runs.startIndex, offsetBy: 1)] - let thirdRun = attributedString.runs[attributedString.runs.index(attributedString.runs.startIndex, offsetBy: 2)] - - guard let h1PointSize = firstRun.uiKit.font?.pointSize else { - XCTFail("Invalid H1 point size") - return - } - - XCTAssertEqual(h1PointSize, secondRun.uiKit.font?.pointSize) - XCTAssertEqual(secondRun.uiKit.font?.pointSize, thirdRun.uiKit.font?.pointSize) - - XCTAssertGreaterThan(h1PointSize, UIFont.preferredFont(forTextStyle: .body).pointSize) - XCTAssertLessThan(h1PointSize, maxHeaderPointSize) - } + XCTAssertEqual(String(attributedString.characters), "H1 Header\n\nH2 Header\n\nH3 Header\n\nH4 Header\n\nH5 Header\n\nH6 Header") + + XCTAssertEqual(attributedString.runs.count, 11) // newlines hold no attributes + + let pointSizes = attributedString.runs.compactMap(\.uiKit.font?.pointSize) + XCTAssertEqual(pointSizes, [23, 23, 23, 21, 19, 17]) } func testRenderHTMLStringWithPreCode() { @@ -79,11 +46,7 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)), 18) - } else { - XCTAssertEqual(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)), 10) - } + XCTAssertEqual(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)), 18) } func testRenderHTMLStringWithLink() { @@ -92,11 +55,7 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(String(attributedString.characters), "Links too:\nMatrix rules! 🤘, beta.org, www.gamma.org, http://delta.org") - } else { - XCTAssertEqual(String(attributedString.characters), "Links too:Matrix rules! 🤘, beta.org, www.gamma.org, http://delta.org") - } + XCTAssertEqual(String(attributedString.characters), "Links too:\nMatrix rules! 🤘, beta.org, www.gamma.org, http://delta.org") let link = attributedString.runs.first { $0.link != nil }?.link @@ -184,13 +143,8 @@ class AttributedStringBuilderV1Tests: XCTestCase { XCTAssertEqual(h1Font, h2Font) XCTAssertEqual(h2Font, h3Font) - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssert(h1Font.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize) - XCTAssert(h1Font.pointSize <= 23) - } else { - XCTAssert(h1Font.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize) - XCTAssert(h1Font.pointSize <= maxHeaderPointSize) - } + XCTAssert(h1Font.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize) + XCTAssert(h1Font.pointSize <= 23) XCTAssertEqual(h1AttributedString.runs.first?.link?.host, "matrix.org") XCTAssertEqual(h2AttributedString.runs.first?.link?.host, "matrix.org") @@ -258,12 +212,6 @@ class AttributedStringBuilderV1Tests: XCTestCase { } XCTAssertEqual(attributedString.runs.count, 3) - - if !AttributedStringBuilder.useNextGenHTMLParser { - for run in attributedString.runs { - XCTAssertEqual(run.uiKit.font?.familyName, UIFont.preferredFont(forTextStyle: .body).familyName) - } - } } func testDefaultForegroundColor() { @@ -402,13 +350,8 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(attributedString.runs.count, 11) - XCTAssertEqual(attributedString.formattedComponents.count, 5) - } else { - XCTAssertEqual(attributedString.runs.count, 7) - XCTAssertEqual(attributedString.formattedComponents.count, 1) - } + XCTAssertEqual(attributedString.runs.count, 11) + XCTAssertEqual(attributedString.formattedComponents.count, 5) var numberOfBlockquotes = 0 for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { @@ -732,11 +675,7 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(String(attributedString.characters), "Hey [img: Smiley face]! How's work[img]?") - } else { - XCTAssertEqual(String(attributedString.characters), "Hey ! How's work?") // No bueno - } + XCTAssertEqual(String(attributedString.characters), "Hey [img: Smiley face]! How's work[img]?") } func testListTags() { @@ -747,11 +686,7 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(String(attributedString.characters), "like\n\n • this\ntest") - } else { - XCTAssertEqual(String(attributedString.characters), "like\n\t•\tthis\u{2028}test") - } + XCTAssertEqual(String(attributedString.characters), "like\n\n • this\ntest") } func testOutOfOrderListNubmering() { @@ -762,11 +697,7 @@ class AttributedStringBuilderV1Tests: XCTestCase { return } - if AttributedStringBuilder.useNextGenHTMLParser { - XCTAssertEqual(String(attributedString.characters), " 2. this is a two") - } else { - XCTAssertEqual(String(attributedString.characters), "\t2.\tthis is a two") - } + XCTAssertEqual(String(attributedString.characters), " 2. this is a two") } // MARK: - Phishing prevention diff --git a/UnitTests/Sources/AttributedStringTests.swift b/UnitTests/Sources/AttributedStringTests.swift index d9bb9a0a4..ad8b821be 100644 --- a/UnitTests/Sources/AttributedStringTests.swift +++ b/UnitTests/Sources/AttributedStringTests.swift @@ -18,7 +18,6 @@ class AttributedStringTests: XCTestCase { XCTFail("The attributed string should be built from the HTML.") return } - originalString.runs.forEach { XCTAssertNotNil($0.uiKit.font, "The original runs should all have a UIFont.") } // When replacing the font with a presentation intent. let string = originalString.replacingFontWithPresentationIntent()