-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
466 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
"""A lightweight book theme based on the pydata sphinx theme.""" | ||
import hashlib | ||
import os | ||
from pathlib import Path | ||
from functools import lru_cache | ||
|
||
from docutils.parsers.rst.directives.body import Sidebar | ||
from docutils import nodes as docutil_nodes | ||
from sphinx.application import Sphinx | ||
from sphinx.locale import get_translation | ||
from sphinx.util import logging | ||
from pydata_sphinx_theme.utils import get_theme_options_dict | ||
|
||
from .nodes import SideNoteNode | ||
from .header_buttons import ( | ||
prep_header_buttons, | ||
add_header_buttons, | ||
update_sourcename, | ||
update_context_with_repository_info, | ||
) | ||
from .header_buttons.launch import add_launch_buttons | ||
from .header_buttons.source import add_source_buttons | ||
from ._transforms import HandleFootnoteTransform | ||
|
||
__version__ = "1.0.1" | ||
"""sphinx-book-theme version""" | ||
|
||
SPHINX_LOGGER = logging.getLogger(__name__) | ||
DEFAULT_LOG_TYPE = "sphinxbooktheme" | ||
MESSAGE_CATALOG_NAME = "booktheme" | ||
|
||
|
||
def get_html_theme_path(): | ||
"""Return list of HTML theme paths.""" | ||
parent = Path(__file__).parent.resolve() | ||
theme_path = parent / "theme" / "sphinx_book_theme" | ||
return theme_path | ||
|
||
|
||
def add_metadata_to_page(app, pagename, templatename, context, doctree): | ||
"""Adds some metadata about the page that we re-use later.""" | ||
# Add the site title to our context so it can be inserted into the navbar | ||
if not context.get("root_doc"): | ||
# TODO: Sphinx renamed master to root in 4.x, deprecate when we drop 3.x | ||
context["root_doc"] = context.get("master_doc") | ||
context["root_title"] = app.env.titles[context["root_doc"]].astext() | ||
|
||
# Update the page title because HTML makes it into the page title occasionally | ||
if pagename in app.env.titles: | ||
title = app.env.titles[pagename] | ||
context["pagetitle"] = title.astext() | ||
|
||
# Add a shortened page text to the context using the sections text | ||
if doctree: | ||
description = "" | ||
for section in doctree.traverse(docutil_nodes.section): | ||
description += section.astext().replace("\n", " ") | ||
description = description[:160] | ||
context["page_description"] = description | ||
|
||
# Add the author if it exists | ||
if app.config.author != "unknown": | ||
context["author"] = app.config.author | ||
|
||
# Translations | ||
translation = get_translation(MESSAGE_CATALOG_NAME) | ||
context["translate"] = translation | ||
|
||
# If search text hasn't been manually specified, use a shorter one here | ||
theme_options = get_theme_options_dict(app) | ||
if "search_bar_text" not in theme_options: | ||
context["theme_search_bar_text"] = translation("Search") + "..." | ||
|
||
|
||
@lru_cache(maxsize=None) | ||
def _gen_hash(path: str) -> str: | ||
return hashlib.sha1(path.read_bytes()).hexdigest() | ||
|
||
|
||
def hash_assets_for_files(assets: list, theme_static: Path, context): | ||
"""Generate a hash for assets, and append to its entry in context. | ||
assets: a list of assets to hash, each path should be relative to | ||
the theme's static folder. | ||
theme_static: a path to the theme's static folder. | ||
context: the Sphinx context object where asset links are stored. These are: | ||
`css_files` and `script_files` keys. | ||
""" | ||
for asset in assets: | ||
# CSS assets are stored in css_files, JS assets in script_files | ||
asset_type = "css_files" if asset.endswith(".css") else "script_files" | ||
if asset_type in context: | ||
# Define paths to the original asset file, and its linked file in Sphinx | ||
asset_sphinx_link = f"_static/{asset}" | ||
asset_source_path = theme_static / asset | ||
if not asset_source_path.exists(): | ||
SPHINX_LOGGER.warning( | ||
f"Asset {asset_source_path} does not exist, not linking." | ||
) | ||
# Find this asset in context, and update it to include the digest | ||
if asset_sphinx_link in context[asset_type]: | ||
hash = _gen_hash(asset_source_path) | ||
ix = context[asset_type].index(asset_sphinx_link) | ||
context[asset_type][ix] = asset_sphinx_link + "?digest=" + hash | ||
|
||
|
||
def hash_html_assets(app, pagename, templatename, context, doctree): | ||
"""Add ?digest={hash} to assets in order to bust cache when changes are made. | ||
The source files are in `static` while the built HTML is in `_static`. | ||
""" | ||
assets = ["scripts/sphinx-book-theme.js"] | ||
# Only append the book theme CSS if it's explicitly this theme. Sub-themes | ||
# will define their own CSS file, so if a sub-theme is used, this code is | ||
# run but the book theme CSS file won't be linked in Sphinx. | ||
if app.config.html_theme == "sphinx_book_theme": | ||
assets.append("styles/sphinx-book-theme.css") | ||
hash_assets_for_files(assets, get_html_theme_path() / "static", context) | ||
|
||
|
||
def update_mode_thebe_config(app): | ||
"""Update thebe configuration with SBT-specific values""" | ||
theme_options = get_theme_options_dict(app) | ||
if theme_options.get("launch_buttons", {}).get("thebe") is True: | ||
# In case somebody specifies they want thebe in a launch button | ||
# but has not activated the sphinx_thebe extension. | ||
if not hasattr(app.env.config, "thebe_config"): | ||
SPHINX_LOGGER.warning( | ||
( | ||
"Thebe is activated but not added to extensions list. " | ||
"Add `sphinx_thebe` to your site's extensions list." | ||
) | ||
) | ||
return | ||
# Will be empty if it doesn't exist | ||
thebe_config = app.env.config.thebe_config | ||
else: | ||
return | ||
|
||
if not theme_options.get("launch_buttons", {}).get("thebe"): | ||
return | ||
|
||
# Update the repository branch and URL | ||
# Assume that if there's already a thebe_config, then we don't want to over-ride | ||
if "repository_url" not in thebe_config: | ||
thebe_config["repository_url"] = theme_options.get("repository_url") | ||
if "repository_branch" not in thebe_config: | ||
branch = theme_options.get("repository_branch") | ||
if not branch: | ||
# Explicitly check in case branch is "" | ||
branch = "master" | ||
thebe_config["repository_branch"] = branch | ||
|
||
app.env.config.thebe_config = thebe_config | ||
|
||
|
||
def check_deprecation_keys(app): | ||
"""Warns about the deprecated keys.""" | ||
|
||
deprecated_config_list = ["single_page"] | ||
for key in deprecated_config_list: | ||
if key in get_theme_options_dict(app): | ||
SPHINX_LOGGER.warning( | ||
f"'{key}' was deprecated from version 0.3.4 onwards. See the CHANGELOG for more information: https://github.com/executablebooks/sphinx-book-theme/blob/master/CHANGELOG.md" # noqa: E501 | ||
f"[{DEFAULT_LOG_TYPE}]", | ||
type=DEFAULT_LOG_TYPE, | ||
) | ||
|
||
|
||
class Margin(Sidebar): | ||
"""Goes in the margin to the right of the page.""" | ||
|
||
optional_arguments = 1 | ||
required_arguments = 0 | ||
|
||
def run(self): | ||
"""Run the directive.""" | ||
if not self.arguments: | ||
self.arguments = [""] | ||
nodes = super().run() | ||
nodes[0].attributes["classes"].append("margin") | ||
|
||
# Remove the "title" node if it is empty | ||
if not self.arguments: | ||
nodes[0].children.pop(0) | ||
return nodes | ||
|
||
|
||
def update_general_config(app): | ||
theme_dir = get_html_theme_path() | ||
# Update templates for sidebar. Needed for jupyter-book builds as jb | ||
# uses an instance of Sphinx class from sphinx.application to build the app. | ||
# The __init__ function of which calls self.config.init_values() just | ||
# before emitting `config-inited` event. The init_values function overwrites | ||
# templates_path variable. | ||
app.config.templates_path.append(os.path.join(theme_dir, "components")) | ||
|
||
|
||
def update_templates(app, pagename, templatename, context, doctree): | ||
"""Update template names and assets for page build. | ||
This is a copy of what the pydata theme does here to include a new section | ||
- https://github.com/pydata/pydata-sphinx-theme/blob/0a4894fab49befc59eb497811949a1d0ede626eb/src/pydata_sphinx_theme/__init__.py#L173 # noqa: E501 | ||
""" | ||
# Allow for more flexibility in template names | ||
template_sections = ["theme_footer_content_items"] | ||
for section in template_sections: | ||
if context.get(section): | ||
# Break apart `,` separated strings so we can use , in the defaults | ||
if isinstance(context.get(section), str): | ||
context[section] = [ | ||
ii.strip() for ii in context.get(section).split(",") | ||
] | ||
|
||
# Add `.html` to templates with no suffix | ||
for ii, template in enumerate(context.get(section)): | ||
if not os.path.splitext(template)[1]: | ||
context[section][ii] = template + ".html" | ||
|
||
|
||
def setup(app: Sphinx): | ||
# Register theme | ||
theme_dir = get_html_theme_path() | ||
app.add_html_theme("sphinx_book_theme", theme_dir) | ||
app.add_js_file("scripts/sphinx-book-theme.js") | ||
|
||
# Translations | ||
locale_dir = os.path.join(theme_dir, "static", "locales") | ||
app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) | ||
|
||
# Events | ||
app.connect("builder-inited", update_mode_thebe_config) | ||
app.connect("builder-inited", check_deprecation_keys) | ||
app.connect("builder-inited", update_sourcename) | ||
app.connect("builder-inited", update_context_with_repository_info) | ||
app.connect("builder-inited", update_general_config) | ||
app.connect("html-page-context", add_metadata_to_page) | ||
app.connect("html-page-context", hash_html_assets) | ||
app.connect("html-page-context", update_templates) | ||
|
||
# Nodes | ||
SideNoteNode.add_node(app) | ||
|
||
# Header buttons | ||
app.connect("html-page-context", prep_header_buttons) | ||
# Bump priority so that it runs after the pydata theme sets up the edit URL func. | ||
app.connect("html-page-context", add_launch_buttons, priority=501) | ||
app.connect("html-page-context", add_source_buttons, priority=501) | ||
app.connect("html-page-context", add_header_buttons, priority=501) | ||
|
||
# Directives | ||
app.add_directive("margin", Margin) | ||
|
||
# Post-transforms | ||
app.add_post_transform(HandleFootnoteTransform) | ||
|
||
# Update templates for sidebar, for builds where config-inited is not called | ||
# (does not work in case of jupyter-book) | ||
app.config.templates_path.append(os.path.join(theme_dir, "components")) | ||
|
||
return { | ||
"parallel_read_safe": True, | ||
"parallel_write_safe": True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
"""Generate compiled static translation assets for Sphinx.""" | ||
import json | ||
import os | ||
from pathlib import Path | ||
import subprocess | ||
|
||
# In case the smodin.io code is different from the Sphinx code | ||
RENAME_LANGUAGE_CODES = { | ||
"zh-cn": "zh_CN", | ||
"zh-tw": "zh_TW", | ||
} | ||
|
||
|
||
def convert_json(folder=None): | ||
"""Convert JSON translations into .mo/.po files for Sphinx. | ||
folder: | ||
the source folder of the JSON translations. This function will put the | ||
compiled .mo/.po files in a specific folder relative to this source | ||
folder. This parameter is just provided to make testing easier. | ||
""" | ||
# Raw translation JSONs that are hand-edited | ||
folder = folder or Path(__file__).parent / "assets" / "translations" | ||
# Location of compiled static translation assets | ||
out_folder = folder / ".." / ".." / "theme" / "sphinx_book_theme" / "static" | ||
|
||
# compile po | ||
for path in (folder / "jsons").glob("*.json"): | ||
data = json.loads(path.read_text("utf8")) | ||
assert data[0]["symbol"] == "en" | ||
english = data[0]["text"] | ||
for item in data[1:]: | ||
language = item["symbol"] | ||
if language in RENAME_LANGUAGE_CODES: | ||
language = RENAME_LANGUAGE_CODES[language] | ||
out_path = ( | ||
out_folder | ||
/ "locales" | ||
/ language | ||
/ "LC_MESSAGES" | ||
/ "booktheme.po" # noqa: E501 | ||
) | ||
if not out_path.parent.exists(): | ||
out_path.parent.mkdir(parents=True) | ||
if not out_path.exists(): | ||
header = f""" | ||
msgid "" | ||
msgstr "" | ||
"Project-Id-Version: Sphinx-Book-Theme\\n" | ||
"MIME-Version: 1.0\\n" | ||
"Content-Type: text/plain; charset=UTF-8\\n" | ||
"Content-Transfer-Encoding: 8bit\\n" | ||
"Language: {language}\\n" | ||
"Plural-Forms: nplurals=2; plural=(n != 1);\\n" | ||
""" | ||
out_path.write_text(header) | ||
|
||
with out_path.open("a") as f: | ||
f.write("\n") | ||
f.write(f'msgid "{english}"\n') | ||
text = item["text"].replace('"', '\\"') | ||
f.write(f'msgstr "{text}"\n') | ||
|
||
# compile mo | ||
for path in (out_folder / "locales").glob("**/booktheme.po"): | ||
print(path) | ||
subprocess.check_call( | ||
[ | ||
"msgfmt", | ||
os.path.abspath(path), | ||
"-o", | ||
os.path.abspath(path.parent / "booktheme.mo"), | ||
] | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
print("[SBT]: Compiling translations") | ||
convert_json() |
Oops, something went wrong.