diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py
index d61f4868f..3f97a1ae5 100644
--- a/sphinx/builders/epub.py
+++ b/sphinx/builders/epub.py
@@ -12,10 +12,10 @@
import os
import re
-import codecs
-import zipfile
from os import path
+from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
from datetime import datetime
+from collections import namedtuple
try:
from PIL import Image
@@ -28,10 +28,12 @@ except ImportError:
from docutils import nodes
from sphinx import addnodes
+from sphinx import package_dir
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util import logging
from sphinx.util import status_iterator
-from sphinx.util.osutil import ensuredir, copyfile, make_filename, EEXIST
+from sphinx.util.osutil import ensuredir, copyfile, make_filename
+from sphinx.util.fileutil import copy_asset_file
from sphinx.util.smartypants import sphinx_smarty_pants as ssp
if False:
@@ -43,101 +45,14 @@ if False:
logger = logging.getLogger(__name__)
-# (Fragment) templates from which the metainfo files content.opf, toc.ncx,
-# mimetype, and META-INF/container.xml are created.
+# (Fragment) templates from which the metainfo files content.opf and
+# toc.ncx are created.
# This template section also defines strings that are embedded in the html
# output but that may be customized by (re-)setting module attributes,
# e.g. from conf.py.
-MIMETYPE_TEMPLATE = 'application/epub+zip' # no EOL!
-
-CONTAINER_TEMPLATE = u'''\
-
-
-
-
-
-
-'''
-
-TOC_TEMPLATE = u'''\
-
-
-
-
-
-
-
-
-
- %(title)s
-
-
-%(navpoints)s
-
-
-'''
-
-NAVPOINT_TEMPLATE = u'''\
-%(indent)s
-%(indent)s
-%(indent)s %(text)s
-%(indent)s
-%(indent)s
-%(indent)s '''
-
-NAVPOINT_INDENT = ' '
-NODE_NAVPOINT_TEMPLATE = 'navPoint%d'
-
-CONTENT_TEMPLATE = u'''\
-
-
-
- %(lang)s
- %(title)s
- %(author)s
- %(publisher)s
- %(copyright)s
- %(id)s
- %(date)s
-
-
-
-%(files)s
-
-
-%(spine)s
-
-
-%(guide)s
-
-
-'''
-
-COVER_TEMPLATE = u'''\
-
-'''
-
COVERPAGE_NAME = u'epub-cover.xhtml'
-FILE_TEMPLATE = u'''\
- '''
-
-SPINE_TEMPLATE = u'''\
- '''
-
-NO_LINEAR_SPINE_TEMPLATE = u'''\
- '''
-
-GUIDE_TEMPLATE = u'''\
- '''
-
TOCTREE_TEMPLATE = u'toctree-l%d'
DOCTYPE = u''' unicode
+ def make_id(self, name):
+ # type: (unicode) -> unicode
# id_cache is intentionally mutable
"""Return a unique id for name."""
- id = id_cache.get(name)
+ id = self.id_cache.get(name)
if not id:
id = 'epub-%d' % self.env.new_serialno('epub')
- id_cache[name] = id
+ self.id_cache[name] = id
return id
def esc(self, name):
@@ -552,24 +464,19 @@ class EpubBuilder(StandaloneHTMLBuilder):
# type: (unicode, unicode) -> None
"""Write the metainfo file mimetype."""
logger.info('writing %s file...', outname)
- with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore
- f.write(self.mimetype_template)
+ copy_asset_file(path.join(self.template_dir, 'mimetype'),
+ path.join(outdir, outname))
def build_container(self, outdir, outname):
# type: (unicode, unicode) -> None
- """Write the metainfo file META-INF/cointainer.xml."""
+ """Write the metainfo file META-INF/container.xml."""
logger.info('writing %s file...', outname)
- fn = path.join(outdir, outname)
- try:
- os.mkdir(path.dirname(fn))
- except OSError as err:
- if err.errno != EEXIST:
- raise
- with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore
- f.write(self.container_template) # type: ignore
+ filename = path.join(outdir, outname)
+ ensuredir(path.dirname(filename))
+ copy_asset_file(path.join(self.template_dir, 'container.xml'), filename)
- def content_metadata(self, files, spine, guide):
- # type: (List[unicode], List[unicode], List[unicode]) -> Dict[unicode, Any]
+ def content_metadata(self):
+ # type: () -> Dict[unicode, Any]
"""Create a dictionary with all metadata for the content.opf
file properly escaped.
"""
@@ -583,9 +490,9 @@ class EpubBuilder(StandaloneHTMLBuilder):
metadata['scheme'] = self.esc(self.config.epub_scheme)
metadata['id'] = self.esc(self.config.epub_identifier)
metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%d"))
- metadata['files'] = files
- metadata['spine'] = spine
- metadata['guide'] = guide
+ metadata['manifest_items'] = []
+ metadata['spines'] = []
+ metadata['guides'] = []
return metadata
def build_content(self, outdir, outname):
@@ -594,12 +501,12 @@ class EpubBuilder(StandaloneHTMLBuilder):
a file list and the spine (the reading order).
"""
logger.info('writing %s file...', outname)
+ metadata = self.content_metadata()
# files
if not outdir.endswith(os.sep):
outdir += os.sep
olen = len(outdir)
- projectfiles = [] # type: List[unicode]
self.files = [] # type: List[unicode]
self.ignored_files = ['.buildinfo', 'mimetype', 'content.opf',
'toc.ncx', 'META-INF/container.xml',
@@ -609,7 +516,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
if not self.use_index:
self.ignored_files.append('genindex' + self.out_suffix)
for root, dirs, files in os.walk(outdir):
- for fn in files:
+ for fn in sorted(files):
filename = path.join(root, fn)[olen:]
if filename in self.ignored_files:
continue
@@ -622,70 +529,57 @@ class EpubBuilder(StandaloneHTMLBuilder):
type='epub', subtype='unknown_project_files')
continue
filename = filename.replace(os.sep, '/')
- projectfiles.append(self.file_template % {
- 'href': self.esc(filename),
- 'id': self.esc(self.make_id(filename)),
- 'media_type': self.esc(self.media_types[ext])
- })
+ item = ManifestItem(self.esc(filename),
+ self.esc(self.make_id(filename)),
+ self.esc(self.media_types[ext]))
+ metadata['manifest_items'].append(item)
self.files.append(filename)
# spine
- spine = []
spinefiles = set()
- for item in self.refnodes:
- if '#' in item['refuri']:
+ for refnode in self.refnodes:
+ if '#' in refnode['refuri']:
continue
- if item['refuri'] in self.ignored_files:
+ if refnode['refuri'] in self.ignored_files:
continue
- spine.append(self.spine_template % {
- 'idref': self.esc(self.make_id(item['refuri']))
- })
- spinefiles.add(item['refuri'])
+ spine = Spine(self.esc(self.make_id(refnode['refuri'])), True)
+ metadata['spines'].append(spine)
+ spinefiles.add(refnode['refuri'])
for info in self.domain_indices:
- spine.append(self.spine_template % {
- 'idref': self.esc(self.make_id(info[0] + self.out_suffix))
- })
+ spine = Spine(self.esc(self.make_id(info[0] + self.out_suffix)), True)
+ metadata['spines'].append(spine)
spinefiles.add(info[0] + self.out_suffix)
if self.use_index:
- spine.append(self.spine_template % {
- 'idref': self.esc(self.make_id('genindex' + self.out_suffix))
- })
+ spine = Spine(self.esc(self.make_id('genindex' + self.out_suffix)), True)
+ metadata['spines'].append(spine)
spinefiles.add('genindex' + self.out_suffix)
# add auto generated files
for name in self.files:
if name not in spinefiles and name.endswith(self.out_suffix):
- spine.append(self.no_linear_spine_template % {
- 'idref': self.esc(self.make_id(name))
- })
+ spine = Spine(self.esc(self.make_id(name)), False)
+ metadata['spines'].append(spine)
# add the optional cover
- content_tmpl = self.content_template
html_tmpl = None
if self.config.epub_cover:
image, html_tmpl = self.config.epub_cover
image = image.replace(os.sep, '/')
- mpos = content_tmpl.rfind('')
- cpos = content_tmpl.rfind('\n', 0, mpos) + 1
- content_tmpl = content_tmpl[:cpos] + \
- COVER_TEMPLATE % {'cover': self.esc(self.make_id(image))} + \
- content_tmpl[cpos:]
+ metadata['cover'] = self.esc(self.make_id(image))
if html_tmpl:
- spine.insert(0, self.spine_template % {
- 'idref': self.esc(self.make_id(self.coverpage_name))})
+ spine = Spine(self.esc(self.make_id(self.coverpage_name)), True)
+ metadata['spines'].insert(0, spine)
if self.coverpage_name not in self.files:
ext = path.splitext(self.coverpage_name)[-1]
self.files.append(self.coverpage_name)
- projectfiles.append(self.file_template % {
- 'href': self.esc(self.coverpage_name),
- 'id': self.esc(self.make_id(self.coverpage_name)),
- 'media_type': self.esc(self.media_types[ext])
- })
+ item = ManifestItem(self.esc(filename),
+ self.esc(self.make_id(filename)),
+ self.esc(self.media_types[ext]))
+ metadata['manifest_items'].append(item)
ctx = {'image': self.esc(image), 'title': self.config.project}
self.handle_page(
path.splitext(self.coverpage_name)[0], ctx, html_tmpl)
spinefiles.add(self.coverpage_name)
- guide = []
auto_add_cover = True
auto_add_toc = True
if self.config.epub_guide:
@@ -697,64 +591,43 @@ class EpubBuilder(StandaloneHTMLBuilder):
auto_add_cover = False
if type == 'toc':
auto_add_toc = False
- guide.append(self.guide_template % {
- 'type': self.esc(type),
- 'title': self.esc(title),
- 'uri': self.esc(uri)
- })
+ metadata['guides'].append(Guide(self.esc(type),
+ self.esc(title),
+ self.esc(uri)))
if auto_add_cover and html_tmpl:
- guide.append(self.guide_template % {
- 'type': 'cover',
- 'title': self.guide_titles['cover'],
- 'uri': self.esc(self.coverpage_name)
- })
+ metadata['guides'].append(Guide('cover',
+ self.guide_titles['cover'],
+ self.esc(self.coverpage_name)))
if auto_add_toc and self.refnodes:
- guide.append(self.guide_template % {
- 'type': 'toc',
- 'title': self.guide_titles['toc'],
- 'uri': self.esc(self.refnodes[0]['refuri'])
- })
- projectfiles = '\n'.join(projectfiles) # type: ignore
- spine = '\n'.join(spine) # type: ignore
- guide = '\n'.join(guide) # type: ignore
+ metadata['guides'].append(Guide('toc',
+ self.guide_titles['toc'],
+ self.esc(self.refnodes[0]['refuri'])))
# write the project file
- with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore
- f.write(content_tmpl % # type: ignore
- self.content_metadata(projectfiles, spine, guide))
+ copy_asset_file(path.join(self.template_dir, 'content.opf_t'),
+ path.join(outdir, outname),
+ metadata)
def new_navpoint(self, node, level, incr=True):
- # type: (nodes.Node, int, bool) -> unicode
+ # type: (nodes.Node, int, bool) -> NavPoint
"""Create a new entry in the toc from the node at given level."""
# XXX Modifies the node
if incr:
self.playorder += 1
self.tocid += 1
- node['indent'] = self.navpoint_indent * level
- node['navpoint'] = self.esc(self.node_navpoint_template % self.tocid)
- node['playorder'] = self.playorder
- return self.navpoint_template % node
-
- def insert_subnav(self, node, subnav):
- # type: (nodes.Node, unicode) -> unicode
- """Insert nested navpoints for given node.
-
- The node and subnav are already rendered to text.
- """
- nlist = node.rsplit('\n', 1)
- nlist.insert(-1, subnav)
- return '\n'.join(nlist)
+ return NavPoint(self.esc('navPoint%d' % self.tocid), self.playorder,
+ node['text'], node['refuri'], [])
def build_navpoints(self, nodes):
- # type: (nodes.Node) -> unicode
+ # type: (nodes.Node) -> List[NavPoint]
"""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
+ navstack = [] # type: List[NavPoint]
+ navstack.append(NavPoint('dummy', '', '', '', []))
+ level = 0
lastnode = None
for node in nodes:
if not node['text']:
@@ -765,32 +638,33 @@ class EpubBuilder(StandaloneHTMLBuilder):
if node['level'] > self.config.epub_tocdepth:
continue
if node['level'] == level:
- navlist.append(self.new_navpoint(node, level))
+ navpoint = self.new_navpoint(node, level)
+ navstack.pop()
+ navstack[-1].children.append(navpoint)
+ navstack.append(navpoint)
elif node['level'] == level + 1:
- navstack.append(navlist)
- navlist = []
level += 1
if lastnode and self.config.epub_tocdup:
# Insert starting point in subtoc with same playOrder
- navlist.append(self.new_navpoint(lastnode, level, False))
- navlist.append(self.new_navpoint(node, level))
+ navstack[-1].children.append(self.new_navpoint(lastnode, level, False))
+ navpoint = self.new_navpoint(node, level)
+ navstack[-1].children.append(navpoint)
+ navstack.append(navpoint)
+ elif node['level'] < level:
+ while node['level'] < len(navstack):
+ navstack.pop()
+ level = node['level']
+ navpoint = self.new_navpoint(node, level)
+ navstack[-1].children.append(navpoint)
+ navstack.append(navpoint)
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))
+ raise
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)
+
+ return navstack[0].children
def toc_metadata(self, level, navpoints):
- # type: (int, List[unicode]) -> Dict[unicode, Any]
+ # type: (int, List[NavPoint]) -> Dict[unicode, Any]
"""Create a dictionary with all metadata for the toc.ncx file
properly escaped.
"""
@@ -818,8 +692,9 @@ class EpubBuilder(StandaloneHTMLBuilder):
navpoints = self.build_navpoints(refnodes)
level = max(item['level'] for item in self.refnodes)
level = min(level, self.config.epub_tocdepth)
- with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore
- f.write(self.toc_template % self.toc_metadata(level, navpoints)) # type: ignore
+ copy_asset_file(path.join(self.template_dir, 'toc.ncx_t'),
+ path.join(outdir, outname),
+ self.toc_metadata(level, navpoints))
def build_epub(self, outdir, outname):
# type: (unicode, unicode) -> None
@@ -829,16 +704,13 @@ class EpubBuilder(StandaloneHTMLBuilder):
entry.
"""
logger.info('writing %s file...', outname)
- projectfiles = ['META-INF/container.xml', 'content.opf', 'toc.ncx'] # type: List[unicode] # NOQA
- projectfiles.extend(self.files)
- epub = zipfile.ZipFile(path.join(outdir, outname), 'w', # type: ignore
- zipfile.ZIP_DEFLATED)
- epub.write(path.join(outdir, 'mimetype'), 'mimetype', # type: ignore
- zipfile.ZIP_STORED)
- for file in projectfiles:
- fp = path.join(outdir, file)
- epub.write(fp, file, zipfile.ZIP_DEFLATED) # type: ignore
- epub.close()
+ epub_filename = path.join(outdir, outname)
+ with ZipFile(epub_filename, 'w', ZIP_DEFLATED) as epub: # type: ignore
+ epub.write(path.join(outdir, 'mimetype'), 'mimetype', ZIP_STORED) # type: ignore
+ for filename in [u'META-INF/container.xml', u'content.opf', u'toc.ncx']:
+ epub.write(path.join(outdir, filename), filename, ZIP_DEFLATED) # type: ignore
+ for filename in self.files:
+ epub.write(path.join(outdir, filename), filename, ZIP_DEFLATED) # type: ignore
def setup(app):
diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py
index 2723bff9c..646ed61ac 100644
--- a/sphinx/builders/epub3.py
+++ b/sphinx/builders/epub3.py
@@ -10,13 +10,15 @@
:license: BSD, see LICENSE for details.
"""
-import codecs
from os import path
from datetime import datetime
+from collections import namedtuple
-from sphinx.config import string_classes
+from sphinx import package_dir
+from sphinx.config import string_classes, ENUM
from sphinx.builders.epub import EpubBuilder
from sphinx.util import logging
+from sphinx.util.fileutil import copy_asset_file
if False:
# For type annotation
@@ -27,79 +29,24 @@ if False:
logger = logging.getLogger(__name__)
-# (Fragment) templates from which the metainfo files content.opf, toc.ncx,
-# mimetype, and META-INF/container.xml are created.
-# This template section also defines strings that are embedded in the html
-# output but that may be customized by (re-)setting module attributes,
-# e.g. from conf.py.
+NavPoint = namedtuple('NavPoint', ['text', 'refuri', 'children'])
-NAVIGATION_DOC_TEMPLATE = u'''\
-
-
-
-
- %(toc_locale)s
-
-
-
-
-
-'''
-
-NAVLIST_TEMPLATE = u'''%(indent)s %(text)s'''
-NAVLIST_TEMPLATE_HAS_CHILD = u'''%(indent)s %(text)s'''
-NAVLIST_TEMPLATE_BEGIN_BLOCK = u'''%(indent)s '''
-NAVLIST_TEMPLATE_END_BLOCK = u'''%(indent)s
-%(indent)s '''
-NAVLIST_INDENT = ' '
-
-PACKAGE_DOC_TEMPLATE = u'''\
-
-
-
- %(lang)s
- %(title)s
- %(description)s
- %(author)s
- %(contributor)s
- %(publisher)s
- %(copyright)s
- %(id)s
- %(date)s
- %(date)s
- %(version)s
- true
- true
- %(ibook_scroll_axis)s
-
-
-
-
-%(files)s
-
-
-%(spine)s
-
-
-%(guide)s
-
-
-'''
+# writing modes
+PAGE_PROGRESSION_DIRECTIONS = {
+ 'horizontal': 'ltr',
+ 'vertical': 'rtl',
+}
+IBOOK_SCROLL_AXIS = {
+ 'horizontal': 'vertical',
+ 'vertical': 'horizontal',
+}
+THEME_WRITING_MODES = {
+ 'vertical': 'vertical-rl',
+ 'horizontal': 'horizontal-tb',
+}
DOCTYPE = u''''''
-# The epub3 publisher
-
class Epub3Builder(EpubBuilder):
"""
@@ -111,13 +58,7 @@ class Epub3Builder(EpubBuilder):
"""
name = 'epub'
- navigation_doc_template = NAVIGATION_DOC_TEMPLATE
- navlist_template = NAVLIST_TEMPLATE
- navlist_template_has_child = NAVLIST_TEMPLATE_HAS_CHILD
- navlist_template_begin_block = NAVLIST_TEMPLATE_BEGIN_BLOCK
- navlist_template_end_block = NAVLIST_TEMPLATE_END_BLOCK
- navlist_indent = NAVLIST_INDENT
- content_template = PACKAGE_DOC_TEMPLATE
+ template_dir = path.join(package_dir, 'templates', 'epub3')
doctype = DOCTYPE
# Finish by building the epub file
@@ -132,77 +73,31 @@ class Epub3Builder(EpubBuilder):
self.build_toc(self.outdir, 'toc.ncx')
self.build_epub(self.outdir, self.config.epub_basename + '.epub')
- def content_metadata(self, files, spine, guide):
- # type: (List[unicode], List[unicode], List[unicode]) -> Dict
+ def content_metadata(self):
+ # type: () -> Dict
"""Create a dictionary with all metadata for the content.opf
file properly escaped.
"""
- metadata = super(Epub3Builder, self).content_metadata(
- files, spine, guide)
+ writing_mode = self.config.epub_writing_mode
+
+ metadata = super(Epub3Builder, self).content_metadata()
metadata['description'] = self.esc(self.config.epub_description)
metadata['contributor'] = self.esc(self.config.epub_contributor)
- metadata['page_progression_direction'] = self._page_progression_direction()
- metadata['ibook_scroll_axis'] = self._ibook_scroll_axis()
+ metadata['page_progression_direction'] = PAGE_PROGRESSION_DIRECTIONS.get(writing_mode)
+ metadata['ibook_scroll_axis'] = IBOOK_SCROLL_AXIS.get(writing_mode)
metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
metadata['version'] = self.esc(self.config.version)
return metadata
- def _page_progression_direction(self):
- # type: () -> unicode
- if self.config.epub_writing_mode == 'horizontal':
- page_progression_direction = 'ltr'
- elif self.config.epub_writing_mode == 'vertical':
- page_progression_direction = 'rtl'
- else:
- page_progression_direction = 'default'
- return page_progression_direction
-
- def _ibook_scroll_axis(self):
- # type: () -> unicode
- if self.config.epub_writing_mode == 'horizontal':
- scroll_axis = 'vertical'
- elif self.config.epub_writing_mode == 'vertical':
- scroll_axis = 'horizontal'
- else:
- scroll_axis = 'default'
- return scroll_axis
-
- def _css_writing_mode(self):
- # type: () -> unicode
- if self.config.epub_writing_mode == 'vertical':
- editing_mode = 'vertical-rl'
- else:
- editing_mode = 'horizontal-tb'
- return editing_mode
-
def prepare_writing(self, docnames):
# type: (Iterable[unicode]) -> None
super(Epub3Builder, self).prepare_writing(docnames)
- self.globalcontext['theme_writing_mode'] = self._css_writing_mode()
- def new_navlist(self, node, level, has_child):
- # type: (nodes.Node, int, bool) -> unicode
- """Create a new entry in the toc from the node at given level."""
- # XXX Modifies the node
- self.tocid += 1
- node['indent'] = self.navlist_indent * level
- if has_child:
- return self.navlist_template_has_child % node
- else:
- return self.navlist_template % node
-
- def begin_navlist_block(self, level):
- # type: (int) -> unicode
- return self.navlist_template_begin_block % {
- "indent": self.navlist_indent * level
- }
-
- def end_navlist_block(self, level):
- # type: (int) -> unicode
- return self.navlist_template_end_block % {"indent": self.navlist_indent * level}
+ writing_mode = self.config.epub_writing_mode
+ self.globalcontext['theme_writing_mode'] = THEME_WRITING_MODES.get(writing_mode)
def build_navlist(self, navnodes):
- # type: (List[nodes.Node]) -> unicode
+ # type: (List[nodes.Node]) -> List[NavPoint]
"""Create the toc navigation structure.
This method is almost same as build_navpoints method in epub.py.
@@ -212,9 +107,9 @@ class Epub3Builder(EpubBuilder):
The difference from build_navpoints method is templates which are used
when generating navigation documents.
"""
- navlist = []
- level = 1
- usenodes = []
+ navstack = [] # type: List[NavPoint]
+ navstack.append(NavPoint('', '', []))
+ level = 0
for node in navnodes:
if not node['text']:
continue
@@ -223,31 +118,33 @@ class Epub3Builder(EpubBuilder):
continue
if node['level'] > self.config.epub_tocdepth:
continue
- usenodes.append(node)
- for i, node in enumerate(usenodes):
- curlevel = node['level']
- if curlevel == level + 1:
- navlist.append(self.begin_navlist_block(level))
- while curlevel < level:
- level -= 1
- navlist.append(self.end_navlist_block(level))
- level = curlevel
- if i != len(usenodes) - 1 and usenodes[i + 1]['level'] > level:
- has_child = True
+
+ navpoint = NavPoint(node['text'], node['refuri'], [])
+ if node['level'] == level:
+ navstack.pop()
+ navstack[-1].children.append(navpoint)
+ navstack.append(navpoint)
+ elif node['level'] == level + 1:
+ level += 1
+ navstack[-1].children.append(navpoint)
+ navstack.append(navpoint)
+ elif node['level'] < level:
+ while node['level'] < len(navstack):
+ navstack.pop()
+ level = node['level']
+ navstack[-1].children.append(navpoint)
+ navstack.append(navpoint)
else:
- has_child = False
- navlist.append(self.new_navlist(node, level, has_child))
- while level != 1:
- level -= 1
- navlist.append(self.end_navlist_block(level))
- return '\n'.join(navlist)
+ raise
+
+ return navstack[0].children
def navigation_doc_metadata(self, navlist):
- # type: (unicode) -> Dict
+ # type: (List[NavPoint]) -> Dict
"""Create a dictionary with all metadata for the nav.xhtml file
properly escaped.
"""
- metadata = {}
+ metadata = {} # type: Dict
metadata['lang'] = self.esc(self.config.epub_language)
metadata['toc_locale'] = self.esc(self.guide_titles['toc'])
metadata['navlist'] = navlist
@@ -268,9 +165,9 @@ class Epub3Builder(EpubBuilder):
# 'includehidden'
refnodes = self.refnodes
navlist = self.build_navlist(refnodes)
- with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore
- f.write(self.navigation_doc_template % # type: ignore
- self.navigation_doc_metadata(navlist))
+ copy_asset_file(path.join(self.template_dir, 'nav.xhtml_t'),
+ path.join(outdir, outname),
+ self.navigation_doc_metadata(navlist))
# Add nav.xhtml to epub file
if outname not in self.files:
@@ -284,7 +181,8 @@ def setup(app):
app.add_config_value('epub_description', '', 'epub3', string_classes)
app.add_config_value('epub_contributor', 'unknown', 'epub3', string_classes)
- app.add_config_value('epub_writing_mode', 'horizontal', 'epub3', string_classes)
+ app.add_config_value('epub_writing_mode', 'horizontal', 'epub3',
+ ENUM('horizontal', 'vertical'))
return {
'version': 'builtin',
diff --git a/sphinx/templates/epub2/container.xml b/sphinx/templates/epub2/container.xml
new file mode 100644
index 000000000..326cf15fa
--- /dev/null
+++ b/sphinx/templates/epub2/container.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/sphinx/templates/epub2/content.opf_t b/sphinx/templates/epub2/content.opf_t
new file mode 100644
index 000000000..65f3b7aa2
--- /dev/null
+++ b/sphinx/templates/epub2/content.opf_t
@@ -0,0 +1,37 @@
+
+
+
+ {{ lang }}
+ {{ title }}
+ {{ author }}
+ {{ publisher }}
+ {{ copyright }}
+ {{ id }}
+ {{ date }}
+ {%- if cover %}
+
+ {%- endif %}
+
+
+
+ {%- for item in manifest_items %}
+
+ {%- endfor %}
+
+
+ {%- for spine in spines %}
+ {%- if spine.linear %}
+
+ {%- else %}
+ '''
+ {%- endif %}
+ {%- endfor %}
+
+
+ {%- for guide in guides %}
+ '''
+ {%- endfor %}
+
+
diff --git a/sphinx/templates/epub2/mimetype b/sphinx/templates/epub2/mimetype
new file mode 100644
index 000000000..57ef03f24
--- /dev/null
+++ b/sphinx/templates/epub2/mimetype
@@ -0,0 +1 @@
+application/epub+zip
\ No newline at end of file
diff --git a/sphinx/templates/epub2/toc.ncx_t b/sphinx/templates/epub2/toc.ncx_t
new file mode 100644
index 000000000..9bb701908
--- /dev/null
+++ b/sphinx/templates/epub2/toc.ncx_t
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+{{ navpoints }}
+
+
diff --git a/sphinx/templates/epub3/container.xml b/sphinx/templates/epub3/container.xml
new file mode 100644
index 000000000..326cf15fa
--- /dev/null
+++ b/sphinx/templates/epub3/container.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/sphinx/templates/epub3/content.opf_t b/sphinx/templates/epub3/content.opf_t
new file mode 100644
index 000000000..11d5ab4ba
--- /dev/null
+++ b/sphinx/templates/epub3/content.opf_t
@@ -0,0 +1,46 @@
+
+
+
+ {{ lang }}
+ {{ title }}
+ {{ description }}
+ {{ author }}
+ {{ contributor }}
+ {{ publisher }}
+ {{ copyright }}
+ {{ id }}
+ {{ date }}
+ {{ date }}
+ {{ version }}
+ true
+ true
+ {{ ibook_scroll_axis }}
+ {%- if cover %}
+
+ {%- endif %}
+
+
+
+
+ {%- for item in manifest_items %}
+
+ {%- endfor %}
+
+
+ {%- for spine in spines %}
+ {%- if spine.linear %}
+
+ {%- else %}
+ '''
+ {%- endif %}
+ {%- endfor %}
+
+
+ {%- for guide in guides %}
+ '''
+ {%- endfor %}
+
+
diff --git a/sphinx/templates/epub3/mimetype b/sphinx/templates/epub3/mimetype
new file mode 100644
index 000000000..57ef03f24
--- /dev/null
+++ b/sphinx/templates/epub3/mimetype
@@ -0,0 +1 @@
+application/epub+zip
\ No newline at end of file
diff --git a/sphinx/templates/epub3/nav.xhtml_t b/sphinx/templates/epub3/nav.xhtml_t
new file mode 100644
index 000000000..2a32c2039
--- /dev/null
+++ b/sphinx/templates/epub3/nav.xhtml_t
@@ -0,0 +1,26 @@
+{%- macro toctree(navlist) -%}
+
+{%- for nav in navlist %}
+ -
+ {{ nav.text }}
+ {%- if nav.children %}
+{{ toctree(nav.children)|indent(4, true) }}
+ {%- endif %}
+
+{%- endfor %}
+
+{%- endmacro -%}
+
+
+
+
+ {{ toc_locale }}
+
+
+
+
+
diff --git a/sphinx/templates/epub3/toc.ncx_t b/sphinx/templates/epub3/toc.ncx_t
new file mode 100644
index 000000000..0ea7ca366
--- /dev/null
+++ b/sphinx/templates/epub3/toc.ncx_t
@@ -0,0 +1,24 @@
+{%- macro navPoints(navlist) %}
+{%- for nav in navlist %}
+
+
+ {{ nav.text }}
+
+ {{ navPoints(nav.children)|indent(2, true) }}
+
+{%- endfor %}
+{%- endmacro -%}
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+ {{ navPoints(navpoints)|indent(4, true) }}
+
+
diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py
new file mode 100644
index 000000000..0c9a4a60c
--- /dev/null
+++ b/tests/test_build_epub.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+"""
+ test_build_html
+ ~~~~~~~~~~~~~~~
+
+ Test the HTML builder and check output against XPath.
+
+ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from xml.etree import ElementTree
+
+import pytest
+
+
+class EPUBElementTree(object):
+ """Test helper for content.opf and tox.ncx"""
+ namespaces = {
+ 'idpf': 'http://www.idpf.org/2007/opf',
+ 'dc': 'http://purl.org/dc/elements/1.1/',
+ 'ibooks': 'http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/',
+ 'ncx': 'http://www.daisy.org/z3986/2005/ncx/',
+ 'xhtml': 'http://www.w3.org/1999/xhtml',
+ 'epub': 'http://www.idpf.org/2007/ops'
+ }
+
+ def __init__(self, tree):
+ self.tree = tree
+
+ @classmethod
+ def fromstring(cls, string):
+ return cls(ElementTree.fromstring(string))
+
+ def find(self, match):
+ ret = self.tree.find(match, namespaces=self.namespaces)
+ if ret is not None:
+ return self.__class__(ret)
+ else:
+ return ret
+
+ def findall(self, match):
+ ret = self.tree.findall(match, namespaces=self.namespaces)
+ return [self.__class__(e) for e in ret]
+
+ def __getattr__(self, name):
+ return getattr(self.tree, name)
+
+ def __iter__(self):
+ for child in self.tree:
+ yield self.__class__(child)
+
+
+@pytest.mark.sphinx('epub', testroot='basic')
+def test_build_epub(app):
+ app.build()
+ assert (app.outdir / 'mimetype').text() == 'application/epub+zip'
+ assert (app.outdir / 'META-INF' / 'container.xml').exists()
+
+ # toc.ncx
+ toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').text())
+ assert toc.find("./ncx:docTitle/ncx:text").text == 'Python documentation'
+
+ # toc.ncx / head
+ meta = list(toc.find("./ncx:head"))
+ assert meta[0].attrib == {'name': 'dtb:uid', 'content': 'unknown'}
+ assert meta[1].attrib == {'name': 'dtb:depth', 'content': '1'}
+ assert meta[2].attrib == {'name': 'dtb:totalPageCount', 'content': '0'}
+ assert meta[3].attrib == {'name': 'dtb:maxPageNumber', 'content': '0'}
+
+ # toc.ncx / navMap
+ navpoints = toc.findall("./ncx:navMap/ncx:navPoint")
+ assert len(navpoints) == 1
+ assert navpoints[0].attrib == {'id': 'navPoint1', 'playOrder': '1'}
+ assert navpoints[0].find("./ncx:content").attrib == {'src': 'index.xhtml'}
+
+ navlabel = navpoints[0].find("./ncx:navLabel/ncx:text")
+ assert navlabel.text == 'The basic Sphinx documentation for testing'
+
+ # content.opf
+ opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text())
+
+ # content.opf / metadata
+ metadata = opf.find("./idpf:metadata")
+ assert metadata.find("./dc:language").text == 'en'
+ assert metadata.find("./dc:title").text == 'Python documentation'
+ assert metadata.find("./dc:description").text is None
+ assert metadata.find("./dc:creator").text == 'unknown'
+ assert metadata.find("./dc:contributor").text == 'unknown'
+ assert metadata.find("./dc:publisher").text == 'unknown'
+ assert metadata.find("./dc:rights").text is None
+ assert metadata.find("./idpf:meta[@property='ibooks:version']").text is None
+ assert metadata.find("./idpf:meta[@property='ibooks:specified-fonts']").text == 'true'
+ assert metadata.find("./idpf:meta[@property='ibooks:binding']").text == 'true'
+ assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'vertical'
+
+ # content.opf / manifest
+ manifest = opf.find("./idpf:manifest")
+ items = list(manifest)
+ assert items[0].attrib == {'id': 'ncx',
+ 'href': 'toc.ncx',
+ 'media-type': 'application/x-dtbncx+xml'}
+ assert items[1].attrib == {'id': 'nav',
+ 'href': 'nav.xhtml',
+ 'media-type': 'application/xhtml+xml',
+ 'properties': 'nav'}
+ assert items[2].attrib == {'id': 'epub-0',
+ 'href': 'genindex.xhtml',
+ 'media-type': 'application/xhtml+xml'}
+ assert items[3].attrib == {'id': 'epub-1',
+ 'href': 'index.xhtml',
+ 'media-type': 'application/xhtml+xml'}
+
+ for i, item in enumerate(items[2:]):
+ # items are named as epub-NN
+ assert item.get('id') == 'epub-%d' % i
+
+ # content.opf / spine
+ spine = opf.find("./idpf:spine")
+ itemrefs = list(spine)
+ assert spine.get('toc') == 'ncx'
+ assert spine.get('page-progression-direction') == 'ltr'
+ assert itemrefs[0].get('idref') == 'epub-1'
+ assert itemrefs[1].get('idref') == 'epub-0'
+
+ # content.opf / guide
+ reference = opf.find("./idpf:guide/idpf:reference")
+ assert reference.get('type') == 'toc'
+ assert reference.get('title') == 'Table of Contents'
+ assert reference.get('href') == 'index.xhtml'
+
+ # nav.xhtml
+ nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').text())
+ assert nav.attrib == {'lang': 'en',
+ '{http://www.w3.org/XML/1998/namespace}lang': 'en'}
+ assert nav.find("./xhtml:head/xhtml:title").text == 'Table of Contents'
+
+ # nav.xhtml / nav
+ navlist = nav.find("./xhtml:body/xhtml:nav")
+ toc = navlist.findall("./xhtml:ol/xhtml:li")
+ assert navlist.find("./xhtml:h1").text == 'Table of Contents'
+ assert len(toc) == 1
+ assert toc[0].find("./xhtml:a").get("href") == 'index.xhtml'
+ assert toc[0].find("./xhtml:a").text == 'The basic Sphinx documentation for testing'
+
+
+@pytest.mark.sphinx('epub', testroot='footnotes',
+ confoverrides={'epub_cover': ('_images/rimg.png', None)})
+def test_epub_cover(app):
+ app.build()
+
+ # content.opf / metadata
+ opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text())
+ cover_image = opf.find("./idpf:manifest/idpf:item[@href='%s']" % app.config.epub_cover[0])
+ cover = opf.find("./idpf:metadata/idpf:meta[@name='cover']")
+ assert cover
+ assert cover.get('content') == cover_image.get('id')
+
+
+@pytest.mark.sphinx('epub', testroot='toctree')
+def test_nested_toc(app):
+ app.build()
+
+ # toc.ncx
+ toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').text())
+ assert toc.find("./ncx:docTitle/ncx:text").text == 'Python documentation'
+
+ # toc.ncx / navPoint
+ def navinfo(elem):
+ label = elem.find("./ncx:navLabel/ncx:text")
+ content = elem.find("./ncx:content")
+ return (elem.get('id'), elem.get('playOrder'),
+ content.get('src'), label.text)
+
+ navpoints = toc.findall("./ncx:navMap/ncx:navPoint")
+ assert len(navpoints) == 4
+ assert navinfo(navpoints[0]) == ('navPoint1', '1', 'index.xhtml',
+ "Welcome to Sphinx Tests's documentation!")
+ assert navpoints[0].findall("./ncx:navPoint") == []
+
+ # toc.ncx / nested navPoints
+ assert navinfo(navpoints[1]) == ('navPoint2', '2', 'foo.xhtml', 'foo')
+ navchildren = navpoints[1].findall("./ncx:navPoint")
+ assert len(navchildren) == 4
+ assert navinfo(navchildren[0]) == ('navPoint3', '2', 'foo.xhtml', 'foo')
+ assert navinfo(navchildren[1]) == ('navPoint4', '3', 'quux.xhtml', 'quux')
+ assert navinfo(navchildren[2]) == ('navPoint5', '4', 'foo.xhtml#foo-1', 'foo.1')
+ assert navinfo(navchildren[3]) == ('navPoint8', '6', 'foo.xhtml#foo-2', 'foo.2')
+
+ # nav.xhtml / nav
+ def navinfo(elem):
+ anchor = elem.find("./xhtml:a")
+ return (anchor.get('href'), anchor.text)
+
+ nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').text())
+ toc = nav.findall("./xhtml:body/xhtml:nav/xhtml:ol/xhtml:li")
+ assert len(toc) == 4
+ assert navinfo(toc[0]) == ('index.xhtml',
+ "Welcome to Sphinx Tests's documentation!")
+ assert toc[0].findall("./xhtml:ol") == []
+
+ # nav.xhtml / nested toc
+ assert navinfo(toc[1]) == ('foo.xhtml', 'foo')
+ tocchildren = toc[1].findall("./xhtml:ol/xhtml:li")
+ assert len(tocchildren) == 3
+ assert navinfo(tocchildren[0]) == ('quux.xhtml', 'quux')
+ assert navinfo(tocchildren[1]) == ('foo.xhtml#foo-1', 'foo.1')
+ assert navinfo(tocchildren[2]) == ('foo.xhtml#foo-2', 'foo.2')
+
+ grandchild = tocchildren[1].findall("./xhtml:ol/xhtml:li")
+ assert len(grandchild) == 1
+ assert navinfo(grandchild[0]) == ('foo.xhtml#foo-1-1', 'foo.1-1')
+
+
+@pytest.mark.sphinx('epub', testroot='basic')
+def test_epub_writing_mode(app):
+ # horizontal (default)
+ app.build()
+
+ # horizontal / page-progression-direction
+ opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text())
+ assert opf.find("./idpf:spine").get('page-progression-direction') == 'ltr'
+
+ # horizontal / ibooks:scroll-axis
+ metadata = opf.find("./idpf:metadata")
+ assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'vertical'
+
+ # horizontal / writing-mode (CSS)
+ css = (app.outdir / '_static' / 'epub.css').text()
+ assert 'writing-mode: horizontal-tb;' in css
+
+ # vertical
+ app.config.epub_writing_mode = 'vertical'
+ (app.outdir / 'index.xhtml').unlink() # forcely rebuild
+ app.build()
+
+ # vertical / page-progression-direction
+ opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text())
+ assert opf.find("./idpf:spine").get('page-progression-direction') == 'rtl'
+
+ # vertical / ibooks:scroll-axis
+ metadata = opf.find("./idpf:metadata")
+ assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'horizontal'
+
+ # vertical / writing-mode (CSS)
+ css = (app.outdir / '_static' / 'epub.css').text()
+ assert 'writing-mode: vertical-rl;' in css