diff --git a/CHANGES b/CHANGES
index c8e0bb941..9c6ca126b 100644
--- a/CHANGES
+++ b/CHANGES
@@ -14,6 +14,7 @@ Bugs fixed
* #3093: gettext build broken on substituted images.
* #3093: gettext build broken on image node under ``note`` directive.
* imgmath: crashes on showing error messages if image generation failed
+* #3117: LaTeX writer crashes if admonition is placed before first section title
Release 1.4.8 (released Oct 1, 2016)
====================================
diff --git a/sphinx/environment/managers/toctree.py b/sphinx/environment/managers/toctree.py
new file mode 100644
index 000000000..26c8f385d
--- /dev/null
+++ b/sphinx/environment/managers/toctree.py
@@ -0,0 +1,580 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.environment.managers.toctree
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Toctree manager for sphinx.environment.
+
+ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from six import iteritems
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.util import url_re
+from sphinx.util.nodes import clean_astext, process_only_nodes
+from sphinx.transforms import SphinxContentsFilter
+from sphinx.environment.managers import EnvironmentManager
+
+if False:
+ # For type annotation
+ from typing import Any, Tuple # NOQA
+ from sphinx.builders import Builder # NOQA
+ from sphinx.environment import BuildEnvironment # NOQA
+
+
+class Toctree(EnvironmentManager):
+ name = 'toctree'
+
+ def __init__(self, env):
+ # type: (BuildEnvironment) -> None
+ super(Toctree, self).__init__(env)
+
+ self.tocs = env.tocs
+ self.toc_num_entries = env.toc_num_entries
+ self.toc_secnumbers = env.toc_secnumbers
+ self.toc_fignumbers = env.toc_fignumbers
+ self.toctree_includes = env.toctree_includes
+ self.files_to_rebuild = env.files_to_rebuild
+ self.glob_toctrees = env.glob_toctrees
+ self.numbered_toctrees = env.numbered_toctrees
+
+ def clear_doc(self, docname):
+ # type: (unicode) -> None
+ self.tocs.pop(docname, None)
+ self.toc_secnumbers.pop(docname, None)
+ self.toc_fignumbers.pop(docname, None)
+ self.toc_num_entries.pop(docname, None)
+ self.toctree_includes.pop(docname, None)
+ self.glob_toctrees.discard(docname)
+ self.numbered_toctrees.discard(docname)
+
+ for subfn, fnset in list(self.files_to_rebuild.items()):
+ fnset.discard(docname)
+ if not fnset:
+ del self.files_to_rebuild[subfn]
+
+ def merge_other(self, docnames, other):
+ # type: (List[unicode], BuildEnvironment) -> None
+ for docname in docnames:
+ self.tocs[docname] = other.tocs[docname]
+ self.toc_num_entries[docname] = other.toc_num_entries[docname]
+ if docname in other.toctree_includes:
+ self.toctree_includes[docname] = other.toctree_includes[docname]
+ if docname in other.glob_toctrees:
+ self.glob_toctrees.add(docname)
+ if docname in other.numbered_toctrees:
+ self.numbered_toctrees.add(docname)
+
+ for subfn, fnset in other.files_to_rebuild.items():
+ self.files_to_rebuild.setdefault(subfn, set()).update(fnset & docnames)
+
+ def process_doc(self, docname, doctree):
+ # type: (unicode, nodes.Node) -> None
+ """Build a TOC from the doctree and store it in the inventory."""
+ numentries = [0] # nonlocal again...
+
+ def traverse_in_section(node, cls):
+ """Like traverse(), but stay within the same section."""
+ result = []
+ if isinstance(node, cls):
+ result.append(node)
+ for child in node.children:
+ if isinstance(child, nodes.section):
+ continue
+ result.extend(traverse_in_section(child, cls))
+ return result
+
+ def build_toc(node, depth=1):
+ entries = []
+ for sectionnode in node:
+ # find all toctree nodes in this section and add them
+ # to the toc (just copying the toctree node which is then
+ # resolved in self.get_and_resolve_doctree)
+ if isinstance(sectionnode, addnodes.only):
+ onlynode = addnodes.only(expr=sectionnode['expr'])
+ blist = build_toc(sectionnode, depth)
+ if blist:
+ onlynode += blist.children
+ entries.append(onlynode)
+ continue
+ if not isinstance(sectionnode, nodes.section):
+ for toctreenode in traverse_in_section(sectionnode,
+ addnodes.toctree):
+ item = toctreenode.copy()
+ entries.append(item)
+ # important: do the inventory stuff
+ self.note_toctree(docname, toctreenode)
+ continue
+ title = sectionnode[0]
+ # copy the contents of the section title, but without references
+ # and unnecessary stuff
+ visitor = SphinxContentsFilter(doctree)
+ title.walkabout(visitor)
+ nodetext = visitor.get_entry_text()
+ if not numentries[0]:
+ # for the very first toc entry, don't add an anchor
+ # as it is the file's title anyway
+ anchorname = ''
+ else:
+ anchorname = '#' + sectionnode['ids'][0]
+ numentries[0] += 1
+ # make these nodes:
+ # list_item -> compact_paragraph -> reference
+ reference = nodes.reference(
+ '', '', internal=True, refuri=docname,
+ anchorname=anchorname, *nodetext)
+ para = addnodes.compact_paragraph('', '', reference)
+ item = nodes.list_item('', para)
+ sub_item = build_toc(sectionnode, depth + 1)
+ item += sub_item
+ entries.append(item)
+ if entries:
+ return nodes.bullet_list('', *entries)
+ return []
+ toc = build_toc(doctree)
+ if toc:
+ self.tocs[docname] = toc
+ else:
+ self.tocs[docname] = nodes.bullet_list('')
+ self.toc_num_entries[docname] = numentries[0]
+
+ def note_toctree(self, docname, toctreenode):
+ # type: (unicode, addnodes.toctree) -> None
+ """Note a TOC tree directive in a document and gather information about
+ file relations from it.
+ """
+ if toctreenode['glob']:
+ self.glob_toctrees.add(docname)
+ if toctreenode.get('numbered'):
+ self.numbered_toctrees.add(docname)
+ includefiles = toctreenode['includefiles']
+ for includefile in includefiles:
+ # note that if the included file is rebuilt, this one must be
+ # too (since the TOC of the included file could have changed)
+ self.files_to_rebuild.setdefault(includefile, set()).add(docname)
+ self.toctree_includes.setdefault(docname, []).extend(includefiles)
+
+ def get_toc_for(self, docname, builder):
+ # type: (unicode, Builder) -> None
+ """Return a TOC nodetree -- for use on the same page only!"""
+ tocdepth = self.env.metadata[docname].get('tocdepth', 0)
+ try:
+ toc = self.tocs[docname].deepcopy()
+ self._toctree_prune(toc, 2, tocdepth)
+ except KeyError:
+ # the document does not exist anymore: return a dummy node that
+ # renders to nothing
+ return nodes.paragraph()
+ process_only_nodes(toc, builder.tags, warn_node=self.env.warn_node)
+ for node in toc.traverse(nodes.reference):
+ node['refuri'] = node['anchorname'] or '#'
+ return toc
+
+ def get_toctree_for(self, docname, builder, collapse, **kwds):
+ # type: (unicode, Builder, bool, Any) -> nodes.Node
+ """Return the global TOC nodetree."""
+ doctree = self.env.get_doctree(self.env.config.master_doc)
+ toctrees = []
+ if 'includehidden' not in kwds:
+ kwds['includehidden'] = True
+ if 'maxdepth' not in kwds:
+ kwds['maxdepth'] = 0
+ kwds['collapse'] = collapse
+ for toctreenode in doctree.traverse(addnodes.toctree):
+ toctree = self.env.resolve_toctree(docname, builder, toctreenode,
+ prune=True, **kwds)
+ if toctree:
+ toctrees.append(toctree)
+ if not toctrees:
+ return None
+ result = toctrees[0]
+ for toctree in toctrees[1:]:
+ result.extend(toctree.children)
+ return result
+
+ def resolve_toctree(self, docname, builder, toctree, prune=True, maxdepth=0,
+ titles_only=False, collapse=False, includehidden=False):
+ # type: (unicode, Builder, addnodes.toctree, bool, int, bool, bool, bool) -> nodes.Node
+ """Resolve a *toctree* node into individual bullet lists with titles
+ as items, returning None (if no containing titles are found) or
+ a new node.
+
+ If *prune* is True, the tree is pruned to *maxdepth*, or if that is 0,
+ to the value of the *maxdepth* option on the *toctree* node.
+ If *titles_only* is True, only toplevel document titles will be in the
+ resulting tree.
+ If *collapse* is True, all branches not containing docname will
+ be collapsed.
+ """
+ if toctree.get('hidden', False) and not includehidden:
+ return None
+
+ # For reading the following two helper function, it is useful to keep
+ # in mind the node structure of a toctree (using HTML-like node names
+ # for brevity):
+ #
+ #
+ # -
+ #
+ #
+ # ...
+ #
+ #
+ #
+ #
+ # The transformation is made in two passes in order to avoid
+ # interactions between marking and pruning the tree (see bug #1046).
+
+ toctree_ancestors = self.get_toctree_ancestors(docname)
+
+ def _toctree_add_classes(node, depth):
+ """Add 'toctree-l%d' and 'current' classes to the toctree."""
+ for subnode in node.children:
+ if isinstance(subnode, (addnodes.compact_paragraph,
+ nodes.list_item)):
+ # for and
, indicate the depth level and recurse
+ subnode['classes'].append('toctree-l%d' % (depth-1))
+ _toctree_add_classes(subnode, depth)
+ elif isinstance(subnode, nodes.bullet_list):
+ # for , just recurse
+ _toctree_add_classes(subnode, depth+1)
+ elif isinstance(subnode, nodes.reference):
+ # for , identify which entries point to the current
+ # document and therefore may not be collapsed
+ if subnode['refuri'] == docname:
+ if not subnode['anchorname']:
+ # give the whole branch a 'current' class
+ # (useful for styling it differently)
+ branchnode = subnode
+ while branchnode:
+ branchnode['classes'].append('current')
+ branchnode = branchnode.parent
+ # mark the list_item as "on current page"
+ if subnode.parent.parent.get('iscurrent'):
+ # but only if it's not already done
+ return
+ while subnode:
+ subnode['iscurrent'] = True
+ subnode = subnode.parent
+
+ def _entries_from_toctree(toctreenode, parents,
+ separate=False, subtree=False):
+ """Return TOC entries for a toctree node."""
+ refs = [(e[0], e[1]) for e in toctreenode['entries']]
+ entries = []
+ for (title, ref) in refs:
+ try:
+ refdoc = None
+ if url_re.match(ref):
+ if title is None:
+ title = ref
+ reference = nodes.reference('', '', internal=False,
+ refuri=ref, anchorname='',
+ *[nodes.Text(title)])
+ para = addnodes.compact_paragraph('', '', reference)
+ item = nodes.list_item('', para)
+ toc = nodes.bullet_list('', item)
+ elif ref == 'self':
+ # 'self' refers to the document from which this
+ # toctree originates
+ ref = toctreenode['parent']
+ if not title:
+ title = clean_astext(self.titles[ref])
+ reference = nodes.reference('', '', internal=True,
+ refuri=ref,
+ anchorname='',
+ *[nodes.Text(title)])
+ para = addnodes.compact_paragraph('', '', reference)
+ item = nodes.list_item('', para)
+ # don't show subitems
+ toc = nodes.bullet_list('', item)
+ else:
+ if ref in parents:
+ self.env.warn(ref, 'circular toctree references '
+ 'detected, ignoring: %s <- %s' %
+ (ref, ' <- '.join(parents)))
+ continue
+ refdoc = ref
+ toc = self.tocs[ref].deepcopy()
+ maxdepth = self.env.metadata[ref].get('tocdepth', 0)
+ if ref not in toctree_ancestors or (prune and maxdepth > 0):
+ self._toctree_prune(toc, 2, maxdepth, collapse)
+ process_only_nodes(toc, builder.tags, warn_node=self.env.warn_node)
+ if title and toc.children and len(toc.children) == 1:
+ child = toc.children[0]
+ for refnode in child.traverse(nodes.reference):
+ if refnode['refuri'] == ref and \
+ not refnode['anchorname']:
+ refnode.children = [nodes.Text(title)]
+ if not toc.children:
+ # empty toc means: no titles will show up in the toctree
+ self.env.warn_node(
+ 'toctree contains reference to document %r that '
+ 'doesn\'t have a title: no link will be generated'
+ % ref, toctreenode)
+ except KeyError:
+ # this is raised if the included file does not exist
+ self.env.warn_node(
+ 'toctree contains reference to nonexisting document %r'
+ % ref, toctreenode)
+ else:
+ # if titles_only is given, only keep the main title and
+ # sub-toctrees
+ if titles_only:
+ # delete everything but the toplevel title(s)
+ # and toctrees
+ for toplevel in toc:
+ # nodes with length 1 don't have any children anyway
+ if len(toplevel) > 1:
+ subtrees = toplevel.traverse(addnodes.toctree)
+ if subtrees:
+ toplevel[1][:] = subtrees
+ else:
+ toplevel.pop(1)
+ # resolve all sub-toctrees
+ for subtocnode in toc.traverse(addnodes.toctree):
+ if not (subtocnode.get('hidden', False) and
+ not includehidden):
+ i = subtocnode.parent.index(subtocnode) + 1
+ for item in _entries_from_toctree(
+ subtocnode, [refdoc] + parents,
+ subtree=True):
+ subtocnode.parent.insert(i, item)
+ i += 1
+ subtocnode.parent.remove(subtocnode)
+ if separate:
+ entries.append(toc)
+ else:
+ entries.extend(toc.children)
+ if not subtree and not separate:
+ ret = nodes.bullet_list()
+ ret += entries
+ return [ret]
+ return entries
+
+ maxdepth = maxdepth or toctree.get('maxdepth', -1)
+ if not titles_only and toctree.get('titlesonly', False):
+ titles_only = True
+ if not includehidden and toctree.get('includehidden', False):
+ includehidden = True
+
+ # NOTE: previously, this was separate=True, but that leads to artificial
+ # separation when two or more toctree entries form a logical unit, so
+ # separating mode is no longer used -- it's kept here for history's sake
+ tocentries = _entries_from_toctree(toctree, [], separate=False)
+ if not tocentries:
+ return None
+
+ newnode = addnodes.compact_paragraph('', '')
+ caption = toctree.attributes.get('caption')
+ if caption:
+ caption_node = nodes.caption(caption, '', *[nodes.Text(caption)])
+ caption_node.line = toctree.line
+ caption_node.source = toctree.source
+ caption_node.rawsource = toctree['rawcaption']
+ if hasattr(toctree, 'uid'):
+ # move uid to caption_node to translate it
+ caption_node.uid = toctree.uid
+ del toctree.uid
+ newnode += caption_node
+ newnode.extend(tocentries)
+ newnode['toctree'] = True
+
+ # prune the tree to maxdepth, also set toc depth and current classes
+ _toctree_add_classes(newnode, 1)
+ self._toctree_prune(newnode, 1, prune and maxdepth or 0, collapse)
+
+ if len(newnode[-1]) == 0: # No titles found
+ return None
+
+ # set the target paths in the toctrees (they are not known at TOC
+ # generation time)
+ for refnode in newnode.traverse(nodes.reference):
+ if not url_re.match(refnode['refuri']):
+ refnode['refuri'] = builder.get_relative_uri(
+ docname, refnode['refuri']) + refnode['anchorname']
+ return newnode
+
+ def get_toctree_ancestors(self, docname):
+ # type: (unicode) -> List[unicode]
+ parent = {}
+ for p, children in iteritems(self.toctree_includes):
+ for child in children:
+ parent[child] = p
+ ancestors = [] # type: List[unicode]
+ d = docname
+ while d in parent and d not in ancestors:
+ ancestors.append(d)
+ d = parent[d]
+ return ancestors
+
+ def _toctree_prune(self, node, depth, maxdepth, collapse=False):
+ # type: (nodes.Node, int, int, bool) -> None
+ """Utility: Cut a TOC at a specified depth."""
+ for subnode in node.children[:]:
+ if isinstance(subnode, (addnodes.compact_paragraph,
+ nodes.list_item)):
+ # for and
- , just recurse
+ self._toctree_prune(subnode, depth, maxdepth, collapse)
+ elif isinstance(subnode, nodes.bullet_list):
+ # for
, determine if the depth is too large or if the
+ # entry is to be collapsed
+ if maxdepth > 0 and depth > maxdepth:
+ subnode.parent.replace(subnode, [])
+ else:
+ # cull sub-entries whose parents aren't 'current'
+ if (collapse and depth > 1 and
+ 'iscurrent' not in subnode.parent):
+ subnode.parent.remove(subnode)
+ else:
+ # recurse on visible children
+ self._toctree_prune(subnode, depth+1, maxdepth, collapse)
+
+ def assign_section_numbers(self):
+ # type: () -> List[unicode]
+ """Assign a section number to each heading under a numbered toctree."""
+ # a list of all docnames whose section numbers changed
+ rewrite_needed = []
+
+ assigned = set() # type: Set[unicode]
+ old_secnumbers = self.toc_secnumbers
+ self.toc_secnumbers = self.env.toc_secnumbers = {}
+
+ def _walk_toc(node, secnums, depth, titlenode=None):
+ # titlenode is the title of the document, it will get assigned a
+ # secnumber too, so that it shows up in next/prev/parent rellinks
+ for subnode in node.children:
+ if isinstance(subnode, nodes.bullet_list):
+ numstack.append(0)
+ _walk_toc(subnode, secnums, depth-1, titlenode)
+ numstack.pop()
+ titlenode = None
+ elif isinstance(subnode, nodes.list_item):
+ _walk_toc(subnode, secnums, depth, titlenode)
+ titlenode = None
+ elif isinstance(subnode, addnodes.only):
+ # at this stage we don't know yet which sections are going
+ # to be included; just include all of them, even if it leads
+ # to gaps in the numbering
+ _walk_toc(subnode, secnums, depth, titlenode)
+ titlenode = None
+ elif isinstance(subnode, addnodes.compact_paragraph):
+ numstack[-1] += 1
+ if depth > 0:
+ number = tuple(numstack)
+ else:
+ number = None
+ secnums[subnode[0]['anchorname']] = \
+ subnode[0]['secnumber'] = number
+ if titlenode:
+ titlenode['secnumber'] = number
+ titlenode = None
+ elif isinstance(subnode, addnodes.toctree):
+ _walk_toctree(subnode, depth)
+
+ def _walk_toctree(toctreenode, depth):
+ if depth == 0:
+ return
+ for (title, ref) in toctreenode['entries']:
+ if url_re.match(ref) or ref == 'self' or ref in assigned:
+ # don't mess with those
+ continue
+ if ref in self.tocs:
+ secnums = self.toc_secnumbers[ref] = {}
+ assigned.add(ref)
+ _walk_toc(self.tocs[ref], secnums, depth,
+ self.env.titles.get(ref))
+ if secnums != old_secnumbers.get(ref):
+ rewrite_needed.append(ref)
+
+ for docname in self.numbered_toctrees:
+ assigned.add(docname)
+ doctree = self.env.get_doctree(docname)
+ for toctreenode in doctree.traverse(addnodes.toctree):
+ depth = toctreenode.get('numbered', 0)
+ if depth:
+ # every numbered toctree gets new numbering
+ numstack = [0]
+ _walk_toctree(toctreenode, depth)
+
+ return rewrite_needed
+
+ def assign_figure_numbers(self):
+ # type: () -> List[unicode]
+ """Assign a figure number to each figure under a numbered toctree."""
+
+ rewrite_needed = []
+
+ assigned = set() # type: Set[unicode]
+ old_fignumbers = self.toc_fignumbers
+ self.toc_fignumbers = self.env.toc_fignumbers = {}
+ fignum_counter = {} # type: Dict[unicode, Dict[Tuple[int], int]]
+
+ def get_section_number(docname, section):
+ anchorname = '#' + section['ids'][0]
+ secnumbers = self.toc_secnumbers.get(docname, {})
+ if anchorname in secnumbers:
+ secnum = secnumbers.get(anchorname)
+ else:
+ secnum = secnumbers.get('')
+
+ return secnum or tuple()
+
+ def get_next_fignumber(figtype, secnum):
+ counter = fignum_counter.setdefault(figtype, {})
+
+ secnum = secnum[:self.env.config.numfig_secnum_depth]
+ counter[secnum] = counter.get(secnum, 0) + 1
+ return secnum + (counter[secnum],)
+
+ def register_fignumber(docname, secnum, figtype, fignode):
+ self.toc_fignumbers.setdefault(docname, {})
+ fignumbers = self.toc_fignumbers[docname].setdefault(figtype, {})
+ figure_id = fignode['ids'][0]
+
+ fignumbers[figure_id] = get_next_fignumber(figtype, secnum)
+
+ def _walk_doctree(docname, doctree, secnum):
+ for subnode in doctree.children:
+ if isinstance(subnode, nodes.section):
+ next_secnum = get_section_number(docname, subnode)
+ if next_secnum:
+ _walk_doctree(docname, subnode, next_secnum)
+ else:
+ _walk_doctree(docname, subnode, secnum)
+ continue
+ elif isinstance(subnode, addnodes.toctree):
+ for title, subdocname in subnode['entries']:
+ if url_re.match(subdocname) or subdocname == 'self':
+ # don't mess with those
+ continue
+
+ _walk_doc(subdocname, secnum)
+
+ continue
+
+ figtype = self.env.get_domain('std').get_figtype(subnode) # type: ignore
+ if figtype and subnode['ids']:
+ register_fignumber(docname, secnum, figtype, subnode)
+
+ _walk_doctree(docname, subnode, secnum)
+
+ def _walk_doc(docname, secnum):
+ if docname not in assigned:
+ assigned.add(docname)
+ doctree = self.env.get_doctree(docname)
+ _walk_doctree(docname, doctree, secnum)
+
+ if self.env.config.numfig:
+ _walk_doc(self.env.config.master_doc, tuple())
+ for docname, fignums in iteritems(self.toc_fignumbers):
+ if fignums != old_fignumbers.get(docname):
+ rewrite_needed.append(docname)
+
+ return rewrite_needed
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index 08afb1b86..1f83cd43f 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -765,36 +765,36 @@ class LaTeXTranslator(nodes.NodeVisitor):
if isinstance(parent, addnodes.seealso):
# the environment already handles this
raise nodes.SkipNode
- elif self.this_is_the_title:
- if len(node.children) != 1 and not isinstance(node.children[0],
- nodes.Text):
- self.builder.warn('document title is not a single Text node',
- (self.curfilestack[-1], node.line))
- if not self.elements['title']:
- # text needs to be escaped since it is inserted into
- # the output literally
- self.elements['title'] = node.astext().translate(tex_escape_map)
- self.this_is_the_title = 0
- raise nodes.SkipNode
elif isinstance(parent, nodes.section):
- short = ''
- if node.traverse(nodes.image):
- short = ('[%s]' %
- u' '.join(clean_astext(node).split()).translate(tex_escape_map))
+ if self.this_is_the_title:
+ if len(node.children) != 1 and not isinstance(node.children[0],
+ nodes.Text):
+ self.builder.warn('document title is not a single Text node',
+ (self.curfilestack[-1], node.line))
+ if not self.elements['title']:
+ # text needs to be escaped since it is inserted into
+ # the output literally
+ self.elements['title'] = node.astext().translate(tex_escape_map)
+ self.this_is_the_title = 0
+ raise nodes.SkipNode
+ else:
+ short = ''
+ if node.traverse(nodes.image):
+ short = ('[%s]' %
+ u' '.join(clean_astext(node).split()).translate(tex_escape_map))
- try:
- self.body.append(r'\%s%s{' % (self.sectionnames[self.sectionlevel], short))
- except IndexError:
- # just use "subparagraph", it's not numbered anyway
- self.body.append(r'\%s%s{' % (self.sectionnames[-1], short))
- self.context.append('}\n')
-
- self.restrict_footnote(node)
- if self.next_section_ids:
- for id in self.next_section_ids:
- self.context[-1] += self.hypertarget(id, anchor=False)
- self.next_section_ids.clear()
+ try:
+ self.body.append(r'\%s%s{' % (self.sectionnames[self.sectionlevel], short))
+ except IndexError:
+ # just use "subparagraph", it's not numbered anyway
+ self.body.append(r'\%s%s{' % (self.sectionnames[-1], short))
+ self.context.append('}\n')
+ self.restrict_footnote(node)
+ if self.next_section_ids:
+ for id in self.next_section_ids:
+ self.context[-1] += self.hypertarget(id, anchor=False)
+ self.next_section_ids.clear()
elif isinstance(parent, (nodes.topic, nodes.sidebar)):
self.body.append(r'\textbf{')
self.context.append('}\n\n\medskip\n\n')
diff --git a/tests/roots/test-latex-title/conf.py b/tests/roots/test-latex-title/conf.py
new file mode 100644
index 000000000..709f1be48
--- /dev/null
+++ b/tests/roots/test-latex-title/conf.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+master_doc = 'index'
+
+# set empty string to the third column to use the first section title to document title
+latex_documents = [
+ (master_doc, 'test.tex', '', 'Sphinx', 'report')
+]
diff --git a/tests/roots/test-latex-title/index.rst b/tests/roots/test-latex-title/index.rst
new file mode 100644
index 000000000..411ad0009
--- /dev/null
+++ b/tests/roots/test-latex-title/index.rst
@@ -0,0 +1,12 @@
+.. admonition:: Notice
+
+ This generates nodes.title node before first section title.
+
+test-latex-title
+================
+
+.. toctree::
+ :numbered:
+
+ foo
+ bar
diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py
index 77ba4ca85..9ffca6b4e 100644
--- a/tests/test_build_latex.py
+++ b/tests/test_build_latex.py
@@ -181,6 +181,26 @@ def test_latex_warnings(app, status, warning):
'--- Got:\n' + warnings
+@with_app(buildername='latex', testroot='basic')
+def test_latex_title(app, status, warning):
+ app.builder.build_all()
+ result = (app.outdir / 'test.tex').text(encoding='utf8')
+ print(result)
+ print(status.getvalue())
+ print(warning.getvalue())
+ assert '\\title{The basic Sphinx documentation for testing}' in result
+
+
+@with_app(buildername='latex', testroot='latex-title')
+def test_latex_title_after_admonitions(app, status, warning):
+ app.builder.build_all()
+ result = (app.outdir / 'test.tex').text(encoding='utf8')
+ print(result)
+ print(status.getvalue())
+ print(warning.getvalue())
+ assert '\\title{test-latex-title}' in result
+
+
@with_app(buildername='latex', testroot='numfig',
confoverrides={'numfig': True})
def test_numref(app, status, warning):