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