From b77ab4bb3d0690bd036b63a847ae9082c5700de3 Mon Sep 17 00:00:00 2001 From: JackLee <809262979@qq.com> Date: Wed, 5 Mar 2025 19:39:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B=E5=B0=8FBU?= =?UTF-8?q?G?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- ctai_cmark-gfm.cpp | 111 +++++++++++++++++++++ ctai_cmark-gfm.h | 43 ++++++++ res/img/btn/btn_info_down.png | Bin 927 -> 1142 bytes res/img/btn/btn_info_up.png | Bin 906 -> 1091 bytes src/ctai_cmark-gfm.cpp | 183 ---------------------------------- src/ctai_cmark-gfm.h | 52 ---------- src/ctai_history_textedit.cpp | 4 +- 8 files changed, 158 insertions(+), 238 deletions(-) create mode 100644 ctai_cmark-gfm.cpp create mode 100644 ctai_cmark-gfm.h delete mode 100644 src/ctai_cmark-gfm.cpp delete mode 100644 src/ctai_cmark-gfm.h diff --git a/.gitignore b/.gitignore index e308aa5..c9be50e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,8 @@ release/* save/* .cache/* .vscode/* - +src/ctai_cmark-gfm.cpp +src/ctai_cmark-gfm.h # Compiled Java class files *.class diff --git a/ctai_cmark-gfm.cpp b/ctai_cmark-gfm.cpp new file mode 100644 index 0000000..90dce5f --- /dev/null +++ b/ctai_cmark-gfm.cpp @@ -0,0 +1,111 @@ +#include "ctai_cmark-gfm.h" + +namespace ctai_cmark_gfm { + +MarkdownStreamProcessor::MarkdownStreamProcessor(QTextEdit* textEdit) + : m_textEdit(textEdit) + , m_buffer() + , m_tempText() { +} + +MarkdownStreamProcessor::~MarkdownStreamProcessor() { + // 确保在销毁时处理所有剩余的缓冲内容 + if (!m_buffer.isEmpty()) { + handleBufferOverflow(); + } +} + +void MarkdownStreamProcessor::appendMarkdown(const QString& text) { + // 将新内容添加到缓冲区 + QString newBuffer = m_buffer + text; + + // 检查是否超出缓冲区大小 + if (newBuffer.size() > BUFFER_SIZE) { + handleBufferOverflow(); + newBuffer = text; + } + + // 检查是否只包含开始标记 + if (hasMarkdownStart(text) && !hasMarkdownEnd(text)) { + m_buffer = newBuffer; + m_tempText = text; + m_textEdit->append(text); + return; + } + + // 检查是否包含结束标记 + if (hasMarkdownEnd(text)) { + QString fullContent = m_buffer + text; + QString rendered = renderMarkdown(fullContent); + replaceTempText(rendered); + m_buffer.clear(); + return; + } + + m_buffer = newBuffer; +} + +bool MarkdownStreamProcessor::hasMarkdownStart(const QString& text) { + // 检查常见的Markdown开始标记 + static const QStringList startMarkers = { + "```", "~~~", "#", ">", "*", "_", "-", "+", "[", "|" + }; + + for (const QString& marker : startMarkers) { + if (text.contains(marker)) { + return true; + } + } + return false; +} + +bool MarkdownStreamProcessor::hasMarkdownEnd(const QString& text) { + // 检查常见的Markdown结束标记 + static const QStringList endMarkers = { + "```", "~~~", "\n\n", "]", "*", "_" + }; + + for (const QString& marker : endMarkers) { + if (text.contains(marker)) { + return true; + } + } + return false; +} + +QString MarkdownStreamProcessor::renderMarkdown(const QString& text) { + QByteArray textUtf8 = text.toUtf8(); + char* rendered = cmark_markdown_to_html( + textUtf8.constData(), + textUtf8.size(), + CMARK_OPT_DEFAULT + ); + + QString result = QString::fromUtf8(rendered); + free(rendered); + return result; +} + +void MarkdownStreamProcessor::handleBufferOverflow() { + if (!m_buffer.isEmpty()) { + QString rendered = renderMarkdown(m_buffer); + replaceTempText(rendered); + m_buffer.clear(); + } +} + +void MarkdownStreamProcessor::replaceTempText(const QString& newContent) { + if (!m_tempText.isEmpty()) { + // 获取当前文本 + QString currentText = m_textEdit->toPlainText(); + // 替换临时文本 + currentText.replace(m_tempText, newContent); + m_textEdit->setHtml(currentText); + m_tempText.clear(); + } else { + m_textEdit->append(newContent); + } +} + +} + diff --git a/ctai_cmark-gfm.h b/ctai_cmark-gfm.h new file mode 100644 index 0000000..de91149 --- /dev/null +++ b/ctai_cmark-gfm.h @@ -0,0 +1,43 @@ +#ifndef CTAI_CMARK_GFM_H +#define CTAI_CMARK_GFM_H + +#include +#include +#include +#include + +namespace ctai_cmark_gfm { + +class MarkdownStreamProcessor { +public: + explicit MarkdownStreamProcessor(QTextEdit* textEdit); + ~MarkdownStreamProcessor(); + + // 追加内容并处理Markdown + void appendMarkdown(const QString& text); + +private: + static constexpr size_t BUFFER_SIZE = 4096; + + QTextEdit* m_textEdit; + QString m_buffer; + QString m_tempText; + + // 检查是否包含Markdown开始标记 + bool hasMarkdownStart(const QString& text); + + // 检查是否包含Markdown结束标记 + bool hasMarkdownEnd(const QString& text); + + // 渲染Markdown内容 + QString renderMarkdown(const QString& text); + + // 处理缓冲区溢出 + void handleBufferOverflow(); + + // 替换临时文本 + void replaceTempText(const QString& newContent); +}; + +} +#endif \ No newline at end of file diff --git a/res/img/btn/btn_info_down.png b/res/img/btn/btn_info_down.png index 5f2bd8595c4b93b160b3921400df3bb5b1cfc0af..3166d614103115ffbcd99ce92de56784cb38a5fc 100644 GIT binary patch delta 1122 zcmV-o1fBbz2lfb%BYy-vNklPX*AB5@q zP5NH*c{A|n``nqi_uPBW+?mu?XYppUxczkk01*KeM?j>2#eV}pkf2BbkpdP+K%@X0 z0DiyUQ(s?y8}FNIEI3AOHrwIh;o+~vb}fj2U@&+Mg2!sic*&fBk&%&;S^!)H0LN<) zfI^{gDHe-$sR9rP1e)x2`<+@$fm|+kB#}r=s{&Aqo>#L;tO8c^IBTzn01*KeM?j>2 z#RFjBfr!`(h=096Q0xW7Ucf@kD3(DBIa}6L5dk6sERKL;3gB>b2e7$?YaPxA+i{o} zuO_TQq0nBO0ro-nE%|)@XgnUjDW|_802p8b9o9>oFjx-`6faZ*2sqd5K`nE>NZ0Rh z;3_Tv5CJnlZ+USifG`LU4pcG-aNHpDYiIQS2EA8v0e{ej1{5Cx1UJ+1?d8V)<_4-nrEWK-n?0jcW4{7+;ebsUk}suii40FZG2!Rd0j z9^f{@7=N3`a~BBaN;yD4*muJT{V<-jBRV-b*`7|Pvs!xT0zeuBXrICTIhhHE@Pq2X zXf&GBn$U>vUaz;oG$_2&i~C*~a=eQbq66P(wcu{ZcL(zQK030MT>_NxmPGg{3x1!^B_QEm(6CA0ChW)?^M}wlbxv%P!tJDJWq)@_Pi4mojwt{XrNmiylL(S+*Flb*c=?|k zmB^01qxGP&yU1&pUU^diASF~P#}}DQCg^s%8z4qSm8oR6BqXRb3G&(t18LXTSS$lx z_<(0OzHMOoeu3xr!1M3)da_hG(<4AqEF2E6fl=gXc3aKrK5hotl{EsS$yLZv5?e8i z>?#6)B66hDTH0DmjmWSY#7HZ^lC~vF0oE!kd11Wk!}XW;@Vwhtqfjav07e7`-=#ub`LIAf6GU}KUK!ZP`>wXk^lez07*qoM6N<$f;O@B+W-In delta 906 zcmV;519klN2%iU#BYy*CNkllF<0mZU6cZp0)H5v1bwY%j=`xCP$i&BfNeF% zQD(k&QF8##8~|$LNbBZ)q!vrz_jq?#5!E;IY7h=D@IK(Er~$CHgH>n+5DoysCsyBX z_RIkAK7i2QB%*Te9Qhu^vr z1gtm>>_D5H*FyooE`=-jzTFJ~vVRwKf4SQ=4K*~43v-k;v=L_CdmUn+?G(=k%TU5p zyMH}`Uq$eHoZ29NnCSZf2@Gxq5FXJ6>STxcU>oLxkASelw)q19y8+;vBLFCXtiwmW zcJ9Sr1jq(jXp+(1Jqa!Gi}cO3;I+iWV9* z%?}2Y9*SB#TR}^Uw2B35MFqWh@E>~6NSiilTYnCED1y|BhzKg$isC^Le-CLAp8+Fr znrwEH>_)P)Q0OMJv)}v7H}8Hk8%K@J4~Nb5*C+rg18hz}O@9HKCx9|RH3ifZusH!W z1>^+q`~9BU+S>Dc-cn`BG3jtPc8`pVe8|?dAOnKI;AtA}t1|H!IfJ94qdkQHoCLuB zY6Or@r-x#(ShpsCKp@cMbUH6o;}l3HlY7R;$KPuLsK$}k(xjdOb{lb7ugUXa|TrQVLdv~@N=KMWGhFN#G_*`Vh2I>2soNvb$1USg8O5cshtzawqt6O5dAi zj%5ds&wuO?{*}@6^J?m$)@YJ7o5-3NZI(#wOfko@0wDM`1V2UKjnaI6$$2Xy|4L6P zV;igRhP(9DB=s}L`#oPg9)G5%A3bf#1|Xl=5yS(679;;nPe7SA_`=deA`v&+9LoYg zm>q%l5v<(bCCsifV|GRWSY?hS0VrU0U8MY3qkp1vwFmG)*dEJaj!`Pd_EIXxq5&Yj zp{}lOO0MjL*%4?M@%PIiKGO#feEffd`-#FS`zXmAivl3bjtE5YvRpE$H=J+QYj#Ef z@y=p3Mb$((&CxIbjmmBsL0b|3#i%eN?S(l~G);2NIthMJm7^g5jJ&Ao`g54wFbkLt z7Jo2FBVvke>-BmUc|4wBt`~a>e-EZ#n%H_V-a);6>jI!*cA}I$zzt97y>|tSCi^zTnUT@R8R4)byNR zMR-TCL7IJCb91x1zP|n<5OzxUdQIFb4FMqi-_nsMa}jjAE3s)=3^ZatMC?biu6+Uk zuOR?D?>Dj{J4Z(%g~X=B?P7wd)HO^AvGF<}9*yYO%a5cV83ItLz^%S-!N;K00Vr90 pKLx;Q;{V(IR0jNSBVu(tzX3#9x=_puz%2j(002ovPDHLkV1gRj_5uI^ delta 884 zcmV-)1B?8_2#N=gBYy)?NklB-w!QgT)!PjkB8m}k^fb8EQ`*&pk zU>M9R2Ot~{4~Igb@eTkk0qB%7@jAg~Dg$0Jvoyg**VjH3z^P<9w=&Z7iBaq=;JST2g#IuG1K$o8Gzj)H!1tsQ;+oAx_|@Zzl8k%2$;4s5dt8q@qX}Q zRspjNntg!T2$`Lr*$SJpfH@4A(;x&mW0>QxxqlCEyAa{k(nzUPI)`F5%Ay%jdM=K=UHK%`wD m_bufferMax * 1.2) - { - int removeCount = m_buffer.length() - m_bufferMax; - m_buffer.remove(0, removeCount); - adjustSyntaxPositions(removeCount); - } -} -// 调整语法标签位置 -void ctai_cmark_gfm::adjustSyntaxPositions(int offset) -{ - std::stack adjustedStack; - while (!m_syntaxStack.empty()) - { - SyntaxTag tag = m_syntaxStack.top(); - m_syntaxStack.pop(); - tag.startPos = qMax(0, tag.startPos - offset); - if (tag.startPos >= 0) - { - adjustedStack.push(tag); - } - } - m_syntaxStack = adjustedStack; -} -// 语法状态检测 -SyntaxState ctai_cmark_gfm::detectSyntaxState(const QString &buffer) -{ - SyntaxState state; - static const QVector> syntaxPatterns = { - {QRegularExpression(R"(\*\*(?!\s))"), Bold}, - {QRegularExpression(R"(\*(?!\s))"), Italic}, - {QRegularExpression(R"(^#{1,6}\s)"), Heading}, - {QRegularExpression(R"(^```+[\s\S]*?```)", QRegularExpression::MultilineOption), CodeBlock}, - {QRegularExpression(R"(^[\*\+-]\s)"), List}, - {QRegularExpression(R"(^>\s)"), Blockquote}, - {QRegularExpression(R"($$.*?$$$.*?$)"), Link}, - {QRegularExpression(R"(!$$.*?$$$.*?$)"), Image}}; - - for (const auto &[pattern, type] : syntaxPatterns) - { - QRegularExpressionMatchIterator it = pattern.globalMatch(buffer); - while (it.hasNext()) - { - QRegularExpressionMatch match = it.next(); - state.activeTags.insert(type, match.captured()); - } - } - - state.needsClosure = !m_syntaxStack.empty() && - (buffer.length() - m_syntaxStack.top().startPos) > m_bufferMax / 2; - return state; -} -// 更新语法标签栈 -void ctai_cmark_gfm::updateSyntaxStack(const SyntaxState &newState) -{ - // 处理标签闭合 - auto it = newState.activeTags.begin(); - while (it != newState.activeTags.end()) - { - if (it.key() == CodeBlock && m_syntaxStack.top().type == CodeBlock) - { - m_syntaxStack.pop(); - } - // 其他标签闭合逻辑... - ++it; - } - - // 处理新标签 - for (auto type : newState.activeTags.keys()) - { - SyntaxTag newTag; - newTag.type = type; - newTag.startPos = m_buffer.lastIndexOf(newState.activeTags[type]); - newTag.opener = newState.activeTags[type]; - newTag.closer = getCloserForType(type); - m_syntaxStack.push(newTag); - } -} -// 获取闭合标签 -QString ctai_cmark_gfm::getCloserForType(SyntaxType type) -{ - static const QMap closers = { - {CodeBlock, "\n```"}, - {Bold, "**"}, - {Italic, "*"}, - {Heading, "\n"}, - {List, "\n"}, - {Blockquote, "\n"}, - {Link, ")"}, - {Image, ")"}}; - return closers.value(type, ""); -} - -// 渲染决策 -bool ctai_cmark_gfm::shouldRender(const SyntaxState &state) -{ - return state.needsClosure || - (!m_syntaxStack.empty() && m_buffer.length() - m_syntaxStack.top().startPos > 512) || - m_buffer.length() >= m_bufferMax; -} - -void ctai_cmark_gfm::renderContent() -{ - QString renderContent = m_buffer; - - // 自动闭合未完成的标签 - while (!m_syntaxStack.empty()) - { - const SyntaxTag &tag = m_syntaxStack.top(); - if (!renderContent.contains(tag.closer)) - { - renderContent += tag.closer; - } - m_syntaxStack.pop(); - } - - // 使用cmark解析 - cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT); - cmark_parser_feed(parser, renderContent.toUtf8().constData(), renderContent.size()); - cmark_node *doc = cmark_parser_finish(parser); - - // 转换为HTML - char *html = cmark_render_html(doc, CMARK_OPT_DEFAULT); - updateEditor(QString::fromUtf8(html)); - - // 清理资源 - cmark_node_free(doc); - cmark_parser_free(parser); - free(html); - - // 重置缓冲区 - m_buffer.clear(); -} -// 更新编辑器内容(保持滚动位置) -void ctai_cmark_gfm::updateEditor(const QString &html) -{ - QTextCursor cursor(m_editor->document()); - int oldHeight = m_editor->document()->size().height(); - bool atBottom = m_editor->verticalScrollBar()->value() == - m_editor->verticalScrollBar()->maximum(); - - cursor.select(QTextCursor::Document); - cursor.insertHtml(html); - - // 滚动位置处理 - if (atBottom) - { - m_editor->verticalScrollBar()->setValue(m_editor->verticalScrollBar()->maximum()); - } - else - { - int newHeight = m_editor->document()->size().height(); - m_editor->verticalScrollBar()->setValue(m_editor->verticalScrollBar()->value() + (newHeight - oldHeight)); - } -} \ No newline at end of file diff --git a/src/ctai_cmark-gfm.h b/src/ctai_cmark-gfm.h deleted file mode 100644 index 37f486b..0000000 --- a/src/ctai_cmark-gfm.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef CTAI_CMARK_GFM_H -#define CTAI_CMARK_GFM_H - -#include -#include -#include -#include -#include -enum SyntaxType { - CodeBlock, // 代码块 - Bold, // 粗体 - Italic, // 斜体 - Heading, // 标题 - List, // 列表 - Blockquote, // 引用 - Link, // 链接 - Image, // 图片 - CustomBlock // 自定义块 -}; -struct SyntaxTag { - SyntaxType type; - int startPos; - QString opener; - QString closer; -}; - -struct SyntaxState { - QMap activeTags; - bool needsClosure = false; -}; - -class ctai_cmark_gfm : public QObject { - Q_OBJECT -public: - ctai_cmark_gfm(QTextEdit *editor, QObject *parent = nullptr); - void processInput(const QString &text); -private: - QTextEdit *m_editor; - const int m_bufferMax; - QString m_buffer; - std::stack m_syntaxStack; // 语法标签栈 - void handleBufferOverflow(); - void adjustSyntaxPositions(int offset); - SyntaxState detectSyntaxState(const QString &buffer); - void updateSyntaxStack(const SyntaxState &newState); - bool shouldRender(const SyntaxState &state); - QString getCloserForType(SyntaxType type); - void renderContent(); - void updateEditor(const QString &html); -}; - -#endif \ No newline at end of file diff --git a/src/ctai_history_textedit.cpp b/src/ctai_history_textedit.cpp index 0ae191c..7faee6a 100644 --- a/src/ctai_history_textedit.cpp +++ b/src/ctai_history_textedit.cpp @@ -53,7 +53,7 @@ void ctai_history_textedit::init_layout(msg_type msg_type_mode) m_msg_menu->setObjectName("m_msg_menu"); m_msg_fold->setObjectName("m_msg_fold"); m_msg_fold->setIcon(QIcon(":res/img/btn/btn_info_up.png")); - m_msg_fold->setMinimumSize(QSize(25, 25)); + m_msg_fold->setIconSize(QSize(25, 25)); header_opts_Layout->addItem(sparcer_item); header_opts_Layout->addWidget(m_msg_copy); header_opts_Layout->addWidget(m_msg_save); @@ -308,7 +308,7 @@ void ctai_history_textedit::add_header_message(const model_data &message) void ctai_history_textedit::on_delete_clicked() { // 发送删除请求信号 - emit delete_requested(m_msg_sned_id); + emit delete_requested(m_msg_sned_id); } void ctai_history_textedit::on_copy_clicked()