Fix lint issues.
This commit is contained in:
parent
d210692d7d
commit
72a5e46dcc
@ -8,7 +8,7 @@ let package = Package(
|
||||
products: [
|
||||
.library(
|
||||
name: "Parser",
|
||||
targets: ["Parser"]),
|
||||
targets: ["Parser"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../RSCore")
|
||||
@ -26,6 +26,6 @@ let package = Package(
|
||||
"Parser"
|
||||
],
|
||||
resources: [.copy("Resources")]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ public enum FeedType: Sendable {
|
||||
return .unknown
|
||||
}
|
||||
let cCharPointer = baseAddress.assumingMemoryBound(to: CChar.self)
|
||||
|
||||
|
||||
if isProbablyJSON(cCharPointer, count) {
|
||||
|
||||
if isPartialData {
|
||||
|
@ -160,7 +160,7 @@ private extension JSONFeedParser {
|
||||
let dateModified = parseDate(itemDictionary[Key.dateModified] as? String)
|
||||
|
||||
let authors = parseAuthors(itemDictionary)
|
||||
var tags: Set<String>? = nil
|
||||
var tags: Set<String>?
|
||||
if let tagsArray = itemDictionary[Key.tags] as? [String] {
|
||||
tags = Set(tagsArray)
|
||||
}
|
||||
|
@ -53,8 +53,7 @@ public struct RSSInJSONParser {
|
||||
|
||||
return ParsedFeed(type: .rssInJSON, title: title, homePageURL: homePageURL, feedURL: feedURL, language: feedLanguage, feedDescription: feedDescription, nextURL: nil, iconURL: nil, faviconURL: nil, authors: nil, expired: false, hubs: nil, items: items)
|
||||
|
||||
}
|
||||
catch { throw error }
|
||||
} catch { throw error }
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +61,7 @@ private extension RSSInJSONParser {
|
||||
|
||||
static func parseItems(_ itemsObject: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
|
||||
|
||||
return Set(itemsObject.compactMap{ (oneItemDictionary) -> ParsedItem? in
|
||||
return Set(itemsObject.compactMap { (oneItemDictionary) -> ParsedItem? in
|
||||
|
||||
return parsedItemWithDictionary(oneItemDictionary, feedURL)
|
||||
})
|
||||
@ -74,7 +73,7 @@ private extension RSSInJSONParser {
|
||||
let title = itemDictionary["title"] as? String
|
||||
|
||||
var contentHTML = itemDictionary["description"] as? String
|
||||
var contentText: String? = nil
|
||||
var contentText: String?
|
||||
if contentHTML != nil && !(contentHTML!.contains("<")) {
|
||||
contentText = contentHTML
|
||||
contentHTML = nil
|
||||
@ -83,7 +82,7 @@ private extension RSSInJSONParser {
|
||||
return nil
|
||||
}
|
||||
|
||||
var datePublished: Date? = nil
|
||||
var datePublished: Date?
|
||||
if let datePublishedString = itemDictionary["pubDate"] as? String {
|
||||
datePublished = DateParser.date(string: datePublishedString)
|
||||
}
|
||||
@ -150,9 +149,8 @@ private extension RSSInJSONParser {
|
||||
return Set([oneTag])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
else if let categoryArray = itemDictionary["category"] as? JSONArray {
|
||||
return Set(categoryArray.compactMap{ $0["#value"] as? String })
|
||||
} else if let categoryArray = itemDictionary["category"] as? JSONArray {
|
||||
return Set(categoryArray.compactMap { $0["#value"] as? String })
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ public final class ParsedAttachment: Hashable, Sendable {
|
||||
if url.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
self.url = url
|
||||
self.mimeType = mimeType
|
||||
self.title = title
|
||||
|
@ -14,7 +14,7 @@ public final class ParsedAuthor: Hashable, Codable, Sendable {
|
||||
public let url: String?
|
||||
public let avatarURL: String?
|
||||
public let emailAddress: String?
|
||||
|
||||
|
||||
public init(name: String?, url: String?, avatarURL: String?, emailAddress: String?) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
@ -39,17 +39,13 @@ public final class ParsedAuthor: Hashable, Codable, Sendable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
if let name {
|
||||
hasher.combine(name)
|
||||
}
|
||||
else if let url {
|
||||
} else if let url {
|
||||
hasher.combine(url)
|
||||
}
|
||||
else if let emailAddress {
|
||||
} else if let emailAddress {
|
||||
hasher.combine(emailAddress)
|
||||
}
|
||||
else if let avatarURL{
|
||||
} else if let avatarURL {
|
||||
hasher.combine(avatarURL)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
hasher.combine("")
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ public final class ParsedHub: Hashable, Sendable {
|
||||
self.type = type
|
||||
self.url = url
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
|
@ -10,8 +10,8 @@ import Foundation
|
||||
|
||||
public final class ParsedItem: Hashable, Sendable {
|
||||
|
||||
public let syncServiceID: String? //Nil when not syncing
|
||||
public let uniqueID: String //RSS guid, for instance; may be calculated
|
||||
public let syncServiceID: String? // Nil when not syncing
|
||||
public let uniqueID: String // RSS guid, for instance; may be calculated
|
||||
public let feedURL: String
|
||||
public let url: String?
|
||||
public let externalURL: String?
|
||||
@ -27,12 +27,12 @@ public final class ParsedItem: Hashable, Sendable {
|
||||
public let authors: Set<ParsedAuthor>?
|
||||
public let tags: Set<String>?
|
||||
public let attachments: Set<ParsedAttachment>?
|
||||
|
||||
|
||||
public init(syncServiceID: String?, uniqueID: String, feedURL: String, url: String?, externalURL: String?, title: String?,
|
||||
language: String?, contentHTML: String?, contentText: String?, summary: String?, imageURL: String?,
|
||||
bannerImageURL: String?,datePublished: Date?, dateModified: Date?, authors: Set<ParsedAuthor>?,
|
||||
bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: Set<ParsedAuthor>?,
|
||||
tags: Set<String>?, attachments: Set<ParsedAttachment>?) {
|
||||
|
||||
|
||||
self.syncServiceID = syncServiceID
|
||||
self.uniqueID = uniqueID
|
||||
self.feedURL = feedURL
|
||||
@ -57,8 +57,7 @@ public final class ParsedItem: Hashable, Sendable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
if let syncServiceID = syncServiceID {
|
||||
hasher.combine(syncServiceID)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
hasher.combine(uniqueID)
|
||||
hasher.combine(feedURL)
|
||||
}
|
||||
@ -69,4 +68,3 @@ public final class ParsedItem: Hashable, Sendable {
|
||||
lhs.syncServiceID == rhs.syncServiceID && lhs.uniqueID == rhs.uniqueID && lhs.feedURL == rhs.feedURL && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.title == rhs.title && lhs.language == rhs.language && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.bannerImageURL == rhs.bannerImageURL && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors && lhs.tags == rhs.tags && lhs.attachments == rhs.attachments
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,29 +181,17 @@ private extension AtomParser {
|
||||
|
||||
if SAXEqualTags(localName, XMLName.id) {
|
||||
currentArticle.guid = currentString(saxParser)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.title) {
|
||||
} else if SAXEqualTags(localName, XMLName.title) {
|
||||
currentArticle.title = currentString(saxParser)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.content) {
|
||||
} else if SAXEqualTags(localName, XMLName.content) {
|
||||
addContent(saxParser, currentArticle)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.summary) {
|
||||
} else if SAXEqualTags(localName, XMLName.summary) {
|
||||
addSummary(saxParser, currentArticle)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.link) {
|
||||
} else if SAXEqualTags(localName, XMLName.link) {
|
||||
addLink(currentArticle)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.published) {
|
||||
} else if SAXEqualTags(localName, XMLName.published) {
|
||||
currentArticle.datePublished = currentDate(saxParser)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.updated) {
|
||||
} else if SAXEqualTags(localName, XMLName.updated) {
|
||||
currentArticle.dateModified = currentDate(saxParser)
|
||||
}
|
||||
|
||||
@ -212,8 +200,7 @@ private extension AtomParser {
|
||||
if currentArticle.datePublished == nil {
|
||||
currentArticle.datePublished = currentDate(saxParser)
|
||||
}
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.modified) {
|
||||
} else if SAXEqualTags(localName, XMLName.modified) {
|
||||
if currentArticle.dateModified == nil {
|
||||
currentArticle.dateModified = currentDate(saxParser)
|
||||
}
|
||||
@ -258,13 +245,11 @@ private extension AtomParser {
|
||||
if article.link == nil {
|
||||
article.link = resolvedURLString
|
||||
}
|
||||
}
|
||||
else if rel == AttributeValue.alternate {
|
||||
} else if rel == AttributeValue.alternate {
|
||||
if article.permalink == nil {
|
||||
article.permalink = resolvedURLString
|
||||
}
|
||||
}
|
||||
else if rel == AttributeValue.enclosure {
|
||||
} else if rel == AttributeValue.enclosure {
|
||||
if let enclosure = enclosure(resolvedURLString, attributes) {
|
||||
article.addEnclosure(enclosure)
|
||||
}
|
||||
@ -372,7 +357,7 @@ extension AtomParser: SAXParserDelegate {
|
||||
currentArticle?.language = xmlAttributes["xml:lang"]
|
||||
}
|
||||
|
||||
let contentType = xmlAttributes["type"];
|
||||
let contentType = xmlAttributes["type"]
|
||||
if contentType == "xhtml" {
|
||||
parsingXHTML = true
|
||||
xhtmlString = ""
|
||||
@ -416,9 +401,7 @@ extension AtomParser: SAXParserDelegate {
|
||||
|
||||
if isContentTag {
|
||||
currentArticle?.body = xhtmlString
|
||||
}
|
||||
|
||||
else if isSummaryTag {
|
||||
} else if isSummaryTag {
|
||||
if (currentArticle?.body?.count ?? 0) < 1 {
|
||||
currentArticle?.body = xhtmlString
|
||||
}
|
||||
@ -438,9 +421,7 @@ extension AtomParser: SAXParserDelegate {
|
||||
} else {
|
||||
assertionFailure("xhtmlString must not be nil when parsingXHTML in xmlEndElement.")
|
||||
}
|
||||
}
|
||||
|
||||
else if parsingAuthor {
|
||||
} else if parsingAuthor {
|
||||
|
||||
if SAXEqualTags(localName, XMLName.author) {
|
||||
parsingAuthor = false
|
||||
@ -448,32 +429,21 @@ extension AtomParser: SAXParserDelegate {
|
||||
currentArticle?.addAuthor(currentAuthor)
|
||||
}
|
||||
currentAuthor = nil
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.name) {
|
||||
} else if SAXEqualTags(localName, XMLName.name) {
|
||||
currentAuthor?.name = saxParser.currentStringWithTrimmedWhitespace
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.email) {
|
||||
} else if SAXEqualTags(localName, XMLName.email) {
|
||||
currentAuthor?.emailAddress = saxParser.currentStringWithTrimmedWhitespace
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.uri) {
|
||||
} else if SAXEqualTags(localName, XMLName.uri) {
|
||||
currentAuthor?.url = saxParser.currentStringWithTrimmedWhitespace
|
||||
}
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.entry) {
|
||||
} else if SAXEqualTags(localName, XMLName.entry) {
|
||||
parsingArticle = false
|
||||
entryDepth = -1
|
||||
}
|
||||
|
||||
else if parsingArticle && !parsingSource && depth == entryDepth + 1 {
|
||||
} else if parsingArticle && !parsingSource && depth == entryDepth + 1 {
|
||||
addArticleElement(saxParser, localName, prefix)
|
||||
}
|
||||
|
||||
else if SAXEqualTags(localName, XMLName.source) {
|
||||
} else if SAXEqualTags(localName, XMLName.source) {
|
||||
parsingSource = false
|
||||
}
|
||||
|
||||
else if !parsingArticle && !parsingSource && SAXEqualTags(localName, XMLName.title) {
|
||||
} else if !parsingArticle && !parsingSource && SAXEqualTags(localName, XMLName.title) {
|
||||
addFeedTitle(saxParser)
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//import FoundationExtras
|
||||
// import FoundationExtras
|
||||
|
||||
final class RSSArticle {
|
||||
|
||||
@ -81,28 +81,21 @@ private extension RSSArticle {
|
||||
if let permalink, !permalink.isEmpty, let datePublishedTimeStampString {
|
||||
s.append(permalink)
|
||||
s.append(datePublishedTimeStampString)
|
||||
}
|
||||
else if let link, !link.isEmpty, let datePublishedTimeStampString {
|
||||
} else if let link, !link.isEmpty, let datePublishedTimeStampString {
|
||||
s.append(link)
|
||||
s.append(datePublishedTimeStampString)
|
||||
}
|
||||
else if let title, !title.isEmpty, let datePublishedTimeStampString {
|
||||
} else if let title, !title.isEmpty, let datePublishedTimeStampString {
|
||||
s.append(title)
|
||||
s.append(datePublishedTimeStampString)
|
||||
}
|
||||
else if let datePublishedTimeStampString {
|
||||
} else if let datePublishedTimeStampString {
|
||||
s.append(datePublishedTimeStampString)
|
||||
}
|
||||
else if let permalink, !permalink.isEmpty {
|
||||
} else if let permalink, !permalink.isEmpty {
|
||||
s.append(permalink)
|
||||
}
|
||||
else if let link, !link.isEmpty {
|
||||
} else if let link, !link.isEmpty {
|
||||
s.append(link)
|
||||
}
|
||||
else if let title, !title.isEmpty {
|
||||
} else if let title, !title.isEmpty {
|
||||
s.append(title)
|
||||
}
|
||||
else if let body, !body.isEmpty {
|
||||
} else if let body, !body.isEmpty {
|
||||
s.append(body)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ final class RSSAuthor {
|
||||
self.avatarURL = avatarURL
|
||||
self.emailAddress = emailAddress
|
||||
}
|
||||
|
||||
|
||||
/// Use when the actual property is unknown. Guess based on contents of the string. (This is common with RSS.)
|
||||
convenience init(singleString: String) {
|
||||
|
||||
|
@ -79,11 +79,9 @@ private extension RSSParser {
|
||||
if feed.link == nil {
|
||||
feed.link = saxParser.currentString
|
||||
}
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.title) {
|
||||
} else if SAXEqualTags(localName, XMLName.title) {
|
||||
feed.title = saxParser.currentString
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.language) {
|
||||
} else if SAXEqualTags(localName, XMLName.language) {
|
||||
feed.language = saxParser.currentString
|
||||
}
|
||||
}
|
||||
@ -118,26 +116,20 @@ private extension RSSParser {
|
||||
if let currentString = saxParser.currentString {
|
||||
if SAXEqualTags(localName, XMLName.guid) {
|
||||
addGuid(currentString, currentArticle)
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.author) {
|
||||
} else if SAXEqualTags(localName, XMLName.author) {
|
||||
addAuthorWithString(currentString, currentArticle)
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.link) {
|
||||
} else if SAXEqualTags(localName, XMLName.link) {
|
||||
currentArticle.link = urlString(currentString)
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.description) {
|
||||
} else if SAXEqualTags(localName, XMLName.description) {
|
||||
if currentArticle.body == nil {
|
||||
currentArticle.body = currentString
|
||||
}
|
||||
}
|
||||
else if !parsingAuthor && SAXEqualTags(localName, XMLName.title) {
|
||||
} else if !parsingAuthor && SAXEqualTags(localName, XMLName.title) {
|
||||
currentArticle.title = currentString
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.pubDate) {
|
||||
} else if SAXEqualTags(localName, XMLName.pubDate) {
|
||||
currentArticle.datePublished = currentDate(saxParser)
|
||||
}
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.enclosure), let currentAttributes {
|
||||
} else if SAXEqualTags(localName, XMLName.enclosure), let currentAttributes {
|
||||
addEnclosure(currentAttributes, currentArticle)
|
||||
}
|
||||
}
|
||||
@ -148,8 +140,7 @@ private extension RSSParser {
|
||||
if let currentString = saxParser.currentString {
|
||||
addAuthorWithString(currentString, currentArticle)
|
||||
}
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.date) {
|
||||
} else if SAXEqualTags(localName, XMLName.date) {
|
||||
currentArticle.datePublished = currentDate(saxParser)
|
||||
}
|
||||
}
|
||||
@ -298,7 +289,7 @@ extension RSSParser: SAXParserDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
var xmlAttributes: StringDictionary? = nil
|
||||
var xmlAttributes: StringDictionary?
|
||||
if (isRDF && SAXEqualTags(localName, XMLName.item)) || SAXEqualTags(localName, XMLName.guid) || SAXEqualTags(localName, XMLName.enclosure) {
|
||||
xmlAttributes = saxParser.attributesDictionary(attributes, attributeCount: attributeCount)
|
||||
}
|
||||
@ -314,11 +305,9 @@ extension RSSParser: SAXParserDelegate {
|
||||
currentArticle.guid = rdfGuid
|
||||
currentArticle.permalink = rdfGuid
|
||||
}
|
||||
}
|
||||
else if prefix == nil && SAXEqualTags(localName, XMLName.image) {
|
||||
} else if prefix == nil && SAXEqualTags(localName, XMLName.image) {
|
||||
parsingChannelImage = true
|
||||
}
|
||||
else if prefix == nil && SAXEqualTags(localName, XMLName.author) {
|
||||
} else if prefix == nil && SAXEqualTags(localName, XMLName.author) {
|
||||
if parsingArticle {
|
||||
parsingAuthor = true
|
||||
}
|
||||
@ -337,23 +326,18 @@ extension RSSParser: SAXParserDelegate {
|
||||
|
||||
if isRDF && SAXEqualTags(localName, XMLName.uppercaseRDF) {
|
||||
endRSSFound = true
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.rss) {
|
||||
} else if SAXEqualTags(localName, XMLName.rss) {
|
||||
endRSSFound = true
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.image) {
|
||||
} else if SAXEqualTags(localName, XMLName.image) {
|
||||
parsingChannelImage = false
|
||||
}
|
||||
else if SAXEqualTags(localName, XMLName.item) {
|
||||
} else if SAXEqualTags(localName, XMLName.item) {
|
||||
parsingArticle = false
|
||||
}
|
||||
else if parsingArticle {
|
||||
} else if parsingArticle {
|
||||
addArticleElement(saxParser, localName, prefix)
|
||||
if SAXEqualTags(localName, XMLName.author) {
|
||||
parsingAuthor = false
|
||||
}
|
||||
}
|
||||
else if !parsingChannelImage {
|
||||
} else if !parsingChannelImage {
|
||||
addFeedElement(saxParser, localName, prefix)
|
||||
}
|
||||
}
|
||||
@ -363,4 +347,3 @@ extension RSSParser: SAXParserDelegate {
|
||||
// Required method.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ private func decodedEntities(_ sourceBuffer: UnsafeBufferPointer<UInt8>, _ didDe
|
||||
|
||||
resultBuffer.initializeMemory(as: UInt8.self, repeating: 0, count: resultBufferByteCount)
|
||||
let result = resultBuffer.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
|
||||
var sourceLocation = 0
|
||||
var resultLocation = 0
|
||||
|
||||
@ -78,7 +78,7 @@ private func decodedEntities(_ sourceBuffer: UnsafeBufferPointer<UInt8>, _ didDe
|
||||
|
||||
let ch = sourceBuffer[sourceLocation]
|
||||
|
||||
var decodedEntity: String? = nil
|
||||
var decodedEntity: String?
|
||||
|
||||
if ch == ampersandCharacter {
|
||||
decodedEntity = decodedEntityValue(sourceBuffer, byteCount, &sourceLocation)
|
||||
@ -112,7 +112,7 @@ private func addDecodedEntity(_ decodedEntity: String, _ result: UnsafeMutablePo
|
||||
}
|
||||
}
|
||||
|
||||
private func decodedEntityValue(_ buffer: UnsafeBufferPointer<UInt8>, _ byteCount: Int, _ sourceLocation: inout Int) -> /*[UInt8]?*/ String? {
|
||||
private func decodedEntityValue(_ buffer: UnsafeBufferPointer<UInt8>, _ byteCount: Int, _ sourceLocation: inout Int) -> String? {
|
||||
|
||||
guard let rawEntity = rawEntityValue(buffer, byteCount, &sourceLocation) else {
|
||||
return nil
|
||||
@ -153,8 +153,7 @@ private func decodedNumericEntity(_ rawEntity: ContiguousArray<UInt8>) -> String
|
||||
|
||||
if rawEntity[1] == xCharacter || rawEntity[1] == XCharacter { // Hex?
|
||||
decodedNumber = decodedHexEntity(rawEntity)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
decodedNumber = decodedDecimalEntity(rawEntity)
|
||||
}
|
||||
|
||||
@ -247,7 +246,7 @@ private func decodedDecimalEntity(_ rawEntity: ContiguousArray<UInt8>) -> UInt32
|
||||
if number == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return number
|
||||
}
|
||||
|
||||
@ -271,7 +270,7 @@ private func rawEntityValue(_ buffer: UnsafeBufferPointer<UInt8>, _ byteCount: I
|
||||
while true {
|
||||
|
||||
sourceLocation += 1
|
||||
if sourceLocation >= byteCount || entityCharactersIndex >= maxEntityCharacters { // did not parse entity
|
||||
if sourceLocation >= byteCount || entityCharactersIndex >= maxEntityCharacters { // did not parse entity
|
||||
sourceLocation = savedSourceLocation
|
||||
return nil
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ public final class HTMLMetadata: Sendable {
|
||||
self.appleTouchIcons = appleTouchIconTags.map { htmlTag in
|
||||
HTMLMetadataAppleTouchIcon(urlString, htmlTag)
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.appleTouchIcons = nil
|
||||
}
|
||||
|
||||
@ -37,8 +36,7 @@ public final class HTMLMetadata: Sendable {
|
||||
self.feedLinks = feedLinkTags.map { htmlTag in
|
||||
HTMLMetadataFeedLink(urlString, htmlTag)
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.feedLinks = nil
|
||||
}
|
||||
|
||||
@ -89,7 +87,7 @@ public final class HTMLMetadata: Sendable {
|
||||
}
|
||||
|
||||
let feedLinkTags = alternateLinkTags.filter { tag in
|
||||
|
||||
|
||||
guard let attributes = tag.attributes, let type = attributes.object(forCaseInsensitiveKey: "type"), typeIsFeedType(type) else {
|
||||
return false
|
||||
}
|
||||
@ -206,8 +204,7 @@ public final class HTMLMetadataAppleTouchIcon: Sendable {
|
||||
let sizeComponents = sizes.components(separatedBy: CharacterSet(charactersIn: "x"))
|
||||
if sizeComponents.count == 2, let width = Double(sizeComponents[0]), let height = Double(sizeComponents[1]) {
|
||||
self.size = CGSize(width: width, height: height)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.size = nil
|
||||
}
|
||||
}
|
||||
@ -313,25 +310,19 @@ private extension HTMLOpenGraphProperties {
|
||||
|
||||
if propertyName == OGValue.ogImage {
|
||||
url = content
|
||||
}
|
||||
else if propertyName == OGValue.ogImageURL {
|
||||
} else if propertyName == OGValue.ogImageURL {
|
||||
url = content
|
||||
}
|
||||
else if propertyName == OGValue.ogImageSecureURL {
|
||||
} else if propertyName == OGValue.ogImageSecureURL {
|
||||
secureURL = content
|
||||
}
|
||||
else if propertyName == OGValue.ogImageType {
|
||||
} else if propertyName == OGValue.ogImageType {
|
||||
mimeType = content
|
||||
}
|
||||
else if propertyName == OGValue.ogImageAlt {
|
||||
} else if propertyName == OGValue.ogImageAlt {
|
||||
altText = content
|
||||
}
|
||||
else if propertyName == OGValue.ogImageWidth {
|
||||
} else if propertyName == OGValue.ogImageWidth {
|
||||
if let value = Double(content) {
|
||||
width = CGFloat(value)
|
||||
}
|
||||
}
|
||||
else if propertyName == OGValue.ogImageHeight {
|
||||
} else if propertyName == OGValue.ogImageHeight {
|
||||
if let value = Double(content) {
|
||||
height = CGFloat(value)
|
||||
}
|
||||
@ -341,14 +332,14 @@ private extension HTMLOpenGraphProperties {
|
||||
if url == nil && secureURL == nil && mimeType == nil && width == nil && height == nil && altText == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return HTMLOpenGraphImage(url: url, secureURL: secureURL, mimeType: mimeType, width: width, height: height, altText: altText)
|
||||
}
|
||||
}
|
||||
|
||||
public final class HTMLOpenGraphImage: Sendable {
|
||||
|
||||
public let url : String?
|
||||
public let url: String?
|
||||
public let secureURL: String?
|
||||
public let mimeType: String?
|
||||
public let width: CGFloat?
|
||||
@ -434,4 +425,3 @@ private func absoluteURLStringWithRelativeURLString(_ relativeURLString: String,
|
||||
}
|
||||
return absoluteURL.absoluteURL.standardized.absoluteString
|
||||
}
|
||||
|
||||
|
@ -34,13 +34,13 @@ private extension HTMLMetadataParser {
|
||||
extension HTMLMetadataParser: SAXHTMLParserDelegate {
|
||||
|
||||
private struct HTMLName {
|
||||
|
||||
|
||||
static let link = "link".utf8CString
|
||||
static let meta = "meta".utf8CString
|
||||
}
|
||||
|
||||
private struct HTMLKey {
|
||||
|
||||
|
||||
static let href = "href"
|
||||
static let src = "src"
|
||||
static let rel = "rel"
|
||||
@ -81,8 +81,7 @@ extension HTMLMetadataParser: SAXHTMLParserDelegate {
|
||||
if let d, !d.isEmpty {
|
||||
handleLinkAttributes(d)
|
||||
}
|
||||
}
|
||||
else if SAXEqualTags(name, HTMLName.meta) {
|
||||
} else if SAXEqualTags(name, HTMLName.meta) {
|
||||
let d = saxHTMLParser.attributesDictionary(attributes)
|
||||
if let d, !d.isEmpty {
|
||||
handleMetaAttributes(d)
|
||||
|
@ -9,8 +9,8 @@ import Foundation
|
||||
|
||||
public final class OPMLDocument: OPMLItem {
|
||||
|
||||
public var title: String? = nil
|
||||
public var url: String? = nil
|
||||
public var title: String?
|
||||
public var url: String?
|
||||
|
||||
init(url: String?) {
|
||||
self.url = url
|
||||
|
@ -37,4 +37,3 @@ public struct OPMLFeedSpecifier: Sendable {
|
||||
self.feedURL = feedURL
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ public class OPMLItem {
|
||||
(items?.count ?? 0) > 0
|
||||
}
|
||||
|
||||
init(attributes: [String : String]?) {
|
||||
init(attributes: [String: String]?) {
|
||||
|
||||
self.titleFromAttributes = attributes?.opml_title ?? attributes?.opml_text
|
||||
self.attributes = attributes
|
||||
@ -33,7 +33,7 @@ public class OPMLItem {
|
||||
}
|
||||
|
||||
public func add(_ item: OPMLItem) {
|
||||
|
||||
|
||||
if items == nil {
|
||||
items = [OPMLItem]()
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ private extension OPMLParser {
|
||||
}
|
||||
|
||||
func canParseData() -> Bool {
|
||||
|
||||
|
||||
data.containsASCIIString("<opml")
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ public extension Dictionary where Key == String, Value == String {
|
||||
if let object = self[key] {
|
||||
return object
|
||||
}
|
||||
|
||||
|
||||
let lowercaseKey = key.lowercased()
|
||||
|
||||
for (oneKey, oneValue) in self {
|
||||
|
@ -103,7 +103,7 @@ public final class SAXHTMLParser {
|
||||
|
||||
var dictionary = [String: String]()
|
||||
var ix = 0
|
||||
var currentKey: String? = nil
|
||||
var currentKey: String?
|
||||
|
||||
while true {
|
||||
let oneAttribute = attributes[ix]
|
||||
|
@ -61,7 +61,7 @@ public final class SAXParser {
|
||||
}
|
||||
|
||||
public func parse() {
|
||||
|
||||
|
||||
guard !data.isEmpty else {
|
||||
return
|
||||
}
|
||||
@ -99,7 +99,7 @@ public final class SAXParser {
|
||||
}
|
||||
|
||||
var dictionary = [String: String]()
|
||||
|
||||
|
||||
let fieldCount = 5
|
||||
var i = 0, j = 0
|
||||
while i < attributeCount {
|
||||
@ -201,4 +201,3 @@ nonisolated(unsafe) private var saxHandlerStruct: xmlSAXHandler = {
|
||||
|
||||
return handler
|
||||
}()
|
||||
|
||||
|
@ -10,8 +10,8 @@ import libxml2
|
||||
|
||||
public func SAXEqualTags(_ localName: XMLPointer, _ tag: ContiguousArray<Int8>) -> Bool {
|
||||
|
||||
return tag.withUnsafeBufferPointer { bufferPointer in
|
||||
|
||||
return tag.withUnsafeBufferPointer { _ in
|
||||
|
||||
let tagCount = tag.count // includes 0 terminator
|
||||
|
||||
for i in 0..<tagCount - 1 {
|
||||
|
@ -14,15 +14,15 @@ final class AtomParserTests: XCTestCase {
|
||||
func testDaringFireballPerformance() {
|
||||
|
||||
// 0.009 sec on my 2012 iMac.
|
||||
let d = parserData("DaringFireball", "atom", "http://daringfireball.net/") //It’s actually an Atom feed
|
||||
let d = parserData("DaringFireball", "atom", "http://daringfireball.net/") // It’s actually an Atom feed
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testDaringFireball() {
|
||||
|
||||
let d = parserData("DaringFireball", "atom", "http://daringfireball.net/") //It’s actually an Atom feed
|
||||
let d = parserData("DaringFireball", "atom", "http://daringfireball.net/") // It’s actually an Atom feed
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
@ -35,8 +35,7 @@ final class AtomParserTests: XCTestCase {
|
||||
let author = article.authors!.first!
|
||||
if author.name == "Daring Fireball Department of Commerce" {
|
||||
XCTAssertNil(author.url)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
XCTAssertEqual(author.name, "John Gruber")
|
||||
XCTAssertEqual(author.url, "http://daringfireball.net/")
|
||||
}
|
||||
@ -78,7 +77,7 @@ final class AtomParserTests: XCTestCase {
|
||||
guard let attachments = article.attachments else {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
XCTAssertEqual(attachments.count, 1)
|
||||
let attachment = attachments.first!
|
||||
|
||||
|
@ -13,7 +13,7 @@ final class DateParserTests: XCTestCase {
|
||||
|
||||
func testDateWithString() {
|
||||
var expectedDateResult = dateWithValues(2010, 5, 28, 21, 3, 38)
|
||||
|
||||
|
||||
var d = date("Fri, 28 May 2010 21:03:38 +0000")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
@ -78,7 +78,7 @@ final class DateParserTests: XCTestCase {
|
||||
let d = date("2010-11-17 08:40:07-05:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
}
|
||||
|
||||
|
||||
func testFeedbinDate() {
|
||||
let expectedDateResult = dateWithValues(2019, 9, 27, 21, 01, 48)
|
||||
let d = date("2019-09-27T21:01:48.000000Z")
|
||||
@ -106,7 +106,7 @@ final class DateParserTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testPubDateParsingPerformance() {
|
||||
|
||||
|
||||
// 0.0001 seconds on my Mac Studio M1
|
||||
self.measure {
|
||||
_ = date("21 May 2010 21:22:53 GMT")
|
||||
|
@ -26,7 +26,7 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
let type = FeedType.feedType(d.data)
|
||||
XCTAssertTrue(type == .notAFeed)
|
||||
}
|
||||
|
||||
|
||||
func testInessentialHTMLType() {
|
||||
|
||||
let d = parserData("inessential", "html", "http://inessential.com/")
|
||||
@ -40,7 +40,7 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
let type = FeedType.feedType(d.data)
|
||||
XCTAssertTrue(type == .notAFeed)
|
||||
}
|
||||
|
||||
|
||||
// MARK: RSS
|
||||
|
||||
func testEMarleyRSSType() {
|
||||
@ -56,7 +56,7 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
let type = FeedType.feedType(d.data)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
|
||||
func testKatieFloydRSSType() {
|
||||
|
||||
let d = parserData("KatieFloyd", "rss", "https://katiefloyd.com/")
|
||||
@ -186,21 +186,21 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
|
||||
// In the case of this feed, the partial data isn’t enough to detect that it’s a JSON Feed.
|
||||
// The type detector should return .unknown rather than .notAFeed.
|
||||
|
||||
|
||||
let d = parserData("allthis-partial", "json", "http://leancrew.com/allthis/")
|
||||
let type = FeedType.feedType(d.data, isPartialData: true)
|
||||
XCTAssertEqual(type, .unknown)
|
||||
}
|
||||
|
||||
// MARK: Performance
|
||||
|
||||
|
||||
func testFeedTypePerformance() {
|
||||
|
||||
// 0.000 on my 2012 iMac.
|
||||
|
||||
let d = parserData("EMarley", "rss", "https://medium.com/@emarley")
|
||||
self.measure {
|
||||
let _ = FeedType.feedType(d.data)
|
||||
_ = FeedType.feedType(d.data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,7 +210,7 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
|
||||
let d = parserData("inessential", "json", "http://inessential.com/")
|
||||
self.measure {
|
||||
let _ = FeedType.feedType(d.data)
|
||||
_ = FeedType.feedType(d.data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +220,7 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
|
||||
let d = parserData("DaringFireball", "html", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = FeedType.feedType(d.data)
|
||||
_ = FeedType.feedType(d.data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,7 +230,7 @@ final class FeedParserTypeTests: XCTestCase {
|
||||
|
||||
let d = parserData("DaringFireball", "rss", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = FeedType.feedType(d.data)
|
||||
_ = FeedType.feedType(d.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ final class HTMLLinkTests: XCTestCase {
|
||||
// 0.003 sec on my 2012 iMac
|
||||
let d = parserData("sixcolors", "html", "http://sixcolors.com/")
|
||||
self.measure {
|
||||
let _ = HTMLLinkParser.htmlLinks(with: d)
|
||||
_ = HTMLLinkParser.htmlLinks(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
// 0.002 sec on my 2012 iMac
|
||||
let d = parserData("DaringFireball", "html", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = HTMLMetadataParser.metadata(with: d)
|
||||
_ = HTMLMetadataParser.metadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
// 0.001 sec on my 2012 iMac
|
||||
let d = parserData("furbo", "html", "http://furbo.org/")
|
||||
self.measure {
|
||||
let _ = HTMLMetadataParser.metadata(with: d)
|
||||
_ = HTMLMetadataParser.metadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml")
|
||||
XCTAssertEqual(feedLink.urlString, "http://inessential.com/xml/rss.xml")
|
||||
|
||||
XCTAssertEqual(metadata.appleTouchIcons?.count ?? 0, 0);
|
||||
XCTAssertEqual(metadata.appleTouchIcons?.count ?? 0, 0)
|
||||
}
|
||||
|
||||
func testInessentialPerformance() {
|
||||
@ -79,7 +79,7 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
// 0.001 sec on my 2012 iMac
|
||||
let d = parserData("inessential", "html", "http://inessential.com/")
|
||||
self.measure {
|
||||
let _ = HTMLMetadataParser.metadata(with: d)
|
||||
_ = HTMLMetadataParser.metadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
// 0.004 sec on my 2012 iMac
|
||||
let d = parserData("coco", "html", "https://www.theatlantic.com/entertainment/archive/2017/11/coco-is-among-pixars-best-movies-in-years/546695/")
|
||||
self.measure {
|
||||
let _ = HTMLMetadataParser.metadata(with: d)
|
||||
_ = HTMLMetadataParser.metadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,17 +99,17 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(metadata.favicons?.first?.urlString, "https://sixcolors.com/images/favicon.ico")
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks?.count, 1);
|
||||
XCTAssertEqual(metadata.feedLinks?.count, 1)
|
||||
let feedLink = (metadata.feedLinks?.first!)!
|
||||
XCTAssertEqual(feedLink.title, "RSS");
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml");
|
||||
XCTAssertEqual(feedLink.urlString, "http://feedpress.me/sixcolors");
|
||||
XCTAssertEqual(feedLink.title, "RSS")
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml")
|
||||
XCTAssertEqual(feedLink.urlString, "http://feedpress.me/sixcolors")
|
||||
|
||||
XCTAssertEqual(metadata.appleTouchIcons!.count, 6);
|
||||
let icon = metadata.appleTouchIcons![3];
|
||||
XCTAssertEqual(icon.rel, "apple-touch-icon");
|
||||
XCTAssertEqual(icon.sizes, "120x120");
|
||||
XCTAssertEqual(icon.urlString, "https://sixcolors.com/apple-touch-icon-120.png");
|
||||
XCTAssertEqual(metadata.appleTouchIcons!.count, 6)
|
||||
let icon = metadata.appleTouchIcons![3]
|
||||
XCTAssertEqual(icon.rel, "apple-touch-icon")
|
||||
XCTAssertEqual(icon.sizes, "120x120")
|
||||
XCTAssertEqual(icon.urlString, "https://sixcolors.com/apple-touch-icon-120.png")
|
||||
}
|
||||
|
||||
func testSixColorsPerformance() {
|
||||
@ -117,7 +117,7 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
// 0.002 sec on my 2012 iMac
|
||||
let d = parserData("sixcolors", "html", "http://sixcolors.com/")
|
||||
self.measure {
|
||||
let _ = HTMLMetadataParser.metadata(with: d)
|
||||
_ = HTMLMetadataParser.metadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,10 +144,10 @@ final class HTMLMetadataTests: XCTestCase {
|
||||
let d = parserData("YouTubeTheVolvoRocks", "html", "https://www.youtube.com/user/TheVolvorocks")
|
||||
let metadata = HTMLMetadataParser.metadata(with: d)
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks!.count, 1);
|
||||
XCTAssertEqual(metadata.feedLinks!.count, 1)
|
||||
let feedLink = metadata.feedLinks!.first!
|
||||
XCTAssertEqual(feedLink.title, "RSS");
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml");
|
||||
XCTAssertEqual(feedLink.urlString, "https://www.youtube.com/feeds/videos.xml?channel_id=UCct7QF2jcWRY6dhXWMSq9LQ");
|
||||
XCTAssertEqual(feedLink.title, "RSS")
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml")
|
||||
XCTAssertEqual(feedLink.urlString, "https://www.youtube.com/feeds/videos.xml?channel_id=UCct7QF2jcWRY6dhXWMSq9LQ")
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ final class JSONFeedParserTests: XCTestCase {
|
||||
// 0.001 sec on my 2012 iMac.
|
||||
let d = parserData("inessential", "json", "http://inessential.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ final class JSONFeedParserTests: XCTestCase {
|
||||
// 0.009 sec on my 2012 iMac.
|
||||
let d = parserData("DaringFireball", "json", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ final class JSONFeedParserTests: XCTestCase {
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.items.count, 20)
|
||||
XCTAssertEqual(parsedFeed.language, "de-DE")
|
||||
|
||||
|
||||
for item in parsedFeed.items {
|
||||
XCTAssertEqual(item.language, "de-DE")
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ final class OPMLTests: XCTestCase {
|
||||
|
||||
// 0.003 sec on my M1 Mac Studio 2022
|
||||
self.measure {
|
||||
let _ = OPMLParser.document(with: self.subsData)
|
||||
_ = OPMLParser.document(with: self.subsData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@ final class OPMLTests: XCTestCase {
|
||||
recursivelyCheckOPMLStructure(opmlDocument!)
|
||||
}
|
||||
|
||||
|
||||
func testFindingTitles() {
|
||||
// https://github.com/brentsimmons/NetNewsWire/issues/527
|
||||
// Fix a bug where titles aren’t found when there’s no title attribute in the OPML,
|
||||
@ -66,8 +65,7 @@ private extension OPMLTests {
|
||||
if !isFolder {
|
||||
XCTAssertNotNil(feedSpecifier!.title)
|
||||
XCTAssertNotNil(feedSpecifier!.feedURL)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
XCTAssertNil(feedSpecifier)
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ final class RSSInJSONParserTests: XCTestCase {
|
||||
// 0.003 sec on my 2012 iMac.
|
||||
let d = parserData("ScriptingNews", "json", "http://scripting.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ final class RSSParserTests: XCTestCase {
|
||||
// 0.002 2022 Mac Studio
|
||||
let d = parserData("scriptingNews", "rss", "http://scripting.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ final class RSSParserTests: XCTestCase {
|
||||
// 0.001 2022 Mac Studio
|
||||
let d = parserData("KatieFloyd", "rss", "http://katiefloyd.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ final class RSSParserTests: XCTestCase {
|
||||
// 0.0004 2022 Mac Studio
|
||||
let d = parserData("EMarley", "rss", "https://medium.com/@emarley")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ final class RSSParserTests: XCTestCase {
|
||||
// 0.0006 2022 Mac Studio
|
||||
let d = parserData("manton", "rss", "http://manton.org/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ final class RSSParserTests: XCTestCase {
|
||||
// 0.002 2022 Mac Studio
|
||||
let d = parserData("allthis", "rss", "http://leancrew.com/all-this")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
_ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,8 +241,7 @@ final class RSSParserTests: XCTestCase {
|
||||
// Tue, 07 Mar 2023 15:15:29 -0500
|
||||
let expectedDatePublished = dateWithValues(2023, 3, 7, 20, 15, 29)
|
||||
XCTAssertEqual(article.datePublished, expectedDatePublished)
|
||||
}
|
||||
else if article.title == "Ventura’s System Settings" {
|
||||
} else if article.title == "Ventura’s System Settings" {
|
||||
didFindSecondTestArticle = true
|
||||
// Sun, 30 Oct 2022 11:58:26 -0500
|
||||
let expectedDatePublished = dateWithValues(2022, 10, 30, 16, 58, 26)
|
||||
|
@ -21,6 +21,6 @@ let package = Package(
|
||||
]),
|
||||
.testTarget(
|
||||
name: "RSCoreTests",
|
||||
dependencies: ["RSCore"]),
|
||||
dependencies: ["RSCore"])
|
||||
]
|
||||
)
|
||||
|
@ -34,4 +34,3 @@ public extension Int {
|
||||
return UInt32(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ public struct KeyboardKey: Hashable {
|
||||
|
||||
var integerValue = 0
|
||||
|
||||
switch(s) {
|
||||
switch s {
|
||||
case "[space]":
|
||||
integerValue = " ".keyboardIntegerValue!
|
||||
case "[uparrow]":
|
||||
|
@ -8,10 +8,10 @@
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
//let keypadEnter: unichar = 3
|
||||
// let keypadEnter: unichar = 3
|
||||
|
||||
@objc public protocol KeyboardDelegate: AnyObject {
|
||||
|
||||
|
||||
// Return true if handled.
|
||||
func keydown(_: NSEvent, in view: NSView) -> Bool
|
||||
}
|
||||
|
@ -69,8 +69,7 @@ public extension NSOutlineView {
|
||||
selectRowAndScrollToVisible(row)
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return // if there are no more items, we’re out of rows
|
||||
}
|
||||
}
|
||||
@ -180,4 +179,3 @@ public extension NSOutlineView {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -9,22 +9,21 @@
|
||||
import AppKit
|
||||
|
||||
public extension NSResponder {
|
||||
|
||||
|
||||
func hasAncestor(_ ancestor: NSResponder) -> Bool {
|
||||
|
||||
|
||||
var nomad: NSResponder = self
|
||||
while(true) {
|
||||
while true {
|
||||
if nomad === ancestor {
|
||||
return true
|
||||
}
|
||||
if let _ = nomad.nextResponder {
|
||||
nomad = nomad.nextResponder!
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,11 @@
|
||||
import AppKit
|
||||
|
||||
public extension NSTableView {
|
||||
|
||||
|
||||
var selectionIsEmpty: Bool {
|
||||
return selectedRowIndexes.startIndex == selectedRowIndexes.endIndex
|
||||
}
|
||||
|
||||
|
||||
func indexesOfAvailableRowsPassingTest(_ test: (Int) -> Bool) -> IndexSet? {
|
||||
|
||||
// Checks visible and in-flight rows.
|
||||
@ -43,12 +43,12 @@ public extension NSTableView {
|
||||
let documentVisibleRect = scrollView.documentVisibleRect
|
||||
|
||||
let r = rect(ofRow: row)
|
||||
if NSContainsRect(documentVisibleRect, r) {
|
||||
if documentVisibleRect.contains(r) {
|
||||
return
|
||||
}
|
||||
|
||||
let rMidY = NSMidY(r)
|
||||
var scrollPoint = NSZeroPoint;
|
||||
let rMidY = r.midY
|
||||
var scrollPoint = NSPoint.zero
|
||||
scrollPoint.y = floor(rMidY - (documentVisibleRect.size.height / 2.0)) + CGFloat(extraHeight)
|
||||
scrollPoint.y = max(scrollPoint.y, 0)
|
||||
|
||||
@ -57,7 +57,7 @@ public extension NSTableView {
|
||||
|
||||
let clipView = scrollView.contentView
|
||||
|
||||
let rClipView = NSMakeRect(scrollPoint.x, scrollPoint.y, NSWidth(clipView.bounds), NSHeight(clipView.bounds))
|
||||
let rClipView = NSRect(x: scrollPoint.x, y: scrollPoint.y, width: clipView.bounds.width, height: clipView.bounds.height)
|
||||
|
||||
clipView.animator().bounds = rClipView
|
||||
}
|
||||
|
@ -9,16 +9,16 @@
|
||||
import AppKit
|
||||
|
||||
extension NSView {
|
||||
|
||||
|
||||
public func asImage() -> NSImage {
|
||||
let rep = bitmapImageRepForCachingDisplay(in: bounds)!
|
||||
cacheDisplay(in: bounds, to: rep)
|
||||
|
||||
|
||||
let img = NSImage(size: bounds.size)
|
||||
img.addRepresentation(rep)
|
||||
return img
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public extension NSView {
|
||||
|
@ -13,11 +13,11 @@ public class RSAppMovementMonitor: NSObject {
|
||||
|
||||
// If provided, the handler will be consulted when the app is moved.
|
||||
// Return true to indicate that the default handler should be invoked.
|
||||
public var appMovementHandler: ((RSAppMovementMonitor) -> Bool)? = nil
|
||||
public var appMovementHandler: ((RSAppMovementMonitor) -> Bool)?
|
||||
|
||||
// DispatchSource offers a monitoring mechanism based on an open file descriptor
|
||||
var fileDescriptor: Int32 = -1
|
||||
var dispatchSource: DispatchSourceFileSystemObject? = nil
|
||||
var dispatchSource: DispatchSourceFileSystemObject?
|
||||
|
||||
// Save the original location of the app in a file reference URL, which will track its new location.
|
||||
// Note this is NSURL, not URL, because file reference URLs violate value-type assumptions of URL.
|
||||
@ -77,7 +77,7 @@ public class RSAppMovementMonitor: NSObject {
|
||||
// every time the app becomes active. This catches a good number of edge-case
|
||||
// changes to the app bundle's path, such as when a containing folder or the
|
||||
// volume name changes.
|
||||
NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: nil) { notification in
|
||||
NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: nil) { _ in
|
||||
// Removing observer in invalidate doesn't seem to prevent this getting called? Maybe
|
||||
// because it's on the same invocation of the runloop?
|
||||
if self.isValid() && self.originalAppURL != self.appTrackingURL?.absoluteURL {
|
||||
@ -130,7 +130,7 @@ public class RSAppMovementMonitor: NSObject {
|
||||
// at the given URL with the "new instance" option to prevent it simply reactivating us.
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
configuration.createsNewApplicationInstance = true
|
||||
NSWorkspace.shared.openApplication(at: appURL, configuration: configuration) { _,_ in
|
||||
NSWorkspace.shared.openApplication(at: appURL, configuration: configuration) { _, _ in
|
||||
NSApp.terminate(self)
|
||||
}
|
||||
}
|
||||
@ -139,7 +139,7 @@ public class RSAppMovementMonitor: NSObject {
|
||||
let quitAlert = NSAlert()
|
||||
quitAlert.alertStyle = .critical
|
||||
quitAlert.addButton(withTitle: self.alertRelaunchButtonText)
|
||||
|
||||
|
||||
quitAlert.messageText = self.alertMessageText
|
||||
quitAlert.informativeText = self.alertInformativeText
|
||||
|
||||
|
@ -35,7 +35,7 @@ private extension RSToolbarItem {
|
||||
return false
|
||||
}
|
||||
|
||||
while(true) {
|
||||
while true {
|
||||
if let validated = validateWithResponder(responder!) {
|
||||
return validated
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ import AppKit
|
||||
public final class UserApp {
|
||||
|
||||
public let bundleID: String
|
||||
public var icon: NSImage? = nil
|
||||
public var icon: NSImage?
|
||||
public var existsOnDisk = false
|
||||
public var path: String? = nil
|
||||
public var runningApplication: NSRunningApplication? = nil
|
||||
public var path: String?
|
||||
public var runningApplication: NSRunningApplication?
|
||||
|
||||
public var isRunning: Bool {
|
||||
|
||||
@ -47,8 +47,7 @@ public final class UserApp {
|
||||
if app == runningApplication {
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if !app.isTerminated {
|
||||
runningApplication = app
|
||||
break
|
||||
@ -61,8 +60,7 @@ public final class UserApp {
|
||||
icon = runningApplication.icon
|
||||
if let bundleURL = runningApplication.bundleURL {
|
||||
path = bundleURL.path
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path
|
||||
}
|
||||
if icon == nil, let path = path {
|
||||
@ -77,8 +75,7 @@ public final class UserApp {
|
||||
icon = NSWorkspace.shared.icon(forFile: path)
|
||||
}
|
||||
existsOnDisk = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
existsOnDisk = false
|
||||
icon = nil
|
||||
}
|
||||
@ -146,4 +143,3 @@ public final class UserApp {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -15,7 +15,7 @@ public extension Array {
|
||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public extension Array where Element: Equatable {
|
||||
|
@ -23,7 +23,7 @@ public final class BatchUpdate {
|
||||
|
||||
/// The shared batch update object.
|
||||
public static let shared = BatchUpdate()
|
||||
|
||||
|
||||
private var count = 0
|
||||
|
||||
/// Is updating in progress?
|
||||
@ -50,15 +50,15 @@ public final class BatchUpdate {
|
||||
public func end() {
|
||||
precondition(Thread.isMainThread)
|
||||
decrementCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BatchUpdate {
|
||||
|
||||
|
||||
func incrementCount() {
|
||||
count = count + 1
|
||||
}
|
||||
|
||||
|
||||
func decrementCount() {
|
||||
count = count - 1
|
||||
if count < 1 {
|
||||
@ -67,7 +67,7 @@ private extension BatchUpdate {
|
||||
postBatchUpdateDidPerform()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func postBatchUpdateDidPerform() {
|
||||
if !Thread.isMainThread {
|
||||
DispatchQueue.main.sync {
|
||||
@ -77,5 +77,5 @@ private extension BatchUpdate {
|
||||
NotificationCenter.default.post(name: .BatchUpdateDidPerform, object: nil, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -39,23 +39,19 @@ public struct BinaryDiskCache {
|
||||
get {
|
||||
do {
|
||||
return try data(forKey: key)
|
||||
}
|
||||
catch {}
|
||||
} catch {}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
set {
|
||||
if let data = newValue {
|
||||
do {
|
||||
try setData(data, forKey: key)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
else {
|
||||
} catch {}
|
||||
} else {
|
||||
do {
|
||||
try deleteData(forKey: key)
|
||||
}
|
||||
catch{}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,5 +26,5 @@ public extension Calendar {
|
||||
static var startOfToday: Date {
|
||||
cached.startOfDay(for: Date())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
public extension Character {
|
||||
|
||||
|
||||
var isSimpleEmoji: Bool {
|
||||
guard let firstScalar = unicodeScalars.first else { return false }
|
||||
return firstScalar.properties.isEmoji && firstScalar.value > 0x238C
|
||||
|
@ -13,16 +13,16 @@ import CloudKit
|
||||
public class CloudKitError: LocalizedError {
|
||||
|
||||
public let error: Error
|
||||
|
||||
|
||||
public init(_ error: Error) {
|
||||
self.error = error
|
||||
}
|
||||
|
||||
|
||||
public var errorDescription: String? {
|
||||
guard let ckError = error as? CKError else {
|
||||
return error.localizedDescription
|
||||
}
|
||||
|
||||
|
||||
switch ckError.code {
|
||||
case .alreadyShared:
|
||||
return NSLocalizedString("Already Shared: a record or share cannot be saved because doing so would cause the same hierarchy of records to exist in multiple shares.", comment: "Known iCloud Error")
|
||||
@ -94,5 +94,5 @@ public class CloudKitError: LocalizedError {
|
||||
return NSLocalizedString("Unhandled Error.", comment: "Unknown iCloud Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ public enum CloudKitZoneError: LocalizedError {
|
||||
}
|
||||
|
||||
public protocol CloudKitZoneDelegate: AnyObject {
|
||||
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
|
||||
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void)
|
||||
}
|
||||
|
||||
public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: CKRecord.ID)
|
||||
@ -54,7 +54,7 @@ public protocol CloudKitZone: AnyObject {
|
||||
func subscribeToZoneChanges()
|
||||
|
||||
/// Process a remove notification
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void)
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable: Any], completion: @escaping () -> Void)
|
||||
|
||||
}
|
||||
|
||||
@ -111,21 +111,21 @@ public extension CloudKitZone {
|
||||
return CKRecord.ID(recordName: UUID().uuidString, zoneID: zoneID)
|
||||
}
|
||||
|
||||
func retryIfPossible(after: Double, block: @escaping () -> ()) {
|
||||
func retryIfPossible(after: Double, block: @escaping () -> Void) {
|
||||
let delayTime = DispatchTime.now() + after
|
||||
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
|
||||
block()
|
||||
})
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable: Any], completion: @escaping () -> Void) {
|
||||
let note = CKRecordZoneNotification(fromRemoteNotificationDictionary: userInfo)
|
||||
guard note?.recordZoneID?.zoneName == zoneID.zoneName else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
fetchChangesInZone() { result in
|
||||
fetchChangesInZone { result in
|
||||
if case .failure(let error) = result {
|
||||
os_log(.error, log: self.log, "%@ zone remote notification fetch error: %@", self.zoneID.zoneName, error.localizedDescription)
|
||||
}
|
||||
@ -140,7 +140,7 @@ public extension CloudKitZone {
|
||||
return
|
||||
}
|
||||
|
||||
database.save(CKRecordZone(zoneID: zoneID)) { (recordZone, error) in
|
||||
database.save(CKRecordZone(zoneID: zoneID)) { (_, error) in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(CloudKitError(error)))
|
||||
@ -211,7 +211,7 @@ public extension CloudKitZone {
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
|
||||
@ -285,7 +285,7 @@ public extension CloudKitZone {
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
||||
@ -315,7 +315,6 @@ public extension CloudKitZone {
|
||||
database?.add(op)
|
||||
}
|
||||
|
||||
|
||||
/// Fetch a CKRecord by using its externalID
|
||||
func fetch(externalID: String?, completion: @escaping (Result<CKRecord, Error>) -> Void) {
|
||||
guard let externalID = externalID else {
|
||||
@ -341,7 +340,7 @@ public extension CloudKitZone {
|
||||
}
|
||||
}
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.fetch(externalID: externalID, completion: completion)
|
||||
@ -405,7 +404,7 @@ public extension CloudKitZone {
|
||||
}
|
||||
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.saveIfNew(records, completion: completion)
|
||||
@ -473,7 +472,7 @@ public extension CloudKitZone {
|
||||
completion(.success((savedSubscription!)))
|
||||
}
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.save(subscription, completion: completion)
|
||||
@ -666,7 +665,7 @@ public extension CloudKitZone {
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||
@ -727,7 +726,7 @@ public extension CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
saveChunks() { result in
|
||||
saveChunks { result in
|
||||
switch result {
|
||||
case .success:
|
||||
deleteChunks()
|
||||
@ -763,11 +762,11 @@ public extension CloudKitZone {
|
||||
op.fetchAllChanges = true
|
||||
op.qualityOfService = Self.qualityOfService
|
||||
|
||||
op.recordZoneChangeTokensUpdatedBlock = { zoneID, token, _ in
|
||||
op.recordZoneChangeTokensUpdatedBlock = { _, token, _ in
|
||||
savedChangeToken = token
|
||||
}
|
||||
|
||||
op.recordWasChangedBlock = { recordID, result in
|
||||
op.recordWasChangedBlock = { _, result in
|
||||
if let record = try? result.get() {
|
||||
changedRecords.append(record)
|
||||
}
|
||||
@ -778,7 +777,7 @@ public extension CloudKitZone {
|
||||
deletedRecordKeys.append(recordKey)
|
||||
}
|
||||
|
||||
op.recordZoneFetchResultBlock = { recordZoneID, result in
|
||||
op.recordZoneFetchResultBlock = { _, result in
|
||||
if let (token, _, _) = try? result.get() {
|
||||
savedChangeToken = token
|
||||
}
|
||||
@ -809,7 +808,7 @@ public extension CloudKitZone {
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
|
||||
case .zoneNotFound:
|
||||
self.createZoneRecord() { result in
|
||||
self.createZoneRecord { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.fetchChangesInZone(completion: completion)
|
||||
|
@ -19,15 +19,15 @@ public enum CloudKitZoneResult {
|
||||
case zoneNotFound
|
||||
case userDeletedZone
|
||||
case failure(error: Error)
|
||||
|
||||
|
||||
public static func resolve(_ error: Error?) -> CloudKitZoneResult {
|
||||
|
||||
|
||||
guard error != nil else { return .success }
|
||||
|
||||
|
||||
guard let ckError = error as? CKError else {
|
||||
return .failure(error: error!)
|
||||
}
|
||||
|
||||
|
||||
switch ckError.code {
|
||||
case .serviceUnavailable, .requestRateLimited, .zoneBusy:
|
||||
if let retry = ckError.userInfo[CKErrorRetryAfterKey] as? NSNumber {
|
||||
@ -60,22 +60,22 @@ public enum CloudKitZoneResult {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension CloudKitZoneResult {
|
||||
|
||||
|
||||
static func anyRequestErrors(_ errors: [AnyHashable: CKError]) -> CloudKitZoneResult? {
|
||||
if errors.values.contains(where: { $0.code == .changeTokenExpired } ) {
|
||||
if errors.values.contains(where: { $0.code == .changeTokenExpired }) {
|
||||
return .changeTokenExpired
|
||||
}
|
||||
if errors.values.contains(where: { $0.code == .zoneNotFound } ) {
|
||||
if errors.values.contains(where: { $0.code == .zoneNotFound }) {
|
||||
return .zoneNotFound
|
||||
}
|
||||
if errors.values.contains(where: { $0.code == .userDeletedZone } ) {
|
||||
if errors.values.contains(where: { $0.code == .userDeletedZone }) {
|
||||
return .userDeletedZone
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ struct QueueCall: Equatable {
|
||||
|
||||
func perform() {
|
||||
|
||||
let _ = target?.perform(selector)
|
||||
_ = target?.perform(selector)
|
||||
}
|
||||
|
||||
static func ==(lhs: QueueCall, rhs: QueueCall) -> Bool {
|
||||
@ -38,7 +38,7 @@ struct QueueCall: Equatable {
|
||||
private let interval: TimeInterval
|
||||
private let maxInterval: TimeInterval
|
||||
private var lastCallTime = Date.distantFuture
|
||||
private var timer: Timer? = nil
|
||||
private var timer: Timer?
|
||||
private var calls = [QueueCall]()
|
||||
|
||||
public init(name: String, interval: TimeInterval = 0.05, maxInterval: TimeInterval = 2.0) {
|
||||
@ -63,12 +63,12 @@ struct QueueCall: Equatable {
|
||||
call.perform()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func timerDidFire(_ sender: Any?) {
|
||||
lastCallTime = Date()
|
||||
performCallsImmediately()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension CoalescingQueue {
|
||||
|
@ -16,7 +16,7 @@ extension Notification.Name {
|
||||
/// A type that provides a name for display to the user.
|
||||
|
||||
public protocol DisplayNameProvider {
|
||||
|
||||
|
||||
var nameForDisplay: String { get }
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@ import Foundation
|
||||
|
||||
public extension FileManager {
|
||||
|
||||
|
||||
/// Returns whether a path refers to a folder.
|
||||
///
|
||||
/// - Parameter path: The file path to check.
|
||||
@ -82,7 +81,7 @@ public extension FileManager {
|
||||
guard let filenames = self.filenames(inFolder: folder) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let url = URL(fileURLWithPath: folder)
|
||||
return filenames.map { url.appendingPathComponent($0).path }
|
||||
}
|
||||
@ -101,7 +100,7 @@ private extension FileManager {
|
||||
assert(fileExists(atPath: source))
|
||||
|
||||
if fileExists(atPath: destination) {
|
||||
if (overwriting) {
|
||||
if overwriting {
|
||||
try removeItem(atPath: destination)
|
||||
}
|
||||
}
|
||||
|
@ -16,11 +16,11 @@ public extension CGRect {
|
||||
/// - Returns: A new rectangle, cenetered vertically in `containerRect`,
|
||||
/// with the same size as the source rectangle.
|
||||
func centeredVertically(in containerRect: CGRect) -> CGRect {
|
||||
var r = self;
|
||||
r.origin.y = containerRect.midY - (r.height / 2.0);
|
||||
r = r.integral;
|
||||
r.size = self.size;
|
||||
return r;
|
||||
var r = self
|
||||
r.origin.y = containerRect.midY - (r.height / 2.0)
|
||||
r = r.integral
|
||||
r.size = self.size
|
||||
return r
|
||||
}
|
||||
|
||||
/// Centers a rectangle horizontally in another rectangle.
|
||||
@ -29,11 +29,11 @@ public extension CGRect {
|
||||
/// - Returns: A new rectangle, cenetered horizontally in `containerRect`,
|
||||
/// with the same size as the source rectangle.
|
||||
func centeredHorizontally(in containerRect: CGRect) -> CGRect {
|
||||
var r = self;
|
||||
r.origin.x = containerRect.midX - (r.width / 2.0);
|
||||
r = r.integral;
|
||||
r.size = self.size;
|
||||
return r;
|
||||
var r = self
|
||||
r.origin.x = containerRect.midX - (r.width / 2.0)
|
||||
r = r.integral
|
||||
r.size = self.size
|
||||
return r
|
||||
}
|
||||
|
||||
/// Centers a rectangle in another rectangle.
|
||||
|
@ -89,8 +89,7 @@ public extension MainThreadOperation {
|
||||
}
|
||||
if Thread.isMainThread {
|
||||
operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.informOperationDelegateOfCompletion()
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ public final class MainThreadOperationQueue {
|
||||
/// those operations will be canceled also.
|
||||
public func cancelOperations(_ operations: [MainThreadOperation]) {
|
||||
precondition(Thread.isMainThread)
|
||||
let operationIDsToCancel = operations.map{ ensureOperationID($0) }
|
||||
let operationIDsToCancel = operations.map { ensureOperationID($0) }
|
||||
assert(allOperationIDsArePendingOrCurrent(operationIDsToCancel))
|
||||
assert(allOperationIDsAreInStorage(operationIDsToCancel))
|
||||
|
||||
@ -182,8 +182,7 @@ private extension MainThreadOperationQueue {
|
||||
|
||||
if operation.isCanceled {
|
||||
dependencies.operationIDWasCanceled(operationID)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
dependencies.operationIDDidComplete(operationID)
|
||||
}
|
||||
|
||||
@ -249,7 +248,7 @@ private extension MainThreadOperationQueue {
|
||||
guard !operationIDs.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let operationIDsToCancel = operationIDsByAddingChildOperationIDs(operationIDs)
|
||||
setCanceledAndRemoveDelegate(for: operationIDsToCancel)
|
||||
callCompletionBlockForOperationIDs(operationIDsToCancel)
|
||||
|
@ -9,13 +9,13 @@
|
||||
@preconcurrency import Foundation
|
||||
|
||||
public final class ManagedResourceFile: NSObject, NSFilePresenter {
|
||||
|
||||
|
||||
private var isDirty = false {
|
||||
didSet {
|
||||
queueSaveToDiskIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var isLoading = false
|
||||
private let fileURL: URL
|
||||
private let operationQueue: OperationQueue
|
||||
@ -30,63 +30,63 @@ public final class ManagedResourceFile: NSObject, NSFilePresenter {
|
||||
saveQueue = CoalescingQueue(name: "ManagedResourceFile Save Queue", interval: saveInterval)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var presentedItemURL: URL? {
|
||||
return fileURL
|
||||
}
|
||||
|
||||
|
||||
public var presentedItemOperationQueue: OperationQueue {
|
||||
return operationQueue
|
||||
}
|
||||
|
||||
|
||||
public init(fileURL: URL, load: @escaping () -> Void, save: @escaping () -> Void) {
|
||||
|
||||
|
||||
self.fileURL = fileURL
|
||||
self.loadCallback = load
|
||||
self.saveCallback = save
|
||||
|
||||
|
||||
saveQueue = CoalescingQueue(name: "ManagedResourceFile Save Queue", interval: saveInterval)
|
||||
operationQueue = OperationQueue()
|
||||
operationQueue.qualityOfService = .userInteractive
|
||||
operationQueue.maxConcurrentOperationCount = 1
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
NSFileCoordinator.addFilePresenter(self)
|
||||
}
|
||||
|
||||
|
||||
public func presentedItemDidChange() {
|
||||
guard !isDirty else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.load()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func savePresentedItemChanges(completionHandler: @escaping (Error?) -> Void) {
|
||||
saveIfNecessary()
|
||||
completionHandler(nil)
|
||||
}
|
||||
|
||||
|
||||
public func relinquishPresentedItem(toReader reader: @escaping ((() -> Void)?) -> Void) {
|
||||
saveQueue.isPaused = true
|
||||
reader() {
|
||||
reader {
|
||||
self.saveQueue.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func relinquishPresentedItem(toWriter writer: @escaping ((() -> Void)?) -> Void) {
|
||||
saveQueue.isPaused = true
|
||||
writer() {
|
||||
writer {
|
||||
self.saveQueue.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func markAsDirty() {
|
||||
if !isLoading {
|
||||
isDirty = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func queueSaveToDiskIfNeeded() {
|
||||
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||
}
|
||||
@ -96,27 +96,27 @@ public final class ManagedResourceFile: NSObject, NSFilePresenter {
|
||||
loadCallback()
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
|
||||
public func saveIfNecessary() {
|
||||
saveQueue.performCallsImmediately()
|
||||
}
|
||||
|
||||
|
||||
public func resume() {
|
||||
NSFileCoordinator.addFilePresenter(self)
|
||||
}
|
||||
|
||||
|
||||
public func suspend() {
|
||||
NSFileCoordinator.removeFilePresenter(self)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
NSFileCoordinator.removeFilePresenter(self)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension ManagedResourceFile {
|
||||
|
||||
|
||||
@objc func saveToDiskIfNeeded() {
|
||||
if isDirty {
|
||||
isDirty = false
|
||||
|
@ -11,7 +11,7 @@ import Foundation
|
||||
// These functions eat errors.
|
||||
|
||||
public func propertyList(withData data: Data) -> Any? {
|
||||
|
||||
|
||||
do {
|
||||
return try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
|
||||
} catch {
|
||||
@ -22,11 +22,10 @@ public func propertyList(withData data: Data) -> Any? {
|
||||
// Create a binary plist.
|
||||
|
||||
public func data(withPropertyList plist: Any) -> Data? {
|
||||
|
||||
|
||||
do {
|
||||
return try PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0)
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -25,25 +25,25 @@ public extension RSImage {
|
||||
/// - Parameter color: The color with which to fill the mask image.
|
||||
/// - Returns: A new masked image.
|
||||
func maskWithColor(color: CGColor) -> RSImage? {
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
guard let maskImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
|
||||
#else
|
||||
guard let maskImage = cgImage else { return nil }
|
||||
#endif
|
||||
|
||||
|
||||
let width = size.width
|
||||
let height = size.height
|
||||
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
|
||||
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)!
|
||||
|
||||
|
||||
context.clip(to: bounds, mask: maskImage)
|
||||
context.setFillColor(color)
|
||||
context.fill(bounds)
|
||||
|
||||
|
||||
if let cgImage = context.makeImage() {
|
||||
#if os(macOS)
|
||||
let coloredImage = RSImage(cgImage: cgImage, size: CGSize(width: cgImage.width, height: cgImage.height))
|
||||
@ -54,7 +54,7 @@ public extension RSImage {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
@ -112,11 +112,11 @@ public extension RSImage {
|
||||
/// - data: The data object containing the image data.
|
||||
/// - maxPixelSize: The maximum dimension of the image.
|
||||
static func scaleImage(_ data: Data, maxPixelSize: Int) -> CGImage? {
|
||||
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let numberOfImages = CGImageSourceGetCount(imageSource)
|
||||
|
||||
// If the image size matches exactly, then return it.
|
||||
@ -134,14 +134,14 @@ public extension RSImage {
|
||||
if imagePixelWidth.intValue != maxPixelSize {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
guard let imagePixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? NSNumber else {
|
||||
continue
|
||||
}
|
||||
if imagePixelHeight.intValue != maxPixelSize {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
return CGImageSourceCreateImageAtIndex(imageSource, i, nil)
|
||||
}
|
||||
|
||||
@ -171,23 +171,22 @@ public extension RSImage {
|
||||
return CGImageSourceCreateImageAtIndex(imageSource, i, nil)
|
||||
}
|
||||
|
||||
|
||||
// If the image data contains a smaller image than the max size, just return it.
|
||||
for i in 0..<numberOfImages {
|
||||
|
||||
|
||||
guard let cfImageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) else {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
let imageProperties = cfImageProperties as NSDictionary
|
||||
|
||||
|
||||
guard let imagePixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? NSNumber else {
|
||||
continue
|
||||
}
|
||||
if imagePixelWidth.intValue < 1 || imagePixelWidth.intValue > maxPixelSize {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
guard let imagePixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? NSNumber else {
|
||||
continue
|
||||
}
|
||||
@ -197,9 +196,9 @@ public extension RSImage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return RSImage.createThumbnail(imageSource, maxPixelSize: maxPixelSize)
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Create a thumbnail from a CGImageSource.
|
||||
@ -208,10 +207,10 @@ public extension RSImage {
|
||||
/// - imageSource: The `CGImageSource` from which to create the thumbnail.
|
||||
/// - maxPixelSize: The maximum dimension of the resulting image.
|
||||
static func createThumbnail(_ imageSource: CGImageSource, maxPixelSize: Int) -> CGImage? {
|
||||
let options = [kCGImageSourceCreateThumbnailWithTransform : true,
|
||||
kCGImageSourceCreateThumbnailFromImageIfAbsent : true,
|
||||
kCGImageSourceThumbnailMaxPixelSize : NSNumber(value: maxPixelSize)]
|
||||
let options = [kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: NSNumber(value: maxPixelSize)]
|
||||
return CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -18,6 +18,5 @@ public protocol Renamable {
|
||||
/// - completion: A block called when the renaming completes or fails.
|
||||
/// - result: The result of the renaming.
|
||||
func rename(to: String, completion: @escaping (_ result: Result<Void, Error>) -> Void)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import Foundation
|
||||
|
||||
public struct SendToBlogEditorApp {
|
||||
|
||||
///The target descriptor of the application.
|
||||
/// The target descriptor of the application.
|
||||
///
|
||||
/// The easiest way to get this is probably `UserApp.targetDescriptor` or `NSAppleEventDescriptor(runningApplication:)`.
|
||||
///
|
||||
@ -49,7 +49,6 @@ public struct SendToBlogEditorApp {
|
||||
self.sourceFeedURL = sourceFeedURL
|
||||
}
|
||||
|
||||
|
||||
/// Sends the receiver's data to the blog editor application described by `targetDescriptor`.
|
||||
public func send() {
|
||||
|
||||
@ -57,7 +56,7 @@ public struct SendToBlogEditorApp {
|
||||
|
||||
appleEvent.setParam(paramDescriptor, forKeyword: keyDirectObject)
|
||||
|
||||
let _ = try? appleEvent.sendEvent(options: [.noReply, .canSwitchLayer, .alwaysInteract], timeout: .AEDefaultTimeout)
|
||||
_ = try? appleEvent.sendEvent(options: [.noReply, .canSwitchLayer, .alwaysInteract], timeout: .AEDefaultTimeout)
|
||||
|
||||
}
|
||||
|
||||
|
@ -46,4 +46,3 @@ public protocol SendToCommand {
|
||||
/// - selectedText: The currently selected text.
|
||||
func sendObject(_ object: Any?, selectedText: String?)
|
||||
}
|
||||
|
||||
|
@ -18,9 +18,9 @@ public extension Set {
|
||||
|
||||
return self[index]
|
||||
}
|
||||
|
||||
|
||||
func anyObject() -> Element? {
|
||||
|
||||
|
||||
if self.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
@ -25,14 +25,14 @@ public extension String {
|
||||
static func htmlWithLink(_ link: String) -> String {
|
||||
return link.htmlByAddingLink(link)
|
||||
}
|
||||
|
||||
|
||||
func hmacUsingSHA1(key: String) -> String {
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
|
||||
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), key, key.count, self, self.count, &digest)
|
||||
let data = Data(digest)
|
||||
return data.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public extension String {
|
||||
@ -87,7 +87,7 @@ public extension String {
|
||||
|
||||
let s = self.trimmingWhitespace
|
||||
|
||||
if (s.isEmpty || (!s.contains(".") && !s.mayBeIPv6URL && !s.hostMayBeLocalhost)) {
|
||||
if s.isEmpty || (!s.contains(".") && !s.mayBeIPv6URL && !s.hostMayBeLocalhost) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ public extension String {
|
||||
return self.replacingCharacters(in: range, with: "")
|
||||
}
|
||||
|
||||
return self;
|
||||
return self
|
||||
}
|
||||
|
||||
/// Removes an HTML tag and everything between its start and end tags.
|
||||
@ -264,7 +264,7 @@ public extension String {
|
||||
|
||||
if let maxCharacters = maxCharacters {
|
||||
charactersAdded += 1
|
||||
if (charactersAdded >= maxCharacters) {
|
||||
if charactersAdded >= maxCharacters {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -309,7 +309,6 @@ public extension String {
|
||||
return s.replacingOccurrences(of: "\\n{3,}", with: "\n\n", options: .regularExpression)
|
||||
}
|
||||
|
||||
|
||||
/// Returns a Boolean value indicating whether the string contains another string, case-insensitively.
|
||||
///
|
||||
/// - Parameter string: The string to search for.
|
||||
|
@ -10,8 +10,8 @@
|
||||
import UIKit
|
||||
|
||||
extension UIResponder {
|
||||
|
||||
private weak static var _currentFirstResponder: UIResponder? = nil
|
||||
|
||||
private weak static var _currentFirstResponder: UIResponder?
|
||||
|
||||
public static var isFirstResponderTextField: Bool {
|
||||
var isTextField = false
|
||||
@ -27,7 +27,7 @@ extension UIResponder {
|
||||
UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
|
||||
return UIResponder._currentFirstResponder
|
||||
}
|
||||
|
||||
|
||||
public static func resignCurrentFirstResponder() {
|
||||
if let responder = currentFirstResponder {
|
||||
responder.resignFirstResponder()
|
||||
@ -37,6 +37,6 @@ extension UIResponder {
|
||||
@objc internal func findFirstResponder(sender: AnyObject) {
|
||||
UIResponder._currentFirstResponder = self
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
|
@ -10,32 +10,32 @@
|
||||
import UIKit
|
||||
|
||||
extension UIView {
|
||||
|
||||
|
||||
public func setFrameIfNotEqual(_ rect: CGRect) {
|
||||
if !self.frame.equalTo(rect) {
|
||||
self.frame = rect
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func addChildAndPin(_ view: UIView) {
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(view)
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
safeAreaLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public func asImage() -> UIImage {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: bounds)
|
||||
return renderer.image { rendererContext in
|
||||
layer.render(in: rendererContext.cgContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
|
@ -10,14 +10,14 @@ import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
|
||||
// MARK: Autolayout
|
||||
|
||||
|
||||
public func addChildAndPinView(_ controller: UIViewController) {
|
||||
view.addChildAndPin(controller.view)
|
||||
addChild(controller)
|
||||
}
|
||||
|
||||
|
||||
public func replaceChildAndPinView(_ controller: UIViewController) {
|
||||
for subview in view.subviews {
|
||||
subview.removeFromSuperview()
|
||||
@ -27,9 +27,9 @@ extension UIViewController {
|
||||
}
|
||||
addChildAndPinView(controller)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Error Handling
|
||||
|
||||
|
||||
public func presentError(title: String, message: String, dismiss: (() -> Void)? = nil) {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
let dismissTitle = NSLocalizedString("OK", comment: "OK")
|
||||
@ -39,7 +39,7 @@ extension UIViewController {
|
||||
alertController.addAction(dismissAction)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: SwiftUI
|
||||
|
@ -9,9 +9,9 @@
|
||||
import UIKit
|
||||
|
||||
extension UIWindow {
|
||||
|
||||
|
||||
public var topViewController: UIViewController? {
|
||||
|
||||
|
||||
var top = self.rootViewController
|
||||
while true {
|
||||
if let presented = top?.presentedViewController {
|
||||
@ -33,10 +33,10 @@ extension UIWindow {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return top
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
|
@ -23,15 +23,15 @@ extension UndoableCommand {
|
||||
public func registerUndo() {
|
||||
|
||||
undoManager.setActionName(undoActionName)
|
||||
undoManager.registerUndo(withTarget: self) { (target) in
|
||||
undoManager.registerUndo(withTarget: self) { (_) in
|
||||
self.undo()
|
||||
}
|
||||
}
|
||||
|
||||
public func registerRedo() {
|
||||
|
||||
|
||||
undoManager.setActionName(redoActionName)
|
||||
undoManager.registerUndo(withTarget: self) { (target) in
|
||||
undoManager.registerUndo(withTarget: self) { (_) in
|
||||
self.perform()
|
||||
}
|
||||
}
|
||||
@ -40,33 +40,33 @@ extension UndoableCommand {
|
||||
// Useful for view controllers.
|
||||
|
||||
public protocol UndoableCommandRunner: AnyObject {
|
||||
|
||||
|
||||
var undoableCommands: [UndoableCommand] { get set }
|
||||
var undoManager: UndoManager? { get }
|
||||
|
||||
|
||||
func runCommand(_ undoableCommand: UndoableCommand)
|
||||
func clearUndoableCommands()
|
||||
}
|
||||
|
||||
public extension UndoableCommandRunner {
|
||||
|
||||
|
||||
func runCommand(_ undoableCommand: UndoableCommand) {
|
||||
|
||||
|
||||
pushUndoableCommand(undoableCommand)
|
||||
undoableCommand.perform()
|
||||
}
|
||||
|
||||
|
||||
func pushUndoableCommand(_ undoableCommand: UndoableCommand) {
|
||||
|
||||
|
||||
undoableCommands += [undoableCommand]
|
||||
}
|
||||
|
||||
|
||||
func clearUndoableCommands() {
|
||||
|
||||
|
||||
// Useful, for example, when timeline is reloaded and the list of articles changes.
|
||||
// Otherwise things like Redo Mark Read are ambiguous.
|
||||
// (Do they apply to the previous articles or to the current articles?)
|
||||
|
||||
|
||||
guard let undoManager else {
|
||||
return
|
||||
}
|
||||
|
@ -67,5 +67,3 @@ private final class IndeterminateProgressWindowController: NSWindowController {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
@ -13,7 +13,7 @@ public final class WebViewWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet private var webview: WKWebView!
|
||||
private var title: String!
|
||||
|
||||
|
||||
public convenience init(title: String) {
|
||||
self.init(window: nil)
|
||||
self.title = title
|
||||
@ -29,7 +29,7 @@ public final class WebViewWindowController: NSWindowController {
|
||||
|
||||
// We assume there might be images, style sheets, etc. contained by the folder that the file appears in, so we get read access to the parent folder.
|
||||
|
||||
let _ = self.window
|
||||
_ = self.window
|
||||
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let folderURL = fileURL.deletingLastPathComponent()
|
||||
|
@ -10,7 +10,7 @@ import XCTest
|
||||
import Foundation
|
||||
@testable import RSCore
|
||||
|
||||
//class Data_RSCoreTests: XCTestCase {
|
||||
// class Data_RSCoreTests: XCTestCase {
|
||||
// var bigHTML: String!
|
||||
//
|
||||
// var pngData: Data!
|
||||
@ -125,10 +125,10 @@ import Foundation
|
||||
//
|
||||
// }
|
||||
//
|
||||
//}
|
||||
// }
|
||||
|
||||
// Tests to compare the result of Data+RSCore with the old Objective-C versions.
|
||||
//extension Data_RSCoreTests {
|
||||
// extension Data_RSCoreTests {
|
||||
//
|
||||
// func testCompare_isProbablyHTML() {
|
||||
// let noLT = "html body".data(using: .utf8)!
|
||||
@ -186,4 +186,4 @@ import Foundation
|
||||
//
|
||||
// }
|
||||
//
|
||||
//}
|
||||
// }
|
||||
|
@ -44,7 +44,7 @@ class MacroProcessorTests: XCTestCase {
|
||||
func testEmptyDelimiters() {
|
||||
do {
|
||||
let template = "foo bar"
|
||||
let _ = try MacroProcessor.renderedText(withTemplate: template, substitutions: substitutions, macroStart: "")
|
||||
_ = try MacroProcessor.renderedText(withTemplate: template, substitutions: substitutions, macroStart: "")
|
||||
XCTFail("Error should be thrown")
|
||||
} catch {
|
||||
// Success
|
||||
@ -52,7 +52,7 @@ class MacroProcessorTests: XCTestCase {
|
||||
|
||||
do {
|
||||
let template = "foo bar"
|
||||
let _ = try MacroProcessor.renderedText(withTemplate: template, substitutions: substitutions, macroEnd: "")
|
||||
_ = try MacroProcessor.renderedText(withTemplate: template, substitutions: substitutions, macroEnd: "")
|
||||
XCTFail("Error should be thrown")
|
||||
} catch {
|
||||
// Success
|
||||
|
@ -212,9 +212,9 @@ class MainThreadOperationTests: XCTestCase {
|
||||
waitForExpectations(timeout: 1.0, handler: nil)
|
||||
XCTAssertTrue(queue.pendingOperationsCount == 0)
|
||||
}
|
||||
|
||||
|
||||
func testCancelingDisownsOperation() {
|
||||
|
||||
|
||||
final class SlowFinishingOperation: MainThreadOperation {
|
||||
|
||||
let didCancelExpectation: XCTestExpectation
|
||||
@ -231,8 +231,8 @@ class MainThreadOperationTests: XCTestCase {
|
||||
var operationDelegate: MainThreadOperationDelegate?
|
||||
var name: String?
|
||||
var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
|
||||
var didStartRunBlock: (() -> ())?
|
||||
|
||||
var didStartRunBlock: (() -> Void)?
|
||||
|
||||
init(didCancelExpectation: XCTestExpectation) {
|
||||
self.didCancelExpectation = didCancelExpectation
|
||||
@ -252,7 +252,7 @@ class MainThreadOperationTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let queue = MainThreadOperationQueue()
|
||||
let didCancelExpectation = expectation(description: "Did Cancel Operation")
|
||||
let completionBlockDidRunExpectation = expectation(description: "Completion Block Did Run")
|
||||
@ -273,10 +273,10 @@ class MainThreadOperationTests: XCTestCase {
|
||||
}
|
||||
return operation
|
||||
}()
|
||||
|
||||
|
||||
// The queue should take ownership of the operation (asserted below).
|
||||
queue.add(operation!)
|
||||
|
||||
|
||||
// Verify something other than this scope has ownership of the operation.
|
||||
weak var addedOperation = operation!
|
||||
operation = nil
|
||||
|
@ -10,6 +10,6 @@ final class RSCoreTests: XCTestCase {
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testExample", testExample),
|
||||
("testExample", testExample)
|
||||
]
|
||||
}
|
||||
|
@ -168,16 +168,16 @@ class String_RSCore: XCTestCase {
|
||||
|
||||
self.measure {
|
||||
for _ in 0..<1000 {
|
||||
let _ = s1.md5String
|
||||
let _ = s2.md5String
|
||||
let _ = s3.md5String
|
||||
let _ = s4.md5String
|
||||
let _ = s5.md5String
|
||||
let _ = s6.md5String
|
||||
let _ = s7.md5String
|
||||
let _ = s8.md5String
|
||||
let _ = s9.md5String
|
||||
let _ = s10.md5String
|
||||
_ = s1.md5String
|
||||
_ = s2.md5String
|
||||
_ = s3.md5String
|
||||
_ = s4.md5String
|
||||
_ = s5.md5String
|
||||
_ = s6.md5String
|
||||
_ = s7.md5String
|
||||
_ = s8.md5String
|
||||
_ = s9.md5String
|
||||
_ = s10.md5String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import XCTest
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(RSCoreTests.allTests),
|
||||
testCase(RSCoreTests.allTests)
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
@ -9,7 +9,7 @@ let package = Package(
|
||||
.library(
|
||||
name: "RSTree",
|
||||
type: .dynamic,
|
||||
targets: ["RSTree"]),
|
||||
targets: ["RSTree"])
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
@ -21,7 +21,7 @@ public extension NSOutlineView {
|
||||
if numberOfNodes < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
let indexOfNodeToSelect = numberOfNodes - 1
|
||||
|
||||
for i in 1...indexOfNodeToSelect { // Start at 1 to skip root node.
|
||||
@ -36,8 +36,7 @@ public extension NSOutlineView {
|
||||
selectRowIndexes(NSIndexSet(index: oneRow) as IndexSet, byExtendingSelection: false)
|
||||
scrollRowToVisible(oneRow)
|
||||
return true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
expandItem(oneNode)
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import Foundation
|
||||
// Main thread only.
|
||||
|
||||
public final class Node: Hashable {
|
||||
|
||||
|
||||
public weak var parent: Node?
|
||||
public let representedObject: AnyObject
|
||||
public var canHaveChildNodes = false
|
||||
@ -26,11 +26,11 @@ public final class Node: Hashable {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
public var numberOfChildNodes: Int {
|
||||
return childNodes.count
|
||||
}
|
||||
|
||||
|
||||
public var indexPath: IndexPath {
|
||||
if let parent = parent {
|
||||
let parentPath = parent.indexPath
|
||||
@ -39,22 +39,22 @@ public final class Node: Hashable {
|
||||
}
|
||||
preconditionFailure("A Node’s parent must contain it as a child.")
|
||||
}
|
||||
return IndexPath(index: 0) //root node
|
||||
return IndexPath(index: 0) // root node
|
||||
}
|
||||
|
||||
|
||||
public var level: Int {
|
||||
if let parent = parent {
|
||||
return parent.level + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
public var isLeaf: Bool {
|
||||
return numberOfChildNodes < 1
|
||||
}
|
||||
|
||||
|
||||
public init(representedObject: AnyObject, parent: Node?) {
|
||||
|
||||
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
self.representedObject = representedObject
|
||||
@ -63,9 +63,9 @@ public final class Node: Hashable {
|
||||
self.uniqueID = Node.incrementingID
|
||||
Node.incrementingID += 1
|
||||
}
|
||||
|
||||
|
||||
public class func genericRootNode() -> Node {
|
||||
|
||||
|
||||
let node = Node(representedObject: TopLevelRepresentedObject(), parent: nil)
|
||||
node.canHaveChildNodes = true
|
||||
return node
|
||||
@ -86,7 +86,7 @@ public final class Node: Hashable {
|
||||
}
|
||||
|
||||
public func childAtIndex(_ index: Int) -> Node? {
|
||||
|
||||
|
||||
if index >= childNodes.count || index < 0 {
|
||||
return nil
|
||||
}
|
||||
@ -94,12 +94,12 @@ public final class Node: Hashable {
|
||||
}
|
||||
|
||||
public func indexOfChild(_ node: Node) -> Int? {
|
||||
|
||||
return childNodes.firstIndex{ (oneChildNode) -> Bool in
|
||||
|
||||
return childNodes.firstIndex { (oneChildNode) -> Bool in
|
||||
oneChildNode === node
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func childNodeRepresentingObject(_ obj: AnyObject) -> Node? {
|
||||
return findNodeRepresentingObject(obj, recursively: false)
|
||||
}
|
||||
@ -182,12 +182,11 @@ public final class Node: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public extension Array where Element == Node {
|
||||
|
||||
func representedObjects() -> [AnyObject] {
|
||||
|
||||
return self.map{ $0.representedObject }
|
||||
return self.map { $0.representedObject }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,7 @@ public struct NodePath {
|
||||
if let parent = nomad.parent {
|
||||
tempArray.append(parent)
|
||||
nomad = parent
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -34,8 +33,7 @@ public struct NodePath {
|
||||
|
||||
if let node = treeController.nodeInTreeRepresentingObject(representedObject) {
|
||||
self.init(node: node)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,11 @@
|
||||
import Foundation
|
||||
|
||||
public protocol TreeControllerDelegate: AnyObject {
|
||||
|
||||
|
||||
func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]?
|
||||
}
|
||||
|
||||
public typealias NodeVisitBlock = (_ : Node) -> Void
|
||||
public typealias NodeVisitBlock = (_: Node) -> Void
|
||||
|
||||
public final class TreeController {
|
||||
|
||||
@ -21,32 +21,32 @@ public final class TreeController {
|
||||
public let rootNode: Node
|
||||
|
||||
public init(delegate: TreeControllerDelegate, rootNode: Node) {
|
||||
|
||||
|
||||
self.delegate = delegate
|
||||
self.rootNode = rootNode
|
||||
rebuild()
|
||||
}
|
||||
|
||||
public convenience init(delegate: TreeControllerDelegate) {
|
||||
|
||||
|
||||
self.init(delegate: delegate, rootNode: Node.genericRootNode())
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
public func rebuild() -> Bool {
|
||||
|
||||
// Rebuild and re-sort. Return true if any changes in the entire tree.
|
||||
|
||||
|
||||
return rebuildChildNodes(node: rootNode)
|
||||
}
|
||||
|
||||
|
||||
public func visitNodes(_ visitBlock: NodeVisitBlock) {
|
||||
|
||||
|
||||
visitNode(rootNode, visitBlock)
|
||||
}
|
||||
|
||||
|
||||
public func nodeInArrayRepresentingObject(nodes: [Node], representedObject: AnyObject, recurse: Bool = false) -> Node? {
|
||||
|
||||
|
||||
for oneNode in nodes {
|
||||
|
||||
if oneNode.representedObject === representedObject {
|
||||
@ -85,17 +85,17 @@ public final class TreeController {
|
||||
}
|
||||
|
||||
private extension TreeController {
|
||||
|
||||
|
||||
func visitNode(_ node: Node, _ visitBlock: NodeVisitBlock) {
|
||||
|
||||
|
||||
visitBlock(node)
|
||||
for oneChildNode in node.childNodes {
|
||||
visitNode(oneChildNode, visitBlock)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func nodeArraysAreEqual(_ nodeArray1: [Node]?, _ nodeArray2: [Node]?) -> Bool {
|
||||
|
||||
|
||||
if nodeArray1 == nil && nodeArray2 == nil {
|
||||
return true
|
||||
}
|
||||
@ -105,25 +105,25 @@ private extension TreeController {
|
||||
if nodeArray1 == nil && nodeArray2 != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return nodeArray1! == nodeArray2!
|
||||
}
|
||||
|
||||
|
||||
func rebuildChildNodes(node: Node) -> Bool {
|
||||
|
||||
|
||||
if !node.canHaveChildNodes {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
var childNodesDidChange = false
|
||||
|
||||
|
||||
let childNodes = delegate?.treeController(treeController: self, childNodesFor: node) ?? [Node]()
|
||||
|
||||
|
||||
childNodesDidChange = !nodeArraysAreEqual(childNodes, node.childNodes)
|
||||
if (childNodesDidChange) {
|
||||
if childNodesDidChange {
|
||||
node.childNodes = childNodes
|
||||
}
|
||||
|
||||
|
||||
for oneChildNode in childNodes {
|
||||
if rebuildChildNodes(node: oneChildNode) {
|
||||
childNodesDidChange = true
|
||||
|
@ -9,11 +9,11 @@ let package = Package(
|
||||
.library(
|
||||
name: "RSWeb",
|
||||
type: .dynamic,
|
||||
targets: ["RSWeb"]),
|
||||
targets: ["RSWeb"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../Parser"),
|
||||
.package(path: "../RSCore"),
|
||||
.package(path: "../RSCore")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@ -26,6 +26,6 @@ let package = Package(
|
||||
),
|
||||
.testTarget(
|
||||
name: "RSWebTests",
|
||||
dependencies: ["RSWeb"]),
|
||||
dependencies: ["RSWeb"])
|
||||
]
|
||||
)
|
||||
|
@ -21,7 +21,7 @@ public struct CacheControlInfo: Codable, Equatable {
|
||||
public var canResume: Bool {
|
||||
Date() >= resumeDate
|
||||
}
|
||||
|
||||
|
||||
public init?(urlResponse: HTTPURLResponse) {
|
||||
guard let cacheControlValue = urlResponse.valueForHTTPHeaderField(HTTPResponseHeader.cacheControl) else {
|
||||
return nil
|
||||
@ -35,7 +35,7 @@ public struct CacheControlInfo: Codable, Equatable {
|
||||
guard let maxAge = Self.parseMaxAge(value) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let d = Date()
|
||||
self.dateCreated = d
|
||||
self.maxAge = maxAge
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Dictionary where Key == String, Value == String {
|
||||
public extension Dictionary where Key == String, Value == String {
|
||||
|
||||
/// Translates a dictionary into a string like `foo=bar¶m2=some%20thing`.
|
||||
var urlQueryString: String? {
|
||||
|
@ -11,12 +11,12 @@ import Foundation
|
||||
// Main thread only.
|
||||
|
||||
public extension Notification.Name {
|
||||
|
||||
|
||||
static let DownloadProgressDidChange = Notification.Name(rawValue: "DownloadProgressDidChange")
|
||||
}
|
||||
|
||||
public final class DownloadProgress {
|
||||
|
||||
|
||||
public var numberOfTasks = 0 {
|
||||
didSet {
|
||||
if numberOfTasks == 0 && numberRemaining != 0 {
|
||||
@ -27,7 +27,7 @@ public final class DownloadProgress {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var numberRemaining = 0 {
|
||||
didSet {
|
||||
if numberRemaining != oldValue {
|
||||
@ -46,22 +46,22 @@ public final class DownloadProgress {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
|
||||
public var isComplete: Bool {
|
||||
assert(Thread.isMainThread)
|
||||
return numberRemaining < 1
|
||||
}
|
||||
|
||||
|
||||
public init(numberOfTasks: Int) {
|
||||
assert(Thread.isMainThread)
|
||||
self.numberOfTasks = numberOfTasks
|
||||
}
|
||||
|
||||
|
||||
public func addToNumberOfTasks(_ n: Int) {
|
||||
assert(Thread.isMainThread)
|
||||
numberOfTasks = numberOfTasks + n
|
||||
}
|
||||
|
||||
|
||||
public func addToNumberOfTasksAndRemaining(_ n: Int) {
|
||||
assert(Thread.isMainThread)
|
||||
numberOfTasks = numberOfTasks + n
|
||||
@ -74,14 +74,14 @@ public final class DownloadProgress {
|
||||
numberRemaining = numberRemaining - 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func completeTasks(_ tasks: Int) {
|
||||
assert(Thread.isMainThread)
|
||||
if numberRemaining >= tasks {
|
||||
numberRemaining = numberRemaining - tasks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func reset() {
|
||||
assert(Thread.isMainThread)
|
||||
numberRemaining = 0
|
||||
@ -92,7 +92,7 @@ public final class DownloadProgress {
|
||||
// MARK: - Private
|
||||
|
||||
private extension DownloadProgress {
|
||||
|
||||
|
||||
func postDidChangeNotification() {
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .DownloadProgressDidChange, object: self)
|
||||
|
@ -40,13 +40,12 @@ public protocol DownloadSessionDelegate {
|
||||
/// These URLs are skipped for the rest of the session.
|
||||
private var urlsWith400s = Set<URL>()
|
||||
|
||||
|
||||
public init(delegate: DownloadSessionDelegate) {
|
||||
|
||||
self.delegate = delegate
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
let sessionConfiguration = URLSessionConfiguration.ephemeral
|
||||
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||
sessionConfiguration.timeoutIntervalForRequest = 15.0
|
||||
@ -60,9 +59,9 @@ public protocol DownloadSessionDelegate {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
|
||||
urlSession = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
|
||||
urlSession = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
urlSession.invalidateAndCancel()
|
||||
}
|
||||
@ -170,7 +169,7 @@ extension DownloadSession: URLSessionDataDelegate {
|
||||
addDataTaskFromQueueIfNecessary()
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
|
||||
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
|
||||
guard let info = infoForTask(dataTask) else {
|
||||
@ -279,7 +278,7 @@ private extension DownloadSession {
|
||||
|
||||
var currentURL = url
|
||||
|
||||
while(true) {
|
||||
while true {
|
||||
|
||||
if let oneRedirectURL = redirectCache[currentURL] {
|
||||
|
||||
@ -289,9 +288,7 @@ private extension DownloadSession {
|
||||
}
|
||||
urls.insert(oneRedirectURL)
|
||||
currentURL = oneRedirectURL
|
||||
}
|
||||
|
||||
else {
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -449,7 +446,7 @@ extension URLSessionTask {
|
||||
// MARK: - DownloadInfo
|
||||
|
||||
private final class DownloadInfo {
|
||||
|
||||
|
||||
let url: URL
|
||||
let data = NSMutableData()
|
||||
var urlResponse: URLResponse?
|
||||
@ -458,9 +455,9 @@ private final class DownloadInfo {
|
||||
|
||||
self.url = url
|
||||
}
|
||||
|
||||
|
||||
func addData(_ d: Data) {
|
||||
|
||||
|
||||
data.append(d)
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ public final class Downloader {
|
||||
sessionConfiguration.httpCookieAcceptPolicy = .never
|
||||
sessionConfiguration.httpMaximumConnectionsPerHost = 1
|
||||
sessionConfiguration.httpCookieStorage = nil
|
||||
|
||||
|
||||
if let userAgentHeaders = UserAgent.headers() {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
@ -47,7 +47,7 @@ public final class Downloader {
|
||||
urlRequestToUse.addSpecialCaseUserAgentIfNeeded()
|
||||
|
||||
let task = urlSession.dataTask(with: urlRequestToUse) { (data, response, error) in
|
||||
DispatchQueue.main.async() {
|
||||
DispatchQueue.main.async {
|
||||
completion?(data, response, error)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ public extension Notification.Name {
|
||||
public final class HTMLMetadataCache: Sendable {
|
||||
|
||||
static let shared = HTMLMetadataCache()
|
||||
|
||||
|
||||
// Sent along with .htmlMetadataAvailable notification
|
||||
public struct UserInfoKey {
|
||||
public static let htmlMetadata = "htmlMetadata"
|
||||
|
@ -19,7 +19,7 @@ public final class HTMLMetadataDownloader: Sendable {
|
||||
|
||||
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HTMLMetadataDownloader")
|
||||
private static let debugLoggingEnabled = false
|
||||
|
||||
|
||||
private let cache = HTMLMetadataCache()
|
||||
private let attemptDatesLock = OSAllocatedUnfairLock(initialState: [String: Date]())
|
||||
private let urlsReturning4xxsLock = OSAllocatedUnfairLock(initialState: Set<String>())
|
||||
@ -88,7 +88,7 @@ private extension HTMLMetadataDownloader {
|
||||
Self.logger.debug("HTMLMetadataDownloader downloading for \(url)")
|
||||
}
|
||||
|
||||
Downloader.shared.download(actualURL) { data, response, error in
|
||||
Downloader.shared.download(actualURL) { data, response, _ in
|
||||
if let data, !data.isEmpty, let response, response.statusIsOK {
|
||||
let urlToUse = response.url ?? actualURL
|
||||
let parserData = ParserData(url: urlToUse.absoluteString, data: data)
|
||||
|
@ -9,10 +9,10 @@
|
||||
import Foundation
|
||||
|
||||
public struct HTTPConditionalGetInfo: Codable, Equatable {
|
||||
|
||||
|
||||
public let lastModified: String?
|
||||
public let etag: String?
|
||||
|
||||
|
||||
public init?(lastModified: String?, etag: String?) {
|
||||
if lastModified == nil && etag == nil {
|
||||
return nil
|
||||
@ -20,19 +20,19 @@ public struct HTTPConditionalGetInfo: Codable, Equatable {
|
||||
self.lastModified = lastModified
|
||||
self.etag = etag
|
||||
}
|
||||
|
||||
|
||||
public init?(urlResponse: HTTPURLResponse) {
|
||||
let lastModified = urlResponse.valueForHTTPHeaderField(HTTPResponseHeader.lastModified)
|
||||
let etag = urlResponse.valueForHTTPHeaderField(HTTPResponseHeader.etag)
|
||||
self.init(lastModified: lastModified, etag: etag)
|
||||
}
|
||||
|
||||
public init?(headers: [AnyHashable : Any]) {
|
||||
public init?(headers: [AnyHashable: Any]) {
|
||||
let lastModified = headers[HTTPResponseHeader.lastModified] as? String
|
||||
let etag = headers[HTTPResponseHeader.etag] as? String
|
||||
self.init(lastModified: lastModified, etag: etag)
|
||||
}
|
||||
|
||||
|
||||
public func addRequestHeadersToURLRequest(_ urlRequest: inout URLRequest) {
|
||||
// Bug seen in the wild: lastModified with last possible 32-bit date, which is in 2038. Ignore those.
|
||||
// TODO: drop this check in late 2037.
|
||||
|
@ -9,15 +9,15 @@
|
||||
import Foundation
|
||||
|
||||
public struct HTTPDateInfo: Codable, Equatable {
|
||||
|
||||
|
||||
private static let formatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "EEEE, dd LLL yyyy HH:mm:ss zzz"
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
public let date: Date?
|
||||
|
||||
|
||||
public init?(urlResponse: HTTPURLResponse) {
|
||||
if let headerDate = urlResponse.valueForHTTPHeaderField(HTTPResponseHeader.date) {
|
||||
date = HTTPDateInfo.formatter.date(from: headerDate)
|
||||
@ -25,5 +25,5 @@ public struct HTTPDateInfo: Codable, Equatable {
|
||||
date = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,33 +9,33 @@
|
||||
import Foundation
|
||||
|
||||
public struct HTTPLinkPagingInfo {
|
||||
|
||||
|
||||
public let nextPage: String?
|
||||
public let lastPage: String?
|
||||
|
||||
|
||||
public init(nextPage: String?, lastPage: String?) {
|
||||
self.nextPage = nextPage
|
||||
self.lastPage = lastPage
|
||||
}
|
||||
|
||||
public init(urlResponse: HTTPURLResponse) {
|
||||
|
||||
|
||||
guard let linkHeader = urlResponse.valueForHTTPHeaderField(HTTPResponseHeader.link) else {
|
||||
self.init(nextPage: nil, lastPage: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let links = linkHeader.components(separatedBy: ",")
|
||||
|
||||
|
||||
var dict: [String: String] = [:]
|
||||
for link in links {
|
||||
let components = link.components(separatedBy:"; ")
|
||||
let components = link.components(separatedBy: "; ")
|
||||
let page = components[0].trimmingCharacters(in: CharacterSet(charactersIn: " <>"))
|
||||
dict[components[1]] = page
|
||||
}
|
||||
|
||||
|
||||
self.init(nextPage: dict["rel=\"next\""], lastPage: dict["rel=\"last\""])
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ public struct HTTPRequestHeader {
|
||||
public static let userAgent = "User-Agent"
|
||||
public static let authorization = "Authorization"
|
||||
public static let contentType = "Content-Type"
|
||||
|
||||
|
||||
// Conditional GET
|
||||
|
||||
|
||||
public static let ifModifiedSince = "If-Modified-Since"
|
||||
public static let ifNoneMatch = "If-None-Match" //Etag
|
||||
public static let ifNoneMatch = "If-None-Match" // Etag
|
||||
}
|
||||
|
@ -12,10 +12,10 @@ public struct HTTPResponseCode {
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
// Not an enum because the main interest is the actual values.
|
||||
|
||||
public static let responseContinue = 100 //"continue" is a language keyword, hence the weird name
|
||||
|
||||
public static let responseContinue = 100 // "continue" is a language keyword, hence the weird name
|
||||
public static let switchingProtocols = 101
|
||||
|
||||
|
||||
public static let OK = 200
|
||||
public static let created = 201
|
||||
public static let accepted = 202
|
||||
@ -23,7 +23,7 @@ public struct HTTPResponseCode {
|
||||
public static let noContent = 204
|
||||
public static let resetContent = 205
|
||||
public static let partialContent = 206
|
||||
|
||||
|
||||
public static let redirectMultipleChoices = 300
|
||||
public static let redirectPermanent = 301
|
||||
public static let redirectTemporary = 302
|
||||
|
@ -17,7 +17,7 @@ public struct HTTPResponseHeader {
|
||||
|
||||
// Conditional GET. See:
|
||||
// http://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers/
|
||||
|
||||
|
||||
public static let lastModified = "Last-Modified"
|
||||
// Changed to the canonical case for lookups against a case sensitive dictionary
|
||||
// https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
|
||||
|
@ -21,7 +21,7 @@ public class MacWebBrowser {
|
||||
return false
|
||||
}
|
||||
|
||||
if (inBackground) {
|
||||
if inBackground {
|
||||
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
configuration.activates = false
|
||||
@ -124,23 +124,23 @@ public class MacWebBrowser {
|
||||
/// - url: The URL to open.
|
||||
/// - inBackground: If `true`, attempt to load the URL without bringing the browser to the foreground.
|
||||
@discardableResult public func openURL(_ url: URL, inBackground: Bool = false) -> Bool {
|
||||
|
||||
|
||||
// TODO: make this function async.
|
||||
|
||||
|
||||
guard let preparedURL = url.preparedForOpeningInBrowser() else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Task { @MainActor in
|
||||
|
||||
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
if inBackground {
|
||||
configuration.activates = false
|
||||
}
|
||||
|
||||
|
||||
NSWorkspace.shared.open([preparedURL], withApplicationAt: self.url, configuration: configuration, completionHandler: nil)
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -148,7 +148,7 @@ public class MacWebBrowser {
|
||||
extension MacWebBrowser: CustomDebugStringConvertible {
|
||||
|
||||
public var debugDescription: String {
|
||||
if let name = name, let bundleIdentifier = bundleIdentifier{
|
||||
if let name = name, let bundleIdentifier = bundleIdentifier {
|
||||
return "MacWebBrowser: \(name) (\(bundleIdentifier))"
|
||||
} else {
|
||||
return "MacWebBrowser"
|
||||
|
@ -9,9 +9,9 @@
|
||||
import Foundation
|
||||
|
||||
public struct MimeType {
|
||||
|
||||
|
||||
// This could certainly use expansion.
|
||||
|
||||
|
||||
public static let png = "image/png"
|
||||
public static let jpeg = "image/jpeg"
|
||||
public static let jpg = "image/jpg"
|
||||
@ -20,29 +20,29 @@ public struct MimeType {
|
||||
}
|
||||
|
||||
public extension String {
|
||||
|
||||
|
||||
func isMimeTypeImage() -> Bool {
|
||||
|
||||
|
||||
return self.isOfGeneralMimeType("image")
|
||||
}
|
||||
|
||||
|
||||
func isMimeTypeAudio() -> Bool {
|
||||
|
||||
|
||||
return self.isOfGeneralMimeType("audio")
|
||||
}
|
||||
|
||||
|
||||
func isMimeTypeVideo() -> Bool {
|
||||
|
||||
|
||||
return self.isOfGeneralMimeType("video")
|
||||
}
|
||||
|
||||
|
||||
func isMimeTypeTimeBasedMedia() -> Bool {
|
||||
|
||||
|
||||
return self.isMimeTypeAudio() || self.isMimeTypeVideo()
|
||||
}
|
||||
|
||||
|
||||
private func isOfGeneralMimeType(_ type: String) -> Bool {
|
||||
|
||||
|
||||
let lower = self.lowercased()
|
||||
if lower.hasPrefix(type) {
|
||||
return true
|
||||
|
@ -48,8 +48,8 @@ public class Reachability {
|
||||
return reachability.connection != .unavailable
|
||||
}
|
||||
|
||||
public typealias NetworkReachable = (Reachability) -> ()
|
||||
public typealias NetworkUnreachable = (Reachability) -> ()
|
||||
public typealias NetworkReachable = (Reachability) -> Void
|
||||
public typealias NetworkUnreachable = (Reachability) -> Void
|
||||
|
||||
public enum Connection: CustomStringConvertible {
|
||||
@available(*, deprecated, renamed: "unavailable")
|
||||
@ -72,7 +72,7 @@ public class Reachability {
|
||||
if flags == nil {
|
||||
try? setReachabilityFlags()
|
||||
}
|
||||
|
||||
|
||||
switch flags?.connection {
|
||||
case .unavailable?, nil: return .unavailable
|
||||
case .none?: return .unavailable
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user