You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
4.4 KiB
168 lines
4.4 KiB
4 years ago
|
"""A clean customisable Sphinx documentation theme."""
|
||
|
|
||
|
__version__ = "2020.12.09.beta21"
|
||
|
|
||
|
import hashlib
|
||
|
from functools import lru_cache
|
||
|
from pathlib import Path
|
||
|
|
||
|
from bs4 import BeautifulSoup
|
||
|
from pygments.token import Text
|
||
|
|
||
|
from .navigation import get_navigation_tree
|
||
|
|
||
|
|
||
|
@lru_cache(maxsize=None)
|
||
|
def has_exactly_one_list_item(toc):
|
||
|
"""Check if the toc has exactly one list item."""
|
||
|
assert toc
|
||
|
|
||
|
soup = BeautifulSoup(toc, "html.parser")
|
||
|
if len(soup.find_all("li")) == 1:
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
|
||
|
def wrap_elements_that_can_get_too_wide(content):
|
||
|
"""Wrap the elements that could get too wide, with a div to allow controlling width.
|
||
|
|
||
|
- <table>
|
||
|
- [class=math]
|
||
|
|
||
|
"""
|
||
|
soup = BeautifulSoup(content, "html.parser")
|
||
|
|
||
|
for table in soup.find_all("table"):
|
||
|
table_wrapper = soup.new_tag("div", attrs={"class": "table-wrapper"})
|
||
|
table.replace_with(table_wrapper)
|
||
|
table_wrapper.append(table)
|
||
|
|
||
|
for math in soup.find_all("div", class_="math"):
|
||
|
wrapper = soup.new_tag("div", attrs={"class": "math-wrapper"})
|
||
|
math.replace_with(wrapper)
|
||
|
wrapper.append(math)
|
||
|
|
||
|
return str(soup)
|
||
|
|
||
|
|
||
|
def get_pygments_style_colors(style, *, fallbacks):
|
||
|
"""Get background/foreground colors for given pygments style."""
|
||
|
background = style.background_color
|
||
|
text_colors = style.style_for_token(Text)
|
||
|
foreground = text_colors["color"]
|
||
|
|
||
|
if not background:
|
||
|
background = fallbacks["background"]
|
||
|
|
||
|
if not foreground:
|
||
|
foreground = fallbacks["foreground"]
|
||
|
else:
|
||
|
foreground = f"#{foreground}"
|
||
|
|
||
|
return {"background": background, "foreground": foreground}
|
||
|
|
||
|
|
||
|
@lru_cache(maxsize=2)
|
||
|
def get_colors_for_codeblocks(highlighter, *, fg, bg):
|
||
|
"""Get background/foreground colors for given pygments style."""
|
||
|
return get_pygments_style_colors(
|
||
|
highlighter.formatter_args["style"],
|
||
|
fallbacks={
|
||
|
"foreground": fg,
|
||
|
"background": bg,
|
||
|
},
|
||
|
)
|
||
|
|
||
|
|
||
|
def _compute_navigation_tree(context):
|
||
|
# The navigation tree, generated from the sphinx-provided ToC tree.
|
||
|
if "toctree" in context:
|
||
|
toctree = context["toctree"]
|
||
|
toctree_html = toctree(
|
||
|
collapse=False,
|
||
|
titles_only=True,
|
||
|
maxdepth=-1,
|
||
|
includehidden=True,
|
||
|
)
|
||
|
else:
|
||
|
toctree_html = ""
|
||
|
|
||
|
return get_navigation_tree(toctree_html)
|
||
|
|
||
|
|
||
|
def _compute_hide_toc(context):
|
||
|
# Should the table of contents be hidden?
|
||
|
file_meta = context.get("meta", None) or {}
|
||
|
if "hide-toc" in file_meta:
|
||
|
return True
|
||
|
elif "toc" not in context:
|
||
|
return True
|
||
|
elif not context["toc"]:
|
||
|
return True
|
||
|
|
||
|
return has_exactly_one_list_item(context["toc"])
|
||
|
|
||
|
|
||
|
@lru_cache(maxsize=None)
|
||
|
def furo_asset_hash(path):
|
||
|
"""Append a `?digest=` to an url based on the file content."""
|
||
|
_static = "_static/"
|
||
|
if _static not in path:
|
||
|
raise ValueError("furo_asset_hash expect a path with '_static' in it")
|
||
|
|
||
|
partial_path = path[path.find(_static) + len(_static) :]
|
||
|
|
||
|
file_path = Path(__file__).parent / "theme/static" / partial_path
|
||
|
with open(file_path, "rb") as f:
|
||
|
digest = hashlib.sha1(f.read()).hexdigest()
|
||
|
|
||
|
return path + f"?digest={digest}"
|
||
|
|
||
|
|
||
|
def _html_page_context(app, pagename, templatename, context, doctree):
|
||
|
if app.config.html_theme != "furo":
|
||
|
return
|
||
|
|
||
|
# Basic constants
|
||
|
context["furo_version"] = __version__
|
||
|
context["furo_asset_hash"] = furo_asset_hash
|
||
|
|
||
|
# Values computed from page-level context.
|
||
|
context["furo_navigation_tree"] = _compute_navigation_tree(context)
|
||
|
context["furo_hide_toc"] = _compute_hide_toc(context)
|
||
|
|
||
|
# Inject information about styles
|
||
|
context["furo_pygments"] = {
|
||
|
"light": get_colors_for_codeblocks(
|
||
|
app.builder.highlighter,
|
||
|
fg="black",
|
||
|
bg="white",
|
||
|
),
|
||
|
"dark": get_colors_for_codeblocks(
|
||
|
app.builder.dark_highlighter,
|
||
|
fg="white",
|
||
|
bg="black",
|
||
|
),
|
||
|
}
|
||
|
|
||
|
# Patch the content
|
||
|
if "body" in context:
|
||
|
context["body"] = wrap_elements_that_can_get_too_wide(context["body"])
|
||
|
|
||
|
|
||
|
def setup(app):
|
||
|
"""Entry point for sphinx theming."""
|
||
|
app.require_sphinx("3.0")
|
||
|
|
||
|
theme_path = (Path(__file__).parent / "theme").resolve()
|
||
|
app.add_html_theme("furo", str(theme_path))
|
||
|
|
||
|
app.connect("html-page-context", _html_page_context)
|
||
|
|
||
|
return {
|
||
|
"parallel_read_safe": True,
|
||
|
"parallel_write_safe": True,
|
||
|
"version": __version__,
|
||
|
}
|