Skip to content

Commit 757acb1

Browse files
authored
Merge pull request #225 from AA-Turner/switchers/refactor
2 parents 1807c70 + 8cb6706 commit 757acb1

File tree

1 file changed

+176
-174
lines changed

1 file changed

+176
-174
lines changed

templates/switchers.js

+176-174
Original file line numberDiff line numberDiff line change
@@ -1,197 +1,199 @@
1-
(function() {
2-
'use strict';
3-
4-
if (!String.prototype.startsWith) {
5-
Object.defineProperty(String.prototype, 'startsWith', {
6-
value: function(search, rawPos) {
7-
const pos = rawPos > 0 ? rawPos|0 : 0;
8-
return this.substring(pos, pos + search.length) === search;
9-
}
10-
});
1+
'use strict';
2+
3+
// File URIs must begin with either one or three forward slashes
4+
const _is_file_uri = (uri) => uri.startsWith('file:/');
5+
6+
const _IS_LOCAL = _is_file_uri(window.location.href);
7+
const _CURRENT_RELEASE = DOCUMENTATION_OPTIONS.VERSION || '';
8+
const _CURRENT_VERSION = _CURRENT_RELEASE.split('.', 2).join('.');
9+
const _CURRENT_LANGUAGE = DOCUMENTATION_OPTIONS.LANGUAGE?.toLowerCase() || 'en';
10+
const _CURRENT_PREFIX = (() => {
11+
if (_IS_LOCAL) return null;
12+
// Sphinx 7.2+ defines the content root data attribute in the HTML element.
13+
const _CONTENT_ROOT = document.documentElement.dataset.content_root;
14+
if (_CONTENT_ROOT !== undefined) {
15+
return new URL(_CONTENT_ROOT, window.location).pathname;
1116
}
17+
// Fallback for older versions of Sphinx (used in Python 3.10 and older).
18+
const _NUM_PREFIX_PARTS = _CURRENT_LANGUAGE === 'en' ? 2 : 3;
19+
return window.location.pathname.split('/', _NUM_PREFIX_PARTS).join('/') + '/';
20+
})();
1221

13-
// Parses versions in URL segments like:
14-
// "3", "dev", "release/2.7" or "3.6rc2"
15-
const version_regexs = [
16-
'(?:\\d)',
17-
'(?:\\d\\.\\d[\\w\\d\\.]*)',
18-
'(?:dev)',
19-
'(?:release/\\d.\\d[\\x\\d\\.]*)'];
20-
21-
const all_versions = $VERSIONS;
22-
const all_languages = $LANGUAGES;
23-
24-
function quote_attr(str) {
25-
return '"' + str.replace('"', '\\"') + '"';
22+
const _ALL_VERSIONS = new Map(Object.entries($VERSIONS));
23+
const _ALL_LANGUAGES = new Map(Object.entries($LANGUAGES));
24+
25+
/**
26+
* @param {Map<string, string>} versions
27+
* @returns {HTMLSelectElement}
28+
* @private
29+
*/
30+
const _create_version_select = (versions) => {
31+
const select = document.createElement('select');
32+
select.className = 'version-select';
33+
if (_IS_LOCAL) {
34+
select.disabled = true;
35+
select.title = 'Version switching is disabled in local builds';
2636
}
2737

28-
function build_version_select(release) {
29-
let buf = ['<select id="version_select" aria-label="Python version">'];
30-
const major_minor = release.split(".").slice(0, 2).join(".");
31-
32-
Object.entries(all_versions).forEach(function([version, title]) {
33-
if (version === major_minor) {
34-
buf.push('<option value=' + quote_attr(version) + ' selected="selected">' + release + '</option>');
35-
} else {
36-
buf.push('<option value=' + quote_attr(version) + '>' + title + '</option>');
37-
}
38-
});
38+
for (const [version, title] of versions) {
39+
const option = document.createElement('option');
40+
option.value = version;
41+
if (version === _CURRENT_VERSION) {
42+
option.text = _CURRENT_RELEASE;
43+
option.selected = true;
44+
} else {
45+
option.text = title;
46+
}
47+
select.add(option);
48+
}
3949

40-
buf.push('</select>');
41-
return buf.join('');
50+
return select;
51+
};
52+
53+
/**
54+
* @param {Map<string, string>} languages
55+
* @returns {HTMLSelectElement}
56+
* @private
57+
*/
58+
const _create_language_select = (languages) => {
59+
if (!languages.has(_CURRENT_LANGUAGE)) {
60+
// In case we are browsing a language that is not yet in languages.
61+
languages.set(_CURRENT_LANGUAGE, _CURRENT_LANGUAGE);
4262
}
4363

44-
function build_language_select(current_language) {
45-
let buf = ['<select id="language_select" aria-label="Language">'];
64+
const select = document.createElement('select');
65+
select.className = 'language-select';
66+
if (_IS_LOCAL) {
67+
select.disabled = true;
68+
select.title = 'Language switching is disabled in local builds';
69+
}
4670

47-
Object.entries(all_languages).forEach(function([language, title]) {
48-
if (language === current_language) {
49-
buf.push('<option value="' + language + '" selected="selected">' + title + '</option>');
50-
} else {
51-
buf.push('<option value="' + language + '">' + title + '</option>');
52-
}
53-
});
54-
if (!(current_language in all_languages)) {
55-
// In case we're browsing a language that is not yet in all_languages.
56-
buf.push('<option value="' + current_language + '" selected="selected">' +
57-
current_language + '</option>');
58-
all_languages[current_language] = current_language;
59-
}
60-
buf.push('</select>');
61-
return buf.join('');
71+
for (const [language, title] of languages) {
72+
const option = document.createElement('option');
73+
option.value = language;
74+
option.text = title;
75+
if (language === _CURRENT_LANGUAGE) option.selected = true;
76+
select.add(option);
6277
}
6378

64-
function navigate_to_first_existing(urls) {
65-
// Navigate to the first existing URL in urls.
66-
const url = urls.shift();
67-
if (urls.length == 0 || url.startsWith("file:///")) {
68-
window.location.href = url;
69-
return;
70-
}
79+
return select;
80+
};
81+
82+
/**
83+
* Change the current page to the first existing URL in the list.
84+
* @param {Array<string>} urls
85+
* @private
86+
*/
87+
const _navigate_to_first_existing = (urls) => {
88+
// Navigate to the first existing URL in urls.
89+
for (const url of urls) {
7190
fetch(url)
72-
.then(function(response) {
91+
.then((response) => {
7392
if (response.ok) {
7493
window.location.href = url;
75-
} else {
76-
navigate_to_first_existing(urls);
94+
return url;
7795
}
7896
})
79-
.catch(function(error) {
80-
navigate_to_first_existing(urls);
97+
.catch((err) => {
98+
console.error(`Error when fetching '${url}'!`);
99+
console.error(err);
81100
});
82101
}
83102

84-
function on_version_switch() {
85-
const selected_version = this.options[this.selectedIndex].value + '/';
86-
const url = window.location.href;
87-
const current_language = language_segment_from_url();
88-
const current_version = version_segment_from_url();
89-
const new_url = url.replace('/' + current_language + current_version,
90-
'/' + current_language + selected_version);
91-
if (new_url != url) {
92-
navigate_to_first_existing([
93-
new_url,
94-
url.replace('/' + current_language + current_version,
95-
'/' + selected_version),
96-
'/' + current_language + selected_version,
97-
'/' + selected_version,
98-
'/'
99-
]);
100-
}
101-
}
102-
103-
function on_language_switch() {
104-
let selected_language = this.options[this.selectedIndex].value + '/';
105-
const url = window.location.href;
106-
const current_language = language_segment_from_url();
107-
const current_version = version_segment_from_url();
108-
if (selected_language == 'en/') // Special 'default' case for English.
109-
selected_language = '';
110-
let new_url = url.replace('/' + current_language + current_version,
111-
'/' + selected_language + current_version);
112-
if (new_url != url) {
113-
navigate_to_first_existing([
114-
new_url,
115-
'/'
116-
]);
117-
}
103+
// if all else fails, redirect to the d.p.o root
104+
window.location.href = '/';
105+
return '/';
106+
};
107+
108+
/**
109+
* Callback for the version switcher.
110+
* @param {Event} event
111+
* @returns {void}
112+
* @private
113+
*/
114+
const _on_version_switch = (event) => {
115+
if (_IS_LOCAL) return;
116+
117+
const selected_version = event.target.value;
118+
// English has no language prefix.
119+
const new_prefix_en = `/${selected_version}/`;
120+
const new_prefix =
121+
_CURRENT_LANGUAGE === 'en'
122+
? new_prefix_en
123+
: `/${_CURRENT_LANGUAGE}/${selected_version}/`;
124+
if (_CURRENT_PREFIX !== new_prefix) {
125+
// Try the following pages in order:
126+
// 1. The current page in the current language with the new version
127+
// 2. The current page in English with the new version
128+
// 3. The documentation home in the current language with the new version
129+
// 4. The documentation home in English with the new version
130+
_navigate_to_first_existing([
131+
window.location.href.replace(_CURRENT_PREFIX, new_prefix),
132+
window.location.href.replace(_CURRENT_PREFIX, new_prefix_en),
133+
new_prefix,
134+
new_prefix_en,
135+
]);
118136
}
119-
120-
// Returns the path segment of the language as a string, like 'fr/'
121-
// or '' if not found.
122-
function language_segment_from_url() {
123-
const path = window.location.pathname;
124-
const language_regexp = '/((?:' + Object.keys(all_languages).join("|") + ')/)'
125-
const match = path.match(language_regexp);
126-
if (match !== null)
127-
return match[1];
128-
return '';
137+
};
138+
139+
/**
140+
* Callback for the language switcher.
141+
* @param {Event} event
142+
* @returns {void}
143+
* @private
144+
*/
145+
const _on_language_switch = (event) => {
146+
if (_IS_LOCAL) return;
147+
148+
const selected_language = event.target.value;
149+
// English has no language prefix.
150+
const new_prefix =
151+
selected_language === 'en'
152+
? `/${_CURRENT_VERSION}/`
153+
: `/${selected_language}/${_CURRENT_VERSION}/`;
154+
if (_CURRENT_PREFIX !== new_prefix) {
155+
// Try the following pages in order:
156+
// 1. The current page in the new language with the current version
157+
// 2. The documentation home in the new language with the current version
158+
_navigate_to_first_existing([
159+
window.location.href.replace(_CURRENT_PREFIX, new_prefix),
160+
new_prefix,
161+
]);
129162
}
130-
131-
// Returns the path segment of the version as a string, like '3.6/'
132-
// or '' if not found.
133-
function version_segment_from_url() {
134-
const path = window.location.pathname;
135-
const language_segment = language_segment_from_url();
136-
const version_segment = '(?:(?:' + version_regexs.join('|') + ')/)';
137-
const version_regexp = language_segment + '(' + version_segment + ')';
138-
const match = path.match(version_regexp);
139-
if (match !== null)
140-
return match[1];
141-
return ''
142-
}
143-
144-
function create_placeholders_if_missing() {
145-
const version_segment = version_segment_from_url();
146-
const language_segment = language_segment_from_url();
147-
const index = "/" + language_segment + version_segment;
148-
149-
if (document.querySelectorAll('.version_switcher_placeholder').length > 0) {
150-
return;
151-
}
152-
153-
const html = '<span class="language_switcher_placeholder"></span> \
154-
<span class="version_switcher_placeholder"></span> \
155-
<a href="/" id="indexlink">Documentation</a> &#187;';
156-
157-
const probable_places = [
158-
"body>div.related>ul>li:not(.right):contains('Documentation'):first",
159-
"body>div.related>ul>li:not(.right):contains('documentation'):first",
160-
];
161-
162-
for (let i = 0; i < probable_places.length; i++) {
163-
let probable_place = $(probable_places[i]);
164-
if (probable_place.length == 1) {
165-
probable_place.html(html);
166-
document.getElementById('indexlink').href = index;
167-
return;
168-
}
169-
}
170-
}
171-
172-
document.addEventListener('DOMContentLoaded', function() {
173-
const language_segment = language_segment_from_url();
174-
const current_language = language_segment.replace(/\/+$/g, '') || 'en';
175-
const version_select = build_version_select(DOCUMENTATION_OPTIONS.VERSION);
176-
177-
create_placeholders_if_missing();
178-
179-
let placeholders = document.querySelectorAll('.version_switcher_placeholder');
180-
placeholders.forEach(function(placeholder) {
181-
placeholder.innerHTML = version_select;
182-
183-
let selectElement = placeholder.querySelector('select');
184-
selectElement.addEventListener('change', on_version_switch);
163+
};
164+
165+
/**
166+
* Initialisation function for the version and language switchers.
167+
* @returns {void}
168+
* @private
169+
*/
170+
const _initialise_switchers = () => {
171+
const versions = _ALL_VERSIONS;
172+
const languages = _ALL_LANGUAGES;
173+
174+
const version_select = _create_version_select(versions);
175+
document
176+
.querySelectorAll('.version_switcher_placeholder')
177+
.forEach((placeholder) => {
178+
const s = version_select.cloneNode(true);
179+
s.addEventListener('change', _on_version_switch);
180+
placeholder.append(s);
181+
placeholder.classList.remove('version_switcher_placeholder');
185182
});
186183

187-
const language_select = build_language_select(current_language);
188-
189-
placeholders = document.querySelectorAll('.language_switcher_placeholder');
190-
placeholders.forEach(function(placeholder) {
191-
placeholder.innerHTML = language_select;
192-
193-
let selectElement = placeholder.querySelector('select');
194-
selectElement.addEventListener('change', on_language_switch);
184+
const language_select = _create_language_select(languages);
185+
document
186+
.querySelectorAll('.language_switcher_placeholder')
187+
.forEach((placeholder) => {
188+
const s = language_select.cloneNode(true);
189+
s.addEventListener('change', _on_language_switch);
190+
placeholder.append(s);
191+
placeholder.classList.remove('language_switcher_placeholder');
195192
});
196-
});
197-
})();
193+
};
194+
195+
if (document.readyState !== 'loading') {
196+
_initialise_switchers();
197+
} else {
198+
document.addEventListener('DOMContentLoaded', _initialise_switchers);
199+
}

0 commit comments

Comments
 (0)