diff --git a/src/jupyterutils.cpp b/src/jupyterutils.cpp index 7a4727e7..58abae38 100644 --- a/src/jupyterutils.cpp +++ b/src/jupyterutils.cpp @@ -1,324 +1,330 @@ /* 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 "jupyterutils.h" #include "lib/backend.h" #include #include #include #include #include #include #include #include #include #include #include const QString JupyterUtils::cellsKey = QLatin1String("cells"); const QString JupyterUtils::metadataKey = QLatin1String("metadata"); const QString JupyterUtils::cantorMetadataKey = QLatin1String("cantor"); const QString JupyterUtils::nbformatKey = QLatin1String("nbformat"); const QString JupyterUtils::nbformatMinorKey = QLatin1String("nbformat_minor"); const QString JupyterUtils::cellTypeKey = QLatin1String("cell_type"); const QString JupyterUtils::sourceKey = QLatin1String("source"); const QString JupyterUtils::outputTypeKey = QLatin1String("output_type"); const QString JupyterUtils::executionCountKey = QLatin1String("execution_count"); const QString JupyterUtils::outputsKey = QLatin1String("outputs"); const QString JupyterUtils::dataKey = QLatin1String("data"); const QString JupyterUtils::pngMime = QLatin1String("image/png"); const QString JupyterUtils::textMime = QLatin1String("text/plain"); const QMimeDatabase JupyterUtils::mimeDatabase; QJsonValue JupyterUtils::toJupyterMultiline(const QString& source) { if (source.contains(QLatin1Char('\n'))) { QJsonArray text; const QStringList& lines = source.split(QLatin1Char('\n')); for (int i = 0; i < lines.size(); i++) { QString line = lines[i]; // Don't add \n to last line if (i != lines.size() - 1) line.append(QLatin1Char('\n')); text.append(line); } return text; } else return QJsonValue(source); } QString JupyterUtils::fromJupyterMultiline(const QJsonValue& source) { QString code; if (source.isString()) code = source.toString(); else if (source.isArray()) for (const QJsonValue& line : source.toArray()) code += line.toString(); return code; } bool JupyterUtils::isJupyterNotebook(const QJsonDocument& doc) { static const QSet notebookScheme = QSet::fromList({cellsKey, metadataKey, nbformatKey, nbformatMinorKey}); bool isNotebook = doc.isObject() && QSet::fromList(doc.object().keys()) == notebookScheme && doc.object().value(cellsKey).isArray() && doc.object().value(metadataKey).isObject() && doc.object().value(nbformatKey).isDouble() && doc.object().value(nbformatMinorKey).isDouble(); return isNotebook; } bool JupyterUtils::isJupyterCell(const QJsonValue& cell) { bool isCell = cell.isObject() && cell.toObject().value(cellTypeKey).isString() && ( cell.toObject().value(cellTypeKey).toString() == QLatin1String("markdown") || cell.toObject().value(cellTypeKey).toString() == QLatin1String("code") || cell.toObject().value(cellTypeKey).toString() == QLatin1String("raw") ) && cell.toObject().value(metadataKey).isObject() && ( cell.toObject().value(sourceKey).isString() || cell.toObject().value(sourceKey).isArray() ); return isCell; } bool JupyterUtils::isJupyterOutput(const QJsonValue& output) { bool isOutput = output.isObject() && output.toObject().value(outputTypeKey).isString() && ( output.toObject().value(outputTypeKey).toString() == QLatin1String("stream") || output.toObject().value(outputTypeKey).toString() == QLatin1String("display_data") || output.toObject().value(outputTypeKey).toString() == QLatin1String("execute_result") || output.toObject().value(outputTypeKey).toString() == QLatin1String("error") ); return isOutput; } bool JupyterUtils::isJupyterDisplayOutput(const QJsonValue& output) { return isJupyterOutput(output) && output.toObject().value(outputTypeKey).toString() == QLatin1String("display_data") && output.toObject().value(metadataKey).isObject() && output.toObject().value(QLatin1String("data")).isObject(); } bool JupyterUtils::isMarkdownCell(const QJsonValue& cell) { return isJupyterCell(cell) && getCellType(cell.toObject()) == QLatin1String("markdown"); } bool JupyterUtils::isCodeCell(const QJsonValue& cell) { return isJupyterCell(cell) && getCellType(cell.toObject()) == QLatin1String("code") && ( cell.toObject().value(executionCountKey).isDouble() || cell.toObject().value(executionCountKey).isNull() ) && cell.toObject().value(outputsKey).isArray(); } bool JupyterUtils::isRawCell(const QJsonValue& cell) { return isJupyterCell(cell) && getCellType(cell.toObject()) == QLatin1String("raw"); } QJsonObject JupyterUtils::getMetadata(const QJsonObject& object) { return object.value(metadataKey).toObject(); } QJsonArray JupyterUtils::getCells(const QJsonObject notebook) { return notebook.value(cellsKey).toArray(); } std::tuple JupyterUtils::getNbformatVersion(const QJsonObject& notebook) { int nbformatMajor = notebook.value(nbformatKey).toInt(); int nbformatMinor = notebook.value(nbformatMinorKey).toInt(); return {nbformatMajor, nbformatMinor}; } QString JupyterUtils::getCellType(const QJsonObject& cell) { return cell.value(cellTypeKey).toString(); } QString JupyterUtils::getSource(const QJsonObject& cell) { return fromJupyterMultiline(cell.value(sourceKey)); } void JupyterUtils::setSource(QJsonObject& cell, const QString& source) { cell.insert(sourceKey, toJupyterMultiline(source)); } QString JupyterUtils::getOutputType(const QJsonObject& output) { return output.value(outputTypeKey).toString(); } QJsonObject JupyterUtils::getCantorMetadata(const QJsonObject object) { return getMetadata(object).value(cantorMetadataKey).toObject(); } QString JupyterUtils::getKernelName(const QJsonValue& kernelspecValue) { QString name; if (kernelspecValue.isObject()) { const QJsonObject& kernelspec = kernelspecValue.toObject(); QString kernelName = kernelspec.value(QLatin1String("name")).toString(); if (!kernelName.isEmpty()) { if (kernelName.startsWith(QLatin1String("julia"))) kernelName = QLatin1String("julia"); else if (kernelName == QLatin1String("sagemath")) kernelName = QLatin1String("sage"); else if (kernelName == QLatin1String("ir")) kernelName = QLatin1String("r"); name = kernelName; } else { name = kernelspec.value(QLatin1String("language")).toString(); } } return name; } QJsonObject JupyterUtils::getKernelspec(const Cantor::Backend* backend) { QJsonObject kernelspec; if (backend) { QString id = backend->id(); if (id == QLatin1String("sage")) id = QLatin1String("sagemath"); else if (id == QLatin1String("r")) id = QLatin1String("ir"); kernelspec.insert(QLatin1String("name"), id); QString lang = backend->id(); if (lang.startsWith(QLatin1String("python"))) lang = QLatin1String("python"); lang[0] = lang[0].toUpper(); kernelspec.insert(QLatin1String("language"), lang); kernelspec.insert(QLatin1String("display_name"), backend->name()); } return kernelspec; } QImage JupyterUtils::loadImage(const QJsonValue& mimeBundle, const QString& key) { QImage image; if (mimeBundle.isObject()) { const QJsonObject& bundleObject = mimeBundle.toObject(); const QJsonValue& data = bundleObject.value(key); if (data.isString()) { // In jupyter mime-bundle key for data is mime type of this data // So we need convert mimetype to format, for example "image/png" to "png" // for loading from data if (QImageReader::supportedMimeTypes().contains(key.toLatin1())) { // https://doc.qt.io/qt-5/qimagereader.html#supportedImageFormats // Maybe there is a better way to convert image key to image format // but this is all that I could to do const QByteArray& format = mimeDatabase.mimeTypeForName(key).preferredSuffix().toLatin1(); const QString& base64 = data.toString(); image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), format.data()); } } } return image; } QJsonObject JupyterUtils::packMimeBundle(const QImage& image, const QString& mime) { QJsonObject mimeBundle; if (QImageWriter::supportedMimeTypes().contains(mime.toLatin1())) { const QByteArray& format = mimeDatabase.mimeTypeForName(mime).preferredSuffix().toLatin1(); QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, format.data()); mimeBundle.insert(mime, QString::fromLatin1(ba.toBase64())); } return mimeBundle; } QStringList JupyterUtils::imageKeys(const QJsonValue& mimeBundle) { QStringList imageKeys; if (mimeBundle.isObject()) { const QStringList& keys = mimeBundle.toObject().keys(); const QList& mimes = QImageReader::supportedMimeTypes(); for (const QString& key : keys) if (mimes.contains(key.toLatin1())) imageKeys.append(key); } return imageKeys; } + +QString JupyterUtils::firstImageKey(const QJsonValue& mimeBundle) +{ + const QStringList& keys = imageKeys(mimeBundle); + return keys.size() >= 1 ? keys[0] : QString(); +} diff --git a/src/jupyterutils.h b/src/jupyterutils.h index 4deafe37..887ddfff 100644 --- a/src/jupyterutils.h +++ b/src/jupyterutils.h @@ -1,100 +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) 2019 Sirgienko Nikita */ #ifndef JUPYTERUTILS_H #define JUPYTERUTILS_H #include #include #include class QJsonValue; class QJsonObject; class QJsonArray; class QJsonDocument; class QImage; class QStringList; namespace Cantor { class Backend; } /** * Static class for storing some common code for working with jupyter json scheme * Like getting 'metadata', getting source code from 'source' tag, scheme validation * handleling missing keys, etc. * */ class JupyterUtils { public: static QJsonObject getMetadata(const QJsonObject& object); static QJsonObject getCantorMetadata(const QJsonObject object); static QJsonArray getCells(const QJsonObject notebook); static std::tuple getNbformatVersion(const QJsonObject& notebook); static QString getCellType(const QJsonObject& cell); static QString getSource(const QJsonObject& cell); static void setSource(QJsonObject& cell, const QString& source); static QString getOutputType(const QJsonObject& output); static bool isJupyterNotebook(const QJsonDocument& doc); static bool isJupyterCell(const QJsonValue& cell); static bool isMarkdownCell(const QJsonValue& cell); static bool isCodeCell(const QJsonValue& cell); static bool isRawCell(const QJsonValue& cell); static bool isJupyterOutput(const QJsonValue& output); static bool isJupyterDisplayOutput(const QJsonValue& output); static QJsonValue toJupyterMultiline(const QString& source); static QString fromJupyterMultiline(const QJsonValue& source); static QString getKernelName(const QJsonValue& kernelspecValue); static QJsonObject getKernelspec(const Cantor::Backend* backend); static QImage loadImage(const QJsonValue& mimeBundle, const QString& key); static QJsonObject packMimeBundle(const QImage& image, const QString& mime); static QStringList imageKeys(const QJsonValue& mimeBundle); - + static QString firstImageKey(const QJsonValue& mimeBundle); public: static const QString cellsKey; static const QString metadataKey; static const QString cantorMetadataKey; static const QString nbformatKey; static const QString nbformatMinorKey; static const QString cellTypeKey; static const QString sourceKey; static const QString outputTypeKey; static const QString executionCountKey; static const QString outputsKey; static const QString dataKey; static const QString pngMime; static const QString textMime; static const QMimeDatabase mimeDatabase; }; #endif // JUPYTERUTILS_H diff --git a/src/lib/textresult.cpp b/src/lib/textresult.cpp index 923ba257..fb7cf4e7 100644 --- a/src/lib/textresult.cpp +++ b/src/lib/textresult.cpp @@ -1,161 +1,166 @@ /* 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 */ #include "textresult.h" using namespace Cantor; #include #include #include #include #include QString rtrim(const QString& s) { QString result = s; while (result.count() > 0 && result[result.count()-1].isSpace() ) { result = result.left(result.count() -1 ); } return result; } class Cantor::TextResultPrivate { public: TextResultPrivate() { format=TextResult::PlainTextFormat; } QString data; QString plain; TextResult::Format format; }; TextResult::TextResult(const QString& data) : d(new TextResultPrivate) { d->data=rtrim(data); d->plain=d->data; } TextResult::TextResult(const QString& data, const QString& plain) : d(new TextResultPrivate) { d->data=rtrim(data); d->plain=rtrim(plain); } TextResult::~TextResult() { delete d; } QString TextResult::toHtml() { QString s=d->data.toHtmlEscaped(); s.replace(QLatin1Char('\n'), QLatin1String("
\n")); s.replace(QLatin1Char(' '), QLatin1String(" ")); return s; } QVariant TextResult::data() { return QVariant(d->data); } QString TextResult::plain() { return d->plain; } int TextResult::type() { return TextResult::Type; } QString TextResult::mimeType() { qDebug()<<"format: "<format; } void TextResult::setFormat(TextResult::Format f) { d->format=f; } QDomElement TextResult::toXml(QDomDocument& doc) { qDebug()<<"saving textresult "<data.split(QLatin1Char('\n')); - for (int i = 0; i < lines.size(); i++) + if (lines.size() == 1) + text = lines[0]; + else { - QString line = lines[i]; - // Don't add \n to last line - if (i != lines.size() - 1) - line.append(QLatin1Char('\n')); - text.append(line); + QJsonArray array; + for (int i = 0; i < lines.size(); i++) + { + QString line = lines[i]; + // Don't add \n to last line + if (i != lines.size() - 1) + line.append(QLatin1Char('\n')); + array.append(line); + } + text = array; } root.insert(QLatin1String("text"), text); return root; } void TextResult::save(const QString& filename) { QFile file(filename); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; QTextStream stream(&file); stream<data; file.close(); } diff --git a/src/loadedexpression.cpp b/src/loadedexpression.cpp index d972aee2..d8d8d5c3 100644 --- a/src/loadedexpression.cpp +++ b/src/loadedexpression.cpp @@ -1,176 +1,176 @@ /* 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 */ #include "loadedexpression.h" #include "jupyterutils.h" #include "lib/imageresult.h" #include "lib/epsresult.h" #include "lib/textresult.h" #include "lib/latexresult.h" #include "lib/animationresult.h" #include #include #include #include #include #include #include #include LoadedExpression::LoadedExpression( Cantor::Session* session ) : Cantor::Expression( session, false, -1) { } void LoadedExpression::interrupt() { //Do nothing } void LoadedExpression::evaluate() { //Do nothing } void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file) { setCommand(xml.firstChildElement(QLatin1String("Command")).text()); const QDomNodeList& results = xml.elementsByTagName(QLatin1String("Result")); for (int i = 0; i < results.size(); i++) { const QDomElement& resultElement = results.at(i).toElement(); const QString& type = resultElement.attribute(QLatin1String("type")); if ( type == QLatin1String("text")) { addResult(new Cantor::TextResult(resultElement.text())); } else if (type == QLatin1String("image") || type == QLatin1String("latex") || type == QLatin1String("animation")) { const KArchiveEntry* imageEntry=file.directory()->entry(resultElement.attribute(QLatin1String("filename"))); if (imageEntry&&imageEntry->isFile()) { const KArchiveFile* imageFile=static_cast(imageEntry); QString dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation); imageFile->copyTo(dir); QUrl imageUrl = QUrl::fromLocalFile(QDir(dir).absoluteFilePath(imageFile->name())); if(type==QLatin1String("latex")) { addResult(new Cantor::LatexResult(resultElement.text(), imageUrl)); }else if(type==QLatin1String("animation")) { addResult(new Cantor::AnimationResult(imageUrl)); }else if(imageFile->name().endsWith(QLatin1String(".eps"))) { addResult(new Cantor::EpsResult(imageUrl)); }else { addResult(new Cantor::ImageResult(imageUrl)); } } } } const QDomElement& errElem = xml.firstChildElement(QLatin1String("Error")); if (!errElem.isNull()) { setErrorMessage(errElem.text()); setStatus(Error); } else setStatus(Done); } void LoadedExpression::loadFromJupyter(const QJsonObject& cell) { setCommand(JupyterUtils::getSource(cell)); const QJsonValue idObject = cell.value(QLatin1String("execution_count")); if (!idObject.isUndefined() && !idObject.isNull()) setId(idObject.toInt()); const QJsonArray& outputs = cell.value(QLatin1String("outputs")).toArray(); for (QJsonArray::const_iterator iter = outputs.begin(); iter != outputs.end(); iter++) { if (!JupyterUtils::isJupyterOutput(*iter)) continue; const QJsonObject& output = iter->toObject(); const QString& outputType = JupyterUtils::getOutputType(output); if (outputType == QLatin1String("stream")) { const QString& text = JupyterUtils::fromJupyterMultiline(output.value(QLatin1String("text"))); addResult(new Cantor::TextResult(text)); } else if (outputType == QLatin1String("error")) { - //Jupyter TODO: there are colors in tracback, add support for them const QJsonArray& tracebackLineArray = output.value(QLatin1String("traceback")).toArray(); QString traceback; - // Jupyter TODO: is this \n necessary? + + // Looks like the traceback in Jupyter joined with '\n', no '' + // So, manually add it for (const QJsonValue& line : tracebackLineArray) traceback += line.toString() + QLatin1Char('\n'); traceback.chop(1); - // Jupyter TODO: IPython return error with terminal colors, should Cantor handle it? + // IPython returns error with terminal colors, we handle it here, but should we? static const QChar ESC(0x1b); traceback.remove(QRegExp(QString(ESC)+QLatin1String("\\[[0-9;]*m"))); setErrorMessage(traceback.toHtmlEscaped().replace(QLatin1String("\n"), QLatin1String("
"))); } else if (outputType == QLatin1String("display_data") || outputType == QLatin1String("execute_result")) { const QJsonObject& data = output.value(QLatin1String("data")).toObject(); const QString& text = JupyterUtils::fromJupyterMultiline(data.value(JupyterUtils::textMime)); - const QStringList& imageKeys = JupyterUtils::imageKeys(data); - // Jupyter TODO: what if keys will be more that 2? - if (imageKeys.size() >= 1) + const QString& imageKey = JupyterUtils::firstImageKey(data); + if (!imageKey.isEmpty()) { - QImage image = JupyterUtils::loadImage(data, imageKeys[0]); + QImage image = JupyterUtils::loadImage(data, imageKey); const QJsonObject& metadata = JupyterUtils::getMetadata(output); const QJsonValue size = metadata.value(JupyterUtils::pngMime); if (size.isObject()) { int w = size.toObject().value(QLatin1String("width")).toInt(); int h = size.toObject().value(QLatin1String("height")).toInt(); if (w != 0 && h != 0) image = image.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } addResult(new Cantor::ImageResult(image, text)); } else if (!text.isEmpty()) { addResult(new Cantor::TextResult(text)); } } } if (errorMessage().isEmpty()) setStatus(Done); else setStatus(Error); } diff --git a/src/markdownentry.cpp b/src/markdownentry.cpp index 03b08a4a..2a8cac6d 100644 --- a/src/markdownentry.cpp +++ b/src/markdownentry.cpp @@ -1,544 +1,542 @@ /* 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 "jupyterutils.h" #include #ifdef Discount_FOUND extern "C" { #include } #endif #include 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())); } 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) { Q_UNUSED(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. } if(rendered) setRenderedHtml(html); else setPlainText(plain); } 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 QStringList& keys = JupyterUtils::imageKeys(attachment); - // Jupyter TODO: what if keys will be 2? - // Is it valid scheme at all? - if (keys.size() == 1) + const QString& mimeKey = JupyterUtils::firstImageKey(attachment); + if (!mimeKey.isEmpty()) { - const QString& mimeKey = keys[0]; 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)); } } + // https://github.com/Orc/discount/issues/211 + // Jupyter TODO: replace this ugly code by Discount '$' support, if the issue will be merged into master setPlainText(adaptJupyterMarkdown(JupyterUtils::getSource(cell))); } QDomElement MarkdownEntry::toXml(QDomDocument& doc, KZip* archive) { Q_UNUSED(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); if(rendered) { QDomElement htmlEl = doc.createElement(QLatin1String("HTML")); htmlEl.appendChild(doc.createTextNode(html)); el.appendChild(htmlEl); } 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); QString source = plain; // Replace our $$ formulas to $ // Better, that if $$ will become $ than $ will become $$ source.replace(QLatin1String("$$"), QLatin1String("$")); JupyterUtils::setSource(entry, source); 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) { setRenderedHtml(html); rendered = true; } else { plain = m_textItem->toPlainText(); rendered = renderMarkdown(plain); } } if (rendered) { // 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')); Cantor::LatexRenderer* renderer=new Cantor::LatexRenderer(this); renderer->setLatexCode(latexCode); renderer->setEquationOnly(true); renderer->setEquationType(Cantor::LatexRenderer::InlineEquation); renderer->setMethod(Cantor::LatexRenderer::LatexMethod); renderer->renderBlocking(); bool success; QTextImageFormat formulaFormat; if (renderer->renderingSuccessful()) { EpsRenderer* epsRend = worksheet()->epsRenderer(); formulaFormat = epsRend->render(m_textItem->document(), renderer); success = !formulaFormat.name().isEmpty(); } else { success = false; } qDebug()<<"rendering successful? "<document()->find(QString(QChar::ObjectReplacementCharacter)); while(!cursor.isNull()) { QTextImageFormat format=cursor.charFormat().toImageFormat(); if (format.hasProperty(EpsRenderer::CantorFormula)) { const QUrl& url=QUrl::fromLocalFile(format.property(EpsRenderer::ImagePath).toString()); worksheet()->epsRenderer()->renderToResource(m_textItem->document(), url, QUrl(format.name())); } 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(); } QTextCursor MarkdownEntry::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; } enum class ParserState {Text, CodeQuote, SingleDollar, DoubleDollar}; QString MarkdownEntry::adaptJupyterMarkdown(const QString& markdown) { QString input = markdown; QString tail, out; do { out += convert(input, tail); input = tail; } while (!tail.isEmpty()); out.replace(QLatin1String("\\\\$"), QLatin1String("$")); return out; } QString MarkdownEntry::convert(const QString& markdown, QString& tail) { static const QLatin1Char CODE_QUOTE('`'); static const QLatin1Char DOLLAR('$'); static const QLatin1Char ESCAPER('\\'); QString buf; QChar prev[2]; ParserState state = ParserState::Text; QString out; // Double dollar state int length = 0; // Quote state int quoteLevel = 0; int quoteSequence = 0; bool beginQuote = true; for (const QChar& sym : markdown) { const bool isEscaping = prev[0] == ESCAPER; switch (state) { case ParserState::Text: { if (sym == CODE_QUOTE && !isEscaping) { state = ParserState::CodeQuote; } const bool isDoubleEscaping = isEscaping && prev[1] == ESCAPER; if (sym == DOLLAR && !isDoubleEscaping) { state = ParserState::SingleDollar; // no write to out variable break; } out += sym; } break; case ParserState::CodeQuote: buf += sym; if (sym == CODE_QUOTE && !isEscaping) { if (beginQuote) { quoteLevel++; } else { quoteSequence++; if (quoteSequence == quoteLevel) { state = ParserState::Text; out += buf; // clean up state buf.clear(); beginQuote = true; quoteLevel = 0; quoteSequence = 0; } } } else if (beginQuote) { beginQuote = false; quoteSequence = 0; } break; case ParserState::SingleDollar: if (sym == DOLLAR) { if (isEscaping) buf += sym; else { // So we have double dollars ($$) and we need go to double dollar state if (buf.isEmpty()) { out += DOLLAR + DOLLAR; state = ParserState::DoubleDollar; } else { // Main purpose of this code // $...$ -> $$...$$ // because Cantor supports only $$...$$ out += DOLLAR + DOLLAR + buf + DOLLAR + DOLLAR; buf.clear(); state = ParserState::Text; } } } else buf += sym; break; - // Jupyter TODO: Strange logic // if we have $$ $...$ (eof) // After converting we will ahve $$ $$...$$ (eof) // And it will be evaluate in wrong way ($$ $$) case ParserState::DoubleDollar: buf += sym; length++; if (sym == DOLLAR && prev[0] == DOLLAR && prev[1] != ESCAPER && length >= 3) { state = ParserState::Text; out += buf; buf.clear(); length = 0; } break; } // Shift previous symbols prev[1] = prev[0]; prev[0] = sym; } tail = buf; return out; }