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

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2023 Alexander Semke <alexander.semke@web.de>
0005 */
0006 
0007 #include "sageexpression.h"
0008 
0009 #include "sagesession.h"
0010 #include "textresult.h"
0011 #include "imageresult.h"
0012 #include "animationresult.h"
0013 #include "helpresult.h"
0014 
0015 #include <QDebug>
0016 #include <KLocalizedString>
0017 #include <QMimeDatabase>
0018 #include <QRegularExpression>
0019 
0020 SageExpression::SageExpression( Cantor::Session* session, bool internal ) : Cantor::Expression(session, internal)
0021 {
0022 }
0023 
0024 void SageExpression::evaluate()
0025 {
0026     m_imagePath.clear();
0027 
0028     m_isHelpRequest=false;
0029 
0030     //check if this is a ?command or help command
0031     if( command().startsWith(QLatin1Char('?'))
0032         || command().endsWith(QLatin1Char('?'))
0033         || command().contains(QLatin1String("help("))
0034     )
0035         m_isHelpRequest = true;
0036 
0037     //coun't how many newlines are in the command,
0038     //as sage will output one "sage: " or "....:" for
0039     //each.
0040     m_promptCount = command().count(QLatin1Char('\n'))+2;
0041 
0042     session()->enqueueExpression(this);
0043 }
0044 
0045 void SageExpression::parseOutput(const QString& text)
0046 {
0047     if (m_syntaxError)
0048     {
0049         setErrorMessage(i18n("Syntax Error"));
0050         setStatus(Cantor::Expression::Error);
0051         return;
0052     }
0053 
0054     if (text.startsWith(QLatin1String("Launched png viewer"))
0055         || text.startsWith(QLatin1String("Launched pdf viewer"))
0056         || text.startsWith(QLatin1String("Launched gif viewer")) )
0057     {
0058         evalFinished();
0059         return;
0060     }
0061 
0062     QString output = text;
0063     //remove carriage returns, we only use \n internally
0064     output.remove(QLatin1Char('\r'));
0065     //replace appearing backspaces, as they mess the whole output up
0066     //with QRegularExpression/PCRE to make \b match a backspace put it inside []
0067     //see https://perldoc.perl.org/perlrecharclass.html#Bracketed-Character-Classes
0068     output.remove(QRegularExpression(QStringLiteral(".[\b]")));
0069     //replace Escape sequences (only tested with `ls` command)
0070     const QChar ESC(0x1b);
0071     output.remove(QRegularExpression(QString(ESC)+QLatin1String("\\][^\a]*\a")));
0072 
0073     const QString promptRegexpBase(QLatin1String("(^|\\n)%1"));
0074     const QRegularExpression promptRegexp(promptRegexpBase.arg(
0075                 QRegularExpression::escape(QLatin1String(SageSession::SagePrompt))));
0076     const QRegularExpression altPromptRegexp(promptRegexpBase.arg(
0077                 QRegularExpression::escape(QLatin1String(SageSession::SageAlternativePrompt))));
0078 
0079     bool endsWithAlternativePrompt=output.endsWith(QLatin1String(SageSession::SageAlternativePrompt));
0080 
0081     //remove all prompts. we do this in a loop, because after we removed the first prompt,
0082     //there could be a second one, that isn't matched by promptRegexp in the first run, because
0083     //it originally isn't at the beginning of a line.
0084     int index =- 1, index2 =- 1;
0085     while ( (index=output.indexOf(promptRegexp)) != -1 || (index2=output.indexOf(altPromptRegexp)) != -1 )
0086     {
0087         qDebug() << "got prompt" << index << "  " << index2;
0088         if(index != -1)
0089         {
0090             m_promptCount--;
0091 
0092             //remove this prompt, the if is needed, because, if the prompt is on the
0093             //beginning of the string, index points to the "s", if it is within the string
0094             //index points to the newline
0095             if(output[index] == QLatin1Char('\n'))
0096                 output.remove(index + 1, SageSession::SagePrompt.length());
0097             else
0098                 output.remove(index, SageSession::SagePrompt.length());
0099         }
0100 
0101         if(index2 != -1)
0102         {
0103             m_promptCount--;
0104 
0105             //see comment above, for the reason for this "if"
0106             if(output[index2] == QLatin1Char('\n'))
0107                 output.remove(index2 + 1, SageSession::SageAlternativePrompt.length());
0108             else
0109                 output.remove(index2, SageSession::SageAlternativePrompt.length());
0110         }
0111 
0112         //reset the indices
0113         index = index2 = -1;
0114     }
0115 
0116     m_outputCache+=output;
0117 
0118     if(m_promptCount<=0)
0119     {
0120         if(m_promptCount<0)
0121             qDebug()<<"got too many prompts";
0122 
0123         //if the output ends with an AlternativePrompt, this means that
0124         //Sage is expecting additional input, although m_promptCount==0
0125         //indicates that all information has been passed to sage.
0126         //This means that the user has entered an invalid command.
0127         //interrupt it and show an error message
0128         if(endsWithAlternativePrompt)
0129         {
0130             // Exit from sage additional input mode
0131             static_cast<SageSession*>(session())->sendInputToProcess(QLatin1String("\x03"));
0132             m_syntaxError = true;
0133         }
0134         else
0135             evalFinished();
0136     }
0137 }
0138 
0139 void SageExpression::parseError(const QString& text)
0140 {
0141     qDebug() << "error";
0142     setErrorMessage(text);
0143     setStatus(Cantor::Expression::Error);
0144 }
0145 
0146 void SageExpression::addFileResult( const QString& path )
0147 {
0148   QUrl url = QUrl::fromLocalFile(path);
0149   QMimeDatabase db;
0150   QMimeType type = db.mimeTypeForUrl(url);
0151 
0152   if(m_imagePath.isEmpty()||type.name().contains(QLatin1String("image"))||path.endsWith(QLatin1String(".png"))||path.endsWith(QLatin1String(".gif")))
0153       m_imagePath = path;
0154 }
0155 
0156 void SageExpression::evalFinished()
0157 {
0158     qDebug()<<"evaluation finished";
0159     qDebug()<<m_outputCache;
0160 
0161     //check if our image path contains a valid image that we can try to show
0162     const bool hasImage=!m_imagePath.isNull();
0163 
0164     if (!m_outputCache.isEmpty())
0165     {
0166         QString stripped = m_outputCache;
0167         const bool isHtml = stripped.contains(QLatin1String("<html>"));
0168         const bool isLatex = m_outputCache.contains(QLatin1String("\\newcommand{\\Bold}")); //Check if it's latex stuff
0169         if(isLatex) //It's latex stuff so encapsulate it into an eqnarray environment
0170         {
0171             int bol_command_len = QLatin1String("\\newcommand{\\Bold}[1]{\\mathbf{#1}}").size();
0172             int curr_index = stripped.indexOf(QLatin1String("\\newcommand{\\Bold}[1]{\\mathbf{#1}}"))+bol_command_len;
0173             // Add an & for the align environment
0174             stripped.insert(curr_index, QLatin1String("&"));
0175             // Strip away any additional "\\newcommand;{\\Bold}" so that it's compilable by LaTeX
0176             if(stripped.count(QLatin1String("\\newcommand{\\Bold}")) > 1){
0177                 while(curr_index != -1){
0178                     curr_index = stripped.indexOf(QLatin1String("\\newcommand{\\Bold}[1]{\\mathbf{#1}}"), curr_index);
0179                     stripped.remove(curr_index, bol_command_len);
0180                     // Also add an & for left alignment
0181                     stripped.insert(curr_index, QLatin1String("&"));
0182                 }
0183             }
0184             // Replace new-line characters with \\ for LaTeX's newline intepretation
0185             stripped.replace(QLatin1Char('\n'), QLatin1String("\\\\"));
0186             stripped.prepend(QLatin1String("\\begin{align*}"));
0187             stripped.append(QLatin1String("\\end{align*}"));
0188             // TODO: Remove for final merge
0189             qDebug() << "NewCommand";
0190             qDebug() << stripped;
0191         }
0192 
0193         //strip html tags
0194         if(isHtml)
0195             stripped.remove( QRegularExpression( QStringLiteral("<[a-zA-Z\\/][^>]*>") ) );
0196 
0197         if (stripped.endsWith(QLatin1Char('\n')))
0198             stripped.chop(1);
0199 
0200         if (m_isHelpRequest)
0201         {
0202             stripped = stripped.toHtmlEscaped();
0203             stripped.replace(QLatin1Char(' '), QLatin1String("&nbsp;"));
0204             stripped.replace(QLatin1Char('\n'), QLatin1String("<br/>\n"));
0205 
0206             //make things quoted in `` `` bold
0207             stripped.replace(QRegularExpression(QStringLiteral("``([^`]*)``")), QStringLiteral("<b>\\1</b>"));
0208 
0209             addResult(new Cantor::HelpResult(stripped, true));
0210         }
0211         else
0212         {
0213             auto* result=new Cantor::TextResult(stripped);
0214             if(isLatex)
0215                 result->setFormat(Cantor::TextResult::LatexFormat);
0216             addResult(result);
0217         }
0218     }
0219     qDebug()<<"has image " << hasImage;
0220 
0221     if (hasImage)
0222     {
0223         QMimeDatabase db;
0224         QMimeType type = db.mimeTypeForUrl(QUrl::fromLocalFile(m_imagePath));
0225         qDebug()<<"mime type " << type;
0226         qDebug()<<"image path " << m_imagePath;
0227         if(type.inherits(QLatin1String("image/gif")))
0228         {
0229             qDebug()<<"adding animation";
0230             addResult( new Cantor::AnimationResult(QUrl::fromLocalFile(m_imagePath), i18n("Result of %1" , command() ) ) );
0231         }
0232         else
0233         {
0234             qDebug()<<"adding image";
0235             addResult( new Cantor::ImageResult(QUrl::fromLocalFile(m_imagePath ), i18n("Result of %1" , command() ) ) );
0236         }
0237     }
0238     setStatus(Cantor::Expression::Done);
0239 }
0240 
0241 void SageExpression::onProcessError(const QString& msg)
0242 {
0243     QString errMsg = i18n("%1\nThe last output was: \n %2", msg, m_outputCache.trimmed());
0244     setErrorMessage(errMsg);
0245     setStatus(Cantor::Expression::Error);
0246 }
0247 
0248 QString SageExpression::additionalLatexHeaders()
0249 {
0250     //The LaTeX sage needs the amsmath package and some specific macros.
0251     //So include them in the header.
0252     //More about the macros requirement in bug #312738
0253     return QLatin1String("\\usepackage{amsmath}\n"                   \
0254                          "\\newcommand{\\ZZ}{\\Bold{Z}}\n"           \
0255                          "\\newcommand{\\NN}{\\Bold{N}}\n"           \
0256                          "\\newcommand{\\RR}{\\Bold{R}}\n"           \
0257                          "\\newcommand{\\CC}{\\Bold{C}}\n"           \
0258                          "\\newcommand{\\QQ}{\\Bold{Q}}\n"           \
0259                          "\\newcommand{\\QQbar}{\\overline{\\QQ}}\n" \
0260                          "\\newcommand{\\GF}[1]{\\Bold{F}_{#1}}\n"   \
0261                          "\\newcommand{\\Zp}[1]{\\ZZ_{#1}}\n"        \
0262                          "\\newcommand{\\Qp}[1]{\\QQ_{#1}}\n"        \
0263                          "\\newcommand{\\Zmod}[1]{\\ZZ/#1\\ZZ}\n"    \
0264                          "\\newcommand{\\CDF}{\\Bold{C}}\n"          \
0265                          "\\newcommand{\\CIF}{\\Bold{C}}\n"          \
0266                          "\\newcommand{\\CLF}{\\Bold{C}}\n"          \
0267                          "\\newcommand{\\RDF}{\\Bold{R}}\n"          \
0268                          "\\newcommand{\\RIF}{\\Bold{I} \\Bold{R}}\n"\
0269                          "\\newcommand{\\RLF}{\\Bold{R}}\n"          \
0270                          "\\newcommand{\\CFF}{\\Bold{CFF}}\n");
0271 }