From cf87259de69527e5f735bf2a01ff07f466c37b45 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:13:14 -0600 Subject: [PATCH 1/2] Remove dead WordPressUI import from WordPressData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ManagedPerson was the only file in WordPressData importing WordPressUI, and the one symbol that looked like it needed it — AvatarURL.canonicalURL — is Gravatar's, already imported directly in the same file. Drop the import and the package dependency. --- Modules/Package.swift | 1 - Modules/Sources/WordPressData/Swift/ManagedPerson.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 9d07d6b80bb7..e139e9e261f0 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -320,7 +320,6 @@ let package = Package( "WordPressShared", "WordPressSharedUI", "WordPressKit", - "WordPressUI", .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), .product(name: "Gravatar", package: "Gravatar-SDK-iOS"), diff --git a/Modules/Sources/WordPressData/Swift/ManagedPerson.swift b/Modules/Sources/WordPressData/Swift/ManagedPerson.swift index aea4c9e0e328..08d8cfa95220 100644 --- a/Modules/Sources/WordPressData/Swift/ManagedPerson.swift +++ b/Modules/Sources/WordPressData/Swift/ManagedPerson.swift @@ -1,7 +1,6 @@ import Foundation import CoreData import WordPressKit -import WordPressUI import Gravatar public typealias Person = RemotePerson From 2473db94794b515c2205c39eb1a76a48b3f22ffc Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:13:45 -0600 Subject: [PATCH 2/2] Split RichContentFormatter out of WordPressSharedUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RichContentFormatter is plain String -> String HTML sanitizing, but it lived in WordPressSharedUI, so WordPressData pulled in the UI layer to use it. Move the eight platform-independent methods and the RegEx table down to WordPressShared; keep formatContentString and resizeGalleryImageURL — which read UIScreen and call PhotonImageURLHelper — in WordPressSharedUI as an @objc extension. WordPressData no longer depends on WordPressSharedUI. The text-formatting tests move with the code and now run on macOS under swift test; the gallery-resize test stays iOS-only. --- Modules/Package.swift | 1 - .../Mapping/ReaderPost+Mapping.swift | 1 - .../Utility}/RichContentFormatter.swift | 83 +--------------- ...RichContentFormatter+DisplayPipeline.swift | 97 +++++++++++++++++++ .../RichContentFormatterTests.swift | 5 - .../RichContentFormatterUITests.swift | 10 ++ Package.swift | 2 +- 7 files changed, 109 insertions(+), 90 deletions(-) rename Modules/Sources/{WordPressSharedUI => WordPressShared/Utility}/RichContentFormatter.swift (78%) create mode 100644 Modules/Sources/WordPressSharedUI/RichContentFormatter+DisplayPipeline.swift create mode 100644 Modules/Tests/WordPressSharedTests/RichContentFormatterUITests.swift diff --git a/Modules/Package.swift b/Modules/Package.swift index e139e9e261f0..4fbf4c4122d6 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -318,7 +318,6 @@ let package = Package( "FormattableContentKit", "SFHFKeychainUtils", "WordPressShared", - "WordPressSharedUI", "WordPressKit", .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), diff --git a/Modules/Sources/WordPressData/Mapping/ReaderPost+Mapping.swift b/Modules/Sources/WordPressData/Mapping/ReaderPost+Mapping.swift index 0dc60ac81fa8..8c7e26e41a6b 100644 --- a/Modules/Sources/WordPressData/Mapping/ReaderPost+Mapping.swift +++ b/Modules/Sources/WordPressData/Mapping/ReaderPost+Mapping.swift @@ -1,7 +1,6 @@ import CoreData import WordPressKit import WordPressShared -import WordPressSharedUI extension ReaderPost { /// Finds an existing `ReaderPost` matching the given `globalID` and `topic`, diff --git a/Modules/Sources/WordPressSharedUI/RichContentFormatter.swift b/Modules/Sources/WordPressShared/Utility/RichContentFormatter.swift similarity index 78% rename from Modules/Sources/WordPressSharedUI/RichContentFormatter.swift rename to Modules/Sources/WordPressShared/Utility/RichContentFormatter.swift index b9cfc55e7cb0..38530a2d2043 100644 --- a/Modules/Sources/WordPressSharedUI/RichContentFormatter.swift +++ b/Modules/Sources/WordPressShared/Utility/RichContentFormatter.swift @@ -1,7 +1,4 @@ import Foundation -import UIKit -import WordPressShared -import WordPressSharedObjCUI /// Contains methods for formatting post or comment content for display. /// @@ -28,7 +25,7 @@ import WordPressSharedObjCUI static let styleAttr = try! NSRegularExpression(pattern: "\\s*style=\"[^\"]*\"", options: .caseInsensitive) // Gallery Images - static let galleryImgTags = try! NSRegularExpression(pattern: "]*data-orig-file[^>]*/>", options: .caseInsensitive) + public static let galleryImgTags = try! NSRegularExpression(pattern: "]*data-orig-file[^>]*/>", options: .caseInsensitive) // Trailing BR Tags static let trailingBRTags = try! NSRegularExpression(pattern: "(\\s*\\s*)+$", options: .caseInsensitive) @@ -38,31 +35,6 @@ import WordPressSharedObjCUI static let gutenbergGalleryListItem = try! NSRegularExpression(pattern: "]+gallery-item[^>]+>(
)", options: .caseInsensitive) } - /// Formats the specified content string for display. Forbidden HTML tags are - /// removed, paragraphs are normalized, etc. - /// - /// - Parameters: - /// - string: The content string to format. - /// - isPrivate: Whether the content is from a private blog. - /// - /// - Returns: The formatted string. - /// - @objc public class func formatContentString(_ string: String, isPrivateSite isPrivate: Bool) -> String { - guard !string.isEmpty else { - return string - } - - var content = string - content = removeForbiddenTags(content) - content = normalizeParagraphs(content) - content = removeInlineStyles(content) - content = (content as NSString).replacingHTMLEmoticonsWithEmoji() as String - content = formatGutenbergGallery(content) - content = resizeGalleryImageURL(content, isPrivateSite: isPrivate) - content = formatVideoTags(content) - return content - } - /// Removes forbidden HTML tags from the specified string. /// /// - Parameters: @@ -197,59 +169,6 @@ import WordPressSharedObjCUI return content } - /// Mutates gallery image URLs to be correctly sized. - /// - /// - Parameters: - /// - string: The content string to format. - /// - isPrivate: Whether the content is from a private blog. - /// - /// - Returns: The formatted string. - /// - @objc public class func resizeGalleryImageURL(_ string: String, isPrivateSite isPrivate: Bool) -> String { - guard !string.isEmpty else { - return string - } - - let imageSize = UIScreen.main.bounds.size - let scale = UIScreen.main.scale - let scaledSize = imageSize.applying(CGAffineTransform(scaleX: scale, y: scale)) - - let mContent = NSMutableString(string: string) - - let matches = RegEx.galleryImgTags.matches(in: mContent as String, options: [], range: NSRange(location: 0, length: mContent.length)) - - for match in matches.reversed() { - let imgElementStr = mContent.substring(with: match.range) - let srcImgURLStr = parseValueForAttribute("src", inElement: imgElementStr) - let originalImgURLStr = parseValueForAttribute("data-orig-file", inElement: imgElementStr) - - guard let originalURL = URL(string: originalImgURLStr) else { - continue - } - - var modifiedURL: URL - if isPrivate { - modifiedURL = WPImageURLHelper.imageURLWithSize(scaledSize, forImageURL: originalURL) - } else { - modifiedURL = PhotonImageURLHelper.photonURL(with: imageSize, forImageURL: originalURL) - } - - guard modifiedURL.absoluteString.isEmpty() == false else { - continue - } - - let mImageStr = NSMutableString(string: imgElementStr) - mImageStr.replaceOccurrences(of: srcImgURLStr, - with: modifiedURL.absoluteString, - options: .literal, - range: NSRange(location: 0, length: imgElementStr.count)) - - mContent.replaceCharacters(in: match.range, with: mImageStr as String) - } - - return mContent as String - } - /// Parses the specified string for the value of the specified attribute. /// /// - Parameters: diff --git a/Modules/Sources/WordPressSharedUI/RichContentFormatter+DisplayPipeline.swift b/Modules/Sources/WordPressSharedUI/RichContentFormatter+DisplayPipeline.swift new file mode 100644 index 000000000000..84cf672f4f2a --- /dev/null +++ b/Modules/Sources/WordPressSharedUI/RichContentFormatter+DisplayPipeline.swift @@ -0,0 +1,97 @@ +import Foundation +import UIKit +import WordPressShared +import WordPressSharedObjCUI + +/// UIKit-dependent additions to `RichContentFormatter`. +/// +/// The platform-independent text transformations live in `WordPressShared`. The +/// display pipeline below depends on the screen (for gallery image sizing) and on +/// `WordPressSharedObjCUI`'s Photon helper, so it stays in the UI layer. +/// +extension RichContentFormatter { + + /// Formats the specified content string for display. Forbidden HTML tags are + /// removed, paragraphs are normalized, etc. + /// + /// - Parameters: + /// - string: The content string to format. + /// - isPrivate: Whether the content is from a private blog. + /// + /// - Returns: The formatted string. + /// + @objc public class func formatContentString(_ string: String, isPrivateSite isPrivate: Bool) -> String { + guard !string.isEmpty else { + return string + } + + var content = string + content = removeForbiddenTags(content) + content = normalizeParagraphs(content) + content = removeInlineStyles(content) + content = (content as NSString).replacingHTMLEmoticonsWithEmoji() as String + content = formatGutenbergGallery(content) + content = resizeGalleryImageURL(content, isPrivateSite: isPrivate) + content = formatVideoTags(content) + return content + } + + /// Mutates gallery image URLs to be correctly sized. + /// + /// - Parameters: + /// - string: The content string to format. + /// - isPrivate: Whether the content is from a private blog. + /// + /// - Returns: The formatted string. + /// + @objc public class func resizeGalleryImageURL(_ string: String, isPrivateSite isPrivate: Bool) -> String { + guard !string.isEmpty else { + return string + } + + let imageSize = UIScreen.main.bounds.size + let scale = UIScreen.main.scale + let scaledSize = imageSize.applying(CGAffineTransform(scaleX: scale, y: scale)) + + let mContent = NSMutableString(string: string) + + let matches = RegEx.galleryImgTags.matches( + in: mContent as String, + options: [], + range: NSRange(location: 0, length: mContent.length) + ) + + for match in matches.reversed() { + let imgElementStr = mContent.substring(with: match.range) + let srcImgURLStr = parseValueForAttribute("src", inElement: imgElementStr) + let originalImgURLStr = parseValueForAttribute("data-orig-file", inElement: imgElementStr) + + guard let originalURL = URL(string: originalImgURLStr) else { + continue + } + + var modifiedURL: URL + if isPrivate { + modifiedURL = WPImageURLHelper.imageURLWithSize(scaledSize, forImageURL: originalURL) + } else { + modifiedURL = PhotonImageURLHelper.photonURL(with: imageSize, forImageURL: originalURL) + } + + guard modifiedURL.absoluteString.isEmpty() == false else { + continue + } + + let mImageStr = NSMutableString(string: imgElementStr) + mImageStr.replaceOccurrences( + of: srcImgURLStr, + with: modifiedURL.absoluteString, + options: .literal, + range: NSRange(location: 0, length: imgElementStr.count) + ) + + mContent.replaceCharacters(in: match.range, with: mImageStr as String) + } + + return mContent as String + } +} diff --git a/Modules/Tests/WordPressSharedTests/RichContentFormatterTests.swift b/Modules/Tests/WordPressSharedTests/RichContentFormatterTests.swift index e5068f17e019..b594c5482908 100644 --- a/Modules/Tests/WordPressSharedTests/RichContentFormatterTests.swift +++ b/Modules/Tests/WordPressSharedTests/RichContentFormatterTests.swift @@ -1,6 +1,5 @@ import XCTest @testable import WordPressShared -@testable import WordPressSharedUI class RichContentFormatterTests: XCTestCase { @@ -32,10 +31,6 @@ class RichContentFormatterTests: XCTestCase { XCTAssertTrue(str == sanitizedStr, "Not all paragraphs were normalized.") } - func testResizeGalleryImageURLsForContentEmptyString() { - XCTAssertTrue("" == RichContentFormatter.resizeGalleryImageURL("", isPrivateSite: false)) - } - func testRemoveTrailingBRTags() { let str = "

test


test

" let styleStr = "

test


test



" diff --git a/Modules/Tests/WordPressSharedTests/RichContentFormatterUITests.swift b/Modules/Tests/WordPressSharedTests/RichContentFormatterUITests.swift new file mode 100644 index 000000000000..8efaddb06f04 --- /dev/null +++ b/Modules/Tests/WordPressSharedTests/RichContentFormatterUITests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import WordPressShared +@testable import WordPressSharedUI + +class RichContentFormatterUITests: XCTestCase { + + func testResizeGalleryImageURLsForContentEmptyString() { + XCTAssertTrue("" == RichContentFormatter.resizeGalleryImageURL("", isPrivateSite: false)) + } +} diff --git a/Package.swift b/Package.swift index 6a9397e7670e..c2eabdc904ba 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,7 @@ let package = Package( path: "Modules/Tests/WordPressSharedTests", exclude: [ "WordPressShared.xctestplan", - "RichContentFormatterTests.swift", + "RichContentFormatterUITests.swift", "WPUserAgentTests.swift" ], swiftSettings: [.swiftLanguageMode(.v5)]