documentation/extensions/spoilers/__init__.py

106 lines
3.7 KiB
Python
Raw Permalink Normal View History

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