Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions extensions/odoo_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def setup(app):
app.add_js_file('js/layout.js')
app.add_js_file('js/menu.js')
app.add_js_file('js/page_toc.js')
app.add_js_file('js/switchers.js')

return {
'parallel_read_safe': True,
Expand Down
27 changes: 27 additions & 0 deletions extensions/odoo_theme/static/js/switchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(function ($) {

document.addEventListener('DOMContentLoaded', () => {
// Enable fallback URLs for broken redirects from the version or language switchers.
_prepareSwitchersFallbacks();
});

/**
* Add event listeners on links in the version and language switchers.
*
* If a link is clicked, the user is redirected to the closest fallback URL (including the
* original target URL) that is available.
*/
const _prepareSwitchersFallbacks = () => {
document.querySelectorAll('a[class="dropdown-item"]').forEach(element => {
element.addEventListener('click', async event => {
if (element.hasAttribute('href')) {
event.preventDefault();
const fallbackUrls = await _generateFallbackUrls(element.getAttribute('href'));
const fallbackUrl = await _getFirstValidUrl(fallbackUrls);
window.location.href = fallbackUrl;
}
});
});
};

})();
97 changes: 97 additions & 0 deletions extensions/odoo_theme/static/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,100 @@ const _prepareAccordion = (tocElement) => {
tocEntryList.parentNode.insertBefore(tocEntryWrapper, tocEntryList);
});
};

/**
* Generate a list of fallback URLs from the closest to the furthest of the target URL and
* return the first one that points to an existing resource.
*
* The generation consists of starting with the target URL and walking back toward the root of
* the documentation while alternating between including the original language or not, if it was
* included in the original URL. The last fallback URL is the root of the documentation with the
* version stripped off to redirect the user to the index of the default version.
*
* Example:
* 1. .../documentation/13.0/contributing/documentation.html
* 2. .../documentation/13.0/contributing.html
* 3. .../documentation/13.0
* 4. .../documentation/
*
* Example:
* 1. .../documentation/15.0/fr/administration/install.html
* 2. .../documentation/15.0/administration/install.html
* 3. .../documentation/15.0/fr/administration.html
* 4. .../documentation/15.0/administration.html
* 5. .../documentation/15.0/fr/
* 6. .../documentation/15.0/
* 7. .../documentation/
*/
const _generateFallbackUrls = async (targetUrl) => {

const _deconstructUrl = (urlObject) => {
let version = '';
let language = '';
const originalPathParts = [];
for (let fragment of urlObject.pathname.split('/').reverse()) {
if (fragment.match(/^(?:saas-)?\d{2}\.\d$|^master$/)) {
version = fragment;
} else if (fragment.match(/^[a-z]{2}(?:_[A-Z]{2})?$/)) {
language = fragment;
} else if (fragment.length > 0) {
originalPathParts.unshift(fragment);
}
}
return [version, language, originalPathParts];
};

const targetUrlObject = new URL(targetUrl);
const [version, language, originalPathParts] = _deconstructUrl(targetUrlObject);
const urlBase = targetUrlObject.origin;

// Generate the fallback URLs.
const fallbackUrls = [];
for (let i = originalPathParts.length; i >= 0; i--) {
const fallbackPathParts = originalPathParts.slice(0, i);

// Append '.html' to the last path part if it is missing and the part is not the root.
if (
fallbackPathParts.length > 0
&& !fallbackPathParts[fallbackPathParts.length - 1].endsWith('.html')
) {
fallbackPathParts[fallbackPathParts.length - 1] += '.html';
}

// Build the fallback URL from the version, language and path parts, if any.
if (version && language)
fallbackUrls.push(
`${urlBase}/${version}/${language}/${fallbackPathParts.join('/')}`,
`${urlBase}/${version}/${fallbackPathParts.join('/')}`,
);
else if (version && !language)
fallbackUrls.push(`${urlBase}/${version}/${fallbackPathParts.join('/')}`);
else if (!version && language)
fallbackUrls.push(
`${urlBase}/${language}/${fallbackPathParts.join('/')}`,
`${urlBase}/${fallbackPathParts.join('/')}`,
);
else if (!version && !language)
fallbackUrls.push(`${urlBase}/${fallbackPathParts.join('/')}`);
}
return fallbackUrls;
};

/**
* Iterate over the provided URLs and return the first one that points to a valid resource.
*
* Since URLs don't have a protocol and cannot be fetched when the documentation is built locally
* without the `ROOT` and `IS_REMOTE_BUILD` Make arguments, the URLs that don't have the protocol
* 'http' or 'https' are not tested.
*/
const _getFirstValidUrl = async (urls) => {
for (let url of urls) {
if (url.startsWith('http')) {
const response = await fetch(url);
if (response.ok) {
return url;
}
}
}
return urls[0]; // No valid URL found, return the first one.
};