Add paragraph index LUT for accurate KOReader position sync
Store per-page paragraph indices in section cache to enable precise XPath-to-page and page-to-XPath mapping without reparsing XHTML. Forward path (upload): generates XPath directly from paragraph LUT instead of byte-offset estimation, eliminating drift in chapters with non-uniform content density. Reverse path (download): resolves incoming KOReader XPath p[N] to the exact page via paragraph LUT lookup. Paragraph counter counts all <p> elements including display:none to match ChapterXPathIndexer and crengine's standard XPath counting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5b710e960f
commit
fd70b3a231
@@ -599,3 +599,43 @@ bool ChapterXPathIndexer::tryExtractSpineIndexFromXPath(const std::string& xpath
|
||||
outSpineIndex = static_cast<int>(parsed) - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChapterXPathIndexer::tryExtractParagraphIndexFromXPath(const std::string& xpath, uint16_t& outParagraphIndex) {
|
||||
outParagraphIndex = 0;
|
||||
if (xpath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string normalized = normalizeXPath(xpath);
|
||||
|
||||
// Find /p[ after the second /body/ (the inner body inside DocFragment)
|
||||
const std::string bodyKey = "/body";
|
||||
size_t secondBody = normalized.find(bodyKey);
|
||||
if (secondBody != std::string::npos) {
|
||||
secondBody = normalized.find(bodyKey, secondBody + bodyKey.size());
|
||||
}
|
||||
|
||||
const std::string pKey = "/p[";
|
||||
const size_t pos = normalized.find(pKey, secondBody != std::string::npos ? secondBody : 0);
|
||||
if (pos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t start = pos + pKey.size();
|
||||
size_t end = start;
|
||||
while (end < normalized.size() && std::isdigit(static_cast<unsigned char>(normalized[end]))) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end == start || end >= normalized.size() || normalized[end] != ']') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const long parsed = std::strtol(normalized.substr(start, end - start).c_str(), nullptr, 10);
|
||||
if (parsed < 1 || parsed > UINT16_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outParagraphIndex = static_cast<uint16_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,4 +64,16 @@ class ChapterXPathIndexer {
|
||||
* (converted to 0-based outSpineIndex); false otherwise
|
||||
*/
|
||||
static bool tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex);
|
||||
|
||||
/**
|
||||
* Extract the paragraph index from a KOReader XPath.
|
||||
* Looks for the first /p[N] segment after /body/ and returns N (1-based).
|
||||
*
|
||||
* Example: "/body/DocFragment[7]/body/p[685]/text().96" → outParagraphIndex = 685
|
||||
*
|
||||
* @param xpath KOReader XPath
|
||||
* @param outParagraphIndex 1-based paragraph index
|
||||
* @return true if a /p[N] segment was found
|
||||
*/
|
||||
static bool tryExtractParagraphIndexFromXPath(const std::string& xpath, uint16_t& outParagraphIndex);
|
||||
};
|
||||
|
||||
@@ -19,12 +19,17 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
|
||||
// Calculate overall book progress (0.0-1.0)
|
||||
result.percentage = epub->calculateProgress(pos.spineIndex, intraSpineProgress);
|
||||
|
||||
// Generate the best available XPath for the current chapter position.
|
||||
// Prefer element-level XPaths from a lightweight XHTML reparse; fall back
|
||||
// to a synthetic chapter-level path if parsing fails.
|
||||
result.xpath = ChapterXPathIndexer::findXPathForProgress(epub, pos.spineIndex, intraSpineProgress);
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = generateXPath(pos.spineIndex);
|
||||
// Generate XPath for the current position.
|
||||
// Prefer paragraph index from the section cache LUT (exact element mapping) over
|
||||
// byte-offset estimation (which can drift in chapters with non-uniform content density).
|
||||
if (pos.hasParagraphIndex && pos.paragraphIndex > 0) {
|
||||
result.xpath = "/body/DocFragment[" + std::to_string(pos.spineIndex + 1) + "]/body/p[" +
|
||||
std::to_string(pos.paragraphIndex) + "]";
|
||||
} else {
|
||||
result.xpath = ChapterXPathIndexer::findXPathForProgress(epub, pos.spineIndex, intraSpineProgress);
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = generateXPath(pos.spineIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Get chapter info for logging
|
||||
@@ -64,6 +69,12 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
||||
resolvedIntraSpineProgress = intraFromXPath;
|
||||
usedXPathMapping = true;
|
||||
}
|
||||
// Extract paragraph index from XPath for direct page lookup via section cache
|
||||
uint16_t pIndex = 0;
|
||||
if (ChapterXPathIndexer::tryExtractParagraphIndexFromXPath(koPos.xpath, pIndex)) {
|
||||
result.paragraphIndex = pIndex;
|
||||
result.hasParagraphIndex = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usedXPathMapping) {
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
* CrossPoint position representation.
|
||||
*/
|
||||
struct CrossPointPosition {
|
||||
int spineIndex; // Current spine item (chapter) index
|
||||
int pageNumber; // Current page within the spine item
|
||||
int totalPages; // Total pages in the current spine item
|
||||
int spineIndex; // Current spine item (chapter) index
|
||||
int pageNumber; // Current page within the spine item (estimated if no paragraph LUT)
|
||||
int totalPages; // Total pages in the current spine item
|
||||
uint16_t paragraphIndex = 0; // 1-based <p> index from XPath (0 if unavailable)
|
||||
bool hasParagraphIndex = false; // True when paragraphIndex was resolved from XPath
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user