From 24a03859978f12b7e5f2c81f24e89b2a383ed968 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:53:58 +0100 Subject: [PATCH] Add ``allow_section_headings`` to SphinxDirective parsing methods (#12503) --- sphinx/directives/__init__.py | 3 +- sphinx/domains/javascript.py | 2 +- sphinx/domains/python/__init__.py | 2 +- sphinx/domains/std/__init__.py | 4 +- sphinx/ext/autosummary/__init__.py | 3 +- sphinx/ext/ifconfig.py | 2 +- sphinx/util/docutils.py | 38 ++++++++++++++++--- sphinx/util/nodes.py | 2 +- sphinx/util/parsing.py | 12 +++++- .../test_util_docutils_sphinx_directive.py | 4 +- 10 files changed, 56 insertions(+), 16 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index cf9b9d0f3..a9699f168 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -278,7 +278,8 @@ class ObjectDescription(SphinxDirective, Generic[ObjDescT]): # needed for association of version{added,changed} directives self.env.temp_data['object'] = self.names[0] self.before_content() - content_node = addnodes.desc_content('', *self.parse_content_to_nodes()) + content_children = self.parse_content_to_nodes(allow_section_headings=True) + content_node = addnodes.desc_content('', *content_children) node.append(content_node) self.transform_content(content_node) self.env.app.emit('object-description-transform', diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 2e49461af..4a65945e0 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -311,7 +311,7 @@ class JSModule(SphinxDirective): self.env.ref_context['js:module'] = mod_name no_index = 'no-index' in self.options or 'noindex' in self.options - content_nodes = self.parse_content_to_nodes() + content_nodes = self.parse_content_to_nodes(allow_section_headings=True) ret: list[Node] = [] if not no_index: diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py index 22a23f226..75c2cddfb 100644 --- a/sphinx/domains/python/__init__.py +++ b/sphinx/domains/python/__init__.py @@ -416,7 +416,7 @@ class PyModule(SphinxDirective): no_index = 'no-index' in self.options or 'noindex' in self.options self.env.ref_context['py:module'] = modname - content_nodes = self.parse_content_to_nodes() + content_nodes = self.parse_content_to_nodes(allow_section_headings=True) ret: list[Node] = [] if not no_index: diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py index 5ac6ae5ac..504c95027 100644 --- a/sphinx/domains/std/__init__.py +++ b/sphinx/domains/std/__init__.py @@ -405,7 +405,9 @@ class Glossary(SphinxDirective): if definition: offset = definition.items[0][1] - definition_nodes = nested_parse_to_nodes(self.state, definition, offset=offset) + definition_nodes = nested_parse_to_nodes( + self.state, definition, offset=offset, allow_section_headings=False, + ) else: definition_nodes = [] termnodes.append(nodes.definition('', *definition_nodes)) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 2d2d7f6de..5bbb0d9dc 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -409,7 +409,8 @@ class Autosummary(SphinxDirective): for text in column_texts: vl = StringList([text], f'{source}:{line}:') with switch_source_input(self.state, vl): - col_nodes = nested_parse_to_nodes(self.state, vl) + col_nodes = nested_parse_to_nodes(self.state, vl, + allow_section_headings=False) if col_nodes and isinstance(col_nodes[0], nodes.paragraph): node = col_nodes[0] else: diff --git a/sphinx/ext/ifconfig.py b/sphinx/ext/ifconfig.py index 489920392..17331a0bd 100644 --- a/sphinx/ext/ifconfig.py +++ b/sphinx/ext/ifconfig.py @@ -47,7 +47,7 @@ class IfConfig(SphinxDirective): node.document = self.state.document self.set_source_info(node) node['expr'] = self.arguments[0] - node += self.parse_content_to_nodes() + node += self.parse_content_to_nodes(allow_section_headings=True) return [node] diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index fdb137798..a43dfb52c 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -434,21 +434,49 @@ class SphinxDirective(Directive): return f':{line}' return '' - def parse_content_to_nodes(self) -> list[Node]: - """Parse the directive's content into nodes.""" - return nested_parse_to_nodes(self.state, self.content, offset=self.content_offset) + def parse_content_to_nodes(self, allow_section_headings: bool = False) -> list[Node]: + """Parse the directive's content into nodes. - def parse_text_to_nodes(self, text: str = '', /, *, offset: int = -1) -> list[Node]: + :param allow_section_headings: + Are titles (sections) allowed in the directive's content? + Note that this option bypasses Docutils' usual checks on + doctree structure, and misuse of this option can lead to + an incoherent doctree. In Docutils, section nodes should + only be children of ``Structural`` nodes, which includes + ``document``, ``section``, and ``sidebar`` nodes. + """ + return nested_parse_to_nodes( + self.state, + self.content, + offset=self.content_offset, + allow_section_headings=allow_section_headings, + ) + + def parse_text_to_nodes( + self, text: str = '', /, *, offset: int = -1, allow_section_headings: bool = False, + ) -> list[Node]: """Parse *text* into nodes. :param text: Text, in string form. ``StringList`` is also accepted. + :param allow_section_headings: + Are titles (sections) allowed in *text*? + Note that this option bypasses Docutils' usual checks on + doctree structure, and misuse of this option can lead to + an incoherent doctree. In Docutils, section nodes should + only be children of ``Structural`` nodes, which includes + ``document``, ``section``, and ``sidebar`` nodes. :param offset: The offset of the content. """ if offset == -1: offset = self.content_offset - return nested_parse_to_nodes(self.state, text, offset=offset) + return nested_parse_to_nodes( + self.state, + text, + offset=offset, + allow_section_headings=allow_section_headings, + ) def parse_inline( self, text: str, *, lineno: int = -1, diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 82672c2ff..9f3e827c8 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -334,7 +334,7 @@ def nested_parse_with_titles(state: RSTState, content: StringList, node: Node, context, such as docstrings. This function is retained for compatability and will be deprecated in - Sphinx 8. Prefer ``parse_block_text()``. + Sphinx 8. Prefer ``nested_parse_to_nodes()``. """ with _fresh_title_style_context(state): ret = state.nested_parse(content, content_offset, node, match_titles=True) diff --git a/sphinx/util/parsing.py b/sphinx/util/parsing.py index f3a186e64..bbf485c46 100644 --- a/sphinx/util/parsing.py +++ b/sphinx/util/parsing.py @@ -20,6 +20,7 @@ def nested_parse_to_nodes( *, source: str = '', offset: int = 0, + allow_section_headings: bool = True, keep_title_context: bool = False, ) -> list[nodes.Node]: # Element | nodes.Text """Parse *text* into nodes. @@ -32,6 +33,13 @@ def nested_parse_to_nodes( The text's source, used when creating a new ``StringList``. :param offset: The offset of the content. + :param allow_section_headings: + Are titles (sections) allowed in *text*? + Note that this option bypasses Docutils' usual checks on + doctree structure, and misuse of this option can lead to + an incoherent doctree. In Docutils, section nodes should + only be children of ``Structural`` nodes, which includes + ``document``, ``section``, and ``sidebar`` nodes. :param keep_title_context: If this is False (the default), then *content* is parsed as if it were an independent document, meaning that title decorations (e.g. underlines) @@ -49,10 +57,10 @@ def nested_parse_to_nodes( node.document = document if keep_title_context: - state.nested_parse(content, offset, node, match_titles=True) + state.nested_parse(content, offset, node, match_titles=allow_section_headings) else: with _fresh_title_style_context(state): - state.nested_parse(content, offset, node, match_titles=True) + state.nested_parse(content, offset, node, match_titles=allow_section_headings) return node.children diff --git a/tests/test_util/test_util_docutils_sphinx_directive.py b/tests/test_util/test_util_docutils_sphinx_directive.py index 281f61f6b..3b6784b17 100644 --- a/tests/test_util/test_util_docutils_sphinx_directive.py +++ b/tests/test_util/test_util_docutils_sphinx_directive.py @@ -100,7 +100,7 @@ def test_sphinx_directive_parse_content_to_nodes(): content = 'spam\n====\n\nEggs! *Lobster thermidor.*' directive.content = StringList(content.split('\n'), source='') - parsed = directive.parse_content_to_nodes() + parsed = directive.parse_content_to_nodes(allow_section_headings=True) assert len(parsed) == 1 node = parsed[0] assert isinstance(node, nodes.section) @@ -115,7 +115,7 @@ def test_sphinx_directive_parse_text_to_nodes(): directive = make_directive(env=SimpleNamespace()) content = 'spam\n====\n\nEggs! *Lobster thermidor.*' - parsed = directive.parse_text_to_nodes(content) + parsed = directive.parse_text_to_nodes(content, allow_section_headings=True) assert len(parsed) == 1 node = parsed[0] assert isinstance(node, nodes.section)