File indexing completed on 2025-02-16 04:49:26

0001 /*
0002    SPDX-FileCopyrightText: 2018-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "markdowninterface.h"
0008 #include "markdownconverter.h"
0009 #include "markdowncreateimagedialog.h"
0010 #include "markdowncreatelinkdialog.h"
0011 #include "markdownplugin_debug.h"
0012 #include "markdownpreviewdialog.h"
0013 #include "markdownutil.h"
0014 #include <KActionCollection>
0015 #include <KConfigGroup>
0016 #include <KLocalizedString>
0017 #include <KPIMTextEdit/RichTextComposer>
0018 #include <KPIMTextEdit/RichTextComposerControler>
0019 #include <KSharedConfig>
0020 #include <QAction>
0021 #include <QMenu>
0022 
0023 #include <MessageComposer/TextPart>
0024 
0025 #include <MessageComposer/StatusBarLabelToggledState>
0026 
0027 MarkdownInterface::MarkdownInterface(QObject *parent)
0028     : MessageComposer::PluginEditorConvertTextInterface(parent)
0029 {
0030 }
0031 
0032 MarkdownInterface::~MarkdownInterface() = default;
0033 
0034 void MarkdownInterface::createAction(KActionCollection *ac)
0035 {
0036     mAction = new QAction(i18n("Generate HTML from markdown language."), this);
0037     mAction->setCheckable(true);
0038     mAction->setChecked(false);
0039     ac->addAction(QStringLiteral("generate_markdown"), mAction);
0040     connect(mAction, &QAction::triggered, this, &MarkdownInterface::slotActivated);
0041     MessageComposer::PluginActionType type(mAction, MessageComposer::PluginActionType::Edit);
0042     addActionType(type);
0043 
0044     mStatusBarLabel = new MessageComposer::StatusBarLabelToggledState(parentWidget());
0045     connect(mStatusBarLabel, &MessageComposer::StatusBarLabelToggledState::toggleModeChanged, this, [this](bool checked) {
0046         mAction->setChecked(checked);
0047         slotActivated(checked);
0048     });
0049     QFont f = mStatusBarLabel->font();
0050     f.setBold(true);
0051     mStatusBarLabel->setFont(f);
0052     setStatusBarWidget(mStatusBarLabel);
0053     mStatusBarLabel->setStateString(i18n("Markdown"), QString());
0054 
0055     mPopupMenuAction = new QAction(i18n("Markdown Action"), this);
0056 
0057     auto mardownMenu = new QMenu(parentWidget());
0058     mPopupMenuAction->setMenu(mardownMenu);
0059     mPopupMenuAction->setEnabled(false);
0060     auto titleMenu = new QMenu(i18n("Add Title"), mardownMenu);
0061     mardownMenu->addMenu(titleMenu);
0062     for (int i = 1; i < 5; ++i) {
0063         titleMenu->addAction(i18n("Level %1", QString::number(i)), this, [this, i]() {
0064             addTitle(i);
0065         });
0066     }
0067     mardownMenu->addAction(i18n("Horizontal Rule"), this, &MarkdownInterface::addHorizontalRule);
0068     mardownMenu->addSeparator();
0069     mBoldAction = mardownMenu->addAction(i18n("Change Selected Text as Bold"), this, &MarkdownInterface::addBold);
0070     mBoldAction->setEnabled(false);
0071     mItalicAction = mardownMenu->addAction(i18n("Change Selected Text as Italic"), this, &MarkdownInterface::addItalic);
0072     mItalicAction->setEnabled(false);
0073     mCodeAction = mardownMenu->addAction(i18n("Change Selected Text as Code"), this, &MarkdownInterface::addCode);
0074     mCodeAction->setEnabled(false);
0075     mBlockQuoteAction = mardownMenu->addAction(i18n("Change Selected Text as Block Quote"), this, &MarkdownInterface::addBlockQuote);
0076     mBlockQuoteAction->setEnabled(false);
0077     mardownMenu->addSeparator();
0078     mardownMenu->addAction(i18n("Add Link"), this, &MarkdownInterface::addLink);
0079     mardownMenu->addAction(i18n("Add Image"), this, &MarkdownInterface::addImage);
0080     MessageComposer::PluginActionType typePopup(mPopupMenuAction, MessageComposer::PluginActionType::PopupMenu);
0081     addActionType(typePopup);
0082     connect(richTextEditor(), &KPIMTextEdit::RichTextComposer::selectionChanged, this, &MarkdownInterface::slotSelectionChanged);
0083 }
0084 
0085 void MarkdownInterface::slotSelectionChanged()
0086 {
0087     const bool enabled = richTextEditor()->textCursor().hasSelection();
0088     mBoldAction->setEnabled(enabled);
0089     mItalicAction->setEnabled(enabled);
0090     mCodeAction->setEnabled(enabled);
0091     mBlockQuoteAction->setEnabled(enabled);
0092 }
0093 
0094 void MarkdownInterface::addHorizontalRule()
0095 {
0096     richTextEditor()->insertPlainText(QStringLiteral("\n---"));
0097 }
0098 
0099 void MarkdownInterface::addBold()
0100 {
0101     const QString selectedText = richTextEditor()->textCursor().selectedText();
0102     if (!selectedText.isEmpty()) {
0103         richTextEditor()->textCursor().insertText(QStringLiteral("**%1**").arg(selectedText));
0104     } else {
0105         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
0106     }
0107 }
0108 
0109 void MarkdownInterface::addBlockQuote()
0110 {
0111     const QString selectedText = richTextEditor()->textCursor().selectedText();
0112     if (!selectedText.isEmpty()) {
0113         richTextEditor()->composerControler()->addQuotes(QStringLiteral(">"));
0114     } else {
0115         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
0116     }
0117 }
0118 
0119 void MarkdownInterface::addCode()
0120 {
0121     const QString selectedText = richTextEditor()->textCursor().selectedText();
0122     if (!selectedText.isEmpty()) {
0123         richTextEditor()->textCursor().insertText(QStringLiteral("`%1`").arg(selectedText));
0124     } else {
0125         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
0126     }
0127 }
0128 
0129 void MarkdownInterface::addItalic()
0130 {
0131     const QString selectedText = richTextEditor()->textCursor().selectedText();
0132     if (!selectedText.isEmpty()) {
0133         richTextEditor()->textCursor().insertText(QStringLiteral("_%1_").arg(selectedText));
0134     } else {
0135         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
0136     }
0137 }
0138 
0139 void MarkdownInterface::addLink()
0140 {
0141     QPointer<MarkdownCreateLinkDialog> dlg = new MarkdownCreateLinkDialog(parentWidget());
0142     if (dlg->exec()) {
0143         const QString str = dlg->linkStr();
0144         if (!str.isEmpty()) {
0145             richTextEditor()->textCursor().insertText(str);
0146         }
0147     }
0148     delete dlg;
0149 }
0150 
0151 void MarkdownInterface::addImage()
0152 {
0153     QPointer<MarkdownCreateImageDialog> dlg = new MarkdownCreateImageDialog(parentWidget());
0154     if (dlg->exec()) {
0155         const QString str = dlg->linkStr();
0156         if (!str.isEmpty()) {
0157             richTextEditor()->textCursor().insertText(str);
0158         }
0159     }
0160     delete dlg;
0161 }
0162 
0163 void MarkdownInterface::addTitle(int index)
0164 {
0165     QString tag = QStringLiteral("#");
0166     for (int i = 1; i < index; ++i) {
0167         tag += QLatin1Char('#');
0168     }
0169     const QString selectedText = richTextEditor()->textCursor().selectedText();
0170     if (!selectedText.trimmed().isEmpty()) {
0171         richTextEditor()->textCursor().insertText(QStringLiteral("%1 %2").arg(tag, selectedText));
0172     } else {
0173         richTextEditor()->textCursor().insertText(QStringLiteral("%1 ").arg(tag));
0174     }
0175 }
0176 
0177 bool MarkdownInterface::reformatText()
0178 {
0179     return false;
0180 }
0181 
0182 void MarkdownInterface::addEmbeddedImages(MessageComposer::TextPart *textPart, QString &textVersion, QString &htmlVersion) const
0183 {
0184     QStringList listImage = MarkdownUtil::imagePaths(textVersion);
0185     QList<QSharedPointer<KPIMTextEdit::EmbeddedImage>> lstEmbeddedImages;
0186     if (!listImage.isEmpty()) {
0187         listImage.removeDuplicates();
0188         QStringList imageNameAdded;
0189         for (const QString &urlImage : std::as_const(listImage)) {
0190             const QUrl url = QUrl::fromUserInput(urlImage);
0191             if (!url.isLocalFile()) {
0192                 qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Url is not a local file " << url;
0193                 continue;
0194             }
0195             QImage image;
0196             if (!image.load(urlImage)) {
0197                 qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Impossible to load " << urlImage;
0198                 continue;
0199             }
0200             const QFileInfo fi(urlImage);
0201             const QString imageName = fi.baseName().isEmpty() ? QStringLiteral("image.png") : QString(fi.baseName() + QLatin1StringView(".png"));
0202 
0203             QString imageNameToAdd = imageName;
0204             int imageNumber = 1;
0205             while (imageNameAdded.contains(imageNameToAdd)) {
0206                 const int firstDot = imageName.indexOf(QLatin1Char('.'));
0207                 if (firstDot == -1) {
0208                     imageNameToAdd = imageName + QString::number(imageNumber++);
0209                 } else {
0210                     imageNameToAdd = imageName.left(firstDot) + QString::number(imageNumber++) + imageName.mid(firstDot);
0211                 }
0212             }
0213 
0214             QSharedPointer<KPIMTextEdit::EmbeddedImage> embeddedImage =
0215                 richTextEditor()->composerControler()->composerImages()->createEmbeddedImage(image, imageNameToAdd);
0216             lstEmbeddedImages.append(embeddedImage);
0217 
0218             const QString newImageName = QLatin1StringView("cid:") + embeddedImage->contentID;
0219             const QString quote(QStringLiteral("\""));
0220             htmlVersion.replace(QString(quote + urlImage + quote), QString(quote + newImageName + quote));
0221             textVersion.replace(urlImage, newImageName);
0222             imageNameAdded << imageNameToAdd;
0223         }
0224         if (!lstEmbeddedImages.isEmpty()) {
0225             textPart->setEmbeddedImages(lstEmbeddedImages);
0226         }
0227     }
0228 }
0229 
0230 MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus MarkdownInterface::convertTextToFormat(MessageComposer::TextPart *textPart)
0231 {
0232     // It can't work on html email
0233     if (richTextEditor()->composerControler()->isFormattingUsed()) {
0234         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "We can't convert html email";
0235         return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
0236     }
0237     if (mAction->isChecked()) {
0238         QString textVersion = richTextEditor()->composerControler()->toCleanPlainText();
0239         if (!textVersion.isEmpty()) {
0240             MarkdownConverter converter;
0241             converter.setEnableEmbeddedLabel(mEnableEmbeddedLabel);
0242             converter.setEnableExtraDefinitionLists(mEnableExtraDefinitionLists);
0243             QString result = converter.convertTextToMarkdown(textVersion);
0244             if (!result.isEmpty()) {
0245                 addEmbeddedImages(textPart, textVersion, result);
0246                 textPart->setCleanPlainText(textVersion);
0247 
0248                 textPart->setWrappedPlainText(textVersion);
0249                 textPart->setCleanHtml(result);
0250                 return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted;
0251             } else {
0252                 qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Impossible to convert text";
0253                 return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Error;
0254             }
0255         } else {
0256             qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "empty text! Bug ?";
0257             return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
0258         }
0259     }
0260     return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
0261 }
0262 
0263 void MarkdownInterface::enableDisablePluginActions(bool richText)
0264 {
0265     if (mAction) {
0266         mAction->setEnabled(!richText);
0267         mPopupMenuAction->setEnabled(!richText && mAction->isChecked());
0268     }
0269 }
0270 
0271 void MarkdownInterface::reloadConfig()
0272 {
0273     KConfigGroup grp(KSharedConfig::openConfig(), QStringLiteral("Markdown"));
0274 
0275     mEnableEmbeddedLabel = grp.readEntry("Enable Embedded Latex", false);
0276     mEnableExtraDefinitionLists = grp.readEntry("Enable Extra Definition Lists", false);
0277 }
0278 
0279 void MarkdownInterface::slotActivated(bool checked)
0280 {
0281     if (mDialog.isNull()) {
0282         mDialog = new MarkdownPreviewDialog(parentWidget());
0283         mDialog->setText(richTextEditor()->toPlainText());
0284         connect(richTextEditor(), &TextCustomEditor::RichTextEditor::textChanged, this, [this]() {
0285             if (mDialog) {
0286                 mDialog->setText(richTextEditor()->toPlainText());
0287             }
0288         });
0289     }
0290     mStatusBarLabel->setToggleMode(checked);
0291     if (checked) {
0292         mDialog->show();
0293     } else {
0294         mDialog->hide();
0295     }
0296     mPopupMenuAction->setEnabled(checked);
0297 }
0298 
0299 #include "moc_markdowninterface.cpp"