@@ -31,8 +31,8 @@ def add_inline_math(node: Node) -> str:
31
31
)
32
32
33
33
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 ."""
36
36
toctree = TocTree (app .env )
37
37
if sphinx .version_info [:2 ] >= (7 , 2 ):
38
38
from sphinx .environment .adapters .toctree import _get_toctree_ancestors
@@ -41,49 +41,47 @@ def _get_ancestor_section(app: Sphinx, pagename: str, startdepth: int) -> str:
41
41
else :
42
42
ancestors = toctree .get_toctree_ancestors (pagename )
43
43
try :
44
- return ancestors [- startdepth ] # will be a pagename (string)?
44
+ out = ancestors [- startdepth ]
45
45
except IndexError :
46
46
# eg for index.rst, but also special pages such as genindex, py-modindex, search
47
47
# 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
49
49
# 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
69
52
70
53
71
54
def add_toctree_functions (
72
55
app : Sphinx , pagename : str , templatename : str , context , doctree
73
56
) -> None :
74
57
"""Add functions so Jinja templates can add toctree objects."""
75
58
76
- def missing_sidebar_toctree (startdepth : int = 1 , ** kwargs ):
59
+ def suppress_sidebar_toctree (startdepth : int = 1 , ** kwargs ):
77
60
"""Check if there's a sidebar TocTree that needs to be rendered.
78
61
79
62
Parameters:
80
63
startdepth : The level of the TocTree at which to start. 0 includes the
81
64
entire TocTree for the site; 1 (default) gets the TocTree for the current
82
65
top-level section.
83
66
84
- kwargs: passed to the Sphinx `toctree` template function.
67
+ kwargs : passed to the Sphinx `toctree` template function.
85
68
"""
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
+ )
87
85
return toctree is None
88
86
89
87
@cache
@@ -118,6 +116,9 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
118
116
if sphinx .version_info [:2 ] >= (7 , 2 ):
119
117
from sphinx .environment .adapters .toctree import _get_toctree_ancestors
120
118
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`).
121
122
active_header_page = [
122
123
* _get_toctree_ancestors (app .env .toctree_includes , pagename )
123
124
]
@@ -127,14 +128,18 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
127
128
# The final list item will be the top-most ancestor
128
129
active_header_page = active_header_page [- 1 ]
129
130
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 ]
132
136
133
- # Iterate through each toctree node in the root document
134
- # Grab the toctree pages and find the relative link + title.
135
137
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
138
143
for title , page in toc .attributes ["entries" ]:
139
144
# if the page is using "self" use the correct link
140
145
page = toc .attributes ["parent" ] if page == "self" else page
@@ -262,17 +267,27 @@ def generate_toctree_html(
262
267
kind : "sidebar" or "raw". Whether to generate HTML meant for sidebar navigation ("sidebar") or to return the raw BeautifulSoup object ("raw").
263
268
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`).
264
269
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.
266
271
267
272
Returns:
268
273
HTML string (if kind == "sidebar") OR BeautifulSoup object (if kind == "raw")
269
274
"""
270
275
if startdepth == 0 :
271
276
html_toctree = context ["toctree" ](** kwargs )
272
277
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
+ )
273
288
# 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
276
291
)
277
292
html_toctree = app .builder .render_partial (toctree_element )["fragment" ]
278
293
@@ -394,7 +409,7 @@ def navbar_align_class() -> List[str]:
394
409
395
410
context ["unique_html_id" ] = unique_html_id
396
411
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
398
413
context ["generate_toctree_html" ] = generate_toctree_html
399
414
context ["generate_toc_html" ] = generate_toc_html
400
415
context ["navbar_align_class" ] = navbar_align_class
@@ -459,36 +474,53 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
459
474
element .insert (1 , checkbox )
460
475
461
476
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)`).
474
496
"""
475
- partial_doctree = toctree .env .tocs [indexname ].deepcopy ()
476
-
477
- toctrees = []
497
+ kwargs .setdefault ("collapse" , True )
478
498
if "maxdepth" not in kwargs or not kwargs ["maxdepth" ]:
479
499
kwargs ["maxdepth" ] = 0
480
500
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 )
489
520
if not toctrees :
490
521
return None
522
+ # ... and merge them into a single entity
491
523
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 )
494
526
return result
0 commit comments