Skip to content

Add allow_section_headings to SphinxDirective parsing methods #12503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sphinx/directives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ def run(self) -> list[Node]:
# 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',
Expand Down
2 changes: 1 addition & 1 deletion sphinx/domains/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def run(self) -> list[Node]:
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:
Expand Down
2 changes: 1 addition & 1 deletion sphinx/domains/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def run(self) -> list[Node]:
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:
Expand Down
4 changes: 3 additions & 1 deletion sphinx/domains/std/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,9 @@ def run(self) -> list[Node]:

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))
Expand Down
3 changes: 2 additions & 1 deletion sphinx/ext/autosummary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,8 @@ def append_row(*column_texts: str) -> None:
for text in column_texts:
vl = StringList([text], f'{source}:{line}:<autosummary>')
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:
Expand Down
2 changes: 1 addition & 1 deletion sphinx/ext/ifconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def run(self) -> list[Node]:
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]


Expand Down
40 changes: 34 additions & 6 deletions sphinx/util/docutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,21 +434,49 @@ def get_location(self) -> str:
return f'<unknown>:{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_text_to_nodes(self, text: str = '', /, *, offset: int = -1) -> list[Node]:
def parse_content_to_nodes(self, allow_section_headings: bool = False) -> list[Node]:
"""Parse the directive's content into nodes.

: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.
Comment on lines +440 to +446
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much better thanks!

"""
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,
Expand Down
2 changes: 1 addition & 1 deletion sphinx/util/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 10 additions & 2 deletions sphinx/util/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def nested_parse_to_nodes(
*,
source: str = '<generated text>',
offset: int = 0,
allow_section_headings: bool = True,
keep_title_context: bool = False,
) -> list[nodes.Node]: # Element | nodes.Text
"""Parse *text* into nodes.
Expand All @@ -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)
Expand All @@ -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


Expand Down
4 changes: 2 additions & 2 deletions tests/test_util/test_util_docutils_sphinx_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='<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)
Expand All @@ -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)
Expand Down