File indexing completed on 2024-04-21 11:14:19

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2012 Martin Kuettler <martin.kuettler@gmail.com>
0005 */
0006 
0007 #include "textentry.h"
0008 #include "lib/renderer.h"
0009 #include "latexrenderer.h"
0010 #include "lib/jupyterutils.h"
0011 #include "mathrender.h"
0012 #include "worksheetview.h"
0013 
0014 #include "settings.h"
0015 
0016 #include <QScopedPointer>
0017 #include <QGraphicsLinearLayout>
0018 #include <QJsonValue>
0019 #include <QJsonObject>
0020 #include <QJsonArray>
0021 #include <QDebug>
0022 #include <KLocalizedString>
0023 #include <KColorScheme>
0024 #include <QRegularExpression>
0025 #include <QStringList>
0026 #include <QInputDialog>
0027 #include <QActionGroup>
0028 
0029 QStringList standartRawCellTargetNames = {QLatin1String("None"), QLatin1String("LaTeX"), QLatin1String("reST"), QLatin1String("HTML"), QLatin1String("Markdown")};
0030 QStringList standartRawCellTargetMimes = {QString(), QLatin1String("text/latex"), QLatin1String("text/restructuredtext"), QLatin1String("text/html"), QLatin1String("text/markdown")};
0031 
0032 TextEntry::TextEntry(Worksheet* worksheet) : WorksheetEntry(worksheet)
0033     , m_rawCell(false)
0034     , m_convertTarget()
0035     , m_targetActionGroup(nullptr)
0036     , m_ownTarget{nullptr}
0037     , m_targetMenu(nullptr)
0038     , m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction))
0039 {
0040     m_textItem->enableRichText(true);
0041 
0042     connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &TextEntry::moveToPreviousEntry);
0043     connect(m_textItem, &WorksheetTextItem::moveToNext, this, &TextEntry::moveToNextEntry);
0044     // Modern syntax of signal/stots don't work on this connection (arguments don't match)
0045     connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
0046     connect(m_textItem, &WorksheetTextItem::doubleClick, this, &TextEntry::resolveImagesAtCursor);
0047 
0048     // Init raw cell target menus
0049     // This used only for raw cells, but removing and creating this on conversion more complex
0050     // that just create them always
0051     m_targetActionGroup= new QActionGroup(this);
0052     m_targetActionGroup->setExclusive(true);
0053     connect(m_targetActionGroup, &QActionGroup::triggered, this, &TextEntry::convertTargetChanged);
0054 
0055     m_targetMenu = new QMenu(i18n("Raw Cell Targets"));
0056     for (const QString& key : standartRawCellTargetNames)
0057     {
0058         QAction* action = new QAction(key, m_targetActionGroup);
0059         action->setCheckable(true);
0060         m_targetMenu->addAction(action);
0061     }
0062     m_ownTarget = new QAction(i18n("Add custom target"), m_targetActionGroup);
0063     m_ownTarget->setCheckable(true);
0064     m_targetMenu->addAction(m_ownTarget);
0065 }
0066 
0067 TextEntry::~TextEntry()
0068 {
0069     m_targetMenu->deleteLater();
0070 }
0071 
0072 void TextEntry::populateMenu(QMenu* menu, QPointF pos)
0073 {
0074     if (m_rawCell)
0075     {
0076         menu->addAction(i18n("Convert to Text Entry"), this, &TextEntry::convertToTextEntry);
0077         menu->addMenu(m_targetMenu);
0078     }
0079     else
0080     {
0081         menu->addAction(i18n("Convert to Raw Cell"), this, &TextEntry::convertToRawCell);
0082 
0083         bool imageSelected = false;
0084         QTextCursor cursor = m_textItem->textCursor();
0085         const QChar repl = QChar::ObjectReplacementCharacter;
0086         if (cursor.hasSelection())
0087         {
0088             QString selection = m_textItem->textCursor().selectedText();
0089             imageSelected = selection.contains(repl);
0090         }
0091         else
0092         {
0093             // we need to try both the current cursor and the one after the that
0094             cursor = m_textItem->cursorForPosition(pos);
0095             for (int i = 2; i; --i)
0096             {
0097                 int p = cursor.position();
0098                 if (m_textItem->document()->characterAt(p-1) == repl &&
0099                     cursor.charFormat().hasProperty(Cantor::Renderer::CantorFormula))
0100                 {
0101                     m_textItem->setTextCursor(cursor);
0102                     imageSelected = true;
0103                     break;
0104                 }
0105                 cursor.movePosition(QTextCursor::NextCharacter);
0106             }
0107         }
0108 
0109         if (imageSelected)
0110         {
0111             menu->addAction(i18n("Show LaTeX code"), this, SLOT(resolveImagesAtCursor()));
0112         }
0113     }
0114     menu->addSeparator();
0115     WorksheetEntry::populateMenu(menu, pos);
0116 }
0117 
0118 bool TextEntry::isEmpty()
0119 {
0120     return m_textItem->document()->isEmpty();
0121 }
0122 
0123 int TextEntry::type() const
0124 {
0125     return Type;
0126 }
0127 
0128 bool TextEntry::acceptRichText()
0129 {
0130     return true;
0131 }
0132 
0133 bool TextEntry::focusEntry(int pos, qreal xCoord)
0134 {
0135     if (aboutToBeRemoved())
0136         return false;
0137     m_textItem->setFocusAt(pos, xCoord);
0138     return true;
0139 }
0140 
0141 void TextEntry::setContent(const QString& content)
0142 {
0143     m_textItem->setPlainText(content);
0144 }
0145 
0146 void TextEntry::setContent(const QDomElement& content, const KZip& file)
0147 {
0148     Q_UNUSED(file);
0149     if(content.firstChildElement(QLatin1String("body")).isNull())
0150         return;
0151 
0152     if (content.hasAttribute(QLatin1String("convertTarget")))
0153     {
0154         convertToRawCell();
0155         m_convertTarget = content.attribute(QLatin1String("convertTarget"));
0156 
0157         // Set current action status
0158         int idx = standartRawCellTargetMimes.indexOf(m_convertTarget);
0159         if (idx != -1)
0160             m_targetMenu->actions()[idx]->setChecked(true);
0161         else
0162             addNewTarget(m_convertTarget);
0163     }
0164     else
0165         convertToTextEntry();
0166 
0167     QDomDocument doc = QDomDocument();
0168     QDomNode n = doc.importNode(content.firstChildElement(QLatin1String("body")), true);
0169     doc.appendChild(n);
0170     QString html = doc.toString();
0171     m_textItem->setHtml(html);
0172 }
0173 
0174 void TextEntry::setContentFromJupyter(const QJsonObject& cell)
0175 {
0176     if (Cantor::JupyterUtils::isRawCell(cell))
0177     {
0178         convertToRawCell();
0179 
0180         const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(cell);
0181         QJsonValue format = metadata.value(QLatin1String("format"));
0182         // Also checks "raw_mimetype", because raw cell don't corresponds Jupyter Notebook specification
0183         // See https://github.com/jupyter/notebook/issues/4730
0184         if (format.isUndefined())
0185             format = metadata.value(QLatin1String("raw_mimetype"));
0186         m_convertTarget = format.toString(QString());
0187 
0188         // Set current action status
0189         int idx = standartRawCellTargetMimes.indexOf(m_convertTarget);
0190         if (idx != -1)
0191             m_targetMenu->actions()[idx]->setChecked(true);
0192         else
0193             addNewTarget(m_convertTarget);
0194 
0195         m_textItem->setPlainText(Cantor::JupyterUtils::getSource(cell));
0196 
0197         setJupyterMetadata(metadata);
0198     }
0199     else if (Cantor::JupyterUtils::isMarkdownCell(cell))
0200     {
0201         convertToTextEntry();
0202 
0203         QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
0204         m_textItem->setHtml(cantorMetadata.value(QLatin1String("text_entry_content")).toString());
0205     }
0206 }
0207 
0208 QJsonValue TextEntry::toJupyterJson()
0209 {
0210     // Simple logic:
0211     // If convertTarget is empty, it's user maded cell and we convert it to a markdown
0212     // If convertTarget set, it's raw cell from Jupyter and we convert it to Jupyter cell
0213 
0214     QTextDocument* doc = m_textItem->document()->clone();
0215     QTextCursor cursor = doc->find(QString(QChar::ObjectReplacementCharacter));
0216     while(!cursor.isNull())
0217     {
0218         QTextCharFormat format = cursor.charFormat();
0219         if (format.hasProperty(Cantor::Renderer::CantorFormula))
0220         {
0221             showLatexCode(cursor);
0222         }
0223 
0224         cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
0225     }
0226 
0227     QJsonObject metadata(jupyterMetadata());
0228 
0229     QString entryData;
0230     QString entryType;
0231 
0232     if (!m_rawCell)
0233     {
0234         entryType = QLatin1String("markdown");
0235 
0236         // Add raw text of entry to metadata, for situation when
0237         // Cantor opens .ipynb converted from our .cws format
0238         QJsonObject cantorMetadata;
0239 
0240         if (Settings::storeTextEntryFormatting())
0241         {
0242             entryData = doc->toHtml();
0243 
0244             // Remove DOCTYPE from html
0245             entryData.remove(QRegularExpression(QStringLiteral("<!DOCTYPE[^>]*>\\n")));
0246 
0247             cantorMetadata.insert(QLatin1String("text_entry_content"), entryData);
0248         }
0249         else
0250             entryData = doc->toPlainText();
0251 
0252         metadata.insert(Cantor::JupyterUtils::cantorMetadataKey, cantorMetadata);
0253 
0254         // Replace our $$ formulas to $
0255         entryData.replace(QLatin1String("$$"), QLatin1String("$"));
0256     }
0257     else
0258     {
0259         entryType = QLatin1String("raw");
0260         metadata.insert(QLatin1String("format"), m_convertTarget);
0261         entryData = doc->toPlainText();
0262     }
0263 
0264     QJsonObject entry;
0265     entry.insert(QLatin1String("cell_type"), entryType);
0266     entry.insert(QLatin1String("metadata"), metadata);
0267     Cantor::JupyterUtils::setSource(entry, entryData);
0268 
0269     return entry;
0270 }
0271 
0272 QDomElement TextEntry::toXml(QDomDocument& doc, KZip* archive)
0273 {
0274     Q_UNUSED(archive);
0275 
0276     QScopedPointer<QTextDocument> document(m_textItem->document()->clone());
0277 
0278     //make sure that the latex code is shown instead of the rendered formulas
0279     QTextCursor cursor = document->find(QString(QChar::ObjectReplacementCharacter));
0280     while(!cursor.isNull())
0281     {
0282         QTextCharFormat format = cursor.charFormat();
0283         if (format.hasProperty(Cantor::Renderer::CantorFormula))
0284             showLatexCode(cursor);
0285 
0286         cursor = document->find(QString(QChar::ObjectReplacementCharacter), cursor);
0287     }
0288 
0289     const QString& html = document->toHtml();
0290     QDomElement el = doc.createElement(QLatin1String("Text"));
0291     QDomDocument myDoc = QDomDocument();
0292     myDoc.setContent(html);
0293     el.appendChild(myDoc.documentElement().firstChildElement(QLatin1String("body")));
0294 
0295     if (m_rawCell)
0296         el.setAttribute(QLatin1String("convertTarget"), m_convertTarget);
0297 
0298     return el;
0299 }
0300 
0301 QString TextEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
0302 {
0303     Q_UNUSED(commandSep);
0304 
0305     if (commentStartingSeq.isEmpty())
0306         return QString();
0307     /*
0308     // would this be plain enough?
0309     QTextCursor cursor = m_textItem->textCursor();
0310     cursor.movePosition(QTextCursor::Start);
0311     cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0312 
0313     QString text = m_textItem->resolveImages(cursor);
0314     text.replace(QChar::ParagraphSeparator, '\n');
0315     text.replace(QChar::LineSeparator, '\n');
0316     */
0317     QString text = m_textItem->toPlainText();
0318     if (!commentEndingSeq.isEmpty())
0319         return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
0320     return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
0321 
0322 }
0323 
0324 bool TextEntry::evaluate(EvaluationOption evalOp)
0325 {
0326     int i = 0;
0327     if (worksheet()->embeddedMathEnabled() && !m_rawCell)
0328     {
0329         // Render math in $$...$$ via Latex
0330         QTextCursor cursor = findLatexCode();
0331         while (!cursor.isNull())
0332         {
0333             QString latexCode = cursor.selectedText();
0334             qDebug()<<"found latex: " << latexCode;
0335 
0336             latexCode.remove(0, 2);
0337             latexCode.remove(latexCode.length() - 2, 2);
0338             latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n'));
0339             latexCode.replace(QChar::LineSeparator, QLatin1Char('\n'));
0340 
0341             MathRenderer* renderer = worksheet()->mathRenderer();
0342             renderer->renderExpression(++i, latexCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
0343 
0344             cursor = findLatexCode(cursor);
0345         }
0346     }
0347 
0348     evaluateNext(evalOp);
0349 
0350     return true;
0351 }
0352 
0353 void TextEntry::updateEntry()
0354 {
0355     qDebug() << "update Entry";
0356     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0357     while(!cursor.isNull())
0358     {
0359         QTextImageFormat format=cursor.charFormat().toImageFormat();
0360 
0361         if (format.hasProperty(Cantor::Renderer::CantorFormula))
0362             worksheet()->mathRenderer()->rerender(m_textItem->document(), format);
0363 
0364         cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
0365     }
0366 }
0367 
0368 void TextEntry::resolveImagesAtCursor()
0369 {
0370     QTextCursor cursor = m_textItem->textCursor();
0371     if (!cursor.hasSelection())
0372         cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
0373     cursor.insertText(m_textItem->resolveImages(cursor));
0374 }
0375 
0376 QTextCursor TextEntry::findLatexCode(const QTextCursor& cursor) const
0377 {
0378     QTextDocument *doc = m_textItem->document();
0379     QTextCursor startCursor;
0380     if (cursor.isNull())
0381         startCursor = doc->find(QLatin1String("$$"));
0382     else
0383         startCursor = doc->find(QLatin1String("$$"), cursor);
0384     if (startCursor.isNull())
0385         return startCursor;
0386     const QTextCursor endCursor = doc->find(QLatin1String("$$"), startCursor);
0387     if (endCursor.isNull())
0388         return endCursor;
0389     startCursor.setPosition(startCursor.selectionStart());
0390     startCursor.setPosition(endCursor.position(), QTextCursor::KeepAnchor);
0391     return startCursor;
0392 }
0393 
0394 QString TextEntry::showLatexCode(QTextCursor& cursor)
0395 {
0396     QString latexCode = cursor.charFormat().property(Cantor::Renderer::Code).toString();
0397     cursor.deletePreviousChar();
0398     latexCode = QLatin1String("$$") + latexCode + QLatin1String("$$");
0399     cursor.insertText(latexCode);
0400     return latexCode;
0401 }
0402 
0403 int TextEntry::searchText(const QString& text, const QString& pattern,
0404                           QTextDocument::FindFlags qt_flags)
0405 {
0406     Qt::CaseSensitivity caseSensitivity;
0407     if (qt_flags & QTextDocument::FindCaseSensitively)
0408         caseSensitivity = Qt::CaseSensitive;
0409     else
0410         caseSensitivity = Qt::CaseInsensitive;
0411 
0412     int position;
0413     if (qt_flags & QTextDocument::FindBackward)
0414         position = text.lastIndexOf(pattern, -1, caseSensitivity);
0415     else
0416         position = text.indexOf(pattern, 0, caseSensitivity);
0417 
0418     return position;
0419 }
0420 
0421 WorksheetCursor TextEntry::search(const QString& pattern, unsigned flags,
0422                                   QTextDocument::FindFlags qt_flags,
0423                                   const WorksheetCursor& pos)
0424 {
0425     if (!(flags & WorksheetEntry::SearchText) ||
0426         (pos.isValid() && pos.entry() != this))
0427         return WorksheetCursor();
0428 
0429     QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
0430     int position = 0;
0431     QTextCursor latexCursor;
0432     QString latex;
0433     if (flags & WorksheetEntry::SearchLaTeX) {
0434         const QString repl = QString(QChar::ObjectReplacementCharacter);
0435         latexCursor = m_textItem->search(repl, qt_flags, pos);
0436         while (!latexCursor.isNull()) {
0437             latex = m_textItem->resolveImages(latexCursor);
0438             position = searchText(latex, pattern, qt_flags);
0439             if (position >= 0) {
0440                 break;
0441             }
0442             WorksheetCursor c(this, m_textItem, latexCursor);
0443             latexCursor = m_textItem->search(repl, qt_flags, c);
0444         }
0445     }
0446 
0447     if (latexCursor.isNull()) {
0448         if (textCursor.isNull())
0449             return WorksheetCursor();
0450         else
0451             return WorksheetCursor(this, m_textItem, textCursor);
0452     } else {
0453         if (textCursor.isNull() || latexCursor < textCursor) {
0454             int start = latexCursor.selectionStart();
0455             latexCursor.insertText(latex);
0456             QTextCursor c = m_textItem->textCursor();
0457             c.setPosition(start + position);
0458             c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
0459                            pattern.length());
0460             return WorksheetCursor(this, m_textItem, c);
0461         } else {
0462             return WorksheetCursor(this, m_textItem, textCursor);
0463         }
0464     }
0465 }
0466 
0467 
0468 void TextEntry::layOutForWidth(qreal entry_zone_x, qreal w, bool force)
0469 {
0470     if (size().width() == w && m_textItem->pos().x() == entry_zone_x && !force)
0471         return;
0472 
0473     const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
0474 
0475     m_textItem->setGeometry(entry_zone_x, 0, w - margin - entry_zone_x);
0476     setSize(QSizeF(m_textItem->width() + margin + entry_zone_x, m_textItem->height() + VerticalMargin));
0477 }
0478 
0479 bool TextEntry::wantToEvaluate()
0480 {
0481     return !findLatexCode().isNull();
0482 }
0483 
0484 bool TextEntry::isConvertableToTextEntry(const QJsonObject& cell)
0485 {
0486     if (!Cantor::JupyterUtils::isMarkdownCell(cell))
0487         return false;
0488 
0489     QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
0490     const QJsonValue& textContentValue = cantorMetadata.value(QLatin1String("text_entry_content"));
0491 
0492     if (!textContentValue.isString())
0493         return false;
0494 
0495     const QString& textContent = textContentValue.toString();
0496     const QString& source = Cantor::JupyterUtils::getSource(cell);
0497 
0498     return textContent == source;
0499 }
0500 
0501 void TextEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
0502 {
0503     if (!result->successful)
0504     {
0505         qDebug() << "TextEntry: math render failed with message" << result->errorMessage;
0506         return;
0507     }
0508 
0509     const QString& code = result->renderedMath.property(Cantor::Renderer::Code).toString();
0510     const QString& delimiter = QLatin1String("$$");
0511     QTextCursor cursor = m_textItem->document()->find(delimiter + code + delimiter);
0512     if (!cursor.isNull())
0513     {
0514         m_textItem->document()->addResource(QTextDocument::ImageResource, result->uniqueUrl, QVariant(result->image));
0515         result->renderedMath.setProperty(Cantor::Renderer::Delimiter, QLatin1String("$$"));
0516         cursor.insertText(QString(QChar::ObjectReplacementCharacter), result->renderedMath);
0517     }
0518 }
0519 
0520 void TextEntry::convertToRawCell()
0521 {
0522     m_rawCell = true;
0523     m_targetMenu->actions().at(0)->setChecked(true);
0524 
0525     KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
0526     m_textItem->setBackgroundColor(scheme.background(KColorScheme::AlternateBackground).color());
0527 
0528     // Resolve all latex inserts
0529     QTextCursor cursor(m_textItem->document());
0530     cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0531     cursor.insertText(m_textItem->resolveImages(cursor));
0532 }
0533 
0534 void TextEntry::convertToTextEntry()
0535 {
0536     m_rawCell = false;
0537     m_convertTarget.clear();
0538 
0539     KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
0540     m_textItem->setBackgroundColor(scheme.background(KColorScheme::NormalBackground).color());
0541 }
0542 
0543 void TextEntry::convertTargetChanged(QAction* action)
0544 {
0545     int index = standartRawCellTargetNames.indexOf(action->text());
0546     if (index != -1)
0547     {
0548         m_convertTarget = standartRawCellTargetMimes[index];
0549     }
0550     else if (action == m_ownTarget)
0551     {
0552         bool ok;
0553         const QString& target = QInputDialog::getText(worksheet()->worksheetView(), i18n("Cantor"), i18n("Target MIME type:"), QLineEdit::Normal, QString(), &ok);
0554         if (ok && !target.isEmpty())
0555         {
0556             addNewTarget(target);
0557             m_convertTarget = target;
0558         }
0559     }
0560     else
0561     {
0562         m_convertTarget = action->text();
0563     }
0564 }
0565 
0566 void TextEntry::addNewTarget(const QString& target)
0567 {
0568     QAction* action = new QAction(target, m_targetActionGroup);
0569     action->setCheckable(true);
0570     action->setChecked(true);
0571     m_targetMenu->insertAction(m_targetMenu->actions().last(), action);
0572 }
0573 
0574 QString TextEntry::text() const
0575 {
0576     return m_textItem->toPlainText();
0577 }