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