ReadViewSDK/Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift
shenlei f50495ad91 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>
2026-06-18 13:25:17 +08:00

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)
}
}