mirror of https://github.com/qTox/qTox.git
Browse Source
noavarice (3): refactor: improved HTML-formatting mechanism for text messages test: Added test for TextFormatter class refactor: message text formatting works better nowpull/4170/merge
10 changed files with 514 additions and 71 deletions
@ -0,0 +1,168 @@
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Copyright © 2017 by The qTox Project Contributors |
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox. |
||||
|
||||
qTox is libre software: you can redistribute it and/or modify |
||||
it under the terms of the GNU General Public License as published by |
||||
the Free Software Foundation, either version 3 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
qTox is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License |
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
|
||||
#include "textformatter.h" |
||||
|
||||
#include <QMap> |
||||
#include <QPair> |
||||
#include <QRegularExpression> |
||||
#include <QVector> |
||||
|
||||
enum TextStyle { |
||||
BOLD = 0, |
||||
ITALIC, |
||||
UNDERLINE, |
||||
STRIKE, |
||||
CODE |
||||
}; |
||||
|
||||
static const QString COMMON_PATTERN = QStringLiteral("(?<=^|[^%1<])" |
||||
"[%1]{%3}" |
||||
"(?![%1 \\n])" |
||||
".+?" |
||||
"(?<![%1< \\n])" |
||||
"[%1]{%3}" |
||||
"(?=$|[^%1])"); |
||||
|
||||
static const QString MULTILINE_CODE = QStringLiteral("(?<=^|[^`])" |
||||
"```" |
||||
"(?!`)" |
||||
"(.|\\n)+" |
||||
"(?<!`)" |
||||
"```" |
||||
"(?=$|[^`])"); |
||||
|
||||
// Items in vector associated with TextStyle values respectively. Do NOT change this order
|
||||
static const QVector<QString> fontStylePatterns |
||||
{ |
||||
QStringLiteral("<b>%1</b>"), |
||||
QStringLiteral("<i>%1</i>"), |
||||
QStringLiteral("<u>%1</u>"), |
||||
QStringLiteral("<s>%1</s>"), |
||||
QStringLiteral("<font color=#595959><code>%1</code></font>") |
||||
}; |
||||
|
||||
// Unfortunately, can't use simple QMap because ordered applying of styles is required
|
||||
static const QVector<QPair<QRegularExpression, QString>> 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] } |
||||
}; |
||||
|
||||
TextFormatter::TextFormatter(const QString &str) |
||||
: sourceString(str) |
||||
{ |
||||
} |
||||
|
||||
/**
|
||||
* @brief Counts equal symbols at the beginning of the string |
||||
* @param str Source string |
||||
* @return Amount of equal symbols at the beginning of the string |
||||
*/ |
||||
static int patternSignsCount(const QString& str) |
||||
{ |
||||
QChar escapeSign = str.at(0); |
||||
int result = 0; |
||||
int length = str.length(); |
||||
while (result < length && str[result] == escapeSign) |
||||
{ |
||||
++result; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/**
|
||||
* @brief Checks HTML tags intersection while applying styles to the message text |
||||
* @param str Checking string |
||||
* @return True, if tag intersection detected |
||||
*/ |
||||
static bool isTagIntersection(const QString& str) |
||||
{ |
||||
const QRegularExpression TAG_PATTERN("(?<=<)/?[a-zA-Z0-9]+(?=>)"); |
||||
|
||||
int openingTagCount = 0; |
||||
int closingTagCount = 0; |
||||
|
||||
QRegularExpressionMatchIterator iter = TAG_PATTERN.globalMatch(str); |
||||
while (iter.hasNext()) |
||||
{ |
||||
iter.next().captured()[0] == '/' |
||||
? ++closingTagCount |
||||
: ++openingTagCount; |
||||
} |
||||
return openingTagCount != closingTagCount; |
||||
} |
||||
|
||||
/**
|
||||
* @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) |
||||
{ |
||||
QString out = sourceString; |
||||
|
||||
for (QPair<QRegularExpression, QString> pair : textPatternStyle) |
||||
{ |
||||
QRegularExpressionMatchIterator matchesIterator = pair.first.globalMatch(out); |
||||
int insertedTagSymbolsCount = 0; |
||||
|
||||
while (matchesIterator.hasNext()) |
||||
{ |
||||
QRegularExpressionMatch match = matchesIterator.next(); |
||||
if (isTagIntersection(match.captured())) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
int capturedStart = match.capturedStart() + insertedTagSymbolsCount; |
||||
int capturedLength = match.capturedLength(); |
||||
|
||||
QString stylingText = out.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)); |
||||
|
||||
out.replace(capturedStart, capturedLength, styledText); |
||||
// Subtracting length of "%1"
|
||||
insertedTagSymbolsCount += pair.second.length() - 2 - 2 * choppingSignsCount; |
||||
} |
||||
} |
||||
return out; |
||||
} |
||||
|
||||
/**
|
||||
* @brief Applies all styling for the text |
||||
* @param showFormattingSymbols True, if it is supposed to include formatting symbols into resulting string |
||||
* @return Styled string |
||||
*/ |
||||
QString TextFormatter::applyStyling(bool showFormattingSymbols) |
||||
{ |
||||
return applyHtmlFontStyling(showFormattingSymbols); |
||||
} |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright © 2017 by The qTox Project Contributors |
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox. |
||||
|
||||
qTox is libre software: you can redistribute it and/or modify |
||||
it under the terms of the GNU General Public License as published by |
||||
the Free Software Foundation, either version 3 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
qTox is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License |
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
|
||||
#ifndef TEXTFORMATTER_H |
||||
#define TEXTFORMATTER_H |
||||
|
||||
#include <QString> |
||||
|
||||
class TextFormatter |
||||
{ |
||||
private: |
||||
|
||||
QString sourceString; |
||||
|
||||
QString applyHtmlFontStyling(bool showFormattingSymbols); |
||||
|
||||
public: |
||||
explicit TextFormatter(const QString& str); |
||||
|
||||
QString applyStyling(bool showFormattingSymbols); |
||||
}; |
||||
|
||||
#endif // TEXTFORMATTER_H
|
@ -0,0 +1,248 @@
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
Copyright © 2017 by The qTox Project Contributors |
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox. |
||||
|
||||
qTox is libre software: you can redistribute it and/or modify |
||||
it under the terms of the GNU General Public License as published by |
||||
the Free Software Foundation, either version 3 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
qTox is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License |
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
|
||||
#include "src/chatlog/textformatter.h" |
||||
#include "test/common.h" |
||||
|
||||
#include <QString> |
||||
#include <QVector> |
||||
#include <QVector> |
||||
#include <QMap> |
||||
#include <QList> |
||||
|
||||
#include <check.h> |
||||
|
||||
using StringToString = QMap<QString, QString>; |
||||
|
||||
static const StringToString signsToTags |
||||
{ |
||||
{ "*", "b" }, |
||||
{ "**", "b" }, |
||||
{ "/", "i" } |
||||
}; |
||||
|
||||
static const StringToString commonWorkCases |
||||
{ |
||||
// Basic
|
||||
{ QStringLiteral("%1a%1"), QStringLiteral("<%2>%1a%1</%2>") }, |
||||
{ QStringLiteral("%1aa%1"), QStringLiteral("<%2>%1aa%1</%2>") }, |
||||
{ QStringLiteral("%1aaa%1"), QStringLiteral("<%2>%1aaa%1</%2>") }, |
||||
|
||||
// Additional text from both sides
|
||||
{ QStringLiteral("aaa%1a%1"), QStringLiteral("aaa<%2>%1a%1</%2>") }, |
||||
{ QStringLiteral("%1a%1aaa"), QStringLiteral("<%2>%1a%1</%2>aaa") }, |
||||
|
||||
// Must allow same formatting more than one time, divided by two and more symbols due to QRegularExpressionIterator
|
||||
{ QStringLiteral("%1aaa%1 aaa %1aaa%1"), QStringLiteral("<%2>%1aaa%1</%2> aaa <%2>%1aaa%1</%2>") } |
||||
}; |
||||
|
||||
static const QVector<QString> commonExceptions |
||||
{ |
||||
// No whitespaces near to formatting symbols from both sides
|
||||
QStringLiteral("%1 a%1"), |
||||
QStringLiteral("%1a %1"), |
||||
|
||||
// No newlines
|
||||
QStringLiteral("%1aa\n%1"), |
||||
|
||||
// Only exact combinations of symbols must encapsulate formatting string
|
||||
QStringLiteral("%1%1aaa%1"), |
||||
QStringLiteral("%1aaa%1%1") |
||||
}; |
||||
|
||||
static const StringToString singleSlash |
||||
{ |
||||
// Must work with inserted tags
|
||||
{ QStringLiteral("/aaa<b>aaa aaa</b>/"), QStringLiteral("<i>aaa<b>aaa aaa</b></i>") } |
||||
}; |
||||
|
||||
static const StringToString doubleSign |
||||
{ |
||||
{ QStringLiteral("**aaa * aaa**"), QStringLiteral("<b>aaa * aaa</b>") } |
||||
}; |
||||
|
||||
static const StringToString mixedFormatting |
||||
{ |
||||
// Must allow mixed formatting if there is no tag overlap in result
|
||||
{ QStringLiteral("aaa *aaa /aaa/ aaa*"), QStringLiteral("aaa <b>aaa <i>aaa</i> aaa</b>") }, |
||||
{ QStringLiteral("aaa *aaa /aaa* aaa/"), QStringLiteral("aaa <b>aaa /aaa</b> aaa/") } |
||||
}; |
||||
|
||||
static const StringToString multilineCode |
||||
{ |
||||
// Must allow newlines
|
||||
{ QStringLiteral("```int main()\n{\n return 0;\n}```"), |
||||
QStringLiteral("<font color=#595959><code>int main()\n{\n return 0;\n}</code></font>") } |
||||
}; |
||||
|
||||
/**
|
||||
* @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 map Grouped cases |
||||
* @param signs Combination of formatting symbols |
||||
*/ |
||||
static void commonTest(bool showSymbols, const StringToString map, const QString signs) |
||||
{ |
||||
for (QString key : map.keys()) |
||||
{ |
||||
QString source = key.arg(signs); |
||||
TextFormatter tf = TextFormatter(source); |
||||
QString result = map[key].arg(showSymbols ? signs : "", signsToTags[signs]); |
||||
ck_assert(tf.applyStyling(showSymbols) == result); |
||||
} |
||||
} |
||||
|
||||
/**
|
||||
* @brief Testing exception cases |
||||
* @param signs Combination of formatting symbols |
||||
*/ |
||||
static void commonExceptionsTest(const QString signs) |
||||
{ |
||||
for (QString source : commonExceptions) |
||||
{ |
||||
TextFormatter tf = TextFormatter(source.arg(signs)); |
||||
ck_assert(tf.applyStyling(false) == source.arg(signs)); |
||||
} |
||||
} |
||||
|
||||
/**
|
||||
* @brief Testing some uncommon, special cases |
||||
* @param map Grouped cases |
||||
*/ |
||||
static void specialTest(const StringToString map) |
||||
{ |
||||
for (QString key : map.keys()) |
||||
{ |
||||
TextFormatter tf = TextFormatter(key); |
||||
ck_assert(tf.applyStyling(false) == map[key]); |
||||
} |
||||
} |
||||
|
||||
START_TEST(singleSignNoSymbolsTest) |
||||
{ |
||||
commonTest(false, commonWorkCases, "*"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(slashNoSymbolsTest) |
||||
{ |
||||
commonTest(false, commonWorkCases, "/"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(doubleSignNoSymbolsTest) |
||||
{ |
||||
commonTest(false, commonWorkCases, "**"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(singleSignWithSymbolsTest) |
||||
{ |
||||
commonTest(true, commonWorkCases, "*"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(slashWithSymbolsTest) |
||||
{ |
||||
commonTest(true, commonWorkCases, "/"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(doubleSignWithSymbolsTest) |
||||
{ |
||||
commonTest(true, commonWorkCases, "**"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(singleSignExceptionsTest) |
||||
{ |
||||
commonExceptionsTest("*"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(slashExceptionsTest) |
||||
{ |
||||
commonExceptionsTest("/"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(doubleSignExceptionsTest) |
||||
{ |
||||
commonExceptionsTest("**"); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(slashSpecialTest) |
||||
{ |
||||
specialTest(singleSlash); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(doubleSignSpecialTest) |
||||
{ |
||||
specialTest(doubleSign); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(mixedFormattingTest) |
||||
{ |
||||
specialTest(mixedFormatting); |
||||
} |
||||
END_TEST |
||||
|
||||
START_TEST(multilineCodeTest) |
||||
{ |
||||
specialTest(multilineCode); |
||||
} |
||||
END_TEST |
||||
|
||||
static Suite* textFormatterSuite(void) |
||||
{ |
||||
Suite* s = suite_create("TextFormatter"); |
||||
|
||||
DEFTESTCASE(singleSignNoSymbols); |
||||
DEFTESTCASE(slashNoSymbols); |
||||
DEFTESTCASE(doubleSignNoSymbols); |
||||
DEFTESTCASE(singleSignWithSymbols); |
||||
DEFTESTCASE(slashWithSymbols); |
||||
DEFTESTCASE(doubleSignWithSymbols); |
||||
DEFTESTCASE(singleSignExceptions); |
||||
DEFTESTCASE(slashExceptions); |
||||
DEFTESTCASE(doubleSignExceptions); |
||||
DEFTESTCASE(slashSpecial); |
||||
DEFTESTCASE(doubleSignSpecial); |
||||
DEFTESTCASE(mixedFormatting); |
||||
DEFTESTCASE(multilineCode); |
||||
|
||||
return s; |
||||
} |
||||
|
||||
int main(int argc, char *argv[]) |
||||
{ |
||||
srand((unsigned int) time(NULL)); |
||||
|
||||
Suite* tf = textFormatterSuite(); |
||||
SRunner* runner = srunner_create(tf); |
||||
srunner_run_all(runner, CK_NORMAL); |
||||
|
||||
int res = srunner_ntests_failed(runner); |
||||
srunner_free(runner); |
||||
|
||||
return res; |
||||
} |
Loading…
Reference in new issue