# -*- 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 class Toctree(EnvironmentManager): name = 'toctree' def __init__(self, env): 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): 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): 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): """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): """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): """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): """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): """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