2017-05-27 19:43:27 +02:00
|
|
|
|
//
|
|
|
|
|
// ArticleRenderer.swift
|
2018-08-29 07:18:24 +02:00
|
|
|
|
// NetNewsWire
|
2017-05-27 19:43:27 +02:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 9/8/15.
|
|
|
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import RSCore
|
2018-07-24 03:29:08 +02:00
|
|
|
|
import Articles
|
2018-07-28 21:16:14 +02:00
|
|
|
|
import Account
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
|
struct ArticleRenderer {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
|
|
|
|
|
private let article: Article?
|
|
|
|
|
private let articleStyle: ArticleStyle
|
|
|
|
|
private let title: String
|
2019-04-14 21:54:17 +02:00
|
|
|
|
private let baseURL: String?
|
2018-11-27 06:38:14 +01:00
|
|
|
|
|
2019-04-03 06:57:34 +02:00
|
|
|
|
private init(article: Article?, style: ArticleStyle) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
self.article = article
|
|
|
|
|
self.articleStyle = style
|
2018-11-27 06:38:14 +01:00
|
|
|
|
self.title = article?.title ?? ""
|
2019-04-14 21:54:17 +02:00
|
|
|
|
self.baseURL = article?.baseURL?.absoluteString
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2019-02-11 07:06:03 +01:00
|
|
|
|
|
|
|
|
|
// MARK: - API
|
|
|
|
|
|
2019-04-03 06:57:34 +02:00
|
|
|
|
static func articleHTML(article: Article, style: ArticleStyle) -> String {
|
|
|
|
|
let renderer = ArticleRenderer(article: article, style: style)
|
2019-02-12 07:36:31 +01:00
|
|
|
|
return renderer.articleHTML
|
2019-02-11 07:06:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-04-03 06:57:34 +02:00
|
|
|
|
static func multipleSelectionHTML(style: ArticleStyle) -> String {
|
|
|
|
|
let renderer = ArticleRenderer(article: nil, style: style)
|
2019-02-12 07:36:31 +01:00
|
|
|
|
return renderer.multipleSelectionHTML
|
2019-02-11 07:06:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-04-03 06:57:34 +02:00
|
|
|
|
static func noSelectionHTML(style: ArticleStyle) -> String {
|
|
|
|
|
let renderer = ArticleRenderer(article: nil, style: style)
|
2019-02-12 07:36:31 +01:00
|
|
|
|
return renderer.noSelectionHTML
|
|
|
|
|
}
|
2018-11-27 06:38:14 +01:00
|
|
|
|
}
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
|
// MARK: - Private
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
private extension ArticleRenderer {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
|
private var articleHTML: String {
|
|
|
|
|
let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: substitutions(), macroStart: "[[", macroEnd: "]]")
|
|
|
|
|
return renderHTML(withBody: body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var multipleSelectionHTML: String {
|
|
|
|
|
let body = "<h3 class='systemMessage'>Multiple selection</h3>"
|
|
|
|
|
return renderHTML(withBody: body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var noSelectionHTML: String {
|
|
|
|
|
let body = "<h3 class='systemMessage'>No selection</h3>"
|
|
|
|
|
return renderHTML(withBody: body)
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
static var faviconImgTagCache = [Feed: String]()
|
|
|
|
|
static var feedIconImgTagCache = [Feed: String]()
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
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"
|
|
|
|
|
}()
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
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
|
|
|
|
|
}()
|
2017-12-30 02:10:50 +01:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func styleString() -> String {
|
|
|
|
|
return articleStyle.css ?? ArticleRenderer.defaultStyleSheet
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func template() -> String {
|
|
|
|
|
return articleStyle.template ?? ArticleRenderer.defaultTemplate
|
2017-12-29 20:31:47 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func titleOrTitleLink() -> String {
|
2018-09-15 03:00:51 +02:00
|
|
|
|
if let link = article?.preferredLink {
|
2019-06-12 17:51:15 +02:00
|
|
|
|
return title.htmlByAddingLink(link)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2019-06-12 17:51:15 +02:00
|
|
|
|
return title
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func substitutions() -> [String: String] {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
var d = [String: String]()
|
|
|
|
|
|
2018-09-15 03:00:51 +02:00
|
|
|
|
guard let article = article else {
|
|
|
|
|
assertionFailure("Article should have been set before calling this function.")
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
let title = titleOrTitleLink()
|
2018-03-01 07:38:29 +01:00
|
|
|
|
d["title"] = title
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
let body = article.body ?? ""
|
2018-03-01 07:38:29 +01:00
|
|
|
|
d["body"] = body
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2017-12-30 02:10:50 +01:00
|
|
|
|
d["avatars"] = ""
|
2017-12-30 05:04:43 +01:00
|
|
|
|
var didAddAvatar = false
|
2017-12-30 05:41:41 +01:00
|
|
|
|
if let avatarHTML = avatarImgTag() {
|
2018-01-03 22:14:14 +01:00
|
|
|
|
d["avatars"] = "<td class=\"header rightAlign avatar\">\(avatarHTML)</td>";
|
2017-12-30 05:04:43 +01:00
|
|
|
|
didAddAvatar = true
|
2017-12-30 02:10:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
var feedLink = ""
|
|
|
|
|
if let feedTitle = article.feed?.nameForDisplay {
|
|
|
|
|
feedLink = feedTitle
|
|
|
|
|
if let feedURL = article.feed?.homePageURL {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
feedLink = feedLink.htmlByAddingLink(feedURL, className: "feedLink")
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
d["feedlink"] = feedLink
|
|
|
|
|
|
2017-12-30 05:04:43 +01:00
|
|
|
|
if !didAddAvatar, let feed = article.feed {
|
|
|
|
|
if let favicon = faviconImgTag(forFeed: feed) {
|
2018-01-03 22:14:14 +01:00
|
|
|
|
d["avatars"] = "<td class=\"header rightAlign\">\(favicon)</td>";
|
2017-12-30 05:04:43 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
let datePublished = article.logicalDatePublished
|
|
|
|
|
let longDate = dateString(datePublished, .long, .medium)
|
|
|
|
|
let mediumDate = dateString(datePublished, .medium, .short)
|
|
|
|
|
let shortDate = dateString(datePublished, .short, .short)
|
|
|
|
|
|
2018-09-03 21:50:24 +02:00
|
|
|
|
if dateShouldBeLink() || self.title == "", let permalink = article.url {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
d["date_long"] = longDate.htmlByAddingLink(permalink)
|
|
|
|
|
d["date_medium"] = mediumDate.htmlByAddingLink(permalink)
|
|
|
|
|
d["date_short"] = shortDate.htmlByAddingLink(permalink)
|
2017-12-30 03:59:04 +01:00
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
d["date_long"] = longDate
|
|
|
|
|
d["date_medium"] = mediumDate
|
|
|
|
|
d["date_short"] = shortDate
|
|
|
|
|
}
|
2017-12-29 20:31:47 +01:00
|
|
|
|
|
|
|
|
|
d["byline"] = byline()
|
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func dateShouldBeLink() -> Bool {
|
2018-09-15 03:00:51 +02:00
|
|
|
|
guard let permalink = article?.url else {
|
2018-09-03 21:50:24 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
2018-09-15 03:00:51 +02:00
|
|
|
|
guard let preferredLink = article?.preferredLink else { // Title uses preferredLink
|
2018-09-03 21:50:24 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return permalink != preferredLink // Make date a link if it’s a different link from the title’s link
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func faviconImgTag(forFeed feed: Feed) -> String? {
|
2017-12-30 05:04:43 +01:00
|
|
|
|
|
|
|
|
|
if let cachedImgTag = ArticleRenderer.faviconImgTagCache[feed] {
|
|
|
|
|
return cachedImgTag
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-15 11:05:11 +02:00
|
|
|
|
if let favicon = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
|
2019-09-15 11:00:24 +02:00
|
|
|
|
if let s = base64String(forImage: favicon) {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
var dimension = min(favicon.size.height, CGFloat(ArticleRenderer.avatarDimension)) // Assuming square images.
|
2018-02-26 07:46:25 +01:00
|
|
|
|
dimension = max(dimension, 16) // Some favicons say they’re < 16. Force them larger.
|
2018-11-27 06:38:14 +01:00
|
|
|
|
if dimension >= CGFloat(ArticleRenderer.avatarDimension) * 0.8 { //Close enough to scale up.
|
|
|
|
|
dimension = CGFloat(ArticleRenderer.avatarDimension)
|
2018-02-26 07:58:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let imgTag: String
|
2018-11-27 06:38:14 +01:00
|
|
|
|
if dimension >= CGFloat(ArticleRenderer.avatarDimension) {
|
2018-02-26 07:58:08 +01:00
|
|
|
|
// Use rounded corners.
|
|
|
|
|
imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=\(Int(dimension)) width=\(Int(dimension)) style=\"border-radius:4px\" />"
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=\(Int(dimension)) width=\(Int(dimension)) />"
|
|
|
|
|
}
|
2017-12-30 05:04:43 +01:00
|
|
|
|
ArticleRenderer.faviconImgTagCache[feed] = imgTag
|
|
|
|
|
return imgTag
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func feedIconImgTag(forFeed feed: Feed) -> String? {
|
2017-12-30 05:41:41 +01:00
|
|
|
|
if let cachedImgTag = ArticleRenderer.feedIconImgTagCache[feed] {
|
|
|
|
|
return cachedImgTag
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let icon = appDelegate.feedIconDownloader.icon(for: feed) {
|
2019-09-15 11:00:24 +02:00
|
|
|
|
if let s = base64String(forImage: icon) {
|
2019-04-15 22:03:05 +02:00
|
|
|
|
#if os(macOS)
|
2017-12-30 05:41:41 +01:00
|
|
|
|
let imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=48 width=48 />"
|
2019-04-15 22:03:05 +02:00
|
|
|
|
#else
|
|
|
|
|
let imgTag = "<img src=\"data:image/png;base64, " + s + "\" height=48 width=48 />"
|
|
|
|
|
#endif
|
2017-12-30 05:41:41 +01:00
|
|
|
|
ArticleRenderer.feedIconImgTagCache[feed] = imgTag
|
|
|
|
|
return imgTag
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
|
func base64String(forImage image: RSImage) -> String? {
|
2019-06-14 22:33:13 +02:00
|
|
|
|
return image.dataRepresentation()?.base64EncodedString()
|
2017-12-30 05:04:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func singleArticleSpecifiedAuthor() -> Author? {
|
2017-12-30 02:10:50 +01:00
|
|
|
|
// The author of this article, if just one.
|
2018-09-15 03:00:51 +02:00
|
|
|
|
if let authors = article?.authors, authors.count == 1 {
|
2017-12-30 02:10:50 +01:00
|
|
|
|
return authors.first!
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func singleFeedSpecifiedAuthor() -> Author? {
|
2018-09-15 03:00:51 +02:00
|
|
|
|
if let authors = article?.feed?.authors, authors.count == 1 {
|
2017-12-30 02:10:50 +01:00
|
|
|
|
return authors.first!
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-15 11:00:24 +02:00
|
|
|
|
static let avatarDimension = 48
|
2017-12-30 02:10:50 +01:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
struct Avatar {
|
|
|
|
|
let imageURL: String
|
|
|
|
|
let url: String?
|
2017-12-30 02:10:50 +01:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func html(dimension: Int) -> String {
|
|
|
|
|
let imageTag = "<img src=\"\(imageURL)\" width=\(dimension) height=\(dimension) />"
|
|
|
|
|
if let url = url {
|
|
|
|
|
return imageTag.htmlByAddingLink(url)
|
|
|
|
|
}
|
|
|
|
|
return imageTag
|
2017-12-30 02:10:50 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func avatarImgTag() -> String? {
|
2017-12-30 05:41:41 +01:00
|
|
|
|
if let author = singleArticleSpecifiedAuthor(), let imageURL = author.avatarURL {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension)
|
2017-12-30 05:41:41 +01:00
|
|
|
|
}
|
2018-09-15 03:00:51 +02:00
|
|
|
|
if let feed = article?.feed, let imgTag = feedIconImgTag(forFeed: feed) {
|
2017-12-30 05:41:41 +01:00
|
|
|
|
return imgTag
|
|
|
|
|
}
|
2018-09-15 03:00:51 +02:00
|
|
|
|
if let feedIconURL = article?.feed?.iconURL {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url).html(dimension: ArticleRenderer.avatarDimension)
|
2017-12-30 05:41:41 +01:00
|
|
|
|
}
|
|
|
|
|
if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension)
|
2017-12-30 05:41:41 +01:00
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
func byline() -> String {
|
2018-09-15 03:00:51 +02:00
|
|
|
|
guard let authors = article?.authors ?? article?.feed?.authors, !authors.isEmpty else {
|
2017-12-29 20:31:47 +01:00
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-07 20:46:00 +02:00
|
|
|
|
// 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 {
|
2018-09-15 03:00:51 +02:00
|
|
|
|
if author.name == article?.feed?.nameForDisplay {
|
2018-09-07 20:46:00 +02:00
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var byline = ""
|
2017-12-29 20:31:47 +01:00
|
|
|
|
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 {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
byline += name.htmlByAddingLink(url)
|
2017-12-29 20:31:47 +01:00
|
|
|
|
}
|
|
|
|
|
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 {
|
2018-11-27 06:38:14 +01:00
|
|
|
|
byline += String.htmlWithLink(url)
|
2017-12-29 20:31:47 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-30 02:10:50 +01:00
|
|
|
|
return byline
|
2017-12-29 20:31:47 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
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 {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2017-11-06 05:31:50 +01:00
|
|
|
|
var s = "<!DOCTYPE html><html><head>\n\n"
|
2019-04-14 21:54:17 +02:00
|
|
|
|
if let baseURL = baseURL {
|
|
|
|
|
s += ("<base href=\"" + baseURL + "\"\n>")
|
|
|
|
|
}
|
2018-11-27 06:38:14 +01:00
|
|
|
|
s += title.htmlBySurroundingWithTag("title")
|
|
|
|
|
s += styleString().htmlBySurroundingWithTag("style")
|
2017-11-06 05:31:50 +01:00
|
|
|
|
|
|
|
|
|
s += """
|
|
|
|
|
|
|
|
|
|
<script type="text/javascript">
|
2019-09-21 02:21:01 +02:00
|
|
|
|
var init = {
|
|
|
|
|
wrapFrames: function () {
|
|
|
|
|
document.querySelectorAll("iframe").forEach(element => {
|
|
|
|
|
var wrapper = document.createElement("div");
|
|
|
|
|
wrapper.classList.add("iframeWrap");
|
|
|
|
|
element.parentNode.insertBefore(wrapper, element);
|
|
|
|
|
wrapper.appendChild(element);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
stripStyles: function() {
|
|
|
|
|
document.getElementsByTagName("body")[0].querySelectorAll("style, link[rel=stylesheet]").forEach(element => element.remove());
|
|
|
|
|
document.getElementsByTagName("body")[0].querySelectorAll("[style]").forEach(element => element.removeAttribute("style"));
|
|
|
|
|
},
|
|
|
|
|
linkHover: function() {
|
|
|
|
|
var anchors = document.getElementsByTagName("a");
|
|
|
|
|
for (var i = 0; i < anchors.length; i++) {
|
|
|
|
|
anchors[i].addEventListener("mouseenter", function() { mouseDidEnterLink(this) });
|
|
|
|
|
anchors[i].addEventListener("mouseleave", function() { mouseDidExitLink(this) });
|
|
|
|
|
}
|
2017-11-06 05:31:50 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-21 02:21:01 +02:00
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function(event) {
|
|
|
|
|
Object.values(init).forEach(item => item());
|
|
|
|
|
})
|
|
|
|
|
|
2017-11-06 05:31:50 +01:00
|
|
|
|
function mouseDidEnterLink(anchor) {
|
|
|
|
|
window.webkit.messageHandlers.mouseDidEnter.postMessage(anchor.href);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mouseDidExitLink(anchor) {
|
|
|
|
|
window.webkit.messageHandlers.mouseDidExit.postMessage(anchor.href);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
"""
|
2018-09-10 20:39:41 +02:00
|
|
|
|
|
2019-09-21 02:21:01 +02:00
|
|
|
|
s += "\n\n</head><body>\n\n"
|
2018-09-15 03:00:51 +02:00
|
|
|
|
s += body
|
2017-11-06 05:31:50 +01:00
|
|
|
|
s += "\n\n</body></html>"
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2018-11-27 06:38:14 +01:00
|
|
|
|
//print(s)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
}
|
2019-04-15 22:03:05 +02:00
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2019-04-14 21:54:17 +02:00
|
|
|
|
|
|
|
|
|
// MARK: - Article extension
|
|
|
|
|
|
|
|
|
|
private extension Article {
|
|
|
|
|
|
|
|
|
|
var baseURL: URL? {
|
|
|
|
|
var s = url
|
|
|
|
|
if s == nil {
|
|
|
|
|
s = feed?.homePageURL
|
|
|
|
|
}
|
|
|
|
|
if s == nil {
|
|
|
|
|
s = 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|