Skip to content

Refactor switchers.js #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Oct 29, 2024
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7e274eb
Remove String.startsWith polyfill
AA-Turner Oct 28, 2024
27e2dc7
Remove create_placeholders_if_missing()
AA-Turner Oct 28, 2024
75594be
Use exact comparison operators
AA-Turner Oct 28, 2024
8a71e42
Remove an unneeded variable
AA-Turner Oct 28, 2024
ce8b474
Remove anonymous function (IIFE)
AA-Turner Oct 28, 2024
cdcd523
Name the initialisation function
AA-Turner Oct 28, 2024
84a3f9c
Construct DOM nodes instead of parsing HTML
AA-Turner Oct 28, 2024
2a5adf9
Use arrow functions
AA-Turner Oct 28, 2024
b4b36c0
Remove unused quote_attr() function
AA-Turner Oct 28, 2024
2cac9f6
Run prettier
AA-Turner Oct 28, 2024
7b5e773
Make the regular expression for version parts a constant
AA-Turner Oct 28, 2024
ab3b079
Use constants from DOCUMENTATION_OPTIONS where possible
AA-Turner Oct 28, 2024
a593646
Remove now-unused segment_from_url() functions
AA-Turner Oct 28, 2024
b62ea7a
Use the event argument of select element callback functions
AA-Turner Oct 28, 2024
e5b53bb
Improve logic for the callback functions
AA-Turner Oct 28, 2024
1179ee9
Iterate over arrays with for...of
AA-Turner Oct 28, 2024
d1b7ad8
Fix _CURRENT_VERSION for pre-releases
AA-Turner Oct 28, 2024
7efb6bf
Use _CURRENT_RELEASE in _create_version_select()
AA-Turner Oct 28, 2024
359c393
Use _CURRENT_LANGUAGE in _create_language_select()
AA-Turner Oct 28, 2024
e55b26c
Add better handling for file URIs
AA-Turner Oct 28, 2024
4c495cc
Remove placeholder classes when initialisation is complete
AA-Turner Oct 28, 2024
3031085
Use a Map for versions and languages
AA-Turner Oct 28, 2024
8cb6706
Add JSDoc comments
AA-Turner Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 176 additions & 174 deletions templates/switchers.js
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but I read through PEP 545 – Python Documentation Translations this weekend and it says:

http://docs.python.org/en/ will redirect to http://docs.python.org/.

Currently /en/ is 404, I wonder if we should add this redirect or if it was decided to be unnecessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also seen that, though I never got around to updating the PEP. I think that we should remove it from the PEP as something that was never implemented, rather than adding another set of redirects to maintain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how many people try /en/, but fine by me. And the PEP should probably Process/Active rather than Process/Final.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Comment on lines +126 to +134
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it would be better to stick in the current language, even if that means going back to the homepage?

It might be surprising to switch to a completely different language set; as you navigate further everything is English and not your chosen language.

And I think different languages should nearly always have the same structure and same pages for any given version? (Although with some untranslated pages.)

That would mean:

Suggested change
// 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,
// 1. The current page in the current language with the new version
// 2. The documentation home in the current language with the new version
// 3. The current page in English 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),
new_prefix,
window.location.href.replace(_CURRENT_PREFIX, new_prefix_en),
new_prefix_en,

But I'm really not sure. Might be worth asking people who regularly uses the docs in another language?

Copy link
Member Author

@AA-Turner AA-Turner Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we create an issue for this? The intent in this PR was for as few behavioural changes as possible (the ideally none!).

Although, the current order does have some logic, given that a reader probably prefers to have the information (which can be translated via browser tools, Google, etc) rather than always being sent to the homepage for a language that doesn't have much translation coverage.

(Also, this order was chosen by a French-speaker!)

A

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

]);
}

// 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> &#187;';

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);
}