// // ArticleRenderer.swift // NetNewsWire // // Created by Brent Simmons on 9/8/15. // Copyright © 2015 Ranchero Software, LLC. All rights reserved. // import Foundation import RSCore import Articles import Account struct ArticleRenderer { typealias Rendering = (style: String, html: String) typealias Page = (html: String, baseURL: URL) static var imageIconScheme = "nnwImageIcon" static var page: Page = { let pageURL = Bundle.main.url(forResource: "page", withExtension: "html")! let html = try! String(contentsOf: pageURL) let baseURL = pageURL.deletingLastPathComponent() return Page(html: html, baseURL: baseURL) }() private let article: Article? private let extractedArticle: ExtractedArticle? private let articleStyle: ArticleStyle private let title: String private let body: String private let baseURL: String? private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle) { self.article = article self.extractedArticle = extractedArticle self.articleStyle = style self.title = article?.title ?? "" if let content = extractedArticle?.content { self.body = content self.baseURL = extractedArticle?.url } else { self.body = article?.body ?? "" self.baseURL = article?.baseURL?.absoluteString } } // MARK: - API static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle, useImageIcon: Bool = false) -> Rendering { let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style) return (renderer.styleString(), renderer.articleHTML) } static func multipleSelectionHTML(style: ArticleStyle) -> Rendering { let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return (renderer.styleString(), renderer.multipleSelectionHTML) } static func loadingHTML(style: ArticleStyle) -> Rendering { let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return (renderer.styleString(), renderer.loadingHTML) } static func noSelectionHTML(style: ArticleStyle) -> Rendering { let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return (renderer.styleString(), renderer.noSelectionHTML) } static func noContentHTML(style: ArticleStyle) -> Rendering { let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return (renderer.styleString(), renderer.noContentHTML) } } // MARK: - Private private extension ArticleRenderer { private var articleHTML: String { let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions(), macroStart: "[[", macroEnd: "]]") return renderHTML(withBody: body) } private var multipleSelectionHTML: String { let body = "

Multiple selection

" return renderHTML(withBody: body) } private var loadingHTML: String { let body = "

Loading...

" return renderHTML(withBody: body) } private var noSelectionHTML: String { let body = "

No selection

" return renderHTML(withBody: body) } private var noContentHTML: String { return renderHTML(withBody: "") } 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 styleString() -> String { return articleStyle.css ?? ArticleRenderer.defaultStyleSheet } func template() -> String { return articleStyle.template ?? ArticleRenderer.defaultTemplate } func titleOrTitleLink() -> String { if let link = article?.preferredLink { return title.htmlByAddingLink(link) } return title } func articleSubstitutions() -> [String: String] { var d = [String: String]() guard let article = article else { assertionFailure("Article should have been set before calling this function.") return d } let title = titleOrTitleLink() d["title"] = title d["body"] = body d["avatars"] = ""; var feedLink = "" if let feedTitle = article.webFeed?.nameForDisplay { feedLink = feedTitle if let feedURL = article.webFeed?.homePageURL { feedLink = feedLink.htmlByAddingLink(feedURL, className: "feedLink") } } d["feedlink"] = feedLink 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"] = longDate.htmlByAddingLink(permalink) d["date_medium"] = mediumDate.htmlByAddingLink(permalink) d["date_short"] = shortDate.htmlByAddingLink(permalink) } else { d["date_long"] = longDate d["date_medium"] = mediumDate d["date_short"] = shortDate } d["byline"] = byline() return d } func dateShouldBeLink() -> Bool { guard let permalink = article?.url else { return false } guard let preferredLink = article?.preferredLink else { // Title uses preferredLink return false } return permalink != preferredLink // Make date a link if it’s a different link from the title’s link } func byline() -> String { guard let authors = article?.authors ?? article?.webFeed?.authors, !authors.isEmpty else { return "" } // If the author's name is the same as the feed, then we don't want to display it. // This code assumes that multiple authors would never match the feed name so that // if there feed owner has an article co-author all authors are given the byline. if authors.count == 1, let author = authors.first { if author.name == article?.webFeed?.nameForDisplay { 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 += name.htmlByAddingLink(url) } else if let name = author.name, let emailAddress = author.emailAddress { byline += "\(name) <\(emailAddress)≶" } 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 += String.htmlWithLink(url) } } return byline } 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) } func renderHTML(withBody body: String) -> String { var s = "" if let baseURL = baseURL { s += ("") } s += title.htmlBySurroundingWithTag("title") s += body return s } } // MARK: - Article extension private extension Article { var baseURL: URL? { var s = url if s == nil { s = webFeed?.homePageURL } if s == nil { s = webFeed?.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 } }