Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add support for per-page secondary sidebar content #1572

Merged
merged 2 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/conf.py
Expand Up @@ -180,7 +180,10 @@
# "content_footer_items": ["test", "test"],
"footer_start": ["copyright"],
"footer_center": ["sphinx-version"],
# "secondary_sidebar_items": ["page-toc"], # Remove the source buttons
"secondary_sidebar_items": {
"**/*": ["page-toc", "edit-this-page", "sourcelink"],
"examples/no-sidebar": [],
},
"switcher": {
"json_url": json_url,
"version_match": version_match,
Expand Down
7 changes: 6 additions & 1 deletion docs/examples/no-sidebar.md
Expand Up @@ -6,6 +6,11 @@ This page shows off what the documentation looks like when you explicitly tell S
html_sidebars = {
"path/to/page": [],
}
html_theme_options = {
"secondary_sidebar_items": {
"path/to/page": [],
},
}
```

The primary sidebar should be entirely gone, and the main content should expand slightly to make up the extra space.
Both the primary and secondary sidebars should be entirely gone, and the main content should expand slightly to make up the extra space.
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>`__.
36 changes: 9 additions & 27 deletions src/pydata_sphinx_theme/__init__.py
@@ -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
@@ -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
115 changes: 113 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, 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,112 @@ 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(
peytondmurray marked this conversation as resolved.
Show resolved Hide resolved
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. Valid
section names include any of the ``sphinx`` or ``html_theme_options`` that take templates
or lists of templates as arguments, for example: ``theme_navbar_start``,
``theme_primary_sidebar_end``, ``theme_secondary_sidebar_items``, ``sidebars``, etc. For
a complete list of valid section names, see the source for
:py:func:`pydata_sphinx_theme.update_and_remove_templates` and
:py:func:`pydata_sphinx_theme.utils.set_secondary_sidebar_items`, both of which call
this function.
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})
peytondmurray marked this conversation as resolved.
Show resolved Hide resolved

# 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.

If a page matches more than one pattern, a warning is emitted, and the templates for the
last matching pattern are used.

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
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