From 2c459f9978691a87f1d638faec57c03c5abc8efb Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 3 Jul 2024 15:56:47 +0200 Subject: [PATCH 01/12] [rst] Add `collabsible` option to admonition directives --- .ruff.toml | 5 +- sphinx/application.py | 1 + sphinx/directives/__init__.py | 37 ++++++---- sphinx/directives/admonitions.py | 120 +++++++++++++++++++++++++++++++ sphinx/directives/other.py | 10 --- sphinx/writers/html5.py | 20 +++++- 6 files changed, 166 insertions(+), 27 deletions(-) create mode 100644 sphinx/directives/admonitions.py diff --git a/.ruff.toml b/.ruff.toml index 4451e8eb538..08ee43e0304 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -445,7 +445,10 @@ exclude = [ "sphinx/builders/*", "sphinx/cmd/*", "sphinx/config.py", - "sphinx/directives/*", + "sphinx/directives/__init__.py ", + "sphinx/directives/code.py", + "sphinx/directives/other.py", + "sphinx/directives/patches.py", "sphinx/domains/*", "sphinx/environment/*", "sphinx/ext/autodoc/__init__.py", diff --git a/sphinx/application.py b/sphinx/application.py index bf828fb8983..095f0b4f9f1 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -81,6 +81,7 @@ 'sphinx.domains.rst', 'sphinx.domains.std', 'sphinx.directives', + 'sphinx.directives.admonitions', 'sphinx.directives.code', 'sphinx.directives.other', 'sphinx.directives.patches', diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index a9699f168ad..46a672c76bc 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -224,18 +224,21 @@ def run(self) -> list[Node]: 'no-index' in self.options # xref RemovedInSphinx90Warning # deprecate noindex in Sphinx 9.0 - or 'noindex' in self.options) + or 'noindex' in self.options + ) node['no-index-entry'] = node['noindexentry'] = ( 'no-index-entry' in self.options # xref RemovedInSphinx90Warning # deprecate noindexentry in Sphinx 9.0 - or 'noindexentry' in self.options) + or 'noindexentry' in self.options + ) node['no-contents-entry'] = node['nocontentsentry'] = ( 'no-contents-entry' in self.options # xref RemovedInSphinx90Warning # deprecate nocontentsentry in Sphinx 9.0 - or 'nocontentsentry' in self.options) - node['no-typesetting'] = ('no-typesetting' in self.options) + or 'nocontentsentry' in self.options + ) + node['no-typesetting'] = 'no-typesetting' in self.options if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) @@ -282,8 +285,9 @@ def run(self) -> list[Node]: content_node = addnodes.desc_content('', *content_children) node.append(content_node) self.transform_content(content_node) - self.env.app.emit('object-description-transform', - self.domain, self.objtype, content_node) + self.env.app.emit( + 'object-description-transform', self.domain, self.objtype, content_node + ) DocFieldTransformer(self).transform_all(content_node) self.env.temp_data['object'] = None self.after_content() @@ -294,8 +298,11 @@ def run(self) -> list[Node]: # If ``:no-index:`` is set, or there are no ids on the node # or any of its children, then just return the index node, # as Docutils expects a target node to have at least one id. - if node_ids := [node_id for el in node.findall(nodes.Element) # type: ignore[var-annotated] - for node_id in el.get('ids', ())]: + if node_ids := [ + node_id + for el in node.findall(nodes.Element) # type: ignore[var-annotated] + for node_id in el.get('ids', ()) + ]: target_node = nodes.target(ids=node_ids) self.set_source_info(target_node) return [self.indexnode, target_node] @@ -316,16 +323,20 @@ def run(self) -> list[Node]: docutils.unregister_role('') return [] role_name = self.arguments[0] - role, messages = roles.role(role_name, self.state_machine.language, - self.lineno, self.state.reporter) + role, messages = roles.role( + role_name, self.state_machine.language, self.lineno, self.state.reporter + ) if role: docutils.register_role('', role) # type: ignore[arg-type] self.env.temp_data['default_role'] = role_name else: literal_block = nodes.literal_block(self.block_text, self.block_text) reporter = self.state.reporter - error = reporter.error('Unknown interpreted text role "%s".' % role_name, - literal_block, line=self.lineno) + error = reporter.error( + 'Unknown interpreted text role "%s".' % role_name, + literal_block, + line=self.lineno, + ) messages += [error] return cast(list[nodes.Node], messages) @@ -355,7 +366,7 @@ def run(self) -> list[Node]: def setup(app: Sphinx) -> ExtensionMetadata: - app.add_config_value("strip_signature_backslash", False, 'env') + app.add_config_value('strip_signature_backslash', False, 'env') directives.register_directive('default-role', DefaultRole) directives.register_directive('default-domain', DefaultDomain) directives.register_directive('describe', ObjectDescription) diff --git a/sphinx/directives/admonitions.py b/sphinx/directives/admonitions.py new file mode 100644 index 00000000000..d44de33f724 --- /dev/null +++ b/sphinx/directives/admonitions.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +from docutils import nodes +from docutils.parsers.rst import directives +from docutils.parsers.rst.roles import set_classes + +from sphinx import addnodes +from sphinx.util.docutils import SphinxDirective + +if TYPE_CHECKING: + from docutils.nodes import Element, Node + + from sphinx.application import Sphinx + from sphinx.util.typing import ExtensionMetadata, OptionSpec + + +class BaseAdmonition(SphinxDirective): + final_argument_whitespace = True + option_spec: ClassVar[OptionSpec] = { + 'class': directives.class_option, + 'name': directives.unchanged, + 'collapsible': directives.flag, + 'open': directives.flag, + } + has_content = True + + node_class: ClassVar[type[Element]] = nodes.admonition + """Subclasses must set this to the appropriate admonition node class.""" + + def run(self) -> list[Node]: + set_classes(self.options) + self.assert_has_content() + if 'collapsible' in self.options: + self.options['collapsible'] = True + if 'open' in self.options: + self.options['open'] = True + admonition_node = self.node_class('\n'.join(self.content), **self.options) + self.add_name(admonition_node) + if self.node_class is nodes.admonition: + title_text = self.arguments[0] + textnodes, messages = self.parse_inline(title_text, lineno=self.lineno) + title = nodes.title(title_text, '', *textnodes) + self.set_source_info(title) + admonition_node += title + admonition_node += messages + if 'classes' not in self.options: + admonition_node['classes'] += ['admonition-' + nodes.make_id(title_text)] + admonition_node.extend(self.parse_content_to_nodes()) + return [admonition_node] + + +class Admonition(BaseAdmonition): + required_arguments = 1 + node_class = nodes.admonition + + +class Attention(BaseAdmonition): + node_class = nodes.attention + + +class Caution(BaseAdmonition): + node_class = nodes.caution + + +class Danger(BaseAdmonition): + node_class = nodes.danger + + +class Error(BaseAdmonition): + node_class = nodes.error + + +class Hint(BaseAdmonition): + node_class = nodes.hint + + +class Important(BaseAdmonition): + node_class = nodes.important + + +class Note(BaseAdmonition): + node_class = nodes.note + + +class Tip(BaseAdmonition): + node_class = nodes.tip + + +class Warning(BaseAdmonition): + node_class = nodes.warning + + +class SeeAlso(BaseAdmonition): + """ + An admonition mentioning things to look at as reference. + """ + + node_class = addnodes.seealso + + +def setup(app: Sphinx) -> ExtensionMetadata: + directives.register_directive('admonition', Admonition) + directives.register_directive('attention', Attention) + directives.register_directive('caution', Caution) + directives.register_directive('danger', Danger) + directives.register_directive('error', Error) + directives.register_directive('hint', Hint) + directives.register_directive('important', Important) + directives.register_directive('note', Note) + directives.register_directive('tip', Tip) + directives.register_directive('warning', Warning) + directives.register_directive('seealso', SeeAlso) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 7fa12f74ead..fa160c3b3e7 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -7,7 +7,6 @@ from docutils import nodes from docutils.parsers.rst import directives -from docutils.parsers.rst.directives.admonitions import BaseAdmonition from docutils.parsers.rst.directives.misc import Class from docutils.parsers.rst.directives.misc import Include as BaseInclude from docutils.statemachine import StateMachine @@ -206,14 +205,6 @@ def run(self) -> list[Node]: return ret -class SeeAlso(BaseAdmonition): - """ - An admonition mentioning things to look at as reference. - """ - - node_class = addnodes.seealso - - class TabularColumns(SphinxDirective): """ Directive to give an explicit tabulary column definition to LaTeX. @@ -426,7 +417,6 @@ def setup(app: Sphinx) -> ExtensionMetadata: directives.register_directive('sectionauthor', Author) directives.register_directive('moduleauthor', Author) directives.register_directive('codeauthor', Author) - directives.register_directive('seealso', SeeAlso) directives.register_directive('tabularcolumns', TabularColumns) directives.register_directive('centered', Centered) directives.register_directive('acks', Acks) diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 1ebea36058a..2d24280e188 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -345,13 +345,24 @@ def visit_comment(self, node: Element) -> None: # overwritten def visit_admonition(self, node: Element, name: str = '') -> None: - self.body.append(self.starttag( - node, 'div', CLASS=('admonition ' + name))) + if node.get('collapsible'): + if node.get('open'): + self.body.append( + self.starttag(node, 'details', CLASS=('admonition ' + name), open='open') + ) + else: + self.body.append(self.starttag(node, 'details', CLASS=('admonition ' + name))) + else: + self.body.append(self.starttag( + node, 'div', CLASS=('admonition ' + name))) if name: node.insert(0, nodes.title(name, admonitionlabels[name])) def depart_admonition(self, node: Element | None = None) -> None: - self.body.append('\n') + if node and node.get('collapsible'): + self.body.append('\n') + else: + self.body.append('\n') def visit_seealso(self, node: Element) -> None: self.visit_admonition(node, 'seealso') @@ -471,6 +482,9 @@ def visit_title(self, node: Element) -> None: self.body.append(self.starttag(node, 'p', '', CLASS='caption', ROLE='heading')) self.body.append('') self.context.append('

\n') + elif isinstance(node.parent, nodes.Admonition) and node.parent.get("collapsible"): # type: ignore[attr-defined] + self.body.append(self.starttag(node, 'summary', '', CLASS='admonition-title')) + self.context.append('\n') else: super().visit_title(node) self.add_secnumber(node) From 2c5d76658c29a84a24d2ea4c4be45457adad7eca Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 3 Jul 2024 16:03:11 +0200 Subject: [PATCH 02/12] revert format --- .ruff.toml | 2 +- sphinx/directives/__init__.py | 37 ++++++++++++----------------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index 08ee43e0304..035fc4bd3a7 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -445,7 +445,7 @@ exclude = [ "sphinx/builders/*", "sphinx/cmd/*", "sphinx/config.py", - "sphinx/directives/__init__.py ", + "sphinx/directives/__init__.py", "sphinx/directives/code.py", "sphinx/directives/other.py", "sphinx/directives/patches.py", diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 46a672c76bc..a9699f168ad 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -224,21 +224,18 @@ def run(self) -> list[Node]: 'no-index' in self.options # xref RemovedInSphinx90Warning # deprecate noindex in Sphinx 9.0 - or 'noindex' in self.options - ) + or 'noindex' in self.options) node['no-index-entry'] = node['noindexentry'] = ( 'no-index-entry' in self.options # xref RemovedInSphinx90Warning # deprecate noindexentry in Sphinx 9.0 - or 'noindexentry' in self.options - ) + or 'noindexentry' in self.options) node['no-contents-entry'] = node['nocontentsentry'] = ( 'no-contents-entry' in self.options # xref RemovedInSphinx90Warning # deprecate nocontentsentry in Sphinx 9.0 - or 'nocontentsentry' in self.options - ) - node['no-typesetting'] = 'no-typesetting' in self.options + or 'nocontentsentry' in self.options) + node['no-typesetting'] = ('no-typesetting' in self.options) if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) @@ -285,9 +282,8 @@ def run(self) -> list[Node]: content_node = addnodes.desc_content('', *content_children) node.append(content_node) self.transform_content(content_node) - self.env.app.emit( - 'object-description-transform', self.domain, self.objtype, content_node - ) + self.env.app.emit('object-description-transform', + self.domain, self.objtype, content_node) DocFieldTransformer(self).transform_all(content_node) self.env.temp_data['object'] = None self.after_content() @@ -298,11 +294,8 @@ def run(self) -> list[Node]: # If ``:no-index:`` is set, or there are no ids on the node # or any of its children, then just return the index node, # as Docutils expects a target node to have at least one id. - if node_ids := [ - node_id - for el in node.findall(nodes.Element) # type: ignore[var-annotated] - for node_id in el.get('ids', ()) - ]: + if node_ids := [node_id for el in node.findall(nodes.Element) # type: ignore[var-annotated] + for node_id in el.get('ids', ())]: target_node = nodes.target(ids=node_ids) self.set_source_info(target_node) return [self.indexnode, target_node] @@ -323,20 +316,16 @@ def run(self) -> list[Node]: docutils.unregister_role('') return [] role_name = self.arguments[0] - role, messages = roles.role( - role_name, self.state_machine.language, self.lineno, self.state.reporter - ) + role, messages = roles.role(role_name, self.state_machine.language, + self.lineno, self.state.reporter) if role: docutils.register_role('', role) # type: ignore[arg-type] self.env.temp_data['default_role'] = role_name else: literal_block = nodes.literal_block(self.block_text, self.block_text) reporter = self.state.reporter - error = reporter.error( - 'Unknown interpreted text role "%s".' % role_name, - literal_block, - line=self.lineno, - ) + error = reporter.error('Unknown interpreted text role "%s".' % role_name, + literal_block, line=self.lineno) messages += [error] return cast(list[nodes.Node], messages) @@ -366,7 +355,7 @@ def run(self) -> list[Node]: def setup(app: Sphinx) -> ExtensionMetadata: - app.add_config_value('strip_signature_backslash', False, 'env') + app.add_config_value("strip_signature_backslash", False, 'env') directives.register_directive('default-role', DefaultRole) directives.register_directive('default-domain', DefaultDomain) directives.register_directive('describe', ObjectDescription) From ff8e7d572afd1c60af5edbf4ffe98b16143548bd Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 3 Jul 2024 17:25:38 +0200 Subject: [PATCH 03/12] Add CSS for internal docs theme, and example in documentation --- doc/_themes/sphinx13/static/sphinx13.css | 105 +++++++++++++++++----- doc/usage/restructuredtext/directives.rst | 26 ++++++ 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/doc/_themes/sphinx13/static/sphinx13.css b/doc/_themes/sphinx13/static/sphinx13.css index ecc952d0e8e..57afd6d15df 100644 --- a/doc/_themes/sphinx13/static/sphinx13.css +++ b/doc/_themes/sphinx13/static/sphinx13.css @@ -31,6 +31,9 @@ --icon-warning: url('data:image/svg+xml;charset=utf-8,'); --icon-failure: url('data:image/svg+xml;charset=utf-8,'); --icon-spark: url('data:image/svg+xml;charset=utf-8,'); + + /* icons used for details summaries */ + --icon-details-open: url('data:image/svg+xml;charset=utf-8,'); } body { @@ -394,7 +397,7 @@ table td, table th { padding: 0.2em 0.5em 0.2em 0.5em; } -div.admonition, div.warning { +div.admonition, div.warning, details.admonition { font-size: 0.9em; margin: 1em 0 1em 0; border: 1px solid #86989B; @@ -403,16 +406,16 @@ div.admonition, div.warning { padding: 1rem; } -div.admonition > p, div.warning > p { +div.admonition > p, div.warning > p, details.admonition > p { margin: 0; padding: 0; } -div.admonition > pre, div.warning > pre { +div.admonition > pre, div.warning > pre, details.admonition > pre { margin: 0.4em 1em 0.4em 1em; } -div.admonition > p.admonition-title { +div.admonition > p.admonition-title, details.admonition > summary.admonition-title { position: relative; font-weight: 500; background-color: var(--color-admonition-bg); @@ -421,33 +424,77 @@ div.admonition > p.admonition-title { border-radius: var(--admonition-radius) var(--admonition-radius) 0 0; } +details.admonition:not([open]) { + padding-bottom: 0; +} +details.admonition > summary.admonition-title { + list-style: none; + cursor: pointer; +} +details.admonition > summary.admonition-title::after { + background-color: currentcolor; + content: ""; + height: 1.2rem; + width: 1.2rem; + -webkit-mask-image: var(--icon-details-open); + mask-image: var(--icon-details-open); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; + transform: rotate(0deg); + transition: transform .25s; + float: right; +} +details.admonition[open] > summary.admonition-title::after { + transform: rotate(90deg); +} +details.admonition:not([open]) > summary.admonition-title { + margin-bottom: 0; + border-radius: var(--admonition-radius); +} + div.attention > p.admonition-title, div.danger > p.admonition-title, -div.error > p.admonition-title { +div.error > p.admonition-title, +details.attention > summary.admonition-title, +details.danger > summary.admonition-title, +details.error > summary.admonition-title { background-color: var(--colour-error-bg); } div.important > p.admonition-title, div.caution > p.admonition-title, -div.warning > p.admonition-title { +div.warning > p.admonition-title, +details.important > summary.admonition-title, +details.caution > summary.admonition-title, +details.warning > summary.admonition-title { background-color: var(--colour-warning-bg); } -div.note > p.admonition-title { +div.note > p.admonition-title, +details.note > summary.admonition-title { background-color: var(--colour-note-bg); } div.hint > p.admonition-title, div.tip > p.admonition-title, -div.seealso > p.admonition-title { +div.seealso > p.admonition-title, +details.hint > summary.admonition-title, +details.tip > summary.admonition-title, +details.seealso > summary.admonition-title { background-color: var(--colour-success-bg); } -div.admonition-todo > p.admonition-title { +div.admonition-todo > p.admonition-title, +details.admonition-todo > summary.admonition-title { background-color: var(--colour-todo-bg); } -p.admonition-title::before { +p.admonition-title::before, +summary.admonition-title::before { content: ""; height: 1rem; left: .5rem; @@ -456,68 +503,80 @@ p.admonition-title::before { background-color: #5f5f5f; } -div.admonition > p.admonition-title::before { +div.admonition > p.admonition-title::before, +details.admonition > summary.admonition-title::before { background-color: var(--color-admonition-fg); -webkit-mask-image: var(--icon-abstract); mask-image: var(--icon-abstract); } -div.attention > p.admonition-title::before { +div.attention > p.admonition-title::before, +details.attention > summary.admonition-title::before { background-color: var(--colour-error-fg); -webkit-mask-image: var(--icon-warning); mask-image: var(--icon-warning); } -div.caution > p.admonition-title::before { +div.caution > p.admonition-title::before, +details.caution > summary.admonition-title::before { background-color: var(--colour-warning-fg); -webkit-mask-image: var(--icon-spark); mask-image: var(--icon-spark); } -div.danger > p.admonition-title::before { +div.danger > p.admonition-title::before, +details.danger > summary.admonition-title::before { background-color: var(--colour-error-fg); -webkit-mask-image: var(--icon-spark); mask-image: var(--icon-spark); } -div.error > p.admonition-title::before { +div.error > p.admonition-title::before, +details.error > summary.admonition-title::before { background-color: var(--colour-error-fg); -webkit-mask-image: var(--icon-failure); mask-image: var(--icon-failure); } -div.hint > p.admonition-title::before { +div.hint > p.admonition-title::before, +details.hint > summary.admonition-title::before { background-color: var(--colour-success-fg); -webkit-mask-image: var(--icon-question); mask-image: var(--icon-question); } -div.important > p.admonition-title::before { +div.important > p.admonition-title::before, +details.important > summary.admonition-title::before { background-color: var(--colour-warning-fg); -webkit-mask-image: var(--icon-flame); mask-image: var(--icon-flame); } -div.note > p.admonition-title::before { +div.note > p.admonition-title::before, +details.note > summary.admonition-title::before { background-color: var(--colour-note-fg); -webkit-mask-image: var(--icon-pencil); mask-image: var(--icon-pencil); } -div.seealso > p.admonition-title::before { +div.seealso > p.admonition-title::before, +details.seealso > summary.admonition-title::before { background-color: var(--colour-success-fg); -webkit-mask-image: var(--icon-info); mask-image: var(--icon-info); } -div.tip > p.admonition-title::before { +div.tip > p.admonition-title::before, +details.tip > summary.admonition-title::before { background-color: var(--colour-success-fg); -webkit-mask-image: var(--icon-info); mask-image: var(--icon-info); } -div.admonition-todo > p.admonition-title::before { +div.admonition-todo > p.admonition-title::before, +details.admonition-todo > summary.admonition-title::before { background-color: var(--colour-todo-fg); -webkit-mask-image: var(--icon-pencil); mask-image: var(--icon-pencil); } -div.warning > p.admonition-title::before { +div.warning > p.admonition-title::before, +details.warning > summary.admonition-title::before { background-color: var(--colour-warning-fg); -webkit-mask-image: var(--icon-warning); mask-image: var(--icon-warning); } -div.warning { +div.warning, details.warning { border: 1px solid #940000; } diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index ff425246fa0..19114c94b9a 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -284,6 +284,32 @@ units as well as normal text. This function is not suitable for sending spam e-mails. + Add a ``:collapsible:`` option to make the note collapsible. + This is useful for long notes that are not always relevant. + Example:: + + .. note:: + :collapsible: + + This note is collapsed. + + .. note:: + :collapsible: + :open: + + This note is collapsible, but initially open. + + .. note:: + :collapsible: + + This note is collapsed. + + .. note:: + :collapsible: + :open: + + This note is collapsible, but initially open. + .. rst:directive:: .. warning:: An important bit of information about an API that a user should be very aware From 42533e529514a62cb9c43f911664a655e0cc3529 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 3 Jul 2024 17:29:55 +0200 Subject: [PATCH 04/12] Update directives.rst --- doc/usage/restructuredtext/directives.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index 19114c94b9a..d9febf902f7 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -284,6 +284,10 @@ units as well as normal text. This function is not suitable for sending spam e-mails. + .. note:: + + This function is not suitable for sending spam e-mails. + Add a ``:collapsible:`` option to make the note collapsible. This is useful for long notes that are not always relevant. Example:: From 1b96eff0006107d4d1b4ce1b272016ab0f1b44f5 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 3 Jul 2024 17:35:52 +0200 Subject: [PATCH 05/12] Update sphinx13.css --- doc/_themes/sphinx13/static/sphinx13.css | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/_themes/sphinx13/static/sphinx13.css b/doc/_themes/sphinx13/static/sphinx13.css index 57afd6d15df..65e289145d4 100644 --- a/doc/_themes/sphinx13/static/sphinx13.css +++ b/doc/_themes/sphinx13/static/sphinx13.css @@ -430,6 +430,7 @@ details.admonition:not([open]) { details.admonition > summary.admonition-title { list-style: none; cursor: pointer; + padding-right: .5rem; } details.admonition > summary.admonition-title::after { background-color: currentcolor; From 0c44f1d24002dac7d62bf49ebb55bdeb62d2a430 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 26 Jan 2025 07:24:31 +0000 Subject: [PATCH 06/12] fmt --- sphinx/directives/admonitions.py | 12 +++++++----- sphinx/writers/html5.py | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/sphinx/directives/admonitions.py b/sphinx/directives/admonitions.py index d44de33f724..ffa079ed0fe 100644 --- a/sphinx/directives/admonitions.py +++ b/sphinx/directives/admonitions.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING from docutils import nodes from docutils.parsers.rst import directives @@ -10,6 +10,8 @@ from sphinx.util.docutils import SphinxDirective if TYPE_CHECKING: + from typing import ClassVar + from docutils.nodes import Element, Node from sphinx.application import Sphinx @@ -46,7 +48,9 @@ def run(self) -> list[Node]: admonition_node += title admonition_node += messages if 'classes' not in self.options: - admonition_node['classes'] += ['admonition-' + nodes.make_id(title_text)] + admonition_node['classes'] += [ + 'admonition-' + nodes.make_id(title_text) + ] admonition_node.extend(self.parse_content_to_nodes()) return [admonition_node] @@ -93,9 +97,7 @@ class Warning(BaseAdmonition): class SeeAlso(BaseAdmonition): - """ - An admonition mentioning things to look at as reference. - """ + """An admonition mentioning things to look at as reference.""" node_class = addnodes.seealso diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index fe2fac43536..8f68ef83151 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -370,13 +370,16 @@ def visit_admonition(self, node: Element, name: str = '') -> None: if node.get('collapsible'): if node.get('open'): self.body.append( - self.starttag(node, 'details', CLASS=('admonition ' + name), open='open') + self.starttag( + node, 'details', CLASS=('admonition ' + name), open='open' + ) ) else: - self.body.append(self.starttag(node, 'details', CLASS=('admonition ' + name))) + self.body.append( + self.starttag(node, 'details', CLASS=('admonition ' + name)) + ) else: - self.body.append(self.starttag( - node, 'div', CLASS=('admonition ' + name))) + self.body.append(self.starttag(node, 'div', CLASS=('admonition ' + name))) if name: node.insert(0, nodes.title(name, admonitionlabels[name])) @@ -512,8 +515,12 @@ def visit_title(self, node: Element) -> None: ) self.body.append('') self.context.append('

\n') - elif isinstance(node.parent, nodes.Admonition) and node.parent.get("collapsible"): # type: ignore[attr-defined] - self.body.append(self.starttag(node, 'summary', '', CLASS='admonition-title')) + elif isinstance(node.parent, nodes.Admonition) and node.parent.get( + 'collapsible' + ): # type: ignore[attr-defined] + self.body.append( + self.starttag(node, 'summary', '', CLASS='admonition-title') + ) self.context.append('\n') else: super().visit_title(node) From d5d3109e06676f220b4e097481a6489dc646656b Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 26 Jan 2025 08:08:48 +0000 Subject: [PATCH 07/12] Updates & tweaks --- doc/usage/restructuredtext/directives.rst | 78 +++++++++++-------- sphinx/directives/admonitions.py | 93 ++++++++++------------- sphinx/ext/todo.py | 31 +++----- sphinx/writers/html5.py | 36 ++++----- 4 files changed, 113 insertions(+), 125 deletions(-) diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index 87e760c6fcc..413a9591618 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -423,36 +423,6 @@ and the generic :rst:dir:`admonition` directive. Remember your sun cream! - .. note:: - - This function is not suitable for sending spam e-mails. - - Add a ``:collapsible:`` option to make the note collapsible. - This is useful for long notes that are not always relevant. - Example:: - - .. note:: - :collapsible: - - This note is collapsed. - - .. note:: - :collapsible: - :open: - - This note is collapsible, but initially open. - - .. note:: - :collapsible: - - This note is collapsed. - - .. note:: - :collapsible: - :open: - - This note is collapsible, but initially open. - .. rst:directive:: .. warning:: An important bit of information that the reader should be very aware of. @@ -510,6 +480,54 @@ and the generic :rst:dir:`admonition` directive. Documentation for tar archive files, including GNU tar extensions. +.. _collapsible-admonitions: + +.. rubric:: Collapsible text + +Each admonition directive supports a ``:collapsible:`` option, +to make the content of the admonition collapsible +(where supported by the output format). +This can be useful for content that is not always relevant. +By default, collapsible admonitions are initially open, +but this can be controlled with the ``open`` and ``closed`` arguments +to the ``:collapsible:`` option, which change the default state. +In output formats that don't support collapsible content, +the text is always included. +For example: + +.. code-block:: rst + + .. note:: + :collapsible: + + This note is collapsible, and initially open by default. + + .. admonition:: Example + :collapsible: open + + This example is collapsible, and initially open. + + .. hint:: + :collapsible: closed + + This hint is collapsible, but initially closed. + +.. note:: + :collapsible: + + This note is collapsible, and initially open by default. + +.. admonition:: Example + :collapsible: open + + This example is collapsible, and initially open. + +.. hint:: + :collapsible: closed + + This hint is collapsible, but initially closed. + + Describing changes between versions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/sphinx/directives/admonitions.py b/sphinx/directives/admonitions.py index ffa079ed0fe..ddd7a9eca73 100644 --- a/sphinx/directives/admonitions.py +++ b/sphinx/directives/admonitions.py @@ -3,8 +3,7 @@ from typing import TYPE_CHECKING from docutils import nodes -from docutils.parsers.rst import directives -from docutils.parsers.rst.roles import set_classes +from docutils.parsers.rst.directives.admonitions import BaseAdmonition from sphinx import addnodes from sphinx.util.docutils import SphinxDirective @@ -12,108 +11,94 @@ if TYPE_CHECKING: from typing import ClassVar - from docutils.nodes import Element, Node + from docutils.nodes import Node from sphinx.application import Sphinx from sphinx.util.typing import ExtensionMetadata, OptionSpec -class BaseAdmonition(SphinxDirective): - final_argument_whitespace = True - option_spec: ClassVar[OptionSpec] = { - 'class': directives.class_option, - 'name': directives.unchanged, - 'collapsible': directives.flag, - 'open': directives.flag, +def _collapsible_arg(argument: str | None) -> str: + if argument is None: + return 'open' + if (value := argument.lower().strip()) in {'open', 'closed'}: + return value + msg = f'"{argument}" unknown; choose from "open" or "closed".' + raise ValueError(msg) + + +class SphinxAdmonition(BaseAdmonition, SphinxDirective): + option_spec: ClassVar[OptionSpec] = BaseAdmonition.option_spec.copy() # type: ignore[union-attr] + option_spec |= { + 'collapsible': _collapsible_arg, } - has_content = True - node_class: ClassVar[type[Element]] = nodes.admonition + node_class: type[nodes.Admonition] = nodes.admonition """Subclasses must set this to the appropriate admonition node class.""" def run(self) -> list[Node]: - set_classes(self.options) - self.assert_has_content() - if 'collapsible' in self.options: - self.options['collapsible'] = True - if 'open' in self.options: - self.options['open'] = True - admonition_node = self.node_class('\n'.join(self.content), **self.options) - self.add_name(admonition_node) - if self.node_class is nodes.admonition: - title_text = self.arguments[0] - textnodes, messages = self.parse_inline(title_text, lineno=self.lineno) - title = nodes.title(title_text, '', *textnodes) - self.set_source_info(title) - admonition_node += title - admonition_node += messages - if 'classes' not in self.options: - admonition_node['classes'] += [ - 'admonition-' + nodes.make_id(title_text) - ] - admonition_node.extend(self.parse_content_to_nodes()) + (admonition_node,) = super().run() return [admonition_node] -class Admonition(BaseAdmonition): +class Admonition(SphinxAdmonition): required_arguments = 1 node_class = nodes.admonition -class Attention(BaseAdmonition): +class Attention(SphinxAdmonition): node_class = nodes.attention -class Caution(BaseAdmonition): +class Caution(SphinxAdmonition): node_class = nodes.caution -class Danger(BaseAdmonition): +class Danger(SphinxAdmonition): node_class = nodes.danger -class Error(BaseAdmonition): +class Error(SphinxAdmonition): node_class = nodes.error -class Hint(BaseAdmonition): +class Hint(SphinxAdmonition): node_class = nodes.hint -class Important(BaseAdmonition): +class Important(SphinxAdmonition): node_class = nodes.important -class Note(BaseAdmonition): +class Note(SphinxAdmonition): node_class = nodes.note -class Tip(BaseAdmonition): +class Tip(SphinxAdmonition): node_class = nodes.tip -class Warning(BaseAdmonition): +class Warning(SphinxAdmonition): node_class = nodes.warning -class SeeAlso(BaseAdmonition): +class SeeAlso(SphinxAdmonition): """An admonition mentioning things to look at as reference.""" node_class = addnodes.seealso def setup(app: Sphinx) -> ExtensionMetadata: - directives.register_directive('admonition', Admonition) - directives.register_directive('attention', Attention) - directives.register_directive('caution', Caution) - directives.register_directive('danger', Danger) - directives.register_directive('error', Error) - directives.register_directive('hint', Hint) - directives.register_directive('important', Important) - directives.register_directive('note', Note) - directives.register_directive('tip', Tip) - directives.register_directive('warning', Warning) - directives.register_directive('seealso', SeeAlso) + app.add_directive('admonition', Admonition, override=True) + app.add_directive('attention', Attention, override=True) + app.add_directive('caution', Caution, override=True) + app.add_directive('danger', Danger, override=True) + app.add_directive('error', Error, override=True) + app.add_directive('hint', Hint, override=True) + app.add_directive('important', Important, override=True) + app.add_directive('note', Note, override=True) + app.add_directive('tip', Tip, override=True) + app.add_directive('warning', Warning, override=True) + app.add_directive('seealso', SeeAlso, override=True) return { 'version': 'builtin', diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 0e192dbdadf..ab23921a6c9 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -12,11 +12,10 @@ from typing import TYPE_CHECKING, cast from docutils import nodes -from docutils.parsers.rst import directives -from docutils.parsers.rst.directives.admonitions import BaseAdmonition import sphinx from sphinx import addnodes +from sphinx.directives.admonitions import SphinxAdmonition from sphinx.domains import Domain from sphinx.errors import NoUri from sphinx.locale import _, __ @@ -46,35 +45,25 @@ class todolist(nodes.General, nodes.Element): pass -class Todo(BaseAdmonition, SphinxDirective): +class Todo(SphinxAdmonition): """A todo entry, displayed (if configured) in the form of an admonition.""" node_class = todo_node - has_content = True - required_arguments = 0 - optional_arguments = 0 - final_argument_whitespace = False - option_spec: ClassVar[OptionSpec] = { - 'class': directives.class_option, - 'name': directives.unchanged, - } def run(self) -> list[Node]: if not self.options.get('class'): self.options['class'] = ['admonition-todo'] (todo,) = super().run() - if isinstance(todo, nodes.system_message): + if not isinstance(todo, todo_node): return [todo] - elif isinstance(todo, todo_node): - todo.insert(0, nodes.title(text=_('Todo'))) - todo['docname'] = self.env.docname - self.add_name(todo) - self.set_source_info(todo) - self.state.document.note_explicit_target(todo) - return [todo] - else: - raise TypeError # never reached here + + todo.insert(0, nodes.title(text=_('Todo'))) + todo['docname'] = self.env.docname + self.add_name(todo) + self.set_source_info(todo) + self.state.document.note_explicit_target(todo) + return [todo] class TodoDomain(Domain): diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 8f68ef83151..c497d6120af 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -367,27 +367,21 @@ def visit_comment(self, node: Element) -> None: # overwritten def visit_admonition(self, node: Element, name: str = '') -> None: - if node.get('collapsible'): - if node.get('open'): - self.body.append( - self.starttag( - node, 'details', CLASS=('admonition ' + name), open='open' - ) - ) - else: - self.body.append( - self.starttag(node, 'details', CLASS=('admonition ' + name)) - ) - else: - self.body.append(self.starttag(node, 'div', CLASS=('admonition ' + name))) + attributes = {} + tag_name = 'div' + if collapsible := node.get('collapsible'): + tag_name = 'details' + if collapsible == 'open': + attributes['open'] = 'open' + self.body.append( + self.starttag(node, tag_name, CLASS=f'admonition {name}', **attributes) + ) + self.context.append(f'\n') if name: node.insert(0, nodes.title(name, admonitionlabels[name])) def depart_admonition(self, node: Element | None = None) -> None: - if node and node.get('collapsible'): - self.body.append('\n') - else: - self.body.append('\n') + self.body.append(self.context.pop()) def visit_seealso(self, node: Element) -> None: self.visit_admonition(node, 'seealso') @@ -515,9 +509,11 @@ def visit_title(self, node: Element) -> None: ) self.body.append('') self.context.append('

\n') - elif isinstance(node.parent, nodes.Admonition) and node.parent.get( - 'collapsible' - ): # type: ignore[attr-defined] + elif ( + isinstance(node.parent, nodes.Admonition) + and isinstance(node.parent, nodes.Element) + and 'collapsible' in node.parent + ): self.body.append( self.starttag(node, 'summary', '', CLASS='admonition-title') ) From 48a729780ecaf81cc9f6e965c70d8adb42be20c1 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 29 Jan 2025 11:35:03 +0100 Subject: [PATCH 08/12] add changes entry --- CHANGES.rst | 2 ++ doc/usage/restructuredtext/directives.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index da1f123ed66..19d308c5b4c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -70,6 +70,8 @@ Features added which defaults to ``True`` for backwards compatibility. The default will change to ``False`` in Sphinx 10. Patch by Adam Turner. +* #12507: Add the :ref:`collapsible ` option to admonition directives. + Patch by Chris Sewell. Bugs fixed ---------- diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index 413a9591618..0472b0ca476 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -484,6 +484,8 @@ and the generic :rst:dir:`admonition` directive. .. rubric:: Collapsible text +.. versionadded:: 8.2.0 + Each admonition directive supports a ``:collapsible:`` option, to make the content of the admonition collapsible (where supported by the output format). From 093779d6f309d3c8d50f04d3434e4916e65ef244 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 29 Jan 2025 12:11:18 +0100 Subject: [PATCH 09/12] Add html test --- .../conf.py | 0 .../index.rst | 17 +++++++++ tests/test_builders/test_build_html.py | 38 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 tests/roots/test-directives-admonition-collapse/conf.py create mode 100644 tests/roots/test-directives-admonition-collapse/index.rst diff --git a/tests/roots/test-directives-admonition-collapse/conf.py b/tests/roots/test-directives-admonition-collapse/conf.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/roots/test-directives-admonition-collapse/index.rst b/tests/roots/test-directives-admonition-collapse/index.rst new file mode 100644 index 00000000000..62387b361c2 --- /dev/null +++ b/tests/roots/test-directives-admonition-collapse/index.rst @@ -0,0 +1,17 @@ +test-directives-admonition-collapse +=================================== + +.. note:: + :collapsible: + + This note is collapsible, and initially open by default. + +.. admonition:: Example + :collapsible: open + + This example is collapsible, and initially open. + +.. hint:: + :collapsible: closed + + This hint is collapsible, but initially closed. diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 035ee90baa1..1377e1cef7a 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -715,3 +715,41 @@ def __call__(self, nodes): r'.//dt[@id="MyList"][1]', chk('class MyList[\nT\n](list[T])'), ) + + +@pytest.mark.sphinx('html', testroot='directives-admonition-collapse') +def test_html_admonition_collapse(app): + app.build() + fname = app.outdir / 'index.html' + etree = etree_parse(fname) + + def _create_check(text: str, open: bool): # type: ignore[no-untyped-def] + def _check(els): + assert len(els) == 1 + el = els[0] + if open: + assert el.attrib['open'] == 'open' + else: + assert 'open' not in el.attrib + assert el.find('p').text == text + + return _check + + check_xpath( + etree, + fname, + r'.//details[@class="admonition note"]', + _create_check('This note is collapsible, and initially open by default.', True), + ) + check_xpath( + etree, + fname, + r'.//details[@class="admonition-example admonition"]', + _create_check('This example is collapsible, and initially open.', True), + ) + check_xpath( + etree, + fname, + r'.//details[@class="admonition hint"]', + _create_check('This hint is collapsible, but initially closed.', False), + ) From 6f124c9a620de04ff0be0374222a64b0a49b7c25 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 29 Jan 2025 12:17:00 +0100 Subject: [PATCH 10/12] Update CHANGES.rst --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3780db8571c..1261d44b6fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -88,7 +88,8 @@ Features added * #13271: Support the ``:abstract:`` option for classes, methods, and properties in the Python domain. Patch by Adam Turner. -* #12507: Add the :ref:`collapsible ` option to admonition directives. +* #12507: Add the :ref:`collapsible ` option + to admonition directives. Patch by Chris Sewell. Bugs fixed From 9c126fa020ffed3ec7c3e5276f78020ab074c4de Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 29 Jan 2025 12:25:29 +0100 Subject: [PATCH 11/12] update test --- tests/roots/test-directives-admonition-collapse/index.rst | 5 +++++ tests/test_builders/test_build_html.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/tests/roots/test-directives-admonition-collapse/index.rst b/tests/roots/test-directives-admonition-collapse/index.rst index 62387b361c2..11023d2f62e 100644 --- a/tests/roots/test-directives-admonition-collapse/index.rst +++ b/tests/roots/test-directives-admonition-collapse/index.rst @@ -1,6 +1,11 @@ test-directives-admonition-collapse =================================== +.. note:: + :class: standard + + This is a standard note. + .. note:: :collapsible: diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 1377e1cef7a..c3f8ca3279e 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -735,6 +735,12 @@ def _check(els): return _check + check_xpath( + etree, + fname, + r'.//div[@class="standard admonition note"]//p', + 'This is a standard note.', + ) check_xpath( etree, fname, From cab019ac6b9ababffd87df754d3be56f390f5b58 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:39:04 +0000 Subject: [PATCH 12/12] Update doc/usage/restructuredtext/directives.rst --- doc/usage/restructuredtext/directives.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index 0472b0ca476..ee085788e1d 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -484,7 +484,7 @@ and the generic :rst:dir:`admonition` directive. .. rubric:: Collapsible text -.. versionadded:: 8.2.0 +.. versionadded:: 8.2 Each admonition directive supports a ``:collapsible:`` option, to make the content of the admonition collapsible