Skip to content

feat: slim footnotes support#1031

Merged
znelson merged 15 commits intocrosspoint-reader:masterfrom
Uri-Tauber:feat/slim-footnotes-support
Feb 26, 2026
Merged

feat: slim footnotes support#1031
znelson merged 15 commits intocrosspoint-reader:masterfrom
Uri-Tauber:feat/slim-footnotes-support

Conversation

@Uri-Tauber
Copy link
Contributor

Summary

What is the goal of this PR? Implement support for footnotes in epub files.
It is based on #553, but simplified — removed the parts which complicated the code and burden the CPU/RAM. This version supports basic footnotes and lets the user jump from location to location inside the epub.

What changes are included?
- FootnoteEntry struct — A small POD struct (number[24], href[64]) shared between parser, page storage, and UI.
- Parser: <a href> detection (ChapterHtmlSlimParser) — During a single parsing pass, internal epub links are detected and collected as footnotes. The link text is underlined to hint navigability. Bracket/whitespace normalization is applied to the display label (e.g. [1] → 1).
- Footnote-to-page assignment (ChapterHtmlSlimParser, Page) — Footnotes are attached to the exact page where their anchor word appears, tracked via a cumulative word counter during layout, surviving paragraph splits and the 750-word mid-paragraph safety flush.
- Page serialization (Page, Section) — Footnotes are serialized/deserialized per page (max 16 per page). Section cache version bumped to 14 to force a clean rebuild.
- Href → spine resolution (Epub) — resolveHrefToSpineIndex() maps an href (e.g. chapter2.xhtml#note1) to its spine index by filename matching.
- Footnotes menu + activity (EpubReaderMenuActivity, EpubReaderFootnotesActivity) — A new "Footnotes" entry in the reader menu lists all footnote links found on the current page. The user scrolls and selects to navigate.
- Navigate & restore (EpubReaderActivity) — navigateToHref() saves the current spine index and page number, then jumps to the target. The Back button restores the saved position when the user is done reading the footnote.

Additional Context

What was removed vs #553: virtual spine items (addVirtualSpineItem, isVirtualSpineItem), two-pass parsing, <aside> content extraction to temp HTML files, <p class="note"> paragraph note extraction, replaceHtmlEntities (master already has lookupHtmlEntity), footnotePages / buildFilteredChapterList, noterefCallback / Noteref struct, and the stack size increase from 8 KB to 24 KB (not needed without two-pass parsing and virtual file I/O on the render task).

Performance: Single-pass parsing. No new heap allocations in the hot path — footnote text is collected into fixed stack buffers (char[24], char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16 footnotes × 88 bytes, mirrored in currentPageFootnotes). Flash usage is unchanged at 97.4%; RAM stays at 31%.

Known limitations: When clicking a footnote, it jumps to the start of the HTML file instead of the specific anchor. This could be problematic for books that don't have separate files for each footnote. (no element-id-to-page mapping yet - will be another PR soon).


AI Usage

Did you use AI tools to help write this code? < PARTIALLY>
Claude Opus 4.6 was used to do most of the migration, I checked manually its work, and fixed some stuff, but I haven't review all the changes yet, so feedback is welcomed.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds EPUB footnote support: new FootnoteEntry type, parser capture of internal links as footnotes, page persistence and serialization for footnotes, href→spine resolver, reader save/restore and navigation for footnotes, a footnotes UI activity and menu entry, and related i18n strings.

Changes

Cohort / File(s) Summary
Footnote Type
lib/Epub/Epub/FootnoteEntry.h
Adds FootnoteEntry struct with fixed-size number and href and default ctor.
Page Persistence & Section Version
lib/Epub/Epub/Page.h, lib/Epub/Epub/Page.cpp, lib/Epub/Epub/Section.cpp
Adds Page::footnotes and Page::addFootnote; serializes/deserializes up to 16 footnotes with validation and error handling; bumps section file version 13→14.
EPUB Core Utility
lib/Epub/Epub.h, lib/Epub/Epub.cpp
Adds public helper int Epub::resolveHrefToSpineIndex(const std::string& href) const to map href (anchors stripped) to a spine index via exact or filename-only match.
HTML Parser: Footnote Detection
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h, lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
Detects internal links, accumulates link text+href into FootnoteEntry, queues pending footnotes by word index, transfers pending footnotes to pages during layout; new parser state and helpers.
Text Block Helper
lib/Epub/Epub/blocks/TextBlock.h
Adds size_t wordCount() const accessor.
I18n Keys & Translations
lib/I18n/I18nKeys.h, lib/I18n/translations/english.yaml
Removes I18nKeys.h; adds STR_FOOTNOTES, STR_NO_FOOTNOTES, and STR_LINK to English translations.
Reader: Navigation, State & Integration
src/activities/reader/EpubReaderActivity.h, src/activities/reader/EpubReaderActivity.cpp
Adds footnote context storage, navigateToHref and restoreSavedPosition helpers, depth-limited saved-position stack, integrates footnote navigation and page-state updates when loading pages.
Footnotes UI Activity
src/activities/reader/EpubReaderFootnotesActivity.h, src/activities/reader/EpubReaderFootnotesActivity.cpp
New EpubReaderFootnotesActivity listing footnotes, handling selection/back, and invoking callbacks to navigate to hrefs or return.
Menu: Footnotes Entry
src/activities/reader/EpubReaderMenuActivity.h, src/activities/reader/EpubReaderMenuActivity.cpp
Adds FOOTNOTES menu action, constructor takes hasFootnotes, introduces buildMenuItems(bool) to conditionally include FOOTNOTES, and adapts loop logic to invoke callbacks safely.

Sequence Diagram

sequenceDiagram
    participant Parser as HTML Parser
    participant Page as Page Storage
    participant EpubCore as Epub Core
    participant Reader as Reader Activity
    participant FootnotesUI as Footnotes Activity

    Note over Parser,Page: parse content and capture footnotes
    Parser->>Parser: detect internal <a>, collect text & href -> FootnoteEntry
    Parser->>Parser: enqueue pendingFootnotes (word index)
    Parser->>Page: transfer pendingFootnotes to Page during layout

    Note over Reader,FootnotesUI: user inspects footnotes
    Reader->>FootnotesUI: open list (pass page footnotes)
    FootnotesUI->>FootnotesUI: render & select footnote
    FootnotesUI->>Reader: onSelectFootnote(href)
    Reader->>Reader: push saved position (optional)
    Reader->>EpubCore: resolveHrefToSpineIndex(href)
    EpubCore-->>Reader: spine index or -1
    Reader->>Reader: navigateToHref -> load target spine/page
    Reader->>Page: load page & render

    Note over Reader: return from footnote
    Reader->>Reader: BACK (if footnoteDepth>0) -> restoreSavedPosition()
    Reader->>Page: load previous page & render
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested reviewers

  • osteotek
  • ngxson
  • daveallie
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: slim footnotes support' clearly describes the main change: adding support for footnotes in EPUB files using a simplified approach.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the footnotes feature implementation, architectural decisions, performance characteristics, and known limitations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Uri-Tauber
Copy link
Contributor Author

This PR is #988 copy.
I accidently deleted my fork, and I couldn't figure out how to reopen the PR.
Sorry about the mess.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22b96ec and e3fd6b4.

📒 Files selected for processing (16)
  • lib/Epub/Epub.cpp
  • lib/Epub/Epub.h
  • lib/Epub/Epub/FootnoteEntry.h
  • lib/Epub/Epub/Page.cpp
  • lib/Epub/Epub/Page.h
  • lib/Epub/Epub/Section.cpp
  • lib/Epub/Epub/blocks/TextBlock.h
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
  • lib/I18n/I18nKeys.h
  • lib/I18n/translations/english.yaml
  • src/activities/reader/EpubReaderActivity.cpp
  • src/activities/reader/EpubReaderActivity.h
  • src/activities/reader/EpubReaderFootnotesActivity.cpp
  • src/activities/reader/EpubReaderFootnotesActivity.h
  • src/activities/reader/EpubReaderMenuActivity.h
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:00.962Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.
📚 Learning: 2026-02-19T12:17:00.962Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:00.962Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.

Applied to files:

  • lib/Epub/Epub/blocks/TextBlock.h
  • lib/Epub/Epub/FootnoteEntry.h
  • lib/Epub/Epub.h
  • src/activities/reader/EpubReaderActivity.cpp
  • lib/Epub/Epub/Page.cpp
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
  • lib/Epub/Epub/Page.h
  • src/activities/reader/EpubReaderActivity.h
  • lib/Epub/Epub/Section.cpp
  • lib/Epub/Epub.cpp
📚 Learning: 2026-02-16T20:43:19.339Z
Learnt from: Levrk
Repo: crosspoint-reader/crosspoint-reader PR: 909
File: src/activities/reader/EpubReaderActivity.cpp:461-461
Timestamp: 2026-02-16T20:43:19.339Z
Learning: In src/activities/reader/EpubReaderActivity.cpp, when using ConfirmationActivity with enterNewActivity(), it's not necessary to call exitActivity() beforehand. The ConfirmationActivity operates as a modal dialog and cleanup is handled via the pendingSubactivityExit flag in the callback lambda. This differs from other activities like EpubReaderChapterSelectionActivity or KOReaderSyncActivity which do require exitActivity() before enterNewActivity().

Applied to files:

  • src/activities/reader/EpubReaderFootnotesActivity.cpp
  • src/activities/reader/EpubReaderFootnotesActivity.h
  • src/activities/reader/EpubReaderMenuActivity.h
  • src/activities/reader/EpubReaderActivity.cpp
  • src/activities/reader/EpubReaderActivity.h
📚 Learning: 2026-02-18T15:43:07.515Z
Learnt from: GavinHigham
Repo: crosspoint-reader/crosspoint-reader PR: 966
File: src/activities/ActivityWithSubactivity.h:20-21
Timestamp: 2026-02-18T15:43:07.515Z
Learning: In the crosspoint-reader activity architecture, when using ActivityWithSubactivity, the currentActivity global variable remains at the top-level activity (e.g., ReaderActivity) even when nested subactivities are active. The supportsOrientation() delegation chain starts from the top-level activity and stops when it reaches a subactivity that overrides the method (e.g., EpubReaderActivity returns true). Deeper nested subactivities (like EpubReaderChapterSelectionActivity or EpubReaderPercentSelectionActivity) do not need to override supportsOrientation() because they never become the currentActivity and the delegation chain doesn't reach them.

Applied to files:

  • src/activities/reader/EpubReaderFootnotesActivity.cpp
  • src/activities/reader/EpubReaderFootnotesActivity.h
  • src/activities/reader/EpubReaderMenuActivity.h
  • src/activities/reader/EpubReaderActivity.cpp
  • src/activities/reader/EpubReaderActivity.h
📚 Learning: 2026-02-19T17:46:26.871Z
Learnt from: Tritlo
Repo: crosspoint-reader/crosspoint-reader PR: 1003
File: src/activities/reader/EpubReaderActivity.cpp:657-674
Timestamp: 2026-02-19T17:46:26.871Z
Learning: In src/activities/reader/EpubReaderActivity.cpp's renderContents() method, when uncached images exist, Phase 1 intentionally calls displayBuffer(forceFullRefresh) to perform a HALF_REFRESH (if needed), while Phase 2 intentionally calls renderer.displayBuffer() directly without forceFullRefresh. This is by design: Phase 1's refresh clears the screen properly to prevent ghosting, so Phase 2 can use a faster refresh mode for better performance. The grayscale anti-aliasing is handled separately after renderContents() via displayGrayBuffer().

Applied to files:

  • src/activities/reader/EpubReaderFootnotesActivity.cpp
  • src/activities/reader/EpubReaderMenuActivity.h
📚 Learning: 2026-02-19T17:46:26.871Z
Learnt from: Tritlo
Repo: crosspoint-reader/crosspoint-reader PR: 1003
File: src/activities/reader/EpubReaderActivity.cpp:657-674
Timestamp: 2026-02-19T17:46:26.871Z
Learning: In EpubReaderActivity.cpp, renderContents() behavior is intentional: if uncached images exist, Phase 1 should call displayBuffer(forceFullRefresh) to perform a HALF_REFRESH and clear ghosting; Phase 2 should call renderer.displayBuffer() without forceFullRefresh for a faster refresh. Grayscale anti-aliasing is applied separately after renderContents() via displayGrayBuffer(). This guidance applies specifically to this file and situation.

Applied to files:

  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-12T06:57:35.955Z
Learnt from: CaptainFrito
Repo: crosspoint-reader/crosspoint-reader PR: 725
File: src/activities/network/CalibreConnectActivity.cpp:218-264
Timestamp: 2026-02-12T06:57:35.955Z
Learning: In src/activities/network/CalibreConnectActivity.cpp, button hints (« Exit) are intentionally omitted from the SERVER_STARTING and ERROR states—only the SERVER_RUNNING state displays navigation hints. This is a deliberate design decision by the author.

Applied to files:

  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-16T22:25:35.674Z
Learnt from: whyte-j
Repo: crosspoint-reader/crosspoint-reader PR: 733
File: lib/I18n/I18nKeys.h:271-272
Timestamp: 2026-02-16T22:25:35.674Z
Learning: In the crosspoint-reader i18n system (lib/I18n/), missing translation keys in non-English YAML files are automatically filled with English strings at build time by the gen_i18n.py script. All generated string arrays (STRINGS_EN, STRINGS_ES, etc.) have identical lengths, so runtime array indexing is safe without per-string fallback logic. The system prints "INFO: '{key}' missing in {lang_code}, using English fallback" during code generation when this occurs.

Applied to files:

  • lib/I18n/translations/english.yaml
🧬 Code graph analysis (6)
src/activities/reader/EpubReaderFootnotesActivity.cpp (1)
src/activities/reader/EpubReaderActivity.cpp (4)
  • loop (121-273)
  • loop (121-121)
  • render (510-645)
  • render (510-510)
src/activities/reader/EpubReaderFootnotesActivity.h (1)
src/activities/reader/EpubReaderFootnotesActivity.cpp (8)
  • onEnter (12-16)
  • onEnter (12-12)
  • onExit (18-18)
  • onExit (18-18)
  • loop (20-51)
  • loop (20-20)
  • render (53-95)
  • render (53-53)
lib/Epub/Epub.h (1)
lib/Epub/Epub.cpp (2)
  • resolveHrefToSpineIndex (850-875)
  • resolveHrefToSpineIndex (850-850)
src/activities/reader/EpubReaderActivity.cpp (3)
src/main.cpp (4)
  • exitActivity (136-142)
  • exitActivity (136-136)
  • enterNewActivity (144-147)
  • enterNewActivity (144-144)
src/activities/ActivityWithSubactivity.cpp (6)
  • exitActivity (20-27)
  • exitActivity (20-20)
  • enterNewActivity (29-34)
  • enterNewActivity (29-29)
  • requestUpdate (42-47)
  • requestUpdate (42-42)
src/activities/Activity.cpp (2)
  • requestUpdate (42-48)
  • requestUpdate (42-42)
lib/Epub/Epub/Page.cpp (2)
lib/Serialization/Serialization.h (1)
  • writePod (8-10)
src/CrossPointSettings.cpp (2)
  • file (94-100)
  • file (94-94)
src/activities/reader/EpubReaderActivity.h (1)
src/activities/reader/EpubReaderActivity.cpp (4)
  • navigateToHref (816-853)
  • navigateToHref (816-816)
  • restoreSavedPosition (855-868)
  • restoreSavedPosition (855-855)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🔇 Additional comments (21)
lib/I18n/I18nKeys.h (1)

354-356: LGTM — new StrId entries look consistent.

lib/I18n/translations/english.yaml (1)

320-322: LGTM — English strings added for new keys.

lib/Epub/Epub/blocks/TextBlock.h (1)

31-31: LGTM — accessor is simple and const-correct.

lib/Epub/Epub/Section.cpp (1)

12-12: LGTM — cache version bump is appropriate.

lib/Epub/Epub.h (1)

75-75: LGTM — public API addition is clear.

lib/Epub/Epub/Page.h (1)

59-69: LGTM — footnote storage and cap look fine.

lib/Epub/Epub/FootnoteEntry.h (1)

1-12: LGTM — simple, clear POD struct.

lib/Epub/Epub.cpp (1)

850-874: Remove concern about anchor-only hrefs; anchor-only references are already handled by the caller before resolveHrefToSpineIndex is invoked.

The navigateToHref function (EpubReaderActivity.cpp line 828) checks for anchor-only hrefs (hrefStr[0] == '#') and handles them by using the current spine index directly, so they never reach resolveHrefToSpineIndex. The remaining concern about filename-only fallback causing mis-resolution with duplicate basenames is a minor edge case—the code attempts an exact match first, and duplicate filenames across directories are uncommon in well-formed EPUBs.

src/activities/reader/EpubReaderActivity.h (1)

3-36: Footnote state scaffolding is clean and self-contained.

Also applies to: 48-50

src/activities/reader/EpubReaderMenuActivity.h (1)

15-15: FOOTNOTES menu action wiring looks consistent.

Also applies to: 42-46

lib/Epub/Epub/Page.cpp (1)

70-81: Footnote (de)serialization handles bounds and errors cleanly.

Also applies to: 107-124

src/activities/reader/EpubReaderFootnotesActivity.h (1)

1-35: Footnotes activity interface is concise and clear.

src/activities/reader/EpubReaderActivity.cpp (5)

13-13: Include for footnotes support is fine.


190-195: Back now restores saved footnote position—nice UX touch.


386-402: Footnotes menu action wires in the new activity cleanly.


636-638: Collecting page footnotes for the UI is straightforward.


855-868: Saved-position restore logic is clear and self-contained.

src/activities/reader/EpubReaderFootnotesActivity.cpp (1)

1-95: Footnotes list navigation and rendering look solid.

lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h (1)

8-10: Footnote tracking state additions are clear.

Also applies to: 68-75

lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp (2)

52-68: Footnote link detection and label normalization are well-contained.

Also applies to: 325-366, 516-528, 649-662


138-139: Cumulative word tracking and footnote-to-page assignment look consistent.

Also applies to: 802-812, 851-859

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/activities/reader/EpubReaderActivity.cpp`:
- Around line 816-853: The navigateToHref function can decrement footnoteDepth
on failure even when nothing was pushed; fix by tracking whether a push actually
occurred (e.g., introduce a local bool pushed = false) and only decrement
footnoteDepth on the error path if pushed is true; ensure the push logic around
savedPositions, section, and footnoteDepth < MAX_FOOTNOTE_DEPTH sets pushed =
true when it stores the current position so later rollback only happens if a
push was performed.

@osteotek
Copy link
Member

osteotek commented Feb 20, 2026

@Uri-Tauber I noticed that scrolling in the footnotes list doesn't overwrap (going up from the first item doesn't go to the last, etc), do you think we should fix this? Otherwise looks good

@Uri-Tauber
Copy link
Contributor Author

@Uri-Tauber I noticed that scrolling in the footnotes list doesn't overwrap (going up from the first item doesn't go to the last, etc), do you think we should fix this? Otherwise looks good

Sure. It's a small fix.
Done.

@Uri-Tauber
Copy link
Contributor Author

Small UX improvement:
The Footnotes entry in the reader menu is now shown only when the current page actually contains footnotes.

I "stole" the idea from #857, where the dictionary lookup option is hidden when no dictionary files are found on the SD card.

osteotek
osteotek previously approved these changes Feb 21, 2026
@osteotek osteotek requested a review from a team February 22, 2026 07:40
@osteotek
Copy link
Member

@Uri-Tauber new conflicts 😄

@Uri-Tauber Uri-Tauber force-pushed the feat/slim-footnotes-support branch from 6fc3fb0 to 9652d03 Compare February 23, 2026 15:02
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/activities/reader/EpubReaderActivity.cpp`:
- Around line 646-649: currentPageFootnotes can persist from a previous page
when render() fails or returns early, causing the Footnotes menu to be
incorrect; update the render() function (and any explicit early-return branches)
to reset/clear currentPageFootnotes (e.g., set to empty container/null) at the
start of render() and before every early return so footnotes reflect the
currently displayed screen rather than stale data.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9b3ef9 and 9652d03.

📒 Files selected for processing (12)
  • lib/Epub/Epub.cpp
  • lib/Epub/Epub/Page.h
  • lib/Epub/Epub/Section.cpp
  • lib/Epub/Epub/blocks/TextBlock.h
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
  • lib/I18n/I18nKeys.h
  • lib/I18n/translations/english.yaml
  • src/activities/reader/EpubReaderActivity.cpp
  • src/activities/reader/EpubReaderActivity.h
  • src/activities/reader/EpubReaderMenuActivity.cpp
  • src/activities/reader/EpubReaderMenuActivity.h
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/activities/reader/EpubReaderActivity.h
  • src/activities/reader/EpubReaderMenuActivity.cpp
  • lib/I18n/translations/english.yaml
  • lib/Epub/Epub/Section.cpp
  • lib/Epub/Epub/blocks/TextBlock.h
  • lib/Epub/Epub/Page.h
📜 Review details
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 1031
File: src/activities/reader/EpubReaderActivity.cpp:816-853
Timestamp: 2026-02-21T16:47:45.578Z
Learning: In src/activities/reader/EpubReaderActivity.cpp, the navigateToHref() function uses a fixed-size mini-stack (MAX_FOOTNOTE_DEPTH = 3) for saved positions rather than a dynamic stack. This is intentional to minimize RAM usage on ESP32-C3, which has limited memory. The design accepts that failed navigation at max depth may decrement footnoteDepth even when no push occurred, as a tradeoff for memory efficiency. Users exit by pressing Back three times maximum.
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:05.421Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.
📚 Learning: 2026-02-19T12:17:05.421Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:05.421Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.

Applied to files:

  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
  • lib/Epub/Epub.cpp
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-21T16:47:45.578Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 1031
File: src/activities/reader/EpubReaderActivity.cpp:816-853
Timestamp: 2026-02-21T16:47:45.578Z
Learning: In src/activities/reader/EpubReaderActivity.cpp, the navigateToHref() function uses a fixed-size mini-stack (MAX_FOOTNOTE_DEPTH = 3) for saved positions rather than a dynamic stack. This is intentional to minimize RAM usage on ESP32-C3, which has limited memory. The design accepts that failed navigation at max depth may decrement footnoteDepth even when no push occurred, as a tradeoff for memory efficiency. Users exit by pressing Back three times maximum.

Applied to files:

  • lib/Epub/Epub.cpp
  • src/activities/reader/EpubReaderMenuActivity.h
  • lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-22T19:13:14.049Z
Learnt from: ngxson
Repo: crosspoint-reader/crosspoint-reader PR: 1016
File: src/activities/reader/EpubReaderActivity.cpp:138-146
Timestamp: 2026-02-22T19:13:14.049Z
Learning: In src/activities/reader/EpubReaderActivity.cpp, the EpubReaderMenuActivity result callback intentionally applies orientation changes (via applyOrientation) before checking result.isCancelled. This differs from other callbacks in the file because orientation is treated as an immediate setting that should persist even when the user cancels the menu, rather than a deferred action requiring confirmation.

Applied to files:

  • src/activities/reader/EpubReaderMenuActivity.h
  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-16T20:43:19.339Z
Learnt from: Levrk
Repo: crosspoint-reader/crosspoint-reader PR: 909
File: src/activities/reader/EpubReaderActivity.cpp:461-461
Timestamp: 2026-02-16T20:43:19.339Z
Learning: In src/activities/reader/EpubReaderActivity.cpp, when using ConfirmationActivity with enterNewActivity(), it's not necessary to call exitActivity() beforehand. The ConfirmationActivity operates as a modal dialog and cleanup is handled via the pendingSubactivityExit flag in the callback lambda. This differs from other activities like EpubReaderChapterSelectionActivity or KOReaderSyncActivity which do require exitActivity() before enterNewActivity().

Applied to files:

  • src/activities/reader/EpubReaderMenuActivity.h
  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-18T15:43:12.258Z
Learnt from: GavinHigham
Repo: crosspoint-reader/crosspoint-reader PR: 966
File: src/activities/ActivityWithSubactivity.h:20-21
Timestamp: 2026-02-18T15:43:12.258Z
Learning: In the crosspoint-reader activity architecture, when using ActivityWithSubactivity, the currentActivity global variable remains at the top-level activity (e.g., ReaderActivity) even when nested subactivities are active. The supportsOrientation() delegation chain starts from the top-level activity and stops when it reaches a subactivity that overrides the method (e.g., EpubReaderActivity returns true). Deeper nested subactivities (like EpubReaderChapterSelectionActivity or EpubReaderPercentSelectionActivity) do not need to override supportsOrientation() because they never become the currentActivity and the delegation chain doesn't reach them.

Applied to files:

  • src/activities/reader/EpubReaderMenuActivity.h
  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-19T17:46:26.871Z
Learnt from: Tritlo
Repo: crosspoint-reader/crosspoint-reader PR: 1003
File: src/activities/reader/EpubReaderActivity.cpp:657-674
Timestamp: 2026-02-19T17:46:26.871Z
Learning: In EpubReaderActivity.cpp, renderContents() behavior is intentional: if uncached images exist, Phase 1 should call displayBuffer(forceFullRefresh) to perform a HALF_REFRESH and clear ghosting; Phase 2 should call renderer.displayBuffer() without forceFullRefresh for a faster refresh. Grayscale anti-aliasing is applied separately after renderContents() via displayGrayBuffer(). This guidance applies specifically to this file and situation.

Applied to files:

  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-23T06:18:08.408Z
Learnt from: raygan
Repo: crosspoint-reader/crosspoint-reader PR: 1116
File: src/activities/settings/WallabagSettingsActivity.cpp:57-75
Timestamp: 2026-02-23T06:18:08.408Z
Learning: In settings activities (e.g., KOReaderSettingsActivity, WallabagSettingsActivity), the established pattern for launching KeyboardEntryActivity is to call exitActivity() on the parent settings activity before enterNewActivity(new KeyboardEntryActivity(...)). The lambda callbacks then call exitActivity() to exit the KeyboardEntryActivity and return to the parent. This differs from ConfirmationActivity which does not require exitActivity() before enterNewActivity().

Applied to files:

  • src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-12T06:57:35.955Z
Learnt from: CaptainFrito
Repo: crosspoint-reader/crosspoint-reader PR: 725
File: src/activities/network/CalibreConnectActivity.cpp:218-264
Timestamp: 2026-02-12T06:57:35.955Z
Learning: In src/activities/network/CalibreConnectActivity.cpp, button hints (« Exit) are intentionally omitted from the SERVER_STARTING and ERROR states—only the SERVER_RUNNING state displays navigation hints. This is a deliberate design decision by the author.

Applied to files:

  • src/activities/reader/EpubReaderActivity.cpp
🧬 Code graph analysis (2)
src/activities/reader/EpubReaderMenuActivity.h (1)
src/activities/reader/EpubReaderMenuActivity.cpp (3)
  • EpubReaderMenuActivity (10-23)
  • buildMenuItems (25-38)
  • buildMenuItems (25-25)
src/activities/reader/EpubReaderActivity.cpp (3)
src/main.cpp (4)
  • exitActivity (136-142)
  • exitActivity (136-136)
  • enterNewActivity (144-147)
  • enterNewActivity (144-144)
src/activities/ActivityWithSubactivity.cpp (6)
  • exitActivity (20-27)
  • exitActivity (20-20)
  • enterNewActivity (29-34)
  • enterNewActivity (29-29)
  • requestUpdate (42-47)
  • requestUpdate (42-42)
src/activities/Activity.cpp (2)
  • requestUpdate (42-48)
  • requestUpdate (42-42)
🔇 Additional comments (19)
lib/I18n/I18nKeys.h (1)

360-363: Footnotes string IDs wired in cleanly.
Placement before the sentinel is correct and keeps enum order stable.

lib/Epub/Epub.cpp (1)

862-887: No issues found. The code correctly handles anchor-only hrefs in the navigateToHref() caller (EpubReaderActivity.cpp:865–869) by detecting hrefStr[0] == '#' and routing to the current spine index, preventing anchor-only references from ever reaching resolveHrefToSpineIndex(). Cross-file hrefs are properly resolved by the function, and -1 failures are logged and handled.

src/activities/reader/EpubReaderMenuActivity.h (2)

15-30: Good: FOOTNOTES action and hasFootnotes flag wired into the menu API.


43-47: Nice refactor to centralize menu item construction.

src/activities/reader/EpubReaderActivity.cpp (6)

13-13: Include for footnotes activity looks correct.


179-183: Menu now correctly reflects whether the current page has footnotes.


192-197: Back behavior cleanly restores the saved position for footnote navigation.


388-404: Footnotes menu action wiring is clear and consistent with the subactivity flow.


850-887: Navigation helper is clean and keeps the reader state consistent.


889-902: Saved-position restore is straightforward and well scoped.

lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h (2)

8-10: Includes are aligned with new footnote state.

Explicitly pulling in <vector> and FootnoteEntry makes the dependency clear.


71-78: Footnote tracking state looks well-scoped.

Fixed-size buffers and the pending list align with the low-RAM design goals.

lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp (7)

52-68: Helper utilities are clean and focused.

Centralizing attribute lookup and internal-link checks simplifies the parsing flow.


141-143: Good reset of cumulative word tracking.

Reinitializing wordsExtractedInBlock per text block keeps indices consistent.


452-494: Internal-link handling is solid.

Buffer flush + underline styling + href capture provides clear link behavior without extra CSS work.


648-659: Footnote label normalization is correct.

Stripping whitespace/brackets yields stable display labels like “1”.


822-835: Cumulative word index logic is correct.

Using wordsExtractedInBlock + currentTextBlock->size() preserves accurate placement after mid-paragraph flushes.
Based on learnings, “when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: wordIndex = wordsExtractedInBlock + currentTextBlock->size()”.


990-1000: Pending-footnote transfer is well-timed.

Advancing wordsExtractedInBlock before assignment ensures anchors land on the correct page.


1039-1047: Fallback footnote transfer is a good safety net.

This prevents edge-case drops at the end of a block.

@Uri-Tauber
Copy link
Contributor Author

I resolved the latest merge conflicts with master (hopefully for the last time).

I flashed the updated firmware to the device and ran validation tests to ensure everything was functioning correctly. During testing, I pressed several buttons simultaneously and encountered a crash. I attempted to reproduce the issue several times but was unable to do so.

I don’t believe the crash is related to the changes introduced in this PR, as it had not occurred previously with this branch. It may be related to other recent changes merged into master, though I might be wrong.

I will continue investigating. In the meantime, here is the serial log captured at the time of the crash:

[15:48:59] [DBG] [ERS] Progress saved: Chapter 5, Page 2
[15:48:59] [DBG] [GFX] Time = 59 ms from clearScreen to displayBuffer
[15:48:59]   Writing frame buffer to BW RAM (48000 bytes)...
[15:48:59]   BW RAM write complete (12 ms)
[15:48:59]   Powering on display 0x1C (fast refresh)...
[15:48:59]   Waiting for display refresh...
[15:49:00]   Wait complete: fast (417 ms)
[15:49:00]   Writing frame buffer to RED RAM (48000 bytes)...
[15:49:00]   RED RAM write complete (13 ms)
[15:49:00] [DBG] [GFX] Stored BW buffer in 6 chunks (8000 bytes each)
[15:49:00]   Writing frame buffer to BW RAM (48000 bytes)...
[15:49:00]   BW RAM write complete (12 ms)
[15:49:00]   Writing frame buffer to RED RAM (48000 bytes)...
[15:49:00]   RED RAM write complete (12 ms)
[15:49:00]   Loading custom LUT...
[15:49:00]   Custom LUT loaded
[15:49:00]   Powering on display 0x0C (fast refresh)...
[15:49:00]   Waiting for display refresh...
[15:49:00]   Wait complete: fast (61 ms)
[15:49:00]   Custom LUT disabled
[15:49:00]   Writing frame buffer to RED RAM (48000 bytes)...
[15:49:00]   RED RAM write complete (12 ms)
[15:49:00] [DBG] [GFX] Restored and freed BW buffer chunks
[15:49:00] [DBG] [ERS] Rendered page in 619ms
assert failed: xTaskPriorityDisinherit tasks.c:5156 (pxTCB == pxCurrentTCBs[ 0 ])
Core  0 register dump:
MEPC    : 0x40386e7c  RA      : 0x40386e40  SP      : 0x3fcb6b50  GP      : 0x3fc90a00
TP      : 0x3fcb7150  T0      : 0x37363534  T1      : 0x7271706f  T2      : 0x33323130
S0/FP   : 0x00000001  S1      : 0x3fcb6cd1  A0      : 0x3fcb6bb4  A1      : 0x3fc93721
A2      : 0x00000001  A3      : 0x00000029  A4      : 0x00000001  A5      : 0x3fca7000
A6      : 0x7a797877  A7      : 0x76757473  S2      : 0x00000076  S3      : 0x3fcb6ba8
S4      : 0x3fcb6ba8  S5      : 0x3fcb6bb4  S6      : 0x3fcb0b94  S7      : 0x000001ff
S8      : 0x000003ff  S9      : 0x3fc93d84  S10     : 0x00000020  S11     : 0x00000000
T3      : 0x6e6d6c6b  T4      : 0x6a696867  T5      : 0x66656463  T6      : 0x62613938
MSTATUS : 0x00001881  MTVEC   : 0x40380001  MCAUSE  : 0x00000002  MTVAL   : 0x00000000
MHARTID : 0x00000000
Stack memory:
3fcb6b50: 0x3fcb6b5c 0x00000000 0x3c434dd0 0x4038dc70 0x00000003 0x00000000 0x00000000 0x3fc93720
3fcb6b70: 0x0000000b 0x3fcb6c0b 0x33000005 0x36353135 0xffffff00 0x3fc93708 0x3c434dd0 0x3fc93c08
3fcb6b90: 0x3c19497e 0x3fc93718 0x3fcb6b7c 0x3fc9371c 0x3c194bfc 0x3fc93720 0x00000000 0x00000000
3fcb6bb0: 0xff000000 0x65737361 0x66207472 0x656c6961 0x78203a64 0x6b736154 0x6f697250 0x79746972
3fcb6bd0: 0x69736944 0x7265686e 0x74207469 0x736b7361 0x353a632e 0x20363531 0x54787028 0x3d204243
3fcb6bf0: 0x7870203d 0x72727543 0x54746e65 0x5b734243 0x5d203020 0x42010029 0x34cb6c80 0x30303038
3fcb6c10: 0xffffffff 0x4201a040 0x3fcb6c90 0x3fc90a00 0xe0000000 0x3fefda25 0x3300000f 0x32313631
3fcb6c30: 0x0000026b 0x00000002 0x3fc94be8 0x3fc94c2c 0x00007b78 0x00000006 0x00000007 0x00000001
3fcb6c50: 0x0000000c 0x3c3d139c 0x3fc94c2c 0x3fc94c34 0x00007bb5 0x3fc94c4c 0x3fc94e8c 0x3fca7000
3fcb6c70: 0x3c170804 0x00000011 0x3fc94be8 0xf30d81a6 0x00007bb5 0x00000002 0xffffffff 0x00000000
3fcb6c90: 0x00000000 0x00000000 0x3fcb0a10 0x40389d2e 0x3c1707a0 0x00000000 0x3fcb0b84 0x40387060
3fcb6cb0: 0x00000000 0x00000000 0x3fcb0b84 0x4038767e 0x3c1707a0 0x3fc93db8 0x00000001 0x00000000
3fcb6cd0: 0x0000000c 0x0609000a 0x05080900 0x3fc93db8 0x00000200 0x3fcb6dfc 0x00000000 0x3fc93db8
3fcb6cf0: 0x00000001 0x3fc94000 0x3fc93d60 0x42000670 0x00000001 0x3fc94000 0x3fc94000 0x420052c2
3fcb6d10: 0x00000001 0x3fc94000 0x3fc94000 0x42005802 0x3fc94000 0x3fc93f7c 0x00000012 0x420054e2
3fcb6d30: 0x00000001 0x00000000 0x00000030 0x00007c80 0x0000009e 0x00000000 0x00000000 0x3fc93db8
3fcb6d50: 0x00000001 0x00007c80 0x3fc94000 0x420057b0 0x42090000 0x4200533e 0x3fc94000 0x42005832
3fcb6d70: 0x3fcb7150 0x4005890e 0x5d800000 0x00000000 0x00007c80 0x00000000 0x3fc93da8 0x42005a7a
3fcb6d90: 0x00000020 0x00000020 0x3fcb6e84 0x42003376 0x3c173ae8 0x3fca1000 0x3fca7000 0x3c173d8c
3fcb6db0: 0x4208a424 0x4208a416 0x3fcb6e30 0x00000000 0x00000000 0x00000000 0x3fc93d84 0x00000000
3fcb6dd0: 0x00000000 0x00000002 0x00000001 0x00000000 0x3fcb6ea8 0x00000000 0x3fcb6e84 0x4200344c
3fcb6df0: 0x3fcb74ce 0x3fcb6ea8 0x00000001 0x00000000 0x3fcb6ea8 0x3fcb6f40 0x3fcb6e84 0x42004470
3fcb6e10: 0x00000000 0x00000000 0x3fcb6f40 0x3fcb6ebe 0x00000005 0x0000018f 0x00000000 0x00000000
3fcb6e30: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000
3fcb6e50: 0x00000000 0x0000018f 0x00000005 0x3fca7000 0x00000602 0x3fcb6f40 0x3fcb6e84 0x42003978
3fcb6e70: 0xa0000000 0x0000000a 0x3fcb6efc 0x3fcb74d9 0x00000000 0x00010080 0x00000000 0x3fc93d84
3fcb6e90: 0x00000000 0x00000002 0x00000000 0x00000000 0x00000000 0x00000000 0x3fcb74cd 0x3fc90a00
3fcb6eb0: 0x3fcb74d8 0x205d0000 0x0000000b 0x52430106 0x5053534f 0x2020317e 0x3fcb6f20 0xf30d81a6
3fcb6ed0: 0x3fcb74cc 0x00000602 0x3fcb74f5 0x00000602 0x3fcb74cc 0x3fc93d84 0x3fcb6f40 0x42004f74
3fcb6ef0: 0x00000005 0x0000018f 0x00000000 0x3c192000 0x3fcb74cc 0x3c191d60 0x3fcb6f30 0x4200524a
3fcb6f10: 0x00000000 0x7277204d 0x3fcb6fb4 0x42006178 0x00000000 0x00001800 0x0000002a 0x00000004
3fcb6f30: 0x3c1a9928 0x00000000 0x000003e8 0x00000029 0x00000000 0x
0000002a 0x0000002a 0x40380a66
ELF file SHA256: 243f66abf
Rebooting...

@Uri-Tauber Uri-Tauber requested a review from osteotek February 23, 2026 15:20
@ngxson
Copy link
Contributor

ngxson commented Feb 23, 2026

The best is that you can decode the stack trace using https://esphome.github.io/esp-stacktrace-decoder/

Note that the stack trace is only valid with a given firmware.elf in your build

osteotek
osteotek previously approved these changes Feb 23, 2026
@znelson
Copy link
Contributor

znelson commented Feb 24, 2026

Here's a patch that adds a test epub document:
crosspoint-reader-0bc2586-Footnotes test epub.patch

And the test epub (zipped the already zipped .epub, because GitHub doesn't accept .epub files):
test_footnotes.epub.zip

@Uri-Tauber Uri-Tauber dismissed stale reviews from znelson and osteotek via de656ed February 25, 2026 06:54
@Uri-Tauber Uri-Tauber force-pushed the feat/slim-footnotes-support branch from 52decc7 to de656ed Compare February 25, 2026 06:54
@Uri-Tauber
Copy link
Contributor Author

Just implemented the optimizations suggested by @znelson.
Thanks, @znelson, for the helpful feedback!

znelson
znelson previously approved these changes Feb 25, 2026
@osteotek osteotek requested a review from znelson February 25, 2026 15:04
int footnoteLinkDepth = -1;
char currentFootnoteLinkText[24] = {};
int currentFootnoteLinkTextLen = 0;
char currentFootnoteLinkHref[64] = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that these should better be abstracted to a StackBuffer<char, length>, see CssParser.cpp. But I'll do that in another PR

// Extract filename (remove #anchor)
std::string target = href;
size_t hashPos = target.find('#');
if (hashPos != std::string::npos) target = target.substr(0, hashPos);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits, but you can use string_view to avoid create copy of the substring

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but I have another change I'll propose soon to use string_view where possible. I'm working on some decent benchmarking measurements first, to better communicate the improvements.

private:
const std::vector<FootnoteEntry>& footnotes;
const std::function<void()> onGoBack;
const std::function<void(const char*)> onSelectFootnote;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the argument will be copied into std::string anyway, it's better to simply pass std::string here

also, note that after #1016, data passing between 2 activities will be a copy to avoid any memory safety issues

@osteotek osteotek requested a review from a team February 26, 2026 12:02
@znelson znelson merged commit 30d8a8d into crosspoint-reader:master Feb 26, 2026
6 checks passed
@Uri-Tauber Uri-Tauber deleted the feat/slim-footnotes-support branch February 26, 2026 16:03
el pushed a commit to el/crosspoint-reader that referenced this pull request Feb 26, 2026
**What is the goal of this PR?** Implement support for footnotes in epub
files.
It is based on crosspoint-reader#553, but simplified — removed the parts which
complicated the code and burden the CPU/RAM. This version supports basic
footnotes and lets the user jump from location to location inside the
epub.

**What changes are included?**
- `FootnoteEntry` struct — A small POD struct (number[24], href[64])
shared between parser, page storage, and UI.
- Parser: `<a href>` detection (`ChapterHtmlSlimParser`) — During a
single parsing pass, internal epub links are detected and collected as
footnotes. The link text is underlined to hint navigability.
Bracket/whitespace normalization is applied to the display label (e.g.
[1] → 1).
- Footnote-to-page assignment (`ChapterHtmlSlimParser`, `Page`) —
Footnotes are attached to the exact page where their anchor word
appears, tracked via a cumulative word counter during layout, surviving
paragraph splits and the 750-word mid-paragraph safety flush.
- Page serialization (`Page`, `Section`) — Footnotes are
serialized/deserialized per page (max 16 per page). Section cache
version bumped to 14 to force a clean rebuild.
- Href → spine resolution (`Epub`) — `resolveHrefToSpineIndex()` maps an
href (e.g. `chapter2.xhtml#note1`) to its spine index by filename
matching.
- Footnotes menu + activity (`EpubReaderMenuActivity`,
`EpubReaderFootnotesActivity`) — A new "Footnotes" entry in the reader
menu lists all footnote links found on the current page. The user
scrolls and selects to navigate.
- Navigate & restore (`EpubReaderActivity`) — `navigateToHref()` saves
the current spine index and page number, then jumps to the target. The
Back button restores the saved position when the user is done reading
the footnote.

  **Additional Context**

**What was removed vs crosspoint-reader#553:** virtual spine items
(`addVirtualSpineItem`, `isVirtualSpineItem`), two-pass parsing,
`<aside>` content extraction to temp HTML files, `<p class="note">`
paragraph note extraction, `replaceHtmlEntities` (master already has
`lookupHtmlEntity`), `footnotePages` / `buildFilteredChapterList`,
`noterefCallback` / `Noteref` struct, and the stack size increase from 8
KB to 24 KB (not needed without two-pass parsing and virtual file I/O on
the render task).

**Performance:** Single-pass parsing. No new heap allocations in the hot
path — footnote text is collected into fixed stack buffers (char[24],
char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16
footnotes × 88 bytes, mirrored in `currentPageFootnotes`). Flash usage
is unchanged at 97.4%; RAM stays at 31%.

**Known limitations:** When clicking a footnote, it jumps to the start
of the HTML file instead of the specific anchor. This could be
problematic for books that don't have separate files for each footnote.
(no element-id-to-page mapping yet - will be another PR soon).

---

Did you use AI tools to help write this code? _**< PARTIALLY>**_
Claude Opus 4.6 was used to do most of the migration, I checked manually
its work, and fixed some stuff, but I haven't review all the changes
yet, so feedback is welcomed.

---------

Co-authored-by: Arthur Tazhitdinov <[email protected]>
el pushed a commit to el/crosspoint-reader that referenced this pull request Feb 26, 2026
**What is the goal of this PR?** Implement support for footnotes in epub
files.
It is based on crosspoint-reader#553, but simplified — removed the parts which
complicated the code and burden the CPU/RAM. This version supports basic
footnotes and lets the user jump from location to location inside the
epub.

**What changes are included?**
- `FootnoteEntry` struct — A small POD struct (number[24], href[64])
shared between parser, page storage, and UI.
- Parser: `<a href>` detection (`ChapterHtmlSlimParser`) — During a
single parsing pass, internal epub links are detected and collected as
footnotes. The link text is underlined to hint navigability.
Bracket/whitespace normalization is applied to the display label (e.g.
[1] → 1).
- Footnote-to-page assignment (`ChapterHtmlSlimParser`, `Page`) —
Footnotes are attached to the exact page where their anchor word
appears, tracked via a cumulative word counter during layout, surviving
paragraph splits and the 750-word mid-paragraph safety flush.
- Page serialization (`Page`, `Section`) — Footnotes are
serialized/deserialized per page (max 16 per page). Section cache
version bumped to 14 to force a clean rebuild.
- Href → spine resolution (`Epub`) — `resolveHrefToSpineIndex()` maps an
href (e.g. `chapter2.xhtml#note1`) to its spine index by filename
matching.
- Footnotes menu + activity (`EpubReaderMenuActivity`,
`EpubReaderFootnotesActivity`) — A new "Footnotes" entry in the reader
menu lists all footnote links found on the current page. The user
scrolls and selects to navigate.
- Navigate & restore (`EpubReaderActivity`) — `navigateToHref()` saves
the current spine index and page number, then jumps to the target. The
Back button restores the saved position when the user is done reading
the footnote.

  **Additional Context**

**What was removed vs crosspoint-reader#553:** virtual spine items
(`addVirtualSpineItem`, `isVirtualSpineItem`), two-pass parsing,
`<aside>` content extraction to temp HTML files, `<p class="note">`
paragraph note extraction, `replaceHtmlEntities` (master already has
`lookupHtmlEntity`), `footnotePages` / `buildFilteredChapterList`,
`noterefCallback` / `Noteref` struct, and the stack size increase from 8
KB to 24 KB (not needed without two-pass parsing and virtual file I/O on
the render task).

**Performance:** Single-pass parsing. No new heap allocations in the hot
path — footnote text is collected into fixed stack buffers (char[24],
char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16
footnotes × 88 bytes, mirrored in `currentPageFootnotes`). Flash usage
is unchanged at 97.4%; RAM stays at 31%.

**Known limitations:** When clicking a footnote, it jumps to the start
of the HTML file instead of the specific anchor. This could be
problematic for books that don't have separate files for each footnote.
(no element-id-to-page mapping yet - will be another PR soon).

---

Did you use AI tools to help write this code? _**< PARTIALLY>**_
Claude Opus 4.6 was used to do most of the migration, I checked manually
its work, and fixed some stuff, but I haven't review all the changes
yet, so feedback is welcomed.

---------

Co-authored-by: Arthur Tazhitdinov <[email protected]>
iandchasse pushed a commit to iandchasse/crosspoint-reader-minRead that referenced this pull request Feb 27, 2026
**What is the goal of this PR?** Implement support for footnotes in epub
files.
It is based on crosspoint-reader#553, but simplified — removed the parts which
complicated the code and burden the CPU/RAM. This version supports basic
footnotes and lets the user jump from location to location inside the
epub.

**What changes are included?**
- `FootnoteEntry` struct — A small POD struct (number[24], href[64])
shared between parser, page storage, and UI.
- Parser: `<a href>` detection (`ChapterHtmlSlimParser`) — During a
single parsing pass, internal epub links are detected and collected as
footnotes. The link text is underlined to hint navigability.
Bracket/whitespace normalization is applied to the display label (e.g.
[1] → 1).
- Footnote-to-page assignment (`ChapterHtmlSlimParser`, `Page`) —
Footnotes are attached to the exact page where their anchor word
appears, tracked via a cumulative word counter during layout, surviving
paragraph splits and the 750-word mid-paragraph safety flush.
- Page serialization (`Page`, `Section`) — Footnotes are
serialized/deserialized per page (max 16 per page). Section cache
version bumped to 14 to force a clean rebuild.
- Href → spine resolution (`Epub`) — `resolveHrefToSpineIndex()` maps an
href (e.g. `chapter2.xhtml#note1`) to its spine index by filename
matching.
- Footnotes menu + activity (`EpubReaderMenuActivity`,
`EpubReaderFootnotesActivity`) — A new "Footnotes" entry in the reader
menu lists all footnote links found on the current page. The user
scrolls and selects to navigate.
- Navigate & restore (`EpubReaderActivity`) — `navigateToHref()` saves
the current spine index and page number, then jumps to the target. The
Back button restores the saved position when the user is done reading
the footnote.

  **Additional Context**

**What was removed vs crosspoint-reader#553:** virtual spine items
(`addVirtualSpineItem`, `isVirtualSpineItem`), two-pass parsing,
`<aside>` content extraction to temp HTML files, `<p class="note">`
paragraph note extraction, `replaceHtmlEntities` (master already has
`lookupHtmlEntity`), `footnotePages` / `buildFilteredChapterList`,
`noterefCallback` / `Noteref` struct, and the stack size increase from 8
KB to 24 KB (not needed without two-pass parsing and virtual file I/O on
the render task).

**Performance:** Single-pass parsing. No new heap allocations in the hot
path — footnote text is collected into fixed stack buffers (char[24],
char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16
footnotes × 88 bytes, mirrored in `currentPageFootnotes`). Flash usage
is unchanged at 97.4%; RAM stays at 31%.

**Known limitations:** When clicking a footnote, it jumps to the start
of the HTML file instead of the specific anchor. This could be
problematic for books that don't have separate files for each footnote.
(no element-id-to-page mapping yet - will be another PR soon).

---

Did you use AI tools to help write this code? _**< PARTIALLY>**_
Claude Opus 4.6 was used to do most of the migration, I checked manually
its work, and fixed some stuff, but I haven't review all the changes
yet, so feedback is welcomed.

---------

Co-authored-by: Arthur Tazhitdinov <[email protected]>
th0m4sek added a commit to th0m4sek/crosspoint-reader-polish that referenced this pull request Feb 28, 2026
…er#1169,crosspoint-reader#1031

feat: Auto Page Turn for Epub Reader (crosspoint-reader#1219)
fix: clarity issue with ambiguous string SET (crosspoint-reader#1169)
feat: slim footnotes support (crosspoint-reader#1031)
znelson pushed a commit that referenced this pull request Feb 28, 2026
## Summary

* **What is the goal of this PR?** Add missing strings and tweaks for
polish language
* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
lukestein pushed a commit to lukestein/crosspoint-reader that referenced this pull request Feb 28, 2026
…t-reader#1169,crosspoint-reader#1031 +tweaks (crosspoint-reader#1227)

## Summary

* **What is the goal of this PR?** Add missing strings and tweaks for
polish language
* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants