mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 04:41:23 +08:00
feat: open-source 小浣熊阅读 for iOS
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user