右对齐文本显示修复(如"巫鸿"只显示"鸿"的问题): - RDEPUBChapterTailNormalizer: 短尾页合并时检查 avoidPageBreakInside 语义, 避免将带有此标记的短文本(如 right-info 署名)错误合并回上一页 - RDEPUBTextContentView: 续段归一化时跳过右对齐/居中对齐的段落, 防止错误修改 firstLineHeadIndent 导致首字不可见 - RDEPUBCSSCompatibilityLayer: 为含 text-align:right/center 的 CSS 块 自动注入 text-indent:0 !important,确保右对齐/居中文本无首行缩进 - RDEPUBTextRendererSupport: 排版属性归一化时将右对齐/居中段落的 firstLineHeadIndent 重置为 0 图片查看器及脚注点击功能: - 新增 RDEPUBImageViewController 和 RDEPUBImageViewerCoordinator, 支持从 WebView 和 TextPage 两种模式查看图片 - epub-bridge.js: 检测图片和脚注图片的点击事件,脚注图片显示弹窗 - RDEPUBJavaScriptBridge: 新增 imageDidTap/footnoteDidTap 桥接消息 - RDEPUBWebView: 新增图片和脚注点击的 delegate 方法 - RDEPUBAttachmentNormalizer: 改进脚注检测,优先使用 alt 文本判断 - RDEPUBPaginationModels: 新增 .footnote 附件类型 - RDEPUBPageLayoutSnapshot: 运行时动态解析附件类型,脚注优先级高于图片 位置解析改进: - RDEPUBReaderController+LocationResolution: 利用 rangeInfo 提升页码定位精度 其他: - RDEPUBTextBookCache: schema 版本升级至 13 - RDEPUBReaderTheme: 主题更新 - Pod 项目文件更新 Co-Authored-By: Claude <noreply@anthropic.com>
207 lines
5.5 KiB
Swift
207 lines
5.5 KiB
Swift
import Foundation
|
|
|
|
public enum RDEPUBTextPageBreakReason: String, Codable, Equatable {
|
|
|
|
case chapterEnd
|
|
|
|
case frameLimit
|
|
|
|
case blockBoundary
|
|
|
|
case attachmentBoundary
|
|
|
|
case semanticBoundary
|
|
}
|
|
|
|
public enum RDEPUBTextAttachmentKind: String, Codable, Equatable {
|
|
|
|
case image
|
|
|
|
case generic
|
|
|
|
case footnote
|
|
}
|
|
|
|
public struct RDEPUBTextPageMetadata: Codable, Equatable {
|
|
|
|
public var breakReason: RDEPUBTextPageBreakReason
|
|
|
|
public var blockRange: NSRange?
|
|
|
|
public var attachmentRanges: [NSRange]
|
|
|
|
public var attachmentKinds: [RDEPUBTextAttachmentKind]
|
|
|
|
public var blockKinds: [RDEPUBTextBlockKind]
|
|
|
|
public var semanticHints: [RDEPUBTextSemanticHint]
|
|
|
|
public var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
|
|
|
|
public var trailingFragmentID: String?
|
|
|
|
public var diagnostics: [String]
|
|
|
|
public init(
|
|
breakReason: RDEPUBTextPageBreakReason,
|
|
blockRange: NSRange? = nil,
|
|
attachmentRanges: [NSRange] = [],
|
|
attachmentKinds: [RDEPUBTextAttachmentKind] = [],
|
|
blockKinds: [RDEPUBTextBlockKind] = [],
|
|
semanticHints: [RDEPUBTextSemanticHint] = [],
|
|
attachmentPlacements: [RDEPUBTextAttachmentPlacement] = [],
|
|
trailingFragmentID: String? = nil,
|
|
diagnostics: [String] = []
|
|
) {
|
|
self.breakReason = breakReason
|
|
self.blockRange = blockRange
|
|
self.attachmentRanges = attachmentRanges
|
|
self.attachmentKinds = attachmentKinds
|
|
self.blockKinds = blockKinds
|
|
self.semanticHints = semanticHints
|
|
self.attachmentPlacements = attachmentPlacements
|
|
self.trailingFragmentID = trailingFragmentID
|
|
self.diagnostics = diagnostics
|
|
}
|
|
}
|
|
|
|
public struct EPUBChapterInfo: Codable, Equatable {
|
|
|
|
public var spineIndex: Int
|
|
|
|
public var title: String
|
|
|
|
public var pageCount: Int
|
|
|
|
public init(spineIndex: Int, title: String, pageCount: Int) {
|
|
self.spineIndex = spineIndex
|
|
self.title = title
|
|
self.pageCount = pageCount
|
|
}
|
|
}
|
|
|
|
public struct EPUBPage: Codable, Equatable {
|
|
|
|
public var spineIndex: Int
|
|
|
|
public var chapterIndex: Int
|
|
|
|
public var pageIndexInChapter: Int
|
|
|
|
public var totalPagesInChapter: Int
|
|
|
|
public var chapterTitle: String
|
|
|
|
public var fixedSpread: EPUBFixedSpread?
|
|
|
|
public init(
|
|
spineIndex: Int,
|
|
chapterIndex: Int,
|
|
pageIndexInChapter: Int,
|
|
totalPagesInChapter: Int,
|
|
chapterTitle: String,
|
|
fixedSpread: EPUBFixedSpread?
|
|
) {
|
|
self.spineIndex = spineIndex
|
|
self.chapterIndex = chapterIndex
|
|
self.pageIndexInChapter = pageIndexInChapter
|
|
self.totalPagesInChapter = totalPagesInChapter
|
|
self.chapterTitle = chapterTitle
|
|
self.fixedSpread = fixedSpread
|
|
}
|
|
}
|
|
|
|
public struct EPUBFixedSpreadResource: Codable, Equatable {
|
|
|
|
public var spineIndex: Int
|
|
|
|
public var href: String
|
|
|
|
public var title: String
|
|
|
|
public var pageSpread: RDEPUBPageSpread?
|
|
|
|
public init(spineIndex: Int, href: String, title: String, pageSpread: RDEPUBPageSpread? = nil) {
|
|
self.spineIndex = spineIndex
|
|
self.href = href
|
|
self.title = title
|
|
self.pageSpread = pageSpread
|
|
}
|
|
}
|
|
|
|
public struct EPUBFixedSpread: Codable, Equatable {
|
|
|
|
public var resources: [EPUBFixedSpreadResource]
|
|
|
|
public init(resources: [EPUBFixedSpreadResource]) {
|
|
self.resources = resources
|
|
}
|
|
|
|
public var primaryResource: EPUBFixedSpreadResource {
|
|
resources.first ?? EPUBFixedSpreadResource(spineIndex: 0, href: "", title: "")
|
|
}
|
|
|
|
public func contains(normalizedHref: String, normalizer: (String) -> String?) -> Bool {
|
|
resources.contains { resource in
|
|
normalizer(resource.href) == normalizedHref
|
|
}
|
|
}
|
|
|
|
public static func makeSpreads(spine: [RDEPUBSpineItem], spreadEnabled: Bool) -> [EPUBFixedSpread] {
|
|
let resources = spine.enumerated().map { index, item in
|
|
EPUBFixedSpreadResource(
|
|
spineIndex: index,
|
|
href: item.href,
|
|
title: item.title,
|
|
pageSpread: item.pageSpread
|
|
)
|
|
}
|
|
|
|
guard spreadEnabled, resources.count > 1 else {
|
|
return resources.map { EPUBFixedSpread(resources: [$0]) }
|
|
}
|
|
|
|
var spreads: [EPUBFixedSpread] = []
|
|
var cursor = 0
|
|
|
|
while cursor < resources.count {
|
|
let current = resources[cursor]
|
|
|
|
guard cursor + 1 < resources.count else {
|
|
spreads.append(EPUBFixedSpread(resources: [current]))
|
|
break
|
|
}
|
|
|
|
let next = resources[cursor + 1]
|
|
if shouldPair(current: current, next: next) {
|
|
spreads.append(EPUBFixedSpread(resources: [current, next]))
|
|
cursor += 2
|
|
} else {
|
|
spreads.append(EPUBFixedSpread(resources: [current]))
|
|
cursor += 1
|
|
}
|
|
}
|
|
|
|
return spreads
|
|
}
|
|
|
|
public static func makeSpreads(parser: RDEPUBParser, spreadEnabled: Bool) -> [EPUBFixedSpread] {
|
|
makeSpreads(spine: parser.spine, spreadEnabled: spreadEnabled)
|
|
}
|
|
|
|
private static func shouldPair(current: EPUBFixedSpreadResource, next: EPUBFixedSpreadResource) -> Bool {
|
|
if current.pageSpread == .center || next.pageSpread == .center {
|
|
return false
|
|
}
|
|
|
|
switch (current.pageSpread, next.pageSpread) {
|
|
case (.left, .right), (.right, .left):
|
|
return true
|
|
case (.left, .left), (.right, .right):
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
}
|