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>
88 lines
2.9 KiB
Swift
88 lines
2.9 KiB
Swift
import Foundation
|
|
|
|
public enum RDEPUBCFIGenerator {
|
|
|
|
public static func makeOffsetCFI(
|
|
href: String,
|
|
fileIndex: Int,
|
|
chapterOffset: Int,
|
|
fragmentID: String? = nil,
|
|
sideBias: RDEPUBCFISideBias? = nil,
|
|
textAssertion: RDEPUBCFITextAssertion? = nil,
|
|
contentPath: RDEPUBCFIPath? = nil
|
|
) -> RDEPUBCFI {
|
|
let packagePath = RDEPUBCFIPath(steps: [
|
|
RDEPUBCFIStep(index: 6),
|
|
RDEPUBCFIStep(index: max((fileIndex + 1) * 2, 2), idAssertion: href)
|
|
])
|
|
let resolvedContentPath = contentPath ?? RDEPUBCFIPath(steps: [
|
|
RDEPUBCFIStep(index: 4),
|
|
RDEPUBCFIStep(index: 2, idAssertion: fragmentID)
|
|
])
|
|
return RDEPUBCFI(
|
|
packagePath: packagePath,
|
|
contentPath: resolvedContentPath,
|
|
characterOffset: max(chapterOffset, 0),
|
|
sideBias: sideBias,
|
|
textAssertion: textAssertion
|
|
)
|
|
}
|
|
|
|
public static func makeCFI(
|
|
href: String,
|
|
fileIndex: Int,
|
|
contentPath: RDEPUBCFIPath,
|
|
characterOffset: Int,
|
|
sideBias: RDEPUBCFISideBias? = nil,
|
|
textAssertion: RDEPUBCFITextAssertion? = nil
|
|
) -> RDEPUBCFI {
|
|
let packagePath = RDEPUBCFIPath(steps: [
|
|
RDEPUBCFIStep(index: 6),
|
|
RDEPUBCFIStep(index: max((fileIndex + 1) * 2, 2), idAssertion: href)
|
|
])
|
|
return RDEPUBCFI(
|
|
packagePath: packagePath,
|
|
contentPath: contentPath,
|
|
characterOffset: max(characterOffset, 0),
|
|
sideBias: sideBias,
|
|
textAssertion: textAssertion
|
|
)
|
|
}
|
|
|
|
public static func makeOffsetRangeCFI(
|
|
href: String,
|
|
fileIndex: Int,
|
|
startOffset: Int,
|
|
endOffset: Int,
|
|
fragmentID: String? = nil,
|
|
startTextAssertion: RDEPUBCFITextAssertion? = nil,
|
|
endTextAssertion: RDEPUBCFITextAssertion? = nil,
|
|
contentPath: RDEPUBCFIPath? = nil
|
|
) -> RDEPUBCFIRange {
|
|
precondition(startOffset <= endOffset, "startOffset (\(startOffset)) must be <= endOffset (\(endOffset))")
|
|
let start = makeOffsetCFI(
|
|
href: href,
|
|
fileIndex: fileIndex,
|
|
chapterOffset: startOffset,
|
|
fragmentID: fragmentID,
|
|
sideBias: .before,
|
|
textAssertion: startTextAssertion,
|
|
contentPath: contentPath
|
|
)
|
|
let end = makeOffsetCFI(
|
|
href: href,
|
|
fileIndex: fileIndex,
|
|
chapterOffset: endOffset,
|
|
fragmentID: fragmentID,
|
|
sideBias: .after,
|
|
textAssertion: endTextAssertion,
|
|
contentPath: contentPath
|
|
)
|
|
let parent = RDEPUBCFI(
|
|
packagePath: start.packagePath.commonPrefix(with: end.packagePath),
|
|
contentPath: start.contentPath.commonPrefix(with: end.contentPath)
|
|
)
|
|
return RDEPUBCFIRange(parent: parent, start: start, end: end)
|
|
}
|
|
}
|