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:
jpirnay
2026-03-22 12:29:26 +01:00
co-authored by Claude Opus 4.6
parent dccb82642d
commit b625b8bd26
17 changed files with 354 additions and 43 deletions
+40
View File
@@ -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;
}