File indexing completed on 2023-05-30 09:03:10

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009-2012 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2017-2022 by Alexander Semke (alexander.semke@web.de)
0005 */
0006 
0007 #include "maximaexpression.h"
0008 
0009 #include <config-cantorlib.h>
0010 
0011 #include "maximasession.h"
0012 #include "textresult.h"
0013 #include "epsresult.h"
0014 #include "imageresult.h"
0015 #include "helpresult.h"
0016 #include "latexresult.h"
0017 #include "settings.h"
0018 
0019 #include <QApplication>
0020 #include <QDesktopWidget>
0021 #include <QDebug>
0022 #include <QDir>
0023 #include <QRegularExpression>
0024 #include <QTemporaryFile>
0025 #include <QTimer>
0026 #include <QUrl>
0027 
0028 #include <KLocalizedString>
0029 
0030 // MaximaExpression use real id from Maxima as expression id, so we don't know id before executing
0031 MaximaExpression::MaximaExpression( Cantor::Session* session, bool internal ) : Cantor::Expression(session, internal, -1)
0032 {
0033 }
0034 
0035 MaximaExpression::~MaximaExpression() {
0036     if(m_tempFile)
0037         delete m_tempFile;
0038 }
0039 
0040 void MaximaExpression::evaluate()
0041 {
0042     m_gotErrorContent = false;
0043 
0044     if(m_tempFile)
0045     {
0046         delete m_tempFile;
0047         m_tempFile = nullptr;
0048         m_isPlot = false;
0049         m_plotResult = nullptr;
0050         m_plotResultIndex = -1;
0051     }
0052 
0053     QString cmd = command();
0054 
0055     //if the user explicitly has entered quit(), do a logout here
0056     //otherwise maxima's process will be stopped after the evaluation of this command
0057     //and we re-start it because of "maxima has crashed".
0058     if (cmd.remove(QLatin1Char(' ')) == QLatin1String("quit()"))
0059     {
0060         session()->logout();
0061         return;
0062     }
0063 
0064     //check if this is a ?command
0065     if(cmd.startsWith(QLatin1String("??"))
0066         || cmd.startsWith(QLatin1String("describe("))
0067         || cmd.startsWith(QLatin1String("example("))
0068         || cmd.startsWith(QLatin1String(":lisp(cl-info::info-exact")))
0069         setIsHelpRequest(true);
0070 
0071     if (MaximaSettings::self()->integratePlots()
0072         && !cmd.contains(QLatin1String("ps_file"))
0073         && cmd.contains(QRegularExpression(QStringLiteral("(?:plot2d|plot3d|contour_plot|draw|draw2d|draw3d)\\s*\\([^\\)]"))))
0074     {
0075         m_isPlot = true;
0076         if (cmd.contains(QRegularExpression(QStringLiteral("(?:draw|draw2d|draw3d)\\s*\\([^\\)]"))))
0077             m_isDraw = true;
0078 
0079         QString extension;
0080         if (MaximaSettings::inlinePlotFormat() == 0)
0081             extension = QLatin1String("pdf");
0082         else if (MaximaSettings::inlinePlotFormat() == 1)
0083             extension = QLatin1String("svg");
0084         else if (MaximaSettings::inlinePlotFormat() == 2)
0085             extension = QLatin1String("png");
0086 
0087         m_tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_maxima-XXXXXX.%1").arg(extension));
0088         m_tempFile->open();
0089 
0090         m_fileWatch.removePaths(m_fileWatch.files());
0091         m_fileWatch.addPath(m_tempFile->fileName());
0092         connect(&m_fileWatch, &QFileSystemWatcher::fileChanged, this, &MaximaExpression::imageChanged,  Qt::UniqueConnection);
0093     }
0094 
0095     bool isComment = true;
0096     int commentLevel = 0;
0097     bool inString = false;
0098     for (int i = 0; i < cmd.size(); ++i) {
0099         if (cmd[i] == QLatin1Char('\\')) {
0100             ++i; // skip the next character
0101             if (commentLevel == 0 && !inString) {
0102                 isComment = false;
0103             }
0104         } else if (cmd[i] == QLatin1Char('"') && commentLevel == 0) {
0105             inString = !inString;
0106             isComment = false;
0107         } else if (cmd.mid(i,2) == QLatin1String("/*") && !inString) {
0108             ++commentLevel;
0109             ++i;
0110         } else if (cmd.mid(i,2) == QLatin1String("*/") && !inString) {
0111             if (commentLevel == 0) {
0112                 qDebug() << "Comments mismatched!";
0113                 setErrorMessage(i18n("Error: Too many */"));
0114                 setStatus(Cantor::Expression::Error);
0115                 return;
0116             }
0117             ++i;
0118             --commentLevel;
0119         } else if (isComment && commentLevel == 0 && !cmd[i].isSpace()) {
0120             isComment = false;
0121         }
0122     }
0123 
0124     if (commentLevel > 0) {
0125         qDebug() << "Comments mismatched!";
0126         setErrorMessage(i18n("Error: Too many /*"));
0127         setStatus(Cantor::Expression::Error);
0128         return;
0129     }
0130     if (inString) {
0131         qDebug() << "String not closed";
0132         setErrorMessage(i18n("Error: expected \" before ;"));
0133         setStatus(Cantor::Expression::Error);
0134         return;
0135     }
0136     if(isComment)
0137     {
0138         setStatus(Cantor::Expression::Done);
0139         return;
0140     }
0141 
0142     session()->enqueueExpression(this);
0143 }
0144 
0145 QString MaximaExpression::internalCommand()
0146 {
0147     QString cmd = command();
0148 
0149     // trim the string so the regex-logic below also works with whitespaces around the actual plot command
0150     cmd = cmd.trimmed();
0151 
0152     //replace all newlines with spaces, as maxima isn't sensitive about
0153     //whitespaces, and without newlines the whole command
0154     //is executed at once, without outputting an input
0155     //prompt after each line.
0156     // Also, this helps to handle plot/draw commands  with line breaks that are not properly handled by the regex below.
0157     cmd.replace(QLatin1Char('\n'), QLatin1Char(' '));
0158 
0159     if(m_isPlot)
0160     {
0161         if(!m_tempFile)
0162         {
0163             qDebug()<<"plotting without tempFile";
0164             return QString();
0165         }
0166 
0167         if (!m_isDraw)
0168         {
0169             QString params;
0170             const auto& fileName = m_tempFile->fileName();
0171             int w, h;
0172             if (MaximaSettings::inlinePlotFormat() == 0) // pdf
0173             {
0174                 // pdfcairo terminal accepts the sizes in cm
0175                 w = MaximaSettings::plotWidth();
0176                 h = MaximaSettings::plotHeight();
0177                 params = QLatin1String("[gnuplot_pdf_term_command, \"set term pdfcairo size %2cm,%3cm\"], [pdf_file, \"%1\"]");
0178             }
0179             else if (MaximaSettings::inlinePlotFormat() == 1) // svg
0180             {
0181                 // svg terminal accepts the sizes in points
0182                 w = MaximaSettings::plotWidth() / 2.54 * 72;
0183                 h = MaximaSettings::plotHeight() / 2.54 * 72;
0184                 params = QLatin1String("[gnuplot_svg_term_command, \"set term svg size %2,%3\"], [svg_file, \"%1\"]");
0185             }
0186             else // png
0187             {
0188                 // png terminal accepts the sizes in pixels
0189                 w = MaximaSettings::plotWidth() / 2.54 * QApplication::desktop()->physicalDpiX();
0190                 h = MaximaSettings::plotHeight() / 2.54 * QApplication::desktop()->physicalDpiX();
0191                 params = QLatin1String("[gnuplot_png_term_command, \"set term png size %2,%3\"], [png_file, \"%1\"]");
0192             }
0193 
0194             cmd.replace(QRegularExpression(QStringLiteral("((plot2d|plot3d|contour_plot)\\s*\\(.*)\\)([;\n$]|$)")),
0195                         QLatin1String("\\1, ") + params.arg(fileName, QString::number(w), QString::number(h)) + QLatin1String(");"));
0196 
0197         }
0198         else
0199         {
0200             //strip off the extension ".png", etc. - the terminal parameter for draw() is adding the extension additionally
0201             QString fileName = m_tempFile->fileName();
0202             QString extension = fileName.right(3);
0203             fileName = fileName.left(fileName.length() - 4);
0204             const QString params = QLatin1String("terminal=%1, file_name = \"%2\"").arg(extension, fileName);
0205             cmd.replace(QRegularExpression(QStringLiteral("((draw|draw2d|draw3d)\\s*\\(.*)*\\)([;\n$]|$)")),
0206                         QLatin1String("\\1, ") + params + QLatin1String(");"));
0207         }
0208 
0209     }
0210 
0211     if (!cmd.endsWith(QLatin1Char('$')))
0212     {
0213         if (!cmd.endsWith(QLatin1String(";")))
0214             cmd += QLatin1Char(';');
0215     }
0216 
0217     //lisp-quiet doesn't print a prompt after the command
0218     //is completed, which causes the parsing to hang.
0219     //replace the command with the non-quiet version
0220     cmd.replace(QRegularExpression(QStringLiteral("^:lisp-quiet")), QStringLiteral(":lisp"));
0221 
0222     return cmd;
0223 }
0224 
0225 void MaximaExpression::forceDone()
0226 {
0227     qDebug()<<"forcing Expression state to DONE";
0228     setResult(nullptr);
0229     setStatus(Cantor::Expression::Done);
0230 }
0231 
0232 /*!
0233     example output for the simple expression '5+5':
0234     latex mode - "<cantor-result><cantor-text>\n(%o1) 10\n</cantor-text><cantor-latex>\\mbox{\\tt\\red(\\mathrm{\\%o1}) \\black}10</cantor-latex></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
0235     text mode  - "<cantor-result><cantor-text>\n(%o1) 10\n</cantor-text></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
0236  */
0237 void MaximaExpression::parseOutput(const QString& out)
0238 {
0239     const int promptStart = out.indexOf(QLatin1String("<cantor-prompt>"));
0240     const int promptEnd = out.indexOf(QLatin1String("</cantor-prompt>"));
0241     const QString prompt = out.mid(promptStart + 15, promptEnd - promptStart - 15).simplified();
0242 
0243     //check whether the result is part of the promt - this is the case when additional input is required from the user
0244     if (prompt.contains(QLatin1String("<cantor-result>")))
0245     {
0246         //text part of the output
0247         const int textContentStart = prompt.indexOf(QLatin1String("<cantor-text>"));
0248         const int textContentEnd = prompt.indexOf(QLatin1String("</cantor-text>"));
0249         QString textContent = prompt.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
0250 
0251         qDebug()<<"asking for additional input for " << textContent;
0252         emit needsAdditionalInformation(textContent);
0253         return;
0254     }
0255 
0256     qDebug()<<"new input label: " << prompt;
0257 
0258     QString errorContent;
0259 
0260     //parse the results
0261     int resultStart = out.indexOf(QLatin1String("<cantor-result>"));
0262     if (resultStart != -1) {
0263         errorContent += out.mid(0, resultStart);
0264         if (!errorContent.isEmpty() && !(isHelpRequest() || m_isHelpRequestAdditional))
0265         {
0266             //there is a result but also the error buffer is not empty. This is the case when
0267             //warnings are generated, for example, the output of rat(0.75*10) is:
0268             //"\nrat: replaced 7.5 by 15/2 = 7.5\n<cantor-result><cantor-text>\n(%o2) 15/2\n</cantor-text></cantor-result>\n<cantor-prompt>(%i3) </cantor-prompt>\n".
0269             //In such cases we just add a new text result with the warning.
0270             qDebug() << "warning: " << errorContent;
0271             auto* result = new Cantor::TextResult(errorContent.trimmed());
0272 
0273             //the output of tex() function is also placed outside of the result section, don't treat it as a warning
0274             if (!command().remove(QLatin1Char(' ')).startsWith(QLatin1String("tex(")))
0275                 result->setIsWarning(true);
0276             addResult(result);
0277         }
0278     }
0279 
0280     while (resultStart != -1)
0281     {
0282         int resultEnd = out.indexOf(QLatin1String("</cantor-result>"), resultStart + 15);
0283         const QString resultContent = out.mid(resultStart + 15, resultEnd - resultStart - 15);
0284         parseResult(resultContent);
0285 
0286         //search for the next openning <cantor-result> tag after the current closing </cantor-result> tag
0287         resultStart = out.indexOf(QLatin1String("<cantor-result>"), resultEnd + 16);
0288     }
0289 
0290     //parse the error message, the part outside of the <cantor*> tags
0291     int lastResultEnd = out.lastIndexOf(QLatin1String("</cantor-result>"));
0292     if (lastResultEnd != -1)
0293         lastResultEnd += 16;
0294     else
0295         lastResultEnd = 0;
0296 
0297     errorContent += out.mid(lastResultEnd, promptStart - lastResultEnd).trimmed();
0298     if (errorContent.isEmpty())
0299     {
0300         // For plots we set Done status in imageChanged
0301         if (!m_isPlot || m_plotResult)
0302             setStatus(Cantor::Expression::Done);
0303     }
0304     else
0305     {
0306         qDebug() << "error content: " << errorContent;
0307 
0308         if (out.contains(QLatin1String("cantor-value-separator"))
0309             || (out.contains(QLatin1String("<cantor-result>")) && !(isHelpRequest() || m_isHelpRequestAdditional)) )
0310         {
0311             //we don't interpret the error output as an error in the following cases:
0312             //1. when fetching variables, in addition to the actual result with variable names and values,
0313             //  Maxima also writes out the names of the variables to the error buffer.
0314             //2. when there is a valid result produced, in this case the error string
0315             //  contains actually a warning that is handled above
0316             setStatus(Cantor::Expression::Done);
0317         }
0318         else if (prompt.trimmed() == QLatin1String("MAXIMA>") )
0319         {
0320             //prompt is "MAXIMA>", i.e. we're switching to the Lisp-mode triggered by to_lisp(). The output in this case is:
0321             //   "Type (to-maxima) to restart, ($quit) to quit Maxima.\n<cantor-prompt>\nMAXIMA> </cantor-prompt>\n"
0322             //Or we're already in the Lisp mode and just need to show the result of the lisp evaluation.
0323             if (static_cast<MaximaSession*>(session())->mode() != MaximaSession::Lisp)
0324                 static_cast<MaximaSession*>(session())->setMode(MaximaSession::Lisp);
0325 
0326             auto* result = new Cantor::TextResult(errorContent.trimmed());
0327             setResult(result);
0328             qDebug()<<"setting status to DONE";
0329             setStatus(Cantor::Expression::Done);
0330         }
0331         else if (prompt.trimmed() != QLatin1String("MAXIMA>") && static_cast<MaximaSession*>(session())->mode() == MaximaSession::Lisp)
0332         {
0333             //"Returning to Maxima:
0334             //output:  "Returning to Maxima\n<cantor-result><cantor-text>\n(%o1) true\n</cantor-text></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
0335             static_cast<MaximaSession*>(session())->setMode(MaximaSession::Maxima);
0336             auto* result = new Cantor::TextResult(errorContent.trimmed());
0337             addResult(result);
0338             setStatus(Cantor::Expression::Done);
0339         }
0340         else if(isHelpRequest() || m_isHelpRequestAdditional) //help messages are also part of the error output
0341         {
0342             //we've got help result, but maybe additional input is required -> check this
0343             const int index = prompt.trimmed().indexOf(MaximaSession::MaximaInputPrompt);
0344             if (index == -1) {
0345                 // No input label found in the prompt -> additional info is required
0346                 qDebug()<<"asking for additional input for the help request" << prompt;
0347                 m_isHelpRequestAdditional = true;
0348                 emit needsAdditionalInformation(prompt);
0349             }
0350 
0351             //set the help result
0352             errorContent.prepend(QLatin1Char(' '));
0353             auto* result = new Cantor::HelpResult(errorContent);
0354             setResult(result);
0355 
0356             //if a new input prompt was found, no further input is expected and we're done
0357             if (index != -1) {
0358                 m_isHelpRequestAdditional = false;
0359                 setStatus(Cantor::Expression::Done);
0360             }
0361         }
0362         else
0363         {
0364             if (isInternal())
0365                 setStatus(Cantor::Expression::Done); //for internal commands no need to handle the error output
0366             else
0367             {
0368                 errorContent = errorContent.replace(QLatin1String("\n\n"), QLatin1String("\n"));
0369                 setErrorMessage(errorContent);
0370                 setStatus(Cantor::Expression::Error);
0371             }
0372         }
0373     }
0374 }
0375 
0376 void MaximaExpression::parseResult(const QString& resultContent)
0377 {
0378     //in case we asked for additional input for the help request,
0379     //no need to process the result - we're not done yet and maxima is waiting for further input
0380     if (m_isHelpRequestAdditional)
0381         return;
0382 
0383     qDebug()<<"result content: " << resultContent;
0384 
0385     //text part of the output
0386     const int textContentStart = resultContent.indexOf(QLatin1String("<cantor-text>"));
0387     const int textContentEnd = resultContent.indexOf(QLatin1String("</cantor-text>"));
0388     QString textContent = resultContent.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
0389     qDebug()<<"text content: " << textContent;
0390 
0391     //output label can be a part of the text content -> determine it
0392     const QRegularExpression regex = QRegularExpression(MaximaSession::MaximaOutputPrompt.pattern());
0393     QRegularExpressionMatch match = regex.match(textContent);
0394     QString outputLabel;
0395     if (match.hasMatch()) // a match is found, so the output contains output label
0396         outputLabel = textContent.mid(match.capturedStart(0), match.capturedLength(0)).trimmed();
0397     qDebug()<<"output label: " << outputLabel;
0398 
0399     //extract the expression id
0400     bool ok;
0401     QString idString = outputLabel.mid(3, outputLabel.length()-4);
0402     int id = idString.toInt(&ok);
0403     if (ok)
0404         setId(id);
0405 
0406     qDebug()<<"expression id: " << this->id();
0407 
0408     //remove the output label from the text content
0409     textContent = textContent.remove(outputLabel).trimmed();
0410 
0411     //determine the actual result
0412     Cantor::Result* result = nullptr;
0413 
0414     const int latexContentStart = resultContent.indexOf(QLatin1String("<cantor-latex>"));
0415     //Handle system maxima output for plotting commands
0416     if (m_isPlot)
0417     {
0418         // plot/draw command can be part of a multi-line input having other non-plot related commands.
0419         // parse the output of every command of the multi-line input and only add plot result if
0420         // the output has plot/draw specific keywords
0421         if ( (!m_isDraw && textContent.endsWith(QString::fromLatin1("\"%1\"]").arg(m_tempFile->fileName())))
0422             || (m_isDraw && (textContent.startsWith(QLatin1String("[gr2d(explicit")) || textContent.startsWith(QLatin1String("[gr3d(explicit"))) ) )
0423         {
0424             m_plotResultIndex = results().size();
0425             // Gnuplot could generate plot before we parse text output from maxima and after
0426             // If we already have plot result, just add it
0427             // Else set info message, and replace it by real result in imageChanged function later
0428             if (m_plotResult)
0429                 result = m_plotResult;
0430             else
0431                 result = new Cantor::TextResult(i18n("Waiting for the plot result"));
0432         }
0433         else
0434             result = new Cantor::TextResult(textContent);
0435     }
0436     else if (latexContentStart != -1)
0437     {
0438         //latex output is available
0439         const int latexContentEnd = resultContent.indexOf(QLatin1String("</cantor-latex>"));
0440         QString latexContent = resultContent.mid(latexContentStart + 14, latexContentEnd - latexContentStart - 14).trimmed();
0441         qDebug()<<"latex content: " << latexContent;
0442 
0443         Cantor::TextResult* textResult;
0444         //replace the \mbox{} environment, if available, by the eqnarray environment
0445         if (latexContent.indexOf(QLatin1String("\\mbox{")) != -1)
0446         {
0447             int i;
0448             int pcount=0;
0449             for(i = latexContent.indexOf(QLatin1String("\\mbox{"))+5; i < latexContent.size(); ++i)
0450             {
0451                 if(latexContent[i]==QLatin1Char('{'))
0452                     pcount++;
0453                 else if(latexContent[i]==QLatin1Char('}'))
0454                     pcount--;
0455 
0456 
0457                 if(pcount==0)
0458                     break;
0459             }
0460 
0461             QString modifiedLatexContent = latexContent.mid(i+1);
0462             if(modifiedLatexContent.trimmed().isEmpty())
0463             {
0464                 //empty content in the \mbox{} environment (e.g. for print() outputs), use the latex string outside of the \mbox{} environment
0465                 modifiedLatexContent = latexContent.left(latexContent.indexOf(QLatin1String("\\mbox{")));
0466             }
0467 
0468             modifiedLatexContent.prepend(QLatin1String("\\begin{eqnarray*}"));
0469             modifiedLatexContent.append(QLatin1String("\\end{eqnarray*}"));
0470             textResult = new Cantor::TextResult(modifiedLatexContent, textContent);
0471             qDebug()<<"modified latex content: " << modifiedLatexContent;
0472         }
0473         else
0474         {
0475             //no \mbox{} available, use what we've got.
0476             textResult = new Cantor::TextResult(latexContent, textContent);
0477         }
0478 
0479         textResult->setFormat(Cantor::TextResult::LatexFormat);
0480         result = textResult;
0481     }
0482     else
0483     {
0484         //no latex output is available, the actual result is part of the textContent string
0485 
0486         // text output is quoted by Maxima, remove the quotes. No need to do it for internal
0487         // commands to fetch the list of current variables, the proper parsing is done in MaximaVariableModel::parse().
0488         if (!isInternal() && textContent.startsWith(QLatin1String("\"")))
0489         {
0490             textContent.remove(0, 1);
0491             textContent.chop(1);
0492             textContent.replace(QLatin1String("\\\""), QLatin1String("\""));
0493         }
0494 
0495         result = new Cantor::TextResult(textContent);
0496     }
0497 
0498     addResult(result);
0499 }
0500 
0501 void MaximaExpression::parseError(const QString& out)
0502 {
0503     m_errorBuffer.append(out);
0504 }
0505 
0506 void MaximaExpression::addInformation(const QString& information)
0507 {
0508     qDebug()<<"adding information";
0509     QString inf=information;
0510     if(!inf.endsWith(QLatin1Char(';')))
0511         inf+=QLatin1Char(';');
0512     Cantor::Expression::addInformation(inf);
0513 
0514     static_cast<MaximaSession*>(session())->sendInputToProcess(inf+QLatin1Char('\n'));
0515 }
0516 
0517 void MaximaExpression::imageChanged()
0518 {
0519     if(m_tempFile->size()>0)
0520     {
0521         m_plotResult = new Cantor::ImageResult( QUrl::fromLocalFile(m_tempFile->fileName()) );
0522         /*
0523         QSizeF size;
0524         const QImage& image = Cantor::Renderer::pdfRenderToImage(QUrl::fromLocalFile(m_tempFile->fileName()), 1., false, &size);
0525         m_plotResult = new Cantor::ImageResult(image);
0526         */
0527 
0528         // Check, that we already parse maxima output for this plot, and if not, keep it up to this moment
0529         // If it's true, replace text info result by real plot and set status as Done
0530         if (m_plotResultIndex != -1)
0531         {
0532             replaceResult(m_plotResultIndex, m_plotResult);
0533             if (status() != Cantor::Expression::Error)
0534                 setStatus(Cantor::Expression::Done);
0535         }
0536     }
0537 }