Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions bookworm/document/formats/epub.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ def get_section_at_position(self, pos):

@cached_property
def epub_html_items(self) -> tuple[str]:
items = ()
if html_items := tuple(self.epub.get_items_of_type(ebooklib.ITEM_DOCUMENT)):
items = html_items
else:
Expand All @@ -311,19 +310,34 @@ def epub_html_items(self) -> tuple[str]:
self.epub.items,
)
)
# Previously the chapters order wouldn't respect the table of content
# In most cases this is not an issue
# However this poses a problem when the chapters do not follow a conventional numeric scheme but rather use something like roman numbers
# As reported in issue 243
# We will now sort the items obtained earlier based on the position that the chapter itself occupies in the TOC
spine = [x[0].split("/")[-1] for x in self.epub.spine]
log.debug(spine)
try:
items = sorted(items, key=lambda x: spine.index(x.id))
except ValueError:
log.warn(
"Failed to order chapters based on the table of content. Order may be inconsistent"
items_by_id = {item.id: item for item in items}
ordered_items = []
ordered_item_ids = set()
unresolved_item_ids = []
for item_id, _linear in self.epub.spine:
item = items_by_id.get(item_id)
if item is None and item_id:
item = items_by_id.get(item_id.rsplit("/", 1)[-1])
if item is None:
unresolved_item_ids.append(item_id)
continue
ordered_items.append(item)
ordered_item_ids.add(item.id)
if ordered_items:
if unresolved_item_ids:
log.warning(
"Could not resolve some EPUB spine documents as HTML: %s",
unresolved_item_ids,
)
ordered_items.extend(
item
for item in items
if item.id not in ordered_item_ids and not isinstance(item, ebooklib.epub.EpubNav)
)
return tuple(ordered_items)
# Preserve support for malformed EPUBs whose spine has no usable HTML entries.
if self.epub.spine:
log.warning("Could not resolve any EPUB spine documents as HTML; using manifest order")
return items

def get_epub_html_item_by_href(self, href):
Expand Down Expand Up @@ -456,7 +470,7 @@ def add_toc_entry(self, entries, parent):

@cached_property
def html_content(self):
cache_key = f"preprocessed-html-v1:{self.uri.to_uri_string()}"
cache_key = f"preprocessed-html-v2:{self.uri.to_uri_string()}"
document_path = self.get_file_system_path()
try:
with Cache(
Expand Down
Binary file added tests/assets/bookworm-order-bug-sample.epub
Binary file not shown.
52 changes: 52 additions & 0 deletions tests/test_epub.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,58 @@ def test_chapter_order_is_unchanged_with_roman_numbers(asset):
assert spine == items


def test_chapters_follow_spine_when_navigation_document_is_not_in_spine(
asset, tmp_path, monkeypatch
):
cache_directory = tmp_path / "cache"
monkeypatch.setattr(EpubDocument, "_get_cache_directory", lambda _: cache_directory)
epub_path = Path(asset("bookworm-order-bug-sample.epub"))
uri = DocumentUri.from_filename(epub_path)
old_cache_key = f"preprocessed-html-v1:{uri.to_uri_string()}"
with Cache(cache_directory) as cache:
cache.set(old_cache_key, b"<html><body>stale cached content</body></html>")
cache_utils.set_document_modified_time(old_cache_key, epub_path, cache)

document = EpubDocument(uri)
document.read()

assert [item.id for item in document.epub_html_items] == [
item_id for item_id, _linear in document.epub.spine
]
assert document.get_content().startswith("Chapter 1")
assert "Contents" not in document.get_content()
assert "stale cached content" not in document.get_content()


def test_epub_html_items_tolerate_malformed_spine():
book = epub.EpubBook()
first = epub.EpubHtml(uid="first", file_name="first.xhtml")
second = epub.EpubHtml(uid="second", file_name="second.xhtml")
notes = epub.EpubHtml(uid="notes", file_name="notes.xhtml")
nav = epub.EpubNav()
for item in (first, second, notes, nav):
book.add_item(item)
book.spine = [("OPS/second", "yes"), ("missing", "yes"), ("first", "yes")]
document = EpubDocument(None)
document.epub = book

assert [item.id for item in document.epub_html_items] == [
"second",
"first",
"notes",
]

book.spine = [("missing", "yes")]
fallback_document = EpubDocument(None)
fallback_document.epub = book
assert [item.id for item in fallback_document.epub_html_items] == [
"first",
"second",
"notes",
"nav",
]


def test_modified_epub_modifies_cache(asset):
book = temp_book()
epub.write_epub(asset("test.epub"), book, {})
Expand Down
Loading