Continue progress on porting feed parsers.

This commit is contained in:
Brent Simmons
2024-09-09 20:54:42 -07:00
parent 77c1e26600
commit 24e7eb90f6
10 changed files with 789 additions and 629 deletions

View File

@@ -1,248 +1,248 @@
////
//// JSONFeedParser.swift
//// RSParser
////
//// Created by Brent Simmons on 6/25/17.
//// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
////
//
// JSONFeedParser.swift
// RSParser
//import Foundation
//import SAX
//
// Created by Brent Simmons on 6/25/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//// See https://jsonfeed.org/version/1.1
//
import Foundation
import SAX
// See https://jsonfeed.org/version/1.1
public struct JSONFeedParser {
struct Key {
static let version = "version"
static let items = "items"
static let title = "title"
static let homePageURL = "home_page_url"
static let feedURL = "feed_url"
static let feedDescription = "description"
static let nextURL = "next_url"
static let icon = "icon"
static let favicon = "favicon"
static let expired = "expired"
static let author = "author"
static let authors = "authors"
static let name = "name"
static let url = "url"
static let avatar = "avatar"
static let hubs = "hubs"
static let type = "type"
static let contentHTML = "content_html"
static let contentText = "content_text"
static let externalURL = "external_url"
static let summary = "summary"
static let image = "image"
static let bannerImage = "banner_image"
static let datePublished = "date_published"
static let dateModified = "date_modified"
static let tags = "tags"
static let uniqueID = "id"
static let attachments = "attachments"
static let mimeType = "mime_type"
static let sizeInBytes = "size_in_bytes"
static let durationInSeconds = "duration_in_seconds"
static let language = "language"
}
static let jsonFeedVersionMarker = "://jsonfeed.org/version/" // Allow for the mistake of not getting the scheme exactly correct.
public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
guard let d = JSONUtilities.dictionary(with: parserData.data) else {
throw FeedParserError(.invalidJSON)
}
guard let version = d[Key.version] as? String, let _ = version.range(of: JSONFeedParser.jsonFeedVersionMarker) else {
throw FeedParserError(.jsonFeedVersionNotFound)
}
guard let itemsArray = d[Key.items] as? JSONArray else {
throw FeedParserError(.jsonFeedItemsNotFound)
}
guard let title = d[Key.title] as? String else {
throw FeedParserError(.jsonFeedTitleNotFound)
}
let authors = parseAuthors(d)
let homePageURL = d[Key.homePageURL] as? String
let feedURL = d[Key.feedURL] as? String ?? parserData.url
let feedDescription = d[Key.feedDescription] as? String
let nextURL = d[Key.nextURL] as? String
let iconURL = d[Key.icon] as? String
let faviconURL = d[Key.favicon] as? String
let expired = d[Key.expired] as? Bool ?? false
let hubs = parseHubs(d)
let language = d[Key.language] as? String
let items = parseItems(itemsArray, parserData.url)
return ParsedFeed(type: .jsonFeed, title: title, homePageURL: homePageURL, feedURL: feedURL, language: language, feedDescription: feedDescription, nextURL: nextURL, iconURL: iconURL, faviconURL: faviconURL, authors: authors, expired: expired, hubs: hubs, items: items)
}
}
private extension JSONFeedParser {
static func parseAuthors(_ dictionary: JSONDictionary) -> Set<ParsedAuthor>? {
if let authorsArray = dictionary[Key.authors] as? JSONArray {
var authors = Set<ParsedAuthor>()
for author in authorsArray {
if let parsedAuthor = parseAuthor(author) {
authors.insert(parsedAuthor)
}
}
return authors
}
guard let authorDictionary = dictionary[Key.author] as? JSONDictionary,
let parsedAuthor = parseAuthor(authorDictionary) else {
return nil
}
return Set([parsedAuthor])
}
static func parseAuthor(_ dictionary: JSONDictionary) -> ParsedAuthor? {
let name = dictionary[Key.name] as? String
let url = dictionary[Key.url] as? String
let avatar = dictionary[Key.avatar] as? String
if name == nil && url == nil && avatar == nil {
return nil
}
return ParsedAuthor(name: name, url: url, avatarURL: avatar, emailAddress: nil)
}
static func parseHubs(_ dictionary: JSONDictionary) -> Set<ParsedHub>? {
guard let hubsArray = dictionary[Key.hubs] as? JSONArray else {
return nil
}
let hubs = hubsArray.compactMap { (hubDictionary) -> ParsedHub? in
guard let hubURL = hubDictionary[Key.url] as? String, let hubType = hubDictionary[Key.type] as? String else {
return nil
}
return ParsedHub(type: hubType, url: hubURL)
}
return hubs.isEmpty ? nil : Set(hubs)
}
static func parseItems(_ itemsArray: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
return Set(itemsArray.compactMap { (oneItemDictionary) -> ParsedItem? in
return parseItem(oneItemDictionary, feedURL)
})
}
static func parseItem(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
guard let uniqueID = parseUniqueID(itemDictionary) else {
return nil
}
let contentHTML = itemDictionary[Key.contentHTML] as? String
let contentText = itemDictionary[Key.contentText] as? String
if contentHTML == nil && contentText == nil {
return nil
}
let url = itemDictionary[Key.url] as? String
let externalURL = itemDictionary[Key.externalURL] as? String
let title = parseTitle(itemDictionary, feedURL)
let language = itemDictionary[Key.language] as? String
let summary = itemDictionary[Key.summary] as? String
let imageURL = itemDictionary[Key.image] as? String
let bannerImageURL = itemDictionary[Key.bannerImage] as? String
let datePublished = parseDate(itemDictionary[Key.datePublished] as? String)
let dateModified = parseDate(itemDictionary[Key.dateModified] as? String)
let authors = parseAuthors(itemDictionary)
var tags: Set<String>? = nil
if let tagsArray = itemDictionary[Key.tags] as? [String] {
tags = Set(tagsArray)
}
let attachments = parseAttachments(itemDictionary)
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: url, externalURL: externalURL, title: title, language: language, contentHTML: contentHTML, contentText: contentText, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments)
}
static func parseTitle(_ itemDictionary: JSONDictionary, _ feedURL: String) -> String? {
guard let title = itemDictionary[Key.title] as? String else {
return nil
}
if isSpecialCaseTitleWithEntitiesFeed(feedURL) {
return (title as NSString).rsparser_stringByDecodingHTMLEntities()
}
return title
}
static func isSpecialCaseTitleWithEntitiesFeed(_ feedURL: String) -> Bool {
// As of 16 Feb. 2018, Kottkes and Heers feeds includes HTML entities in the title elements.
// If we find more feeds like this, well add them here. If these feeds get fixed, well remove them.
let lowerFeedURL = feedURL.lowercased()
let matchStrings = ["kottke.org", "pxlnv.com", "macstories.net", "macobserver.com"]
for matchString in matchStrings {
if lowerFeedURL.contains(matchString) {
return true
}
}
return false
}
static func parseUniqueID(_ itemDictionary: JSONDictionary) -> String? {
if let uniqueID = itemDictionary[Key.uniqueID] as? String {
return uniqueID // Spec says it must be a string
}
// Version 1 spec also says that if its a number, even though thats incorrect, it should be coerced to a string.
if let uniqueID = itemDictionary[Key.uniqueID] as? Int {
return "\(uniqueID)"
}
if let uniqueID = itemDictionary[Key.uniqueID] as? Double {
return "\(uniqueID)"
}
return nil
}
static func parseDate(_ dateString: String?) -> Date? {
guard let dateString = dateString, !dateString.isEmpty else {
return nil
}
return RSDateWithString(dateString)
}
static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
guard let attachmentsArray = itemDictionary[Key.attachments] as? JSONArray else {
return nil
}
return Set(attachmentsArray.compactMap { parseAttachment($0) })
}
static func parseAttachment(_ attachmentObject: JSONDictionary) -> ParsedAttachment? {
guard let url = attachmentObject[Key.url] as? String else {
return nil
}
guard let mimeType = attachmentObject[Key.mimeType] as? String else {
return nil
}
let title = attachmentObject[Key.title] as? String
let sizeInBytes = attachmentObject[Key.sizeInBytes] as? Int
let durationInSeconds = attachmentObject[Key.durationInSeconds] as? Int
return ParsedAttachment(url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds)
}
}
//public struct JSONFeedParser {
//
// struct Key {
// static let version = "version"
// static let items = "items"
// static let title = "title"
// static let homePageURL = "home_page_url"
// static let feedURL = "feed_url"
// static let feedDescription = "description"
// static let nextURL = "next_url"
// static let icon = "icon"
// static let favicon = "favicon"
// static let expired = "expired"
// static let author = "author"
// static let authors = "authors"
// static let name = "name"
// static let url = "url"
// static let avatar = "avatar"
// static let hubs = "hubs"
// static let type = "type"
// static let contentHTML = "content_html"
// static let contentText = "content_text"
// static let externalURL = "external_url"
// static let summary = "summary"
// static let image = "image"
// static let bannerImage = "banner_image"
// static let datePublished = "date_published"
// static let dateModified = "date_modified"
// static let tags = "tags"
// static let uniqueID = "id"
// static let attachments = "attachments"
// static let mimeType = "mime_type"
// static let sizeInBytes = "size_in_bytes"
// static let durationInSeconds = "duration_in_seconds"
// static let language = "language"
// }
//
// static let jsonFeedVersionMarker = "://jsonfeed.org/version/" // Allow for the mistake of not getting the scheme exactly correct.
//
// public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
//
// guard let d = JSONUtilities.dictionary(with: parserData.data) else {
// throw FeedParserError(.invalidJSON)
// }
//
// guard let version = d[Key.version] as? String, let _ = version.range(of: JSONFeedParser.jsonFeedVersionMarker) else {
// throw FeedParserError(.jsonFeedVersionNotFound)
// }
// guard let itemsArray = d[Key.items] as? JSONArray else {
// throw FeedParserError(.jsonFeedItemsNotFound)
// }
// guard let title = d[Key.title] as? String else {
// throw FeedParserError(.jsonFeedTitleNotFound)
// }
//
// let authors = parseAuthors(d)
// let homePageURL = d[Key.homePageURL] as? String
// let feedURL = d[Key.feedURL] as? String ?? parserData.url
// let feedDescription = d[Key.feedDescription] as? String
// let nextURL = d[Key.nextURL] as? String
// let iconURL = d[Key.icon] as? String
// let faviconURL = d[Key.favicon] as? String
// let expired = d[Key.expired] as? Bool ?? false
// let hubs = parseHubs(d)
// let language = d[Key.language] as? String
//
// let items = parseItems(itemsArray, parserData.url)
//
// return ParsedFeed(type: .jsonFeed, title: title, homePageURL: homePageURL, feedURL: feedURL, language: language, feedDescription: feedDescription, nextURL: nextURL, iconURL: iconURL, faviconURL: faviconURL, authors: authors, expired: expired, hubs: hubs, items: items)
// }
//}
//
//private extension JSONFeedParser {
//
// static func parseAuthors(_ dictionary: JSONDictionary) -> Set<ParsedAuthor>? {
//
// if let authorsArray = dictionary[Key.authors] as? JSONArray {
// var authors = Set<ParsedAuthor>()
// for author in authorsArray {
// if let parsedAuthor = parseAuthor(author) {
// authors.insert(parsedAuthor)
// }
// }
// return authors
// }
//
// guard let authorDictionary = dictionary[Key.author] as? JSONDictionary,
// let parsedAuthor = parseAuthor(authorDictionary) else {
// return nil
// }
//
// return Set([parsedAuthor])
// }
//
// static func parseAuthor(_ dictionary: JSONDictionary) -> ParsedAuthor? {
// let name = dictionary[Key.name] as? String
// let url = dictionary[Key.url] as? String
// let avatar = dictionary[Key.avatar] as? String
// if name == nil && url == nil && avatar == nil {
// return nil
// }
// return ParsedAuthor(name: name, url: url, avatarURL: avatar, emailAddress: nil)
// }
//
// static func parseHubs(_ dictionary: JSONDictionary) -> Set<ParsedHub>? {
//
// guard let hubsArray = dictionary[Key.hubs] as? JSONArray else {
// return nil
// }
//
// let hubs = hubsArray.compactMap { (hubDictionary) -> ParsedHub? in
// guard let hubURL = hubDictionary[Key.url] as? String, let hubType = hubDictionary[Key.type] as? String else {
// return nil
// }
// return ParsedHub(type: hubType, url: hubURL)
// }
// return hubs.isEmpty ? nil : Set(hubs)
// }
//
// static func parseItems(_ itemsArray: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
//
// return Set(itemsArray.compactMap { (oneItemDictionary) -> ParsedItem? in
// return parseItem(oneItemDictionary, feedURL)
// })
// }
//
// static func parseItem(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
//
// guard let uniqueID = parseUniqueID(itemDictionary) else {
// return nil
// }
//
// let contentHTML = itemDictionary[Key.contentHTML] as? String
// let contentText = itemDictionary[Key.contentText] as? String
// if contentHTML == nil && contentText == nil {
// return nil
// }
//
// let url = itemDictionary[Key.url] as? String
// let externalURL = itemDictionary[Key.externalURL] as? String
// let title = parseTitle(itemDictionary, feedURL)
// let language = itemDictionary[Key.language] as? String
// let summary = itemDictionary[Key.summary] as? String
// let imageURL = itemDictionary[Key.image] as? String
// let bannerImageURL = itemDictionary[Key.bannerImage] as? String
//
// let datePublished = parseDate(itemDictionary[Key.datePublished] as? String)
// let dateModified = parseDate(itemDictionary[Key.dateModified] as? String)
//
// let authors = parseAuthors(itemDictionary)
// var tags: Set<String>? = nil
// if let tagsArray = itemDictionary[Key.tags] as? [String] {
// tags = Set(tagsArray)
// }
// let attachments = parseAttachments(itemDictionary)
//
// return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: url, externalURL: externalURL, title: title, language: language, contentHTML: contentHTML, contentText: contentText, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments)
// }
//
// static func parseTitle(_ itemDictionary: JSONDictionary, _ feedURL: String) -> String? {
//
// guard let title = itemDictionary[Key.title] as? String else {
// return nil
// }
//
// if isSpecialCaseTitleWithEntitiesFeed(feedURL) {
// return (title as NSString).rsparser_stringByDecodingHTMLEntities()
// }
//
// return title
// }
//
// static func isSpecialCaseTitleWithEntitiesFeed(_ feedURL: String) -> Bool {
//
// // As of 16 Feb. 2018, Kottkes and Heers feeds includes HTML entities in the title elements.
// // If we find more feeds like this, well add them here. If these feeds get fixed, well remove them.
//
// let lowerFeedURL = feedURL.lowercased()
// let matchStrings = ["kottke.org", "pxlnv.com", "macstories.net", "macobserver.com"]
// for matchString in matchStrings {
// if lowerFeedURL.contains(matchString) {
// return true
// }
// }
//
// return false
// }
//
// static func parseUniqueID(_ itemDictionary: JSONDictionary) -> String? {
//
// if let uniqueID = itemDictionary[Key.uniqueID] as? String {
// return uniqueID // Spec says it must be a string
// }
// // Version 1 spec also says that if its a number, even though thats incorrect, it should be coerced to a string.
// if let uniqueID = itemDictionary[Key.uniqueID] as? Int {
// return "\(uniqueID)"
// }
// if let uniqueID = itemDictionary[Key.uniqueID] as? Double {
// return "\(uniqueID)"
// }
// return nil
// }
//
// static func parseDate(_ dateString: String?) -> Date? {
//
// guard let dateString = dateString, !dateString.isEmpty else {
// return nil
// }
// return RSDateWithString(dateString)
// }
//
// static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
//
// guard let attachmentsArray = itemDictionary[Key.attachments] as? JSONArray else {
// return nil
// }
// return Set(attachmentsArray.compactMap { parseAttachment($0) })
// }
//
// static func parseAttachment(_ attachmentObject: JSONDictionary) -> ParsedAttachment? {
//
// guard let url = attachmentObject[Key.url] as? String else {
// return nil
// }
// guard let mimeType = attachmentObject[Key.mimeType] as? String else {
// return nil
// }
//
// let title = attachmentObject[Key.title] as? String
// let sizeInBytes = attachmentObject[Key.sizeInBytes] as? Int
// let durationInSeconds = attachmentObject[Key.durationInSeconds] as? Int
//
// return ParsedAttachment(url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds)
// }
//}

View File

@@ -1,182 +1,182 @@
////
//// RSSInJSONParser.swift
//// RSParser
////
//// Created by Brent Simmons on 6/24/17.
//// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
////
//
// RSSInJSONParser.swift
// RSParser
//import Foundation
//import SAX
//
// Created by Brent Simmons on 6/24/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//// See https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md
//// Also: http://cyber.harvard.edu/rss/rss.html
//
import Foundation
import SAX
// See https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md
// Also: http://cyber.harvard.edu/rss/rss.html
public struct RSSInJSONParser {
public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
do {
guard let parsedObject = try JSONSerialization.jsonObject(with: parserData.data) as? JSONDictionary else {
throw FeedParserError(.invalidJSON)
}
guard let rssObject = parsedObject["rss"] as? JSONDictionary else {
throw FeedParserError(.rssChannelNotFound)
}
guard let channelObject = rssObject["channel"] as? JSONDictionary else {
throw FeedParserError(.rssChannelNotFound)
}
// Id bet money that in practice the items array wont always appear correctly inside the channel object.
// Id also bet that sometimes it gets called "items" instead of "item".
var itemsObject = channelObject["item"] as? JSONArray
if itemsObject == nil {
itemsObject = parsedObject["item"] as? JSONArray
}
if itemsObject == nil {
itemsObject = channelObject["items"] as? JSONArray
}
if itemsObject == nil {
itemsObject = parsedObject["items"] as? JSONArray
}
if itemsObject == nil {
throw FeedParserError(.rssItemsNotFound)
}
let title = channelObject["title"] as? String
let homePageURL = channelObject["link"] as? String
let feedURL = parserData.url
let feedDescription = channelObject["description"] as? String
let feedLanguage = channelObject["language"] as? String
let items = parseItems(itemsObject!, parserData.url)
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 }
}
}
private extension RSSInJSONParser {
static func parseItems(_ itemsObject: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
return Set(itemsObject.compactMap{ (oneItemDictionary) -> ParsedItem? in
return parsedItemWithDictionary(oneItemDictionary, feedURL)
})
}
static func parsedItemWithDictionary(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
let externalURL = itemDictionary["link"] as? String
let title = itemDictionary["title"] as? String
var contentHTML = itemDictionary["description"] as? String
var contentText: String? = nil
if contentHTML != nil && !(contentHTML!.contains("<")) {
contentText = contentHTML
contentHTML = nil
}
if contentHTML == nil && contentText == nil && title == nil {
return nil
}
var datePublished: Date? = nil
if let datePublishedString = itemDictionary["pubDate"] as? String {
datePublished = RSDateWithString(datePublishedString)
}
let authors = parseAuthors(itemDictionary)
let tags = parseTags(itemDictionary)
let attachments = parseAttachments(itemDictionary)
var uniqueID: String? = itemDictionary["guid"] as? String
if uniqueID == nil {
// Calculate a uniqueID based on a combination of non-empty elements. Then hash the result.
// Items should have guids. When they don't, re-runs are very likely
// because there's no other 100% reliable way to determine identity.
// This calculated uniqueID is valid only for this particular feed. (Just like ids in JSON Feed.)
var s = ""
if let datePublished = datePublished {
s += "\(datePublished.timeIntervalSince1970)"
}
if let title = title {
s += title
}
if let externalURL = externalURL {
s += externalURL
}
if let authorEmailAddress = authors?.first?.emailAddress {
s += authorEmailAddress
}
if let oneAttachmentURL = attachments?.first?.url {
s += oneAttachmentURL
}
if s.isEmpty {
// Sheesh. Tough case.
if let _ = contentHTML {
s = contentHTML!
}
if let _ = contentText {
s = contentText!
}
}
uniqueID = (s as NSString).rsparser_md5Hash()
}
if let uniqueID = uniqueID {
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: nil, externalURL: externalURL, title: title, language: nil, contentHTML: contentHTML, contentText: contentText, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: datePublished, dateModified: nil, authors: authors, tags: tags, attachments: attachments)
}
return nil
}
static func parseAuthors(_ itemDictionary: JSONDictionary) -> Set<ParsedAuthor>? {
guard let authorEmailAddress = itemDictionary["author"] as? String else {
return nil
}
let parsedAuthor = ParsedAuthor(name: nil, url: nil, avatarURL: nil, emailAddress: authorEmailAddress)
return Set([parsedAuthor])
}
static func parseTags(_ itemDictionary: JSONDictionary) -> Set<String>? {
if let categoryObject = itemDictionary["category"] as? JSONDictionary {
if let oneTag = categoryObject["#value"] as? String {
return Set([oneTag])
}
return nil
}
else if let categoryArray = itemDictionary["category"] as? JSONArray {
return Set(categoryArray.compactMap{ $0["#value"] as? String })
}
return nil
}
static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
guard let enclosureObject = itemDictionary["enclosure"] as? JSONDictionary else {
return nil
}
guard let attachmentURL = enclosureObject["url"] as? String else {
return nil
}
var attachmentSize = enclosureObject["length"] as? Int
if attachmentSize == nil {
if let attachmentSizeString = enclosureObject["length"] as? String {
attachmentSize = (attachmentSizeString as NSString).integerValue
}
}
let type = enclosureObject["type"] as? String
if let attachment = ParsedAttachment(url: attachmentURL, mimeType: type, title: nil, sizeInBytes: attachmentSize, durationInSeconds: nil) {
return Set([attachment])
}
return nil
}
}
//public struct RSSInJSONParser {
//
// public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
//
// do {
// guard let parsedObject = try JSONSerialization.jsonObject(with: parserData.data) as? JSONDictionary else {
// throw FeedParserError(.invalidJSON)
// }
// guard let rssObject = parsedObject["rss"] as? JSONDictionary else {
// throw FeedParserError(.rssChannelNotFound)
// }
// guard let channelObject = rssObject["channel"] as? JSONDictionary else {
// throw FeedParserError(.rssChannelNotFound)
// }
//
// // Id bet money that in practice the items array wont always appear correctly inside the channel object.
// // Id also bet that sometimes it gets called "items" instead of "item".
// var itemsObject = channelObject["item"] as? JSONArray
// if itemsObject == nil {
// itemsObject = parsedObject["item"] as? JSONArray
// }
// if itemsObject == nil {
// itemsObject = channelObject["items"] as? JSONArray
// }
// if itemsObject == nil {
// itemsObject = parsedObject["items"] as? JSONArray
// }
// if itemsObject == nil {
// throw FeedParserError(.rssItemsNotFound)
// }
//
// let title = channelObject["title"] as? String
// let homePageURL = channelObject["link"] as? String
// let feedURL = parserData.url
// let feedDescription = channelObject["description"] as? String
// let feedLanguage = channelObject["language"] as? String
//
// let items = parseItems(itemsObject!, parserData.url)
//
// 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 }
// }
//}
//
//private extension RSSInJSONParser {
//
// static func parseItems(_ itemsObject: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
//
// return Set(itemsObject.compactMap{ (oneItemDictionary) -> ParsedItem? in
//
// return parsedItemWithDictionary(oneItemDictionary, feedURL)
// })
// }
//
// static func parsedItemWithDictionary(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
//
// let externalURL = itemDictionary["link"] as? String
// let title = itemDictionary["title"] as? String
//
// var contentHTML = itemDictionary["description"] as? String
// var contentText: String? = nil
// if contentHTML != nil && !(contentHTML!.contains("<")) {
// contentText = contentHTML
// contentHTML = nil
// }
// if contentHTML == nil && contentText == nil && title == nil {
// return nil
// }
//
// var datePublished: Date? = nil
// if let datePublishedString = itemDictionary["pubDate"] as? String {
// datePublished = RSDateWithString(datePublishedString)
// }
//
// let authors = parseAuthors(itemDictionary)
// let tags = parseTags(itemDictionary)
// let attachments = parseAttachments(itemDictionary)
//
// var uniqueID: String? = itemDictionary["guid"] as? String
// if uniqueID == nil {
//
// // Calculate a uniqueID based on a combination of non-empty elements. Then hash the result.
// // Items should have guids. When they don't, re-runs are very likely
// // because there's no other 100% reliable way to determine identity.
// // This calculated uniqueID is valid only for this particular feed. (Just like ids in JSON Feed.)
//
// var s = ""
// if let datePublished = datePublished {
// s += "\(datePublished.timeIntervalSince1970)"
// }
// if let title = title {
// s += title
// }
// if let externalURL = externalURL {
// s += externalURL
// }
// if let authorEmailAddress = authors?.first?.emailAddress {
// s += authorEmailAddress
// }
// if let oneAttachmentURL = attachments?.first?.url {
// s += oneAttachmentURL
// }
// if s.isEmpty {
// // Sheesh. Tough case.
// if let _ = contentHTML {
// s = contentHTML!
// }
// if let _ = contentText {
// s = contentText!
// }
// }
// uniqueID = (s as NSString).rsparser_md5Hash()
// }
//
// if let uniqueID = uniqueID {
// return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: nil, externalURL: externalURL, title: title, language: nil, contentHTML: contentHTML, contentText: contentText, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: datePublished, dateModified: nil, authors: authors, tags: tags, attachments: attachments)
// }
// return nil
// }
//
// static func parseAuthors(_ itemDictionary: JSONDictionary) -> Set<ParsedAuthor>? {
//
// guard let authorEmailAddress = itemDictionary["author"] as? String else {
// return nil
// }
// let parsedAuthor = ParsedAuthor(name: nil, url: nil, avatarURL: nil, emailAddress: authorEmailAddress)
// return Set([parsedAuthor])
// }
//
// static func parseTags(_ itemDictionary: JSONDictionary) -> Set<String>? {
//
// if let categoryObject = itemDictionary["category"] as? JSONDictionary {
// if let oneTag = categoryObject["#value"] as? String {
// return Set([oneTag])
// }
// return nil
// }
// else if let categoryArray = itemDictionary["category"] as? JSONArray {
// return Set(categoryArray.compactMap{ $0["#value"] as? String })
// }
// return nil
// }
//
// static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
//
// guard let enclosureObject = itemDictionary["enclosure"] as? JSONDictionary else {
// return nil
// }
// guard let attachmentURL = enclosureObject["url"] as? String else {
// return nil
// }
//
// var attachmentSize = enclosureObject["length"] as? Int
// if attachmentSize == nil {
// if let attachmentSizeString = enclosureObject["length"] as? String {
// attachmentSize = (attachmentSizeString as NSString).integerValue
// }
// }
//
// let type = enclosureObject["type"] as? String
// if let attachment = ParsedAttachment(url: attachmentURL, mimeType: type, title: nil, sizeInBytes: attachmentSize, durationInSeconds: nil) {
// return Set([attachment])
// }
// return nil
// }
//}