Skip to content

fix(docreader): read EPUB chapters in spine order - #3977

Open
L4XB wants to merge 1 commit into
Tencent:mainfrom
L4XB:fix/epub-spine-order
Open

L4XB wants to merge 1 commit into
Tencent:mainfrom
L4XB:fix/epub-spine-order

Conversation

@L4XB

@L4XB L4XB commented Oct 5, 2026

Copy link
Copy Markdown

Description

EPUBParser._extract_content meant to order chapters by the table of contents, but that branch never runs:

  • book.get_table_of_contents() does not exist on ebooklib's EpubBook (checked on ebooklib 0.20, the version in docreader/uv.lock), so the AttributeError is caught and toc is always [].
  • When reading a file, ebooklib returns TOC entries as epub.Link objects, which have no get_name(), so the loop would match nothing either way.

Every book therefore falls back to book.get_items(), which is manifest order. The manifest may list files in any order. The spine is the reading order. Real books differ:

book manifest order (what the builtin engine reads) spine order
Standard Ebooks, Pride and Prejudice (src/epub/content.opf) chapters 1 to 61, colophon, imprint, titlepage, uncopyright titlepage, imprint, chapters 1 to 61, colophon, uncopyright
IDPF epub3-samples, moby-dick copyright, titlepage, cover, preface ... cover, titlepage, toc-short, preface ...
IDPF epub3-samples, accessible_epub_3 index, pr01, ... spi-ad, index, pr01, ...

The fix reads the documents in spine order and then appends any document the spine leaves out, in manifest order. The set of documents read stays the same. Only their order changes, and the dead TOC code goes. The ZIP fallback for EPUBs that ebooklib cannot open is untouched.

This affects the builtin DocReader engine, which handles EPUB when it is selected for a knowledge base or when the server is built without anydoc (preferAnydocWhenAvailable routes EPUB to anydoc when it is linked). #3924 also edits this file, but only the image-alias helpers further down, so the two changes do not overlap.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 📚 Documentation update
  • 🎨 Refactor
  • ⚡ Performance improvement
  • 🧪 Test
  • 🔧 Configuration / Build / CI

Related Issue

None filed.

Testing

Environment: uv sync --project docreader --locked --no-dev --python 3.10.18 (as in docreader.yml), macOS.

New tests in docreader/tests/test_epub_parser.py build EPUBs with ebooklib whose manifest order differs from the spine:

  • test_chapters_follow_the_spine_not_the_manifest: manifest three, title, one, two, spine title, one, two, three, plus an SVG cover in the spine that must stay out.
  • test_documents_outside_the_spine_follow_it: a document missing from the spine is still read, after the spine.
  • test_a_document_listed_twice_in_the_spine_is_read_once.

With main's parser the first two fail (AssertionError: Lists differ: [44, 72, 98, 14] != [14, 44, 72, 98] and [74, 48, 17] != [17, 48, 74]). Five mutants of the new function each fail at least one test: no spine loop, spine reversed, documents outside the spine dropped, the document filter removed (the SVG gets in), and no de-duplication.

python -m unittest discover -s docreader/tests -p "test_epub_parser.py"   # Ran 7 tests, OK
python -m unittest discover -s docreader/tests -p "test_*.py"             # Ran 266 tests, 13 skipped, 1 error

The one error is test_ssrf_proxy.TestSSRFProxy.test_webkit_redirects_and_subresources_use_proxy, which needs the Playwright WebKit browser that CI installs and this machine does not have. It is unrelated to this change.

Checklist

  • git diff --check origin/main...HEAD passes
  • Changed source files are formatted (no Python formatter is configured for docreader; the new code follows the file's existing style)
  • Targeted tests for the changed packages/components pass
  • Diff-scoped lint passes where applicable (for Go: golangci-lint run --new-from-rev=origin/main ./...) (no Go changes)
  • Full-repository checks were run, or any unrelated/environment-dependent failures are documented above
  • Self-reviewed the code
  • Added/updated tests covering the change
  • Updated related documentation (README, website-docs/, Swagger annotations, etc.)
  • Breaking changes are clearly called out in the description above

EPUBParser took the chapter order from book.get_table_of_contents(),
which ebooklib's EpubBook does not have. The AttributeError was caught,
so every book fell through to get_items(), which follows the manifest.
The manifest may list files in any order; the spine is the reading order.
Standard Ebooks' Pride and Prejudice, for example, lists its title page
and imprint after chapter 61 in the manifest.

Read the documents in spine order, followed by any the spine leaves out
in manifest order, so the set of documents read is unchanged.
Copilot AI balanced review requested due to automatic review settings October 5, 2026 15:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
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.

2 participants