@@ -269,6 +269,128 @@ namespace {
269
269
builder->message ().badgeInfos = badgeInfos;
270
270
}
271
271
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 ®ex,
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
+
272
394
} // namespace
273
395
274
396
TwitchMessageBuilder::TwitchMessageBuilder (
@@ -419,7 +541,9 @@ MessagePtr TwitchMessageBuilder::build()
419
541
this ->tags , this ->originalMessage_ , this ->messageOffset_ );
420
542
421
543
// 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);
423
547
424
548
std::sort (twitchEmotes.begin (), twitchEmotes.end (),
425
549
[](const auto &a, const auto &b) {
@@ -960,12 +1084,12 @@ void TwitchMessageBuilder::appendUsername()
960
1084
}
961
1085
}
962
1086
963
- void TwitchMessageBuilder::runIgnoreReplaces (
1087
+ void TwitchMessageBuilder::processIgnorePhrases (
1088
+ const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
964
1089
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
965
1090
{
966
1091
using SizeType = QString::size_type;
967
1092
968
- auto phrases = getSettings ()->ignoredMessages .readOnly ();
969
1093
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
970
1094
// all emotes outside the range come before `it`
971
1095
// all emotes in the range start at `it`
@@ -1034,20 +1158,20 @@ void TwitchMessageBuilder::runIgnoreReplaces(
1034
1158
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
1035
1159
SizeType length, const QString &replacement) {
1036
1160
auto removedEmotes = removeEmotesInRange (from, length);
1037
- this -> originalMessage_ .replace (from, length, replacement);
1161
+ originalMessage .replace (from, length, replacement);
1038
1162
auto wordStart = from;
1039
1163
while (wordStart > 0 )
1040
1164
{
1041
- if (this -> originalMessage_ [wordStart - 1 ] == ' ' )
1165
+ if (originalMessage [wordStart - 1 ] == ' ' )
1042
1166
{
1043
1167
break ;
1044
1168
}
1045
1169
--wordStart;
1046
1170
}
1047
1171
auto wordEnd = from + replacement.length ();
1048
- while (wordEnd < this -> originalMessage_ .length ())
1172
+ while (wordEnd < originalMessage .length ())
1049
1173
{
1050
- if (this -> originalMessage_ [wordEnd] == ' ' )
1174
+ if (originalMessage [wordEnd] == ' ' )
1051
1175
{
1052
1176
break ;
1053
1177
}
@@ -1058,11 +1182,11 @@ void TwitchMessageBuilder::runIgnoreReplaces(
1058
1182
static_cast <int >(replacement.length () - length));
1059
1183
1060
1184
#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);
1063
1187
#else
1064
1188
auto midExtendedRef =
1065
- this -> originalMessage_ .midRef (wordStart, wordEnd - wordStart);
1189
+ originalMessage .midRef (wordStart, wordEnd - wordStart);
1066
1190
#endif
1067
1191
1068
1192
for (auto &emote : removedEmotes)
@@ -1088,7 +1212,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
1088
1212
addReplEmotes (phrase, midExtendedRef, wordStart);
1089
1213
};
1090
1214
1091
- for (const auto &phrase : * phrases)
1215
+ for (const auto &phrase : phrases)
1092
1216
{
1093
1217
if (phrase.isBlock ())
1094
1218
{
@@ -1110,16 +1234,22 @@ void TwitchMessageBuilder::runIgnoreReplaces(
1110
1234
QRegularExpressionMatch match;
1111
1235
size_t iterations = 0 ;
1112
1236
SizeType from = 0 ;
1113
- while ((from = this ->originalMessage_ .indexOf (regex, from,
1114
- &match)) != -1 )
1237
+ while ((from = originalMessage.indexOf (regex, from, &match)) != -1 )
1115
1238
{
1239
+ auto replacement = phrase.getReplace ();
1240
+ if (regex.captureCount () > 0 )
1241
+ {
1242
+ replacement = makeRegexReplacement (originalMessage, regex,
1243
+ match, replacement);
1244
+ }
1245
+
1116
1246
replaceMessageAt (phrase, from, match.capturedLength (),
1117
- phrase. getReplace () );
1247
+ replacement );
1118
1248
from += phrase.getReplace ().length ();
1119
1249
iterations++;
1120
1250
if (iterations >= 128 )
1121
1251
{
1122
- this -> originalMessage_ =
1252
+ originalMessage =
1123
1253
u" Too many replacements - check your ignores!" _s;
1124
1254
return ;
1125
1255
}
@@ -1129,8 +1259,8 @@ void TwitchMessageBuilder::runIgnoreReplaces(
1129
1259
}
1130
1260
1131
1261
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 )
1134
1264
{
1135
1265
replaceMessageAt (phrase, from, pattern.length (),
1136
1266
phrase.getReplace ());
0 commit comments