diff --git a/noxfile.py b/noxfile.py index 7ca70c39f..94038e677 100644 --- a/noxfile.py +++ b/noxfile.py @@ -92,7 +92,8 @@ def profile(session): (path_tmp / "many").mkdir() # Create a bunch of empty pages to slow the build - for ii in range(50): + n_extra_pages = 50 + for ii in range(n_extra_pages): (path_tmp / "many" / f"{ii}.rst").write_text("Test\n====\n\nbody\n") if "-o" in session.posargs: @@ -100,8 +101,12 @@ def profile(session): else: output = "profile.svg" - # Specify our output directory and profile the build + # Specify our output directory path_tmp_out = path_tmp / "_build" + + # 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 ) + print(f"py-spy profiler output at this file: {output}") diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 519f01506..18ff597fc 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -4,11 +4,13 @@ import os import warnings from pathlib import Path +from functools import lru_cache import jinja2 from bs4 import BeautifulSoup as bs from sphinx import addnodes from sphinx.environment.adapters.toctree import TocTree +from sphinx.addnodes import toctree as toctree_node from sphinx.errors import ExtensionError from sphinx.util import logging from pygments.formatters import HtmlFormatter @@ -197,21 +199,125 @@ def update_templates(app, pagename, templatename, context, doctree): def add_toctree_functions(app, pagename, templatename, context, doctree): """Add functions so Jinja templates can add toctree objects.""" - def generate_nav_html( - kind, startdepth=None, show_nav_level=1, n_links_before_dropdown=5, **kwargs - ): + @lru_cache(maxsize=None) + def generate_header_nav_html(n_links_before_dropdown=5): """ - Return the navigation link structure in HTML. Arguments are passed - to Sphinx "toctree" function (context["toctree"] below). + 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. - We use beautifulsoup to add the right CSS classes / structure for bootstrap. + This includes two kinds of links: - See https://www.sphinx-doc.org/en/master/templating.html#toctree. + - 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 ---------- - kind : ["navbar", "sidebar", "raw"] - The kind of UI element this toctree is generated for. + 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 = [] + # Can 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 _, page in toc.attributes["entries"]: + # If this is the active ancestor page, add a class so we highlight it + current = " current active" if page == active_header_page else "" + title = app.env.titles[page].astext() + 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""" + """ # noqa + ) + + # 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 + + # TODO: Deprecate after v0.12 + def generate_nav_html(*args, **kwargs): + logger.warning( + "`generate_nav_html` is deprecated and will be removed." + "Use `generate_toctree_html` instead." + ) + generate_toctree_html(*args, **kwargs) + + # Cache this function because it is expensive to run, and becaues 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 @@ -221,40 +327,27 @@ def generate_nav_html( 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. - n_links_before_dropdown : int (default: 5) - The number of links to show before nesting the remaining links in - a Dropdown element. kwargs: passed to the Sphinx `toctree` template function. Returns ------- - HTML string (if kind in ["navbar", "sidebar"]) - or BeautifulSoup object (if kind == "raw") + HTML string (if kind == "sidebar") OR + BeautifulSoup object (if kind == "raw") """ - if startdepth is None: - startdepth = 1 if kind == "sidebar" else 0 - 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) - 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}" - ) - 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 navbar/sidebar links to sub-headers on the page + # Remove sidebar links to sub-headers on the page for li in soup.select("li"): # Remove if li.find("a"): @@ -262,47 +355,7 @@ def generate_nav_html( if "#" in href and href != "#": li.decompose() - # For navbar, generate only top-level links and add external links - if kind == "navbar": - links = soup("li") - - # Add CSS for bootstrap - for li in links: - li["class"].append("nav-item") - li.find("a")["class"].append("nav-link") - - # Convert to HTML so we can append external links - links_html = [ii.prettify() for ii in links] - - # Add external links - for external_link in context["theme_external_links"]: - links_html.append( - f""" - """ # noqa - ) - - # Wrap the final few header items in a "more" block - links_solo = links_html[:n_links_before_dropdown] - links_dropdown = links_html[n_links_before_dropdown:] - - out = "\n".join(links_solo) - if links_dropdown: - links_dropdown_html = "\n".join(links_dropdown) - out += f""" - - """ # noqa - - # For sidebar, we generate links starting at the second level of the active page - elif kind == "sidebar": + 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"] @@ -329,7 +382,7 @@ def generate_nav_html( # Add icons and labels for collapsible nested sections _add_collapse_checkboxes(soup) - # Open the navbar to the proper depth + # Open the sidebar navigation to the proper depth for ii in range(int(show_nav_level)): for checkbox in soup.select( f"li.toctree-l{ii} > input.toctree-checkbox" @@ -342,6 +395,7 @@ def generate_nav_html( return out + @lru_cache(maxsize=None) def generate_toc_html(kind="html"): """Return the within-page TOC links in HTML.""" @@ -406,10 +460,14 @@ def navbar_align_class(): ) return align_options[align] - context["generate_nav_html"] = generate_nav_html + context["generate_header_nav_html"] = generate_header_nav_html + context["generate_toctree_html"] = generate_toctree_html context["generate_toc_html"] = generate_toc_html context["navbar_align_class"] = navbar_align_class + # TODO: Deprecate after v0.12 + context["generate_nav_html"] = generate_nav_html + def _add_collapse_checkboxes(soup): """Add checkboxes to collapse children in a toctree.""" diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/navbar-nav.html b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/navbar-nav.html index 8e036c049..d4a41ed6a 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/navbar-nav.html +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/navbar-nav.html @@ -1,3 +1,3 @@ diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html index d99fa5e39..60ff5e48c 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html @@ -5,7 +5,7 @@ {# Create the sidebar links HTML here to re-use in a few places #} {# If we have no sidebar links, pop the links component from the sidebar list #} -{%- set sidebar_nav_html = generate_nav_html("sidebar", +{%- set sidebar_nav_html = generate_toctree_html("sidebar", show_nav_level=theme_show_nav_level|int, maxdepth=theme_navigation_depth|int, collapse=theme_collapse_navigation|tobool, diff --git a/tests/sites/sidebars/_templates_sidebar_level2/sidebar-nav-bs.html b/tests/sites/sidebars/_templates_sidebar_level2/sidebar-nav-bs.html index ca6673436..53937ec6c 100644 --- a/tests/sites/sidebars/_templates_sidebar_level2/sidebar-nav-bs.html +++ b/tests/sites/sidebars/_templates_sidebar_level2/sidebar-nav-bs.html @@ -2,9 +2,9 @@
{% if pagename.startswith("section1/subsection1") %} - {{ generate_nav_html("sidebar", startdepth=2, maxdepth=4, collapse=True, includehidden=True, titles_only=True) }} + {{ generate_toctree_html("sidebar", startdepth=2, maxdepth=4, collapse=True, includehidden=True, titles_only=True) }} {% else %} - {{ generate_nav_html("sidebar", maxdepth=4, collapse=True, includehidden=True, titles_only=True) }} + {{ generate_toctree_html("sidebar", maxdepth=4, collapse=True, includehidden=True, titles_only=True) }} {% endif %}
diff --git a/tests/sites/sidebars/_templates_single_sidebar/components/sidebar-nav-bs.html b/tests/sites/sidebars/_templates_single_sidebar/components/sidebar-nav-bs.html index 597d57772..b8c0367a5 100644 --- a/tests/sites/sidebars/_templates_single_sidebar/components/sidebar-nav-bs.html +++ b/tests/sites/sidebars/_templates_single_sidebar/components/sidebar-nav-bs.html @@ -1,6 +1,6 @@ diff --git a/tests/test_build/navbar_ix.html b/tests/test_build/navbar_ix.html index 806b1015f..728caf086 100644 --- a/tests/test_build/navbar_ix.html +++ b/tests/test_build/navbar_ix.html @@ -1,19 +1,19 @@