diff --git a/docs/_static/demo-navigation_startdepth-0.gif b/docs/_static/demo-navigation_startdepth-0.gif new file mode 100644 index 000000000..28822db70 Binary files /dev/null and b/docs/_static/demo-navigation_startdepth-0.gif differ diff --git a/docs/_static/demo-show_nav_level-0.gif b/docs/_static/demo-show_nav_level-0.gif new file mode 100644 index 000000000..1013011fa Binary files /dev/null and b/docs/_static/demo-show_nav_level-0.gif differ diff --git a/docs/_static/demo-toc_caption_maxdepth-3.gif b/docs/_static/demo-toc_caption_maxdepth-3.gif new file mode 100644 index 000000000..96671a77f Binary files /dev/null and b/docs/_static/demo-toc_caption_maxdepth-3.gif differ diff --git a/docs/conf.py b/docs/conf.py index be25b184c..7fb1054e9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -145,6 +145,8 @@ "navbar_align": "left", # [left, content, right] For testing that the navbar items align properly "navbar_center": ["version-switcher", "navbar-nav"], "announcement": "https://raw.githubusercontent.com/pydata/pydata-sphinx-theme/main/docs/_templates/custom-template.html", + # "navigation_startdepth": 0, + # "toc_caption_maxdepth": 3, # "show_nav_level": 2, # "navbar_start": ["navbar-logo"], # "navbar_end": ["theme-switcher", "navbar-icon-links"], diff --git a/docs/examples/subpages/index.rst b/docs/examples/subpages/index.rst index d49126ce1..0a8218848 100644 --- a/docs/examples/subpages/index.rst +++ b/docs/examples/subpages/index.rst @@ -5,6 +5,7 @@ To create an additional level of nesting in the sidebar, construct a nested ``toctree``: .. toctree:: + :caption: Sub TOC with caption subpage1 subpage2 diff --git a/docs/examples/subpages/subsubpages/index.rst b/docs/examples/subpages/subsubpages/index.rst index fe89301c3..fec720037 100644 --- a/docs/examples/subpages/subsubpages/index.rst +++ b/docs/examples/subpages/subsubpages/index.rst @@ -5,7 +5,13 @@ To create an additional level of nesting in the sidebar, construct a nested ``toctree``: .. toctree:: + :caption: Sub sub TOC with caption subsubpage1 subsubpage2 - subsubpage3 + + +.. toctree:: + :caption: Sub sub TOC with another caption + + subsubpage3 \ No newline at end of file diff --git a/docs/user_guide/navigation.rst b/docs/user_guide/navigation.rst index be8b03aa6..d23a3e787 100644 --- a/docs/user_guide/navigation.rst +++ b/docs/user_guide/navigation.rst @@ -47,11 +47,51 @@ If your ``toctree`` does not have a caption defined, then all of the pages under (the same as the default theme behavior). See `the toctree documentation `_ for more details. +.. image:: /_static/demo-show_nav_level-0.gif + .. note:: In some Sphinx sites, the top-level ``toctree`` groupings make up "parts" in the documentation, with each page beneath making up a "chapter". +.. _toc-caption-levels: + +Categorize sub-pages with toctree captions +------------------------------------------ + +It is possible to categorize pages in the :ref:`Primary Sidebar` by placing all pages on a specific topic in their own ``toctree`` with a ``:caption:`` that is used as the category title. + +An example that will generate output similar to this website may look something like this: + +.. code:: restructuredtext + + .. toctree:: + :caption: Get started + + install + layout + + .. toctree:: + :caption: Navigation and links + + navigation + page-toc + +By default, this behavior is only present for pages on the second navigation level +(i.e. the first navigation level that is shown in the primary sidebar). +To show categories at deeper levels, set the ``toc_caption_maxdepth`` option to your desired depth: + +.. code:: python + + html_theme_options = { + "toc_caption_maxdepth": 3 + } + +.. image:: /_static/demo-toc_caption_maxdepth-3.gif + +.. note:: + Changing the ``toc_caption_maxdepth`` is not supported when collapsible toc captions are enabled with ``"show_nav_level": 0`` + .. _navigation-levels: Control the number of navigation levels @@ -66,6 +106,31 @@ in the sidebar (with a default of 4): "navigation_depth": 2 } +Control the navigation startdepth +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + This functionality is not supported when collapsible toc captions are enabled with ``"show_nav_level": 0`` + +By default, the ``toctree`` displayed in the :ref:`Primary Sidebar` starts at the second navigation level, while the first navigation level is shown only in the :ref:`layout-header`. +It is possible to override this behavior by explicitly setting a navigation startdepth: + +.. code:: python + + html_theme_options = { + "navigation_startdepth": 0 + } + +To preserve the default behavior for categories made with toctree captions (see :ref:`toc-caption-levels`), it is necessary to edit the ``toc_caption_maxdepth`` parameter correspondingly (default = 1): + +.. code:: python + + html_theme_options = { + "navigation_startdepth": 0, + "toc_caption_maxdepth": 2 + } + +.. image:: /_static/demo-navigation_startdepth-0.gif Remove reveal buttons for sidebar items --------------------------------------- diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 3633f20cf..e5d15e57b 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -1,6 +1,7 @@ """ Bootstrap-based sphinx theme from the PyData community """ +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Tuple, cast import os from pathlib import Path from functools import lru_cache @@ -11,14 +12,17 @@ import jinja2 from bs4 import BeautifulSoup as bs from docutils import nodes +from docutils.nodes import Element from sphinx import addnodes +from sphinx.locale import __ from sphinx.application import Sphinx from sphinx.environment.adapters.toctree import TocTree from sphinx.addnodes import toctree as toctree_node from sphinx.transforms.post_transforms import SphinxPostTransform -from sphinx.util.nodes import NodeMatcher +from sphinx.util.nodes import NodeMatcher, clean_astext, process_only_nodes +from sphinx.util.matching import Matcher from sphinx.errors import ExtensionError -from sphinx.util import logging, isurl +from sphinx.util import logging, isurl, url_re from sphinx.util.fileutil import copy_asset_file from pygments.formatters import HtmlFormatter from pygments.styles import get_all_styles @@ -27,6 +31,9 @@ from .translator import BootstrapHTML5TranslatorMixin +if TYPE_CHECKING: + from sphinx.builders import Builder + __version__ = "0.13.2dev0" logger = logging.getLogger(__name__) @@ -483,11 +490,7 @@ def generate_toctree_html(kind, startdepth=1, show_nav_level=1, **kwargs): HTML string (if kind == "sidebar") OR BeautifulSoup object (if kind == "raw") """ - if startdepth == 0: - toc_sphinx = context["toctree"](**kwargs) - else: - # select the "active" subset of the navigation tree for the sidebar - toc_sphinx = index_toctree(app, pagename, startdepth, **kwargs) + toc_sphinx = index_toctree(app, pagename, startdepth, **kwargs) soup = bs(toc_sphinx, "html.parser") @@ -694,7 +697,7 @@ def _get_local_toctree_for( # FIX: Can just use "findall" once docutils 0.18+ is required meth = "findall" if hasattr(doctree, "findall") else "traverse" for toctreenode in getattr(doctree, meth)(addnodes.toctree): - toctree = self.resolve(docname, builder, toctreenode, prune=True, **kwargs) + toctree = _resolve(self, docname, builder, toctreenode, prune=True, **kwargs) if toctree: toctrees.append(toctree) if not toctrees: @@ -727,14 +730,19 @@ def index_toctree(app, pagename: str, startdepth: int, collapse: bool = True, ** toctree = TocTree(app.env) ancestors = toctree.get_toctree_ancestors(pagename) - try: - indexname = ancestors[-startdepth] - except IndexError: - # eg for index.rst, but also special pages such as genindex, py-modindex, search - # those pages don't have a "current" element in the toctree, so we can - # directly return an empty string instead of using the default sphinx - # toctree.get_toctree_for(pagename, app.builder, collapse, **kwargs) - return "" + + if startdepth == 0: + # Special case as the toplevel index.rst is not listed in the ancestors list. + indexname = "index" + else: + try: + indexname = ancestors[-startdepth] + except IndexError: + # eg for index.rst, but also special pages such as genindex, py-modindex, search + # those pages don't have a "current" element in the toctree, so we can + # directly return an empty string instead of using the default sphinx + # toctree.get_toctree_for(pagename, app.builder, collapse, **kwargs) + return "" toctree_element = _get_local_toctree_for( toctree, indexname, pagename, app.builder, collapse, **kwargs @@ -797,7 +805,289 @@ def extract_level_recursive(ul, navs_list): return navs +def _resolve(self: TocTree, docname: str, builder: "Builder", toctree: addnodes.toctree, + prune: bool = True, maxdepth: int = 0, titles_only: bool = False, + collapse: bool = False, includehidden: bool = False, + toc_caption_maxdepth: int = 1) -> Optional[Element]: + """Resolve a *toctree* node into individual bullet lists with titles + as items, returning None (if no containing titles are found) or + a new node. + + If *prune* is True, the tree is pruned to *maxdepth*, or if that is 0, + to the value of the *maxdepth* option on the *toctree* node. + If *titles_only* is True, only toplevel document titles will be in the + resulting tree. + If *collapse* is True, all branches not containing docname will + be collapsed. + """ + # this is a copy of `TocTree.resolve`, but where the sphinx version + # only renders toctree captions for top-level toctree-nodes + # we here allow to set `toc_caption_maxdepth` + # for finer control of this behavior. + + if toctree.get('hidden', False) and not includehidden: + return None + generated_docnames: Dict[str, Tuple[str, str, str]] = self.env.domains['std'].initial_data['labels'].copy() # NoQA: E501 + + # For reading the following two helper function, it is useful to keep + # in mind the node structure of a toctree (using HTML-like node names + # for brevity): + # + # + # + # The transformation is made in two passes in order to avoid + # interactions between marking and pruning the tree (see bug #1046). + + toctree_ancestors = self.get_toctree_ancestors(docname) + + excluded = Matcher(self.env.config.exclude_patterns) + + def _toctree_add_classes(node: Element, depth: int) -> None: + """Add 'toctree-l%d' and 'current' classes to the toctree.""" + for subnode in node.children: + if isinstance(subnode, (addnodes.compact_paragraph, + nodes.list_item)): + # for

and

  • , indicate the depth level and recurse + subnode['classes'].append(f'toctree-l{depth - 1}') + _toctree_add_classes(subnode, depth) + elif isinstance(subnode, nodes.bullet_list): + # for