mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
various fixes and broadening footnotes identification
This commit is contained in:
parent
ce9ae77f79
commit
2714c58046
@ -55,35 +55,80 @@ std::string replaceHtmlEntities(const char* text) {
|
|||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
std::string s(text);
|
std::string s(text);
|
||||||
|
|
||||||
// Replace common entities
|
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
while ((pos = s.find("<", pos)) != std::string::npos) {
|
while (pos < s.length()) {
|
||||||
s.replace(pos, 4, "<");
|
if (s[pos] == '&') {
|
||||||
pos += 1;
|
bool replaced = false;
|
||||||
}
|
const char* ptr = s.c_str() + pos; // Get pointer to current position (no allocation)
|
||||||
pos = 0;
|
|
||||||
while ((pos = s.find(">", pos)) != std::string::npos) {
|
if (pos + 1 < s.length()) {
|
||||||
s.replace(pos, 4, ">");
|
switch (s[pos + 1]) {
|
||||||
pos += 1;
|
case 'l': // <
|
||||||
}
|
if (pos + 3 < s.length() && strncmp(ptr, "<", 4) == 0) {
|
||||||
pos = 0;
|
s.replace(pos, 4, "<");
|
||||||
while ((pos = s.find("&", pos)) != std::string::npos) {
|
replaced = true;
|
||||||
s.replace(pos, 5, "&");
|
}
|
||||||
pos += 1;
|
break;
|
||||||
}
|
|
||||||
pos = 0;
|
case 'g': // >
|
||||||
while ((pos = s.find(""", pos)) != std::string::npos) {
|
if (pos + 3 < s.length() && strncmp(ptr, ">", 4) == 0) {
|
||||||
s.replace(pos, 6, "\"");
|
s.replace(pos, 4, ">");
|
||||||
pos += 1;
|
replaced = true;
|
||||||
}
|
}
|
||||||
pos = 0;
|
break;
|
||||||
while ((pos = s.find("'", pos)) != std::string::npos) {
|
|
||||||
s.replace(pos, 6, "'");
|
case 'a': // & or '
|
||||||
pos += 1;
|
if (pos + 4 < s.length() && strncmp(ptr, "&", 5) == 0) {
|
||||||
|
s.replace(pos, 5, "&");
|
||||||
|
replaced = true;
|
||||||
|
} else if (pos + 5 < s.length() && strncmp(ptr, "'", 6) == 0) {
|
||||||
|
s.replace(pos, 6, "'");
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'q': // "
|
||||||
|
if (pos + 5 < s.length() && strncmp(ptr, """, 6) == 0) {
|
||||||
|
s.replace(pos, 6, "\"");
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't increment pos if we replaced - allows nested entity handling
|
||||||
|
// Example: &lt; -> < (iteration 1) -> < (iteration 2)
|
||||||
|
if (!replaced) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if href points to internal EPUB location (not external URL)
|
||||||
|
bool isInternalEpubLink(const char* href) {
|
||||||
|
if (!href) return false;
|
||||||
|
|
||||||
|
switch (href[0]) {
|
||||||
|
case 'h': // http/https
|
||||||
|
if (strncmp(href, "http", 4) == 0) return false;
|
||||||
|
case 'f': // ftp
|
||||||
|
if (strncmp(href, "ftp://", 6) == 0) return false;
|
||||||
|
case 'm': // mailto
|
||||||
|
if (strncmp(href, "mailto:", 7) == 0) return false;
|
||||||
|
case 't': // tel
|
||||||
|
if (strncmp(href, "tel:", 4) == 0) return false;
|
||||||
|
case 's': // sms
|
||||||
|
if (strncmp(href, "sms:", 4) == 0) return false;
|
||||||
|
}
|
||||||
|
// Everything else is internal (relative paths, anchors, etc.)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
EpdFontFamily::Style ChapterHtmlSlimParser::getCurrentFontStyle() const {
|
EpdFontFamily::Style ChapterHtmlSlimParser::getCurrentFontStyle() const {
|
||||||
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
||||||
return EpdFontFamily::BOLD_ITALIC;
|
return EpdFontFamily::BOLD_ITALIC;
|
||||||
@ -240,130 +285,57 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
// Pass 2: Skip the aside (we already have it from Pass 1)
|
// Pass 2: Skip the aside (we already have it from Pass 1)
|
||||||
Serial.printf("[%lu] [ASIDE] Skipping aside in Pass 2: id=%s\n", millis(), id);
|
Serial.printf("[%lu] [ASIDE] Skipping aside in Pass 2: id=%s\n", millis(), id);
|
||||||
|
|
||||||
// Find the inline footnote text
|
|
||||||
for (int i = 0; i < self->inlineFootnoteCount; i++) {
|
|
||||||
if (strcmp(self->inlineFootnotes[i].id, id) == 0 && self->inlineFootnotes[i].text) {
|
|
||||||
// Output the footnote text as normal text
|
|
||||||
const char* text = self->inlineFootnotes[i].text;
|
|
||||||
int textLen = strlen(text);
|
|
||||||
|
|
||||||
// Process it through characterData
|
|
||||||
self->characterData(self, text, textLen);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [ASIDE] Rendered aside text: %.80s...\n", millis(), text);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip the aside element itself
|
|
||||||
self->skipUntilDepth = self->depth;
|
self->skipUntilDepth = self->depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PASS 1: Skip everything else
|
|
||||||
// ============================================================================
|
|
||||||
if (self->isPass1CollectingAsides) {
|
|
||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// PASS 2: Skip <p class="note"> (we already have them from Pass 1)
|
// PASS 2: FOOTNOTE DETECTION
|
||||||
|
// All <a> tags with internal hrefs are treated as footnotes
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
if (strcmp(name, "p") == 0) {
|
if (!self->isPass1CollectingAsides && strcmp(name, "a") == 0) {
|
||||||
const char* classAttr = getAttribute(atts, "class");
|
|
||||||
|
|
||||||
if (classAttr && (strcmp(classAttr, "note") == 0 || strstr(classAttr, "note"))) {
|
|
||||||
Serial.printf("[%lu] [PNOTE] Skipping paragraph note in Pass 2\n", millis());
|
|
||||||
self->skipUntilDepth = self->depth;
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PASS 2: Normal parsing
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Middle of skip
|
|
||||||
if (self->skipUntilDepth < self->depth) {
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rest of startElement logic for pass 2...
|
|
||||||
if (strcmp(name, "sup") == 0) {
|
|
||||||
self->supDepth = self->depth;
|
|
||||||
|
|
||||||
// Case A: Found <sup> inside a normal <a> (which wasn't marked as a note yet)
|
|
||||||
// Example: <a href="..."><sup>*</sup></a>
|
|
||||||
if (self->anchorDepth != -1 && !self->insideNoteref) {
|
|
||||||
Serial.printf("[%lu] [NOTEREF] Found <sup> inside <a>, promoting to noteref\n", millis());
|
|
||||||
|
|
||||||
// Flush the current word buffer (text before the sup is normal text)
|
|
||||||
if (self->partWordBufferIndex > 0) {
|
|
||||||
self->flushPartWordBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate footnote mode
|
|
||||||
self->insideNoteref = true;
|
|
||||||
self->currentNoterefTextLen = 0;
|
|
||||||
self->currentNoterefText[0] = '\0';
|
|
||||||
// Note: The href was already saved to currentNoterefHref when the <a> was opened (see below)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Update the existing A block ===
|
|
||||||
if (strcmp(name, "a") == 0) {
|
|
||||||
const char* epubType = getAttribute(atts, "epub:type");
|
|
||||||
const char* href = getAttribute(atts, "href");
|
const char* href = getAttribute(atts, "href");
|
||||||
|
|
||||||
// Save Anchor state
|
// Flush pending word buffer before starting footnote
|
||||||
self->anchorDepth = self->depth;
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
}
|
||||||
|
// Check for internal EPUB link
|
||||||
|
bool isInternalLink = isInternalEpubLink(href);
|
||||||
|
|
||||||
// Optimistically save the href, in case this becomes a footnote later (via internal <sup>)
|
// Special case: javascript:void(0) links with data attributes
|
||||||
if (!self->insideNoteref) {
|
// Example: <a href="javascript:void(0)"
|
||||||
if (href) {
|
// data-xyz="{"name":"OPS/ch2.xhtml","frag":"id46"}">
|
||||||
strncpy(self->currentNoterefHref, href, 127);
|
if (href && strncmp(href, "javascript:", 11) == 0) {
|
||||||
self->currentNoterefHref[127] = '\0';
|
isInternalLink = false;
|
||||||
} else {
|
|
||||||
self->currentNoterefHref[0] = '\0';
|
// TODO: Parse data-* attributes to extract actual href
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footnote detection: via epub:type, rnote pattern, or if we are already inside a <sup>
|
// If it's an internal link, treat it as a footnote
|
||||||
// Case B: Found <a> inside <sup>
|
if (isInternalLink && href) {
|
||||||
// Example: <sup><a href="...">1</a></sup>
|
Serial.printf("[%lu] [FOOTNOTE] Found internal link (footnote candidate): href=%s\n", millis(), href);
|
||||||
bool isNoteref = (epubType && strcmp(epubType, "noteref") == 0);
|
|
||||||
|
|
||||||
if (!isNoteref && href && href[0] == '#' && strncmp(href + 1, "rnote", 5) == 0) {
|
self->insideFootnoteLink = true;
|
||||||
isNoteref = true;
|
self->footnoteLinkDepth = self->depth;
|
||||||
|
self->currentFootnoteLinkHref[0] = '\0';
|
||||||
|
strncpy(self->currentFootnoteLinkHref, href, 63);
|
||||||
|
self->currentFootnoteLinkHref[63] = '\0';
|
||||||
|
|
||||||
|
self->currentFootnoteLinkText[0] = '\0';
|
||||||
|
self->currentFootnoteLinkTextLen = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New detection: if we are inside SUP, this link is a footnote
|
self->depth += 1;
|
||||||
if (!isNoteref && self->supDepth != -1) {
|
return;
|
||||||
isNoteref = true;
|
|
||||||
Serial.printf("[%lu] [NOTEREF] Found <a> inside <sup>, treating as noteref\n", millis());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNoteref) {
|
|
||||||
Serial.printf("[%lu] [NOTEREF] Found noteref: href=%s\n", millis(), href ? href : "null");
|
|
||||||
// Flush word buffer
|
|
||||||
if (self->partWordBufferIndex > 0) {
|
|
||||||
self->flushPartWordBuffer();
|
|
||||||
}
|
|
||||||
self->insideNoteref = true;
|
|
||||||
self->currentNoterefTextLen = 0;
|
|
||||||
self->currentNoterefText[0] = '\0';
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Handle other tags
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// Special handling for tables - show placeholder text instead of dropping silently
|
// Special handling for tables - show placeholder text instead of dropping silently
|
||||||
if (strcmp(name, "table") == 0) {
|
if (strcmp(name, "table") == 0) {
|
||||||
@ -533,13 +505,13 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rest of characterData logic for pass 2...
|
// Rest of characterData logic for pass 2...
|
||||||
if (self->insideNoteref) {
|
if (self->insideFootnoteLink) {
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
unsigned char c = (unsigned char)s[i];
|
unsigned char c = (unsigned char)s[i];
|
||||||
// Skip whitespace and brackets []
|
// Skip whitespace and brackets []
|
||||||
if (!isWhitespace(c) && c != '[' && c != ']' && self->currentNoterefTextLen < 15) {
|
if (!isWhitespace(c) && c != '[' && c != ']' && self->currentFootnoteLinkTextLen < 63) {
|
||||||
self->currentNoterefText[self->currentNoterefTextLen++] = c;
|
self->currentFootnoteLinkText[self->currentFootnoteLinkTextLen++] = c;
|
||||||
self->currentNoterefText[self->currentNoterefTextLen] = '\0';
|
self->currentFootnoteLinkText[self->currentFootnoteLinkTextLen] = '\0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -600,144 +572,95 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
|
|
||||||
// Closing paragraph note in Pass 1
|
// ============================================================================
|
||||||
if (strcmp(name, "p") == 0 && self->insideParagraphNote && self->depth - 1 == self->paragraphNoteDepth) {
|
// PASS 1: End of <aside epub:type="footnote">
|
||||||
if (self->isPass1CollectingAsides && self->currentParagraphNoteTextLen > 0 && self->paragraphNoteCount < 32 &&
|
// ============================================================================
|
||||||
self->currentParagraphNoteId[0] != '\0') {
|
if (strcmp(name, "aside") == 0 && self->insideAsideFootnote && self->depth == self->asideDepth + 1) {
|
||||||
// Copy ID
|
if (self->isPass1CollectingAsides) {
|
||||||
strncpy(self->paragraphNotes[self->paragraphNoteCount].id, self->currentParagraphNoteId, 15);
|
// Store the inline footnote
|
||||||
self->paragraphNotes[self->paragraphNoteCount].id[15] = '\0';
|
if (self->inlineFootnoteCount < 32) { // MAX_INLINE_FOOTNOTES
|
||||||
|
InlineFootnote& fn = self->inlineFootnotes[self->inlineFootnoteCount];
|
||||||
|
|
||||||
// Allocate memory for text
|
strncpy(fn.id, self->currentAsideId, 15);
|
||||||
size_t textLen = strlen(self->currentParagraphNoteText);
|
fn.id[15] = '\0';
|
||||||
self->paragraphNotes[self->paragraphNoteCount].text = static_cast<char*>(malloc(textLen + 1));
|
|
||||||
|
|
||||||
if (self->paragraphNotes[self->paragraphNoteCount].text) {
|
strncpy(fn.text, self->currentAsideText, 255);
|
||||||
strcpy(self->paragraphNotes[self->paragraphNoteCount].text, self->currentParagraphNoteText);
|
fn.text[255] = '\0';
|
||||||
|
|
||||||
Serial.printf("[%lu] [PNOTE] Stored: %s -> %.80s... (allocated %d bytes)\n", millis(),
|
self->inlineFootnoteCount++;
|
||||||
self->currentParagraphNoteId, self->currentParagraphNoteText, textLen + 1);
|
|
||||||
|
Serial.printf("[%lu] [ASIDE] Stored inline footnote: id=%s, text=%.80s\n", millis(), fn.id, fn.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self->insideAsideFootnote = false;
|
||||||
|
self->currentAsideTextLen = 0;
|
||||||
|
self->currentAsideText[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PASS 1: End of <p class="note">
|
||||||
|
// ============================================================================
|
||||||
|
if (strcmp(name, "p") == 0 && self->insideParagraphNote && self->depth == self->paragraphNoteDepth + 1) {
|
||||||
|
if (self->isPass1CollectingAsides && self->currentParagraphNoteId[0] != '\0') {
|
||||||
|
// Store the paragraph note
|
||||||
|
if (self->paragraphNoteCount < 32) { // MAX_PARAGRAPH_NOTES
|
||||||
|
ParagraphNote& pn = self->paragraphNotes[self->paragraphNoteCount];
|
||||||
|
|
||||||
|
strncpy(pn.id, self->currentParagraphNoteId, 15);
|
||||||
|
pn.id[15] = '\0';
|
||||||
|
|
||||||
|
strncpy(pn.text, self->currentParagraphNoteText, 255);
|
||||||
|
pn.text[255] = '\0';
|
||||||
|
|
||||||
self->paragraphNoteCount++;
|
self->paragraphNoteCount++;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [PNOTE] Stored paragraph note: id=%s, text=%.80s\n", millis(), pn.id, pn.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self->insideParagraphNote = false;
|
self->insideParagraphNote = false;
|
||||||
self->depth -= 1;
|
self->currentParagraphNoteTextLen = 0;
|
||||||
return;
|
self->currentParagraphNoteText[0] = '\0';
|
||||||
|
self->currentParagraphNoteId[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closing aside - handle differently for Pass 1 vs Pass 2
|
// ============================================================================
|
||||||
if (strcmp(name, "aside") == 0 && self->insideAsideFootnote && self->depth - 1 == self->asideDepth) {
|
// PASS 2: End of footnote link
|
||||||
// Store footnote ONLY in Pass 1
|
// ============================================================================
|
||||||
if (self->isPass1CollectingAsides && self->currentAsideTextLen > 0 && self->inlineFootnoteCount < 16) {
|
if (!self->isPass1CollectingAsides && strcmp(name, "a") == 0 && self->insideFootnoteLink &&
|
||||||
// Copy ID (max 2 digits)
|
self->depth == self->footnoteLinkDepth + 1) {
|
||||||
strncpy(self->inlineFootnotes[self->inlineFootnoteCount].id, self->currentAsideId, 2);
|
// We have collected the footnote link text
|
||||||
self->inlineFootnotes[self->inlineFootnoteCount].id[2] = '\0';
|
// Now add it to the current text block as a footnote
|
||||||
|
if (self->currentFootnoteLinkText[0] != '\0' && self->currentFootnoteLinkHref[0] != '\0') {
|
||||||
|
Serial.printf("[%lu] [FOOTNOTE] Complete footnote: text='%s', href='%s'\n", millis(),
|
||||||
|
self->currentFootnoteLinkText, self->currentFootnoteLinkHref);
|
||||||
|
|
||||||
// DYNAMIC ALLOCATION: allocate exactly the needed size + 1
|
// Add footnote to current text block
|
||||||
size_t textLen = strlen(self->currentAsideText);
|
if (self->currentTextBlock) {
|
||||||
self->inlineFootnotes[self->inlineFootnoteCount].text = static_cast<char*>(malloc(textLen + 1));
|
auto footnote = self->createFootnoteEntry(self->currentFootnoteLinkText, self->currentFootnoteLinkHref);
|
||||||
|
|
||||||
if (self->inlineFootnotes[self->inlineFootnoteCount].text) {
|
|
||||||
strcpy(self->inlineFootnotes[self->inlineFootnoteCount].text, self->currentAsideText);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [ASIDE] Stored: %s -> %.80s... (allocated %d bytes)\n", millis(), self->currentAsideId,
|
|
||||||
self->currentAsideText, textLen + 1);
|
|
||||||
|
|
||||||
self->inlineFootnoteCount++;
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%lu] [ASIDE] ERROR: Failed to allocate %d bytes for footnote %s\n", millis(), textLen + 1,
|
|
||||||
self->currentAsideId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state AFTER processing
|
|
||||||
self->insideAsideFootnote = false;
|
|
||||||
self->depth -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// During pass 1, skip all other processing
|
|
||||||
if (self->isPass1CollectingAsides) {
|
|
||||||
self->depth -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// PASS 2: Normal Parsing Logic
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
|
|
||||||
// [NEW] 1. Reset Superscript State
|
|
||||||
// We must ensure we know when we are leaving a <sup> tag
|
|
||||||
if (strcmp(name, "sup") == 0) {
|
|
||||||
if (self->supDepth == self->depth) {
|
|
||||||
self->supDepth = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [MODIFIED] 2. Handle 'a' tags (Anchors/Footnotes)
|
|
||||||
// We check "a" generally now, to handle both Noterefs AND resetting regular links
|
|
||||||
if (strcmp(name, "a") == 0) {
|
|
||||||
// Track if this was a noteref so we can return early later
|
|
||||||
bool wasNoteref = self->insideNoteref;
|
|
||||||
|
|
||||||
if (self->insideNoteref) {
|
|
||||||
self->insideNoteref = false;
|
|
||||||
|
|
||||||
if (self->currentNoterefTextLen > 0) {
|
|
||||||
Serial.printf("[%lu] [NOTEREF] %s -> %s\n", millis(), self->currentNoterefText, self->currentNoterefHref);
|
|
||||||
|
|
||||||
// Create the footnote entry (this does the rewriting)
|
|
||||||
std::unique_ptr<FootnoteEntry> footnote =
|
|
||||||
self->createFootnoteEntry(self->currentNoterefText, self->currentNoterefHref);
|
|
||||||
|
|
||||||
// Then call callback with the REWRITTEN href
|
|
||||||
if (self->noterefCallback && footnote) {
|
|
||||||
Noteref noteref;
|
|
||||||
strncpy(noteref.number, self->currentNoterefText, 15);
|
|
||||||
noteref.number[15] = '\0';
|
|
||||||
|
|
||||||
strncpy(noteref.href, footnote->href, 127);
|
|
||||||
noteref.href[127] = '\0';
|
|
||||||
|
|
||||||
self->noterefCallback(noteref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure [1] appears inline after the word it references
|
|
||||||
EpdFontFamily::Style fontStyle = self->getCurrentFontStyle();
|
|
||||||
|
|
||||||
// Format the noteref text with brackets
|
// Format the noteref text with brackets
|
||||||
char formattedNoteref[32];
|
char formattedNoteref[32];
|
||||||
snprintf(formattedNoteref, sizeof(formattedNoteref), "[%s]", self->currentNoterefText);
|
snprintf(formattedNoteref, sizeof(formattedNoteref), "[%s]", self->currentFootnoteLinkText);
|
||||||
|
|
||||||
// Add it as a word to the current text block with the footnote attached
|
// Add it as a word to the current text block with the footnote attached
|
||||||
if (self->currentTextBlock) {
|
EpdFontFamily::Style fontStyle = self->getCurrentFontStyle();
|
||||||
self->currentTextBlock->addWord(formattedNoteref, fontStyle, std::move(footnote));
|
|
||||||
}
|
self->currentTextBlock->addWord(formattedNoteref, fontStyle, std::move(footnote));
|
||||||
}
|
}
|
||||||
|
|
||||||
self->currentNoterefTextLen = 0;
|
|
||||||
self->currentNoterefText[0] = '\0';
|
|
||||||
self->currentNoterefHrefLen = 0;
|
|
||||||
// Note: We do NOT clear currentNoterefHref here yet, we do it below
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// [NEW] Reset Anchor Depth
|
self->insideFootnoteLink = false;
|
||||||
// This runs for BOTH footnotes and regular links to ensure state is clean
|
self->currentFootnoteLinkTextLen = 0;
|
||||||
if (self->anchorDepth == self->depth) {
|
self->currentFootnoteLinkText[0] = '\0';
|
||||||
self->anchorDepth = -1;
|
self->currentFootnoteLinkHref[0] = '\0';
|
||||||
self->currentNoterefHref[0] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it was a noteref, we are done with this tag, return early
|
|
||||||
if (wasNoteref) {
|
|
||||||
self->depth -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->partWordBufferIndex > 0) {
|
// ============================================================================
|
||||||
|
// PASS 2: Normal end element handling
|
||||||
|
// ============================================================================
|
||||||
|
if (!self->isPass1CollectingAsides) {
|
||||||
const bool shouldBreakText =
|
const bool shouldBreakText =
|
||||||
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
||||||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||||
@ -843,13 +766,10 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
boldUntilDepth = INT_MAX;
|
boldUntilDepth = INT_MAX;
|
||||||
italicUntilDepth = INT_MAX;
|
italicUntilDepth = INT_MAX;
|
||||||
partWordBufferIndex = 0;
|
partWordBufferIndex = 0;
|
||||||
insideNoteref = false;
|
|
||||||
insideAsideFootnote = false;
|
insideAsideFootnote = false;
|
||||||
|
insideFootnoteLink = false;
|
||||||
isPass1CollectingAsides = false;
|
isPass1CollectingAsides = false;
|
||||||
|
|
||||||
supDepth = -1;
|
|
||||||
anchorDepth = -1;
|
|
||||||
|
|
||||||
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
||||||
|
|
||||||
const XML_Parser parser2 = XML_ParserCreate(nullptr);
|
const XML_Parser parser2 = XML_ParserCreate(nullptr);
|
||||||
|
|||||||
@ -70,7 +70,6 @@ class ChapterHtmlSlimParser {
|
|||||||
bool hyphenationEnabled;
|
bool hyphenationEnabled;
|
||||||
|
|
||||||
// Noteref tracking
|
// Noteref tracking
|
||||||
bool insideNoteref = false;
|
|
||||||
char currentNoterefText[16] = {0};
|
char currentNoterefText[16] = {0};
|
||||||
int currentNoterefTextLen = 0;
|
int currentNoterefTextLen = 0;
|
||||||
char currentNoterefHref[128] = {0};
|
char currentNoterefHref[128] = {0};
|
||||||
@ -90,6 +89,13 @@ class ChapterHtmlSlimParser {
|
|||||||
char currentParagraphNoteText[MAX_PNOTE_BUFFER] = {0};
|
char currentParagraphNoteText[MAX_PNOTE_BUFFER] = {0};
|
||||||
int currentParagraphNoteTextLen = 0;
|
int currentParagraphNoteTextLen = 0;
|
||||||
|
|
||||||
|
// Footnote link state
|
||||||
|
bool insideFootnoteLink = false;
|
||||||
|
int footnoteLinkDepth = -1;
|
||||||
|
char currentFootnoteLinkText[64];
|
||||||
|
char currentFootnoteLinkHref[64];
|
||||||
|
size_t currentFootnoteLinkTextLen = 0;
|
||||||
|
|
||||||
// Temporary buffer for accumulation, will be copied to dynamic allocation
|
// Temporary buffer for accumulation, will be copied to dynamic allocation
|
||||||
static constexpr int MAX_ASIDE_BUFFER = 1024;
|
static constexpr int MAX_ASIDE_BUFFER = 1024;
|
||||||
char currentAsideText[MAX_ASIDE_BUFFER] = {0};
|
char currentAsideText[MAX_ASIDE_BUFFER] = {0};
|
||||||
@ -98,10 +104,6 @@ class ChapterHtmlSlimParser {
|
|||||||
// Flag to indicate we're in Pass 1 (collecting asides only)
|
// Flag to indicate we're in Pass 1 (collecting asides only)
|
||||||
bool isPass1CollectingAsides = false;
|
bool isPass1CollectingAsides = false;
|
||||||
|
|
||||||
// Track superscript depth
|
|
||||||
int supDepth = -1;
|
|
||||||
int anchorDepth = -1;
|
|
||||||
|
|
||||||
std::unique_ptr<FootnoteEntry> createFootnoteEntry(const char* number, const char* href);
|
std::unique_ptr<FootnoteEntry> createFootnoteEntry(const char* number, const char* href);
|
||||||
void startNewTextBlock(TextBlock::Style style);
|
void startNewTextBlock(TextBlock::Style style);
|
||||||
EpdFontFamily::Style getCurrentFontStyle() const;
|
EpdFontFamily::Style getCurrentFontStyle() const;
|
||||||
|
|||||||
@ -27,13 +27,9 @@ void EpubReaderTocActivity::onEnter() {
|
|||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Init chapters state
|
// Init chapters state
|
||||||
buildFilteredChapterList();
|
chaptersSelectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
chaptersSelectorIndex = 0;
|
if (chaptersSelectorIndex == -1) {
|
||||||
for (size_t i = 0; i < filteredSpineIndices.size(); i++) {
|
chaptersSelectorIndex = 0;
|
||||||
if (filteredSpineIndices[i] == currentSpineIndex) {
|
|
||||||
chaptersSelectorIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (hasSyncOption()) {
|
if (hasSyncOption()) {
|
||||||
chaptersSelectorIndex += 1;
|
chaptersSelectorIndex += 1;
|
||||||
@ -110,33 +106,38 @@ void EpubReaderTocActivity::loopChapters() {
|
|||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||||
const int totalItems = getChaptersTotalItems();
|
const int totalItems = getChaptersTotalItems();
|
||||||
|
const int pageItems = getChaptersPageItems(renderer.getScreenHeight() - CONTENT_START_Y - 60);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (hasSyncOption() && (chaptersSelectorIndex == 0 || chaptersSelectorIndex == totalItems - 1)) {
|
if (isSyncItem(chaptersSelectorIndex)) {
|
||||||
launchSyncActivity();
|
launchSyncActivity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int filteredIndex = chaptersSelectorIndex;
|
const int tocIndex = tocIndexFromItemIndex(chaptersSelectorIndex);
|
||||||
if (hasSyncOption()) filteredIndex -= 1;
|
const int newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex);
|
||||||
|
|
||||||
if (filteredIndex >= 0 && filteredIndex < static_cast<int>(filteredSpineIndices.size())) {
|
if (newSpineIndex == -1) {
|
||||||
onSelectSpineIndex(filteredSpineIndices[filteredIndex]);
|
onGoBack();
|
||||||
|
} else {
|
||||||
|
onSelectSpineIndex(newSpineIndex);
|
||||||
}
|
}
|
||||||
} else if (upReleased) {
|
} else if (upReleased) {
|
||||||
if (totalItems > 0) {
|
if (totalItems > 0) {
|
||||||
if (skipPage) {
|
if (skipPage) {
|
||||||
// TODO: implement page-skip navigation once page size is available
|
chaptersSelectorIndex = ((chaptersSelectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
|
||||||
|
} else {
|
||||||
|
chaptersSelectorIndex = (chaptersSelectorIndex + totalItems - 1) % totalItems;
|
||||||
}
|
}
|
||||||
chaptersSelectorIndex = (chaptersSelectorIndex + totalItems - 1) % totalItems;
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
} else if (downReleased) {
|
} else if (downReleased) {
|
||||||
if (totalItems > 0) {
|
if (totalItems > 0) {
|
||||||
if (skipPage) {
|
if (skipPage) {
|
||||||
// TODO: implement page-skip navigation once page size is available
|
chaptersSelectorIndex = ((chaptersSelectorIndex / pageItems + 1) * pageItems) % totalItems;
|
||||||
|
} else {
|
||||||
|
chaptersSelectorIndex = (chaptersSelectorIndex + 1) % totalItems;
|
||||||
}
|
}
|
||||||
chaptersSelectorIndex = (chaptersSelectorIndex + 1) % totalItems;
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,13 +145,13 @@ void EpubReaderTocActivity::loopChapters() {
|
|||||||
|
|
||||||
void EpubReaderTocActivity::loopFootnotes() {
|
void EpubReaderTocActivity::loopFootnotes() {
|
||||||
bool needsRedraw = false;
|
bool needsRedraw = false;
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||||
if (footnotesSelectedIndex > 0) {
|
if (footnotesSelectedIndex > 0) {
|
||||||
footnotesSelectedIndex--;
|
footnotesSelectedIndex--;
|
||||||
needsRedraw = true;
|
needsRedraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||||
if (footnotesSelectedIndex < footnotes.getCount() - 1) {
|
if (footnotesSelectedIndex < footnotes.getCount() - 1) {
|
||||||
footnotesSelectedIndex++;
|
footnotesSelectedIndex++;
|
||||||
needsRedraw = true;
|
needsRedraw = true;
|
||||||
@ -222,22 +223,13 @@ void EpubReaderTocActivity::renderChapters(int contentTop, int contentHeight) {
|
|||||||
if (isSyncItem(itemIndex)) {
|
if (isSyncItem(itemIndex)) {
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
|
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
|
||||||
} else {
|
} else {
|
||||||
int filteredIndex = itemIndex;
|
const int tocIndex = tocIndexFromItemIndex(itemIndex);
|
||||||
if (hasSyncOption()) filteredIndex -= 1;
|
auto item = epub->getTocItem(tocIndex);
|
||||||
|
|
||||||
if (filteredIndex >= 0 && filteredIndex < static_cast<int>(filteredSpineIndices.size())) {
|
const int indentSize = 20 + (item.level > 0 ? item.level - 1 : 0) * 15;
|
||||||
int spineIndex = filteredSpineIndices[filteredIndex];
|
const char* title = item.title.empty() ? "Unnamed" : item.title.c_str();
|
||||||
int tocIndex = this->epub->getTocIndexForSpineIndex(spineIndex);
|
const std::string chapterName = renderer.truncatedText(UI_10_FONT_ID, title, pageWidth - 40 - indentSize);
|
||||||
if (tocIndex == -1) {
|
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, displayY, "Unnamed", !isSelected);
|
|
||||||
} else {
|
|
||||||
auto item = this->epub->getTocItem(tocIndex);
|
|
||||||
const int indentSize = 20 + (item.level - 1) * 15;
|
|
||||||
const std::string chapterName =
|
|
||||||
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);
|
|
||||||
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,16 +253,6 @@ void EpubReaderTocActivity::renderFootnotes(int contentTop, int contentHeight) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderTocActivity::buildFilteredChapterList() {
|
|
||||||
filteredSpineIndices.clear();
|
|
||||||
for (int i = 0; i < this->epub->getSpineItemsCount(); i++) {
|
|
||||||
if (this->epub->shouldHideFromToc(i)) continue;
|
|
||||||
int tocIndex = this->epub->getTocIndexForSpineIndex(i);
|
|
||||||
if (tocIndex == -1) continue;
|
|
||||||
filteredSpineIndices.push_back(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EpubReaderTocActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
bool EpubReaderTocActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
||||||
|
|
||||||
bool EpubReaderTocActivity::isSyncItem(int index) const {
|
bool EpubReaderTocActivity::isSyncItem(int index) const {
|
||||||
@ -278,9 +260,14 @@ bool EpubReaderTocActivity::isSyncItem(int index) const {
|
|||||||
return index == 0 || index == getChaptersTotalItems() - 1;
|
return index == 0 || index == getChaptersTotalItems() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int EpubReaderTocActivity::tocIndexFromItemIndex(int itemIndex) const {
|
||||||
|
const int offset = hasSyncOption() ? 1 : 0;
|
||||||
|
return itemIndex - offset;
|
||||||
|
}
|
||||||
|
|
||||||
int EpubReaderTocActivity::getChaptersTotalItems() const {
|
int EpubReaderTocActivity::getChaptersTotalItems() const {
|
||||||
const int syncCount = hasSyncOption() ? 2 : 0;
|
const int syncCount = hasSyncOption() ? 2 : 0;
|
||||||
return filteredSpineIndices.size() + syncCount;
|
return epub->getTocItemsCount() + syncCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
int EpubReaderTocActivity::getChaptersPageItems(int contentHeight) const {
|
int EpubReaderTocActivity::getChaptersPageItems(int contentHeight) const {
|
||||||
|
|||||||
@ -31,7 +31,6 @@ class EpubReaderTocActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
// Chapters tab state
|
// Chapters tab state
|
||||||
int chaptersSelectorIndex = 0;
|
int chaptersSelectorIndex = 0;
|
||||||
std::vector<int> filteredSpineIndices;
|
|
||||||
|
|
||||||
// Footnotes tab state
|
// Footnotes tab state
|
||||||
int footnotesSelectedIndex = 0;
|
int footnotesSelectedIndex = 0;
|
||||||
@ -53,7 +52,6 @@ class EpubReaderTocActivity final : public ActivityWithSubactivity {
|
|||||||
void renderFootnotes(int contentTop, int contentHeight);
|
void renderFootnotes(int contentTop, int contentHeight);
|
||||||
|
|
||||||
// Chapters helpers
|
// Chapters helpers
|
||||||
void buildFilteredChapterList();
|
|
||||||
bool hasSyncOption() const;
|
bool hasSyncOption() const;
|
||||||
bool isSyncItem(int index) const;
|
bool isSyncItem(int index) const;
|
||||||
int getChaptersTotalItems() const;
|
int getChaptersTotalItems() const;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user