diff --git a/marimo/_output/md.py b/marimo/_output/md.py
index aa760b060ac..21cc60efa22 100644
--- a/marimo/_output/md.py
+++ b/marimo/_output/md.py
@@ -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
@@ -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)
+
+
class _md(Html):
def __init__(
self,
@@ -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
tags with as HTML doesn't allow nested s in
s
html_text = html_text.replace(
"
", ''
diff --git a/tests/_output/test_md.py b/tests/_output/test_md.py
index d0bea3d3b68..1a3706fb717 100644
--- a/tests/_output/test_md.py
+++ b/tests/_output/test_md.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import re
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@@ -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 = 'Here is a footnote reference.\n'
- assert _md(footnote_input, apply_markdown_class=False).text == (
- expected_output
+ rendered = _md(footnote_input, apply_markdown_class=False).text
+ match = re.search(
+ r'Here is a footnote reference'
+ r'\.\n'
+ r'",
+ rendered,
)
+ assert match is not None, rendered
def test_md_iconify() -> None:
@@ -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()
@@ -640,7 +657,11 @@ 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"
@@ -648,6 +669,7 @@ def test_b64_extension_in_wasm(
# 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()
@@ -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)
@@ -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  syntax and converts to base64