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)]