Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 0 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,3 @@ Other sites that are using this theme:

demo/index
changelog

2 changes: 1 addition & 1 deletion docs/user_guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ User Guide

install
configuring
customizing
customizing
211 changes: 101 additions & 110 deletions pydata_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,121 @@
import os

from sphinx.errors import ExtensionError
from bs4 import BeautifulSoup as bs

from .bootstrap_html_translator import BootstrapHTML5Translator
import docutils

__version__ = "0.4.2dev0"


def add_toctree_functions(app, pagename, templatename, context, doctree):
"""Add functions so Jinja templates can add toctree objects.
"""Add functions so Jinja templates can add toctree objects."""

This converts the docutils nodes into a nested dictionary that Jinja can
use in our templating.
"""
from sphinx.environment.adapters.toctree import TocTree
def get_nav_object(kind, **kwargs):
"""Return the navigation link structure in HTML. Arguments are passed
to Sphinx "toctree" function (context["toctree"] below).

def get_nav_object(maxdepth=None, collapse=True, **kwargs):
"""Return a list of nav links that can be accessed from Jinja.
We use beautifulsoup to add the right CSS classes / structure for bootstrap.

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

Parameters
----------
maxdepth: int
How many layers of TocTree will be returned
collapse: bool
Whether to only include sub-pages of the currently-active page,
instead of sub-pages of all top-level pages of the site.
kwargs: key/val pairs
Passed to the `TocTree.get_toctree_for` Sphinx method

kind : ["navbar", "sidebar", "raw"]
The kind of UI element this toctree is generated for.
kwargs: passed to the Sphinx `toctree` template function.

Returns
-------

nav_object : HTML string (if kind in ["navbar", "sidebar"]) or BeautifulSoup
object (if kind == "raw")
"""
# The TocTree will contain the full site TocTree including sub-pages.
# "collapse=True" collapses sub-pages of non-active TOC pages.
# maxdepth controls how many TOC levels are returned
toctree = TocTree(app.env).get_toctree_for(
pagename, app.builder, collapse=collapse, maxdepth=maxdepth, **kwargs
)
# If no toctree is defined (AKA a single-page site), skip this
if toctree is None:
return []

# toctree has this structure
# <caption>
# <bullet_list>
# <list_item classes="toctree-l1">
# <list_item classes="toctree-l1">
# `list_item`s are the actual TOC links and are the only thing we want
toc_items = [
item
for child in toctree.children
for item in child
if isinstance(item, docutils.nodes.list_item)
]

# Now convert our docutils nodes into dicts that Jinja can use
nav = [docutils_node_to_jinja(child, only_pages=True) for child in toc_items]

return nav

def get_page_toc_object():
"""Return a list of within-page TOC links that can be accessed from Jinja."""
self_toc = TocTree(app.env).get_toc_for(pagename, app.builder)

try:
# If there's only one child, assume we have a single "title" as top header
# so start the TOC at the first item's children (AKA, level 2 headers)
if len(self_toc.children) == 1:
nav = docutils_node_to_jinja(self_toc.children[0]).get("children", [])
toc_sphinx = context["toctree"](**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")

if kind == "navbar":
# Add CSS for bootstrap
for li in soup("li"):
li["class"].append("nav-item")
li.find("a")["class"].append("nav-link")
out = "\n".join([ii.prettify() for ii in soup.find_all("li")])

elif kind == "sidebar":
# Remove sidebar links to sub-headers on the page
for li in soup.select("li.current ul li"):
# Remove
if li.find("a"):
href = li.find("a")["href"]
if "#" in href and href != "#":
li.decompose()

# Join all the top-level `li`s together for display
current_lis = soup.select("li.current.toctree-l1 li.toctree-l2")
out = "\n".join([ii.prettify() for ii in current_lis])

elif kind == "raw":
out = soup

return out

def get_page_toc_object(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 classes
def add_header_level_recursive(ul, level):
for li in ul("li", recursive=False):
li["class"] = li.get("class", []) + [f"toc-h{level}"]
ul = li.find("ul", recursive=False)
if ul:
add_header_level_recursive(ul, 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 i in range(1, context["theme_show_toc_level"] + 1):
for li in soup.select("li.toc-h{}".format(i + 1)):
ul = li.find_parent("ul")
classes = ul.get("class", [])
if "visible" not in classes:
ul["class"] = classes + ["visible"]
Copy link
Member

@jorisvandenbossche jorisvandenbossche Dec 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@choldgraf this is "a" way that I found to get show_toc_level working, but I find it kind of hacky ..

The problem with the basic

        for ul in soup("ul"):
            ul["class"] = ...

loop above this loop is that this is a flat iterator, and so you don't know how "deep" you are for each ul.

This "workaround" uses the toc-hN class names we added above, to find the appropriate levels of ul for which to add the class. I didn't directly find a way with beautifulsoup to only search a certain number of levels down.

Copy link
Collaborator Author

@choldgraf choldgraf Dec 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems reasonable to me :-), but maybe we should add a comment to explain your reasoning here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure why I missed that earlier. But of course the code that adds those toc-hN classes (the classes that I was then using here to navigate the soup as a workaround), is already recursively iterating through the lists keeping track of the level ... :)
So simplified this


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

# Keep only the sub-sections of the title (so no title is shown)
title = soup.find("a", attrs={"href": "#"})
if title:
title = title.parent
# Only show if children of the title item exist
if title.select("ul li"):
out = title.find("ul").prettify()
else:
nav = [docutils_node_to_jinja(item) for item in self_toc.children]
return nav
except Exception:
return {}
out = ""
else:
out = ""

# 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."""
Expand All @@ -97,60 +142,6 @@ def navbar_align_class():
context["navbar_align_class"] = navbar_align_class


def docutils_node_to_jinja(list_item, only_pages=False):
"""Convert a docutils node to a structure that can be read by Jinja.

Parameters
----------
list_item : docutils list_item node
A parent item, potentially with children, corresponding to the level
of a TocTree.
only_pages : bool
Only include items for full pages in the output dictionary. Exclude
anchor links (TOC items with a URL that starts with #)

Returns
-------
nav : dict
The TocTree, converted into a dictionary with key/values that work
within Jinja.
"""
if not list_item.children:
return None

# We assume this structure of a list item:
# <list_item>
# <compact_paragraph >
# <reference> <-- the thing we want
reference = list_item.children[0].children[0]
title = reference.astext()
url = reference.attributes["refuri"]
active = "current" in list_item.attributes["classes"]

# If we've got an anchor link, skip it if we wish
if only_pages and "#" in url:
return None

# Converting the docutils attributes into jinja-friendly objects
nav = {}
nav["title"] = title
nav["url"] = url
nav["active"] = active

# Recursively convert children as well
# If there are sub-pages for this list_item, there should be two children:
# a paragraph, and a bullet_list.
nav["children"] = []
if len(list_item.children) > 1:
# The `.children` of the bullet_list has the nodes of the sub-pages.
subpage_list = list_item.children[1].children
for sub_page in subpage_list:
child_nav = docutils_node_to_jinja(sub_page, only_pages=only_pages)
if child_nav is not None:
nav["children"].append(child_nav)
return nav


# -----------------------------------------------------------------------------


Expand Down
35 changes: 5 additions & 30 deletions pydata_sphinx_theme/_templates/sidebar-nav-bs.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,7 @@
<nav class="bd-links" id="bd-docs-nav" aria-label="Main navigation">

<div class="bd-toc-item active">
{% set nav = get_nav_object(maxdepth=3, collapse=True) %}

<div class="bd-toc-item active">
<ul class="nav bd-sidenav">
{% for main_nav_item in nav %}
{% if main_nav_item.active %}
{% for nav_item in main_nav_item.children %}
{% if nav_item.children %}

<li class="{% if nav_item.active%}active{% endif %}">
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<ul>
{% for nav_item in nav_item.children %}
<li class="{% if nav_item.active%}active{% endif %}">
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li class="{% if nav_item.active%}active{% endif %}">
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
</ul>

</nav>
{{ get_nav_object("sidebar", maxdepth=4, collapse=True, includehidden=True, titles_only=True) }}
</ul>
</div>
</nav>
8 changes: 2 additions & 6 deletions pydata_sphinx_theme/docs-navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,11 @@
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

{% set navbar_class, navbar_align = navbar_align_class() %}
<div id="navbar-menu" class="{{ navbar_class }} collapse navbar-collapse">
<ul id="navbar-main-elements" class="navbar-nav {{ navbar_align }}">
{% set nav = get_nav_object(maxdepth=1, collapse=True) %}
{% for main_nav_item in nav %}
<li class="nav-item {% if main_nav_item.active%}active{% endif %}">
<a class="nav-link" href="{{ main_nav_item.url }}">{{ main_nav_item.title }}</a>
</li>
{% endfor %}
{{ get_nav_object("navbar", maxdepth=1, collapse=True, includehidden=True, titles_only=True) }}
{% for external_link in theme_external_links %}
<li class="nav-item">
<a class="nav-link nav-external" href="{{ external_link.url }}">{{ external_link.name }}<i class="fas fa-external-link-alt"></i></a>
Expand Down
13 changes: 1 addition & 12 deletions pydata_sphinx_theme/docs-toc.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,7 @@
{%- endif %}

<nav id="bd-toc-nav">
<ul class="nav section-nav flex-column">
{% for item in page_toc recursive %}
<li class="nav-item toc-entry toc-h{{ loop.depth + 1 }}">
<a href="{{ item.url }}" class="nav-link">{{ item.title }}</a>
{%- if item.children -%}
<ul class="nav section-nav flex-column{% if (loop.depth + 1) <= theme_show_toc_level %} visible{% endif %}">
{{ loop(item.children) }}
</ul>
{%- endif %}
</li>
{% endfor %}
</ul>
{{ page_toc }}
</nav>

{% include "edit_this_page.html" %}

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pydata_sphinx_theme/static/webpack-macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@

{% macro head_pre_bootstrap() %}
<link href="{{ pathto('_static/css/theme.css', 1) }}" rel="stylesheet" />
<link href="{{ pathto('_static/css/index.4eb26a98c046fa117c2b75e31c15adbb.css', 1) }}" rel="stylesheet" />
<link href="{{ pathto('_static/css/index.c3e2c6722ba453f25ccfb016116cf7cd.css', 1) }}" rel="stylesheet" />
{% endmacro %}

{% macro head_js_preload() %}
<link rel="preload" as="script" href="{{ pathto('_static/js/index.3edd59071b60732de7f5.js', 1) }}">
<link rel="preload" as="script" href="{{ pathto('_static/js/index.67ef65a5ee5d0ac23bf5.js', 1) }}">
{% endmacro %}

{% macro body_post() %}
<script src="{{ pathto('_static/js/index.3edd59071b60732de7f5.js', 1) }}"></script>
<script src="{{ pathto('_static/js/index.67ef65a5ee5d0ac23bf5.js', 1) }}"></script>
{% endmacro %}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def find_version(*file_paths):
include_package_data=True,
# See http://www.sphinx-doc.org/en/stable/theming.html#distribute-your-theme-as-a-python-package
entry_points={"sphinx.html_themes": ["pydata_sphinx_theme = pydata_sphinx_theme"]},
install_requires=["sphinx"],
install_requires=["sphinx", "beautifulsoup4"],
python_requires=">=3.5",
classifiers=[
"Development Status :: 4 - Beta",
Expand Down
Loading