Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
akarimp committed Aug 10, 2023
1 parent b989acc commit e046fe0
Show file tree
Hide file tree
Showing 4 changed files with 466 additions and 0 deletions.
266 changes: 266 additions & 0 deletions docs/_templates/__init__.py
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,
}
78 changes: 78 additions & 0 deletions docs/_templates/_compile_translations.py
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()
Loading

0 comments on commit e046fe0

Please sign in to comment.