File indexing completed on 2024-06-23 05:18:25

0001 /*
0002    SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "richtextcomposersignatures.h"
0008 #include "richtextcomposerng.h"
0009 
0010 #include <KIdentityManagementCore/Signature>
0011 
0012 #include <QRegularExpression>
0013 #include <QTextBlock>
0014 using namespace MessageComposer;
0015 
0016 class RichTextComposerSignatures::RichTextComposerSignaturesPrivate
0017 {
0018 public:
0019     RichTextComposerSignaturesPrivate(RichTextComposerNg *composer)
0020         : richTextComposer(composer)
0021     {
0022     }
0023 
0024     void cleanWhitespaceHelper(const QRegularExpression &regExp, const QString &newText, const KIdentityManagementCore::Signature &sig);
0025     [[nodiscard]] QList<QPair<int, int>> signaturePositions(const KIdentityManagementCore::Signature &sig) const;
0026     RichTextComposerNg *const richTextComposer;
0027 };
0028 
0029 RichTextComposerSignatures::RichTextComposerSignatures(MessageComposer::RichTextComposerNg *composer, QObject *parent)
0030     : QObject(parent)
0031     , d(new RichTextComposerSignaturesPrivate(composer))
0032 {
0033 }
0034 
0035 RichTextComposerSignatures::~RichTextComposerSignatures() = default;
0036 
0037 void RichTextComposerSignatures::RichTextComposerSignaturesPrivate::cleanWhitespaceHelper(const QRegularExpression &regExp,
0038                                                                                           const QString &newText,
0039                                                                                           const KIdentityManagementCore::Signature &sig)
0040 {
0041     int currentSearchPosition = 0;
0042 
0043     for (;;) {
0044         // Find the text
0045         const QString text = richTextComposer->document()->toPlainText();
0046         const auto currentMatch = regExp.match(text, currentSearchPosition);
0047         if (!currentMatch.hasMatch()) {
0048             break;
0049         }
0050         currentSearchPosition = currentMatch.capturedStart();
0051 
0052         // Select the text
0053         QTextCursor cursor(richTextComposer->document());
0054         cursor.setPosition(currentSearchPosition);
0055         cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, currentMatch.capturedLength());
0056         // Skip quoted text
0057         if (richTextComposer->isLineQuoted(cursor.block().text())) {
0058             currentSearchPosition += currentMatch.capturedLength();
0059             continue;
0060         }
0061         // Skip text inside signatures
0062         bool insideSignature = false;
0063         const QList<QPair<int, int>> sigPositions = signaturePositions(sig);
0064         for (const QPair<int, int> &position : sigPositions) {
0065             if (cursor.position() >= position.first && cursor.position() <= position.second) {
0066                 insideSignature = true;
0067             }
0068         }
0069         if (insideSignature) {
0070             currentSearchPosition += currentMatch.capturedLength();
0071             continue;
0072         }
0073 
0074         // Replace the text
0075         cursor.removeSelectedText();
0076         cursor.insertText(newText);
0077         currentSearchPosition += newText.length();
0078     }
0079 }
0080 
0081 void RichTextComposerSignatures::cleanWhitespace(const KIdentityManagementCore::Signature &sig)
0082 {
0083     QTextCursor cursor(d->richTextComposer->document());
0084     cursor.beginEditBlock();
0085 
0086     // Squeeze tabs and spaces
0087     d->cleanWhitespaceHelper(QRegularExpression(QLatin1StringView("[\t ]+")), QStringLiteral(" "), sig);
0088 
0089     // Remove trailing whitespace
0090     d->cleanWhitespaceHelper(QRegularExpression(QLatin1StringView("[\t ][\n]")), QStringLiteral("\n"), sig);
0091 
0092     // Single space lines
0093     d->cleanWhitespaceHelper(QRegularExpression(QLatin1StringView("[\n]{3,}")), QStringLiteral("\n\n"), sig);
0094 
0095     if (!d->richTextComposer->textCursor().hasSelection()) {
0096         d->richTextComposer->textCursor().clearSelection();
0097     }
0098 
0099     cursor.endEditBlock();
0100 }
0101 
0102 QList<QPair<int, int>> RichTextComposerSignatures::RichTextComposerSignaturesPrivate::signaturePositions(const KIdentityManagementCore::Signature &sig) const
0103 {
0104     QList<QPair<int, int>> signaturePositions;
0105     if (!sig.rawText().isEmpty()) {
0106         QString sigText = sig.toPlainText();
0107 
0108         int currentSearchPosition = 0;
0109         for (;;) {
0110             // Find the next occurrence of the signature text
0111             const QString text = richTextComposer->document()->toPlainText();
0112             const int currentMatch = text.indexOf(sigText, currentSearchPosition);
0113             currentSearchPosition = currentMatch + sigText.length();
0114             if (currentMatch == -1) {
0115                 break;
0116             }
0117 
0118             signaturePositions.append(QPair<int, int>(currentMatch, currentMatch + sigText.length()));
0119         }
0120     }
0121     return signaturePositions;
0122 }
0123 
0124 bool RichTextComposerSignatures::replaceSignature(const KIdentityManagementCore::Signature &oldSig, const KIdentityManagementCore::Signature &newSig)
0125 {
0126     bool found = false;
0127     if (oldSig == newSig) {
0128         return false;
0129     }
0130     QString oldSigText = oldSig.toPlainText();
0131     if (oldSigText.isEmpty()) {
0132         return false;
0133     }
0134     QTextCursor cursor(d->richTextComposer->document());
0135     cursor.beginEditBlock();
0136     int currentSearchPosition = 0;
0137     for (;;) {
0138         // Find the next occurrence of the signature text
0139         const QString text = d->richTextComposer->document()->toPlainText();
0140         const int currentMatch = text.indexOf(oldSigText, currentSearchPosition);
0141         currentSearchPosition = currentMatch;
0142         if (currentMatch == -1) {
0143             break;
0144         }
0145 
0146         // Select the signature
0147         cursor.setPosition(currentMatch);
0148 
0149         // If the new signature is completely empty, we also want to remove the
0150         // signature separator, so include it in the selection
0151         int additionalMove = 0;
0152         if (newSig.rawText().isEmpty() && text.mid(currentMatch - 4, 4) == QLatin1StringView("-- \n")) {
0153             cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 4);
0154             additionalMove = 4;
0155         } else if (newSig.rawText().isEmpty() && text.mid(currentMatch - 1, 1) == QLatin1Char('\n')) {
0156             cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 1);
0157             additionalMove = 1;
0158         }
0159         cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, oldSigText.length() + additionalMove);
0160         // Skip quoted signatures
0161         if (d->richTextComposer->isLineQuoted(cursor.block().text())) {
0162             currentSearchPosition += oldSig.toPlainText().length();
0163             continue;
0164         }
0165         // Remove the old and insert the new signature
0166         cursor.removeSelectedText();
0167         d->richTextComposer->setTextCursor(cursor);
0168         d->richTextComposer->insertSignature(newSig, KIdentityManagementCore::Signature::AtCursor, KIdentityManagementCore::Signature::AddNothing);
0169         found = true;
0170 
0171         currentSearchPosition += newSig.toPlainText().length();
0172     }
0173 
0174     cursor.endEditBlock();
0175     return found;
0176 }
0177 
0178 #include "moc_richtextcomposersignatures.cpp"