Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 24 additions & 5 deletions marimo/_output/md.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import re
import threading
from functools import cache
from importlib.util import find_spec
from inspect import cleandoc
Expand Down Expand Up @@ -231,6 +232,28 @@ def _get_extensions() -> list[str | markdown.Extension]:
return extensions


@cache
def _get_markdown() -> tuple[markdown.Markdown, threading.Lock]:
# 1. Markdown construction is expensive
# 2. User reported stack trace indicates there may be some race condition
# leading to failure (jedi previews also run through here).
# 3. Calling reset properly bumps footnote ids.
return (
markdown.Markdown(
extensions=_get_extensions(),
extension_configs=_get_extension_configs(),
),
threading.Lock(),
)


def _render_markdown(text: str) -> str:
md, lock = _get_markdown()
with lock:
md.reset()
return md.convert(text)
Comment thread
dmadisetti marked this conversation as resolved.


class _md(Html):
def __init__(
self,
Expand All @@ -245,11 +268,7 @@ def __init__(
self._markdown_text = text

# markdown.markdown appends a newline, hence strip
html_text = markdown.markdown(
text,
extensions=_get_extensions(),
extension_configs=_get_extension_configs(),
).strip()
html_text = _render_markdown(text).strip()
# replace <p> tags with <span> as HTML doesn't allow nested <div>s in <p>s
html_text = html_text.replace(
"<p>", '<span class="paragraph">'
Expand Down
41 changes: 34 additions & 7 deletions tests/_output/test_md.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

Expand Down Expand Up @@ -80,14 +81,25 @@ def test_md_links() -> None:


def test_md_footnotes() -> None:
# Test footnote conversion
# Test footnote conversion. The footnote extension assigns a unique
# prefix on each render (so multi-document pages don't collide), so
# match the structure rather than the exact prefix.
footnote_input = (
"Here is a footnote reference[^1].\n\n[^1]: Here is the footnote."
)
expected_output = '<span class="paragraph">Here is a footnote reference<sup id="fnref:2-1"><a class="footnote-ref" href="#fn:2-1">1</a></sup>.</span>\n<div class="footnote">\n<hr />\n<ol>\n<li id="fn:2-1">Here is the footnote.&#160;<a class="footnote-backref" href="#fnref:2-1" title="Jump back to footnote 1 in the text">&#8617;</a></li>\n</ol>\n</div>'
assert _md(footnote_input, apply_markdown_class=False).text == (
expected_output
rendered = _md(footnote_input, apply_markdown_class=False).text
match = re.search(
r'<span class="paragraph">Here is a footnote reference'
r'<sup id="fnref:(\d+-1)"><a class="footnote-ref" '
r'href="#fn:\1">1</a></sup>\.</span>\n'
r'<div class="footnote">\n<hr />\n<ol>\n'
r'<li id="fn:\1">Here is the footnote\.&#160;'
r'<a class="footnote-backref" href="#fnref:\1" '
r'title="Jump back to footnote 1 in the text">&#8617;</a></li>\n'
r"</ol>\n</div>",
rendered,
)
assert match is not None, rendered
Comment thread
dmadisetti marked this conversation as resolved.


def test_md_iconify() -> None:
Expand Down Expand Up @@ -617,13 +629,18 @@ def test_latex_via_url(mock_urlopen: MagicMock, output: MagicMock) -> None:
@patch("marimo._output.md.is_pyodide")
def test_b64_extension_not_in_non_wasm(mock_is_pyodide: MagicMock) -> None:
# Test that b64 extension is NOT included in non-WASM mode
from marimo._output.md import _get_extension_configs, _get_extensions
from marimo._output.md import (
_get_extension_configs,
_get_extensions,
_get_markdown,
)

mock_is_pyodide.return_value = False

# Clear the cache to ensure fresh evaluation
_get_extensions.cache_clear()
_get_extension_configs.cache_clear()
_get_markdown.cache_clear()

extensions = _get_extensions()
configs = _get_extension_configs()
Expand All @@ -640,14 +657,19 @@ def test_b64_extension_in_wasm(
mock_is_pyodide: MagicMock, mock_notebook_dir: MagicMock
) -> None:
# Test that b64 extension IS included in WASM mode
from marimo._output.md import _get_extension_configs, _get_extensions
from marimo._output.md import (
_get_extension_configs,
_get_extensions,
_get_markdown,
)

mock_is_pyodide.return_value = True
mock_notebook_dir.return_value = "/fake/notebook/dir"

# Clear the cache to ensure fresh evaluation
_get_extensions.cache_clear()
_get_extension_configs.cache_clear()
_get_markdown.cache_clear()

extensions = _get_extensions()
configs = _get_extension_configs()
Expand All @@ -667,7 +689,11 @@ def test_md_with_b64_in_wasm(
tmp_path: Path,
) -> None:
# Test that markdown with image paths gets base64 encoded in WASM mode
from marimo._output.md import _get_extension_configs, _get_extensions
from marimo._output.md import (
_get_extension_configs,
_get_extensions,
_get_markdown,
)

mock_is_pyodide.return_value = True
mock_notebook_dir.return_value = str(tmp_path)
Expand All @@ -685,6 +711,7 @@ def test_md_with_b64_in_wasm(
# Clear the cache to ensure fresh evaluation
_get_extensions.cache_clear()
_get_extension_configs.cache_clear()
_get_markdown.cache_clear()

# Test markdown with image reference using b64 syntax
# The pymdownx.b64 extension uses ![](test.png) syntax and converts to base64
Expand Down
Loading