diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index be8fe1f2..07f24bc2 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -12,7 +12,7 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 25; +constexpr uint8_t SECTION_FILE_VERSION = 26; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(int) + // fontId sizeof(float) + // lineCompression @@ -29,8 +29,11 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(uint32_t) + // anchor map offset sizeof(uint32_t); // paragraph LUT offset -// On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex. -constexpr uint32_t PARAGRAPH_LUT_ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t); +// On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex + u16 listItemIndex. +// listItemIndex is the running
- and
-anchored XPaths.
+ // Returns nullopt if the LUT is not available or the index is out of range.
+ std::optional element on or before the page.
// Returns nullopt if the paragraph LUT is not available (old cache format).
diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
index 10db5834..f6c3f2cd 100644
--- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
+++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
@@ -250,7 +250,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
// Callers must ensure currentPage is non-null and carries content; the helper resets
// currentPage to a fresh Page and zeroes currentPageNextY so the caller can keep building.
void ChapterHtmlSlimParser::emitPage(uint32_t xhtmlByteOffset) {
- paragraphLutPerPage.push_back({xhtmlByteOffset, xpathParagraphIndex});
+ paragraphLutPerPage.push_back({xhtmlByteOffset, xpathParagraphIndex, xpathListItemIndex});
completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset(new Page());
@@ -814,6 +814,13 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
}
}
+ // sibling index (1-based)
- int xpathBodyDepth = -1; // depth of the -anchored XPaths use xpathParagraphIndex.
+ uint16_t xpathListItemIndex = 0;
+ int xpathBodyDepth = -1; // depth of the index at page completion
+ uint16_t listItemIndex; // running -anchored targets we use the existing paragraph index path.
+ bool targetEndsInLi = false;
+
MatchTier bestTier = MatchTier::NONE;
int bestDepth = -1;
size_t bestOffset = 0;
bool bestExact = false;
const char* bestTierName = nullptr;
+ // Snapshot of liCount at the moment the best match was captured. The reverse
+ // mapper surfaces this so the runtime can call Section::getPageForListItemIndex()
+ // and snap a list-item XPath to the precise page on download.
+ int bestLiIndex = 0;
ReverseState(const int spineIndex, const std::string& xpath) : spineIndex(spineIndex) {
// Parse optional text-node suffix before normalizing for element matching.
@@ -91,11 +102,27 @@ struct ReverseState : StackState {
}
targetNorm = normalizeXPath(xpath);
targetNoIndex = removeIndices(targetNorm);
+
+ // Detect /li[N] as the deepest segment of targetNorm. normalizeXPath has already
+ // stripped any /text() and /text()[N].M suffix and lower-cased the tag names, so
+ // a simple tail check is enough.
+ const size_t lastSlash = targetNorm.rfind('/');
+ if (lastSlash != std::string::npos) {
+ const std::string tail = targetNorm.substr(lastSlash + 1);
+ if (tail.size() >= 2 && tail.compare(0, 2, "li") == 0 && (tail.size() == 2 || tail[2] == '[')) {
+ targetEndsInLi = true;
+ }
+ }
}
void onStartElement(const XML_Char* rawName) {
inParentTextNode = false;
pushElement(rawName);
+ // Increment after pushElement so stack.back().tag is already lowercased and
+ // matches the parser-side counter, which also fires on startElement.
+ if (!stack.empty() && stack.back().tag == "li") {
+ liCount++;
+ }
}
void onEndElement() {
@@ -134,6 +161,7 @@ struct ReverseState : StackState {
bestOffset = pos;
bestExact = true;
bestTierName = "text-node-exact";
+ bestLiIndex = liCount;
}
}
codepointsInCurrentTextNode += codepoints;
@@ -191,6 +219,7 @@ struct ReverseState : StackState {
bestOffset = totalTextBytes;
bestExact = isExact;
bestTierName = tierName;
+ bestLiIndex = liCount;
}
}
};
@@ -198,9 +227,12 @@ struct ReverseState : StackState {
} // namespace
bool findProgressForXPathInternal(const std::shared_ptr index (0 if unavailable)
bool hasParagraphIndex = false; // True when paragraphIndex is valid
+ uint16_t listItemIndex = 0; // 1-based running index from XPath
bool hasParagraphIndex = false; // true when paragraphIndex is available
+ uint16_t listItemIndex = 0; // running LUT, so try the
+ // li LUT first when set; fall back to the paragraph LUT (which handles direct
+ // children of LUT.
+ bool pendingListItemLookup = false;
+ uint16_t pendingListItemIndex = 0;
bool pendingScreenshot = false;
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
ReaderUtils::InputDrainGuard inputDrainGuard;
diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp
index 2d86d6b5..ba9d60b0 100644
--- a/src/activities/reader/KOReaderSyncActivity.cpp
+++ b/src/activities/reader/KOReaderSyncActivity.cpp
@@ -220,6 +220,8 @@ void KOReaderSyncActivity::performFetchAndCompare() {
remotePosition.totalPages = 0;
remotePosition.paragraphIndex = 0;
remotePosition.hasParagraphIndex = false;
+ remotePosition.listItemIndex = 0;
+ remotePosition.hasListItemIndex = false;
remoteChapterLabel.clear();
if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE || syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
@@ -249,6 +251,8 @@ void KOReaderSyncActivity::performFetchAndCompare() {
sync.resultPage = remotePosition.pageNumber;
sync.resultParagraphIndex = remotePosition.paragraphIndex;
sync.resultHasParagraphIndex = remotePosition.hasParagraphIndex;
+ sync.resultListItemIndex = remotePosition.listItemIndex;
+ sync.resultHasListItemIndex = remotePosition.hasListItemIndex;
APP_STATE.saveToFile();
if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
@@ -503,6 +507,8 @@ void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome,
sync.resultPage = appliedResult->page;
sync.resultParagraphIndex = appliedResult->paragraphIndex;
sync.resultHasParagraphIndex = appliedResult->hasParagraphIndex;
+ sync.resultListItemIndex = appliedResult->listItemIndex;
+ sync.resultHasListItemIndex = appliedResult->hasListItemIndex;
} else if (outcome != KOReaderSyncOutcomeState::APPLIED_REMOTE) {
// Only zero the result fields when not resuming an already-applied remote
// position. The PULL_REMOTE path pre-saves the mapped result into APP_STATE
@@ -511,6 +517,8 @@ void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome,
sync.resultPage = 0;
sync.resultParagraphIndex = 0;
sync.resultHasParagraphIndex = false;
+ sync.resultListItemIndex = 0;
+ sync.resultHasListItemIndex = false;
}
// Honor exit-to-home flag set by reader-close auto-sync — bouncing back into the reader
// the user just left would be jarring. The session state is consumed and cleared by the
@@ -792,8 +800,9 @@ void KOReaderSyncActivity::loop() {
return;
}
// Wifi will be turned off in onExit()
- const SyncResult result = {remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex,
- remotePosition.hasParagraphIndex};
+ const SyncResult result = {remotePosition.spineIndex, remotePosition.pageNumber,
+ remotePosition.paragraphIndex, remotePosition.hasParagraphIndex,
+ remotePosition.listItemIndex, remotePosition.hasListItemIndex};
resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE, &result);
} else if (selectedOption == 1) {
// Upload local progress
diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp
index 0d12c201..b3a8d3af 100644
--- a/src/activities/reader/ReaderActivity.cpp
+++ b/src/activities/reader/ReaderActivity.cpp
@@ -149,6 +149,8 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr/
at any depth, so count it globally —
+ // not at body-child level. The running count must match what the runtime reverse
+ // mapper sees so getPageForListItemIndex can snap a KOReader li XPath to a page.
+ if (self->xpathBodyDepth >= 0 && strcmp(name, "li") == 0) {
+ self->xpathListItemIndex++;
+ }
+
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
// start skip
self->skipUntilDepth = self->depth;
diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
index 30467157..08647baf 100644
--- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
+++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
@@ -108,7 +108,11 @@ class ChapterHtmlSlimParser final : public Print {
// Stored per page in the section cache so that XPath p[N] can be resolved to a page
// without reparsing, and current page can generate an XPath without reparsing.
uint16_t xpathParagraphIndex = 0; // current