diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index cd16a2750..6241c7a38 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -79,7 +79,7 @@ 849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */; }; 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97711ED9EC04007D329B /* TimelineCellData.swift */; }; 849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97721ED9EC04007D329B /* TimelineCellLayout.swift */; }; - 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */; }; + 849A97791ED9EC04007D329B /* TimelineStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */; }; 849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97741ED9EC04007D329B /* TimelineTableCellView.swift */; }; 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */; }; 849A977F1ED9EC42007D329B /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; }; @@ -557,7 +557,7 @@ 849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineCellAppearance.swift; sourceTree = ""; }; 849A97711ED9EC04007D329B /* TimelineCellData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineCellData.swift; sourceTree = ""; }; 849A97721ED9EC04007D329B /* TimelineCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineCellLayout.swift; sourceTree = ""; }; - 849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineStringUtilities.swift; sourceTree = ""; }; + 849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineStringFormatter.swift; sourceTree = ""; }; 849A97741ED9EC04007D329B /* TimelineTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineTableCellView.swift; sourceTree = ""; }; 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadIndicatorView.swift; sourceTree = ""; }; 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleRenderer.swift; sourceTree = ""; }; @@ -1013,7 +1013,7 @@ 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */, 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */, 849A97711ED9EC04007D329B /* TimelineCellData.swift */, - 849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */, + 849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */, 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */, ); path = Cell; @@ -1950,7 +1950,7 @@ 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */, 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */, - 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, + 849A97791ED9EC04007D329B /* TimelineStringFormatter.swift in Sources */, 84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */, diff --git a/NetNewsWire/AppDelegate.swift b/NetNewsWire/AppDelegate.swift index 39048ef69..04f506fab 100644 --- a/NetNewsWire/AppDelegate.swift +++ b/NetNewsWire/AppDelegate.swift @@ -180,7 +180,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, func applicationDidResignActive(_ notification: Notification) { - timelineEmptyCaches() + TimelineStringFormatter.emptyCaches() saveState() } diff --git a/NetNewsWire/MainWindow/Timeline/Cell/TimelineCellData.swift b/NetNewsWire/MainWindow/Timeline/Cell/TimelineCellData.swift index 14ca34d94..c4f389760 100644 --- a/NetNewsWire/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/NetNewsWire/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -24,13 +24,13 @@ struct TimelineCellData { init(article: Article, appearance: TimelineCellAppearance, showFeedName: Bool, feedName: String?, avatar: NSImage?, showAvatar: Bool, featuredImage: NSImage?) { - self.title = timelineTruncatedTitle(article) - self.text = timelineTruncatedSummary(article) + self.title = TimelineStringFormatter.truncatedTitle(article) + self.text = TimelineStringFormatter.truncatedSummary(article) - self.dateString = timelineDateString(article.logicalDatePublished) + self.dateString = TimelineStringFormatter.dateString(article.logicalDatePublished) if let feedName = feedName { - self.feedName = timelineTruncatedFeedName(feedName) + self.feedName = TimelineStringFormatter.truncatedFeedName(feedName) } else { self.feedName = "" diff --git a/NetNewsWire/MainWindow/Timeline/Cell/TimelineStringFormatter.swift b/NetNewsWire/MainWindow/Timeline/Cell/TimelineStringFormatter.swift new file mode 100644 index 000000000..32ec36cc7 --- /dev/null +++ b/NetNewsWire/MainWindow/Timeline/Cell/TimelineStringFormatter.swift @@ -0,0 +1,109 @@ +// +// TimelineStringFormatter.swift +// NetNewsWire +// +// Created by Brent Simmons on 8/31/15. +// Copyright © 2015 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import Articles +import RSParser + +class TimelineStringFormatter { + + private static var feedNameCache = [String: String]() + private static var titleCache = [String: String]() + private static var summaryCache = [String: String]() + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + private static let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + + static func emptyCaches() { + feedNameCache = [String: String]() + titleCache = [String: String]() + summaryCache = [String: String]() + } + + static func truncatedFeedName(_ feedName: String) -> String { + if let cachedFeedName = feedNameCache[feedName] { + return cachedFeedName + } + + let maxFeedNameLength = 100 + if feedName.count < maxFeedNameLength { + feedNameCache[feedName] = feedName + return feedName + } + + let s = (feedName as NSString).substring(to: maxFeedNameLength) + feedNameCache[feedName] = s + return s + } + + static func truncatedTitle(_ article: Article) -> String { + guard let title = article.title else { + return "" + } + + if let cachedTitle = titleCache[title] { + return cachedTitle + } + + var s = title.replacingOccurrences(of: "\n", with: "") + s = s.replacingOccurrences(of: "\r", with: "") + s = s.replacingOccurrences(of: "\t", with: "") + s = s.rsparser_stringByDecodingHTMLEntities() + s = s.replacingOccurrences(of: "↦", with: "") + s = s.rs_stringByTrimmingWhitespace() + s = s.rs_stringWithCollapsedWhitespace() + + let maxLength = 1000 + if s.count < maxLength { + titleCache[title] = s + return s + } + + s = (s as NSString).substring(to: maxLength) + titleCache[title] = s + return s + } + + static func truncatedSummary(_ article: Article) -> String { + guard let body = article.body else { + return "" + } + + if let cachedBody = summaryCache[body] { + return cachedBody + } + var s = body.rsparser_stringByDecodingHTMLEntities() + s = s.rs_string(byStrippingHTML: 300) + s = s.rs_stringByTrimmingWhitespace() + s = s.rs_stringWithCollapsedWhitespace() + if s == "Comments" { // Hacker News. + s = "" + } + summaryCache[body] = s + return s + } + + static func dateString(_ date: Date) -> String { + if NSCalendar.rs_dateIsToday(date) { + return timeFormatter.string(from: date) + } + return dateFormatter.string(from: date) + } +} + diff --git a/NetNewsWire/MainWindow/Timeline/Cell/TimelineStringUtilities.swift b/NetNewsWire/MainWindow/Timeline/Cell/TimelineStringUtilities.swift deleted file mode 100644 index c164e8963..000000000 --- a/NetNewsWire/MainWindow/Timeline/Cell/TimelineStringUtilities.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// TimelineStringUtilities.swift -// NetNewsWire -// -// Created by Brent Simmons on 8/31/15. -// Copyright © 2015 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Articles -import RSParser - -// TODO: Don’t make all this at top level. - -private var truncatedFeedNameCache = [String: String]() -private let truncatedTitleCache = NSMutableDictionary() -private let normalizedTextCache = NSMutableDictionary() -private let textCache = NSMutableDictionary() -private let summaryCache = NSMutableDictionary() - -func timelineEmptyCaches() { - - truncatedFeedNameCache = [String: String]() - truncatedTitleCache.removeAllObjects() - normalizedTextCache.removeAllObjects() - textCache.removeAllObjects() - summaryCache.removeAllObjects() -} - -func timelineTruncatedFeedName(_ feedName: String) -> String { - - if let cachedFeedName = truncatedFeedNameCache[feedName] { - return cachedFeedName - } - - let maxFeedNameLength = 100 - if feedName.count < maxFeedNameLength { - truncatedFeedNameCache[feedName] = feedName - return feedName - } - - let s = (feedName as NSString).substring(to: maxFeedNameLength) - truncatedFeedNameCache[feedName] = s - return s -} - -func timelineTruncatedTitle(_ article: Article) -> String { - - guard let title = article.title else { - return "" - } - - if let cachedTitle = truncatedTitleCache[title] as? String { - return cachedTitle - } - - var s = title.replacingOccurrences(of: "\n", with: "") - s = s.replacingOccurrences(of: "\r", with: "") - s = s.replacingOccurrences(of: "\t", with: "") - s = s.replacingOccurrences(of: "↦", with: "") - s = s.rs_stringByTrimmingWhitespace() - - let maxLength = 1000 - if s.count < maxLength { - truncatedTitleCache[title] = s - return s - } - - s = (s as NSString).substring(to: maxLength) - truncatedTitleCache[title] = s - return s -} - -func timelineTruncatedSummary(_ article: Article) -> String { - - return timelineSummaryForArticle(article) -} - -func timelineNormalizedText(_ text: String) -> String { - - if text.isEmpty { - return "" - } - if let cachedText = normalizedTextCache[text] as? String { - return cachedText - } - - var s = (text as NSString).rs_stringByTrimmingWhitespace() - s = s.rs_stringWithCollapsedWhitespace() - - let result = s as String - normalizedTextCache[text] = result - return result -} - -func timelineNormalizedTextTruncated(_ text: String) -> String { - - if text.isEmpty { - return "" - } - - if let cachedText = textCache[text] as? String { - return cachedText - } - - var s: NSString = (text as NSString).rsparser_stringByDecodingHTMLEntities() as NSString - s = s.rs_stringByTrimmingWhitespace() as NSString - s = s.rs_stringWithCollapsedWhitespace() as NSString - - let maxLength = 512 - if s.length > maxLength { - s = s.substring(to: maxLength) as NSString - } - - textCache[text] = String(s) - return s as String -} - - -func timelineSummaryForArticle(_ article: Article) -> String { - - guard let body = article.body else { - return "" - } - - if let cachedBody = summaryCache[body] as? String { - return cachedBody - } - - var s = body.rs_string(byStrippingHTML: 300) - s = timelineNormalizedText(s) - if s == "Comments" { // Hacker News. - s = "" - } - summaryCache[body] = s - return s -} - -private let dateFormatter: DateFormatter = { - - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .none - return formatter -}() - -private let timeFormatter: DateFormatter = { - - let formatter = DateFormatter() - formatter.dateStyle = .none - formatter.timeStyle = .short - return formatter -}() - -private var token: Int = 0 - -func timelineDateString(_ date: Date) -> String { - - if NSCalendar.rs_dateIsToday(date) { - return timeFormatter.string(from: date) - } - - return dateFormatter.string(from: date) -} -