mirror of https://github.com/qTox/qTox.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
10 KiB
255 lines
10 KiB
/* |
|
Copyright © 2017-2018 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 <QRegularExpression> |
|
#include <QVector> |
|
|
|
// clang-format off |
|
|
|
// Note: escaping of '\' is only needed because QStringLiteral is broken by linebreak |
|
static const QString SINGLE_SIGN_PATTERN = QStringLiteral("(?<=^|\\s)" |
|
"[%1]" |
|
"(?!\\s)" |
|
"([^%1\\n]+?)" |
|
"(?<!\\s)" |
|
"[%1]" |
|
"(?=$|\\s)"); |
|
|
|
static const QString SINGLE_SLASH_PATTERN = QStringLiteral("(?<=^|\\s)" |
|
"/" |
|
"(?!\\s)" |
|
"([^/\\n]+?)" |
|
"(?<!\\s)" |
|
"/" |
|
"(?=$|\\s)"); |
|
|
|
static const QString DOUBLE_SIGN_PATTERN = QStringLiteral("(?<=^|\\s)" |
|
"[%1]{2}" |
|
"(?!\\s)" |
|
"([^\\n]+?)" |
|
"(?<!\\s)" |
|
"[%1]{2}" |
|
"(?=$|\\s)"); |
|
|
|
static const QString MULTILINE_CODE = QStringLiteral("(?<=^|\\s)" |
|
"```" |
|
"(?!`)" |
|
"((.|\\n)+?)" |
|
"(?<!`)" |
|
"```" |
|
"(?=$|\\s)"); |
|
|
|
#define REGEXP_WRAPPER_PAIR(pattern, wrapper)\ |
|
{QRegularExpression(pattern,QRegularExpression::UseUnicodePropertiesOption),QStringLiteral(wrapper)} |
|
|
|
static const QPair<QRegularExpression, QString> REGEX_TO_WRAPPER[] { |
|
REGEXP_WRAPPER_PAIR(SINGLE_SLASH_PATTERN, "<i>%1</i>"), |
|
REGEXP_WRAPPER_PAIR(SINGLE_SIGN_PATTERN.arg('*'), "<b>%1</b>"), |
|
REGEXP_WRAPPER_PAIR(SINGLE_SIGN_PATTERN.arg('_'), "<u>%1</u>"), |
|
REGEXP_WRAPPER_PAIR(SINGLE_SIGN_PATTERN.arg('~'), "<s>%1</s>"), |
|
REGEXP_WRAPPER_PAIR(SINGLE_SIGN_PATTERN.arg('`'), "<font color=#595959><code>%1</code></font>"), |
|
REGEXP_WRAPPER_PAIR(DOUBLE_SIGN_PATTERN.arg('*'), "<b>%1</b>"), |
|
REGEXP_WRAPPER_PAIR(DOUBLE_SIGN_PATTERN.arg('/'), "<i>%1</i>"), |
|
REGEXP_WRAPPER_PAIR(DOUBLE_SIGN_PATTERN.arg('_'), "<u>%1</u>"), |
|
REGEXP_WRAPPER_PAIR(DOUBLE_SIGN_PATTERN.arg('~'), "<s>%1</s>"), |
|
REGEXP_WRAPPER_PAIR(MULTILINE_CODE, "<font color=#595959><code>%1</code></font>"), |
|
}; |
|
|
|
#undef REGEXP_WRAPPER_PAIR |
|
|
|
static const QString HREF_WRAPPER = QStringLiteral(R"(<a href="%1">%1</a>)"); |
|
static const QString WWW_WRAPPER = QStringLiteral(R"(<a href="http://%1">%1</a>)"); |
|
|
|
static const QVector<QRegularExpression> WWW_WORD_PATTERN = { |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*((www\.)\S+))")) |
|
}; |
|
|
|
static const QVector<QRegularExpression> URI_WORD_PATTERNS = { |
|
// Note: This does not match only strictly valid URLs, but we broaden search to any string following scheme to |
|
// allow UTF-8 "IRI"s instead of ASCII-only URLs |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*((((http[s]?)|ftp)://)\S+))")), |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*((file|smb)://([\S| ]*)))")), |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(tox:[a-zA-Z\d]{76}))")), |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(mailto:\S+@\S+\.\S+))")), |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(tox:\S+@\S+))")), |
|
QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(magnet:[?]((xt(.\d)?=urn:)|(mt=)|(kt=)|(tr=)|(dn=)|(xl=)|(xs=)|(as=)|(x.))[\S| ]+))")), |
|
}; |
|
|
|
|
|
// clang-format on |
|
|
|
struct MatchingUri { |
|
bool valid{false}; |
|
int length{0}; |
|
}; |
|
|
|
// pairs of characters that are ignored when surrounding a URI |
|
static const QPair<QString, QString> URI_WRAPPING_CHARS[] = { |
|
{QString("("), QString(")")}, |
|
{QString("["), QString("]")}, |
|
{QString("""), QString(""")}, |
|
{QString("'"), QString("'")} |
|
}; |
|
|
|
// characters which are ignored from the end of URI |
|
static const QChar URI_ENDING_CHARS[] = { |
|
QChar::fromLatin1('?'), |
|
QChar::fromLatin1('.'), |
|
QChar::fromLatin1('!'), |
|
QChar::fromLatin1(':'), |
|
QChar::fromLatin1(',') |
|
}; |
|
|
|
/** |
|
* @brief Strips wrapping characters and ending punctuation from URI |
|
* @param QRegularExpressionMatch of a word containing a URI |
|
* @return MatchingUri containing info on the stripped URI |
|
*/ |
|
MatchingUri stripSurroundingChars(const QStringRef wrappedUri, const int startOfBareUri) |
|
{ |
|
bool matchFound; |
|
int curValidationStartPos = 0; |
|
int curValidationEndPos = wrappedUri.length(); |
|
do { |
|
matchFound = false; |
|
for (auto const& surroundChars : URI_WRAPPING_CHARS) |
|
{ |
|
const int openingCharLength = surroundChars.first.length(); |
|
const int closingCharLength = surroundChars.second.length(); |
|
if (surroundChars.first == wrappedUri.mid(curValidationStartPos, openingCharLength) && |
|
surroundChars.second == wrappedUri.mid(curValidationEndPos - closingCharLength, closingCharLength)) { |
|
curValidationStartPos += openingCharLength; |
|
curValidationEndPos -= closingCharLength; |
|
matchFound = true; |
|
break; |
|
} |
|
} |
|
for (QChar const endChar : URI_ENDING_CHARS) { |
|
const int charLength = 1; |
|
if (endChar == wrappedUri.at(curValidationEndPos - charLength)) { |
|
curValidationEndPos -= charLength; |
|
matchFound = true; |
|
break; |
|
} |
|
} |
|
} while (matchFound); |
|
MatchingUri strippedMatch; |
|
if (startOfBareUri != curValidationStartPos) { |
|
strippedMatch.valid = false; |
|
} else { |
|
strippedMatch.valid = true; |
|
strippedMatch.length = curValidationEndPos - startOfBareUri; |
|
} |
|
return strippedMatch; |
|
} |
|
|
|
/** |
|
* @brief Wrap substrings matching "patterns" with "wrapper" in "message" |
|
* @param message Where search for patterns |
|
* @param patterns Array of regex patterns to find strings to wrap |
|
* @param wrapper Surrounds the matched strings |
|
* @note done separately from URI since the link must have a scheme added to be valid |
|
* @return Copy of message with highlighted URLs |
|
*/ |
|
QString highlight(const QString& message, const QVector<QRegularExpression>& patterns, const QString& wrapper) |
|
{ |
|
QString result = message; |
|
for (const QRegularExpression& exp : patterns) { |
|
const int startLength = result.length(); |
|
int offset = 0; |
|
QRegularExpressionMatchIterator iter = exp.globalMatch(result); |
|
while (iter.hasNext()) { |
|
const QRegularExpressionMatch match = iter.next(); |
|
const int uriWithWrapMatch{0}; |
|
const int uriWithoutWrapMatch{1}; |
|
MatchingUri matchUri = stripSurroundingChars(match.capturedRef(uriWithWrapMatch), |
|
match.capturedStart(uriWithoutWrapMatch) - match.capturedStart(uriWithWrapMatch)); |
|
if (!matchUri.valid) { |
|
continue; |
|
} |
|
const QString wrappedURL = wrapper.arg(match.captured(uriWithoutWrapMatch).left(matchUri.length)); |
|
result.replace(match.capturedStart(uriWithoutWrapMatch) + offset, matchUri.length, wrappedURL); |
|
offset = result.length() - startLength; |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* @brief Highlights URLs within passed message string |
|
* @param message Where search for URLs |
|
* @return Copy of message with highlighted URLs |
|
*/ |
|
QString highlightURI(const QString& message) |
|
{ |
|
QString result = highlight(message, URI_WORD_PATTERNS, HREF_WRAPPER); |
|
result = highlight(result, WWW_WORD_PATTERN, WWW_WRAPPER); |
|
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 markdown to passed message string |
|
* @param message Formatting string |
|
* @param showFormattingSymbols True, if it is supposed to include formatting symbols into resulting |
|
* string |
|
* @return Copy of message with markdown applied |
|
*/ |
|
QString applyMarkdown(const QString& message, bool showFormattingSymbols) |
|
{ |
|
QString result = message; |
|
for (const QPair<QRegularExpression, QString>& pair : REGEX_TO_WRAPPER) { |
|
QRegularExpressionMatchIterator iter = pair.first.globalMatch(result); |
|
int offset = 0; |
|
while (iter.hasNext()) { |
|
const QRegularExpressionMatch match = iter.next(); |
|
QString captured = match.captured(!showFormattingSymbols); |
|
if (isTagIntersection(captured)) { |
|
continue; |
|
} |
|
|
|
const int length = match.capturedLength(); |
|
const QString wrappedText = pair.second.arg(captured); |
|
const int startPos = match.capturedStart() + offset; |
|
result.replace(startPos, length, wrappedText); |
|
offset += wrappedText.length() - length; |
|
} |
|
} |
|
return result; |
|
}
|
|
|