1 /*
2    SPDX-FileCopyrightText: 2018-2021 Laurent Montel <montel@kde.org>
3 
4    SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "markdowninterface.h"
8 #include "markdownconverter.h"
9 #include "markdowncreateimagedialog.h"
10 #include "markdowncreatelinkdialog.h"
11 #include "markdownplugin_debug.h"
12 #include "markdownpreviewdialog.h"
13 #include "markdownutil.h"
14 #include <KActionCollection>
15 #include <KConfigGroup>
16 #include <KLocalizedString>
17 #include <KPIMTextEdit/RichTextComposer>
18 #include <KPIMTextEdit/RichTextComposerControler>
19 #include <KSharedConfig>
20 #include <QAction>
21 #include <QMenu>
22 
23 #include <MessageComposer/TextPart>
24 
25 #include <MessageComposer/StatusBarLabelToggledState>
26 
MarkdownInterface(QObject * parent)27 MarkdownInterface::MarkdownInterface(QObject *parent)
28     : MessageComposer::PluginEditorConvertTextInterface(parent)
29 {
30 }
31 
~MarkdownInterface()32 MarkdownInterface::~MarkdownInterface()
33 {
34 }
35 
createAction(KActionCollection * ac)36 void MarkdownInterface::createAction(KActionCollection *ac)
37 {
38     mAction = new QAction(i18n("Generate HTML from markdown language."), this);
39     mAction->setCheckable(true);
40     mAction->setChecked(false);
41     ac->addAction(QStringLiteral("generate_markdown"), mAction);
42     connect(mAction, &QAction::triggered, this, &MarkdownInterface::slotActivated);
43     MessageComposer::PluginActionType type(mAction, MessageComposer::PluginActionType::Edit);
44     addActionType(type);
45 
46     mStatusBarLabel = new MessageComposer::StatusBarLabelToggledState(parentWidget());
47     connect(mStatusBarLabel, &MessageComposer::StatusBarLabelToggledState::toggleModeChanged, this, [this](bool checked) {
48         mAction->setChecked(checked);
49         slotActivated(checked);
50     });
51     QFont f = mStatusBarLabel->font();
52     f.setBold(true);
53     mStatusBarLabel->setFont(f);
54     setStatusBarWidget(mStatusBarLabel);
55     mStatusBarLabel->setStateString(i18n("Markdown"), QString());
56 
57     mPopupMenuAction = new QAction(i18n("Markdown Action"), this);
58 
59     auto mardownMenu = new QMenu(parentWidget());
60     mPopupMenuAction->setMenu(mardownMenu);
61     mPopupMenuAction->setEnabled(false);
62     auto titleMenu = new QMenu(i18n("Add Title"), mardownMenu);
63     mardownMenu->addMenu(titleMenu);
64     for (int i = 1; i < 5; ++i) {
65         titleMenu->addAction(i18n("Level %1", QString::number(i)), this, [this, i]() {
66             addTitle(i);
67         });
68     }
69     mardownMenu->addAction(i18n("Horizontal Rule"), this, &MarkdownInterface::addHorizontalRule);
70     mardownMenu->addSeparator();
71     mBoldAction = mardownMenu->addAction(i18n("Change Selected Text as Bold"), this, &MarkdownInterface::addBold);
72     mBoldAction->setEnabled(false);
73     mItalicAction = mardownMenu->addAction(i18n("Change Selected Text as Italic"), this, &MarkdownInterface::addItalic);
74     mItalicAction->setEnabled(false);
75     mCodeAction = mardownMenu->addAction(i18n("Change Selected Text as Code"), this, &MarkdownInterface::addCode);
76     mCodeAction->setEnabled(false);
77     mBlockQuoteAction = mardownMenu->addAction(i18n("Change Selected Text as Block Quote"), this, &MarkdownInterface::addBlockQuote);
78     mBlockQuoteAction->setEnabled(false);
79     mardownMenu->addSeparator();
80     mardownMenu->addAction(i18n("Add Link"), this, &MarkdownInterface::addLink);
81     mardownMenu->addAction(i18n("Add Image"), this, &MarkdownInterface::addImage);
82     MessageComposer::PluginActionType typePopup(mPopupMenuAction, MessageComposer::PluginActionType::PopupMenu);
83     addActionType(typePopup);
84     connect(richTextEditor(), &KPIMTextEdit::RichTextComposer::selectionChanged, this, &MarkdownInterface::slotSelectionChanged);
85 }
86 
slotSelectionChanged()87 void MarkdownInterface::slotSelectionChanged()
88 {
89     const bool enabled = richTextEditor()->textCursor().hasSelection();
90     mBoldAction->setEnabled(enabled);
91     mItalicAction->setEnabled(enabled);
92     mCodeAction->setEnabled(enabled);
93     mBlockQuoteAction->setEnabled(enabled);
94 }
95 
addHorizontalRule()96 void MarkdownInterface::addHorizontalRule()
97 {
98     richTextEditor()->insertPlainText(QStringLiteral("\n---"));
99 }
100 
addBold()101 void MarkdownInterface::addBold()
102 {
103     const QString selectedText = richTextEditor()->textCursor().selectedText();
104     if (!selectedText.isEmpty()) {
105         richTextEditor()->textCursor().insertText(QStringLiteral("**%1**").arg(selectedText));
106     } else {
107         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
108     }
109 }
110 
addBlockQuote()111 void MarkdownInterface::addBlockQuote()
112 {
113     const QString selectedText = richTextEditor()->textCursor().selectedText();
114     if (!selectedText.isEmpty()) {
115         richTextEditor()->composerControler()->addQuotes(QStringLiteral(">"));
116     } else {
117         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
118     }
119 }
120 
addCode()121 void MarkdownInterface::addCode()
122 {
123     const QString selectedText = richTextEditor()->textCursor().selectedText();
124     if (!selectedText.isEmpty()) {
125         richTextEditor()->textCursor().insertText(QStringLiteral("`%1`").arg(selectedText));
126     } else {
127         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
128     }
129 }
130 
addItalic()131 void MarkdownInterface::addItalic()
132 {
133     const QString selectedText = richTextEditor()->textCursor().selectedText();
134     if (!selectedText.isEmpty()) {
135         richTextEditor()->textCursor().insertText(QStringLiteral("_%1_").arg(selectedText));
136     } else {
137         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
138     }
139 }
140 
addLink()141 void MarkdownInterface::addLink()
142 {
143     QPointer<MarkdownCreateLinkDialog> dlg = new MarkdownCreateLinkDialog(parentWidget());
144     if (dlg->exec()) {
145         const QString str = dlg->linkStr();
146         if (!str.isEmpty()) {
147             richTextEditor()->textCursor().insertText(str);
148         }
149     }
150     delete dlg;
151 }
152 
addImage()153 void MarkdownInterface::addImage()
154 {
155     QPointer<MarkdownCreateImageDialog> dlg = new MarkdownCreateImageDialog(parentWidget());
156     if (dlg->exec()) {
157         const QString str = dlg->linkStr();
158         if (!str.isEmpty()) {
159             richTextEditor()->textCursor().insertText(str);
160         }
161     }
162     delete dlg;
163 }
164 
addTitle(int index)165 void MarkdownInterface::addTitle(int index)
166 {
167     QString tag = QStringLiteral("#");
168     for (int i = 1; i < index; ++i) {
169         tag += QLatin1Char('#');
170     }
171     const QString selectedText = richTextEditor()->textCursor().selectedText();
172     if (!selectedText.trimmed().isEmpty()) {
173         richTextEditor()->textCursor().insertText(QStringLiteral("%1 %2").arg(tag, selectedText));
174     } else {
175         richTextEditor()->textCursor().insertText(QStringLiteral("%1 ").arg(tag));
176     }
177 }
178 
reformatText()179 bool MarkdownInterface::reformatText()
180 {
181     return false;
182 }
183 
addEmbeddedImages(MessageComposer::TextPart * textPart,QString & textVersion,QString & htmlVersion) const184 void MarkdownInterface::addEmbeddedImages(MessageComposer::TextPart *textPart, QString &textVersion, QString &htmlVersion) const
185 {
186     QStringList listImage = MarkdownUtil::imagePaths(textVersion);
187     QVector<QSharedPointer<KPIMTextEdit::EmbeddedImage>> lstEmbeddedImages;
188     if (!listImage.isEmpty()) {
189         listImage.removeDuplicates();
190         QStringList imageNameAdded;
191         for (const QString &urlImage : std::as_const(listImage)) {
192             const QUrl url = QUrl::fromUserInput(urlImage);
193             if (!url.isLocalFile()) {
194                 qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Url is not a local file " << url;
195                 continue;
196             }
197             QImage image;
198             if (!image.load(urlImage)) {
199                 qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Impossible to load " << urlImage;
200                 continue;
201             }
202             const QFileInfo fi(urlImage);
203             const QString imageName = fi.baseName().isEmpty() ? QStringLiteral("image.png") : QString(fi.baseName() + QLatin1String(".png"));
204 
205             QString imageNameToAdd = imageName;
206             int imageNumber = 1;
207             while (imageNameAdded.contains(imageNameToAdd)) {
208                 const int firstDot = imageName.indexOf(QLatin1Char('.'));
209                 if (firstDot == -1) {
210                     imageNameToAdd = imageName + QString::number(imageNumber++);
211                 } else {
212                     imageNameToAdd = imageName.left(firstDot) + QString::number(imageNumber++) + imageName.mid(firstDot);
213                 }
214             }
215 
216             QSharedPointer<KPIMTextEdit::EmbeddedImage> embeddedImage =
217                 richTextEditor()->composerControler()->composerImages()->createEmbeddedImage(image, imageNameToAdd);
218             lstEmbeddedImages.append(embeddedImage);
219 
220             const QString newImageName = QLatin1String("cid:") + embeddedImage->contentID;
221             const QString quote(QStringLiteral("\""));
222             htmlVersion.replace(QString(quote + urlImage + quote), QString(quote + newImageName + quote));
223             textVersion.replace(urlImage, newImageName);
224             imageNameAdded << imageNameToAdd;
225         }
226         if (!lstEmbeddedImages.isEmpty()) {
227             textPart->setEmbeddedImages(lstEmbeddedImages);
228         }
229     }
230 }
231 
convertTextToFormat(MessageComposer::TextPart * textPart)232 MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus MarkdownInterface::convertTextToFormat(MessageComposer::TextPart *textPart)
233 {
234     // It can't work on html email
235     if (richTextEditor()->composerControler()->isFormattingUsed()) {
236         qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "We can't convert html email";
237         return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
238     }
239     if (mAction->isChecked()) {
240         QString textVersion = richTextEditor()->composerControler()->toCleanPlainText();
241         if (!textVersion.isEmpty()) {
242             MarkdownConverter converter;
243             converter.setEnableEmbeddedLabel(mEnableEmbeddedLabel);
244             converter.setEnableExtraDefinitionLists(mEnableExtraDefinitionLists);
245             QString result = converter.convertTextToMarkdown(textVersion);
246             if (!result.isEmpty()) {
247                 addEmbeddedImages(textPart, textVersion, result);
248                 textPart->setCleanPlainText(textVersion);
249 
250                 textPart->setWrappedPlainText(textVersion);
251                 textPart->setCleanHtml(result);
252                 return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted;
253             } else {
254                 qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Impossible to convert text";
255                 return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Error;
256             }
257         } else {
258             qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "empty text! Bug ?";
259             return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
260         }
261     }
262     return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
263 }
264 
enableDisablePluginActions(bool richText)265 void MarkdownInterface::enableDisablePluginActions(bool richText)
266 {
267     if (mAction) {
268         mAction->setEnabled(!richText);
269         mPopupMenuAction->setEnabled(!richText && mAction->isChecked());
270     }
271 }
272 
reloadConfig()273 void MarkdownInterface::reloadConfig()
274 {
275     KConfigGroup grp(KSharedConfig::openConfig(), "Markdown");
276 
277     mEnableEmbeddedLabel = grp.readEntry("Enable Embedded Latex", false);
278     mEnableExtraDefinitionLists = grp.readEntry("Enable Extra Definition Lists", false);
279 }
280 
slotActivated(bool checked)281 void MarkdownInterface::slotActivated(bool checked)
282 {
283     if (mDialog.isNull()) {
284         mDialog = new MarkdownPreviewDialog(parentWidget());
285         mDialog->setText(richTextEditor()->toPlainText());
286         connect(richTextEditor(), &KPIMTextEdit::RichTextEditor::textChanged, this, [this]() {
287             if (mDialog) {
288                 mDialog->setText(richTextEditor()->toPlainText());
289             }
290         });
291     }
292     mStatusBarLabel->setToggleMode(checked);
293     if (checked) {
294         mDialog->show();
295     } else {
296         mDialog->hide();
297     }
298     mPopupMenuAction->setEnabled(checked);
299 }
300