2017-07-02 02:22:19 +02:00
|
|
|
//
|
|
|
|
// Article.swift
|
2019-07-09 07:58:19 +02:00
|
|
|
// NetNewsWire
|
2017-07-02 02:22:19 +02:00
|
|
|
//
|
|
|
|
// Created by Brent Simmons on 7/1/17.
|
|
|
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
2019-07-06 05:06:31 +02:00
|
|
|
public typealias ArticleSetBlock = (Set<Article>) -> Void
|
|
|
|
|
2017-09-05 02:10:02 +02:00
|
|
|
public struct Article: Hashable {
|
2017-07-02 02:22:19 +02:00
|
|
|
|
2017-09-05 02:10:02 +02:00
|
|
|
public let articleID: String // Unique database ID (possibly sync service ID)
|
|
|
|
public let accountID: String
|
2019-11-15 03:11:41 +01:00
|
|
|
public let webFeedID: String // Likely a URL, but not necessarily
|
2017-07-12 21:55:48 +02:00
|
|
|
public let uniqueID: String // Unique per feed (RSS guid, for example)
|
2017-09-05 02:10:02 +02:00
|
|
|
public let title: String?
|
|
|
|
public let contentHTML: String?
|
|
|
|
public let contentText: String?
|
|
|
|
public let url: String?
|
|
|
|
public let externalURL: String?
|
|
|
|
public let summary: String?
|
|
|
|
public let imageURL: String?
|
|
|
|
public let datePublished: Date?
|
|
|
|
public let dateModified: Date?
|
|
|
|
public let authors: Set<Author>?
|
2017-09-18 21:59:42 +02:00
|
|
|
public let status: ArticleStatus
|
2018-12-28 06:07:34 +01:00
|
|
|
|
2019-12-29 05:53:36 +01:00
|
|
|
public init(accountID: String, articleID: String?, webFeedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, datePublished: Date?, dateModified: Date?, authors: Set<Author>?, status: ArticleStatus) {
|
2017-09-05 02:10:02 +02:00
|
|
|
self.accountID = accountID
|
2019-11-15 03:11:41 +01:00
|
|
|
self.webFeedID = webFeedID
|
2017-07-12 21:55:48 +02:00
|
|
|
self.uniqueID = uniqueID
|
2017-07-02 02:22:19 +02:00
|
|
|
self.title = title
|
|
|
|
self.contentHTML = contentHTML
|
|
|
|
self.contentText = contentText
|
|
|
|
self.url = url
|
|
|
|
self.externalURL = externalURL
|
|
|
|
self.summary = summary
|
|
|
|
self.imageURL = imageURL
|
|
|
|
self.datePublished = datePublished
|
|
|
|
self.dateModified = dateModified
|
|
|
|
self.authors = authors
|
2017-09-18 21:59:42 +02:00
|
|
|
self.status = status
|
|
|
|
|
2017-08-21 06:23:17 +02:00
|
|
|
if let articleID = articleID {
|
|
|
|
self.articleID = articleID
|
2017-07-17 04:36:38 +02:00
|
|
|
}
|
|
|
|
else {
|
2019-11-15 03:11:41 +01:00
|
|
|
self.articleID = Article.calculatedArticleID(webFeedID: webFeedID, uniqueID: uniqueID)
|
2017-07-17 04:36:38 +02:00
|
|
|
}
|
2017-07-02 02:22:19 +02:00
|
|
|
}
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
public static func calculatedArticleID(webFeedID: String, uniqueID: String) -> String {
|
|
|
|
return databaseIDWithString("\(webFeedID) \(uniqueID)")
|
2017-09-19 07:00:35 +02:00
|
|
|
}
|
2018-12-28 06:07:34 +01:00
|
|
|
|
|
|
|
// MARK: - Hashable
|
|
|
|
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
|
|
hasher.combine(articleID)
|
|
|
|
}
|
2020-04-30 20:42:52 +02:00
|
|
|
|
|
|
|
// MARK: - Equatable
|
|
|
|
|
|
|
|
static public func ==(lhs: Article, rhs: Article) -> Bool {
|
2020-04-30 23:26:26 +02:00
|
|
|
return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors
|
2020-04-30 20:42:52 +02:00
|
|
|
}
|
2017-07-02 02:22:19 +02:00
|
|
|
}
|
|
|
|
|
2017-10-09 06:06:25 +02:00
|
|
|
public extension Set where Element == Article {
|
|
|
|
|
2019-02-12 16:04:18 +01:00
|
|
|
func articleIDs() -> Set<String> {
|
2017-10-09 06:06:25 +02:00
|
|
|
return Set<String>(map { $0.articleID })
|
|
|
|
}
|
2018-02-10 22:21:43 +01:00
|
|
|
|
2019-02-12 16:04:18 +01:00
|
|
|
func unreadArticles() -> Set<Article> {
|
2018-02-10 22:21:43 +01:00
|
|
|
let articles = self.filter { !$0.status.read }
|
|
|
|
return Set(articles)
|
|
|
|
}
|
2019-11-16 01:26:52 +01:00
|
|
|
|
|
|
|
func contains(accountID: String, articleID: String) -> Bool {
|
|
|
|
return contains(where: { $0.accountID == accountID && $0.articleID == articleID})
|
|
|
|
}
|
|
|
|
|
2017-10-09 06:06:25 +02:00
|
|
|
}
|
2017-09-05 02:10:02 +02:00
|
|
|
|
2017-10-09 06:06:25 +02:00
|
|
|
public extension Array where Element == Article {
|
|
|
|
|
2019-02-12 16:04:18 +01:00
|
|
|
func articleIDs() -> [String] {
|
2017-10-09 06:06:25 +02:00
|
|
|
return map { $0.articleID }
|
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:04:50 +02:00
|
|
|
|
|
|
|
public extension Article {
|
2020-04-10 22:33:16 +02:00
|
|
|
static let allowedTags: Set = ["b", "bdi", "bdo", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", /* "rb", "rp", "rt", "rtc", "ruby", */ "s", "samp", "small", "strong", "sub", "sup", "time", "u", "var"]
|
2020-04-07 23:04:50 +02:00
|
|
|
|
|
|
|
func sanitizedTitle(forHTML: Bool = true) -> String? {
|
|
|
|
guard let title = title else { return nil }
|
|
|
|
|
|
|
|
let scanner = Scanner(string: title)
|
|
|
|
scanner.charactersToBeSkipped = nil
|
|
|
|
var result = ""
|
|
|
|
result.reserveCapacity(title.count)
|
|
|
|
|
|
|
|
while !scanner.isAtEnd {
|
|
|
|
if let text = scanner.scanUpToString("<") {
|
|
|
|
result.append(text)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let _ = scanner.scanString("<") {
|
|
|
|
// All the allowed tags currently don't allow attributes
|
|
|
|
if let tag = scanner.scanUpToString(">") {
|
|
|
|
if Self.allowedTags.contains(tag.replacingOccurrences(of: "/", with: "")) {
|
|
|
|
forHTML ? result.append("<\(tag)>") : result.append("")
|
|
|
|
} else {
|
|
|
|
forHTML ? result.append("<\(tag)>") : result.append("<\(tag)>")
|
|
|
|
}
|
|
|
|
|
|
|
|
let _ = scanner.scanString(">")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|