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;
}