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

Merge pull request #4186

noavarice (3):
      refactor: improved HTML-formatting mechanism for text messages
      test: Added test for TextFormatter class
      refactor: message text formatting works better now
This commit is contained in:
Diadlo 2017-02-21 09:33:04 +03:00
commit da8c3f1100
No known key found for this signature in database
GPG Key ID: 5AF9F2E29107C727
10 changed files with 514 additions and 71 deletions

View File

@ -188,6 +188,8 @@ set(${PROJECT_NAME}_SOURCES
src/chatlog/documentcache.h src/chatlog/documentcache.h
src/chatlog/pixmapcache.cpp src/chatlog/pixmapcache.cpp
src/chatlog/pixmapcache.h src/chatlog/pixmapcache.h
src/chatlog/textformatter.cpp
src/chatlog/textformatter.h
src/core/coreav.cpp src/core/coreav.cpp
src/core/coreav.h src/core/coreav.h
src/core/core.cpp src/core/core.cpp

View File

@ -25,4 +25,5 @@ search_dependency(CHECK PACKAGE check OPTIONAL)
if (CHECK_FOUND) if (CHECK_FOUND)
auto_test(core toxpk) auto_test(core toxpk)
auto_test(core toxid) auto_test(core toxid)
auto_test(chatlog textformatter)
endif() endif()

View File

@ -436,6 +436,7 @@ HEADERS += \
src/chatlog/customtextdocument.h \ src/chatlog/customtextdocument.h \
src/chatlog/documentcache.h \ src/chatlog/documentcache.h \
src/chatlog/pixmapcache.h \ src/chatlog/pixmapcache.h \
src/chatlog/textformatter.h \
src/core/core.h \ src/core/core.h \
src/core/coreav.h \ src/core/coreav.h \
src/core/coredefines.h \ src/core/coredefines.h \
@ -554,6 +555,7 @@ SOURCES += \
src/chatlog/customtextdocument.cpp\ src/chatlog/customtextdocument.cpp\
src/chatlog/documentcache.cpp \ src/chatlog/documentcache.cpp \
src/chatlog/pixmapcache.cpp \ src/chatlog/pixmapcache.cpp \
src/chatlog/textformatter.cpp \
src/core/core.cpp \ src/core/core.cpp \
src/core/coreav.cpp \ src/core/coreav.cpp \
src/core/corefile.cpp \ src/core/corefile.cpp \

View File

@ -19,6 +19,7 @@
#include "chatmessage.h" #include "chatmessage.h"
#include "chatlinecontentproxy.h" #include "chatlinecontentproxy.h"
#include "textformatter.h"
#include "content/text.h" #include "content/text.h"
#include "content/timestamp.h" #include "content/timestamp.h"
#include "content/spinner.h" #include "content/spinner.h"
@ -56,8 +57,13 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString &sender, const QSt
text = detectQuotes(detectAnchors(text), type); text = detectQuotes(detectAnchors(text), type);
//text styling //text styling
if (Settings::getInstance().getStylePreference() != Settings::StyleType::NONE) Settings::StyleType styleType = Settings::getInstance().getStylePreference();
text = detectStyle(text); if (styleType != Settings::StyleType::NONE)
{
TextFormatter tf = TextFormatter(text);
text = tf.applyStyling(styleType == Settings::StyleType::WITH_CHARS);
}
switch(type) switch(type)
{ {
@ -199,71 +205,6 @@ void ChatMessage::hideDate()
c->hide(); c->hide();
} }
QString ChatMessage::detectStyle(const QString &str)
{
QString out = str;
// Create regex for text styling syntax
QRegExp exp("(\\*)([^\\*]{2,})(\\*)" // Bold *text*
"|(\\*\\*)([^\\*\\*]{2,})(\\*\\*)" // Bold **text**
"|(\\/)([^\\/]{2,})(\\/)" // Italics /text/
"|(\\/\\/)([^\\/\\/]{2,})(\\/\\/)" // Italics //text//
"|(\\_)([^\\_]{2,})(\\_)" // Underline _text_
"|(\\_\\_)([^\\_\\_]{2,})(\\_\\_)" // Underline __text__
"|(\\~)([^\\~]{2,})(\\~)" // Strike ~text~
"|(\\~\\~)([^\\~\\~]{2,})(\\~\\~)" // Strike ~~text~~
"|(\\`)([^\\`]{2,})(\\`)" // Codeblock `text`
"|(\\`\\`\\`)([^\\`\\`\\`]{2,})(\\`\\`\\`)" // Codeblock ```\ntext\n```
);
int offset = 0;
while ((offset = exp.indexIn(out, offset)) != -1)
{
QString snipCheck = out.mid(offset-1,exp.cap(0).length()+2);
QString snippet = exp.cap(0).trimmed();
QString htmledSnippet;
// Only parse if surrounded by spaces, newline(s) and/or beginning/end of line
if ((snipCheck.startsWith(' ') || snipCheck.startsWith('>') || offset == 0)
&& ((snipCheck.endsWith(' ') || snipCheck.endsWith('<')) || offset + snippet.length() == out.length()))
{
int mul = 0; // Determines how many characters to strip from text
// Set mul depending on styleownPreference
if (Settings::getInstance().getStylePreference() == Settings::StyleType::WITHOUT_CHARS)
mul = 2;
// Match captured string to corresponding style format
if (exp.cap(1) == "*" && snippet.length() > 2) // Bold *text*
htmledSnippet = QString("<b>%1</b>").arg(snippet.mid(mul/2,snippet.length()-mul));
else if (exp.cap(4) == "**" && snippet.length() > 4) // Bold **text**
htmledSnippet = QString("<b>%1</b>").arg(snippet.mid(mul,snippet.length()-2*mul));
else if (exp.cap(7) == "/" && snippet.length() > 2) // Italics /text/
htmledSnippet = QString("<i>%1</i>").arg(snippet.mid(mul/2,snippet.length()-mul));
else if (exp.cap(10) == "//" && snippet.length() > 4) // Italics //text//
htmledSnippet = QString("<i>%1</i>").arg(snippet.mid(mul,snippet.length()-2*mul));
else if (exp.cap(13) == "_"&& snippet.length() > 2) // Underline _text_
htmledSnippet = QString("<u>%1</u>").arg(snippet.mid(mul/2,snippet.length()-mul));
else if (exp.cap(16) == "__" && snippet.length() > 4) // Underline __text__
htmledSnippet = QString("<u>%1</u>").arg(snippet.mid(mul,snippet.length()-2*mul));
else if (exp.cap(19) == "~" && snippet.length() > 2) // Strike ~text~
htmledSnippet = QString("<s>%1</s>").arg(snippet.mid(mul/2,snippet.length()-mul));
else if (exp.cap(22) == "~~" && snippet.length() > 4) // Strike ~~text~~
htmledSnippet = QString("<s>%1</s>").arg(snippet.mid(mul,snippet.length()-2*mul));
else if (exp.cap(25) == "`" && snippet.length() > 2) // Codeblock `text`
htmledSnippet = QString("<font color=#595959><code>%1</code></font>").arg(snippet.mid(mul/2,snippet.length()-mul));
else if (exp.cap(28) == "```" && snippet.length() > 6) // Codeblock ```text```
htmledSnippet = QString("<font color=#595959><code>%1</code></font>").arg(snippet.mid(4*mul,snippet.length()-8*mul));
else
htmledSnippet = snippet;
out.replace(offset, exp.cap().length(), htmledSnippet);
offset += htmledSnippet.length();
} else
offset += snippet.length();
}
return out;
}
QString ChatMessage::detectAnchors(const QString &str) QString ChatMessage::detectAnchors(const QString &str)
{ {
QString out = str; QString out = str;
@ -314,17 +255,22 @@ QString ChatMessage::detectQuotes(const QString& str, MessageType type)
// don't quote first line in action message. This makes co-existence of // don't quote first line in action message. This makes co-existence of
// quotes and action messages possible, since only first line can cause // quotes and action messages possible, since only first line can cause
// problems in case where there is quote in it used. // problems in case where there is quote in it used.
if (QRegExp("^(&gt;|).*").exactMatch(messageLines[i])) { if (QRegExp("^(&gt;|).*").exactMatch(messageLines[i]))
{
if (i > 0 || type != ACTION) if (i > 0 || type != ACTION)
quotedText += "<span class=quote>" + messageLines[i] + "</span>"; quotedText += "<span class=quote>" + messageLines[i] + "</span>";
else else
quotedText += messageLines[i]; quotedText += messageLines[i];
} else { }
else
{
quotedText += messageLines[i]; quotedText += messageLines[i];
} }
if (i < messageLines.size() - 1) if (i < messageLines.size() - 1)
quotedText += "<br/>"; {
quotedText += '\n';
}
} }
return quotedText; return quotedText;

View File

@ -61,7 +61,6 @@ public:
void hideDate(); void hideDate();
protected: protected:
static QString detectStyle(const QString& str);
static QString detectAnchors(const QString& str); 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

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

View File

@ -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

View File

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

View File

@ -1,3 +1,22 @@
/*
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/core/toxid.h" #include "src/core/toxid.h"
#include "test/common.h" #include "test/common.h"

View File

@ -1,3 +1,22 @@
/*
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/core/toxid.h" #include "src/core/toxid.h"
#include "test/common.h" #include "test/common.h"