Search code examples
pythonepubebooklib

How to use ebooklib to create a nested chapter hierarchy?


I am trying to create an EPUB that has a nested chapter structure. There are top-level sections, with sub-chapters, and then subheadings below those sub-chapters. I would like this to be reflected in the TOC. Here is an example of what it looks like:

Section 1

Here is some preamble about what is going to happen in Chapter 1

Chapter 1

This is the contents of Section 1, Chapter 1.

Chapter 2

This is the contents of Section 1, Chapter 2.

Section 2

Preamble for section 2.

Chapter 1

Section 2, Chapter 1.

I have created this MWE that I think should represent the structure I want:

# /// script
# dependencies = [
#    "ebooklib"
# ]
# ///

from ebooklib import epub


def create_epub():
    book = epub.EpubBook()
    book.set_title("Minimal EPUB Example")
    book.set_language("en")
    book.add_author("Author Name")

    # Section 1
    section1_preamble = epub.EpubHtml(
        title="Section 1 Preamble",
        file_name="section1_preamble.xhtml",
        content="<h1>Section 1</h1><p>Preamble text...</p>",
    )
    chapter1 = epub.EpubHtml(
        title="Chapter 1",
        file_name="chapter1.xhtml",
        content="<h2>Chapter 1</h2><p>Content of Chapter 1</p>",
    )
    chapter2 = epub.EpubHtml(
        title="Chapter 2",
        file_name="chapter2.xhtml",
        content="<h2>Chapter 2</h2><p>Content of Chapter 2</p>",
    )

    # Section 2
    chapter3 = epub.EpubHtml(
        title="Chapter 1 (Section 2)",
        file_name="chapter3.xhtml",
        content="<h2>Chapter 1 (Section 2)</h2><p>Content of Chapter 1 in Section 2</p>",
    )

    # Add chapters to book
    for item in [section1_preamble, chapter1, chapter2, chapter3]:
        book.add_item(item)

    # Define table of contents with nesting
    book.toc = (
        (epub.Section("Section 1"), [section1_preamble, chapter1, chapter2]),
        (epub.Section("Section 2"), [chapter3]),
    )

    # Define book spine
    book.spine = ["nav", section1_preamble, chapter1, chapter2, chapter3]

    # Add navigation files
    book.add_item(epub.EpubNcx())
    book.add_item(epub.EpubNav())

    # Write to file
    epub.write_epub("minimal_epub.epub", book, {})
    print("EPUB created: minimal_epub.epub")


if __name__ == "__main__":
    create_epub()

However, the EPUB this creates doesn't work quite right:

Screenshot of calibre's ebook viewer with a "Destination does not exist" error

There are three problems I'm having with this:

  1. The "Section" links don't work (I get a "Destination does not exist" error)
  2. I would prefer it if the "preamble" text were not its own chapter. I want the "Section" link to be a chapter with sub-chapters.
  3. I would like it if there were not an enforced page break after each sub-chapter, and certainly not after the preamble. The way I've got it structured now, "Section 1 Preamble" points to this:

A page where the only text is "Section 1. Preamble text"

But I would like it if "Chapter 1" were a subheading here, directly under "Preamble text". There can still be enforced page breaks at the end of a section.


Solution

  • So I think I actually figured this out, and all three are solved by:

    1. Using epub.Link or a tuple of epub.Link and a tuple of epub.Link objects pointing to sub-chapters (recursively, for further hierarchies).
    2. Instead of using epub.EpubHtml items for each chapter, concatenating the chapter contents into the top-level chapter and manually adding an <a name="unique-anchor-here"> tag to the chapter heading. which is the target of the links.

    Here's an updated MWE:

    # /// script
    # dependencies = [
    #    "ebooklib"
    # ]
    # ///
    
    from ebooklib import epub
    
    
    def create_epub():
        book = epub.EpubBook()
        book.set_title("Minimal EPUB Example")
        book.set_language("en")
        book.add_author("Author Name")
    
        # Section 1
        section1 = epub.EpubHtml(
            title="Section 1",
            file_name="section1.xhtml",
            content="<a name='section1'><h1>Section 1</h1><p>Preamble text...</p>\n" +
                    "<a name='section1-chapter1'><h2>Chapter 1</h2><p>Content of Chapter 1</p>\n" +
                    "<a name='section1-chapter1'><h2>Chapter 2</h2><p>Content of Chapter 2</p>",
        )
    
        # Section 2
        chapter3 = epub.EpubHtml(
            title="Chapter 1 (Section 2)",
            file_name="chapter3.xhtml",
            content="<h2>Chapter 1 (Section 2)</h2><p>Content of Chapter 1 in Section 2</p>",
        )
    
        # Add chapters to book
        for item in [section1, chapter3]:
            book.add_item(item)
    
        # Define table of contents with Link
        book.toc = [
            (epub.Link("section1.xhtml", "Section 1", "sec1"), (
                epub.Link("section1.xhtml#section1-chapter1", "Chapter 1", "chap1"),
                epub.Link("section1.xhtml#section1-chapter2", "Chapter 2", "chap2"),
            )),
            epub.Link("chapter3.xhtml", "Chapter 1 (Section 2)", "chap3"),
        ]
    
        # Define book spine
        book.spine = ["nav", section1, chapter3]
    
        # Add navigation files
        book.add_item(epub.EpubNcx())
        book.add_item(epub.EpubNav())
    
        # Write to file
        epub.write_epub("minimal_epub_link.epub", book, {})
        print("EPUB created: minimal_epub_link.epub")
    
    
    if __name__ == "__main__":
        create_epub()