feat: usercss compilation and better settings storage
This commit is contained in:
parent
0cc420fb45
commit
723191ba9b
9 changed files with 176 additions and 31 deletions
|
@ -42,10 +42,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/less": "^3.0.4",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/stylus": "^0.48.39",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
|
@ -71,7 +73,8 @@
|
|||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||
"eslint@8.46.0": "patches/eslint@8.46.0.patch"
|
||||
"eslint@8.46.0": "patches/eslint@8.46.0.patch",
|
||||
"@types/less@3.0.4": "patches/@types__less@3.0.4.patch"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
|
|
13
patches/@types__less@3.0.4.patch
Normal file
13
patches/@types__less@3.0.4.patch
Normal file
|
@ -0,0 +1,13 @@
|
|||
diff --git a/index.d.ts b/index.d.ts
|
||||
index eb4f07d47b932fb9cc8c8cd451ab107f648bd013..18a3e15a1997734e1773718e5be55d252ed9478c 100644
|
||||
--- a/index.d.ts
|
||||
+++ b/index.d.ts
|
||||
@@ -306,7 +306,5 @@ interface LessStatic {
|
||||
}
|
||||
|
||||
declare module "less" {
|
||||
- export = less;
|
||||
+ export = LessStatic;
|
||||
}
|
||||
-
|
||||
-declare var less: LessStatic;
|
|
@ -1,6 +1,9 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
patchedDependencies:
|
||||
'@types/less@3.0.4':
|
||||
hash: krcufrsfhsuxuoj7hocqugs6zi
|
||||
path: patches/@types__less@3.0.4.patch
|
||||
eslint-plugin-path-alias@1.0.0:
|
||||
hash: m6sma4g6bh67km3q6igf6uxaja
|
||||
path: patches/eslint-plugin-path-alias@1.0.0.patch
|
||||
|
@ -38,6 +41,9 @@ devDependencies:
|
|||
'@types/diff':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
'@types/less':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(patch_hash=krcufrsfhsuxuoj7hocqugs6zi)
|
||||
'@types/lodash':
|
||||
specifier: ^4.14.194
|
||||
version: 4.14.194
|
||||
|
@ -50,6 +56,9 @@ devDependencies:
|
|||
'@types/react-dom':
|
||||
specifier: ^18.2.1
|
||||
version: 18.2.1
|
||||
'@types/stylus':
|
||||
specifier: ^0.48.39
|
||||
version: 0.48.39
|
||||
'@types/yazl':
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2
|
||||
|
@ -531,6 +540,11 @@ packages:
|
|||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||
dev: true
|
||||
|
||||
/@types/less@3.0.4(patch_hash=krcufrsfhsuxuoj7hocqugs6zi):
|
||||
resolution: {integrity: sha512-djlMpTdDF+tLaqVpK/0DWGNIr7BFjN8ykDLkgS0sQGYYLop51imRRE3foTjl+dMAH1zFE8bMZAG0VbYPEcSgsA==}
|
||||
dev: true
|
||||
patched: true
|
||||
|
||||
/@types/lodash@4.14.194:
|
||||
resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
|
||||
dev: true
|
||||
|
@ -580,6 +594,12 @@ packages:
|
|||
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
|
||||
dev: true
|
||||
|
||||
/@types/stylus@0.48.39:
|
||||
resolution: {integrity: sha512-98a0QrJorrq8+Vsan9yfxol2Qr6nvUWBeV3oYnSMks4QdLMebAzZvRd9IuoZOcnB6Erfjcjn1J2J+63MPCxJnw==}
|
||||
dependencies:
|
||||
'@types/node': 18.16.3
|
||||
dev: true
|
||||
|
||||
/@types/yauzl@2.10.0:
|
||||
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
|
||||
requiresBuild: true
|
||||
|
|
|
@ -62,7 +62,11 @@ export interface Settings {
|
|||
settingsSyncVersion: number;
|
||||
};
|
||||
|
||||
userCssVars: Record<string, string>;
|
||||
userCssVars: {
|
||||
[fileName: string]: {
|
||||
[varName: string]: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type StylusRenderer = require("stylus/lib/renderer");
|
||||
import type LessStatic from "less";
|
||||
|
||||
import { makeLazy } from "./lazy";
|
||||
|
||||
/*
|
||||
|
@ -85,3 +88,18 @@ export const rnnoiseWorkletSrc = `${rnnoiseDist}/rnnoise/workletProcessor.js`;
|
|||
|
||||
// @ts-expect-error SHUT UP
|
||||
export const getStegCloak = makeLazy(() => import("https://unpkg.com/stegcloak-dist@1.0.0/index.js"));
|
||||
|
||||
export const getStylus = makeLazy(async () => {
|
||||
const stylusScript = await fetch("https://unpkg.com/stylus-lang-bundle@0.58.1/dist/stylus-renderer.min.js").then(r => r.text());
|
||||
// the stylus bundle doesn't have a header that checks for export conditions so we can just patch the script to
|
||||
// return the renderer itself
|
||||
const patchedScript = stylusScript.replace("var StylusRenderer=", "return ");
|
||||
return Function(patchedScript)() as typeof StylusRenderer;
|
||||
});
|
||||
|
||||
export const getLess = makeLazy(async () => {
|
||||
const lessScript = await fetch("https://unpkg.com/less@4.2.0/dist/less.min.js").then(r => r.text());
|
||||
const module = { exports: {} };
|
||||
Function("module", "exports", lessScript)(module, module.exports);
|
||||
return module.exports as LessStatic;
|
||||
});
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
*/
|
||||
|
||||
import { addSettingsListener, Settings } from "@api/Settings";
|
||||
import { parse as usercssParse } from "@utils/themes/usercss";
|
||||
|
||||
import { compileUsercss } from "./themes/usercss/compiler";
|
||||
|
||||
|
||||
let style: HTMLStyleElement;
|
||||
let themesStyle: HTMLStyleElement;
|
||||
|
@ -51,39 +53,30 @@ async function initThemes() {
|
|||
const links: string[] = [...themeLinks];
|
||||
|
||||
if (IS_WEB) {
|
||||
for (const theme of enabledThemes) {
|
||||
// UserCSS handled separately
|
||||
for (const theme of enabledThemes) if (!theme.endsWith(".user.css")) {
|
||||
const themeData = await VencordNative.themes.getThemeData(theme);
|
||||
if (!themeData) continue;
|
||||
|
||||
const blob = new Blob([themeData], { type: "text/css" });
|
||||
links.push(URL.createObjectURL(blob));
|
||||
}
|
||||
} else {
|
||||
const localThemes = enabledThemes.map(theme => `vencord:///themes/${theme}?v=${Date.now()}`);
|
||||
links.push(...localThemes);
|
||||
}
|
||||
|
||||
const cssVars: string[] = [];
|
||||
|
||||
// for UserCSS, we need to inject the variables
|
||||
for (const theme of enabledThemes) if (theme.endsWith(".user.css")) {
|
||||
const themeData = await VencordNative.themes.getThemeData(theme);
|
||||
if (!themeData) continue;
|
||||
|
||||
const { vars } = usercssParse(themeData, theme);
|
||||
|
||||
for (const [id, meta] of Object.entries(vars)) {
|
||||
let normalizedValue: string = userCssVars[id] ?? meta.default;
|
||||
|
||||
if (meta.type === "range") {
|
||||
normalizedValue = `${normalizedValue}${meta.units ?? ""}`;
|
||||
}
|
||||
|
||||
cssVars.push(`--${id}:${normalizedValue};`);
|
||||
for (const theme of enabledThemes) if (!theme.endsWith(".user.css")) {
|
||||
links.push(`vencord:///themes/${theme}?v=${Date.now()}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const theme of enabledThemes) if (theme.endsWith(".user.css")) {
|
||||
// UserCSS goes through a compile step first
|
||||
const css = await compileUsercss(theme);
|
||||
if (!css) continue; // something went wrong during the compile step...
|
||||
|
||||
const blob = new Blob([css], { type: "text/css" });
|
||||
links.push(URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n");
|
||||
if (cssVars.length > 0) themesStyle.textContent += `:root{${cssVars.join("\n")}}`;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
|
88
src/utils/themes/usercss/compiler.ts
Normal file
88
src/utils/themes/usercss/compiler.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { getLess, getStylus } from "@utils/dependencies";
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
import { parse as usercssParse } from ".";
|
||||
|
||||
const UserCSSLogger = new Logger("UserCSS:Compiler", "#d2acf5");
|
||||
|
||||
const preprocessors: { [preprocessor: string]: (text: string, vars: Record<string, string>) => Promise<string>; } = {
|
||||
async default(text: string, vars: Record<string, string>) {
|
||||
const variables = Object.entries(vars)
|
||||
.map(([name, value]) => `--${name}: ${value}`)
|
||||
.join("; ");
|
||||
|
||||
return `/* ==Vencord== */\n:root{${variables}}\n/* ==/Vencord== */${text}`;
|
||||
},
|
||||
|
||||
async uso(text: string, vars: Record<string, string>) {
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
text = text.replace(new RegExp(`\\/\\*\\[\\[${k}\\]\\]\\*\\/`, "g"), v);
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
|
||||
async stylus(text: string, vars: Record<string, string>) {
|
||||
const StylusRenderer = await getStylus();
|
||||
|
||||
const variables = Object.entries(vars)
|
||||
.map(([name, value]) => `${name} = ${value}`)
|
||||
.join("\n");
|
||||
|
||||
const stylusDoc = `// ==Vencord==\n${variables}\n// ==/Vencord==\n${text}`;
|
||||
|
||||
return new StylusRenderer(stylusDoc).render();
|
||||
},
|
||||
|
||||
async less(text: string, vars: Record<string, string>) {
|
||||
const less = await getLess();
|
||||
|
||||
const variables = Object.entries(vars)
|
||||
.map(([name, value]) => `@${name}: ${value};`)
|
||||
.join("\n");
|
||||
|
||||
const lessDoc = `// ==Vencord==\n${variables}\n// ==/Vencord==\n${text}`;
|
||||
|
||||
return less.render(lessDoc).then(r => r.css);
|
||||
}
|
||||
};
|
||||
|
||||
export async function compileUsercss(fileName: string) {
|
||||
const themeData = await VencordNative.themes.getThemeData(fileName);
|
||||
if (!themeData) return null;
|
||||
|
||||
const { preprocessor: definedPreprocessor, vars } = usercssParse(themeData, fileName);
|
||||
|
||||
// UserCSS preprocessor order look like this:
|
||||
// - use the preprocessor defined
|
||||
// - if variables are set, `uso`
|
||||
// - otherwise, `default`
|
||||
const usedPreprocessor = definedPreprocessor ?? (Object.keys(vars).length > 0 ? "uso" : "default");
|
||||
|
||||
const preprocessorFn = preprocessors[usedPreprocessor];
|
||||
|
||||
if (!preprocessorFn) {
|
||||
UserCSSLogger.error("File", fileName, "requires preprocessor", usedPreprocessor, "which isn't known to Vencord");
|
||||
return null;
|
||||
}
|
||||
|
||||
const varsToPass = {};
|
||||
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
varsToPass[k] = Settings.userCssVars[fileName]?.[k] ?? v.default;
|
||||
}
|
||||
|
||||
try {
|
||||
return await preprocessorFn(themeData, varsToPass);
|
||||
} catch (error) {
|
||||
UserCSSLogger.error("File", fileName, "failed to compile with preprocessor", usedPreprocessor, error);
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -4,10 +4,18 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { parse as originalParse, UserstyleHeader } from "usercss-meta";
|
||||
|
||||
const UserCSSLogger = new Logger("UserCSS", "#d2acf5");
|
||||
|
||||
export function parse(text: string, fileName: string): UserstyleHeader {
|
||||
const { metadata } = originalParse(text.replace(/\r/g, ""));
|
||||
const { metadata, errors } = originalParse(text.replace(/\r/g, ""));
|
||||
|
||||
if (errors.length) {
|
||||
UserCSSLogger.warn("Parsed", fileName, "with errors:", errors);
|
||||
}
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
fileName,
|
||||
|
|
6
src/utils/themes/usercss/usercss-meta.d.ts
vendored
6
src/utils/themes/usercss/usercss-meta.d.ts
vendored
|
@ -93,10 +93,8 @@ declare module "usercss-meta" {
|
|||
license?: string;
|
||||
/**
|
||||
* The CSS preprocessor used to write this style.
|
||||
*
|
||||
* @vencord Unimplemented in Vencord, just part of the metadata.
|
||||
*/
|
||||
preprocessor?: string;
|
||||
preprocessor?: "default" | "uso" | "less" | "stylus";
|
||||
|
||||
/**
|
||||
* A list of variables the style defines.
|
||||
|
@ -104,5 +102,5 @@ declare module "usercss-meta" {
|
|||
vars: Record<string, UserCSSVariable>;
|
||||
}
|
||||
|
||||
export function parse(text: string): { metadata: UserstyleHeader; };
|
||||
export function parse(text: string): { metadata: UserstyleHeader; errors: { code: string; args: any; }[] };
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue