diff --git a/src/markdownentry.cpp b/src/markdownentry.cpp index 5990b21d..f0ba5afb 100644 --- a/src/markdownentry.cpp +++ b/src/markdownentry.cpp @@ -1,595 +1,628 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2018 Yifei Wu */ #include "markdownentry.h" #include #include #include #include #include #include #include #include #include #include #include "jupyterutils.h" #include "mathrender.h" #include #ifdef Discount_FOUND extern "C" { #include } #endif MarkdownEntry::MarkdownEntry(Worksheet* worksheet) : WorksheetEntry(worksheet), m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)), rendered(false) { m_textItem->enableRichText(false); m_textItem->setOpenExternalLinks(true); m_textItem->installEventFilter(this); connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &MarkdownEntry::moveToPreviousEntry); connect(m_textItem, &WorksheetTextItem::moveToNext, this, &MarkdownEntry::moveToNextEntry); connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate())); } void MarkdownEntry::populateMenu(QMenu* menu, QPointF pos) { bool imageSelected = false; QTextCursor cursor = m_textItem->textCursor(); const QChar repl = QChar::ObjectReplacementCharacter; if (cursor.hasSelection()) { QString selection = m_textItem->textCursor().selectedText(); imageSelected = selection.contains(repl); } else { // we need to try both the current cursor and the one after the that cursor = m_textItem->cursorForPosition(pos); for (int i = 2; i; --i) { int p = cursor.position(); if (m_textItem->document()->characterAt(p-1) == repl && cursor.charFormat().hasProperty(EpsRenderer::CantorFormula)) { m_textItem->setTextCursor(cursor); imageSelected = true; break; } cursor.movePosition(QTextCursor::NextCharacter); } } if (imageSelected) { menu->addAction(i18n("Show LaTeX code"), this, &MarkdownEntry::resolveImagesAtCursor); menu->addSeparator(); } WorksheetEntry::populateMenu(menu, pos); } bool MarkdownEntry::isEmpty() { return m_textItem->document()->isEmpty(); } int MarkdownEntry::type() const { return Type; } bool MarkdownEntry::acceptRichText() { return false; } bool MarkdownEntry::focusEntry(int pos, qreal xCoord) { if (aboutToBeRemoved()) return false; m_textItem->setFocusAt(pos, xCoord); return true; } void MarkdownEntry::setContent(const QString& content) { rendered = false; plain = content; setPlainText(plain); } void MarkdownEntry::setContent(const QDomElement& content, const KZip& file) { rendered = content.attribute(QLatin1String("rendered"), QLatin1String("1")) == QLatin1String("1"); QDomElement htmlEl = content.firstChildElement(QLatin1String("HTML")); if(!htmlEl.isNull()) html = htmlEl.text(); else { html = QLatin1String(""); rendered = false; // No html provided. Assume that it hasn't been rendered. } QDomElement plainEl = content.firstChildElement(QLatin1String("Plain")); if(!plainEl.isNull()) plain = plainEl.text(); else { plain = QLatin1String(""); html = QLatin1String(""); // No plain text provided. The entry shouldn't render anything, or the user can't re-edit it. } const QDomNodeList& attachments = content.elementsByTagName(QLatin1String("Attachment")); for (int x = 0; x < attachments.count(); x++) { const QDomElement& attachment = attachments.at(x).toElement(); QUrl url(attachment.attribute(QLatin1String("url"))); const QString& base64 = attachment.text(); QImage image; image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), "PNG"); attachedImages.push_back(std::make_pair(url, QLatin1String("image/png"))); m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image)); } if(rendered) setRenderedHtml(html); else setPlainText(plain); // Handle math after setting html const QDomNodeList& maths = content.elementsByTagName(QLatin1String("EmbeddedMath")); + foundMath.clear(); for (int i = 0; i < maths.count(); i++) { const QDomElement& math = maths.at(i).toElement(); - bool mathRendered = math.attribute(QLatin1String("rendered")).toInt(); const QString mathCode = math.text(); - foundMath.push_back(std::make_pair(mathCode, mathRendered)); + foundMath.push_back(std::make_pair(mathCode, false)); + } + + if (rendered) + { + markUpMath(); - if (rendered && mathRendered) + for (int i = 0; i < maths.count(); i++) { - const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path"))); - if (imageEntry && imageEntry->isFile()) + const QDomElement& math = maths.at(i).toElement(); + bool mathRendered = math.attribute(QLatin1String("rendered")).toInt(); + const QString mathCode = math.text(); + + if (mathRendered) { - const KArchiveFile* imageFile=static_cast(imageEntry); - const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation); - imageFile->copyTo(dir); - const QString& pdfPath = dir + QDir::separator() + imageFile->name(); - - QString latex; - Cantor::LatexRenderer::EquationType type; - std::tie(latex, type) = parseMathCode(mathCode); - - // Get uuid by removing 'cantor_' and '.pdf' extention - // len('cantor_') == 7, len('.pdf') == 4 - QString uuid = pdfPath; - uuid.remove(0, 7); - uuid.chop(4); - - bool success; - const auto& data = worksheet()->mathRenderer()->renderExpressionFromPdf(pdfPath, uuid, latex, type, &success); - if (success) + const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path"))); + if (imageEntry && imageEntry->isFile()) { - QUrl internal; - internal.setScheme(QLatin1String("internal")); - internal.setPath(uuid); - setRenderedMath(data.first, internal, data.second); + const KArchiveFile* imageFile=static_cast(imageEntry); + const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation); + imageFile->copyTo(dir); + const QString& pdfPath = dir + QDir::separator() + imageFile->name(); + + QString latex; + Cantor::LatexRenderer::EquationType type; + std::tie(latex, type) = parseMathCode(mathCode); + + // Get uuid by removing 'cantor_' and '.pdf' extention + // len('cantor_') == 7, len('.pdf') == 4 + QString uuid = pdfPath; + uuid.remove(0, 7); + uuid.chop(4); + + bool success; + const auto& data = worksheet()->mathRenderer()->renderExpressionFromPdf(pdfPath, uuid, latex, type, &success); + if (success) + { + QUrl internal; + internal.setScheme(QLatin1String("internal")); + internal.setPath(uuid); + setRenderedMath(i+1, data.first, internal, data.second); + } } + else + renderMathExpression(i+1, mathCode); } - else - renderMathExpression(mathCode); } } } void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell) { if (!JupyterUtils::isMarkdownCell(cell)) return; // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata // There isn't Jupyter metadata for markdown cells, which could be handled by Cantor const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject(); for (const QString& key : attachments.keys()) { const QJsonValue& attachment = attachments.value(key); const QString& mimeKey = JupyterUtils::firstImageKey(attachment); if (!mimeKey.isEmpty()) { const QImage& image = JupyterUtils::loadImage(attachment, mimeKey); QUrl resourceUrl; resourceUrl.setUrl(QLatin1String("attachment:")+key); attachedImages.push_back(std::make_pair(resourceUrl, mimeKey)); m_textItem->document()->addResource(QTextDocument::ImageResource, resourceUrl, QVariant(image)); } } setPlainText(JupyterUtils::getSource(cell)); } QDomElement MarkdownEntry::toXml(QDomDocument& doc, KZip* archive) { if(!rendered) plain = m_textItem->toPlainText(); QDomElement el = doc.createElement(QLatin1String("Markdown")); el.setAttribute(QLatin1String("rendered"), (int)rendered); QDomElement plainEl = doc.createElement(QLatin1String("Plain")); plainEl.appendChild(doc.createTextNode(plain)); el.appendChild(plainEl); QDomElement htmlEl = doc.createElement(QLatin1String("HTML")); htmlEl.appendChild(doc.createTextNode(html)); el.appendChild(htmlEl); QUrl url; QString key; for (const auto& data : attachedImages) { std::tie(url, key) = std::move(data); QDomElement attachmentEl = doc.createElement(QLatin1String("Attachment")); attachmentEl.setAttribute(QStringLiteral("url"), url.toString()); const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value(); QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); attachmentEl.appendChild(doc.createTextNode(QString::fromLatin1(ba.toBase64()))); el.appendChild(attachmentEl); } // If math rendered, then append result .pdf to archive QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter)); for (const auto& data : foundMath) { QDomElement mathEl = doc.createElement(QLatin1String("EmbeddedMath")); mathEl.setAttribute(QStringLiteral("rendered"), data.second); mathEl.appendChild(doc.createTextNode(data.first)); if (data.second) { bool foundNeededImage = false; while(!cursor.isNull() && !foundNeededImage) { QTextImageFormat format=cursor.charFormat().toImageFormat(); if (format.hasProperty(EpsRenderer::CantorFormula)) { const QString& latex = format.property(EpsRenderer::Code).toString(); const QString& delimiter = format.property(EpsRenderer::Delimiter).toString(); const QString& code = delimiter + latex + delimiter; if (code == data.first) { const QUrl& url = QUrl::fromLocalFile(format.property(EpsRenderer::ImagePath).toString()); - qDebug() << QFile::exists(url.toLocalFile()); archive->addLocalFile(url.toLocalFile(), url.fileName()); mathEl.setAttribute(QStringLiteral("path"), url.fileName()); foundNeededImage = true; } } cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); } } el.appendChild(mathEl); } return el; } QJsonValue MarkdownEntry::toJupyterJson() { QJsonObject entry; entry.insert(QLatin1String("cell_type"), QLatin1String("markdown")); entry.insert(QLatin1String("metadata"), QJsonObject()); QJsonObject attachments; QUrl url; QString key; for (const auto& data : attachedImages) { std::tie(url, key) = std::move(data); const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value(); QString attachmentKey = url.toString().remove(QLatin1String("attachment:")); attachments.insert(attachmentKey, JupyterUtils::packMimeBundle(image, key)); } if (!attachments.isEmpty()) entry.insert(QLatin1String("attachments"), attachments); JupyterUtils::setSource(entry, plain); return entry; } QString MarkdownEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) { Q_UNUSED(commandSep); if (commentStartingSeq.isEmpty()) return QString(); QString text(plain); if (!commentEndingSeq.isEmpty()) return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n"); return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n"); } void MarkdownEntry::interruptEvaluation() { } bool MarkdownEntry::evaluate(EvaluationOption evalOp) { if(!rendered) { if (m_textItem->toPlainText() == plain && !html.isEmpty()) { setRenderedHtml(html); rendered = true; + for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++) + iter->second = false; + markUpMath(); } else { plain = m_textItem->toPlainText(); rendered = renderMarkdown(plain); } } if (worksheet()->embeddedMathEnabled()) renderMath(); evaluateNext(evalOp); return true; } bool MarkdownEntry::renderMarkdown(QString& plain) { #ifdef Discount_FOUND QByteArray mdCharArray = plain.toUtf8(); MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size()+1, 0); if(!mkd_compile(mdHandle, MKD_LATEX | MKD_FENCEDCODE | MKD_GITHUBTAGS)) { qDebug()<<"Failed to compile the markdown document"; mkd_cleanup(mdHandle); return false; } char *htmlDocument; int htmlSize = mkd_document(mdHandle, &htmlDocument); html = QString::fromUtf8(htmlDocument, htmlSize); char *latexData; int latexDataSize = mkd_latextext(mdHandle, &latexData); QStringList latexUnits = QString::fromUtf8(latexData, latexDataSize).split(QLatin1Char(31), QString::SkipEmptyParts); foundMath.clear(); - for (const QString& latex : latexUnits) - { - foundMath.push_back(std::make_pair(latex, false)); - html.replace(latex, latex.toHtmlEscaped()); - } - qDebug() << "founded math:" << foundMath; mkd_cleanup(mdHandle); setRenderedHtml(html); + + QTextCursor cursor(m_textItem->document()); + for (const QString& latex : latexUnits) + foundMath.push_back(std::make_pair(latex, false)); + + markUpMath(); + return true; #else Q_UNUSED(plain); return false; #endif } void MarkdownEntry::updateEntry() { QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter)); while(!cursor.isNull()) { QTextImageFormat format=cursor.charFormat().toImageFormat(); if (format.hasProperty(EpsRenderer::CantorFormula)) worksheet()->mathRenderer()->rerender(m_textItem->document(), format); cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); } } WorksheetCursor MarkdownEntry::search(const QString& pattern, unsigned flags, QTextDocument::FindFlags qt_flags, const WorksheetCursor& pos) { if (!(flags & WorksheetEntry::SearchText) || (pos.isValid() && pos.entry() != this)) return WorksheetCursor(); QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos); if (textCursor.isNull()) return WorksheetCursor(); else return WorksheetCursor(this, m_textItem, textCursor); } void MarkdownEntry::layOutForWidth(qreal w, bool force) { if (size().width() == w && !force) return; m_textItem->setGeometry(0, 0, w); setSize(QSizeF(m_textItem->width(), m_textItem->height() + VerticalMargin)); } bool MarkdownEntry::eventFilter(QObject* object, QEvent* event) { if(object == m_textItem) { if(event->type() == QEvent::GraphicsSceneMouseDoubleClick) { QGraphicsSceneMouseEvent* mouseEvent = dynamic_cast(event); if(!mouseEvent) return false; if(mouseEvent->button() == Qt::LeftButton) { if (rendered) { setPlainText(plain); m_textItem->setCursorPosition(mouseEvent->pos()); m_textItem->textCursor().clearSelection(); rendered = false; return true; } } } } return false; } bool MarkdownEntry::wantToEvaluate() { return !rendered; } void MarkdownEntry::setRenderedHtml(const QString& html) { m_textItem->setHtml(html); m_textItem->denyEditing(); } void MarkdownEntry::setPlainText(const QString& plain) { QTextDocument* doc = m_textItem->document(); doc->setPlainText(plain); m_textItem->setDocument(doc); m_textItem->allowEditing(); } void MarkdownEntry::resolveImagesAtCursor() { QTextCursor cursor = m_textItem->textCursor(); if (!cursor.hasSelection()) cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); const QString& mathCode = m_textItem->resolveImages(cursor); for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++) if (iter->first == mathCode) { iter->second = false; break; } cursor.insertText(mathCode); } void MarkdownEntry::renderMath() { + // Jupyter TODO: what about \( \) and \[ \]? Supports or not? QTextCursor cursor(m_textItem->document()); - cursor.movePosition(QTextCursor::Start); - for (std::pair pair : foundMath) - { - // Jupyter TODO: what about \( \) and \[ \]? Supports or not? - QString searchText = pair.first; - searchText.replace(QRegExp(QLatin1String("\\s+")), QLatin1String(" ")); - - QTextCursor found = m_textItem->document()->find(searchText, cursor); - if (found.isNull()) - continue; - cursor = found; - - QString latexCode = cursor.selectedText(); - latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); - latexCode.replace(QChar::LineSeparator, QLatin1Char('\n')); - qDebug()<<"found latex: " << latexCode; - - renderMathExpression(latexCode); - } + for (int i = 0; i < (int)foundMath.size(); i++) + renderMathExpression(i+1, foundMath[i].first); } void MarkdownEntry::handleMathRender(QSharedPointer result) { if (!result->successfull) { qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage; return; } - setRenderedMath(result->renderedMath, result->uniqueUrl, result->image); + setRenderedMath(result->jobId, result->renderedMath, result->uniqueUrl, result->image); } -void MarkdownEntry::renderMathExpression(QString mathCode) +void MarkdownEntry::renderMathExpression(int jobId, QString mathCode) { QString latex; Cantor::LatexRenderer::EquationType type; std::tie(latex, type) = parseMathCode(mathCode); if (!latex.isNull()) - worksheet()->mathRenderer()->renderExpression(latex, type, this, SLOT(handleMathRender(QSharedPointer))); + worksheet()->mathRenderer()->renderExpression(jobId, latex, type, this, SLOT(handleMathRender(QSharedPointer))); } std::pair MarkdownEntry::parseMathCode(QString mathCode) { static const QLatin1String inlineDelimiter("$"); static const QLatin1String displayedDelimiter("$$"); if (mathCode.startsWith(displayedDelimiter) && mathCode.endsWith(displayedDelimiter)) { mathCode.remove(0, 2); mathCode.chop(2); return std::make_pair(mathCode, Cantor::LatexRenderer::FullEquation); } else if (mathCode.startsWith(inlineDelimiter) && mathCode.endsWith(inlineDelimiter)) { mathCode.remove(0, 1); mathCode.chop(1); return std::make_pair(mathCode, Cantor::LatexRenderer::InlineEquation); } else return std::make_pair(QString(), Cantor::LatexRenderer::InlineEquation); } -void MarkdownEntry::setRenderedMath(const QTextImageFormat& format, const QUrl& internal, const QImage& image) +void MarkdownEntry::setRenderedMath(int jobId, const QTextImageFormat& format, const QUrl& internal, const QImage& image) { - const QString& code = format.property(EpsRenderer::Code).toString(); - const QString& delimiter = format.property(EpsRenderer::Delimiter).toString(); - QString searchText = delimiter + code + delimiter; - searchText.replace(QRegExp(QLatin1String("\\s")), QLatin1String(" ")); + if ((int)foundMath.size() < jobId) + return; + + const auto& iter = foundMath.begin() + jobId-1; + + QTextCursor cursor = findMath(jobId); + QString searchText = iter->first; + searchText.replace(QRegExp(QLatin1String("\\s+")), QLatin1String(" ")); + cursor = m_textItem->document()->find(searchText, cursor); - QTextCursor cursor = m_textItem->document()->find(searchText); if (!cursor.isNull()) { m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image)); cursor.insertText(QString(QChar::ObjectReplacementCharacter), format); // Set that the formulas is rendered - for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++) - { - const QString& formulas = delimiter + code + delimiter; - if (iter->first == formulas) - { - iter->second = true; - break; - } - } + iter->second = true; + } +} + +QTextCursor MarkdownEntry::findMath(int id) +{ + QTextCursor cursor(m_textItem->document()); + do + { + QTextCharFormat format = cursor.charFormat(); + if (format.intProperty(10000) == id) + break; + } + while (cursor.movePosition(QTextCursor::NextCharacter)); + + // Jupyter TODO: fix it + if (cursor.position() != 0) + cursor.movePosition(QTextCursor::PreviousCharacter); + + return cursor; +} + +void MarkdownEntry::markUpMath() +{ + QTextCursor cursor(m_textItem->document()); + for (int i = 0; i < (int)foundMath.size(); i++) + { + if (foundMath[i].second) + continue; + + QString searchText = foundMath[i].first; + searchText.replace(QRegExp(QLatin1String("\\s+")), QLatin1String(" ")); + + cursor = m_textItem->document()->find(searchText, cursor); + QTextCharFormat format = cursor.charFormat(); + + // Use index+1 in math array as property tag + format.setProperty(10000, i+1); + cursor.setCharFormat(format); } } diff --git a/src/markdownentry.h b/src/markdownentry.h index 010b104d..c121e3b3 100644 --- a/src/markdownentry.h +++ b/src/markdownentry.h @@ -1,98 +1,100 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2018 Yifei Wu */ #ifndef MARKDOWNENTRY_H #define MARKDOWNENTRY_H #include #include #include "worksheetentry.h" #include "worksheettextitem.h" #include "mathrendertask.h" #include "lib/latexrenderer.h" class QJsonObject; class MarkdownEntry : public WorksheetEntry { Q_OBJECT public: explicit MarkdownEntry(Worksheet* worksheet); ~MarkdownEntry() override = default; enum {Type = UserType + 7}; int type() const override; bool isEmpty() override; bool acceptRichText() override; bool focusEntry(int pos = WorksheetTextItem::TopLeft, qreal xCoord=0) override; void setContent(const QString& content) override; void setContent(const QDomElement& content, const KZip& file) override; void setContentFromJupyter(const QJsonObject& cell) override; QDomElement toXml(QDomDocument& doc, KZip* archive) override; QJsonValue toJupyterJson() override; QString toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) override; void interruptEvaluation() override; void layOutForWidth(qreal w, bool force = false) override; WorksheetCursor search(const QString& pattern, unsigned flags, QTextDocument::FindFlags qt_flags, const WorksheetCursor& pos = WorksheetCursor()) override; public Q_SLOTS: bool evaluate(WorksheetEntry::EvaluationOption evalOp = FocusNext) override; void updateEntry() override; void resolveImagesAtCursor(); void populateMenu(QMenu* menu, QPointF pos) override; protected: bool renderMarkdown(QString& plain); bool eventFilter(QObject* object, QEvent* event) override; bool wantToEvaluate() override; void setRenderedHtml(const QString& html); void setPlainText(const QString& plain); void renderMath(); - void renderMathExpression(QString mathCode); - void setRenderedMath(const QTextImageFormat& format, const QUrl& internal, const QImage& image); + void renderMathExpression(int jobId, QString mathCode); + void setRenderedMath(int jobId, const QTextImageFormat& format, const QUrl& internal, const QImage& image); + QTextCursor findMath(int id); + void markUpMath(); static std::pair parseMathCode(QString mathCode); protected Q_SLOTS: void handleMathRender(QSharedPointer result); protected: WorksheetTextItem* m_textItem; QString plain; QString html; bool rendered; std::vector> attachedImages; std::vector> foundMath; }; #endif //MARKDOWNENTRY_H diff --git a/src/mathrender.cpp b/src/mathrender.cpp index 62dfb064..1bf33528 100644 --- a/src/mathrender.cpp +++ b/src/mathrender.cpp @@ -1,107 +1,107 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2019 Sirgienko Nikita */ #include "mathrender.h" #include #include #include #include #include "mathrendertask.h" #include "epsrenderer.h" QMutex MathRenderer::popplerMutex; MathRenderer::MathRenderer(): m_scale(1.0), m_useHighRes(false) { qRegisterMetaType>(); } MathRenderer::~MathRenderer() { } bool MathRenderer::mathRenderAvailable() { return QStandardPaths::findExecutable(QLatin1String("pdflatex")).isEmpty() == false; } qreal MathRenderer::scale() { return m_scale; } void MathRenderer::setScale(qreal scale) { m_scale = scale; } void MathRenderer::useHighResolution(bool b) { m_useHighRes = b; } -void MathRenderer::renderExpression(const QString& mathExpression, Cantor::LatexRenderer::EquationType type, const QObject* receiver, const char* resultHandler) +void MathRenderer::renderExpression(int jobId, const QString& mathExpression, Cantor::LatexRenderer::EquationType type, const QObject* receiver, const char* resultHandler) { - MathRenderTask* task = new MathRenderTask(mathExpression, type, m_scale, m_useHighRes, &popplerMutex); + MathRenderTask* task = new MathRenderTask(jobId, mathExpression, type, m_scale, m_useHighRes, &popplerMutex); task->setHandler(receiver, resultHandler); task->setAutoDelete(false); QThreadPool::globalInstance()->start(task); } void MathRenderer::rerender(QTextDocument* document, const QTextImageFormat& math) { const QString& filename = math.property(EpsRenderer::ImagePath).toString(); if (!QFile::exists(filename)) return; bool success; QString errorMessage; QImage img = MathRenderTask::renderPdf(filename, m_scale, m_useHighRes, &success, nullptr, &errorMessage, &popplerMutex); if (success) { QUrl internal(math.name()); document->addResource(QTextDocument::ImageResource, internal, QVariant(img)); } else { qDebug() << "Rerender embedded math failed with message: " << errorMessage; } } std::pair MathRenderer::renderExpressionFromPdf(const QString& filename, const QString& uuid, const QString& code, Cantor::LatexRenderer::EquationType type, bool* outSuccess) { if (!QFile::exists(filename)) { if (outSuccess) *outSuccess = false; return std::make_pair(QTextImageFormat(), QImage()); } bool success; QString errorMessage; const auto& data = MathRenderTask::renderPdfToFormat(filename, code, uuid, type, m_scale, m_useHighRes, &success, &errorMessage, &popplerMutex); if (success == false) qDebug() << "Render embedded math from pdf failed with message: " << errorMessage; if (outSuccess) *outSuccess = success; return data; } diff --git a/src/mathrender.h b/src/mathrender.h index 0fad4c8d..d0e64bf0 100644 --- a/src/mathrender.h +++ b/src/mathrender.h @@ -1,83 +1,84 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2019 Sirgienko Nikita */ #ifndef MATHRENDER_H #define MATHRENDER_H #include #include #include #include "lib/latexrenderer.h" /** * Special class for renderning embeded math in MarkdownEntry and TextEntry * Instead of LatexRenderer+EpsRenderer provide all needed functianality in one class * Even if we add some speed optimization in future, API of the class probably won't change */ class MathRenderer : public QObject { Q_OBJECT public: MathRenderer(); ~MathRenderer(); bool mathRenderAvailable(); // Resulution contol void setScale(qreal scale); qreal scale(); void useHighResolution(bool b); /** * This function will run render task in Qt thread pool and * call resultHandler SLOT with MathRenderResult* argument on finish * receiver will be managed about pointer, task only create it */ void renderExpression( + int jobId, const QString& mathExpression, Cantor::LatexRenderer::EquationType type, const QObject *receiver, const char *resultHandler); /** * Rerender renderer math expression in document * Unlike MathRender::renderExpression this method isn't async, because * rerender already rendered math is not long operation */ void rerender(QTextDocument* document, const QTextImageFormat& math); /** * Render math expression from existing .pdf * Like MathRenderer::rerender is blocking */ std::pair renderExpressionFromPdf( const QString& filename, const QString& uuid, const QString& code, Cantor::LatexRenderer::EquationType type, bool* success ); private: double m_scale; bool m_useHighRes; // We need this, because poppler-qt5 not threadsafe before 0.73.0 and 0.73.0 is too new // and not common widespread in repositories static QMutex popplerMutex; }; #endif /* MATHRENDER_H */ diff --git a/src/mathrendertask.cpp b/src/mathrendertask.cpp index 936eff91..cd9ce458 100644 --- a/src/mathrendertask.cpp +++ b/src/mathrendertask.cpp @@ -1,300 +1,302 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2019 Sirgienko Nikita */ #include "mathrendertask.h" #include #include #include #include #include #include #include #include #include #include "epsrenderer.h" // Jupyter TODO: pagecolor don't work with preview // For example there are question about it: // https://tex.stackexchange.com/questions/499712/pagecolor-ignored-when-preview-package-used static const QLatin1String mathTex("\\documentclass{minimal}"\ "\\usepackage{amsfonts,amssymb}"\ "\\usepackage{amsmath}"\ "\\usepackage[utf8]{inputenc}"\ "\\usepackage{color}"\ "\\usepackage[active,textmath,tightpage]{%1}"\ /* "\\setlength\\textwidth{5in}"\ "\\setlength{\\parindent}{0pt}"\ "\\pagestyle{empty}"\ */ "\\begin{document}"\ "\\begin{preview}"\ "\\pagecolor[rgb]{%2,%3,%4}"\ "\\color[rgb]{%5,%6,%7}"\ "%8"\ "\\end{preview}"\ "\\end{document}"); static const QLatin1String eqnHeader("$\\displaystyle %1$"); static const QLatin1String inlineEqnHeader("$%1$"); MathRenderTask::MathRenderTask( + int jobId, const QString& code, Cantor::LatexRenderer::EquationType type, double scale, bool highResolution, QMutex* mutex - ): m_code(code), m_type(type), m_scale(scale), m_highResolution(highResolution), m_mutex(mutex) + ): m_jobId(jobId), m_code(code), m_type(type), m_scale(scale), m_highResolution(highResolution), m_mutex(mutex) {} void MathRenderTask::setHandler(const QObject* receiver, const char* resultHandler) { connect(this, SIGNAL(finish(QSharedPointer)), receiver, resultHandler); } void MathRenderTask::run() { QSharedPointer result(new MathRenderResult()); const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation); QTemporaryFile texFile(dir + QDir::separator() + QLatin1String("cantor_tex-XXXXXX.tex")); texFile.open(); KColorScheme scheme(QPalette::Active); const QColor &backgroundColor=scheme.background().color(); const QColor &foregroundColor=scheme.foreground().color(); // Search preview.sty QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("latex/preview.sty")); if (file.isEmpty()) file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("cantor/latex/preview.sty")); if (file.isEmpty()) { result->successfull = false; result->errorMessage = QString::fromLatin1("needed for math render preview.sty file not found"); finalize(result); return; } QString expressionTex=mathTex; file.chop(4); //remove '.sty' extention expressionTex=expressionTex.arg(file); expressionTex=expressionTex .arg(backgroundColor.redF()).arg(backgroundColor.greenF()).arg(backgroundColor.blueF()) .arg(foregroundColor.redF()).arg(foregroundColor.greenF()).arg(foregroundColor.blueF()); switch(m_type) { case Cantor::LatexRenderer::FullEquation: expressionTex=expressionTex.arg(eqnHeader); break; case Cantor::LatexRenderer::InlineEquation: expressionTex=expressionTex.arg(inlineEqnHeader); break; } expressionTex=expressionTex.arg(m_code); texFile.write(expressionTex.toUtf8()); texFile.flush(); KProcess p; p.setWorkingDirectory(dir); // Create unique uuid for this job // It will be used as pdf filename, for preventing names collisions // And as internal url path too const QString& uuid = genUuid(); const QString& pdflatex = QStandardPaths::findExecutable(QLatin1String("pdflatex")); p << pdflatex << QStringLiteral("-interaction=batchmode") << QStringLiteral("-jobname=cantor_") + uuid << QStringLiteral("-halt-on-error") << texFile.fileName(); p.start(); p.waitForFinished(); if (p.exitCode() != 0) { // pdflatex render failed and we haven't pdf file result->successfull = false; result->errorMessage = QString::fromLatin1("pdflatex failed to render pdf and exit with code %1").arg(p.exitCode()); finalize(result); texFile.setAutoRemove(false); //Usefull for debug return; } //Clean up .aux and .log files QString pathWithoutExtention = dir + QDir::separator() + QLatin1String("cantor_")+uuid; QFile::remove(pathWithoutExtention + QLatin1String(".log")); QFile::remove(pathWithoutExtention + QLatin1String(".aux")); const QString& pdfFileName = pathWithoutExtention + QLatin1String(".pdf"); bool success; QString errorMessage; QSizeF size; result->image = renderPdf(pdfFileName, m_scale, m_highResolution, &success, &size, &errorMessage, m_mutex); result->successfull = success; result->errorMessage = errorMessage; if (success == false) { finalize(result); return; } const auto& data = renderPdfToFormat(pdfFileName, m_code, uuid, m_type, m_scale, m_highResolution, &success, &errorMessage, m_mutex); result->successfull = success; result->errorMessage = errorMessage; if (success == false) { finalize(result); return; } result->renderedMath = data.first; result->image = data.second; + result->jobId = m_jobId; QUrl internal; internal.setScheme(QLatin1String("internal")); internal.setPath(uuid); result->uniqueUrl = internal; finalize(result); } void MathRenderTask::finalize(QSharedPointer result) { emit finish(result); deleteLater(); } QImage MathRenderTask::renderPdf(const QString& filename, double scale, bool highResolution, bool* success, QSizeF* size, QString* errorReason, QMutex* mutex) { if (mutex) mutex->lock(); Poppler::Document* document = Poppler::Document::load(filename); if (mutex) mutex->unlock(); if (document == nullptr) { if (success) *success = false; if (errorReason) *errorReason = QString::fromLatin1("Poppler library have failed to open file % as pdf").arg(filename); return QImage(); } Poppler::Page* pdfPage = document->page(0); if (pdfPage == nullptr) { if (success) *success = false; if (errorReason) *errorReason = QString::fromLatin1("Poppler library failed to access first page of %1 document").arg(filename); delete document; return QImage(); } QSize pageSize = pdfPage->pageSize(); double realSclae; qreal w, h; if(highResolution) { realSclae = 1.2 * 5 * 1.8; w = 1.2 * pageSize.width(); h = 1.2 * pageSize.height(); } else { realSclae = 2.4 * scale * 1.8; w = 2.4 * pageSize.width(); h = 2.4 * pageSize.height(); } QImage image = pdfPage->renderToImage(72.0*realSclae, 72.0*realSclae); delete pdfPage; if (mutex) mutex->lock(); delete document; if (mutex) mutex->unlock(); if (image.isNull()) { if (success) *success = false; if (errorReason) *errorReason = QString::fromLatin1("Poppler library failed to render pdf %1 to image").arg(filename); return image; } // Resize with smooth transformation for more beautiful result image = image.convertToFormat(QImage::Format_ARGB32).scaled(image.size()/1.8, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); if (success) *success = true; if (size) *size = QSizeF(w, h); return image; } std::pair MathRenderTask::renderPdfToFormat(const QString& filename, const QString& code, const QString uuid, Cantor::LatexRenderer::EquationType type, double scale, bool highResulution, bool* success, QString* errorReason, QMutex* mutex) { QSizeF size; const QImage& image = renderPdf(filename, scale, highResulution, success, &size, errorReason, mutex); if (success && *success == false) return std::make_pair(QTextImageFormat(), QImage()); QTextImageFormat format; QUrl internal; internal.setScheme(QLatin1String("internal")); internal.setPath(uuid); format.setName(internal.url()); format.setWidth(size.width()); format.setHeight(size.height()); format.setProperty(EpsRenderer::CantorFormula, type); format.setProperty(EpsRenderer::ImagePath, filename); format.setProperty(EpsRenderer::Code, code); format.setVerticalAlignment(QTextCharFormat::AlignBaseline); switch(type) { case Cantor::LatexRenderer::FullEquation: format.setProperty(EpsRenderer::Delimiter, QLatin1String("$$")); break; case Cantor::LatexRenderer::InlineEquation: format.setProperty(EpsRenderer::Delimiter, QLatin1String("$")); break; } return std::make_pair(std::move(format), std::move(image)); } QString MathRenderTask::genUuid() { QString uuid = QUuid::createUuid().toString(); uuid.remove(0, 1); uuid.chop(1); uuid.replace(QLatin1Char('-'), QLatin1Char('_')); return uuid; } diff --git a/src/mathrendertask.h b/src/mathrendertask.h index bb120d69..f34a96e7 100644 --- a/src/mathrendertask.h +++ b/src/mathrendertask.h @@ -1,93 +1,96 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2019 Sirgienko Nikita */ #ifndef MATHRENDERTASK_H #define MATHRENDERTASK_H #include #include #include #include #include #include #include #include "lib/latexrenderer.h" class QMutex; struct MathRenderResult { + int jobId; bool successfull; QString errorMessage; QTextImageFormat renderedMath; QUrl uniqueUrl; QImage image; }; Q_DECLARE_METATYPE(MathRenderResult) Q_DECLARE_METATYPE(QSharedPointer) class MathRenderTask : public QObject, public QRunnable { Q_OBJECT public: MathRenderTask( + int jobId, const QString& code, Cantor::LatexRenderer::EquationType type, double scale, bool highResolution, QMutex* mutex ); void setHandler(const QObject *receiver, const char *resultHandler); void run() override; static QImage renderPdf( const QString& filename, double scale, bool highResulution, bool* success = nullptr, QSizeF* size = nullptr, QString* errorReason = nullptr, QMutex* mutex = nullptr ); static std::pair renderPdfToFormat( const QString& filename, const QString& code, const QString uuid, Cantor::LatexRenderer::EquationType type, double scale, bool highResulution, bool* success = nullptr, QString* errorReason = nullptr, QMutex* mutex = nullptr ); static QString genUuid(); Q_SIGNALS: void finish(QSharedPointer result); private: void finalize(QSharedPointer result); private: + int m_jobId; QString m_code; Cantor::LatexRenderer::EquationType m_type; double m_scale; bool m_highResolution; QMutex* m_mutex; }; #endif /* MATHRENDERTASK_H */ diff --git a/src/textentry.cpp b/src/textentry.cpp index 6207c2b3..2005f672 100644 --- a/src/textentry.cpp +++ b/src/textentry.cpp @@ -1,483 +1,475 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2009 Alexander Rieder Copyright (C) 2012 Martin Kuettler */ #include "textentry.h" #include "worksheettextitem.h" #include "epsrenderer.h" #include "latexrenderer.h" #include "jupyterutils.h" #include "mathrender.h" #include "settings.h" #include #include #include #include #include #include #include TextEntry::TextEntry(Worksheet* worksheet) : WorksheetEntry(worksheet) , m_convertCell(false) , m_convertTarget() , m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)) { m_textItem->enableRichText(true); connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &TextEntry::moveToPreviousEntry); connect(m_textItem, &WorksheetTextItem::moveToNext, this, &TextEntry::moveToNextEntry); connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate())); connect(m_textItem, &WorksheetTextItem::doubleClick, this, &TextEntry::resolveImagesAtCursor); } void TextEntry::populateMenu(QMenu* menu, QPointF pos) { bool imageSelected = false; QTextCursor cursor = m_textItem->textCursor(); const QChar repl = QChar::ObjectReplacementCharacter; if (cursor.hasSelection()) { QString selection = m_textItem->textCursor().selectedText(); imageSelected = selection.contains(repl); } else { // we need to try both the current cursor and the one after the that cursor = m_textItem->cursorForPosition(pos); qDebug() << cursor.position(); for (int i = 2; i; --i) { int p = cursor.position(); if (m_textItem->document()->characterAt(p-1) == repl && cursor.charFormat().hasProperty(EpsRenderer::CantorFormula)) { m_textItem->setTextCursor(cursor); imageSelected = true; break; } cursor.movePosition(QTextCursor::NextCharacter); } } if (imageSelected) { menu->addAction(i18n("Show LaTeX code"), this, SLOT(resolveImagesAtCursor())); menu->addSeparator(); } WorksheetEntry::populateMenu(menu, pos); } bool TextEntry::isEmpty() { return m_textItem->document()->isEmpty(); } int TextEntry::type() const { return Type; } bool TextEntry::acceptRichText() { return true; } bool TextEntry::focusEntry(int pos, qreal xCoord) { if (aboutToBeRemoved()) return false; m_textItem->setFocusAt(pos, xCoord); return true; } void TextEntry::setContent(const QString& content) { m_textItem->setPlainText(content); } void TextEntry::setContent(const QDomElement& content, const KZip& file) { Q_UNUSED(file); if(content.firstChildElement(QLatin1String("body")).isNull()) return; if (content.hasAttribute(QLatin1String("convertTarget"))) { m_convertCell = true; m_convertTarget = content.attribute(QLatin1String("convertTarget")); } else m_convertCell = false; QDomDocument doc = QDomDocument(); QDomNode n = doc.importNode(content.firstChildElement(QLatin1String("body")), true); doc.appendChild(n); QString html = doc.toString(); qDebug() << html; m_textItem->setHtml(html); } void TextEntry::setContentFromJupyter(const QJsonObject& cell) { if (JupyterUtils::isRawCell(cell)) { m_convertCell = true; const QJsonObject& metadata = cell.value(QLatin1String("metadata")).toObject(QJsonObject()); QJsonValue format = metadata.value(QLatin1String("format")); // Also checks "raw_mimetype", because raw cell don't corresponds Jupyter Notebook specification // See https://github.com/jupyter/notebook/issues/4730 if (format.isUndefined()) format = metadata.value(QLatin1String("raw_mimetype")); m_convertTarget = format.toString(QString()); m_textItem->setPlainText(JupyterUtils::getSource(cell)); } else if (JupyterUtils::isMarkdownCell(cell)) { m_convertCell = false; m_convertTarget.clear(); QJsonObject cantorMetadata = JupyterUtils::getCantorMetadata(cell); m_textItem->setHtml(cantorMetadata.value(QLatin1String("text_entry_content")).toString()); } } QJsonValue TextEntry::toJupyterJson() { // Simple logic: // If convertTarget is empty, it's user maded cell and we convert it to a markdown // If convertTarget setted, it's raw cell from Jupyter and we convert it to Jupyter cell QTextDocument* doc = m_textItem->document()->clone(); QTextCursor cursor = doc->find(QString(QChar::ObjectReplacementCharacter)); while(!cursor.isNull()) { QTextCharFormat format = cursor.charFormat(); if (format.hasProperty(EpsRenderer::CantorFormula)) { showLatexCode(cursor); } cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); } QJsonObject metadata; QString entryData; QString entryType; if (!m_convertCell) { entryType = QLatin1String("markdown"); // Add raw text of entry to metadata, for situation when // Cantor opens .ipynb converted from our .cws format QJsonObject cantorMetadata; if (Settings::storeTextEntryFormatting()) { entryData = doc->toHtml(); // Remove DOCTYPE from html entryData.remove(QRegExp(QLatin1String("]*>\\n"))); cantorMetadata.insert(QLatin1String("text_entry_content"), entryData); } else entryData = doc->toPlainText(); metadata.insert(JupyterUtils::cantorMetadataKey, cantorMetadata); // Replace our $$ formulas to $ entryData.replace(QLatin1String("$$"), QLatin1String("$")); } else { entryType = QLatin1String("raw"); metadata.insert(QLatin1String("format"), m_convertTarget); entryData = doc->toPlainText(); } QJsonObject entry; entry.insert(QLatin1String("cell_type"), entryType); entry.insert(QLatin1String("metadata"), metadata); JupyterUtils::setSource(entry, entryData); return entry; } QDomElement TextEntry::toXml(QDomDocument& doc, KZip* archive) { Q_UNUSED(archive); QScopedPointer document(m_textItem->document()->clone()); //make sure that the latex code is shown instead of the rendered formulas QTextCursor cursor = document->find(QString(QChar::ObjectReplacementCharacter)); while(!cursor.isNull()) { QTextCharFormat format = cursor.charFormat(); if (format.hasProperty(EpsRenderer::CantorFormula)) showLatexCode(cursor); cursor = document->find(QString(QChar::ObjectReplacementCharacter), cursor); } const QString& html = document->toHtml(); qDebug() << html; QDomElement el = doc.createElement(QLatin1String("Text")); QDomDocument myDoc = QDomDocument(); myDoc.setContent(html); el.appendChild(myDoc.documentElement().firstChildElement(QLatin1String("body"))); if (m_convertCell) el.setAttribute(QLatin1String("convertTarget"), m_convertTarget); return el; } QString TextEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) { Q_UNUSED(commandSep); if (commentStartingSeq.isEmpty()) return QString(); /* // would this be plain enough? QTextCursor cursor = m_textItem->textCursor(); cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); QString text = m_textItem->resolveImages(cursor); text.replace(QChar::ParagraphSeparator, '\n'); text.replace(QChar::LineSeparator, '\n'); */ QString text = m_textItem->toPlainText(); if (!commentEndingSeq.isEmpty()) return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n"); return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n"); } void TextEntry::interruptEvaluation() { } bool TextEntry::evaluate(EvaluationOption evalOp) { + int i = 0; if (worksheet()->embeddedMathEnabled()) { // Render math in $$...$$ via Latex QTextCursor cursor = findLatexCode(); while (!cursor.isNull()) { QString latexCode = cursor.selectedText(); qDebug()<<"found latex: " << latexCode; latexCode.remove(0, 2); latexCode.remove(latexCode.length() - 2, 2); latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); latexCode.replace(QChar::LineSeparator, QLatin1Char('\n')); MathRenderer* renderer = worksheet()->mathRenderer(); - renderer->renderExpression(latexCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer))); + renderer->renderExpression(++i, latexCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer))); + qDebug() << i; cursor = findLatexCode(cursor); } } evaluateNext(evalOp); return true; } void TextEntry::updateEntry() { qDebug() << "update Entry"; QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter)); while(!cursor.isNull()) { QTextImageFormat format=cursor.charFormat().toImageFormat(); - if (format.hasProperty(EpsRenderer::CantorFormula)) - { - qDebug() << "found a formula... rendering the eps..."; - const QUrl& url=QUrl::fromLocalFile(format.property(EpsRenderer::ImagePath).toString()); - QSizeF s = worksheet()->epsRenderer()->renderToResource(m_textItem->document(), url, QUrl(format.name())); - qDebug() << "rendering successful? " << s.isValid(); - //cursor.deletePreviousChar(); - //cursor.insertText(QString(QChar::ObjectReplacementCharacter), format); - } + if (format.hasProperty(EpsRenderer::CantorFormula)) + worksheet()->mathRenderer()->rerender(m_textItem->document(), format); cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); } } void TextEntry::resolveImagesAtCursor() { QTextCursor cursor = m_textItem->textCursor(); if (!cursor.hasSelection()) cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); cursor.insertText(m_textItem->resolveImages(cursor)); } QTextCursor TextEntry::findLatexCode(const QTextCursor& cursor) const { QTextDocument *doc = m_textItem->document(); QTextCursor startCursor; if (cursor.isNull()) startCursor = doc->find(QLatin1String("$$")); else startCursor = doc->find(QLatin1String("$$"), cursor); if (startCursor.isNull()) return startCursor; const QTextCursor endCursor = doc->find(QLatin1String("$$"), startCursor); if (endCursor.isNull()) return endCursor; startCursor.setPosition(startCursor.selectionStart()); startCursor.setPosition(endCursor.position(), QTextCursor::KeepAnchor); return startCursor; } QString TextEntry::showLatexCode(QTextCursor& cursor) { QString latexCode = cursor.charFormat().property(EpsRenderer::Code).toString(); cursor.deletePreviousChar(); latexCode = QLatin1String("$$") + latexCode + QLatin1String("$$"); cursor.insertText(latexCode); return latexCode; } int TextEntry::searchText(const QString& text, const QString& pattern, QTextDocument::FindFlags qt_flags) { Qt::CaseSensitivity caseSensitivity; if (qt_flags & QTextDocument::FindCaseSensitively) caseSensitivity = Qt::CaseSensitive; else caseSensitivity = Qt::CaseInsensitive; int position; if (qt_flags & QTextDocument::FindBackward) position = text.lastIndexOf(pattern, -1, caseSensitivity); else position = text.indexOf(pattern, 0, caseSensitivity); return position; } WorksheetCursor TextEntry::search(const QString& pattern, unsigned flags, QTextDocument::FindFlags qt_flags, const WorksheetCursor& pos) { if (!(flags & WorksheetEntry::SearchText) || (pos.isValid() && pos.entry() != this)) return WorksheetCursor(); QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos); int position = 0; QTextCursor latexCursor; QString latex; if (flags & WorksheetEntry::SearchLaTeX) { const QString repl = QString(QChar::ObjectReplacementCharacter); latexCursor = m_textItem->search(repl, qt_flags, pos); while (!latexCursor.isNull()) { latex = m_textItem->resolveImages(latexCursor); position = searchText(latex, pattern, qt_flags); if (position >= 0) { break; } WorksheetCursor c(this, m_textItem, latexCursor); latexCursor = m_textItem->search(repl, qt_flags, c); } } if (latexCursor.isNull()) { if (textCursor.isNull()) return WorksheetCursor(); else return WorksheetCursor(this, m_textItem, textCursor); } else { if (textCursor.isNull() || latexCursor < textCursor) { int start = latexCursor.selectionStart(); latexCursor.insertText(latex); QTextCursor c = m_textItem->textCursor(); c.setPosition(start + position); c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, pattern.length()); return WorksheetCursor(this, m_textItem, c); } else { return WorksheetCursor(this, m_textItem, textCursor); } } } void TextEntry::layOutForWidth(qreal w, bool force) { if (size().width() == w && !force) return; m_textItem->setGeometry(0, 0, w); setSize(QSizeF(m_textItem->width(), m_textItem->height() + VerticalMargin)); } bool TextEntry::wantToEvaluate() { return !findLatexCode().isNull(); } bool TextEntry::isConvertableToTextEntry(const QJsonObject& cell) { if (!JupyterUtils::isMarkdownCell(cell)) return false; QJsonObject cantorMetadata = JupyterUtils::getCantorMetadata(cell); const QJsonValue& textContentValue = cantorMetadata.value(QLatin1String("text_entry_content")); if (!textContentValue.isString()) return false; const QString& textContent = textContentValue.toString(); const QString& source = JupyterUtils::getSource(cell); return textContent == source; } void TextEntry::handleMathRender(QSharedPointer result) { if (!result->successfull) { qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage; return; } const QString& code = result->renderedMath.property(EpsRenderer::Code).toString(); - // Jupyter TODO: add support for $...$ math and starts use - // result->renderedMath.property(EpsRenderer::Delimiter).toString(); const QString& delimiter = QLatin1String("$$"); QTextCursor cursor = m_textItem->document()->find(delimiter + code + delimiter); if (!cursor.isNull()) { m_textItem->document()->addResource(QTextDocument::ImageResource, result->uniqueUrl, QVariant(result->image)); - // Jupyter TODO: add support for $...$ math and don't change delimiter here (needed for image resolving) result->renderedMath.setProperty(EpsRenderer::Delimiter, QLatin1String("$$")); cursor.insertText(QString(QChar::ObjectReplacementCharacter), result->renderedMath); } }