feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -5,24 +5,20 @@ import UIKit
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
/// 章节分页计数器:顶层编排循环,将富文本按页面尺寸拆分为多帧。
|
||||
///
|
||||
/// 分页策略优先级(从高到低):
|
||||
/// 1. avoidPageBreakInside — 不在保护块内分页(WXRead 行级回退扫描)
|
||||
/// 2. keepWithNext — 标题等元素需与下一段同页
|
||||
/// 3. 语义边界 — pageBreakBefore/After 等显式分页标记
|
||||
/// 4. pageRelate — 微信读书式的跨页关联元素
|
||||
/// 5. 附件边界 — 块级附件应整体移到下一页
|
||||
/// 6. 帧限制 — 默认按 CoreText 可视范围分页
|
||||
struct RDEPUBChapterPageCounter {
|
||||
|
||||
private let factory: RDEPUBCoreTextPageFrameFactory
|
||||
|
||||
private let attributedString: NSAttributedString
|
||||
|
||||
private let pageSize: CGSize
|
||||
|
||||
private let config: RDEPUBTextLayoutConfig
|
||||
|
||||
private let pageBreakPolicy: RDEPUBPageBreakPolicy
|
||||
|
||||
private let framesetter: CTFramesetter
|
||||
|
||||
/// DTCoreText 路径可直接消费的单矩形布局区域
|
||||
private let dtLayoutRect: CGRect
|
||||
|
||||
init(factory: RDEPUBCoreTextPageFrameFactory) {
|
||||
@@ -35,7 +31,6 @@ struct RDEPUBChapterPageCounter {
|
||||
self.dtLayoutRect = factory.config.contentRect(fallback: factory.pageSize)
|
||||
}
|
||||
|
||||
/// 执行分页,返回布局帧列表(每帧对应一页)。
|
||||
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] {
|
||||
guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else {
|
||||
return []
|
||||
@@ -48,8 +43,6 @@ struct RDEPUBChapterPageCounter {
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - CoreText 分页路径(回退方案)
|
||||
|
||||
private func layoutFramesUsingCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] {
|
||||
guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else {
|
||||
return []
|
||||
@@ -129,9 +122,8 @@ struct RDEPUBChapterPageCounter {
|
||||
return frames
|
||||
}
|
||||
|
||||
// MARK: - DTCoreText 分页路径(首选方案)
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
|
||||
private func layoutFramesUsingDTCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] {
|
||||
guard config.numberOfColumns == 1 else {
|
||||
return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets)
|
||||
@@ -252,9 +244,6 @@ struct RDEPUBChapterPageCounter {
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - WXRead 分页对齐
|
||||
|
||||
/// 读取 CoreText frame 当前可见的字符范围;多栏 path 下也能返回整页可见区。
|
||||
private func proposedVisibleRange(
|
||||
from frame: CTFrame,
|
||||
start location: Int,
|
||||
|
||||
+6
-41
@@ -5,13 +5,14 @@ import UIKit
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
/// CoreText 帧工厂:负责帧创建、行级裁剪、属性查询和诊断构建。
|
||||
///
|
||||
/// 叶节点组件,被 RDEPUBChapterPageCounter 和 RDEPUBPageBreakPolicy 调用。
|
||||
struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
|
||||
let attributedString: NSAttributedString
|
||||
|
||||
let pageSize: CGSize
|
||||
|
||||
let config: RDEPUBTextLayoutConfig
|
||||
|
||||
private let pageBreakPolicy: RDEPUBPageBreakPolicy
|
||||
|
||||
init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default) {
|
||||
@@ -21,13 +22,10 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
self.pageBreakPolicy = RDEPUBPageBreakPolicy(attributedString: attributedString)
|
||||
}
|
||||
|
||||
/// 协议要求的便捷初始化(使用默认配置)
|
||||
init() {
|
||||
self.init(attributedString: NSAttributedString(), pageSize: .zero, config: .default)
|
||||
}
|
||||
|
||||
// MARK: - RDEPUBPageFrameBuilding 协议
|
||||
|
||||
func makeFrames(
|
||||
attributedString: NSAttributedString,
|
||||
pageSize: CGSize,
|
||||
@@ -39,9 +37,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return counter.layoutFrames(fragmentOffsets: fragmentOffsets)
|
||||
}
|
||||
|
||||
// MARK: - 帧构建
|
||||
|
||||
/// 从列矩形构建 CGPath(用于 CTFramesetterCreateFrame)。
|
||||
static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath {
|
||||
let columnRects = config.columnRects(fallback: pageSize)
|
||||
guard columnRects.count > 1 else {
|
||||
@@ -55,9 +50,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return path
|
||||
}
|
||||
|
||||
// MARK: - 行级裁剪
|
||||
|
||||
/// 从 CTFrame 最后一行向前扫描,移除落在 avoidPageBreakInside 保护块内的尾部行。
|
||||
func trimmedRangeForAvoidPageBreakInside(
|
||||
from frame: CTFrame,
|
||||
proposed: NSRange
|
||||
@@ -74,7 +66,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return trimmedRangeForAvoidPageBreakInside(proposed: proposed, lineRanges: lineRanges)
|
||||
}
|
||||
|
||||
/// CoreText 路径的 keepWithNext 处理:从最后行向前扫描
|
||||
func trimmedRangeForKeepWithNext(
|
||||
from frame: CTFrame,
|
||||
proposed: NSRange
|
||||
@@ -88,7 +79,7 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
}
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
/// DTCoreText 路径的 avoidPageBreakInside 处理
|
||||
|
||||
func trimmedRangeForAvoidPageBreakInside(
|
||||
from layoutFrame: DTCoreTextLayoutFrame,
|
||||
proposed: NSRange
|
||||
@@ -102,7 +93,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return trimmedRangeForAvoidPageBreakInside(proposed: proposed, lineRanges: lineRanges)
|
||||
}
|
||||
|
||||
/// DTCoreText 路径的 keepWithNext 处理
|
||||
func trimmedRangeForKeepWithNext(
|
||||
from layoutFrame: DTCoreTextLayoutFrame,
|
||||
proposed: NSRange
|
||||
@@ -115,7 +105,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
}
|
||||
#endif
|
||||
|
||||
/// 从最后行向前扫描,移除落在 avoidPageBreakInside 保护块内的尾部行(通用路径)。
|
||||
func trimmedRangeForAvoidPageBreakInside(
|
||||
proposed: NSRange,
|
||||
lineRanges: [NSRange]
|
||||
@@ -155,7 +144,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return NSRange(location: proposed.location, length: adjustedLength)
|
||||
}
|
||||
|
||||
/// 从最后行向前扫描,移除落在 keepWithNext 保护块内的尾部行。
|
||||
func trimmedRangeForKeepWithNext(
|
||||
proposed: NSRange,
|
||||
lineRanges: [NSRange]
|
||||
@@ -195,7 +183,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return NSRange(location: proposed.location, length: adjustedLength)
|
||||
}
|
||||
|
||||
/// 根据 avoidWidows / avoidOrphans 修正页尾断点,避免段首孤悬页尾或段末独悬下一页。
|
||||
func trimmedRangeForWidowAndOrphanControl(
|
||||
proposed: NSRange,
|
||||
lineRanges: [NSRange]
|
||||
@@ -227,9 +214,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return adjusted
|
||||
}
|
||||
|
||||
// MARK: - 行信息提取
|
||||
|
||||
/// 获取 CTFrame 中所有行的字符范围
|
||||
static func lineRanges(from frame: CTFrame) -> [NSRange] {
|
||||
let lines = CTFrameGetLines(frame) as! [CTLine]
|
||||
return lines.map {
|
||||
@@ -239,7 +223,7 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
}
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
/// 获取 DTCoreTextLayoutFrame 中所有行的字符范围
|
||||
|
||||
static func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] {
|
||||
guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else {
|
||||
return []
|
||||
@@ -248,9 +232,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - 属性查询
|
||||
|
||||
/// 获取指定位置的块级元素范围
|
||||
func blockRange(at location: Int) -> NSRange? {
|
||||
guard location >= 0, location < attributedString.length else { return nil }
|
||||
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
|
||||
@@ -260,7 +241,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 获取指定位置的块级元素类型
|
||||
func blockKind(at location: Int) -> RDEPUBTextBlockKind? {
|
||||
guard location >= 0, location < attributedString.length else { return nil }
|
||||
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
|
||||
@@ -268,7 +248,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return RDEPUBTextBlockKind(rawValue: rawValue)
|
||||
}
|
||||
|
||||
/// 获取指定位置的附件布局方式
|
||||
func attachmentPlacement(at location: Int) -> RDEPUBTextAttachmentPlacement? {
|
||||
guard location >= 0, location < attributedString.length else { return nil }
|
||||
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
|
||||
@@ -276,7 +255,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return RDEPUBTextAttachmentPlacement(rawValue: rawValue)
|
||||
}
|
||||
|
||||
/// 获取包含指定位置的段落范围
|
||||
func paragraphRange(containing location: Int) -> NSRange {
|
||||
let source = attributedString.string as NSString
|
||||
guard source.length > 0 else { return NSRange(location: 0, length: 0) }
|
||||
@@ -284,7 +262,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return source.paragraphRange(for: NSRange(location: safeLocation, length: 0))
|
||||
}
|
||||
|
||||
/// 获取指定范围内的所有附件字符范围
|
||||
func attachmentRanges(in range: NSRange) -> [NSRange] {
|
||||
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||
return []
|
||||
@@ -297,7 +274,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return results
|
||||
}
|
||||
|
||||
/// 获取指定位置的语义提示列表
|
||||
func semanticHints(at location: Int) -> [RDEPUBTextSemanticHint] {
|
||||
guard location >= 0, location < attributedString.length else { return [] }
|
||||
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
|
||||
@@ -307,8 +283,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
||||
}
|
||||
|
||||
// MARK: - Widow / Orphan 控制
|
||||
|
||||
private func trimmedRangeAvoidingWidow(
|
||||
proposed: NSRange,
|
||||
lineRanges: [NSRange]
|
||||
@@ -408,7 +382,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return lineCount
|
||||
}
|
||||
|
||||
/// 获取指定范围内的附件类型列表(去重)
|
||||
func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] {
|
||||
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||
return []
|
||||
@@ -425,7 +398,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return kinds
|
||||
}
|
||||
|
||||
/// 获取指定范围内的块级元素类型列表(去重)
|
||||
func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] {
|
||||
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||
return []
|
||||
@@ -442,7 +414,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return kinds
|
||||
}
|
||||
|
||||
/// 获取指定范围内的语义提示列表(去重)
|
||||
func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] {
|
||||
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||
return []
|
||||
@@ -457,7 +428,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return hints
|
||||
}
|
||||
|
||||
/// 获取指定范围内的附件布局方式列表(去重)
|
||||
func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] {
|
||||
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||
return []
|
||||
@@ -474,7 +444,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return placements
|
||||
}
|
||||
|
||||
/// 将范围裁剪到 attributedString 的合法边界内。
|
||||
func clampedRange(_ range: NSRange) -> NSRange? {
|
||||
guard range.location >= 0, range.length >= 0 else { return nil }
|
||||
guard attributedString.length > 0 else {
|
||||
@@ -486,7 +455,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
return NSRange(location: range.location, length: min(range.length, maxLength))
|
||||
}
|
||||
|
||||
/// 查找指定位置之前最近的 fragment ID(用于阅读位置恢复)
|
||||
func nearestTrailingFragmentID(
|
||||
endingAt location: Int,
|
||||
fragmentOffsets: [String: Int]
|
||||
@@ -497,9 +465,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
|
||||
.key
|
||||
}
|
||||
|
||||
// MARK: - 诊断日志
|
||||
|
||||
/// 生成分页诊断日志
|
||||
func diagnostics(
|
||||
reason: RDEPUBTextPageBreakReason,
|
||||
range: NSRange,
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// 分页规则策略:语义边界搜索、行级保护检查、范围调整调度器。
|
||||
///
|
||||
/// 不构建 CoreText frame,只基于 attributed string 属性做规则判定。
|
||||
struct RDEPUBPageBreakPolicy {
|
||||
|
||||
private let attributedString: NSAttributedString
|
||||
|
||||
init(attributedString: NSAttributedString) {
|
||||
self.attributedString = attributedString
|
||||
}
|
||||
|
||||
// MARK: - 行级保护检查
|
||||
|
||||
/// 检查指定行范围是否落在 avoidPageBreakInside 保护块内。
|
||||
func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool {
|
||||
guard let probeRange = clampedProbeRange(for: lineRange) else {
|
||||
return false
|
||||
@@ -35,7 +30,6 @@ struct RDEPUBPageBreakPolicy {
|
||||
return found
|
||||
}
|
||||
|
||||
/// 检查指定行范围是否落在 keepWithNext 保护块内。
|
||||
func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool {
|
||||
guard let probeRange = clampedProbeRange(for: lineRange) else {
|
||||
return false
|
||||
@@ -54,14 +48,6 @@ struct RDEPUBPageBreakPolicy {
|
||||
return found
|
||||
}
|
||||
|
||||
// MARK: - 范围调整调度器
|
||||
|
||||
/// 对 CoreText/DTCoreText 提出的分页范围进行语义边界调整。
|
||||
///
|
||||
/// 调整优先级:
|
||||
/// 1. 若已达章节末尾,直接返回 chapterEnd
|
||||
/// 2. pageRelate 跨页关联边界
|
||||
/// 3. 以上都不满足时,使用原始帧限制分页
|
||||
func adjustedRange(
|
||||
from proposedRange: NSRange,
|
||||
totalLength: Int,
|
||||
@@ -111,8 +97,6 @@ struct RDEPUBPageBreakPolicy {
|
||||
let currentSemanticHints = proposedSemanticHints
|
||||
let currentAttachmentPlacements = proposedAttachmentPlacements
|
||||
|
||||
// 对齐 WXRead:默认按 CTFrame 已经容纳的行数分页,仅保留 pageRelate 这种
|
||||
// 微信读书特有的跨页关联规则。
|
||||
if let pageRelateBoundary = preferredPageRelateBoundary(
|
||||
after: proposedRange,
|
||||
minimumEnd: proposedRange.location + 1,
|
||||
@@ -142,7 +126,6 @@ struct RDEPUBPageBreakPolicy {
|
||||
)
|
||||
}
|
||||
|
||||
// 帧限制(默认分页)
|
||||
return (
|
||||
range: proposedRange,
|
||||
breakReason: .frameLimit,
|
||||
@@ -164,9 +147,6 @@ struct RDEPUBPageBreakPolicy {
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 语义边界查找
|
||||
|
||||
/// 查找 pageRelate 跨页关联边界。
|
||||
func preferredPageRelateBoundary(
|
||||
after range: NSRange,
|
||||
minimumEnd: Int,
|
||||
@@ -192,8 +172,6 @@ struct RDEPUBPageBreakPolicy {
|
||||
return lastLineStart
|
||||
}
|
||||
|
||||
// MARK: - 内部工具
|
||||
|
||||
private func shouldTreatAvoidHintAsBlockProtection(_ attributes: [NSAttributedString.Key: Any]) -> Bool {
|
||||
guard let rawValue = attributes[.rdPageSemanticHints] as? String else {
|
||||
return false
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import Foundation
|
||||
|
||||
/// CoreText 分页引擎产生的单帧(单页)布局数据。
|
||||
///
|
||||
/// 由 `RDEPUBTextLayouter` 在分页过程中生成,记录了该页的内容范围、分页原因、
|
||||
/// 附件信息和语义标记。`RDEPUBTextBookBuilder` 会将 `RDEPUBTextLayoutFrame` 转换为
|
||||
/// `RDEPUBTextPage`,并纳入最终的 `RDEPUBTextBook` 模型。
|
||||
struct RDEPUBTextLayoutFrame: Equatable {
|
||||
/// 该帧在章节富文本中的字符范围
|
||||
|
||||
var contentRange: NSRange
|
||||
/// 分页原因(语义边界、附件边界、帧限制等)
|
||||
|
||||
var breakReason: RDEPUBTextPageBreakReason
|
||||
/// 所属 HTML 块级元素的范围(用于调试)
|
||||
|
||||
var blockRange: NSRange?
|
||||
/// 帧内附件的字符范围列表
|
||||
|
||||
var attachmentRanges: [NSRange]
|
||||
/// 帧内附件类型列表(图片、通用附件等)
|
||||
|
||||
var attachmentKinds: [RDEPUBTextAttachmentKind]
|
||||
/// 帧内包含的 HTML 块级元素类型列表(段落、列表、表格等)
|
||||
|
||||
var blockKinds: [RDEPUBTextBlockKind]
|
||||
/// 帧内语义提示列表(避免分页、保持与下一段同页等)
|
||||
|
||||
var semanticHints: [RDEPUBTextSemanticHint]
|
||||
/// 帧内附件的布局方式(行内、基线、居中)
|
||||
|
||||
var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
|
||||
/// 帧尾部最近的 fragment ID(用于恢复阅读位置)
|
||||
|
||||
var trailingFragmentID: String?
|
||||
/// 诊断日志列表(分页原因详情、范围信息等)
|
||||
|
||||
var diagnostics: [String]
|
||||
|
||||
/// 将布局帧数据转换为页元数据模型
|
||||
var metadata: RDEPUBTextPageMetadata {
|
||||
RDEPUBTextPageMetadata(
|
||||
breakReason: breakReason,
|
||||
|
||||
@@ -5,12 +5,6 @@ import UIKit
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
/// CoreText 分页引擎 Facade:将富文本按页面尺寸拆分为多帧(每帧对应一页)。
|
||||
///
|
||||
/// 内部委托给三个组件:
|
||||
/// - RDEPUBCoreTextPageFrameFactory — 帧创建、属性查询、诊断
|
||||
/// - RDEPUBPageBreakPolicy — 分页规则(语义保护、keepWithNext)
|
||||
/// - RDEPUBChapterPageCounter — 顶层分页循环编排
|
||||
struct RDEPUBTextLayouter {
|
||||
private let counter: RDEPUBChapterPageCounter
|
||||
|
||||
@@ -19,7 +13,6 @@ struct RDEPUBTextLayouter {
|
||||
self.counter = RDEPUBChapterPageCounter(factory: factory)
|
||||
}
|
||||
|
||||
/// 执行分页,返回布局帧列表(每帧对应一页)。
|
||||
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] {
|
||||
counter.layoutFrames(fragmentOffsets: fragmentOffsets)
|
||||
}
|
||||
|
||||
+5
-24
@@ -1,30 +1,18 @@
|
||||
// RDEPUBTextPaginationInterfaces.swift
|
||||
// EPUB 分页接口定义,包括断页决策、章节数页和页面帧构建协议。
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: - 分页接口定义
|
||||
|
||||
/// 单次断页决策,记录断页位置、原因及诊断信息。
|
||||
struct RDEPUBPageBreakDecision {
|
||||
/// 本页在富文本中的字符范围
|
||||
|
||||
var range: NSRange
|
||||
/// 断页原因
|
||||
|
||||
var reason: RDEPUBTextPageBreakReason
|
||||
/// 分页过程中的诊断日志
|
||||
|
||||
var diagnostics: [String]
|
||||
}
|
||||
|
||||
/// 章节页数计算接口,将富文本按页面尺寸拆分为断页决策序列。
|
||||
protocol RDEPUBChapterPageCounting {
|
||||
/// 计算章节中每页的断页位置。
|
||||
/// - Parameters:
|
||||
/// - attributedString: 章节富文本
|
||||
/// - pageSize: 页面尺寸
|
||||
/// - config: 排版配置
|
||||
/// - fragmentOffsets: fragment 锚点偏移映射
|
||||
/// - Returns: 断页决策数组,每个元素对应一页
|
||||
|
||||
func pageRanges(
|
||||
for attributedString: NSAttributedString,
|
||||
pageSize: CGSize,
|
||||
@@ -33,15 +21,8 @@ protocol RDEPUBChapterPageCounting {
|
||||
) -> [RDEPUBPageBreakDecision]
|
||||
}
|
||||
|
||||
/// 页面帧构建接口,将富文本转换为可渲染的页面帧数组。
|
||||
protocol RDEPUBPageFrameBuilding {
|
||||
/// 将富文本构建为页面帧数组。
|
||||
/// - Parameters:
|
||||
/// - attributedString: 待分页的富文本
|
||||
/// - pageSize: 页面尺寸
|
||||
/// - config: 排版配置
|
||||
/// - fragmentOffsets: fragment 锚点偏移映射
|
||||
/// - Returns: 页面帧数组
|
||||
|
||||
func makeFrames(
|
||||
attributedString: NSAttributedString,
|
||||
pageSize: CGSize,
|
||||
|
||||
+2
-16
@@ -1,18 +1,8 @@
|
||||
import CoreText
|
||||
import UIKit
|
||||
|
||||
// MARK: - NSAttributedString 分页扩展
|
||||
|
||||
/// 为 NSAttributedString 提供便捷的分页方法,是渲染链路中 CoreText 分页的入口点。
|
||||
extension NSAttributedString {
|
||||
/// 将富文本按指定页面尺寸分页,返回布局帧列表(每帧对应一页)。
|
||||
///
|
||||
/// 这是 `RDEPUBTextBookBuilder` 和 `RDPlainTextBookBuilder` 调用的核心分页方法。
|
||||
/// - Parameters:
|
||||
/// - size: 页面尺寸
|
||||
/// - fragmentOffsets: fragment ID → 字符偏移量映射(用于阅读位置恢复)
|
||||
/// - config: 布局配置(孤行控制、avoidPageBreakInside 等)
|
||||
/// - Returns: 分页后的布局帧列表
|
||||
|
||||
func rd_paginatedFrames(
|
||||
size: CGSize,
|
||||
fragmentOffsets: [String: Int] = [:],
|
||||
@@ -23,17 +13,13 @@ extension NSAttributedString {
|
||||
return counter.layoutFrames(fragmentOffsets: fragmentOffsets)
|
||||
}
|
||||
|
||||
/// 简化版分页:只返回每页的 NSRange 列表(不含语义元数据)
|
||||
func ss_pageRanges(size: CGSize) -> [NSRange] {
|
||||
rd_paginatedFrames(size: size).map(\.contentRange)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIColor CSS 转换扩展
|
||||
|
||||
/// 为 UIColor 提供 CSS 颜色字符串转换,用于动态生成暗色模式 CSS。
|
||||
extension UIColor {
|
||||
/// 将 UIColor 转换为 CSS rgba() 字符串格式
|
||||
|
||||
var ss_cssString: String {
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
|
||||
Reference in New Issue
Block a user