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