/*
* 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 .
*/
import { makeCodeblock } from "../utils";
import { debounce } from "../utils/debounce";
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, TextInput } from "../webpack/common";
import { search } from "../webpack/webpack";
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]]]);
});
function ReplacementComponent({ module, match, replacement, setReplacementError }) {
const [id, fact] = module;
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", "");
try {
var patched = src.replace(match, replacement);
setReplacementError(void 0);
} catch (e) {
setReplacementError((e as Error).message);
return ["", [], []];
}
const m = src.match(match);
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 Regex doesn't match!;
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
const groups = matchResult.length > 1
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
: "";
return (
<>
{Parser.parse(fullMatch)}
{Parser.parse(groups)}
>
);
}
function renderDiff() {
return diff?.map(p => {
const color = p.added ? "lime" : p.removed ? "red" : "grey";
return {p.value};
});
}
return (
<>
Module {id}
{!!matchResult?.[0]?.length && (
<>
Match
{renderMatch()}
>)
}
{!!diff?.length && (
<>
Diff
{renderDiff()}
>
)}
{!!diff?.length && (
)}
{compileResult &&
{compileResult[1]}
}
>
);
}
function ReplacementInput({ replacement, setReplacement, replacementError }) {
const [isFunc, setIsFunc] = React.useState(false);
const [error, setError] = React.useState();
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 (
<>
replacement
{!isFunc && (
<>
Cheat Sheet
{Object.entries({
"$$": "Insert a $",
"$&": "Insert the entire match",
"$`": "Insert the substring before the match",
"$'": "Insert the substring after the match",
"$n": "Insert the nth capturing group ($1, $2...)"
}).map(([placeholder, desc]) => (
{Parser.parse("`" + placeholder + "`")}: {desc}
))}
>
)}
Treat as Function
>
);
}
function PatchHelper() {
const [find, setFind] = React.useState("");
const [match, setMatch] = React.useState("");
const [replacement, setReplacement] = React.useState("");
const [replacementError, setReplacementError] = React.useState();
const [module, setModule] = React.useState<[number, Function]>();
const [findError, setFindError] = React.useState();
const code = React.useMemo(() => {
return `
{
find: ${JSON.stringify(find)},
replacement: {
match: /${match.replace(/(?
find
match
{
try {
return (new RegExp(v), true);
} catch (e) {
return (e as Error).message;
}
}}
/>
{module && (
)}
{!!(find && match && replacement) && (
<>
Code
{Parser.parse(makeCodeblock(code, "ts"))}
>
)}
>
);
}
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;