diff --git a/myst_nb/core/nb_to_tokens.py b/myst_nb/core/nb_to_tokens.py index c8e48f03..59448334 100644 --- a/myst_nb/core/nb_to_tokens.py +++ b/myst_nb/core/nb_to_tokens.py @@ -48,6 +48,7 @@ def notebook_to_tokens( # generate tokens tokens: list[Token] + cell_id = nb_cell.get("id", None) if nb_cell["cell_type"] == "markdown": # https://nbformat.readthedocs.io/en/5.1.3/format_description.html#markdown-cells # TODO if cell has tag output-caption, then use as caption for next/preceding cell? @@ -60,6 +61,7 @@ def notebook_to_tokens( meta={ "index": cell_index, "metadata": nb_node_to_dict(nb_cell["metadata"]), + "id": cell_id, }, map=[0, len(nb_cell["source"].splitlines()) - 1], ), @@ -88,6 +90,7 @@ def notebook_to_tokens( meta={ "index": cell_index, "metadata": nb_node_to_dict(nb_cell["metadata"]), + "id": cell_id, }, map=[0, 0], ) @@ -106,6 +109,7 @@ def notebook_to_tokens( meta={ "index": cell_index, "metadata": nb_node_to_dict(nb_cell["metadata"]), + "id": cell_id, }, map=[0, 0], ) diff --git a/myst_nb/core/read.py b/myst_nb/core/read.py index 2b3dae4b..56609097 100644 --- a/myst_nb/core/read.py +++ b/myst_nb/core/read.py @@ -31,6 +31,8 @@ class NbReader: """The configuration for parsing markdown cells.""" read_fmt: dict | None = dc.field(default=None) """The type of the reader, if known.""" + support_cell_ids: bool = False + """Whether the format supports stable cell IDs""" def standard_nb_read(text: str) -> nbf.NotebookNode: @@ -75,7 +77,11 @@ def create_nb_reader( if commonmark_only: # Markdown cells should be read as Markdown only md_config = dc.replace(md_config, commonmark_only=True) - return NbReader(partial(reader, **(reader_kwargs or {})), md_config) # type: ignore + return NbReader( + partial(reader, **(reader_kwargs or {})), # type: ignore + md_config, + support_cell_ids=suffix == ".ipynb", + ) # a Markdown file is a special case, since we only treat it as a notebook, # if it starts with certain "top-matter" @@ -89,6 +95,7 @@ def create_nb_reader( ), md_config, {"type": "plugin", "name": "myst_nb_md"}, + support_cell_ids=False, ) # if we get here, we did not find a reader diff --git a/myst_nb/core/render.py b/myst_nb/core/render.py index 14b82816..5baecf85 100644 --- a/myst_nb/core/render.py +++ b/myst_nb/core/render.py @@ -64,6 +64,7 @@ class MditRenderMixin: current_node: Any current_node_context: Any create_highlighted_code_block: Any + _heading_slugs: Any # TODO create & expose a method in `DocutilsRenderer` @property def nb_config(self: SelfType) -> NbParserConfig: @@ -179,6 +180,17 @@ def render_nb_cell_code(self: SelfType, token: SyntaxTreeNode) -> None: cell_metadata=token.meta["metadata"], classes=classes, ) + + cell_id = token.meta["id"] + if cell_id and not cell_id.startswith("RANDOM_CELL_ID"): + self.document.note_implicit_target(cell_container, cell_container) + slug = "cell-id=" + cell_id + cell_container["slug"] = slug + self._heading_slugs[slug] = ( + cell_container.line, + cell_container["ids"][0], + "", + ) if hide_mode: cell_container["hide_mode"] = hide_mode code_prompt_show = self.get_cell_level_config( diff --git a/myst_nb/sphinx_.py b/myst_nb/sphinx_.py index 4885d9ff..e3598622 100644 --- a/myst_nb/sphinx_.py +++ b/myst_nb/sphinx_.py @@ -88,6 +88,12 @@ def parse(self, inputstring: str, document: nodes.document) -> None: return super().parse(inputstring, document) notebook = nb_reader.read(inputstring) + if not nb_reader.support_cell_ids: + # mark randomly generated cell IDs + for cell in notebook["cells"]: + if "id" in cell: + cell["id"] = "RANDOM_CELL_ID_" + cell["id"] + # update the global markdown config with the file-level config warning = lambda wtype, msg: create_warning( # noqa: E731 document, msg, line=1, append_to=document, subtype=wtype