mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
106 lines
4.8 KiB
Swift
106 lines
4.8 KiB
Swift
import Foundation
|
|
#if canImport(FoundationXML)
|
|
import FoundationXML
|
|
#endif
|
|
|
|
public struct RSSParseResult: Sendable, Equatable {
|
|
public var articles: [RssArticle]
|
|
public var nextURL: String?
|
|
public init(articles: [RssArticle], nextURL: String? = nil) {
|
|
self.articles = articles
|
|
self.nextURL = nextURL
|
|
}
|
|
}
|
|
|
|
public enum RSSDefaultParser {
|
|
public static func parse(sortName: String, xml: String, sourceURL: String) throws -> RSSParseResult {
|
|
let delegate = Delegate(sortName: sortName, sourceURL: sourceURL)
|
|
let parser = XMLParser(data: Data(xml.utf8))
|
|
parser.shouldProcessNamespaces = false
|
|
parser.delegate = delegate
|
|
guard parser.parse() else { throw parser.parserError ?? RSSParserError.invalidXML }
|
|
return .init(articles: delegate.articles)
|
|
}
|
|
|
|
private final class Delegate: NSObject, XMLParserDelegate {
|
|
let sortName: String
|
|
let sourceURL: String
|
|
var articles: [RssArticle] = []
|
|
var article = RssArticle()
|
|
var insideItem = false
|
|
var activeField: String?
|
|
var text = ""
|
|
|
|
init(sortName: String, sourceURL: String) {
|
|
self.sortName = sortName
|
|
self.sourceURL = sourceURL
|
|
}
|
|
|
|
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
|
|
let name = elementName.lowercased()
|
|
if name == "item" { insideItem = true; article = RssArticle(); return }
|
|
guard insideItem else { return }
|
|
if name == "media:thumbnail" {
|
|
article.image = attribute(attributeDict, named: "url")
|
|
} else if name == "enclosure", attribute(attributeDict, named: "type")?.localizedCaseInsensitiveContains("image/") == true {
|
|
article.image = attribute(attributeDict, named: "url")
|
|
}
|
|
if ["title", "link", "description", "content:encoded", "pubdate", "time"].contains(name) {
|
|
activeField = name
|
|
text = ""
|
|
}
|
|
}
|
|
|
|
func parser(_ parser: XMLParser, foundCharacters string: String) {
|
|
if insideItem, activeField != nil { text += string }
|
|
}
|
|
func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
|
|
if insideItem, activeField != nil { text += String(decoding: CDATABlock, as: UTF8.self) }
|
|
}
|
|
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
|
let name = elementName.lowercased()
|
|
if name == "item" {
|
|
article.origin = sourceURL; article.sort = sortName; articles.append(article)
|
|
insideItem = false; activeField = nil
|
|
return
|
|
}
|
|
guard insideItem, name == activeField else { return }
|
|
let value = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
switch name {
|
|
case "title": article.title = value
|
|
case "link": article.link = value
|
|
case "description": article.description = value; if article.image == nil { article.image = firstImageURL(in: text) }
|
|
case "content:encoded": article.content = value; if article.image == nil { article.image = firstImageURL(in: text) }
|
|
case "pubdate", "time": article.pubDate = value
|
|
default: break
|
|
}
|
|
activeField = nil; text = ""
|
|
}
|
|
|
|
private func attribute(_ values: [String: String], named name: String) -> String? {
|
|
values.first { $0.key.caseInsensitiveCompare(name) == .orderedSame }?.value
|
|
}
|
|
private func firstImageURL(in input: String) -> String? {
|
|
guard let expression = try? NSRegularExpression(pattern: "(<img [^>]*>)", options: .caseInsensitive),
|
|
let image = expression.firstMatch(in: input, range: NSRange(input.startIndex..., in: input)),
|
|
let tagRange = Range(image.range(at: 1), in: input) else { return nil }
|
|
let tag = String(input[tagRange])
|
|
guard let expression = try? NSRegularExpression(pattern: #"src\s*=\s*\"([^\"]+)\""#, options: .caseInsensitive),
|
|
let source = expression.firstMatch(in: tag, range: NSRange(tag.startIndex..., in: tag)),
|
|
let sourceRange = Range(source.range(at: 1), in: tag) else { return nil }
|
|
return String(tag[sourceRange]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum RSSParserError: Error, LocalizedError, Sendable {
|
|
case invalidXML
|
|
case emptyBody(String)
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .invalidXML: "RSS XML解析失败"
|
|
case .emptyBody(let source): "获取网页内容失败:\(source)"
|
|
}
|
|
}
|
|
}
|