feat: usercss compilation and better settings storage

This commit is contained in:
Lewis Crichton 2023-09-08 14:59:41 +01:00
parent 0cc420fb45
commit 723191ba9b
No known key found for this signature in database
9 changed files with 176 additions and 31 deletions

View file

@ -42,10 +42,12 @@
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^5.0.3", "@types/diff": "^5.0.3",
"@types/less": "^3.0.4",
"@types/lodash": "^4.14.194", "@types/lodash": "^4.14.194",
"@types/node": "^18.16.3", "@types/node": "^18.16.3",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1", "@types/react-dom": "^18.2.1",
"@types/stylus": "^0.48.39",
"@types/yazl": "^2.4.2", "@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1", "@typescript-eslint/parser": "^5.59.1",
@ -71,7 +73,8 @@
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch", "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": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [

View 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;

View file

@ -1,6 +1,9 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
patchedDependencies: patchedDependencies:
'@types/less@3.0.4':
hash: krcufrsfhsuxuoj7hocqugs6zi
path: patches/@types__less@3.0.4.patch
eslint-plugin-path-alias@1.0.0: eslint-plugin-path-alias@1.0.0:
hash: m6sma4g6bh67km3q6igf6uxaja hash: m6sma4g6bh67km3q6igf6uxaja
path: patches/eslint-plugin-path-alias@1.0.0.patch path: patches/eslint-plugin-path-alias@1.0.0.patch
@ -38,6 +41,9 @@ devDependencies:
'@types/diff': '@types/diff':
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3 version: 5.0.3
'@types/less':
specifier: ^3.0.4
version: 3.0.4(patch_hash=krcufrsfhsuxuoj7hocqugs6zi)
'@types/lodash': '@types/lodash':
specifier: ^4.14.194 specifier: ^4.14.194
version: 4.14.194 version: 4.14.194
@ -50,6 +56,9 @@ devDependencies:
'@types/react-dom': '@types/react-dom':
specifier: ^18.2.1 specifier: ^18.2.1
version: 18.2.1 version: 18.2.1
'@types/stylus':
specifier: ^0.48.39
version: 0.48.39
'@types/yazl': '@types/yazl':
specifier: ^2.4.2 specifier: ^2.4.2
version: 2.4.2 version: 2.4.2
@ -531,6 +540,11 @@ packages:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true 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: /@types/lodash@4.14.194:
resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
dev: true dev: true
@ -580,6 +594,12 @@ packages:
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
dev: true 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: /@types/yauzl@2.10.0:
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
requiresBuild: true requiresBuild: true

View file

@ -62,7 +62,11 @@ export interface Settings {
settingsSyncVersion: number; settingsSyncVersion: number;
}; };
userCssVars: Record<string, string>; userCssVars: {
[fileName: string]: {
[varName: string]: string;
};
};
} }
const DefaultSettings: Settings = { const DefaultSettings: Settings = {

View file

@ -16,6 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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"; import { makeLazy } from "./lazy";
/* /*
@ -85,3 +88,18 @@ export const rnnoiseWorkletSrc = `${rnnoiseDist}/rnnoise/workletProcessor.js`;
// @ts-expect-error SHUT UP // @ts-expect-error SHUT UP
export const getStegCloak = makeLazy(() => import("https://unpkg.com/stegcloak-dist@1.0.0/index.js")); 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;
});

View file

@ -17,7 +17,9 @@
*/ */
import { addSettingsListener, Settings } from "@api/Settings"; import { addSettingsListener, Settings } from "@api/Settings";
import { parse as usercssParse } from "@utils/themes/usercss";
import { compileUsercss } from "./themes/usercss/compiler";
let style: HTMLStyleElement; let style: HTMLStyleElement;
let themesStyle: HTMLStyleElement; let themesStyle: HTMLStyleElement;
@ -51,39 +53,30 @@ async function initThemes() {
const links: string[] = [...themeLinks]; const links: string[] = [...themeLinks];
if (IS_WEB) { 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); const themeData = await VencordNative.themes.getThemeData(theme);
if (!themeData) continue; if (!themeData) continue;
const blob = new Blob([themeData], { type: "text/css" }); const blob = new Blob([themeData], { type: "text/css" });
links.push(URL.createObjectURL(blob)); links.push(URL.createObjectURL(blob));
} }
} else { } else {
const localThemes = enabledThemes.map(theme => `vencord:///themes/${theme}?v=${Date.now()}`); for (const theme of enabledThemes) if (!theme.endsWith(".user.css")) {
links.push(...localThemes); links.push(`vencord:///themes/${theme}?v=${Date.now()}`);
}
} }
const cssVars: string[] = [];
// for UserCSS, we need to inject the variables
for (const theme of enabledThemes) if (theme.endsWith(".user.css")) { for (const theme of enabledThemes) if (theme.endsWith(".user.css")) {
const themeData = await VencordNative.themes.getThemeData(theme); // UserCSS goes through a compile step first
if (!themeData) continue; const css = await compileUsercss(theme);
if (!css) continue; // something went wrong during the compile step...
const { vars } = usercssParse(themeData, theme); const blob = new Blob([css], { type: "text/css" });
links.push(URL.createObjectURL(blob));
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};`);
}
} }
themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n"); themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n");
if (cssVars.length > 0) themesStyle.textContent += `:root{${cssVars.join("\n")}}`;
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View 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;
}
}

View file

@ -4,10 +4,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Logger } from "@utils/Logger";
import { parse as originalParse, UserstyleHeader } from "usercss-meta"; import { parse as originalParse, UserstyleHeader } from "usercss-meta";
const UserCSSLogger = new Logger("UserCSS", "#d2acf5");
export function parse(text: string, fileName: string): UserstyleHeader { 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 { return {
...metadata, ...metadata,
fileName, fileName,

View file

@ -93,10 +93,8 @@ declare module "usercss-meta" {
license?: string; license?: string;
/** /**
* The CSS preprocessor used to write this style. * 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. * A list of variables the style defines.
@ -104,5 +102,5 @@ declare module "usercss-meta" {
vars: Record<string, UserCSSVariable>; vars: Record<string, UserCSSVariable>;
} }
export function parse(text: string): { metadata: UserstyleHeader; }; export function parse(text: string): { metadata: UserstyleHeader; errors: { code: string; args: any; }[] };
} }