File indexing completed on 2025-03-09 04:54:35

0001 /*
0002    SPDX-FileCopyrightText: 2016 Sandro Knauß <sknauss@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "quotehtml.h"
0008 
0009 #include "utils/iconnamecache.h"
0010 #include "viewer/csshelperbase.h"
0011 
0012 #include <MessageViewer/HtmlWriter>
0013 #include <MessageViewer/MessagePartRendererBase>
0014 
0015 #include <KTextToHTML>
0016 
0017 #include <QSharedPointer>
0018 
0019 /** Check if the newline at position @p newLinePos in string @p s
0020     seems to separate two paragraphs (important for correct BiDi
0021     behavior, but is heuristic because paragraphs are not
0022     well-defined) */
0023 // Guesstimate if the newline at newLinePos actually separates paragraphs in the text s
0024 // We use several heuristics:
0025 // 1. If newLinePos points after or before (=at the very beginning of) text, it is not between paragraphs
0026 // 2. If the previous line was longer than the wrap size, we want to consider it a paragraph on its own
0027 //    (some clients, notably Outlook, send each para as a line in the plain-text version).
0028 // 3. Otherwise, we check if the newline could have been inserted for wrapping around; if this
0029 //    was the case, then the previous line will be shorter than the wrap size (which we already
0030 //    know because of item 2 above), but adding the first word from the next line will make it
0031 //    longer than the wrap size.
0032 bool looksLikeParaBreak(const QString &s, int newLinePos)
0033 {
0034     const int WRAP_COL = 78;
0035 
0036     int length = s.length();
0037     // 1. Is newLinePos at an end of the text?
0038     if (newLinePos >= length - 1 || newLinePos == 0) {
0039         return false;
0040     }
0041 
0042     // 2. Is the previous line really a paragraph -- longer than the wrap size?
0043 
0044     // First char of prev line -- works also for first line
0045     int prevStart = s.lastIndexOf(QLatin1Char('\n'), newLinePos - 1) + 1;
0046     int prevLineLength = newLinePos - prevStart;
0047     if (prevLineLength > WRAP_COL) {
0048         return true;
0049     }
0050 
0051     // find next line to delimit search for first word
0052     int nextStart = newLinePos + 1;
0053     int nextEnd = s.indexOf(QLatin1Char('\n'), nextStart);
0054     if (nextEnd == -1) {
0055         nextEnd = length;
0056     }
0057     QString nextLine = s.mid(nextStart, nextEnd - nextStart);
0058     length = nextLine.length();
0059     // search for first word in next line
0060     int wordStart;
0061     bool found = false;
0062     for (wordStart = 0; !found && wordStart < length; wordStart++) {
0063         switch (nextLine[wordStart].toLatin1()) {
0064         case '>':
0065         case '|':
0066         case ' ': // spaces, tabs and quote markers don't count
0067         case '\t':
0068         case '\r':
0069             break;
0070         default:
0071             found = true;
0072             break;
0073         }
0074     } /* for() */
0075 
0076     if (!found) {
0077         // next line is essentially empty, it seems -- empty lines are
0078         // para separators
0079         return true;
0080     }
0081     // Find end of first word.
0082     // Note: flowText (in kmmessage.cpp) separates words for wrap by
0083     // spaces only. This should be consistent, which calls for some
0084     // refactoring.
0085     int wordEnd = nextLine.indexOf(QLatin1Char(' '), wordStart);
0086     if (wordEnd == (-1)) {
0087         wordEnd = length;
0088     }
0089     int wordLength = wordEnd - wordStart;
0090 
0091     // 3. If adding a space and the first word to the prev line don't
0092     //    make it reach the wrap column, then the break was probably
0093     //    meaningful
0094     return prevLineLength + wordLength + 1 < WRAP_COL;
0095 }
0096 
0097 void quotedHTML(const QString &s, MessageViewer::RenderContext *context, MessageViewer::HtmlWriter *htmlWriter)
0098 {
0099     const auto cssHelper = context->cssHelper();
0100     Q_ASSERT(cssHelper);
0101 
0102     KTextToHTML::Options convertFlags = KTextToHTML::PreserveSpaces | KTextToHTML::HighlightText | KTextToHTML::ConvertPhoneNumbers;
0103     if (context->showEmoticons()) {
0104         convertFlags |= KTextToHTML::ReplaceSmileys;
0105     }
0106 
0107     const QString normalStartTag = cssHelper->nonQuotedFontTag();
0108     QString quoteFontTag[3];
0109     QString deepQuoteFontTag[3];
0110     for (int i = 0; i < 3; ++i) {
0111         quoteFontTag[i] = cssHelper->quoteFontTag(i);
0112         deepQuoteFontTag[i] = cssHelper->quoteFontTag(i + 3);
0113     }
0114     const QString normalEndTag = QStringLiteral("</div>");
0115     const QString quoteEnd = QStringLiteral("</div>");
0116 
0117     const int length = s.length();
0118     bool paraIsRTL = false;
0119     bool startNewPara = true;
0120     int pos;
0121     int beg;
0122 
0123     // skip leading empty lines
0124     for (pos = 0; pos < length && s[pos] <= QLatin1Char(' '); ++pos) { }
0125     while (pos > 0 && (s[pos - 1] == QLatin1Char(' ') || s[pos - 1] == QLatin1Char('\t'))) {
0126         pos--;
0127     }
0128     beg = pos;
0129 
0130     int currQuoteLevel = -2; // -2 == no previous lines
0131     bool curHidden = false; // no hide any block
0132 
0133     QString collapseIconPath;
0134     QString expandIconPath;
0135     if (context->showExpandQuotesMark()) {
0136         collapseIconPath = MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quotecollapse.png"));
0137         expandIconPath = MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quoteexpand.png"));
0138     }
0139 
0140     int previousQuoteDepth = -1;
0141     while (beg < length) {
0142         /* search next occurrence of '\n' */
0143         pos = s.indexOf(QLatin1Char('\n'), beg, Qt::CaseInsensitive);
0144         if (pos == -1) {
0145             pos = length;
0146         }
0147 
0148         QString line(s.mid(beg, pos - beg));
0149         beg = pos + 1;
0150 
0151         bool foundQuote = false;
0152         /* calculate line's current quoting depth */
0153         int actQuoteLevel = -1;
0154         const int numberOfCaracters(line.length());
0155         int quoteLength = 0;
0156         for (int p = 0; p < numberOfCaracters; ++p) {
0157             switch (line[p].toLatin1()) {
0158             case '>':
0159             case '|':
0160                 if (p == 0 || foundQuote) {
0161                     actQuoteLevel++;
0162                     quoteLength = p;
0163                     foundQuote = true;
0164                 }
0165                 break;
0166             case ' ': // spaces and tabs are allowed between the quote markers
0167             case '\t':
0168             case '\r':
0169                 break;
0170             default: // stop quoting depth calculation
0171                 p = numberOfCaracters;
0172                 break;
0173             }
0174         } /* for() */
0175         if (!foundQuote) {
0176             quoteLength = 0;
0177         }
0178         bool actHidden = false;
0179 
0180         // This quoted line needs be hidden
0181         if (context->showExpandQuotesMark() && context->levelQuote() >= 0 && context->levelQuote() <= actQuoteLevel) {
0182             actHidden = true;
0183         }
0184 
0185         if (actQuoteLevel != currQuoteLevel) {
0186             /* finish last quotelevel */
0187             if (currQuoteLevel == -1) {
0188                 htmlWriter->write(normalEndTag);
0189             } else if (currQuoteLevel >= 0 && !curHidden) {
0190                 htmlWriter->write(quoteEnd);
0191             }
0192             // Close blockquote
0193             if (previousQuoteDepth > actQuoteLevel) {
0194                 htmlWriter->write(cssHelper->addEndBlockQuote(previousQuoteDepth - actQuoteLevel));
0195             }
0196 
0197             /* start new quotelevel */
0198             if (actQuoteLevel == -1) {
0199                 htmlWriter->write(normalStartTag);
0200             } else {
0201                 if (context->showExpandQuotesMark()) {
0202                     // Add blockquote
0203                     if (previousQuoteDepth < actQuoteLevel) {
0204                         htmlWriter->write(cssHelper->addStartBlockQuote(actQuoteLevel - previousQuoteDepth));
0205                     }
0206                     if (actHidden) {
0207                         // only show the QuoteMark when is the first line of the level hidden
0208                         if (!curHidden) {
0209                             // Expand all quotes
0210                             htmlWriter->write(QStringLiteral("<div class=\"quotelevelmark\" >"));
0211                             htmlWriter->write(QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
0212                                                              "<img src=\"%2\"/></a>")
0213                                                   .arg(-1)
0214                                                   .arg(expandIconPath));
0215                             htmlWriter->write(QStringLiteral("</div><br/>"));
0216                         }
0217                     } else {
0218                         htmlWriter->write(QStringLiteral("<div class=\"quotelevelmark\" >"));
0219                         htmlWriter->write(QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
0220                                                          "<img src=\"%2\"/></a>")
0221                                               .arg(actQuoteLevel)
0222                                               .arg(collapseIconPath));
0223                         htmlWriter->write(QStringLiteral("</div>"));
0224                         if (actQuoteLevel < 3) {
0225                             htmlWriter->write(quoteFontTag[actQuoteLevel]);
0226                         } else {
0227                             htmlWriter->write(deepQuoteFontTag[actQuoteLevel % 3]);
0228                         }
0229                     }
0230                 } else {
0231                     // Add blockquote
0232                     if (previousQuoteDepth < actQuoteLevel) {
0233                         htmlWriter->write(cssHelper->addStartBlockQuote(actQuoteLevel - previousQuoteDepth));
0234                     }
0235 
0236                     if (actQuoteLevel < 3) {
0237                         htmlWriter->write(quoteFontTag[actQuoteLevel]);
0238                     } else {
0239                         htmlWriter->write(deepQuoteFontTag[actQuoteLevel % 3]);
0240                     }
0241                 }
0242             }
0243             currQuoteLevel = actQuoteLevel;
0244         }
0245         curHidden = actHidden;
0246 
0247         if (!actHidden) {
0248             // don't write empty <div ...></div> blocks (they have zero height)
0249             // ignore ^M DOS linebreaks
0250             if (!line.remove(QLatin1Char('\015')).isEmpty()) {
0251                 if (startNewPara) {
0252                     paraIsRTL = line.isRightToLeft();
0253                 }
0254                 htmlWriter->write(QStringLiteral("<div dir=\"%1\">").arg(paraIsRTL ? QStringLiteral("rtl") : QStringLiteral("ltr")));
0255                 // if quoteLengh == 0 && foundQuote => a simple quote
0256                 if (foundQuote) {
0257                     quoteLength++;
0258                     const int rightString = (line.length()) - quoteLength;
0259                     if (rightString > 0) {
0260                         htmlWriter->write(QStringLiteral("<span class=\"quotemarks\">%1</span>").arg(line.left(quoteLength)));
0261                         htmlWriter->write(QStringLiteral("<font color=\"%1\">").arg(cssHelper->quoteColorName(actQuoteLevel)));
0262                         const QString str = KTextToHTML::convertToHtml(line.right(rightString), convertFlags, 4096, 512);
0263                         htmlWriter->write(str);
0264                         htmlWriter->write(QStringLiteral("</font>"));
0265                     } else {
0266                         htmlWriter->write(QStringLiteral("<span class=\"quotemarksemptyline\">%1</span>").arg(line.left(quoteLength)));
0267                     }
0268                 } else {
0269                     htmlWriter->write(KTextToHTML::convertToHtml(line, convertFlags, 4096, 512));
0270                 }
0271 
0272                 htmlWriter->write(QStringLiteral("</div>"));
0273                 startNewPara = looksLikeParaBreak(s, pos);
0274             } else {
0275                 htmlWriter->write(QStringLiteral("<br/>"));
0276                 // after an empty line, always start a new paragraph
0277                 startNewPara = true;
0278             }
0279         }
0280         previousQuoteDepth = actQuoteLevel;
0281     } /* while() */
0282 
0283     /* really finish the last quotelevel */
0284     if (currQuoteLevel == -1) {
0285         htmlWriter->write(normalEndTag);
0286     } else {
0287         htmlWriter->write(quoteEnd + cssHelper->addEndBlockQuote(currQuoteLevel + 1));
0288     }
0289 }