Skip to content

Commit 4267454

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

File tree

9 files changed

+233
-30
lines changed

9 files changed

+233
-30
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

+8-26
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Bootstrap-based sphinx theme from the PyData community."""
22

33
import json
4-
import os
54
from functools import partial
65
from pathlib import Path
76
from typing import Dict
@@ -203,36 +202,18 @@ def update_and_remove_templates(
203202
"theme_footer_start",
204203
"theme_footer_center",
205204
"theme_footer_end",
206-
"theme_secondary_sidebar_items",
207205
"theme_primary_sidebar_end",
208206
"sidebars",
209207
]
210208
for section in template_sections:
211209
if context.get(section):
212-
# Break apart `,` separated strings so we can use , in the defaults
213-
if isinstance(context.get(section), str):
214-
context[section] = [
215-
ii.strip() for ii in context.get(section).split(",")
216-
]
217-
218-
# Add `.html` to templates with no suffix
219-
for ii, template in enumerate(context.get(section)):
220-
if not os.path.splitext(template)[1]:
221-
context[section][ii] = template + ".html"
222-
223-
# If this is the page TOC, check if it is empty and remove it if so
224-
def _remove_empty_templates(tname):
225-
# These templates take too long to render, so skip them.
226-
# They should never be empty anyway.
227-
SKIP_EMPTY_TEMPLATE_CHECKS = ["sidebar-nav-bs.html", "navbar-nav.html"]
228-
if not any(tname.endswith(temp) for temp in SKIP_EMPTY_TEMPLATE_CHECKS):
229-
# Render the template and see if it is totally empty
230-
rendered = app.builder.templates.render(tname, context)
231-
if len(rendered.strip()) == 0:
232-
return False
233-
return True
234-
235-
context[section] = list(filter(_remove_empty_templates, context[section]))
210+
context[section] = utils._update_and_remove_templates(
211+
app=app,
212+
context=context,
213+
templates=context.get(section, []),
214+
section=section,
215+
templates_skip_empty_check=["sidebar-nav-bs.html", "navbar-nav.html"],
216+
)
236217

237218
# Remove a duplicate entry of the theme CSS. This is because it is in both:
238219
# - theme.conf
@@ -291,6 +272,7 @@ def setup(app: Sphinx) -> Dict[str, str]:
291272
app.connect("html-page-context", toctree.add_toctree_functions)
292273
app.connect("html-page-context", update_and_remove_templates)
293274
app.connect("html-page-context", logo.setup_logo_path)
275+
app.connect("html-page-context", utils.set_secondary_sidebar_items)
294276
app.connect("build-finished", pygment.overwrite_pygments_css)
295277
app.connect("build-finished", logo.copy_logo_images)
296278

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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+
{# Note: secondary_sidebar_items is set by set_secondary_sidebar_items() in utils.py #}
4+
{% for toc_item in secondary_sidebar_items %}
45
<div class="sidebar-secondary-item">{% include toc_item %}</div>
56
{% endfor %}
67
</div>

src/pydata_sphinx_theme/utils.py

+103-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, Optional, 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,102 @@ 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,
84+
context: Dict[str, Any],
85+
templates: Union[List, str],
86+
section: str,
87+
templates_skip_empty_check: Optional[List[str]] = None,
88+
) -> List[str]:
89+
"""Update templates to include html suffix if needed; remove templates which render empty.
90+
91+
Args:
92+
app: Sphinx application passed to the html page context
93+
context: The html page context; dictionary of values passed to the templating engine
94+
templates: A list of template names, or a string of comma separated template names
95+
section: Name of the template section where the templates are to be rendered
96+
templates_skip_empty_check: Names of any templates which should never be removed from the list
97+
of filtered templates returned by this function. These templates aren't checked if they
98+
render empty, which can save time if the template is slow to render.
99+
100+
Returns:
101+
A list of template names (including '.html' suffix) to render into the section
102+
"""
103+
if templates_skip_empty_check is None:
104+
templates_skip_empty_check = []
105+
106+
# Break apart `,` separated strings so we can use , in the defaults
107+
if isinstance(templates, str):
108+
templates = [template.strip() for template in templates.split(",")]
109+
110+
# Add `.html` to templates with no suffix
111+
suffixed_templates = []
112+
for template in templates:
113+
if os.path.splitext(template)[1]:
114+
suffixed_templates.append(template)
115+
else:
116+
suffixed_templates.append(f"{template}.html")
117+
118+
ctx = copy.copy(context)
119+
ctx.update({section: suffixed_templates})
120+
121+
# Check whether the template renders to an empty string; remove if this is the case
122+
# Skip templates that are slow to render with templates_skip_empty_check
123+
filtered_templates = []
124+
for template in suffixed_templates:
125+
if any(template.endswith(item) for item in templates_skip_empty_check):
126+
filtered_templates.append(template)
127+
else:
128+
rendered = app.builder.templates.render(template, ctx)
129+
if len(rendered.strip()) != 0:
130+
filtered_templates.append(template)
131+
132+
return filtered_templates
133+
134+
135+
def _get_matching_sidebar_items(
136+
pagename: str, sidebars: Dict[str, List[str]]
137+
) -> List[str]:
138+
"""Get the matching sidebar templates to render for the given pagename.
139+
140+
This function was adapted from sphinx.builders.html.StandaloneHTMLBuilder.add_sidebars.
141+
"""
142+
matched = None
143+
secondary_sidebar_items = []
144+
for pattern, sidebar_items in sidebars.items():
145+
if matching.patmatch(pagename, pattern):
146+
if matched and _has_wildcard(pattern) and _has_wildcard(matched):
147+
SPHINX_LOGGER.warning(
148+
f"Page {pagename} matches two wildcard patterns in secondary_sidebar_items: {matched} and {pattern}"
149+
),
150+
151+
matched = pattern
152+
secondary_sidebar_items = sidebar_items
153+
return secondary_sidebar_items
154+
155+
156+
def _has_wildcard(pattern: str) -> bool:
157+
"""Check whether the pattern contains a wildcard.
158+
159+
Taken from sphinx.builders.StandaloneHTMLBuilder.add_sidebars.
160+
"""
161+
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)