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

✨ NEW: add version switcher #436

Merged
merged 28 commits into from Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
84d0562
add version switcher (cleaner diff this time)
drammock Jul 28, 2021
5f4a023
remove unreachable ancient versions
drammock Aug 10, 2021
93b0aa0
fallback on version if name missing
drammock Aug 10, 2021
e913a48
simplify redundancies in switcher.json
drammock Aug 10, 2021
cefafeb
separate out version switcher
drammock Sep 3, 2021
0f12757
remove language-related stuff
drammock Oct 25, 2021
0ff914e
add recent versions to JSON
drammock Oct 25, 2021
852cc4e
do AJAX onclick, not in advance
drammock Oct 25, 2021
ac2a57b
use href instead of data-href
drammock Oct 26, 2021
26e5988
better comments
drammock Oct 27, 2021
0328868
update the dropdown button text
drammock Oct 27, 2021
2a38901
fix url shown on hover
drammock Oct 27, 2021
768a76e
fix template URL
drammock Oct 27, 2021
5318996
change display name "latest" → "stable"
drammock Oct 27, 2021
ab261a3
add ID to div
drammock Oct 28, 2021
def2d67
document the switcher
drammock Oct 28, 2021
d968eef
change default button text to be sphinx "version" variable
drammock Oct 28, 2021
fec276c
add comment about JS needing jinja
drammock Oct 30, 2021
24da28e
doc rewrite
drammock Nov 1, 2021
407159d
better variable name, remove vars from theme.conf, style current vers…
drammock Nov 1, 2021
f009b9d
minor cleanups
drammock Nov 1, 2021
87c0dc9
use html_theme_options instead of html_context
drammock Nov 1, 2021
191a847
fix copy-paste error
drammock Nov 1, 2021
e683a50
better code comments
drammock Nov 1, 2021
3258028
make switcher config a single dict
drammock Nov 2, 2021
9ad61e2
Merge remote-tracking branch 'upstream/master' into redo-switcher
jorisvandenbossche Dec 3, 2021
a010d24
switch url
jorisvandenbossche Dec 3, 2021
865ed64
prettier json
jorisvandenbossche Dec 3, 2021
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
51 changes: 51 additions & 0 deletions docs/_static/switcher.json
@@ -0,0 +1,51 @@
[
drammock marked this conversation as resolved.
Show resolved Hide resolved
{
"name": "v0.7.1 (stable)",
"version": "0.7.1"
},
{
"version": "0.7.0"
},
{
"version": "0.6.3"
},
{
"version": "0.6.2"
},
{
"version": "0.6.1"
},
{
"version": "0.6.0"
},
{
"version": "0.5.2"
},
{
"version": "0.5.1"
},
{
"version": "0.5.0"
},
{
"version": "0.4.3"
},
{
"version": "0.4.2"
},
{
"version": "0.4.1"
},
{
"version": "0.4.0"
},
{
"version": "0.3.2"
},
{
"version": "0.3.1"
},
{
"version": "0.3.0"
}
]
11 changes: 9 additions & 2 deletions docs/conf.py
Expand Up @@ -24,7 +24,8 @@

import pydata_sphinx_theme

version = pydata_sphinx_theme.__version__.replace("dev0", "")
release = pydata_sphinx_theme.__version__
version = release.replace("dev0", "")

# -- General configuration ---------------------------------------------------

Expand Down Expand Up @@ -96,8 +97,14 @@
# "navbar_align": "left", # [left, content, right] For testing that the navbar items align properly
# "navbar_start": ["navbar-logo", "navbar-version"],
# "navbar_center": ["navbar-nav", "navbar-version"], # Just for testing
# "navbar_end": ["navbar-icon-links", "navbar-version"] # Just for testing
"navbar_end": ["version-switcher", "navbar-icon-links"],
# "footer_items": ["copyright", "sphinx-version", ""]
"switcher": {
"json_url": "/_static/switcher.json",
# "json_url": "https://pydata-sphinx-theme.readthedocs.io/en/latest/_static/switcher.json",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the "last change before merge" suggestion (because it will break the switcher functionality on the CI PR build)

Suggested change
"json_url": "/_static/switcher.json",
# "json_url": "https://pydata-sphinx-theme.readthedocs.io/en/latest/_static/switcher.json",
"json_url": "https://pydata-sphinx-theme.readthedocs.io/en/latest/_static/switcher.json",

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump? I think I've adressed all feedback here, and all that remains is for someone with merge rights to sign off on the new section in the rendered docs, and if they're happy, apply this suggestion and then merge.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder to apply this suggestion before merging; it will break the switcher in the PR doc build but is needed for the switcher to work on the real docs.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understood it right or if I am missing anything in this lengthy context. Can someone please help me with the latest repo with both versions and language drop-down implementation for the RTD theme in Sphinx? @drammock , @astrojuanlu, or @humitos

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sachin-Suresh I'm not sure to understand what is your question. I'd recommend you to open a new issue with a longer and detailed description. Otherwise, it'd be hard to parse your question in a PR that has ~150 comments already and 1 year old.

"url_template": "https://pydata-sphinx-theme.readthedocs.io/en/v{version}/",
"version_match": version,
},
}


Expand Down
192 changes: 192 additions & 0 deletions docs/user_guide/configuring.rst
Expand Up @@ -277,6 +277,198 @@ at the bottom. You can hide these buttons with the following configuration:
}


Add a dropdown to switch between docs versions
==============================================

You can add a button to your site that allows users to
switch between versions of your documentation. The links in the version
switcher will differ depending on which page of the docs is being viewed. For
example, on the page ``https://mysite.org/en/v2.0/changelog.html``, the
switcher links will go to ``changelog.html`` in the other versions of your
docs. When clicked, the switcher will check for the existence of that page, and
if it doesn't exist, redirect to the homepage of that docs version instead.

The switcher requires the following configuration steps:

1. Add a JSON file containing a list of the documentation versions that the
switcher should show on each page.

2. Add a configuration dictionary called ``switcher`` to the
``html_theme_options`` dict in ``conf.py``. ``switcher`` should have 3 keys:

- ``json_url``: the persistent location of the JSON file described above.
- ``url_template``: a template string used to generate the correct URLs for
the different documentation versions.
- ``version_match``: a string stating the version of the documentation that
is currently being browsed.

3. Specify where to place the switcher in your page layout. For example, add
the ``"version-switcher"`` template to one of the layout lists in
``html_theme_options`` (e.g., ``navbar_end``, ``footer_items``, etc).

Below is a more in-depth description of each of these configuration steps.


Add a JSON file to define your switcher's versions
--------------------------------------------------

First, write a JSON file stating which versions of your docs will be listed in
the switcher's dropdown menu. That file should contain a list of entries that
each have one or two fields:

- ``version``: a version string. This will be inserted into
``switcher['url_template']`` to create the links to other docs versions, and
also checked against ``switcher['version_match']`` to provide styling to the
switcher.
- ``name``: an optional name to display in the switcher dropdown instead of the
version string (e.g., "latest", "stable", "dev", etc).

Here is an example JSON file:

.. code:: json

[
{
"name": "v2.1 (stable)",
"version": "2.1"
},
{
"version": "2.0"
},
{
"version": "1.0"
},
]

See the discussion of ``switcher['json_url']`` (below) for options of where to
save the JSON file.


Configure ``switcher['json_url']``
----------------------------------

The JSON file needs to be at a stable, persistent, fully-resolved URL (i.e.,
not specified as a path relative to the sphinx root of the current doc build).
Each version of your documentation should point to the same URL, so that as new
versions are added to the JSON file all the older versions of the docs will
gain switcher dropdown entries linking to the new versions. This could be done
a few different ways:

- The location could be one that is always associated with the most recent
documentation build (i.e., if your docs server aliases "latest" to the most
recent version, it could point to a location in the build tree of version
"latest"). For example:

.. code:: python

html_theme_options = {
...,
"switcher": {
"json_url": "https://mysite.org/en/latest/_static/switcher.json",
}
}

In this case the JSON is versioned alongside the rest of the docs pages but
only the most recent version is ever loaded (even by older versions of the
docs).

- The JSON could be saved in a folder that is listed under your site's
``html_static_path`` configuration. See `the Sphinx static path documentation
<https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path>`_
for more information.

- The JSON could be stored outside the doc build trees. This probably means it
would be outside the software repo, and would require you to add new version
entries to the JSON file manually as part of your release process. Example:

.. code:: python

html_theme_options = {
...,
"switcher": {
"json_url": "https://mysite.org/switcher.json",
}
}


Configure ``switcher['url_template']``
--------------------------------------

The switcher's links to other versions of your docs are made by combining the
*version strings* from the JSON file with a *template string* you provide in
``switcher['url_template']``. The template string must contain a placeholder
``{version}`` and otherwise be a fully-resolved URL. For example:

.. code:: python

html_theme_options = {
...,
"switcher": {
"url_template": "https://mysite.org/en/version-{version}/",
}
}

The example above will result in a link to
``https://mysite.org/en/version-1.0/`` for the JSON entry for version
``"1.0"``.


Configure ``switcher['version_match']``
---------------------------------------

This configuration value tells the switcher what docs version is currently
being viewed, and is used to style the switcher (i.e., to highlight the current
docs version in the switcher's dropdown menu, and to change the text displayed
on the switcher button).

Typically you can re-use one of the sphinx variables ``version``
or ``release`` as the value of ``switcher['version_match']``; which one you use
depends on how granular your docs versioning is. See
`the Sphinx "project info" documentation
<https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information>`__
for more information). Example:

.. code:: python

version = my_package_name.__version__.replace("dev0", "") # may differ
html_theme_options = {
...,
"switcher": {
"version_match": version,
}
}


Specify where to display the switcher
-------------------------------------

Finally, tell the theme where on your site's pages you want the switcher to
appear. There are many choices here: you can add ``"version-switcher"`` to one
of the locations in ``html_theme_options`` (e.g., ``navbar_end``,
``footer_items``, etc). For example:

.. code:: python

html_theme_options = {
...,
"navbar_end": ["version-switcher"]
}


Alternatively, you could override one of the other templates to include the
version switcher in a sidebar. For example, you could define
``_templates/sidebar-nav-bs.html`` as:

.. code:: jinja

{%- include 'version-switcher.html' -%}
{{ super() }}

to insert a version switcher at the top of the left sidebar, while still
keeping the default navigation below it. See :doc:`sections` for more
information.


Add an Edit this Page button
============================

Expand Down
5 changes: 3 additions & 2 deletions docs/user_guide/sections.rst
Expand Up @@ -11,7 +11,7 @@ You can choose which templates show up in each section, as well as the order in
which they appear. This page describes the major areas that you can customize.

.. note::

When configuring templates in each section, you may omit the ``.html``
suffix after each template if you wish.

Expand Down Expand Up @@ -118,6 +118,7 @@ will be named accordingly).
- ``sidebar-ethical-ads.html``
- ``sidebar-nav-bs.html``
- ``sphinx-version.html``
- ``version-switcher.html``

Add your own HTML templates to theme sections
=============================================
Expand All @@ -136,7 +137,7 @@ could do so with the following steps:
{{ version }}

1. Now add the file to your menu items for one of the sections above. For example:

.. code-block:: python

html_theme_options = {
Expand Down
79 changes: 79 additions & 0 deletions pydata_sphinx_theme/_templates/version-switcher.html
@@ -0,0 +1,79 @@
<div class="dropdown" id="version_switcher">
<button type="button" class="btn btn-primary btn-sm navbar-btn dropdown-toggle" id="version_switcher_button" data-toggle="dropdown">
{{ theme_switcher.get('version_match') }} <!-- this text may get changed later by javascript -->
<span class="caret"></span>
</button>
<div id="version_switcher_menu" class="dropdown-menu list-group-flush py-0" aria-labelledby="version_switcher_button">
<!-- dropdown will be populated by javascript on page load -->
</div>
</div>

<!-- NOTE: this JS must live here (not in our global JS file) because it relies
on being processed by Jinja before it is run (specifically for replacing
variables {{ pagename }} and {{ theme_switcher }}.
-->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should somehow address this one before merging the PR, @drammock.

We should try to move most of the JS to the src/js/index.js file. In general, I believe we should minimize the amount of JS code in the template as far as we can. And we can borrow some ideas from #433 to execute that idea. You can see they are only adding JS code in the template to "persist" the information about pagename and theme_swticher. They are doing that using the window object. We may use the same approach or maybe some other ones such as data-* attributes that could be more specific? ones (instead of polluting the window namespace) and it might play better with the template...
But the important piece, whatever implementation you think about, is the idea/aim to minimize the JS code in the template and group that code in the "natural" JS place we currently have, IMHO.

@drammock, I know this is a long-lasting PR but would you be willing to make this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like many (most?) contributors here, I am not a JS developer, I am a python developer who knows just enough JS to get the job done, and figure out a lot as I go along. I invite/encourage you to push commits to this branch if you have a clear idea of how to do what you suggest. Otherwise I can say that I will intend to do as you suggest, but offer no promises as to the timeline because I have no way of estimating how long it will take me to figure it out / get it right, which means I'm less likely to try to squeeze it in when I have just a few minutes to spare.

If that's not acceptable, another option is to merge now (once merge conflict is fixed, I see this PR has again gone stale) and migrate the JS in a follow-up PR, either by me (when I get around to it) or by anyone else who is inspired.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I think it would indeed be good to have our JS in src/js/index.js, but if that makes this quite a bit more complicated, I also think that practicality beats purity in this case.

Now, from the explanation above, it seems there is a possible solution already. But at the same time, this is in the end also only an implementation detail, not impacting in any way how the end user uses/configures this. So I would say that this therefore is also perfectly well suited as a follow-up (given how long this already has taken).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback @drammock and @jorisvandenbossche!
A follow-up PR seems sensible to me (btw, I had to ask you first @drammock, in case you wanted to do it 😉 ).


<script type="text/javascript">
drammock marked this conversation as resolved.
Show resolved Hide resolved
// Construct the target URL from the JSON components
function buildURL(entry) {
var template = "{{ theme_switcher.get('url_template') }}"; // supplied by jinja
template = template.replace("{version}", entry.version);
return template;
}

// Check if corresponding page path exists in other version of docs
// and, if so, go there instead of the homepage of the other docs version
function checkPageExistsAndRedirect(event) {
const currentFilePath = "{{ pagename }}.html",
tryUrl = event.target.getAttribute("href");
let otherDocsHomepage = tryUrl.replace(currentFilePath, "");
$.ajax({
type: 'HEAD',
url: tryUrl,
// if the page exists, go there
success: function() {
location.href = tryUrl;
}
}).fail(function() {
location.href = otherDocsHomepage;
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested (via console.log()) to make sure that tryURL is indeed trying the correct URL, and in my local testing using nox -s docs-live, this all works (my browser console shows a 200 response to the head request, but then the call is flagged as failing anyway because of the CORS policy on the readthedocs server). The same happens with the PR test build (https://pydata-sphinx-theme--436.org.readthedocs.build/en/436/demo/index.html) because it's still a cross-domain AJAX request. I'm fairly sure that, once merged, these calls will succeed and the version switching will go to the corresponding page in the other docs version (if it exists), rather than the docs homepage for that version. However, it would still be better (if possible) if somebody who has the relevant permissions could enable cross-domain CORS requests at least for https://pydata-sphinx-theme--*.org.readthedocs.build (this won't make it work for testing with localhost, but it should make it work for PR test builds).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am out of my depth here, not being very familiar with CORS and those topics. But I am not sure there is something we can enable ourselves?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am out of my depth here, not being very familiar with CORS and those topics

that makes 2 of us. Personally I would say this: the version switcher still works (you can still switch between docs versions) even in the face of these cross-domain requests getting blocked. In theory, once this PR is merged and live, the requests won't be cross-domain anymore and the behavior should improve (links go to corresponding pages in other doc versions), but even if the behavior doesn't improve, at least we won't be in a broken state (the switcher will still go to the homepage of the correct docs version). So if a conversation with the RTD folks about CORS configuration is too bothersome (or in case they're unwilling to change server config in this way), let's just merge as-is for now and let the live doc build (not the PR build) be our test case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me on giving this a merge and seeing what happens :-)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to play and test this PR locally with CORS disabled (btw, I do not recommend disabling sec features in the browser unless you know what you are doing) and I can confirm this seems to be working as expected: it redirects to the proper path in the previous (whatever you choose) version of the docs.
At the time to dev/test this sort of issue, there might be other "solutions" using some proxies: https://github.com/Rob--W/cors-anywhere, but it is probably too much stuff for the thing we need to test here (this is why I went wild with an insecure browser test in a controlled environment 😉 ).

// this prevents the browser from following the href of the clicked node
// (which is fine because this function takes care of redirecting)
return false;
}

// Populate the version switcher from the JSON config file
(function () {
$.getJSON("{{ theme_switcher.get('json_url') }}", function(data, textStatus, jqXHR) {
const currentFilePath = "{{ pagename }}.html";
// create links to the corresponding page in the other docs versions
$.each(data, function(index, entry) {
// if no custom name specified (e.g., "latest"), use version string
if (!("name" in entry)) {
entry.name = entry.version;
}
// create the node
const node = document.createElement("a");
node.setAttribute("class", "list-group-item list-group-item-action py-1");
node.textContent = `${entry.name}`;
// get the base URL for that doc version, add the current page's
// path to it, and set as `href`
entry.url = buildURL(entry);
node.setAttribute("href", `${entry.url}${currentFilePath}`);
// on click, AJAX calls will check if the linked page exists before
// trying to redirect, and if not, will redirect to the homepage
// for that version of the docs.
node.onclick = checkPageExistsAndRedirect;
$("#version_switcher_menu").append(node);
// 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 == "{{ theme_switcher.get('version_match') }}") {
node.classList.add("active");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@choldgraf I'm just calling attention to this line as it's a new idea that occurred to me, not something anyone requested. In the switcher dropdown menu, it highlights the entry for the currently-being-viewed version.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it :-)

$("#version_switcher_button").text(entry.name);
}
});
});
})();
</script>
3 changes: 2 additions & 1 deletion pydata_sphinx_theme/theme.conf
Expand Up @@ -31,4 +31,5 @@ navbar_start = navbar-logo.html
navbar_center = navbar-nav.html
navbar_end = navbar-icon-links.html
footer_items = copyright.html, sphinx-version.html
page_sidebar_items = page-toc.html, edit-this-page.html
page_sidebar_items = page-toc.html, edit-this-page.html
switcher =