Skip to content

Commit 7034724

Browse files
committed
Add ability to set per-page secondary sidebars
1 parent 194f6a0 commit 7034724

File tree

9 files changed

+201
-5
lines changed

9 files changed

+201
-5
lines changed

docs/user_guide/page-toc.rst

+29
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,32 @@ Remove the Table of Contents
2424

2525
To remove the Table of Contents, add ``:html_theme.sidebar_secondary.remove:`` to the `file-wide metadata <https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html#file-wide-metadata>`_ at the top of a page.
2626
This will remove the Table of Contents from that page only.
27+
28+
Per-page secondary-sidebar content
29+
----------------------------------
30+
31+
``html_theme_options['secondary_sidebar_items']`` accepts either a ``list`` of secondary sidebar
32+
templates to render on every page:
33+
34+
.. code-block:: python
35+
36+
html_theme_options = {
37+
"secondary_sidebar_items": ["page-toc", "sourcelink"]
38+
}
39+
40+
or a ``dict`` which maps page names to ``list`` of secondary sidebar templates:
41+
42+
.. code-block:: python
43+
44+
html_theme_options = {
45+
"secondary_sidebar_items": {
46+
"**": ["page-toc", "sourcelink"],
47+
"index": ["page-toc"],
48+
}
49+
}
50+
51+
If a ``dict`` is specified, the keys can contain glob-style patterns; page names which
52+
match the pattern will contain the sidebar templates specified. This closely follows the behavior of
53+
the ``html_sidebars`` option that is part of Sphinx itself, except that it operates on the
54+
secondary sidebar instead of the primary sidebar. For more information, see `the Sphinx
55+
documentation <https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_sidebars>`__.

src/pydata_sphinx_theme/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ def update_and_remove_templates(
203203
"theme_footer_start",
204204
"theme_footer_center",
205205
"theme_footer_end",
206-
"theme_secondary_sidebar_items",
207206
"theme_primary_sidebar_end",
208207
"sidebars",
209208
]
@@ -291,6 +290,7 @@ def setup(app: Sphinx) -> Dict[str, str]:
291290
app.connect("html-page-context", toctree.add_toctree_functions)
292291
app.connect("html-page-context", update_and_remove_templates)
293292
app.connect("html-page-context", logo.setup_logo_path)
293+
app.connect("html-page-context", utils.set_secondary_sidebar_items)
294294
app.connect("build-finished", pygment.overwrite_pygments_css)
295295
app.connect("build-finished", logo.copy_logo_images)
296296

src/pydata_sphinx_theme/theme/pydata_sphinx_theme/sections/sidebar-secondary.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{% if theme_secondary_sidebar_items -%}
1+
{% if secondary_sidebar_items -%}
22
<div class="sidebar-secondary-items sidebar-secondary__inner">
3-
{% for toc_item in theme_secondary_sidebar_items %}
3+
{% for toc_item in secondary_sidebar_items %}
44
<div class="sidebar-secondary-item">{% include toc_item %}</div>
55
{% endfor %}
66
</div>

src/pydata_sphinx_theme/utils.py

+79-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""General helpers for the management of config parameters."""
22

3+
import copy
4+
import os
35
import re
4-
from typing import Any, Dict, Iterator
6+
from typing import Any, Dict, Iterator, List, Union
57

68
from docutils.nodes import Node
79
from sphinx.application import Sphinx
8-
from sphinx.util import logging
10+
from sphinx.util import logging, matching
911

1012

1113
def get_theme_options_dict(app: Sphinx) -> Dict[str, Any]:
@@ -58,3 +60,78 @@ def maybe_warn(app: Sphinx, msg, *args, **kwargs):
5860
should_warn = theme_options.get("surface_warnings", False)
5961
if should_warn:
6062
SPHINX_LOGGER.warning(msg, *args, **kwargs)
63+
64+
65+
def set_secondary_sidebar_items(
66+
app: Sphinx, pagename: str, templatename: str, context, doctree
67+
) -> None:
68+
"""Set the secondary sidebar items to render for the given pagename."""
69+
if "theme_secondary_sidebar_items" in context:
70+
templates = context["theme_secondary_sidebar_items"]
71+
if isinstance(templates, dict):
72+
templates = _get_matching_sidebar_items(pagename, templates)
73+
74+
context["secondary_sidebar_items"] = _update_and_remove_templates(
75+
app,
76+
context,
77+
templates,
78+
"theme_secondary_sidebar_items",
79+
)
80+
81+
82+
def _update_and_remove_templates(
83+
app: Sphinx, context, templates: Union[List, str], template_key: str
84+
) -> List[str]:
85+
# Break apart `,` separated strings so we can use , in the defaults
86+
if isinstance(templates, str):
87+
templates = [ii.strip() for ii in templates.split(",")]
88+
# Add `.html` to templates with no suffix
89+
suffixed_templates = []
90+
for template in templates:
91+
if os.path.splitext(template)[1]:
92+
suffixed_templates.append(template)
93+
else:
94+
suffixed_templates.append(f"{template}.html")
95+
96+
ctx = copy.copy(context)
97+
ctx.update({template_key: suffixed_templates})
98+
99+
# Check whether the template renders to an empty string; remove if this is the case
100+
filtered_templates = []
101+
for template in suffixed_templates:
102+
rendered = app.builder.templates.render(template, ctx)
103+
if len(rendered.strip()) != 0:
104+
filtered_templates.append(template)
105+
106+
return filtered_templates
107+
108+
109+
def _get_matching_sidebar_items(
110+
pagename: str, sidebars: Dict[str, List[str]]
111+
) -> List[str]:
112+
"""Get the matching sidebar templates to render for the given pagename.
113+
114+
This function was adapted from sphinx.builders.html.StandaloneHTMLBuilder.add_sidebars.
115+
116+
Cached to avoid overhead when html context event is fired multiple times on the same page.
117+
"""
118+
matched = None
119+
secondary_sidebar_items = []
120+
for pattern, sidebar_items in sidebars.items():
121+
if matching.patmatch(pagename, pattern):
122+
if matched and _has_wildcard(pattern) and _has_wildcard(matched):
123+
SPHINX_LOGGER.warning(
124+
f"Page {pagename} matches two wildcard patterns in secondary_sidebar_items: {matched} and {pattern}"
125+
),
126+
127+
matched = pattern
128+
secondary_sidebar_items = sidebar_items
129+
return secondary_sidebar_items
130+
131+
132+
def _has_wildcard(pattern: str) -> bool:
133+
"""Check whether the pattern contains a wildcard.
134+
135+
Taken from sphinx.builders.StandaloneHTMLBuilder.add_sidebars.
136+
"""
137+
return any(char in pattern for char in "*?[")

tests/sites/sidebars/index.rst

+5
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ Sidebar depth variations
1111
:caption: Caption 2
1212

1313
section2/index
14+
15+
Other content
16+
-------------
17+
18+
This is some other content.

tests/sites/sidebars/section1/index.rst

+5
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ Section 1 index
66

77
subsection1/index
88
page2
9+
10+
Other Content
11+
-------------
12+
13+
This is some other content
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
Section 1 sub 1 page 2
22
======================
3+
4+
5+
Section A
6+
---------

tests/sites/sidebars/section2/index.rst

+6
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ Section 2 index
55

66
page1
77
https://google.com
8+
9+
10+
Other Content
11+
-------------
12+
13+
This is some other content

tests/test_build.py

+70
Original file line numberDiff line numberDiff line change
@@ -986,3 +986,73 @@ def test_translations(sphinx_build_factory) -> None:
986986
# Search bar
987987
# TODO: Add translations where there are english phrases below
988988
assert "Search the docs" in str(index.select(".bd-search")[0])
989+
990+
991+
def test_render_secondary_sidebar_list(sphinx_build_factory) -> None:
992+
"""Test that the secondary sidebar can be built with a list of templates."""
993+
confoverrides = {
994+
"html_context": {
995+
"github_user": "pydata",
996+
"github_repo": "pydata-sphinx-theme",
997+
"github_version": "main",
998+
},
999+
"html_theme_options": {
1000+
"use_edit_page_button": True,
1001+
"secondary_sidebar_items": ["page-toc", "edit-this-page"],
1002+
},
1003+
}
1004+
sphinx_build = sphinx_build_factory("sidebars", confoverrides=confoverrides)
1005+
# Basic build with defaults
1006+
sphinx_build.build()
1007+
1008+
# Check that the page-toc template gets rendered
1009+
assert sphinx_build.html_tree("index.html").select("div.page-toc")
1010+
assert sphinx_build.html_tree("section1/index.html").select("div.page-toc")
1011+
assert sphinx_build.html_tree("section2/index.html").select("div.page-toc")
1012+
1013+
# Check that the edit-this-page template gets rendered
1014+
assert sphinx_build.html_tree("index.html").select("div.editthispage")
1015+
assert sphinx_build.html_tree("section1/index.html").select("div.editthispage")
1016+
assert sphinx_build.html_tree("section2/index.html").select("div.editthispage")
1017+
1018+
# Check that sourcelink is not rendered
1019+
assert not sphinx_build.html_tree("index.html").select("div.sourcelink")
1020+
assert not sphinx_build.html_tree("section1/index.html").select("div.sourcelink")
1021+
assert not sphinx_build.html_tree("section2/index.html").select("div.sourcelink")
1022+
1023+
1024+
def test_render_secondary_sidebar_dict(sphinx_build_factory) -> None:
1025+
"""Test that the secondary sidebar can be built with a dict of templates."""
1026+
confoverrides = {
1027+
"html_context": {
1028+
"github_user": "pydata",
1029+
"github_repo": "pydata-sphinx-theme",
1030+
"github_version": "main",
1031+
},
1032+
"html_theme_options": {
1033+
"use_edit_page_button": True,
1034+
"secondary_sidebar_items": {
1035+
"**": ["page-toc", "edit-this-page"],
1036+
"section1/index": [],
1037+
"section2/index": ["sourcelink"],
1038+
},
1039+
},
1040+
}
1041+
sphinx_build = sphinx_build_factory("sidebars", confoverrides=confoverrides)
1042+
# Basic build with defaults
1043+
sphinx_build.build()
1044+
1045+
# Check that the page-toc template gets rendered
1046+
assert sphinx_build.html_tree("index.html").select("div.page-toc")
1047+
assert not sphinx_build.html_tree("section1/index.html").select("div.page-toc")
1048+
assert not sphinx_build.html_tree("section2/index.html").select("div.page-toc")
1049+
1050+
# Check that the edit-this-page template gets rendered
1051+
assert sphinx_build.html_tree("index.html").select("div.editthispage")
1052+
assert not sphinx_build.html_tree("section1/index.html").select("div.editthispage")
1053+
assert not sphinx_build.html_tree("section2/index.html").select("div.editthispage")
1054+
1055+
# Check that sourcelink is not rendered
1056+
assert not sphinx_build.html_tree("index.html").select("div.sourcelink")
1057+
assert not sphinx_build.html_tree("section1/index.html").select("div.sourcelink")
1058+
assert sphinx_build.html_tree("section2/index.html").select("div.sourcelink")

0 commit comments

Comments
 (0)