// // ArticleRenderer.swift // Evergreen // // Created by Brent Simmons on 9/8/15. // Copyright © 2015 Ranchero Software, LLC. All rights reserved. // import Foundation import RSCore import Data 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 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 }() var html: String { return renderedHTML() } init(article: Article, style: ArticleStyle) { self.article = article self.articleStyle = style } // MARK: Private private func textInsideTag(_ text: String, _ tag: String) -> String { return "<\(tag)>\(text)" } private func styleString() -> String { if let s = articleStyle.css { return s } 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" } return cachedStyleString } 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 } 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 { if let link = article.preferredLink { return linkWithText(title, link) } return title } private func substitutions() -> [String: String] { var d = [String: String]() let title = titleOrTitleLink() d["newsitem_title"] = title d["article_title"] = title let body = article.body == nil ? "" : article.body d["article_description"] = body d["newsitem_description"] = body d["avatars"] = "" var didAddAvatar = false if let avatarHTML = avatarImgTag() { // d["avatars"] = avatarHTML d["avatars"] = "\(avatarHTML)"; didAddAvatar = true } var feedLink = "" if let feedTitle = article.feed?.nameForDisplay { feedLink = feedTitle if let feedURL = article.feed?.homePageURL { feedLink = linkWithTextAndClass(feedTitle, feedURL, "feedLink") } } d["feedlink"] = feedLink d["feedlink_withfavicon"] = feedLink // d["favicon"] = "" if !didAddAvatar, let feed = article.feed { if let favicon = faviconImgTag(forFeed: feed) { d["avatars"] = "\(favicon)"; // d["favicon"] = favicon } } let longDate = longDateFormatter.string(from: article.logicalDatePublished) let mediumDate = mediumDateFormatter.string(from: article.logicalDatePublished) let shortDate = shortDateFormatter.string(from: article.logicalDatePublished) if let permalink = article.url { d["date_long"] = linkWithText(longDate, permalink) d["date_medium"] = linkWithText(mediumDate, permalink) d["date_short"] = linkWithText(shortDate, permalink) } else { d["date_long"] = longDate d["date_medium"] = mediumDate d["date_short"] = shortDate } d["byline"] = byline() // d["author_avatar"] = authorAvatar() return d } struct Avatar { let imageURL: String let url: String? func html(dimension: Int) -> String { let imageTag = " String? { if let cachedImgTag = ArticleRenderer.faviconImgTagCache[feed] { return cachedImgTag } if let favicon = appDelegate.faviconDownloader.favicon(for: feed) { if let s = base64String(forImage: favicon) { let imgTag = "" ArticleRenderer.faviconImgTagCache[feed] = imgTag return imgTag } } return nil } private func feedIconImgTag(forFeed feed: Feed) -> String? { if let cachedImgTag = ArticleRenderer.feedIconImgTagCache[feed] { return cachedImgTag } if let icon = appDelegate.feedIconDownloader.icon(for: feed) { if let s = base64String(forImage: icon) { let imgTag = "" ArticleRenderer.feedIconImgTagCache[feed] = imgTag return imgTag } } return nil } private func base64String(forImage image: NSImage) -> String? { let d = image.tiffRepresentation return d?.base64EncodedString() } private 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? { if let authors = article.feed?.authors, authors.count == 1 { return authors.first! } return nil } private func feedAvatar() -> Avatar? { guard let feedIconURL = article.feed?.iconURL else { return nil } return Avatar(imageURL: feedIconURL, url: article.feed?.homePageURL ?? article.feed?.url) } private func authorAvatar() -> Avatar? { 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) } 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) } if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL { return Avatar(imageURL: imageURL, url: author.url).html(dimension: 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 { guard let authors = article.authors ?? article.feed?.authors, !authors.isEmpty else { return "" } var byline = "" var isFirstAuthor = true for author in authors { if !isFirstAuthor { byline += ", " } isFirstAuthor = false if let emailAddress = author.emailAddress, emailAddress.contains(" ") { byline += emailAddress // probably name plus email address } else if let name = author.name, let url = author.url { byline += linkWithText(name, 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 } else if let emailAddress = author.emailAddress { byline += "<\(emailAddress)>" // TODO: mailto link } else if let url = author.url { byline += linkWithLink(url) } } return byline } private func renderedHTML() -> String { var s = "\n\n" s += textInsideTag(title, "title") s += textInsideTag(styleString(), "style") s += """ """ s += "\n\n\n\n" s += RSMacroProcessor.renderedText(withTemplate: template(), substitutions: substitutions(), macroStart: "[[", macroEnd: "]]") s += "\n\n" // print(s) return s } }