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(""); + } + 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