[IMP] extensions: add content tabs (backport of cf6ca0fb
)
closes odoo/documentation#1624 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
parent
3a51738995
commit
286b01a241
7
conf.py
7
conf.py
@ -120,9 +120,13 @@ extensions = [
|
|||||||
# Code switcher (switcher and case directives)
|
# Code switcher (switcher and case directives)
|
||||||
'switcher',
|
'switcher',
|
||||||
|
|
||||||
|
# Content tabs
|
||||||
|
'sphinx_tabs.tabs',
|
||||||
|
|
||||||
# Strange html domain logic used in memento pages
|
# Strange html domain logic used in memento pages
|
||||||
'html_domain',
|
'html_domain',
|
||||||
]
|
]
|
||||||
|
|
||||||
if odoo_dir_in_path:
|
if odoo_dir_in_path:
|
||||||
# GitHub links generation
|
# GitHub links generation
|
||||||
extensions += [
|
extensions += [
|
||||||
@ -175,6 +179,9 @@ languages_names = {
|
|||||||
# The specifications of redirect rules used by the redirects extension.
|
# The specifications of redirect rules used by the redirects extension.
|
||||||
redirects_file = 'redirects.txt'
|
redirects_file = 'redirects.txt'
|
||||||
|
|
||||||
|
sphinx_tabs_disable_tab_closing = True
|
||||||
|
sphinx_tabs_disable_css_loading = False
|
||||||
|
|
||||||
#=== Options for HTML output ===#
|
#=== Options for HTML output ===#
|
||||||
|
|
||||||
html_theme = 'odoo_theme'
|
html_theme = 'odoo_theme'
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
Install
|
Install
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
.. If you add content on this page, remove the redirect rule 'install -> install/install'
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
install/install
|
install/install
|
||||||
|
@ -521,6 +521,219 @@ Render
|
|||||||
|
|
||||||
Customize this admonition with a **Title** of your choice.
|
Customize this admonition with a **Title** of your choice.
|
||||||
|
|
||||||
|
.. _contributing/tabs:
|
||||||
|
|
||||||
|
Content tabs
|
||||||
|
============
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
The `tabs` directive may not work well in some situations. In particular:
|
||||||
|
|
||||||
|
- The tabs' headers cannot be translated.
|
||||||
|
- A tab cannot contain :ref:`headings <contributing/headings>`.
|
||||||
|
- An :ref:`admonition <contributing/admonitions>` cannot contain tabs.
|
||||||
|
- A tab cannot contain :ref:`internal hyperlink targets
|
||||||
|
<contributing/internal-hyperlink-targets>`.
|
||||||
|
|
||||||
|
Basic tabs
|
||||||
|
----------
|
||||||
|
|
||||||
|
RST
|
||||||
|
~~~
|
||||||
|
|
||||||
|
.. code-block:: rst
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: Odoo Online
|
||||||
|
|
||||||
|
Content dedicated to Odoo Online users.
|
||||||
|
|
||||||
|
.. tab:: Odoo.sh
|
||||||
|
|
||||||
|
Alternative for Odoo.sh users.
|
||||||
|
|
||||||
|
.. tab:: On-premise
|
||||||
|
|
||||||
|
Third version for On-premise users.
|
||||||
|
|
||||||
|
Render
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: Odoo Online
|
||||||
|
|
||||||
|
Content dedicated to Odoo Online users.
|
||||||
|
|
||||||
|
.. tab:: Odoo.sh
|
||||||
|
|
||||||
|
Alternative for Odoo.sh users.
|
||||||
|
|
||||||
|
.. tab:: On-premise
|
||||||
|
|
||||||
|
Third version for On-premise users.
|
||||||
|
|
||||||
|
Nested tabs
|
||||||
|
-----------
|
||||||
|
|
||||||
|
RST
|
||||||
|
~~~
|
||||||
|
|
||||||
|
.. code-block:: rst
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: Stars
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: The Sun
|
||||||
|
|
||||||
|
The closest star to us.
|
||||||
|
|
||||||
|
.. tab:: Proxima Centauri
|
||||||
|
|
||||||
|
The second closest star to us.
|
||||||
|
|
||||||
|
.. tab:: Polaris
|
||||||
|
|
||||||
|
The North Star.
|
||||||
|
|
||||||
|
.. tab:: Moons
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: The Moon
|
||||||
|
|
||||||
|
Orbits the Earth.
|
||||||
|
|
||||||
|
.. tab:: Titan
|
||||||
|
|
||||||
|
Orbits Jupiter.
|
||||||
|
|
||||||
|
Render
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: Stars
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: The Sun
|
||||||
|
|
||||||
|
The closest star to us.
|
||||||
|
|
||||||
|
.. tab:: Proxima Centauri
|
||||||
|
|
||||||
|
The second closest star to us.
|
||||||
|
|
||||||
|
.. tab:: Polaris
|
||||||
|
|
||||||
|
The North Star.
|
||||||
|
|
||||||
|
.. tab:: Moons
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: The Moon
|
||||||
|
|
||||||
|
Orbits the Earth.
|
||||||
|
|
||||||
|
.. tab:: Titan
|
||||||
|
|
||||||
|
Orbits Jupiter.
|
||||||
|
|
||||||
|
Group tabs
|
||||||
|
----------
|
||||||
|
|
||||||
|
RST
|
||||||
|
~~~
|
||||||
|
|
||||||
|
.. code-block:: rst
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. group-tab:: C++
|
||||||
|
|
||||||
|
C++
|
||||||
|
|
||||||
|
.. group-tab:: Python
|
||||||
|
|
||||||
|
Python
|
||||||
|
|
||||||
|
.. group-tab:: Java
|
||||||
|
|
||||||
|
Java
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. group-tab:: C++
|
||||||
|
|
||||||
|
.. code-block:: c++
|
||||||
|
|
||||||
|
int main(const int argc, const char **argv) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. group-tab:: Python
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def main():
|
||||||
|
return
|
||||||
|
|
||||||
|
.. group-tab:: Java
|
||||||
|
|
||||||
|
.. code-block:: java
|
||||||
|
|
||||||
|
class Main {
|
||||||
|
public static void main(String[] args) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Render
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. group-tab:: C++
|
||||||
|
|
||||||
|
C++
|
||||||
|
|
||||||
|
.. group-tab:: Python
|
||||||
|
|
||||||
|
Python
|
||||||
|
|
||||||
|
.. group-tab:: Java
|
||||||
|
|
||||||
|
Java
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. group-tab:: C++
|
||||||
|
|
||||||
|
.. code-block:: c++
|
||||||
|
|
||||||
|
int main(const int argc, const char **argv) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. group-tab:: Python
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def main():
|
||||||
|
return
|
||||||
|
|
||||||
|
.. group-tab:: Java
|
||||||
|
|
||||||
|
.. code-block:: java
|
||||||
|
|
||||||
|
class Main {
|
||||||
|
public static void main(String[] args) {}
|
||||||
|
}
|
||||||
|
|
||||||
.. _contributing/document-metadata:
|
.. _contributing/document-metadata:
|
||||||
|
|
||||||
Document metadata
|
Document metadata
|
||||||
|
5
extensions/sphinx_tabs/__init__.py
Normal file
5
extensions/sphinx_tabs/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
""" Salvaged from https://github.com/executablebooks/sphinx-tabs """
|
||||||
|
|
||||||
|
__version__ = "3.2.0"
|
||||||
|
|
||||||
|
__import__("pkg_resources").declare_namespace(__name__)
|
100
extensions/sphinx_tabs/static/tabs.css
Normal file
100
extensions/sphinx_tabs/static/tabs.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
.sphinx-tabs {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
border-bottom: 1px solid #a0b3bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinx-tabs-tab {
|
||||||
|
position: relative;
|
||||||
|
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
|
||||||
|
color: #1D5C87;
|
||||||
|
line-height: 24px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
border: 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinx-tabs-tab[aria-selected="true"] {
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid #a0b3bf;
|
||||||
|
border-bottom: 1px solid white;
|
||||||
|
margin: -1px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinx-tabs-tab:focus {
|
||||||
|
z-index: 1;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinx-tabs-panel {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #a0b3bf;
|
||||||
|
margin: 0px -1px -1px -1px;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
border-top: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinx-tabs-panel.code-tab {
|
||||||
|
padding: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinx-tab img {
|
||||||
|
margin-bottom: 24 px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme preference styling */
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body:not([data-theme="light"]) .sphinx-tabs-panel {
|
||||||
|
color: white;
|
||||||
|
background-color: rgb(50, 50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-theme="light"]) .sphinx-tabs-tab {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-theme="light"]) .sphinx-tabs-tab[aria-selected="true"] {
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid #a0b3bf;
|
||||||
|
border-bottom: 1px solid rgb(50, 50, 50);
|
||||||
|
margin: -1px;
|
||||||
|
background-color: rgb(50, 50, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Explicit dark theme styling */
|
||||||
|
|
||||||
|
body[data-theme="dark"] .sphinx-tabs-panel {
|
||||||
|
color: white;
|
||||||
|
background-color: rgb(50, 50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .sphinx-tabs-tab {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .sphinx-tabs-tab[aria-selected="true"] {
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid #a0b3bf;
|
||||||
|
border-bottom: 2px solid rgb(50, 50, 50);
|
||||||
|
margin: -1px;
|
||||||
|
background-color: rgb(50, 50, 50);
|
||||||
|
}
|
145
extensions/sphinx_tabs/static/tabs.js
Normal file
145
extensions/sphinx_tabs/static/tabs.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
try {
|
||||||
|
var session = window.sessionStorage || {};
|
||||||
|
} catch (e) {
|
||||||
|
var session = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const allTabs = document.querySelectorAll('.sphinx-tabs-tab');
|
||||||
|
const tabLists = document.querySelectorAll('[role="tablist"]');
|
||||||
|
|
||||||
|
allTabs.forEach(tab => {
|
||||||
|
tab.addEventListener("click", changeTabs);
|
||||||
|
});
|
||||||
|
|
||||||
|
tabLists.forEach(tabList => {
|
||||||
|
tabList.addEventListener("keydown", keyTabs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore group tab selection from session
|
||||||
|
const lastSelected = session.getItem('sphinx-tabs-last-selected');
|
||||||
|
if (lastSelected != null) selectNamedTabs(lastSelected);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key focus left and right between sibling elements using arrows
|
||||||
|
* @param {Node} e the element in focus when key was pressed
|
||||||
|
*/
|
||||||
|
function keyTabs(e) {
|
||||||
|
const tab = e.target;
|
||||||
|
let nextTab = null;
|
||||||
|
if (e.keyCode === 39 || e.keyCode === 37) {
|
||||||
|
tab.setAttribute("tabindex", -1);
|
||||||
|
// Move right
|
||||||
|
if (e.keyCode === 39) {
|
||||||
|
nextTab = tab.nextElementSibling;
|
||||||
|
if (nextTab === null) {
|
||||||
|
nextTab = tab.parentNode.firstElementChild;
|
||||||
|
}
|
||||||
|
// Move left
|
||||||
|
} else if (e.keyCode === 37) {
|
||||||
|
nextTab = tab.previousElementSibling;
|
||||||
|
if (nextTab === null) {
|
||||||
|
nextTab = tab.parentNode.lastElementChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTab !== null) {
|
||||||
|
nextTab.setAttribute("tabindex", 0);
|
||||||
|
nextTab.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select or deselect clicked tab. If a group tab
|
||||||
|
* is selected, also select tab in other tabLists.
|
||||||
|
* @param {Node} e the element that was clicked
|
||||||
|
*/
|
||||||
|
function changeTabs(e) {
|
||||||
|
// Use this instead of the element that was clicked, in case it's a child
|
||||||
|
const notSelected = this.getAttribute("aria-selected") === "false";
|
||||||
|
const positionBefore = this.parentNode.getBoundingClientRect().top;
|
||||||
|
const notClosable = !this.parentNode.classList.contains("closeable");
|
||||||
|
|
||||||
|
deselectTabList(this);
|
||||||
|
|
||||||
|
if (notSelected || notClosable) {
|
||||||
|
selectTab(this);
|
||||||
|
const name = this.getAttribute("name");
|
||||||
|
selectNamedTabs(name, this.id);
|
||||||
|
|
||||||
|
if (this.classList.contains("group-tab")) {
|
||||||
|
// Persist during session
|
||||||
|
session.setItem('sphinx-tabs-last-selected', name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionAfter = this.parentNode.getBoundingClientRect().top;
|
||||||
|
const positionDelta = positionAfter - positionBefore;
|
||||||
|
// Scroll to offset content resizing
|
||||||
|
window.scrollTo(0, window.scrollY + positionDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select tab and show associated panel.
|
||||||
|
* @param {Node} tab tab to select
|
||||||
|
*/
|
||||||
|
function selectTab(tab) {
|
||||||
|
tab.setAttribute("aria-selected", true);
|
||||||
|
|
||||||
|
// Show the associated panel
|
||||||
|
document
|
||||||
|
.getElementById(tab.getAttribute("aria-controls"))
|
||||||
|
.removeAttribute("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the panels associated with all tabs within the
|
||||||
|
* tablist containing this tab.
|
||||||
|
* @param {Node} tab a tab within the tablist to deselect
|
||||||
|
*/
|
||||||
|
function deselectTabList(tab) {
|
||||||
|
const parent = tab.parentNode;
|
||||||
|
const grandparent = parent.parentNode;
|
||||||
|
|
||||||
|
Array.from(parent.children)
|
||||||
|
.forEach(t => t.setAttribute("aria-selected", false));
|
||||||
|
|
||||||
|
Array.from(grandparent.children)
|
||||||
|
.slice(1) // Skip tablist
|
||||||
|
.forEach(panel => panel.setAttribute("hidden", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select grouped tabs with the same name, but no the tab
|
||||||
|
* with the given id.
|
||||||
|
* @param {Node} name name of grouped tab to be selected
|
||||||
|
* @param {Node} clickedId id of clicked tab
|
||||||
|
*/
|
||||||
|
function selectNamedTabs(name, clickedId=null) {
|
||||||
|
const groupedTabs = document.querySelectorAll(`.sphinx-tabs-tab[name="${name}"]`);
|
||||||
|
const tabLists = Array.from(groupedTabs).map(tab => tab.parentNode);
|
||||||
|
|
||||||
|
tabLists
|
||||||
|
.forEach(tabList => {
|
||||||
|
// Don't want to change the tabList containing the clicked tab
|
||||||
|
const clickedTab = tabList.querySelector(`[id="${clickedId}"]`);
|
||||||
|
if (clickedTab === null ) {
|
||||||
|
// Select first tab with matching name
|
||||||
|
const tab = tabList.querySelector(`.sphinx-tabs-tab[name="${name}"]`);
|
||||||
|
deselectTabList(tab);
|
||||||
|
selectTab(tab);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof exports === 'undefined') {
|
||||||
|
exports = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.keyTabs = keyTabs;
|
||||||
|
exports.changeTabs = changeTabs;
|
||||||
|
exports.selectTab = selectTab;
|
||||||
|
exports.deselectTabList = deselectTabList;
|
||||||
|
exports.selectNamedTabs = selectNamedTabs;
|
370
extensions/sphinx_tabs/tabs.py
Normal file
370
extensions/sphinx_tabs/tabs.py
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
""" Tabbed views for Sphinx, with HTML builder
|
||||||
|
|
||||||
|
Salvaged from https://github.com/executablebooks/sphinx-tabs
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
|
from functools import partial
|
||||||
|
import sphinx
|
||||||
|
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.parsers.rst import directives
|
||||||
|
from pygments.lexers import get_all_lexers
|
||||||
|
from sphinx.highlighting import lexer_classes
|
||||||
|
from sphinx.util.docutils import SphinxDirective
|
||||||
|
from sphinx.directives.code import CodeBlock
|
||||||
|
|
||||||
|
|
||||||
|
FILES = [
|
||||||
|
"tabs.js",
|
||||||
|
"tabs.css",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
LEXER_MAP = {}
|
||||||
|
for lexer in get_all_lexers():
|
||||||
|
for short_name in lexer[1]:
|
||||||
|
LEXER_MAP[short_name] = lexer[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_compatible_builders(app):
|
||||||
|
builders = [
|
||||||
|
"html",
|
||||||
|
"singlehtml",
|
||||||
|
"dirhtml",
|
||||||
|
"readthedocs",
|
||||||
|
"readthedocsdirhtml",
|
||||||
|
"readthedocssinglehtml",
|
||||||
|
"readthedocssinglehtmllocalmedia",
|
||||||
|
"spelling",
|
||||||
|
]
|
||||||
|
builders.extend(app.config["sphinx_tabs_valid_builders"])
|
||||||
|
return builders
|
||||||
|
|
||||||
|
|
||||||
|
class SphinxTabsContainer(nodes.container):
|
||||||
|
tagname = "div"
|
||||||
|
|
||||||
|
|
||||||
|
class SphinxTabsPanel(nodes.container):
|
||||||
|
tagname = "div"
|
||||||
|
|
||||||
|
|
||||||
|
class SphinxTabsTab(nodes.paragraph):
|
||||||
|
tagname = "button"
|
||||||
|
|
||||||
|
|
||||||
|
class SphinxTabsTablist(nodes.container):
|
||||||
|
tagname = "div"
|
||||||
|
|
||||||
|
|
||||||
|
def visit(translator, node):
|
||||||
|
# Borrowed from `sphinx-inline-tabs`
|
||||||
|
attrs = node.attributes.copy()
|
||||||
|
attrs.pop("classes")
|
||||||
|
attrs.pop("ids")
|
||||||
|
attrs.pop("names")
|
||||||
|
attrs.pop("dupnames")
|
||||||
|
attrs.pop("backrefs")
|
||||||
|
text = translator.starttag(node, node.tagname, **attrs)
|
||||||
|
translator.body.append(text.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def depart(translator, node):
|
||||||
|
translator.body.append(f"</{node.tagname}>")
|
||||||
|
|
||||||
|
|
||||||
|
class TabsDirective(SphinxDirective):
|
||||||
|
"""Top-level tabs directive"""
|
||||||
|
|
||||||
|
has_content = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Parse a tabs directive"""
|
||||||
|
self.assert_has_content()
|
||||||
|
|
||||||
|
node = nodes.container(type="tab-element")
|
||||||
|
node["classes"].append("sphinx-tabs")
|
||||||
|
|
||||||
|
if "next_tabs_id" not in self.env.temp_data:
|
||||||
|
self.env.temp_data["next_tabs_id"] = 0
|
||||||
|
if "tabs_stack" not in self.env.temp_data:
|
||||||
|
self.env.temp_data["tabs_stack"] = []
|
||||||
|
|
||||||
|
tabs_id = self.env.temp_data["next_tabs_id"]
|
||||||
|
tabs_key = f"tabs_{tabs_id}"
|
||||||
|
self.env.temp_data["next_tabs_id"] += 1
|
||||||
|
self.env.temp_data["tabs_stack"].append(tabs_id)
|
||||||
|
|
||||||
|
self.env.temp_data[tabs_key] = {}
|
||||||
|
self.env.temp_data[tabs_key]["tab_ids"] = []
|
||||||
|
self.env.temp_data[tabs_key]["tab_titles"] = []
|
||||||
|
self.env.temp_data[tabs_key]["is_first_tab"] = True
|
||||||
|
|
||||||
|
self.state.nested_parse(self.content, self.content_offset, node)
|
||||||
|
|
||||||
|
if self.env.app.builder.name in get_compatible_builders(self.env.app):
|
||||||
|
tablist = SphinxTabsTablist()
|
||||||
|
tablist["role"] = "tablist"
|
||||||
|
tablist["aria-label"] = "Tabbed content"
|
||||||
|
if not self.env.config["sphinx_tabs_disable_tab_closing"]:
|
||||||
|
tablist["classes"].append("closeable")
|
||||||
|
|
||||||
|
tab_titles = self.env.temp_data[tabs_key]["tab_titles"]
|
||||||
|
for idx, [data_tab, tab_name] in enumerate(tab_titles):
|
||||||
|
tab_name.attributes["role"] = "tab"
|
||||||
|
tab_name["ids"] = [f"tab-{tabs_id}-{data_tab}"]
|
||||||
|
tab_name["name"] = data_tab
|
||||||
|
tab_name["tabindex"] = "0" if idx == 0 else "-1"
|
||||||
|
tab_name["aria-selected"] = "true" if idx == 0 else "false"
|
||||||
|
tab_name["aria-controls"] = tab_name["ids"][0].replace("tab-", "panel-")
|
||||||
|
|
||||||
|
tablist += tab_name
|
||||||
|
|
||||||
|
node.insert(0, tablist)
|
||||||
|
|
||||||
|
self.env.temp_data["tabs_stack"].pop()
|
||||||
|
return [node]
|
||||||
|
|
||||||
|
|
||||||
|
class TabDirective(SphinxDirective):
|
||||||
|
"""Tab directive, for adding a tab to a collection of tabs"""
|
||||||
|
|
||||||
|
has_content = True
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.tab_id = None
|
||||||
|
self.tab_classes = set()
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Parse a tab directive"""
|
||||||
|
self.assert_has_content()
|
||||||
|
|
||||||
|
tabs_id = self.env.temp_data["tabs_stack"][-1]
|
||||||
|
tabs_key = f"tabs_{tabs_id}"
|
||||||
|
|
||||||
|
include_tabs_id_in_data_tab = False
|
||||||
|
if self.tab_id is None:
|
||||||
|
tab_id = self.env.new_serialno(tabs_key)
|
||||||
|
include_tabs_id_in_data_tab = True
|
||||||
|
else:
|
||||||
|
tab_id = self.tab_id
|
||||||
|
|
||||||
|
tab_name = SphinxTabsTab()
|
||||||
|
self.state.nested_parse(self.content[0:1], 0, tab_name)
|
||||||
|
# Remove the paragraph node that is created by nested_parse
|
||||||
|
tab_name.children[0].replace_self(tab_name.children[0].children)
|
||||||
|
|
||||||
|
tab_name["classes"].append("sphinx-tabs-tab")
|
||||||
|
tab_name["classes"].extend(sorted(self.tab_classes))
|
||||||
|
|
||||||
|
i = 1
|
||||||
|
while tab_id in self.env.temp_data[tabs_key]["tab_ids"]:
|
||||||
|
tab_id = f"{tab_id}-{i}"
|
||||||
|
i += 1
|
||||||
|
self.env.temp_data[tabs_key]["tab_ids"].append(tab_id)
|
||||||
|
|
||||||
|
data_tab = str(tab_id)
|
||||||
|
if include_tabs_id_in_data_tab:
|
||||||
|
data_tab = f"{tabs_id}-{data_tab}"
|
||||||
|
|
||||||
|
self.env.temp_data[tabs_key]["tab_titles"].append((data_tab, tab_name))
|
||||||
|
|
||||||
|
panel = SphinxTabsPanel()
|
||||||
|
panel["role"] = "tabpanel"
|
||||||
|
panel["ids"] = [f"panel-{tabs_id}-{data_tab}"]
|
||||||
|
panel["name"] = data_tab
|
||||||
|
panel["tabindex"] = 0
|
||||||
|
panel["aria-labelledby"] = panel["ids"][0].replace("panel-", "tab-")
|
||||||
|
panel["classes"].append("sphinx-tabs-panel")
|
||||||
|
panel["classes"].extend(sorted(self.tab_classes))
|
||||||
|
|
||||||
|
if self.env.temp_data[tabs_key]["is_first_tab"]:
|
||||||
|
self.env.temp_data[tabs_key]["is_first_tab"] = False
|
||||||
|
else:
|
||||||
|
panel["hidden"] = "true"
|
||||||
|
|
||||||
|
self.state.nested_parse(self.content[2:], self.content_offset, panel)
|
||||||
|
|
||||||
|
if self.env.app.builder.name not in get_compatible_builders(self.env.app):
|
||||||
|
# Use base docutils classes
|
||||||
|
outer_node = nodes.container()
|
||||||
|
tab = nodes.container()
|
||||||
|
tab_name = nodes.container()
|
||||||
|
panel = nodes.container()
|
||||||
|
|
||||||
|
self.state.nested_parse(self.content[0:1], 0, tab_name)
|
||||||
|
self.state.nested_parse(self.content[2:], self.content_offset, panel)
|
||||||
|
|
||||||
|
tab += tab_name
|
||||||
|
outer_node += tab
|
||||||
|
outer_node += panel
|
||||||
|
|
||||||
|
return [outer_node]
|
||||||
|
|
||||||
|
return [panel]
|
||||||
|
|
||||||
|
|
||||||
|
class GroupTabDirective(TabDirective):
|
||||||
|
"""Tab directive that toggles with same tab names across page"""
|
||||||
|
|
||||||
|
has_content = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.assert_has_content()
|
||||||
|
self.tab_classes.add("group-tab")
|
||||||
|
group_name = self.content[0]
|
||||||
|
if self.tab_id is None:
|
||||||
|
self.tab_id = base64.b64encode(group_name.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
node = super().run()
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class CodeTabDirective(GroupTabDirective):
|
||||||
|
"""Tab directive with a codeblock as its content"""
|
||||||
|
|
||||||
|
has_content = True
|
||||||
|
required_arguments = 1 # Lexer name
|
||||||
|
optional_arguments = 1 # Custom label
|
||||||
|
final_argument_whitespace = True
|
||||||
|
option_spec = { # From sphinx CodeBlock
|
||||||
|
"force": directives.flag,
|
||||||
|
"linenos": directives.flag,
|
||||||
|
"dedent": int,
|
||||||
|
"lineno-start": int,
|
||||||
|
"emphasize-lines": directives.unchanged_required,
|
||||||
|
"caption": directives.unchanged_required,
|
||||||
|
"class": directives.class_option,
|
||||||
|
"name": directives.unchanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Parse a code-tab directive"""
|
||||||
|
self.assert_has_content()
|
||||||
|
|
||||||
|
if len(self.arguments) > 1:
|
||||||
|
tab_name = self.arguments[1]
|
||||||
|
elif self.arguments[0] in lexer_classes and not isinstance(
|
||||||
|
lexer_classes[self.arguments[0]], partial
|
||||||
|
):
|
||||||
|
tab_name = lexer_classes[self.arguments[0]].name
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
tab_name = LEXER_MAP[self.arguments[0]]
|
||||||
|
except KeyError as invalid_lexer_error:
|
||||||
|
raise ValueError(
|
||||||
|
f"Lexer not implemented: {self.arguments[0]}"
|
||||||
|
) from invalid_lexer_error
|
||||||
|
|
||||||
|
self.tab_classes.add("code-tab")
|
||||||
|
|
||||||
|
# All content parsed as code
|
||||||
|
code_block = CodeBlock.run(self)
|
||||||
|
|
||||||
|
# Reset to generate panel
|
||||||
|
self.content.data = [tab_name, ""]
|
||||||
|
self.content.items = [(None, 0), (None, 1)]
|
||||||
|
|
||||||
|
node = super().run()
|
||||||
|
node[0].extend(code_block)
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class _FindTabsDirectiveVisitor(nodes.NodeVisitor):
|
||||||
|
"""Visitor pattern than looks for a sphinx tabs
|
||||||
|
directive in a document"""
|
||||||
|
|
||||||
|
def __init__(self, document):
|
||||||
|
nodes.NodeVisitor.__init__(self, document)
|
||||||
|
self._found = False
|
||||||
|
|
||||||
|
def unknown_visit(self, node):
|
||||||
|
if (
|
||||||
|
not self._found
|
||||||
|
and isinstance(node, nodes.container)
|
||||||
|
and "classes" in node
|
||||||
|
and isinstance(node["classes"], list)
|
||||||
|
):
|
||||||
|
self._found = "sphinx-tabs" in node["classes"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def found_tabs_directive(self):
|
||||||
|
"""Return whether a sphinx tabs directive was found"""
|
||||||
|
return self._found
|
||||||
|
|
||||||
|
|
||||||
|
def update_config(app, config):
|
||||||
|
"""Adds sphinx-tabs CSS and JS asset files"""
|
||||||
|
for path in [Path(path) for path in FILES]:
|
||||||
|
if not config.sphinx_tabs_disable_css_loading and path.suffix == ".css":
|
||||||
|
if "add_css_file" in dir(app):
|
||||||
|
app.add_css_file(path.as_posix())
|
||||||
|
else:
|
||||||
|
app.add_stylesheet(path.as_posix())
|
||||||
|
if path.suffix == ".js":
|
||||||
|
if "add_script_file" in dir(app):
|
||||||
|
app.add_script_file(path.as_posix())
|
||||||
|
else:
|
||||||
|
app.add_js_file(path.as_posix())
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def update_context(app, pagename, templatename, context, doctree):
|
||||||
|
"""Remove sphinx-tabs CSS and JS asset files if not used in a page"""
|
||||||
|
if doctree is None:
|
||||||
|
return
|
||||||
|
visitor = _FindTabsDirectiveVisitor(doctree)
|
||||||
|
doctree.walk(visitor)
|
||||||
|
|
||||||
|
include_assets_in_all_pages = False
|
||||||
|
if sphinx.version_info >= (4, 1, 0):
|
||||||
|
include_assets_in_all_pages = app.registry.html_assets_policy == "always"
|
||||||
|
|
||||||
|
if not visitor.found_tabs_directive and not include_assets_in_all_pages:
|
||||||
|
paths = [Path("_static") / f for f in FILES]
|
||||||
|
if "css_files" in context:
|
||||||
|
context["css_files"] = context["css_files"][:]
|
||||||
|
for path in paths:
|
||||||
|
if path.suffix == ".css" and path in context["css_files"]:
|
||||||
|
context["css_files"].remove(path.as_posix())
|
||||||
|
if "script_files" in context:
|
||||||
|
context["script_files"] = context["script_files"][:]
|
||||||
|
for path in paths:
|
||||||
|
if path.suffix == ".js" and path.as_posix() in context["script_files"]:
|
||||||
|
context["script_files"].remove(path.as_posix())
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: enable=unused-argument
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
"""Set up the plugin"""
|
||||||
|
app.add_config_value("sphinx_tabs_valid_builders", [], "")
|
||||||
|
app.add_config_value("sphinx_tabs_disable_css_loading", False, "html", [bool])
|
||||||
|
app.add_config_value("sphinx_tabs_disable_tab_closing", False, "html", [bool])
|
||||||
|
app.add_node(SphinxTabsContainer, html=(visit, depart))
|
||||||
|
app.add_node(SphinxTabsPanel, html=(visit, depart))
|
||||||
|
app.add_node(SphinxTabsTab, html=(visit, depart))
|
||||||
|
app.add_node(SphinxTabsTablist, html=(visit, depart))
|
||||||
|
app.add_directive("tabs", TabsDirective)
|
||||||
|
app.add_directive("tab", TabDirective)
|
||||||
|
app.add_directive("group-tab", GroupTabDirective)
|
||||||
|
app.add_directive("code-tab", CodeTabDirective)
|
||||||
|
static_dir = Path(__file__).parent / "static"
|
||||||
|
app.connect(
|
||||||
|
"builder-inited",
|
||||||
|
(lambda app: app.config.html_static_path.append(static_dir.as_posix())),
|
||||||
|
)
|
||||||
|
app.connect("config-inited", update_config)
|
||||||
|
app.connect("html-page-context", update_context)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"parallel_read_safe": True,
|
||||||
|
"parallel_write_safe": True,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user