1 /*
2     SPDX-License-Identifier: GPL-2.0-or-later
3     SPDX-FileCopyrightText: 2009-2012 Alexander Rieder <alexanderrieder@gmail.com>
4     SPDX-FileCopyrightText: 2017-2021 by Alexander Semke (alexander.semke@web.de)
5 */
6 
7 #include "maximaexpression.h"
8 
9 #include <config-cantorlib.h>
10 
11 #include "maximasession.h"
12 #include "textresult.h"
13 #include "epsresult.h"
14 #include "imageresult.h"
15 #include "helpresult.h"
16 #include "latexresult.h"
17 #include "settings.h"
18 
19 #include <QDir>
20 #include <QTemporaryFile>
21 
22 #include <KLocalizedString>
23 #include <QDebug>
24 #include <QTimer>
25 #include <QRegularExpression>
26 #include <QChar>
27 #include <QUrl>
28 
29 // MaximaExpression use real id from Maxima as expression id, so we don't know id before executing
MaximaExpression(Cantor::Session * session,bool internal)30 MaximaExpression::MaximaExpression( Cantor::Session* session, bool internal ) : Cantor::Expression(session, internal, -1)
31 {
32 }
33 
~MaximaExpression()34 MaximaExpression::~MaximaExpression() {
35     if(m_tempFile)
36         delete m_tempFile;
37 }
38 
evaluate()39 void MaximaExpression::evaluate()
40 {
41     m_isHelpRequest = false;
42     m_gotErrorContent = false;
43 
44     if(m_tempFile)
45     {
46         delete m_tempFile;
47         m_tempFile = nullptr;
48         m_isPlot = false;
49         m_plotResult = nullptr;
50         m_plotResultIndex = -1;
51     }
52 
53     QString cmd = command();
54 
55     //if the user explicitly has entered quit(), do a logout here
56     //otherwise maxima's process will be stopped after the evaluation of this command
57     //and we re-start it because of "maxima has crashed".
58     if (cmd.remove(QLatin1Char(' ')) == QLatin1String("quit()"))
59     {
60         session()->logout();
61         return;
62     }
63 
64     //check if this is a ?command
65     if(cmd.startsWith(QLatin1String("??"))
66         || cmd.startsWith(QLatin1String("describe("))
67         || cmd.startsWith(QLatin1String("example("))
68         || cmd.startsWith(QLatin1String(":lisp(cl-info::info-exact")))
69         m_isHelpRequest=true;
70 
71     if (MaximaSettings::self()->integratePlots()
72         && !cmd.contains(QLatin1String("ps_file"))
73         && cmd.contains(QRegularExpression(QStringLiteral("(?:plot2d|plot3d|contour_plot)\\s*\\([^\\)]"))))
74     {
75         m_isPlot=true;
76 #ifdef WITH_EPS
77         m_tempFile=new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_maxima-XXXXXX.eps" ));
78 #else
79         m_tempFile=new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_maxima-XXXXXX.png"));
80 #endif
81         m_tempFile->open();
82 
83         m_fileWatch.removePaths(m_fileWatch.files());
84         m_fileWatch.addPath(m_tempFile->fileName());
85         connect(&m_fileWatch, &QFileSystemWatcher::fileChanged, this, &MaximaExpression::imageChanged,  Qt::UniqueConnection);
86     }
87 
88     bool isComment = true;
89     int commentLevel = 0;
90     bool inString = false;
91     for (int i = 0; i < cmd.size(); ++i) {
92         if (cmd[i] == QLatin1Char('\\')) {
93             ++i; // skip the next character
94             if (commentLevel == 0 && !inString) {
95                 isComment = false;
96             }
97         } else if (cmd[i] == QLatin1Char('"') && commentLevel == 0) {
98             inString = !inString;
99             isComment = false;
100         } else if (cmd.mid(i,2) == QLatin1String("/*") && !inString) {
101             ++commentLevel;
102             ++i;
103         } else if (cmd.mid(i,2) == QLatin1String("*/") && !inString) {
104             if (commentLevel == 0) {
105                 qDebug() << "Comments mismatched!";
106                 setErrorMessage(i18n("Error: Too many */"));
107                 setStatus(Cantor::Expression::Error);
108                 return;
109             }
110             ++i;
111             --commentLevel;
112         } else if (isComment && commentLevel == 0 && !cmd[i].isSpace()) {
113             isComment = false;
114         }
115     }
116 
117     if (commentLevel > 0) {
118         qDebug() << "Comments mismatched!";
119         setErrorMessage(i18n("Error: Too many /*"));
120         setStatus(Cantor::Expression::Error);
121         return;
122     }
123     if (inString) {
124         qDebug() << "String not closed";
125         setErrorMessage(i18n("Error: expected \" before ;"));
126         setStatus(Cantor::Expression::Error);
127         return;
128     }
129     if(isComment)
130     {
131         setStatus(Cantor::Expression::Done);
132         return;
133     }
134 
135     session()->enqueueExpression(this);
136 }
137 
interrupt()138 void MaximaExpression::interrupt()
139 {
140     qDebug()<<"interrupting";
141     setStatus(Cantor::Expression::Interrupted);
142 }
143 
internalCommand()144 QString MaximaExpression::internalCommand()
145 {
146     QString cmd=command();
147 
148     if(m_isPlot)
149     {
150         if(!m_tempFile)
151         {
152             qDebug()<<"plotting without tempFile";
153             return QString();
154         }
155         QString fileName = m_tempFile->fileName();
156 
157 #ifdef WITH_EPS
158         const QString psParam=QLatin1String("[gnuplot_ps_term_command, \"set size 1.0,  1.0; set term postscript eps color solid \"]");
159         const QString plotParameters = QLatin1String("[ps_file, \"")+ fileName+QLatin1String("\"],")+psParam;
160 #else
161         const QString plotParameters = QLatin1String("[gnuplot_term, \"png size 500,340\"], [gnuplot_out_file, \"")+fileName+QLatin1String("\"]");
162 
163 #endif
164         cmd.replace(QRegularExpression(QStringLiteral("((plot2d|plot3d|contour_plot)\\s*\\(.*)\\)([;\n$]|$)")),
165                     QLatin1String("\\1, ") + plotParameters + QLatin1String(");"));
166 
167     }
168 
169     if (!cmd.endsWith(QLatin1Char('$')))
170     {
171         if (!cmd.endsWith(QLatin1String(";")))
172             cmd+=QLatin1Char(';');
173     }
174 
175     //replace all newlines with spaces, as maxima isn't sensitive about
176     //whitespaces, and without newlines the whole command
177     //is executed at once, without outputting an input
178     //prompt after each line
179     cmd.replace(QLatin1Char('\n'), QLatin1Char(' '));
180 
181     //lisp-quiet doesn't print a prompt after the command
182     //is completed, which causes the parsing to hang.
183     //replace the command with the non-quiet version
184     cmd.replace(QRegularExpression(QStringLiteral("^:lisp-quiet")), QStringLiteral(":lisp"));
185 
186     return cmd;
187 }
188 
forceDone()189 void MaximaExpression::forceDone()
190 {
191     qDebug()<<"forcing Expression state to DONE";
192     setResult(nullptr);
193     setStatus(Cantor::Expression::Done);
194 }
195 
196 /*!
197     example output for the simple expression '5+5':
198     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"
199     text mode  - "<cantor-result><cantor-text>\n(%o1) 10\n</cantor-text></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
200  */
parseOutput(QString & out)201 bool MaximaExpression::parseOutput(QString& out)
202 {
203     const int promptStart = out.indexOf(QLatin1String("<cantor-prompt>"));
204     const int promptEnd = out.indexOf(QLatin1String("</cantor-prompt>"));
205     const QString prompt = out.mid(promptStart + 15, promptEnd - promptStart - 15).simplified();
206 
207     //check whether the result is part of the promt - this is the case when additional input is required from the user
208     if (prompt.contains(QLatin1String("<cantor-result>")))
209     {
210         //text part of the output
211         const int textContentStart = prompt.indexOf(QLatin1String("<cantor-text>"));
212         const int textContentEnd = prompt.indexOf(QLatin1String("</cantor-text>"));
213         QString textContent = prompt.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
214 
215         qDebug()<<"asking for additional input for " << textContent;
216         emit needsAdditionalInformation(textContent);
217         return true;
218     }
219 
220     qDebug()<<"new input label: " << prompt;
221 
222     QString errorContent;
223 
224     //parse the results
225     int resultStart = out.indexOf(QLatin1String("<cantor-result>"));
226     if (resultStart != -1)
227         errorContent += out.mid(0, resultStart);
228 
229     while (resultStart != -1)
230     {
231         int resultEnd = out.indexOf(QLatin1String("</cantor-result>"), resultStart + 15);
232         const QString resultContent = out.mid(resultStart + 15, resultEnd - resultStart - 15);
233         parseResult(resultContent);
234 
235         //search for the next openning <cantor-result> tag after the current closing </cantor-result> tag
236         resultStart = out.indexOf(QLatin1String("<cantor-result>"), resultEnd + 16);
237     }
238 
239     //parse the error message, the part outside of the <cantor*> tags
240     int lastResultEnd = out.lastIndexOf(QLatin1String("</cantor-result>"));
241     if (lastResultEnd != -1)
242         lastResultEnd += 16;
243     else
244         lastResultEnd = 0;
245 
246     errorContent += out.mid(lastResultEnd, promptStart - lastResultEnd).trimmed();
247     if (errorContent.isEmpty())
248     {
249         // For plots we set Done status in imageChanged
250         if (!m_isPlot || m_plotResult)
251             setStatus(Cantor::Expression::Done);
252     }
253     else
254     {
255         qDebug() << "error content: " << errorContent;
256 
257         if (out.contains(QLatin1String("cantor-value-separator")))
258         {
259             //when fetching variables, in addition to the actual result with variable names and values,
260             //Maxima also writes out the names of the variables to the error buffer.
261             //we don't interpret this as an error.
262             setStatus(Cantor::Expression::Done);
263         }
264         else if(m_isHelpRequest || m_isHelpRequestAdditional) //help messages are also part of the error output
265         {
266             //we've got help result, but maybe additional input is required -> check this
267             const int index = prompt.trimmed().indexOf(MaximaSession::MaximaInputPrompt);
268             if (index == -1) {
269                 // No input label found in the prompt -> additional info is required
270                 qDebug()<<"asking for additional input for the help request" << prompt;
271                 m_isHelpRequestAdditional = true;
272                 emit needsAdditionalInformation(prompt);
273             }
274 
275             //set the help result
276             errorContent.prepend(QLatin1Char(' '));
277             Cantor::HelpResult* result = new Cantor::HelpResult(errorContent);
278             setResult(result);
279 
280             //if a new input prompt was found, no further input is expected and we're done
281             if (index != -1) {
282                 m_isHelpRequestAdditional = false;
283                 setStatus(Cantor::Expression::Done);
284             }
285         }
286         else
287         {
288             errorContent = errorContent.replace(QLatin1String("\n\n"), QLatin1String("\n"));
289             setErrorMessage(errorContent);
290             setStatus(Cantor::Expression::Error);
291         }
292     }
293 
294     return true;
295 }
296 
parseResult(const QString & resultContent)297 void MaximaExpression::parseResult(const QString& resultContent)
298 {
299     //in case we asked for additional input for the help request,
300     //no need to process the result - we're not done yet and maxima is waiting for further input
301     if (m_isHelpRequestAdditional)
302         return;
303 
304     qDebug()<<"result content: " << resultContent;
305 
306     //text part of the output
307     const int textContentStart = resultContent.indexOf(QLatin1String("<cantor-text>"));
308     const int textContentEnd = resultContent.indexOf(QLatin1String("</cantor-text>"));
309     QString textContent = resultContent.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
310     qDebug()<<"text content: " << textContent;
311 
312     //output label can be a part of the text content -> determine it
313     const QRegularExpression regex = QRegularExpression(MaximaSession::MaximaOutputPrompt.pattern());
314     QRegularExpressionMatch match = regex.match(textContent);
315     QString outputLabel;
316     if (match.hasMatch()) // a match is found, so the output contains output label
317         outputLabel = textContent.mid(match.capturedStart(0), match.capturedLength(0)).trimmed();
318     qDebug()<<"output label: " << outputLabel;
319 
320     //extract the expression id
321     bool ok;
322     QString idString = outputLabel.mid(3, outputLabel.length()-4);
323     int id = idString.toInt(&ok);
324     if (ok)
325         setId(id);
326 
327     qDebug()<<"expression id: " << this->id();
328 
329     //remove the output label from the text content
330     textContent = textContent.remove(outputLabel).trimmed();
331 
332     //determine the actual result
333     Cantor::Result* result = nullptr;
334 
335     const int latexContentStart = resultContent.indexOf(QLatin1String("<cantor-latex>"));
336     //Handle system maxima output for plotting commands
337     if (m_isPlot && textContent.endsWith(QString::fromLatin1("\"%1\"]").arg(m_tempFile->fileName())))
338     {
339         m_plotResultIndex = results().size();
340         // Gnuplot could generate plot before we parse text output from maxima and after
341         // If we already have plot result, just add it
342         // Else set info message, and replace it by real result in imageChanged function later
343         if (m_plotResult)
344             result = m_plotResult;
345         else
346             result = new Cantor::TextResult(i18n("Waiting for the plot result"));
347     }
348     else if (latexContentStart != -1)
349     {
350         //latex output is available
351         const int latexContentEnd = resultContent.indexOf(QLatin1String("</cantor-latex>"));
352         QString latexContent = resultContent.mid(latexContentStart + 14, latexContentEnd - latexContentStart - 14).trimmed();
353         qDebug()<<"latex content: " << latexContent;
354 
355         Cantor::TextResult* textResult;
356         //replace the \mbox{} environment, if available, by the eqnarray environment
357         if (latexContent.indexOf(QLatin1String("\\mbox{")) != -1)
358         {
359             int i;
360             int pcount=0;
361             for(i = latexContent.indexOf(QLatin1String("\\mbox{"))+5; i < latexContent.size(); ++i)
362             {
363                 if(latexContent[i]==QLatin1Char('{'))
364                     pcount++;
365                 else if(latexContent[i]==QLatin1Char('}'))
366                     pcount--;
367 
368                 if(pcount==0)
369                     break;
370             }
371 
372             QString modifiedLatexContent = latexContent.mid(i+1);
373             if(modifiedLatexContent.trimmed().isEmpty())
374             {
375                 //empty content in the \mbox{} environment (e.g. for print() outputs), use the latex string outside of the \mbox{} environment
376                 modifiedLatexContent = latexContent.left(latexContent.indexOf(QLatin1String("\\mbox{")));
377             }
378 
379             modifiedLatexContent.prepend(QLatin1String("\\begin{eqnarray*}"));
380             modifiedLatexContent.append(QLatin1String("\\end{eqnarray*}"));
381             textResult = new Cantor::TextResult(modifiedLatexContent, textContent);
382             qDebug()<<"modified latex content: " << modifiedLatexContent;
383         }
384         else
385         {
386             //no \mbox{} available, use what we've got.
387             textResult = new Cantor::TextResult(latexContent, textContent);
388         }
389 
390         textResult->setFormat(Cantor::TextResult::LatexFormat);
391         result = textResult;
392     }
393     else
394     {
395         //no latex output is available, the actual result is part of the textContent string
396         result = new Cantor::TextResult(textContent);
397     }
398 
399     addResult(result);
400 }
401 
parseError(const QString & out)402 void MaximaExpression::parseError(const QString& out)
403 {
404     m_errorBuffer.append(out);
405 }
406 
addInformation(const QString & information)407 void MaximaExpression::addInformation(const QString& information)
408 {
409     qDebug()<<"adding information";
410     QString inf=information;
411     if(!inf.endsWith(QLatin1Char(';')))
412         inf+=QLatin1Char(';');
413     Cantor::Expression::addInformation(inf);
414 
415     static_cast<MaximaSession*>(session())->sendInputToProcess(inf+QLatin1Char('\n'));
416 }
417 
imageChanged()418 void MaximaExpression::imageChanged()
419 {
420     if(m_tempFile->size()>0)
421     {
422 #ifdef WITH_EPS
423         m_plotResult = new Cantor::EpsResult( QUrl::fromLocalFile(m_tempFile->fileName()) );
424 #else
425         m_plotResult = new Cantor::ImageResult( QUrl::fromLocalFile(m_tempFile->fileName()) );
426 #endif
427         // Check, that we already parse maxima output for this plot, and if not, keep it up to this moment
428         // If it's true, replace text info result by real plot and set status as Done
429         if (m_plotResultIndex != -1)
430         {
431             replaceResult(m_plotResultIndex, m_plotResult);
432             if (status() != Cantor::Expression::Error)
433                 setStatus(Cantor::Expression::Done);
434         }
435     }
436 }
437