From 3e2bfdd5482daaa21dfa54b0f78125359ca16bad Mon Sep 17 00:00:00 2001 From: noavarice Date: Wed, 1 Mar 2017 21:47:08 +0300 Subject: [PATCH] refactor: further improvement of message formatting Brief list of changes: - names of some variables and constants were replaced for reading convenience; - URL-highlighting method moved to TextFormatter so URL's don't conflict with italic text formatting; - as I understand, in previous version 'file://' URL does not work because of bad regex. I fix this with help of wiki page that referenced in comment for old code. Important note: there are two equal 'file://' URL syntax: 'file:///...' and 'file://localhost/...' and the second one does NOT work in KDE but works in Gnome so that's not a bug Fix #4233 --- src/chatlog/chatmessage.cpp | 41 +------------- src/chatlog/chatmessage.h | 1 - src/chatlog/textformatter.cpp | 104 +++++++++++++++++++++++++--------- src/chatlog/textformatter.h | 6 +- 4 files changed, 82 insertions(+), 70 deletions(-) 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..5374761f7 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 applying 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);