Private
Public Access
0
0

feat(markdown): pure-Python port of imgui_md with overlap fix

ADD src/md_renderer_py.py: Full port of mekhontsev/imgui_md to pure Python.
  - Uses markdown-it-py (already a transitive dep) for AST parsing.
  - Walks the token tree, calling imgui primitives directly.
  - Mirrors the C++ API surface: MarkdownOptions, MarkdownCallbacks,
    MarkdownRenderer.render(), render_unindented().
  - Code blocks delegated via set_external_code_block_handler callback.
  - All other content (paragraphs, headings, lists, code, tables, hr,
    emphasis, strong, links, blockquotes) rendered natively.

ROOT CAUSE OF BULLET OVERLAP (now fixed at the source):
  imgui-md C++ BLOCK_P guards NewLine() behind 'if (!m_list_stack.empty())'
  (imgui_md.cpp line ~145). Inside lists, paragraph transitions don't
  advance the cursor Y. The Python port calls imgui.new_line() explicitly
  between paragraphs in a list item, eliminating the overlap.

ROOT CAUSE OF '*' BULLET Y-OVERLAP (now fixed at the source):
  imgui-md C++ BLOCK_LI for '*' delim calls ImGui::Bullet() without
  ImGui::SameLine() (imgui_md.cpp line ~95). The Python port calls
  imgui.bullet() + imgui.same_line() for all markers uniformly.

REMOVED in src/markdown_helper.py:
  - _normalize_bullet_delimiters (no longer needed)
  - _normalize_nested_list_endings (no longer needed)
  - _normalize_list_continuations (no longer needed)
  - parse_tables / render_table (renderer handles tables natively)
  - All 'imgui_md' body rendering (replaced by Python port)

TESTS:
  - tests/test_md_renderer_py.py (new): 16 unit tests for the Python port
    covering paragraphs, headings, lists, nested lists, emphasis, strong,
    code, links, tables, hr, unindented.
  - tests/test_markdown_helper_bullets.py (rewritten): 13 tests for the
    integration with the public MarkdownRenderer class.
  - tests/test_markdown_render_robust.py (updated): 2 tests verifying
    table content is routed through the new Python renderer (not imgui_md).
  - tests/test_markdown_table.py / _render.py / _columns.py / _wrapped.py:
    unchanged (test the standalone render_table which is still used by
    the new renderer as a fallback for any unhandled cases).

42/42 markdown tests pass. 1-space indentation. 1 C++ dependency
removed (imgui_md is no longer used at runtime).

NOT FIXED (known limitations of the new renderer):
  - Inline code rendering uses a tinted small_button (not monospace)
  - Heading fonts use the default font (no separate bold/large fonts)
  - Image rendering shows a placeholder text
  - These can be improved by subclassing MarkdownRenderer
This commit is contained in:
Conductor
2026-06-03 22:43:41 -04:00
parent 58cd759968
commit fe618055ca
5 changed files with 1061 additions and 340 deletions
+125 -85
View File
@@ -1,106 +1,146 @@
from unittest.mock import patch, MagicMock
from src.markdown_helper import MarkdownRenderer
def _mock_table(mock_imgui):
def _mock_imgui(mock_imgui):
mock_imgui.TableFlags_ = type("T", (), {"borders": 1, "row_bg": 2, "resizable": 4})()
mock_imgui.TableColumnFlags_ = type("C", (), {"width_stretch": 8})()
mock_imgui.begin_table.return_value = True
mock_imgui.table_next_column = lambda: None
mock_imgui.table_next_row = lambda: None
mock_imgui.table_headers_row = lambda: None
mock_imgui.text = lambda *a, **k: None
mock_imgui.text_wrapped = lambda *a, **k: None
mock_imgui.end_table = lambda: None
mock_imgui.same_line = lambda: None
mock_imgui.spacing = lambda: None
mock_imgui.indent = lambda *a: None
mock_imgui.unindent = lambda *a: None
mock_imgui.Col_ = type("C", (), {"text": 0, "text_disabled": 1, "frame_bg": 2,
"button": 3, "button_hovered": 4, "button_active": 5})()
mock_imgui.begin_table = MagicMock(return_value=True)
mock_imgui.end_table = MagicMock()
mock_imgui.table_setup_column = MagicMock()
mock_imgui.table_next_row = MagicMock()
mock_imgui.table_next_column = MagicMock()
mock_imgui.text = MagicMock()
mock_imgui.text_wrapped = MagicMock()
mock_imgui.bullet = MagicMock()
mock_imgui.same_line = MagicMock()
mock_imgui.new_line = MagicMock()
mock_imgui.indent = MagicMock()
mock_imgui.unindent = MagicMock()
mock_imgui.spacing = MagicMock()
mock_imgui.separator = MagicMock()
mock_imgui.push_style_color = MagicMock()
mock_imgui.pop_style_color = MagicMock()
mock_imgui.push_font = MagicMock()
mock_imgui.pop_font = MagicMock()
mock_imgui.small_button = MagicMock()
mock_imgui.get_style = MagicMock(return_value=MagicMock(
colors={i: MagicMock(x=1, y=1, z=1, w=1) for i in range(6)}
))
mock_imgui.get_io = MagicMock(return_value=MagicMock(fonts=MagicMock(fonts=[None])))
mock_imgui.calc_text_size = MagicMock(return_value=MagicMock(x=50, y=20))
mock_imgui.get_item_rect_min = MagicMock(return_value=MagicMock(x=0, y=0))
mock_imgui.get_window_draw_list = MagicMock(return_value=MagicMock(
add_line=MagicMock()
))
mock_imgui.is_item_hovered = MagicMock(return_value=False)
mock_imgui.is_mouse_released = MagicMock(return_value=False)
mock_imgui.get_text_line_height = MagicMock(return_value=20.0)
mock_imgui.ImVec2 = lambda *a: MagicMock(x=a[0] if a else 0, y=a[1] if len(a) > 1 else 0)
def test_render_calls_imgui_md_render_for_bullet_chunks():
def test_render_passes_bullet_chunks_to_python_renderer():
md = "- one\n- two\n- three\n"
with patch("src.markdown_helper.imgui_md") as mock_md, \
patch("src.markdown_helper.imgui") as mock_imgui, \
patch("src.markdown_table.imgui") as mock_table_imgui:
_mock_table(mock_table_imgui)
mock_md.render = MagicMock()
mock_imgui.spacing = MagicMock()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="bullets")
assert mock_md.render.call_count == 1, f"expected 1 imgui_md.render call (full chunk passed through), got {mock_md.render.call_count}"
assert mock_imgui.spacing.call_count >= 1, "imgui.spacing must be called to force vertical gap between chunks"
assert mock_imgui.bullet.call_count == 3, f"expected 3 bullets, got {mock_imgui.bullet.call_count}"
assert mock_imgui.text_wrapped.called
def test_render_does_not_strip_bullet_prefix_from_markdown():
md = "- one\n- two\n"
with patch("src.markdown_helper.imgui_md") as mock_md, \
patch("src.markdown_helper.imgui") as mock_imgui, \
patch("src.markdown_table.imgui") as mock_table_imgui:
_mock_table(mock_table_imgui)
mock_md.render = MagicMock()
mock_imgui.spacing = MagicMock()
def test_render_passes_dash_and_asterisk_bullets_uniformly():
md = "- dash\n* asterisk\n+ plus\n"
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="bullets")
args, _ = mock_md.render.call_args
rendered_text = args[0]
assert "- one" in rendered_text, f"bullet prefix must NOT be stripped (regression: was double-rendering as bullet + imgui_md numbered list), got {rendered_text!r}"
assert "- two" in rendered_text
assert mock_imgui.bullet.call_count == 3, f"all 3 markers should render as bullets (no upstream '*' Y-overlap bug), got {mock_imgui.bullet.call_count}"
def test_render_passes_numbered_list_intact_to_imgui_md():
def test_render_passes_numbered_list_intact():
md = "1. First question\n2. Second question\n"
with patch("src.markdown_helper.imgui_md") as mock_md, \
patch("src.markdown_helper.imgui") as mock_imgui, \
patch("src.markdown_table.imgui") as mock_table_imgui:
_mock_table(mock_table_imgui)
mock_md.render = MagicMock()
mock_imgui.spacing = MagicMock()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="numbered")
assert mock_md.render.call_count == 1
args, _ = mock_md.render.call_args
rendered_text = args[0]
assert "1. First question" in rendered_text
assert "2. Second question" in rendered_text
assert not mock_imgui.bullet.called, "no manual imgui.bullet should be added — let imgui_md handle list rendering"
text_args = [str(c) for c in mock_imgui.text.call_args_list]
assert any("1." in s for s in text_args), f"expected '1.' in numbered list, got {text_args!r}"
assert any("2." in s for s in text_args), f"expected '2.' in numbered list, got {text_args!r}"
def test_normalize_nested_list_endings_inserts_blank_after_nested_item():
r = MarkdownRenderer()
text = "- top\n - nested last\nnext paragraph\n"
out = r._normalize_nested_list_endings(text)
assert " - nested last\n\nnext paragraph" in out, f"expected blank line between nested list item and less-indented next line, got {out!r}"
def test_render_explicit_newline_between_list_paragraphs_fixes_overlap():
md = "- bullet text (long enough to wrap maybe)\n\n continuation paragraph after a blank line"
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="cont")
assert mock_imgui.new_line.call_count >= 3, (
f"expected at least 3 new_line calls (before bullet, between paragraphs, after list), "
f"got {mock_imgui.new_line.call_count}. The pure-Python renderer MUST emit explicit "
f"newlines to avoid the imgui-md C++ BLOCK_P no-NewLine-inside-list bug."
)
def test_normalize_nested_list_endings_does_not_insert_blank_for_top_level_list():
def test_render_handles_empty_input():
r = MarkdownRenderer()
text = "- one\n- two\n- three\nnext paragraph\n"
out = r._normalize_nested_list_endings(text)
assert out == text, f"top-level list ending should not trigger blank line insertion, got {out!r}"
r.render("")
def test_normalize_nested_list_endings_does_not_double_blank():
r = MarkdownRenderer()
text = "- a\n - b\n\nalready blank\n"
out = r._normalize_nested_list_endings(text)
assert out == text, f"already-blank separator should not get doubled, got {out!r}"
def test_render_handles_nested_lists():
md = "- outer\n - inner1\n - inner2\n- outer2"
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="nested")
assert mock_imgui.bullet.call_count == 4, f"expected 4 bullets (1 outer + 2 inner + 1 outer2), got {mock_imgui.bullet.call_count}"
def test_normalize_bullet_delimiters_still_converts_asterisk():
r = MarkdownRenderer()
text = "- one\n* two\n+ three\n"
out = r._normalize_bullet_delimiters(text)
assert "- one" in out
assert "- two" in out and "* two" not in out, f"* must be converted to -, got {out!r}"
assert "+ three" in out, f"+ must be left alone, got {out!r}"
assert "+ three" in out, f"+ must be left alone, got {out!r}"
def test_render_handles_emphasis_inline():
md = "This is *emphasized* text."
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="em")
assert mock_imgui.push_style_color.called, "em should push style color"
assert mock_imgui.pop_style_color.called
def test_normalize_list_continuations_strips_blank_between_bullet_and_indented_continuation():
r = MarkdownRenderer()
text = "- bullet text\n\n continuation paragraph\n\nnext paragraph\n"
out = r._normalize_list_continuations(text)
assert " continuation paragraph" in out
assert "- bullet text\n continuation paragraph" in out, f"blank between bullet and indented continuation must be stripped (workaround for imgui_md BLOCK_P no-NewLine-inside-list bug), got {out!r}"
assert "next paragraph" in out
def test_render_handles_strong_inline():
md = "This is **strong** text."
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="strong")
assert mock_imgui.push_style_color.called
def test_normalize_list_continuations_preserves_blank_between_indented_and_next_paragraph():
r = MarkdownRenderer()
text = "- bullet\n cont\n\nnext\n"
out = r._normalize_list_continuations(text)
assert "- bullet\n cont\n\nnext\n" == out, f"blank between continuation and next paragraph must be preserved (it ends the list item), got {out!r}"
def test_render_handles_inline_code():
md = "Use `foo()` inline."
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="code")
assert mock_imgui.small_button.called, "inline code should use small_button"
def test_normalize_list_continuations_leaves_simple_list_alone():
r = MarkdownRenderer()
text = "- one\n- two\n- three\n"
out = r._normalize_list_continuations(text)
assert out == text, f"simple list with no continuations should be unchanged, got {out!r}"
def test_render_handles_table():
md = "| A | B |\n|---|---|\n| 1 | 2 |"
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="table")
assert mock_imgui.begin_table.called
assert mock_imgui.end_table.called
assert mock_imgui.table_setup_column.call_count == 2
def test_render_handles_link():
md = "Click [here](https://example.com)."
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="link")
assert mock_imgui.text_wrapped.called
assert mock_imgui.push_style_color.called
def test_render_unindented_strips_common_indent():
md = " - a\n - b\n - c"
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
MarkdownRenderer().render_unindented(md)
assert mock_imgui.bullet.call_count == 3, f"expected 3 bullets after unindenting, got {mock_imgui.bullet.call_count}"
def test_render_does_not_use_imgui_md_anymore():
md = "# heading\n\n- item 1\n- item 2\n\nplain prose"
with patch("src.md_renderer_py.imgui") as mock_imgui, \
patch("src.markdown_helper.imgui_md") as mock_md:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(md, context_id="all")
assert mock_md.render.call_count == 0, (
"imgui_md.render must NOT be called — the new design uses src.md_renderer_py "
"to avoid the C++ imgui-md BLOCK_P no-NewLine-inside-list bug and BLOCK_LI "
"asteroid-Y-overlap bug"
)
+53 -21
View File
@@ -1,17 +1,48 @@
from unittest.mock import patch
from unittest.mock import patch, MagicMock
from src.markdown_helper import MarkdownRenderer
from src.markdown_table import parse_tables
def _mock_table_calls(mock_imgui):
mock_imgui.TableFlags_ = type("T", (), {"borders": 1, "row_bg": 2, "resizable": 4})()
mock_imgui.begin_table.return_value = True
mock_imgui.table_next_column = lambda: None
mock_imgui.table_next_row = lambda: None
mock_imgui.table_headers_row = lambda: None
mock_imgui.text = lambda *a, **k: None
mock_imgui.end_table = lambda: None
def test_tables_in_crlf_text_all_get_masked():
def _mock_imgui(mock_imgui):
mock_imgui.TableFlags_ = type("T", (), {"borders": 1, "row_bg": 2, "resizable": 4})()
mock_imgui.TableColumnFlags_ = type("C", (), {"width_stretch": 8})()
mock_imgui.Col_ = type("C", (), {"text": 0, "text_disabled": 1, "frame_bg": 2,
"button": 3, "button_hovered": 4, "button_active": 5})()
mock_imgui.begin_table = MagicMock(return_value=True)
mock_imgui.end_table = MagicMock()
mock_imgui.table_setup_column = MagicMock()
mock_imgui.table_next_row = MagicMock()
mock_imgui.table_next_column = MagicMock()
mock_imgui.text = MagicMock()
mock_imgui.text_wrapped = MagicMock()
mock_imgui.bullet = MagicMock()
mock_imgui.same_line = MagicMock()
mock_imgui.new_line = MagicMock()
mock_imgui.indent = MagicMock()
mock_imgui.unindent = MagicMock()
mock_imgui.spacing = MagicMock()
mock_imgui.separator = MagicMock()
mock_imgui.push_style_color = MagicMock()
mock_imgui.pop_style_color = MagicMock()
mock_imgui.push_font = MagicMock()
mock_imgui.pop_font = MagicMock()
mock_imgui.small_button = MagicMock()
mock_imgui.get_style = MagicMock(return_value=MagicMock(
colors={i: MagicMock(x=1, y=1, z=1, w=1) for i in range(6)}
))
mock_imgui.get_io = MagicMock(return_value=MagicMock(fonts=MagicMock(fonts=[None])))
mock_imgui.calc_text_size = MagicMock(return_value=MagicMock(x=50, y=20))
mock_imgui.get_item_rect_min = MagicMock(return_value=MagicMock(x=0, y=0))
mock_imgui.get_window_draw_list = MagicMock(return_value=MagicMock(
add_line=MagicMock()
))
mock_imgui.is_item_hovered = MagicMock(return_value=False)
mock_imgui.is_mouse_released = MagicMock(return_value=False)
mock_imgui.get_text_line_height = MagicMock(return_value=20.0)
mock_imgui.ImVec2 = lambda *a: MagicMock(x=a[0] if a else 0, y=a[1] if len(a) > 1 else 0)
def test_tables_in_crlf_text_are_handled_by_python_renderer():
text = (
"# Title\r\n"
"\r\n"
@@ -27,14 +58,15 @@ def test_tables_in_crlf_text_all_get_masked():
)
blocks = parse_tables(text)
assert len(blocks) == 2
with patch("src.markdown_helper.imgui_md") as mock_md, patch("src.markdown_helper.imgui") as mock_imgui, patch("src.markdown_table.imgui") as mock_table_imgui:
_mock_table_calls(mock_table_imgui)
with patch("src.md_renderer_py.imgui") as mock_imgui, \
patch("src.markdown_helper.imgui_md") as mock_md:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(text, context_id="t")
full = "".join(str(c) for c in mock_md.render.call_args_list)
for needle in ["| A | B |", "|---|", "| 1 | 2 |", "| X | Y |", "| 3 | 4 |"]:
assert needle not in full, f"Raw table text leaked to imgui_md: {needle!r} in calls: {full!r}"
assert mock_md.render.call_count == 0, "imgui_md.render must NOT be called — the new design uses the pure-Python renderer"
assert mock_imgui.begin_table.call_count >= 2, f"expected at least 2 begin_table calls (one per table), got {mock_imgui.begin_table.call_count}"
assert mock_imgui.text_wrapped.called, "body text should still render via text_wrapped"
def test_duplicate_table_content_both_get_replaced():
def test_duplicate_table_content_both_get_handled():
text = (
"| A | B |\r\n"
"|---|---|\r\n"
@@ -48,9 +80,9 @@ def test_duplicate_table_content_both_get_replaced():
)
blocks = parse_tables(text)
assert len(blocks) == 2
with patch("src.markdown_helper.imgui_md") as mock_md, patch("src.markdown_helper.imgui") as mock_imgui, patch("src.markdown_table.imgui") as mock_table_imgui:
_mock_table_calls(mock_table_imgui)
with patch("src.md_renderer_py.imgui") as mock_imgui, \
patch("src.markdown_helper.imgui_md") as mock_md:
_mock_imgui(mock_imgui)
MarkdownRenderer().render(text, context_id="dup")
full = "".join(str(c) for c in mock_md.render.call_args_list)
assert "| A | B |" not in full, f"Raw table text leaked on duplicate: {full!r}"
assert mock_md.render.call_count == 0
assert mock_imgui.begin_table.call_count >= 2, f"expected 2 begin_table calls, got {mock_imgui.begin_table.call_count}"
+190
View File
@@ -0,0 +1,190 @@
from unittest.mock import patch, MagicMock
from src.md_renderer_py import MarkdownRenderer, MdOptions
def _mock_imgui(mock_imgui):
mock_imgui.TableFlags_ = type("T", (), {"borders": 1, "row_bg": 2, "resizable": 4})()
mock_imgui.TableColumnFlags_ = type("C", (), {"width_stretch": 8})()
mock_imgui.Col_ = type("C", (), {
"text": 0, "text_disabled": 1, "frame_bg": 2, "button": 3,
"button_hovered": 4, "button_active": 5,
})()
mock_imgui.begin_table = MagicMock(return_value=True)
mock_imgui.end_table = MagicMock()
mock_imgui.table_setup_column = MagicMock()
mock_imgui.table_next_row = MagicMock()
mock_imgui.table_next_column = MagicMock()
mock_imgui.text = MagicMock()
mock_imgui.text_wrapped = MagicMock()
mock_imgui.bullet = MagicMock()
mock_imgui.same_line = MagicMock()
mock_imgui.new_line = MagicMock()
mock_imgui.indent = MagicMock()
mock_imgui.unindent = MagicMock()
mock_imgui.spacing = MagicMock()
mock_imgui.separator = MagicMock()
mock_imgui.push_style_color = MagicMock()
mock_imgui.pop_style_color = MagicMock()
mock_imgui.push_font = MagicMock()
mock_imgui.pop_font = MagicMock()
mock_imgui.small_button = MagicMock()
mock_imgui.get_style = MagicMock(return_value=MagicMock(
colors={
0: MagicMock(x=1, y=1, z=1, w=1),
1: MagicMock(x=0.5, y=0.5, z=0.5, w=1),
2: MagicMock(x=0.1, y=0.1, z=0.1, w=1),
3: MagicMock(x=0.3, y=0.5, z=1.0, w=1),
4: MagicMock(x=0.4, y=0.6, z=1.0, w=1),
5: MagicMock(x=0.5, y=0.7, z=1.0, w=1),
}
))
mock_imgui.get_io = MagicMock(return_value=MagicMock(fonts=MagicMock(fonts=[None])))
mock_imgui.calc_text_size = MagicMock(return_value=MagicMock(x=50, y=20))
mock_imgui.get_item_rect_min = MagicMock(return_value=MagicMock(x=0, y=0))
mock_imgui.get_window_draw_list = MagicMock(return_value=MagicMock(
add_line=MagicMock()
))
mock_imgui.is_item_hovered = MagicMock(return_value=False)
mock_imgui.is_mouse_released = MagicMock(return_value=False)
return mock_imgui
def test_renderer_parses_simple_paragraph():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("Hello, world.")
assert mock_imgui.text_wrapped.called
def test_renderer_renders_h1_with_separator():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("# Heading 1")
assert mock_imgui.push_font.called
assert mock_imgui.pop_font.called
assert mock_imgui.separator.called
def test_renderer_renders_bullet_list_with_bullets():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("- one\n- two\n- three")
assert mock_imgui.bullet.call_count == 3, f"expected 3 bullets, got {mock_imgui.bullet.call_count}"
assert mock_imgui.indent.call_count == 3
assert mock_imgui.unindent.call_count == 3
def test_renderer_renders_ordered_list_with_numbers():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("1. first\n2. second\n3. third")
assert mock_imgui.bullet.call_count == 0, "ordered list should not use bullets"
assert mock_imgui.text.called, "ordered list should render the number+delim text"
for call in mock_imgui.text.call_args_list:
args, _ = call
assert any(s in str(args[0]) for s in ["1.", "2.", "3."]), f"got {args[0]!r}"
def test_renderer_renders_emphasis_with_dim_color():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("This is *emphasized* text.")
assert mock_imgui.push_style_color.call_count >= 2, "em should push 2 style colors"
assert mock_imgui.pop_style_color.call_count >= 2, "em should pop 2 style colors"
assert mock_imgui.text_wrapped.called
def test_renderer_renders_strong_with_bright_color():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("This is **strong** text.")
assert mock_imgui.push_style_color.call_count >= 2, "strong should push 2 style colors"
assert mock_imgui.pop_style_color.call_count >= 2
def test_renderer_renders_inline_code_with_button():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("Use `code` inline.")
assert mock_imgui.small_button.called, "inline code should use small_button"
assert mock_imgui.same_line.called
def test_renderer_renders_table_with_columns_and_rows():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
md = "| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |"
r.render(md)
assert mock_imgui.begin_table.called, "table should call begin_table"
assert mock_imgui.end_table.called, "table should call end_table"
assert mock_imgui.table_setup_column.call_count == 2, f"expected 2 columns, got {mock_imgui.table_setup_column.call_count}"
assert mock_imgui.table_next_row.call_count == 3, f"expected 3 rows (1 header + 2 body), got {mock_imgui.table_next_row.call_count}"
def test_renderer_renders_link_with_underline():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("Click [here](https://example.com).")
assert mock_imgui.push_style_color.called
assert mock_imgui.text_wrapped.called
def test_renderer_link_callback_fires_on_click():
callback = MagicMock()
opts = MdOptions()
opts.callbacks.on_open_link = callback
r = MarkdownRenderer(opts)
with patch("src.md_renderer_py.imgui") as mock_imgui:
mock_imgui = _mock_imgui(mock_imgui)
mock_imgui.is_item_hovered = MagicMock(return_value=True)
mock_imgui.is_mouse_released = MagicMock(return_value=True)
r.render("[link](https://example.com)")
assert callback.called
args, _ = callback.call_args
assert "https://example.com" in args
def test_renderer_renders_code_block_via_external_handler():
r = MarkdownRenderer()
handler = MagicMock()
r.set_external_code_block_handler(handler)
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("```python\nprint('hi')\n```")
assert handler.called
args, _ = handler.call_args
assert args[0] == "print('hi')\n", f"expected code content, got {args[0]!r}"
assert args[1] == "python", f"expected lang, got {args[1]!r}"
def test_renderer_explicit_newline_between_list_paragraphs():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("- first\n\n continuation paragraph")
assert mock_imgui.new_line.call_count >= 3, f"expected at least 3 new_lines (one before bullet, one between paragraphs, one after list), got {mock_imgui.new_line.call_count}"
def test_renderer_handles_empty_input():
r = MarkdownRenderer()
r.render("")
r.render(" \n \n")
def test_renderer_handles_nested_lists():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("- outer\n - inner1\n - inner2\n- outer2")
assert mock_imgui.bullet.call_count == 4, f"expected 4 bullets (1 outer + 2 inner + 1 outer2), got {mock_imgui.bullet.call_count}"
assert mock_imgui.unindent.call_count == 4
def test_renderer_renders_horizontal_rule():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render("Above\n\n---\n\nBelow")
assert mock_imgui.separator.called
def test_renderer_render_unindented_strips_common_indent():
r = MarkdownRenderer()
with patch("src.md_renderer_py.imgui") as mock_imgui:
_mock_imgui(mock_imgui)
r.render_unindented(" - a\n - b\n - c")
assert mock_imgui.bullet.call_count == 3