Files
legado-ios/Sources/LegadoCore/Domain/BookSourceEditorDocument.swift
T

200 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
public struct BookSourceEditorField: Identifiable, Sendable {
public let path: String
public let title: String
public var id: String { path }
public var help: String {
switch path {
case "bookSourceUrl": return "填写书源站点地址,例如 https://example.com;它也是书源的唯一标识。"
case "bookSourceName": return "显示在书源列表中的名称,不能为空。"
case "bookSourceGroup": return "多个分组可用逗号或分号分隔。"
case "bookUrlPattern": return "匹配该书源的书籍详情 URL 的正则表达式,用于从网址添加书籍。"
case "concurrentRate": return "1000 表示请求间隔 1000 ms;1/1000 表示每 1000 ms 最多 1 次请求。"
case "header": return "填写请求头 JSON,或用 @js: / <js> 动态生成。User-Agent 为 null 可禁用默认 UA。"
case "jsLib": return "填写共享 JavaScript,或以 JSON 对象配置在线脚本库。"
case "searchUrl": return "用 {{key}} 表示关键词、{{page}} 表示页码;URL 后可用逗号附加请求选项 JSON。"
case "exploreUrl": return "填写 名称::地址,多项用换行或 && 分隔;也支持 JSON 分类数组和 JavaScript。"
case "loginUi": return "填写登录界面 JSON。"
case "ruleToc.formatJs": return "目录去重后处理标题,可使用 title、index(从 1 开始)和 gInt。"
case "ruleContent.nextContentUrl": return "提取当前章的下一分页地址,而非下一章。"
case "ruleContent.replaceRegex": return "在多页正文合并后执行的替换规则,使用 ## 分隔正则和替换内容。"
case "ruleContent.imageStyle": return "FULL 表示图片铺满;留空使用默认样式。"
default:
if isJavaScript { return "使用 JavaScript 编写,可通过更多菜单调试当前书源草稿。" }
if path.contains(".") { return "支持 CSS / JSoup、XPath、JSONPath、正则和 JavaScript;留空表示不配置此项。" }
return "留空表示不配置此项。"
}
}
public var isJavaScript: Bool {
path.lowercased().contains("js") || path == "jsLib" || path.hasSuffix("imageDecode") || path.hasSuffix("payAction")
}
}
public struct BookSourceEditorSection: Identifiable, Sendable {
public let id: String
public let title: String
public let fields: [BookSourceEditorField]
}
public enum BookSourceEditorSchema {
public static let sections: [BookSourceEditorSection] = [
.init(id: "basic", title: "基础", fields: [
.init(path: "bookSourceName", title: "书源名称"),
.init(path: "bookSourceUrl", title: "书源地址"),
.init(path: "bookSourceGroup", title: "分组"),
.init(path: "bookSourceComment", title: "备注"),
.init(path: "bookUrlPattern", title: "书籍 URL 正则"),
.init(path: "concurrentRate", title: "并发率"),
.init(path: "header", title: "请求头"),
.init(path: "jsLib", title: "JavaScript 库"),
.init(path: "variableComment", title: "变量说明"),
]),
.init(id: "login", title: "登录", fields: [
.init(path: "loginUrl", title: "登录地址 / 脚本"),
.init(path: "loginUi", title: "自定义登录界面"),
.init(path: "loginCheckJs", title: "登录检测脚本"),
.init(path: "coverDecodeJs", title: "封面解密脚本"),
]),
.init(id: "search", title: "搜索", fields: [
.init(path: "searchUrl", title: "搜索地址"),
.init(path: "ruleSearch.checkKeyWord", title: "测试关键词"),
.init(path: "ruleSearch.bookList", title: "书籍列表"),
.init(path: "ruleSearch.name", title: "书名"),
.init(path: "ruleSearch.author", title: "作者"),
.init(path: "ruleSearch.intro", title: "简介"),
.init(path: "ruleSearch.kind", title: "分类"),
.init(path: "ruleSearch.lastChapter", title: "最新章节"),
.init(path: "ruleSearch.updateTime", title: "更新时间"),
.init(path: "ruleSearch.bookUrl", title: "详情地址"),
.init(path: "ruleSearch.coverUrl", title: "封面地址"),
.init(path: "ruleSearch.wordCount", title: "字数"),
]),
.init(id: "explore", title: "发现", fields: [
.init(path: "exploreUrl", title: "发现分类地址"),
.init(path: "exploreScreen", title: "发现筛选配置"),
.init(path: "ruleExplore.bookList", title: "书籍列表"),
.init(path: "ruleExplore.name", title: "书名"),
.init(path: "ruleExplore.author", title: "作者"),
.init(path: "ruleExplore.intro", title: "简介"),
.init(path: "ruleExplore.kind", title: "分类"),
.init(path: "ruleExplore.lastChapter", title: "最新章节"),
.init(path: "ruleExplore.updateTime", title: "更新时间"),
.init(path: "ruleExplore.bookUrl", title: "详情地址"),
.init(path: "ruleExplore.coverUrl", title: "封面地址"),
.init(path: "ruleExplore.wordCount", title: "字数"),
]),
.init(id: "info", title: "详情", fields: [
.init(path: "ruleBookInfo.init", title: "预处理"),
.init(path: "ruleBookInfo.name", title: "书名"),
.init(path: "ruleBookInfo.author", title: "作者"),
.init(path: "ruleBookInfo.intro", title: "简介"),
.init(path: "ruleBookInfo.kind", title: "分类"),
.init(path: "ruleBookInfo.lastChapter", title: "最新章节"),
.init(path: "ruleBookInfo.updateTime", title: "更新时间"),
.init(path: "ruleBookInfo.coverUrl", title: "封面地址"),
.init(path: "ruleBookInfo.tocUrl", title: "目录地址"),
.init(path: "ruleBookInfo.wordCount", title: "字数"),
.init(path: "ruleBookInfo.canReName", title: "允许修改书名作者"),
.init(path: "ruleBookInfo.downloadUrls", title: "文件下载地址"),
]),
.init(id: "toc", title: "目录", fields: [
.init(path: "ruleToc.preUpdateJs", title: "更新前脚本"),
.init(path: "ruleToc.chapterList", title: "章节列表"),
.init(path: "ruleToc.chapterName", title: "章节名称"),
.init(path: "ruleToc.chapterUrl", title: "章节地址"),
.init(path: "ruleToc.formatJs", title: "标题处理脚本"),
.init(path: "ruleToc.isVolume", title: "卷名标识"),
.init(path: "ruleToc.isVip", title: "VIP 标识"),
.init(path: "ruleToc.isPay", title: "已购买标识"),
.init(path: "ruleToc.updateTime", title: "章节信息"),
.init(path: "ruleToc.nextTocUrl", title: "目录下一页"),
]),
.init(id: "content", title: "正文", fields: [
.init(path: "ruleContent.content", title: "正文"),
.init(path: "ruleContent.subContent", title: "副正文"),
.init(path: "ruleContent.title", title: "正文标题"),
.init(path: "ruleContent.nextContentUrl", title: "正文下一页"),
.init(path: "ruleContent.webJs", title: "WebView 注入脚本"),
.init(path: "ruleContent.sourceRegex", title: "资源正则"),
.init(path: "ruleContent.replaceRegex", title: "合并后替换"),
.init(path: "ruleContent.imageStyle", title: "图片样式"),
.init(path: "ruleContent.imageDecode", title: "图片解密脚本"),
.init(path: "ruleContent.payAction", title: "购买脚本"),
.init(path: "ruleContent.callBackJs", title: "事件回调脚本"),
]),
.init(id: "review", title: "段评", fields: [
.init(path: "ruleReview.reviewUrl", title: "段评地址"),
.init(path: "ruleReview.avatarRule", title: "头像"),
.init(path: "ruleReview.contentRule", title: "内容"),
.init(path: "ruleReview.postTimeRule", title: "发布时间"),
.init(path: "ruleReview.reviewQuoteUrl", title: "回复地址"),
.init(path: "ruleReview.voteUpUrl", title: "点赞地址"),
.init(path: "ruleReview.voteDownUrl", title: "点踩地址"),
.init(path: "ruleReview.postReviewUrl", title: "发布段评"),
.init(path: "ruleReview.postQuoteUrl", title: "发布回复"),
.init(path: "ruleReview.deleteUrl", title: "删除地址"),
]),
]
}
public struct BookSourceEditorDocument: Sendable, Equatable {
public private(set) var fields: [String: JSONValue]
public init(source: BookSource) throws {
self = try Self(json: String(decoding: JSONEncoder().encode(source), as: UTF8.self))
}
public init(json: String) throws {
guard case .object(let object) = try JSONValue.decode(data: Data(json.utf8)) else {
throw BookSourceImportError.invalidDocument("编辑器需要单个书源 JSON 对象")
}
fields = object
for key in Self.ruleKeys {
if case .string(let text) = fields[key], !text.isEmpty,
case .object(let object) = try JSONValue.decode(data: Data(text.utf8)) {
fields[key] = .object(object)
}
}
}
public func value(at path: String) -> JSONValue? {
let keys = path.split(separator: ".").map(String.init)
guard let key = keys.first else { return nil }
return keys.count == 1 ? fields[key] : fields[key]?[keys[1]]
}
public mutating func set(_ value: JSONValue?, at path: String) {
let keys = path.split(separator: ".").map(String.init)
guard let key = keys.first else { return }
if keys.count == 1 { fields[key] = value; return }
var object: [String: JSONValue] = [:]
if case .object(let existing) = fields[key] { object = existing }
object[keys[1]] = value
fields[key] = object.isEmpty ? nil : .object(object)
}
public func json() throws -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
return String(decoding: try encoder.encode(fields), as: UTF8.self)
}
public func source() throws -> BookSource {
let source = try JSONDecoder().decode(BookSource.self, from: JSONEncoder().encode(fields))
let errors = BookSourceValidator.validate(source).filter { $0.severity == .error }
guard errors.isEmpty else { throw BookSourceImportError.invalidSources(errors) }
return source
}
static let ruleKeys = ["ruleSearch", "ruleExplore", "ruleBookInfo", "ruleToc", "ruleContent", "ruleReview"]
}
struct SourceCodingKey: CodingKey {
let stringValue: String
let intValue: Int? = nil
init(_ value: String) { stringValue = value }
init?(stringValue: String) { self.init(stringValue) }
init?(intValue: Int) { return nil }
}