From 2c459f9978691a87f1d638faec57c03c5abc8efb Mon Sep 17 00:00:00 2001
From: Chris Sewell <chrisj_sewell@hotmail.com>
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('</div>\n')
+        if node and node.get('collapsible'):
+            self.body.append('</details>\n')
+        else:
+            self.body.append('</div>\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('<span class="caption-text">')
             self.context.append('</span></p>\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('</summary>\n')
         else:
             super().visit_title(node)
         self.add_secnumber(node)

From 2c5d76658c29a84a24d2ea4c4be45457adad7eca Mon Sep 17 00:00:00 2001
From: Chris Sewell <chrisj_sewell@hotmail.com>
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 <chrisj_sewell@hotmail.com>
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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 14h-2v-4h2m0 8h-2v-2h2M1 21h22L12 2 1 21z"/></svg>');
     --icon-failure: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2c5.53 0 10 4.47 10 10s-4.47 10-10 10S2 17.53 2 12 6.47 2 12 2m3.59 5L12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59 13.41 12 17 8.41 15.59 7z"/></svg>');
     --icon-spark: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.5 20l4.86-9.73H13V4l-5 9.73h3.5V20M12 2c2.75 0 5.1 1 7.05 2.95C21 6.9 22 9.25 22 12s-1 5.1-2.95 7.05C17.1 21 14.75 22 12 22s-5.1-1-7.05-2.95C3 17.1 2 14.75 2 12s1-5.1 2.95-7.05C6.9 3 9.25 2 12 2z"/></svg>');
+
+    /* icons used for details summaries */
+    --icon-details-open: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z"/></svg>');
 }
 
 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 <chrisj_sewell@hotmail.com>
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 <chrisj_sewell@hotmail.com>
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('<span class="caption-text">')
             self.context.append('</span></p>\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('</summary>\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'</{tag_name}>\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('</details>\n')
-        else:
-            self.body.append('</div>\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('<span class="caption-text">')
             self.context.append('</span></p>\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 <chrisj_sewell@hotmail.com>
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 <collapsible-admonitions>` 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 <chrisj_sewell@hotmail.com>
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 <chrisj_sewell@hotmail.com>
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 <collapsible-admonitions>` option to admonition directives.
+* #12507: Add the :ref:`collapsible <collapsible-admonitions>` option
+  to admonition directives.
   Patch by Chris Sewell.
 
 Bugs fixed

From 9c126fa020ffed3ec7c3e5276f78020ab074c4de Mon Sep 17 00:00:00 2001
From: Chris Sewell <chrisj_sewell@hotmail.com>
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