KOReaderSync: add text-node offset mapping and trim debug noise
This commit is contained in:
@@ -44,6 +44,68 @@ size_t countVisibleBytes(const XML_Char* text, const int len) {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t countUtf8Codepoints(const XML_Char* text, const int len) {
|
||||||
|
size_t count = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
if ((static_cast<unsigned char>(text[i]) & 0xC0) != 0x80) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map a visible-byte index within a UTF-8 chunk to a 0-based codepoint offset.
|
||||||
|
// Returns the codepoint index of the character that contains the targetVisibleByte-th
|
||||||
|
// visible (non-whitespace) byte.
|
||||||
|
size_t codepointAtVisibleByte(const XML_Char* text, const int len, const size_t targetVisibleByte) {
|
||||||
|
size_t codepoints = 0;
|
||||||
|
size_t visibleBytes = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
const unsigned char uc = static_cast<unsigned char>(text[i]);
|
||||||
|
const bool isLeadByte = (uc & 0xC0) != 0x80;
|
||||||
|
if (isLeadByte) {
|
||||||
|
codepoints++;
|
||||||
|
}
|
||||||
|
if (!std::isspace(uc)) {
|
||||||
|
if (visibleBytes == targetVisibleByte) {
|
||||||
|
return codepoints - 1;
|
||||||
|
}
|
||||||
|
visibleBytes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codepoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count visible (non-whitespace) bytes before the target codepoint index.
|
||||||
|
// targetCodepointOffset is 0-based and measured in Unicode codepoints.
|
||||||
|
size_t visibleBytesBeforeCodepoint(const XML_Char* text, const int len, const size_t targetCodepointOffset) {
|
||||||
|
size_t visibleBytes = 0;
|
||||||
|
size_t codepointIndex = 0;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < len) {
|
||||||
|
if (codepointIndex >= targetCodepointOffset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int cpStart = i;
|
||||||
|
i++;
|
||||||
|
while (i < len && (static_cast<unsigned char>(text[i]) & 0xC0) == 0x80) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = cpStart; j < i; j++) {
|
||||||
|
if (!std::isspace(static_cast<unsigned char>(text[j]))) {
|
||||||
|
visibleBytes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codepointIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleBytes;
|
||||||
|
}
|
||||||
|
|
||||||
// Canonicalize a KOReader XPath for comparison:
|
// Canonicalize a KOReader XPath for comparison:
|
||||||
// - remove whitespace, lowercase, strip /text() with optional char offset,
|
// - remove whitespace, lowercase, strip /text() with optional char offset,
|
||||||
// strip trailing .N text-child-index suffix on the last segment (e.g. br.0 → br).
|
// strip trailing .N text-child-index suffix on the last segment (e.g. br.0 → br).
|
||||||
@@ -356,16 +418,57 @@ struct ForwardState : StackState {
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
|
|
||||||
|
// Body-level text-node tracking for text()[N].M XPath emission.
|
||||||
|
// crengine counts ALL text nodes (including whitespace-only) and uses
|
||||||
|
// 0-based Unicode codepoint offsets for .M.
|
||||||
|
int bodyTextNodeCount = 0;
|
||||||
|
size_t codepointsInBodyTextNode = 0;
|
||||||
|
bool inBodyTextNode = false;
|
||||||
|
|
||||||
ForwardState(const int spineIndex, const size_t targetOffset) : spineIndex(spineIndex), targetOffset(targetOffset) {}
|
ForwardState(const int spineIndex, const size_t targetOffset) : spineIndex(spineIndex), targetOffset(targetOffset) {}
|
||||||
|
|
||||||
|
void pushElement(const XML_Char* rawName) {
|
||||||
|
inBodyTextNode = false;
|
||||||
|
StackState::pushElement(rawName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void popElement() {
|
||||||
|
inBodyTextNode = false;
|
||||||
|
StackState::popElement();
|
||||||
|
}
|
||||||
|
|
||||||
void onChar(const XML_Char* text, const int len) {
|
void onChar(const XML_Char* text, const int len) {
|
||||||
if (shouldSkipText(len) || isWhitespaceOnly(text, len) || found) {
|
if (shouldSkipText(len) || found) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track body-level text nodes: count ALL text nodes (including whitespace-only)
|
||||||
|
// to match crengine's DOM text-node indexing.
|
||||||
|
const bool atBodyLevel = bodyIdx() + 1 == static_cast<int>(stack.size());
|
||||||
|
if (atBodyLevel && !inBodyTextNode) {
|
||||||
|
inBodyTextNode = true;
|
||||||
|
bodyTextNodeCount++;
|
||||||
|
codepointsInBodyTextNode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWhitespaceOnly(text, len)) {
|
||||||
|
if (atBodyLevel) {
|
||||||
|
codepointsInBodyTextNode += countUtf8Codepoints(text, len);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t visible = countVisibleBytes(text, len);
|
const size_t visible = countVisibleBytes(text, len);
|
||||||
if (totalTextBytes + visible >= targetOffset) {
|
if (totalTextBytes + visible >= targetOffset) {
|
||||||
result = currentXPath(spineIndex);
|
if (atBodyLevel && bodyTextNodeCount > 0) {
|
||||||
|
const size_t targetVisibleByteInChunk = targetOffset - totalTextBytes;
|
||||||
|
const size_t cpInChunk = codepointAtVisibleByte(text, len, targetVisibleByteInChunk);
|
||||||
|
const size_t charOff = codepointsInBodyTextNode + cpInChunk;
|
||||||
|
result =
|
||||||
|
currentXPath(spineIndex) + "/text()[" + std::to_string(bodyTextNodeCount) + "]." + std::to_string(charOff);
|
||||||
|
} else {
|
||||||
|
result = currentXPath(spineIndex);
|
||||||
|
}
|
||||||
found = true;
|
found = true;
|
||||||
if (parser) {
|
if (parser) {
|
||||||
XML_StopParser(parser, XML_FALSE);
|
XML_StopParser(parser, XML_FALSE);
|
||||||
@@ -373,6 +476,9 @@ struct ForwardState : StackState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
totalTextBytes += visible;
|
totalTextBytes += visible;
|
||||||
|
if (atBodyLevel) {
|
||||||
|
codepointsInBodyTextNode += countUtf8Codepoints(text, len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -415,7 +521,7 @@ struct ReverseState : StackState {
|
|||||||
int targetTextNodeIndex = 0;
|
int targetTextNodeIndex = 0;
|
||||||
int targetCharOffset = 0;
|
int targetCharOffset = 0;
|
||||||
bool inParentTextNode = false;
|
bool inParentTextNode = false;
|
||||||
size_t bytesInCurrentTextNode = 0;
|
size_t codepointsInCurrentTextNode = 0;
|
||||||
int currentTextNodeCount = 0;
|
int currentTextNodeCount = 0;
|
||||||
|
|
||||||
MatchTier bestTier = MatchTier::NONE;
|
MatchTier bestTier = MatchTier::NONE;
|
||||||
@@ -473,10 +579,12 @@ struct ReverseState : StackState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onChar(const XML_Char* text, const int len) {
|
void onChar(const XML_Char* text, const int len) {
|
||||||
if (shouldSkipText(len) || isWhitespaceOnly(text, len)) {
|
if (shouldSkipText(len)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t visible = countVisibleBytes(text, len);
|
const size_t visible = countVisibleBytes(text, len);
|
||||||
|
const size_t codepoints = countUtf8Codepoints(text, len);
|
||||||
|
|
||||||
// Text-node targeting: count direct text children of targetNorm element.
|
// Text-node targeting: count direct text children of targetNorm element.
|
||||||
if (targetTextNodeIndex > 0 && !stack.empty()) {
|
if (targetTextNodeIndex > 0 && !stack.empty()) {
|
||||||
@@ -487,12 +595,13 @@ struct ReverseState : StackState {
|
|||||||
if (!inParentTextNode) {
|
if (!inParentTextNode) {
|
||||||
inParentTextNode = true;
|
inParentTextNode = true;
|
||||||
currentTextNodeCount++;
|
currentTextNodeCount++;
|
||||||
bytesInCurrentTextNode = 0;
|
codepointsInCurrentTextNode = 0;
|
||||||
}
|
}
|
||||||
if (currentTextNodeCount == targetTextNodeIndex && bestTier < MatchTier::EXACT) {
|
if (currentTextNodeCount == targetTextNodeIndex && bestTier < MatchTier::EXACT) {
|
||||||
const size_t charOff = static_cast<size_t>(targetCharOffset);
|
const size_t charOff = static_cast<size_t>(targetCharOffset);
|
||||||
if (charOff >= bytesInCurrentTextNode && charOff < bytesInCurrentTextNode + visible) {
|
if (charOff >= codepointsInCurrentTextNode && charOff <= codepointsInCurrentTextNode + codepoints) {
|
||||||
const size_t pos = totalTextBytes + (charOff - bytesInCurrentTextNode);
|
const size_t cpInChunk = charOff - codepointsInCurrentTextNode;
|
||||||
|
const size_t pos = totalTextBytes + visibleBytesBeforeCodepoint(text, len, cpInChunk);
|
||||||
bestTier = MatchTier::EXACT;
|
bestTier = MatchTier::EXACT;
|
||||||
bestDepth = pathDepth(xpath);
|
bestDepth = pathDepth(xpath);
|
||||||
bestOffset = pos;
|
bestOffset = pos;
|
||||||
@@ -500,12 +609,16 @@ struct ReverseState : StackState {
|
|||||||
bestTierName = "text-node-exact";
|
bestTierName = "text-node-exact";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bytesInCurrentTextNode += visible;
|
codepointsInCurrentTextNode += codepoints;
|
||||||
totalTextBytes += visible;
|
totalTextBytes += visible;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isWhitespaceOnly(text, len)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Standard: check match once per element (at first text).
|
// Standard: check match once per element (at first text).
|
||||||
if (!stack.empty() && !stack.back().hasText) {
|
if (!stack.empty() && !stack.back().hasText) {
|
||||||
stack.back().hasText = true;
|
stack.back().hasText = true;
|
||||||
@@ -672,8 +785,8 @@ bool ChapterXPathIndexer::findProgressForXPath(const std::shared_ptr<Epub>& epub
|
|||||||
outIntraSpineProgress = std::max(0.0f, std::min(1.0f, outIntraSpineProgress));
|
outIntraSpineProgress = std::max(0.0f, std::min(1.0f, outIntraSpineProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f for '%s'", spineIndex, state.bestTierName,
|
LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f for '%s'", spineIndex,
|
||||||
state.bestOffset, state.totalTextBytes, outIntraSpineProgress, xpath.c_str());
|
state.bestTierName, state.bestOffset, state.totalTextBytes, outIntraSpineProgress, xpath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user