Skip to content

Commit be95c84

Browse files
peytondmurrayivanov
authored andcommitted
Feature: Add support for per-page secondary sidebar content (pydata#1572)
* Add ability to set per-page secondary sidebars * Add additional secondary sidebar content tests; expanded utils docstring
1 parent dfc34cf commit be95c84

File tree

11 files changed

+356
-33
lines changed

11 files changed

+356
-33
lines changed

docs/conf.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,10 @@
180180
# "content_footer_items": ["test", "test"],
181181
"footer_start": ["copyright"],
182182
"footer_center": ["sphinx-version"],
183-
# "secondary_sidebar_items": ["page-toc"], # Remove the source buttons
183+
"secondary_sidebar_items": {
184+
"**/*": ["page-toc", "edit-this-page", "sourcelink"],
185+
"examples/no-sidebar": [],
186+
},
184187
"switcher": {
185188
"json_url": json_url,
186189
"version_match": version_match,

docs/examples/no-sidebar.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ This page shows off what the documentation looks like when you explicitly tell S
66
html_sidebars = {
77
"path/to/page": [],
88
}
9+
html_theme_options = {
10+
"secondary_sidebar_items": {
11+
"path/to/page": [],
12+
},
13+
}
914
```
1015

11-
The primary sidebar should be entirely gone, and the main content should expand slightly to make up the extra space.
16+
Both the primary and secondary sidebars should be entirely gone, and the main content should expand slightly to make up the extra space.

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

+9-27
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,37 +202,19 @@ 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]))
236-
#
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+
)
217+
237218
# Remove a duplicate entry of the theme CSS. This is because it is in both:
238219
# - theme.conf
239220
# - manually linked in `webpack-macros.html`
@@ -296,6 +277,7 @@ def setup(app: Sphinx) -> Dict[str, str]:
296277
app.connect("html-page-context", toctree.add_toctree_functions)
297278
app.connect("html-page-context", update_and_remove_templates)
298279
app.connect("html-page-context", logo.setup_logo_path)
280+
app.connect("html-page-context", utils.set_secondary_sidebar_items)
299281
app.connect("build-finished", pygment.overwrite_pygments_css)
300282
app.connect("build-finished", logo.copy_logo_images)
301283

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

+113-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,112 @@ 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. Valid
96+
section names include any of the ``sphinx`` or ``html_theme_options`` that take templates
97+
or lists of templates as arguments, for example: ``theme_navbar_start``,
98+
``theme_primary_sidebar_end``, ``theme_secondary_sidebar_items``, ``sidebars``, etc. For
99+
a complete list of valid section names, see the source for
100+
:py:func:`pydata_sphinx_theme.update_and_remove_templates` and
101+
:py:func:`pydata_sphinx_theme.utils.set_secondary_sidebar_items`, both of which call
102+
this function.
103+
templates_skip_empty_check: Names of any templates which should never be removed from the list
104+
of filtered templates returned by this function. These templates aren't checked if they
105+
render empty, which can save time if the template is slow to render.
106+
107+
Returns:
108+
A list of template names (including '.html' suffix) to render into the section
109+
"""
110+
if templates_skip_empty_check is None:
111+
templates_skip_empty_check = []
112+
113+
# Break apart `,` separated strings so we can use , in the defaults
114+
if isinstance(templates, str):
115+
templates = [template.strip() for template in templates.split(",")]
116+
117+
# Add `.html` to templates with no suffix
118+
suffixed_templates = []
119+
for template in templates:
120+
if os.path.splitext(template)[1]:
121+
suffixed_templates.append(template)
122+
else:
123+
suffixed_templates.append(f"{template}.html")
124+
125+
ctx = copy.copy(context)
126+
ctx.update({section: suffixed_templates})
127+
128+
# Check whether the template renders to an empty string; remove if this is the case
129+
# Skip templates that are slow to render with templates_skip_empty_check
130+
filtered_templates = []
131+
for template in suffixed_templates:
132+
if any(template.endswith(item) for item in templates_skip_empty_check):
133+
filtered_templates.append(template)
134+
else:
135+
rendered = app.builder.templates.render(template, ctx)
136+
if len(rendered.strip()) != 0:
137+
filtered_templates.append(template)
138+
139+
return filtered_templates
140+
141+
142+
def _get_matching_sidebar_items(
143+
pagename: str, sidebars: Dict[str, List[str]]
144+
) -> List[str]:
145+
"""Get the matching sidebar templates to render for the given pagename.
146+
147+
If a page matches more than one pattern, a warning is emitted, and the templates for the
148+
last matching pattern are used.
149+
150+
This function was adapted from sphinx.builders.html.StandaloneHTMLBuilder.add_sidebars.
151+
"""
152+
matched = None
153+
secondary_sidebar_items = []
154+
for pattern, sidebar_items in sidebars.items():
155+
if matching.patmatch(pagename, pattern):
156+
if matched and _has_wildcard(pattern) and _has_wildcard(matched):
157+
SPHINX_LOGGER.warning(
158+
f"Page {pagename} matches two wildcard patterns in secondary_sidebar_items: {matched} and {pattern}"
159+
),
160+
161+
matched = pattern
162+
secondary_sidebar_items = sidebar_items
163+
return secondary_sidebar_items
164+
165+
166+
def _has_wildcard(pattern: str) -> bool:
167+
"""Check whether the pattern contains a wildcard.
168+
169+
Taken from sphinx.builders.StandaloneHTMLBuilder.add_sidebars.
170+
"""
171+
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

0 commit comments

Comments
 (0)