Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
19 kB · 688 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689#include "ChapterXPathResolver.h"
#include <Epub/VisibleTextUtils.h>#include <Logging.h>#include <Print.h>#include <Utf8.h>#include <XmlParserUtils.h>#include <expat.h>
#include <algorithm>#include <cmath>#include <cstring>#include <string>#include <utility>#include <vector>
namespace {std::string stripPrefix(const XML_Char* name) { if (!name) { return ""; }
const char* local = std::strrchr(name, ':'); return local ? std::string(local + 1) : std::string(name);}
struct NameCounter { std::string name; int count;};
struct ParentState { std::vector<NameCounter> children;
int nextIndex(const std::string& name) { for (auto& child : children) { if (child.name == name) { child.count++; return child.count; } }
children.push_back({name, 1}); return 1; }};
struct PathSegment { std::string name; int index;};
std::string buildParagraphXPath(const int spineIndex, const std::vector<PathSegment>& path, const int textNodeIndex, const size_t charOffset) { std::string xpath = "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body"; for (const auto& segment : path) { xpath += "/" + segment.name + "[" + std::to_string(segment.index) + "]"; } if (textNodeIndex > 0) { xpath += "/text()[" + std::to_string(textNodeIndex) + "]." + std::to_string(charOffset); } return xpath;}
size_t countUtf8Codepoints(const XML_Char* data, const int len) { if (!data || len <= 0) { return 0; }
size_t count = 0; const unsigned char* ptr = reinterpret_cast<const unsigned char*>(data); const unsigned char* end = ptr + len; while (ptr < end) { utf8NextCodepoint(&ptr); count++; }
return count;}
class ParagraphTextCounter final : public Print { public: ParagraphTextCounter() { parser = XML_ParserCreate(nullptr); if (!parser) { LOG_ERR("KOX", "Failed to create XML parser"); return; }
XML_SetUserData(parser, this); XML_SetElementHandler(parser, &ParagraphTextCounter::startElement, &ParagraphTextCounter::endElement); XML_SetCharacterDataHandler(parser, &ParagraphTextCounter::characterData); }
~ParagraphTextCounter() override { destroyXmlParser(parser); }
bool ok() const { return parser != nullptr && parseOk; }
bool finish() { if (!parser || !parseOk || stopped) { return parseOk; }
if (XML_Parse(parser, "", 0, XML_TRUE) == XML_STATUS_ERROR) { LOG_ERR("KOX", "Final XML parse error: %s", XML_ErrorString(XML_GetErrorCode(parser))); parseOk = false; } return parseOk; }
size_t write(uint8_t c) override { return write(&c, 1); }
size_t write(const uint8_t* buffer, size_t size) override { if (!parser || !parseOk || stopped) { return size; }
if (XML_Parse(parser, reinterpret_cast<const char*>(buffer), static_cast<int>(size), XML_FALSE) != XML_STATUS_OK) { const enum XML_Error error = XML_GetErrorCode(parser); if (error != XML_ERROR_ABORTED) { LOG_ERR("KOX", "XML parse error: %s", XML_ErrorString(error)); parseOk = false; } }
return size; }
size_t totalVisibleChars() const { return visibleChars; }
private: static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char**) { auto* self = static_cast<ParagraphTextCounter*>(userData); self->onStartElement(name); }
static void XMLCALL endElement(void* userData, const XML_Char* name) { auto* self = static_cast<ParagraphTextCounter*>(userData); self->onEndElement(name); }
static void XMLCALL characterData(void* userData, const XML_Char* data, const int len) { auto* self = static_cast<ParagraphTextCounter*>(userData); self->onCharacterData(data, len); }
void onStartElement(const XML_Char* rawName) { const std::string name = stripPrefix(rawName);
if (!insideBody) { if (name == "body") { insideBody = true; bodyDepth = depth; } depth++; return; }
if (nonVisibleDepth > 0 || VisibleTextUtils::isNonVisibleElement(name)) { nonVisibleDepth++; } if (name == "p") { paragraphDepth++; } depth++; }
void onEndElement(const XML_Char* rawName) { const std::string name = stripPrefix(rawName);
depth--; if (!insideBody) { return; }
if (depth == bodyDepth && name == "body") { insideBody = false; nonVisibleDepth = 0; return; }
if (nonVisibleDepth > 0) { nonVisibleDepth--; } if (name == "p" && paragraphDepth > 0) { paragraphDepth--; } }
void onCharacterData(const XML_Char* data, const int len) { if (!insideBody || nonVisibleDepth > 0 || paragraphDepth <= 0 || len <= 0) { return; }
visibleChars += countUtf8Codepoints(data, len); }
private: XML_Parser parser = nullptr; bool parseOk = true; bool insideBody = false; bool stopped = false; int depth = 0; int bodyDepth = -1; int paragraphDepth = 0; uint16_t nonVisibleDepth = 0; size_t visibleChars = 0;};
class XPathParagraphResolver final : public Print { public: explicit XPathParagraphResolver(const int targetParagraph) : targetParagraph(targetParagraph) { parser = XML_ParserCreate(nullptr); if (!parser) { LOG_ERR("KOX", "Failed to create XML parser"); return; }
XML_SetUserData(parser, this); XML_SetElementHandler(parser, &XPathParagraphResolver::startElement, &XPathParagraphResolver::endElement); }
~XPathParagraphResolver() override { destroyXmlParser(parser); }
bool ok() const { return parser != nullptr && parseOk; }
bool finish() { if (!parser || !parseOk || stopped) { return parseOk; }
if (XML_Parse(parser, "", 0, XML_TRUE) == XML_STATUS_ERROR) { LOG_ERR("KOX", "Final XML parse error: %s", XML_ErrorString(XML_GetErrorCode(parser))); parseOk = false; } return parseOk; }
bool hasMatch() const { return !xpath.empty(); } const std::string& getXPath() const { return xpath; }
size_t write(uint8_t c) override { return write(&c, 1); }
size_t write(const uint8_t* buffer, size_t size) override { if (!parser || !parseOk || stopped) { return size; }
if (XML_Parse(parser, reinterpret_cast<const char*>(buffer), static_cast<int>(size), XML_FALSE) != XML_STATUS_OK) { const enum XML_Error error = XML_GetErrorCode(parser); if (error != XML_ERROR_ABORTED) { LOG_ERR("KOX", "XML parse error: %s", XML_ErrorString(error)); parseOk = false; } }
return size; }
int spineIndex = 0;
private: static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char**) { auto* self = static_cast<XPathParagraphResolver*>(userData); self->onStartElement(name); }
static void XMLCALL endElement(void* userData, const XML_Char* name) { auto* self = static_cast<XPathParagraphResolver*>(userData); self->onEndElement(name); }
void onStartElement(const XML_Char* rawName) { const std::string name = stripPrefix(rawName);
if (!insideBody) { if (name == "body") { insideBody = true; bodyDepth = depth; parentStates.emplace_back(); } depth++; return; }
const int siblingIndex = parentStates.back().nextIndex(name); path.push_back({name, siblingIndex}); parentStates.emplace_back();
// Count both <p> and <li> as paragraph-like positions, matching how the section // layout tracks them (xpathParagraphIndex and xpathListItemIndex). This ensures // KOReader progress in list items maps to the correct XPath. if (name == "p") { paragraphCount++; } else if (name == "li") { paragraphCount++; } if (paragraphCount == targetParagraph) { xpath = buildParagraphXPath(spineIndex, path, 0, 0); stopped = true; XML_StopParser(parser, XML_FALSE); }
depth++; }
void onEndElement(const XML_Char* rawName) { const std::string name = stripPrefix(rawName);
depth--; if (!insideBody) { return; }
if (depth == bodyDepth && name == "body") { insideBody = false; parentStates.clear(); path.clear(); return; }
if (!path.empty()) { path.pop_back(); } if (!parentStates.empty()) { parentStates.pop_back(); } }
XML_Parser parser = nullptr; const int targetParagraph; bool parseOk = true; bool insideBody = false; bool stopped = false; int depth = 0; int bodyDepth = -1; int paragraphCount = 0; std::vector<ParentState> parentStates; std::vector<PathSegment> path; std::string xpath;};
class XPathProgressResolver final : public Print { public: enum class BoundaryMode { Exclusive, Inclusive };
explicit XPathProgressResolver(const size_t targetVisibleChar, const BoundaryMode boundaryMode = BoundaryMode::Exclusive) : targetVisibleChar(targetVisibleChar), boundaryMode(boundaryMode) { parser = XML_ParserCreate(nullptr); if (!parser) { LOG_ERR("KOX", "Failed to create XML parser"); return; }
XML_SetUserData(parser, this); XML_SetElementHandler(parser, &XPathProgressResolver::startElement, &XPathProgressResolver::endElement); XML_SetCharacterDataHandler(parser, &XPathProgressResolver::characterData); XML_SetCommentHandler(parser, &XPathProgressResolver::comment); XML_SetProcessingInstructionHandler(parser, &XPathProgressResolver::processingInstruction); XML_SetCdataSectionHandler(parser, &XPathProgressResolver::startCdataSection, &XPathProgressResolver::endCdataSection); }
~XPathProgressResolver() override { destroyXmlParser(parser); }
bool ok() const { return parser != nullptr && parseOk; }
bool finish() { if (!parser || !parseOk || stopped) { return parseOk; }
if (XML_Parse(parser, "", 0, XML_TRUE) == XML_STATUS_ERROR) { LOG_ERR("KOX", "Final XML parse error: %s", XML_ErrorString(XML_GetErrorCode(parser))); parseOk = false; } return parseOk; }
bool hasMatch() const { return !xpath.empty(); } const std::string& getXPath() const { return xpath; }
size_t write(uint8_t c) override { return write(&c, 1); }
size_t write(const uint8_t* buffer, size_t size) override { if (!parser || !parseOk || stopped) { return size; }
if (XML_Parse(parser, reinterpret_cast<const char*>(buffer), static_cast<int>(size), XML_FALSE) != XML_STATUS_OK) { const enum XML_Error error = XML_GetErrorCode(parser); if (error != XML_ERROR_ABORTED) { LOG_ERR("KOX", "XML parse error: %s", XML_ErrorString(error)); parseOk = false; } }
return size; }
int spineIndex = 0;
private: static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char**) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onStartElement(name); }
static void XMLCALL endElement(void* userData, const XML_Char* name) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onEndElement(name); }
static void XMLCALL characterData(void* userData, const XML_Char* data, const int len) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onCharacterData(data, len); }
static void XMLCALL comment(void* userData, const XML_Char*) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onMarkupBoundary(); }
static void XMLCALL processingInstruction(void* userData, const XML_Char*, const XML_Char*) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onMarkupBoundary(); }
static void XMLCALL startCdataSection(void* userData) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onMarkupBoundary(); }
static void XMLCALL endCdataSection(void* userData) { auto* self = static_cast<XPathProgressResolver*>(userData); self->onMarkupBoundary(); }
void onStartElement(const XML_Char* rawName) { const std::string name = stripPrefix(rawName);
if (!insideBody) { if (name == "body") { insideBody = true; bodyDepth = depth; parentStates.emplace_back(); } depth++; return; }
const int siblingIndex = parentStates.back().nextIndex(name); path.push_back({name, siblingIndex}); parentStates.emplace_back(); textNodeIndexStack.push_back(0); pendingTextNode = true;
if (nonVisibleDepth > 0 || VisibleTextUtils::isNonVisibleElement(name)) { nonVisibleDepth++; }
if (name == "p") { paragraphDepth++; } if (name == "li") { liDepth++; }
depth++; }
void onEndElement(const XML_Char* rawName) { const std::string name = stripPrefix(rawName);
depth--; if (!insideBody) { return; }
if (depth == bodyDepth && name == "body") { insideBody = false; parentStates.clear(); path.clear(); textNodeIndexStack.clear(); nonVisibleDepth = 0; return; }
if (nonVisibleDepth > 0) { nonVisibleDepth--; } if (name == "p" && paragraphDepth > 0) { paragraphDepth--; } if (name == "li" && liDepth > 0) { liDepth--; }
if (!textNodeIndexStack.empty()) { textNodeIndexStack.pop_back(); } if (paragraphDepth > 0 || liDepth > 0) { pendingTextNode = true; } if (!path.empty()) { path.pop_back(); } if (!parentStates.empty()) { parentStates.pop_back(); } }
void onCharacterData(const XML_Char* data, const int len) { if (!insideBody || nonVisibleDepth > 0 || (paragraphDepth <= 0 && liDepth <= 0) || len <= 0 || stopped) { return; }
const size_t codepointCount = countUtf8Codepoints(data, len); if (codepointCount == 0) { return; }
// Start a new text node on first non-empty content after any structural boundary. // Only counting non-empty nodes matches KOReader's text()[N] indexing behavior, // which skips empty text nodes created by bare <a id="anchor"/> anchors. if (pendingTextNode) { if (!textNodeIndexStack.empty()) { textNodeIndexStack.back()++; } textNodeStartChars = visibleChars; pendingTextNode = false; }
const size_t nextVisibleChars = visibleChars + codepointCount; const bool targetInCurrentChunk = boundaryMode == BoundaryMode::Inclusive ? targetVisibleChar <= nextVisibleChars : targetVisibleChar < nextVisibleChars; if (targetInCurrentChunk) { const size_t delta = targetVisibleChar - visibleChars; const int texNode = textNodeIndexStack.empty() ? 0 : textNodeIndexStack.back(); const size_t charOff = visibleChars - textNodeStartChars + delta; xpath = buildParagraphXPath(spineIndex, path, texNode, charOff); stopped = true; XML_StopParser(parser, XML_FALSE); return; }
visibleChars = nextVisibleChars; }
void onMarkupBoundary() { if (!insideBody || nonVisibleDepth > 0 || (paragraphDepth <= 0 && liDepth <= 0) || stopped || pendingTextNode) { return; }
pendingTextNode = true; }
XML_Parser parser = nullptr; const size_t targetVisibleChar; const BoundaryMode boundaryMode; bool parseOk = true; bool insideBody = false; bool stopped = false; bool pendingTextNode = true; int depth = 0; int bodyDepth = -1; int paragraphDepth = 0; int liDepth = 0; uint16_t nonVisibleDepth = 0; size_t visibleChars = 0; size_t textNodeStartChars = 0; std::vector<int> textNodeIndexStack; std::vector<ParentState> parentStates; std::vector<PathSegment> path; std::string xpath;};} // namespace
std::string ChapterXPathResolver::findXPathForParagraph(const std::shared_ptr<Epub>& epub, const int spineIndex, const uint16_t paragraphIndex) { if (!epub || paragraphIndex == 0 || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) { return ""; }
const auto href = epub->getSpineItem(spineIndex).href; if (href.empty()) { return ""; }
XPathParagraphResolver resolver(paragraphIndex); if (!resolver.ok()) { return ""; }
resolver.spineIndex = spineIndex; if (!epub->readItemContentsToStream(href, resolver, 1024) || !resolver.finish()) { return ""; }
if (resolver.hasMatch()) { LOG_DBG("KOX", "Resolved paragraph %u in spine %d -> %s", paragraphIndex, spineIndex, resolver.getXPath().c_str()); return resolver.getXPath(); }
LOG_DBG("KOX", "Paragraph %u not found in spine %d", paragraphIndex, spineIndex); return "";}
std::string ChapterXPathResolver::findXPathForVisibleTextOffset(const std::shared_ptr<Epub>& epub, const int spineIndex, const uint32_t visibleTextOffset) { if (!epub || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) { return ""; }
const auto href = epub->getSpineItem(spineIndex).href; if (href.empty()) { return ""; }
XPathProgressResolver resolver(visibleTextOffset); if (!resolver.ok()) { return ""; }
resolver.spineIndex = spineIndex; if (!epub->readItemContentsToStream(href, resolver, 1024) || !resolver.finish()) { return ""; }
if (resolver.hasMatch()) { LOG_DBG("KOX", "Resolved visible offset %u in spine %d -> %s", visibleTextOffset, spineIndex, resolver.getXPath().c_str()); return resolver.getXPath(); }
LOG_DBG("KOX", "Visible offset %u not found in spine %d", visibleTextOffset, spineIndex); return "";}
std::string ChapterXPathResolver::findXPathForProgress(const std::shared_ptr<Epub>& epub, const int spineIndex, const float intraSpineProgress) { if (!epub || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) { return ""; }
const auto href = epub->getSpineItem(spineIndex).href; if (href.empty()) { return ""; }
if (!(intraSpineProgress > 0.0f)) { return "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body"; }
ParagraphTextCounter counter; if (!counter.ok() || !epub->readItemContentsToStream(href, counter, 1024) || !counter.finish()) { return ""; }
const size_t totalVisibleChars = counter.totalVisibleChars(); if (totalVisibleChars == 0) { return ""; }
const float clamped = std::max(0.0f, std::min(1.0f, intraSpineProgress)); const size_t targetVisibleChar = std::max<size_t>(1, std::min(totalVisibleChars, static_cast<size_t>(std::ceil(clamped * totalVisibleChars))));
XPathProgressResolver resolver(targetVisibleChar, XPathProgressResolver::BoundaryMode::Inclusive); if (!resolver.ok()) { return ""; }
resolver.spineIndex = spineIndex; if (!epub->readItemContentsToStream(href, resolver, 1024) || !resolver.finish()) { return ""; }
if (resolver.hasMatch()) { LOG_DBG("KOX", "Resolved progress %.3f in spine %d -> %s", intraSpineProgress, spineIndex, resolver.getXPath().c_str()); return resolver.getXPath(); }
LOG_DBG("KOX", "Could not resolve progress %.3f in spine %d", intraSpineProgress, spineIndex); return "";}