Skip to content

Commit 29fcd08

Browse files
authored
Version warningbar (#1354)
* update supporting files * improve code comments * do switcher loading as a function; add warning banner function * fix variable rename * cleanup after rebase * banner background color * docs * add TODO * be forgiving about version specs * update package-lock * more documentation * handle more prerelease indicators in version strings * Update docs/user_guide/announcements.rst * add HTML snippet to docs * unrelated formatting fix * disambiguate button message * use parsable version string for stable entry in JSON * update test for new color palette * handle Bokeh case elegantly
1 parent 1408fb0 commit 29fcd08

File tree

11 files changed

+215
-38
lines changed

11 files changed

+215
-38
lines changed

docs/_static/switcher.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[
22
{
3-
"name": "dev",
4-
"version": "latest",
3+
"version": "dev",
54
"url": "https://pydata-sphinx-theme.readthedocs.io/en/latest/"
65
},
76
{
87
"name": "0.13.3 (stable)",
9-
"version": "stable",
10-
"url": "https://pydata-sphinx-theme.readthedocs.io/en/stable/"
8+
"version": "v0.13.3",
9+
"url": "https://pydata-sphinx-theme.readthedocs.io/en/stable/",
10+
"preferred": true
1111
},
1212
{
1313
"name": "0.12.0",

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
# For local development, infer the version to match from the package.
105105
release = pydata_sphinx_theme.__version__
106106
if "dev" in release or "rc" in release:
107-
version_match = "latest"
107+
version_match = "dev"
108108
# We want to keep the relative reference if we are in dev mode
109109
# but we want the whole url if we are effectively in a released version
110110
json_url = "_static/switcher.json"
@@ -164,6 +164,7 @@
164164
"navbar_align": "left", # [left, content, right] For testing that the navbar items align properly
165165
"navbar_center": ["version-switcher", "navbar-nav"],
166166
"announcement": "https://raw.githubusercontent.com/pydata/pydata-sphinx-theme/main/docs/_templates/custom-template.html",
167+
"show_version_warning_banner": True,
167168
# "show_nav_level": 2,
168169
# "navbar_start": ["navbar-logo"],
169170
# "navbar_end": ["theme-switcher", "navbar-icon-links"],

docs/user_guide/announcements.rst

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ By default, the value of your ``html_theme_options["announcement"]`` will be ins
1515
For example, the following configuration adds a simple announcement.
1616

1717
.. code-block:: python
18+
:caption: conf.py
1819
1920
html_theme_options = {
2021
...
@@ -28,13 +29,57 @@ You can specify an arbitrary URL that will be used as the HTML source for your a
2829
When the page is loaded, JavaScript will attempt to fetch this HTML and insert it as-is into the announcement banner.
2930
This allows you to define a single HTML announcement that you can pull into multiple documentation sites or versions.
3031

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

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

3536
.. code-block:: python
37+
:caption: conf.py
3638
3739
html_theme_options = {
3840
...
3941
"announcement": "https://github.com/pydata/pydata-sphinx-theme/raw/main/docs/_templates/custom-template.html",
4042
}
43+
44+
45+
.. _version-warning-banners:
46+
47+
Version warning banners
48+
-----------------------
49+
50+
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``:
51+
52+
.. code-block:: python
53+
:caption: conf.py
54+
55+
html_theme_options = {
56+
...
57+
"show_version_warning_banner": True,
58+
}
59+
60+
.. important::
61+
62+
This functionality relies on the :ref:`version switcher <version-dropdowns>` to determine the version number of the latest stable release.
63+
*It will only work* if your version switcher ``.json`` has exactly one entry with property ``"preferred": true``
64+
and your entries have ``version`` properties that are parsable by the `compare-versions node module <https://www.npmjs.com/package/compare-versions>`__, for example:
65+
66+
.. code-block:: json
67+
68+
{
69+
"name": "stable",
70+
"version": "9.9.9",
71+
"url": "https://anything",
72+
"preferred": true
73+
}
74+
75+
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):
76+
77+
.. code-block:: html
78+
79+
<div style="background-color: rgb(248, 215, 218); color: rgb(114, 28, 36); text-align: center;">
80+
<div>
81+
<div>This is documentation for <strong>an old version</strong>.
82+
<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>
83+
</div>
84+
</div>
85+
</div>

docs/user_guide/version-dropdown.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _version-dropdowns:
2+
13
Version switcher dropdowns
24
==========================
35

@@ -40,6 +42,8 @@ each can have the following fields:
4042
- ``url``: the URL for this version.
4143
- ``name``: an optional name to display in the switcher dropdown instead of the
4244
version string (e.g., "latest", "stable", "dev", etc.).
45+
- ``preferred``: an optional field that *should occur on at most one entry* in the JSON file.
46+
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).
4347

4448
Here is an example JSON file:
4549

package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"dependencies": {
2222
"@fortawesome/fontawesome-free": "6.1.2",
2323
"@popperjs/core": "^2.11.6",
24-
"bootstrap": "^5.2.2"
24+
"bootstrap": "^5.2.2",
25+
"compare-versions": "^5.0.3"
2526
}
2627
}

src/pydata_sphinx_theme/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ def _remove_empty_templates(tname):
255255
js = f"""
256256
DOCUMENTATION_OPTIONS.theme_switcher_json_url = '{json_url}';
257257
DOCUMENTATION_OPTIONS.theme_switcher_version_match = '{version_match}';
258+
DOCUMENTATION_OPTIONS.show_version_warning_banner = {str(context["theme_show_version_warning_banner"]).lower()};
258259
"""
259260
app.add_js_file(None, body=js)
260261

src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js

Lines changed: 126 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Define the custom behavior of the page
22
import { documentReady } from "./mixin";
3+
import { compare, validate } from "compare-versions";
34

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

@@ -299,60 +300,67 @@ function checkPageExistsAndRedirect(event) {
299300
location.href = otherDocsHomepage;
300301
});
301302

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

306308
/**
307-
* Check if the corresponding url is absolute and make a absolute path from root if necessary
309+
* Load and parse the version switcher JSON file from an absolute or relative URL.
308310
*
309-
* @param {string} url the url to check
311+
* @param {string} url The URL to load version switcher entries from.
310312
*/
311313
async function fetchVersionSwitcherJSON(url) {
312314
// first check if it's a valid URL
313315
try {
314316
var result = new URL(url);
315317
} catch (err) {
316-
// if not, assume relative path and fix accordingly
317318
if (err instanceof TypeError) {
318-
// workaround for redirects like https://pydata-sphinx-theme.readthedocs.io
319-
// fetch() automatically follows redirects so it should work in every builder
320-
// (RDT, GitHub actions, etc)
321-
const origin = await fetch(window.location.origin, {
322-
method: "HEAD",
323-
});
319+
// assume we got a relative path, and fix accordingly. But first, we need to
320+
// use `fetch()` to follow redirects so we get the correct final base URL
321+
const origin = await fetch(window.location.origin, { method: "HEAD" });
324322
result = new URL(url, origin.url);
325323
} else {
324+
// something unexpected happened
326325
throw err;
327326
}
328327
}
329-
328+
// load and return the JSON
330329
const response = await fetch(result);
331330
const data = await response.json();
332331
return data;
333332
}
334333

335-
// Populate the version switcher from the JSON config file
336-
var versionSwitcherBtns = document.querySelectorAll(
337-
".version-switcher__button"
338-
);
339-
340-
if (versionSwitcherBtns.length) {
341-
const data = await fetchVersionSwitcherJSON(
342-
DOCUMENTATION_OPTIONS.theme_switcher_json_url
343-
);
334+
// Populate the version switcher from the JSON data
335+
function populateVersionSwitcher(data, versionSwitcherBtns) {
344336
const currentFilePath = `${DOCUMENTATION_OPTIONS.pagename}.html`;
345337
versionSwitcherBtns.forEach((btn) => {
346338
// Set empty strings by default so that these attributes exist and can be used in CSS selectors
347339
btn.dataset["activeVersionName"] = "";
348340
btn.dataset["activeVersion"] = "";
349341
});
350-
// create links to the corresponding page in the other docs versions
351-
data.forEach((entry) => {
342+
// in case there are multiple entries with the same version string, this helps us
343+
// decide which entry's `name` to put on the button itself. Without this, it would
344+
// always be the *last* version-matching entry; now it will be either the
345+
// version-matching entry that is also marked as `"preferred": true`, or if that
346+
// doesn't exist: the *first* version-matching entry.
347+
data = data.map((entry) => {
348+
// does this entry match the version that we're currently building/viewing?
349+
entry.match =
350+
entry.version == DOCUMENTATION_OPTIONS.theme_switcher_version_match;
351+
entry.preferred = entry.preferred || false;
352352
// if no custom name specified (e.g., "latest"), use version string
353353
if (!("name" in entry)) {
354354
entry.name = entry.version;
355355
}
356+
return entry;
357+
});
358+
const hasMatchingPreferredEntry = data
359+
.map((entry) => entry.preferred && entry.match)
360+
.some(Boolean);
361+
var foundMatch = false;
362+
// create links to the corresponding page in the other docs versions
363+
data.forEach((entry) => {
356364
// create the node
357365
const anchor = document.createElement("a");
358366
anchor.setAttribute("class", "list-group-item list-group-item-action py-1");
@@ -365,17 +373,20 @@ if (versionSwitcherBtns.length) {
365373
// to apply CSS styling based on this information.
366374
anchor.dataset["versionName"] = entry.name;
367375
anchor.dataset["version"] = entry.version;
368-
// replace dropdown button text with the preferred display name of
369-
// this version, rather than using sphinx's {{ version }} variable.
370-
// also highlight the dropdown entry for the currently-viewed
371-
// version's entry
372-
if (entry.version == DOCUMENTATION_OPTIONS.theme_switcher_version_match) {
376+
// replace dropdown button text with the preferred display name of the
377+
// currently-viewed version, rather than using sphinx's {{ version }} variable.
378+
// also highlight the dropdown entry for the currently-viewed version's entry
379+
let matchesAndIsPreferred = hasMatchingPreferredEntry && entry.preferred;
380+
let matchesAndIsFirst =
381+
!hasMatchingPreferredEntry && !foundMatch && entry.match;
382+
if (matchesAndIsPreferred || matchesAndIsFirst) {
373383
anchor.classList.add("active");
374384
versionSwitcherBtns.forEach((btn) => {
375385
btn.innerText = entry.name;
376386
btn.dataset["activeVersionName"] = entry.name;
377387
btn.dataset["activeVersion"] = entry.version;
378388
});
389+
foundMatch = true;
379390
}
380391
// There may be multiple version-switcher elements, e.g. one
381392
// in a slide-over panel displayed on smaller screens.
@@ -392,6 +403,73 @@ if (versionSwitcherBtns.length) {
392403
});
393404
}
394405

406+
/*******************************************************************************
407+
* Warning banner when viewing non-stable version of the docs.
408+
*/
409+
410+
/**
411+
* Show a warning banner when viewing a non-stable version of the docs.
412+
*
413+
* adapted 2023-06 from https://mne.tools/versionwarning.js, which was
414+
* originally adapted 2020-05 from https://scikit-learn.org/versionwarning.js
415+
*
416+
* @param {Array} data The version data used to populate the switcher menu.
417+
*/
418+
function showVersionWarningBanner(data) {
419+
const version = DOCUMENTATION_OPTIONS.VERSION;
420+
// figure out what latest stable version is
421+
var preferredEntries = data.filter((entry) => entry.preferred);
422+
if (preferredEntries.length !== 1) {
423+
const howMany = preferredEntries.length == 0 ? "No" : "Multiple";
424+
throw new Error(
425+
`[PST] ${howMany} versions marked "preferred" found in versions JSON`
426+
);
427+
}
428+
const preferredVersion = preferredEntries[0].version;
429+
const preferredURL = preferredEntries[0].url;
430+
// if already on preferred version, nothing to do
431+
const versionsAreComparable = validate(version) && validate(preferredVersion);
432+
if (versionsAreComparable && compare(version, preferredVersion, "=")) {
433+
return;
434+
}
435+
// now construct the warning banner
436+
var outer = document.createElement("div");
437+
const middle = document.createElement("div");
438+
const inner = document.createElement("div");
439+
const bold = document.createElement("strong");
440+
const button = document.createElement("a");
441+
// these classes exist since pydata-sphinx-theme v0.10.0
442+
outer.classList = "bd-header-version-warning container-fluid";
443+
middle.classList = "bd-header-announcement__content";
444+
inner.classList = "sidebar-message";
445+
button.classList =
446+
"sd-btn sd-btn-danger sd-shadow-sm sd-text-wrap font-weight-bold ms-3 my-1 align-baseline";
447+
button.href = `${preferredURL}${DOCUMENTATION_OPTIONS.pagename}.html`;
448+
button.innerText = "Switch to stable version";
449+
button.onclick = checkPageExistsAndRedirect;
450+
// add the version-dependent text
451+
inner.innerText = "This is documentation for an ";
452+
const isDev =
453+
version.includes("dev") ||
454+
version.includes("rc") ||
455+
version.includes("pre");
456+
const newerThanPreferred =
457+
versionsAreComparable && compare(version, preferredVersion, ">");
458+
if (isDev || newerThanPreferred) {
459+
bold.innerText = "unstable development version";
460+
} else if (versionsAreComparable && compare(version, preferredVersion, "<")) {
461+
bold.innerText = `old version (${version})`;
462+
} else {
463+
bold.innerText = `version ${version}`;
464+
}
465+
outer.appendChild(middle);
466+
middle.appendChild(inner);
467+
inner.appendChild(bold);
468+
inner.appendChild(document.createTextNode("."));
469+
inner.appendChild(button);
470+
document.body.prepend(outer);
471+
}
472+
395473
/*******************************************************************************
396474
* MutationObserver to move the ReadTheDocs button
397475
*/
@@ -423,6 +501,27 @@ function initRTDObserver() {
423501
observer.observe(document.body, config);
424502
}
425503

504+
// fetch the JSON version data (only once), then use it to populate the version
505+
// switcher and maybe show the version warning bar
506+
var versionSwitcherBtns = document.querySelectorAll(
507+
".version-switcher__button"
508+
);
509+
const hasSwitcherMenu = versionSwitcherBtns.length > 0;
510+
const hasVersionsJSON = DOCUMENTATION_OPTIONS.hasOwnProperty(
511+
"theme_switcher_json_url"
512+
);
513+
const wantsWarningBanner = DOCUMENTATION_OPTIONS.show_version_warning_banner;
514+
515+
if (hasVersionsJSON && (hasSwitcherMenu || wantsWarningBanner)) {
516+
const data = await fetchVersionSwitcherJSON(
517+
DOCUMENTATION_OPTIONS.theme_switcher_json_url
518+
);
519+
populateVersionSwitcher(data, versionSwitcherBtns);
520+
if (wantsWarningBanner) {
521+
showVersionWarningBanner(data);
522+
}
523+
}
524+
426525
/*******************************************************************************
427526
* Call functions after document loading.
428527
*/

0 commit comments

Comments
 (0)