Merge branch 'feat-koysnc-xpath' of https://github.com/jpirnay/crosspoint-reader into mybuild

This commit is contained in:
jpirnay
2026-03-20 09:44:21 +01:00
2 changed files with 57 additions and 4 deletions
+56 -4
View File
@@ -45,7 +45,7 @@ struct ParserState {
std::vector<std::unordered_map<std::string, int>> siblingCounters;
std::vector<XPathAnchor> anchors;
std::string baseXPath() const { return "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body"; }
std::string baseXPath() const { return "/body/docfragment[" + std::to_string(spineIndex + 1) + "]/body"; }
// Canonicalize incoming KOReader XPath before matching:
// - remove all whitespace
@@ -135,7 +135,7 @@ struct ParserState {
const std::string& anchorPath = ignoreIndices ? anchor.xpathNoIndex : anchor.xpath;
if (anchorPath == probe) {
const int depth = pathDepth(anchorPath);
if (!found || depth > bestDepth || (depth == bestDepth && anchor.textOffset < bestOffset)) {
if (!found || depth > bestDepth || (depth == bestDepth && anchor.textOffset > bestOffset)) {
found = true;
bestDepth = depth;
bestOffset = anchor.textOffset;
@@ -463,11 +463,63 @@ static std::optional<ParserState> parseSpineItem(const std::shared_ptr<Epub>& ep
return state;
}
// Keep a single parsed spine in memory to avoid repeated decompress+parse work
// in the KO sync flow.
//
// Why this helps here:
// - Sync result screen calls both mapping directions (remote->local for compare,
// local->KOReader for display).
// - If the user chooses Upload, local->KOReader mapping is performed again.
// - Without caching, the same chapter can be decompressed and reparsed multiple
// times during one activity session.
//
// Why the cache is exactly one entry:
// - We only need to accelerate immediate repeat lookups for the current chapter.
// - A single-entry cache bounds RAM use and avoids long-lived heap growth on
// ESP32-C3.
static const ParserState* getParsedStateCached(const std::shared_ptr<Epub>& epub, const int spineIndex) {
static const Epub* cachedEpub = nullptr;
static int cachedSpineIndex = -1;
static std::string cachedHref;
static std::optional<ParserState> cachedState;
if (!epub || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) {
return nullptr;
}
const auto spineItem = epub->getSpineItem(spineIndex);
if (spineItem.href.empty()) {
return nullptr;
}
const bool cacheHit = cachedState.has_value() && cachedEpub == epub.get() && cachedSpineIndex == spineIndex &&
cachedHref == spineItem.href;
if (cacheHit) {
LOG_DBG("KOX", "Cache hit: spine=%d anchors=%zu textBytes=%zu", spineIndex, cachedState->anchors.size(),
cachedState->totalTextBytes);
return &cachedState.value();
}
auto parsed = parseSpineItem(epub, spineIndex);
if (!parsed) {
return nullptr;
}
cachedEpub = epub.get();
cachedSpineIndex = spineIndex;
cachedHref = spineItem.href;
cachedState = std::move(parsed);
LOG_DBG("KOX", "Cache store: spine=%d anchors=%zu textBytes=%zu", spineIndex, cachedState->anchors.size(),
cachedState->totalTextBytes);
return &cachedState.value();
}
} // namespace
std::string ChapterXPathIndexer::findXPathForProgress(const std::shared_ptr<Epub>& epub, const int spineIndex,
const float intraSpineProgress) {
const auto state = parseSpineItem(epub, spineIndex);
const auto* state = getParsedStateCached(epub, spineIndex);
if (!state) {
return "";
}
@@ -488,7 +540,7 @@ bool ChapterXPathIndexer::findProgressForXPath(const std::shared_ptr<Epub>& epub
return false;
}
const auto state = parseSpineItem(epub, spineIndex);
const auto* state = getParsedStateCached(epub, spineIndex);
if (!state) {
return false;
}
@@ -203,6 +203,7 @@ void KOReaderSyncActivity::onEnter() {
// Check if already connected (e.g. from settings page auth)
if (WiFi.status() == WL_CONNECTED) {
LOG_DBG("KOSync", "Already connected to WiFi");
onWifiSelectionComplete(true);
return;
}