diff --git a/python_docs_theme/__init__.py b/python_docs_theme/__init__.py index bbe1352..68bb9e3 100644 --- a/python_docs_theme/__init__.py +++ b/python_docs_theme/__init__.py @@ -52,12 +52,11 @@ def _html_page_context( def setup(app): current_dir = os.path.abspath(os.path.dirname(__file__)) - app.add_html_theme( - 'python_docs_theme', current_dir) + app.add_html_theme("python_docs_theme", current_dir) app.connect("html-page-context", _html_page_context) return { - 'parallel_read_safe': True, - 'parallel_write_safe': True, + "parallel_read_safe": True, + "parallel_write_safe": True, } diff --git a/python_docs_theme/static/copybutton.js b/python_docs_theme/static/copybutton.js index 16dee00..7367c4a 100644 --- a/python_docs_theme/static/copybutton.js +++ b/python_docs_theme/static/copybutton.js @@ -1,64 +1,92 @@ -$(document).ready(function() { - /* Add a [>>>] button on the top-right corner of code samples to hide +// ``function*`` denotes a generator in JavaScript, see +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* +function* getHideableCopyButtonElements(rootElement) { + // yield all elements with the "go" (Generic.Output), + // "gp" (Generic.Prompt), or "gt" (Generic.Traceback) CSS class + for (const el of rootElement.querySelectorAll('.go, .gp, .gt')) { + yield el + } + // tracebacks (.gt) contain bare text elements that need to be + // wrapped in a span to hide or show the element + for (let el of rootElement.querySelectorAll('.gt')) { + while ((el = el.nextSibling) && el.nodeType !== Node.DOCUMENT_NODE) { + // stop wrapping text nodes when we hit the next output or + // prompt element + if (el.nodeType === Node.ELEMENT_NODE && el.matches(".gp, .go")) { + break + } + // if the node is a text node with content, wrap it in a + // span element so that we can control visibility + if (el.nodeType === Node.TEXT_NODE && el.textContent.trim()) { + const wrapper = document.createElement("span") + el.after(wrapper) + wrapper.appendChild(el) + el = wrapper + } + yield el + } + } +} + + +const loadCopyButton = () => { + /* Add a [>>>] button in the top-right corner of code samples to hide * the >>> and ... prompts and the output and thus make the code * copyable. */ - var div = $('.highlight-python .highlight,' + - '.highlight-python3 .highlight,' + - '.highlight-pycon .highlight,' + - '.highlight-pycon3 .highlight,' + - '.highlight-default .highlight'); - var pre = div.find('pre'); + const hide_text = "Hide the prompts and output" + const show_text = "Show the prompts and output" - // get the styles from the current theme - pre.parent().parent().css('position', 'relative'); - var hide_text = 'Hide the prompts and output'; - var show_text = 'Show the prompts and output'; - var border_width = pre.css('border-top-width'); - var border_style = pre.css('border-top-style'); - var border_color = pre.css('border-top-color'); - var button_styles = { - 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', - 'border-color': border_color, 'border-style': border_style, - 'border-width': border_width, 'color': border_color, 'text-size': '75%', - 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', - 'border-radius': '0 3px 0 0' + const button = document.createElement("span") + button.classList.add("copybutton") + button.innerText = ">>>" + button.title = hide_text + button.dataset.hidden = "false" + const buttonClick = event => { + // define the behavior of the button when it's clicked + event.preventDefault() + const buttonEl = event.currentTarget + const codeEl = buttonEl.nextElementSibling + if (buttonEl.dataset.hidden === "false") { + // hide the code output + for (const el of getHideableCopyButtonElements(codeEl)) { + el.hidden = true + } + buttonEl.title = show_text + buttonEl.dataset.hidden = "true" + } else { + // show the code output + for (const el of getHideableCopyButtonElements(codeEl)) { + el.hidden = false + } + buttonEl.title = hide_text + buttonEl.dataset.hidden = "false" + } } + const highlightedElements = document.querySelectorAll( + ".highlight-python .highlight," + + ".highlight-python3 .highlight," + + ".highlight-pycon .highlight," + + ".highlight-pycon3 .highlight," + + ".highlight-default .highlight" + ) + // create and add the button to all the code blocks that contain >>> - div.each(function(index) { - var jthis = $(this); - if (jthis.find('.gp').length > 0) { - var button = $('>>>'); - button.css(button_styles) - button.attr('title', hide_text); - button.data('hidden', 'false'); - jthis.prepend(button); - } - // tracebacks (.gt) contain bare text elements that need to be - // wrapped in a span to work with .nextUntil() (see later) - jthis.find('pre:has(.gt)').contents().filter(function() { - return ((this.nodeType == 3) && (this.data.trim().length > 0)); - }).wrap(''); - }); + highlightedElements.forEach(el => { + el.style.position = "relative" - // define the behavior of the button when it's clicked - $('.copybutton').click(function(e){ - e.preventDefault(); - var button = $(this); - if (button.data('hidden') === 'false') { - // hide the code output - button.parent().find('.go, .gp, .gt').hide(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); - button.css('text-decoration', 'line-through'); - button.attr('title', show_text); - button.data('hidden', 'true'); - } else { - // show the code output - button.parent().find('.go, .gp, .gt').show(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); - button.css('text-decoration', 'none'); - button.attr('title', hide_text); - button.data('hidden', 'false'); + // if we find a console prompt (.gp), prepend the (deeply cloned) button + const clonedButton = button.cloneNode(true) + // the onclick attribute is not cloned, set it on the new element + clonedButton.onclick = buttonClick + if (el.querySelector(".gp") !== null) { + el.prepend(clonedButton) } - }); -}); + }) +} + +if (document.readyState !== "loading") { + loadCopyButton() +} else { + document.addEventListener("DOMContentLoaded", loadCopyButton) +} diff --git a/python_docs_theme/static/menu.js b/python_docs_theme/static/menu.js index b2eabb3..e233585 100644 --- a/python_docs_theme/static/menu.js +++ b/python_docs_theme/static/menu.js @@ -1,55 +1,57 @@ -document.addEventListener('DOMContentLoaded', function () { +document.addEventListener("DOMContentLoaded", function () { // Make tables responsive by wrapping them in a div and making them scrollable - const tables = document.querySelectorAll('table.docutils'); + const tables = document.querySelectorAll("table.docutils") tables.forEach(function(table){ - table.outerHTML = '
' + table.outerHTML + '
' - }); + table.outerHTML = '
' + table.outerHTML + "
" + }) - const togglerInput = document.querySelector('.toggler__input'); - const togglerLabel = document.querySelector('.toggler__label'); - const sideMenu = document.querySelector('.menu-wrapper'); - const menuItems = document.querySelectorAll('.menu') - const doc = document.querySelector('.document'); - const body = document.querySelector('body'); + const togglerInput = document.querySelector(".toggler__input") + const togglerLabel = document.querySelector(".toggler__label") + const sideMenu = document.querySelector(".menu-wrapper") + const menuItems = document.querySelectorAll(".menu") + const doc = document.querySelector(".document") + const body = document.querySelector("body") function closeMenu() { - togglerInput.checked = false; - sideMenu.setAttribute("aria-expanded", 'false'); - sideMenu.setAttribute('aria-hidden', 'true'); - togglerLabel.setAttribute('aria-pressed', 'false'); - body.style.overflow = 'visible'; + togglerInput.checked = false + sideMenu.setAttribute("aria-expanded", "false") + sideMenu.setAttribute("aria-hidden", "true") + togglerLabel.setAttribute("aria-pressed", "false") + body.style.overflow = "visible" } function openMenu() { - togglerInput.checked = true; - sideMenu.setAttribute("aria-expanded", 'true'); - sideMenu.setAttribute('aria-hidden', 'false'); - togglerLabel.setAttribute('aria-pressed', 'true'); - body.style.overflow = 'hidden'; + togglerInput.checked = true + sideMenu.setAttribute("aria-expanded", "true") + sideMenu.setAttribute("aria-hidden", "false") + togglerLabel.setAttribute("aria-pressed", "true") + body.style.overflow = "hidden" } // Close menu when link on the sideMenu is clicked - sideMenu.addEventListener('click', function (event) { - let target = event.target; - if (target.tagName.toLowerCase() !== 'a') return; - closeMenu(); + sideMenu.addEventListener("click", function (event) { + let target = event.target + if (target.tagName.toLowerCase() !== "a") { + return + } + closeMenu() }) // Add accessibility data when sideMenu is opened/closed - togglerInput.addEventListener('change', function (e) { - togglerInput.checked ? openMenu() : closeMenu(); - }); + togglerInput.addEventListener("change", function (_event) { + togglerInput.checked ? openMenu() : closeMenu() + }) // Make sideMenu links tabbable only when visible for(let menuItem of menuItems) { if(togglerInput.checked) { - menuItem.setAttribute('tabindex', '0'); + menuItem.setAttribute("tabindex", "0") } else { - menuItem.setAttribute('tabindex', '-1'); + menuItem.setAttribute("tabindex", "-1") } } // Close sideMenu when document body is clicked - doc.addEventListener('click', function () { + doc.addEventListener("click", function () { if (togglerInput.checked) { - closeMenu(); + closeMenu() } }) }) \ No newline at end of file diff --git a/python_docs_theme/static/pydoctheme.css b/python_docs_theme/static/pydoctheme.css index 6c141f8..5c0d394 100644 --- a/python_docs_theme/static/pydoctheme.css +++ b/python_docs_theme/static/pydoctheme.css @@ -104,10 +104,12 @@ div.sphinxsidebar h4 { } div.sphinxsidebarwrapper { + width: 217px; box-sizing: border-box; height: 100%; overflow-x: hidden; overflow-y: auto; + float: left; } div.sphinxsidebarwrapper > h3:first-child { @@ -135,6 +137,33 @@ div.sphinxsidebar input[type='text'] { max-width: 150px; } +#sidebarbutton { + /* Sphinx 4.x and earlier compat */ + height: 100%; + background-color: #CCCCCC; + margin-left: 0; + color: #444444; + font-size: 1.2em; + cursor: pointer; + padding-top: 1px; + float: right; + display: table; + /* after Sphinx 4.x and earlier is dropped, only the below is needed */ + width: 12px; + border-radius: 0 5px 5px 0; + border-left: none; +} + +#sidebarbutton span { + /* Sphinx 4.x and earlier compat */ + display: table-cell; + vertical-align: middle; +} + +#sidebarbutton:hover { + background-color: #AAAAAA; +} + div.body { padding: 0 0 0 1.2em; } @@ -279,6 +308,26 @@ div.genindex-jumpbox a { text-align: center; } +.copybutton { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + text-size: 75%; + font-family: monospace; + padding-left: 0.2em; + padding-right: 0.2em; + border-radius: 0 3px 0 0; + color: #ac9; /* follows div.body pre */ + border-color: #ac9; /* follows div.body pre */ + border-style: solid; /* follows div.body pre */ + border-width: 1px; /* follows div.body pre */ +} + +.copybutton[data-hidden='true'] { + text-decoration: line-through; +} + @media (max-width: 1023px) { /* Body layout */ div.body { diff --git a/python_docs_theme/static/sidebar.js b/python_docs_theme/static/sidebar.js index 6b5c694..70886d4 100644 --- a/python_docs_theme/static/sidebar.js +++ b/python_docs_theme/static/sidebar.js @@ -2,142 +2,83 @@ * sidebar.js * ~~~~~~~~~~ * - * This script makes the Sphinx sidebar collapsible. This is a slightly - * modified version of Sphinx's own sidebar.js. + * This file is functionally identical to "sidebar.js" in Sphinx 5.0. + * When support for Sphinx 4 and earlier is dropped from the theme, + * this file can be removed. * - * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds in - * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to - * collapse and expand the sidebar. + * This script makes the Sphinx sidebar collapsible. * - * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the - * width of the sidebar and the margin-left of the document are decreased. - * When the sidebar is expanded the opposite happens. This script saves a - * per-browser/per-session cookie used to remember the position of the sidebar - * among the pages. Once the browser is closed the cookie is deleted and the - * position reset to the default (expanded). + * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds + * in .sphinxsidebar, after .sphinxsidebarwrapper, the #sidebarbutton + * used to collapse and expand the sidebar. * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden + * and the width of the sidebar and the margin-left of the document + * are decreased. When the sidebar is expanded the opposite happens. + * This script saves a per-browser/per-session cookie used to + * remember the position of the sidebar among the pages. + * Once the browser is closed the cookie is deleted and the position + * reset to the default (expanded). + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ -$(function() { +const initialiseSidebar = () => { // global elements used by the functions. - // the 'sidebarbutton' element is defined as global after its - // creation, in the add_sidebar_button function - var bodywrapper = $('.bodywrapper'); - var sidebar = $('.sphinxsidebar'); - var sidebarwrapper = $('.sphinxsidebarwrapper'); - - // original margin-left of the bodywrapper and width of the sidebar - // with the sidebar expanded - var bw_margin_expanded = bodywrapper.css('margin-left'); - var ssb_width_expanded = sidebar.width(); - - // margin-left of the bodywrapper and width of the sidebar - // with the sidebar collapsed - var bw_margin_collapsed = '.8em'; - var ssb_width_collapsed = '.8em'; - - // colors used by the current theme - var dark_color = '#AAAAAA'; - var light_color = '#CCCCCC'; + const bodyWrapper = document.getElementsByClassName("bodywrapper")[0] + const sidebar = document.getElementsByClassName("sphinxsidebar")[0] + const sidebarWrapper = document.getElementsByClassName("sphinxsidebarwrapper")[0] - function sidebar_is_collapsed() { - return sidebarwrapper.is(':not(:visible)'); + // exit early if the document has no sidebar for some reason + if (typeof sidebar === "undefined") { + return } - function toggle_sidebar() { - if (sidebar_is_collapsed()) - expand_sidebar(); - else - collapse_sidebar(); - } + // create the sidebar button element + const sidebarButton = document.createElement("div") + sidebarButton.id = "sidebarbutton" + // create the sidebar button arrow element + const sidebarArrow = document.createElement("span") + sidebarArrow.innerText = "«" + sidebarButton.appendChild(sidebarArrow) + sidebar.appendChild(sidebarButton) - function collapse_sidebar() { - sidebarwrapper.hide(); - sidebar.css('width', ssb_width_collapsed); - bodywrapper.css('margin-left', bw_margin_collapsed); - sidebarbutton.css({ - 'margin-left': '0', - 'border-radius': '5px' - }); - sidebarbutton.find('span').text('»'); - sidebarbutton.attr('title', _('Expand sidebar')); - document.cookie = 'sidebar=collapsed'; + const collapse_sidebar = () => { + bodyWrapper.style.marginLeft = ".8em" + sidebar.style.width = ".8em" + sidebarWrapper.style.display = "none" + sidebarArrow.innerText = "»" + sidebarButton.title = _("Expand sidebar") + window.localStorage.setItem("sidebar", "collapsed") } - function expand_sidebar() { - bodywrapper.css('margin-left', bw_margin_expanded); - sidebar.css('width', ssb_width_expanded); - sidebarwrapper.show(); - sidebarbutton.css({ - 'margin-left': ssb_width_expanded-12, - 'border-radius': '0 5px 5px 0' - }); - sidebarbutton.find('span').text('«'); - sidebarbutton.attr('title', _('Collapse sidebar')); - document.cookie = 'sidebar=expanded'; + const expand_sidebar = () => { + bodyWrapper.style.marginLeft = "" + sidebar.style.removeProperty("width") + sidebarWrapper.style.display = "" + sidebarArrow.innerText = "«" + sidebarButton.title = _("Collapse sidebar") + window.localStorage.setItem("sidebar", "expanded") } - function add_sidebar_button() { - sidebarwrapper.css({ - 'float': 'left', - 'margin-right': '0', - 'width': ssb_width_expanded - 13 - }); - // create the button - sidebar.append( - '
«
' - ); - var sidebarbutton = $('#sidebarbutton'); - sidebarbutton.find('span').css({ - 'display': 'block', - 'position': 'fixed', - 'top': '50%' - }); - - sidebarbutton.click(toggle_sidebar); - sidebarbutton.attr('title', _('Collapse sidebar')); - sidebarbutton.css({ - 'border-radius': '0 5px 5px 0', - 'color': '#444444', - 'background-color': '#CCCCCC', - 'font-size': '1.2em', - 'cursor': 'pointer', - 'height': '100%', - 'padding-left': '1px', - 'margin-left': ssb_width_expanded - 12 - }); + sidebarButton.addEventListener("click", () => { + (sidebarWrapper.style.display === "none") ? expand_sidebar() : collapse_sidebar() + }) - sidebarbutton.hover( - function () { - $(this).css('background-color', dark_color); - }, - function () { - $(this).css('background-color', light_color); - } - ); + const sidebar_state = window.localStorage.getItem("sidebar") + if (sidebar_state === "collapsed") { + collapse_sidebar() } - - function set_position_from_cookie() { - if (!document.cookie) - return; - var items = document.cookie.split(';'); - for(var k=0; k