diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp
index d14f910..08406e3 100644
--- a/gui/chatblockmodel.cpp
+++ b/gui/chatblockmodel.cpp
@@ -201,9 +201,13 @@ QString ChatBlockModel::renderMarkdown(const QString &markdown) const
auto it = fenceRe.globalMatch(markdown);
while (it.hasNext()) {
const auto match = it.next();
- QTextDocument prose;
- prose.setMarkdown(markdown.mid(cursor, match.capturedStart() - cursor));
- result += prose.toHtml();
+ // Prose before this fence: HTML-escape and wrap in pre-wrap div to
+ // preserve newlines and whitespace. QTextDocument::setMarkdown would
+ // collapse single newlines (standard Markdown behavior).
+ const QString prose = markdown.mid(cursor, match.capturedStart() - cursor);
+ if (!prose.isEmpty())
+ result += QStringLiteral("
")
+ + prose.toHtmlEscaped() + QStringLiteral("
");
result += highlightCode(match.captured(2), match.captured(1));
cursor = match.capturedEnd();
}
@@ -215,16 +219,18 @@ QString ChatBlockModel::renderMarkdown(const QString &markdown) const
R"(^```([A-Za-z0-9_+.-]*)[^\n]*\n([\s\S]*)$)",
QRegularExpression::MultilineOption);
const auto tail = markdown.mid(cursor);
- const auto open = openFenceRe.match(tail);
- if (open.hasMatch()) {
- QTextDocument prose;
- prose.setMarkdown(tail.left(open.capturedStart()));
- result += prose.toHtml();
- result += highlightCode(open.captured(2), open.captured(1));
- } else {
- QTextDocument prose;
- prose.setMarkdown(tail);
- result += prose.toHtml();
+ if (!tail.isEmpty()) {
+ const auto open = openFenceRe.match(tail);
+ if (open.hasMatch()) {
+ const QString prose = tail.left(open.capturedStart());
+ if (!prose.isEmpty())
+ result += QStringLiteral("")
+ + prose.toHtmlEscaped() + QStringLiteral("
");
+ result += highlightCode(open.captured(2), open.captured(1));
+ } else {
+ result += QStringLiteral("")
+ + tail.toHtmlEscaped() + QStringLiteral("
");
+ }
}
return result;
}