22 #include <QRegularExpression>
28 static const QString SINGLE_SIGN_PATTERN = QStringLiteral(
"(?<=^|\\s)"
36 static const QString SINGLE_SLASH_PATTERN = QStringLiteral(
"(?<=^|\\s)"
44 static const QString DOUBLE_SIGN_PATTERN = QStringLiteral(
"(?<=^|\\s)"
52 static const QString MULTILINE_CODE = QStringLiteral(
"(?<=^|\\s)"
60 #define REGEXP_WRAPPER_PAIR(pattern, wrapper)\
61 {QRegularExpression(pattern,QRegularExpression::UseUnicodePropertiesOption),QStringLiteral(wrapper)}
63 static const QPair<QRegularExpression, QString> REGEX_TO_WRAPPER[] {
68 REGEXP_WRAPPER_PAIR(SINGLE_SIGN_PATTERN.arg(
'`'),
"<font color=#595959><code>%1</code></font>"),
76 #undef REGEXP_WRAPPER_PAIR
78 static const QString HREF_WRAPPER = QStringLiteral(R
"(<a href="%1">%1</a>)");
79 static const QString WWW_WRAPPER = QStringLiteral(R
"(<a href="http://%1">%1</a>)");
81 static const QVector<QRegularExpression> WWW_WORD_PATTERN = {
82 QRegularExpression(QStringLiteral(R
"((?<=^|\s)\S*((www\.)\S+))"))
85 static const QVector<QRegularExpression> URI_WORD_PATTERNS = {
88 QRegularExpression(QStringLiteral(R
"((?<=^|\s)\S*((((http[s]?)|ftp)://)\S+))")),
89 QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*((file|smb)://([\S| ]*)))")),
90 QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(tox:[a-zA-Z\d]{76}))")),
91 QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(mailto:\S+@\S+\.\S+))")),
92 QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(magnet:[?]((xt(.\d)?=urn:)|(mt=)|(kt=)|(tr=)|(dn=)|(xl=)|(xs=)|(as=)|(x.))[\S| ]+))")),
93 QRegularExpression(QStringLiteral(R"((?<=^|\s)\S*(gemini://\S+))")),
105 static const QPair<QString, QString> URI_WRAPPING_CHARS[] = {
106 {QString(
"("), QString(
")")},
107 {QString(
"["), QString(
"]")},
108 {QString(
"""), QString(
""")},
109 {QString(
"'"), QString(
"'")}
113 static const QChar URI_ENDING_CHARS[] = {
114 QChar::fromLatin1(
'?'),
115 QChar::fromLatin1(
'.'),
116 QChar::fromLatin1(
'!'),
117 QChar::fromLatin1(
':'),
118 QChar::fromLatin1(
',')
126 MatchingUri stripSurroundingChars(
const QStringRef wrappedUri,
const int startOfBareUri)
129 int curValidationStartPos = 0;
130 int curValidationEndPos = wrappedUri.length();
133 for (
auto const& surroundChars : URI_WRAPPING_CHARS)
135 const int openingCharLength = surroundChars.first.length();
136 const int closingCharLength = surroundChars.second.length();
137 if (surroundChars.first == wrappedUri.mid(curValidationStartPos, openingCharLength) &&
138 surroundChars.second == wrappedUri.mid(curValidationEndPos - closingCharLength, closingCharLength)) {
139 curValidationStartPos += openingCharLength;
140 curValidationEndPos -= closingCharLength;
145 for (QChar
const endChar : URI_ENDING_CHARS) {
146 const int charLength = 1;
147 if (endChar == wrappedUri.at(curValidationEndPos - charLength)) {
148 curValidationEndPos -= charLength;
153 }
while (matchFound);
154 MatchingUri strippedMatch;
155 if (startOfBareUri != curValidationStartPos) {
156 strippedMatch.valid =
false;
158 strippedMatch.valid =
true;
159 strippedMatch.length = curValidationEndPos - startOfBareUri;
161 return strippedMatch;
172 QString highlight(
const QString& message,
const QVector<QRegularExpression>& patterns,
const QString& wrapper)
175 for (
const QRegularExpression& exp : patterns) {
176 const int startLength = result.length();
178 QRegularExpressionMatchIterator iter = exp.globalMatch(result);
179 while (iter.hasNext()) {
180 const QRegularExpressionMatch match = iter.next();
181 const int uriWithWrapMatch{0};
182 const int uriWithoutWrapMatch{1};
183 MatchingUri matchUri = stripSurroundingChars(match.capturedRef(uriWithWrapMatch),
184 match.capturedStart(uriWithoutWrapMatch) - match.capturedStart(uriWithWrapMatch));
185 if (!matchUri.valid) {
188 const QString wrappedURL = wrapper.arg(match.captured(uriWithoutWrapMatch).left(matchUri.length));
189 result.replace(match.capturedStart(uriWithoutWrapMatch) + offset, matchUri.length, wrappedURL);
190 offset = result.length() - startLength;
203 QString result = highlight(message, URI_WORD_PATTERNS, HREF_WRAPPER);
204 result = highlight(result, WWW_WORD_PATTERN, WWW_WRAPPER);
213 static bool isTagIntersection(
const QString& str)
215 const QRegularExpression TAG_PATTERN(
"(?<=<)/?[a-zA-Z0-9]+(?=>)");
217 int openingTagCount = 0;
218 int closingTagCount = 0;
220 QRegularExpressionMatchIterator iter = TAG_PATTERN.globalMatch(str);
221 while (iter.hasNext()) {
222 iter.next().captured()[0] ==
'/' ? ++closingTagCount : ++openingTagCount;
224 return openingTagCount != closingTagCount;
234 QString
applyMarkdown(
const QString& message,
bool showFormattingSymbols)
237 for (
const QPair<QRegularExpression, QString>& pair : REGEX_TO_WRAPPER) {
238 QRegularExpressionMatchIterator iter = pair.first.globalMatch(result);
240 while (iter.hasNext()) {
241 const QRegularExpressionMatch match = iter.next();
242 QString captured = match.captured(!showFormattingSymbols);
243 if (isTagIntersection(captured)) {
247 const int length = match.capturedLength();
248 const QString wrappedText = pair.second.arg(captured);
249 const int startPos = match.capturedStart() + offset;
250 result.replace(startPos, length, wrappedText);
251 offset += wrappedText.length() - length;