[IMP] odoo_theme: introduce fallback URLs for the page switchers

When a user clicks on the link of an alternate page in the version or
language switcher, we now check if the page referenced by the target URL
exists or not. If not, we generate a series of fallback URLs from the
target URL and check whether the targeted resource exists or not, until
we read the root of the documentation. As soon as we find a valid URL,
we redirect the user to it.

This is inspired by the behaviour of docs.python.org's version and
language switchers.

task-2534669

closes odoo/documentation#2052

X-original-commit: 25e863a64c
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
Co-authored-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Martin Trigaux 2022-02-14 13:07:29 +00:00 committed by Antoine Vandevenne (anv)
parent d5d232bb50
commit a81e0623b0
3 changed files with 125 additions and 0 deletions

View File

@ -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,

View File

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

View File

@ -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.
};