From e217826d1dbe2513d887ecb882c7acf4441c2b35 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 17 Mar 2026 18:37:42 -0400 Subject: [PATCH 1/5] Add support for
tags to GutenbergExcerptGenerator --- .../Utility/GutenbergExcerptGenerator.swift | 3 ++- .../GutenbergExcerptGeneratorTests.swift | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift index b21be87dbf7a..cfb4d1a61f1b 100644 --- a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift +++ b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift @@ -14,8 +14,9 @@ public struct GutenbergExcerptGenerator { return "" } - // Extract content + // Extract content while convering
,
,
to newlines first let rawText = String(content[tagEnd.upperBound..", with: "\n", options: .regularExpression) // Remove HTML tags AND shortcodes in one pass let range = NSRange(rawText.startIndex..., in: rawText) diff --git a/Modules/Tests/WordPressSharedTests/GutenbergExcerptGeneratorTests.swift b/Modules/Tests/WordPressSharedTests/GutenbergExcerptGeneratorTests.swift index eff9361a7686..a5461d4c8580 100644 --- a/Modules/Tests/WordPressSharedTests/GutenbergExcerptGeneratorTests.swift +++ b/Modules/Tests/WordPressSharedTests/GutenbergExcerptGeneratorTests.swift @@ -31,4 +31,12 @@ struct GutenbergPostExcerptGeneratorTests { let summary = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 150) #expect(summary == "Before") } + + @Test func testPostWithBRTags() { + let content = #"

Yes,
look behind
in remembrance and with gratitude.

Then,
stay present,
or miss memories in the making.

"# + + let summary = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 150) + print(summary) + #expect(summary == "Yes, look behind in remembrance and with gratitude.") + } } From d4f95f088eff27442568789979284e2c61d966ca Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 17 Mar 2026 18:42:05 -0400 Subject: [PATCH 2/5] Update wpkit_summarized to do the same --- Modules/Sources/WordPressKitModels/NSString+Summary.swift | 1 + .../WordPressShared/Utility/GutenbergExcerptGenerator.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/WordPressKitModels/NSString+Summary.swift b/Modules/Sources/WordPressKitModels/NSString+Summary.swift index 2a2696f56e2f..bf4dd07e689a 100644 --- a/Modules/Sources/WordPressKitModels/NSString+Summary.swift +++ b/Modules/Sources/WordPressKitModels/NSString+Summary.swift @@ -17,6 +17,7 @@ extension NSString { let characterSet = CharacterSet(charactersIn: "\n") return (self as String).strippingGutenbergContentForExcerpt() + .replacingOccurrences(of: "", with: " ", options: .regularExpression) .strippingShortcodes() .makePlainText() .trimmingCharacters(in: characterSet) diff --git a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift index cfb4d1a61f1b..4c095973a066 100644 --- a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift +++ b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift @@ -16,7 +16,7 @@ public struct GutenbergExcerptGenerator { // Extract content while convering
,
,
to newlines first let rawText = String(content[tagEnd.upperBound..", with: "\n", options: .regularExpression) + .replacingOccurrences(of: "", with: " ", options: .regularExpression) // Remove HTML tags AND shortcodes in one pass let range = NSRange(rawText.startIndex..., in: rawText) From 26c5c2b394bac31cd3c5ecf4cc4b3f0af19c21d4 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 18 Mar 2026 08:32:01 -0400 Subject: [PATCH 3/5] GutenbergExcerptGenerator to always add ellipsis even for shorter posts --- .../Utility/GutenbergExcerptGenerator.swift | 16 +++++++--------- .../GutenbergExcerptGeneratorTests.swift | 8 ++++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift index 4c095973a066..0ac5934c9fb0 100644 --- a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift +++ b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift @@ -18,21 +18,19 @@ public struct GutenbergExcerptGenerator { let rawText = String(content[tagEnd.upperBound..", with: " ", options: .regularExpression) - // Remove HTML tags AND shortcodes in one pass let range = NSRange(rawText.startIndex..., in: rawText) - let text = (regex?.stringByReplacingMatches(in: rawText, options: [], range: range, withTemplate: "") ?? rawText) + var text = (regex?.stringByReplacingMatches(in: rawText, options: [], range: range, withTemplate: "") ?? rawText) .stringByDecodingXMLCharacters() .trimmingCharacters(in: .whitespacesAndNewlines) - // Truncate if needed - if text.count <= maxLength { - return text + if text.count > maxLength { + let truncated = String(text.prefix(maxLength)) + text = truncated.lastIndex(of: " ").map { String(truncated[..<$0]) } ?? truncated } - let truncated = String(text.prefix(maxLength)) - if let lastSpace = truncated.lastIndex(of: " ") { - return String(truncated[..

Some Content

" let summary = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 150) - #expect(summary == "Some Content") + #expect(summary == "Some Content…") } @Test func summaryForContentWithGallery2() { let content = "

Before

\n

After

" let summary = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 150) - #expect(summary == "Before") + #expect(summary == "Before…") } @Test @@ -29,7 +29,7 @@ struct GutenbergPostExcerptGeneratorTests { let content = "

Before

\n\n
\nhttps://videopress.com/v/AbCDe?resizeToParent=true&cover=true&preloadContent=metadata&useAverageColor=true\n
\n\n

After

" let summary = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 150) - #expect(summary == "Before") + #expect(summary == "Before…") } @Test func testPostWithBRTags() { @@ -37,6 +37,6 @@ struct GutenbergPostExcerptGeneratorTests { let summary = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 150) print(summary) - #expect(summary == "Yes, look behind in remembrance and with gratitude.") + #expect(summary == "Yes, look behind in remembrance and with gratitude…") } } From c7c2052fa3360dba19d7010a7a0af5bc0aa024ac Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 19 Mar 2026 15:49:05 -0400 Subject: [PATCH 4/5] Integrate GutenbergExcerptGenerator --- .../WordPressKitModels/NSString+Summary.swift | 68 ++----------------- .../WordPressKitObjC/RemoteReaderPost.m | 2 +- 2 files changed, 6 insertions(+), 64 deletions(-) diff --git a/Modules/Sources/WordPressKitModels/NSString+Summary.swift b/Modules/Sources/WordPressKitModels/NSString+Summary.swift index bf4dd07e689a..ab7cfd6a3690 100644 --- a/Modules/Sources/WordPressKitModels/NSString+Summary.swift +++ b/Modules/Sources/WordPressKitModels/NSString+Summary.swift @@ -5,77 +5,19 @@ import WordPressShared /// and convert HTML into plain text. /// extension NSString { - - static let PostDerivedSummaryLength = 150 - /// Create a summary for the post based on the post's content. /// /// - Returns: A summary for the post. /// @objc public func wpkit_summarized() -> String { - let characterSet = CharacterSet(charactersIn: "\n") - - return (self as String).strippingGutenbergContentForExcerpt() - .replacingOccurrences(of: "", with: " ", options: .regularExpression) - .strippingShortcodes() - .makePlainText() - .trimmingCharacters(in: characterSet) - .wp_stringByEllipsizing(withMaxLength: NSString.PostDerivedSummaryLength, preserveWords: true) + GutenbergExcerptGenerator.firstParagraph(from: (self as String)) } -} -private extension String { - func makePlainText() -> String { - let characterSet = NSCharacterSet.whitespacesAndNewlines - - return self.strippingHTML() + @objc + public func wpkit_makePlainText() -> String { + self.strippingHTML() .stringByDecodingXMLCharacters() - .trimmingCharacters(in: characterSet) - } - - /// Creates a new string by stripping all shortcodes from this string. - /// - func strippingShortcodes() -> String { - let pattern = "\\[[^\\]]+\\]" - - return removingMatches(pattern: pattern, options: .caseInsensitive) - } - - /// This method is the main entry point to generate excerpts for Gutenberg content. - /// - func strippingGutenbergContentForExcerpt() -> String { - return strippingGutenbergGalleries().strippingGutenbergVideoPress() - } - - /// Strips Gutenberg galleries from strings. - /// - func strippingGutenbergGalleries() -> String { - let pattern = "(?s)" - - return removingMatches(pattern: pattern, options: .caseInsensitive) - } - - /// Strips VideoPress references from Gutenberg VideoPress and Video blocks. - /// - func strippingGutenbergVideoPress() -> String { - let pattern = "(?s)\n?" - - return removingMatches(pattern: pattern, options: .caseInsensitive) - } - - /// Creates a new string by removing all matches of the specified regex. - /// - func removingMatches(pattern: String, options: NSRegularExpression.Options = []) -> String { - let range = NSRange(location: 0, length: self.utf16.count) - let regex: NSRegularExpression - - do { - regex = try NSRegularExpression(pattern: pattern, options: options) - } catch { - return self - } - - return regex.stringByReplacingMatches(in: self, options: .reportCompletion, range: range, withTemplate: "") + .trimmingCharacters(in: .whitespacesAndNewlines) } } diff --git a/Modules/Sources/WordPressKitObjC/RemoteReaderPost.m b/Modules/Sources/WordPressKitObjC/RemoteReaderPost.m index 2f7b0557d1c6..8611bc202402 100644 --- a/Modules/Sources/WordPressKitObjC/RemoteReaderPost.m +++ b/Modules/Sources/WordPressKitObjC/RemoteReaderPost.m @@ -707,7 +707,7 @@ - (NSString *)createSummaryFromContent:(NSString *)string */ - (NSString *)makePlainText:(NSString *)string { - return [string wpkit_summarized]; + return [string wpkit_makePlainText]; } /** From 0e16c02397ba978ec880750477b2f2921263fd8d Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 19 Mar 2026 15:56:06 -0400 Subject: [PATCH 5/5] Revert the addition of excerpts --- .../Utility/GutenbergExcerptGenerator.swift | 15 ++++++++------- .../GutenbergExcerptGeneratorTests.swift | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift index 0ac5934c9fb0..cd3aa5db4d2f 100644 --- a/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift +++ b/Modules/Sources/WordPressShared/Utility/GutenbergExcerptGenerator.swift @@ -19,18 +19,19 @@ public struct GutenbergExcerptGenerator { .replacingOccurrences(of: "", with: " ", options: .regularExpression) let range = NSRange(rawText.startIndex..., in: rawText) - var text = (regex?.stringByReplacingMatches(in: rawText, options: [], range: range, withTemplate: "") ?? rawText) + let text = (regex?.stringByReplacingMatches(in: rawText, options: [], range: range, withTemplate: "") ?? rawText) .stringByDecodingXMLCharacters() .trimmingCharacters(in: .whitespacesAndNewlines) - if text.count > maxLength { - let truncated = String(text.prefix(maxLength)) - text = truncated.lastIndex(of: " ").map { String(truncated[..<$0]) } ?? truncated + // Truncate if needed + if text.count <= maxLength { + return text } - if text.hasSuffix(".") { - text = String(text.dropLast()) + let truncated = String(text.prefix(maxLength)) + if let lastSpace = truncated.lastIndex(of: " ") { + return String(truncated[..