[IMP] extensions/spoilers: add an extension for spoilers

It is often needed in developer tutorials to show excerpts of code for
examples and solutions. This takes quite some page space and, for the
latter, it is not always desired to show the final code result right
after the exercise objective.

This commit adds a spoiler feature to the documentation to allow hiding
content until the reader wants to display it.

The feature relies on the new `.. spoiler:: Button label` directive.

task-3036845

closes odoo/documentation#2877

X-original-commit: 4d7a88639a
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Morgan Meganck <morm@odoo.com>
Co-authored-by: Stefano Rigano <sri@odoo.com>
This commit is contained in:
Antoine Vandevenne (anv) 2022-10-21 13:39:46 +00:00
parent ece2d4c081
commit a154e01cee
6 changed files with 183 additions and 0 deletions

View File

@ -145,6 +145,9 @@ extensions = [
# Content tabs # Content tabs
'sphinx_tabs.tabs', 'sphinx_tabs.tabs',
# Spoilers
'spoilers',
# Strange html domain logic used in memento pages # Strange html domain logic used in memento pages
'html_domain', 'html_domain',
] ]

View File

@ -714,6 +714,24 @@ Code blocks
def main(): def main():
print("Hello world!") print("Hello world!")
.. _contributing/spoilers:
Spoilers
========
.. list-table::
:class: o-showcase-table
* - .. spoiler:: Answer to the Ultimate Question of Life, the Universe, and Everything
**42**
* - .. code-block:: text
.. spoiler:: Answer to the Ultimate Question of Life, the Universe, and Everything
**42**
.. _contributing/tabs: .. _contributing/tabs:
Content tabs Content tabs

View File

@ -241,3 +241,10 @@ $padding-l: 3rem;
$margin-s: $padding-s; $margin-s: $padding-s;
$margin-m: $padding-m; $margin-m: $padding-m;
$margin-l: $padding-l; $margin-l: $padding-l;
//------------------------------------------------------------------------------
// Components
//------------------------------------------------------------------------------
// Accordion
$o-accordion-bg: tint-color($doc_exercise, 90%)!default;

View File

@ -27,3 +27,17 @@ $dropdown-min-width: 4.5rem;
// Modals // Modals
$modal-backdrop-opacity: .7; $modal-backdrop-opacity: .7;
// Accordion
$accordion-body-padding-y: 0 !default;
$accordion-body-padding-x: 0 !default;
$accordion-color: shade-color($doc_exercise, 35%) !default;
$accordion-bg: $o-accordion-bg;
$accordion-button-color: $accordion-color !default;
$accordion-button-active-color: $accordion-button-color !default;
$accordion-button-bg: $accordion-bg !default;
$accordion-button-active-bg: $accordion-button-bg !default;
$accordion-icon-transform: rotate(0deg) !default;

View File

@ -1094,6 +1094,42 @@ header {
} }
} }
} }
//------------------------------------------------------------------------------
// Spoilers
//------------------------------------------------------------------------------
.o_spoiler {
border-color: tint-color($doc_exercise, 30%);
background-color: $o-accordion-bg;
.accordion-button {
&::after {
margin: 0 .2rem 0 0;
}
&.collapsed::after {
transform: rotate(-90deg);
}
}
.accordion-body {
#wrap & { // FIXME: Temporary workaround to take priority over pre-existing rules.
> *:first-child {
margin-top: 1rem;
}
code, div[class^="highlight"] {
border-color: tint-color($doc-exercise, 50%);
&:first-child:last-child {
margin-bottom: 0;
}
}
}
}
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Content Tabs // Content Tabs
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

View File

@ -0,0 +1,105 @@
from docutils import nodes
from sphinx.locale import get_translation
from sphinx.util.docutils import SphinxDirective
class Spoiler(SphinxDirective):
""" Implement a `spoiler` directive with Bootstrap's accordion component. """
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True
has_content = True
def run(self):
""" Process the content of the directive.
We use custom node classes to represent HTML elements (e.g., 'div') rather than the
corresponding sphinx.nodes.* class (e.g., sphinx.nodes.container) to prevent automatically
setting the name of the node as class (e.g., "container") on the element.
"""
_ = get_translation('spoilers')
self.assert_has_content()
spoiler_id = self.env.new_serialno("spoiler")
heading_id = f'o_spoiler_header_{spoiler_id}'
content_id = f'o_spoiler_content_{spoiler_id}'
# Create the main accordion container.
accordion_container = Container(
classes=['accordion', 'accordion-flush', 'o_spoiler', 'alert']
) # Reuse the SCSS rules already defined for `alert`. Same thing for `alert-title`.
# Create the accordion item container.
accordion_item_container = Container(classes=['accordion-item'])
accordion_container.append(accordion_item_container)
# Create the header.
header = Header(ids=[heading_id], classes=['accordion-header'])
accordion_item_container.append(header)
# Create the toggle button.
button = Button(
classes=[
'accordion-button', 'collapsed', 'flex-row-reverse', 'justify-content-end',
'fw-bold', 'p-0', 'border-bottom-0'
],
**{
'type': 'button',
'data-bs-toggle': 'collapse',
'data-bs-target': f'#{content_id}',
'aria-expanded': 'false',
'aria-controls': content_id,
}
)
header.append(button)
# Create the button label.
label = self.arguments[0] if self.arguments else _("Spoiler")
button_label = nodes.Text(label)
button.append(button_label)
# Create the accordion collapse container.
accordion_collapse_container = Container(
ids=[content_id],
classes=['accordion-collapse', 'collapse', 'border-bottom-0'],
**{'aria-labelledby': heading_id},
)
accordion_item_container.append(accordion_collapse_container)
# Create the accordion body container.
accordion_body_container = Container(classes=['accordion-body'])
self.state.nested_parse(self.content, self.content_offset, accordion_body_container)
accordion_collapse_container.append(accordion_body_container)
return [accordion_container]
class Container(nodes.General, nodes.Element):
custom_tag_name = 'div'
class Header(nodes.General, nodes.Element):
custom_tag_name = 'span'
class Button(nodes.General, nodes.Element):
custom_tag_name = 'button'
def visit_node(translator, node):
custom_attr = {k: v for k, v in node.attributes.items() if k not in node.known_attributes}
translator.body.append(translator.starttag(node, node.custom_tag_name, **custom_attr).rstrip())
def depart_node(translator, node):
translator.body.append(f'</{node.custom_tag_name}>')
def setup(app):
app.add_directive('spoiler', Spoiler)
app.add_node(Container, html=(visit_node, depart_node))
app.add_node(Header, html=(visit_node, depart_node))
app.add_node(Button, html=(visit_node, depart_node))
return {
'parallel_read_safe': True,
'parallel_write_safe': True,
}