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')