File indexing completed on 2025-04-27 09:51:26
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 }