Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4a284c2

Browse files
committed
Add language and version switchers
1 parent f1ba0f8 commit 4a284c2

File tree

4 files changed

+221
-3
lines changed

4 files changed

+221
-3
lines changed

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ classifiers = [
2929
"Topic :: Documentation",
3030
"Topic :: Software Development :: Documentation",
3131
]
32+
dependencies = [
33+
"httpx>=0.25",
34+
'tomli>=2; python_version < "3.11"',
35+
]
3236
urls.Code = "https://github.com/python/python-docs-theme"
3337
urls.Download = "https://pypi.org/project/python-docs-theme/"
3438
urls.Homepage = "https://github.com/python/python-docs-theme/"

python_docs_theme/__init__.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,85 @@
22

33
import hashlib
44
import os
5+
import sys
56
from functools import lru_cache
67
from pathlib import Path
7-
from typing import Any
8+
from typing import Any, Literal
89

10+
import httpx
911
import sphinx.application
1012
from sphinx.builders.html import StandaloneHTMLBuilder
1113

14+
if sys.version_info[:2] >= (3, 11):
15+
import tomllib
16+
else:
17+
import tomli as tomllib
18+
1219
THEME_PATH = Path(__file__).parent.resolve()
1320

1421

22+
def _version_label(
23+
version_name: str,
24+
status: Literal["feature", "prerelease", "bugfix", "security", "end-of-life"],
25+
) -> str:
26+
if status == "feature":
27+
return f"dev ({version_name})"
28+
if status == "prerelease":
29+
return f"pre ({version_name})"
30+
if status in {"end-of-life", "security", "bugfix"}:
31+
return version_name
32+
msg = f"Unknown status: {status}"
33+
raise ValueError(msg)
34+
35+
36+
def _builder_inited(app):
37+
html_context = app.config.html_context
38+
language = app.config.language
39+
release = app.config.release
40+
if app.config.html_theme != "python_docs_theme":
41+
return
42+
43+
# Get the current branch statuses
44+
releases = httpx.get(
45+
"https://raw.githubusercontent.com/python/devguide/main/include/release-cycle.json",
46+
timeout=30,
47+
).json()
48+
# Get appropriate version labels
49+
release_labels = {
50+
name: _version_label(name, release["status"])
51+
for name, release in releases.items()
52+
}
53+
# Update the current version to be the full release string
54+
if (short_version := ".".join(release.split(".", 2)[:2])) in release_labels:
55+
release_labels[short_version] = release
56+
57+
# Store the versions in the context as a sorted list of tuples
58+
html_context["switchers_versions"] = sorted(
59+
release_labels.items(),
60+
key=lambda release_label: tuple(map(int, release_label[0].split("."))),
61+
reverse=True,
62+
)
63+
64+
# Get the languages from the docsbuild-scripts config
65+
docsbuild_config = httpx.get(
66+
"https://raw.githubusercontent.com/python/docsbuild-scripts/main/config.toml",
67+
timeout=30,
68+
).text
69+
# Convert language tags and extract language names
70+
languages = [
71+
(iso639_tag.replace("_", "-").lower(), section["name"])
72+
for iso639_tag, section in tomllib.loads(docsbuild_config)["languages"].items()
73+
if section.get("in_prod", True)
74+
]
75+
76+
# If we are working on a language that is not in the list, add it
77+
if language and language not in dict(languages):
78+
languages.append((language, language))
79+
80+
# Store the versions in the context as a sorted list of tuples
81+
html_context["switchers_languages"] = sorted(languages)
82+
83+
1584
@lru_cache(maxsize=None)
1685
def _asset_hash(path: str) -> str:
1786
"""Append a `?digest=` to an url based on the file content."""
@@ -56,6 +125,7 @@ def setup(app):
56125
current_dir = os.path.abspath(os.path.dirname(__file__))
57126
app.add_html_theme("python_docs_theme", current_dir)
58127

128+
app.connect("builder-inited", _builder_inited)
59129
app.connect("html-page-context", _html_page_context)
60130

61131
return {

python_docs_theme/layout.html

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,20 @@ <h3>{{ _('Navigation') }}</h3>
1717
<li><img src="{{ pathto('_static/' ~ theme_root_icon, 1) }}" alt="{{ theme_root_icon_alt_text }}" style="vertical-align: middle; margin-top: -1px"/></li>
1818
<li><a href="{{theme_root_url}}">{{theme_root_name}}</a>{{ reldelim1 }}</li>
1919
<li class="switchers">
20-
<div class="language_switcher_placeholder"></div>
21-
<div class="version_switcher_placeholder"></div>
20+
<div class="language_switcher_placeholder">{% if switchers_languages %}
21+
<select class="language">
22+
{% for lang_code, lang_name in switchers_languages -%}
23+
<option value="{{ lang_code }}" {%- if lang_code == language %} selected="true" {%- endif %}>{{ lang_name }}</option>
24+
{% endfor -%}
25+
</select>
26+
{% endif -%}</div>
27+
<div class="version_switcher_placeholder">{% if switchers_versions %}
28+
<select class="version-select">
29+
{% for (version_name, version_title) in switchers_versions -%}
30+
<option value="{{ version_name }}" {%- if version_title == release %} selected="true" {%- endif %}>{{ version_title }}</option>
31+
{% endfor -%}
32+
</select>
33+
{% endif %}</div>
2234
</li>
2335
<li>
2436
{% if theme_root_include_title %}
@@ -74,6 +86,7 @@ <h3>{{ _('Navigation') }}</h3>
7486
<link rel="shortcut icon" type="image/png" href="{{ pathto('_static/' ~ theme_root_icon, 1) }}" />
7587
{%- if builder != "htmlhelp" %}
7688
{%- if not embedded %}
89+
<script type="text/javascript" src="{{ pathto('_static/switchers.js', 1) }}"></script>
7790
<script type="text/javascript" src="{{ pathto('_static/copybutton.js', 1) }}"></script>
7891
<script type="text/javascript" src="{{ pathto('_static/menu.js', 1) }}"></script>
7992
<script type="text/javascript" src="{{ pathto('_static/search-focus.js', 1) }}"></script>

python_docs_theme/static/switchers.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict';
2+
3+
const _is_file_uri = (uri) => uri.startsWith('file://');
4+
5+
const _IS_LOCAL = _is_file_uri(window.location.href);
6+
const _CONTENT_ROOT = document.documentElement.dataset.content_root;
7+
const _CURRENT_PREFIX = _IS_LOCAL
8+
? null
9+
: new URL(_CONTENT_ROOT, window.location).pathname;
10+
const _CURRENT_RELEASE = DOCUMENTATION_OPTIONS.VERSION || '';
11+
const _CURRENT_VERSION = _CURRENT_RELEASE.split('.').slice(0, 2).join('.');
12+
const _CURRENT_LANGUAGE = DOCUMENTATION_OPTIONS.LANGUAGE?.toLowerCase() || 'en';
13+
14+
/**
15+
* Change the current page to the first existing URL in the list.
16+
* @param {Array<string>} urls
17+
* @private
18+
*/
19+
const _navigate_to_first_existing = async (urls) => {
20+
// Navigate to the first existing URL of urls.
21+
for (const url of urls) {
22+
try {
23+
const response = await fetch(url, { method: 'GET' })
24+
if (response.ok) {
25+
window.location.href = url;
26+
return url; // Avoid race conditions with multiple redirects
27+
}
28+
} catch(err) {
29+
console.error(`Error in: ${url}`);
30+
console.error(err)
31+
}
32+
}
33+
34+
// if all else fails, redirect to the d.p.o root
35+
window.location.href = '/';
36+
};
37+
38+
/**
39+
* Navigate to the selected version.
40+
* @param {Event} event
41+
* @returns {Promise<void>}
42+
*/
43+
const on_version_switch = async (event) => {
44+
if (_IS_LOCAL) return;
45+
46+
const selected_version = event.target.value;
47+
// Special 'default' case for English.
48+
const new_prefix =
49+
_CURRENT_LANGUAGE === 'en'
50+
? `/${selected_version}/`
51+
: `/${_CURRENT_LANGUAGE}/${selected_version}/`;
52+
const new_prefix_en = `/${selected_version}/`;
53+
if (_CURRENT_PREFIX !== new_prefix) {
54+
// Try the following pages in order:
55+
// 1. The current page in the current language with the new version
56+
// 2. The current page in English with the new version
57+
// 3. The documentation home in the current language with the new version
58+
// 4. The documentation home in English with the new version
59+
await _navigate_to_first_existing([
60+
window.location.href.replace(_CURRENT_PREFIX, new_prefix),
61+
window.location.href.replace(_CURRENT_PREFIX, new_prefix_en),
62+
new_prefix,
63+
new_prefix_en,
64+
]);
65+
}
66+
};
67+
68+
/**
69+
* Navigate to the selected language.
70+
* @param {Event} event
71+
* @returns {Promise<void>}
72+
*/
73+
const on_language_switch = async (event) => {
74+
if (_IS_LOCAL) return;
75+
76+
const selected_language = event.target.value;
77+
// Special 'default' case for English.
78+
const new_prefix =
79+
selected_language === 'en'
80+
? `/${_CURRENT_VERSION}/`
81+
: `/${selected_language}/${_CURRENT_VERSION}/`;
82+
if (_CURRENT_PREFIX !== new_prefix) {
83+
// Try the following pages in order:
84+
// 1. The current page in the new language with the current version
85+
// 2. The documentation home in the new language with the current version
86+
await _navigate_to_first_existing([
87+
window.location.href.replace(_CURRENT_PREFIX, new_prefix),
88+
new_prefix,
89+
]);
90+
}
91+
};
92+
93+
/**
94+
* Set up the version and language switchers.
95+
* @returns {Promise<void>}
96+
*/
97+
const initialise_switchers = async () => {
98+
try {
99+
// Update the version select elements
100+
document
101+
.querySelectorAll('.version_switcher_placeholder select')
102+
.forEach((select) => {
103+
if (_IS_LOCAL) {
104+
select.disabled = true;
105+
select.title = 'Version switching is disabled in local builds';
106+
}
107+
select.addEventListener('change', on_version_switch);
108+
select.parentElement.classList.remove('version_switcher_placeholder');
109+
});
110+
111+
// Update the language select elements
112+
document
113+
.querySelectorAll('.language_switcher_placeholder select')
114+
.forEach((select) => {
115+
if (_IS_LOCAL) {
116+
select.disabled = true;
117+
select.title = 'Language switching is disabled in local builds';
118+
}
119+
select.addEventListener('change', on_language_switch);
120+
select.parentElement.classList.remove('language_switcher_placeholder');
121+
});
122+
} catch (error) {
123+
console.error(error);
124+
}
125+
};
126+
127+
if (document.readyState !== 'loading') {
128+
initialise_switchers();
129+
} else {
130+
document.addEventListener('DOMContentLoaded', initialise_switchers);
131+
}

0 commit comments

Comments
 (0)