194 lines
8.9 KiB
Python
194 lines
8.9 KiB
Python
"""
|
|
This script intends to better integrate sphinx-gallery into pydata-sphinx-theme. In
|
|
particular, it moves the download links and badge links in the footer of each generated
|
|
example page into the secondary sidebar, then removes the footer and the top note
|
|
pointing to the footer.
|
|
|
|
The download links are for Python source code and Jupyter notebook respectively, and
|
|
the badge links are for JupyterLite and Binder.
|
|
|
|
Currently this is achieved via post-processing the HTML generated by sphinx-gallery.
|
|
This hack can be removed if the following upstream issue is resolved:
|
|
https://github.com/sphinx-gallery/sphinx-gallery/issues/1258
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from bs4 import BeautifulSoup
|
|
from sphinx.util.display import status_iterator
|
|
from sphinx.util.logging import getLogger
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
def move_gallery_links(app, exception):
|
|
if exception is not None:
|
|
return
|
|
|
|
for gallery_dir in app.config.sphinx_gallery_conf["gallery_dirs"]:
|
|
html_gallery_dir = Path(app.builder.outdir, gallery_dir)
|
|
|
|
# Get all gallery example files to be tweaked; tuples (file, docname)
|
|
flat = []
|
|
for file in html_gallery_dir.rglob("*.html"):
|
|
if file.name in ("index.html", "sg_execution_times.html"):
|
|
# These are not gallery example pages, skip
|
|
continue
|
|
|
|
# Extract the documentation name from the path
|
|
docname = file.relative_to(app.builder.outdir).with_suffix("").as_posix()
|
|
if docname in app.config.html_context["redirects"]:
|
|
# This is a redirected page, skip
|
|
continue
|
|
if docname not in app.project.docnames:
|
|
# This should not happen, warn
|
|
logger.warning(f"Document {docname} not found but {file} exists")
|
|
continue
|
|
flat.append((file, docname))
|
|
|
|
for html_file, _ in status_iterator(
|
|
flat,
|
|
length=len(flat),
|
|
summary="Tweaking gallery links... ",
|
|
verbosity=app.verbosity,
|
|
stringify_func=lambda x: x[1], # display docname
|
|
):
|
|
with html_file.open("r", encoding="utf-8") as f:
|
|
html = f.read()
|
|
soup = BeautifulSoup(html, "html.parser")
|
|
|
|
# Find the secondary sidebar; it should exist in all gallery example pages
|
|
secondary_sidebar = soup.find("div", class_="sidebar-secondary-items")
|
|
if secondary_sidebar is None:
|
|
logger.warning(f"Secondary sidebar not found in {html_file}")
|
|
continue
|
|
|
|
def _create_secondary_sidebar_component(items):
|
|
"""Create a new component in the secondary sidebar.
|
|
|
|
`items` should be a list of dictionaries with "element" being the bs4
|
|
tag of the component and "title" being the title (None if not needed).
|
|
"""
|
|
component = soup.new_tag("div", **{"class": "sidebar-secondary-item"})
|
|
for item in items:
|
|
item_wrapper = soup.new_tag("div")
|
|
item_wrapper.append(item["element"])
|
|
if item["title"]:
|
|
item_wrapper["title"] = item["title"]
|
|
component.append(item_wrapper)
|
|
secondary_sidebar.append(component)
|
|
|
|
def _create_download_link(link, is_jupyter=False):
|
|
"""Create a download link to be appended to a component.
|
|
|
|
`link` should be the bs4 tag of the original download link, either for
|
|
the Python source code (is_jupyter=False) of for the Jupyter notebook
|
|
(is_jupyter=True). `link` will not be removed; instead the whole
|
|
footnote would be removed where `link` is located.
|
|
|
|
This returns a dictionary with "element" being the bs4 tag of the new
|
|
download link and "title" being the name of the file to download.
|
|
"""
|
|
new_link = soup.new_tag("a", href=link["href"], download="")
|
|
|
|
# Place a download icon at the beginning of the new link
|
|
download_icon = soup.new_tag("i", **{"class": "fa-solid fa-download"})
|
|
new_link.append(download_icon)
|
|
|
|
# Create the text of the new link; it is shortend to fit better into
|
|
# the secondary sidebar. The leading space before "Download ..." is
|
|
# intentional to create a small gap between the icon and the text,
|
|
# being consistent with the other pydata-sphinx-theme components
|
|
link_type = "Jupyter notebook" if is_jupyter else "source code"
|
|
new_text = soup.new_string(f" Download {link_type}")
|
|
new_link.append(new_text)
|
|
|
|
# Get the file name to download and use it as the title of the new link
|
|
# which will show up when hovering over the link; the file name is
|
|
# expected to be in the last span of `link`
|
|
link_spans = link.find_all("span")
|
|
title = link_spans[-1].text if link_spans else None
|
|
|
|
return {"element": new_link, "title": title}
|
|
|
|
def _create_badge_link(link):
|
|
"""Create a badge link to be appended to a component.
|
|
|
|
`link` should be the bs4 tag of the original badge link, either for
|
|
binder or JupyterLite. `link` will not be removed; instead the whole
|
|
footnote would be removed where `link` is located.
|
|
|
|
This returns a dictionary with "element" being the bs4 tag of the new
|
|
download link and "title" being `None` (no need).
|
|
"""
|
|
new_link = soup.new_tag("a", href=link["href"])
|
|
|
|
# The link would essentially be an anchor wrapper outside the image of
|
|
# the badge; we get the src and alt attributes by finding the original
|
|
# image and limit the height to 20px (fixed) so that the secondary
|
|
# sidebar will appear neater
|
|
badge_img = link.find("img")
|
|
new_img = soup.new_tag(
|
|
"img", src=badge_img["src"], alt=badge_img["alt"], height=20
|
|
)
|
|
new_link.append(new_img)
|
|
|
|
return {"element": new_link, "title": None}
|
|
|
|
try:
|
|
# `sg_note` is the "go to the end" note at the top of the page
|
|
# `sg_footer` is the footer with the download links and badge links
|
|
# These will be removed at the end if new links are successfully created
|
|
sg_note = soup.find("div", class_="sphx-glr-download-link-note")
|
|
sg_footer = soup.find("div", class_="sphx-glr-footer")
|
|
|
|
# If any one of these two is not found, we directly give up tweaking
|
|
if sg_note is None or sg_footer is None:
|
|
continue
|
|
|
|
# Move the download links into the secondary sidebar
|
|
py_link_div = sg_footer.find("div", class_="sphx-glr-download-python")
|
|
ipy_link_div = sg_footer.find("div", class_="sphx-glr-download-jupyter")
|
|
_create_secondary_sidebar_component(
|
|
[
|
|
_create_download_link(py_link_div.a, is_jupyter=False),
|
|
_create_download_link(ipy_link_div.a, is_jupyter=True),
|
|
]
|
|
)
|
|
|
|
# Move the badge links into the secondary sidebar
|
|
lite_link_div = sg_footer.find("div", class_="lite-badge")
|
|
binder_link_div = sg_footer.find("div", class_="binder-badge")
|
|
_create_secondary_sidebar_component(
|
|
[
|
|
_create_badge_link(lite_link_div.a),
|
|
_create_badge_link(binder_link_div.a),
|
|
]
|
|
)
|
|
|
|
# Remove the sourcelink component from the secondary sidebar; the reason
|
|
# we do not remove it by configuration is that we need the secondary
|
|
# sidebar to be present for this script to work, while in-page toc alone
|
|
# could have been empty
|
|
sourcelink = secondary_sidebar.find("div", class_="sourcelink")
|
|
if sourcelink is not None:
|
|
sourcelink.parent.extract() # because sourcelink has a wrapper div
|
|
|
|
# Remove the the top note and the whole footer
|
|
sg_note.extract()
|
|
sg_footer.extract()
|
|
|
|
except Exception:
|
|
# If any step fails we directly skip the file
|
|
continue
|
|
|
|
# Write the modified file back
|
|
with html_file.open("w", encoding="utf-8") as f:
|
|
f.write(str(soup))
|
|
|
|
|
|
def setup(app):
|
|
# Default priority is 500 which sphinx-gallery uses for its build-finished events;
|
|
# we need a larger priority to run after sphinx-gallery (larger is later)
|
|
app.connect("build-finished", move_gallery_links, priority=900)
|