diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7012dd3c5..9e4cc8877 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,6 +39,7 @@ jobs: # run our test suite on various combinations of OS / Python / Sphinx version run-pytest: + needs: [lint] strategy: fail-fast: false matrix: @@ -93,6 +94,7 @@ jobs: # Build our site on the 3 major OSes and check for Sphinx warnings build-site: + needs: [lint] strategy: fail-fast: false matrix: @@ -120,6 +122,7 @@ jobs: # Run local Lighthouse audit against built site audit: + needs: [build-site] strategy: matrix: os: [ubuntu-latest] @@ -163,6 +166,7 @@ jobs: # Generate a profile of the code and upload as an artifact profile: + needs: [build-site, run-pytest] strategy: matrix: os: [ubuntu-latest] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 466efae7c..1b5e3aa41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,22 +17,10 @@ repos: hooks: - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.0.215" hooks: - - id: flake8 - additional_dependencies: [Flake8-pyproject] - - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-builtin-literals - - id: check-case-conflict - - id: check-toml - - id: check-yaml - - id: debug-statements - - id: end-of-file-fixer - - id: trailing-whitespace + - id: ruff - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 diff --git a/docs/scripts/gallery_directive/__init__.py b/docs/_extension/gallery_directive.py similarity index 91% rename from docs/scripts/gallery_directive/__init__.py rename to docs/_extension/gallery_directive.py index 2119d9872..e80d2c8e7 100644 --- a/docs/scripts/gallery_directive/__init__.py +++ b/docs/_extension/gallery_directive.py @@ -8,14 +8,15 @@ It currently exists for maintainers of the pydata-sphinx-theme, but might be abstracted into a standalone package if it proves useful. """ -from yaml import safe_load -from typing import List from pathlib import Path +from typing import Any, Dict, List from docutils import nodes from docutils.parsers.rst import directives -from sphinx.util.docutils import SphinxDirective +from sphinx.application import Sphinx from sphinx.util import logging +from sphinx.util.docutils import SphinxDirective +from yaml import safe_load logger = logging.getLogger(__name__) @@ -142,3 +143,19 @@ def run(self) -> List[nodes.Node]: if self.options.get("container-class", []): container.attributes["classes"] += self.options.get("class", []) return [container] + + +def setup(app: Sphinx) -> Dict[str, Any]: + """Add custom configuration to sphinx app. + + Args: + app: the Sphinx application + Returns: + the 2 parallel parameters set to ``True``. + """ + app.add_directive("gallery-grid", GalleryDirective) + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/conf.py b/docs/conf.py index 162d8f005..f33cf1eb3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,10 +1,20 @@ +"""Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full +list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + # -- Path setup -------------------------------------------------------------- import os import sys +from pathlib import Path +from typing import Any, Dict + import pydata_sphinx_theme +from sphinx.application import Sphinx -sys.path.append("scripts") -from gallery_directive import GalleryDirective +sys.path.append(str(Path(".").resolve())) # -- Project information ----------------------------------------------------- @@ -22,6 +32,7 @@ "sphinxext.rediraffe", "sphinx_design", "sphinx_copybutton", + "_extension.gallery_directive", # For extension examples and demos "ablog", "jupyter_sphinx", @@ -57,7 +68,6 @@ autosummary_generate = True - # -- Internationalization ---------------------------------------------------- # specifying the natural language populates some key tags @@ -223,9 +233,13 @@ # -- application setup ------------------------------------------------------- -def setup_to_main(app, pagename, templatename, context, doctree): +def setup_to_main( + app: Sphinx, pagename: str, templatename: str, context, doctree +) -> None: + """Add a function that jinja can access for returning an "edit this page" link pointing to `main`.""" + def to_main(link: str) -> str: - """Transform "edit on github" links and make sure they always point to the main branch + """Transform "edit on github" links and make sure they always point to the main branch. Args: link: the link to the github edit interface @@ -240,6 +254,17 @@ def to_main(link: str) -> str: context["to_main"] = to_main -def setup(app): - app.add_directive("gallery-grid", GalleryDirective) +def setup(app: Sphinx) -> Dict[str, Any]: + """Add custom configuration to sphinx app. + + Args: + app: the Sphinx application + Returns: + the 2 parralel parameters set to ``True``. + """ app.connect("html-page-context", setup_to_main) + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/examples/test_py_module/__init__.py b/docs/examples/test_py_module/__init__.py index e69de29bb..a2ee367d8 100644 --- a/docs/examples/test_py_module/__init__.py +++ b/docs/examples/test_py_module/__init__.py @@ -0,0 +1 @@ +"""Package definition empty file.""" diff --git a/docs/examples/test_py_module/test.py b/docs/examples/test_py_module/test.py index c76f978d0..800930749 100644 --- a/docs/examples/test_py_module/test.py +++ b/docs/examples/test_py_module/test.py @@ -2,7 +2,6 @@ class Foo: - """Docstring for class Foo. This text tests for the formatting of docstrings generated from output @@ -65,7 +64,6 @@ def add(self, val1, val2): :rtype: int """ - return val1 + val2 def capitalize(self, myvalue): @@ -76,12 +74,10 @@ def capitalize(self, myvalue): :rtype: string """ - return myvalue.upper() def another_function(self, a, b, **kwargs): - """ - Here is another function. + """Here is another function. :param a: The number of green hats you own. :type a: int diff --git a/docs/scripts/generate_collaborators_gallery.py b/docs/scripts/generate_collaborators_gallery.py index d0437cc39..064098efd 100644 --- a/docs/scripts/generate_collaborators_gallery.py +++ b/docs/scripts/generate_collaborators_gallery.py @@ -1,14 +1,13 @@ -"""Uses the GitHub API to list a gallery of all people with direct access -to the repository. -""" +"""Uses the GitHub API to list a gallery of all people with direct access to the repository.""" -from yaml import dump -from subprocess import run -import shlex import json +import shlex from pathlib import Path +from subprocess import run + +from yaml import dump -COLLABORATORS_API = "https://api.github.com/repos/pydata/pydata-sphinx-theme/collaborators?affiliation=direct" # noqa +COLLABORATORS_API = "https://api.github.com/repos/pydata/pydata-sphinx-theme/collaborators?affiliation=direct" print("Grabbing latest collaborators with GitHub API via GitHub's CLI...") out = run(shlex.split(f"gh api {COLLABORATORS_API}"), capture_output=True) diff --git a/docs/scripts/generate_gallery_images.py b/docs/scripts/generate_gallery_images.py index 7ea15dcce..d2de76188 100644 --- a/docs/scripts/generate_gallery_images.py +++ b/docs/scripts/generate_gallery_images.py @@ -1,24 +1,21 @@ -""" -Use playwright to build a gallery of website using this theme -""" +"""Use playwright to build a gallery of website using this theme.""" from pathlib import Path -from yaml import safe_load from shutil import copy -from playwright.sync_api import sync_playwright, TimeoutError -from rich.progress import track + +from playwright.sync_api import TimeoutError, sync_playwright from rich import print +from rich.progress import track +from yaml import safe_load -def regenerate_gallery(): - """ - Regenerate images of snapshots for our gallery. +def regenerate_gallery() -> None: + """Regenerate images of snapshots for our gallery. This function should only be triggered in RTD builds as it increases the build time by 30-60s. Developers can still execute this function from time to time to populate their local gallery images with updated files. """ - # get the existing folders path _static_dir = Path(__file__).parents[1] / "_static" diff --git a/docs/scripts/update_kitchen_sink.py b/docs/scripts/update_kitchen_sink.py index cb855c92c..230f79fa9 100644 --- a/docs/scripts/update_kitchen_sink.py +++ b/docs/scripts/update_kitchen_sink.py @@ -1,5 +1,7 @@ -from urllib.request import urlopen +"""Script run to update the kitchen sink from https://sphinx-themes.org.""" + from pathlib import Path +from urllib.request import urlopen EXTRA_MESSAGE = """\ .. note:: @@ -11,7 +13,7 @@ :color: primary Go to Sphinx Themes -""" # noqa +""" kitchen_sink_files = [ "admonitions.rst", @@ -29,7 +31,7 @@ path_sink = Path(__file__).parent.parent / "examples" / "kitchen-sink" for ifile in kitchen_sink_files: print(f"Reading {ifile}...") - url = f"https://github.com/sphinx-themes/sphinx-themes.org/raw/master/sample-docs/kitchen-sink/{ifile}" # noqa + url = f"https://github.com/sphinx-themes/sphinx-themes.org/raw/master/sample-docs/kitchen-sink/{ifile}" text = urlopen(url).read().decode() # The sphinx-themes docs expect Furo to be installed, so we overwrite w/ PST text = text.replace("src/furo", "src/pydata_sphinx_theme") diff --git a/noxfile.py b/noxfile.py index c188f2362..71903094f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,19 +1,23 @@ """Automatically build our documentation or run tests. Environments are re-used by default. - Re-install the environment from scratch: nox -s docs -- -r """ -import nox +import shutil as sh +import tempfile from pathlib import Path from shlex import split +from textwrap import dedent + +import nox nox.options.reuse_existing_virtualenvs = True +ROOT = Path(__file__).parent -def _should_install(session): +def _should_install(session: nox.Session) -> bool: """Decide if we should install an environment or if it already exists. This speeds up the local install considerably because building the wheel @@ -21,6 +25,9 @@ def _should_install(session): We assume that if `sphinx-build` is in the bin/ path, the environment is installed. + + Parameter: + session: the current nox session """ if session.bin_paths is None: session.log("Running with `--no-venv` so don't install anything...") @@ -36,20 +43,25 @@ def _should_install(session): return should_install -def _compile_translations(session): - session.run(*split("pybabel compile -d src/pydata_sphinx_theme/locale -D sphinx")) +@nox.session(reuse_venv=True) +def lint(session: nox.Session) -> None: + """Check the themes pre-commit before any other session.""" + session.install("pre-commit") + session.run("pre-commit", "run", "-a") -@nox.session(name="compile") -def compile(session): +@nox.session() +def compile(session: nox.Session) -> None: """Compile the theme's web assets with sphinx-theme-builder.""" if _should_install(session): session.install("-e", ".") session.install("sphinx-theme-builder[cli]") + session.run("stb", "compile") + -@nox.session(name="docs") -def docs(session): +@nox.session() +def docs(session: nox.Session) -> None: """Build the documentation and place in docs/_build/html. Use --no-compile to skip compilation.""" if _should_install(session): session.install("-e", ".[doc]") @@ -60,27 +72,27 @@ def docs(session): @nox.session(name="docs-live") -def docs_live(session): +def docs_live(session: nox.Session) -> None: """Build the docs with a live server that re-loads as you make changes.""" - _compile_translations(session) + session.run(*split("pybabel compile -d src/pydata_sphinx_theme/locale -D sphinx")) if _should_install(session): session.install("-e", ".[doc]") session.install("sphinx-theme-builder[cli]") session.run("stb", "serve", "docs", "--open-browser") -@nox.session(name="test") -def test(session): +@nox.session() +def test(session: nox.Session) -> None: """Run the test suite.""" if _should_install(session): session.install("-e", ".[test]") - _compile_translations(session) + session.run(*split("pybabel compile -d src/pydata_sphinx_theme/locale -D sphinx")) session.run("pytest", *session.posargs) @nox.session(name="test-sphinx") @nox.parametrize("sphinx", ["4", "5", "6"]) -def test_sphinx(session, sphinx): +def test_sphinx(session: nox.Session, sphinx: int) -> None: """Run the test suite with a specific version of Sphinx.""" if _should_install(session): session.install("-e", ".[test]") @@ -89,44 +101,54 @@ def test_sphinx(session, sphinx): @nox.session() -def translate(session): - """Translation commands. Available commands after `--` : extract, update, compile""" - session.install("Babel") - if "extract" in session.posargs: - session.run( - *split( - "pybabel extract . -F babel.cfg -o src/pydata_sphinx_theme/locale/sphinx.pot -k '_ __ l_ lazy_gettext'" - ) - ) - elif "update" in session.posargs: - session.run( - *split( - "pybabel update -i src/pydata_sphinx_theme/locale/sphinx.pot -d src/pydata_sphinx_theme/locale -D sphinx" - ) - ) - elif "compile" in session.posargs: - _compile_translations(session) - elif "init" in session.posargs: - language = session.posargs[-1] - session.run( - *split( - f"pybabel init -i src/pydata_sphinx_theme/locale/sphinx.pot -d src/pydata_sphinx_theme/locale -D sphinx -l {language}" - ) - ) - else: +def translate(session: nox.Session) -> None: + """Translation commands. Available commands after `--` : extract, update, compile, init.""" + # get the command from posargs, default to "update" + pybabel_cmd, found = ("update", False) + for c in ["extract", "update", "compile", "init"]: + if c in session.posargs: + pybabel_cmd, found = (c, True) + + if found is False: print( "No translate command found. Use like: `nox -s translate -- COMMAND`." - "\n\n Available commands: extract, update, compile, init" + "\ndefaulting to `update`" + "\nAvailable commands: extract, update, compile, init" ) + # get the language from parameters default to en. + # it can be deceiving but we don't have a table of accepted languages yet + lan = "en" if len(session.posargs) < 2 else session.posargs[-1] -@nox.session(name="profile") -def profile(session): - """Generate a profile chart with py-spy. The chart will be placed at profile.svg.""" - import shutil as sh - import tempfile - from textwrap import dedent + # get the path to the differnet local related pieces + locale_dir = str(ROOT / "src" / "pydata_sphinx_theme" / "locale") + babel_cfg = str(ROOT / "babel.cfg") + pot_file = str(locale_dir / "sphinx.pot") + + # install deps + session.install("Babel") + + # build the command from the parameters + cmd = ["pybabel", pybabel_cmd] + if pybabel_cmd == "extract": + cmd += [ROOT, "-F", babel_cfg, "-o", pot_file, "-k", "_ __ l_ lazy_gettext"] + + elif pybabel_cmd == "update": + cmd += ["-i", pot_file, "-d", locale_dir, "-D", "sphinx"] + + elif pybabel_cmd == "compile": + cmd += ["-d", locale_dir, "-D", "sphinx"] + + elif pybabel_cmd == "init": + cmd += ["-i", pot_file, "-d", locale_dir, "-D", "sphinx", "-l", lan] + + session.run(cmd) + + +@nox.session() +def profile(session: nox.Session) -> None: + """Generate a profile chart with py-spy. The chart will be placed at profile.svg.""" if _should_install(session): session.install("-e", ".[test]") session.install("py-spy") @@ -167,6 +189,6 @@ def profile(session): # Profile the build print(f"Profiling build with {n_extra_pages} pages with py-spy...") session.run( - *f"py-spy record -o {output} -- sphinx-build {path_tmp} {path_tmp_out}".split() # noqa + *f"py-spy record -o {output} -- sphinx-build {path_tmp} {path_tmp_out}".split() ) print(f"py-spy profiler output at this file: {output}") diff --git a/pyproject.toml b/pyproject.toml index 8f1603177..423c57295 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ dependencies = [ "packaging", "Babel", "pygments>=2.7", - "accessible-pygments" + "accessible-pygments", + "typing-extensions" ] license = { file = "LICENSE" } @@ -95,13 +96,20 @@ dev = [ [tool.doc8] ignore = ["D001"] # we follow a 1 line = 1 paragraph style -[tool.flake8] -# E203: space before ":" | needed for how black formats slicing -# E501: line too long | Black take care of it -# W503: line break before binary operator | Black take care of it -# W605: invalid escape sequence | we escape specific characters for sphinx -ignore = ["E203", "E501", "W503", "W605"] -exclude = ["setup.py", "docs/conf.py", "node_modules", "docs", "build", "dist"] +[tool.ruff] +ignore-init-module-imports = true +fix = true +select = ["E", "F", "W", "I", "D", "RUF"] +ignore = [ + "E501", # line too long | Black take care of it + "D107", # Missing docstring in `__init__` | set the docstring in the class +] + +[tool.ruff.flake8-quotes] +docstring-quotes = "double" + +[tool.ruff.pydocstyle] +convention = "google" [tool.djlint] profile = "jinja" diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 82a23fbd7..fd403b045 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -1,66 +1,31 @@ -""" -Bootstrap-based sphinx theme from the PyData community -""" +"""Bootstrap-based sphinx theme from the PyData community.""" + +import json import os from pathlib import Path -from functools import lru_cache -import json -from urllib.parse import urlparse, urlunparse -import types +from typing import Dict +from urllib.parse import urlparse -import jinja2 -from bs4 import BeautifulSoup as bs -from docutils import nodes -from sphinx import addnodes -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.errors import ExtensionError -from sphinx.util import logging, isurl -from sphinx.util.fileutil import copy_asset_file -from pygments.formatters import HtmlFormatter -from pygments.styles import get_all_styles import requests from requests.exceptions import ConnectionError, HTTPError, RetryError +from sphinx.application import Sphinx +from sphinx.errors import ExtensionError +from sphinx.util import logging -from .translator import BootstrapHTML5TranslatorMixin +from . import edit_this_page, logo, pygment, short_link, toctree, translator, utils __version__ = "0.13.3dev0" logger = logging.getLogger(__name__) -def _get_theme_options_dict(app): - """Get the Sphinx theme options dictionary (or fallback to an empty dict). - - The "top-level" mapping (the one we should usually check first, and modify - if desired) is ``app.builder.theme_options``. It is created by Sphinx as a - copy of ``app.config.html_theme_options`` (containing user-configs from - their ``conf.py``); sometimes that copy never occurs though which is why we - check both. - """ - if hasattr(app.builder, "theme_options"): - return app.builder.theme_options - elif hasattr(app.config, "html_theme_options"): - return app.config.html_theme_options - else: - return {} - - -def _config_provided_by_user(app, key): - """Check if the user has manually provided the config.""" - return any(key in ii for ii in [app.config.overrides, app.config._raw_config]) - - def update_config(app): """Update config with new default values and handle deprecated keys.""" # By the time `builder-inited` happens, `app.builder.theme_options` already exists. # At this point, modifying app.config.html_theme_options will NOT update the # page's HTML context (e.g. in jinja, `theme_keyword`). # To do this, you must manually modify `app.builder.theme_options`. - theme_options = _get_theme_options_dict(app) + theme_options = utils.get_theme_options_dict(app) # TODO: deprecation; remove after 0.14 release if theme_options.get("logo_text"): @@ -103,7 +68,7 @@ def update_config(app): ) # Set the anchor link default to be # if the user hasn't provided their own - if not _config_provided_by_user(app, "html_permalinks_icon"): + if not utils.config_provided_by_user(app, "html_permalinks_icon"): app.config.html_permalinks_icon = "#" # Raise a warning for a deprecated theme switcher config @@ -190,9 +155,8 @@ def update_config(app): app.add_js_file(None, body=gid_script) # Update ABlog configuration default if present - if "ablog" in app.config.extensions and not _config_provided_by_user( - app, "fontawesome_included" - ): + fa_provided = utils.config_provided_by_user(app, "fontawesome_included") + if "ablog" in app.config.extensions and not fa_provided: app.config.fontawesome_included = True # Handle icon link shortcuts @@ -229,7 +193,9 @@ def update_config(app): theme_options["logo"] = theme_logo -def update_and_remove_templates(app, pagename, templatename, context, doctree): +def update_and_remove_templates( + app: Sphinx, pagename: str, templatename: str, context, doctree +) -> None: """Update template names and assets for page build.""" # Allow for more flexibility in template names template_sections = [ @@ -314,903 +280,23 @@ def _remove_empty_templates(tname): context["theme_version"] = __version__ -def add_inline_math(node): - """Render a node with HTML tags that activate MathJax processing. - This is meant for use with rendering section titles with math in them, because - math outputs are ignored by pydata-sphinx-theme's header. - - related to the behaviour of a normal math node from: - https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/mathjax.py#L28 - """ - - return ( - '' rf"\({node.astext()}\)" "" - ) - - -def add_toctree_functions(app, pagename, templatename, context, doctree): - """Add functions so Jinja templates can add toctree objects.""" - - @lru_cache(maxsize=None) - def generate_header_nav_html(n_links_before_dropdown=5): - """ - Generate top-level links that are meant for the header navigation. - We use this function instead of the TocTree-based one used for the - sidebar because this one is much faster for generating the links and - we don't need the complexity of the full Sphinx TocTree. - - This includes two kinds of links: - - - Links to pages described listed in the root_doc TocTrees - - External links defined in theme configuration - - Additionally it will create a dropdown list for several links after - a cutoff. - - Parameters - ---------- - n_links_before_dropdown : int (default: 5) - The number of links to show before nesting the remaining links in - a Dropdown element. - """ - - try: - n_links_before_dropdown = int(n_links_before_dropdown) - except Exception: - raise ValueError( - f"n_links_before_dropdown is not an int: {n_links_before_dropdown}" - ) - toctree = TocTree(app.env) - - # Find the active header navigation item so we decide whether to highlight - # Will be empty if there is no active page (root_doc, or genindex etc) - active_header_page = toctree.get_toctree_ancestors(pagename) - if active_header_page: - # The final list item will be the top-most ancestor - active_header_page = active_header_page[-1] - - # Find the root document because it lists our top-level toctree pages - root = app.env.tocs[app.config.root_doc] - - # Iterate through each toctree node in the root document - # Grab the toctree pages and find the relative link + title. - links_html = [] - # TODO: just use "findall" once docutils min version >=0.18.1 - meth = "findall" if hasattr(root, "findall") else "traverse" - for toc in getattr(root, meth)(toctree_node): - for title, page in toc.attributes["entries"]: - # if the page is using "self" use the correct link - page = toc.attributes["parent"] if page == "self" else page - - # If this is the active ancestor page, add a class so we highlight it - current = " current active" if page == active_header_page else "" - - # sanitize page title for use in the html output if needed - if title is None: - title = "" - for node in app.env.titles[page].children: - if isinstance(node, nodes.math): - title += add_inline_math(node) - else: - title += node.astext() - - # set up the status of the link and the path - # if the path is relative then we use the context for the path - # resolution and the internal class. - # If it's an absolute one then we use the external class and - # the complete url. - is_absolute = bool(urlparse(page).netloc) - link_status = "external" if is_absolute else "internal" - link_href = page if is_absolute else context["pathto"](page) - - # create the html output - links_html.append( - f""" - - """ - ) - - # Add external links defined in configuration as sibling list items - for external_link in context["theme_external_links"]: - links_html.append( - f""" - - """ - ) - - # The first links will always be visible - links_solo = links_html[:n_links_before_dropdown] - out = "\n".join(links_solo) - - # Wrap the final few header items in a "more" dropdown - links_dropdown = links_html[n_links_before_dropdown:] - if links_dropdown: - links_dropdown_html = "\n".join(links_dropdown) - out += f""" - - """ # noqa - - return out - - # Cache this function because it is expensive to run, and because Sphinx - # somehow runs this twice in some circumstances in unpredictable ways. - @lru_cache(maxsize=None) - def generate_toctree_html(kind, startdepth=1, show_nav_level=1, **kwargs): - """ - Return the navigation link structure in HTML. This is similar to Sphinx's - own default TocTree generation, but it is modified to generate TocTrees - for *second*-level pages and below (not supported by default in Sphinx). - This is used for our sidebar, which starts at the second-level page. - - It also modifies the generated TocTree slightly for Bootstrap classes - and structure (via BeautifulSoup). - - Arguments are passed to Sphinx "toctree" function (context["toctree"] below). - - ref: https://www.sphinx-doc.org/en/master/templating.html#toctree - - Parameters - ---------- - kind : "sidebar" or "raw" - Whether to generate HTML meant for sidebar navigation ("sidebar") - or to return the raw BeautifulSoup object ("raw"). - startdepth : int - The level of the toctree at which to start. By default, for - the navbar uses the normal toctree (`startdepth=0`), and for - the sidebar starts from the second level (`startdepth=1`). - show_nav_level : int - The level of the navigation bar to toggle as visible on page load. - By default, this level is 1, and only top-level pages are shown, - with drop-boxes to reveal children. Increasing `show_nav_level` - will show child levels as well. - - kwargs: passed to the Sphinx `toctree` template function. - - Returns - ------- - 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) - - soup = bs(toc_sphinx, "html.parser") - - # pair "current" with "active" since that's what we use w/ bootstrap - for li in soup("li", {"class": "current"}): - li["class"].append("active") - - # Remove sidebar links to sub-headers on the page - for li in soup.select("li"): - # Remove - if li.find("a"): - href = li.find("a")["href"] - if "#" in href and href != "#": - li.decompose() - - if kind == "sidebar": - # Add bootstrap classes for first `ul` items - for ul in soup("ul", recursive=False): - ul.attrs["class"] = ul.attrs.get("class", []) + ["nav", "bd-sidenav"] - - # Add collapse boxes for parts/captions. - # Wraps the TOC part in an extra