-
Notifications
You must be signed in to change notification settings - Fork 208
Expand file tree
/
Copy path__init__.py
More file actions
258 lines (209 loc) · 10.1 KB
/
__init__.py
File metadata and controls
258 lines (209 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""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 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 .directives import Margin
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 ._compat import findall
from ._transforms import HandleFootnoteTransform
__version__ = "1.3.0.dev"
"""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 reuse 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 findall(doctree, 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, app):
"""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_path in assets:
# CSS assets are stored in css_files, JS assets in script_files
asset_type = "css_files" if asset_path.endswith(".css") else "script_files"
if asset_type in context:
# Define paths to the original asset file, and its linked file in Sphinx
asset_source_path = theme_static / asset_path
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
for ii, other_asset in enumerate(context[asset_type]):
# TODO: eventually the contents of context['css_files'] etc should probably
# only be _CascadingStyleSheet etc. For now, assume mixed with strings.
if getattr(other_asset, "filename", str(other_asset)) != asset_path:
continue
# Take priority from existing asset or use default priority (500)
priority = getattr(other_asset, "priority", 500)
# Remove existing asset
del context[asset_type][ii]
# Add new asset
app.add_css_file(
asset_path,
digest=_gen_hash(asset_source_path),
priority=priority,
)
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, app)
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,
)
def update_general_config(app, config):
theme_dir = get_html_theme_path()
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("html-page-context", add_metadata_to_page)
app.connect("html-page-context", hash_html_assets)
app.connect("html-page-context", update_templates)
# This extension has both theme-like and extension-like features.
# Themes are initialised immediately before use, thus we cannot
# rely on an event to set the config - the theme config must be
# set in setup(app):
update_general_config(app, app.config)
# Meanwhile, extensions are initialised _first_, and any config
# values set during setup() will be overwritten. We must therefore
# register the `config-inited` event to set these config options
app.connect("config-inited", update_general_config)
# 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)
return {
"parallel_read_safe": True,
"parallel_write_safe": True,
}