Plugin

TextReplace

Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server

Chat Customisation Utility
index.tsx
Download

Source

src/plugins/textReplace/index.tsx
1import "./styles.css";
2
3import { definePluginSettings } from "@api/Settings";
4import { Button } from "@components/Button";
5import { ExpandableSection } from "@components/ExpandableCard";
6import { Flex } from "@components/Flex";
7import { HeadingSecondary } from "@components/Heading";
8import { Paragraph } from "@components/Paragraph";
9import { Span } from "@components/Span";
10import { TooltipContainer } from "@components/TooltipContainer";
11import { Devs } from "@utils/constants";
12import { classNameFactory } from "@utils/css";
13import { Logger } from "@utils/Logger";
14import definePlugin, { OptionType } from "@utils/types";
15import { React, TextInput, useState } from "@webpack/common";
16
17const cl = classNameFactory("vc-textReplace-");
18
19type Rule = Record<"find" | "replace" | "onlyIfIncludes" | "id", string>;
20
21interface TextReplaceProps {
22 title: string;
23 description: string;
24 rulesArray: Rule[];
25 isRegex?: boolean;
26}
27
28const makeEmptyRule: () => Rule = () => ({
29 find: "",
30 replace: "",
31 onlyIfIncludes: "",
32 id: crypto.randomUUID()
33});
34const makeEmptyRuleArray = () => [makeEmptyRule()];
35
36const 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 <TextReplace
46 title="Simple Replacements"
47 description="Simple find and replace rules. For example, find &#039;brb&#039; and replace it with &#039;be right back&#039;"
48 rulesArray={stringRules}
49 />
50 <TextReplace
51 title="Regex Replacements"
52 description="More powerful replacements using Regular Expressions. This section is for advanced users. If you don&#039;t understand it, just ignore it"
53 rulesArray={regexRules}
54 isRegex
55 />
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
70function stringToRegex(str: string) {
71 const match = str.match(/^(\/)?(.+?)(?:\/([gimsuyv]*))?$/); class="ts-cmt">// Regex to match regex
72 return match
73 ? new RegExp(
74 match[2], class="ts-cmt">// Pattern
75 match[3]
76 ?.split("") class="ts-cmt">// Remove duplicate flags
77 .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos)
78 .join("")
79 ?? "g"
80 )
81 : new RegExp(str); class="ts-cmt">// Not a regex, return string
82}
83
84function 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
97function 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 <TextInput
105 placeholder={placeholder}
106 value={value}
107 onChange={setValue}
108 spellCheck={false}
109 onBlur={() => value !== initialValue && setTimeout(() => onChange(value), 0)}
110 />
111 );
112}
113
114function 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 <Input
121 placeholder={description}
122 initialValue={value}
123 onChange={onChange}
124 />
125 </>
126 );
127}
128
129const isEmptyRule = (rule: Rule) => !rule.find;
130
131function 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 it
140 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 <ExpandableSection
154 key={rule.id}
155 renderContent={() => (
156 <>
157 <div className={cl("input-grid")}>
158 <TextRow
159 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 <TextRow
165 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 <TextRow
171 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 <Button
179 className={cl("delete-button")}
180 variant="dangerPrimary"
181 onClick={() => onClickRemove(index)}
182 >
183 Delete Rule
184 </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 <Button
197 onClick={() => rulesArray.push(makeEmptyRule())}
198 disabled={rulesArray.length > 0 && isEmptyRule(rulesArray[rulesArray.length - 1])}
199 >
200 Add Rule
201 </Button>
202 </Flex>
203 </>
204 );
205}
206
207function 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
221function 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
249const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479";
250
251export default definePlugin({
252 name: "TextReplace",
253 description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord&#039;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 messy
266 if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;
267 msg.content = applyRules(msg.content);
268 }
269});
270