documentation/extensions/spoilers/__init__.py
Antoine Vandevenne (anv) cdb4b9398c [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#2876

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>
2022-10-28 15:51:21 +02:00

106 lines
3.7 KiB
Python

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,
}