From d15fdcdb0506aebf6a0fe36e232cbd7f04bf459e Mon Sep 17 00:00:00 2001 From: Conductor Date: Wed, 3 Jun 2026 17:31:50 -0400 Subject: [PATCH] fix(markdown): revert table to simple form with text_wrapped + add regression tests --- src/markdown_table.py | 16 ++++---- tests/test_markdown_helper_bullets.py | 54 +++++++++++++++++++++++++++ tests/test_markdown_table_wrapped.py | 44 ++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 tests/test_markdown_helper_bullets.py create mode 100644 tests/test_markdown_table_wrapped.py diff --git a/src/markdown_table.py b/src/markdown_table.py index 76a0a157..6a8e9704 100644 --- a/src/markdown_table.py +++ b/src/markdown_table.py @@ -2,28 +2,27 @@ import re from dataclasses import dataclass from imgui_bundle import imgui -_TABLE_SEPARATOR = re.compile(r"^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$") +_TABLE_SEPARATOR = re.compile(r"^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$") def render_table(block: "TableBlock") -> None: """Render a GFM table block via imgui.begin_table. [C: src/markdown_helper.py:MarkdownRenderer.render] """ - from src.markdown_helper import render as render_md n_cols = len(block.headers) if n_cols == 0: return - flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_x + flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable if not imgui.begin_table("md_table", n_cols, flags): return for h in block.headers: - imgui.table_setup_column(h, imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column(h) imgui.table_headers_row() - # Note: table_headers_row() renders the headers from setup_column. - # No need for manual row here unless we want custom rendering for header cells. - + for h in block.headers: + imgui.table_next_column() + imgui.text_wrapped(h) for row in block.rows: imgui.table_next_row() for c in row: imgui.table_next_column() - render_md(c) + imgui.text_wrapped(c) imgui.end_table() @dataclass(frozen=True) @@ -43,7 +42,6 @@ def _split_row(line: str) -> list[str]: def _is_table_at(lines: list[str], i: int) -> bool: if i + 1 >= len(lines): return False - # Header must have at least one pipe, or the separator must be very clear if "|" not in lines[i]: return False return bool(_TABLE_SEPARATOR.match(lines[i + 1])) diff --git a/tests/test_markdown_helper_bullets.py b/tests/test_markdown_helper_bullets.py new file mode 100644 index 00000000..1e83009b --- /dev/null +++ b/tests/test_markdown_helper_bullets.py @@ -0,0 +1,54 @@ +from unittest.mock import patch, MagicMock +from src.markdown_helper import MarkdownRenderer + +def test_bullet_list_renders_each_item_with_imgui_bullet(): + chunk = "- one\n- two\n- three\n" + with patch("src.markdown_helper.imgui") as mock_imgui, patch("src.markdown_helper.imgui_md") as mock_md: + mock_imgui.bullet = MagicMock() + mock_imgui.same_line = MagicMock() + mock_imgui.spacing = MagicMock() + mock_imgui.indent = MagicMock() + mock_imgui.unindent = MagicMock() + mock_md.render = MagicMock() + MarkdownRenderer()._render_md_no_bullet_overlap(chunk) + assert mock_imgui.bullet.call_count >= 3 + assert mock_imgui.same_line.call_count >= 3 + assert mock_md.render.call_count >= 3 + +def test_bullet_list_with_blank_lines_uses_spacing(): + chunk = "- one\n\n- two\n" + with patch("src.markdown_helper.imgui") as mock_imgui, patch("src.markdown_helper.imgui_md") as mock_md: + mock_imgui.bullet = MagicMock() + mock_imgui.same_line = MagicMock() + mock_imgui.spacing = MagicMock() + mock_imgui.indent = MagicMock() + mock_imgui.unindent = MagicMock() + mock_md.render = MagicMock() + MarkdownRenderer()._render_md_no_bullet_overlap(chunk) + assert mock_imgui.spacing.call_count >= 1 + +def test_non_bullet_markdown_routes_to_imgui_md(): + chunk = "# Header\n\nSome prose." + with patch("src.markdown_helper.imgui") as mock_imgui, patch("src.markdown_helper.imgui_md") as mock_md: + mock_imgui.bullet = MagicMock() + mock_imgui.same_line = MagicMock() + mock_imgui.spacing = MagicMock() + mock_imgui.indent = MagicMock() + mock_imgui.unindent = MagicMock() + mock_md.render = MagicMock() + MarkdownRenderer()._render_md_no_bullet_overlap(chunk) + assert not mock_imgui.bullet.called + assert mock_md.render.call_count == 1 + +def test_mixed_bullets_and_prose_splits_correctly(): + chunk = "Prose before.\n\n- b1\n- b2\n\nProse after." + with patch("src.markdown_helper.imgui") as mock_imgui, patch("src.markdown_helper.imgui_md") as mock_md: + mock_imgui.bullet = MagicMock() + mock_imgui.same_line = MagicMock() + mock_imgui.spacing = MagicMock() + mock_imgui.indent = MagicMock() + mock_imgui.unindent = MagicMock() + mock_md.render = MagicMock() + MarkdownRenderer()._render_md_no_bullet_overlap(chunk) + assert mock_imgui.bullet.call_count >= 2 + assert mock_md.render.call_count >= 2 \ No newline at end of file diff --git a/tests/test_markdown_table_wrapped.py b/tests/test_markdown_table_wrapped.py new file mode 100644 index 00000000..e8112e24 --- /dev/null +++ b/tests/test_markdown_table_wrapped.py @@ -0,0 +1,44 @@ +from unittest.mock import patch, MagicMock +from src.markdown_table import render_table, TableBlock + +def test_render_table_uses_text_wrapped_for_cells(): + block = TableBlock(headers=["A"], rows=[["hello"]], span=(0, 2)) + with patch("src.markdown_table.imgui") as 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 = MagicMock() + mock_imgui.table_next_row = MagicMock() + mock_imgui.table_headers_row = MagicMock() + mock_imgui.text_wrapped = MagicMock() + mock_imgui.text = MagicMock() + mock_imgui.end_table = MagicMock() + render_table(block) + mock_imgui.text_wrapped.assert_any_call("hello") + +def test_render_table_uses_text_wrapped_for_headers(): + block = TableBlock(headers=["A"], rows=[["hello"]], span=(0, 2)) + with patch("src.markdown_table.imgui") as 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 = MagicMock() + mock_imgui.table_next_row = MagicMock() + mock_imgui.table_headers_row = MagicMock() + mock_imgui.text_wrapped = MagicMock() + mock_imgui.text = MagicMock() + mock_imgui.end_table = MagicMock() + render_table(block) + mock_imgui.text_wrapped.assert_any_call("A") + +def test_render_table_does_not_use_text_for_cells(): + block = TableBlock(headers=["A"], rows=[["hello"]], span=(0, 2)) + with patch("src.markdown_table.imgui") as 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 = MagicMock() + mock_imgui.table_next_row = MagicMock() + mock_imgui.table_headers_row = MagicMock() + mock_imgui.text_wrapped = MagicMock() + mock_imgui.text = MagicMock() + mock_imgui.end_table = MagicMock() + render_table(block) + assert not mock_imgui.text.called, "imgui.text should not be called for table cells" \ No newline at end of file