aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/shikiCodeblocks/components
diff options
context:
space:
mode:
authorJustice Almanzar <superdash993@gmail.com>2022-12-02 10:43:37 -0500
committerGitHub <noreply@github.com>2022-12-02 16:43:37 +0100
commit41dddc9eee6f19fb5055545811aff1e282790a9c (patch)
treebf38148e87242e169adfa39919aa636a9d0551ce /src/plugins/shikiCodeblocks/components
parent4760af7f0ee275caa1eee440f4945032057d2b56 (diff)
downloadVencord-41dddc9eee6f19fb5055545811aff1e282790a9c.tar.gz
Vencord-41dddc9eee6f19fb5055545811aff1e282790a9c.tar.bz2
Vencord-41dddc9eee6f19fb5055545811aff1e282790a9c.zip
feat(plugin): ShikiCodeblocks (#267)
Co-authored-by: ArjixWasTaken <53124886+ArjixWasTaken@users.noreply.github.com> Co-authored-by: Ven <vendicated@riseup.net>
Diffstat (limited to 'src/plugins/shikiCodeblocks/components')
-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
5 files changed, 344 insertions, 0 deletions
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>
+ );
+};
+