diff --git a/NetNewsWire/MainWindow/Detail/ArticleRenderer.swift b/NetNewsWire/MainWindow/Detail/ArticleRenderer.swift
index 2d26b5d0c..d2c6e879f 100644
--- a/NetNewsWire/MainWindow/Detail/ArticleRenderer.swift
+++ b/NetNewsWire/MainWindow/Detail/ArticleRenderer.swift
@@ -11,78 +11,9 @@ import RSCore
import Articles
import Account
-var cachedStyleString = ""
-var cachedTemplate = ""
-
-// NOTE: THIS CODE IS A TOTAL MESS RIGHT NOW WHILE WE’RE EXPERIMENTING WITH DIFFERENT LAYOUTS. DON’T JUDGE, YOU!
-
class ArticleRenderer {
- let article: Article?
- let articleStyle: ArticleStyle
- let appearance: NSAppearance?
-
- static var faviconImgTagCache = [Feed: String]()
- static var feedIconImgTagCache = [Feed: String]()
-
- lazy var longDateFormatter: DateFormatter = {
- let dateFormatter = DateFormatter()
- dateFormatter.dateStyle = .long
- dateFormatter.timeStyle = .medium
- return dateFormatter
- }()
-
- lazy var mediumDateFormatter: DateFormatter = {
- let dateFormatter = DateFormatter()
- dateFormatter.dateStyle = .medium
- dateFormatter.timeStyle = .short
- return dateFormatter
- }()
-
- lazy var shortDateFormatter: DateFormatter = {
- let dateFormatter = DateFormatter()
- dateFormatter.dateStyle = .short
- dateFormatter.timeStyle = .short
- return dateFormatter
- }()
-
- lazy var title: String = {
- if let articleTitle = self.article?.title {
- return articleTitle
- }
-
- return ""
- }()
-
- lazy var baseURL: URL? = {
-
- var s = self.article?.url
- if s == nil {
- s = self.article?.feed?.homePageURL
- }
- if s == nil {
- s = self.article?.feed?.url
- }
- if s == nil {
- return nil
- }
-
- var urlComponents = URLComponents(string: s!)
- if urlComponents == nil {
- return nil
- }
-
- // Can’t use url-with-fragment as base URL. The webview won’t load. See scripting.com/rss.xml for example.
- urlComponents!.fragment = nil
-
- if let url = urlComponents!.url {
- if url.scheme == "http" || url.scheme == "https" {
- return url
- }
- }
-
- return nil
- }()
+ let baseURL: URL?
var articleHTML: String {
let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: substitutions(), macroStart: "[[", macroEnd: "]]")
@@ -93,86 +24,70 @@ class ArticleRenderer {
let body = "
Multiple selection
"
return renderHTML(withBody: body)
}
-
+
var noSelectionHTML: String {
let body = "No selection
"
return renderHTML(withBody: body)
}
-
+
+ private let article: Article?
+ private let articleStyle: ArticleStyle
+ private let appearance: NSAppearance?
+ private let title: String
+
init(article: Article?, style: ArticleStyle, appearance: NSAppearance? = nil) {
self.article = article
self.articleStyle = style
self.appearance = appearance
- }
-
- // MARK: Private
-
- private func textInsideTag(_ text: String, _ tag: String) -> String {
-
- return "<\(tag)>\(text)\(tag)>"
- }
-
- private func styleString() -> String {
-
- if let s = articleStyle.css {
- return s
+ self.title = article?.title ?? ""
+ if let article = article {
+ self.baseURL = ArticleRenderer.baseURL(for: article)
}
-
- if cachedStyleString.isEmpty {
-
- let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
- let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
- cachedStyleString = "\n\(s)\n"
+ else {
+ self.baseURL = nil
}
+ }
+}
- return cachedStyleString
+// MARK: Private
+
+private extension ArticleRenderer {
+
+ static var faviconImgTagCache = [Feed: String]()
+ static var feedIconImgTagCache = [Feed: String]()
+
+ static var defaultStyleSheet: String = {
+ let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
+ let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
+ return "\n\(s)\n"
+ }()
+
+ static let defaultTemplate: String = {
+ let path = Bundle.main.path(forResource: "template", ofType: "html")!
+ let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
+ return s as String
+ }()
+
+// func textInsideTag(_ text: String, _ tag: String) -> String {
+// return "<\(tag)>\(text)\(tag)>"
+// }
+
+ func styleString() -> String {
+ return articleStyle.css ?? ArticleRenderer.defaultStyleSheet
}
- private func template() -> String {
-
- if let s = articleStyle.template {
- return s
- }
-
- if cachedTemplate.isEmpty {
- let path = Bundle.main.path(forResource: "template", ofType: "html")!
- let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
- cachedTemplate = s as String
- }
-
- return cachedTemplate
+ func template() -> String {
+ return articleStyle.template ?? ArticleRenderer.defaultTemplate
}
- private func linkWithTextAndClass(_ text: String, _ href: String, _ className: String) -> String {
-
- return "\(text)"
- }
-
- private func linkWithText(_ text: String, _ href: String) -> String {
-
- return ArticleRenderer.linkWithText(text, href)
- }
-
- private static func linkWithText(_ text: String, _ href: String) -> String {
-
- return "\(text)"
- }
-
- private func linkWithLink(_ href: String) -> String {
-
- return linkWithText(href, href)
- }
-
- private func titleOrTitleLink() -> String {
-
+ func titleOrTitleLink() -> String {
if let link = article?.preferredLink {
- return linkWithText(title, link)
+ return title.htmlByAddingLink(link)
}
return title
}
- private func substitutions() -> [String: String] {
-
+ func substitutions() -> [String: String] {
var d = [String: String]()
guard let article = article else {
@@ -183,13 +98,12 @@ class ArticleRenderer {
let title = titleOrTitleLink()
d["title"] = title
- let body = article.body == nil ? "" : article.body
+ let body = article.body ?? ""
d["body"] = body
d["avatars"] = ""
var didAddAvatar = false
if let avatarHTML = avatarImgTag() {
-// d["avatars"] = avatarHTML
d["avatars"] = "";
didAddAvatar = true
}
@@ -198,27 +112,26 @@ class ArticleRenderer {
if let feedTitle = article.feed?.nameForDisplay {
feedLink = feedTitle
if let feedURL = article.feed?.homePageURL {
- feedLink = linkWithTextAndClass(feedTitle, feedURL, "feedLink")
+ feedLink = feedLink.htmlByAddingLink(feedURL, className: "feedLink")
}
}
d["feedlink"] = feedLink
- d["feedlink_withfavicon"] = feedLink
-// d["favicon"] = ""
if !didAddAvatar, let feed = article.feed {
if let favicon = faviconImgTag(forFeed: feed) {
d["avatars"] = "";
-// d["favicon"] = favicon
}
}
- let longDate = longDateFormatter.string(from: article.logicalDatePublished)
- let mediumDate = mediumDateFormatter.string(from: article.logicalDatePublished)
- let shortDate = shortDateFormatter.string(from: article.logicalDatePublished)
+ let datePublished = article.logicalDatePublished
+ let longDate = dateString(datePublished, .long, .medium)
+ let mediumDate = dateString(datePublished, .medium, .short)
+ let shortDate = dateString(datePublished, .short, .short)
+
if dateShouldBeLink() || self.title == "", let permalink = article.url {
- d["date_long"] = linkWithText(longDate, permalink)
- d["date_medium"] = linkWithText(mediumDate, permalink)
- d["date_short"] = linkWithText(shortDate, permalink)
+ d["date_long"] = longDate.htmlByAddingLink(permalink)
+ d["date_medium"] = mediumDate.htmlByAddingLink(permalink)
+ d["date_short"] = shortDate.htmlByAddingLink(permalink)
}
else {
d["date_long"] = longDate
@@ -227,12 +140,11 @@ class ArticleRenderer {
}
d["byline"] = byline()
- // d["author_avatar"] = authorAvatar()
return d
}
- private func dateShouldBeLink() -> Bool {
+ func dateShouldBeLink() -> Bool {
guard let permalink = article?.url else {
return false
}
@@ -242,21 +154,7 @@ class ArticleRenderer {
return permalink != preferredLink // Make date a link if it’s a different link from the title’s link
}
- struct Avatar {
- let imageURL: String
- let url: String?
-
- func html(dimension: Int) -> String {
-
- let imageTag = "
"
- if let url = url {
- return linkWithText(imageTag, url)
- }
- return imageTag
- }
- }
-
- private func faviconImgTag(forFeed feed: Feed) -> String? {
+ func faviconImgTag(forFeed feed: Feed) -> String? {
if let cachedImgTag = ArticleRenderer.faviconImgTagCache[feed] {
return cachedImgTag
@@ -264,14 +162,14 @@ class ArticleRenderer {
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
if let s = base64String(forImage: favicon) {
- var dimension = min(favicon.size.height, CGFloat(avatarDimension)) // Assuming square images.
+ var dimension = min(favicon.size.height, CGFloat(ArticleRenderer.avatarDimension)) // Assuming square images.
dimension = max(dimension, 16) // Some favicons say they’re < 16. Force them larger.
- if dimension >= CGFloat(avatarDimension) * 0.8 { //Close enough to scale up.
- dimension = CGFloat(avatarDimension)
+ if dimension >= CGFloat(ArticleRenderer.avatarDimension) * 0.8 { //Close enough to scale up.
+ dimension = CGFloat(ArticleRenderer.avatarDimension)
}
let imgTag: String
- if dimension >= CGFloat(avatarDimension) {
+ if dimension >= CGFloat(ArticleRenderer.avatarDimension) {
// Use rounded corners.
imgTag = "
"
}
@@ -286,8 +184,7 @@ class ArticleRenderer {
return nil
}
- private func feedIconImgTag(forFeed feed: Feed) -> String? {
-
+ func feedIconImgTag(forFeed feed: Feed) -> String? {
if let cachedImgTag = ArticleRenderer.feedIconImgTagCache[feed] {
return cachedImgTag
}
@@ -303,116 +200,57 @@ class ArticleRenderer {
return nil
}
- private func base64String(forImage image: NSImage) -> String? {
-
-
- let d = image.tiffRepresentation
- return d?.base64EncodedString()
+ func base64String(forImage image: NSImage) -> String? {
+ return image.tiffRepresentation?.base64EncodedString()
}
- private func singleArticleSpecifiedAuthor() -> Author? {
-
+ func singleArticleSpecifiedAuthor() -> Author? {
// The author of this article, if just one.
-
if let authors = article?.authors, authors.count == 1 {
return authors.first!
}
return nil
}
- private func singleFeedSpecifiedAuthor() -> Author? {
-
+ func singleFeedSpecifiedAuthor() -> Author? {
if let authors = article?.feed?.authors, authors.count == 1 {
return authors.first!
}
return nil
}
- private func feedAvatar() -> Avatar? {
+ static let avatarDimension = 48
- guard let feedIconURL = article?.feed?.iconURL else {
- return nil
+ struct Avatar {
+ let imageURL: String
+ let url: String?
+
+ func html(dimension: Int) -> String {
+ let imageTag = "
"
+ if let url = url {
+ return imageTag.htmlByAddingLink(url)
+ }
+ return imageTag
}
- return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url)
}
- private func authorAvatar() -> Avatar? {
-
+ func avatarImgTag() -> String? {
if let author = singleArticleSpecifiedAuthor(), let imageURL = author.avatarURL {
- return Avatar(imageURL: imageURL, url: author.url)
- }
- if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL {
- return Avatar(imageURL: imageURL, url: author.url)
- }
- return nil
- }
-
- private func avatarsToShow() -> [Avatar]? {
-
- var avatars = [Avatar]()
- if let avatar = feedAvatar() {
- avatars.append(avatar)
- }
- if let avatar = authorAvatar() {
- avatars.append(avatar)
- }
- return avatars.isEmpty ? nil : avatars
- }
-
- private func avatarToUse() -> Avatar? {
-
- // Use author if article specifies an author, otherwise use feed icon.
- // If no feed icon, use feed-specified author.
-
- if let author = singleArticleSpecifiedAuthor(), let imageURL = author.avatarURL {
- return Avatar(imageURL: imageURL, url: author.url)
- }
- if let feedIconURL = article?.feed?.iconURL {
- return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url)
- }
- if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL {
- return Avatar(imageURL: imageURL, url: author.url)
- }
- return nil
- }
-
- private let avatarDimension = 48
-
- private func avatarImgTag() -> String? {
-
- if let author = singleArticleSpecifiedAuthor(), let imageURL = author.avatarURL {
- return Avatar(imageURL: imageURL, url: author.url).html(dimension: avatarDimension)
+ return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension)
}
if let feed = article?.feed, let imgTag = feedIconImgTag(forFeed: feed) {
return imgTag
}
if let feedIconURL = article?.feed?.iconURL {
- return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url).html(dimension: avatarDimension)
+ return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url).html(dimension: ArticleRenderer.avatarDimension)
}
if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL {
- return Avatar(imageURL: imageURL, url: author.url).html(dimension: avatarDimension)
+ return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension)
}
return nil
}
-// private func authorAvatar() -> String {
-//
-// guard let authors = article.authors, authors.count == 1, let author = authors.first else {
-// return ""
-// }
-// guard let avatarURL = author.avatarURL else {
-// return ""
-// }
-//
-// var imageTag = "
"
-// if let authorURL = author.url {
-// imageTag = linkWithText(imageTag, authorURL)
-// }
-// return "\(imageTag)
"
-// }
-
- private func byline() -> String {
-
+ func byline() -> String {
guard let authors = article?.authors ?? article?.feed?.authors, !authors.isEmpty else {
return ""
}
@@ -439,11 +277,10 @@ class ArticleRenderer {
byline += emailAddress // probably name plus email address
}
else if let name = author.name, let url = author.url {
- byline += linkWithText(name, url)
+ byline += name.htmlByAddingLink(url)
}
else if let name = author.name, let emailAddress = author.emailAddress {
byline += "\(name) <\(emailAddress)≶"
-// byline += linkWithText(name, "mailto:\(emailAddress)") //TODO
}
else if let name = author.name {
byline += name
@@ -452,19 +289,50 @@ class ArticleRenderer {
byline += "<\(emailAddress)>" // TODO: mailto link
}
else if let url = author.url {
- byline += linkWithLink(url)
+ byline += String.htmlWithLink(url)
}
}
return byline
-
}
- private func renderHTML(withBody body: String) -> String {
+ func dateString(_ date: Date, _ dateStyle: DateFormatter.Style, _ timeStyle: DateFormatter.Style) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = dateStyle
+ dateFormatter.timeStyle = timeStyle
+ return dateFormatter.string(from: date)
+ }
+
+ static func baseURL(for article: Article) -> URL? {
+ var s = article.url
+ if s == nil {
+ s = article.feed?.homePageURL
+ }
+ if s == nil {
+ s = article.feed?.url
+ }
+
+ guard let urlString = s else {
+ return nil
+ }
+ var urlComponents = URLComponents(string: urlString)
+ if urlComponents == nil {
+ return nil
+ }
+
+ // Can’t use url-with-fragment as base URL. The webview won’t load. See scripting.com/rss.xml for example.
+ urlComponents!.fragment = nil
+ guard let url = urlComponents!.url, url.scheme == "http" || url.scheme == "https" else {
+ return nil
+ }
+ return url
+ }
+
+ func renderHTML(withBody body: String) -> String {
var s = "\n\n"
- s += textInsideTag(title, "title")
- s += textInsideTag(styleString(), "style")
+ s += title.htmlBySurroundingWithTag("title")
+ s += styleString().htmlBySurroundingWithTag("style")
s += """
@@ -492,15 +360,12 @@ class ArticleRenderer {
let appearanceClass = appearance?.isDarkMode ?? false ? "dark" : "light"
s += "\n\n\n\n"
-
s += body
-
s += "\n\n"
- //print(s)
+ //print(s)
return s
}
-
}