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