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%#~()*-]|&)+" // allowed characters 0056 "(?:[/@=$_+'0-9a-zA-Z%#~-]|&)" // termination 0057 ")" // end of hyperlink 0058 "|" 0059 "(" // 2: e-mail 0060 "(?:[a-zA-Z0-9_.!#$%'*+/=?^`{|}~-]|&)+" 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 "eLevel, 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(">"); 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(">"); 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(">"); 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 }