Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version warningbar #1354

Merged
merged 19 commits into from Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/_static/switcher.json
@@ -1,13 +1,13 @@
[
{
"name": "dev",
"version": "latest",
"version": "dev",
"url": "https://pydata-sphinx-theme.readthedocs.io/en/latest/"
},
{
"name": "0.13.3 (stable)",
"version": "stable",
"url": "https://pydata-sphinx-theme.readthedocs.io/en/stable/"
"version": "v0.13.3",
"url": "https://pydata-sphinx-theme.readthedocs.io/en/stable/",
"preferred": true
},
{
"name": "0.12.0",
Expand Down
3 changes: 2 additions & 1 deletion docs/conf.py
Expand Up @@ -104,7 +104,7 @@
# For local development, infer the version to match from the package.
release = pydata_sphinx_theme.__version__
if "dev" in release or "rc" in release:
version_match = "latest"
version_match = "dev"
# We want to keep the relative reference if we are in dev mode
# but we want the whole url if we are effectively in a released version
json_url = "_static/switcher.json"
Expand Down Expand Up @@ -164,6 +164,7 @@
"navbar_align": "left", # [left, content, right] For testing that the navbar items align properly
"navbar_center": ["version-switcher", "navbar-nav"],
"announcement": "https://raw.githubusercontent.com/pydata/pydata-sphinx-theme/main/docs/_templates/custom-template.html",
"show_version_warning_banner": True,
# "show_nav_level": 2,
# "navbar_start": ["navbar-logo"],
# "navbar_end": ["theme-switcher", "navbar-icon-links"],
Expand Down
47 changes: 46 additions & 1 deletion docs/user_guide/announcements.rst
Expand Up @@ -15,6 +15,7 @@ By default, the value of your ``html_theme_options["announcement"]`` will be ins
For example, the following configuration adds a simple announcement.

.. code-block:: python
:caption: conf.py

html_theme_options = {
...
Expand All @@ -28,13 +29,57 @@ You can specify an arbitrary URL that will be used as the HTML source for your a
When the page is loaded, JavaScript will attempt to fetch this HTML and insert it as-is into the announcement banner.
This allows you to define a single HTML announcement that you can pull into multiple documentation sites or versions.

If the value of ``html_theme_options["announcement"]`` begins with **``http``** it will be treated as a URL to remote HTML.
If the value of ``html_theme_options["announcement"]`` begins with ``http`` it will be treated as a URL to remote HTML.

For example, the following configuration tells the theme to load the ``custom-template.html`` example from this documentation's GitHub repository:

.. code-block:: python
:caption: conf.py

html_theme_options = {
...
"announcement": "https://github.com/pydata/pydata-sphinx-theme/raw/main/docs/_templates/custom-template.html",
}


.. _version-warning-banners:

Version warning banners
-----------------------

In addition to the general-purpose announcement banner, the theme includes a built-in banner to warn users when they are viewing versions of your docs other than the latest stable version. To use this feature, add the following to your ``conf.py``:

.. code-block:: python
:caption: conf.py

html_theme_options = {
...
"show_version_warning_banner": True,
}

.. important::

This functionality relies on the :ref:`version switcher <version-dropdowns>` to determine the version number of the latest stable release.
*It will only work* if your version switcher ``.json`` has exactly one entry with property ``"preferred": true``
and your entries have ``version`` properties that are parsable by the `compare-versions node module <https://www.npmjs.com/package/compare-versions>`__, for example:

.. code-block:: json

{
"name": "stable",
"version": "9.9.9",
"url": "https://anything",
"preferred": true
}

If you want similar functionality for *older* versions of your docs (i.e. those built before the ``show_version_warning_banner`` configuration option was available), you can manually add a banner by prepending the following HTML to all pages (be sure to replace ``URL_OF_STABLE_VERSION_OF_PROJECT`` with a valid URL, and adjust styling as desired):

.. code-block:: html

<div style="background-color: rgb(248, 215, 218); color: rgb(114, 28, 36); text-align: center;">
<div>
<div>This is documentation for <strong>an old version</strong>.
<a href="{{ URL_OF_STABLE_VERSION_OF_PROJECT }}" style="background-color: rgb(220, 53, 69); color: rgb(255, 255, 255); margin: 1rem; padding: 0.375rem 0.75rem; border-radius: 4px; display: inline-block; text-align: center;">Switch to stable version</a>
</div>
</div>
</div>
4 changes: 4 additions & 0 deletions docs/user_guide/version-dropdown.rst
@@ -1,3 +1,5 @@
.. _version-dropdowns:

Version switcher dropdowns
==========================

Expand Down Expand Up @@ -40,6 +42,8 @@ each can have the following fields:
- ``url``: the URL for this version.
- ``name``: an optional name to display in the switcher dropdown instead of the
version string (e.g., "latest", "stable", "dev", etc.).
- ``preferred``: an optional field that *should occur on at most one entry* in the JSON file.
It specifies which version is considered "latest stable", and is used to customize the message used on :ref:`version-warning-banners` (if they are enabled).

Here is an example JSON file:

Expand Down
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "6.1.2",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.2"
"bootstrap": "^5.2.2",
"compare-versions": "^5.0.3"
}
}
1 change: 1 addition & 0 deletions src/pydata_sphinx_theme/__init__.py
Expand Up @@ -273,6 +273,7 @@ def _remove_empty_templates(tname):
js = f"""
DOCUMENTATION_OPTIONS.theme_switcher_json_url = '{json_url}';
DOCUMENTATION_OPTIONS.theme_switcher_version_match = '{version_match}';
DOCUMENTATION_OPTIONS.show_version_warning_banner = {str(context["theme_show_version_warning_banner"]).lower()};
"""
app.add_js_file(None, body=js)

Expand Down
153 changes: 126 additions & 27 deletions src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js
@@ -1,5 +1,6 @@
// Define the custom behavior of the page
import { documentReady } from "./mixin";
import { compare, validate } from "compare-versions";

import "../styles/pydata-sphinx-theme.scss";

Expand Down Expand Up @@ -299,60 +300,67 @@ function checkPageExistsAndRedirect(event) {
location.href = otherDocsHomepage;
});

// ensure we don't follow the initial link
// ↓ this prevents the browser from following the href of the clicked node
// ↓ (which is fine because this function takes care of redirecting)
event.preventDefault();
}

/**
* Check if the corresponding url is absolute and make a absolute path from root if necessary
* Load and parse the version switcher JSON file from an absolute or relative URL.
*
* @param {string} url the url to check
* @param {string} url The URL to load version switcher entries from.
*/
async function fetchVersionSwitcherJSON(url) {
// first check if it's a valid URL
try {
var result = new URL(url);
} catch (err) {
// if not, assume relative path and fix accordingly
if (err instanceof TypeError) {
// workaround for redirects like https://pydata-sphinx-theme.readthedocs.io
// fetch() automatically follows redirects so it should work in every builder
// (RDT, GitHub actions, etc)
const origin = await fetch(window.location.origin, {
method: "HEAD",
});
// assume we got a relative path, and fix accordingly. But first, we need to
// use `fetch()` to follow redirects so we get the correct final base URL
const origin = await fetch(window.location.origin, { method: "HEAD" });
result = new URL(url, origin.url);
} else {
// something unexpected happened
throw err;
}
}

// load and return the JSON
const response = await fetch(result);
const data = await response.json();
return data;
}

// Populate the version switcher from the JSON config file
var versionSwitcherBtns = document.querySelectorAll(
".version-switcher__button"
);

if (versionSwitcherBtns.length) {
const data = await fetchVersionSwitcherJSON(
DOCUMENTATION_OPTIONS.theme_switcher_json_url
);
// Populate the version switcher from the JSON data
function populateVersionSwitcher(data, versionSwitcherBtns) {
const currentFilePath = `${DOCUMENTATION_OPTIONS.pagename}.html`;
versionSwitcherBtns.forEach((btn) => {
// Set empty strings by default so that these attributes exist and can be used in CSS selectors
btn.dataset["activeVersionName"] = "";
btn.dataset["activeVersion"] = "";
});
// create links to the corresponding page in the other docs versions
data.forEach((entry) => {
// in case there are multiple entries with the same version string, this helps us
// decide which entry's `name` to put on the button itself. Without this, it would
// always be the *last* version-matching entry; now it will be either the
// version-matching entry that is also marked as `"preferred": true`, or if that
// doesn't exist: the *first* version-matching entry.
data = data.map((entry) => {
// does this entry match the version that we're currently building/viewing?
entry.match =
entry.version == DOCUMENTATION_OPTIONS.theme_switcher_version_match;
entry.preferred = entry.preferred || false;
// if no custom name specified (e.g., "latest"), use version string
if (!("name" in entry)) {
entry.name = entry.version;
}
return entry;
});
const hasMatchingPreferredEntry = data
.map((entry) => entry.preferred && entry.match)
.some(Boolean);
var foundMatch = false;
// create links to the corresponding page in the other docs versions
data.forEach((entry) => {
// create the node
const anchor = document.createElement("a");
anchor.setAttribute("class", "list-group-item list-group-item-action py-1");
Expand All @@ -365,17 +373,20 @@ if (versionSwitcherBtns.length) {
// to apply CSS styling based on this information.
anchor.dataset["versionName"] = entry.name;
anchor.dataset["version"] = entry.version;
// replace dropdown button text with the preferred display name of
// this version, rather than using sphinx's {{ version }} variable.
// also highlight the dropdown entry for the currently-viewed
// version's entry
if (entry.version == DOCUMENTATION_OPTIONS.theme_switcher_version_match) {
// replace dropdown button text with the preferred display name of the
// currently-viewed version, rather than using sphinx's {{ version }} variable.
// also highlight the dropdown entry for the currently-viewed version's entry
let matchesAndIsPreferred = hasMatchingPreferredEntry && entry.preferred;
let matchesAndIsFirst =
!hasMatchingPreferredEntry && !foundMatch && entry.match;
if (matchesAndIsPreferred || matchesAndIsFirst) {
anchor.classList.add("active");
versionSwitcherBtns.forEach((btn) => {
btn.innerText = entry.name;
btn.dataset["activeVersionName"] = entry.name;
btn.dataset["activeVersion"] = entry.version;
});
foundMatch = true;
}
// There may be multiple version-switcher elements, e.g. one
// in a slide-over panel displayed on smaller screens.
Expand All @@ -392,6 +403,73 @@ if (versionSwitcherBtns.length) {
});
}

/*******************************************************************************
* Warning banner when viewing non-stable version of the docs.
*/

/**
* Show a warning banner when viewing a non-stable version of the docs.
*
* adapted 2023-06 from https://mne.tools/versionwarning.js, which was
* originally adapted 2020-05 from https://scikit-learn.org/versionwarning.js
*
* @param {Array} data The version data used to populate the switcher menu.
*/
function showVersionWarningBanner(data) {
const version = DOCUMENTATION_OPTIONS.VERSION;
// figure out what latest stable version is
var preferredEntries = data.filter((entry) => entry.preferred);
if (preferredEntries.length !== 1) {
const howMany = preferredEntries.length == 0 ? "No" : "Multiple";
throw new Error(
`[PST] ${howMany} versions marked "preferred" found in versions JSON`
);
}
const preferredVersion = preferredEntries[0].version;
const preferredURL = preferredEntries[0].url;
// if already on preferred version, nothing to do
const versionsAreComparable = validate(version) && validate(preferredVersion);
if (versionsAreComparable && compare(version, preferredVersion, "=")) {
return;
}
// now construct the warning banner
var outer = document.createElement("div");
const middle = document.createElement("div");
const inner = document.createElement("div");
const bold = document.createElement("strong");
const button = document.createElement("a");
// these classes exist since pydata-sphinx-theme v0.10.0
outer.classList = "bd-header-version-warning container-fluid";
middle.classList = "bd-header-announcement__content";
inner.classList = "sidebar-message";
button.classList =
"sd-btn sd-btn-danger sd-shadow-sm sd-text-wrap font-weight-bold ms-3 my-1 align-baseline";
button.href = `${preferredURL}${DOCUMENTATION_OPTIONS.pagename}.html`;
button.innerText = "Switch to stable version";
button.onclick = checkPageExistsAndRedirect;
// add the version-dependent text
inner.innerText = "This is documentation for an ";
const isDev =
version.includes("dev") ||
version.includes("rc") ||
version.includes("pre");
const newerThanPreferred =
versionsAreComparable && compare(version, preferredVersion, ">");
if (isDev || newerThanPreferred) {
bold.innerText = "unstable development version";
} else if (versionsAreComparable && compare(version, preferredVersion, "<")) {
bold.innerText = `old version (${version})`;
} else {
bold.innerText = `version ${version}`;
}
outer.appendChild(middle);
middle.appendChild(inner);
inner.appendChild(bold);
inner.appendChild(document.createTextNode("."));
inner.appendChild(button);
document.body.prepend(outer);
}

/*******************************************************************************
* MutationObserver to move the ReadTheDocs button
*/
Expand Down Expand Up @@ -423,6 +501,27 @@ function initRTDObserver() {
observer.observe(document.body, config);
}

// fetch the JSON version data (only once), then use it to populate the version
// switcher and maybe show the version warning bar
var versionSwitcherBtns = document.querySelectorAll(
".version-switcher__button"
);
const hasSwitcherMenu = versionSwitcherBtns.length > 0;
const hasVersionsJSON = DOCUMENTATION_OPTIONS.hasOwnProperty(
"theme_switcher_json_url"
);
const wantsWarningBanner = DOCUMENTATION_OPTIONS.show_version_warning_banner;

if (hasVersionsJSON && (hasSwitcherMenu || wantsWarningBanner)) {
const data = await fetchVersionSwitcherJSON(
DOCUMENTATION_OPTIONS.theme_switcher_json_url
);
populateVersionSwitcher(data, versionSwitcherBtns);
if (wantsWarningBanner) {
showVersionWarningBanner(data);
}
}

/*******************************************************************************
* Call functions after document loading.
*/
Expand Down