// RDEPUBTextAnchor.swift // EPUB 文本锚点和范围锚点 // 定义精确的文本位置锚点(RDEPUBTextAnchor,含 fileIndex/row/column/chapterOffset/fragmentID), // 以及文本范围锚点(RDEPUBTextRangeAnchor,起止锚点对)。 // 支持 Codable 序列化(兼容 spineIndex/fileIndex 两种 key)。 import Foundation /// 文本锚点,精确定位到 EPUB 中的某个字符位置 /// 包含文件索引、行号、列号、章节内偏移和可选的 fragment ID public struct RDEPUBTextAnchor: Codable, Equatable { /// 文件索引(等同于 spineIndex) public let fileIndex: Int /// 行号 public let row: Int /// 列号(行内偏移) public let column: Int /// 章节内的绝对字符偏移量 public let chapterOffset: Int /// 最近的 fragment ID(用于 URL 锚点定位) public let fragmentID: String? /// spine 索引的别名(与 fileIndex 等价) public var spineIndex: Int { fileIndex } public init( fileIndex: Int, row: Int, column: Int, chapterOffset: Int, fragmentID: String? = nil ) { self.fileIndex = fileIndex self.row = row self.column = column self.chapterOffset = chapterOffset self.fragmentID = fragmentID } enum CodingKeys: String, CodingKey { case fileIndex case spineIndex case row case column case chapterOffset case fragmentID } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let decodedFileIndex = try container.decodeIfPresent(Int.self, forKey: .fileIndex) ?? container.decode(Int.self, forKey: .spineIndex) self.fileIndex = decodedFileIndex self.row = try container.decodeIfPresent(Int.self, forKey: .row) ?? 0 self.column = try container.decodeIfPresent(Int.self, forKey: .column) ?? 0 self.chapterOffset = try container.decode(Int.self, forKey: .chapterOffset) self.fragmentID = try container.decodeIfPresent(String.self, forKey: .fragmentID) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(fileIndex, forKey: .fileIndex) try container.encode(row, forKey: .row) try container.encode(column, forKey: .column) try container.encode(chapterOffset, forKey: .chapterOffset) try container.encodeIfPresent(fragmentID, forKey: .fragmentID) } } /// 文本范围锚点,由起止锚点组成的区间 public struct RDEPUBTextRangeAnchor: Codable, Equatable { /// 起始锚点 public let start: RDEPUBTextAnchor /// 结束锚点 public let end: RDEPUBTextAnchor public init(start: RDEPUBTextAnchor, end: RDEPUBTextAnchor) { self.start = start self.end = end } /// 转换为 NSRange(基于 chapterOffset) public var nsRange: NSRange { NSRange(location: start.chapterOffset, length: max(end.chapterOffset - start.chapterOffset, 0)) } }