fix: CFI module — 11 issues from code review
Phase 1 (correctness): - #1: rawValue changed from stored to computed property, eliminating stale cache risk - #2: tokenSamples/textMarker unified to UTF-16 offsets, fixing emoji/CJK positioning - #3: makeOffsetCFI now accepts optional contentPath parameter Phase 2 (robustness): - #4: Resolver uses fixed index access instead of last(where:) for manifest/spine steps - #5: HTML comments and CDATA stripped before regex matching - #6: Text assertion parser handles backslash escapes (\[ \] \) - #7: Token matching uses prefix/suffix with length ratio constraints - #8: makeOffsetRangeCFI validates startOffset <= endOffset Phase 3 (code quality): - #9: Shared internal nilIfEmpty extension in RDEPUBCFIUtilities.swift - #10: RDEPUBCFIMap has markerByPath index dictionary for O(1) lookup - #11: parseRange unified to use try (not try?) for parent parsing Review fixes: - Documented init(rawValue:) parameter is ignored (computed property) - Fixed escape unescaping to handle \\ → \ correctly - Lowered token minLength threshold from 4 to 2 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user