File indexing completed on 2024-04-28 11:20:59

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2018 Yifei Wu <kqwyfg@gmail.com>
0004     SPDX-FileCopyrightText: 2019-2021 Alexander Semke <alexander.semke@web.de>
0005 */
0006 
0007 #include "markdownentry.h"
0008 #include "jupyterutils.h"
0009 #include "mathrender.h"
0010 #include <config-cantor.h>
0011 #include "settings.h"
0012 #include "worksheetview.h"
0013 
0014 #include <QJsonArray>
0015 #include <QJsonObject>
0016 #include <QJsonValue>
0017 #include <QImage>
0018 #include <QImageReader>
0019 #include <QBuffer>
0020 #include <QDebug>
0021 #include <QKeyEvent>
0022 #include <QRegularExpression>
0023 #include <QStandardPaths>
0024 #include <QDir>
0025 #include <QFileDialog>
0026 #include <QClipboard>
0027 #include <QMimeData>
0028 #include <QGraphicsSceneDragDropEvent>
0029 #include <QTextBlock>
0030 
0031 #include <KLocalizedString>
0032 #include <KZip>
0033 #include <KMessageBox>
0034 
0035 #ifdef Discount_FOUND
0036 extern "C" {
0037 #include <mkdio.h>
0038 }
0039 #endif
0040 
0041 
0042 MarkdownEntry::MarkdownEntry(Worksheet* worksheet) : WorksheetEntry(worksheet),
0043 m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)),
0044 rendered(false)
0045 {
0046     m_textItem->enableRichText(false);
0047     m_textItem->setOpenExternalLinks(true);
0048     m_textItem->installEventFilter(this);
0049     m_textItem->setAcceptDrops(true);
0050     connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &MarkdownEntry::moveToPreviousEntry);
0051     connect(m_textItem, &WorksheetTextItem::moveToNext, this, &MarkdownEntry::moveToNextEntry);
0052     connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
0053 }
0054 
0055 void MarkdownEntry::populateMenu(QMenu* menu, QPointF pos)
0056 {
0057     WorksheetEntry::populateMenu(menu, pos);
0058 
0059     QAction* firstAction;
0060     if (!rendered)
0061     {
0062         firstAction = menu->actions().at(1); //insert the first action for Markdown after the "Evaluate" action
0063         QAction* action = new QAction(QIcon::fromTheme(QLatin1String("viewimage")), i18n("Insert Image"));
0064         connect(action, &QAction::triggered, this, &MarkdownEntry::insertImage);
0065         menu->insertAction(firstAction, action);
0066     }
0067     else
0068     {
0069         firstAction = menu->actions().at(0);
0070         QAction* action = new QAction(QIcon::fromTheme(QLatin1String("edit-entry")), i18n("Enter Edit Mode"));
0071         connect(action, &QAction::triggered, this, &MarkdownEntry::enterEditMode);
0072         menu->insertAction(firstAction, action);
0073         menu->insertSeparator(firstAction);
0074     }
0075 
0076     if (attachedImages.size() != 0)
0077     {
0078         QAction* action = new QAction(QIcon::fromTheme(QLatin1String("edit-clear")), i18n("Clear Attachments"));
0079         connect(action, &QAction::triggered, this, &MarkdownEntry::clearAttachments);
0080         menu->insertAction(firstAction, action);
0081     }
0082 }
0083 
0084 bool MarkdownEntry::isEmpty()
0085 {
0086     return m_textItem->document()->isEmpty();
0087 }
0088 
0089 int MarkdownEntry::type() const
0090 {
0091     return Type;
0092 }
0093 
0094 bool MarkdownEntry::acceptRichText()
0095 {
0096     return false;
0097 }
0098 
0099 bool MarkdownEntry::focusEntry(int pos, qreal xCoord)
0100 {
0101     if (aboutToBeRemoved())
0102         return false;
0103     m_textItem->setFocusAt(pos, xCoord);
0104     return true;
0105 }
0106 
0107 void MarkdownEntry::setContent(const QString& content)
0108 {
0109     rendered = false;
0110     plain = content;
0111     setPlainText(plain);
0112 }
0113 
0114 void MarkdownEntry::setContent(const QDomElement& content, const KZip& file)
0115 {
0116     rendered = content.attribute(QLatin1String("rendered"), QLatin1String("1")) == QLatin1String("1");
0117     QDomElement htmlEl = content.firstChildElement(QLatin1String("HTML"));
0118     if(!htmlEl.isNull())
0119         html = htmlEl.text();
0120     else
0121     {
0122         html = QLatin1String("");
0123         rendered = false; // No html provided. Assume that it hasn't been rendered.
0124     }
0125     QDomElement plainEl = content.firstChildElement(QLatin1String("Plain"));
0126     if(!plainEl.isNull())
0127         plain = plainEl.text();
0128     else
0129     {
0130         plain = QLatin1String("");
0131         html = QLatin1String(""); // No plain text provided. The entry shouldn't render anything, or the user can't re-edit it.
0132     }
0133 
0134     const QDomNodeList& attachments = content.elementsByTagName(QLatin1String("Attachment"));
0135     for (int x = 0; x < attachments.count(); x++)
0136     {
0137         const QDomElement& attachment = attachments.at(x).toElement();
0138         QUrl url(attachment.attribute(QLatin1String("url")));
0139 
0140         const QString& base64 = attachment.text();
0141         QImage image;
0142         image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), "PNG");
0143 
0144         attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
0145 
0146         m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image));
0147     }
0148 
0149     if(rendered)
0150         setRenderedHtml(html);
0151     else
0152         setPlainText(plain);
0153 
0154     // Handle math after setting html
0155     const QDomNodeList& maths = content.elementsByTagName(QLatin1String("EmbeddedMath"));
0156     foundMath.clear();
0157     for (int i = 0; i < maths.count(); i++)
0158     {
0159         const QDomElement& math = maths.at(i).toElement();
0160         const QString mathCode = math.text();
0161 
0162         foundMath.push_back(std::make_pair(mathCode, false));
0163     }
0164 
0165     if (rendered)
0166     {
0167         markUpMath();
0168 
0169         for (int i = 0; i < maths.count(); i++)
0170         {
0171             const QDomElement& math = maths.at(i).toElement();
0172             bool mathRendered = math.attribute(QLatin1String("rendered")).toInt();
0173             const QString mathCode = math.text();
0174 
0175             if (mathRendered)
0176             {
0177                 const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path")));
0178                 if (imageEntry && imageEntry->isFile())
0179                 {
0180                     const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
0181                     const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
0182                     imageFile->copyTo(dir);
0183                     const QString& pdfPath = dir + QDir::separator() + imageFile->name();
0184 
0185                     QString latex;
0186                     Cantor::LatexRenderer::EquationType type;
0187                     std::tie(latex, type) = parseMathCode(mathCode);
0188 
0189                     // Get uuid by removing 'cantor_' and '.pdf' extension
0190                     // len('cantor_') == 7, len('.pdf') == 4
0191                     QString uuid = pdfPath;
0192                     uuid.remove(0, 7);
0193                     uuid.chop(4);
0194 
0195                     bool success;
0196                     const auto& data = worksheet()->mathRenderer()->renderExpressionFromPdf(pdfPath, uuid, latex, type, &success);
0197                     if (success)
0198                     {
0199                         QUrl internal;
0200                         internal.setScheme(QLatin1String("internal"));
0201                         internal.setPath(uuid);
0202                         setRenderedMath(i+1, data.first, internal, data.second);
0203                     }
0204                 }
0205                 else if (worksheet()->embeddedMathEnabled())
0206                     renderMathExpression(i+1, mathCode);
0207             }
0208         }
0209     }
0210 
0211     // Because, all previous actions was on load stage,
0212     // them shoudl unconverted by user
0213     m_textItem->document()->clearUndoRedoStacks();
0214 }
0215 
0216 void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell)
0217 {
0218     if (!Cantor::JupyterUtils::isMarkdownCell(cell))
0219         return;
0220 
0221     // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata
0222     // There isn't Jupyter metadata for markdown cells, which could be handled by Cantor
0223     // So we just store it
0224     setJupyterMetadata(Cantor::JupyterUtils::getMetadata(cell));
0225 
0226     const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject();
0227     for (const QString& key : attachments.keys())
0228     {
0229         const QJsonValue& attachment = attachments.value(key);
0230         const QString& mimeKey = Cantor::JupyterUtils::firstImageKey(attachment);
0231         if (!mimeKey.isEmpty())
0232         {
0233             const QImage& image = Cantor::JupyterUtils::loadImage(attachment, mimeKey);
0234 
0235             QUrl resourceUrl;
0236             resourceUrl.setUrl(QLatin1String("attachment:")+key);
0237             attachedImages.push_back(std::make_pair(resourceUrl, mimeKey));
0238             m_textItem->document()->addResource(QTextDocument::ImageResource, resourceUrl, QVariant(image));
0239         }
0240     }
0241 
0242     setPlainText(Cantor::JupyterUtils::getSource(cell));
0243     m_textItem->document()->clearUndoRedoStacks();
0244 }
0245 
0246 QDomElement MarkdownEntry::toXml(QDomDocument& doc, KZip* archive)
0247 {
0248     if(!rendered)
0249         plain = m_textItem->toPlainText();
0250 
0251     QDomElement el = doc.createElement(QLatin1String("Markdown"));
0252     el.setAttribute(QLatin1String("rendered"), (int)rendered);
0253 
0254     QDomElement plainEl = doc.createElement(QLatin1String("Plain"));
0255     plainEl.appendChild(doc.createTextNode(plain));
0256     el.appendChild(plainEl);
0257 
0258     QDomElement htmlEl = doc.createElement(QLatin1String("HTML"));
0259     htmlEl.appendChild(doc.createTextNode(html));
0260     el.appendChild(htmlEl);
0261 
0262     QUrl url;
0263     QString key;
0264     for (const auto& data : attachedImages)
0265     {
0266         std::tie(url, key) = std::move(data);
0267 
0268         QDomElement attachmentEl = doc.createElement(QLatin1String("Attachment"));
0269         attachmentEl.setAttribute(QStringLiteral("url"), url.toString());
0270 
0271         const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value<QImage>();
0272 
0273         QByteArray ba;
0274         QBuffer buffer(&ba);
0275         buffer.open(QIODevice::WriteOnly);
0276         image.save(&buffer, "PNG");
0277 
0278         attachmentEl.appendChild(doc.createTextNode(QString::fromLatin1(ba.toBase64())));
0279 
0280         el.appendChild(attachmentEl);
0281     }
0282 
0283     // If math rendered, then append result .pdf to archive
0284     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0285     for (const auto& data : foundMath)
0286     {
0287         QDomElement mathEl = doc.createElement(QLatin1String("EmbeddedMath"));
0288         mathEl.setAttribute(QStringLiteral("rendered"), data.second);
0289         mathEl.appendChild(doc.createTextNode(data.first));
0290 
0291         if (data.second)
0292         {
0293             bool foundNeededImage = false;
0294             while(!cursor.isNull() && !foundNeededImage)
0295             {
0296                 QTextImageFormat format=cursor.charFormat().toImageFormat();
0297                 if (format.hasProperty(Cantor::Renderer::CantorFormula))
0298                 {
0299                     const QString& latex = format.property(Cantor::Renderer::Code).toString();
0300                     const QString& delimiter = format.property(Cantor::Renderer::Delimiter).toString();
0301                     const QString& code = delimiter + latex + delimiter;
0302                     if (code == data.first)
0303                     {
0304                         const QUrl& url = QUrl::fromLocalFile(format.property(Cantor::Renderer::ImagePath).toString());
0305                         archive->addLocalFile(url.toLocalFile(), url.fileName());
0306                         mathEl.setAttribute(QStringLiteral("path"), url.fileName());
0307                         foundNeededImage = true;
0308                     }
0309                 }
0310                 cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
0311             }
0312         }
0313 
0314         el.appendChild(mathEl);
0315     }
0316 
0317     return el;
0318 }
0319 
0320 QJsonValue MarkdownEntry::toJupyterJson()
0321 {
0322     QJsonObject entry;
0323 
0324     entry.insert(QLatin1String("cell_type"), QLatin1String("markdown"));
0325 
0326     entry.insert(QLatin1String("metadata"), jupyterMetadata());
0327 
0328     QJsonObject attachments;
0329     QUrl url;
0330     QString key;
0331     for (const auto& data : attachedImages)
0332     {
0333         std::tie(url, key) = std::move(data);
0334 
0335         const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value<QImage>();
0336         QString attachmentKey = url.toString().remove(QLatin1String("attachment:"));
0337         attachments.insert(attachmentKey, Cantor::JupyterUtils::packMimeBundle(image, key));
0338     }
0339     if (!attachments.isEmpty())
0340         entry.insert(QLatin1String("attachments"), attachments);
0341 
0342     Cantor::JupyterUtils::setSource(entry, plain);
0343 
0344     return entry;
0345 }
0346 
0347 QString MarkdownEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
0348 {
0349     Q_UNUSED(commandSep);
0350 
0351     if (commentStartingSeq.isEmpty())
0352         return QString();
0353 
0354     QString text(plain);
0355 
0356     if (!commentEndingSeq.isEmpty())
0357         return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
0358     return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
0359 }
0360 
0361 bool MarkdownEntry::evaluate(EvaluationOption evalOp)
0362 {
0363     if(!rendered)
0364     {
0365         if (m_textItem->toPlainText() == plain && !html.isEmpty())
0366         {
0367             setRenderedHtml(html);
0368             rendered = true;
0369             for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++)
0370                 iter->second = false;
0371             markUpMath();
0372         }
0373         else
0374         {
0375             plain = m_textItem->toPlainText();
0376             rendered = renderMarkdown(plain);
0377         }
0378         m_textItem->document()->clearUndoRedoStacks();
0379     }
0380 
0381     if (rendered && worksheet()->embeddedMathEnabled())
0382         renderMath();
0383 
0384     evaluateNext(evalOp);
0385     return true;
0386 }
0387 
0388 bool MarkdownEntry::renderMarkdown(QString& plain)
0389 {
0390 #ifdef Discount_FOUND
0391     QByteArray mdCharArray = plain.toUtf8();
0392     MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size()+1, 0);
0393     if(!mkd_compile(mdHandle, MKD_LATEX | MKD_FENCEDCODE | MKD_GITHUBTAGS))
0394     {
0395         qDebug()<<"Failed to compile the markdown document";
0396         mkd_cleanup(mdHandle);
0397         return false;
0398     }
0399     char *htmlDocument;
0400     int htmlSize = mkd_document(mdHandle, &htmlDocument);
0401     html = QString::fromUtf8(htmlDocument, htmlSize);
0402 
0403     char *latexData;
0404     int latexDataSize = mkd_latextext(mdHandle, &latexData);
0405     QStringList latexUnits = QString::fromUtf8(latexData, latexDataSize).split(QLatin1Char(31), QString::SkipEmptyParts);
0406     foundMath.clear();
0407 
0408     mkd_cleanup(mdHandle);
0409 
0410     setRenderedHtml(html);
0411 
0412     QTextCursor cursor(m_textItem->document());
0413     for (const QString& latex : latexUnits)
0414         foundMath.push_back(std::make_pair(latex, false));
0415 
0416     markUpMath();
0417 
0418     return true;
0419 #else
0420     Q_UNUSED(plain);
0421 
0422     return false;
0423 #endif
0424 }
0425 
0426 void MarkdownEntry::updateEntry()
0427 {
0428     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0429     while(!cursor.isNull())
0430     {
0431         QTextImageFormat format=cursor.charFormat().toImageFormat();
0432         if (format.hasProperty(Cantor::Renderer::CantorFormula))
0433             worksheet()->mathRenderer()->rerender(m_textItem->document(), format);
0434 
0435         cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
0436     }
0437 }
0438 
0439 WorksheetCursor MarkdownEntry::search(const QString& pattern, unsigned flags,
0440                                   QTextDocument::FindFlags qt_flags,
0441                                   const WorksheetCursor& pos)
0442 {
0443     if (!(flags & WorksheetEntry::SearchText) ||
0444         (pos.isValid() && pos.entry() != this))
0445         return WorksheetCursor();
0446 
0447     QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
0448     if (textCursor.isNull())
0449         return WorksheetCursor();
0450     else
0451         return WorksheetCursor(this, m_textItem, textCursor);
0452 }
0453 
0454 void MarkdownEntry::layOutForWidth(qreal entry_zone_x, qreal w, bool force)
0455 {
0456     if (size().width() == w && m_textItem->pos().x() == entry_zone_x && !force)
0457         return;
0458 
0459     const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
0460 
0461     m_textItem->setGeometry(entry_zone_x, 0, w - margin - entry_zone_x);
0462     setSize(QSizeF(m_textItem->width() + margin + entry_zone_x, m_textItem->height() + VerticalMargin));
0463 }
0464 
0465 bool MarkdownEntry::eventFilter(QObject* object, QEvent* event)
0466 {
0467     if(object == m_textItem)
0468     {
0469         if(event->type() == QEvent::GraphicsSceneMouseDoubleClick)
0470         {
0471             QGraphicsSceneMouseEvent* mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
0472             if(!mouseEvent) return false;
0473             if(mouseEvent->button() == Qt::LeftButton)
0474             {
0475                 if (rendered)
0476                 {
0477                     setPlainText(plain);
0478                     m_textItem->setCursorPosition(mouseEvent->pos());
0479                     m_textItem->textCursor().clearSelection();
0480                     rendered = false;
0481                     return true;
0482                 }
0483             }
0484         }
0485         else if (event->type() == QEvent::KeyPress)
0486         {
0487             auto* key_event = static_cast<QKeyEvent*>(event);
0488             if (key_event->matches(QKeySequence::Cancel))
0489             {
0490                 setRenderedHtml(html);
0491                 for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++)
0492                     iter->second = false;
0493                 rendered = true;
0494                 markUpMath();
0495                 if (worksheet()->embeddedMathEnabled())
0496                     renderMath();
0497 
0498                 return true;
0499             }
0500             if (key_event->matches(QKeySequence::Paste))
0501             {
0502                 QClipboard *clipboard = QGuiApplication::clipboard();
0503                 const QImage& clipboardImage = clipboard->image();
0504                 if (!clipboardImage.isNull())
0505                 {
0506                     int idx = 0;
0507                     static const QString clipboardImageNamePrefix = QLatin1String("clipboard_image_");
0508                     for (auto& data : attachedImages)
0509                     {
0510                         const QString& name = data.first.path();
0511                         if (name.startsWith(clipboardImageNamePrefix))
0512                         {
0513                             bool isIntParsed = false;
0514                             int parsedIndex = name.right(name.size() - clipboardImageNamePrefix.size()).toInt(&isIntParsed);
0515                             if (isIntParsed)
0516                                 idx = std::max(idx, parsedIndex);
0517                         }
0518                     }
0519                     idx++;
0520                     const QString& name = clipboardImageNamePrefix+QString::number(idx);
0521 
0522                     addImageAttachment(name, clipboardImage);
0523                     return true;
0524                 }
0525             }
0526         }
0527         else if (event->type() == QEvent::GraphicsSceneDrop)
0528         {
0529             auto* dragEvent = static_cast<QGraphicsSceneDragDropEvent*>(event);
0530             const QMimeData* mimeData = dragEvent->mimeData();
0531             if (mimeData->hasUrls())
0532             {
0533                 QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
0534 
0535                 for (const QUrl &url : mimeData->urls())
0536                 {
0537                     const QString filename = url.toLocalFile();
0538                     QFileInfo info(filename);
0539                     if (supportedFormats.contains(info.completeSuffix().toUtf8()))
0540                     {
0541                         QImage image(filename);
0542                         addImageAttachment(info.fileName(), image);
0543                         m_textItem->textCursor().insertText(QLatin1String("\n"));
0544                     }
0545                 }
0546                 return true;
0547             }
0548         }
0549     }
0550     return false;
0551 }
0552 
0553 bool MarkdownEntry::wantToEvaluate()
0554 {
0555     return !rendered;
0556 }
0557 
0558 void MarkdownEntry::setRenderedHtml(const QString& html)
0559 {
0560     m_textItem->setHtml(html);
0561     m_textItem->denyEditing();
0562 }
0563 
0564 void MarkdownEntry::setPlainText(const QString& plain)
0565 {
0566     QTextDocument* doc = m_textItem->document();
0567     doc->setPlainText(plain);
0568     m_textItem->setDocument(doc);
0569     m_textItem->allowEditing();
0570 }
0571 
0572 void MarkdownEntry::renderMath()
0573 {
0574     QTextCursor cursor(m_textItem->document());
0575     for (int i = 0; i < (int)foundMath.size(); i++)
0576         if (foundMath[i].second == false)
0577             renderMathExpression(i+1, foundMath[i].first);
0578 }
0579 
0580 void MarkdownEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
0581 {
0582     if (!result->successful)
0583     {
0584         if (Settings::self()->showMathRenderError())
0585         {
0586             QApplication::restoreOverrideCursor();
0587             KMessageBox::error(worksheetView(), result->errorMessage, i18n("Cantor Math Error"));
0588         }
0589         else
0590             qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage;
0591         return;
0592     }
0593 
0594     setRenderedMath(result->jobId, result->renderedMath, result->uniqueUrl, result->image);
0595 }
0596 
0597 void MarkdownEntry::renderMathExpression(int jobId, QString mathCode)
0598 {
0599     QString latex;
0600     Cantor::LatexRenderer::EquationType type;
0601     std::tie(latex, type) = parseMathCode(mathCode);
0602     if (!latex.isNull())
0603         worksheet()->mathRenderer()->renderExpression(jobId, latex, type, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
0604 }
0605 
0606 std::pair<QString, Cantor::LatexRenderer::EquationType> MarkdownEntry::parseMathCode(QString mathCode)
0607 {
0608     static const QLatin1String inlineDelimiter("$");
0609     static const QLatin1String displayedDelimiter("$$");
0610 
0611     if (mathCode.startsWith(displayedDelimiter) && mathCode.endsWith(displayedDelimiter))
0612     {
0613         mathCode.remove(0, 2);
0614         mathCode.chop(2);
0615 
0616         if (mathCode[0] == QChar(6))
0617             mathCode.remove(0, 1);
0618 
0619         return std::make_pair(mathCode, Cantor::LatexRenderer::FullEquation);
0620     }
0621     else if (mathCode.startsWith(inlineDelimiter) && mathCode.endsWith(inlineDelimiter))
0622     {
0623         mathCode.remove(0, 1);
0624         mathCode.chop(1);
0625 
0626         if (mathCode[0] == QChar(6))
0627             mathCode.remove(0, 1);
0628 
0629         return std::make_pair(mathCode, Cantor::LatexRenderer::InlineEquation);
0630     }
0631     else if (mathCode.startsWith(QString::fromUtf8("\\begin{")) && mathCode.endsWith(QLatin1Char('}')))
0632     {
0633         if (mathCode[1] == QChar(6))
0634             mathCode.remove(1, 1);
0635 
0636         return std::make_pair(mathCode, Cantor::LatexRenderer::CustomEquation);
0637     }
0638     else
0639         return std::make_pair(QString(), Cantor::LatexRenderer::InlineEquation);
0640 }
0641 
0642 void MarkdownEntry::setRenderedMath(int jobId, const QTextImageFormat& format, const QUrl& internal, const QImage& image)
0643 {
0644     if ((int)foundMath.size() < jobId)
0645         return;
0646 
0647     const auto& iter = foundMath.begin() + jobId-1;
0648 
0649     QTextCursor cursor = findMath(jobId);
0650 
0651     const QString delimiter = format.property(Cantor::Renderer::Delimiter).toString();
0652     QString searchText = delimiter + format.property(Cantor::Renderer::Code).toString() + delimiter;
0653 
0654     Cantor::LatexRenderer::EquationType type
0655         = (Cantor::LatexRenderer::EquationType)format.intProperty(Cantor::Renderer::CantorFormula);
0656 
0657     // From findMath we will be first symbol of math expression
0658     // So in order to select all symbols of the expression, we need to go to previous symbol first
0659     // But it working strange sometimes: some times we need to go to previous character, sometimes not
0660     // So the code tests that we on '$' symbol and if it isn't true, then we revert back
0661     cursor.movePosition(QTextCursor::PreviousCharacter);
0662     bool withDollarDelimiter = type == Cantor::LatexRenderer::InlineEquation || type == Cantor::LatexRenderer::FullEquation;
0663     if (withDollarDelimiter && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('$'))
0664         cursor.movePosition(QTextCursor::NextCharacter);
0665     else if (type == Cantor::LatexRenderer::CustomEquation && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('\\') )
0666         cursor.movePosition(QTextCursor::NextCharacter);
0667 
0668     cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, searchText.size());
0669 
0670     if (!cursor.isNull())
0671     {
0672         m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
0673 
0674         // Don't add new line for $$...$$ on document's begin and end
0675         // And if we in block, which haven't non-space characters except out math expression
0676         // In another sitation, Cantor will move rendered image into another QTextBlock
0677         QTextCursor prevSymCursor = m_textItem->document()->find(QRegularExpression(QStringLiteral("[^\\s]")),
0678                                                                  cursor, QTextDocument::FindBackward);
0679         if (type == Cantor::LatexRenderer::FullEquation
0680             && cursor.selectionStart() != 0
0681             && prevSymCursor.block() == cursor.block()
0682         )
0683         {
0684             cursor.insertBlock();
0685 
0686             cursor.setPosition(prevSymCursor.position()+2, QTextCursor::KeepAnchor);
0687             cursor.removeSelectedText();
0688         }
0689 
0690         cursor.insertText(QString(QChar::ObjectReplacementCharacter), format);
0691 
0692         bool atDocEnd = cursor.position() == m_textItem->document()->characterCount()-1;
0693         QTextCursor nextSymCursor = m_textItem->document()->find(QRegularExpression(QStringLiteral("[^\\s]")), cursor);
0694         if (type == Cantor::LatexRenderer::FullEquation && !atDocEnd && nextSymCursor.block() == cursor.block())
0695         {
0696             cursor.setPosition(nextSymCursor.position()-1, QTextCursor::KeepAnchor);
0697             cursor.removeSelectedText();
0698             cursor.insertBlock();
0699         }
0700 
0701         // Set that the formulas is rendered
0702         iter->second = true;
0703 
0704         m_textItem->document()->clearUndoRedoStacks();
0705     }
0706 }
0707 
0708 QTextCursor MarkdownEntry::findMath(int id)
0709 {
0710     QTextCursor cursor(m_textItem->document());
0711     do
0712     {
0713         QTextCharFormat format = cursor.charFormat();
0714         if (format.intProperty(JobProperty) == id)
0715             break;
0716     }
0717     while (cursor.movePosition(QTextCursor::NextCharacter));
0718 
0719     return cursor;
0720 }
0721 
0722 void MarkdownEntry::markUpMath()
0723 {
0724     QTextCursor cursor(m_textItem->document());
0725     for (int i = 0; i < (int)foundMath.size(); i++)
0726     {
0727         if (foundMath[i].second)
0728             continue;
0729 
0730         QString searchText = foundMath[i].first;
0731         searchText.replace(QRegularExpression(QStringLiteral("\\s+")), QStringLiteral(" "));
0732 
0733         cursor = m_textItem->document()->find(searchText, cursor);
0734 
0735         // Mark up founded math code
0736         QTextCharFormat format = cursor.charFormat();
0737         // Use index+1 in math array as property tag
0738         format.setProperty(JobProperty, i+1);
0739 
0740         // We found the math expression, so remove 'marker' (ACII symbol 'Acknowledgement')
0741         // The marker have been placed after "$" or "$$"
0742         // We remove the marker, only if it presents
0743         QString codeWithoutMarker = foundMath[i].first;
0744         if (searchText.startsWith(QLatin1String("$$")))
0745         {
0746             if (codeWithoutMarker[2] == QChar(6))
0747                 codeWithoutMarker.remove(2, 1);
0748         }
0749         else if (searchText.startsWith(QLatin1String("$")))
0750         {
0751             if (codeWithoutMarker[1] == QChar(6))
0752                 codeWithoutMarker.remove(1, 1);
0753         }
0754         else if (searchText.startsWith(QLatin1String("\\")))
0755         {
0756             if (codeWithoutMarker[1] == QChar(6))
0757                 codeWithoutMarker.remove(1, 1);
0758         }
0759         cursor.insertText(codeWithoutMarker, format);
0760     }
0761 }
0762 
0763 void MarkdownEntry::insertImage()
0764 {
0765     KConfigGroup conf(KSharedConfig::openConfig(), QLatin1String("MarkdownEntry"));
0766     const QString& dir = conf.readEntry(QLatin1String("LastImageDir"), QString());
0767 
0768     QString formats;
0769     for (const QByteArray& format : QImageReader::supportedImageFormats())
0770         formats += QLatin1String("*.") + QLatin1String(format.constData()) + QLatin1Char(' ');
0771 
0772     const QString& path = QFileDialog::getOpenFileName(worksheet()->worksheetView(),
0773                                                        i18n("Open image file"),
0774                                                        dir,
0775                                                        i18n("Images (%1)", formats));
0776     if (path.isEmpty())
0777         return; //cancel was clicked in the file-dialog
0778 
0779     //save the last used directory, if changed
0780     const int pos = path.lastIndexOf(QLatin1String("/"));
0781     if (pos != -1) {
0782         const QString& newDir = path.left(pos);
0783         if (newDir != dir)
0784             conf.writeEntry(QLatin1String("LastImageDir"), newDir);
0785     }
0786 
0787     QImageReader reader(path);
0788     const QImage& img = reader.read();
0789     if (!img.isNull())
0790     {
0791         const QString& name = QFileInfo(path).fileName();
0792         addImageAttachment(name, img);
0793     }
0794     else
0795         KMessageBox::error(worksheetView(),
0796                            i18n("Failed to read the image \"%1\". Error \"%2\"", path, reader.errorString()),
0797                            i18n("Cantor"));
0798 }
0799 
0800 void MarkdownEntry::clearAttachments()
0801 {
0802     for (auto& attachment: attachedImages)
0803     {
0804         const QUrl& url = attachment.first;
0805         m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant());
0806     }
0807     attachedImages.clear();
0808     animateSizeChange();
0809 }
0810 
0811 void MarkdownEntry::enterEditMode()
0812 {
0813     setPlainText(plain);
0814     m_textItem->textCursor().clearSelection();
0815     rendered = false;
0816 }
0817 
0818 QString MarkdownEntry::plainText() const
0819 {
0820     return m_textItem->toPlainText();
0821 }
0822 
0823 void MarkdownEntry::addImageAttachment(const QString& name, const QImage& image)
0824 {
0825     QUrl url;
0826     url.setScheme(QLatin1String("attachment"));
0827     url.setPath(name);
0828 
0829     attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
0830     m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image));
0831 
0832     QTextCursor cursor = m_textItem->textCursor();
0833     cursor.insertText(QString::fromLatin1("![%1](attachment:%1)").arg(name));
0834 
0835     animateSizeChange();
0836 }