@@ -31,8 +31,8 @@ def add_inline_math(node: Node) -> str:
3131 )
3232
3333
34- def _get_ancestor_section (app : Sphinx , pagename : str , startdepth : int ) -> str :
35- """Get the TocTree node ` startdepth` levels below the root that dominates `pagename` ."""
34+ def _get_ancestor_pagename (app : Sphinx , pagename : str , startdepth : int ) -> str :
35+ """Get the name of `pagename`'s ancestor that is rooted ` startdepth` levels below the global root ."""
3636 toctree = TocTree (app .env )
3737 if sphinx .version_info [:2 ] >= (7 , 2 ):
3838 from sphinx .environment .adapters .toctree import _get_toctree_ancestors
@@ -41,49 +41,47 @@ def _get_ancestor_section(app: Sphinx, pagename: str, startdepth: int) -> str:
4141 else :
4242 ancestors = toctree .get_toctree_ancestors (pagename )
4343 try :
44- return ancestors [- startdepth ] # will be a pagename (string)?
44+ out = ancestors [- startdepth ]
4545 except IndexError :
4646 # eg for index.rst, but also special pages such as genindex, py-modindex, search
4747 # those pages don't have a "current" element in the toctree, so we can
48- # directly return an empty string instead of using the default sphinx
48+ # directly return None instead of using the default sphinx
4949 # toctree.get_toctree_for(pagename, app.builder, collapse, **kwargs)
50- return None
51-
52-
53- def get_unrendered_local_toctree (app : Sphinx , pagename : str , startdepth : int , ** kwargs ):
54- """Get the "local" (starting at `startdepth`) TocTree containing `pagename`.
55-
56- This is similar to `context["toctree"](**kwargs)` in sphinx templating,
57- but using the startdepth-local instead of global TOC tree.
58- """
59- kwargs .setdefault ("collapse" , True )
60- if kwargs .get ("maxdepth" ) == "" :
61- kwargs .pop ("maxdepth" )
62- toctree = TocTree (app .env )
63- indexname = _get_ancestor_section (app = app , pagename = pagename , startdepth = startdepth )
64- if indexname is None :
65- return None
66- return get_local_toctree_for_doc (
67- toctree , indexname , pagename , app .builder , ** kwargs
68- )
50+ out = None
51+ return out , toctree
6952
7053
7154def add_toctree_functions (
7255 app : Sphinx , pagename : str , templatename : str , context , doctree
7356) -> None :
7457 """Add functions so Jinja templates can add toctree objects."""
7558
76- def missing_sidebar_toctree (startdepth : int = 1 , ** kwargs ):
59+ def suppress_sidebar_toctree (startdepth : int = 1 , ** kwargs ):
7760 """Check if there's a sidebar TocTree that needs to be rendered.
7861
7962 Parameters:
8063 startdepth : The level of the TocTree at which to start. 0 includes the
8164 entire TocTree for the site; 1 (default) gets the TocTree for the current
8265 top-level section.
8366
84- kwargs: passed to the Sphinx `toctree` template function.
67+ kwargs : passed to the Sphinx `toctree` template function.
8568 """
86- toctree = get_unrendered_local_toctree (app , pagename , startdepth , ** kwargs )
69+ ancestorname , toctree_obj = _get_ancestor_pagename (
70+ app = app , pagename = pagename , startdepth = startdepth
71+ )
72+ if ancestorname is None :
73+ return True # suppress
74+ if kwargs .get ("includehidden" , False ):
75+ # if ancestor is found and `includehidden=True` we're guaranteed there's a
76+ # TocTree to be shown, so don't suppress
77+ return False
78+
79+ # we've found an ancestor page, but `includehidden=False` so we can't be sure if
80+ # there's a TocTree fragment that should be shown on this page; unfortunately we
81+ # must resolve the whole TOC subtree to find out
82+ toctree = get_nonroot_toctree (
83+ app , pagename , ancestorname , toctree_obj , ** kwargs
84+ )
8785 return toctree is None
8886
8987 @cache
@@ -118,6 +116,9 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
118116 if sphinx .version_info [:2 ] >= (7 , 2 ):
119117 from sphinx .environment .adapters .toctree import _get_toctree_ancestors
120118
119+ # NOTE: `env.toctree_includes` is a dict mapping pagenames to any (possibly
120+ # hidden) TocTree directives on that page (i.e., the "child" pages nested
121+ # under `pagename`).
121122 active_header_page = [
122123 * _get_toctree_ancestors (app .env .toctree_includes , pagename )
123124 ]
@@ -127,14 +128,18 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
127128 # The final list item will be the top-most ancestor
128129 active_header_page = active_header_page [- 1 ]
129130
130- # Find the root document because it lists our top-level toctree pages
131- root = app .env .tocs [app .config .root_doc ]
131+ # NOTE: `env.tocs` is a dict mapping pagenames to hierarchical bullet-lists
132+ # ("nodetrees" in Sphinx parlance) of in-page headings (including `toctree::`
133+ # directives). Thus the `tocs` of `root_doc` yields the top-level pages that sit
134+ # just below the root of our site
135+ root_toc = app .env .tocs [app .config .root_doc ]
132136
133- # Iterate through each toctree node in the root document
134- # Grab the toctree pages and find the relative link + title.
135137 links_html = []
136- # TODO: use `root.findall(TocTreeNodeClass)` once docutils min version >=0.18.1
137- for toc in traverse_or_findall (root , TocTreeNodeClass ):
138+ # Iterate through each node in the root document toc.
139+ # Grab the toctree pages and find the relative link + title.
140+ for toc in traverse_or_findall (root_toc , TocTreeNodeClass ):
141+ # TODO: ↑↑↑ use `root_toc.findall(TocTreeNodeClass)` ↑↑↑
142+ # once docutils min version >=0.18.1
138143 for title , page in toc .attributes ["entries" ]:
139144 # if the page is using "self" use the correct link
140145 page = toc .attributes ["parent" ] if page == "self" else page
@@ -262,17 +267,27 @@ def generate_toctree_html(
262267 kind : "sidebar" or "raw". Whether to generate HTML meant for sidebar navigation ("sidebar") or to return the raw BeautifulSoup object ("raw").
263268 startdepth : The level of the toctree at which to start. By default, for the navbar uses the normal toctree (`startdepth=0`), and for the sidebar starts from the second level (`startdepth=1`).
264269 show_nav_level : The level of the navigation bar to toggle as visible on page load. By default, this level is 1, and only top-level pages are shown, with drop-boxes to reveal children. Increasing `show_nav_level` will show child levels as well.
265- kwargs: passed to the Sphinx `toctree` template function.
270+ kwargs : passed to the Sphinx `toctree` template function.
266271
267272 Returns:
268273 HTML string (if kind == "sidebar") OR BeautifulSoup object (if kind == "raw")
269274 """
270275 if startdepth == 0 :
271276 html_toctree = context ["toctree" ](** kwargs )
272277 else :
278+ # find relevant ancestor page; some pages (search, genindex) won't have one
279+ ancestorname , toctree_obj = _get_ancestor_pagename (
280+ app = app , pagename = pagename , startdepth = startdepth
281+ )
282+ if ancestorname is None :
283+ raise RuntimeError (
284+ "Template requested to generate a TocTree fragment but no suitable "
285+ "ancestor found to act as root node. Please report this to theme "
286+ "developers."
287+ )
273288 # select the "active" subset of the navigation tree for the sidebar
274- toctree_element = get_unrendered_local_toctree (
275- app , pagename , startdepth , ** kwargs
289+ toctree_element = get_nonroot_toctree (
290+ app , pagename , ancestorname , toctree_obj , ** kwargs
276291 )
277292 html_toctree = app .builder .render_partial (toctree_element )["fragment" ]
278293
@@ -394,7 +409,7 @@ def navbar_align_class() -> List[str]:
394409
395410 context ["unique_html_id" ] = unique_html_id
396411 context ["generate_header_nav_html" ] = generate_header_nav_html
397- context ["missing_sidebar_toctree " ] = missing_sidebar_toctree
412+ context ["suppress_sidebar_toctree " ] = suppress_sidebar_toctree
398413 context ["generate_toctree_html" ] = generate_toctree_html
399414 context ["generate_toc_html" ] = generate_toc_html
400415 context ["navbar_align_class" ] = navbar_align_class
@@ -459,36 +474,53 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
459474 element .insert (1 , checkbox )
460475
461476
462- def get_local_toctree_for_doc (
463- toctree : TocTree , indexname : str , pagename : str , builder , collapse : bool , ** kwargs
464- ) -> List [BeautifulSoup ]:
465- """Get the "local" TocTree containing `pagename` rooted at `indexname`.
466-
467- The Sphinx equivalent is TocTree.get_toctree_for(), which always uses the "root"
468- or "global" TocTree:
469-
470- doctree = self.env.get_doctree(self.env.config.root_doc)
471-
472- Whereas here we return a subset of the global toctree, rooted at `indexname`
473- (e.g. starting at a second level for the sidebar).
477+ def get_nonroot_toctree (
478+ app : Sphinx , pagename : str , ancestorname : str , toctree , ** kwargs
479+ ):
480+ """Get the partial TocTree (rooted at `ancestorname`) that dominates `pagename`.
481+
482+ Parameters:
483+ app : Sphinx app.
484+ pagename : Name of the current page (as Sphinx knows it; i.e., its relative path
485+ from the documentation root).
486+ ancestorname : Name of a page that dominates `pagename` and that will serve as the
487+ root of the TocTree fragment.
488+ toctree : A Sphinx TocTree object. Since this is always needed when finding the
489+ ancestorname (see _get_ancestor_pagename), it's more efficient to pass it here to
490+ re-use it.
491+ kwargs : passed to the Sphinx `toctree` template function.
492+
493+ This is similar to `context["toctree"](**kwargs)` (AKA `toctree(**kwargs)` within a
494+ Jinja template), or `TocTree.get_toctree_for()`, which always uses the "root"
495+ doctree (i.e., `doctree = self.env.get_doctree(self.env.config.root_doc)`).
474496 """
475- partial_doctree = toctree .env .tocs [indexname ].deepcopy ()
476-
477- toctrees = []
497+ kwargs .setdefault ("collapse" , True )
478498 if "maxdepth" not in kwargs or not kwargs ["maxdepth" ]:
479499 kwargs ["maxdepth" ] = 0
480500 kwargs ["maxdepth" ] = int (kwargs ["maxdepth" ])
481- kwargs ["collapse" ] = collapse
482-
483- # TODO: use `doctree.findall(TocTreeNodeClass)` once docutils min version >=0.18.1
484- for _node in traverse_or_findall (partial_doctree , TocTreeNodeClass ):
485- # defaults for resolve: prune=True, maxdepth=0, titles_only=False, collapse=False, includehidden=False
486- _toctree = toctree .resolve (pagename , builder , _node , ** kwargs )
487- if _toctree :
488- toctrees .append (_toctree )
501+ # starting from ancestor page, recursively parse `toctree::` elements
502+ ancestor_doctree = toctree .env .tocs [ancestorname ].deepcopy ()
503+ toctrees = []
504+
505+ # for each `toctree::` directive in the ancestor page...
506+ for toctree_node in traverse_or_findall (ancestor_doctree , TocTreeNodeClass ):
507+ # TODO: ↑↑↑↑↑↑ use `ancestor_doctree.findall(TocTreeNodeClass)` ↑↑↑↑↑↑
508+ # once docutils min version >=0.18.1
509+
510+ # ... resolve that `toctree::` (recursively get children, prune, collapse, etc)
511+ resolved_toctree = toctree .resolve (
512+ docname = pagename ,
513+ builder = app .builder ,
514+ toctree = toctree_node ,
515+ ** kwargs ,
516+ )
517+ # ... keep the non-empty ones
518+ if resolved_toctree :
519+ toctrees .append (resolved_toctree )
489520 if not toctrees :
490521 return None
522+ # ... and merge them into a single entity
491523 result = toctrees [0 ]
492- for toctree in toctrees [1 :]:
493- result .extend (toctree .children )
524+ for resolved_toctree in toctrees [1 :]:
525+ result .extend (resolved_toctree .children )
494526 return result
0 commit comments