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