Plugin
TextReplace
Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server
1
import "./styles.css";2
3
import { definePluginSettings } from "@api/Settings";4
import { Button } from "@components/Button";5
import { ExpandableSection } from "@components/ExpandableCard";6
import { Flex } from "@components/Flex";7
import { HeadingSecondary } from "@components/Heading";8
import { Paragraph } from "@components/Paragraph";9
import { Span } from "@components/Span";10
import { TooltipContainer } from "@components/TooltipContainer";11
import { Devs } from "@utils/constants";12
import { classNameFactory } from "@utils/css";13
import { Logger } from "@utils/Logger";14
import definePlugin, { OptionType } from "@utils/types";15
import { React, TextInput, useState } from "@webpack/common";16
17
const cl = classNameFactory("vc-textReplace-");18
19
type Rule = Record<"find" | "replace" | "onlyIfIncludes" | "id", string>;20
21
interface TextReplaceProps {22
title: string;23
description: string;24
rulesArray: Rule[];25
isRegex?: boolean;26
}27
28
const makeEmptyRule: () => Rule = () => ({29
find: "",30
replace: "",31
onlyIfIncludes: "",32
id: crypto.randomUUID()33
});34
const makeEmptyRuleArray = () => [makeEmptyRule()];35
36
const settings = definePluginSettings({37
replace: {38
type: OptionType.COMPONENT,39
component: () => {40
const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]);41
42
return (43
<>44
<TextReplaceTesting />45
<TextReplace46
title="Simple Replacements"47
description="Simple find and replace rules. For example, find 039;brb039; and replace it with 039;be right back039;"48
rulesArray={stringRules}49
/>50
<TextReplace51
title="Regex Replacements"52
description="More powerful replacements using Regular Expressions. This section is for advanced users. If you don039;t understand it, just ignore it"53
rulesArray={regexRules}54
isRegex55
/>56
</>57
);58
}59
},60
stringRules: {61
type: OptionType.CUSTOM,62
default: makeEmptyRuleArray(),63
},64
regexRules: {65
type: OptionType.CUSTOM,66
default: makeEmptyRuleArray(),67
}68
});69
70
function stringToRegex(str: string) {71
const match = str.match(/^(\/)?(.+?)(?:\/([gimsuyv]*))?$/); class="ts-cmt">// Regex to match regex72
return match73
? new RegExp(74
match[2], class="ts-cmt">// Pattern75
match[3]76
?.split("") class="ts-cmt">// Remove duplicate flags77
.filter((char, pos, flagArr) => flagArr.indexOf(char) === pos)78
.join("")79
?? "g"80
)81
: new RegExp(str); class="ts-cmt">// Not a regex, return string82
}83
84
function renderFindError(find: string) {85
try {86
stringToRegex(find);87
return null;88
} catch (e) {89
return (90
<span style={{ color: "var(--text-feedback-critical)" }}>91
{String(e)}92
</span>93
);94
}95
}96
97
function Input({ initialValue, onChange, placeholder }: {98
placeholder: string;99
initialValue: string;100
onChange(value: string): void;101
}) {102
const [value, setValue] = useState(initialValue);103
return (104
<TextInput105
placeholder={placeholder}106
value={value}107
onChange={setValue}108
spellCheck={false}109
onBlur={() => value !== initialValue && setTimeout(() => onChange(value), 0)}110
/>111
);112
}113
114
function TextRow({ label, description, value, onChange }: { label: string; description: string; value: string; onChange(value: string): void; }) {115
return (116
<>117
<TooltipContainer text={description}>118
<Span weight="medium" size="md">{label}</Span>119
</TooltipContainer>120
<Input121
placeholder={description}122
initialValue={value}123
onChange={onChange}124
/>125
</>126
);127
}128
129
const isEmptyRule = (rule: Rule) => !rule.find;130
131
function TextReplace({ title, description, rulesArray, isRegex = false }: TextReplaceProps) {132
function onClickRemove(index: number) {133
rulesArray.splice(index, 1);134
}135
136
function onChange(e: string, index: number, key: string) {137
rulesArray[index][key] = e;138
139
// If a rule is empty after editing and is not the last rule, remove it140
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) {141
rulesArray.splice(index, 1);142
}143
}144
145
return (146
<>147
<div>148
<HeadingSecondary>{title}</HeadingSecondary>149
<Paragraph>{description}</Paragraph>150
</div>151
<Flex flexDirection="column" style={{ gap: "0.5em" }}>152
{rulesArray.map((rule, index) =>153
<ExpandableSection154
key={rule.id}155
renderContent={() => (156
<>157
<div className={cl("input-grid")}>158
<TextRow159
label="Find"160
description={isRegex ? "The regex pattern" : "The text to replace"}161
value={rule.find}162
onChange={e => onChange(e, index, "find")}163
/>164
<TextRow165
label="Replace"166
description="The text to replace the found text with"167
value={rule.replace}168
onChange={e => onChange(e, index, "replace")}169
/>170
<TextRow171
label="Only if includes"172
description="This rule will only be applied if the message includes this text. This is optional"173
value={rule.onlyIfIncludes}174
onChange={e => onChange(e, index, "onlyIfIncludes")}175
/>176
</div>177
{isRegex && renderFindError(rule.find)}178
<Button179
className={cl("delete-button")}180
variant="dangerPrimary"181
onClick={() => onClickRemove(index)}182
>183
Delete Rule184
</Button>185
</>186
)}187
>188
<Paragraph weight="medium" size="md">189
{isEmptyRule(rule)190
? `Empty Rule ${index + 1}`191
: `Rule ${index + 1} - ${rule.find}`192
}193
</Paragraph>194
</ExpandableSection>195
)}196
<Button197
onClick={() => rulesArray.push(makeEmptyRule())}198
disabled={rulesArray.length > 0 && isEmptyRule(rulesArray[rulesArray.length - 1])}199
>200
Add Rule201
</Button>202
</Flex>203
</>204
);205
}206
207
function TextReplaceTesting() {208
const [value, setValue] = useState("");209
210
return (211
<div>212
<HeadingSecondary>Rule Tester</HeadingSecondary>213
<Flex flexDirection="column" gap={6}>214
<TextInput placeholder="Type a message to test rules on" onChange={setValue} />215
<TextInput placeholder="Message with rules applied" editable={false} value={applyRules(value)} style={{ opacity: 0.7 }} />216
</Flex>217
</div>218
);219
}220
221
function applyRules(content: string): string {222
if (content.length === 0) {223
return content;224
}225
226
for (const rule of settings.store.stringRules) {227
if (!rule.find) continue;228
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;229
230
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");231
}232
233
for (const rule of settings.store.regexRules) {234
if (!rule.find) continue;235
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;236
237
try {238
const regex = stringToRegex(rule.find);239
content = content.replace(regex, rule.replace.replaceAll("\\n", "\n"));240
} catch (e) {241
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);242
}243
}244
245
content = content.trim();246
return content;247
}248
249
const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479";250
251
export default definePlugin({252
name: "TextReplace",253
description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord039;s Server",254
tags: ["Chat", "Customisation", "Utility"],255
authors: [Devs.AutumnVN, Devs.TheKodeToad],256
257
settings,258
259
start() {260
settings.store.regexRules.forEach(rule => rule.id ??= crypto.randomUUID());261
settings.store.stringRules.forEach(rule => rule.id ??= crypto.randomUUID());262
},263
264
onBeforeMessageSend(channelId, msg) {265
// Channel used for sharing rules, applying rules here would be messy266
if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;267
msg.content = applyRules(msg.content);268
}269
});270