File indexing completed on 2024-05-26 05:28:44

0001 /* Copyright (C) 2012 Thomas Lübking <thomas.luebking@gmail.com>
0002    Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0003    Copyright (C) 2018 Erik Quaeghebeur <kde@equaeghe.nospammail.net>
0004 
0005    This file is part of the Trojita Qt IMAP e-mail client,
0006    http://trojita.flaska.net/
0007 
0008    This program is free software; you can redistribute it and/or
0009    modify it under the terms of the GNU General Public License as
0010    published by the Free Software Foundation; either version 2 of
0011    the License or (at your option) version 3 or any later version
0012    accepted by the membership of KDE e.V. (or its successor approved
0013    by the membership of KDE e.V.), which shall act as a proxy
0014    defined in Section 14 of version 3 of the license.
0015 
0016    This program is distributed in the hope that it will be useful,
0017    but WITHOUT ANY WARRANTY; without even the implied warranty of
0018    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0019    GNU General Public License for more details.
0020 
0021    You should have received a copy of the GNU General Public License
0022    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0023 */
0024 
0025 #include <limits>
0026 #include <QColor>
0027 #include <QDateTime>
0028 #include <QFileInfo>
0029 #include <QFontInfo>
0030 #include <QMap>
0031 #include <QModelIndex>
0032 #include <QPair>
0033 #include <QRegularExpression>
0034 #include <QRegularExpressionMatch>
0035 #include <QRegularExpressionMatchIterator>
0036 #include <QStack>
0037 #include "PlainTextFormatter.h"
0038 #include "Common/Paths.h"
0039 #include "Imap/Model/ItemRoles.h"
0040 #include "UiUtils/Color.h"
0041 
0042 namespace UiUtils {
0043 
0044 /** @short Helper for plainTextToHtml for applying the HTML formatting
0045 
0046 This function recognizes http and https links, e-mail addresses, *bold*, /italic/ and _underline_ text.
0047 */
0048 QString helperHtmlifySingleLine(QString line)
0049 {
0050     // Static regexps for the engine construction.
0051     // Warning, these operate on the *escaped* HTML!
0052     static const QRegularExpression patternRe(QLatin1String(
0053                         "(" // 1: hyperlink
0054                             "https?://" // scheme prefix
0055                             "(?:[][;/?:@=$_.+!',0-9a-zA-Z%#~()*-]|&amp;)+" // allowed characters
0056                             "(?:[/@=$_+'0-9a-zA-Z%#~-]|&amp;)" // termination
0057                         ")" // end of hyperlink
0058                         "|"
0059                         "(" // 2: e-mail
0060                             "(?:[a-zA-Z0-9_.!#$%'*+/=?^`{|}~-]|&amp;)+"
0061                             "@"
0062                             "[a-zA-Z0-9._-]+"
0063                         ")" // end of e-mail
0064                         "|"
0065                         "(?<=^|[[({\\s])" // markup group surroundings
0066                             "(" // 3: markup group
0067                                 "([*/_])(?!\\4)" // 4: markup character, not repeated
0068                                     "(\\S+?)" // 5: marked-up text
0069                                 "\\4(?!\\4)" // markup character, not repeated
0070                           ")" // end of markup group
0071                         "(?=$|[])}\\s,;.])" // markup group surroundings
0072                     ), QRegularExpression::CaseInsensitiveOption);
0073 
0074     // Escape the HTML entities
0075     line = line.toHtmlEscaped();
0076 
0077     static const QMap<QString, QChar> markupletter({{QStringLiteral("*"), QLatin1Char('b')},
0078                                                     {QStringLiteral("/"), QLatin1Char('i')},
0079                                                     {QStringLiteral("_"), QLatin1Char('u')}});
0080 
0081     const uint orig_length = line.length();
0082 
0083     // Now prepare markup *bold*, /italic/ and _underline_ and also turn links into HTML.
0084     // This is a bit more involved because we want to apply the regular expressions in a certain order and
0085     // also at the same time prevent the lower-priority regexps from clobbering the output of the previous stages.
0086     QRegularExpressionMatchIterator i = patternRe.globalMatch(line);
0087     QRegularExpressionMatch match;
0088     uint growth = 0;
0089     while (i.hasNext()) {
0090         match = i.next();
0091         switch (match.lastCapturedIndex()) { // at most one match 1 xor 2 xor 5 (5 implies 4 and 3)
0092         case 1:
0093             line.replace(match.capturedStart(1) + growth, match.capturedLength(1),
0094                          QStringLiteral("<a href=\"%1\">%1</a>").arg(match.captured(1)));
0095             break;
0096         case 2:
0097             line.replace(match.capturedStart(2) + growth, match.capturedLength(2),
0098                          QStringLiteral("<a href=\"mailto:%1\">%1</a>").arg(match.captured(2)));
0099             break;
0100         case 5: // Careful here; the inner contents of the current match shall be formatted as well which is why we need recursion
0101             line.replace(match.capturedStart(3) + growth, match.capturedLength(3),
0102                          QStringLiteral("<%1>%2%3%2</%1>")
0103                             .arg(markupletter[match.captured(4)],
0104                                  QStringLiteral("<span class=\"markup\">%1</span>").arg(match.captured(4)),
0105                                  helperHtmlifySingleLine(match.captured(5))));
0106             break;
0107         }
0108         growth = line.length() - orig_length;
0109     }
0110 
0111     return line;
0112 }
0113 
0114 
0115 /** @short Return a preview of the quoted text
0116 
0117 The goal is to produce "roughly N" lines of non-empty text. Blanks are ignored and
0118 lines longer than charsPerLine are broken into multiple chunks.
0119 */
0120 QString firstNLines(const QString &input, int numLines, const int charsPerLine)
0121 {
0122     Q_ASSERT(numLines >= 2);
0123     QString out = input.section(QLatin1Char('\n'), 0, numLines - 1, QString::SectionSkipEmpty);
0124     const int cutoff = numLines * charsPerLine;
0125     if (out.size() >= cutoff) {
0126         int pos = input.indexOf(QLatin1Char(' '), cutoff);
0127         if (pos != -1)
0128             return out.left(pos - 1);
0129     }
0130     return out;
0131 }
0132 
0133 /** @short Helper for closing blockquotes and adding the interactive control elements at the right places */
0134 void closeQuotesUpTo(QStringList &markup, QStack<QPair<int, int> > &controlStack, int &quoteLevel, const int finalQuoteLevel)
0135 {
0136     static QString closingLabel(QStringLiteral("<label for=\"q%1\"></label>"));
0137     static QLatin1String closeSingleQuote("</blockquote>");
0138     static QLatin1String closeQuoteBlock("</span></span>");
0139 
0140     Q_ASSERT(quoteLevel >= finalQuoteLevel);
0141 
0142     while (quoteLevel > finalQuoteLevel) {
0143         // Check whether an interactive control element is supposed to be present here
0144         bool controlBlock = !controlStack.isEmpty() && (quoteLevel == controlStack.top().first);
0145         if (controlBlock) {
0146             markup << closingLabel.arg(controlStack.pop().second);
0147         }
0148         markup << closeSingleQuote;
0149         --quoteLevel;
0150         if (controlBlock) {
0151             markup << closeQuoteBlock;
0152         }
0153     }
0154 }
0155 
0156 /** @short Return a a regular expression which matches the signature separators */
0157 QRegularExpression signatureSeparator()
0158 {
0159     // "-- " is the standards-compliant signature separator.
0160     // "Line of underscores" is non-standard garbage which Mailman happily generates. Yes, it's nasty and ugly.
0161     return QRegularExpression(QLatin1String("^(-- |_{45,55})(\\r)?$"));
0162 }
0163 
0164 struct TextInfo {
0165     int depth;
0166     QString text;
0167 
0168     TextInfo(const int depth, const QString &text): depth(depth), text(text)
0169     {
0170     }
0171 };
0172 
0173 static QString lineWithoutTrailingCr(const QString &line)
0174 {
0175     return line.endsWith(QLatin1Char('\r')) ? line.left(line.size() - 1) : line;
0176 }
0177 
0178 QString plainTextToHtml(const QString &plaintext, const FlowedFormat flowed)
0179 {
0180     QRegularExpression quotemarks;
0181     switch (flowed) {
0182     case FlowedFormat::FLOWED:
0183     case FlowedFormat::FLOWED_DELSP:
0184         quotemarks = QRegularExpression(QLatin1String("^>+"));
0185         break;
0186     case FlowedFormat::PLAIN:
0187         // Also accept > interleaved by spaces. That's what KMail happily produces.
0188         // A single leading space is accepted, too. That's what Gerrit produces.
0189         quotemarks = QRegularExpression(QLatin1String("^( >|>)+"));
0190         break;
0191     }
0192     const int SIGNATURE_SEPARATOR = -2;
0193 
0194     auto lines = plaintext.split(QLatin1Char('\n'));
0195     std::vector<TextInfo> lineBuffer;
0196     lineBuffer.reserve(lines.size());
0197 
0198     // First pass: determine the quote level for each source line.
0199     // The quote level is ignored for the signature.
0200     bool signatureSeparatorSeen = false;
0201     Q_FOREACH(const QString &line, lines) {
0202 
0203         // Fast path for empty lines
0204         if (line.isEmpty()) {
0205             lineBuffer.emplace_back(0, line);
0206             continue;
0207         }
0208 
0209         // Special marker for the signature separator
0210         if (signatureSeparator().match(line).hasMatch()) {
0211             lineBuffer.emplace_back(SIGNATURE_SEPARATOR, lineWithoutTrailingCr(line));
0212             signatureSeparatorSeen = true;
0213             continue;
0214         }
0215 
0216         // Determine the quoting level
0217         int quoteLevel = 0;
0218         QRegularExpressionMatch match = quotemarks.match(line);
0219         if (!signatureSeparatorSeen && match.capturedStart() == 0) {
0220             quoteLevel = match.captured().count(QLatin1Char('>'));
0221         }
0222 
0223         lineBuffer.emplace_back(quoteLevel, lineWithoutTrailingCr(line));
0224     }
0225 
0226     // Second pass:
0227     // - Remove the quotemarks for everything prior to the signature separator.
0228     // - Collapse the lines with the same quoting level into a single block
0229     //   (optionally into a single line if format=flowed is active)
0230     auto it = lineBuffer.begin();
0231     while (it < lineBuffer.end() && it->depth != SIGNATURE_SEPARATOR) {
0232 
0233         // Remove the quotemarks
0234         it->text.remove(quotemarks);
0235 
0236         switch (flowed) {
0237         case FlowedFormat::FLOWED:
0238         case FlowedFormat::FLOWED_DELSP:
0239             if (flowed == FlowedFormat::FLOWED || flowed == FlowedFormat::FLOWED_DELSP) {
0240                 // check for space-stuffing
0241                 if (it->text.startsWith(QLatin1Char(' '))) {
0242                     it->text.remove(0, 1);
0243                 }
0244 
0245                 // quirk: fix a flowed line which actually isn't flowed
0246                 if (it->text.endsWith(QLatin1Char(' ')) && (
0247                         it+1 == lineBuffer.end() || // end-of-document
0248                         (it+1)->depth == SIGNATURE_SEPARATOR || // right in front of the separator
0249                         (it+1)->depth != it->depth // end of paragraph
0250                    )) {
0251                     it->text.chop(1);
0252                 }
0253             }
0254             break;
0255         case FlowedFormat::PLAIN:
0256             if (it->depth > 0 && it->text.startsWith(QLatin1Char(' '))) {
0257                 // Because the space is re-added when we prepend the quotes. Adding that space is done
0258                 // in order to make it look nice, i.e. to prevent lines like ">>something".
0259                 it->text.remove(0, 1);
0260             }
0261             break;
0262         }
0263 
0264 
0265         if (it == lineBuffer.begin()) {
0266             // No "previous line"
0267             ++it;
0268             continue;
0269         }
0270 
0271         // Check for the line joining
0272         auto prev = it - 1;
0273         if (prev->depth == it->depth) {
0274 
0275             QString separator = QStringLiteral("\n");
0276             switch (flowed) {
0277             case FlowedFormat::PLAIN:
0278                 // nothing fancy to do here, we cannot really join lines
0279                 break;
0280             case FlowedFormat::FLOWED:
0281             case FlowedFormat::FLOWED_DELSP:
0282                 // CR LF trailing is stripped already (LFs by the split into lines, CRs by lineWithoutTrailingCr in pass #1),
0283                 // so we only have to check for the trailing space
0284                 if (prev->text.endsWith(QLatin1Char(' '))) {
0285 
0286                     // implement the DelSp thingy
0287                     if (flowed == FlowedFormat::FLOWED_DELSP) {
0288                         prev->text.chop(1);
0289                     }
0290 
0291                     if (it->text.isEmpty() || prev->text.isEmpty()) {
0292                         // This one or the previous line is a blank one, so we cannot really join them
0293                     } else {
0294                         separator = QString();
0295                     }
0296                 }
0297                 break;
0298             }
0299             prev->text += separator + it->text;
0300             it = lineBuffer.erase(it);
0301         } else {
0302             ++it;
0303         }
0304     }
0305 
0306     // Third pass: HTML escaping, formatting and adding fancy markup
0307     signatureSeparatorSeen = false;
0308     int quoteLevel = 0;
0309     QStringList markup;
0310     int interactiveControlsId = 0;
0311     QStack<QPair<int,int> > controlStack;
0312     for (it = lineBuffer.begin(); it != lineBuffer.end(); ++it) {
0313 
0314         if (it->depth == SIGNATURE_SEPARATOR && !signatureSeparatorSeen) {
0315             // The first signature separator
0316             signatureSeparatorSeen = true;
0317             closeQuotesUpTo(markup, controlStack, quoteLevel, 0);
0318             markup << QLatin1String("<span class=\"signature\">") + helperHtmlifySingleLine(it->text);
0319             markup << QStringLiteral("\n");
0320             continue;
0321         }
0322 
0323         if (signatureSeparatorSeen) {
0324             // Just copy the data
0325             markup << helperHtmlifySingleLine(it->text);
0326             if (it+1 != lineBuffer.end())
0327                 markup << QStringLiteral("\n");
0328             continue;
0329         }
0330 
0331         Q_ASSERT(quoteLevel == 0 || quoteLevel != it->depth);
0332 
0333         if (quoteLevel > it->depth) {
0334             // going back in the quote hierarchy
0335             closeQuotesUpTo(markup, controlStack, quoteLevel, it->depth);
0336         }
0337 
0338         // Pretty-formatted block of the ">>>" characters
0339         QString quotemarks;
0340 
0341         if (it->depth) {
0342             quotemarks += QLatin1String("<span class=\"quotemarks\">");
0343             for (int i = 0; i < it->depth; ++i) {
0344                 quotemarks += QLatin1String("&gt;");
0345             }
0346             quotemarks += QLatin1String(" </span>");
0347         }
0348 
0349         static const int previewLines = 5;
0350         static const int charsPerLineEquivalent = 160;
0351         static const int forceCollapseAfterLines = 10;
0352 
0353         if (quoteLevel < it->depth) {
0354             // We're going deeper in the quote hierarchy
0355             QString line;
0356             while (quoteLevel < it->depth) {
0357                 ++quoteLevel;
0358 
0359                 // Check whether there is anything at the newly entered level of nesting
0360                 bool anythingOnJustThisLevel = false;
0361 
0362                 // A short summary of the quotation
0363                 QString preview;
0364 
0365                 auto runner = it;
0366                 while (runner != lineBuffer.end()) {
0367                     if (runner->depth == quoteLevel) {
0368                         anythingOnJustThisLevel = true;
0369 
0370                         ++interactiveControlsId;
0371                         controlStack.push(qMakePair(quoteLevel, interactiveControlsId));
0372 
0373                         QString omittedStuff;
0374                         QString previewPrefix, previewSuffix;
0375                         QString currentChunk = firstNLines(runner->text, previewLines, charsPerLineEquivalent);
0376                         QString omittedPrefix, omittedSuffix;
0377                         QString previewQuotemarks;
0378 
0379                         if (runner != it ) {
0380                             // we have skipped something, make it obvious to the user
0381 
0382                             // Find the closest level which got collapsed
0383                             int closestDepth = std::numeric_limits<int>::max();
0384                             auto depthRunner(it);
0385                             while (depthRunner != runner) {
0386                                 closestDepth = std::min(closestDepth, depthRunner->depth);
0387                                 ++depthRunner;
0388                             }
0389 
0390                             // The [...] marks shall be prefixed by the closestDepth quote markers
0391                             omittedStuff = QStringLiteral("<span class=\"quotemarks\">");
0392                             for (int i = 0; i < closestDepth; ++i) {
0393                                 omittedStuff += QLatin1String("&gt;");
0394                             }
0395                             for (int i = runner->depth; i < closestDepth; ++i) {
0396                                 omittedPrefix += QLatin1String("<blockquote>");
0397                                 omittedSuffix += QLatin1String("</blockquote>");
0398                             }
0399                             omittedStuff += QStringLiteral(" </span><label for=\"q%1\">...</label>").arg(interactiveControlsId);
0400 
0401                             // Now produce the proper quotation for the preview itself
0402                             for (int i = quoteLevel; i < runner->depth; ++i) {
0403                                 previewPrefix.append(QLatin1String("<blockquote>"));
0404                                 previewSuffix.append(QLatin1String("</blockquote>"));
0405                             }
0406                         }
0407 
0408                         previewQuotemarks = QStringLiteral("<span class=\"quotemarks\">");
0409                         for (int i = 0; i < runner->depth; ++i) {
0410                             previewQuotemarks += QLatin1String("&gt;");
0411                         }
0412                         previewQuotemarks += QLatin1String(" </span>");
0413 
0414                         preview = previewPrefix
0415                                     + omittedPrefix + omittedStuff + omittedSuffix
0416                                 + previewQuotemarks
0417                                 + helperHtmlifySingleLine(currentChunk)
0418                                     .replace(QLatin1String("\n"), QLatin1String("\n") + previewQuotemarks)
0419                                 + previewSuffix;
0420 
0421                         break;
0422                     }
0423                     if (runner->depth < quoteLevel) {
0424                         // This means that we have left the current level of nesting, so there cannot possible be anything else
0425                         // at the current level of nesting *and* in the current quote block
0426                         break;
0427                     }
0428                     ++runner;
0429                 }
0430 
0431                 // Is there nothing but quotes until the end of mail or until the signature separator?
0432                 bool nothingButQuotesAndSpaceTillSignature = true;
0433                 runner = it;
0434                 while (++runner != lineBuffer.end()) {
0435                     if (runner->depth == SIGNATURE_SEPARATOR)
0436                         break;
0437                     if (runner->depth > 0)
0438                         continue;
0439                     if (runner->depth == 0 && !runner->text.isEmpty()) {
0440                         nothingButQuotesAndSpaceTillSignature = false;
0441                         break;
0442                     }
0443                 }
0444 
0445                 // Size of the current level, including the nested stuff
0446                 int currentLevelCharCount = 0;
0447                 int currentLevelLineCount = 0;
0448                 runner = it;
0449                 while (runner != lineBuffer.end() && runner->depth >= quoteLevel) {
0450                     currentLevelCharCount += runner->text.size();
0451                     // one for the actual block
0452                     currentLevelLineCount += runner->text.count(QLatin1Char('\n')) + 1;
0453                     ++runner;
0454                 }
0455 
0456 
0457                 if (!anythingOnJustThisLevel) {
0458                     // no need for fancy UI controls
0459                     line += QLatin1String("<blockquote>");
0460                     continue;
0461                 }
0462 
0463                 if (quoteLevel == it->depth
0464                         && currentLevelCharCount <= charsPerLineEquivalent * previewLines
0465                         && currentLevelLineCount <= previewLines) {
0466                     // special case: the quote is very short, no point in making it collapsible
0467                     line += QStringLiteral("<span class=\"level\"><input type=\"checkbox\" id=\"q%1\"/>").arg(interactiveControlsId)
0468                             + QLatin1String("<span class=\"shortquote\"><blockquote>") + quotemarks
0469                             + helperHtmlifySingleLine(it->text).replace(QLatin1String("\n"), QLatin1String("\n") + quotemarks);
0470                 } else {
0471                     bool collapsed = nothingButQuotesAndSpaceTillSignature
0472                             || quoteLevel > 1
0473                             || currentLevelCharCount >= charsPerLineEquivalent * forceCollapseAfterLines
0474                             || currentLevelLineCount >= forceCollapseAfterLines;
0475 
0476                     line += QStringLiteral("<span class=\"level\"><input type=\"checkbox\" id=\"q%1\" %2/>")
0477                             .arg(QString::number(interactiveControlsId),
0478                                  collapsed ? QStringLiteral("checked=\"checked\"") : QString())
0479                             + QLatin1String("<span class=\"short\"><blockquote>")
0480                               + preview
0481                               + QStringLiteral(" <label for=\"q%1\">...</label>").arg(interactiveControlsId)
0482                               + QLatin1String("</blockquote></span>")
0483                             + QLatin1String("<span class=\"full\"><blockquote>");
0484                     if (quoteLevel == it->depth) {
0485                         // We're now finally on the correct level of nesting so we can output the current line
0486                         line += quotemarks + helperHtmlifySingleLine(it->text)
0487                                 .replace(QLatin1String("\n"), QLatin1String("\n") + quotemarks);
0488                     }
0489                 }
0490             }
0491             markup << line;
0492         } else {
0493             // Either no quotation or we're continuing an old quote block and there was a nested quotation before
0494             markup << quotemarks + helperHtmlifySingleLine(it->text)
0495                       .replace(QLatin1String("\n"), QLatin1String("\n") + quotemarks);
0496         }
0497 
0498         auto next = it + 1;
0499         if (next != lineBuffer.end()) {
0500             if (next->depth >= 0 && next->depth < it->depth) {
0501                 // Decreasing the quotation level -> no starting <blockquote>
0502                 markup << QStringLiteral("\n");
0503             } else if (it->depth == 0) {
0504                 // Non-quoted block which is not enclosed in a <blockquote>
0505                 markup << QStringLiteral("\n");
0506             }
0507         }
0508     }
0509 
0510     if (signatureSeparatorSeen) {
0511         // Terminate the signature
0512         markup << QStringLiteral("</span>");
0513     }
0514 
0515     if (quoteLevel) {
0516         // Terminate the quotes
0517         closeQuotesUpTo(markup, controlStack, quoteLevel, 0);
0518     }
0519 
0520     Q_ASSERT(controlStack.isEmpty());
0521 
0522     return markup.join(QString());
0523 }
0524 
0525 QString htmlizedTextPart(const QModelIndex &partIndex, const QFontInfo &font, const QColor &backgroundColor, const QColor &textColor,
0526                          const QColor &linkColor, const QColor &visitedLinkColor)
0527 {
0528     static const QString defaultStyle = QString::fromUtf8(
0529         "pre{word-wrap: break-word; white-space: pre-wrap;}"
0530         // The following line, sadly, produces a warning "QFont::setPixelSize: Pixel size <= 0 (0)".
0531         // However, if it is not in place or if the font size is set higher, even to 0.1px, WebKit reserves space for the
0532         // quotation characters and therefore a weird white area appears. Even width: 0px doesn't help, so it looks like
0533         // we will have to live with this warning for the time being.
0534         ".quotemarks{color:transparent;font-size:0px;}"
0535 
0536         // Cannot really use the :dir(rtl) selector for putting the quote indicator to the "correct" side.
0537         // It's CSS4 and it isn't supported yet.
0538         "blockquote{font-size:90%; margin: 4pt 0 4pt 0; padding: 0 0 0 1em; border-left: 2px solid %1; unicode-bidi: -webkit-plaintext}"
0539 
0540         // Stop the font size from getting smaller after reaching two levels of quotes
0541         // (ie. starting on the third level, don't make the size any smaller than what it already is)
0542         "blockquote blockquote blockquote {font-size: 100%}"
0543         ".signature{opacity: 0.6;}"
0544 
0545         // Dynamic quote collapsing via pure CSS, yay
0546         "input {display: none}"
0547         "input ~ span.full {display: block}"
0548         "input ~ span.short {display: none}"
0549         "input:checked ~ span.full {display: none}"
0550         "input:checked ~ span.short {display: block}"
0551         "label {border: 1px solid %2; border-radius: 5px; padding: 0px 4px 0px 4px; white-space: nowrap}"
0552         // BLACK UP-POINTING SMALL TRIANGLE (U+25B4)
0553         // BLACK DOWN-POINTING SMALL TRIANGLE (U+25BE)
0554         "span.full > blockquote > label:before {content: \"\u25b4\"}"
0555         "span.short > blockquote > label:after {content: \" \u25be\"}"
0556         "span.shortquote > blockquote > label {display: none}"
0557     );
0558 
0559     QString fontSpecification(QStringLiteral("pre{"));
0560     if (font.italic())
0561         fontSpecification += QLatin1String("font-style: italic; ");
0562     if (font.bold())
0563         fontSpecification += QLatin1String("font-weight: bold; ");
0564     fontSpecification += QStringLiteral("font-size: %1px; font-family: \"%2\", monospace }").arg(
0565                 QString::number(font.pixelSize()), font.family());
0566 
0567     QString textColors = QString::fromUtf8("body { background-color: %1; color: %2 }"
0568                                            "a:link { color: %3 } a:visited { color: %4 } a:hover { color: %3 }").arg(
0569                 backgroundColor.name(), textColor.name(), linkColor.name(), visitedLinkColor.name());
0570     // looks like there's no special color for hovered links in Qt
0571 
0572     // build stylesheet and html header
0573     QColor tintForQuoteIndicator = backgroundColor;
0574     tintForQuoteIndicator.setAlpha(0x66);
0575     QString stylesheet = defaultStyle.arg(linkColor.name(),
0576                                                  tintColor(textColor, tintForQuoteIndicator).name());
0577 
0578     QFile file(Common::writablePath(Common::LOCATION_DATA) + QLatin1String("message.css"));
0579     if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0580         const QString userSheet = QString::fromLocal8Bit(file.readAll().data());
0581         stylesheet += QLatin1Char('\n') + userSheet;
0582         file.close();
0583     }
0584 
0585     // The dir="auto" is required for WebKit to treat all paragraphs as entities with possibly different text direction.
0586     // The individual paragraphs unfortunately share the same text alignment, though, as per
0587     // https://bugs.webkit.org/show_bug.cgi?id=71194 (fixed in Blink already).
0588     QString htmlHeader(QLatin1String("<html><head><style type=\"text/css\"><!--") + textColors + fontSpecification + stylesheet +
0589                        QLatin1String("--></style></head><body><pre dir=\"auto\">"));
0590     static QString htmlFooter(QStringLiteral("\n</pre></body></html>"));
0591 
0592 
0593     // We cannot rely on the QWebFrame's toPlainText because of https://bugs.kde.org/show_bug.cgi?id=321160
0594     QString markup = plainTextToHtml(partIndex.data(Imap::Mailbox::RolePartUnicodeText).toString(), flowedFormatForPart(partIndex));
0595 
0596     return htmlHeader + markup + htmlFooter;
0597 }
0598 
0599 FlowedFormat flowedFormatForPart(const QModelIndex &partIndex)
0600 {
0601     FlowedFormat flowedFormat = FlowedFormat::PLAIN;
0602     if (partIndex.data(Imap::Mailbox::RolePartContentFormat).toString().toLower() == QLatin1String("flowed")) {
0603         flowedFormat = FlowedFormat::FLOWED;
0604 
0605         if (partIndex.data(Imap::Mailbox::RolePartContentDelSp).toString().toLower() == QLatin1String("yes"))
0606             flowedFormat = FlowedFormat::FLOWED_DELSP;
0607     }
0608     return flowedFormat;
0609 }
0610 
0611 }