mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
108 lines
5.7 KiB
Swift
108 lines
5.7 KiB
Swift
import Foundation
|
|
|
|
public struct RSSRuleParseReport: Sendable {
|
|
public var result: RSSParseResult
|
|
public var diagnostics: [RuleDiagnostic]
|
|
public init(result: RSSParseResult, diagnostics: [RuleDiagnostic] = []) {
|
|
self.result = result
|
|
self.diagnostics = diagnostics
|
|
}
|
|
}
|
|
|
|
public struct RSSRuleParser: Sendable {
|
|
private let engine: RuleEngine
|
|
public init(engine: RuleEngine = .init()) { self.engine = engine }
|
|
|
|
public func parse(
|
|
sortName: String,
|
|
sortURL: String,
|
|
redirectURL: String,
|
|
body: String,
|
|
source: RssSource,
|
|
variables: [String: String] = [:]
|
|
) async throws -> RSSRuleParseReport {
|
|
guard !body.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
throw RSSParserError.emptyBody(source.sourceUrl)
|
|
}
|
|
guard var listRule = source.ruleArticles, !listRule.isEmpty else {
|
|
return .init(result: try RSSDefaultParser.parse(sortName: sortName, xml: body, sourceURL: source.sourceUrl))
|
|
}
|
|
let reverse = listRule.hasPrefix("-")
|
|
if reverse { listRule.removeFirst() }
|
|
let adapter = source.bookSourceAdapter
|
|
let baseContext = RuleExecutionContext(variables: variables, baseURL: sortURL, source: adapter)
|
|
let list = await engine.evaluate(listRule, inText: body, baseURL: sortURL, listMode: true, context: baseContext)
|
|
var diagnostics = list.diagnostics
|
|
var nextURL: String?
|
|
if let nextRule = source.ruleNextPage, !nextRule.isEmpty {
|
|
if nextRule.uppercased() == "PAGE" {
|
|
nextURL = sortURL
|
|
} else {
|
|
let next = await engine.evaluate(nextRule, inText: body, baseURL: sortURL, context: .init(variables: list.variables, baseURL: sortURL, source: adapter))
|
|
diagnostics += next.diagnostics
|
|
if let value = next.joinedString, !value.isEmpty {
|
|
nextURL = URL(string: value, relativeTo: URL(string: sortURL))?.absoluteURL.absoluteString ?? value
|
|
}
|
|
}
|
|
}
|
|
let variable = encodeVariables(list.variables)
|
|
var articles: [RssArticle] = []
|
|
for item in flatten(list.values) {
|
|
var context = RuleExecutionContext(variables: list.variables, baseURL: sortURL, source: adapter)
|
|
let title = await evaluate(source.ruleTitle, item: item, context: context); context.variables = title.variables
|
|
let pubDate = await evaluate(source.rulePubDate, item: item, context: context); context.variables = pubDate.variables
|
|
let description = await evaluate(source.ruleDescription, item: item, context: context); context.variables = description.variables
|
|
let image = await evaluate(source.ruleImage, item: item, context: context); context.variables = image.variables
|
|
let link = await evaluate(source.ruleLink, item: item, context: context)
|
|
diagnostics += [title, pubDate, description, image, link].flatMap(\.diagnostics)
|
|
guard let titleValue = title.joinedString?.trimmingCharacters(in: .whitespacesAndNewlines), !titleValue.isEmpty else { continue }
|
|
let rawLink = link.leadingString ?? ""
|
|
let resolvedLink = URL(string: rawLink, relativeTo: URL(string: sortURL))?
|
|
.absoluteURL.absoluteString ?? rawLink
|
|
var article = RssArticle(origin: source.sourceUrl, sort: sortName, title: titleValue, link: resolvedLink)
|
|
article.pubDate = pubDate.joinedString
|
|
article.description = source.ruleDescription?.isEmpty == false ? description.joinedString : nil
|
|
if let imageValue = image.joinedString, !imageValue.isEmpty {
|
|
article.image = URL(string: imageValue, relativeTo: URL(string: source.sourceUrl))?.absoluteURL.absoluteString ?? imageValue
|
|
}
|
|
article.variable = variable
|
|
article.type = source.type
|
|
articles.append(article)
|
|
}
|
|
if reverse { articles.reverse() }
|
|
_ = redirectURL
|
|
return .init(result: .init(articles: articles, nextURL: nextURL), diagnostics: diagnostics)
|
|
}
|
|
|
|
private func evaluate(_ rule: String?, item: JSONValue, context: RuleExecutionContext) async -> RuleEvaluation {
|
|
guard let rule, !rule.isEmpty else { return .init(variables: context.variables) }
|
|
if case .string(let text) = item {
|
|
return await engine.evaluate(rule, inText: text, baseURL: context.baseURL, context: context)
|
|
}
|
|
return await engine.evaluate(rule, in: item, context: context)
|
|
}
|
|
private func flatten(_ values: [JSONValue]) -> [JSONValue] {
|
|
values.flatMap { value in
|
|
if case .array(let items) = value { return items }
|
|
return [value]
|
|
}
|
|
}
|
|
private func encodeVariables(_ values: [String: String]) -> String? {
|
|
guard !values.isEmpty, let data = try? JSONEncoder().encode(values) else { return nil }
|
|
return String(decoding: data, as: UTF8.self)
|
|
}
|
|
}
|
|
|
|
extension RssSource {
|
|
var bookSourceAdapter: BookSource {
|
|
var value = BookSource(bookSourceUrl: sourceUrl, bookSourceName: sourceName)
|
|
value.bookSourceGroup = sourceGroup; value.bookSourceType = type; value.customOrder = customOrder
|
|
value.enabled = enabled; value.jsLib = jsLib; value.enabledCookieJar = enabledCookieJar
|
|
value.concurrentRate = concurrentRate; value.header = header; value.loginUrl = loginUrl
|
|
value.loginUi = loginUi; value.loginCheckJs = loginCheckJs; value.coverDecodeJs = coverDecodeJs
|
|
value.bookSourceComment = sourceComment; value.variableComment = variableComment
|
|
value.lastUpdateTime = lastUpdateTime; value.searchUrl = searchUrl
|
|
return value
|
|
}
|
|
}
|