feat(plugin): ShikiCodeblocks (#267)
Co-authored-by: ArjixWasTaken <53124886+ArjixWasTaken@users.noreply.github.com> Co-authored-by: Ven <vendicated@riseup.net>main
parent
4760af7f0e
commit
41dddc9eee
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
@ -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());
|
||||
};
|
@ -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>;
|
||||
}
|
@ -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>;
|
||||
};
|
@ -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>
|
||||
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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));
|
||||
}
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
@ -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%;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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));
|
||||
}
|
@ -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();
|
||||
};
|
@ -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;
|
||||
};
|
@ -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];
|
||||
};
|
@ -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(" ");
|
Loading…
Reference in new issue