-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Integrate ReaderPostHeaderView #25465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kean
wants to merge
1
commit into
task/reader-reamove-featured-image-current
Choose a base branch
from
task/reader-integrate-new-header-view
base: task/reader-reamove-featured-image-current
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
262 changes: 198 additions & 64 deletions
262
Modules/Sources/WordPressReader/Detail/ReaderPostHeaderView.swift
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import Foundation | ||
|
|
||
| public enum ReaderReadTime { | ||
| /// Computes the estimated reading time in minutes from raw post content | ||
| /// (HTML or Markdown), accounting for words, images, and code blocks. | ||
| /// | ||
| /// - Parameters: | ||
| /// - text: The raw post content (may contain HTML/Markdown). | ||
| /// - wpm: Words per minute reading speed (default 238). | ||
| /// - Returns: Estimated reading time in minutes (minimum 1). | ||
| public static func compute(_ text: String, wpm: Double = 200) -> Int { | ||
| // 1. Strip HTML & Markdown | ||
| var clean = text | ||
| clean = clean.replacing(#/<[^>]+>/#, with: "") | ||
| clean = clean.replacing(#/!\[.*?\]\(.*?\)/#, with: "") | ||
| clean = clean.replacing(#/\[.*?\]\(.*?\)/#, with: " ") | ||
|
|
||
| // 2. Count words | ||
| let wordCount = clean.matches(of: #/\b\w+\b/#).count | ||
|
|
||
| // 3. Base reading time (seconds) | ||
| var totalSeconds = (Double(wordCount) / wpm) * 60 | ||
|
|
||
| // 4. Image penalty (12s → 3s floor, decreasing per image) | ||
| let imageCount = text.matches(of: #/<img|!\[/#).count | ||
| for i in 0..<imageCount { | ||
| totalSeconds += Double(max(12 - i, 3)) | ||
| } | ||
|
|
||
| // 5. Code block penalty (extra half-speed cost for code) | ||
| let codeMatches = text.matches(of: #/```[\s\S]*?```|`[^`]+`/#) | ||
| for match in codeMatches { | ||
| let codeWords = String(match.output).split(separator: " ").count | ||
| totalSeconds += (Double(codeWords) / wpm) * 60 | ||
| } | ||
|
|
||
| return max(1, Int((totalSeconds / 60.0).rounded(.up))) | ||
| } | ||
| } | ||
61 changes: 61 additions & 0 deletions
61
Modules/Tests/WordPressReaderTests/ReaderReadTimeTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import Testing | ||
| @testable import WordPressReader | ||
|
|
||
| struct ReaderReadTimeTests { | ||
|
|
||
| @Test func shortText() { | ||
| #expect(ReaderReadTime.compute("Hello world") == 1) | ||
| } | ||
|
|
||
| @Test func plainText200Words() { | ||
| // 200 words at 200 WPM = exactly 1 minute | ||
| let text = String(repeating: "word ", count: 200) | ||
| #expect(ReaderReadTime.compute(text) == 1) | ||
| } | ||
|
|
||
| @Test func plainText500Words() { | ||
| // 500 words / 200 WPM = 2.5 → rounds up to 3 | ||
| let text = String(repeating: "word ", count: 500) | ||
| #expect(ReaderReadTime.compute(text) == 3) | ||
| } | ||
|
|
||
| @Test func plainText1000Words() { | ||
| // 1000 words / 200 WPM = 5 minutes | ||
| let text = String(repeating: "word ", count: 1000) | ||
| #expect(ReaderReadTime.compute(text) == 5) | ||
| } | ||
|
|
||
| @Test func htmlTagsAreStripped() { | ||
| let html = "<p>" + String(repeating: "word ", count: 500) + "</p>" | ||
| let plain = String(repeating: "word ", count: 500) | ||
| #expect(ReaderReadTime.compute(html) == ReaderReadTime.compute(plain)) | ||
| } | ||
|
|
||
| @Test func imagesAddPenalty() { | ||
| // 200 words = 60s base. 3 images add 12 + 11 + 10 = 33s → 93s → 2 min | ||
| let base = String(repeating: "word ", count: 200) | ||
| let withImages = base + "<img src=\"a.png\"><img src=\"b.png\"><img src=\"c.png\">" | ||
| #expect(ReaderReadTime.compute(base) == 1) | ||
| #expect(ReaderReadTime.compute(withImages) == 2) | ||
| } | ||
|
|
||
| @Test func codeBlocksAddPenalty() { | ||
| let base = String(repeating: "word ", count: 200) | ||
| let withCode = base + "```let x = 1; let y = 2; let z = 3```" | ||
| #expect(ReaderReadTime.compute(withCode) >= ReaderReadTime.compute(base)) | ||
| } | ||
|
|
||
| @Test func longPost() { | ||
| // ~2500 word blog post with HTML, images, and code | ||
| var post = "<h1>Getting Started with Swift Concurrency</h1>" | ||
| post += "<p>" + String(repeating: "This is a detailed explanation of the concept. ", count: 100) + "</p>" | ||
| post += "<img src=\"diagram1.png\">" | ||
| post += "<p>" + String(repeating: "Here we explore another important aspect of the topic. ", count: 100) + "</p>" | ||
| post += "<img src=\"diagram2.png\">" | ||
| post += "<pre><code>```func fetchData() async throws { let data = try await URLSession.shared.data(from: url)```</code></pre>" | ||
| post += "<p>" + String(repeating: "In conclusion this wraps up the discussion nicely. ", count: 50) + "</p>" | ||
| // ~2500 words / 200 WPM ≈ 12.5 min + image/code penalties → ~13 min | ||
| let result = ReaderReadTime.compute(post) | ||
| #expect(result == 12) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import Foundation | ||
| import WordPressData | ||
| import WordPressReader | ||
| import WordPressUI | ||
| import SwiftSoup | ||
|
|
||
|
|
@@ -63,6 +64,49 @@ extension ReaderPost { | |
| try? lookup(withID: postID, forSiteWithID: siteID, in: context) | ||
| } | ||
|
|
||
| /// Returns estimated reading time in minutes. | ||
| /// | ||
| /// Uses the API-provided `readingTime` when available, otherwise computes | ||
| /// it from the post content accounting for words, images, and code blocks. | ||
| func getEstimatedReadingTime() -> Int { | ||
| if let minutes = readingTime?.intValue, minutes > 0 { | ||
| return minutes | ||
| } | ||
| guard let content = contentForDisplay(), !content.isEmpty else { | ||
| return 0 | ||
| } | ||
| return ReaderReadTime.compute(content) | ||
| } | ||
|
|
||
| /// Returns the excerpt only if it was explicitly provided by the post author. | ||
| /// | ||
| /// The API always returns a `excerpt`, but it's usually auto-generated by | ||
| /// truncating the post content. This method compares the summary against the | ||
| /// beginning of the content — if the summary is just a prefix of the content | ||
| /// (optionally ending with `[…]` or `…`), it's considered auto-generated | ||
| /// and `nil` is returned. | ||
| func getUserProvidedExcerpt() -> String? { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a temporary implementation. I'm working with the backend folks to add a "raw_excerpt" field to the response to eliminate this code. |
||
| guard let excerpt = mt_excerpt?.makePlainText(), !excerpt.isEmpty else { | ||
| return nil | ||
| } | ||
| guard let content = contentForDisplay(), !content.isEmpty else { | ||
| return excerpt | ||
| } | ||
|
|
||
| // Auto-generated excerpts end with a truncation marker | ||
| if excerpt.hasSuffix("[…]") || excerpt.hasSuffix("…") { | ||
| return nil | ||
| } | ||
|
|
||
| // If the content starts with the excerpt, it's auto-generated | ||
| let plainContent = content.makePlainText() | ||
| if plainContent.hasPrefix(excerpt.prefix(50)) { | ||
| return nil | ||
| } | ||
|
|
||
| return excerpt | ||
| } | ||
|
|
||
| func makeExceptHTML() -> String { | ||
| """ | ||
| <html> | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is WIP and subject to future changes. We need to see how well it works in practice.