|
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