summaryrefslogtreecommitdiffstats
path: root/llamachat/ui.py
diff options
context:
space:
mode:
Diffstat (limited to 'llamachat/ui.py')
-rw-r--r--llamachat/ui.py29
1 files changed, 28 insertions, 1 deletions
diff --git a/llamachat/ui.py b/llamachat/ui.py
index fd2380c..f599bdd 100644
--- a/llamachat/ui.py
+++ b/llamachat/ui.py
@@ -1291,6 +1291,33 @@ _PRE_STYLE = (
)
+_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})", re.MULTILINE)
+
+
+def _close_fences(text: str) -> str:
+ """Close an unterminated code fence and neuter a lone inline backtick.
+
+ A reply is streamed, so a fence is unclosed on nearly every frame; a
+ model can also emit an odd backtick by accident. Either way the rest of
+ the reply would reflow as code, which is how a whole message turns into
+ one grey block. Closing the fence here bounds the damage to the text
+ that was really meant as code.
+ """
+ fences = _FENCE_RE.findall(text)
+ if len(fences) % 2:
+ # Match the opener's own length; a longer run is legal and only a
+ # fence at least as long as the opener actually closes it.
+ closer = fences[-1][0] * max(3, len(fences[-1]))
+ nl = "" if text.endswith("\n") else "\n"
+ return f"{text}{nl}{closer}"
+ if not fences and text.count("`") % 2:
+ # No fences at all, one dangling inline tick: drop it rather than
+ # let it open a run to the end of the message.
+ head, _, tail = text.rpartition("`")
+ return head + tail
+ return text
+
+
def _markdown_to_fragment(text: str) -> str:
"""Render markdown to an HTML fragment safe to splice into the page.
@@ -1300,7 +1327,7 @@ def _markdown_to_fragment(text: str) -> str:
if not text:
return ""
doc = QTextDocument()
- doc.setMarkdown(text, MARKDOWN_FLAGS)
+ doc.setMarkdown(_close_fences(text), MARKDOWN_FLAGS)
html_text = doc.toHtml()
start = html_text.find("<body")