/* * 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 { CheckedTextInput } from "@components/CheckedTextInput"; import { CodeBlock } from "@components/CodeBlock"; import { debounce } from "@shared/debounce"; import { Margins } from "@utils/margins"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { makeCodeblock } from "@utils/text"; import { Patch, ReplaceFn } from "@utils/types"; import { search } from "@webpack"; import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common"; import { SettingsTab, wrapTab } from "./shared"; // 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 ({ finds, setModule, setError }) { const candidates = search(...finds); 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]]]); }); interface ReplacementComponentProps { module: [id: number, factory: Function]; match: string | RegExp; replacement: string | ReplaceFn; setReplacementError(error: any): void; } function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) { const [id, fact] = module; const [compileResult, setCompileResult] = React.useState<[boolean, string]>(); const [patchedCode, matchResult, diff] = React.useMemo(() => { const src: string = fact.toString().replaceAll("\n", ""); const canonicalMatch = canonicalizeMatch(match); try { const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin"); var patched = src.replace(canonicalMatch, canonicalReplace as string); setReplacementError(void 0); } catch (e) { setReplacementError((e as Error).message); return ["", [], []]; } const m = src.match(canonicalMatch); 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 ( <> {/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */} replacement {!isFunc && (
Cheat Sheet {Object.entries({ "\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)", "$$": "Insert a $", "$&": "Insert the entire match", "$`\u200b": "Insert the substring before the match", "$'": "Insert the substring after the match", "$n": "Insert the nth capturing group ($1, $2...)", "$self": "Insert the plugin instance", }).map(([placeholder, desc]) => ( {Parser.parse("`" + placeholder + "`")}: {desc} ))}
)} Treat as Function ); } interface FullPatchInputProps { setFind(v: string): void; setFinds(v: string[]): void; setMatch(v: string): void; setReplacement(v: string | ReplaceFn): void; } function FullPatchInput({ setFind, setFinds, setMatch, setReplacement }: FullPatchInputProps) { const [fullPatch, setFullPatch] = React.useState(""); const [fullPatchError, setFullPatchError] = React.useState(""); function update() { if (fullPatch === "") { setFullPatchError(""); setFind(""); setMatch(""); setReplacement(""); return; } try { const parsed = (0, eval)(`(${fullPatch})`) as Patch; if (!parsed.find) throw new Error("No 'find' field"); if (!parsed.replacement) throw new Error("No 'replacement' field"); if (parsed.replacement instanceof Array) { if (parsed.replacement.length === 0) throw new Error("Invalid replacement"); parsed.replacement = { match: parsed.replacement[0].match, replace: parsed.replacement[0].replace }; } if (!parsed.replacement.match) throw new Error("No 'replacement.match' field"); if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field"); setFind(JSON.stringify(parsed.find)); setFinds(parsed.find instanceof Array ? parsed.find : [parsed.find]); setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match); setReplacement(parsed.replacement.replace); setFullPatchError(""); } catch (e) { setFullPatchError((e as Error).message); } } return <> Paste your full JSON patch here to fill out the fields