From 9b7691042c22337c9d8740145b7381bd30659e3f Mon Sep 17 00:00:00 2001 From: Minijackson Date: Wed, 4 Feb 2026 19:17:58 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20support=20headings=20in=20ne?= =?UTF-8?q?sted=20parse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes using Markdown headings in Sphinx objects, like the `object`, `describe`, `py:data` directives, etc. This works by adding the heading to the temp root parent instead of at the end of the previous section from the main document. Fixes #1050 --- myst_parser/mdit_to_docutils/base.py | 51 +++++++++++-------- .../fixtures/sphinx_syntax_elements.md | 31 +++++++++++ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py index 85d64be3..f89960c3 100644 --- a/myst_parser/mdit_to_docutils/base.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -416,28 +416,35 @@ def copy_attributes( continue node[key] = value - def update_section_level_state(self, section: nodes.section, level: int) -> None: + def update_section_level_state( + self, + section: nodes.section, + level: int, + *, + parent: nodes.Element | None = None, + ) -> None: """Update the section level state, with the new current section and level.""" # find the closest parent section - parent_level = max( - section_level - for section_level in self._level_to_section - if level > section_level - ) - parent = self._level_to_section[parent_level] - - # if we are jumping up to a non-consecutive level, - # then warn about this, since this will not be propagated in the docutils AST - if (level > parent_level) and (parent_level + 1 != level): - msg = f"Non-consecutive header level increase; H{parent_level} to H{level}" - if parent_level == 0: - msg = f"Document headings start at H{level}, not H1" - self.create_warning( - msg, - MystWarnings.MD_HEADING_NON_CONSECUTIVE, - line=section.line, - append_to=self.current_node, + if parent is None: + parent_level = max( + section_level + for section_level in self._level_to_section + if level > section_level ) + parent = self._level_to_section[parent_level] + + # if we are jumping up to a non-consecutive level, + # then warn about this, since this will not be propagated in the docutils AST + if (level > parent_level) and (parent_level + 1 != level): + msg = f"Non-consecutive header level increase; H{parent_level} to H{level}" + if parent_level == 0: + msg = f"Document headings start at H{level}, not H1" + self.create_warning( + msg, + MystWarnings.MD_HEADING_NON_CONSECUTIVE, + line=section.line, + append_to=self.current_node, + ) # append the new section to the parent parent.append(section) @@ -838,7 +845,11 @@ def render_heading(self, token: SyntaxTreeNode) -> None: new_section["classes"].extend(["tex2jax_ignore", "mathjax_ignore"]) # update the state of the section levels - self.update_section_level_state(new_section, level) + self.update_section_level_state( + new_section, + level, + parent=self.current_node if parent_of_temp_root else None, + ) # create the title for this section title_node = nodes.title(token.children[0].content if token.children else "") diff --git a/tests/test_renderers/fixtures/sphinx_syntax_elements.md b/tests/test_renderers/fixtures/sphinx_syntax_elements.md index 6b90d3ad..3fb73b3c 100644 --- a/tests/test_renderers/fixtures/sphinx_syntax_elements.md +++ b/tests/test_renderers/fixtures/sphinx_syntax_elements.md @@ -121,6 +121,37 @@ Nested heading heading . +Nested heading in object +. +```{object} foo +bar +# level 1 +wunderbar +## level 2 +wunderbar +``` +. + + + + + + foo + + + bar +
+ + level 1 + <paragraph> + wunderbar + <section ids="level-2" names="level\ 2"> + <title> + level 2 + <paragraph> + wunderbar +. + Block Code: . foo From b2475db835b726de8bd73e12ce8964b77c1ee404 Mon Sep 17 00:00:00 2001 From: Minijackson <minijackson@riseup.net> Date: Wed, 4 Feb 2026 20:30:59 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=91=8C=20offset=20nested=20headers=20?= =?UTF-8?q?by=20current=20heading=20level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes it so nested headings (in blockquotes, admonitions, etc.) generates rubrics that are offset by the current heading level, meaning that: ```markdown # Title ## Sub-title > Quote: > # Title in quote > Content. ``` will generate a rubric level 3 (level 1 + offset by 2 by the "Sub-title") inside the blockquote. This also makes those rubrics support the Sphinx option `heading-level`, for properly generating `<h1>`, `<h2>`, etc. A fix in the documentation was necessary, as an example generated a level-4 heading, whose id wasn't generated due to a myst_heading_anchors value too low. --- docs/conf.py | 2 +- docs/syntax/cross-referencing.md | 4 ++-- myst_parser/mdit_to_docutils/base.py | 3 +++ .../fixtures/docutil_syntax_elements.md | 13 ++++++++++--- .../fixtures/sphinx_syntax_elements.md | 13 ++++++++++--- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 748bec75..55dea6d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -136,7 +136,7 @@ }, } myst_number_code_blocks = ["typescript"] -myst_heading_anchors = 2 +myst_heading_anchors = 3 myst_footnote_transition = True myst_dmath_double_inline = True myst_enable_checkboxes = True diff --git a/docs/syntax/cross-referencing.md b/docs/syntax/cross-referencing.md index 133fbd7d..9a9620e4 100644 --- a/docs/syntax/cross-referencing.md +++ b/docs/syntax/cross-referencing.md @@ -74,9 +74,9 @@ The anchor "slugs" are created according to the [GitHub implementation](https:// For example, using `myst_heading_anchors = 2`: ::::{myst-example} -## A heading with slug +# A heading with slug -## A heading with slug +# A heading with slug <project:#a-heading-with-slug> diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py index f89960c3..5b01d220 100644 --- a/myst_parser/mdit_to_docutils/base.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -825,8 +825,11 @@ def render_heading(self, token: SyntaxTreeNode) -> None: parent_of_temp_root or isinstance(self.current_node, nodes.document | nodes.section) ): + level_offset = max(self._level_to_section.keys()) + level += level_offset # if this is not the case, we create a rubric node instead rubric = nodes.rubric(token.content, "", level=level) + rubric["heading-level"] = level self.add_line_and_source_path(rubric, token) self.copy_attributes(token, rubric, ("class", "id")) with self.current_node_context(rubric, append=True): diff --git a/tests/test_renderers/fixtures/docutil_syntax_elements.md b/tests/test_renderers/fixtures/docutil_syntax_elements.md index 8c323b52..3c33a73d 100644 --- a/tests/test_renderers/fixtures/docutil_syntax_elements.md +++ b/tests/test_renderers/fixtures/docutil_syntax_elements.md @@ -113,12 +113,19 @@ Heading Levels: Nested heading . +# Main heading > # heading +> ## sub-heading . <document source="notset"> - <block_quote> - <rubric ids="heading" level="1" names="heading"> - heading + <section ids="main-heading" names="main\ heading"> + <title> + Main heading + <block_quote> + <rubric heading-level="2" ids="heading" level="2" names="heading"> + heading + <rubric heading-level="3" ids="sub-heading" level="3" names="sub-heading"> + sub-heading . Block Code: diff --git a/tests/test_renderers/fixtures/sphinx_syntax_elements.md b/tests/test_renderers/fixtures/sphinx_syntax_elements.md index 3fb73b3c..400d2a76 100644 --- a/tests/test_renderers/fixtures/sphinx_syntax_elements.md +++ b/tests/test_renderers/fixtures/sphinx_syntax_elements.md @@ -113,12 +113,19 @@ Heading Levels: Nested heading . +# Main heading > # heading +> ## sub-heading . <document source="<src>/index.md"> - <block_quote> - <rubric ids="heading" level="1" names="heading"> - heading + <section ids="main-heading" names="main\ heading"> + <title> + Main heading + <block_quote> + <rubric heading-level="2" ids="heading" level="2" names="heading"> + heading + <rubric heading-level="3" ids="sub-heading" level="3" names="sub-heading"> + sub-heading . Nested heading in object