Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ include sphinx_book_theme/translations/README.md
recursive-include sphinx_book_theme/translations *.mo
exclude jsons
recursive-exclude sphinx_book_theme/translations *.json
recursive-include sphinx_book_theme *.eot
recursive-include sphinx_book_theme *.ttf
recursive-include sphinx_book_theme *.woff
recursive-include sphinx_book_theme *.woff2
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
"click~=7.1",
"docutils>=0.15,<0.17",
'importlib-resources>=3.0,<3.5; python_version < "3.7"',
"pydata-sphinx-theme~=0.7.2",
"sphinx-basic-ng",
"pyyaml",
"sphinx>=3,<5",
"sphinx-design",
],
extras_require={
"code_style": ["pre-commit~=2.7.0"],
Expand All @@ -49,7 +50,6 @@
"pandas",
"plotly",
"sphinx~=4.0", # Force Sphinx to be the latest version
"sphinx-design",
"sphinx-copybutton",
"sphinx-togglebutton>=0.2.1",
"sphinx-thebe",
Expand Down
304 changes: 303 additions & 1 deletion sphinx_book_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from sphinx.application import Sphinx
from sphinx.locale import get_translation
from sphinx.util import logging
from sphinx.environment.adapters.toctree import TocTree
from sphinx import addnodes

from .launch import add_hub_urls
from . import static as theme_static
Expand Down Expand Up @@ -135,6 +137,9 @@ def sbt_generate_nav_html(

return toctree.prettify()

# Ensure that the max TOC level is an integer
context["theme_show_toc_level"] = int(context.get("theme_show_toc_level", 1))

context["sbt_generate_nav_html"] = sbt_generate_nav_html

# Update the page title because HTML makes it into the page title occasionally
Expand Down Expand Up @@ -199,6 +204,302 @@ def sbt_generate_nav_html(
)


def _get_local_toctree_for(
self: TocTree, indexname: str, docname: str, builder, collapse: bool, **kwargs
):
"""Return the "local" TOC nodetree (relative to `indexname`)."""
# this is a copy of `TocTree.get_toctree_for`, but where the sphinx version
# always uses the "master" doctree:
# doctree = self.env.get_doctree(self.env.config.master_doc)
# we here use the `indexname` additional argument to be able to use a subset
# of the doctree (e.g. starting at a second level for the sidebar):
# doctree = app.env.tocs[indexname].deepcopy()

doctree = self.env.tocs[indexname].deepcopy()

toctrees = []
if "includehidden" not in kwargs:
kwargs["includehidden"] = True
if "maxdepth" not in kwargs or not kwargs["maxdepth"]:
kwargs["maxdepth"] = 0
else:
kwargs["maxdepth"] = int(kwargs["maxdepth"])
kwargs["collapse"] = collapse

for toctreenode in doctree.traverse(addnodes.toctree):
toctree = self.resolve(docname, builder, toctreenode, prune=True, **kwargs)
if toctree:
toctrees.append(toctree)
if not toctrees:
return None
result = toctrees[0]
for toctree in toctrees[1:]:
result.extend(toctree.children)
return result


def index_toctree(app, pagename: str, startdepth: int, collapse: bool = True, **kwargs):
"""
Returns the "local" (starting at `startdepth`) TOC tree containing the
current page, rendered as HTML bullet lists.

This is the equivalent of `context["toctree"](**kwargs)` in sphinx
templating, but using the startdepth-local instead of global TOC tree.
"""
# this is a variant of the function stored in `context["toctree"]`, which is
# defined as `lambda **kwargs: self._get_local_toctree(pagename, **kwargs)`
# with `self` being the HMTLBuilder and the `_get_local_toctree` basically
# returning:
# return self.render_partial(TocTree(self.env).get_toctree_for(
# pagename, self, collapse, **kwargs))['fragment']

if "includehidden" not in kwargs:
kwargs["includehidden"] = False
if kwargs.get("maxdepth") == "":
kwargs.pop("maxdepth")

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 emtpy 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
)
return app.builder.render_partial(toctree_element)["fragment"]


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, **kwargs):
"""
Return the navigation link structure in HTML. Arguments are passed
to Sphinx "toctree" function (context["toctree"] below).

We use beautifulsoup to add the right CSS classes / structure for bootstrap.

See https://www.sphinx-doc.org/en/master/templating.html#toctree.

Parameters
----------
kind : ["navbar", "sidebar", "raw"]
The kind of UI element this toctree is generated for.
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`).
kwargs: passed to the Sphinx `toctree` template function.

Returns
-------
HTML string (if kind in ["navbar", "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)

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
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 == "navbar":
# Add CSS for bootstrap
for li in soup("li"):
li["class"].append("nav-item")
li.find("a")["class"].append("nav-link")
# only select li items (not eg captions)
out = "\n".join([ii.prettify() for ii in soup.find_all("li")])

elif 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 icons and labels for collapsible nested sections
_add_collapse_checkboxes(soup)

out = soup.prettify()

elif kind == "raw":
out = soup

return out

def generate_toc_html(kind="html"):
"""Return the within-page TOC links in HTML."""

if "toc" not in context:
return ""

soup = bs(context["toc"], "html.parser")

# Add toc-hN + visible classes
def add_header_level_recursive(ul, level):
if ul is None:
return
if level <= (context["theme_show_toc_level"] + 1):
ul["class"] = ul.get("class", []) + ["visible"]
for li in ul("li", recursive=False):
li["class"] = li.get("class", []) + [f"toc-h{level}"]
add_header_level_recursive(li.find("ul", recursive=False), level + 1)

add_header_level_recursive(soup.find("ul"), 1)

# Add in CSS classes for bootstrap
for ul in soup("ul"):
ul["class"] = ul.get("class", []) + ["nav", "section-nav", "flex-column"]

for li in soup("li"):
li["class"] = li.get("class", []) + ["nav-item", "toc-entry"]
if li.find("a"):
a = li.find("a")
a["class"] = a.get("class", []) + ["nav-link"]

# If we only have one h1 header, assume it's a title
h1_headers = soup.select(".toc-h1")
if len(h1_headers) == 1:
title = h1_headers[0]
# If we have no sub-headers of a title then we won't have a TOC
if not title.select(".toc-h2"):
out = ""
else:
out = title.find("ul").prettify()
# Else treat the h1 headers as sections
else:
out = soup.prettify()

# Return the toctree object
if kind == "html":
return out
else:
return soup

def navbar_align_class():
"""Return the class that aligns the navbar based on config."""
align = context.get("theme_navbar_align", "content")
align_options = {
"content": ("col-lg-9", "mr-auto"),
"left": ("", "mr-auto"),
"right": ("", "ml-auto"),
}
if align not in align_options:
raise ValueError(
(
"Theme optione navbar_align must be one of"
f"{align_options.keys()}, got: {align}"
)
)
return align_options[align]

def generate_google_analytics_script(id):
"""Handle the two types of google analytics id."""
if id:
if "G-" in id:
script = f"""
<script
async
src='https://www.googletagmanager.com/gtag/js?id={id}'
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){{ dataLayer.push(arguments); }}
gtag('js', new Date());
gtag('config', '{id}');
</script>
"""
else:
script = f"""
<script
async
src='https://www.google-analytics.com/analytics.js'
></script>
<script>
window.ga = window.ga || function () {{
(ga.q = ga.q || []).push(arguments) }};
ga.l = +new Date;
ga('create', '{id}', 'auto');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
</script>
"""
soup = bs(script, "html.parser")
return soup
else:
return ""

context["generate_toc_html"] = generate_toc_html
context["generate_nav_html"] = generate_nav_html
context["generate_google_analytics_script"] = generate_google_analytics_script


def _add_collapse_checkboxes(soup):
# based on https://github.com/pradyunsg/furo

toctree_checkbox_count = 0

for element in soup.find_all("li", recursive=True):
# We check all "li" elements, to add a "current-page" to the correct li.
classes = element.get("class", [])

# Nothing more to do, unless this has "children"
if not element.find("ul"):
continue

# Add a class to indicate that this has children.
element["class"] = classes + ["has-children"]

# We're gonna add a checkbox.
toctree_checkbox_count += 1
checkbox_name = f"toctree-checkbox-{toctree_checkbox_count}"

# Add the "label" for the checkbox which will get filled.
if soup.new_tag is None:
continue
label = soup.new_tag("label", attrs={"for": checkbox_name})
label.append(soup.new_tag("i", attrs={"class": "fas fa-chevron-down"}))
element.insert(1, label)

# Add the checkbox that's used to store expanded/collapsed state.
checkbox = soup.new_tag(
"input",
attrs={
"type": "checkbox",
"class": ["toctree-checkbox"],
"id": checkbox_name,
"name": checkbox_name,
},
)
# if this has a "current" class, be expanded by default
# (by checking the checkbox)
if "current" in classes:
checkbox.attrs["checked"] = ""

element.insert(1, checkbox)


def update_thebe_config(app, env, docnames):
"""Update thebe configuration with SBT-specific values"""
theme_options = env.config.html_theme_options
Expand Down Expand Up @@ -274,11 +575,12 @@ def run(self):


def setup(app: Sphinx):
# app.setup_extension("sphinx_design")
app.connect("env-before-read-docs", update_thebe_config)

# Configuration for Juypter Book
app.connect("html-page-context", add_hub_urls)

app.connect("html-page-context", add_toctree_functions)
app.connect("builder-inited", add_static_paths)
app.connect("env-updated", update_all)

Expand Down
Loading