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