diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py index 7d1a8af9..61c8d72d 100644 --- a/myst_parser/mdit_to_docutils/base.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -118,6 +118,7 @@ def __getattr__(self, name: str): "current_node", "reporter", "language_module_rst", + "_heading_offset", "_level_to_section", ): raise AttributeError( @@ -142,6 +143,7 @@ def setup_render( self.language_module_rst: ModuleType = get_language_rst( self.document.settings.language_code ) + self._heading_offset: int = 0 # a mapping of heading levels to its currently associated node self._level_to_section: dict[int, nodes.document | nodes.section] = { 0: self.document @@ -324,6 +326,7 @@ def nested_render_text( lineno: int, inline: bool = False, temp_root_node: None | nodes.Element = None, + heading_offset: int = 0, ) -> None: """Render unparsed text (appending to the current node). @@ -331,6 +334,7 @@ def nested_render_text( :param lineno: the starting line number of the text, within the full source :param inline: whether the text is inline or block :param temp_root_node: If set, allow sections to be created as children of this node + :param heading_offset: offset heading levels by this amount """ tokens = ( self.md.parseInline(text, self.md_env) @@ -347,22 +351,27 @@ def nested_render_text( if token.map: token.map = [token.map[0] + lineno, token.map[1] + lineno] - if temp_root_node is None: - self._render_tokens(tokens) - else: - # we need to temporarily set the root node, - # and we also want to restore the level_to_section mapping at the end - current_level_to_section = { - i: node for i, node in self._level_to_section.items() - } - current_root_node = self.md_env.get("temp_root_node", None) - try: + @contextmanager + def _restore(): + current_heading_offset = self._heading_offset + self._heading_offset = heading_offset + if temp_root_node is not None: + # we need to temporarily set the root node, + # and we also want to restore the level_to_section mapping at the end + current_level_to_section = { + i: node for i, node in self._level_to_section.items() + } + current_root_node = self.md_env.get("temp_root_node", None) self.md_env["temp_root_node"] = temp_root_node - self._render_tokens(tokens) - finally: + yield + self._heading_offset = current_heading_offset + if temp_root_node is not None: self.md_env["temp_root_node"] = current_root_node self._level_to_section = current_level_to_section + with _restore(): + self._render_tokens(tokens) + @contextmanager def current_node_context( self, node: nodes.Element, append: bool = False @@ -826,7 +835,7 @@ def generate_heading_target( def render_heading(self, token: SyntaxTreeNode) -> None: """Render a heading, e.g. `# Heading`.""" - level = int(token.tag[1]) + level = int(token.tag[1]) + self._heading_offset # sections are only allowed as a parent of a document or another section # the only exception to this, is if a directive has called a nested parse, @@ -1667,6 +1676,7 @@ def run_directive( # to allow for altering relative image reference links directive_class.option_spec["relative-images"] = directives.flag directive_class.option_spec["relative-docs"] = directives.path + directive_class.option_spec["heading-offset"] = directives.nonnegative_int try: parsed = parse_directive_text(directive_class, first_line, content) diff --git a/myst_parser/mocking.py b/myst_parser/mocking.py index 9c8848b4..d91b9c5e 100644 --- a/myst_parser/mocking.py +++ b/myst_parser/mocking.py @@ -469,7 +469,11 @@ def run(self) -> list[nodes.Element]: source_dir, path.parent, ) - self.renderer.nested_render_text(file_content, startline + 1) + self.renderer.nested_render_text( + file_content, + startline + 1, + heading_offset=self.options.get("heading-offset", 0), + ) finally: self.renderer.document["source"] = source self.renderer.reporter.source = rsource