aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/settings.ts11
-rw-r--r--src/components/VencordSettings/Updater.tsx2
-rw-r--r--src/components/VencordSettings/VencordTab.tsx4
-rw-r--r--src/patcher.ts30
-rw-r--r--src/plugins/pronoundb/components/PronounsChatComponent.tsx9
-rw-r--r--src/plugins/pronoundb/components/PronounsProfileWrapper.tsx9
-rw-r--r--src/plugins/reviewDB/components/ReviewsView.tsx14
-rw-r--r--src/plugins/shikiCodeblocks/api/languages.ts74
-rw-r--r--src/plugins/shikiCodeblocks/api/shiki.ts119
-rw-r--r--src/plugins/shikiCodeblocks/api/themes.ts67
-rw-r--r--src/plugins/shikiCodeblocks/components/ButtonRow.tsx46
-rw-r--r--src/plugins/shikiCodeblocks/components/Code.tsx92
-rw-r--r--src/plugins/shikiCodeblocks/components/CopyButton.tsx41
-rw-r--r--src/plugins/shikiCodeblocks/components/Header.tsx42
-rw-r--r--src/plugins/shikiCodeblocks/components/Highlighter.tsx123
-rw-r--r--src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts34
-rw-r--r--src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts25
-rw-r--r--src/plugins/shikiCodeblocks/hooks/useTheme.ts49
-rw-r--r--src/plugins/shikiCodeblocks/index.ts154
-rw-r--r--src/plugins/shikiCodeblocks/style.css100
-rw-r--r--src/plugins/shikiCodeblocks/types.ts78
-rw-r--r--src/plugins/shikiCodeblocks/utils/color.ts32
-rw-r--r--src/plugins/shikiCodeblocks/utils/createStyle.ts36
-rw-r--r--src/plugins/shikiCodeblocks/utils/misc.ts50
-rw-r--r--src/utils/constants.ts6
-rw-r--r--src/utils/dependencies.ts4
-rw-r--r--src/utils/misc.tsx49
-rw-r--r--src/utils/react.ts62
-rw-r--r--src/utils/text.ts36
-rw-r--r--src/webpack/common.tsx2
30 files changed, 1366 insertions, 34 deletions
diff --git a/src/api/settings.ts b/src/api/settings.ts
index b7c143a..2617903 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -141,14 +141,19 @@ export const Settings = makeProxy(settings);
* Settings hook for React components. Returns a smart settings
* object that automagically triggers a rerender if any properties
* are altered
+ * @param paths An optional list of paths to whitelist for rerenders
* @returns Settings
*/
-export function useSettings() {
+export function useSettings(paths?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
+ const onUpdate: SubscriptionCallback = paths
+ ? (value, path) => paths.includes(path) && forceUpdate()
+ : forceUpdate;
+
React.useEffect(() => {
- subscriptions.add(forceUpdate);
- return () => void subscriptions.delete(forceUpdate);
+ subscriptions.add(onUpdate);
+ return () => void subscriptions.delete(onUpdate);
}, []);
return Settings;
diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx
index bb344f5..3369069 100644
--- a/src/components/VencordSettings/Updater.tsx
+++ b/src/components/VencordSettings/Updater.tsx
@@ -179,7 +179,7 @@ function Newer(props: CommonProps) {
}
function Updater() {
- const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
+ const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
if (err)
diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx
index 746fcf0..ad8fe14 100644
--- a/src/components/VencordSettings/VencordTab.tsx
+++ b/src/components/VencordSettings/VencordTab.tsx
@@ -27,7 +27,9 @@ import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
const st = (style: string) => `vcSettings${style}`;
function VencordSettings() {
- const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
+ const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
+ fallbackValue: "Loading..."
+ });
const settings = useSettings();
const [donateImage] = React.useState(
diff --git a/src/patcher.ts b/src/patcher.ts
index 0849e5a..0cf7e24 100644
--- a/src/patcher.ts
+++ b/src/patcher.ts
@@ -109,16 +109,36 @@ if (!process.argv.includes("--vanilla")) {
// Remove CSP
+ type PolicyResult = Record<string, string[]>;
+
+ const parsePolicy = (policy: string): PolicyResult => {
+ const result: PolicyResult = {};
+ policy.split(";").forEach(directive => {
+ const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
+ if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
+ result[directiveKey] = directiveValue;
+ }
+ });
+ return result;
+ };
+ const stringifyPolicy = (policy: PolicyResult): string =>
+ Object.entries(policy)
+ .filter(([, values]) => values?.length)
+ .map(directive => directive.flat().join(" "))
+ .join("; ");
+
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
- let patchedHeader = headers[header][0];
- for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src"]) {
- patchedHeader = patchedHeader.replace(new RegExp(`${directive}.+?;`), `${directive} * blob: data: 'unsafe-inline';`);
+ const csp = parsePolicy(headers[header][0]);
+
+ for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
+ csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
- patchedHeader = patchedHeader.replace(/script-src.+?(?=;)/, "$& 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com");
- headers[header] = [patchedHeader];
+ csp["script-src"] ??= [];
+ csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
+ headers[header] = [stringifyPolicy(csp)];
}
}
diff --git a/src/plugins/pronoundb/components/PronounsChatComponent.tsx b/src/plugins/pronoundb/components/PronounsChatComponent.tsx
index 9225fc5..ce67754 100644
--- a/src/plugins/pronoundb/components/PronounsChatComponent.tsx
+++ b/src/plugins/pronoundb/components/PronounsChatComponent.tsx
@@ -39,11 +39,10 @@ export default function PronounsChatComponentWrapper({ message }: { message: Mes
}
function PronounsChatComponent({ message }: { message: Message; }) {
- const [result, , isPending] = useAwaiter(
- () => fetchPronouns(message.author.id),
- null,
- e => console.error("Fetching pronouns failed: ", e)
- );
+ const [result, , isPending] = useAwaiter(() => fetchPronouns(message.author.id), {
+ fallbackValue: null,
+ onError: e => console.error("Fetching pronouns failed: ", e)
+ });
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {
diff --git a/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx b/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx
index 9540bb9..79fce23 100644
--- a/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx
+++ b/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx
@@ -45,11 +45,10 @@ function ProfilePronouns(
leProps: UserProfilePronounsProps;
}
) {
- const [result, , isPending] = useAwaiter(
- () => fetchPronouns(userId),
- null,
- e => console.error("Fetching pronouns failed: ", e)
- );
+ const [result, , isPending] = useAwaiter(() => fetchPronouns(userId), {
+ fallbackValue: null,
+ onError: e => console.error("Fetching pronouns failed: ", e),
+ });
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {
diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx
index 999b970..57f974e 100644
--- a/src/plugins/reviewDB/components/ReviewsView.tsx
+++ b/src/plugins/reviewDB/components/ReviewsView.tsx
@@ -18,7 +18,7 @@
import { classes, useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack";
-import { Forms, Text, UserStore } from "@webpack/common";
+import { Forms, React, Text, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react";
import { addReview, getReviews } from "../Utils/ReviewDBAPI";
@@ -27,7 +27,13 @@ import ReviewComponent from "./ReviewComponent";
const Classes = findLazy(m => typeof m.textarea === "string");
export default function ReviewsView({ userId }: { userId: string; }) {
- const [reviews, _, isLoading, refetch] = useAwaiter(() => getReviews(userId), []);
+ const [refetchCount, setRefetchCount] = React.useState(0);
+ const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
+ fallbackValue: [],
+ deps: [refetchCount],
+ });
+
+ const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
if (isLoading) return null;
@@ -40,7 +46,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
}).then(res => {
if (res === 0 || res === 1) {
(target as HTMLInputElement).value = ""; // clear the input
- refetch();
+ dirtyRefetch();
}
});
}
@@ -64,7 +70,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
<ReviewComponent
key={review.id}
review={review}
- refetch={refetch}
+ refetch={dirtyRefetch}
/>
)}
{reviews?.length === 0 && (
diff --git a/src/plugins/shikiCodeblocks/api/languages.ts b/src/plugins/shikiCodeblocks/api/languages.ts
new file mode 100644
index 0000000..f14a4dc
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/api/languages.ts
@@ -0,0 +1,74 @@
+/*
+ * 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/>.
+*/
+
+import { ILanguageRegistration } from "@vap/shiki";
+
+export const VPC_REPO = "Vap0r1ze/vapcord";
+export const VPC_REPO_COMMIT = "88a7032a59cca40da170926651b08201ea3b965a";
+export const vpcRepoAssets = `https://raw.githubusercontent.com/${VPC_REPO}/${VPC_REPO_COMMIT}/assets/shiki-codeblocks`;
+export const vpcRepoGrammar = (fileName: string) => `${vpcRepoAssets}/${fileName}`;
+export const vpcRepoLanguages = `${vpcRepoAssets}/languages.json`;
+
+export interface Language {
+ name: string;
+ id: string;
+ devicon?: string;
+ grammarUrl: string,
+ grammar?: ILanguageRegistration["grammar"];
+ scopeName: string;
+ aliases?: string[];
+ custom?: boolean;
+}
+export interface LanguageJson {
+ name: string;
+ id: string;
+ fileName: string;
+ devicon?: string;
+ scopeName: string;
+ aliases?: string[];
+}
+
+export const languages: Record<string, Language> = {};
+
+export const loadLanguages = async () => {
+ const langsJson: LanguageJson[] = await fetch(vpcRepoLanguages).then(res => res.json());
+ const loadedLanguages = Object.fromEntries(
+ langsJson.map(lang => [lang.id, {
+ ...lang,
+ grammarUrl: vpcRepoGrammar(lang.fileName),
+ }])
+ );
+ Object.assign(languages, loadedLanguages);
+};
+
+export const getGrammar = (lang: Language): Promise<NonNullable<ILanguageRegistration["grammar"]>> => {
+ if (lang.grammar) return Promise.resolve(lang.grammar);
+ return fetch(lang.grammarUrl).then(res => res.json());
+};
+
+const aliasCache = new Map<string, Language>();
+export function resolveLang(idOrAlias: string) {
+ if (Object.prototype.hasOwnProperty.call(languages, idOrAlias)) return languages[idOrAlias];
+
+ const lang = Object.values(languages).find(lang => lang.aliases?.includes(idOrAlias));
+
+ if (!lang) return null;
+
+ aliasCache.set(idOrAlias, lang);
+ return lang;
+}
diff --git a/src/plugins/shikiCodeblocks/api/shiki.ts b/src/plugins/shikiCodeblocks/api/shiki.ts
new file mode 100644
index 0000000..e7691ce
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/api/shiki.ts
@@ -0,0 +1,119 @@
+/*
+ * 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/>.
+*/
+
+
+import { shikiOnigasmSrc, shikiWorkerSrc } from "@utils/dependencies";
+import { WorkerClient } from "@vap/core/ipc";
+import type { IShikiTheme, IThemedToken } from "@vap/shiki";
+
+import { dispatchTheme } from "../hooks/useTheme";
+import type { ShikiSpec } from "../types";
+import { getGrammar, languages, loadLanguages, resolveLang } from "./languages";
+import { themes } from "./themes";
+
+const themeUrls = Object.values(themes);
+
+let resolveClient: (client: WorkerClient<ShikiSpec>) => void;
+
+export const shiki = {
+ client: null as WorkerClient<ShikiSpec> | null,
+ currentTheme: null as IShikiTheme | null,
+ currentThemeUrl: null as string | null,
+ timeoutMs: 10000,
+ languages,
+ themes,
+ loadedThemes: new Set<string>(),
+ loadedLangs: new Set<string>(),
+ clientPromise: new Promise<WorkerClient<ShikiSpec>>(resolve => resolveClient = resolve),
+
+ init: async (initThemeUrl: string | undefined) => {
+ /** https://stackoverflow.com/q/58098143 */
+ const workerBlob = await fetch(shikiWorkerSrc).then(res => res.blob());
+
+ const client = shiki.client = new WorkerClient<ShikiSpec>(
+ "shiki-client",
+ "shiki-host",
+ workerBlob,
+ { name: "ShikiWorker" },
+ );
+ await client.init();
+
+ const themeUrl = initThemeUrl || themeUrls[0];
+
+ await loadLanguages();
+ await client.run("setOnigasm", { wasm: shikiOnigasmSrc });
+ await client.run("setHighlighter", { theme: themeUrl, langs: [] });
+ shiki.loadedThemes.add(themeUrl);
+ await shiki._setTheme(themeUrl);
+ resolveClient(client);
+ },
+ _setTheme: async (themeUrl: string) => {
+ shiki.currentThemeUrl = themeUrl;
+ const { themeData } = await shiki.client!.run("getTheme", { theme: themeUrl });
+ shiki.currentTheme = JSON.parse(themeData);
+ dispatchTheme({ id: themeUrl, theme: shiki.currentTheme });
+ },
+ loadTheme: async (themeUrl: string) => {
+ const client = await shiki.clientPromise;
+ if (shiki.loadedThemes.has(themeUrl)) return;
+
+ await client.run("loadTheme", { theme: themeUrl });
+
+ shiki.loadedThemes.add(themeUrl);
+ },
+ setTheme: async (themeUrl: string) => {
+ await shiki.clientPromise;
+ themeUrl ||= themeUrls[0];
+ if (!shiki.loadedThemes.has(themeUrl)) await shiki.loadTheme(themeUrl);
+
+ await shiki._setTheme(themeUrl);
+ },
+ loadLang: async (langId: string) => {
+ const client = await shiki.clientPromise;
+ const lang = resolveLang(langId);
+
+ if (!lang || shiki.loadedLangs.has(lang.id)) return;
+
+ await client.run("loadLanguage", {
+ lang: {
+ ...lang,
+ grammar: lang.grammar ?? await getGrammar(lang),
+ }
+ });
+ shiki.loadedLangs.add(lang.id);
+ },
+ tokenizeCode: async (code: string, langId: string): Promise<IThemedToken[][]> => {
+ const client = await shiki.clientPromise;
+ const lang = resolveLang(langId);
+ if (!lang) return [];
+
+ if (!shiki.loadedLangs.has(lang.id)) await shiki.loadLang(lang.id);
+
+ return await client.run("codeToThemedTokens", {
+ code,
+ lang: langId,
+ theme: shiki.currentThemeUrl ?? themeUrls[0],
+ });
+ },
+ destroy() {
+ shiki.currentTheme = null;
+ shiki.currentThemeUrl = null;
+ dispatchTheme({ id: null, theme: null });
+ shiki.client?.destroy();
+ }
+};
diff --git a/src/plugins/shikiCodeblocks/api/themes.ts b/src/plugins/shikiCodeblocks/api/themes.ts
new file mode 100644
index 0000000..f31ce60
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/api/themes.ts
@@ -0,0 +1,67 @@
+/*
+ * 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/>.
+*/
+
+import { IShikiTheme } from "@vap/shiki";
+
+export const SHIKI_REPO = "shikijs/shiki";
+export const SHIKI_REPO_COMMIT = "0b28ad8ccfbf2615f2d9d38ea8255416b8ac3043";
+export const shikiRepoTheme = (name: string) => `https://raw.githubusercontent.com/${SHIKI_REPO}/${SHIKI_REPO_COMMIT}/packages/shiki/themes/${name}.json`;
+
+export const themes = {
+ // Default
+ DarkPlus: shikiRepoTheme("dark-plus"),
+
+ // Dev Choices
+ MaterialCandy: "https://raw.githubusercontent.com/millsp/material-candy/master/material-candy.json",
+
+ // More from Shiki repo
+ DraculaSoft: shikiRepoTheme("dracula-soft"),
+ Dracula: shikiRepoTheme("dracula"),
+ GithubDarkDimmed: shikiRepoTheme("github-dark-dimmed"),
+ GithubDark: shikiRepoTheme("github-dark"),
+ GithubLight: shikiRepoTheme("github-light"),
+ LightPlus: shikiRepoTheme("light-plus"),
+ MaterialDarker: shikiRepoTheme("material-darker"),
+ MaterialDefault: shikiRepoTheme("material-default"),
+ MaterialLighter: shikiRepoTheme("material-lighter"),
+ MaterialOcean: shikiRepoTheme("material-ocean"),
+ MaterialPalenight: shikiRepoTheme("material-palenight"),
+ MinDark: shikiRepoTheme("min-dark"),
+ MinLight: shikiRepoTheme("min-light"),
+ Monokai: shikiRepoTheme("monokai"),
+ Nord: shikiRepoTheme("nord"),
+ OneDarkPro: shikiRepoTheme("one-dark-pro"),
+ Poimandres: shikiRepoTheme("poimandres"),
+ RosePineDawn: shikiRepoTheme("rose-pine-dawn"),
+ RosePineMoon: shikiRepoTheme("rose-pine-moon"),
+ RosePine: shikiRepoTheme("rose-pine"),
+ SlackDark: shikiRepoTheme("slack-dark"),
+ SlackOchin: shikiRepoTheme("slack-ochin"),
+ SolarizedDark: shikiRepoTheme("solarized-dark"),
+ SolarizedLight: shikiRepoTheme("solarized-light"),
+ VitesseDark: shikiRepoTheme("vitesse-dark"),
+ VitesseLight: shikiRepoTheme("vitesse-light"),
+ CssVariables: shikiRepoTheme("css-variables"),
+};
+
+export const themeCache = new Map<string, IShikiTheme>();
+
+export const getTheme = (url: string): Promise<IShikiTheme> => {
+ if (themeCache.has(url)) return Promise.resolve(themeCache.get(url)!);
+ return fetch(url).then(res => res.json());
+};
diff --git a/src/plugins/shikiCodeblocks/components/ButtonRow.tsx b/src/plugins/shikiCodeblocks/components/ButtonRow.tsx
new file mode 100644
index 0000000..e73eb72
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/ButtonRow.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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/>.
+*/
+
+import { Clipboard } from "@webpack/common";
+
+import { cl } from "../utils/misc";
+import { CopyButton } from "./CopyButton";
+
+export interface ButtonRowProps {
+ theme: import("./Highlighter").ThemeBase;
+ content: string;
+}
+
+export function ButtonRow({ content, theme }: ButtonRowProps) {
+ const buttons: JSX.Element[] = [];
+
+ if (Clipboard.SUPPORTS_COPY) {
+ buttons.push(
+ <CopyButton
+ content={content}
+ className={cl("btn")}
+ style={{
+ backgroundColor: theme.accentBgColor,
+ color: theme.accentFgColor,
+ }}
+ />
+ );
+ }
+
+ return <div className={cl("btns")}>{buttons}</div>;
+}
diff --git a/src/plugins/shikiCodeblocks/components/Code.tsx b/src/plugins/shikiCodeblocks/components/Code.tsx
new file mode 100644
index 0000000..ae41113
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/Code.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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/>.
+*/
+
+import type { IThemedToken } from "@vap/shiki";
+
+import { cl } from "../utils/misc";
+import { ThemeBase } from "./Highlighter";
+
+export interface CodeProps {
+ theme: ThemeBase;
+ useHljs: boolean;
+ lang?: string;
+ content: string;
+ tokens: IThemedToken[][] | null;
+}
+
+export const Code = ({
+ theme,
+ useHljs,
+ lang,
+ content,
+ tokens,
+}: CodeProps) => {
+ let lines!: JSX.Element[];
+
+ if (useHljs) {
+ try {
+ const { value: hljsHtml } = hljs.highlight(lang!, content, true);
+ lines = hljsHtml
+ .split("\n")
+ .map((line, i) => <span key={i} dangerouslySetInnerHTML={{ __html: line }} />);
+ } catch {
+ lines = content.split("\n").map(line => <span>{line}</span>);
+ }
+ } else {
+ const renderTokens =
+ tokens ??
+ content
+ .split("\n")
+ .map(line => [{ color: theme.plainColor, content: line } as IThemedToken]);
+
+ lines = renderTokens.map(line => {
+ // [Cynthia] this makes it so when you highlight the codeblock
+ // empty lines are also selected and copied when you Ctrl+C.
+ if (line.length === 0) {
+ return <span>{"\n"}</span>;
+ }
+
+ return (
+ <>
+ {line.map(({ content, color, fontStyle }, i) => (
+ <span
+ key={i}
+ style={{
+ color,
+ fontStyle: (fontStyle ?? 0) & 1 ? "italic" : undefined,
+ fontWeight: (fontStyle ?? 0) & 2 ? "bold" : undefined,
+ textDecoration: (fontStyle ?? 0) & 4 ? "underline" : undefined,
+ }}
+ >
+ {content}
+ </span>
+ ))}
+ </>
+ );
+ });
+ }
+
+ const codeTableRows = lines.map((line, i) => (
+ <tr key={i}>
+ <td style={{ color: theme.plainColor }}>{i + 1}</td>
+ <td>{line}</td>
+ </tr>
+ ));
+
+ return <table className={cl("table")}>{...codeTableRows}</table>;
+};
diff --git a/src/plugins/shikiCodeblocks/components/CopyButton.tsx b/src/plugins/shikiCodeblocks/components/CopyButton.tsx
new file mode 100644
index 0000000..153b3cd
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/CopyButton.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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/>.
+*/
+
+import { useCopyCooldown } from "../hooks/useCopyCooldown";
+
+export interface CopyButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
+ content: string;
+}
+
+export function CopyButton({ content, ...props }: CopyButtonProps) {
+ const [copyCooldown, copy] = useCopyCooldown(1000);
+
+ return (
+ <button
+ {...props}
+ style={{
+ ...props.style,
+ cursor: copyCooldown ? "default" : undefined,
+ }}
+ onClick={() => copy(content)}
+ >
+ {copyCooldown ? "Copied!" : "Copy"}
+ </button>
+
+ );
+}
diff --git a/src/plugins/shikiCodeblocks/components/Header.tsx b/src/plugins/shikiCodeblocks/components/Header.tsx
new file mode 100644
index 0000000..c2db386
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/Header.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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/>.
+*/
+
+import { Language } from "../api/languages";
+import { DeviconSetting } from "../types";
+import { cl } from "../utils/misc";
+
+export interface HeaderProps {
+ langName?: string;
+ useDevIcon: DeviconSetting;
+ shikiLang: Language | null;
+}
+
+export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) {
+ if (!langName) return <></>;
+
+ return (
+ <div className={cl("lang")}>
+ {useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && (
+ <i
+ className={`devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`}
+ />
+ )}
+ {langName}
+ </div>
+ );
+}
diff --git a/src/plugins/shikiCodeblocks/components/Highlighter.tsx b/src/plugins/shikiCodeblocks/components/Highlighter.tsx
new file mode 100644
index 0000000..6067fd8
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/components/Highlighter.tsx
@@ -0,0 +1,123 @@
+/*
+ * 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/>.
+*/
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { useAwaiter } from "@utils/misc";
+import { useIntersection } from "@utils/react";
+import { hljs, React } from "@webpack/common";
+
+import { resolveLang } from "../api/languages";
+import { shiki } from "../api/shiki";
+import { useShikiSettings } from "../hooks/useShikiSettings";
+import { useTheme } from "../hooks/useTheme";
+import { hex2Rgb } from "../utils/color";
+import { cl, shouldUseHljs } from "../utils/misc";
+import { ButtonRow } from "./ButtonRow";
+import { Code } from "./Code";
+import { Header } from "./Header";
+
+export interface ThemeBase {
+ plainColor: string;
+ accentBgColor: string;
+ accentFgColor: string;
+ backgroundColor: string;
+}
+
+export interface HighlighterProps {
+ lang?: string;
+ content: string;
+ isPreview: boolean;
+}
+
+export const createHighlighter = (props: HighlighterProps) => (
+ <ErrorBoundary>
+ <Highlighter {...props} />
+ </ErrorBoundary>
+);
+export const Highlighter = ({
+ lang,
+ content,
+ isPreview,
+}: HighlighterProps) => {
+ const { tryHljs, useDevIcon, bgOpacity } = useShikiSettings(["tryHljs", "useDevIcon", "bgOpacity"]);
+ const { id: currentThemeId, theme: currentTheme } = useTheme();
+
+ const shikiLang = lang ? resolveLang(lang) : null;
+ const useHljs = shouldUseHljs({ lang, tryHljs });
+
+ const [preRef, isIntersecting] = useIntersection(true);
+
+ const [tokens] = useAwaiter(async () => {
+ if (!shikiLang || useHljs || !isIntersecting) return null;
+ return await shiki.tokenizeCode(content, lang!);
+ }, {
+ fallbackValue: null,
+ deps: [lang, content, currentThemeId, isIntersecting],
+ });
+
+ const themeBase: ThemeBase = {
+ plainColor: currentTheme?.fg || "var(--text-normal)",
+ accentBgColor:
+ currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"),
+ accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF",
+ backgroundColor:
+ currentTheme?.colors?.["editor.background"] || "var(--background-secondary)",
+ };
+
+ let langName;
+ if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name;
+
+ const preClasses = [cl("root")];
+ if (!langName) preClasses.push(cl("plain"));
+ if (isPreview) preClasses.push(cl("preview"));
+
+ return (
+ <pre
+ ref={preRef}
+ className={preClasses.join(" ")}
+ style={{
+ backgroundColor: useHljs
+ ? themeBase.backgroundColor
+ : `rgba(${hex2Rgb(themeBase.backgroundColor)
+ .concat(bgOpacity / 100)
+ .join(", ")})`,
+ color: themeBase.plainColor,
+ }}
+ >
+ <code>
+ <Header
+ langName={langName}
+ useDevIcon={useDevIcon}
+ shikiLang={shikiLang}
+ />
+ <Code
+ theme={themeBase}
+ useHljs={useHljs}
+ lang={lang}
+ content={content}
+ tokens={tokens}
+ />
+ {!isPreview && <ButtonRow
+ content={content}
+ theme={themeBase}
+ />}
+ </code>
+ </pre>
+ );
+};
+
diff --git a/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts b/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts
new file mode 100644
index 0000000..414500b
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts
@@ -0,0 +1,34 @@
+/*
+ * 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/>.
+*/
+
+import { Clipboard, React } from "@webpack/common";
+
+export function useCopyCooldown(cooldown: number) {
+ const [copyCooldown, setCopyCooldown] = React.useState(false);
+
+ function copy(text: string) {
+ Clipboard.copy(text);
+ setCopyCooldown(true);
+
+ setTimeout(() => {
+ setCopyCooldown(false);
+ }, cooldown);
+ }
+
+ return [copyCooldown, copy] as const;
+}
diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
new file mode 100644
index 0000000..416f8e9
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
@@ -0,0 +1,25 @@
+/*
+ * 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/>.
+*/
+
+import { useSettings } from "@api/settings";
+
+import { ShikiSettings } from "../types";
+
+export function useShikiSettings(settings: (keyof ShikiSettings)[]) {
+ return useSettings(settings.map(setting => `plugins.ShikiCodeblocks.${setting}`)).plugins.ShikiCodeblocks as ShikiSettings;
+}
diff --git a/src/plugins/shikiCodeblocks/hooks/useTheme.ts b/src/plugins/shikiCodeblocks/hooks/useTheme.ts
new file mode 100644
index 0000000..fae5796
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/hooks/useTheme.ts
@@ -0,0 +1,49 @@
+/*
+ * 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/>.
+*/
+
+import { React } from "@webpack/common";
+
+type Shiki = typeof import("../api/shiki").shiki;
+interface ThemeState {
+ id: Shiki["currentThemeUrl"],
+ theme: Shiki["currentTheme"],
+}
+
+const currentTheme: ThemeState = {
+ id: null,
+ theme: null,
+};
+
+const themeSetters = new Set<React.Dispatch<React.SetStateAction<ThemeState>>>();
+
+export const useTheme = (): ThemeState => {
+ const [, setTheme] = React.useState<ThemeState>(currentTheme);
+
+ React.useEffect(() => {
+ themeSetters.add(setTheme);
+ return () => void themeSetters.delete(setTheme);
+ }, []);
+
+ return currentTheme;
+};
+
+export function dispatchTheme(state: ThemeState) {
+ if (currentTheme.id === state.id) return;
+ Object.assign(currentTheme, state);
+ themeSetters.forEach(setTheme => setTheme(state));
+}
diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts
new file mode 100644
index 0000000..a8be92a
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/index.ts
@@ -0,0 +1,154 @@
+/*
+ * 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/>.
+*/
+
+import { Devs } from "@utils/constants";
+import { parseUrl } from "@utils/misc";
+import { wordsFromPascal, wordsToTitle } from "@utils/text";
+import definePlugin, { OptionType } from "@utils/types";
+
+import cssText from "~fileContent/style.css";
+
+import { Settings } from "../../Vencord";
+import { shiki } from "./api/shiki";
+import { themes } from "./api/themes";
+import { createHighlighter } from "./components/Highlighter";
+import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types";
+import { clearStyles, removeStyle, setStyle } from "./utils/createStyle";
+
+const themeNames = Object.keys(themes);
+const devIconCss = "@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');";
+
+const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
+
+export default definePlugin({
+ name: "ShikiCodeblocks",
+ description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
+ authors: [Devs.Vap],
+ patches: [
+ {
+ find: "codeBlock:{react:function",
+ replacement: {
+ match: /codeBlock:\{react:function\((.),(.),(.)\)\{/,
+ replace: "$&return Vencord.Plugins.plugins.ShikiCodeblocks.renderHighlighter($1,$2,$3);",
+ },
+ },
+ ],
+ start: async () => {
+ setStyle(cssText, StyleSheets.Main);
+ if (getSettings().useDevIcon !== DeviconSetting.Disabled)
+ setStyle(devIconCss, StyleSheets.DevIcons);
+
+ await shiki.init(getSettings().customTheme || getSettings().theme);
+ },
+ stop: () => {
+ shiki.destroy();
+ clearStyles();
+ },
+ options: {
+ theme: {
+ type: OptionType.SELECT,
+ description: "Default themes",
+ options: themeNames.map(themeName => ({
+ label: wordsToTitle(wordsFromPascal(themeName)),
+ value: themes[themeName],
+ default: themes[themeName] === themes.DarkPlus,
+ })),
+ disabled: () => !!getSettings().customTheme,
+ onChange: shiki.setTheme,
+ },
+ customTheme: {
+ type: OptionType.STRING,
+ description: "A link to a custom vscode theme",
+ placeholder: themes.MaterialCandy,
+ isValid: value => {
+ if (!value) return true;
+ const url = parseUrl(value);
+ if (!url) return "Must be a valid URL";
+
+ if (!url.pathname.endsWith(".json")) return "Must be a json file";
+
+ return true;
+ },
+ onChange: value => shiki.setTheme(value || getSettings().theme),
+ },
+ tryHljs: {
+ type: OptionType.SELECT,
+ description: "Use the more lightweight default Discord highlighter and theme.",
+ options: [
+ {
+ label: "Never",
+ value: HljsSetting.Never,
+ },
+ {
+ label: "Prefer Shiki instead of Highlight.js",
+ value: HljsSetting.Secondary,
+ default: true,
+ },
+ {
+ label: "Prefer Highlight.js instead of Shiki",
+ value: HljsSetting.Primary,
+ },
+ {
+ label: "Always",
+ value: HljsSetting.Always,
+ },
+ ],
+ },
+ useDevIcon: {
+ type: OptionType.SELECT,
+ description: "How to show language icons on codeblocks",
+ options: [
+ {
+ label: "Disabled",
+ value: DeviconSetting.Disabled,
+ },
+ {
+ label: "Colorless",
+ value: DeviconSetting.Greyscale,
+ default: true,
+ },
+ {
+ label: "Colored",
+ value: DeviconSetting.Color,
+ },
+ ],
+ onChange: (newValue: DeviconSetting) => {
+ if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons);
+ else setStyle(devIconCss, StyleSheets.DevIcons);
+ },
+ },
+ bgOpacity: {
+ type: OptionType.SLIDER,
+ description: "Background opacity",
+ markers: [0, 20, 40, 60, 80, 100],
+ default: 100,
+ stickToMarkers: false,
+ },
+ },
+
+ // exports
+ shiki,
+ createHighlighter,
+ renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => {
+ return createHighlighter({
+ lang,
+ content,
+ isPreview: false,
+ });
+ },
+});
diff --git a/src/plugins/shikiCodeblocks/style.css b/src/plugins/shikiCodeblocks/style.css
new file mode 100644
index 0000000..119ff80
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/style.css
@@ -0,0 +1,100 @@
+.shiki-root {
+ border-radius: 4px;
+}
+
+.shiki-root code {
+ display: block;
+ overflow-x: auto;
+ padding: 0.5em;
+ position: relative;
+
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ text-indent: 0;
+ white-space: pre-wrap;
+ background: transparent;
+ border: none;
+}
+
+.shiki-root [class^='devicon-'],
+.shiki-root [class*=' devicon-'] {
+ margin-right: 8px;
+ user-select: none;
+}
+
+.shiki-plain code {
+ padding-top: 8px;
+}
+
+.shiki-btns {
+ font-size: 1em;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+}
+
+.shiki-root:hover .shiki-btns {
+ opacity: 1;
+}
+
+.shiki-btn {
+ border-radius: 4px 4px 0 0;
+ padding: 4px 8px;
+}
+
+.shiki-btn~.shiki-btn {
+ margin-left: 4px;
+}
+
+.shiki-btn:last-child {
+ border-radius: 4px 0;
+}
+
+.shiki-spinner-container {
+ align-items: center;
+ background-color: rgba(0, 0, 0, 0.6);
+ display: flex;
+ position: absolute;
+ justify-content: center;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+.shiki-preview {
+ margin-bottom: 2em;
+}
+
+.shiki-lang {
+ padding: 0 5px;
+ margin-bottom: 6px;
+ font-weight: bold;
+ text-transform: capitalize;
+ display: flex;
+ align-items: center;
+}
+
+.shiki-table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.shiki-table tr {
+ height: 19px;
+ width: 100%;
+}
+
+.shiki-root td:first-child {
+ border-right: 1px solid transparent;
+ padding-left: 5px;
+ padding-right: 8px;
+ user-select: none;
+}
+
+.shiki-root td:last-child {
+ padding-left: 8px;
+ word-break: break-word;
+ width: 100%;
+}
diff --git a/src/plugins/shikiCodeblocks/types.ts b/src/plugins/shikiCodeblocks/types.ts
new file mode 100644
index 0000000..ee5aa9e
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/types.ts
@@ -0,0 +1,78 @@
+/*
+ * 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/>.
+*/
+
+import type {
+ ILanguageRegistration,
+ IShikiTheme,
+ IThemedToken,
+ IThemeRegistration,
+} from "@vap/shiki";
+
+import type { Settings } from "../../Vencord";
+
+/** This must be atleast a subset of the `@vap/shiki-worker` spec */
+export type ShikiSpec = {
+ setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>;
+ setHighlighter: ({ theme, langs }: {
+ theme: IThemeRegistration | void;
+ langs: ILanguageRegistration[];
+ }) => Promise<void>;
+ loadTheme: ({ theme }: {
+ theme: string | IShikiTheme;
+ }) => Promise<void>;
+ getTheme: ({ theme }: { theme: string; }) => Promise<{ themeData: string; }>;
+ loadLanguage: ({ lang }: { lang: ILanguageRegistration; }) => Promise<void>;
+ codeToThemedTokens: ({
+ code,
+ lang,
+ theme,
+ }: {
+ code: string;
+ lang?: string;
+ theme?: string;
+ }) => Promise<IThemedToken[][]>;
+};
+
+export enum StyleSheets {
+ Main = "MAIN",
+ DevIcons = "DEVICONS",
+}
+
+export enum HljsSetting {
+ Never = "NEVER",
+ Secondary = "SECONDARY",
+ Primary = "PRIMARY",
+ Always = "ALWAYS",
+}
+export enum DeviconSetting {
+ Disabled = "DISABLED",
+ Greyscale = "GREYSCALE",
+ Color = "COLOR"
+}
+
+type CommonSettings = {
+ [K in keyof Settings["plugins"][string]as K extends `${infer V}` ? K : never]: Settings["plugins"][string][K];
+};
+
+export interface ShikiSettings extends CommonSettings {
+ theme: string;
+ customTheme: string;
+ tryHljs: HljsSetting;
+ useDevIcon: DeviconSetting;
+ bgOpacity: number;
+}
diff --git a/src/plugins/shikiCodeblocks/utils/color.ts b/src/plugins/shikiCodeblocks/utils/color.ts
new file mode 100644
index 0000000..e74ec52
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/utils/color.ts
@@ -0,0 +1,32 @@
+/*
+ * 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/>.
+*/
+
+export function hex2Rgb(hex: string) {
+ hex = hex.slice(1);
+ if (hex.length < 6)
+ hex = hex
+ .split("")
+ .map(c => c + c)
+ .join("");
+ if (hex.length === 6) hex += "ff";
+ if (hex.length > 6) hex = hex.slice(0, 6);
+ return hex
+ .split(/(..)/)
+ .filter(Boolean)
+ .map(c => parseInt(c, 16));
+}
diff --git a/src/plugins/shikiCodeblocks/utils/createStyle.ts b/src/plugins/shikiCodeblocks/utils/createStyle.ts
new file mode 100644
index 0000000..734f7dc
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/utils/createStyle.ts
@@ -0,0 +1,36 @@
+/*
+ * 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/>.
+*/
+
+const styles = new Map<string, HTMLStyleElement>();
+
+export function setStyle(css: string, id: string) {
+ const style = document.createElement("style");
+ style.innerText = css;
+ document.head.appendChild(style);
+ styles.set(id, style);
+}
+
+export function removeStyle(id: string) {
+ styles.get(id)?.remove();
+ return styles.delete(id);
+}
+
+export const clearStyles = () => {
+ styles.forEach(style => style.remove());
+ styles.clear();
+};
diff --git a/src/plugins/shikiCodeblocks/utils/misc.ts b/src/plugins/shikiCodeblocks/utils/misc.ts
new file mode 100644
index 0000000..1342ff5
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/utils/misc.ts
@@ -0,0 +1,50 @@
+/*
+ * 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/>.
+*/
+
+import { hljs } from "@webpack/common";
+
+import { resolveLang } from "../api/languages";
+import { HighlighterProps } from "../components/Highlighter";
+import { HljsSetting, ShikiSettings } from "../types";
+
+export const cl = (className: string) => `shiki-${className}`;
+
+export const shouldUseHljs = ({
+ lang,
+ tryHljs,
+}: {
+ lang: HighlighterProps["lang"],
+ tryHljs: ShikiSettings["tryHljs"],
+}) => {
+ const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;
+ const shikiLang = lang ? resolveLang(lang) : null;
+ const langName = shikiLang?.name;
+
+ switch (tryHljs) {
+ case HljsSetting.Always:
+ return true;
+ case HljsSetting.Primary:
+ return !!hljsLang || lang === "";
+ case HljsSetting.Secondary:
+ return !langName && !!hljsLang;
+ case HljsSetting.Never:
+ return false;
+ }
+
+ return false;
+};
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index eead2a3..f45e8b0 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -157,8 +157,12 @@ export const Devs = Object.freeze({
name: "Luny",
id: 821472922140803112n
},
+ Vap: {
+ name: "Vap0r1ze",
+ id: 454072114492866560n
+ },
KingFish: {
name: "King Fish",
id: 499400512559382538n
- }
+ },
});
diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts
index a7766de..ed26644 100644
--- a/src/utils/dependencies.ts
+++ b/src/utils/dependencies.ts
@@ -74,3 +74,7 @@ export interface ApngFrameData {
frames: ApngFrame[];
playTime: number;
}
+
+const shikiWorkerDist = "https://unpkg.com/@vap/shiki-worker@0.0.8/dist";
+export const shikiWorkerSrc = `${shikiWorkerDist}/${IS_DEV ? "index.js" : "index.min.js"}`;
+export const shikiOnigasmSrc = "https://unpkg.com/@vap/shiki@0.10.3/dist/onig.wasm";
diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx
index d9164a0..8b7cea2 100644
--- a/src/utils/misc.tsx
+++ b/src/utils/misc.tsx
@@ -28,7 +28,12 @@ export function makeLazy<T>(factory: () => T): () => T {
return () => cache ?? (cache = factory());
}
-type AwaiterRes<T> = [T, any, boolean, () => void];
+type AwaiterRes<T> = [T, any, boolean];
+interface AwaiterOpts<T> {
+ fallbackValue: T,
+ deps?: unknown[],
+ onError?(e: any): void,
+}
/**
* Await a promise
* @param factory Factory
@@ -36,26 +41,31 @@ type AwaiterRes<T> = [T, any, boolean, () => void];
* @returns [value, error, isPending]
*/
export function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>;
-export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T): AwaiterRes<T>;
-export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: null, onError: (e: unknown) => unknown): AwaiterRes<T>;
-export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null = null, onError?: (e: unknown) => unknown): AwaiterRes<T | null> {
+export function useAwaiter<T>(factory: () => Promise<T>, providedOpts: AwaiterOpts<T>): AwaiterRes<T>;
+export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterOpts<T | null>): AwaiterRes<T | null> {
+ const opts: Required<AwaiterOpts<T | null>> = Object.assign({
+ fallbackValue: null,
+ deps: [],
+ onError: null,
+ }, providedOpts);
const [state, setState] = React.useState({
- value: fallbackValue,
+ value: opts.fallbackValue,
error: null,
pending: true
});
- const [signal, setSignal] = React.useState(0);
React.useEffect(() => {
let isAlive = true;
+ if (!state.pending) setState({ ...state, pending: true });
+
factory()
.then(value => isAlive && setState({ value, error: null, pending: false }))
- .catch(error => isAlive && (setState({ value: null, error, pending: false }), onError?.(error)));
+ .catch(error => isAlive && (setState({ value: null, error, pending: false }), opts.onError?.(error)));
return () => void (isAlive = false);
- }, [signal]);
+ }, opts.deps);
- return [state.value, state.error, state.pending, () => setSignal(signal + 1)];
+ return [state.value, state.error, state.pending];
}
/**
@@ -197,3 +207,24 @@ export function copyWithToast(text: string, toastMessage = "Copied to clipboard!
export function isObject(obj: unknown): obj is object {
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
}
+
+/**
+ * Returns null if value is not a URL, otherwise return URL object.
+ * Avoids having to wrap url checks in a try/catch
+ */
+export function parseUrl(urlString: string): URL | null {
+ try {
+ return new URL(urlString);
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Checks whether an element is on screen
+ */
+export const checkIntersecting = (el: Element) => {
+ const elementBox = el.getBoundingClientRect();
+ const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
+ return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0);
+};
diff --git a/src/utils/react.ts b/src/utils/react.ts
new file mode 100644
index 0000000..8585846
--- /dev/null
+++ b/src/utils/react.ts
@@ -0,0 +1,62 @@
+/*
+ * 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/>.
+*/
+
+import { React } from "@webpack/common";
+
+import { checkIntersecting } from "./misc";
+
+/**
+ * Check if an element is on screen
+ * @param intersectOnly If `true`, will only update the state when the element comes into view
+ * @returns [refCallback, isIntersecting]
+ */
+export const useIntersection = (intersectOnly = false): [
+ refCallback: React.RefCallback<Element>,
+ isIntersecting: boolean,
+] => {
+ const observerRef = React.useRef<IntersectionObserver | null>(null);
+ const [isIntersecting, setIntersecting] = React.useState(false);
+
+ const refCallback = (element: Element | null) => {
+ observerRef.current?.disconnect();
+ observerRef.current = null;
+
+ if (!element) return;
+
+ if (checkIntersecting(element)) {
+ setIntersecting(true);
+ if (intersectOnly) return;
+ }
+
+ observerRef.current = new IntersectionObserver(entries => {
+ for (const entry of entries) {
+ if (entry.target !== element) continue;
+ if (entry.isIntersecting && intersectOnly) {
+ setIntersecting(true);
+ observerRef.current?.disconnect();
+ observerRef.current = null;
+ } else {
+ setIntersecting(entry.isIntersecting);
+ }
+ }
+ });
+ observerRef.current.observe(element);
+ };
+
+ return [refCallback, isIntersecting];
+};
diff --git a/src/utils/text.ts b/src/utils/text.ts
new file mode 100644
index 0000000..17826e8
--- /dev/null
+++ b/src/utils/text.ts
@@ -0,0 +1,36 @@
+/*
+ * 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/>.
+*/
+
+// Utils for readable text transformations eg: `toTitle(fromKebab())`
+
+// Case style to words
+export const wordsFromCamel = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());
+export const wordsFromSnake = (text: string) => text.toLowerCase().split("_");
+export const wordsFromKebab = (text: string) => text.toLowerCase().split("-");
+export const wordsFromPascal = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());
+export const wordsFromTitle = (text: string) => text.toLowerCase().split(" ");
+
+// Words to case style
+export const wordsToCamel = (words: string[]) =>
+ words.map((w, i) => (i ? w[0].toUpperCase() + w.slice(1) : w)).join("");
+export const wordsToSnake = (words: string[]) => words.join("_").toUpperCase();
+export const wordsToKebab = (words: string[]) => words.join("-").toLowerCase();
+export const wordsToPascal = (words: string[]) =>
+ words.map(w => w[0].toUpperCase() + w.slice(1)).join("");
+export const wordsToTitle = (words: string[]) =>
+ words.map(w => w[0].toUpperCase() + w.slice(1)).join(" ");
diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx
index 8c43577..56846c2 100644
--- a/src/webpack/common.tsx
+++ b/src/webpack/common.tsx
@@ -37,6 +37,8 @@ export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPorta
export const RestAPI = findByPropsLazy("getAPIBaseURL", "get");
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");
+export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight");
+
export const MessageStore = findByPropsLazy("getRawMessages") as Omit<Stores.MessageStore, "getMessages"> & {
getMessages(chanId: string): any;
};