From 201ae9c1acd3800c224666e2031846cda719e48e Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 8 Jan 2017 15:47:34 +0900 Subject: [PATCH 01/11] epub: Use templates to generate EPUB files --- sphinx/builders/epub.py | 42 +++++++++------------------- sphinx/builders/epub3.py | 3 ++ sphinx/templates/epub2/container.xml | 6 ++++ sphinx/templates/epub2/mimetype | 1 + sphinx/templates/epub3/container.xml | 6 ++++ sphinx/templates/epub3/mimetype | 1 + tests/test_build_epub.py | 19 +++++++++++++ 7 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 sphinx/templates/epub2/container.xml create mode 100644 sphinx/templates/epub2/mimetype create mode 100644 sphinx/templates/epub3/container.xml create mode 100644 sphinx/templates/epub3/mimetype create mode 100644 tests/test_build_epub.py diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index d61f4868f..cdb766012 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -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,25 +45,12 @@ 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'''\ @@ -190,6 +179,8 @@ class EpubBuilder(StandaloneHTMLBuilder): """ name = 'epub2' + template_dir = path.join(package_dir, 'templates', 'epub2') + # don't copy the reST source copysource = False supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', @@ -208,8 +199,6 @@ class EpubBuilder(StandaloneHTMLBuilder): # don't generate search index or include search page search = False - mimetype_template = MIMETYPE_TEMPLATE - container_template = CONTAINER_TEMPLATE toc_template = TOC_TEMPLATE navpoint_template = NAVPOINT_TEMPLATE navpoint_indent = NAVPOINT_INDENT @@ -552,21 +541,16 @@ 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] diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index 2723bff9c..040f4fc41 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -14,6 +14,7 @@ import codecs from os import path from datetime import datetime +from sphinx import package_dir from sphinx.config import string_classes from sphinx.builders.epub import EpubBuilder from sphinx.util import logging @@ -111,6 +112,8 @@ class Epub3Builder(EpubBuilder): """ name = 'epub' + template_dir = path.join(package_dir, 'templates', 'epub3') + navigation_doc_template = NAVIGATION_DOC_TEMPLATE navlist_template = NAVLIST_TEMPLATE navlist_template_has_child = NAVLIST_TEMPLATE_HAS_CHILD 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/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/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/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/tests/test_build_epub.py b/tests/test_build_epub.py new file mode 100644 index 000000000..ad08dbe7d --- /dev/null +++ b/tests/test_build_epub.py @@ -0,0 +1,19 @@ +# -*- 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. +""" + +import pytest + + +@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() From fec760bf94a68b42a42e146b5e3f20c806823c44 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 9 Jan 2017 00:30:29 +0900 Subject: [PATCH 02/11] Add testcases for epub --- tests/test_build_epub.py | 122 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index ad08dbe7d..3e377bb9a 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -9,11 +9,133 @@ :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/', + } + + 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': 'navPoint2', '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' + + +@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') From fa6cc544ee72298390717c9485ccb484cb41d00b Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 9 Jan 2017 02:22:09 +0900 Subject: [PATCH 03/11] epub: Use templates for content.opf --- sphinx/builders/epub.py | 149 ++++++++------------------- sphinx/builders/epub3.py | 44 +------- sphinx/templates/epub2/content.opf_t | 37 +++++++ sphinx/templates/epub3/content.opf_t | 46 +++++++++ 4 files changed, 127 insertions(+), 149 deletions(-) create mode 100644 sphinx/templates/epub2/content.opf_t create mode 100644 sphinx/templates/epub3/content.opf_t diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index cdb766012..434d46328 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -16,6 +16,7 @@ import codecs import zipfile from os import path from datetime import datetime +from collections import namedtuple try: from PIL import Image @@ -80,53 +81,8 @@ NAVPOINT_TEMPLATE = u'''\ 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''' Dict[unicode, Any] + def content_metadata(self): + # type: () -> Dict[unicode, Any] """Create a dictionary with all metadata for the content.opf file properly escaped. """ @@ -567,9 +522,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): @@ -578,12 +533,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', @@ -606,70 +561,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']: continue if item['refuri'] in self.ignored_files: continue - spine.append(self.spine_template % { - 'idref': self.esc(self.make_id(item['refuri'])) - }) + spine = Spine(self.esc(self.make_id(item['refuri'])), True) + metadata['spines'].append(spine) spinefiles.add(item['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))}) 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: @@ -681,31 +623,22 @@ 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 diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index 040f4fc41..a1849accc 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -60,42 +60,6 @@ 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 - - -''' DOCTYPE = u'''''' @@ -120,7 +84,6 @@ class Epub3Builder(EpubBuilder): 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 # Finish by building the epub file @@ -135,13 +98,12 @@ 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) + 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() 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/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 %} + + From a051fe2c6d48ddf8dd2f8c05e84b5e758a859fc7 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 9 Jan 2017 03:53:31 +0900 Subject: [PATCH 04/11] epub: Add template for tox.ncx --- sphinx/builders/epub.py | 97 +++++++++----------------------- sphinx/templates/epub2/toc.ncx_t | 15 +++++ sphinx/templates/epub3/toc.ncx_t | 24 ++++++++ tests/test_build_epub.py | 31 ++++++++++ 4 files changed, 97 insertions(+), 70 deletions(-) create mode 100644 sphinx/templates/epub2/toc.ncx_t create mode 100644 sphinx/templates/epub3/toc.ncx_t diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index 434d46328..293a6ffe6 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -12,7 +12,6 @@ import os import re -import codecs import zipfile from os import path from datetime import datetime @@ -52,35 +51,6 @@ logger = logging.getLogger(__name__) # output but that may be customized by (re-)setting module attributes, # e.g. from conf.py. -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' - COVERPAGE_NAME = u'epub-cover.xhtml' TOCTREE_TEMPLATE = u'toctree-l%d' @@ -126,6 +96,7 @@ 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 @@ -160,10 +131,6 @@ class EpubBuilder(StandaloneHTMLBuilder): # don't generate search index or include search page search = False - toc_template = TOC_TEMPLATE - navpoint_template = NAVPOINT_TEMPLATE - navpoint_indent = NAVPOINT_INDENT - node_navpoint_template = NODE_NAVPOINT_TEMPLATE coverpage_name = COVERPAGE_NAME toctree_template = TOCTREE_TEMPLATE doctype = DOCTYPE @@ -647,20 +614,8 @@ class EpubBuilder(StandaloneHTMLBuilder): 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 @@ -669,9 +624,9 @@ class EpubBuilder(StandaloneHTMLBuilder): 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']: @@ -682,29 +637,30 @@ 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] @@ -735,8 +691,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 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/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 index 3e377bb9a..71d94f820 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -139,3 +139,34 @@ def test_epub_cover(app): 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]) == ('navPoint9', '1', 'index.xhtml', + "Welcome to Sphinx Tests's documentation!") + assert navpoints[0].findall("./ncx:navPoint") == [] + + # toc.ncx / nested navPoints + assert navinfo(navpoints[1]) == ('navPoint10', '2', 'foo.xhtml', 'foo') + navchildren = navpoints[1].findall("./ncx:navPoint") + assert len(navchildren) == 4 + assert navinfo(navchildren[0]) == ('navPoint11', '2', 'foo.xhtml', 'foo') + assert navinfo(navchildren[1]) == ('navPoint12', '3', 'quux.xhtml', 'quux') + assert navinfo(navchildren[2]) == ('navPoint13', '4', 'foo.xhtml#foo-1', 'foo.1') + assert navinfo(navchildren[3]) == ('navPoint16', '6', 'foo.xhtml#foo-2', 'foo.2') From 065d86a2009d7675decf5566f4e9c93a9d3219d1 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 9 Jan 2017 13:33:41 +0900 Subject: [PATCH 05/11] epub: Fix mypy violations --- sphinx/builders/epub.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index 293a6ffe6..ff9ed5e2a 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -536,14 +536,14 @@ class EpubBuilder(StandaloneHTMLBuilder): # 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 = Spine(self.esc(self.make_id(item['refuri'])), True) + spine = Spine(self.esc(self.make_id(refnode['refuri'])), True) metadata['spines'].append(spine) - spinefiles.add(item['refuri']) + spinefiles.add(refnode['refuri']) for info in self.domain_indices: spine = Spine(self.esc(self.make_id(info[0] + self.out_suffix)), True) metadata['spines'].append(spine) @@ -565,8 +565,8 @@ class EpubBuilder(StandaloneHTMLBuilder): image = image.replace(os.sep, '/') 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) @@ -608,7 +608,7 @@ class EpubBuilder(StandaloneHTMLBuilder): 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: @@ -618,7 +618,7 @@ class EpubBuilder(StandaloneHTMLBuilder): 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 @@ -663,7 +663,7 @@ class EpubBuilder(StandaloneHTMLBuilder): 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. """ From ad317483d2fbabea558fdb52a71aab8d5c6fa2a6 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 11 Feb 2017 15:53:48 +0900 Subject: [PATCH 06/11] epub: refactor to use "with" context --- sphinx/builders/epub.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index ff9ed5e2a..ef6d40ae7 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -12,8 +12,8 @@ import os import re -import zipfile from os import path +from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile from datetime import datetime from collections import namedtuple @@ -703,16 +703,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): From bbd52dd3474cacc1bc4e4c3307ddae8867690c44 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 11 Feb 2017 17:22:26 +0900 Subject: [PATCH 07/11] epub: Do not count up tocid on creating nav.xhtml --- sphinx/builders/epub3.py | 1 - tests/test_build_epub.py | 54 ++++++++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index a1849accc..e1d7bc9a2 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -149,7 +149,6 @@ class Epub3Builder(EpubBuilder): # 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 diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index 71d94f820..08844300c 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -21,6 +21,8 @@ class EPUBElementTree(object): '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): @@ -69,7 +71,7 @@ def test_build_epub(app): # toc.ncx / navMap navpoints = toc.findall("./ncx:navMap/ncx:navPoint") assert len(navpoints) == 1 - assert navpoints[0].attrib == {'id': 'navPoint2', 'playOrder': '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") @@ -127,6 +129,20 @@ def test_build_epub(app): 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)}) @@ -158,15 +174,39 @@ def test_nested_toc(app): navpoints = toc.findall("./ncx:navMap/ncx:navPoint") assert len(navpoints) == 4 - assert navinfo(navpoints[0]) == ('navPoint9', '1', 'index.xhtml', + 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]) == ('navPoint10', '2', 'foo.xhtml', 'foo') + assert navinfo(navpoints[1]) == ('navPoint2', '2', 'foo.xhtml', 'foo') navchildren = navpoints[1].findall("./ncx:navPoint") assert len(navchildren) == 4 - assert navinfo(navchildren[0]) == ('navPoint11', '2', 'foo.xhtml', 'foo') - assert navinfo(navchildren[1]) == ('navPoint12', '3', 'quux.xhtml', 'quux') - assert navinfo(navchildren[2]) == ('navPoint13', '4', 'foo.xhtml#foo-1', 'foo.1') - assert navinfo(navchildren[3]) == ('navPoint16', '6', 'foo.xhtml#foo-2', 'foo.2') + 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') From 95cf4e043fdfdb1c1fde19d07d6f68a049a3c93e Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 11 Feb 2017 17:23:11 +0900 Subject: [PATCH 08/11] epub: Add template for nav.xhtml --- sphinx/builders/epub3.py | 117 ++++++++--------------------- sphinx/templates/epub3/nav.xhtml_t | 26 +++++++ 2 files changed, 57 insertions(+), 86 deletions(-) create mode 100644 sphinx/templates/epub3/nav.xhtml_t diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index e1d7bc9a2..4cea39956 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -10,14 +10,15 @@ :license: BSD, see LICENSE for details. """ -import codecs from os import path from datetime import datetime +from collections import namedtuple from sphinx import package_dir from sphinx.config import string_classes from sphinx.builders.epub import EpubBuilder from sphinx.util import logging +from sphinx.util.fileutil import copy_asset_file if False: # For type annotation @@ -28,38 +29,7 @@ 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. - -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 = ' ' - +NavPoint = namedtuple('NavPoint', ['text', 'refuri', 'children']) DOCTYPE = u'''''' @@ -77,13 +47,6 @@ class Epub3Builder(EpubBuilder): name = 'epub' template_dir = path.join(package_dir, 'templates', 'epub3') - - 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 doctype = DOCTYPE # Finish by building the epub file @@ -145,28 +108,8 @@ class Epub3Builder(EpubBuilder): 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 - 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): - # 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. @@ -176,9 +119,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 @@ -187,31 +130,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 @@ -232,9 +177,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: 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 }} + + + + + From 4a72778e06b31b06ff88cdc096f2ba87e3e650fc Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 11 Feb 2017 18:26:54 +0900 Subject: [PATCH 09/11] epub: Refactor around epub_writing_mode --- sphinx/builders/epub3.py | 59 ++++++++++++++++------------------------ tests/test_build_epub.py | 35 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index 4cea39956..646ed61ac 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -15,7 +15,7 @@ from datetime import datetime from collections import namedtuple from sphinx import package_dir -from sphinx.config import string_classes +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 @@ -31,9 +31,21 @@ logger = logging.getLogger(__name__) NavPoint = namedtuple('NavPoint', ['text', 'refuri', 'children']) -DOCTYPE = u'''''' +# 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', +} -# The epub3 publisher +DOCTYPE = u'''''' class Epub3Builder(EpubBuilder): @@ -66,47 +78,23 @@ class Epub3Builder(EpubBuilder): """Create a dictionary with all metadata for the content.opf file properly escaped. """ + 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() + + 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]) -> List[NavPoint] @@ -193,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/tests/test_build_epub.py b/tests/test_build_epub.py index 08844300c..0c9a4a60c 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -210,3 +210,38 @@ def test_nested_toc(app): 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 From 03f4a07aa19faea077485633cfb2afb2c64a3f19 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 13 Feb 2017 12:31:52 +0900 Subject: [PATCH 10/11] epub: Fix ID-cache is shared over the builds --- sphinx/builders/epub.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index ef6d40ae7..5e248ce3b 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -148,6 +148,7 @@ class EpubBuilder(StandaloneHTMLBuilder): self.link_suffix = '.xhtml' self.playorder = 0 self.tocid = 0 + self.id_cache = {} # type: Dict[unicode, unicode] self.use_index = self.get_builder_config('use_index', 'epub') def get_theme_config(self): @@ -155,14 +156,14 @@ class EpubBuilder(StandaloneHTMLBuilder): return self.config.epub_theme, self.config.epub_theme_options # generic support functions - def make_id(self, name, id_cache={}): - # type: (unicode, Dict[unicode, unicode]) -> 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): From 0b7c73a98133236883f1c80afbd6acf530928e70 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 13 Feb 2017 13:23:52 +0900 Subject: [PATCH 11/11] epub: Sort manifest entries by filename --- sphinx/builders/epub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index 5e248ce3b..3f97a1ae5 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -516,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