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