File indexing completed on 2024-05-12 11:48:04
0001 /* 0002 SPDX-FileCopyrightText: 2002 Dave Corrie <kde@davecorrie.com> 0003 SPDX-FileCopyrightText: 2014 Daniel Vrátil <dvratil@redhat.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "ktexttohtml.h" 0009 #include "ktexttohtml_p.h" 0010 #include "ktexttohtmlemoticonsinterface.h" 0011 0012 #include <QCoreApplication> 0013 #include <QFile> 0014 #include <QPluginLoader> 0015 #include <QRegularExpression> 0016 #include <QStringList> 0017 0018 #include <limits.h> 0019 0020 static KTextToHTMLEmoticonsInterface *s_emoticonsInterface = nullptr; 0021 0022 static void loadEmoticonsPlugin() 0023 { 0024 static bool triedLoadPlugin = false; 0025 if (!triedLoadPlugin) { 0026 triedLoadPlugin = true; 0027 0028 // Check if QGuiApplication::platformName property exists. This is a 0029 // hackish way of determining whether we are running QGuiApplication, 0030 // because we cannot load the FrameworkIntegration plugin into a 0031 // QCoreApplication, as it would crash immediately 0032 if (qApp->metaObject()->indexOfProperty("platformName") > -1) { 0033 QPluginLoader lib(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/KEmoticonsIntegrationPlugin")); 0034 QObject *rootObj = lib.instance(); 0035 if (rootObj) { 0036 s_emoticonsInterface = rootObj->property(KTEXTTOHTMLEMOTICONS_PROPERTY).value<KTextToHTMLEmoticonsInterface *>(); 0037 } 0038 } 0039 } 0040 if (!s_emoticonsInterface) { 0041 s_emoticonsInterface = new KTextToHTMLEmoticonsDummy(); 0042 } 0043 } 0044 0045 KTextToHTMLHelper::KTextToHTMLHelper(const QString &plainText, int pos, int maxUrlLen, int maxAddressLen) 0046 : mText(plainText) 0047 , mMaxUrlLen(maxUrlLen) 0048 , mMaxAddressLen(maxAddressLen) 0049 , mPos(pos) 0050 { 0051 } 0052 0053 KTextToHTMLEmoticonsInterface *KTextToHTMLHelper::emoticonsInterface() const 0054 { 0055 if (!s_emoticonsInterface) { 0056 loadEmoticonsPlugin(); 0057 } 0058 return s_emoticonsInterface; 0059 } 0060 0061 QString KTextToHTMLHelper::getEmailAddress() 0062 { 0063 QString address; 0064 0065 if (mPos < mText.length() && mText.at(mPos) == QLatin1Char('@')) { 0066 // the following characters are allowed in a dot-atom (RFC 2822): 0067 // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ 0068 const QString allowedSpecialChars = QStringLiteral(".!#$%&'*+-/=?^_`{|}~"); 0069 0070 // determine the local part of the email address 0071 int start = mPos - 1; 0072 while (start >= 0 && mText.at(start).unicode() < 128 0073 && (mText.at(start).isLetterOrNumber() // 0074 || mText.at(start) == QLatin1Char('@') // allow @ to find invalid email addresses 0075 || allowedSpecialChars.indexOf(mText.at(start)) != -1)) { 0076 if (mText.at(start) == QLatin1Char('@')) { 0077 return QString(); // local part contains '@' -> no email address 0078 } 0079 --start; 0080 } 0081 ++start; 0082 // we assume that an email address starts with a letter or a digit 0083 while ((start < mPos) && !mText.at(start).isLetterOrNumber()) { 0084 ++start; 0085 } 0086 if (start == mPos) { 0087 return QString(); // local part is empty -> no email address 0088 } 0089 0090 // determine the domain part of the email address 0091 int dotPos = INT_MAX; 0092 int end = mPos + 1; 0093 while (end < mText.length() 0094 && (mText.at(end).isLetterOrNumber() // 0095 || mText.at(end) == QLatin1Char('@') // allow @ to find invalid email addresses 0096 || mText.at(end) == QLatin1Char('.') // 0097 || mText.at(end) == QLatin1Char('-'))) { 0098 if (mText.at(end) == QLatin1Char('@')) { 0099 return QString(); // domain part contains '@' -> no email address 0100 } 0101 if (mText.at(end) == QLatin1Char('.')) { 0102 dotPos = qMin(dotPos, end); // remember index of first dot in domain 0103 } 0104 ++end; 0105 } 0106 // we assume that an email address ends with a letter or a digit 0107 while ((end > mPos) && !mText.at(end - 1).isLetterOrNumber()) { 0108 --end; 0109 } 0110 if (end == mPos) { 0111 return QString(); // domain part is empty -> no email address 0112 } 0113 if (dotPos >= end) { 0114 return QString(); // domain part doesn't contain a dot 0115 } 0116 0117 if (end - start > mMaxAddressLen) { 0118 return QString(); // too long -> most likely no email address 0119 } 0120 address = mText.mid(start, end - start); 0121 0122 mPos = end - 1; 0123 } 0124 return address; 0125 } 0126 0127 QString KTextToHTMLHelper::getPhoneNumber() 0128 { 0129 if (!mText.at(mPos).isDigit() && mText.at(mPos) != QLatin1Char('+')) { 0130 return {}; 0131 } 0132 0133 const QString allowedBeginSeparators = QStringLiteral(" \r\t\n:"); 0134 if (mPos > 0 && !allowedBeginSeparators.contains(mText.at(mPos - 1))) { 0135 return {}; 0136 } 0137 0138 // this isn't 100% accurate, we filter stuff below that is too hard to capture with a regexp 0139 static const QRegularExpression telPattern(QStringLiteral(R"([+0](( |( ?[/-] ?)?)\(?\d+\)?+){6,30})")); 0140 const auto match = telPattern.match(mText, mPos, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); 0141 if (match.hasMatch()) { 0142 QStringView matchedText = match.capturedView(); 0143 // check for maximum number of digits (15), see https://en.wikipedia.org/wiki/Telephone_numbering_plan 0144 const int digitsCount = std::count_if(matchedText.cbegin(), matchedText.cend(), [](const QChar c) { 0145 return c.isDigit(); 0146 }); 0147 0148 if (digitsCount > 15) { 0149 return {}; 0150 } 0151 0152 // only one / is allowed, otherwise we trigger on dates 0153 if (matchedText.count(QLatin1Char('/')) > 1) { 0154 return {}; 0155 } 0156 0157 // parenthesis need to be balanced, and must not be nested 0158 int openIdx = -1; 0159 for (int i = 0, size = matchedText.size(); i < size; ++i) { 0160 const QChar ch = matchedText.at(i); 0161 if ((ch == QLatin1Char('(') && openIdx >= 0) || (ch == QLatin1Char(')') && openIdx < 0)) { 0162 return {}; 0163 } 0164 0165 if (ch == QLatin1Char('(')) { 0166 openIdx = i; 0167 } else if (ch == QLatin1Char(')')) { 0168 openIdx = -1; 0169 } 0170 } 0171 0172 if (openIdx > 0) { 0173 matchedText.truncate(openIdx - 1); 0174 matchedText = matchedText.trimmed(); 0175 } 0176 0177 // check if there's a plausible separator at the end 0178 const int matchedTextLength = matchedText.size(); 0179 const int endIdx = mPos + matchedTextLength; 0180 if (endIdx < mText.size() && !QStringView(u" \r\t\n,.").contains(mText.at(endIdx))) { 0181 return {}; 0182 } 0183 0184 mPos += matchedTextLength - 1; 0185 return matchedText.toString(); 0186 } 0187 return {}; 0188 } 0189 0190 static QString normalizePhoneNumber(const QString &str) 0191 { 0192 QString res; 0193 res.reserve(str.size()); 0194 for (const auto c : str) { 0195 if (c.isDigit() || c == QLatin1Char('+')) { 0196 res.push_back(c); 0197 } 0198 } 0199 return res; 0200 } 0201 0202 // The following characters are allowed in a dot-atom (RFC 2822): 0203 // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ 0204 static const char s_allowedSpecialChars[] = ".!#$%&'*+-/=?^_`{|}~"; 0205 0206 bool KTextToHTMLHelper::atUrl() const 0207 { 0208 // The character directly before the URL must not be a letter, a number or 0209 // any other character allowed in a dot-atom (RFC 2822). 0210 if (mPos > 0) { 0211 const auto chBefore = mText.at(mPos - 1); 0212 if (chBefore.isLetterOrNumber() || QLatin1String(s_allowedSpecialChars).contains(chBefore)) { 0213 return false; 0214 } 0215 } 0216 0217 const auto segment = QStringView(mText).mid(mPos); 0218 /* clang-format off */ 0219 return segment.startsWith(QLatin1String("http://")) 0220 || segment.startsWith(QLatin1String("https://")) 0221 || segment.startsWith(QLatin1String("vnc://")) 0222 || segment.startsWith(QLatin1String("fish://")) 0223 || segment.startsWith(QLatin1String("ftp://")) 0224 || segment.startsWith(QLatin1String("ftps://")) 0225 || segment.startsWith(QLatin1String("sftp://")) 0226 || segment.startsWith(QLatin1String("smb://")) 0227 || segment.startsWith(QLatin1String("irc://")) 0228 || segment.startsWith(QLatin1String("ircs://")) 0229 || segment.startsWith(QLatin1String("mailto:")) 0230 || segment.startsWith(QLatin1String("www.")) 0231 || segment.startsWith(QLatin1String("ftp.")) 0232 || segment.startsWith(QLatin1String("file://")) 0233 || segment.startsWith(QLatin1String("news:")) 0234 || segment.startsWith(QLatin1String("tel:")) 0235 || segment.startsWith(QLatin1String("xmpp:")); 0236 /* clang-format on */ 0237 } 0238 0239 bool KTextToHTMLHelper::isEmptyUrl(const QString &url) const 0240 { 0241 /* clang-format off */ 0242 return url.isEmpty() 0243 || url == QLatin1String("http://") 0244 || url == QLatin1String("https://") 0245 || url == QLatin1String("fish://") 0246 || url == QLatin1String("ftp://") 0247 || url == QLatin1String("ftps://") 0248 || url == QLatin1String("sftp://") 0249 || url == QLatin1String("smb://") 0250 || url == QLatin1String("vnc://") 0251 || url == QLatin1String("irc://") 0252 || url == QLatin1String("ircs://") 0253 || url == QLatin1String("mailto") 0254 || url == QLatin1String("mailto:") 0255 || url == QLatin1String("www") 0256 || url == QLatin1String("ftp") 0257 || url == QLatin1String("news:") 0258 || url == QLatin1String("news://") 0259 || url == QLatin1String("tel") 0260 || url == QLatin1String("tel:") 0261 || url == QLatin1String("xmpp:"); 0262 /* clang-format on */ 0263 } 0264 0265 QString KTextToHTMLHelper::getUrl(bool *badurl) 0266 { 0267 QString url; 0268 if (atUrl()) { 0269 // NOTE: see http://tools.ietf.org/html/rfc3986#appendix-A and especially appendix-C 0270 // Appendix-C mainly says, that when extracting URLs from plain text, line breaks shall 0271 // be allowed and should be ignored when the URI is extracted. 0272 0273 // This implementation follows this recommendation and 0274 // allows the URL to be enclosed within different kind of brackets/quotes 0275 // If an URL is enclosed, whitespace characters are allowed and removed, otherwise 0276 // the URL ends with the first whitespace 0277 // Also, if the URL is enclosed in brackets, the URL itself is not allowed 0278 // to contain the closing bracket, as this would be detected as the end of the URL 0279 0280 QChar beforeUrl; 0281 QChar afterUrl; 0282 0283 // detect if the url has been surrounded by brackets or quotes 0284 if (mPos > 0) { 0285 beforeUrl = mText.at(mPos - 1); 0286 0287 /*if ( beforeUrl == '(' ) { 0288 afterUrl = ')'; 0289 } else */ 0290 if (beforeUrl == QLatin1Char('[')) { 0291 afterUrl = QLatin1Char(']'); 0292 } else if (beforeUrl == QLatin1Char('<')) { 0293 afterUrl = QLatin1Char('>'); 0294 } else if (beforeUrl == QLatin1Char('>')) { // for e.g. <link>http://.....</link> 0295 afterUrl = QLatin1Char('<'); 0296 } else if (beforeUrl == QLatin1Char('"')) { 0297 afterUrl = QLatin1Char('"'); 0298 } 0299 } 0300 url.reserve(mMaxUrlLen); // avoid allocs 0301 int start = mPos; 0302 bool previousCharIsSpace = false; 0303 bool previousCharIsADoubleQuote = false; 0304 bool previousIsAnAnchor = false; 0305 /* clang-format off */ 0306 while (mPos < mText.length() // 0307 && (mText.at(mPos).isPrint() || mText.at(mPos).isSpace()) 0308 && ((afterUrl.isNull() && !mText.at(mPos).isSpace()) 0309 || (!afterUrl.isNull() && mText.at(mPos) != afterUrl))) { 0310 if (!previousCharIsSpace 0311 && mText.at(mPos) == QLatin1Char('<') 0312 && (mPos + 1) < mText.length()) { /* clang-format on */ 0313 // Fix Bug #346132: allow "http://www.foo.bar<http://foo.bar/>" 0314 // < inside a URL is not allowed, however there is a test which 0315 // checks that "http://some<Host>/path" should be allowed 0316 // Therefore: check if what follows is another URL and if so, stop here 0317 mPos++; 0318 if (atUrl()) { 0319 mPos--; 0320 break; 0321 } 0322 mPos--; 0323 } 0324 if (!previousCharIsSpace && (mText.at(mPos) == QLatin1Char(' ')) && ((mPos + 1) < mText.length())) { 0325 // Fix kmail bug: allow "http://www.foo.bar http://foo.bar/" 0326 // Therefore: check if what follows is another URL and if so, stop here 0327 mPos++; 0328 if (atUrl()) { 0329 mPos--; 0330 break; 0331 } 0332 mPos--; 0333 } 0334 if (mText.at(mPos).isSpace()) { 0335 previousCharIsSpace = true; 0336 } else if (!previousIsAnAnchor && mText.at(mPos) == QLatin1Char('[')) { 0337 break; 0338 } else if (!previousIsAnAnchor && mText.at(mPos) == QLatin1Char(']')) { 0339 break; 0340 } else { // skip whitespace 0341 if (previousCharIsSpace && mText.at(mPos) == QLatin1Char('<')) { 0342 url.append(QLatin1Char(' ')); 0343 break; 0344 } 0345 previousCharIsSpace = false; 0346 if (mText.at(mPos) == QLatin1Char('>') && previousCharIsADoubleQuote) { 0347 // it's an invalid url 0348 if (badurl) { 0349 *badurl = true; 0350 } 0351 return QString(); 0352 } 0353 if (mText.at(mPos) == QLatin1Char('"')) { 0354 previousCharIsADoubleQuote = true; 0355 } else { 0356 previousCharIsADoubleQuote = false; 0357 } 0358 if (mText.at(mPos) == QLatin1Char('#')) { 0359 previousIsAnAnchor = true; 0360 } 0361 url.append(mText.at(mPos)); 0362 if (url.length() > mMaxUrlLen) { 0363 break; 0364 } 0365 } 0366 0367 ++mPos; 0368 } 0369 0370 if (isEmptyUrl(url) || (url.length() > mMaxUrlLen)) { 0371 mPos = start; 0372 url.clear(); 0373 return url; 0374 } else { 0375 --mPos; 0376 } 0377 } 0378 0379 // HACK: This is actually against the RFC. However, most people don't properly escape the URL in 0380 // their text with "" or <>. That leads to people writing an url, followed immediately by 0381 // a dot to finish the sentence. That would lead the parser to include the dot in the url, 0382 // even though that is not wanted. So work around that here. 0383 // Most real-life URLs hopefully don't end with dots or commas. 0384 const QString wordBoundaries = QStringLiteral(".,:!?)>"); 0385 if (url.length() > 1) { 0386 do { 0387 if (wordBoundaries.contains(url.at(url.length() - 1))) { 0388 url.chop(1); 0389 --mPos; 0390 } else { 0391 break; 0392 } 0393 } while (url.length() > 1); 0394 } 0395 return url; 0396 } 0397 0398 QString KTextToHTMLHelper::highlightedText() 0399 { 0400 // formating symbols must be prepended with a whitespace 0401 if ((mPos > 0) && !mText.at(mPos - 1).isSpace()) { 0402 return QString(); 0403 } 0404 0405 const QChar ch = mText.at(mPos); 0406 if (ch != QLatin1Char('/') && ch != QLatin1Char('*') && ch != QLatin1Char('_') && ch != QLatin1Char('-')) { 0407 return QString(); 0408 } 0409 0410 QRegularExpression re(QStringLiteral("\\%1([^\\s|^\\%1].*[^\\s|^\\%1])\\%1").arg(ch)); 0411 re.setPatternOptions(QRegularExpression::InvertedGreedinessOption); 0412 const auto match = re.match(mText, mPos, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); 0413 0414 if (match.hasMatch()) { 0415 if (match.capturedStart() == mPos) { 0416 int length = match.capturedLength(); 0417 // there must be a whitespace after the closing formating symbol 0418 if (mPos + length < mText.length() && !mText.at(mPos + length).isSpace()) { 0419 return QString(); 0420 } 0421 mPos += length - 1; 0422 switch (ch.toLatin1()) { 0423 case '*': 0424 return QLatin1String("<b>*") + match.capturedView(1) + QLatin1String("*</b>"); 0425 case '_': 0426 return QLatin1String("<u>_") + match.capturedView(1) + QLatin1String("_</u>"); 0427 case '/': 0428 return QLatin1String("<i>/") + match.capturedView(1) + QLatin1String("/</i>"); 0429 case '-': 0430 return QLatin1String("<s>-") + match.capturedView(1) + QLatin1String("-</s>"); 0431 } 0432 } 0433 } 0434 return QString(); 0435 } 0436 0437 QString KTextToHTML::convertToHtml(const QString &plainText, const KTextToHTML::Options &flags, int maxUrlLen, int maxAddressLen) 0438 { 0439 KTextToHTMLHelper helper(plainText, 0, maxUrlLen, maxAddressLen); 0440 0441 QString str; 0442 QString result(static_cast<QChar *>(nullptr), helper.mText.length() * 2); 0443 QChar ch; 0444 int x; 0445 bool startOfLine = true; 0446 0447 for (helper.mPos = 0, x = 0; helper.mPos < helper.mText.length(); ++helper.mPos, ++x) { 0448 ch = helper.mText.at(helper.mPos); 0449 if (flags & PreserveSpaces) { 0450 if (ch == QLatin1Char(' ')) { 0451 if (helper.mPos + 1 < helper.mText.length()) { 0452 if (helper.mText.at(helper.mPos + 1) != QLatin1Char(' ')) { 0453 // A single space, make it breaking if not at the start or end of the line 0454 const bool endOfLine = helper.mText.at(helper.mPos + 1) == QLatin1Char('\n'); 0455 if (!startOfLine && !endOfLine) { 0456 result += QLatin1Char(' '); 0457 } else { 0458 result += QLatin1String(" "); 0459 } 0460 } else { 0461 // Whitespace of more than one space, make it all non-breaking 0462 while (helper.mPos < helper.mText.length() && helper.mText.at(helper.mPos) == QLatin1Char(' ')) { 0463 result += QLatin1String(" "); 0464 ++helper.mPos; 0465 ++x; 0466 } 0467 0468 // We incremented once to often, undo that 0469 --helper.mPos; 0470 --x; 0471 } 0472 } else { 0473 // Last space in the text, it is non-breaking 0474 result += QLatin1String(" "); 0475 } 0476 0477 if (startOfLine) { 0478 startOfLine = false; 0479 } 0480 continue; 0481 } else if (ch == QLatin1Char('\t')) { 0482 do { 0483 result += QLatin1String(" "); 0484 ++x; 0485 } while ((x & 7) != 0); 0486 --x; 0487 startOfLine = false; 0488 continue; 0489 } 0490 } 0491 if (ch == QLatin1Char('\n')) { 0492 result += QLatin1String("<br />\n"); // Keep the \n, so apps can figure out the quoting levels correctly. 0493 startOfLine = true; 0494 x = -1; 0495 continue; 0496 } 0497 0498 startOfLine = false; 0499 if (ch == QLatin1Char('&')) { 0500 result += QLatin1String("&"); 0501 } else if (ch == QLatin1Char('"')) { 0502 result += QLatin1String("""); 0503 } else if (ch == QLatin1Char('<')) { 0504 result += QLatin1String("<"); 0505 } else if (ch == QLatin1Char('>')) { 0506 result += QLatin1String(">"); 0507 } else { 0508 const int start = helper.mPos; 0509 if (!(flags & IgnoreUrls)) { 0510 bool badUrl = false; 0511 str = helper.getUrl(&badUrl); 0512 if (badUrl) { 0513 QString resultBadUrl; 0514 for (const QChar chBadUrl : std::as_const(helper.mText)) { 0515 if (chBadUrl == QLatin1Char('&')) { 0516 resultBadUrl += QLatin1String("&"); 0517 } else if (chBadUrl == QLatin1Char('"')) { 0518 resultBadUrl += QLatin1String("""); 0519 } else if (chBadUrl == QLatin1Char('<')) { 0520 resultBadUrl += QLatin1String("<"); 0521 } else if (chBadUrl == QLatin1Char('>')) { 0522 resultBadUrl += QLatin1String(">"); 0523 } else { 0524 resultBadUrl += chBadUrl; 0525 } 0526 } 0527 return resultBadUrl; 0528 } 0529 if (!str.isEmpty()) { 0530 QString hyperlink; 0531 if (str.startsWith(QLatin1String("www."))) { 0532 hyperlink = QLatin1String("http://") + str; 0533 } else if (str.startsWith(QLatin1String("ftp."))) { 0534 hyperlink = QLatin1String("ftp://") + str; 0535 } else { 0536 hyperlink = str; 0537 } 0538 result += QLatin1String("<a href=\"") + hyperlink + QLatin1String("\">") + str.toHtmlEscaped() + QLatin1String("</a>"); 0539 x += helper.mPos - start; 0540 continue; 0541 } 0542 str = helper.getEmailAddress(); 0543 if (!str.isEmpty()) { 0544 // len is the length of the local part 0545 int len = str.indexOf(QLatin1Char('@')); 0546 QString localPart = str.left(len); 0547 0548 // remove the local part from the result (as '&'s have been expanded to 0549 // & we have to take care of the 4 additional characters per '&') 0550 result.truncate(result.length() - len - (localPart.count(QLatin1Char('&')) * 4)); 0551 x -= len; 0552 0553 result += QLatin1String("<a href=\"mailto:") + str + QLatin1String("\">") + str + QLatin1String("</a>"); 0554 x += str.length() - 1; 0555 continue; 0556 } 0557 if (flags & ConvertPhoneNumbers) { 0558 str = helper.getPhoneNumber(); 0559 if (!str.isEmpty()) { 0560 result += QLatin1String("<a href=\"tel:") + normalizePhoneNumber(str) + QLatin1String("\">") + str + QLatin1String("</a>"); 0561 x += str.length() - 1; 0562 continue; 0563 } 0564 } 0565 } 0566 if (flags & HighlightText) { 0567 str = helper.highlightedText(); 0568 if (!str.isEmpty()) { 0569 result += str; 0570 x += helper.mPos - start; 0571 continue; 0572 } 0573 } 0574 result += ch; 0575 } 0576 } 0577 0578 if (flags & ReplaceSmileys) { 0579 const QStringList exclude = {QStringLiteral("(c)"), QStringLiteral("(C)"), QStringLiteral(">:-("), QStringLiteral(">:("), QStringLiteral("(B)"), 0580 QStringLiteral("(b)"), QStringLiteral("(P)"), QStringLiteral("(p)"), QStringLiteral("(O)"), QStringLiteral("(o)"), 0581 QStringLiteral("(D)"), QStringLiteral("(d)"), QStringLiteral("(E)"), QStringLiteral("(e)"), QStringLiteral("(K)"), 0582 QStringLiteral("(k)"), QStringLiteral("(I)"), QStringLiteral("(i)"), QStringLiteral("(L)"), QStringLiteral("(l)"), 0583 QStringLiteral("(8)"), QStringLiteral("(T)"), QStringLiteral("(t)"), QStringLiteral("(G)"), QStringLiteral("(g)"), 0584 QStringLiteral("(F)"), QStringLiteral("(f)"), QStringLiteral("(H)"), QStringLiteral("8)"), QStringLiteral("(N)"), 0585 QStringLiteral("(n)"), QStringLiteral("(Y)"), QStringLiteral("(y)"), QStringLiteral("(U)"), QStringLiteral("(u)"), 0586 QStringLiteral("(W)"), QStringLiteral("(w)"), QStringLiteral("(6)")}; 0587 0588 result = helper.emoticonsInterface()->parseEmoticons(result, true, exclude); 0589 } 0590 0591 return result; 0592 }