Files
Crosspoint/lib/KOReaderSync/ChapterXPathIndexer.cpp
T
2026-05-10 15:46:33 +02:00

132 lines
4.8 KiB
C++

#include "ChapterXPathIndexer.h"
#include <Logging.h>
#include <cctype>
#include <cstdlib>
#include <limits>
#include <string>
#include "ChapterXPathForwardMapper.h"
#include "ChapterXPathIndexerInternal.h"
#include "ChapterXPathReverseMapper.h"
using namespace ChapterXPathIndexerInternal;
// Public facade used by ProgressMapper. It intentionally stays thin and delegates
// heavy parsing/mapping work to the internal forward/reverse modules.
std::string ChapterXPathIndexer::findXPathForProgress(const std::shared_ptr<Epub>& epub, const int spineIndex,
const float intraSpineProgress) {
return findXPathForProgressInternal(epub, spineIndex, intraSpineProgress);
}
std::string ChapterXPathIndexer::findXPathForParagraph(const std::shared_ptr<Epub>& epub, const int spineIndex,
const uint16_t paragraphIndex, const uint32_t seekHint,
const uint16_t startParagraphCount) {
return findXPathForParagraphInternal(epub, spineIndex, paragraphIndex, seekHint, startParagraphCount);
}
bool ChapterXPathIndexer::findProgressForXPath(const std::shared_ptr<Epub>& epub, const int spineIndex,
const std::string& xpath, float& outIntraSpineProgress,
bool& outExactMatch, uint16_t* outListItemIndex) {
return findProgressForXPathInternal(epub, spineIndex, xpath, outIntraSpineProgress, outExactMatch, outListItemIndex);
}
bool ChapterXPathIndexer::tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex) {
outSpineIndex = -1;
if (xpath.empty()) {
return false;
}
const std::string normalized = normalizeXPath(xpath);
const std::string key = "/docfragment[";
const size_t pos = normalized.find(key);
if (pos == std::string::npos) {
LOG_DBG("KOX", "No DocFragment in xpath: '%s'", xpath.c_str());
return false;
}
const size_t start = pos + key.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 std::string value = normalized.substr(start, end - start);
const long parsed = std::strtol(value.c_str(), nullptr, 10);
// XPath uses 1-based predicates; internal spine indexing is 0-based.
if (parsed < 1 || parsed > std::numeric_limits<int>::max()) {
return false;
}
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);
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 searchStart = secondBody != std::string::npos ? secondBody : 0;
const size_t pos = normalized.find(pKey, searchStart);
if (pos == std::string::npos) {
return false;
}
// Only accept p[...] that is a direct child of the /body segment.
// The section paragraph LUT counts only direct-body-child <p> elements (matching
// KOReader's crengine pure-XML counting). A nested path like /body/div[2]/p[4]
// cannot be mapped to our flat LUT index — the p[4] there is the 4th sibling inside
// div[2], not the 4th <p> child of <body>. Deeply-nested XPaths fall through to
// ChapterXPathIndexer::findProgressForXPath which handles full-ancestry matching.
size_t bodyEnd = (secondBody != std::string::npos ? secondBody : 0) + bodyKey.size();
if (bodyEnd < normalized.size() && normalized[bodyEnd] == '[') {
const size_t idxStart = bodyEnd + 1;
size_t idxEnd = idxStart;
while (idxEnd < normalized.size() && std::isdigit(static_cast<unsigned char>(normalized[idxEnd]))) {
idxEnd++;
}
if (idxEnd < normalized.size() && normalized[idxEnd] == ']') {
bodyEnd = idxEnd + 1;
}
}
if (pos != bodyEnd) {
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);
// Paragraph index is preserved as 1-based to match XPath p[N] convention.
if (parsed < 1 || parsed > UINT16_MAX) {
return false;
}
outParagraphIndex = static_cast<uint16_t>(parsed);
return true;
}