Merge pull request #3414 from tk0miya/refactor_epub_builder

Refactor epub builder
This commit is contained in:
Takeshi KOMIYA 2017-02-17 02:27:07 +09:00 committed by GitHub
commit 24fd651bbb
12 changed files with 571 additions and 392 deletions

View File

@ -12,10 +12,10 @@
import os import os
import re import re
import codecs
import zipfile
from os import path from os import path
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
from datetime import datetime from datetime import datetime
from collections import namedtuple
try: try:
from PIL import Image from PIL import Image
@ -28,10 +28,12 @@ except ImportError:
from docutils import nodes from docutils import nodes
from sphinx import addnodes from sphinx import addnodes
from sphinx import package_dir
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util import logging from sphinx.util import logging
from sphinx.util import status_iterator 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 from sphinx.util.smartypants import sphinx_smarty_pants as ssp
if False: if False:
@ -43,101 +45,14 @@ if False:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# (Fragment) templates from which the metainfo files content.opf, toc.ncx, # (Fragment) templates from which the metainfo files content.opf and
# mimetype, and META-INF/container.xml are created. # toc.ncx are created.
# This template section also defines strings that are embedded in the html # This template section also defines strings that are embedded in the html
# output but that may be customized by (re-)setting module attributes, # output but that may be customized by (re-)setting module attributes,
# e.g. from conf.py. # e.g. from conf.py.
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 = ' '
NODE_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>
<dc:date>%(date)s</dc:date>
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" />
%(files)s
</manifest>
<spine toc="ncx">
%(spine)s
</spine>
<guide>
%(guide)s
</guide>
</package>
'''
COVER_TEMPLATE = u'''\
<meta name="cover" content="%(cover)s"/>
'''
COVERPAGE_NAME = u'epub-cover.xhtml' COVERPAGE_NAME = u'epub-cover.xhtml'
FILE_TEMPLATE = u'''\
<item id="%(id)s"
href="%(href)s"
media-type="%(media_type)s" />'''
SPINE_TEMPLATE = u'''\
<itemref idref="%(idref)s" />'''
NO_LINEAR_SPINE_TEMPLATE = u'''\
<itemref idref="%(idref)s" linear="no" />'''
GUIDE_TEMPLATE = u'''\
<reference type="%(type)s" title="%(title)s" href="%(uri)s" />'''
TOCTREE_TEMPLATE = u'toctree-l%d' TOCTREE_TEMPLATE = u'toctree-l%d'
DOCTYPE = u'''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" DOCTYPE = u'''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
@ -178,6 +93,12 @@ VECTOR_GRAPHICS_EXTENSIONS = ('.svg',)
REFURI_RE = re.compile("([^#:]*#)(.*)") REFURI_RE = re.compile("([^#:]*#)(.*)")
ManifestItem = namedtuple('ManifestItem', ['href', 'id', 'media_type'])
Spine = namedtuple('Spine', ['idref', 'linear'])
Guide = namedtuple('Guide', ['type', 'title', 'uri'])
NavPoint = namedtuple('NavPoint', ['navpoint', 'playorder', 'text', 'refuri', 'children'])
# The epub publisher # The epub publisher
class EpubBuilder(StandaloneHTMLBuilder): class EpubBuilder(StandaloneHTMLBuilder):
@ -190,6 +111,8 @@ class EpubBuilder(StandaloneHTMLBuilder):
""" """
name = 'epub2' name = 'epub2'
template_dir = path.join(package_dir, 'templates', 'epub2')
# don't copy the reST source # don't copy the reST source
copysource = False copysource = False
supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
@ -208,19 +131,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
# don't generate search index or include search page # don't generate search index or include search page
search = False search = False
mimetype_template = MIMETYPE_TEMPLATE
container_template = CONTAINER_TEMPLATE
toc_template = TOC_TEMPLATE
navpoint_template = NAVPOINT_TEMPLATE
navpoint_indent = NAVPOINT_INDENT
node_navpoint_template = NODE_NAVPOINT_TEMPLATE
content_template = CONTENT_TEMPLATE
cover_template = COVER_TEMPLATE
coverpage_name = COVERPAGE_NAME coverpage_name = COVERPAGE_NAME
file_template = FILE_TEMPLATE
spine_template = SPINE_TEMPLATE
no_linear_spine_template = NO_LINEAR_SPINE_TEMPLATE
guide_template = GUIDE_TEMPLATE
toctree_template = TOCTREE_TEMPLATE toctree_template = TOCTREE_TEMPLATE
doctype = DOCTYPE doctype = DOCTYPE
link_target_template = LINK_TARGET_TEMPLATE link_target_template = LINK_TARGET_TEMPLATE
@ -237,6 +148,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
self.link_suffix = '.xhtml' self.link_suffix = '.xhtml'
self.playorder = 0 self.playorder = 0
self.tocid = 0 self.tocid = 0
self.id_cache = {} # type: Dict[unicode, unicode]
self.use_index = self.get_builder_config('use_index', 'epub') self.use_index = self.get_builder_config('use_index', 'epub')
def get_theme_config(self): def get_theme_config(self):
@ -244,14 +156,14 @@ class EpubBuilder(StandaloneHTMLBuilder):
return self.config.epub_theme, self.config.epub_theme_options return self.config.epub_theme, self.config.epub_theme_options
# generic support functions # generic support functions
def make_id(self, name, id_cache={}): def make_id(self, name):
# type: (unicode, Dict[unicode, unicode]) -> unicode # type: (unicode) -> unicode
# id_cache is intentionally mutable # id_cache is intentionally mutable
"""Return a unique id for name.""" """Return a unique id for name."""
id = id_cache.get(name) id = self.id_cache.get(name)
if not id: if not id:
id = 'epub-%d' % self.env.new_serialno('epub') id = 'epub-%d' % self.env.new_serialno('epub')
id_cache[name] = id self.id_cache[name] = id
return id return id
def esc(self, name): def esc(self, name):
@ -552,24 +464,19 @@ class EpubBuilder(StandaloneHTMLBuilder):
# type: (unicode, unicode) -> None # type: (unicode, unicode) -> None
"""Write the metainfo file mimetype.""" """Write the metainfo file mimetype."""
logger.info('writing %s file...', outname) logger.info('writing %s file...', outname)
with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore copy_asset_file(path.join(self.template_dir, 'mimetype'),
f.write(self.mimetype_template) path.join(outdir, outname))
def build_container(self, outdir, outname): def build_container(self, outdir, outname):
# type: (unicode, unicode) -> None # 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) logger.info('writing %s file...', outname)
fn = path.join(outdir, outname) filename = path.join(outdir, outname)
try: ensuredir(path.dirname(filename))
os.mkdir(path.dirname(fn)) copy_asset_file(path.join(self.template_dir, 'container.xml'), filename)
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
def content_metadata(self, files, spine, guide): def content_metadata(self):
# type: (List[unicode], List[unicode], List[unicode]) -> Dict[unicode, Any] # type: () -> Dict[unicode, Any]
"""Create a dictionary with all metadata for the content.opf """Create a dictionary with all metadata for the content.opf
file properly escaped. file properly escaped.
""" """
@ -583,9 +490,9 @@ class EpubBuilder(StandaloneHTMLBuilder):
metadata['scheme'] = self.esc(self.config.epub_scheme) metadata['scheme'] = self.esc(self.config.epub_scheme)
metadata['id'] = self.esc(self.config.epub_identifier) metadata['id'] = self.esc(self.config.epub_identifier)
metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%d")) metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%d"))
metadata['files'] = files metadata['manifest_items'] = []
metadata['spine'] = spine metadata['spines'] = []
metadata['guide'] = guide metadata['guides'] = []
return metadata return metadata
def build_content(self, outdir, outname): def build_content(self, outdir, outname):
@ -594,12 +501,12 @@ class EpubBuilder(StandaloneHTMLBuilder):
a file list and the spine (the reading order). a file list and the spine (the reading order).
""" """
logger.info('writing %s file...', outname) logger.info('writing %s file...', outname)
metadata = self.content_metadata()
# files # files
if not outdir.endswith(os.sep): if not outdir.endswith(os.sep):
outdir += os.sep outdir += os.sep
olen = len(outdir) olen = len(outdir)
projectfiles = [] # type: List[unicode]
self.files = [] # type: List[unicode] self.files = [] # type: List[unicode]
self.ignored_files = ['.buildinfo', 'mimetype', 'content.opf', self.ignored_files = ['.buildinfo', 'mimetype', 'content.opf',
'toc.ncx', 'META-INF/container.xml', 'toc.ncx', 'META-INF/container.xml',
@ -609,7 +516,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
if not self.use_index: if not self.use_index:
self.ignored_files.append('genindex' + self.out_suffix) self.ignored_files.append('genindex' + self.out_suffix)
for root, dirs, files in os.walk(outdir): for root, dirs, files in os.walk(outdir):
for fn in files: for fn in sorted(files):
filename = path.join(root, fn)[olen:] filename = path.join(root, fn)[olen:]
if filename in self.ignored_files: if filename in self.ignored_files:
continue continue
@ -622,70 +529,57 @@ class EpubBuilder(StandaloneHTMLBuilder):
type='epub', subtype='unknown_project_files') type='epub', subtype='unknown_project_files')
continue continue
filename = filename.replace(os.sep, '/') filename = filename.replace(os.sep, '/')
projectfiles.append(self.file_template % { item = ManifestItem(self.esc(filename),
'href': self.esc(filename), self.esc(self.make_id(filename)),
'id': self.esc(self.make_id(filename)), self.esc(self.media_types[ext]))
'media_type': self.esc(self.media_types[ext]) metadata['manifest_items'].append(item)
})
self.files.append(filename) self.files.append(filename)
# spine # spine
spine = []
spinefiles = set() spinefiles = set()
for item in self.refnodes: for refnode in self.refnodes:
if '#' in item['refuri']: if '#' in refnode['refuri']:
continue continue
if item['refuri'] in self.ignored_files: if refnode['refuri'] in self.ignored_files:
continue continue
spine.append(self.spine_template % { spine = Spine(self.esc(self.make_id(refnode['refuri'])), True)
'idref': self.esc(self.make_id(item['refuri'])) metadata['spines'].append(spine)
}) spinefiles.add(refnode['refuri'])
spinefiles.add(item['refuri'])
for info in self.domain_indices: for info in self.domain_indices:
spine.append(self.spine_template % { spine = Spine(self.esc(self.make_id(info[0] + self.out_suffix)), True)
'idref': self.esc(self.make_id(info[0] + self.out_suffix)) metadata['spines'].append(spine)
})
spinefiles.add(info[0] + self.out_suffix) spinefiles.add(info[0] + self.out_suffix)
if self.use_index: if self.use_index:
spine.append(self.spine_template % { spine = Spine(self.esc(self.make_id('genindex' + self.out_suffix)), True)
'idref': self.esc(self.make_id('genindex' + self.out_suffix)) metadata['spines'].append(spine)
})
spinefiles.add('genindex' + self.out_suffix) spinefiles.add('genindex' + self.out_suffix)
# add auto generated files # add auto generated files
for name in self.files: for name in self.files:
if name not in spinefiles and name.endswith(self.out_suffix): if name not in spinefiles and name.endswith(self.out_suffix):
spine.append(self.no_linear_spine_template % { spine = Spine(self.esc(self.make_id(name)), False)
'idref': self.esc(self.make_id(name)) metadata['spines'].append(spine)
})
# add the optional cover # add the optional cover
content_tmpl = self.content_template
html_tmpl = None html_tmpl = None
if self.config.epub_cover: if self.config.epub_cover:
image, html_tmpl = self.config.epub_cover image, html_tmpl = self.config.epub_cover
image = image.replace(os.sep, '/') image = image.replace(os.sep, '/')
mpos = content_tmpl.rfind('</metadata>') metadata['cover'] = self.esc(self.make_id(image))
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:]
if html_tmpl: if html_tmpl:
spine.insert(0, self.spine_template % { spine = Spine(self.esc(self.make_id(self.coverpage_name)), True)
'idref': self.esc(self.make_id(self.coverpage_name))}) metadata['spines'].insert(0, spine)
if self.coverpage_name not in self.files: if self.coverpage_name not in self.files:
ext = path.splitext(self.coverpage_name)[-1] ext = path.splitext(self.coverpage_name)[-1]
self.files.append(self.coverpage_name) self.files.append(self.coverpage_name)
projectfiles.append(self.file_template % { item = ManifestItem(self.esc(filename),
'href': self.esc(self.coverpage_name), self.esc(self.make_id(filename)),
'id': self.esc(self.make_id(self.coverpage_name)), self.esc(self.media_types[ext]))
'media_type': self.esc(self.media_types[ext]) metadata['manifest_items'].append(item)
})
ctx = {'image': self.esc(image), 'title': self.config.project} ctx = {'image': self.esc(image), 'title': self.config.project}
self.handle_page( self.handle_page(
path.splitext(self.coverpage_name)[0], ctx, html_tmpl) path.splitext(self.coverpage_name)[0], ctx, html_tmpl)
spinefiles.add(self.coverpage_name) spinefiles.add(self.coverpage_name)
guide = []
auto_add_cover = True auto_add_cover = True
auto_add_toc = True auto_add_toc = True
if self.config.epub_guide: if self.config.epub_guide:
@ -697,64 +591,43 @@ class EpubBuilder(StandaloneHTMLBuilder):
auto_add_cover = False auto_add_cover = False
if type == 'toc': if type == 'toc':
auto_add_toc = False auto_add_toc = False
guide.append(self.guide_template % { metadata['guides'].append(Guide(self.esc(type),
'type': self.esc(type), self.esc(title),
'title': self.esc(title), self.esc(uri)))
'uri': self.esc(uri)
})
if auto_add_cover and html_tmpl: if auto_add_cover and html_tmpl:
guide.append(self.guide_template % { metadata['guides'].append(Guide('cover',
'type': 'cover', self.guide_titles['cover'],
'title': self.guide_titles['cover'], self.esc(self.coverpage_name)))
'uri': self.esc(self.coverpage_name)
})
if auto_add_toc and self.refnodes: if auto_add_toc and self.refnodes:
guide.append(self.guide_template % { metadata['guides'].append(Guide('toc',
'type': 'toc', self.guide_titles['toc'],
'title': self.guide_titles['toc'], self.esc(self.refnodes[0]['refuri'])))
'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
# write the project file # write the project file
with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore copy_asset_file(path.join(self.template_dir, 'content.opf_t'),
f.write(content_tmpl % # type: ignore path.join(outdir, outname),
self.content_metadata(projectfiles, spine, guide)) metadata)
def new_navpoint(self, node, level, incr=True): 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.""" """Create a new entry in the toc from the node at given level."""
# XXX Modifies the node # XXX Modifies the node
if incr: if incr:
self.playorder += 1 self.playorder += 1
self.tocid += 1 self.tocid += 1
node['indent'] = self.navpoint_indent * level return NavPoint(self.esc('navPoint%d' % self.tocid), self.playorder,
node['navpoint'] = self.esc(self.node_navpoint_template % self.tocid) node['text'], node['refuri'], [])
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)
def build_navpoints(self, nodes): def build_navpoints(self, nodes):
# type: (nodes.Node) -> unicode # type: (nodes.Node) -> List[NavPoint]
"""Create the toc navigation structure. """Create the toc navigation structure.
Subelements of a node are nested inside the navpoint. For nested nodes Subelements of a node are nested inside the navpoint. For nested nodes
the parent node is reinserted in the subnav. the parent node is reinserted in the subnav.
""" """
navstack = [] navstack = [] # type: List[NavPoint]
navlist = [] navstack.append(NavPoint('dummy', '', '', '', []))
level = 1 level = 0
lastnode = None lastnode = None
for node in nodes: for node in nodes:
if not node['text']: if not node['text']:
@ -765,32 +638,33 @@ class EpubBuilder(StandaloneHTMLBuilder):
if node['level'] > self.config.epub_tocdepth: if node['level'] > self.config.epub_tocdepth:
continue continue
if node['level'] == level: 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: elif node['level'] == level + 1:
navstack.append(navlist)
navlist = []
level += 1 level += 1
if lastnode and self.config.epub_tocdup: if lastnode and self.config.epub_tocdup:
# Insert starting point in subtoc with same playOrder # Insert starting point in subtoc with same playOrder
navlist.append(self.new_navpoint(lastnode, level, False)) navstack[-1].children.append(self.new_navpoint(lastnode, level, False))
navlist.append(self.new_navpoint(node, level)) 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: else:
while node['level'] < level: raise
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 lastnode = node
while level != 1:
subnav = '\n'.join(navlist) return navstack[0].children
navlist = navstack.pop()
navlist[-1] = self.insert_subnav(navlist[-1], subnav)
level -= 1
return '\n'.join(navlist)
def toc_metadata(self, level, navpoints): 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 """Create a dictionary with all metadata for the toc.ncx file
properly escaped. properly escaped.
""" """
@ -818,8 +692,9 @@ class EpubBuilder(StandaloneHTMLBuilder):
navpoints = self.build_navpoints(refnodes) navpoints = self.build_navpoints(refnodes)
level = max(item['level'] for item in self.refnodes) level = max(item['level'] for item in self.refnodes)
level = min(level, self.config.epub_tocdepth) level = min(level, self.config.epub_tocdepth)
with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore copy_asset_file(path.join(self.template_dir, 'toc.ncx_t'),
f.write(self.toc_template % self.toc_metadata(level, navpoints)) # type: ignore path.join(outdir, outname),
self.toc_metadata(level, navpoints))
def build_epub(self, outdir, outname): def build_epub(self, outdir, outname):
# type: (unicode, unicode) -> None # type: (unicode, unicode) -> None
@ -829,16 +704,13 @@ class EpubBuilder(StandaloneHTMLBuilder):
entry. entry.
""" """
logger.info('writing %s file...', outname) logger.info('writing %s file...', outname)
projectfiles = ['META-INF/container.xml', 'content.opf', 'toc.ncx'] # type: List[unicode] # NOQA epub_filename = path.join(outdir, outname)
projectfiles.extend(self.files) with ZipFile(epub_filename, 'w', ZIP_DEFLATED) as epub: # type: ignore
epub = zipfile.ZipFile(path.join(outdir, outname), 'w', # type: ignore epub.write(path.join(outdir, 'mimetype'), 'mimetype', ZIP_STORED) # type: ignore
zipfile.ZIP_DEFLATED) for filename in [u'META-INF/container.xml', u'content.opf', u'toc.ncx']:
epub.write(path.join(outdir, 'mimetype'), 'mimetype', # type: ignore epub.write(path.join(outdir, filename), filename, ZIP_DEFLATED) # type: ignore
zipfile.ZIP_STORED) for filename in self.files:
for file in projectfiles: epub.write(path.join(outdir, filename), filename, ZIP_DEFLATED) # type: ignore
fp = path.join(outdir, file)
epub.write(fp, file, zipfile.ZIP_DEFLATED) # type: ignore
epub.close()
def setup(app): def setup(app):

View File

@ -10,13 +10,15 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import codecs
from os import path from os import path
from datetime import datetime 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.builders.epub import EpubBuilder
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.fileutil import copy_asset_file
if False: if False:
# For type annotation # For type annotation
@ -27,79 +29,24 @@ if False:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# (Fragment) templates from which the metainfo files content.opf, toc.ncx, NavPoint = namedtuple('NavPoint', ['text', 'refuri', 'children'])
# 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.
NAVIGATION_DOC_TEMPLATE = u'''\ # writing modes
<?xml version="1.0" encoding="UTF-8"?> PAGE_PROGRESSION_DIRECTIONS = {
<!DOCTYPE html> 'horizontal': 'ltr',
<html xmlns="http://www.w3.org/1999/xhtml"\ 'vertical': 'rtl',
xmlns:epub="http://www.idpf.org/2007/ops" lang="%(lang)s" xml:lang="%(lang)s"> }
<head> IBOOK_SCROLL_AXIS = {
<title>%(toc_locale)s</title> 'horizontal': 'vertical',
</head> 'vertical': 'horizontal',
<body> }
<nav epub:type="toc"> THEME_WRITING_MODES = {
<h1>%(toc_locale)s</h1> 'vertical': 'vertical-rl',
<ol> 'horizontal': 'horizontal-tb',
%(navlist)s }
</ol>
</nav>
</body>
</html>
'''
NAVLIST_TEMPLATE = u'''%(indent)s <li><a href="%(refuri)s">%(text)s</a></li>'''
NAVLIST_TEMPLATE_HAS_CHILD = u'''%(indent)s <li><a href="%(refuri)s">%(text)s</a>'''
NAVLIST_TEMPLATE_BEGIN_BLOCK = u'''%(indent)s <ol>'''
NAVLIST_TEMPLATE_END_BLOCK = u'''%(indent)s </ol>
%(indent)s </li>'''
NAVLIST_INDENT = ' '
PACKAGE_DOC_TEMPLATE = u'''\
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="%(lang)s"
unique-identifier="%(uid)s"
prefix="ibooks: http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/">
<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:description>%(description)s</dc:description>
<dc:creator>%(author)s</dc:creator>
<dc:contributor>%(contributor)s</dc:contributor>
<dc:publisher>%(publisher)s</dc:publisher>
<dc:rights>%(copyright)s</dc:rights>
<dc:identifier id="%(uid)s">%(id)s</dc:identifier>
<dc:date>%(date)s</dc:date>
<meta property="dcterms:modified">%(date)s</meta>
<meta property="ibooks:version">%(version)s</meta>
<meta property="ibooks:specified-fonts">true</meta>
<meta property="ibooks:binding">true</meta>
<meta property="ibooks:scroll-axis">%(ibook_scroll_axis)s</meta>
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" />
<item id="nav" href="nav.xhtml"\
media-type="application/xhtml+xml" properties="nav"/>
%(files)s
</manifest>
<spine toc="ncx" page-progression-direction="%(page_progression_direction)s">
%(spine)s
</spine>
<guide>
%(guide)s
</guide>
</package>
'''
DOCTYPE = u'''<!DOCTYPE html>''' DOCTYPE = u'''<!DOCTYPE html>'''
# The epub3 publisher
class Epub3Builder(EpubBuilder): class Epub3Builder(EpubBuilder):
""" """
@ -111,13 +58,7 @@ class Epub3Builder(EpubBuilder):
""" """
name = 'epub' name = 'epub'
navigation_doc_template = NAVIGATION_DOC_TEMPLATE template_dir = path.join(package_dir, 'templates', 'epub3')
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
doctype = DOCTYPE doctype = DOCTYPE
# Finish by building the epub file # Finish by building the epub file
@ -132,77 +73,31 @@ class Epub3Builder(EpubBuilder):
self.build_toc(self.outdir, 'toc.ncx') self.build_toc(self.outdir, 'toc.ncx')
self.build_epub(self.outdir, self.config.epub_basename + '.epub') self.build_epub(self.outdir, self.config.epub_basename + '.epub')
def content_metadata(self, files, spine, guide): def content_metadata(self):
# type: (List[unicode], List[unicode], List[unicode]) -> Dict # type: () -> Dict
"""Create a dictionary with all metadata for the content.opf """Create a dictionary with all metadata for the content.opf
file properly escaped. file properly escaped.
""" """
metadata = super(Epub3Builder, self).content_metadata( writing_mode = self.config.epub_writing_mode
files, spine, guide)
metadata = super(Epub3Builder, self).content_metadata()
metadata['description'] = self.esc(self.config.epub_description) metadata['description'] = self.esc(self.config.epub_description)
metadata['contributor'] = self.esc(self.config.epub_contributor) metadata['contributor'] = self.esc(self.config.epub_contributor)
metadata['page_progression_direction'] = self._page_progression_direction() metadata['page_progression_direction'] = PAGE_PROGRESSION_DIRECTIONS.get(writing_mode)
metadata['ibook_scroll_axis'] = self._ibook_scroll_axis() metadata['ibook_scroll_axis'] = IBOOK_SCROLL_AXIS.get(writing_mode)
metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")) metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
metadata['version'] = self.esc(self.config.version) metadata['version'] = self.esc(self.config.version)
return metadata 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): def prepare_writing(self, docnames):
# type: (Iterable[unicode]) -> None # type: (Iterable[unicode]) -> None
super(Epub3Builder, self).prepare_writing(docnames) super(Epub3Builder, self).prepare_writing(docnames)
self.globalcontext['theme_writing_mode'] = self._css_writing_mode()
def new_navlist(self, node, level, has_child): writing_mode = self.config.epub_writing_mode
# type: (nodes.Node, int, bool) -> unicode self.globalcontext['theme_writing_mode'] = THEME_WRITING_MODES.get(writing_mode)
"""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}
def build_navlist(self, navnodes): def build_navlist(self, navnodes):
# type: (List[nodes.Node]) -> unicode # type: (List[nodes.Node]) -> List[NavPoint]
"""Create the toc navigation structure. """Create the toc navigation structure.
This method is almost same as build_navpoints method in epub.py. 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 The difference from build_navpoints method is templates which are used
when generating navigation documents. when generating navigation documents.
""" """
navlist = [] navstack = [] # type: List[NavPoint]
level = 1 navstack.append(NavPoint('', '', []))
usenodes = [] level = 0
for node in navnodes: for node in navnodes:
if not node['text']: if not node['text']:
continue continue
@ -223,31 +118,33 @@ class Epub3Builder(EpubBuilder):
continue continue
if node['level'] > self.config.epub_tocdepth: if node['level'] > self.config.epub_tocdepth:
continue continue
usenodes.append(node)
for i, node in enumerate(usenodes): navpoint = NavPoint(node['text'], node['refuri'], [])
curlevel = node['level'] if node['level'] == level:
if curlevel == level + 1: navstack.pop()
navlist.append(self.begin_navlist_block(level)) navstack[-1].children.append(navpoint)
while curlevel < level: navstack.append(navpoint)
level -= 1 elif node['level'] == level + 1:
navlist.append(self.end_navlist_block(level)) level += 1
level = curlevel navstack[-1].children.append(navpoint)
if i != len(usenodes) - 1 and usenodes[i + 1]['level'] > level: navstack.append(navpoint)
has_child = True elif node['level'] < level:
while node['level'] < len(navstack):
navstack.pop()
level = node['level']
navstack[-1].children.append(navpoint)
navstack.append(navpoint)
else: else:
has_child = False raise
navlist.append(self.new_navlist(node, level, has_child))
while level != 1: return navstack[0].children
level -= 1
navlist.append(self.end_navlist_block(level))
return '\n'.join(navlist)
def navigation_doc_metadata(self, navlist): def navigation_doc_metadata(self, navlist):
# type: (unicode) -> Dict # type: (List[NavPoint]) -> Dict
"""Create a dictionary with all metadata for the nav.xhtml file """Create a dictionary with all metadata for the nav.xhtml file
properly escaped. properly escaped.
""" """
metadata = {} metadata = {} # type: Dict
metadata['lang'] = self.esc(self.config.epub_language) metadata['lang'] = self.esc(self.config.epub_language)
metadata['toc_locale'] = self.esc(self.guide_titles['toc']) metadata['toc_locale'] = self.esc(self.guide_titles['toc'])
metadata['navlist'] = navlist metadata['navlist'] = navlist
@ -268,9 +165,9 @@ class Epub3Builder(EpubBuilder):
# 'includehidden' # 'includehidden'
refnodes = self.refnodes refnodes = self.refnodes
navlist = self.build_navlist(refnodes) navlist = self.build_navlist(refnodes)
with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: # type: ignore copy_asset_file(path.join(self.template_dir, 'nav.xhtml_t'),
f.write(self.navigation_doc_template % # type: ignore path.join(outdir, outname),
self.navigation_doc_metadata(navlist)) self.navigation_doc_metadata(navlist))
# Add nav.xhtml to epub file # Add nav.xhtml to epub file
if outname not in self.files: 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_description', '', 'epub3', string_classes)
app.add_config_value('epub_contributor', 'unknown', '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 { return {
'version': 'builtin', 'version': 'builtin',

View File

@ -0,0 +1,6 @@
<?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>

View File

@ -0,0 +1,37 @@
<?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 }}</dc:language>
<dc:title>{{ title }}</dc:title>
<dc:creator opf:role="aut">{{ author }}</dc:creator>
<dc:publisher>{{ publisher }}</dc:publisher>
<dc:rights>{{ copyright }}</dc:rights>
<dc:identifier id="{{ uid }}" opf:scheme="{{ scheme }}">{{ id }}</dc:identifier>
<dc:date>{{ date }}</dc:date>
{%- if cover %}
<meta name="cover" content="{{ cover }}"/>
{%- endif %}
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" />
{%- for item in manifest_items %}
<item id="{{ item.id }}" href="{{ item.href }}" media-type="{{ item.media_type }}" />
{%- endfor %}
</manifest>
<spine toc="ncx">
{%- for spine in spines %}
{%- if spine.linear %}
<itemref idref="{{ spine.idref }}" />
{%- else %}
<itemref idref="{{ spine.idref }}" linear="no" />'''
{%- endif %}
{%- endfor %}
</spine>
<guide>
{%- for guide in guides %}
<reference type="{{ guide.type }}" title="{{ guide.title }}" href="{{ guide.uri }}" />'''
{%- endfor %}
</guide>
</package>

View File

@ -0,0 +1 @@
application/epub+zip

View File

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<ncx version="2005-1" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<head>
<meta name="dtb:uid" content="{{ uid }}"/>
<meta name="dtb:depth" content="{{ level }}"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle>
<text>{{ title }}</text>
</docTitle>
<navMap>
{{ navpoints }}
</navMap>
</ncx>

View File

@ -0,0 +1,6 @@
<?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>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="{{ lang }}"
unique-identifier="{{ uid }}"
prefix="ibooks: http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/">
<metadata xmlns:opf="http://www.idpf.org/2007/opf"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:language>{{ lang }}</dc:language>
<dc:title>{{ title }}</dc:title>
<dc:description>{{ description }}</dc:description>
<dc:creator>{{ author }}</dc:creator>
<dc:contributor>{{ contributor }}</dc:contributor>
<dc:publisher>{{ publisher }}</dc:publisher>
<dc:rights>{{ copyright }}</dc:rights>
<dc:identifier id="{{ uid }}">{{ id }}</dc:identifier>
<dc:date>{{ date }}</dc:date>
<meta property="dcterms:modified">{{ date }}</meta>
<meta property="ibooks:version">{{ version }}</meta>
<meta property="ibooks:specified-fonts">true</meta>
<meta property="ibooks:binding">true</meta>
<meta property="ibooks:scroll-axis">{{ ibook_scroll_axis }}</meta>
{%- if cover %}
<meta name="cover" content="{{ cover }}"/>
{%- endif %}
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" />
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
{%- for item in manifest_items %}
<item id="{{ item.id }}" href="{{ item.href }}" media-type="{{ item.media_type }}" />
{%- endfor %}
</manifest>
<spine toc="ncx" page-progression-direction="{{ page_progression_direction }}">
{%- for spine in spines %}
{%- if spine.linear %}
<itemref idref="{{ spine.idref }}" />
{%- else %}
<itemref idref="{{ spine.idref }}" linear="no" />'''
{%- endif %}
{%- endfor %}
</spine>
<guide>
{%- for guide in guides %}
<reference type="{{ guide.type }}" title="{{ guide.title }}" href="{{ guide.uri }}" />'''
{%- endfor %}
</guide>
</package>

View File

@ -0,0 +1 @@
application/epub+zip

View File

@ -0,0 +1,26 @@
{%- macro toctree(navlist) -%}
<ol>
{%- for nav in navlist %}
<li>
<a href="{{ nav.refuri }}">{{ nav.text }}</a>
{%- if nav.children %}
{{ toctree(nav.children)|indent(4, true) }}
{%- endif %}
</li>
{%- endfor %}
</ol>
{%- endmacro -%}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops" lang="{{ lang }}" xml:lang="{{ lang }}">
<head>
<title>{{ toc_locale }}</title>
</head>
<body>
<nav epub:type="toc">
<h1>{{ toc_locale }}</h1>
{{ toctree(navlist)|indent(6, true) }}
</nav>
</body>
</html>

View File

@ -0,0 +1,24 @@
{%- macro navPoints(navlist) %}
{%- for nav in navlist %}
<navPoint id="{{ nav.navpoint }}" playOrder="{{ nav.playorder }}">
<navLabel>
<text>{{ nav.text }}</text>
</navLabel>
<content src="{{ nav.refuri }}" />{{ navPoints(nav.children)|indent(2, true) }}
</navPoint>
{%- endfor %}
{%- endmacro -%}
<?xml version="1.0"?>
<ncx version="2005-1" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<head>
<meta name="dtb:uid" content="{{ uid }}"/>
<meta name="dtb:depth" content="{{ level }}"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle>
<text>{{ title }}</text>
</docTitle>
<navMap>{{ navPoints(navpoints)|indent(4, true) }}
</navMap>
</ncx>

247
tests/test_build_epub.py Normal file
View File

@ -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