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 }