106 lines
3.7 KiB
Python
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,
|
||
|
}
|