Skip to content

Commit

Permalink
Add ability to set per-page secondary sidebars
Browse files Browse the repository at this point in the history
  • Loading branch information
peytondmurray committed Nov 25, 2023
1 parent 194f6a0 commit 7034724
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 5 deletions.
29 changes: 29 additions & 0 deletions docs/user_guide/page-toc.rst
Expand Up @@ -24,3 +24,32 @@ Remove the Table of Contents

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.
This will remove the Table of Contents from that page only.

Per-page secondary-sidebar content
----------------------------------

``html_theme_options['secondary_sidebar_items']`` accepts either a ``list`` of secondary sidebar
templates to render on every page:

.. code-block:: python
html_theme_options = {
"secondary_sidebar_items": ["page-toc", "sourcelink"]
}
or a ``dict`` which maps page names to ``list`` of secondary sidebar templates:

.. code-block:: python
html_theme_options = {
"secondary_sidebar_items": {
"**": ["page-toc", "sourcelink"],
"index": ["page-toc"],
}
}
If a ``dict`` is specified, the keys can contain glob-style patterns; page names which
match the pattern will contain the sidebar templates specified. This closely follows the behavior of
the ``html_sidebars`` option that is part of Sphinx itself, except that it operates on the
secondary sidebar instead of the primary sidebar. For more information, see `the Sphinx
documentation <https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_sidebars>`__.
2 changes: 1 addition & 1 deletion src/pydata_sphinx_theme/__init__.py
Expand Up @@ -203,7 +203,6 @@ def update_and_remove_templates(
"theme_footer_start",
"theme_footer_center",
"theme_footer_end",
"theme_secondary_sidebar_items",
"theme_primary_sidebar_end",
"sidebars",
]
Expand Down Expand Up @@ -291,6 +290,7 @@ def setup(app: Sphinx) -> Dict[str, str]:
app.connect("html-page-context", toctree.add_toctree_functions)
app.connect("html-page-context", update_and_remove_templates)
app.connect("html-page-context", logo.setup_logo_path)
app.connect("html-page-context", utils.set_secondary_sidebar_items)
app.connect("build-finished", pygment.overwrite_pygments_css)
app.connect("build-finished", logo.copy_logo_images)

Expand Down
@@ -1,6 +1,6 @@
{% if theme_secondary_sidebar_items -%}
{% if secondary_sidebar_items -%}
<div class="sidebar-secondary-items sidebar-secondary__inner">
{% for toc_item in theme_secondary_sidebar_items %}
{% for toc_item in secondary_sidebar_items %}
<div class="sidebar-secondary-item">{% include toc_item %}</div>
{% endfor %}
</div>
Expand Down
81 changes: 79 additions & 2 deletions src/pydata_sphinx_theme/utils.py
@@ -1,11 +1,13 @@
"""General helpers for the management of config parameters."""

import copy
import os
import re
from typing import Any, Dict, Iterator
from typing import Any, Dict, Iterator, List, Union

from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.util import logging
from sphinx.util import logging, matching


def get_theme_options_dict(app: Sphinx) -> Dict[str, Any]:
Expand Down Expand Up @@ -58,3 +60,78 @@ def maybe_warn(app: Sphinx, msg, *args, **kwargs):
should_warn = theme_options.get("surface_warnings", False)
if should_warn:
SPHINX_LOGGER.warning(msg, *args, **kwargs)


def set_secondary_sidebar_items(
app: Sphinx, pagename: str, templatename: str, context, doctree
) -> None:
"""Set the secondary sidebar items to render for the given pagename."""
if "theme_secondary_sidebar_items" in context:
templates = context["theme_secondary_sidebar_items"]
if isinstance(templates, dict):
templates = _get_matching_sidebar_items(pagename, templates)

context["secondary_sidebar_items"] = _update_and_remove_templates(
app,
context,
templates,
"theme_secondary_sidebar_items",
)


def _update_and_remove_templates(
app: Sphinx, context, templates: Union[List, str], template_key: str
) -> List[str]:
# Break apart `,` separated strings so we can use , in the defaults
if isinstance(templates, str):
templates = [ii.strip() for ii in templates.split(",")]
# Add `.html` to templates with no suffix
suffixed_templates = []
for template in templates:
if os.path.splitext(template)[1]:
suffixed_templates.append(template)
else:
suffixed_templates.append(f"{template}.html")

ctx = copy.copy(context)
ctx.update({template_key: suffixed_templates})

# Check whether the template renders to an empty string; remove if this is the case
filtered_templates = []
for template in suffixed_templates:
rendered = app.builder.templates.render(template, ctx)
if len(rendered.strip()) != 0:
filtered_templates.append(template)

return filtered_templates


def _get_matching_sidebar_items(
pagename: str, sidebars: Dict[str, List[str]]
) -> List[str]:
"""Get the matching sidebar templates to render for the given pagename.
This function was adapted from sphinx.builders.html.StandaloneHTMLBuilder.add_sidebars.
Cached to avoid overhead when html context event is fired multiple times on the same page.
"""
matched = None
secondary_sidebar_items = []
for pattern, sidebar_items in sidebars.items():
if matching.patmatch(pagename, pattern):
if matched and _has_wildcard(pattern) and _has_wildcard(matched):
SPHINX_LOGGER.warning(
f"Page {pagename} matches two wildcard patterns in secondary_sidebar_items: {matched} and {pattern}"
),

matched = pattern
secondary_sidebar_items = sidebar_items
return secondary_sidebar_items


def _has_wildcard(pattern: str) -> bool:
"""Check whether the pattern contains a wildcard.
Taken from sphinx.builders.StandaloneHTMLBuilder.add_sidebars.
"""
return any(char in pattern for char in "*?[")
5 changes: 5 additions & 0 deletions tests/sites/sidebars/index.rst
Expand Up @@ -11,3 +11,8 @@ Sidebar depth variations
:caption: Caption 2

section2/index

Other content
-------------

This is some other content.
5 changes: 5 additions & 0 deletions tests/sites/sidebars/section1/index.rst
Expand Up @@ -6,3 +6,8 @@ Section 1 index

subsection1/index
page2

Other Content
-------------

This is some other content
4 changes: 4 additions & 0 deletions tests/sites/sidebars/section1/subsection1/page2.rst
@@ -1,2 +1,6 @@
Section 1 sub 1 page 2
======================


Section A
---------
6 changes: 6 additions & 0 deletions tests/sites/sidebars/section2/index.rst
Expand Up @@ -5,3 +5,9 @@ Section 2 index

page1
https://google.com


Other Content
-------------

This is some other content
70 changes: 70 additions & 0 deletions tests/test_build.py
Expand Up @@ -986,3 +986,73 @@ def test_translations(sphinx_build_factory) -> None:
# Search bar
# TODO: Add translations where there are english phrases below
assert "Search the docs" in str(index.select(".bd-search")[0])


def test_render_secondary_sidebar_list(sphinx_build_factory) -> None:
"""Test that the secondary sidebar can be built with a list of templates."""
confoverrides = {
"html_context": {
"github_user": "pydata",
"github_repo": "pydata-sphinx-theme",
"github_version": "main",
},
"html_theme_options": {
"use_edit_page_button": True,
"secondary_sidebar_items": ["page-toc", "edit-this-page"],
},
}
sphinx_build = sphinx_build_factory("sidebars", confoverrides=confoverrides)
# Basic build with defaults
sphinx_build.build()

# Check that the page-toc template gets rendered
assert sphinx_build.html_tree("index.html").select("div.page-toc")
assert sphinx_build.html_tree("section1/index.html").select("div.page-toc")
assert sphinx_build.html_tree("section2/index.html").select("div.page-toc")

# Check that the edit-this-page template gets rendered
assert sphinx_build.html_tree("index.html").select("div.editthispage")
assert sphinx_build.html_tree("section1/index.html").select("div.editthispage")
assert sphinx_build.html_tree("section2/index.html").select("div.editthispage")

# Check that sourcelink is not rendered
assert not sphinx_build.html_tree("index.html").select("div.sourcelink")
assert not sphinx_build.html_tree("section1/index.html").select("div.sourcelink")
assert not sphinx_build.html_tree("section2/index.html").select("div.sourcelink")


def test_render_secondary_sidebar_dict(sphinx_build_factory) -> None:
"""Test that the secondary sidebar can be built with a dict of templates."""
confoverrides = {
"html_context": {
"github_user": "pydata",
"github_repo": "pydata-sphinx-theme",
"github_version": "main",
},
"html_theme_options": {
"use_edit_page_button": True,
"secondary_sidebar_items": {
"**": ["page-toc", "edit-this-page"],
"section1/index": [],
"section2/index": ["sourcelink"],
},
},
}
sphinx_build = sphinx_build_factory("sidebars", confoverrides=confoverrides)
# Basic build with defaults
sphinx_build.build()

# Check that the page-toc template gets rendered
assert sphinx_build.html_tree("index.html").select("div.page-toc")
assert not sphinx_build.html_tree("section1/index.html").select("div.page-toc")
assert not sphinx_build.html_tree("section2/index.html").select("div.page-toc")

# Check that the edit-this-page template gets rendered
assert sphinx_build.html_tree("index.html").select("div.editthispage")
assert not sphinx_build.html_tree("section1/index.html").select("div.editthispage")
assert not sphinx_build.html_tree("section2/index.html").select("div.editthispage")

# Check that sourcelink is not rendered
assert not sphinx_build.html_tree("index.html").select("div.sourcelink")
assert not sphinx_build.html_tree("section1/index.html").select("div.sourcelink")
assert sphinx_build.html_tree("section2/index.html").select("div.sourcelink")

0 comments on commit 7034724

Please sign in to comment.