Skip to content

Commit

Permalink
Add language and version switchers
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner committed Jul 16, 2024
1 parent f1ba0f8 commit 4a284c2
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 3 deletions.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ classifiers = [
"Topic :: Documentation",
"Topic :: Software Development :: Documentation",
]
dependencies = [
"httpx>=0.25",
'tomli>=2; python_version < "3.11"',
]
urls.Code = "https://github.com/python/python-docs-theme"
urls.Download = "https://pypi.org/project/python-docs-theme/"
urls.Homepage = "https://github.com/python/python-docs-theme/"
Expand Down
72 changes: 71 additions & 1 deletion python_docs_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,85 @@

import hashlib
import os
import sys
from functools import lru_cache
from pathlib import Path
from typing import Any
from typing import Any, Literal

import httpx
import sphinx.application
from sphinx.builders.html import StandaloneHTMLBuilder

if sys.version_info[:2] >= (3, 11):
import tomllib
else:
import tomli as tomllib

THEME_PATH = Path(__file__).parent.resolve()


def _version_label(
version_name: str,
status: Literal["feature", "prerelease", "bugfix", "security", "end-of-life"],
) -> str:
if status == "feature":
return f"dev ({version_name})"
if status == "prerelease":
return f"pre ({version_name})"
if status in {"end-of-life", "security", "bugfix"}:
return version_name
msg = f"Unknown status: {status}"
raise ValueError(msg)


def _builder_inited(app):
html_context = app.config.html_context
language = app.config.language
release = app.config.release
if app.config.html_theme != "python_docs_theme":
return

# Get the current branch statuses
releases = httpx.get(
"https://raw.githubusercontent.com/python/devguide/main/include/release-cycle.json",
timeout=30,
).json()
# Get appropriate version labels
release_labels = {
name: _version_label(name, release["status"])
for name, release in releases.items()
}
# Update the current version to be the full release string
if (short_version := ".".join(release.split(".", 2)[:2])) in release_labels:
release_labels[short_version] = release

# Store the versions in the context as a sorted list of tuples
html_context["switchers_versions"] = sorted(
release_labels.items(),
key=lambda release_label: tuple(map(int, release_label[0].split("."))),
reverse=True,
)

# Get the languages from the docsbuild-scripts config
docsbuild_config = httpx.get(
"https://raw.githubusercontent.com/python/docsbuild-scripts/main/config.toml",
timeout=30,
).text
# Convert language tags and extract language names
languages = [
(iso639_tag.replace("_", "-").lower(), section["name"])
for iso639_tag, section in tomllib.loads(docsbuild_config)["languages"].items()
if section.get("in_prod", True)
]

# If we are working on a language that is not in the list, add it
if language and language not in dict(languages):
languages.append((language, language))

# Store the versions in the context as a sorted list of tuples
html_context["switchers_languages"] = sorted(languages)


@lru_cache(maxsize=None)
def _asset_hash(path: str) -> str:
"""Append a `?digest=` to an url based on the file content."""
Expand Down Expand Up @@ -56,6 +125,7 @@ def setup(app):
current_dir = os.path.abspath(os.path.dirname(__file__))
app.add_html_theme("python_docs_theme", current_dir)

app.connect("builder-inited", _builder_inited)
app.connect("html-page-context", _html_page_context)

return {
Expand Down
17 changes: 15 additions & 2 deletions python_docs_theme/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@ <h3>{{ _('Navigation') }}</h3>
<li><img src="{{ pathto('_static/' ~ theme_root_icon, 1) }}" alt="{{ theme_root_icon_alt_text }}" style="vertical-align: middle; margin-top: -1px"/></li>
<li><a href="{{theme_root_url}}">{{theme_root_name}}</a>{{ reldelim1 }}</li>
<li class="switchers">
<div class="language_switcher_placeholder"></div>
<div class="version_switcher_placeholder"></div>
<div class="language_switcher_placeholder">{% if switchers_languages %}
<select class="language">
{% for lang_code, lang_name in switchers_languages -%}
<option value="{{ lang_code }}" {%- if lang_code == language %} selected="true" {%- endif %}>{{ lang_name }}</option>
{% endfor -%}
</select>
{% endif -%}</div>
<div class="version_switcher_placeholder">{% if switchers_versions %}
<select class="version-select">
{% for (version_name, version_title) in switchers_versions -%}
<option value="{{ version_name }}" {%- if version_title == release %} selected="true" {%- endif %}>{{ version_title }}</option>
{% endfor -%}
</select>
{% endif %}</div>
</li>
<li>
{% if theme_root_include_title %}
Expand Down Expand Up @@ -74,6 +86,7 @@ <h3>{{ _('Navigation') }}</h3>
<link rel="shortcut icon" type="image/png" href="{{ pathto('_static/' ~ theme_root_icon, 1) }}" />
{%- if builder != "htmlhelp" %}
{%- if not embedded %}
<script type="text/javascript" src="{{ pathto('_static/switchers.js', 1) }}"></script>
<script type="text/javascript" src="{{ pathto('_static/copybutton.js', 1) }}"></script>
<script type="text/javascript" src="{{ pathto('_static/menu.js', 1) }}"></script>
<script type="text/javascript" src="{{ pathto('_static/search-focus.js', 1) }}"></script>
Expand Down
131 changes: 131 additions & 0 deletions python_docs_theme/static/switchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
'use strict';

const _is_file_uri = (uri) => uri.startsWith('file://');

const _IS_LOCAL = _is_file_uri(window.location.href);
const _CONTENT_ROOT = document.documentElement.dataset.content_root;
const _CURRENT_PREFIX = _IS_LOCAL
? null
: new URL(_CONTENT_ROOT, window.location).pathname;
const _CURRENT_RELEASE = DOCUMENTATION_OPTIONS.VERSION || '';
const _CURRENT_VERSION = _CURRENT_RELEASE.split('.').slice(0, 2).join('.');
const _CURRENT_LANGUAGE = DOCUMENTATION_OPTIONS.LANGUAGE?.toLowerCase() || 'en';

/**
* Change the current page to the first existing URL in the list.
* @param {Array<string>} urls
* @private
*/
const _navigate_to_first_existing = async (urls) => {
// Navigate to the first existing URL of urls.
for (const url of urls) {
try {
const response = await fetch(url, { method: 'GET' })
if (response.ok) {
window.location.href = url;
return url; // Avoid race conditions with multiple redirects
}
} catch(err) {
console.error(`Error in: ${url}`);
console.error(err)
}
}

// if all else fails, redirect to the d.p.o root
window.location.href = '/';
};

/**
* Navigate to the selected version.
* @param {Event} event
* @returns {Promise<void>}
*/
const on_version_switch = async (event) => {
if (_IS_LOCAL) return;

const selected_version = event.target.value;
// Special 'default' case for English.
const new_prefix =
_CURRENT_LANGUAGE === 'en'
? `/${selected_version}/`
: `/${_CURRENT_LANGUAGE}/${selected_version}/`;
const new_prefix_en = `/${selected_version}/`;
if (_CURRENT_PREFIX !== new_prefix) {
// Try the following pages in order:
// 1. The current page in the current language with the new version
// 2. The current page in English with the new version
// 3. The documentation home in the current language with the new version
// 4. The documentation home in English with the new version
await _navigate_to_first_existing([
window.location.href.replace(_CURRENT_PREFIX, new_prefix),
window.location.href.replace(_CURRENT_PREFIX, new_prefix_en),
new_prefix,
new_prefix_en,
]);
}
};

/**
* Navigate to the selected language.
* @param {Event} event
* @returns {Promise<void>}
*/
const on_language_switch = async (event) => {
if (_IS_LOCAL) return;

const selected_language = event.target.value;
// Special 'default' case for English.
const new_prefix =
selected_language === 'en'
? `/${_CURRENT_VERSION}/`
: `/${selected_language}/${_CURRENT_VERSION}/`;
if (_CURRENT_PREFIX !== new_prefix) {
// Try the following pages in order:
// 1. The current page in the new language with the current version
// 2. The documentation home in the new language with the current version
await _navigate_to_first_existing([
window.location.href.replace(_CURRENT_PREFIX, new_prefix),
new_prefix,
]);
}
};

/**
* Set up the version and language switchers.
* @returns {Promise<void>}
*/
const initialise_switchers = async () => {
try {
// Update the version select elements
document
.querySelectorAll('.version_switcher_placeholder select')
.forEach((select) => {
if (_IS_LOCAL) {
select.disabled = true;
select.title = 'Version switching is disabled in local builds';
}
select.addEventListener('change', on_version_switch);
select.parentElement.classList.remove('version_switcher_placeholder');
});

// Update the language select elements
document
.querySelectorAll('.language_switcher_placeholder select')
.forEach((select) => {
if (_IS_LOCAL) {
select.disabled = true;
select.title = 'Language switching is disabled in local builds';
}
select.addEventListener('change', on_language_switch);
select.parentElement.classList.remove('language_switcher_placeholder');
});
} catch (error) {
console.error(error);
}
};

if (document.readyState !== 'loading') {
initialise_switchers();
} else {
document.addEventListener('DOMContentLoaded', initialise_switchers);
}

0 comments on commit 4a284c2

Please sign in to comment.