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 Dec 14, 2023
1 parent 8bc48a9 commit 4ca870b
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 31 deletions.
29 changes: 29 additions & 0 deletions docs/user_guide/page-toc.rst
Original file line number Diff line number Diff line change
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>`__.
36 changes: 9 additions & 27 deletions src/pydata_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Bootstrap-based sphinx theme from the PyData community."""

import json
import os
from functools import partial
from pathlib import Path
from typing import Dict
Expand Down Expand Up @@ -203,37 +202,19 @@ def update_and_remove_templates(
"theme_footer_start",
"theme_footer_center",
"theme_footer_end",
"theme_secondary_sidebar_items",
"theme_primary_sidebar_end",
"sidebars",
]
for section in template_sections:
if context.get(section):
# Break apart `,` separated strings so we can use , in the defaults
if isinstance(context.get(section), str):
context[section] = [
ii.strip() for ii in context.get(section).split(",")
]

# Add `.html` to templates with no suffix
for ii, template in enumerate(context.get(section)):
if not os.path.splitext(template)[1]:
context[section][ii] = template + ".html"

# If this is the page TOC, check if it is empty and remove it if so
def _remove_empty_templates(tname):
# These templates take too long to render, so skip them.
# They should never be empty anyway.
SKIP_EMPTY_TEMPLATE_CHECKS = ["sidebar-nav-bs.html", "navbar-nav.html"]
if not any(tname.endswith(temp) for temp in SKIP_EMPTY_TEMPLATE_CHECKS):
# Render the template and see if it is totally empty
rendered = app.builder.templates.render(tname, context)
if len(rendered.strip()) == 0:
return False
return True

context[section] = list(filter(_remove_empty_templates, context[section]))
#
context[section] = utils._update_and_remove_templates(
app=app,
context=context,
templates=context.get(section, []),
section=section,
templates_skip_empty_check=["sidebar-nav-bs.html", "navbar-nav.html"],
)

# Remove a duplicate entry of the theme CSS. This is because it is in both:
# - theme.conf
# - manually linked in `webpack-macros.html`
Expand Down Expand Up @@ -296,6 +277,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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% 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 %}
{# Note: secondary_sidebar_items is set by set_secondary_sidebar_items() in utils.py #}
{% for toc_item in secondary_sidebar_items %}
<div class="sidebar-secondary-item">{% include toc_item %}</div>
{% endfor %}
</div>
Expand Down
105 changes: 103 additions & 2 deletions src/pydata_sphinx_theme/utils.py
Original file line number Diff line number Diff line change
@@ -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, Optional, 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,102 @@ 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: Dict[str, Any],
templates: Union[List, str],
section: str,
templates_skip_empty_check: Optional[List[str]] = None,
) -> List[str]:
"""Update templates to include html suffix if needed; remove templates which render empty.
Args:
app: Sphinx application passed to the html page context
context: The html page context; dictionary of values passed to the templating engine
templates: A list of template names, or a string of comma separated template names
section: Name of the template section where the templates are to be rendered
templates_skip_empty_check: Names of any templates which should never be removed from the list
of filtered templates returned by this function. These templates aren't checked if they
render empty, which can save time if the template is slow to render.
Returns:
A list of template names (including '.html' suffix) to render into the section
"""
if templates_skip_empty_check is None:
templates_skip_empty_check = []

# Break apart `,` separated strings so we can use , in the defaults
if isinstance(templates, str):
templates = [template.strip() for template 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({section: suffixed_templates})

# Check whether the template renders to an empty string; remove if this is the case
# Skip templates that are slow to render with templates_skip_empty_check
filtered_templates = []
for template in suffixed_templates:
if any(template.endswith(item) for item in templates_skip_empty_check):
filtered_templates.append(template)
else:
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.
"""
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
Section 1 sub 1 page 2
======================


Section A
---------
6 changes: 6 additions & 0 deletions tests/sites/sidebars/section2/index.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 4ca870b

Please sign in to comment.