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 }