Skip to content

Commit 7f741f6

Browse files
committed
adding captions to left sidebar
1 parent 8a76bc8 commit 7f741f6

File tree

4 files changed

+116
-27
lines changed

4 files changed

+116
-27
lines changed

pydata_sphinx_theme/__init__.py

+103-24
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,51 @@
77

88
import sphinx.builders.html
99
from sphinx.errors import ExtensionError
10+
from sphinx.environment.adapters.toctree import TocTree
11+
from sphinx import addnodes
12+
from docutils import nodes
13+
from pathlib import Path
1014

1115
from .bootstrap_html_translator import BootstrapHTML5Translator
1216
import docutils
1317

1418
__version__ = "0.1.1"
1519

1620

21+
class MyTocTree(TocTree):
22+
def get_toctree_for_subpage(
23+
self, pagename, builder, collapse=True, maxdepth=-1, **kwargs
24+
):
25+
"""Return the global TOC nodetree."""
26+
if pagename in ["genindex", "search"]:
27+
return
28+
doctree = self.env.get_doctree(pagename)
29+
toctrees = [] # type: List[Element]
30+
if "includehidden" not in kwargs:
31+
kwargs["includehidden"] = True
32+
if "maxdepth" not in kwargs:
33+
kwargs["maxdepth"] = 0
34+
kwargs["collapse"] = collapse
35+
for toctreenode in doctree.traverse(addnodes.toctree):
36+
toctree = self.resolve(pagename, builder, toctreenode, prune=True, **kwargs)
37+
if toctree:
38+
toctrees.append(toctree)
39+
if not toctrees:
40+
return None
41+
result = toctrees[0]
42+
for toctree in toctrees[1:]:
43+
result.extend(toctree.children)
44+
return result
45+
46+
1747
def add_toctree_functions(app, pagename, templatename, context, doctree):
1848
"""Add functions so Jinja templates can add toctree objects.
1949
2050
This converts the docutils nodes into a nested dictionary that Jinja can
2151
use in our templating.
2252
"""
23-
from sphinx.environment.adapters.toctree import TocTree
2453

25-
def get_nav_object(maxdepth=None, collapse=True, **kwargs):
54+
def get_nav_object(maxdepth=None, collapse=True, subpage_caption=False, **kwargs):
2655
"""Return a list of nav links that can be accessed from Jinja.
2756
2857
Parameters
@@ -38,26 +67,59 @@ def get_nav_object(maxdepth=None, collapse=True, **kwargs):
3867
# The TocTree will contain the full site TocTree including sub-pages.
3968
# "collapse=True" collapses sub-pages of non-active TOC pages.
4069
# maxdepth controls how many TOC levels are returned
41-
toctree = TocTree(app.env).get_toctree_for(
70+
toc = MyTocTree(app.env)
71+
toctree = toc.get_toctree_for(
4272
pagename, app.builder, collapse=collapse, maxdepth=maxdepth, **kwargs
4373
)
74+
4475
# If no toctree is defined (AKA a single-page site), skip this
4576
if toctree is None:
4677
return []
4778

79+
if subpage_caption:
80+
if pagename not in [app.env.config.master_doc, "genindex", "search"]:
81+
def is_first_active_page(node):
82+
return isinstance(node, nodes.bullet_list) and node.attributes.get("iscurrent")
83+
84+
active_first_page = list(toctree.traverse(is_first_active_page))[0]
85+
# A path to the active TOC item's first page, relative to the current page
86+
first_page_path = list(active_first_page.traverse(nodes.reference))[0].attributes.get("refuri")
87+
if first_page_path == "":
88+
# First TOC item's first page *is* the active page
89+
first_page_path = Path(pagename).name
90+
else:
91+
first_page_path = Path(first_page_path).with_suffix("")
92+
rel_first_page_path = str(Path(pagename).parent.joinpath(first_page_path))
93+
94+
# We only wish to show a single page's descendants, so we'll keep their captions
95+
subpage_toctree = toc.get_toctree_for_subpage(
96+
rel_first_page_path, app.builder, collapse=collapse, maxdepth=maxdepth, **kwargs
97+
)
98+
if subpage_toctree is not None:
99+
# Find the current page in the top-level children
100+
for item in toctree.children:
101+
if isinstance(item, nodes.bullet_list) and item.attributes.get("iscurrent", []):
102+
# Append that pages' toctree so we get captions
103+
subpage_list = item.children[0]
104+
subpage_list.children = [subpage_list.children[0]] + subpage_toctree.children
105+
48106
# toctree has this structure
49107
# <caption>
50108
# <bullet_list>
51109
# <list_item classes="toctree-l1">
52110
# <list_item classes="toctree-l1">
53111
# `list_item`s are the actual TOC links and are the only thing we want
54-
toc_items = [item for child in toctree.children for item in child
55-
if isinstance(item, docutils.nodes.list_item)]
112+
toc_items = []
113+
for child in toctree.children:
114+
if isinstance(child, docutils.nodes.caption):
115+
toc_items.append(child)
116+
elif isinstance(child, docutils.nodes.bullet_list):
117+
for list_entry in child:
118+
if isinstance(list_entry, docutils.nodes.list_item):
119+
toc_items.append(list_entry)
56120

57121
# Now convert our docutils nodes into dicts that Jinja can use
58-
nav = [docutils_node_to_jinja(child, only_pages=True)
59-
for child in toc_items]
60-
122+
nav = [docutils_node_to_jinja(child, only_pages=True) for child in toc_items]
61123
return nav
62124

63125
def get_page_toc_object():
@@ -73,6 +135,7 @@ def get_page_toc_object():
73135
context["get_nav_object"] = get_nav_object
74136
context["get_page_toc_object"] = get_page_toc_object
75137

138+
76139
def docutils_node_to_jinja(list_item, only_pages=False):
77140
"""Convert a docutils node to a structure that can be read by Jinja.
78141
@@ -91,6 +154,12 @@ def docutils_node_to_jinja(list_item, only_pages=False):
91154
The TocTree, converted into a dictionary with key/values that work
92155
within Jinja.
93156
"""
157+
# If a caption, pass it through
158+
if isinstance(list_item, docutils.nodes.caption):
159+
nav = {"text": list_item.astext(), "type": "caption"}
160+
return nav
161+
162+
# Else, we assume it's a list item and need to parse the item content
94163
if not list_item.children:
95164
return None
96165

@@ -100,48 +169,58 @@ def docutils_node_to_jinja(list_item, only_pages=False):
100169
# <reference> <-- the thing we want
101170
reference = list_item.children[0].children[0]
102171
title = reference.astext()
103-
url = reference.attributes["refuri"]
172+
173+
url = reference.attributes.get("refuri", "")
104174
active = "current" in list_item.attributes["classes"]
105175

106176
# If we've got an anchor link, skip it if we wish
107-
if only_pages and '#' in url:
177+
if only_pages and "#" in url:
108178
return None
109179

110180
# Converting the docutils attributes into jinja-friendly objects
111181
nav = {}
112182
nav["title"] = title
113183
nav["url"] = url
114184
nav["active"] = active
185+
nav["type"] = "ref"
115186

116187
# Recursively convert children as well
117188
# If there are sub-pages for this list_item, there should be two children:
118189
# a paragraph, and a bullet_list.
119190
nav["children"] = []
120191
if len(list_item.children) > 1:
121-
# The `.children` of the bullet_list has the nodes of the sub-pages.
122-
subpage_list = list_item.children[1].children
123-
for sub_page in subpage_list:
124-
child_nav = docutils_node_to_jinja(sub_page, only_pages=only_pages)
125-
if child_nav is not None:
126-
nav["children"].append(child_nav)
192+
child_pages = list_item.children[1:]
193+
for child in child_pages:
194+
# Will either be a caption or another bullet list
195+
if isinstance(child, nodes.bullet_list):
196+
for subpage in child.children:
197+
this_nav = docutils_node_to_jinja(subpage, only_pages=only_pages)
198+
nav["children"].append(this_nav)
199+
else:
200+
this_nav = docutils_node_to_jinja(child, only_pages=only_pages)
201+
nav["children"].append(this_nav)
127202
return nav
128203

129204

130205
# -----------------------------------------------------------------------------
131206

207+
132208
def setup_edit_url(app, pagename, templatename, context, doctree):
133209
"""Add a function that jinja can access for returning the edit URL of a page."""
210+
134211
def get_edit_url():
135212
"""Return a URL for an "edit this page" link."""
136213
required_values = ["github_user", "github_repo", "github_version"]
137214
for val in required_values:
138215
if not context.get(val):
139-
raise ExtensionError("Missing required value for `edit this page` button. "
140-
"Add %s to your `html_context` configuration" % val)
141-
142-
github_user = context['github_user']
143-
github_repo = context['github_repo']
144-
github_version = context['github_version']
216+
raise ExtensionError(
217+
"Missing required value for `edit this page` button. "
218+
"Add %s to your `html_context` configuration" % val
219+
)
220+
221+
github_user = context["github_user"]
222+
github_repo = context["github_repo"]
223+
github_version = context["github_version"]
145224
file_name = f"{pagename}{context['page_source_suffix']}"
146225

147226
# Make sure that doc_path has a path separator only if it exists (to avoid //)
@@ -153,7 +232,7 @@ def get_edit_url():
153232
url_edit = f"https://github.com/{github_user}/{github_repo}/edit/{github_version}/{doc_path}{file_name}"
154233
return url_edit
155234

156-
context['get_edit_url'] = get_edit_url
235+
context["get_edit_url"] = get_edit_url
157236

158237

159238
# -----------------------------------------------------------------------------
@@ -174,6 +253,6 @@ def setup(app):
174253
# uses a special "dirhtml" builder so we need to replace these both with
175254
# our custom HTML builder
176255
app.set_translator("readthedocs", BootstrapHTML5Translator, override=True)
177-
app.set_translator('readthedocsdirhtml', BootstrapHTML5Translator, override=True)
256+
app.set_translator("readthedocsdirhtml", BootstrapHTML5Translator, override=True)
178257
app.connect("html-page-context", setup_edit_url)
179258
app.connect("html-page-context", add_toctree_functions)

pydata_sphinx_theme/docs-sidebar.html

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
<nav class="bd-links" id="bd-docs-nav" aria-label="Main navigation">
88

99
<div class="bd-toc-item active">
10-
{% set nav = get_nav_object(maxdepth=3, collapse=True) %}
10+
{% set nav = get_nav_object(maxdepth=3, collapse=True, subpage_caption=True) %}
1111

1212
<ul class="nav bd-sidenav">
13+
<!-- top-level pages -->
1314
{% for main_nav_item in nav %}
1415
{% if main_nav_item.active %}
16+
<!-- sub-pages within each top level page -->
1517
{% for nav_item in main_nav_item.children %}
16-
{% if nav_item.children %}
18+
{% if nav_item["type"] == "caption" %}
19+
<li class="nav-caption">{{ nav_item.text }}</li>
20+
{% elif nav_item.children %}
1721

1822
<li class="{% if nav_item.active%}active{% endif %}">
1923
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>

0 commit comments

Comments
 (0)