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": {
|
"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": [
|
||||||
|
|
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'
|
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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
|
@ -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")) {
|
|
||||||
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")) {
|
||||||
|
// 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");
|
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", () => {
|
||||||
|
|
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
|
* 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,
|
||||||
|
|
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;
|
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; }[] };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue