No OneTemporary

File Metadata

Created
Fri, May 3, 4:30 PM
diff --git a/src/markdownentry.cpp b/src/markdownentry.cpp
index f715d6ec..c0f7d98b 100644
--- a/src/markdownentry.cpp
+++ b/src/markdownentry.cpp
@@ -1,723 +1,734 @@
/*
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 <kqwyfg@gmail.com>
*/
#include "markdownentry.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QImage>
#include <QImageReader>
#include <QBuffer>
#include <KLocalizedString>
#include <QDebug>
+#include <QKeyEvent>
#include <QStandardPaths>
#include <QDir>
#include <QFileDialog>
#include <KMessageBox>
#include "jupyterutils.h"
#include "mathrender.h"
#include <config-cantor.h>
#include "settings.h"
#ifdef Discount_FOUND
extern "C" {
#include <mkdio.h>
}
#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)
{
if (!rendered)
menu->addAction(i18n("Insert Image Attachment"), this, &MarkdownEntry::insertImage);
if (attachedImages.size() != 0)
menu->addAction(i18n("Clear Attachments"), this, &MarkdownEntry::clearAttachments);
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();
const QString mathCode = math.text();
foundMath.push_back(std::make_pair(mathCode, false));
}
if (rendered)
{
markUpMath();
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();
if (mathRendered)
{
const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path")));
if (imageEntry && imageEntry->isFile())
{
const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(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' extension
// 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 if (worksheet()->embeddedMathEnabled())
renderMathExpression(i+1, mathCode);
}
}
}
// Because, all previous actions was on load stage,
// them shoudl unconverted by user
m_textItem->document()->clearUndoRedoStacks();
}
void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell)
{
if (!Cantor::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
// So we just store it
setJupyterMetadata(Cantor::JupyterUtils::getMetadata(cell));
const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject();
for (const QString& key : attachments.keys())
{
const QJsonValue& attachment = attachments.value(key);
const QString& mimeKey = Cantor::JupyterUtils::firstImageKey(attachment);
if (!mimeKey.isEmpty())
{
const QImage& image = Cantor::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(Cantor::JupyterUtils::getSource(cell));
m_textItem->document()->clearUndoRedoStacks();
}
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<QImage>();
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(Cantor::Renderer::CantorFormula))
{
const QString& latex = format.property(Cantor::Renderer::Code).toString();
const QString& delimiter = format.property(Cantor::Renderer::Delimiter).toString();
const QString& code = delimiter + latex + delimiter;
if (code == data.first)
{
const QUrl& url = QUrl::fromLocalFile(format.property(Cantor::Renderer::ImagePath).toString());
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"), jupyterMetadata());
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<QImage>();
QString attachmentKey = url.toString().remove(QLatin1String("attachment:"));
attachments.insert(attachmentKey, Cantor::JupyterUtils::packMimeBundle(image, key));
}
if (!attachments.isEmpty())
entry.insert(QLatin1String("attachments"), attachments);
Cantor::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);
}
m_textItem->document()->clearUndoRedoStacks();
}
if (rendered && 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();
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(Cantor::Renderer::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<QGraphicsSceneMouseEvent*>(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;
}
}
}
+ else if (event->type() == QEvent::KeyPress)
+ {
+ auto* key_event = static_cast<QKeyEvent*>(event);
+ if (key_event->matches(QKeySequence::Cancel))
+ {
+ setRenderedHtml(html);
+ rendered = true;
+ 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::renderMath()
{
QTextCursor cursor(m_textItem->document());
for (int i = 0; i < (int)foundMath.size(); i++)
if (foundMath[i].second == false)
renderMathExpression(i+1, foundMath[i].first);
}
void MarkdownEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
{
if (!result->successful)
{
if (Settings::self()->showMathRenderError())
KMessageBox::error(worksheetView(), result->errorMessage, i18n("Cantor Math Error"));
else
qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage;
return;
}
setRenderedMath(result->jobId, result->renderedMath, result->uniqueUrl, result->image);
}
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(jobId, latex, type, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
}
std::pair<QString, Cantor::LatexRenderer::EquationType> 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);
if (mathCode[0] == QChar(6))
mathCode.remove(0, 1);
return std::make_pair(mathCode, Cantor::LatexRenderer::FullEquation);
}
else if (mathCode.startsWith(inlineDelimiter) && mathCode.endsWith(inlineDelimiter))
{
mathCode.remove(0, 1);
mathCode.chop(1);
if (mathCode[0] == QChar(6))
mathCode.remove(0, 1);
return std::make_pair(mathCode, Cantor::LatexRenderer::InlineEquation);
}
else if (mathCode.startsWith(QString::fromUtf8("\\begin{")) && mathCode.endsWith(QLatin1Char('}')))
{
if (mathCode[1] == QChar(6))
mathCode.remove(1, 1);
return std::make_pair(mathCode, Cantor::LatexRenderer::CustomEquation);
}
else
return std::make_pair(QString(), Cantor::LatexRenderer::InlineEquation);
}
void MarkdownEntry::setRenderedMath(int jobId, const QTextImageFormat& format, const QUrl& internal, const QImage& image)
{
if ((int)foundMath.size() < jobId)
return;
const auto& iter = foundMath.begin() + jobId-1;
QTextCursor cursor = findMath(jobId);
const QString delimiter = format.property(Cantor::Renderer::Delimiter).toString();
QString searchText = delimiter + format.property(Cantor::Renderer::Code).toString() + delimiter;
Cantor::LatexRenderer::EquationType type
= (Cantor::LatexRenderer::EquationType)format.intProperty(Cantor::Renderer::CantorFormula);
// From findMath we will be first symbol of math expression
// So in order to select all symbols of the expression, we need to go to previous symbol first
// But it working strange sometimes: some times we need to go to previous character, sometimes not
// So the code tests that we on '$' symbol and if it isn't true, then we revert back
cursor.movePosition(QTextCursor::PreviousCharacter);
bool withDollarDelimiter = type == Cantor::LatexRenderer::InlineEquation || type == Cantor::LatexRenderer::FullEquation;
if (withDollarDelimiter && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('$'))
cursor.movePosition(QTextCursor::NextCharacter);
else if (type == Cantor::LatexRenderer::CustomEquation && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('\\') )
cursor.movePosition(QTextCursor::NextCharacter);
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, searchText.size());
if (!cursor.isNull())
{
m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
// Don't add new line for $$...$$ on document's begin and end
// And if we in block, which haven't non-space characters except out math expression
// In another sitation, Cantor will move rendered image into another QTextBlock
QTextCursor prevSymCursor = m_textItem->document()->find(QRegExp(QLatin1String("[^\\s]")), cursor, QTextDocument::FindBackward);
if (type == Cantor::LatexRenderer::FullEquation
&& cursor.selectionStart() != 0
&& prevSymCursor.block() == cursor.block()
)
{
cursor.insertBlock();
cursor.setPosition(prevSymCursor.position()+2, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
}
cursor.insertText(QString(QChar::ObjectReplacementCharacter), format);
bool atDocEnd = cursor.position() == m_textItem->document()->characterCount()-1;
QTextCursor nextSymCursor = m_textItem->document()->find(QRegExp(QLatin1String("[^\\s]")), cursor);
if (type == Cantor::LatexRenderer::FullEquation && !atDocEnd && nextSymCursor.block() == cursor.block())
{
cursor.setPosition(nextSymCursor.position()-1, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
cursor.insertBlock();
}
// Set that the formulas is rendered
iter->second = true;
m_textItem->document()->clearUndoRedoStacks();
}
}
QTextCursor MarkdownEntry::findMath(int id)
{
QTextCursor cursor(m_textItem->document());
do
{
QTextCharFormat format = cursor.charFormat();
if (format.intProperty(JobProperty) == id)
break;
}
while (cursor.movePosition(QTextCursor::NextCharacter));
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);
// Mark up founded math code
QTextCharFormat format = cursor.charFormat();
// Use index+1 in math array as property tag
format.setProperty(JobProperty, i+1);
// We found the math expression, so remove 'marker' (ACII symbol 'Acknowledgement')
// The marker have been placed after "$" or "$$"
// We remove the marker, only if it presents
QString codeWithoutMarker = foundMath[i].first;
if (searchText.startsWith(QLatin1String("$$")))
{
if (codeWithoutMarker[2] == QChar(6))
codeWithoutMarker.remove(2, 1);
}
else if (searchText.startsWith(QLatin1String("$")))
{
if (codeWithoutMarker[1] == QChar(6))
codeWithoutMarker.remove(1, 1);
}
else if (searchText.startsWith(QLatin1String("\\")))
{
if (codeWithoutMarker[1] == QChar(6))
codeWithoutMarker.remove(1, 1);
}
cursor.insertText(codeWithoutMarker, format);
}
}
void MarkdownEntry::insertImage()
{
const QString& filename = QFileDialog::getOpenFileName(worksheet()->worksheetView(), i18n("Choose Image"), QString(), i18n("Images (*.png *.bmp *.jpg *.svg)"));
if (!filename.isEmpty())
{
QImageReader reader(filename);
const QImage img = reader.read();
if (!img.isNull())
{
const QString& name = QFileInfo(filename).fileName();
QUrl url;
url.setScheme(QLatin1String("attachment"));
url.setPath(name);
attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(img));
QTextCursor cursor = m_textItem->textCursor();
cursor.insertText(QString::fromLatin1("![%1](attachment:%1)").arg(name));
animateSizeChange();
}
else
KMessageBox::error(worksheetView(), i18n("Cantor failed to read image with error \"%1\"", reader.errorString()), i18n("Cantor"));
}
}
void MarkdownEntry::clearAttachments()
{
for (auto& attachment: attachedImages)
{
const QUrl& url = attachment.first;
m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant());
}
attachedImages.clear();
animateSizeChange();
}
QString MarkdownEntry::plainText() const
{
return m_textItem->toPlainText();
}