Commits

Anonymous committed f6ffe65 Merge

merge with epub fork

Comments (0)

Files changed (16)

 	@echo "  dirhtml   to make HTML files called index.html in directories"
 	@echo "  pickle    to make pickle files"
 	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  epub      to make an epub file"
 	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 	@echo "  changes   to make an overview over all changed/added/deprecated items"
 	@echo "  linkcheck to check all external links for integrity"
 	@echo "To view the help collection:"
 	@echo "# assistant -collectionFile _build/qthelp/Sphinx.qhc"
 
+epub:
+	mkdir -p _build/epub _build/doctrees
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) _build/epub
+	@echo
+	@echo "Build finished. The epub file is in _build/epub."
 latex:
 	mkdir -p _build/latex _build/doctrees
 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex

doc/_templates/layout.html

 {% extends "!layout.html" %}
 
 {% block extrahead %}
+{%- if not embedded %}
 <style type="text/css">
   table.right { float: right; margin-left: 20px; }
   table.right td { border: 1px solid #ccc; }
 </style>
+{% endif %}
 {% endblock %}
 
 {% block rootrellink %}
 
    Its name is ``devhelp``.
 
+.. module:: sphinx.builders.epub
+.. class:: EpubBuilder
+
+   This builder produces the same output as the standalone HTML builder, but
+   also generates an *epub* file for ebook readers.
+   This builder is meant to be used together with the
+   :confval:`html_theme` ``'epub'``.
+   See `<http://www.idpf.org/specs.htm>`_ or
+   `<http://en.wikipedia.org/wiki/EPUB>`_ for the definition of epubs.
+
+   Its name is ``epub``.
+
 .. module:: sphinx.builders.latex
 .. class:: LaTeXBuilder
 
 show_authors = True
 
 # The HTML template theme.
-html_theme = 'sphinxdoc'
+html_theme = 'epub'
 
 # A list of ignored prefixes names for module index sorting.
 modindex_common_prefix = ['sphinx.']
 # Output file base name for HTML help builder.
 htmlhelp_basename = 'Sphinxdoc'
 
+# Epub fields
+epub_basename = 'sphinx'
+epub_author = 'Georg  Brandl'
+epub_publisher = 'http://sphinx.pocoo.org/'
+epub_scheme = 'url'
+epub_identifier = epub_publisher
+epub_pre_files = [ ('index', 'Welcome')]
+epub_exclude_files = ['_static/opensearch.xml', '_static/doctools.js',
+    '_static/jquery.js', '_static/searchtools.js',
+    '_static/basic.css', 'search.html']
+
+
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [('contents', 'sphinx.tex', 'Sphinx Documentation',
    Output file base name for HTML help builder.  Default is ``'pydoc'``.
 
 
+.. _epub-options:
+
+Options for epub output
+-----------------------
+
+These options influence the epub output. As this writer derives from the
+HTMLWriter the HTML options also apply where appropriate.
+The actual values for some of the options is not really important,
+they just have to be entered into
+the `Dublin Core metadata <http://dublincore.org/>`_.
+
+.. confval:: epub_basename
+
+  The basename for the epub file. It defaults to the :confval:`project` name.
+
+.. confval:: epub_title
+
+  The title of the document.
+  It defaults to the :confval:`html_title` option
+  but can be set independently for epub creation.
+
+.. confval:: epub_author
+
+  The author of the document.
+  This is put in the Dublin Core metadata.
+  The default value is ``'unknown'``.
+
+.. confval:: epub_language
+
+  The language of the document.
+  This is put in the Dublin Core metadata.
+  The default is the :confval:`language` option or ``'en'`` if unset.
+
+.. confval:: epub_publisher
+
+  The publisher of the document.
+  This is put in the Dublin Core metadata.
+  You may use any sensible string, e.g. the project homepage.
+  The default value is ``'unknown'``.
+
+.. confval:: epub_copyright
+
+  The copyright of the document.
+  It defaults to the :confval:`copyright` option
+  but can be set independently for epub creation.
+
+.. confval:: epub_identifier
+
+  An identifier for the document.
+  This is put in the Dublin Core metadata.
+  For published documents this is the ISBN number, but you can
+  also use an alternative scheme, e.g. the project homepage.
+  The default value is ``'unknown'``.
+
+.. confval:: epub_scheme
+
+  The publication scheme for the :confval:`epub_identifier`.
+  This is put in the Dublin Core metadata.
+  For published books the scheme is ``'ISBN'``.
+  If you use the project homepage, ``'URL'`` seems reasonable.
+
+.. confval:: epub_uid
+
+  A unique identifier for the document.
+  This is put in the Dublin Core metadata.
+  You may use a random string.
+  The default value is ``'unknown'``.
+
+.. confval:: epub_pre_files
+
+  Additional files that should be inserted before the text generated by
+  Sphinx. It is a list of tuples containing the file name and the title.
+  Example::
+
+      epub_pre_files = [
+         ('index.html', 'Welcome'),
+      ]
+
+  The default value is ``[]``.
+
+.. confval:: epub_post_files
+
+  Additional files that should be inserted after the text generated by
+  Sphinx. It is a list of tuples containing the file name and the title.
+  The default value is ``[]``.
+
+.. confval:: epub_exclude_files
+
+  A list of files that are generated/copied in the build directory
+  but should not be included in the epub file.
+  The default value is ``[]``.
+
+
 .. _latex-options:
 
 Options for LaTeX output
    Sphinx HTML output.
 
 
+Epub
+----
+
+The EpubBuilder is currently in an experimental stage.
+It has only been tested with the Sphinx documentation itself.
+If you want to create epubs, here are some notes:
+
+* Split the text into several files. The longer the individual HTML files
+  are, the longer it takes the ebook reader to render them.
+  In extreme cases, the rendering can take up to one minute.
+
+* Try to minimize the markup. This also pays in rendering time.
+
+* For some readers you can use embedded or external fonts
+  using the CSS ``@font-face`` directive.
+  This is *extremely* useful for code listings which are often cut
+  at the right margin. The default Courier font (or variant) is quite
+  wide and you can only display up to 60 characters on a line.
+  If you replace it with a narrower font, you can get more characters
+  on a line. You may even use
+  `fontforge <http://fontforge.sourceforge.net/>`_
+  and create narrow variants
+  of some free font. In my case I get up to 70 characters on a line.
+
+  You may have to experiment a little until you get reasonable results.
+
+* Test the created epubs. You can use several alternatives.
+  The ones I am aware of are
+  Epubcheck
+  (`http://code.google.com/p/epubcheck/
+  <http://code.google.com/p/epubcheck/>`_),
+  Calibre 
+  (`http://calibre-ebook.com/ <http://calibre-ebook.com/>`_),
+  FBreader (`http://www.fbreader.org/ <http://www.fbreader.org/>`_,
+  although it does not render the CSS), and
+  Bookworm (`http://bookworm.oreilly.com/ <http://bookworm.oreilly.com/>`_).
+  For bookworm you can download the source from
+  `http://code.google.com/p/threepress/ <http://code.google.com/p/threepress/>`_
+  and run you own local server.
+
+
 .. _api role: http://git.savannah.gnu.org/cgit/kenozooid.git/tree/doc/extapi.py
 .. _xhtml to reST: http://docutils.sourceforge.net/sandbox/xhtml2rest/xhtml2rest.py
 * **traditional** -- A theme resembling the old Python documentation.  There are
   currently no options beyond *nosidebar*.
 
+* **epub** -- A theme for the epub formatter. There are currently no
+  options. This theme tries to reduce visual space which is a sparse
+  resource on ebook readers.
+
 
 Creating themes
 ---------------

sphinx/builders/__init__.py

     'htmlhelp':  ('htmlhelp', 'HTMLHelpBuilder'),
     'devhelp':   ('devhelp', 'DevhelpBuilder'),
     'qthelp':    ('qthelp', 'QtHelpBuilder'),
+    'epub':      ('epub', 'EpubBuilder'),
     'latex':     ('latex', 'LaTeXBuilder'),
     'text':      ('text', 'TextBuilder'),
     'changes':   ('changes', 'ChangesBuilder'),

sphinx/builders/epub.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.builders.epub
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Build epub files.
+    Originally derived from qthelp.py.
+
+    :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import os
+import re
+import codecs
+from os import path
+import zipfile
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.builders.html import StandaloneHTMLBuilder
+
+
+# (Fragment) templates from which the metainfo files
+# content.opf, toc.ncx, mimetype, and META-INF/container.xml
+# are created.
+
+_mimetype_template = 'application/epub+zip' # no EOL!
+
+_container_template = u'''\
+<?xml version="1.0" encoding="UTF-8"?>
+<container version="1.0"
+      xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
+  <rootfiles>
+    <rootfile full-path="content.opf"
+        media-type="application/oebps-package+xml"/>
+  </rootfiles>
+</container>
+'''
+
+_toc_template = u'''\
+<?xml version="1.0"?>
+<ncx version="2005-1" xmlns="http://www.daisy.org/z3986/2005/ncx/">
+  <head>
+    <meta name="dtb:uid" content="%(uid)s"/>
+    <meta name="dtb:depth" content="%(level)d"/>
+    <meta name="dtb:totalPageCount" content="0"/>
+    <meta name="dtb:maxPageNumber" content="0"/>
+  </head>
+  <docTitle>
+    <text>%(title)s</text>
+  </docTitle>
+  <navMap>
+%(navpoints)s
+  </navMap>
+</ncx>
+'''
+
+_navpoint_template = u'''\
+%(indent)s  <navPoint id="%(navpoint)s" playOrder="%(playorder)d">
+%(indent)s    <navLabel>
+%(indent)s      <text>%(text)s</text>
+%(indent)s    </navLabel>
+%(indent)s    <content src="%(refuri)s" />
+%(indent)s  </navPoint>'''
+
+_navpoint_indent = '  '
+_navPoint_template = 'navPoint%d'
+
+_content_template = u'''\
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://www.idpf.org/2007/opf" version="2.0"
+      unique-identifier="%(uid)s">
+  <metadata xmlns:opf="http://www.idpf.org/2007/opf"
+        xmlns:dc="http://purl.org/dc/elements/1.1/">
+    <dc:language>%(lang)s</dc:language>
+    <dc:title>%(title)s</dc:title>
+    <dc:creator opf:role="aut">%(author)s</dc:creator>
+    <dc:publisher>%(publisher)s</dc:publisher>
+    <dc:rights>%(copyright)s</dc:rights>
+    <dc:identifier id="%(uid)s" opf:scheme="%(scheme)s">%(id)s</dc:identifier>
+  </metadata>
+  <manifest>
+    <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" />
+%(files)s
+  </manifest>
+  <spine toc="ncx">
+%(spine)s
+  </spine>
+</package>
+'''
+
+_file_template = u'''\
+    <item id="%(id)s"
+          href="%(href)s"
+          media-type="%(media_type)s" />'''
+
+_spine_template = u'''\
+    <itemref idref="%(idref)s" />'''
+
+_toctree_template = u'toctree-l%d'
+
+_media_types = {
+    '.html': 'application/xhtml+xml',
+    '.css': 'text/css',
+    '.png': 'image/png',
+    '.gif': 'image/gif',
+    '.svg': 'image/svg+xml',
+    '.jpg': 'image/jpeg',
+    '.jpeg': 'image/jpeg',
+    '.otf': 'application/x-font-otf',
+    '.ttf': 'application/x-font-ttf',
+}
+
+
+# The epub publisher
+
+class EpubBuilder(StandaloneHTMLBuilder):
+    """Builder that outputs epub files.
+    It creates the metainfo files
+    container.opf, toc.ncx, mimetype, and META-INF/container.xml.
+    Afterwards, all necessary files are zipped to an epub file.
+    """
+    name = 'epub'
+
+    # don't copy the reST source
+    copysource = False
+    supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
+                             'image/jpeg']
+
+    # don't add links
+    add_permalinks = False
+    # don't add sidebar etc.
+    embedded = True
+
+    def init(self):
+        StandaloneHTMLBuilder.init(self)
+        # the output files for HTML help must be .html only
+        self.out_suffix = '.html'
+        self.playorder = 0
+
+
+    # generic support functions
+    def make_id(self, name):
+        """Replace all characters not allowed for (X)HTML ids"""
+        return name.replace('/', '_')
+
+    def esc(self, name):
+        """Replace all characters not allowed in text an attribute values"""
+        # Like cgi.escape, but also replace apostrophe
+        name = name.replace('&', '&amp;')
+        name = name.replace('<', '&lt;')
+        name = name.replace('>', '&gt;')
+        name = name.replace('"', '&quot;')
+        name = name.replace('\'', '&apos;')
+        return name
+
+    def collapse_text(self, doctree, result):
+       """Remove all HTML markup and return only the text nodes"""
+       for c in doctree.children:
+            if isinstance(c, nodes.Text):
+                result.append(c.data)
+            else:
+                result = self.collapse_text(c, result)
+       return result
+
+    def get_refnodes(self, doctree, result):
+        """Collect section titles, their depth in the toc and the refuri"""
+        # XXX: is there a betterr way than checking the attribute
+        # toctree-l[1-6] on the parent node?
+        if isinstance(doctree, nodes.reference):
+            classes = doctree.parent.attributes['classes']
+            level = 1
+            for l in range(5,0,-1): # or range(1,6)?
+                if (_toctree_template % l) in classes:
+                    level = l
+            result.append({
+                'level': level,
+                'refuri': self.esc(doctree['refuri']),
+                'text': self.esc(''.join(self.collapse_text(doctree, [])))
+            })
+        else:
+            for elem in doctree.children:
+                result = self.get_refnodes(elem, result)
+        return result
+
+    def get_toc(self):
+        """Get the total table of contents, containg the master_doc
+        and pre and post files not managed by sphinx"""
+        doctree = self.env.get_and_resolve_doctree(self.config.master_doc, self)
+        self.refnodes = self.get_refnodes(doctree, [])
+        self.refnodes.insert(0, {
+            'level': 1,
+            'refuri': self.esc(self.config.master_doc + '.html'),
+            'text': self.esc(''.join(self.collapse_text(
+                self.env.titles[self.config.master_doc], []
+            ))),
+        })
+        # XXX: is reversed ok?
+        for file, text in reversed(self.config.epub_pre_files):
+            self.refnodes.insert(0, {
+                'level': 1,
+                'refuri': self.esc(file + '.html'),
+                'text': self.esc(text)
+            })
+        for file, text in self.config.epub_post_files:
+            self.refnodes.append({
+                'level': 1,
+                'refuri': self.esc(file + '.html'),
+                'text': self.esc(text)
+            })
+
+
+    # Finish by building the epub file
+    def handle_finish(self):
+        """Create the metainfo files and finally the epub"""
+        self.get_toc()
+        self.build_mimetype(self.outdir, 'mimetype')
+        self.build_container(self.outdir, 'META-INF/container.xml')
+        self.build_content(self.outdir, 'content.opf')
+        self.build_toc(self.outdir, 'toc.ncx')
+        self.build_epub(self.outdir, self.config.epub_basename + '.epub')
+
+    def build_mimetype(self, outdir, outname):
+        """Write the metainfo file mimetype"""
+        self.info('writing %s file...' % outname)
+        f = codecs.open(path.join(outdir, outname), 'w', 'utf-8')
+        try:
+            f.write(_mimetype_template)
+        finally:
+            f.close()
+
+    def build_container(self, outdir, outname):
+        """Write the metainfo file META-INF/cointainer.xml"""
+        self.info('writing %s file...' % outname)
+        fn = path.join(outdir, outname)
+        try:
+            os.mkdir(path.dirname(fn))
+        except OSError, err:
+            if err.errno != os.errno.EEXIST:
+                raise
+        f = codecs.open(path.join(outdir, outname), 'w', 'utf-8')
+        try:
+            f.write(_container_template)
+        finally:
+            f.close()
+
+    def content_metadata(self, files, spine):
+        """Create a dictionary with all metadata for the content.opf
+        file properly escaped"""
+        metadata = {}
+        metadata['title'] = self.esc(self.config.epub_title)
+        metadata['author'] = self.esc(self.config.epub_author)
+        metadata['uid'] = self.esc(self.config.epub_uid)
+        metadata['lang'] = self.esc(self.config.epub_language)
+        metadata['publisher'] = self.esc(self.config.epub_publisher)
+        metadata['copyright'] = self.esc(self.config.epub_copyright)
+        metadata['scheme'] = self.esc(self.config.epub_scheme)
+        metadata['id'] = self.esc(self.config.epub_identifier)
+        metadata['files'] = files
+        metadata['spine'] = spine
+        return metadata
+
+    def build_content(self, outdir, outname):
+        """Write the metainfo file content.opf
+        It contains bibliographic data, a file list and
+        the spine (the reading order)."""
+        self.info('writing %s file...' % outname)
+
+        # files
+        if not outdir.endswith(os.sep):
+            outdir += os.sep
+        olen = len(outdir)
+        projectfiles = []
+        self.files = []
+        self.ignored_files = ['.buildinfo',
+            'mimetype', 'content.opf', 'toc.ncx', 'META-INF/container.xml',
+            self.config.epub_basename + '.epub'] + \
+            self.config.epub_exclude_files
+        for root, dirs, files in os.walk(outdir):
+            for fn in files:
+                filename = path.join(root, fn)[olen:]
+                if filename in self.ignored_files:
+                    # self.warn("ignoring %s" % filename)
+                    continue
+                ext = path.splitext(filename)[-1]
+                if ext not in _media_types:
+                    self.warn("unknown mimetype for %s, ignoring" % filename)
+                    continue
+                projectfiles.append(_file_template % {
+                    'href': self.esc(filename),
+                    'id': self.esc(self.make_id(filename)),
+                    'media_type': self.esc(_media_types[ext])
+                })
+                self.files.append(filename)
+        projectfiles = '\n'.join(projectfiles)
+
+        # spine
+        spine = []
+        for item in self.refnodes:
+            if '#' in item['refuri']:
+                continue
+            if item['refuri'] in self.ignored_files:
+                continue
+            spine.append(_spine_template % {
+                'idref': self.esc(self.make_id(item['refuri']))
+            })
+        spine = '\n'.join(spine)
+
+        # write the project file
+        f = codecs.open(path.join(outdir, outname), 'w', 'utf-8')
+        try:
+            f.write(_content_template % \
+                self.content_metadata(projectfiles, spine))
+        finally:
+            f.close()
+
+    def new_navpoint(self, node, level, incr=True):
+        """Create a new entry in the toc from the node at given level"""
+        # XXX Modifies the node
+        if incr:
+            self.playorder += 1
+        node['indent'] = _navpoint_indent * level
+        node['navpoint'] = self.esc(_navPoint_template % self.playorder)
+        node['playorder'] = self.playorder
+        return _navpoint_template % node
+
+    def insert_subnav(self, node, subnav):
+        """Insert nested navpoints for given node
+        The node and subnav are already rendered to text"""
+        nlist = node.split('\n')
+        nlist.insert(-1, subnav)
+        return '\n'.join(nlist)
+
+    def build_navpoints(self, nodes):
+        """Create the toc navigation structure
+        Subelements of a node are nested inside the navpoint.
+        For nested nodes the parent node is reinserted in the subnav."""
+        navstack = []
+        navlist = []
+        level = 1
+        lastnode = None
+        for node in nodes:
+            file = node['refuri'].split('#')[0]
+            if file in self.ignored_files:
+                continue
+            if node['level'] == level:
+                navlist.append(self.new_navpoint(node,level))
+            elif node['level'] == level + 1:
+                navstack.append(navlist)
+                navlist = []
+                level += 1
+                if lastnode:
+                    # Insert starting point in subtoc with same playOrder
+                    navlist.append(self.new_navpoint(lastnode, level, False))
+                navlist.append(self.new_navpoint(node, level))
+            else:
+                while node['level'] < level:
+                    subnav = '\n'.join(navlist)
+                    navlist = navstack.pop()
+                    navlist[-1] = self.insert_subnav(navlist[-1], subnav)
+                    level -= 1
+                navlist.append(self.new_navpoint(node, level))
+            lastnode = node
+        while level != 1:
+            subnav = '\n'.join(navlist)
+            navlist = navstack.pop()
+            navlist[-1] = self.insert_subnav(navlist[-1], subnav)
+            level -= 1
+        return '\n'.join(navlist)
+
+    def toc_metadata(self, level, navpoints):
+        """Create a dictionary with all metadata for the toc.ncx
+        file properly escaped"""
+        metadata = {}
+        metadata['uid'] = self.config.epub_uid
+        metadata['title'] = self.config.epub_title
+        metadata['level'] = level
+        metadata['navpoints'] = navpoints
+        return metadata
+
+    def build_toc(self, outdir, outname):
+        """Write the metainfo file toc.ncx"""
+        self.info('writing %s file...' % outname)
+
+        navpoints = self.build_navpoints(self.refnodes)
+        level = max(item['level'] for item in self.refnodes)
+        f = codecs.open(path.join(outdir, outname), 'w', 'utf-8')
+        try:
+            f.write(_toc_template % self.toc_metadata(level, navpoints))
+        finally:
+            f.close()
+
+    def build_epub(self, outdir, outname):
+        """Write the epub file
+        It is a zip file with the mimetype file stored uncompressed
+        as the first entry."""
+        self.info('writing %s file...' % outname)
+        projectfiles = ['META-INF/container.xml', 'content.opf', 'toc.ncx'] \
+            + self.files
+        epub = zipfile.ZipFile(path.join(outdir, outname), 'w', \
+            zipfile.ZIP_DEFLATED)
+        epub.write(path.join(outdir, 'mimetype'), 'mimetype', \
+            zipfile.ZIP_STORED)
+        for file in projectfiles:
+            epub.write(path.join(outdir, file), file, zipfile.ZIP_DEFLATED)
+        epub.close()
+
         latex_docclass = ({}, None),
         # now deprecated - use latex_elements
         latex_preamble = ('', None),
+
+        # Epub options
+        epub_basename = (lambda self: make_filename(self.project), None),
+        epub_title = (lambda self: self.html_title, 'html'),
+        epub_author = ('unknown', 'html'),
+        epub_language = (lambda self: self.language or 'en', 'html'),
+        epub_publisher = ('unknown', 'html'),
+        epub_copyright = (lambda self: self.copyright, 'html'),
+        epub_identifier = ('unknown', 'html'),
+        epub_scheme = ('unknown', 'html'),
+        epub_uid = ('unknown', 'env'),
+        epub_pre_files = ([], 'env'),
+        epub_post_files = ([], 'env'),
+        epub_exclude_files = ([], 'env'),
     )
 
     def __init__(self, dirname, filename, overrides, tags):

sphinx/quickstart.py

 
 # If false, no module index is generated.
 #latex_use_modindex = True
+
+
+# -- Options for Epub output ---------------------------------------------------
+
+# Please also set the html_theme to 'epub' or any other approriate theme.
+# The display size is quite small for ebook readers.
+# The default themes may take too much space.
+
+# bibliographic Dublin Core description of the content.opf and
+# in the toc.ncx file. It defaults to the html_title option.
+#epub_title = ''
+
+# The author of the text. The author is inserted into the
+# bibliographic Dublin Core description of the content.opf file.
+#epub_author = ''
+
+# The language of the text. It defaults to the language option
+# or en if the language is not set.
+#epub_language = ''
+
+# The publisher of the text. The publisher is inserted  into the
+# bibliographic Dublin Core description of the content.opf file.
+# You may use the project homepage.
+#epub_publisher = ''
+
+# The copyright of the text. The copyright is inserted into the
+# bibliographci Dublin Core description of the content.opf file.
+# It defaults to the copyright option.
+#epub_copyright = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#epub_identifier = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+#epub_scheme = ''
+
+# A unique identification for the text.
+#epub_uid = ''
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_pre_files = []
+
+# HTML files shat should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+#epub_exclude_files = []
 '''
 
 INTERSPHINX_CONFIG = '''
 ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) \
 $(SPHINXOPTS) %(rsrcdir)s
 
-.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes \
-linkcheck doctest
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp epub \
+latex changes linkcheck doctest
 
 help:
 \t@echo "Please use \\`make <target>' where <target> is one of"
 \t@echo "  htmlhelp  to make HTML files and a HTML help project"
 \t@echo "  qthelp    to make HTML files and a qthelp project"
 \t@echo "  devhelp   to make HTML files and a Devhelp project"
+\t@echo "  epub      to make an epub"
 \t@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 \t@echo "  latexpdf  to make LaTeX files and run them through pdflatex"
 \t@echo "  changes   to make an overview of all changed/added/deprecated items"
  $$HOME/.local/share/devhelp/%(project_fn)s"
 \t@echo "# devhelp"
 
+epub:
+\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+\t@echo
+\t@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
 latex:
 \t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
 \t@echo
 \techo.  htmlhelp  to make HTML files and a HTML help project
 \techo.  qthelp    to make HTML files and a qthelp project
 \techo.  devhelp   to make HTML files and a Devhelp project
+\techo.  epub      to make an epub
 \techo.  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter
 \techo.  changes   to make an overview over all changed/added/deprecated items
 \techo.  linkcheck to check all external links for integrity
 \tgoto end
 )
 
+if "%%1" == "epub" (
+\t%%SPHINXBUILD%% -b epub %%ALLSPHINXOPTS%% %%BUILDDIR%%/epub
+\techo.
+\techo.Build finished. The epub file is in %%BUILDDIR%%/epub.
+\tgoto end
+)
+
 if "%%1" == "latex" (
 \t%%SPHINXBUILD%% -b latex %%ALLSPHINXOPTS%% %%BUILDDIR%%/latex
 \techo.

sphinx/themes/epub/layout.html

+{% extends "basic/layout.html" %}
+
+{# add only basic navigation links #}
+{% block sidebar1 %}{% endblock %}
+{% block sidebar2 %}{% endblock %}
+{% block relbar2 %}{% endblock %}
+{% block linktags %}{% endblock %}

sphinx/themes/epub/static/epub.css

+/**
+ * Sphinx stylesheet -- epub theme
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+    clear: both;
+}
+
+a:link, a:visited {
+    color: #3333ff;
+    text-decoration: underline;
+}
+
+img {
+    border: 0;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+    width: 100%;
+    font-family: sans-serif;
+    font-size: 90%;
+}
+
+div.related h3 {
+    display: none;
+}
+
+div.related ul {
+    margin: 0;
+    padding: 0 0 0 10px;
+    list-style: none;
+}
+
+div.related li {
+    display: inline;
+}
+
+div.related li.right {
+    float: right;
+    margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+    padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+    float: left;
+    width: 230px;
+    margin-left: -100%;
+    font-size: 90%;
+}
+
+div.sphinxsidebar ul {
+    list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+    margin-left: 20px;
+    list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+    margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 100%;
+}
+
+img {
+    border: 0;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+    margin: 10px 0 0 20px;
+    padding: 0;
+}
+
+ul.search li {
+    padding: 5px 0 5px 20px;
+    background-image: url(file.png);
+    background-repeat: no-repeat;
+    background-position: 0 7px;
+}
+
+ul.search li a {
+    font-weight: bold;
+}
+
+ul.search li div.context {
+    color: #888;
+    margin: 2px 0 0 30px;
+    text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+    font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+    width: 90%;
+}
+
+table.contentstable p.biglink {
+    line-height: 150%;
+}
+
+a.biglink {
+    font-size: 130%;
+}
+
+span.linkdescr {
+    font-style: italic;
+    padding-top: 5px;
+    font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable td {
+    text-align: left;
+    vertical-align: top;
+}
+
+table.indextable dl, table.indextable dd {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+table.indextable tr.pcap {
+    height: 10px;
+}
+
+table.indextable tr.cap {
+    margin-top: 10px;
+    background-color: #f2f2f2;
+}
+
+img.toggler {
+    margin-right: 3px;
+    margin-top: 3px;
+    cursor: pointer;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+a.headerlink {
+    visibility: hidden;
+}
+
+div.body p.caption {
+    text-align: inherit;
+}
+
+div.body td {
+    text-align: left;
+}
+
+.field-list ul {
+    padding-left: 100%;
+}
+
+.first {
+    margin-top: 0 !important;
+}
+
+p.rubric {
+    margin-top: 30px;
+    font-weight: bold;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar {
+    margin: 0 0 0.5em 1em;
+    border: 1px solid #ddb;
+    padding: 7px 7px 0 7px;
+    background-color: #ffe;
+    width: 40%;
+    float: right;
+}
+
+p.sidebar-title {
+    font-weight: bold;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+div.topic {
+    border: 1px solid #ccc;
+    padding: 7px 7px 0 7px;
+    margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+    font-size: 110%;
+    font-weight: bold;
+    margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 7px;
+}
+
+div.admonition dt {
+    font-weight: bold;
+}
+
+div.admonition dl {
+    margin-bottom: 0;
+}
+
+p.admonition-title {
+    margin: 0px 10px 5px 0px;
+    font-weight: bold;
+}
+
+div.body p.centered {
+    text-align: center;
+    margin-top: 25px;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+    border: 0;
+    border-collapse: collapse;
+}
+
+table.docutils td, table.docutils th {
+    padding: 1px 8px 1px 5px;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
+    border-bottom: 1px solid #aaa;
+}
+
+table.field-list td, table.field-list th {
+    border: 0 !important;
+}
+
+table.footnote td, table.footnote th {
+    border: 0 !important;
+}
+
+th {
+    text-align: left;
+    padding-right: 5px;
+}
+
+/* -- other body styles ----------------------------------------------------- */
+
+dl {
+    margin-bottom: 15px;
+}
+
+dd p {
+    margin-top: 0px;
+}
+
+dd ul, dd table {
+    margin-bottom: 10px;
+}
+
+dd {
+    margin-top: 3px;
+    margin-bottom: 10px;
+    margin-left: 30px;
+}
+
+dt:target, .highlight {
+    background-color: #ddd;
+}
+
+dl.glossary dt {
+    font-weight: bold;
+    font-size: 110%;
+}
+
+.field-list ul {
+    margin: 0;
+    padding-left: 1em;
+}
+
+.field-list p {
+    margin: 0;
+}
+
+.refcount {
+    color: #060;
+}
+
+.optional {
+    font-size: 130%;
+}
+
+.versionmodified {
+    font-style: italic;
+}
+
+.system-message {
+    background-color: #fda;
+    padding: 5px;
+    border: 3px solid red;
+}
+
+.footnote:target  {
+    background-color: #dddddd;
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+    font-family: "LiberationNarrow", monospace;
+    overflow: auto;
+}
+
+td.linenos pre {
+    padding: 5px 0px;
+    border: 0;
+    background-color: transparent;
+    color: #aaa;
+}
+
+table.highlighttable {
+    margin-left: 0.5em;
+}
+
+table.highlighttable td {
+    padding: 0 0.5em 0 0.5em;
+}
+
+tt {
+    font-family: "LiberationNarrow", monospace;
+}
+
+tt.descname {
+    background-color: transparent;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+tt.descclassname {
+    background-color: transparent;
+}
+
+tt.xref, a tt {
+    background-color: transparent;
+    font-weight: bold;
+}
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+    background-color: transparent;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+    vertical-align: middle;
+}
+
+div.body div.math p {
+    text-align: center;
+}
+
+span.eqno {
+    float: right;
+}
+
+/* -- special divs  --------------------------------------------------------- */
+
+div.quotebar {
+    background-color: #e3eff1;
+    max-width: 250px;
+    float: right;
+    font-family: sans-serif;
+    padding: 7px 7px;
+    border: 1px solid #ccc;
+}
+div.footer {
+    background-color: #e3eff1;
+    padding: 3px 8px 3px 0;
+    clear: both;
+    font-family: sans-serif;
+    font-size: 80%;
+    text-align: right;
+}
+
+div.footer a {
+    text-decoration: underline;
+}
+
+@font-face {
+    font-family: "LiberationNarrow";
+    font-style: normal;
+    font-weight: normal;
+    src: url("res:///Data/fonts/LiberationNarrow-Regular.otf")
+        format("opentype");
+}
+@font-face {
+    font-family: "LiberationNarrow";
+    font-style: oblique, italic;
+    font-weight: normal;
+    src: url("res:///Data/fonts/LiberationNarrow-Italic.otf")
+        format("opentype");
+}
+@font-face {
+    font-family: "LiberationNarrow";
+    font-style: normal;
+    font-weight: bold;
+    src: url("res:///Data/fonts/LiberationNarrow-Bold.otf")
+        format("opentype");
+}
+@font-face {
+    font-family: "LiberationNarrow";
+    font-style: oblique, italic;
+    font-weight: bold;
+    src: url("res:///Data/fonts/LiberationNarrow-BoldItalic.otf")
+        format("opentype");
+}
+

sphinx/themes/epub/theme.conf

+[theme]
+inherit = basic
+stylesheet = epub.css
+pygments_style = none

tests/test_build.py

 def test_qthelp(app):
     app.builder.build_all()
 
+@with_app(buildername='epub')
+def test_epub(app):
+    app.builder.build_all()
+
 @with_app(buildername='changes', cleanenv=True)
 def test_changes(app):
     app.builder.build_all()

tests/test_theming.py

     # test Theme class API
     assert set(Theme.themes.keys()) == \
            set(['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc',
-                'traditional', 'testtheme', 'ziptheme'])
+                'traditional', 'testtheme', 'ziptheme', 'epub'])
     assert Theme.themes['testtheme'][1] is None
     assert isinstance(Theme.themes['ziptheme'][1], zipfile.ZipFile)