diff --git a/kde/gui/chatblockmodel.cpp b/kde/gui/chatblockmodel.cpp
index 7a652ba..f060e53 100644
--- a/kde/gui/chatblockmodel.cpp
+++ b/kde/gui/chatblockmodel.cpp
@@ -319,6 +319,86 @@ QString ChatBlockModel::escapeForHtml(const QString &text) const
return result;
}
+bool ChatBlockModel::isTableLine(const QString &line)
+{
+ const QString trimmed = line.trimmed();
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.count('|') >= 2;
+}
+
+QString ChatBlockModel::renderTable(const QStringList &tableLines) const
+{
+ const QString defaultColor = getDefaultTextColor();
+
+ QString html = QStringLiteral("
");
+
+ bool pastSeparator = false;
+
+ for (const QString &line : tableLines) {
+ const QString trimmed = line.trimmed();
+ // Skip separator rows (|---|---|)
+ static QRegularExpression sepRe(QStringLiteral(R"(^\|[\s:|-]+\|$)"));
+ if (sepRe.match(trimmed).hasMatch()) {
+ pastSeparator = true;
+ continue;
+ }
+
+ // Strip leading/trailing |, split by |
+ QString inner = trimmed;
+ if (inner.startsWith('|')) inner = inner.mid(1);
+ if (inner.endsWith('|')) inner.chop(1);
+ const QStringList cells = inner.split('|');
+
+ const QString tag = pastSeparator ? QStringLiteral("td") : QStringLiteral("th");
+
+ html += QStringLiteral("");
+ for (const QString &cell : cells) {
+ html += QStringLiteral("<") + tag + QStringLiteral(">")
+ + cell.trimmed().toHtmlEscaped()
+ + QStringLiteral("") + tag + QStringLiteral(">");
+ }
+ html += QStringLiteral("
");
+ }
+
+ html += QStringLiteral("
");
+ return html;
+}
+
+QString ChatBlockModel::renderProse(const QStringList &lines) const
+{
+ QString html;
+ QStringList textRun;
+ QStringList tableRun;
+
+ auto flushText = [&]() {
+ if (!textRun.isEmpty()) {
+ html += escapeForHtml(textRun.join('\n'));
+ textRun.clear();
+ }
+ };
+
+ auto flushTable = [&]() {
+ if (!tableRun.isEmpty()) {
+ html += renderTable(tableRun);
+ tableRun.clear();
+ }
+ };
+
+ for (const QString &line : lines) {
+ if (isTableLine(line)) {
+ flushText();
+ tableRun.append(line);
+ } else {
+ flushTable();
+ textRun.append(line);
+ }
+ }
+
+ flushText();
+ flushTable();
+ return html;
+}
+
void ChatBlockModel::rebuildBlockHtml(int blockIndex)
{
if (blockIndex < 0 || blockIndex >= m_blocks.size())
@@ -337,7 +417,7 @@ void ChatBlockModel::rebuildBlockHtml(int blockIndex)
auto flushProse = [&]() {
if (!proseLines.isEmpty()) {
- QString proseHtml = escapeForHtml(proseLines.join('\n'));
+ QString proseHtml = renderProse(proseLines);
html += proseHtml;
BlockSegment seg;
@@ -460,6 +540,13 @@ void ChatBlockModel::incrementalAppend(int blockIndex, const QString &line)
// Check for fence open
auto match = s_fenceOpenRe.match(line);
if (match.hasMatch()) {
+ // Flush any accumulated table into a finalized segment
+ if (rs.inTable && !rs.tableLines.isEmpty()) {
+ if (!block.segments.isEmpty())
+ block.segments.last().html = renderTable(rs.tableLines);
+ rs.tableLines.clear();
+ rs.inTable = false;
+ }
// Flush any accumulated prose into a finalized text segment
if (!rs.proseLines.isEmpty()) {
// The last segment should already be Text with these prose lines;
@@ -479,6 +566,37 @@ void ChatBlockModel::incrementalAppend(int blockIndex, const QString &line)
return;
}
+ if (isTableLine(line)) {
+ // Transitioning from prose to table — flush prose, start table segment
+ if (!rs.inTable) {
+ if (!rs.proseLines.isEmpty())
+ rs.proseLines.clear();
+ rs.inTable = true;
+ rs.tableLines.clear();
+ rs.tableLines.append(line);
+
+ // Start a new segment for the table
+ BlockSegment seg;
+ seg.type = BlockSegment::Text;
+ seg.html = renderTable(rs.tableLines);
+ block.segments.append(seg);
+ } else {
+ // Continue accumulating table lines
+ rs.tableLines.append(line);
+ if (!block.segments.isEmpty())
+ block.segments.last().html = renderTable(rs.tableLines);
+ }
+ rebuildRenderedHtmlFromSegments(block);
+ return;
+ }
+
+ // Non-table prose line
+ if (rs.inTable) {
+ // Table just ended — finalize the table segment, start new prose
+ rs.tableLines.clear();
+ rs.inTable = false;
+ }
+
// Normal prose line — extend or create last text segment
rs.proseLines.append(line);
@@ -635,25 +753,44 @@ void ChatBlockModel::renderLastSegmentPartial(int blockIndex, const QString &par
+ inner + QStringLiteral("");
}
} else {
- // Show partial line appended to current prose — just escape one line
- const QString defaultColor = getDefaultTextColor();
- const QString escapedPartial = partialLine.toHtmlEscaped();
-
- if (!block.segments.isEmpty() && block.segments.last().type == BlockSegment::Text) {
- // Temporarily append to the segment (caller will restore)
- QString &segHtml = block.segments.last().html;
- if (segHtml.endsWith(QStringLiteral(""))) {
- segHtml.chop(6);
- segHtml += '\n' + escapedPartial + QStringLiteral("");
+ // Show partial line appended to current prose/table
+ if (rs.inTable) {
+ // Currently building a table — show partial line as part of the table
+ if (isTableLine(partialLine)) {
+ QStringList preview = rs.tableLines;
+ preview.append(partialLine);
+ if (!block.segments.isEmpty())
+ block.segments.last().html = renderTable(preview);
} else {
- segHtml = escapeForHtml((rs.proseLines + QStringList{partialLine}).join('\n'));
+ // Partial line breaks the table — just show it as escaped text after
+ const QString defaultColor = getDefaultTextColor();
+ const QString escapedPartial = partialLine.toHtmlEscaped();
+ BlockSegment seg;
+ seg.type = BlockSegment::Text;
+ seg.html = QStringLiteral("")
+ + escapedPartial + QStringLiteral("
");
+ block.segments.append(seg);
}
} else {
- BlockSegment seg;
- seg.type = BlockSegment::Text;
- seg.html = QStringLiteral("")
- + escapedPartial + QStringLiteral("
");
- block.segments.append(seg);
+ const QString defaultColor = getDefaultTextColor();
+ const QString escapedPartial = partialLine.toHtmlEscaped();
+
+ if (!block.segments.isEmpty() && block.segments.last().type == BlockSegment::Text) {
+ // Temporarily append to the segment (caller will restore)
+ QString &segHtml = block.segments.last().html;
+ if (segHtml.endsWith(QStringLiteral(""))) {
+ segHtml.chop(6);
+ segHtml += '\n' + escapedPartial + QStringLiteral("");
+ } else {
+ segHtml = escapeForHtml((rs.proseLines + QStringList{partialLine}).join('\n'));
+ }
+ } else {
+ BlockSegment seg;
+ seg.type = BlockSegment::Text;
+ seg.html = QStringLiteral("")
+ + escapedPartial + QStringLiteral("
");
+ block.segments.append(seg);
+ }
}
}
rebuildRenderedHtmlFromSegments(block);
diff --git a/kde/gui/chatblockmodel.h b/kde/gui/chatblockmodel.h
index d900765..3d23eb0 100644
--- a/kde/gui/chatblockmodel.h
+++ b/kde/gui/chatblockmodel.h
@@ -37,6 +37,8 @@ struct BlockRenderState {
QString fenceLang; // Language of the open fence
QStringList fenceLines; // Accumulated lines in the open fence
QStringList proseLines; // Accumulated prose lines (not yet flushed)
+ QStringList tableLines; // Accumulated table lines (being built)
+ bool inTable = false; // Currently accumulating table lines
int committedLineCount = 0; // Number of content lines already processed incrementally
// Incremental syntax highlighting state for open code fences
KSyntaxHighlighting::State hlState; // Highlighter state after last processed line
@@ -113,6 +115,9 @@ private:
QString highlightCode(const QString &code, const QString &language) const;
QString highlightCodeWrapped(const QString &innerHtml) const;
QString escapeForHtml(const QString &text) const;
+ QString renderProse(const QStringList &lines) const;
+ QString renderTable(const QStringList &tableLines) const;
+ static bool isTableLine(const QString &line);
QString getDefaultTextColor() const;
// Diagram rendering