import Foundation public struct RDEPUBCFIMap: Codable, Equatable { public var href: String public var renderVersion: Int public var domVersion: Int public var markers: [RDEPUBCFIMarker] public var textAssertions: [String: RDEPUBCFITextAssertion] public var pathRanges: [RDEPUBCFIPathRange] public var recoveryMetadata: RDEPUBCFIRecoveryMetadata /// Lazy O(1) lookup index for markers by cfiPath private var markerByPath: [RDEPUBCFIPath: Int]? public init( href: String, renderVersion: Int = 1, domVersion: Int = 1, markers: [RDEPUBCFIMarker] = [], textAssertions: [String: RDEPUBCFITextAssertion] = [:], pathRanges: [RDEPUBCFIPathRange] = [], recoveryMetadata: RDEPUBCFIRecoveryMetadata = .empty ) { self.href = href self.renderVersion = renderVersion self.domVersion = domVersion self.markers = markers self.textAssertions = textAssertions self.pathRanges = pathRanges self.recoveryMetadata = recoveryMetadata } public func marker(matching path: RDEPUBCFIPath) -> RDEPUBCFIMarker? { if let index = markerByPath?[path] { return markers[index] } return markers.first { $0.cfiPath == path } } /// Build or rebuild the marker lookup index. Call after mutating `markers`. public mutating func rebuildMarkerIndex() { markerByPath = Dictionary( markers.enumerated().map { ($0.element.cfiPath, $0.offset) }, uniquingKeysWith: { _, last in last } ) } enum CodingKeys: String, CodingKey { case href case renderVersion case domVersion case markers case textAssertions case pathRanges case recoveryMetadata } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) href = try container.decode(String.self, forKey: .href) renderVersion = try container.decodeIfPresent(Int.self, forKey: .renderVersion) ?? 1 domVersion = try container.decodeIfPresent(Int.self, forKey: .domVersion) ?? 1 markers = try container.decodeIfPresent([RDEPUBCFIMarker].self, forKey: .markers) ?? [] textAssertions = try container.decodeIfPresent([String: RDEPUBCFITextAssertion].self, forKey: .textAssertions) ?? [:] pathRanges = try container.decodeIfPresent([RDEPUBCFIPathRange].self, forKey: .pathRanges) ?? [] recoveryMetadata = try container.decodeIfPresent(RDEPUBCFIRecoveryMetadata.self, forKey: .recoveryMetadata) ?? .empty markerByPath = nil } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(href, forKey: .href) try container.encode(renderVersion, forKey: .renderVersion) try container.encode(domVersion, forKey: .domVersion) try container.encode(markers, forKey: .markers) try container.encode(textAssertions, forKey: .textAssertions) try container.encode(pathRanges, forKey: .pathRanges) try container.encode(recoveryMetadata, forKey: .recoveryMetadata) } public static func == (lhs: RDEPUBCFIMap, rhs: RDEPUBCFIMap) -> Bool { lhs.href == rhs.href && lhs.renderVersion == rhs.renderVersion && lhs.domVersion == rhs.domVersion && lhs.markers == rhs.markers && lhs.textAssertions == rhs.textAssertions && lhs.pathRanges == rhs.pathRanges && lhs.recoveryMetadata == rhs.recoveryMetadata } } public struct RDEPUBCFIMarker: Codable, Equatable { public var cfiPath: RDEPUBCFIPath public var chapterOffset: Int? public var fragmentID: String? public var textNodeLength: Int? public var textNodeChecksum: UInt64? public var normalizedTextPreview: String? public var domSiblingSignature: String? public init( cfiPath: RDEPUBCFIPath, chapterOffset: Int? = nil, fragmentID: String? = nil, textNodeLength: Int? = nil, textNodeChecksum: UInt64? = nil, normalizedTextPreview: String? = nil, domSiblingSignature: String? = nil ) { self.cfiPath = cfiPath self.chapterOffset = chapterOffset self.fragmentID = fragmentID self.textNodeLength = textNodeLength self.textNodeChecksum = textNodeChecksum self.normalizedTextPreview = normalizedTextPreview self.domSiblingSignature = domSiblingSignature } } public struct RDEPUBCFIPathRange: Codable, Equatable { public var cfiPath: RDEPUBCFIPath public var startOffset: Int public var endOffset: Int public var textNodeLength: Int public init( cfiPath: RDEPUBCFIPath, startOffset: Int, endOffset: Int, textNodeLength: Int ) { self.cfiPath = cfiPath self.startOffset = startOffset self.endOffset = endOffset self.textNodeLength = textNodeLength } } public struct RDEPUBCFIRecoveryMetadata: Codable, Equatable { public var domFingerprint: String public var normalizedTextChecksum: String public var tokenIndex: [RDEPUBCFITokenAnchor] public var fragmentPathMap: [String: RDEPUBCFIPath] public init( domFingerprint: String, normalizedTextChecksum: String, tokenIndex: [RDEPUBCFITokenAnchor] = [], fragmentPathMap: [String: RDEPUBCFIPath] = [:] ) { self.domFingerprint = domFingerprint self.normalizedTextChecksum = normalizedTextChecksum self.tokenIndex = tokenIndex self.fragmentPathMap = fragmentPathMap } public static let empty = RDEPUBCFIRecoveryMetadata( domFingerprint: "", normalizedTextChecksum: "", tokenIndex: [], fragmentPathMap: [:] ) } public struct RDEPUBCFITokenAnchor: Codable, Equatable { public var token: String public var occurrence: Int public var chapterOffset: Int public var cfiPath: RDEPUBCFIPath public init( token: String, occurrence: Int, chapterOffset: Int, cfiPath: RDEPUBCFIPath ) { self.token = token self.occurrence = occurrence self.chapterOffset = chapterOffset self.cfiPath = cfiPath } }