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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Table of contents display.
- Improved navigation UI with a modern sidebar layout for Table of Contents.
- Navigation controls (Previous/Next) and TOC interaction logic implemented in Python.
- Automatic navigation button state management (disabling at spine ends).
- Active Table of Contents entry highlighting.
- End-to-end tests using Playwright.
- Representative screenshot in README.md.
- Project documentation (`CONTRIBUTING.md`, `CHANGELOG.md`, `AGENTS.md`).
- Custom exception hierarchy for error handling.
- `LICENSE` file (Apache 2.0).

### Changed
- Refactored `index.html` to support a more structured layout.
- Extended `DOMElement` protocol to include `disabled` and `className` properties.
- Updated documentation for accuracy and completeness.
- Updated Pyodide version in demo to v0.29.1.

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This is the initial implementation of the `Imposition` library, a Python library intended to run under Pyodide for parsing and rendering EPUB files.

![Imposition EPUB Reader Screenshot](assets/screenshot.png)

## Installation

This package is not yet available on PyPI. To install it, you must build the wheel from the source and then install it using `pip`:
Expand Down
Binary file added assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
146 changes: 127 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,25 +1,141 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Imposition EPUB Reader</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.1/full/pyodide.js"></script>
<style>
body {
font-family: sans-serif;
margin: 0;
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f0f2f5;
}
header {
background-color: #333;
color: white;
padding: 1rem;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
header h1 {
margin: 0;
font-size: 1.5rem;
}
main {
display: flex;
flex: 1;
overflow: hidden;
padding: 1rem;
gap: 1rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
#toc {
width: 250px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow-y: auto;
padding: 1rem;
}
#toc h3 {
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
#toc ul {
list-style: none;
padding: 0;
}
#toc li {
margin-bottom: 0.5rem;
}
#toc a {
text-decoration: none;
color: #007bff;
display: block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
#toc a:hover {
background-color: #f8f9fa;
}
#toc a.active {
font-weight: bold;
color: #0056b3;
background-color: #e9ecef;
}
#viewer-container {
flex: 1;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
#viewer {
width: 100%;
height: 100%;
}
footer {
background-color: white;
padding: 1rem;
display: flex;
justify-content: center;
gap: 1rem;
border-top: 1px solid #ddd;
}
button {
padding: 0.5rem 1.5rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid #007bff;
background-color: white;
color: #007bff;
border-radius: 4px;
transition: all 0.2s;
min-width: 120px;
}
button:hover:not(:disabled) {
background-color: #007bff;
color: white;
}
button:disabled {
border-color: #ccc;
color: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body style="background-color: lightblue;">
<div id="navigation" style="display: flex; gap: 20px; align-items: flex-start;">
<div id="toc" style="width: 200px; height: 600px; overflow-y: auto; border: 1px solid black;"></div>
<div id="viewer" style="width: 800px; height: 600px; border: 1px solid black;"></div>
</div>
<body>
<header>
<h1>Imposition EPUB Reader</h1>
</header>

<main>
<div id="toc">
<h3>Contents</h3>
<!-- TOC will be injected here -->
</div>
<div id="viewer-container">
<div id="viewer"></div>
</div>
</main>

<div id="controls" style="padding-top: 10px; display: flex; gap: 10px;">
<button id="prev">Previous</button>
<button id="next">Next</button>
</div>
<footer>
<button id="prev">Previous</button>
<button id="next">Next</button>
</footer>

<script type="text/javascript">
// Provide a real pyfetch implementation for the browser
window.pyfetch = async (url) => {
const response = await fetch(url);
// pyodide.http.pyfetch returns a response with a bytes() method.
// We need to replicate that, but browser fetch returns an ArrayBuffer.
const buffer = await response.arrayBuffer();
return {
bytes: () => Promise.resolve(new Uint8Array(buffer)),
Expand All @@ -45,14 +161,6 @@
} catch (e) {
console.error("Error running Python script:", e);
}

document.getElementById("prev").onclick = () => {
window.rendition.previous_chapter();
};

document.getElementById("next").onclick = () => {
window.rendition.next_chapter();
};
}
main();
</script>
Expand Down
1 change: 1 addition & 0 deletions run_imposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ async def main() -> None:
js.window.rendition = rendition

rendition.display_toc()
rendition.setup_controls("prev", "next")
rendition.display(book.spine[0])
2 changes: 2 additions & 0 deletions src/imposition/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class DOMElement(Protocol):
onclick: Callable[[Any], None]
onload: str
src: str
disabled: bool
className: str

def appendChild(self, child: "DOMElement") -> None:
...
Expand Down
63 changes: 62 additions & 1 deletion src/imposition/rendition.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, Optional
from typing import TYPE_CHECKING, Any, Callable, Optional, List, Tuple

import xml.etree.ElementTree as ET
import base64
Expand Down Expand Up @@ -41,13 +41,51 @@ def __init__(self, book: Book, dom_adapter: DOMAdapter, target_id: str) -> None:
self.iframe.style.width = '100%'
self.iframe.style.height = '100%'
self.iframe.style.border = 'none'
self.toc_links: List[Tuple[DOMElement, str]] = []
self.prev_button: Optional[DOMElement] = None
self.next_button: Optional[DOMElement] = None

def setup_controls(self, prev_id: str, next_id: str) -> None:
"""
Sets up the navigation controls by attaching event listeners.

:param prev_id: The ID of the 'Previous' button element.
:type prev_id: str
:param next_id: The ID of the 'Next' button element.
:type next_id: str
"""
self.prev_button = self.dom_adapter.get_element_by_id(prev_id)
self.next_button = self.dom_adapter.get_element_by_id(next_id)

self.prev_button.onclick = self.dom_adapter.create_proxy(self.previous_chapter)
self.next_button.onclick = self.dom_adapter.create_proxy(self.next_chapter)

self.update_controls()

def display_toc(self) -> None:
"""
Renders the table of contents into the 'toc' element.
"""
toc_container: DOMElement = self.dom_adapter.get_element_by_id('toc')
# Preserve the <h3> header if it exists, otherwise clear
# Actually, in index.html I have <h3>Contents</h3>.
# rendition previously did: toc_container.innerHTML = ''
# Let's see if there's a specific container for the list.
# In my new index.html I didn't add a specific div for the list,
# but I can just append the ul to the toc div.
# To avoid clearing the <h3>, I could find the ul or just append.
# Let's just clear everything for now as it was before, or better,
# find where to inject.

# If I want to be safe, I'll look for a <ul> and replace it, or just clear.
# The previous implementation was:
toc_container.innerHTML = ''
# I'll add the <h3> back from Python if I clear it.
header: DOMElement = self.dom_adapter.create_element('h3')
header.textContent = "Contents"
toc_container.appendChild(header)

self.toc_links = []
ul: DOMElement = self.dom_adapter.create_element('ul')

for item in self.book.toc:
Expand All @@ -66,10 +104,12 @@ def handler(event: DOMElement) -> None:
# Create a proxy for the onclick event handler
a.onclick = self.dom_adapter.create_proxy(create_handler(item['url']))

self.toc_links.append((a, item['url']))
li.appendChild(a)
ul.appendChild(li)

toc_container.appendChild(ul)
self.update_controls()


def display(self, chapter_url: Optional[str] = None) -> None:
Expand All @@ -95,6 +135,9 @@ def display(self, chapter_url: Optional[str] = None) -> None:
else:
chapter_href = self.book.spine[0]

if chapter_href in self.book.spine:
self.current_chapter_index = self.book.spine.index(chapter_href)

chapter_content: bytes = self.book.zip_file.read(chapter_href)

try:
Expand Down Expand Up @@ -134,6 +177,7 @@ def display(self, chapter_url: Optional[str] = None) -> None:

self.target_element.innerHTML = ''
self.target_element.appendChild(self.iframe)
self.update_controls()

def _embed_asset(self, element: ET.Element, attribute: str, chapter_path: str) -> None:
asset_path: Optional[str] = element.get(attribute)
Expand All @@ -157,6 +201,23 @@ def _embed_asset(self, element: ET.Element, attribute: str, chapter_path: str) -
print(f"Asset not found: {full_asset_path}")
pass

def update_controls(self) -> None:
"""
Updates the state of navigation buttons and TOC highlighting.
"""
if self.prev_button:
self.prev_button.disabled = (self.current_chapter_index == 0)
if self.next_button:
self.next_button.disabled = (self.current_chapter_index >= len(self.book.spine) - 1)

current_href = self.book.spine[self.current_chapter_index]
for a, url in self.toc_links:
chapter_url = url.split('#')[0]
if chapter_url == current_href:
a.className = 'active'
else:
a.className = ''

def next_chapter(self, event: Optional[Any] = None) -> None:
"""
Displays the next chapter in the book's spine.
Expand Down
2 changes: 2 additions & 0 deletions tests/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def __init__(self, tag_name: str) -> None:
self.onclick: Optional[Callable[[Any], None]] = None
self.onload: str = ""
self.src: str = ""
self.disabled: bool = False
self.className: str = ""
self.preventDefault: Mock = Mock()

def appendChild(self, child: "MockDOMElement") -> None:
Expand Down
Binary file modified tests/screenshots/test_renders_first_chapter_passed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading