2022-11-01 00:49:41 +00:00
|
|
|
/*
|
|
|
|
* Vencord, a modification for Discord's desktop app
|
|
|
|
* Copyright (c) 2022 Vendicated and contributors
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2022-11-28 12:37:55 +00:00
|
|
|
import { debounce } from "@utils/debounce";
|
|
|
|
import { makeCodeblock } from "@utils/misc";
|
2022-12-19 22:59:54 +00:00
|
|
|
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
2022-11-28 12:37:55 +00:00
|
|
|
import { search } from "@webpack";
|
|
|
|
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
|
|
|
|
2022-11-01 00:49:41 +00:00
|
|
|
import { CheckedTextInput } from "./CheckedTextInput";
|
|
|
|
import ErrorBoundary from "./ErrorBoundary";
|
|
|
|
|
|
|
|
// Do not include diff in non dev builds (side effects import)
|
|
|
|
if (IS_DEV) {
|
|
|
|
var differ = require("diff") as typeof import("diff");
|
|
|
|
}
|
|
|
|
|
|
|
|
const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|
|
|
const candidates = search(find);
|
|
|
|
const keys = Object.keys(candidates);
|
|
|
|
const len = keys.length;
|
|
|
|
if (len === 0)
|
|
|
|
setError("No match. Perhaps that module is lazy loaded?");
|
|
|
|
else if (len !== 1)
|
|
|
|
setError("Multiple matches. Please refine your filter");
|
|
|
|
else
|
|
|
|
setModule([keys[0], candidates[keys[0]]]);
|
|
|
|
});
|
|
|
|
|
2022-12-19 22:59:54 +00:00
|
|
|
interface ReplacementComponentProps {
|
|
|
|
module: [id: number, factory: Function];
|
|
|
|
match: string | RegExp;
|
|
|
|
replacement: string | ReplaceFn;
|
|
|
|
setReplacementError(error: any): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
|
2022-11-01 00:49:41 +00:00
|
|
|
const [id, fact] = module;
|
|
|
|
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
|
|
|
|
|
|
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
|
|
|
const src: string = fact.toString().replaceAll("\n", "");
|
2022-12-19 22:59:54 +00:00
|
|
|
const canonicalMatch = canonicalizeMatch(match);
|
2022-11-01 00:49:41 +00:00
|
|
|
try {
|
2022-12-19 22:59:54 +00:00
|
|
|
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
|
|
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
2022-11-01 00:49:41 +00:00
|
|
|
setReplacementError(void 0);
|
|
|
|
} catch (e) {
|
|
|
|
setReplacementError((e as Error).message);
|
|
|
|
return ["", [], []];
|
|
|
|
}
|
2022-12-19 22:59:54 +00:00
|
|
|
const m = src.match(canonicalMatch);
|
2022-11-01 00:49:41 +00:00
|
|
|
return [patched, m, makeDiff(src, patched, m)];
|
|
|
|
}, [id, match, replacement]);
|
|
|
|
|
|
|
|
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
|
|
|
|
if (!match || original === patched) return null;
|
|
|
|
|
|
|
|
const changeSize = patched.length - original.length;
|
|
|
|
|
|
|
|
// Use 200 surrounding characters of context
|
|
|
|
const start = Math.max(0, match.index! - 200);
|
|
|
|
const end = Math.min(original.length, match.index! + match[0].length + 200);
|
|
|
|
// (changeSize may be negative)
|
|
|
|
const endPatched = end + changeSize;
|
|
|
|
|
|
|
|
const context = original.slice(start, end);
|
|
|
|
const patchedContext = patched.slice(start, endPatched);
|
|
|
|
|
|
|
|
return differ.diffWordsWithSpace(context, patchedContext);
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderMatch() {
|
|
|
|
if (!matchResult)
|
|
|
|
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
|
|
|
|
|
|
|
|
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
|
|
|
|
const groups = matchResult.length > 1
|
2022-11-03 18:12:50 +00:00
|
|
|
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
|
2022-11-01 00:49:41 +00:00
|
|
|
: "";
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
2022-11-05 09:02:29 +00:00
|
|
|
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
|
|
|
|
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
|
2022-11-01 00:49:41 +00:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderDiff() {
|
|
|
|
return diff?.map(p => {
|
|
|
|
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
2022-11-05 09:02:29 +00:00
|
|
|
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
|
2022-11-01 00:49:41 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Forms.FormTitle>Module {id}</Forms.FormTitle>
|
|
|
|
|
|
|
|
{!!matchResult?.[0]?.length && (
|
|
|
|
<>
|
|
|
|
<Forms.FormTitle>Match</Forms.FormTitle>
|
|
|
|
{renderMatch()}
|
|
|
|
</>)
|
|
|
|
}
|
|
|
|
|
|
|
|
{!!diff?.length && (
|
|
|
|
<>
|
|
|
|
<Forms.FormTitle>Diff</Forms.FormTitle>
|
|
|
|
{renderDiff()}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!!diff?.length && (
|
|
|
|
<Button className={Margins.marginTop20} onClick={() => {
|
|
|
|
try {
|
2022-11-01 13:27:13 +00:00
|
|
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
2022-11-01 00:49:41 +00:00
|
|
|
setCompileResult([true, "Compiled successfully"]);
|
|
|
|
} catch (err) {
|
|
|
|
setCompileResult([false, (err as Error).message]);
|
|
|
|
}
|
|
|
|
}}>Compile</Button>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{compileResult &&
|
|
|
|
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
|
|
|
|
{compileResult[1]}
|
|
|
|
</Forms.FormText>
|
|
|
|
}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|
|
|
const [isFunc, setIsFunc] = React.useState(false);
|
|
|
|
const [error, setError] = React.useState<string>();
|
|
|
|
|
|
|
|
function onChange(v: string) {
|
|
|
|
setError(void 0);
|
|
|
|
|
|
|
|
if (isFunc) {
|
|
|
|
try {
|
|
|
|
const func = (0, eval)(v);
|
|
|
|
if (typeof func === "function")
|
|
|
|
setReplacement(() => func);
|
|
|
|
else
|
|
|
|
setError("Replacement must be a function");
|
|
|
|
} catch (e) {
|
|
|
|
setReplacement(v);
|
|
|
|
setError((e as Error).message);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
setReplacement(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
React.useEffect(
|
|
|
|
() => void (isFunc ? onChange(replacement) : setError(void 0)),
|
|
|
|
[isFunc]
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Forms.FormTitle>replacement</Forms.FormTitle>
|
|
|
|
<TextInput
|
|
|
|
value={replacement?.toString()}
|
|
|
|
onChange={onChange}
|
|
|
|
error={error ?? replacementError}
|
|
|
|
/>
|
2022-11-03 18:12:50 +00:00
|
|
|
{!isFunc && (
|
|
|
|
<>
|
|
|
|
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
|
|
|
{Object.entries({
|
|
|
|
"$$": "Insert a $",
|
|
|
|
"$&": "Insert the entire match",
|
2022-12-19 22:59:54 +00:00
|
|
|
"$`\u200b": "Insert the substring before the match",
|
2022-11-03 18:12:50 +00:00
|
|
|
"$'": "Insert the substring after the match",
|
2022-12-19 22:59:54 +00:00
|
|
|
"$n": "Insert the nth capturing group ($1, $2...)",
|
|
|
|
"$self": "Insert the plugin instance",
|
2022-11-03 18:12:50 +00:00
|
|
|
}).map(([placeholder, desc]) => (
|
|
|
|
<Forms.FormText key={placeholder}>
|
|
|
|
{Parser.parse("`" + placeholder + "`")}: {desc}
|
|
|
|
</Forms.FormText>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
)}
|
2022-11-01 00:49:41 +00:00
|
|
|
|
|
|
|
<Switch
|
|
|
|
className={Margins.marginTop8}
|
|
|
|
value={isFunc}
|
|
|
|
onChange={setIsFunc}
|
|
|
|
note="'replacement' will be evaled if this is toggled"
|
|
|
|
hideBorder={true}
|
|
|
|
>
|
|
|
|
Treat as Function
|
|
|
|
</Switch>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function PatchHelper() {
|
|
|
|
const [find, setFind] = React.useState<string>("");
|
|
|
|
const [match, setMatch] = React.useState<string>("");
|
2022-12-19 22:59:54 +00:00
|
|
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
2022-11-01 00:49:41 +00:00
|
|
|
|
|
|
|
const [replacementError, setReplacementError] = React.useState<string>();
|
|
|
|
|
|
|
|
const [module, setModule] = React.useState<[number, Function]>();
|
|
|
|
const [findError, setFindError] = React.useState<string>();
|
|
|
|
|
|
|
|
const code = React.useMemo(() => {
|
|
|
|
return `
|
|
|
|
{
|
|
|
|
find: ${JSON.stringify(find)},
|
|
|
|
replacement: {
|
|
|
|
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
2022-11-03 18:12:50 +00:00
|
|
|
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
2022-11-01 00:49:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
`.trim();
|
|
|
|
}, [find, match, replacement]);
|
|
|
|
|
|
|
|
function onFindChange(v: string) {
|
|
|
|
setFindError(void 0);
|
|
|
|
setFind(v);
|
|
|
|
if (v.length) {
|
|
|
|
findCandidates({ find: v, setModule, setError: setFindError });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onMatchChange(v: string) {
|
|
|
|
try {
|
|
|
|
new RegExp(v);
|
|
|
|
setFindError(void 0);
|
|
|
|
setMatch(v);
|
|
|
|
} catch (e: any) {
|
|
|
|
setFindError((e as Error).message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2022-11-25 22:38:55 +00:00
|
|
|
<Forms.FormSection>
|
|
|
|
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
2022-11-01 00:49:41 +00:00
|
|
|
<Forms.FormTitle>find</Forms.FormTitle>
|
|
|
|
<TextInput
|
|
|
|
type="text"
|
|
|
|
value={find}
|
|
|
|
onChange={onFindChange}
|
|
|
|
error={findError}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Forms.FormTitle>match</Forms.FormTitle>
|
|
|
|
<CheckedTextInput
|
|
|
|
value={match}
|
|
|
|
onChange={onMatchChange}
|
|
|
|
validate={v => {
|
|
|
|
try {
|
|
|
|
return (new RegExp(v), true);
|
|
|
|
} catch (e) {
|
|
|
|
return (e as Error).message;
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<ReplacementInput
|
|
|
|
replacement={replacement}
|
|
|
|
setReplacement={setReplacement}
|
|
|
|
replacementError={replacementError}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Forms.FormDivider />
|
|
|
|
{module && (
|
|
|
|
<ReplacementComponent
|
|
|
|
module={module}
|
|
|
|
match={new RegExp(match)}
|
|
|
|
replacement={replacement}
|
|
|
|
setReplacementError={setReplacementError}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!!(find && match && replacement) && (
|
|
|
|
<>
|
|
|
|
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
2022-11-05 09:02:29 +00:00
|
|
|
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
2022-11-01 00:49:41 +00:00
|
|
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
|
|
|
</>
|
|
|
|
)}
|
2022-11-25 22:38:55 +00:00
|
|
|
</Forms.FormSection>
|
2022-11-01 00:49:41 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;
|