diff --git a/src/chatlog/chatmessage.cpp b/src/chatlog/chatmessage.cpp index 335bcdc13..08f6a5beb 100644 --- a/src/chatlog/chatmessage.cpp +++ b/src/chatlog/chatmessage.cpp @@ -55,7 +55,7 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt text = SmileyPack::getInstance().smileyfied(text); // quotes (green text) - text = detectQuotes(detectAnchors(text), type); + text = detectQuotes(text, type); // text styling Settings::StyleType styleType = Settings::getInstance().getStylePreference(); @@ -227,45 +227,6 @@ void ChatMessage::hideDate() c->hide(); } -QString ChatMessage::detectAnchors(const QString& str) -{ - QString out = str; - - // detect URIs - QRegExp exp( - "(" - "(?:\\b)((www\\.)|(http[s]?|ftp)://)" // (protocol)://(printable - non-special character) - // http://ONEORMOREALHPA-DIGIT - "\\w+\\S+)" // any other character, lets domains and other - // ↓ link to a file, or samba share - // https://en.wikipedia.org/wiki/File_URI_scheme - "|(?:\\b)((file|smb)://)([\\S| ]*)" - "|(?:\\b)(tox:[a-zA-Z\\d]{76})" // link with full user address - "|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)" //@mail link - "|(?:\\b)(tox:\\S+@\\S+)"); // starts with `tox` then : and only alpha-digits till the end - // also accepts tox:agilob@net as simplified TOX ID - - int offset = 0; - while ((offset = exp.indexIn(out, offset)) != -1) { - QString url = exp.cap(); - // If there's a trailing " it's a HTML attribute, e.g. a smiley img's title=":tox:" - if (url == "tox:\"") { - offset += url.length(); - continue; - } - QString htmledUrl; - // add scheme if not specified - if (exp.cap(2) == "www.") - htmledUrl = QString("%1").arg(url); - else - htmledUrl = QString("%1").arg(url); - out.replace(offset, exp.cap().length(), htmledUrl); - offset += htmledUrl.length(); - } - - return out; -} - QString ChatMessage::detectQuotes(const QString& str, MessageType type) { // detect text quotes diff --git a/src/chatlog/chatmessage.h b/src/chatlog/chatmessage.h index 310d01b81..9de05e224 100644 --- a/src/chatlog/chatmessage.h +++ b/src/chatlog/chatmessage.h @@ -65,7 +65,6 @@ public: void hideDate(); protected: - static QString detectAnchors(const QString& str); static QString detectQuotes(const QString& str, MessageType type); static QString wrapDiv(const QString& str, const QString& div); diff --git a/src/chatlog/textformatter.cpp b/src/chatlog/textformatter.cpp index e12fcd3c9..adb7ee1bc 100644 --- a/src/chatlog/textformatter.cpp +++ b/src/chatlog/textformatter.cpp @@ -24,21 +24,24 @@ #include #include +#include + enum TextStyle { BOLD = 0, ITALIC, UNDERLINE, STRIKE, - CODE + CODE, + HREF }; static const QString COMMON_PATTERN = QStringLiteral("(?<=^|[^%1<])" - "[%1]{%3}" + "[%1]{%2}" "(?![%1 \\n])" ".+?" "(? fontStylePatterns{QStringLiteral("%1"), +static const QVector htmlPatterns{QStringLiteral("%1"), QStringLiteral("%1"), QStringLiteral("%1"), QStringLiteral("%1"), QStringLiteral( - "%1")}; + "%1"), + QStringLiteral("%2")}; -// Unfortunately, can't use simple QMap because ordered applying of styles is required static const QVector> textPatternStyle{ - {QRegularExpression(COMMON_PATTERN.arg("*", "1")), fontStylePatterns[BOLD]}, - {QRegularExpression(COMMON_PATTERN.arg("/", "1")), fontStylePatterns[ITALIC]}, - {QRegularExpression(COMMON_PATTERN.arg("_", "1")), fontStylePatterns[UNDERLINE]}, - {QRegularExpression(COMMON_PATTERN.arg("~", "1")), fontStylePatterns[STRIKE]}, - {QRegularExpression(COMMON_PATTERN.arg("`", "1")), fontStylePatterns[CODE]}, - {QRegularExpression(COMMON_PATTERN.arg("*", "2")), fontStylePatterns[BOLD]}, - {QRegularExpression(COMMON_PATTERN.arg("/", "2")), fontStylePatterns[ITALIC]}, - {QRegularExpression(COMMON_PATTERN.arg("_", "2")), fontStylePatterns[UNDERLINE]}, - {QRegularExpression(COMMON_PATTERN.arg("~", "2")), fontStylePatterns[STRIKE]}, - {QRegularExpression(MULTILINE_CODE), fontStylePatterns[CODE]}}; + {QRegularExpression(COMMON_PATTERN.arg("*", "1")), htmlPatterns[BOLD]}, + {QRegularExpression(COMMON_PATTERN.arg("/", "1")), htmlPatterns[ITALIC]}, + {QRegularExpression(COMMON_PATTERN.arg("_", "1")), htmlPatterns[UNDERLINE]}, + {QRegularExpression(COMMON_PATTERN.arg("~", "1")), htmlPatterns[STRIKE]}, + {QRegularExpression(COMMON_PATTERN.arg("`", "1")), htmlPatterns[CODE]}, + {QRegularExpression(COMMON_PATTERN.arg("*", "2")), htmlPatterns[BOLD]}, + {QRegularExpression(COMMON_PATTERN.arg("/", "2")), htmlPatterns[ITALIC]}, + {QRegularExpression(COMMON_PATTERN.arg("_", "2")), htmlPatterns[UNDERLINE]}, + {QRegularExpression(COMMON_PATTERN.arg("~", "2")), htmlPatterns[STRIKE]}, + {QRegularExpression(MULTILINE_CODE), htmlPatterns[CODE]}}; + +static const QVector urlPatterns { + QRegularExpression("((\\bhttp[s]?://(www\\.)?)|(\\bwww\\.))" + "[^. \\n]+\\.[^ \\n]+"), + QRegularExpression("\\b(ftp|smb)://[^ \\n]+"), + QRegularExpression("\\bfile://(localhost)?/[^ \\n]+"), + QRegularExpression("\\btox:[a-zA-Z\\d]{76}"), + QRegularExpression("\\b(mailto|tox):[^ \\n]+@[^ \\n]+") +}; + +/** + * @class TextFormatter + * + * @brief This class applies formatting to the text messages, e.g. font styling and URL highlighting + */ TextFormatter::TextFormatter(const QString& str) - : sourceString(str) + : message(str) { } @@ -110,18 +128,37 @@ static bool isTagIntersection(const QString& str) return openingTagCount != closingTagCount; } +/** + * @brief Applies a function for URL's which can be extracted from passed string + * @param str String in which we are looking for URL's + * @param func Function which is applied to URL + */ +static void processUrl(QString& str, std::function func) +{ + for (QRegularExpression exp : urlPatterns) { + QRegularExpressionMatchIterator iter = exp.globalMatch(str); + while (iter.hasNext()) { + QRegularExpressionMatch match = iter.next(); + int startPos = match.capturedStart(); + int length = match.capturedLength(); + QString url = str.mid(startPos, length); + str.replace(startPos, length, func(url)); + } + } +} + /** * @brief Applies styles to the font of text that was passed to the constructor * @param showFormattingSymbols True, if it is supposed to include formatting symbols into resulting * string - * @return Source text with styled font */ -QString TextFormatter::applyHtmlFontStyling(bool showFormattingSymbols) +void TextFormatter::applyHtmlFontStyling(bool showFormattingSymbols) { - QString out = sourceString; - + processUrl(message, [] (QString& str) { + return str.replace("/", "/"); + }); for (QPair pair : textPatternStyle) { - QRegularExpressionMatchIterator matchesIterator = pair.first.globalMatch(out); + QRegularExpressionMatchIterator matchesIterator = pair.first.globalMatch(message); int insertedTagSymbolsCount = 0; while (matchesIterator.hasNext()) { @@ -133,19 +170,29 @@ QString TextFormatter::applyHtmlFontStyling(bool showFormattingSymbols) int capturedStart = match.capturedStart() + insertedTagSymbolsCount; int capturedLength = match.capturedLength(); - QString stylingText = out.mid(capturedStart, capturedLength); + QString stylingText = message.mid(capturedStart, capturedLength); int choppingSignsCount = showFormattingSymbols ? 0 : patternSignsCount(stylingText); int textStart = capturedStart + choppingSignsCount; int textLength = capturedLength - 2 * choppingSignsCount; - QString styledText = pair.second.arg(out.mid(textStart, textLength)); + QString styledText = pair.second.arg(message.mid(textStart, textLength)); - out.replace(capturedStart, capturedLength, styledText); + message.replace(capturedStart, capturedLength, styledText); // Subtracting length of "%1" insertedTagSymbolsCount += pair.second.length() - 2 - 2 * choppingSignsCount; } } - return out; + message.replace("/", "/"); +} + +/** + * @brief Wraps all found URL's in HTML hyperlink tag + */ +void TextFormatter::wrapUrl() +{ + processUrl(message, [] (QString& str) { + return htmlPatterns[TextStyle::HREF].arg(str.startsWith("www") ? "http://" : "", str); + }); } /** @@ -156,5 +203,8 @@ QString TextFormatter::applyHtmlFontStyling(bool showFormattingSymbols) */ QString TextFormatter::applyStyling(bool showFormattingSymbols) { - return applyHtmlFontStyling(showFormattingSymbols); + message.toHtmlEscaped(); + applyHtmlFontStyling(showFormattingSymbols); + wrapUrl(); + return message; } diff --git a/src/chatlog/textformatter.h b/src/chatlog/textformatter.h index b25ec08de..a10d0ff4d 100644 --- a/src/chatlog/textformatter.h +++ b/src/chatlog/textformatter.h @@ -25,9 +25,11 @@ class TextFormatter { private: - QString sourceString; + QString message; - QString applyHtmlFontStyling(bool showFormattingSymbols); + void wrapUrl(); + + void applyHtmlFontStyling(bool showFormattingSymbols); public: explicit TextFormatter(const QString& str); diff --git a/test/chatlog/textformatter_test.cpp b/test/chatlog/textformatter_test.cpp index e05c355cd..14822bbbb 100644 --- a/test/chatlog/textformatter_test.cpp +++ b/test/chatlog/textformatter_test.cpp @@ -74,6 +74,18 @@ static const StringToString multilineCode{ {QStringLiteral("```int main()\n{\n return 0;\n}```"), QStringLiteral("int main()\n{\n return 0;\n}")}}; +static const StringToString urlCases { + {QStringLiteral("https://github.com/qTox/qTox/issues/4233"), + QStringLiteral("" + "https://github.com/qTox/qTox/issues/4233")}, + {QStringLiteral("No conflicts with /italic https://github.com/qTox/qTox/issues/4233 font/"), + QStringLiteral("No conflicts with italic " + "" + "https://github.com/qTox/qTox/issues/4233 font")}, + {QStringLiteral("www.youtube.com"), QStringLiteral("" + "www.youtube.com")} +}; + /** * @brief Testing cases which are common for all types of formatting except multiline code * @param noSymbols True if it's not allowed to show formatting symbols @@ -192,6 +204,12 @@ START_TEST(multilineCodeTest) } END_TEST +START_TEST(urlTest) +{ + specialTest(urlCases); +} +END_TEST + static Suite* textFormatterSuite(void) { Suite* s = suite_create("TextFormatter"); @@ -209,6 +227,7 @@ static Suite* textFormatterSuite(void) DEFTESTCASE(doubleSignSpecial); DEFTESTCASE(mixedFormatting); DEFTESTCASE(multilineCode); + DEFTESTCASE(url); return s; }