ReadViewSDK/Doc/WXRead/decompiled/WREpubPositionConverter.m
2026-05-21 19:40:51 +08:00

327 lines
10 KiB
Objective-C

//
// WREpubPositionConverter.m
// WeRead (微信读书)
// Reverse-engineered implementation reconstruction
//
// Detailed pseudo-code based on binary analysis, ivar types, known methods,
// and contextual knowledge of EPUB position/bookmark handling.
//
#import "WREpubPositionConverter.h"
// ---------------------------------------------------------------------------
// WRPositionPair
// ---------------------------------------------------------------------------
@implementation WRPositionPair
- (instancetype)initWithRow:(NSInteger)row column:(NSInteger)column
{
self = [super init];
if (self) {
_row = row;
_column = column;
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<WRPositionPair row=%ld col=%ld>",
(long)_row, (long)_column];
}
@end
// ---------------------------------------------------------------------------
// WREpubPositionConverter
// ---------------------------------------------------------------------------
@implementation WREpubPositionConverter
{
// Core data from binary analysis
NSArray *_filePaths;
NSArray *_attributedStrings;
NSMutableArray *_fileLengths; // character count per file
NSMutableArray *_cumulativeOffsets; // prefix sum of character counts
NSMutableDictionary *_indexCache; // cache: key -> index
NSMutableDictionary *_stringRangeCache; // cache: key -> NSRange
// Additional state inferred from context
NSInteger _baseOffset; // offset for non-chapter content
BOOL _containsIntroFlyleaf; // includes cover/flyleaf page
BOOL _indicesBuilt; // whether initIndices has been called
}
#pragma mark - Lifecycle
- (instancetype)initWithFilePaths:(NSArray<NSString *> *)filePaths
attributedStrings:(NSArray<NSAttributedString *> *)attributedStrings
offset:(NSInteger)offset
isContainIntroFlyleaf:(BOOL)isContainIntroFlyleaf
{
self = [super init];
if (self) {
_filePaths = [filePaths copy];
_attributedStrings = [attributedStrings copy];
_baseOffset = offset;
_containsIntroFlyleaf = isContainIntroFlyleaf;
_fileLengths = [NSMutableArray arrayWithCapacity:filePaths.count];
_cumulativeOffsets = [NSMutableArray arrayWithCapacity:filePaths.count];
_indexCache = [NSMutableDictionary dictionary];
_stringRangeCache = [NSMutableDictionary dictionary];
_indicesBuilt = NO;
}
return self;
}
#pragma mark - Index Building
- (void)initIndices
{
if (_indicesBuilt) return;
[_fileLengths removeAllObjects];
[_cumulativeOffsets removeAllObjects];
NSInteger cumulative = _baseOffset;
for (NSUInteger i = 0; i < _attributedStrings.count; i++) {
NSAttributedString *attrStr = _attributedStrings[i];
NSInteger length = (NSInteger)attrStr.string.length;
[_fileLengths addObject:@(length)];
// Cumulative offset = sum of lengths of all previous files + baseOffset
[_cumulativeOffsets addObject:@(cumulative)];
cumulative += length;
}
_indicesBuilt = YES;
}
#pragma mark - Position Conversion: Row/Column -> String Index
- (void)indicesInFile:(NSString *)filePath
forRowColumnPairs:(NSArray<WRPositionPair *> *)rowColumnPairs
stringIndices:(NSArray *_Nullable *_Nullable)stringIndices
string:(NSString *)string
fileIndexOffset:(NSInteger)fileIndexOffset
stringIndexOffset:(NSInteger)stringIndexOffset
{
// 1. Find the file index for the given path
NSInteger fileIndex = [_filePaths indexOfObject:filePath];
if (fileIndex == NSNotFound) {
// Try matching by last path component (filename only)
NSString *filename = [filePath lastPathComponent];
for (NSUInteger i = 0; i < _filePaths.count; i++) {
if ([[_filePaths[i] lastPathComponent] isEqualToString:filename]) {
fileIndex = (NSInteger)i;
break;
}
}
}
if (fileIndex == NSNotFound) {
if (stringIndices) *stringIndices = @[];
return;
}
fileIndex += fileIndexOffset;
// 2. Build a line-offset table from the string
//
// We need to map (row, column) -> character offset within the string.
// A "row" is a line number; "column" is the character position in that line.
//
// Strategy: scan the string and record the starting offset of each line.
NSMutableArray<NSNumber *> *lineStarts = [NSMutableArray array];
[lineStarts addObject:@(0)]; // line 0 starts at offset 0
NSUInteger len = string.length;
for (NSUInteger i = 0; i < len; i++) {
unichar c = [string characterAtIndex:i];
if (c == '\n' || c == '\r') {
// Handle \r\n as a single line ending
if (c == '\r' && i + 1 < len && [string characterAtIndex:i + 1] == '\n') {
i++; // skip the \n
}
if (i + 1 < len) {
[lineStarts addObject:@(i + 1)];
}
}
}
// 3. Convert each (row, column) pair to a string index
NSMutableArray<NSNumber *> *results =
[NSMutableArray arrayWithCapacity:rowColumnPairs.count];
for (WRPositionPair *pair in rowColumnPairs) {
NSInteger row = pair.row;
NSInteger column = pair.column;
if (row < 0 || row >= (NSInteger)lineStarts.count) {
[results addObject:@(NSNotFound)];
continue;
}
NSInteger lineStart = [lineStarts[row] integerValue];
// Find the end of this line
NSInteger lineEnd;
if (row + 1 < (NSInteger)lineStarts.count) {
lineEnd = [lineStarts[row + 1] integerValue] - 1;
} else {
lineEnd = (NSInteger)len;
}
NSInteger lineLength = lineEnd - lineStart;
if (column > lineLength) {
column = lineLength; // clamp to end of line
}
NSInteger stringIndex = lineStart + column + stringIndexOffset;
// Also add the cumulative offset for this file to get the global position
if (fileIndex >= 0 && fileIndex < (NSInteger)_cumulativeOffsets.count) {
stringIndex += [_cumulativeOffsets[fileIndex] integerValue];
}
[results addObject:@(stringIndex)];
}
if (stringIndices) {
*stringIndices = [results copy];
}
}
#pragma mark - Position Conversion: File Range -> String Range
- (NSRange)stringRangeFromFileRange:(NSDictionary *)fileRange
{
// Expected keys in fileRange:
// @"fileIndex" -> NSNumber (NSInteger)
// @"start" -> NSNumber (NSInteger) local offset within the file
// @"end" -> NSNumber (NSInteger) local offset within the file
NSNumber *fileIndexNum = fileRange[@"fileIndex"];
NSNumber *startNum = fileRange[@"start"];
NSNumber *endNum = fileRange[@"end"];
if (!fileIndexNum || !startNum || !endNum) {
return NSMakeRange(NSNotFound, 0);
}
NSInteger fileIndex = [fileIndexNum integerValue];
NSInteger localStart = [startNum integerValue];
NSInteger localEnd = [endNum integerValue];
// Check cache
NSString *cacheKey =
[NSString stringWithFormat:@"%ld:%ld:%ld", (long)fileIndex,
(long)localStart, (long)localEnd];
NSNumber *cached = _stringRangeCache[cacheKey];
if (cached) {
return NSRangeFromString(cached.stringValue);
}
// Validate file index
if (!_indicesBuilt) [self initIndices];
if (fileIndex < 0 || fileIndex >= (NSInteger)_cumulativeOffsets.count) {
return NSMakeRange(NSNotFound, 0);
}
// Get the cumulative offset for this file
NSInteger cumulativeOffset = [_cumulativeOffsets[fileIndex] integerValue];
// File-local length
NSInteger fileLength = [_fileLengths[fileIndex] integerValue];
// Clamp to file bounds
if (localStart < 0) localStart = 0;
if (localEnd > fileLength) localEnd = fileLength;
if (localStart >= localEnd) {
return NSMakeRange(NSNotFound, 0);
}
NSInteger globalStart = cumulativeOffset + localStart;
NSInteger length = localEnd - localStart;
NSRange result = NSMakeRange(globalStart, length);
// Cache the result
_stringRangeCache[cacheKey] = NSStringFromRange(result);
return result;
}
#pragma mark - Utility
- (NSInteger)totalCharacterCount
{
if (!_indicesBuilt) [self initIndices];
if (_cumulativeOffsets.count == 0) return _baseOffset;
NSInteger lastCumulative =
[_cumulativeOffsets.lastObject integerValue];
NSInteger lastLength = [_fileLengths.lastObject integerValue];
return lastCumulative + lastLength;
}
- (NSInteger)fileIndexForCharacterPosition:(NSInteger)position
{
if (!_indicesBuilt) [self initIndices];
// Binary search through cumulative offsets
NSInteger lo = 0;
NSInteger hi = (NSInteger)_cumulativeOffsets.count - 1;
NSInteger result = -1;
while (lo <= hi) {
NSInteger mid = lo + (hi - lo) / 2;
NSInteger offset = [_cumulativeOffsets[mid] integerValue];
if (offset <= position) {
result = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
// Verify the position is within this file's range
if (result >= 0) {
NSInteger fileLen = [_fileLengths[result] integerValue];
NSInteger cumOff = [_cumulativeOffsets[result] integerValue];
if (position >= cumOff + fileLen) {
result = -1; // beyond end of last file
}
}
return result;
}
- (NSInteger)localOffsetInFileAtIndex:(NSInteger)fileIndex
forGlobalPosition:(NSInteger)position
{
if (!_indicesBuilt) [self initIndices];
if (fileIndex < 0 || fileIndex >= (NSInteger)_cumulativeOffsets.count) {
return NSNotFound;
}
NSInteger cumulativeOffset = [_cumulativeOffsets[fileIndex] integerValue];
NSInteger localOffset = position - cumulativeOffset;
NSInteger fileLen = [_fileLengths[fileIndex] integerValue];
if (localOffset < 0 || localOffset >= fileLen) {
return NSNotFound;
}
return localOffset;
}
@end