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"