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 reference1.\n

\n
\n
    \n
  1. Here is the footnote. 
  2. \n
\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'1\.\n' + r'
\n
\n
    \n' + r'
  1. Here is the footnote\. ' + r'
  2. \n' + r"
\n
", + 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 ![](test.png) syntax and converts to base64