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 } }