mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #3414 from tk0miya/refactor_epub_builder
Refactor epub builder
This commit is contained in:
commit
24fd651bbb
@ -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):
|
||||||
|
@ -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',
|
||||||
|
6
sphinx/templates/epub2/container.xml
Normal file
6
sphinx/templates/epub2/container.xml
Normal 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>
|
37
sphinx/templates/epub2/content.opf_t
Normal file
37
sphinx/templates/epub2/content.opf_t
Normal 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>
|
1
sphinx/templates/epub2/mimetype
Normal file
1
sphinx/templates/epub2/mimetype
Normal file
@ -0,0 +1 @@
|
|||||||
|
application/epub+zip
|
15
sphinx/templates/epub2/toc.ncx_t
Normal file
15
sphinx/templates/epub2/toc.ncx_t
Normal 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>
|
6
sphinx/templates/epub3/container.xml
Normal file
6
sphinx/templates/epub3/container.xml
Normal 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>
|
46
sphinx/templates/epub3/content.opf_t
Normal file
46
sphinx/templates/epub3/content.opf_t
Normal 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>
|
1
sphinx/templates/epub3/mimetype
Normal file
1
sphinx/templates/epub3/mimetype
Normal file
@ -0,0 +1 @@
|
|||||||
|
application/epub+zip
|
26
sphinx/templates/epub3/nav.xhtml_t
Normal file
26
sphinx/templates/epub3/nav.xhtml_t
Normal 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>
|
24
sphinx/templates/epub3/toc.ncx_t
Normal file
24
sphinx/templates/epub3/toc.ncx_t
Normal 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
247
tests/test_build_epub.py
Normal 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
|
Loading…
Reference in New Issue
Block a user