Skip to content

Commit 36ef8fb

Browse files
authored
fix: support captures in ignores (#5126)
1 parent c32ee8e commit 36ef8fb

File tree

4 files changed

+295
-22
lines changed

4 files changed

+295
-22
lines changed

CHANGELOG.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@
5353
- Bugfix: Fixed thread popup window missing messages for nested threads. (#4923)
5454
- Bugfix: Fixed an occasional crash for channel point redemptions with text input. (#4949)
5555
- Bugfix: Fixed triple click on message also selecting moderation buttons. (#4961)
56-
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965)
56+
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126)
5757
- Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110)
58-
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965)
59-
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965)
58+
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126)
59+
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126)
6060
- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971)
6161
- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971)
6262
- Bugfix: Fixed an issue on macOS where the image uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011)

src/providers/twitch/TwitchMessageBuilder.cpp

+147-17
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,128 @@ namespace {
269269
builder->message().badgeInfos = badgeInfos;
270270
}
271271

272+
/**
273+
* Computes (only) the replacement of @a match in @a source.
274+
* The parts before and after the match in @a source are ignored.
275+
*
276+
* Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced
277+
* with the string captured by the corresponding capturing group.
278+
* This function should only be used if the regex contains capturing groups.
279+
*
280+
* Since Qt doesn't provide a way of replacing a single match with some replacement
281+
* while supporting both capturing groups and lookahead/-behind in the regex,
282+
* this is included here. It's essentially the implementation of
283+
* QString::replace(const QRegularExpression &, const QString &).
284+
* @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703
285+
*/
286+
QString makeRegexReplacement(QStringView source,
287+
const QRegularExpression &regex,
288+
const QRegularExpressionMatch &match,
289+
const QString &replacement)
290+
{
291+
using SizeType = QString::size_type;
292+
struct QStringCapture {
293+
SizeType pos;
294+
SizeType len;
295+
int captureNumber;
296+
};
297+
298+
qsizetype numCaptures = regex.captureCount();
299+
300+
// 1. build the backreferences list, holding where the backreferences
301+
// are in the replacement string
302+
QVarLengthArray<QStringCapture> backReferences;
303+
304+
SizeType replacementLength = replacement.size();
305+
for (SizeType i = 0; i < replacementLength - 1; i++)
306+
{
307+
if (replacement[i] != u'\\')
308+
{
309+
continue;
310+
}
311+
312+
int no = replacement[i + 1].digitValue();
313+
if (no <= 0 || no > numCaptures)
314+
{
315+
continue;
316+
}
317+
318+
QStringCapture backReference{.pos = i, .len = 2};
319+
320+
if (i < replacementLength - 2)
321+
{
322+
int secondDigit = replacement[i + 2].digitValue();
323+
if (secondDigit != -1 &&
324+
((no * 10) + secondDigit) <= numCaptures)
325+
{
326+
no = (no * 10) + secondDigit;
327+
++backReference.len;
328+
}
329+
}
330+
331+
backReference.captureNumber = no;
332+
backReferences.append(backReference);
333+
}
334+
335+
// 2. iterate on the matches.
336+
// For every match, copy the replacement string in chunks
337+
// with the proper replacements for the backreferences
338+
339+
// length of the new string, with all the replacements
340+
SizeType newLength = 0;
341+
QVarLengthArray<QStringView> chunks;
342+
QStringView replacementView{replacement};
343+
344+
// Initially: empty, as we only care about the replacement
345+
SizeType len = 0;
346+
SizeType lastEnd = 0;
347+
for (const QStringCapture &backReference :
348+
std::as_const(backReferences))
349+
{
350+
// part of "replacement" before the backreference
351+
len = backReference.pos - lastEnd;
352+
if (len > 0)
353+
{
354+
chunks << replacementView.mid(lastEnd, len);
355+
newLength += len;
356+
}
357+
358+
// backreference itself
359+
len = match.capturedLength(backReference.captureNumber);
360+
if (len > 0)
361+
{
362+
chunks << source.mid(
363+
match.capturedStart(backReference.captureNumber), len);
364+
newLength += len;
365+
}
366+
367+
lastEnd = backReference.pos + backReference.len;
368+
}
369+
370+
// add the last part of the replacement string
371+
len = replacementView.size() - lastEnd;
372+
if (len > 0)
373+
{
374+
chunks << replacementView.mid(lastEnd, len);
375+
newLength += len;
376+
}
377+
378+
// 3. assemble the chunks together
379+
QString dst;
380+
dst.reserve(newLength);
381+
for (const QStringView &chunk : std::as_const(chunks))
382+
{
383+
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
384+
static_assert(sizeof(QChar) == sizeof(decltype(*chunk.utf16())));
385+
dst.append(reinterpret_cast<const QChar *>(chunk.utf16()),
386+
chunk.length());
387+
#else
388+
dst += chunk;
389+
#endif
390+
}
391+
return dst;
392+
}
393+
272394
} // namespace
273395

274396
TwitchMessageBuilder::TwitchMessageBuilder(
@@ -419,7 +541,9 @@ MessagePtr TwitchMessageBuilder::build()
419541
this->tags, this->originalMessage_, this->messageOffset_);
420542

421543
// This runs through all ignored phrases and runs its replacements on this->originalMessage_
422-
this->runIgnoreReplaces(twitchEmotes);
544+
TwitchMessageBuilder::processIgnorePhrases(
545+
*getSettings()->ignoredMessages.readOnly(), this->originalMessage_,
546+
twitchEmotes);
423547

424548
std::sort(twitchEmotes.begin(), twitchEmotes.end(),
425549
[](const auto &a, const auto &b) {
@@ -960,12 +1084,12 @@ void TwitchMessageBuilder::appendUsername()
9601084
}
9611085
}
9621086

963-
void TwitchMessageBuilder::runIgnoreReplaces(
1087+
void TwitchMessageBuilder::processIgnorePhrases(
1088+
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
9641089
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
9651090
{
9661091
using SizeType = QString::size_type;
9671092

968-
auto phrases = getSettings()->ignoredMessages.readOnly();
9691093
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
9701094
// all emotes outside the range come before `it`
9711095
// all emotes in the range start at `it`
@@ -1034,20 +1158,20 @@ void TwitchMessageBuilder::runIgnoreReplaces(
10341158
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
10351159
SizeType length, const QString &replacement) {
10361160
auto removedEmotes = removeEmotesInRange(from, length);
1037-
this->originalMessage_.replace(from, length, replacement);
1161+
originalMessage.replace(from, length, replacement);
10381162
auto wordStart = from;
10391163
while (wordStart > 0)
10401164
{
1041-
if (this->originalMessage_[wordStart - 1] == ' ')
1165+
if (originalMessage[wordStart - 1] == ' ')
10421166
{
10431167
break;
10441168
}
10451169
--wordStart;
10461170
}
10471171
auto wordEnd = from + replacement.length();
1048-
while (wordEnd < this->originalMessage_.length())
1172+
while (wordEnd < originalMessage.length())
10491173
{
1050-
if (this->originalMessage_[wordEnd] == ' ')
1174+
if (originalMessage[wordEnd] == ' ')
10511175
{
10521176
break;
10531177
}
@@ -1058,11 +1182,11 @@ void TwitchMessageBuilder::runIgnoreReplaces(
10581182
static_cast<int>(replacement.length() - length));
10591183

10601184
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1061-
auto midExtendedRef = QStringView{this->originalMessage_}.mid(
1062-
wordStart, wordEnd - wordStart);
1185+
auto midExtendedRef =
1186+
QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart);
10631187
#else
10641188
auto midExtendedRef =
1065-
this->originalMessage_.midRef(wordStart, wordEnd - wordStart);
1189+
originalMessage.midRef(wordStart, wordEnd - wordStart);
10661190
#endif
10671191

10681192
for (auto &emote : removedEmotes)
@@ -1088,7 +1212,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
10881212
addReplEmotes(phrase, midExtendedRef, wordStart);
10891213
};
10901214

1091-
for (const auto &phrase : *phrases)
1215+
for (const auto &phrase : phrases)
10921216
{
10931217
if (phrase.isBlock())
10941218
{
@@ -1110,16 +1234,22 @@ void TwitchMessageBuilder::runIgnoreReplaces(
11101234
QRegularExpressionMatch match;
11111235
size_t iterations = 0;
11121236
SizeType from = 0;
1113-
while ((from = this->originalMessage_.indexOf(regex, from,
1114-
&match)) != -1)
1237+
while ((from = originalMessage.indexOf(regex, from, &match)) != -1)
11151238
{
1239+
auto replacement = phrase.getReplace();
1240+
if (regex.captureCount() > 0)
1241+
{
1242+
replacement = makeRegexReplacement(originalMessage, regex,
1243+
match, replacement);
1244+
}
1245+
11161246
replaceMessageAt(phrase, from, match.capturedLength(),
1117-
phrase.getReplace());
1247+
replacement);
11181248
from += phrase.getReplace().length();
11191249
iterations++;
11201250
if (iterations >= 128)
11211251
{
1122-
this->originalMessage_ =
1252+
originalMessage =
11231253
u"Too many replacements - check your ignores!"_s;
11241254
return;
11251255
}
@@ -1129,8 +1259,8 @@ void TwitchMessageBuilder::runIgnoreReplaces(
11291259
}
11301260

11311261
SizeType from = 0;
1132-
while ((from = this->originalMessage_.indexOf(
1133-
pattern, from, phrase.caseSensitivity())) != -1)
1262+
while ((from = originalMessage.indexOf(pattern, from,
1263+
phrase.caseSensitivity())) != -1)
11341264
{
11351265
replaceMessageAt(phrase, from, pattern.length(),
11361266
phrase.getReplace());

src/providers/twitch/TwitchMessageBuilder.hpp

+5-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
2020
class Channel;
2121
class TwitchChannel;
2222
class MessageThread;
23+
class IgnorePhrase;
2324
struct HelixVip;
2425
using HelixModerator = HelixVip;
2526
struct ChannelPointReward;
@@ -108,6 +109,10 @@ class TwitchMessageBuilder : public SharedMessageBuilder
108109
const QVariantMap &tags, const QString &originalMessage,
109110
int messageOffset);
110111

112+
static void processIgnorePhrases(
113+
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
114+
std::vector<TwitchEmoteOccurrence> &twitchEmotes);
115+
111116
private:
112117
void parseUsernameColor() override;
113118
void parseUsername() override;
@@ -118,8 +123,6 @@ class TwitchMessageBuilder : public SharedMessageBuilder
118123
void parseThread();
119124
void appendUsername();
120125

121-
void runIgnoreReplaces(std::vector<TwitchEmoteOccurrence> &twitchEmotes);
122-
123126
Outcome tryAppendEmote(const EmoteName &name) override;
124127

125128
void addWords(const QStringList &words,

0 commit comments

Comments
 (0)