Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Modules/Sources/WordPressKitModels/NSString+Summary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ extension NSString {
public func wpkit_makePlainText() -> String {
makePlainText()
}

/// Converts an HTML excerpt into single-line plain text, turning `<br>`
/// tags into spaces so adjacent words don't run together.
@objc
public func wpkit_makeSingleLinePlainText() -> String {
GutenbergExcerptGenerator.singleLinePlainText(from: (self as String))
}
}
6 changes: 3 additions & 3 deletions Modules/Sources/WordPressKitObjC/RemoteReaderPost.m
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ - (NSString *)postTitleFromPostDictionary:(NSDictionary *)dict {
- (NSString *)postSummaryFromPostDictionary:(NSDictionary *)dict orPostContent:(NSString *)content {
NSString *summary = [self stringOrEmptyString:[dict stringForKey:PostRESTKeyExcerpt]];
summary = [self formatSummary:summary];
if (!summary) {
if (summary.length == 0) {
summary = [self createSummaryFromContent:content];
}
return summary;
Expand Down Expand Up @@ -670,14 +670,14 @@ - (NSArray *)slugsFromDiscoverPostTaxonomies:(NSArray *)discoverPostTaxonomies

/**
Formats a post's summary. The excerpts provided by the REST API contain HTML and have some extra content appened to the end.
HTML is stripped and the extra bit is removed.
HTML is stripped and the extra bit is removed. `<br>` tags become spaces so adjacent words don't run together.

@param summary The summary to format.
@return The formatted summary.
*/
- (NSString *)formatSummary:(NSString *)summary
{
summary = [self makePlainText:summary];
summary = [summary wpkit_makeSingleLinePlainText];

NSString *continueReading = NSLocalizedString(@"Continue reading", @"Part of a prompt suggesting that there is more content for the user to read.");
continueReading = [NSString stringWithFormat:@"%@ →", continueReading];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,7 @@ public struct GutenbergExcerptGenerator {
}

let paragraph = String(content[paragraphTag.upperBound..<pEnd.lowerBound])

// Convert `<br>` runs to spaces so words don't run together, then remove
// any remaining tags and shortcodes.
let withoutBreaks = replacingMatches(of: lineBreakRegex, in: paragraph, withTemplate: " ")
let stripped = replacingMatches(of: tagOrShortcodeRegex, in: withoutBreaks, withTemplate: "")

// Decode entities and collapse every whitespace run into a single space,
// yielding single-line plain text.
let text = stripped
.stringByDecodingXMLCharacters()
.components(separatedBy: collapsibleWhitespace)
.filter { !$0.isEmpty }
.joined(separator: " ")
let text = singleLinePlainText(from: paragraph)

// Truncate if needed.
if text.count <= maxLength {
Expand All @@ -53,6 +41,25 @@ public struct GutenbergExcerptGenerator {
return truncated + "…"
}

/// Converts HTML into single-line plain text. Unlike a plain tag stripper,
/// `<br>` runs become spaces so adjacent words don't run together.
/// Remaining tags and shortcodes are removed, entities are decoded, and
/// whitespace runs collapse into single spaces.
public static func singleLinePlainText(from html: String) -> String {
// Convert `<br>` runs to spaces so words don't run together, then remove
// any remaining tags and shortcodes.
let withoutBreaks = replacingMatches(of: lineBreakRegex, in: html, withTemplate: " ")
let stripped = replacingMatches(of: tagOrShortcodeRegex, in: withoutBreaks, withTemplate: "")

// Decode entities and collapse every whitespace run into a single space,
// yielding single-line plain text.
return stripped
.stringByDecodingXMLCharacters()
.components(separatedBy: collapsibleWhitespace)
.filter { !$0.isEmpty }
.joined(separator: " ")
}

/// Returns the range of the first match of `regex` in `string`, or `nil`.
private static func firstMatch(of regex: NSRegularExpression?, in string: String) -> Range<String.Index>? {
guard let regex else { return nil }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ - (void)testSummaryIsPlainText {
XCTAssertTrue([str isEqualToString:sanatizedStr], @"The post summary was not plain text.");
}

- (void)testSummaryConvertsLineBreaksToSpaces {
RemoteReaderPost *remoteReaderPost = [RemoteReaderPost alloc];
NSDictionary *dict = @{@"excerpt": @"<p>Yes,<br>look behind</p>"};
NSString *summary = [remoteReaderPost postSummaryFromPostDictionary:dict orPostContent:@""];
XCTAssertEqualObjects(summary, @"Yes, look behind", @"A <br> in the API excerpt should become a space instead of running words together.");
}

- (void)testSummaryFallsBackToContentWhenExcerptIsEmpty {
RemoteReaderPost *remoteReaderPost = [RemoteReaderPost alloc];
NSDictionary *dict = @{@"excerpt": @""};
NSString *content = @"<p>Generated from the post content.</p>";
NSString *summary = [remoteReaderPost postSummaryFromPostDictionary:dict orPostContent:content];
XCTAssertEqualObjects(summary, @"Generated from the post content.", @"An empty API excerpt should fall back to a summary generated from the post content.");
}

- (void)testSiteIsAtomic {
NSString *key = @"site_is_atomic";

Expand Down