1 /*
2     SPDX-License-Identifier: GPL-2.0-or-later
3     SPDX-FileCopyrightText: 2018 Yifei Wu <kqwyfg@gmail.com>
4     SPDX-FileCopyrightText: 2019-2021 Alexander Semke <alexander.semke@web.de>
5 */
6 
7 #include "markdownentry.h"
8 #include "jupyterutils.h"
9 #include "mathrender.h"
10 #include <config-cantor.h>
11 #include "settings.h"
12 #include "worksheetview.h"
13 
14 #include <QJsonArray>
15 #include <QJsonObject>
16 #include <QJsonValue>
17 #include <QImage>
18 #include <QImageReader>
19 #include <QBuffer>
20 #include <QDebug>
21 #include <QKeyEvent>
22 #include <QRegularExpression>
23 #include <QStandardPaths>
24 #include <QDir>
25 #include <QFileDialog>
26 #include <QClipboard>
27 #include <QMimeData>
28 #include <QGraphicsSceneDragDropEvent>
29 
30 #include <KLocalizedString>
31 #include <KZip>
32 #include <KMessageBox>
33 
34 #ifdef Discount_FOUND
35 extern "C" {
36 #include <mkdio.h>
37 }
38 #endif
39 
40 
MarkdownEntry(Worksheet * worksheet)41 MarkdownEntry::MarkdownEntry(Worksheet* worksheet) : WorksheetEntry(worksheet),
42 m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)),
43 rendered(false)
44 {
45     m_textItem->enableRichText(false);
46     m_textItem->setOpenExternalLinks(true);
47     m_textItem->installEventFilter(this);
48     m_textItem->setAcceptDrops(true);
49     connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &MarkdownEntry::moveToPreviousEntry);
50     connect(m_textItem, &WorksheetTextItem::moveToNext, this, &MarkdownEntry::moveToNextEntry);
51     connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
52 }
53 
populateMenu(QMenu * menu,QPointF pos)54 void MarkdownEntry::populateMenu(QMenu* menu, QPointF pos)
55 {
56     WorksheetEntry::populateMenu(menu, pos);
57 
58     QAction* firstAction;
59     if (!rendered)
60     {
61         firstAction = menu->actions().at(1); //insert the first action for Markdown after the "Evaluate" action
62         QAction* action = new QAction(QIcon::fromTheme(QLatin1String("viewimage")), i18n("Insert Image"));
63         connect(action, &QAction::triggered, this, &MarkdownEntry::insertImage);
64         menu->insertAction(firstAction, action);
65     }
66     else
67     {
68         firstAction = menu->actions().at(0);
69         QAction* action = new QAction(QIcon::fromTheme(QLatin1String("edit-entry")), i18n("Enter Edit Mode"));
70         connect(action, &QAction::triggered, this, &MarkdownEntry::enterEditMode);
71         menu->insertAction(firstAction, action);
72         menu->insertSeparator(firstAction);
73     }
74 
75     if (attachedImages.size() != 0)
76     {
77         QAction* action = new QAction(QIcon::fromTheme(QLatin1String("edit-clear")), i18n("Clear Attachments"));
78         connect(action, &QAction::triggered, this, &MarkdownEntry::clearAttachments);
79         menu->insertAction(firstAction, action);
80     }
81 }
82 
isEmpty()83 bool MarkdownEntry::isEmpty()
84 {
85     return m_textItem->document()->isEmpty();
86 }
87 
type() const88 int MarkdownEntry::type() const
89 {
90     return Type;
91 }
92 
acceptRichText()93 bool MarkdownEntry::acceptRichText()
94 {
95     return false;
96 }
97 
focusEntry(int pos,qreal xCoord)98 bool MarkdownEntry::focusEntry(int pos, qreal xCoord)
99 {
100     if (aboutToBeRemoved())
101         return false;
102     m_textItem->setFocusAt(pos, xCoord);
103     return true;
104 }
105 
setContent(const QString & content)106 void MarkdownEntry::setContent(const QString& content)
107 {
108     rendered = false;
109     plain = content;
110     setPlainText(plain);
111 }
112 
setContent(const QDomElement & content,const KZip & file)113 void MarkdownEntry::setContent(const QDomElement& content, const KZip& file)
114 {
115     rendered = content.attribute(QLatin1String("rendered"), QLatin1String("1")) == QLatin1String("1");
116     QDomElement htmlEl = content.firstChildElement(QLatin1String("HTML"));
117     if(!htmlEl.isNull())
118         html = htmlEl.text();
119     else
120     {
121         html = QLatin1String("");
122         rendered = false; // No html provided. Assume that it hasn't been rendered.
123     }
124     QDomElement plainEl = content.firstChildElement(QLatin1String("Plain"));
125     if(!plainEl.isNull())
126         plain = plainEl.text();
127     else
128     {
129         plain = QLatin1String("");
130         html = QLatin1String(""); // No plain text provided. The entry shouldn't render anything, or the user can't re-edit it.
131     }
132 
133     const QDomNodeList& attachments = content.elementsByTagName(QLatin1String("Attachment"));
134     for (int x = 0; x < attachments.count(); x++)
135     {
136         const QDomElement& attachment = attachments.at(x).toElement();
137         QUrl url(attachment.attribute(QLatin1String("url")));
138 
139         const QString& base64 = attachment.text();
140         QImage image;
141         image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), "PNG");
142 
143         attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
144 
145         m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image));
146     }
147 
148     if(rendered)
149         setRenderedHtml(html);
150     else
151         setPlainText(plain);
152 
153     // Handle math after setting html
154     const QDomNodeList& maths = content.elementsByTagName(QLatin1String("EmbeddedMath"));
155     foundMath.clear();
156     for (int i = 0; i < maths.count(); i++)
157     {
158         const QDomElement& math = maths.at(i).toElement();
159         const QString mathCode = math.text();
160 
161         foundMath.push_back(std::make_pair(mathCode, false));
162     }
163 
164     if (rendered)
165     {
166         markUpMath();
167 
168         for (int i = 0; i < maths.count(); i++)
169         {
170             const QDomElement& math = maths.at(i).toElement();
171             bool mathRendered = math.attribute(QLatin1String("rendered")).toInt();
172             const QString mathCode = math.text();
173 
174             if (mathRendered)
175             {
176                 const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path")));
177                 if (imageEntry && imageEntry->isFile())
178                 {
179                     const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
180                     const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
181                     imageFile->copyTo(dir);
182                     const QString& pdfPath = dir + QDir::separator() + imageFile->name();
183 
184                     QString latex;
185                     Cantor::LatexRenderer::EquationType type;
186                     std::tie(latex, type) = parseMathCode(mathCode);
187 
188                     // Get uuid by removing 'cantor_' and '.pdf' extension
189                     // len('cantor_') == 7, len('.pdf') == 4
190                     QString uuid = pdfPath;
191                     uuid.remove(0, 7);
192                     uuid.chop(4);
193 
194                     bool success;
195                     const auto& data = worksheet()->mathRenderer()->renderExpressionFromPdf(pdfPath, uuid, latex, type, &success);
196                     if (success)
197                     {
198                         QUrl internal;
199                         internal.setScheme(QLatin1String("internal"));
200                         internal.setPath(uuid);
201                         setRenderedMath(i+1, data.first, internal, data.second);
202                     }
203                 }
204                 else if (worksheet()->embeddedMathEnabled())
205                     renderMathExpression(i+1, mathCode);
206             }
207         }
208     }
209 
210     // Because, all previous actions was on load stage,
211     // them shoudl unconverted by user
212     m_textItem->document()->clearUndoRedoStacks();
213 }
214 
setContentFromJupyter(const QJsonObject & cell)215 void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell)
216 {
217     if (!Cantor::JupyterUtils::isMarkdownCell(cell))
218         return;
219 
220     // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata
221     // There isn't Jupyter metadata for markdown cells, which could be handled by Cantor
222     // So we just store it
223     setJupyterMetadata(Cantor::JupyterUtils::getMetadata(cell));
224 
225     const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject();
226     for (const QString& key : attachments.keys())
227     {
228         const QJsonValue& attachment = attachments.value(key);
229         const QString& mimeKey = Cantor::JupyterUtils::firstImageKey(attachment);
230         if (!mimeKey.isEmpty())
231         {
232             const QImage& image = Cantor::JupyterUtils::loadImage(attachment, mimeKey);
233 
234             QUrl resourceUrl;
235             resourceUrl.setUrl(QLatin1String("attachment:")+key);
236             attachedImages.push_back(std::make_pair(resourceUrl, mimeKey));
237             m_textItem->document()->addResource(QTextDocument::ImageResource, resourceUrl, QVariant(image));
238         }
239     }
240 
241     setPlainText(Cantor::JupyterUtils::getSource(cell));
242     m_textItem->document()->clearUndoRedoStacks();
243 }
244 
toXml(QDomDocument & doc,KZip * archive)245 QDomElement MarkdownEntry::toXml(QDomDocument& doc, KZip* archive)
246 {
247     if(!rendered)
248         plain = m_textItem->toPlainText();
249 
250     QDomElement el = doc.createElement(QLatin1String("Markdown"));
251     el.setAttribute(QLatin1String("rendered"), (int)rendered);
252 
253     QDomElement plainEl = doc.createElement(QLatin1String("Plain"));
254     plainEl.appendChild(doc.createTextNode(plain));
255     el.appendChild(plainEl);
256 
257     QDomElement htmlEl = doc.createElement(QLatin1String("HTML"));
258     htmlEl.appendChild(doc.createTextNode(html));
259     el.appendChild(htmlEl);
260 
261     QUrl url;
262     QString key;
263     for (const auto& data : attachedImages)
264     {
265         std::tie(url, key) = std::move(data);
266 
267         QDomElement attachmentEl = doc.createElement(QLatin1String("Attachment"));
268         attachmentEl.setAttribute(QStringLiteral("url"), url.toString());
269 
270         const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value<QImage>();
271 
272         QByteArray ba;
273         QBuffer buffer(&ba);
274         buffer.open(QIODevice::WriteOnly);
275         image.save(&buffer, "PNG");
276 
277         attachmentEl.appendChild(doc.createTextNode(QString::fromLatin1(ba.toBase64())));
278 
279         el.appendChild(attachmentEl);
280     }
281 
282     // If math rendered, then append result .pdf to archive
283     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
284     for (const auto& data : foundMath)
285     {
286         QDomElement mathEl = doc.createElement(QLatin1String("EmbeddedMath"));
287         mathEl.setAttribute(QStringLiteral("rendered"), data.second);
288         mathEl.appendChild(doc.createTextNode(data.first));
289 
290         if (data.second)
291         {
292             bool foundNeededImage = false;
293             while(!cursor.isNull() && !foundNeededImage)
294             {
295                 QTextImageFormat format=cursor.charFormat().toImageFormat();
296                 if (format.hasProperty(Cantor::Renderer::CantorFormula))
297                 {
298                     const QString& latex = format.property(Cantor::Renderer::Code).toString();
299                     const QString& delimiter = format.property(Cantor::Renderer::Delimiter).toString();
300                     const QString& code = delimiter + latex + delimiter;
301                     if (code == data.first)
302                     {
303                         const QUrl& url = QUrl::fromLocalFile(format.property(Cantor::Renderer::ImagePath).toString());
304                         archive->addLocalFile(url.toLocalFile(), url.fileName());
305                         mathEl.setAttribute(QStringLiteral("path"), url.fileName());
306                         foundNeededImage = true;
307                     }
308                 }
309                 cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
310             }
311         }
312 
313         el.appendChild(mathEl);
314     }
315 
316     return el;
317 }
318 
toJupyterJson()319 QJsonValue MarkdownEntry::toJupyterJson()
320 {
321     QJsonObject entry;
322 
323     entry.insert(QLatin1String("cell_type"), QLatin1String("markdown"));
324 
325     entry.insert(QLatin1String("metadata"), jupyterMetadata());
326 
327     QJsonObject attachments;
328     QUrl url;
329     QString key;
330     for (const auto& data : attachedImages)
331     {
332         std::tie(url, key) = std::move(data);
333 
334         const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value<QImage>();
335         QString attachmentKey = url.toString().remove(QLatin1String("attachment:"));
336         attachments.insert(attachmentKey, Cantor::JupyterUtils::packMimeBundle(image, key));
337     }
338     if (!attachments.isEmpty())
339         entry.insert(QLatin1String("attachments"), attachments);
340 
341     Cantor::JupyterUtils::setSource(entry, plain);
342 
343     return entry;
344 }
345 
toPlain(const QString & commandSep,const QString & commentStartingSeq,const QString & commentEndingSeq)346 QString MarkdownEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
347 {
348     Q_UNUSED(commandSep);
349 
350     if (commentStartingSeq.isEmpty())
351         return QString();
352 
353     QString text(plain);
354 
355     if (!commentEndingSeq.isEmpty())
356         return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
357     return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
358 }
359 
evaluate(EvaluationOption evalOp)360 bool MarkdownEntry::evaluate(EvaluationOption evalOp)
361 {
362     if(!rendered)
363     {
364         if (m_textItem->toPlainText() == plain && !html.isEmpty())
365         {
366             setRenderedHtml(html);
367             rendered = true;
368             for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++)
369                 iter->second = false;
370             markUpMath();
371         }
372         else
373         {
374             plain = m_textItem->toPlainText();
375             rendered = renderMarkdown(plain);
376         }
377         m_textItem->document()->clearUndoRedoStacks();
378     }
379 
380     if (rendered && worksheet()->embeddedMathEnabled())
381         renderMath();
382 
383     evaluateNext(evalOp);
384     return true;
385 }
386 
renderMarkdown(QString & plain)387 bool MarkdownEntry::renderMarkdown(QString& plain)
388 {
389 #ifdef Discount_FOUND
390     QByteArray mdCharArray = plain.toUtf8();
391     MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size()+1, 0);
392     if(!mkd_compile(mdHandle, MKD_LATEX | MKD_FENCEDCODE | MKD_GITHUBTAGS))
393     {
394         qDebug()<<"Failed to compile the markdown document";
395         mkd_cleanup(mdHandle);
396         return false;
397     }
398     char *htmlDocument;
399     int htmlSize = mkd_document(mdHandle, &htmlDocument);
400     html = QString::fromUtf8(htmlDocument, htmlSize);
401 
402     char *latexData;
403     int latexDataSize = mkd_latextext(mdHandle, &latexData);
404     QStringList latexUnits = QString::fromUtf8(latexData, latexDataSize).split(QLatin1Char(31), QString::SkipEmptyParts);
405     foundMath.clear();
406 
407     mkd_cleanup(mdHandle);
408 
409     setRenderedHtml(html);
410 
411     QTextCursor cursor(m_textItem->document());
412     for (const QString& latex : latexUnits)
413         foundMath.push_back(std::make_pair(latex, false));
414 
415     markUpMath();
416 
417     return true;
418 #else
419     Q_UNUSED(plain);
420 
421     return false;
422 #endif
423 }
424 
updateEntry()425 void MarkdownEntry::updateEntry()
426 {
427     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
428     while(!cursor.isNull())
429     {
430         QTextImageFormat format=cursor.charFormat().toImageFormat();
431         if (format.hasProperty(Cantor::Renderer::CantorFormula))
432             worksheet()->mathRenderer()->rerender(m_textItem->document(), format);
433 
434         cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
435     }
436 }
437 
search(const QString & pattern,unsigned flags,QTextDocument::FindFlags qt_flags,const WorksheetCursor & pos)438 WorksheetCursor MarkdownEntry::search(const QString& pattern, unsigned flags,
439                                   QTextDocument::FindFlags qt_flags,
440                                   const WorksheetCursor& pos)
441 {
442     if (!(flags & WorksheetEntry::SearchText) ||
443         (pos.isValid() && pos.entry() != this))
444         return WorksheetCursor();
445 
446     QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
447     if (textCursor.isNull())
448         return WorksheetCursor();
449     else
450         return WorksheetCursor(this, m_textItem, textCursor);
451 }
452 
layOutForWidth(qreal entry_zone_x,qreal w,bool force)453 void MarkdownEntry::layOutForWidth(qreal entry_zone_x, qreal w, bool force)
454 {
455     if (size().width() == w && m_textItem->pos().x() == entry_zone_x && !force)
456         return;
457 
458     const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
459 
460     m_textItem->setGeometry(entry_zone_x, 0, w - margin - entry_zone_x);
461     setSize(QSizeF(m_textItem->width() + margin + entry_zone_x, m_textItem->height() + VerticalMargin));
462 }
463 
eventFilter(QObject * object,QEvent * event)464 bool MarkdownEntry::eventFilter(QObject* object, QEvent* event)
465 {
466     if(object == m_textItem)
467     {
468         if(event->type() == QEvent::GraphicsSceneMouseDoubleClick)
469         {
470             QGraphicsSceneMouseEvent* mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
471             if(!mouseEvent) return false;
472             if(mouseEvent->button() == Qt::LeftButton)
473             {
474                 if (rendered)
475                 {
476                     setPlainText(plain);
477                     m_textItem->setCursorPosition(mouseEvent->pos());
478                     m_textItem->textCursor().clearSelection();
479                     rendered = false;
480                     return true;
481                 }
482             }
483         }
484         else if (event->type() == QEvent::KeyPress)
485         {
486             auto* key_event = static_cast<QKeyEvent*>(event);
487             if (key_event->matches(QKeySequence::Cancel))
488             {
489                 setRenderedHtml(html);
490                 for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++)
491                     iter->second = false;
492                 rendered = true;
493                 markUpMath();
494                 if (worksheet()->embeddedMathEnabled())
495                     renderMath();
496 
497                 return true;
498             }
499             if (key_event->matches(QKeySequence::Paste))
500             {
501                 QClipboard *clipboard = QGuiApplication::clipboard();
502                 const QImage& clipboardImage = clipboard->image();
503                 if (!clipboardImage.isNull())
504                 {
505                     int idx = 0;
506                     static const QString clipboardImageNamePrefix = QLatin1String("clipboard_image_");
507                     for (auto& data : attachedImages)
508                     {
509                         const QString& name = data.first.path();
510                         if (name.startsWith(clipboardImageNamePrefix))
511                         {
512                             bool isIntParsed = false;
513                             int parsedIndex = name.right(name.size() - clipboardImageNamePrefix.size()).toInt(&isIntParsed);
514                             if (isIntParsed)
515                                 idx = std::max(idx, parsedIndex);
516                         }
517                     }
518                     idx++;
519                     const QString& name = clipboardImageNamePrefix+QString::number(idx);
520 
521                     addImageAttachment(name, clipboardImage);
522                     return true;
523                 }
524             }
525         }
526         else if (event->type() == QEvent::GraphicsSceneDrop)
527         {
528             auto* dragEvent = static_cast<QGraphicsSceneDragDropEvent*>(event);
529             const QMimeData* mimeData = dragEvent->mimeData();
530             if (mimeData->hasUrls())
531             {
532                 QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
533 
534                 for (const QUrl url : mimeData->urls())
535                 {
536                     const QString filename = url.toLocalFile();
537                     QFileInfo info(filename);
538                     if (supportedFormats.contains(info.completeSuffix().toUtf8()))
539                     {
540                         QImage image(filename);
541                         addImageAttachment(info.fileName(), image);
542                         m_textItem->textCursor().insertText(QLatin1String("\n"));
543                     }
544                 }
545                 return true;
546             }
547         }
548     }
549     return false;
550 }
551 
wantToEvaluate()552 bool MarkdownEntry::wantToEvaluate()
553 {
554     return !rendered;
555 }
556 
setRenderedHtml(const QString & html)557 void MarkdownEntry::setRenderedHtml(const QString& html)
558 {
559     m_textItem->setHtml(html);
560     m_textItem->denyEditing();
561 }
562 
setPlainText(const QString & plain)563 void MarkdownEntry::setPlainText(const QString& plain)
564 {
565     QTextDocument* doc = m_textItem->document();
566     doc->setPlainText(plain);
567     m_textItem->setDocument(doc);
568     m_textItem->allowEditing();
569 }
570 
renderMath()571 void MarkdownEntry::renderMath()
572 {
573     QTextCursor cursor(m_textItem->document());
574     for (int i = 0; i < (int)foundMath.size(); i++)
575         if (foundMath[i].second == false)
576             renderMathExpression(i+1, foundMath[i].first);
577 }
578 
handleMathRender(QSharedPointer<MathRenderResult> result)579 void MarkdownEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
580 {
581     if (!result->successful)
582     {
583         if (Settings::self()->showMathRenderError())
584         {
585             QApplication::restoreOverrideCursor();
586             KMessageBox::error(worksheetView(), result->errorMessage, i18n("Cantor Math Error"));
587         }
588         else
589             qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage;
590         return;
591     }
592 
593     setRenderedMath(result->jobId, result->renderedMath, result->uniqueUrl, result->image);
594 }
595 
renderMathExpression(int jobId,QString mathCode)596 void MarkdownEntry::renderMathExpression(int jobId, QString mathCode)
597 {
598     QString latex;
599     Cantor::LatexRenderer::EquationType type;
600     std::tie(latex, type) = parseMathCode(mathCode);
601     if (!latex.isNull())
602         worksheet()->mathRenderer()->renderExpression(jobId, latex, type, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
603 }
604 
parseMathCode(QString mathCode)605 std::pair<QString, Cantor::LatexRenderer::EquationType> MarkdownEntry::parseMathCode(QString mathCode)
606 {
607     static const QLatin1String inlineDelimiter("$");
608     static const QLatin1String displayedDelimiter("$$");
609 
610     if (mathCode.startsWith(displayedDelimiter) && mathCode.endsWith(displayedDelimiter))
611     {
612         mathCode.remove(0, 2);
613         mathCode.chop(2);
614 
615         if (mathCode[0] == QChar(6))
616             mathCode.remove(0, 1);
617 
618         return std::make_pair(mathCode, Cantor::LatexRenderer::FullEquation);
619     }
620     else if (mathCode.startsWith(inlineDelimiter) && mathCode.endsWith(inlineDelimiter))
621     {
622         mathCode.remove(0, 1);
623         mathCode.chop(1);
624 
625         if (mathCode[0] == QChar(6))
626             mathCode.remove(0, 1);
627 
628         return std::make_pair(mathCode, Cantor::LatexRenderer::InlineEquation);
629     }
630     else if (mathCode.startsWith(QString::fromUtf8("\\begin{")) && mathCode.endsWith(QLatin1Char('}')))
631     {
632         if (mathCode[1] == QChar(6))
633             mathCode.remove(1, 1);
634 
635         return std::make_pair(mathCode, Cantor::LatexRenderer::CustomEquation);
636     }
637     else
638         return std::make_pair(QString(), Cantor::LatexRenderer::InlineEquation);
639 }
640 
setRenderedMath(int jobId,const QTextImageFormat & format,const QUrl & internal,const QImage & image)641 void MarkdownEntry::setRenderedMath(int jobId, const QTextImageFormat& format, const QUrl& internal, const QImage& image)
642 {
643     if ((int)foundMath.size() < jobId)
644         return;
645 
646     const auto& iter = foundMath.begin() + jobId-1;
647 
648     QTextCursor cursor = findMath(jobId);
649 
650     const QString delimiter = format.property(Cantor::Renderer::Delimiter).toString();
651     QString searchText = delimiter + format.property(Cantor::Renderer::Code).toString() + delimiter;
652 
653     Cantor::LatexRenderer::EquationType type
654         = (Cantor::LatexRenderer::EquationType)format.intProperty(Cantor::Renderer::CantorFormula);
655 
656     // From findMath we will be first symbol of math expression
657     // So in order to select all symbols of the expression, we need to go to previous symbol first
658     // But it working strange sometimes: some times we need to go to previous character, sometimes not
659     // So the code tests that we on '$' symbol and if it isn't true, then we revert back
660     cursor.movePosition(QTextCursor::PreviousCharacter);
661     bool withDollarDelimiter = type == Cantor::LatexRenderer::InlineEquation || type == Cantor::LatexRenderer::FullEquation;
662     if (withDollarDelimiter && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('$'))
663         cursor.movePosition(QTextCursor::NextCharacter);
664     else if (type == Cantor::LatexRenderer::CustomEquation && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('\\') )
665         cursor.movePosition(QTextCursor::NextCharacter);
666 
667     cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, searchText.size());
668 
669     if (!cursor.isNull())
670     {
671         m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
672 
673         // Don't add new line for $$...$$ on document's begin and end
674         // And if we in block, which haven't non-space characters except out math expression
675         // In another sitation, Cantor will move rendered image into another QTextBlock
676         QTextCursor prevSymCursor = m_textItem->document()->find(QRegularExpression(QStringLiteral("[^\\s]")),
677                                                                  cursor, QTextDocument::FindBackward);
678         if (type == Cantor::LatexRenderer::FullEquation
679             && cursor.selectionStart() != 0
680             && prevSymCursor.block() == cursor.block()
681         )
682         {
683             cursor.insertBlock();
684 
685             cursor.setPosition(prevSymCursor.position()+2, QTextCursor::KeepAnchor);
686             cursor.removeSelectedText();
687         }
688 
689         cursor.insertText(QString(QChar::ObjectReplacementCharacter), format);
690 
691         bool atDocEnd = cursor.position() == m_textItem->document()->characterCount()-1;
692         QTextCursor nextSymCursor = m_textItem->document()->find(QRegularExpression(QStringLiteral("[^\\s]")), cursor);
693         if (type == Cantor::LatexRenderer::FullEquation && !atDocEnd && nextSymCursor.block() == cursor.block())
694         {
695             cursor.setPosition(nextSymCursor.position()-1, QTextCursor::KeepAnchor);
696             cursor.removeSelectedText();
697             cursor.insertBlock();
698         }
699 
700         // Set that the formulas is rendered
701         iter->second = true;
702 
703         m_textItem->document()->clearUndoRedoStacks();
704     }
705 }
706 
findMath(int id)707 QTextCursor MarkdownEntry::findMath(int id)
708 {
709     QTextCursor cursor(m_textItem->document());
710     do
711     {
712         QTextCharFormat format = cursor.charFormat();
713         if (format.intProperty(JobProperty) == id)
714             break;
715     }
716     while (cursor.movePosition(QTextCursor::NextCharacter));
717 
718     return cursor;
719 }
720 
markUpMath()721 void MarkdownEntry::markUpMath()
722 {
723     QTextCursor cursor(m_textItem->document());
724     for (int i = 0; i < (int)foundMath.size(); i++)
725     {
726         if (foundMath[i].second)
727             continue;
728 
729         QString searchText = foundMath[i].first;
730         searchText.replace(QRegularExpression(QStringLiteral("\\s+")), QStringLiteral(" "));
731 
732         cursor = m_textItem->document()->find(searchText, cursor);
733 
734         // Mark up founded math code
735         QTextCharFormat format = cursor.charFormat();
736         // Use index+1 in math array as property tag
737         format.setProperty(JobProperty, i+1);
738 
739         // We found the math expression, so remove 'marker' (ACII symbol 'Acknowledgement')
740         // The marker have been placed after "$" or "$$"
741         // We remove the marker, only if it presents
742         QString codeWithoutMarker = foundMath[i].first;
743         if (searchText.startsWith(QLatin1String("$$")))
744         {
745             if (codeWithoutMarker[2] == QChar(6))
746                 codeWithoutMarker.remove(2, 1);
747         }
748         else if (searchText.startsWith(QLatin1String("$")))
749         {
750             if (codeWithoutMarker[1] == QChar(6))
751                 codeWithoutMarker.remove(1, 1);
752         }
753         else if (searchText.startsWith(QLatin1String("\\")))
754         {
755             if (codeWithoutMarker[1] == QChar(6))
756                 codeWithoutMarker.remove(1, 1);
757         }
758         cursor.insertText(codeWithoutMarker, format);
759     }
760 }
761 
insertImage()762 void MarkdownEntry::insertImage()
763 {
764     KConfigGroup conf(KSharedConfig::openConfig(), QLatin1String("MarkdownEntry"));
765     const QString& dir = conf.readEntry(QLatin1String("LastImageDir"), QString());
766 
767     QString formats;
768     for (const QByteArray& format : QImageReader::supportedImageFormats())
769         formats += QLatin1String("*.") + QLatin1String(format.constData()) + QLatin1Char(' ');
770 
771     const QString& path = QFileDialog::getOpenFileName(worksheet()->worksheetView(),
772                                                        i18n("Open image file"),
773                                                        dir,
774                                                        i18n("Images (%1)", formats));
775     if (path.isEmpty())
776         return; //cancel was clicked in the file-dialog
777 
778     //save the last used directory, if changed
779     const int pos = path.lastIndexOf(QLatin1String("/"));
780     if (pos != -1) {
781         const QString& newDir = path.left(pos);
782         if (newDir != dir)
783             conf.writeEntry(QLatin1String("LastImageDir"), newDir);
784     }
785 
786     QImageReader reader(path);
787     const QImage& img = reader.read();
788     if (!img.isNull())
789     {
790         const QString& name = QFileInfo(path).fileName();
791         addImageAttachment(name, img);
792     }
793     else
794         KMessageBox::error(worksheetView(),
795                            i18n("Failed to read the image \"%1\". Error \"%2\"", path, reader.errorString()),
796                            i18n("Cantor"));
797 }
798 
clearAttachments()799 void MarkdownEntry::clearAttachments()
800 {
801     for (auto& attachment: attachedImages)
802     {
803         const QUrl& url = attachment.first;
804         m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant());
805     }
806     attachedImages.clear();
807     animateSizeChange();
808 }
809 
enterEditMode()810 void MarkdownEntry::enterEditMode()
811 {
812     setPlainText(plain);
813     m_textItem->textCursor().clearSelection();
814     rendered = false;
815 }
816 
plainText() const817 QString MarkdownEntry::plainText() const
818 {
819     return m_textItem->toPlainText();
820 }
821 
addImageAttachment(const QString & name,const QImage & image)822 void MarkdownEntry::addImageAttachment(const QString& name, const QImage& image)
823 {
824     QUrl url;
825     url.setScheme(QLatin1String("attachment"));
826     url.setPath(name);
827 
828     attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
829     m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image));
830 
831     QTextCursor cursor = m_textItem->textCursor();
832     cursor.insertText(QString::fromLatin1("![%1](attachment:%1)").arg(name));
833 
834     animateSizeChange();
835 }
836