Picking up some itsthisjutin ideas
This commit is contained in:
@@ -124,6 +124,124 @@ size_t getTotalTextBytesCached(const std::shared_ptr<Epub>& epub, const int spin
|
||||
|
||||
} // namespace
|
||||
|
||||
// Paragraph-targeted forward mapper.
|
||||
// Counts direct-body-child <p> elements (matching ChapterHtmlSlimParser's xpathBodyDepth guard)
|
||||
// and stops at the Nth one, emitting its full-ancestry XPath. The seek hint avoids scanning
|
||||
// from byte 0 when the section LUT has a byte offset for a nearby page break.
|
||||
namespace {
|
||||
|
||||
struct ParagraphState : StackState {
|
||||
int spineIndex;
|
||||
uint16_t targetParagraph; // 1-based
|
||||
uint16_t paragraphCount = 0;
|
||||
std::string result;
|
||||
XML_Parser parser = nullptr;
|
||||
// When parsing from a seek offset, the DOM context (html/body ancestors) is missing from
|
||||
// the parser's perspective. partialParse=true relaxes the bodyIdx() check and instead
|
||||
// counts any <p> at depth 0 relative to the first element seen (a heuristic that works
|
||||
// because we know we're already inside <body> in the source document).
|
||||
bool partialParse = false;
|
||||
int partialBaseDepth = -1; // stack depth when the first element is seen in partial mode
|
||||
|
||||
ParagraphState(const int spineIndex, const uint16_t targetParagraph, const uint16_t startParagraphCount,
|
||||
const bool partialParse)
|
||||
: spineIndex(spineIndex),
|
||||
targetParagraph(targetParagraph),
|
||||
paragraphCount(startParagraphCount),
|
||||
partialParse(partialParse) {}
|
||||
};
|
||||
|
||||
void XMLCALL paragraphStartCb(void* ud, const XML_Char* rawName, const XML_Char**) {
|
||||
auto* s = static_cast<ParagraphState*>(ud);
|
||||
s->pushElement(rawName);
|
||||
if (!s->result.empty() || s->stack.empty() || s->stack.back().tag != "p") {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isDirectBodyChild = false;
|
||||
if (s->partialParse) {
|
||||
// In partial mode the DOM context (html/body ancestors) is absent from the parser.
|
||||
// We record the stack depth of the first element encountered as the body-equivalent
|
||||
// depth; direct body children are one level deeper. This only works for flat EPUBs
|
||||
// where paragraphs are direct children of <body> — for wrapped chapters the partial
|
||||
// parse will find nothing and the caller retries from byte 0 with full context.
|
||||
if (s->partialBaseDepth < 0) {
|
||||
s->partialBaseDepth = static_cast<int>(s->stack.size()) - 1;
|
||||
}
|
||||
isDirectBodyChild = (static_cast<int>(s->stack.size()) - 1 == s->partialBaseDepth);
|
||||
} else {
|
||||
const int bi = s->bodyIdx();
|
||||
isDirectBodyChild = (bi >= 0 && static_cast<int>(s->stack.size()) == bi + 2);
|
||||
}
|
||||
|
||||
if (isDirectBodyChild) {
|
||||
s->paragraphCount++;
|
||||
if (s->paragraphCount >= s->targetParagraph) {
|
||||
s->result = s->currentXPath(s->spineIndex);
|
||||
if (s->parser) {
|
||||
XML_StopParser(s->parser, XML_FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL paragraphEndCb(void* ud, const XML_Char*) { static_cast<ParagraphState*>(ud)->popElement(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string findXPathForParagraphInternal(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||
const uint16_t paragraphIndex, const uint32_t seekHint,
|
||||
const uint16_t startParagraphCount) {
|
||||
if (!epub || paragraphIndex == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string tmpPath = decompressToTempFile(epub, spineIndex);
|
||||
if (tmpPath.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const bool partialParse = seekHint > 0;
|
||||
ParagraphState state(spineIndex, paragraphIndex, partialParse ? startParagraphCount : 0, partialParse);
|
||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Storage.remove(tmpPath.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
state.parser = parser;
|
||||
XML_SetUserData(parser, &state);
|
||||
XML_SetElementHandler(parser, paragraphStartCb, paragraphEndCb);
|
||||
// No character data handler needed — we only care about element structure.
|
||||
XML_SetDefaultHandlerExpand(parser, parserDefaultCb<ParagraphState>);
|
||||
|
||||
// Use seek hint from section LUT if available — avoids scanning the whole chapter.
|
||||
// If the partial parse misses the target (e.g. the hint overshot), retry from byte 0.
|
||||
runParseFromOffset(parser, tmpPath, seekHint);
|
||||
|
||||
if (state.result.empty() && seekHint > 0) {
|
||||
// Partial parse missed — reset and retry from beginning with full-document context.
|
||||
XML_ParserFree(parser);
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (parser) {
|
||||
ParagraphState fullState(spineIndex, paragraphIndex, 0, false);
|
||||
fullState.parser = parser;
|
||||
XML_SetUserData(parser, &fullState);
|
||||
XML_SetElementHandler(parser, paragraphStartCb, paragraphEndCb);
|
||||
XML_SetDefaultHandlerExpand(parser, parserDefaultCb<ParagraphState>);
|
||||
runParse(parser, tmpPath);
|
||||
state.result = fullState.result;
|
||||
}
|
||||
}
|
||||
|
||||
XML_ParserFree(parser);
|
||||
Storage.remove(tmpPath.c_str());
|
||||
|
||||
LOG_DBG("KOX", "Paragraph: spine=%d p[%u] seekHint=%u -> %s", spineIndex, paragraphIndex, seekHint,
|
||||
state.result.empty() ? "(not found)" : state.result.c_str());
|
||||
return state.result;
|
||||
}
|
||||
|
||||
std::string findXPathForProgressInternal(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||
const float intraSpineProgress) {
|
||||
const std::string tmpPath = decompressToTempFile(epub, spineIndex);
|
||||
|
||||
@@ -9,4 +9,13 @@ namespace ChapterXPathIndexerInternal {
|
||||
|
||||
std::string findXPathForProgressInternal(const std::shared_ptr<Epub>& epub, int spineIndex, float intraSpineProgress);
|
||||
|
||||
// Find the full-ancestry XPath for the paragraphIndex-th direct-body-child <p> element.
|
||||
// paragraphIndex is 1-based, matching the section paragraph LUT and KOReader XPath convention.
|
||||
// seekHint is an optional XHTML byte offset to start scanning from (0 = scan from beginning).
|
||||
// startParagraphCount is the number of body-child <p> elements already seen before seekHint
|
||||
// (i.e. the paragraphIndex of the LUT entry at the seek page, minus 1). Ignored when seekHint=0.
|
||||
// Returns empty string on failure; caller should fall back to findXPathForProgressInternal.
|
||||
std::string findXPathForParagraphInternal(const std::shared_ptr<Epub>& epub, int spineIndex, uint16_t paragraphIndex,
|
||||
uint32_t seekHint = 0, uint16_t startParagraphCount = 0);
|
||||
|
||||
} // namespace ChapterXPathIndexerInternal
|
||||
|
||||
@@ -21,6 +21,12 @@ std::string ChapterXPathIndexer::findXPathForProgress(const std::shared_ptr<Epub
|
||||
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) {
|
||||
@@ -83,9 +89,12 @@ bool ChapterXPathIndexer::tryExtractParagraphIndexFromXPath(const std::string& x
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only accept p[...] that is a direct child of the /body segment — reject
|
||||
// paths with intermediate ancestor segments (e.g. /body/.../div[4]/p[1])
|
||||
// which would collapse structurally different locations to the same index.
|
||||
// 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.
|
||||
const size_t bodyEnd = (secondBody != std::string::npos ? secondBody : 0) + bodyKey.size();
|
||||
if (pos != bodyEnd) {
|
||||
return false;
|
||||
|
||||
@@ -65,6 +65,22 @@ class ChapterXPathIndexer {
|
||||
*/
|
||||
static bool tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex);
|
||||
|
||||
/**
|
||||
* Find the full-ancestry XPath for the Nth direct-body-child <p> element.
|
||||
*
|
||||
* Counts only <p> elements that are direct children of <body>, matching the semantics
|
||||
* of the section paragraph LUT built by ChapterHtmlSlimParser.
|
||||
*
|
||||
* @param epub Loaded EPUB instance
|
||||
* @param spineIndex Spine item index to parse
|
||||
* @param paragraphIndex 1-based paragraph index (from section LUT or XPath p[N])
|
||||
* @param seekHint Optional XHTML byte offset to start scanning from (0 = from beginning).
|
||||
* Pass Section::getXhtmlByteOffsetForPage() to avoid scanning the whole file.
|
||||
* @return Full-ancestry XPath like "/body/DocFragment[N]/body/div[1]/p[3]", or empty on failure
|
||||
*/
|
||||
static std::string findXPathForParagraph(const std::shared_ptr<Epub>& epub, int spineIndex, uint16_t paragraphIndex,
|
||||
uint32_t seekHint = 0, uint16_t startParagraphCount = 0);
|
||||
|
||||
/**
|
||||
* Extract the paragraph index from a KOReader XPath.
|
||||
* Looks for the first /p[N] segment after /body/ and returns N (1-based).
|
||||
|
||||
@@ -272,6 +272,42 @@ bool runParse(XML_Parser parser, const std::string& path) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool runParseFromOffset(XML_Parser parser, const std::string& path, const uint32_t seekBytes) {
|
||||
if (seekBytes == 0) {
|
||||
return runParse(parser, path);
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("KOX", path, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.seek(seekBytes)) {
|
||||
file.close();
|
||||
return runParse(parser, path); // fall back to full scan if seek fails
|
||||
}
|
||||
|
||||
constexpr size_t kBufSize = 1024;
|
||||
bool ok = true;
|
||||
int done;
|
||||
do {
|
||||
void* const buf = XML_GetBuffer(parser, kBufSize);
|
||||
if (!buf) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
const size_t len = file.read(buf, kBufSize);
|
||||
done = file.available() == 0;
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
ok = (XML_GetErrorCode(parser) == XML_ERROR_ABORTED);
|
||||
break;
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
file.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool isEntityRef(const XML_Char* text, const int len) {
|
||||
if (len < 3 || text[0] != '&' || text[len - 1] != ';') {
|
||||
return false;
|
||||
|
||||
@@ -25,6 +25,10 @@ bool isAncestorPath(const std::string& prefix, const std::string& path);
|
||||
|
||||
std::string decompressToTempFile(const std::shared_ptr<Epub>& epub, int spineIndex);
|
||||
bool runParse(XML_Parser parser, const std::string& path);
|
||||
// Like runParse but skips the first seekBytes bytes before feeding data to the parser.
|
||||
// Valid only when the parser is freshly created and the seek position is known to be on an XML
|
||||
// boundary (e.g. the Expat byte offset recorded at a page break).
|
||||
bool runParseFromOffset(XML_Parser parser, const std::string& path, uint32_t seekBytes);
|
||||
bool isEntityRef(const XML_Char* text, int len);
|
||||
size_t countTotalTextBytes(const std::string& tmpPath);
|
||||
|
||||
|
||||
@@ -58,10 +58,17 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
|
||||
result.percentage = epub->calculateProgress(pos.spineIndex, intraSpineProgress);
|
||||
|
||||
// Generate XPath for the current position.
|
||||
// Always use the indexer which SAX-parses the actual XHTML to find the correct
|
||||
// element path — a naive "/body/DocFragment[N]/body/p[M]" would assume paragraphs
|
||||
// are direct children of <body>, which breaks for wrapped chapters (e.g. div/section).
|
||||
result.xpath = ChapterXPathIndexer::findXPathForProgress(epub, pos.spineIndex, intraSpineProgress);
|
||||
// When we have a paragraph index from the section LUT, target that specific <p> element
|
||||
// directly — this produces a structurally precise full-ancestry path even for chapters
|
||||
// where paragraphs are nested inside divs/sections. Fall back to the progress-based
|
||||
// scan (which works for any content) when no paragraph index is available.
|
||||
if (pos.hasParagraphIndex && pos.paragraphIndex > 0) {
|
||||
result.xpath =
|
||||
ChapterXPathIndexer::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex, pos.xhtmlSeekHint);
|
||||
}
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = ChapterXPathIndexer::findXPathForProgress(epub, pos.spineIndex, intraSpineProgress);
|
||||
}
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = generateXPath(pos.spineIndex);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ struct CrossPointPosition {
|
||||
int spineIndex; // Current spine item (chapter) index
|
||||
int pageNumber; // Current page within the spine item (estimated if no paragraph LUT)
|
||||
int totalPages; // Total pages in the current spine item
|
||||
uint16_t paragraphIndex = 0; // 1-based <p> index from XPath (0 if unavailable)
|
||||
bool hasParagraphIndex = false; // True when paragraphIndex was resolved from XPath
|
||||
uint16_t paragraphIndex = 0; // 1-based <p> index (0 if unavailable)
|
||||
bool hasParagraphIndex = false; // True when paragraphIndex is valid
|
||||
uint32_t xhtmlSeekHint = 0; // Byte offset hint for findXPathForParagraph (0 = no hint)
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user