1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

Merge pull request #4236

noavarice (3):
      refactor: further improvement of message formatting
      test: added test on URL highlighting in text messages
      fix: fixed documentation mistake
This commit is contained in:
sudden6 2017-03-07 15:03:52 +01:00
commit 9c482455a8
No known key found for this signature in database
GPG Key ID: 279509B499E032B9
5 changed files with 101 additions and 70 deletions

View File

@ -55,7 +55,7 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
text = SmileyPack::getInstance().smileyfied(text); text = SmileyPack::getInstance().smileyfied(text);
// quotes (green text) // quotes (green text)
text = detectQuotes(detectAnchors(text), type); text = detectQuotes(text, type);
// text styling // text styling
Settings::StyleType styleType = Settings::getInstance().getStylePreference(); Settings::StyleType styleType = Settings::getInstance().getStylePreference();
@ -227,45 +227,6 @@ void ChatMessage::hideDate()
c->hide(); 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("<a href=\"http://%1\">%1</a>").arg(url);
else
htmledUrl = QString("<a href=\"%1\">%1</a>").arg(url);
out.replace(offset, exp.cap().length(), htmledUrl);
offset += htmledUrl.length();
}
return out;
}
QString ChatMessage::detectQuotes(const QString& str, MessageType type) QString ChatMessage::detectQuotes(const QString& str, MessageType type)
{ {
// detect text quotes // detect text quotes

View File

@ -65,7 +65,6 @@ public:
void hideDate(); void hideDate();
protected: protected:
static QString detectAnchors(const QString& str);
static QString detectQuotes(const QString& str, MessageType type); static QString detectQuotes(const QString& str, MessageType type);
static QString wrapDiv(const QString& str, const QString& div); static QString wrapDiv(const QString& str, const QString& div);

View File

@ -24,21 +24,24 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QVector> #include <QVector>
#include <functional>
enum TextStyle enum TextStyle
{ {
BOLD = 0, BOLD = 0,
ITALIC, ITALIC,
UNDERLINE, UNDERLINE,
STRIKE, STRIKE,
CODE CODE,
HREF
}; };
static const QString COMMON_PATTERN = QStringLiteral("(?<=^|[^%1<])" static const QString COMMON_PATTERN = QStringLiteral("(?<=^|[^%1<])"
"[%1]{%3}" "[%1]{%2}"
"(?![%1 \\n])" "(?![%1 \\n])"
".+?" ".+?"
"(?<![%1< \\n])" "(?<![%1< \\n])"
"[%1]{%3}" "[%1]{%2}"
"(?=$|[^%1])"); "(?=$|[^%1])");
static const QString MULTILINE_CODE = QStringLiteral("(?<=^|[^`])" static const QString MULTILINE_CODE = QStringLiteral("(?<=^|[^`])"
@ -50,28 +53,43 @@ static const QString MULTILINE_CODE = QStringLiteral("(?<=^|[^`])"
"(?=$|[^`])"); "(?=$|[^`])");
// Items in vector associated with TextStyle values respectively. Do NOT change this order // Items in vector associated with TextStyle values respectively. Do NOT change this order
static const QVector<QString> fontStylePatterns{QStringLiteral("<b>%1</b>"), static const QVector<QString> htmlPatterns{QStringLiteral("<b>%1</b>"),
QStringLiteral("<i>%1</i>"), QStringLiteral("<i>%1</i>"),
QStringLiteral("<u>%1</u>"), QStringLiteral("<u>%1</u>"),
QStringLiteral("<s>%1</s>"), QStringLiteral("<s>%1</s>"),
QStringLiteral( QStringLiteral(
"<font color=#595959><code>%1</code></font>")}; "<font color=#595959><code>%1</code></font>"),
QStringLiteral("<a href=\"%1%2\">%2</a>")};
// Unfortunately, can't use simple QMap because ordered applying of styles is required
static const QVector<QPair<QRegularExpression, QString>> textPatternStyle{ static const QVector<QPair<QRegularExpression, QString>> textPatternStyle{
{QRegularExpression(COMMON_PATTERN.arg("*", "1")), fontStylePatterns[BOLD]}, {QRegularExpression(COMMON_PATTERN.arg("*", "1")), htmlPatterns[BOLD]},
{QRegularExpression(COMMON_PATTERN.arg("/", "1")), fontStylePatterns[ITALIC]}, {QRegularExpression(COMMON_PATTERN.arg("/", "1")), htmlPatterns[ITALIC]},
{QRegularExpression(COMMON_PATTERN.arg("_", "1")), fontStylePatterns[UNDERLINE]}, {QRegularExpression(COMMON_PATTERN.arg("_", "1")), htmlPatterns[UNDERLINE]},
{QRegularExpression(COMMON_PATTERN.arg("~", "1")), fontStylePatterns[STRIKE]}, {QRegularExpression(COMMON_PATTERN.arg("~", "1")), htmlPatterns[STRIKE]},
{QRegularExpression(COMMON_PATTERN.arg("`", "1")), fontStylePatterns[CODE]}, {QRegularExpression(COMMON_PATTERN.arg("`", "1")), htmlPatterns[CODE]},
{QRegularExpression(COMMON_PATTERN.arg("*", "2")), fontStylePatterns[BOLD]}, {QRegularExpression(COMMON_PATTERN.arg("*", "2")), htmlPatterns[BOLD]},
{QRegularExpression(COMMON_PATTERN.arg("/", "2")), fontStylePatterns[ITALIC]}, {QRegularExpression(COMMON_PATTERN.arg("/", "2")), htmlPatterns[ITALIC]},
{QRegularExpression(COMMON_PATTERN.arg("_", "2")), fontStylePatterns[UNDERLINE]}, {QRegularExpression(COMMON_PATTERN.arg("_", "2")), htmlPatterns[UNDERLINE]},
{QRegularExpression(COMMON_PATTERN.arg("~", "2")), fontStylePatterns[STRIKE]}, {QRegularExpression(COMMON_PATTERN.arg("~", "2")), htmlPatterns[STRIKE]},
{QRegularExpression(MULTILINE_CODE), fontStylePatterns[CODE]}}; {QRegularExpression(MULTILINE_CODE), htmlPatterns[CODE]}};
static const QVector<QRegularExpression> 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) TextFormatter::TextFormatter(const QString& str)
: sourceString(str) : message(str)
{ {
} }
@ -110,18 +128,37 @@ static bool isTagIntersection(const QString& str)
return openingTagCount != closingTagCount; 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<QString(QString&)> 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 * @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 * @param showFormattingSymbols True, if it is supposed to include formatting symbols into resulting
* string * 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("/", "&#47");
});
for (QPair<QRegularExpression, QString> pair : textPatternStyle) { for (QPair<QRegularExpression, QString> pair : textPatternStyle) {
QRegularExpressionMatchIterator matchesIterator = pair.first.globalMatch(out); QRegularExpressionMatchIterator matchesIterator = pair.first.globalMatch(message);
int insertedTagSymbolsCount = 0; int insertedTagSymbolsCount = 0;
while (matchesIterator.hasNext()) { while (matchesIterator.hasNext()) {
@ -133,19 +170,29 @@ QString TextFormatter::applyHtmlFontStyling(bool showFormattingSymbols)
int capturedStart = match.capturedStart() + insertedTagSymbolsCount; int capturedStart = match.capturedStart() + insertedTagSymbolsCount;
int capturedLength = match.capturedLength(); int capturedLength = match.capturedLength();
QString stylingText = out.mid(capturedStart, capturedLength); QString stylingText = message.mid(capturedStart, capturedLength);
int choppingSignsCount = showFormattingSymbols ? 0 : patternSignsCount(stylingText); int choppingSignsCount = showFormattingSymbols ? 0 : patternSignsCount(stylingText);
int textStart = capturedStart + choppingSignsCount; int textStart = capturedStart + choppingSignsCount;
int textLength = capturedLength - 2 * 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" // Subtracting length of "%1"
insertedTagSymbolsCount += pair.second.length() - 2 - 2 * choppingSignsCount; insertedTagSymbolsCount += pair.second.length() - 2 - 2 * choppingSignsCount;
} }
} }
return out; message.replace("&#47", "/");
}
/**
* @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) QString TextFormatter::applyStyling(bool showFormattingSymbols)
{ {
return applyHtmlFontStyling(showFormattingSymbols); message.toHtmlEscaped();
applyHtmlFontStyling(showFormattingSymbols);
wrapUrl();
return message;
} }

View File

@ -25,9 +25,11 @@
class TextFormatter class TextFormatter
{ {
private: private:
QString sourceString; QString message;
QString applyHtmlFontStyling(bool showFormattingSymbols); void wrapUrl();
void applyHtmlFontStyling(bool showFormattingSymbols);
public: public:
explicit TextFormatter(const QString& str); explicit TextFormatter(const QString& str);

View File

@ -74,6 +74,18 @@ static const StringToString multilineCode{
{QStringLiteral("```int main()\n{\n return 0;\n}```"), {QStringLiteral("```int main()\n{\n return 0;\n}```"),
QStringLiteral("<font color=#595959><code>int main()\n{\n return 0;\n}</code></font>")}}; QStringLiteral("<font color=#595959><code>int main()\n{\n return 0;\n}</code></font>")}};
static const StringToString urlCases {
{QStringLiteral("https://github.com/qTox/qTox/issues/4233"),
QStringLiteral("<a href=\"https://github.com/qTox/qTox/issues/4233\">"
"https://github.com/qTox/qTox/issues/4233</a>")},
{QStringLiteral("No conflicts with /italic https://github.com/qTox/qTox/issues/4233 font/"),
QStringLiteral("No conflicts with <i>italic "
"<a href=\"https://github.com/qTox/qTox/issues/4233\">"
"https://github.com/qTox/qTox/issues/4233</a> font</i>")},
{QStringLiteral("www.youtube.com"), QStringLiteral("<a href=\"http://www.youtube.com\">"
"www.youtube.com</a>")}
};
/** /**
* @brief Testing cases which are common for all types of formatting except multiline code * @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 * @param noSymbols True if it's not allowed to show formatting symbols
@ -192,6 +204,12 @@ START_TEST(multilineCodeTest)
} }
END_TEST END_TEST
START_TEST(urlTest)
{
specialTest(urlCases);
}
END_TEST
static Suite* textFormatterSuite(void) static Suite* textFormatterSuite(void)
{ {
Suite* s = suite_create("TextFormatter"); Suite* s = suite_create("TextFormatter");
@ -209,6 +227,7 @@ static Suite* textFormatterSuite(void)
DEFTESTCASE(doubleSignSpecial); DEFTESTCASE(doubleSignSpecial);
DEFTESTCASE(mixedFormatting); DEFTESTCASE(mixedFormatting);
DEFTESTCASE(multilineCode); DEFTESTCASE(multilineCode);
DEFTESTCASE(url);
return s; return s;
} }