-
-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #225 from AA-Turner/switchers/refactor
- Loading branch information
Showing
1 changed file
with
176 additions
and
174 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 |
---|---|---|
@@ -1,197 +1,199 @@ | ||
(function() { | ||
'use strict'; | ||
|
||
if (!String.prototype.startsWith) { | ||
Object.defineProperty(String.prototype, 'startsWith', { | ||
value: function(search, rawPos) { | ||
const pos = rawPos > 0 ? rawPos|0 : 0; | ||
return this.substring(pos, pos + search.length) === search; | ||
} | ||
}); | ||
'use strict'; | ||
|
||
// File URIs must begin with either one or three forward slashes | ||
const _is_file_uri = (uri) => uri.startsWith('file:/'); | ||
|
||
const _IS_LOCAL = _is_file_uri(window.location.href); | ||
const _CURRENT_RELEASE = DOCUMENTATION_OPTIONS.VERSION || ''; | ||
const _CURRENT_VERSION = _CURRENT_RELEASE.split('.', 2).join('.'); | ||
const _CURRENT_LANGUAGE = DOCUMENTATION_OPTIONS.LANGUAGE?.toLowerCase() || 'en'; | ||
const _CURRENT_PREFIX = (() => { | ||
if (_IS_LOCAL) return null; | ||
// Sphinx 7.2+ defines the content root data attribute in the HTML element. | ||
const _CONTENT_ROOT = document.documentElement.dataset.content_root; | ||
if (_CONTENT_ROOT !== undefined) { | ||
return new URL(_CONTENT_ROOT, window.location).pathname; | ||
} | ||
// Fallback for older versions of Sphinx (used in Python 3.10 and older). | ||
const _NUM_PREFIX_PARTS = _CURRENT_LANGUAGE === 'en' ? 2 : 3; | ||
return window.location.pathname.split('/', _NUM_PREFIX_PARTS).join('/') + '/'; | ||
})(); | ||
|
||
// Parses versions in URL segments like: | ||
// "3", "dev", "release/2.7" or "3.6rc2" | ||
const version_regexs = [ | ||
'(?:\\d)', | ||
'(?:\\d\\.\\d[\\w\\d\\.]*)', | ||
'(?:dev)', | ||
'(?:release/\\d.\\d[\\x\\d\\.]*)']; | ||
|
||
const all_versions = $VERSIONS; | ||
const all_languages = $LANGUAGES; | ||
|
||
function quote_attr(str) { | ||
return '"' + str.replace('"', '\\"') + '"'; | ||
const _ALL_VERSIONS = new Map(Object.entries($VERSIONS)); | ||
const _ALL_LANGUAGES = new Map(Object.entries($LANGUAGES)); | ||
|
||
/** | ||
* @param {Map<string, string>} versions | ||
* @returns {HTMLSelectElement} | ||
* @private | ||
*/ | ||
const _create_version_select = (versions) => { | ||
const select = document.createElement('select'); | ||
select.className = 'version-select'; | ||
if (_IS_LOCAL) { | ||
select.disabled = true; | ||
select.title = 'Version switching is disabled in local builds'; | ||
} | ||
|
||
function build_version_select(release) { | ||
let buf = ['<select id="version_select" aria-label="Python version">']; | ||
const major_minor = release.split(".").slice(0, 2).join("."); | ||
|
||
Object.entries(all_versions).forEach(function([version, title]) { | ||
if (version === major_minor) { | ||
buf.push('<option value=' + quote_attr(version) + ' selected="selected">' + release + '</option>'); | ||
} else { | ||
buf.push('<option value=' + quote_attr(version) + '>' + title + '</option>'); | ||
} | ||
}); | ||
for (const [version, title] of versions) { | ||
const option = document.createElement('option'); | ||
option.value = version; | ||
if (version === _CURRENT_VERSION) { | ||
option.text = _CURRENT_RELEASE; | ||
option.selected = true; | ||
} else { | ||
option.text = title; | ||
} | ||
select.add(option); | ||
} | ||
|
||
buf.push('</select>'); | ||
return buf.join(''); | ||
return select; | ||
}; | ||
|
||
/** | ||
* @param {Map<string, string>} languages | ||
* @returns {HTMLSelectElement} | ||
* @private | ||
*/ | ||
const _create_language_select = (languages) => { | ||
if (!languages.has(_CURRENT_LANGUAGE)) { | ||
// In case we are browsing a language that is not yet in languages. | ||
languages.set(_CURRENT_LANGUAGE, _CURRENT_LANGUAGE); | ||
} | ||
|
||
function build_language_select(current_language) { | ||
let buf = ['<select id="language_select" aria-label="Language">']; | ||
const select = document.createElement('select'); | ||
select.className = 'language-select'; | ||
if (_IS_LOCAL) { | ||
select.disabled = true; | ||
select.title = 'Language switching is disabled in local builds'; | ||
} | ||
|
||
Object.entries(all_languages).forEach(function([language, title]) { | ||
if (language === current_language) { | ||
buf.push('<option value="' + language + '" selected="selected">' + title + '</option>'); | ||
} else { | ||
buf.push('<option value="' + language + '">' + title + '</option>'); | ||
} | ||
}); | ||
if (!(current_language in all_languages)) { | ||
// In case we're browsing a language that is not yet in all_languages. | ||
buf.push('<option value="' + current_language + '" selected="selected">' + | ||
current_language + '</option>'); | ||
all_languages[current_language] = current_language; | ||
} | ||
buf.push('</select>'); | ||
return buf.join(''); | ||
for (const [language, title] of languages) { | ||
const option = document.createElement('option'); | ||
option.value = language; | ||
option.text = title; | ||
if (language === _CURRENT_LANGUAGE) option.selected = true; | ||
select.add(option); | ||
} | ||
|
||
function navigate_to_first_existing(urls) { | ||
// Navigate to the first existing URL in urls. | ||
const url = urls.shift(); | ||
if (urls.length == 0 || url.startsWith("file:///")) { | ||
window.location.href = url; | ||
return; | ||
} | ||
return select; | ||
}; | ||
|
||
/** | ||
* Change the current page to the first existing URL in the list. | ||
* @param {Array<string>} urls | ||
* @private | ||
*/ | ||
const _navigate_to_first_existing = (urls) => { | ||
// Navigate to the first existing URL in urls. | ||
for (const url of urls) { | ||
fetch(url) | ||
.then(function(response) { | ||
.then((response) => { | ||
if (response.ok) { | ||
window.location.href = url; | ||
} else { | ||
navigate_to_first_existing(urls); | ||
return url; | ||
} | ||
}) | ||
.catch(function(error) { | ||
navigate_to_first_existing(urls); | ||
.catch((err) => { | ||
console.error(`Error when fetching '${url}'!`); | ||
console.error(err); | ||
}); | ||
} | ||
|
||
function on_version_switch() { | ||
const selected_version = this.options[this.selectedIndex].value + '/'; | ||
const url = window.location.href; | ||
const current_language = language_segment_from_url(); | ||
const current_version = version_segment_from_url(); | ||
const new_url = url.replace('/' + current_language + current_version, | ||
'/' + current_language + selected_version); | ||
if (new_url != url) { | ||
navigate_to_first_existing([ | ||
new_url, | ||
url.replace('/' + current_language + current_version, | ||
'/' + selected_version), | ||
'/' + current_language + selected_version, | ||
'/' + selected_version, | ||
'/' | ||
]); | ||
} | ||
} | ||
|
||
function on_language_switch() { | ||
let selected_language = this.options[this.selectedIndex].value + '/'; | ||
const url = window.location.href; | ||
const current_language = language_segment_from_url(); | ||
const current_version = version_segment_from_url(); | ||
if (selected_language == 'en/') // Special 'default' case for English. | ||
selected_language = ''; | ||
let new_url = url.replace('/' + current_language + current_version, | ||
'/' + selected_language + current_version); | ||
if (new_url != url) { | ||
navigate_to_first_existing([ | ||
new_url, | ||
'/' | ||
]); | ||
} | ||
// if all else fails, redirect to the d.p.o root | ||
window.location.href = '/'; | ||
return '/'; | ||
}; | ||
|
||
/** | ||
* Callback for the version switcher. | ||
* @param {Event} event | ||
* @returns {void} | ||
* @private | ||
*/ | ||
const _on_version_switch = (event) => { | ||
if (_IS_LOCAL) return; | ||
|
||
const selected_version = event.target.value; | ||
// English has no language prefix. | ||
const new_prefix_en = `/${selected_version}/`; | ||
const new_prefix = | ||
_CURRENT_LANGUAGE === 'en' | ||
? new_prefix_en | ||
: `/${_CURRENT_LANGUAGE}/${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 | ||
_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, | ||
]); | ||
} | ||
|
||
// Returns the path segment of the language as a string, like 'fr/' | ||
// or '' if not found. | ||
function language_segment_from_url() { | ||
const path = window.location.pathname; | ||
const language_regexp = '/((?:' + Object.keys(all_languages).join("|") + ')/)' | ||
const match = path.match(language_regexp); | ||
if (match !== null) | ||
return match[1]; | ||
return ''; | ||
}; | ||
|
||
/** | ||
* Callback for the language switcher. | ||
* @param {Event} event | ||
* @returns {void} | ||
* @private | ||
*/ | ||
const _on_language_switch = (event) => { | ||
if (_IS_LOCAL) return; | ||
|
||
const selected_language = event.target.value; | ||
// English has no language prefix. | ||
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 | ||
_navigate_to_first_existing([ | ||
window.location.href.replace(_CURRENT_PREFIX, new_prefix), | ||
new_prefix, | ||
]); | ||
} | ||
|
||
// Returns the path segment of the version as a string, like '3.6/' | ||
// or '' if not found. | ||
function version_segment_from_url() { | ||
const path = window.location.pathname; | ||
const language_segment = language_segment_from_url(); | ||
const version_segment = '(?:(?:' + version_regexs.join('|') + ')/)'; | ||
const version_regexp = language_segment + '(' + version_segment + ')'; | ||
const match = path.match(version_regexp); | ||
if (match !== null) | ||
return match[1]; | ||
return '' | ||
} | ||
|
||
function create_placeholders_if_missing() { | ||
const version_segment = version_segment_from_url(); | ||
const language_segment = language_segment_from_url(); | ||
const index = "/" + language_segment + version_segment; | ||
|
||
if (document.querySelectorAll('.version_switcher_placeholder').length > 0) { | ||
return; | ||
} | ||
|
||
const html = '<span class="language_switcher_placeholder"></span> \ | ||
<span class="version_switcher_placeholder"></span> \ | ||
<a href="/" id="indexlink">Documentation</a> »'; | ||
|
||
const probable_places = [ | ||
"body>div.related>ul>li:not(.right):contains('Documentation'):first", | ||
"body>div.related>ul>li:not(.right):contains('documentation'):first", | ||
]; | ||
|
||
for (let i = 0; i < probable_places.length; i++) { | ||
let probable_place = $(probable_places[i]); | ||
if (probable_place.length == 1) { | ||
probable_place.html(html); | ||
document.getElementById('indexlink').href = index; | ||
return; | ||
} | ||
} | ||
} | ||
|
||
document.addEventListener('DOMContentLoaded', function() { | ||
const language_segment = language_segment_from_url(); | ||
const current_language = language_segment.replace(/\/+$/g, '') || 'en'; | ||
const version_select = build_version_select(DOCUMENTATION_OPTIONS.VERSION); | ||
|
||
create_placeholders_if_missing(); | ||
|
||
let placeholders = document.querySelectorAll('.version_switcher_placeholder'); | ||
placeholders.forEach(function(placeholder) { | ||
placeholder.innerHTML = version_select; | ||
|
||
let selectElement = placeholder.querySelector('select'); | ||
selectElement.addEventListener('change', on_version_switch); | ||
}; | ||
|
||
/** | ||
* Initialisation function for the version and language switchers. | ||
* @returns {void} | ||
* @private | ||
*/ | ||
const _initialise_switchers = () => { | ||
const versions = _ALL_VERSIONS; | ||
const languages = _ALL_LANGUAGES; | ||
|
||
const version_select = _create_version_select(versions); | ||
document | ||
.querySelectorAll('.version_switcher_placeholder') | ||
.forEach((placeholder) => { | ||
const s = version_select.cloneNode(true); | ||
s.addEventListener('change', _on_version_switch); | ||
placeholder.append(s); | ||
placeholder.classList.remove('version_switcher_placeholder'); | ||
}); | ||
|
||
const language_select = build_language_select(current_language); | ||
|
||
placeholders = document.querySelectorAll('.language_switcher_placeholder'); | ||
placeholders.forEach(function(placeholder) { | ||
placeholder.innerHTML = language_select; | ||
|
||
let selectElement = placeholder.querySelector('select'); | ||
selectElement.addEventListener('change', on_language_switch); | ||
const language_select = _create_language_select(languages); | ||
document | ||
.querySelectorAll('.language_switcher_placeholder') | ||
.forEach((placeholder) => { | ||
const s = language_select.cloneNode(true); | ||
s.addEventListener('change', _on_language_switch); | ||
placeholder.append(s); | ||
placeholder.classList.remove('language_switcher_placeholder'); | ||
}); | ||
}); | ||
})(); | ||
}; | ||
|
||
if (document.readyState !== 'loading') { | ||
_initialise_switchers(); | ||
} else { | ||
document.addEventListener('DOMContentLoaded', _initialise_switchers); | ||
} |