#!/usr/bin/env python3
#
# collapse.py
r"""
Adds a collapsible section to an HTML page using a details_ element.
.. _details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
.. versionadded:: 2.5.0
.. extensions:: sphinx_toolbox.collapse
Usage
------
.. rst:directive:: .. collapse:: [label]
Adds a collapsible section to an HTML page using a details_ element.
.. _details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
With non-HTML builders, the content will be added as-is.
.. rest-example::
.. collapse:: Details
Something small enough to escape casual notice.
.. collapse:: A Different Label
:class: custom-summary
:name: summary0
Something else that might escape notice.
.. collapse:: A long code block
.. code-block:: python
print("Not really")
.. rst:directive:option:: open
:type: flag
The ``:open:`` option can be used to have the section open by default.
.. versionadded:: 3.0.0
.. rest-example::
.. collapse:: Open
:open:
This section is open by default.
API Reference
----------------
"""
#
# Copyright © 2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# stdlib
from typing import Optional, Sequence
# 3rd party
from docutils import nodes
from docutils.parsers.rst import directives
from docutils.parsers.rst.roles import set_classes
from domdf_python_tools.stringlist import DelimitedList
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
from sphinx.writers.html5 import HTML5Translator
# this package
from sphinx_toolbox.utils import SphinxExtMetadata, flag, metadata_add_version
__all__ = ("CollapseDirective", "CollapseNode", "visit_collapse_node", "depart_collapse_node", "setup")
[docs]class CollapseDirective(SphinxDirective):
r"""
A Sphinx directive to add a collapsible section to an HTML page using a details_ element.
.. _details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
"""
final_argument_whitespace: bool = True
has_content: bool = True
# The label
required_arguments: int = 1
option_spec = {
"class": directives.class_option,
"name": directives.unchanged,
"open": flag,
}
[docs] def run(self) -> Sequence[nodes.Node]: # type: ignore[override]
"""
Process the content of the directive.
"""
set_classes(self.options)
self.assert_has_content()
text = '\n'.join(self.content)
label = self.arguments[0]
collapse_node = CollapseNode(text, label, **self.options)
self.add_name(collapse_node)
collapse_node["classes"].append(f"summary-{nodes.make_id(label)}")
self.state.nested_parse(self.content, self.content_offset, collapse_node)
return [collapse_node]
[docs]class CollapseNode(nodes.Body, nodes.Element):
"""
Node that represents a collapsible section.
:param rawsource:
:param label:
"""
def __init__(self, rawsource: str = '', label: Optional[str] = None, *children, **attributes):
super().__init__(rawsource, *children, **attributes)
self["label"] = label
[docs]def visit_collapse_node(translator: HTML5Translator, node: CollapseNode) -> None:
"""
Visit a :class:`~.CollapseNode`.
:param translator:
:param node: The node being visited.
"""
tag_parts = DelimitedList(["details"])
if node.get("names", None):
names = DelimitedList(node["names"])
tag_parts.append(f'name="{names: }"')
if node.get("classes", None):
classes = DelimitedList(node["classes"])
tag_parts.append(f'class="{classes: }"')
if node.attributes.get("open", False):
tag_parts.append("open")
translator.body.append(f"<{tag_parts: }>\n<summary>{node['label']}</summary>")
translator.context.append("</details>")
[docs]def depart_collapse_node(translator: HTML5Translator, node: CollapseNode) -> None:
"""
Depart a :class:`~.CollapseNode`.
:param translator:
:param node: The node being visited.
"""
translator.body.append(translator.context.pop())
[docs]@metadata_add_version
def setup(app: Sphinx) -> SphinxExtMetadata:
"""
Setup :mod:`sphinx_toolbox.collapse`.
:param app: The Sphinx application.
"""
app.add_directive("collapse", CollapseDirective)
app.add_node(
CollapseNode,
html=(visit_collapse_node, depart_collapse_node),
latex=(lambda *args, **kwargs: None, lambda *args, **kwargs: None)
)
return {
"parallel_read_safe": True,
}