Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 14 additions & 6 deletions packages/markitdown/src/markitdown/converters/_ipynb_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,31 @@ def _convert(self, notebook_content: dict) -> DocumentConverterResult:

for cell in notebook_content.get("cells", []):
cell_type = cell.get("cell_type", "")
source_lines = cell.get("source", [])
# nbformat's `multiline_string` is a string *or* a list of lines, and
# both are valid; `nbformat.reads` keeps whichever the file used.
# Joining the list and taking the string as-is give the same text.
source = cell.get("source", [])
source_text = source if isinstance(source, str) else "".join(source)

if cell_type == "markdown":
md_output.append("".join(source_lines))
md_output.append(source_text)

# Extract the first # heading as title if not already found
# Extract the first # heading as title if not already found.
# Split the text rather than walking the raw value: a string
# source would otherwise be iterated one character at a time,
# and a list entry holding several lines would hide a heading
# that is not on its first one.
if title is None:
for line in source_lines:
for line in source_text.splitlines():
if line.startswith("# "):
title = line.removeprefix("# ").strip()
break

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(f"```python\n{source_text}\n```")
elif cell_type == "raw":
md_output.append(f"```\n{''.join(source_lines)}\n```")
md_output.append(f"```\n{source_text}\n```")

md_text = "\n\n".join(md_output)

Expand Down
79 changes: 79 additions & 0 deletions packages/markitdown/tests/test_module_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1927,3 +1927,82 @@ def fake_run(args, **kwargs):
test()
print("OK")
print("All tests passed!")


def test_ipynb_string_source_is_read_as_text_not_characters() -> None:
"""`source` may be a single string, and the title must still be found.

nbformat's `multiline_string` is a string or a list of lines. Both validate,
and `nbformat.reads` keeps whichever the file used. Walking the raw value
iterated a string one character at a time, so no character ever started with
"# " and the notebook came out titleless.
"""
from markitdown.converters._ipynb_converter import IpynbConverter

notebook = {
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {},
"cells": [
{
"cell_type": "markdown",
"source": "# My Notebook\nintro text\n",
"metadata": {},
},
{"cell_type": "code", "source": "print('hello')\n", "metadata": {}},
],
}

result = IpynbConverter()._convert(notebook)

assert result.title == "My Notebook"
assert "# My Notebook" in result.markdown
assert "```python\nprint('hello')" in result.markdown


def test_ipynb_string_and_list_sources_agree() -> None:
"""The two spellings of the same notebook convert to the same thing."""
from markitdown.converters._ipynb_converter import IpynbConverter

def notebook(markdown_source, code_source):
return {
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {},
"cells": [
{
"cell_type": "markdown",
"source": markdown_source,
"metadata": {},
},
{"cell_type": "code", "source": code_source, "metadata": {}},
],
}

as_list = IpynbConverter()._convert(
notebook(["# Title\n", "body\n"], ["print(1)\n"])
)
as_string = IpynbConverter()._convert(notebook("# Title\nbody\n", "print(1)\n"))

assert as_list.title == as_string.title == "Title"
assert as_list.markdown == as_string.markdown


def test_ipynb_heading_below_the_first_line_of_a_source_entry() -> None:
"""A list entry can hold several lines; a heading on a later one still counts."""
from markitdown.converters._ipynb_converter import IpynbConverter

notebook = {
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {},
"cells": [
{
"cell_type": "markdown",
"source": ["intro paragraph\n# My Notebook\n"],
"metadata": {},
}
],
}

assert IpynbConverter()._convert(notebook).title == "My Notebook"