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("&nbsp;");
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("&nbsp;");
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("&nbsp;");
0475                 }
0476 
0477                 if (startOfLine) {
0478                     startOfLine = false;
0479                 }
0480                 continue;
0481             } else if (ch == QLatin1Char('\t')) {
0482                 do {
0483                     result += QLatin1String("&nbsp;");
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("&amp;");
0501         } else if (ch == QLatin1Char('"')) {
0502             result += QLatin1String("&quot;");
0503         } else if (ch == QLatin1Char('<')) {
0504             result += QLatin1String("&lt;");
0505         } else if (ch == QLatin1Char('>')) {
0506             result += QLatin1String("&gt;");
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("&amp;");
0517                         } else if (chBadUrl == QLatin1Char('"')) {
0518                             resultBadUrl += QLatin1String("&quot;");
0519                         } else if (chBadUrl == QLatin1Char('<')) {
0520                             resultBadUrl += QLatin1String("&lt;");
0521                         } else if (chBadUrl == QLatin1Char('>')) {
0522                             resultBadUrl += QLatin1String("&gt;");
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                     // &amp; 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("&gt;:-("), QStringLiteral("&gt;:("), 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 }