File indexing completed on 2024-04-28 11:20:32

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     // properly end the input in the Maxima mode
0212     if (static_cast<MaximaSession*>(session())->mode() == MaximaSession::Maxima && !cmd.endsWith(QLatin1Char('$')))
0213     {
0214         if (!cmd.endsWith(QLatin1String(";")))
0215             cmd += QLatin1Char(';');
0216     }
0217 
0218     //lisp-quiet doesn't print a prompt after the command
0219     //is completed, which causes the parsing to hang.
0220     //replace the command with the non-quiet version
0221     cmd.replace(QRegularExpression(QStringLiteral("^:lisp-quiet")), QStringLiteral(":lisp"));
0222 
0223     return cmd;
0224 }
0225 
0226 void MaximaExpression::forceDone()
0227 {
0228     qDebug()<<"forcing Expression state to DONE";
0229     setResult(nullptr);
0230     setStatus(Cantor::Expression::Done);
0231 }
0232 
0233 /*!
0234     example output for the simple expression '5+5':
0235     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"
0236     text mode  - "<cantor-result><cantor-text>\n(%o1) 10\n</cantor-text></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
0237  */
0238 void MaximaExpression::parseOutput(const QString& out)
0239 {
0240     const int promptStart = out.indexOf(QLatin1String("<cantor-prompt>"));
0241     const int promptEnd = out.indexOf(QLatin1String("</cantor-prompt>"));
0242     const QString prompt = out.mid(promptStart + 15, promptEnd - promptStart - 15).simplified();
0243 
0244     //check whether the result is part of the promt - this is the case when additional input is required from the user
0245     if (prompt.contains(QLatin1String("<cantor-result>")))
0246     {
0247         //text part of the output
0248         const int textContentStart = prompt.indexOf(QLatin1String("<cantor-text>"));
0249         const int textContentEnd = prompt.indexOf(QLatin1String("</cantor-text>"));
0250         QString textContent = prompt.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
0251 
0252         qDebug()<<"asking for additional input for " << textContent;
0253         emit needsAdditionalInformation(textContent);
0254         return;
0255     }
0256 
0257     qDebug()<<"new input label: " << prompt;
0258 
0259     QString errorContent;
0260 
0261     //parse the results
0262     int resultStart = out.indexOf(QLatin1String("<cantor-result>"));
0263     if (resultStart != -1) {
0264         errorContent += out.mid(0, resultStart);
0265         if (!errorContent.isEmpty() && !(isHelpRequest() || m_isHelpRequestAdditional))
0266         {
0267             //there is a result but also the error buffer is not empty. This is the case when
0268             //warnings are generated, for example, the output of rat(0.75*10) is:
0269             //"\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".
0270             //In such cases we just add a new text result with the warning.
0271             qDebug() << "warning: " << errorContent;
0272             auto* result = new Cantor::TextResult(errorContent.trimmed());
0273 
0274             //the output of tex() function is also placed outside of the result section, don't treat it as a warning
0275             if (!command().remove(QLatin1Char(' ')).startsWith(QLatin1String("tex(")))
0276                 result->setIsWarning(true);
0277             addResult(result);
0278         }
0279     }
0280 
0281     while (resultStart != -1)
0282     {
0283         int resultEnd = out.indexOf(QLatin1String("</cantor-result>"), resultStart + 15);
0284         const QString resultContent = out.mid(resultStart + 15, resultEnd - resultStart - 15);
0285         parseResult(resultContent);
0286 
0287         //search for the next openning <cantor-result> tag after the current closing </cantor-result> tag
0288         resultStart = out.indexOf(QLatin1String("<cantor-result>"), resultEnd + 16);
0289     }
0290 
0291     //parse the error message, the part outside of the <cantor*> tags
0292     int lastResultEnd = out.lastIndexOf(QLatin1String("</cantor-result>"));
0293     if (lastResultEnd != -1)
0294         lastResultEnd += 16;
0295     else
0296         lastResultEnd = 0;
0297 
0298     errorContent += out.mid(lastResultEnd, promptStart - lastResultEnd).trimmed();
0299     if (errorContent.isEmpty())
0300     {
0301         // For plots we set Done status in imageChanged
0302         if (!m_isPlot || m_plotResult)
0303             setStatus(Cantor::Expression::Done);
0304     }
0305     else
0306     {
0307         qDebug() << "error content: " << errorContent;
0308 
0309         if (prompt.trimmed() == QLatin1String("MAXIMA>") )
0310         {
0311             //prompt is "MAXIMA>", i.e. we're switching to the Lisp-mode triggered by to_lisp(). The output in this case is:
0312             //   "Type (to-maxima) to restart, ($quit) to quit Maxima.\n<cantor-prompt>\nMAXIMA> </cantor-prompt>\n"
0313             //Or we're already in the Lisp mode and just need to show the result of the lisp evaluation.
0314             if (static_cast<MaximaSession*>(session())->mode() != MaximaSession::Lisp)
0315                 static_cast<MaximaSession*>(session())->setMode(MaximaSession::Lisp);
0316 
0317             auto* result = new Cantor::TextResult(errorContent.trimmed());
0318             setResult(result);
0319             qDebug()<<"setting status to DONE";
0320             setStatus(Cantor::Expression::Done);
0321         }
0322         else if (prompt.trimmed() != QLatin1String("MAXIMA>") && static_cast<MaximaSession*>(session())->mode() == MaximaSession::Lisp)
0323         {
0324             //"Returning to Maxima:
0325             //output:  "Returning to Maxima\n<cantor-result><cantor-text>\n(%o1) true\n</cantor-text></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
0326             static_cast<MaximaSession*>(session())->setMode(MaximaSession::Maxima);
0327             auto* result = new Cantor::TextResult(errorContent.trimmed());
0328             addResult(result);
0329             setStatus(Cantor::Expression::Done);
0330         }
0331         else if (out.contains(QLatin1String("cantor-value-separator"))
0332             || (out.contains(QLatin1String("<cantor-result>")) && !(isHelpRequest() || m_isHelpRequestAdditional)) )
0333         {
0334             //we don't interpret the error output as an error in the following cases:
0335             //1. when fetching variables, in addition to the actual result with variable names and values,
0336             //  Maxima also writes out the names of the variables to the error buffer.
0337             //2. when there is a valid result produced, in this case the error string
0338             //  contains actually a warning that is handled above
0339             setStatus(Cantor::Expression::Done);
0340         }
0341         else if(isHelpRequest() || m_isHelpRequestAdditional) //help messages are also part of the error output
0342         {
0343             //we've got help result, but maybe additional input is required -> check this
0344             const int index = prompt.trimmed().indexOf(MaximaSession::MaximaInputPrompt);
0345             if (index == -1) {
0346                 // No input label found in the prompt -> additional info is required
0347                 qDebug()<<"asking for additional input for the help request" << prompt;
0348                 m_isHelpRequestAdditional = true;
0349                 emit needsAdditionalInformation(prompt);
0350             }
0351 
0352             //set the help result
0353             errorContent.prepend(QLatin1Char(' '));
0354             auto* result = new Cantor::HelpResult(errorContent);
0355             setResult(result);
0356 
0357             //if a new input prompt was found, no further input is expected and we're done
0358             if (index != -1) {
0359                 m_isHelpRequestAdditional = false;
0360                 setStatus(Cantor::Expression::Done);
0361             }
0362         }
0363         else
0364         {
0365             if (isInternal())
0366                 setStatus(Cantor::Expression::Done); //for internal commands no need to handle the error output
0367             else
0368             {
0369                 errorContent = errorContent.replace(QLatin1String("\n\n"), QLatin1String("\n"));
0370                 setErrorMessage(errorContent);
0371                 setStatus(Cantor::Expression::Error);
0372             }
0373         }
0374     }
0375 }
0376 
0377 void MaximaExpression::parseResult(const QString& resultContent)
0378 {
0379     //in case we asked for additional input for the help request,
0380     //no need to process the result - we're not done yet and maxima is waiting for further input
0381     if (m_isHelpRequestAdditional)
0382         return;
0383 
0384     qDebug()<<"result content: " << resultContent;
0385 
0386     //text part of the output
0387     const int textContentStart = resultContent.indexOf(QLatin1String("<cantor-text>"));
0388     const int textContentEnd = resultContent.indexOf(QLatin1String("</cantor-text>"));
0389     QString textContent = resultContent.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
0390     qDebug()<<"text content: " << textContent;
0391 
0392     //output label can be a part of the text content -> determine it
0393     const QRegularExpression regex = QRegularExpression(MaximaSession::MaximaOutputPrompt.pattern());
0394     QRegularExpressionMatch match = regex.match(textContent);
0395     QString outputLabel;
0396     if (match.hasMatch()) // a match is found, so the output contains output label
0397         outputLabel = textContent.mid(match.capturedStart(0), match.capturedLength(0)).trimmed();
0398     qDebug()<<"output label: " << outputLabel;
0399 
0400     //extract the expression id
0401     bool ok;
0402     QString idString = outputLabel.mid(3, outputLabel.length()-4);
0403     int id = idString.toInt(&ok);
0404     if (ok)
0405         setId(id);
0406 
0407     qDebug()<<"expression id: " << this->id();
0408 
0409     //remove the output label from the text content
0410     textContent = textContent.remove(outputLabel).trimmed();
0411 
0412     //determine the actual result
0413     Cantor::Result* result = nullptr;
0414 
0415     const int latexContentStart = resultContent.indexOf(QLatin1String("<cantor-latex>"));
0416     //Handle system maxima output for plotting commands
0417     if (m_isPlot)
0418     {
0419         // plot/draw command can be part of a multi-line input having other non-plot related commands.
0420         // parse the output of every command of the multi-line input and only add plot result if
0421         // the output has plot/draw specific keywords
0422         if ( (!m_isDraw && textContent.endsWith(QString::fromLatin1("\"%1\"]").arg(m_tempFile->fileName())))
0423             || (m_isDraw && (textContent.startsWith(QLatin1String("[gr2d(explicit")) || textContent.startsWith(QLatin1String("[gr3d(explicit"))) ) )
0424         {
0425             m_plotResultIndex = results().size();
0426             // Gnuplot could generate plot before we parse text output from maxima and after
0427             // If we already have plot result, just add it
0428             // Else set info message, and replace it by real result in imageChanged function later
0429             if (m_plotResult)
0430                 result = m_plotResult;
0431             else
0432                 result = new Cantor::TextResult(i18n("Waiting for the plot result"));
0433         }
0434         else
0435             result = new Cantor::TextResult(textContent);
0436     }
0437     else if (latexContentStart != -1)
0438     {
0439         //latex output is available
0440         const int latexContentEnd = resultContent.indexOf(QLatin1String("</cantor-latex>"));
0441         QString latexContent = resultContent.mid(latexContentStart + 14, latexContentEnd - latexContentStart - 14).trimmed();
0442         qDebug()<<"latex content: " << latexContent;
0443 
0444         Cantor::TextResult* textResult;
0445         //replace the \mbox{} environment, if available, by the eqnarray environment
0446         if (latexContent.indexOf(QLatin1String("\\mbox{")) != -1)
0447         {
0448             int i;
0449             int pcount=0;
0450             for(i = latexContent.indexOf(QLatin1String("\\mbox{"))+5; i < latexContent.size(); ++i)
0451             {
0452                 if(latexContent[i]==QLatin1Char('{'))
0453                     pcount++;
0454                 else if(latexContent[i]==QLatin1Char('}'))
0455                     pcount--;
0456 
0457 
0458                 if(pcount==0)
0459                     break;
0460             }
0461 
0462             QString modifiedLatexContent = latexContent.mid(i+1);
0463             if(modifiedLatexContent.trimmed().isEmpty())
0464             {
0465                 //empty content in the \mbox{} environment (e.g. for print() outputs), use the latex string outside of the \mbox{} environment
0466                 modifiedLatexContent = latexContent.left(latexContent.indexOf(QLatin1String("\\mbox{")));
0467             }
0468 
0469             modifiedLatexContent.prepend(QLatin1String("\\begin{eqnarray*}"));
0470             modifiedLatexContent.append(QLatin1String("\\end{eqnarray*}"));
0471             textResult = new Cantor::TextResult(modifiedLatexContent, textContent);
0472             qDebug()<<"modified latex content: " << modifiedLatexContent;
0473         }
0474         else
0475         {
0476             //no \mbox{} available, use what we've got.
0477             textResult = new Cantor::TextResult(latexContent, textContent);
0478         }
0479 
0480         textResult->setFormat(Cantor::TextResult::LatexFormat);
0481         result = textResult;
0482     }
0483     else
0484     {
0485         //no latex output is available, the actual result is part of the textContent string
0486 
0487         // text output is quoted by Maxima, remove the quotes. No need to do it for internal
0488         // commands to fetch the list of current variables, the proper parsing is done in MaximaVariableModel::parse().
0489         if (!isInternal() && textContent.startsWith(QLatin1String("\"")))
0490         {
0491             textContent.remove(0, 1);
0492             textContent.chop(1);
0493             textContent.replace(QLatin1String("\\\""), QLatin1String("\""));
0494         }
0495 
0496         result = new Cantor::TextResult(textContent);
0497     }
0498 
0499     addResult(result);
0500 }
0501 
0502 void MaximaExpression::parseError(const QString& out)
0503 {
0504     m_errorBuffer.append(out);
0505 }
0506 
0507 void MaximaExpression::addInformation(const QString& information)
0508 {
0509     qDebug()<<"adding information";
0510     QString inf=information;
0511     if(!inf.endsWith(QLatin1Char(';')))
0512         inf+=QLatin1Char(';');
0513     Cantor::Expression::addInformation(inf);
0514 
0515     static_cast<MaximaSession*>(session())->sendInputToProcess(inf+QLatin1Char('\n'));
0516 }
0517 
0518 void MaximaExpression::imageChanged()
0519 {
0520     if(m_tempFile->size()>0)
0521     {
0522         m_plotResult = new Cantor::ImageResult( QUrl::fromLocalFile(m_tempFile->fileName()) );
0523         /*
0524         QSizeF size;
0525         const QImage& image = Cantor::Renderer::pdfRenderToImage(QUrl::fromLocalFile(m_tempFile->fileName()), 1., false, &size);
0526         m_plotResult = new Cantor::ImageResult(image);
0527         */
0528 
0529         // Check, that we already parse maxima output for this plot, and if not, keep it up to this moment
0530         // If it's true, replace text info result by real plot and set status as Done
0531         if (m_plotResultIndex != -1)
0532         {
0533             replaceResult(m_plotResultIndex, m_plotResult);
0534             if (status() != Cantor::Expression::Error)
0535                 setStatus(Cantor::Expression::Done);
0536         }
0537     }
0538 }