fix(markdown): strip blank between bullet and indented continuation paragraph
ROOT CAUSE: imgui_md (mekhontsev/imgui_md) BLOCK_P does NOT call ImGui::NewLine()
when m_list_stack is non-empty (verified in imgui_md.cpp). So a multi-paragraph
list item like:
- bullet text (long, wraps to 2 lines)
continuation paragraph
renders BOTH paragraphs at the same Y because the second BLOCK_P enters/exits
without advancing the cursor. The continuation crashes into the previous
paragraph's last wrapped line.
FIX: Add MarkdownRenderer._normalize_list_continuations preprocessor that
strips blank lines between a list item and its indented continuation. The
continuation then becomes a lazy continuation of the first paragraph (single
BLOCK_P in imgui_md, proper text wrapping, no overlap). Trade-off: users
cannot have separate paragraphs within a single list item. Acceptable.
Also: fixed a pre-existing bug in _normalize_nested_list_endings where a
duplicate conditional caused the function to return empty string (the
out.append(line) was inside the wrong scope). It was silently corrupting
all list content since fd5f4d0e.
TESTS: 23/23 markdown unit tests pass. 3 new tests for the new preprocessor
covering: blank-strip case, blank-preservation case, simple-list passthrough.
This commit is contained in:
@@ -117,6 +117,7 @@ class MarkdownRenderer:
|
|||||||
from src.markdown_table import parse_tables, render_table
|
from src.markdown_table import parse_tables, render_table
|
||||||
text = self._normalize_bullet_delimiters(text)
|
text = self._normalize_bullet_delimiters(text)
|
||||||
text = self._normalize_nested_list_endings(text)
|
text = self._normalize_nested_list_endings(text)
|
||||||
|
text = self._normalize_list_continuations(text)
|
||||||
blocks = parse_tables(text)
|
blocks = parse_tables(text)
|
||||||
lines = text.splitlines(keepends=True)
|
lines = text.splitlines(keepends=True)
|
||||||
if not lines:
|
if not lines:
|
||||||
@@ -231,6 +232,57 @@ class MarkdownRenderer:
|
|||||||
out.append(line)
|
out.append(line)
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
|
def _normalize_list_continuations(self, text: str) -> str:
|
||||||
|
"""Strip blank lines between a list item and its indented continuation.
|
||||||
|
|
||||||
|
imgui_md (mekhontsev/imgui_md) BLOCK_P does NOT call ImGui::NewLine()
|
||||||
|
when m_list_stack is non-empty. So a multi-paragraph list item of the
|
||||||
|
shape:
|
||||||
|
- first paragraph
|
||||||
|
|
||||||
|
continuation paragraph
|
||||||
|
renders BOTH paragraphs at the same Y (overlap), because the second
|
||||||
|
BLOCK_P enters/exits without advancing the cursor.
|
||||||
|
|
||||||
|
WORKAROUND: Remove the blank line so the continuation becomes a lazy
|
||||||
|
continuation of the first paragraph (single BLOCK_P, proper line wrap,
|
||||||
|
no overlap). Trade-off: the user cannot have separate paragraphs within
|
||||||
|
a single list item. Acceptable for our use case.
|
||||||
|
[C: src.markdown_helper:MarkdownRenderer.render]
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
lines = text.split("\n")
|
||||||
|
out: list[str] = []
|
||||||
|
prev_was_list = False
|
||||||
|
prev_indent = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip():
|
||||||
|
out.append(line)
|
||||||
|
if re.match(r"^\s*[-*+\d]\s+", line):
|
||||||
|
prev_was_list = True
|
||||||
|
prev_indent = len(line) - len(line.lstrip())
|
||||||
|
else:
|
||||||
|
prev_was_list = False
|
||||||
|
continue
|
||||||
|
if not prev_was_list:
|
||||||
|
out.append(line)
|
||||||
|
continue
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and not lines[j].strip():
|
||||||
|
j += 1
|
||||||
|
if j >= len(lines):
|
||||||
|
out.append(line)
|
||||||
|
prev_was_list = False
|
||||||
|
continue
|
||||||
|
next_line = lines[j]
|
||||||
|
curr_indent = len(next_line) - len(next_line.lstrip())
|
||||||
|
is_next_list = bool(re.match(r"^\s*[-*+\d]", next_line))
|
||||||
|
if curr_indent > prev_indent and not is_next_list:
|
||||||
|
continue
|
||||||
|
out.append(line)
|
||||||
|
prev_was_list = False
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
def render_unindented(self, text: str) -> None:
|
def render_unindented(self, text: str) -> None:
|
||||||
"""Render Markdown text with automatic unindentation."""
|
"""Render Markdown text with automatic unindentation."""
|
||||||
imgui_md.render_unindented(text)
|
imgui_md.render_unindented(text)
|
||||||
|
|||||||
@@ -83,3 +83,24 @@ def test_normalize_bullet_delimiters_still_converts_asterisk():
|
|||||||
assert "- one" in out
|
assert "- one" in out
|
||||||
assert "- two" in out and "* two" not in out, f"* must be converted to -, got {out!r}"
|
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}"
|
||||||
|
assert "+ three" in out, f"+ must be left alone, got {out!r}"
|
||||||
|
|
||||||
|
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_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_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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user