import Foundation public enum RDEPUBCFISerializer { public static func serialize(_ cfi: RDEPUBCFI) -> String { var body = serializePath(cfi.packagePath) let content = serializePath(cfi.contentPath) if !content.isEmpty || cfi.characterOffset != nil { body += "!" + content } if let characterOffset = cfi.characterOffset { body += ":\(max(characterOffset, 0))" } if let sideBias = cfi.sideBias { body += ";s=\(sideBias.rawValue)" } if let assertion = cfi.textAssertion { body += "[\(assertion.prefix ?? ""),\(assertion.exact ?? ""),\(assertion.suffix ?? "")]" } return "epubcfi(\(body))" } public static func serializeRange(_ range: RDEPUBCFIRange) -> String { let canonical = canonicalRangeComponents(for: range) let parentBody = serializeRangeParent(canonical.parent) let startBody = serializeRangeTerminal( canonical.start, relativeToPackagePath: canonical.parent.packagePath, contentPrefix: canonical.parent.contentPath ) let endBody = serializeRangeTerminal( canonical.end, relativeToPackagePath: canonical.parent.packagePath, contentPrefix: canonical.parent.contentPath ) return "epubcfi(\(parentBody),\(startBody),\(endBody))" } public static func serializePath(_ path: RDEPUBCFIPath) -> String { guard !path.steps.isEmpty else { return "" } return path.steps.map { step in if let idAssertion = step.idAssertion { return "/\(step.index)[\(idAssertion)]" } return "/\(step.index)" }.joined() } private static func canonicalRangeComponents(for range: RDEPUBCFIRange) -> ( parent: RDEPUBCFI, start: RDEPUBCFI, end: RDEPUBCFI ) { if let parent = range.parent { return (parent, range.start, range.end) } let sharedPackage = range.start.packagePath.commonPrefix(with: range.end.packagePath) let sharedContent = range.start.contentPath.commonPrefix(with: range.end.contentPath) let parent = RDEPUBCFI( packagePath: sharedPackage, contentPath: sharedContent, characterOffset: nil, sideBias: nil, textAssertion: nil ) return (parent, range.start, range.end) } private static func serializeRangeParent(_ parent: RDEPUBCFI) -> String { var body = serializePath(parent.packagePath) let content = serializePath(parent.contentPath) if !content.isEmpty { body += "!" + content } return body } private static func serializeRangeTerminal( _ cfi: RDEPUBCFI, relativeToPackagePath parentPackagePath: RDEPUBCFIPath, contentPrefix: RDEPUBCFIPath ) -> String { let relativePackage = cfi.packagePath.droppingPrefix(parentPackagePath) let relativeContent = cfi.contentPath.droppingPrefix(contentPrefix) var body = serializePath(relativePackage) let content = serializePath(relativeContent) if !content.isEmpty || cfi.characterOffset != nil || !relativePackage.steps.isEmpty { body += content } if let characterOffset = cfi.characterOffset { body += ":\(max(characterOffset, 0))" } if let sideBias = cfi.sideBias { body += ";s=\(sideBias.rawValue)" } if let assertion = cfi.textAssertion { body += "[\(assertion.prefix ?? ""),\(assertion.exact ?? ""),\(assertion.suffix ?? "")]" } return body } }