|
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; |
11 | 16 | }
|
| 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 | +})(); |
12 | 21 |
|
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'; |
26 | 36 | }
|
27 | 37 |
|
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 | + } |
39 | 49 |
|
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); |
42 | 62 | }
|
43 | 63 |
|
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 | + } |
46 | 70 |
|
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); |
62 | 77 | }
|
63 | 78 |
|
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) { |
71 | 90 | fetch(url)
|
72 |
| - .then(function(response) { |
| 91 | + .then((response) => { |
73 | 92 | if (response.ok) {
|
74 | 93 | window.location.href = url;
|
75 |
| - } else { |
76 |
| - navigate_to_first_existing(urls); |
| 94 | + return url; |
77 | 95 | }
|
78 | 96 | })
|
79 |
| - .catch(function(error) { |
80 |
| - navigate_to_first_existing(urls); |
| 97 | + .catch((err) => { |
| 98 | + console.error(`Error when fetching '${url}'!`); |
| 99 | + console.error(err); |
81 | 100 | });
|
82 | 101 | }
|
83 | 102 |
|
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 | + ]); |
118 | 136 | }
|
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 | + ]); |
129 | 162 | }
|
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> »'; |
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'); |
185 | 182 | });
|
186 | 183 |
|
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'); |
195 | 192 | });
|
196 |
| - }); |
197 |
| -})(); |
| 193 | +}; |
| 194 | + |
| 195 | +if (document.readyState !== 'loading') { |
| 196 | + _initialise_switchers(); |
| 197 | +} else { |
| 198 | + document.addEventListener('DOMContentLoaded', _initialise_switchers); |
| 199 | +} |
0 commit comments