From 6971e6505f8f12b10b5d2adebb975db8d27d9d0b Mon Sep 17 00:00:00 2001 From: YellowFoxH4XOR Date: Mon, 29 Jun 2026 09:31:21 +0530 Subject: [PATCH] fix(ipynb): use dynamic code fence so cells containing backticks don't leak Code and raw notebook cells were always wrapped in a 3-backtick fence without inspecting the cell content. A cell whose source itself contains a ``` line (common in notebooks that demo Markdown or print fenced strings) closed the fence early, leaking the rest of the cell out as prose and corrupting the document structure. Wrap cells with a fence longer than the longest backtick run in the content, per CommonMark. Adds a regression test. --- .../markitdown/converters/_ipynb_converter.py | 20 +++++++++- packages/markitdown/tests/test_module_misc.py | 37 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/markitdown/src/markitdown/converters/_ipynb_converter.py b/packages/markitdown/src/markitdown/converters/_ipynb_converter.py index b15e77aa2..3ec423080 100644 --- a/packages/markitdown/src/markitdown/converters/_ipynb_converter.py +++ b/packages/markitdown/src/markitdown/converters/_ipynb_converter.py @@ -1,10 +1,24 @@ from typing import BinaryIO, Any import json +import re from .._base_converter import DocumentConverter, DocumentConverterResult from .._exceptions import FileConversionException from .._stream_info import StreamInfo + +def _fenced_code_block(content: str, info_string: str = "") -> str: + """Wrap content in a Markdown code fence that is guaranteed not to be + closed prematurely by backticks inside the content. Per CommonMark, the + fence must be longer than the longest run of backticks it contains, so a + cell that itself prints ``` (common in notebooks demoing Markdown) does not + leak out as prose.""" + longest_backtick_run = max( + (len(m) for m in re.findall(r"`+", content)), default=0 + ) + fence = "`" * max(3, longest_backtick_run + 1) + return f"{fence}{info_string}\n{content}\n{fence}" + CANDIDATE_MIME_TYPE_PREFIXES = [ "application/json", ] @@ -76,9 +90,11 @@ def _convert(self, notebook_content: dict) -> DocumentConverterResult: elif cell_type == "code": # Code cells are wrapped in Markdown code blocks - md_output.append(f"```python\n{''.join(source_lines)}\n```") + md_output.append( + _fenced_code_block("".join(source_lines), "python") + ) elif cell_type == "raw": - md_output.append(f"```\n{''.join(source_lines)}\n```") + md_output.append(_fenced_code_block("".join(source_lines))) md_text = "\n\n".join(md_output) diff --git a/packages/markitdown/tests/test_module_misc.py b/packages/markitdown/tests/test_module_misc.py index 4d62e4919..ffacdffce 100644 --- a/packages/markitdown/tests/test_module_misc.py +++ b/packages/markitdown/tests/test_module_misc.py @@ -532,9 +532,46 @@ def test_markitdown_llm() -> None: validate_strings(result, PPTX_TEST_STRINGS) +def test_ipynb_code_cell_with_backtick_fence() -> None: + """A notebook code/raw cell whose source itself contains a ``` fence must + be wrapped in a longer fence, otherwise the inner backticks close the code + block early and the cell's content leaks out as prose. The cell source must + survive verbatim as a single fenced block.""" + import json + + inner = 'print("""\n```\nnot python\n```\n""")' + notebook = { + "nbformat": 4, + "nbformat_minor": 5, + "metadata": {}, + "cells": [ + { + "cell_type": "code", + "source": [inner], + "metadata": {}, + "outputs": [], + "execution_count": None, + } + ], + } + result = MarkItDown().convert_stream( + io.BytesIO(json.dumps(notebook).encode("utf-8")), file_extension=".ipynb" + ) + + # The opening fence must be longer than the 3-backtick run inside the cell. + fence_match = re.match(r"(`{4,})python\n", result.markdown) + assert fence_match, ( + f"code cell was not wrapped in a long-enough fence: {result.markdown!r}" + ) + fence = fence_match.group(1) + # The cell's source must appear intact, enclosed by the longer fence. + assert f"{fence}python\n{inner}\n{fence}" in result.markdown + + if __name__ == "__main__": """Runs this file's tests from the command line.""" for test in [ + test_ipynb_code_cell_with_backtick_fence, test_stream_info_operations, test_data_uris, test_file_uris,