diff --git a/package-lock.json b/package-lock.json
index d3dc8da..1507cbe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "jankclient",
- "version": "0.2.0",
+ "name": "fermi",
+ "version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "jankclient",
- "version": "0.2.0",
+ "name": "fermi",
+ "version": "0.0.1",
"license": "GPL-3.0",
"dependencies": {
"@swc/core": "1.11.24"
@@ -16,7 +16,7 @@
"@types/node": "^25.5.0",
"@types/node-fetch": "^2.6.13",
"prettier": "^3.7.4",
- "typescript": "5.9.3"
+ "typescript": "^7.0.2"
}
},
"node_modules/@swc/core": {
@@ -272,6 +272,346 @@
"form-data": "^4.0.4"
}
},
+ "node_modules/@typescript/typescript-aix-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
+ "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
+ "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
+ "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
+ "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
+ "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
+ "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-loong64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
+ "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-mips64el": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
+ "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
+ "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-riscv64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
+ "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-s390x": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
+ "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
+ "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-sunos-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
+ "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
+ "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
+ "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -551,17 +891,38 @@
}
},
"node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+ "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
+ "tsc": "bin/tsc"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=16.20.0"
+ },
+ "optionalDependencies": {
+ "@typescript/typescript-aix-ppc64": "7.0.2",
+ "@typescript/typescript-darwin-arm64": "7.0.2",
+ "@typescript/typescript-darwin-x64": "7.0.2",
+ "@typescript/typescript-freebsd-arm64": "7.0.2",
+ "@typescript/typescript-freebsd-x64": "7.0.2",
+ "@typescript/typescript-linux-arm": "7.0.2",
+ "@typescript/typescript-linux-arm64": "7.0.2",
+ "@typescript/typescript-linux-loong64": "7.0.2",
+ "@typescript/typescript-linux-mips64el": "7.0.2",
+ "@typescript/typescript-linux-ppc64": "7.0.2",
+ "@typescript/typescript-linux-riscv64": "7.0.2",
+ "@typescript/typescript-linux-s390x": "7.0.2",
+ "@typescript/typescript-linux-x64": "7.0.2",
+ "@typescript/typescript-netbsd-arm64": "7.0.2",
+ "@typescript/typescript-netbsd-x64": "7.0.2",
+ "@typescript/typescript-openbsd-arm64": "7.0.2",
+ "@typescript/typescript-openbsd-x64": "7.0.2",
+ "@typescript/typescript-sunos-x64": "7.0.2",
+ "@typescript/typescript-win32-arm64": "7.0.2",
+ "@typescript/typescript-win32-x64": "7.0.2"
}
},
"node_modules/undici-types": {
diff --git a/package.json b/package.json
index 30da0a5..63566be 100644
--- a/package.json
+++ b/package.json
@@ -20,12 +20,15 @@
"@types/node": "^25.5.0",
"@types/node-fetch": "^2.6.13",
"prettier": "^3.7.4",
- "typescript": "5.9.3"
+ "typescript": "^7.0.2"
},
"prettier": {
"useTabs": true,
"printWidth": 100,
"semi": true,
"bracketSpacing": false
+ },
+ "allowScripts": {
+ "@swc/core@1.11.24": true
}
}
diff --git a/src/webpage/app.html b/src/webpage/app.html
index d8590f7..47b2abb 100644
--- a/src/webpage/app.html
+++ b/src/webpage/app.html
@@ -91,11 +91,8 @@
-
-
+
diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts
index b9aee97..e405ac8 100644
--- a/src/webpage/channel.ts
+++ b/src/webpage/channel.ts
@@ -55,12 +55,16 @@ class Channel extends SnowFlake {
get lastreadmessageid() {
return this.lastreadmessageidint;
}
- async getOverwrites(): Promise<[User | Role, Permissions][]> {
+ async getOverwrites(): Promise<[User | Role | {id: string}, Permissions][]> {
return await Promise.all(
[...this.permissionOverwriteMap].map(async ([id, p]) => {
const role = this.guild.roleids.get(id);
if (role) return [role, p] as const;
- return [await this.localuser.getUser(id), p] as const;
+ try {
+ return [await this.localuser.getUser(id), p] as const;
+ } catch {
+ return [{id}, p] as const;
+ }
}),
);
}
@@ -230,11 +234,7 @@ class Channel extends SnowFlake {
},
{
visible: function () {
- return (
- this.hasPermission("MANAGE_CHANNELS") ||
- this.owner_id === this.localuser.user.id ||
- true
- );
+ return this.hasPermission("MANAGE_CHANNELS") || this.owner_id === this.localuser.user.id;
},
icon: {
css: "svg-settings",
@@ -1553,6 +1553,7 @@ class Channel extends SnowFlake {
const buttons = options.addOptions("", {ltr: true});
buttons.addButtonInput("", "Yes", () => {
this.perminfo.nsfwOk = true;
+ this.localuser.focusChannel = undefined;
this.getHTML();
});
buttons.addButtonInput("", "No", () => {
@@ -1915,8 +1916,8 @@ class Channel extends SnowFlake {
};
this.voice.onconnect = () => {
if (!this.voice) return;
- for (const [_, user] of this.voice.users) {
- this.decorateLive(user);
+ for (const [id] of this.liveMap) {
+ this.decorateLive(id);
}
};
this.voice.onLeaveStream = (id) => {
@@ -1932,8 +1933,8 @@ class Channel extends SnowFlake {
this.purgeVid(id);
}
if (!this.voice) return;
- for (const [_, user] of this.voice.users) {
- this.decorateLive(user);
+ for (const [id] of this.liveMap) {
+ this.decorateLive(id);
}
};
}
diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts
index 5ae090a..a5529bc 100644
--- a/src/webpage/embed.ts
+++ b/src/webpage/embed.ts
@@ -462,6 +462,7 @@ class Embed {
img.onclick = async () => {
if (this.json.video) {
const url = new URL(this.json.video.url);
+ if (!(url.protocol === "http:" || url.protocol === "https:")) return;
if (url.host !== "youtube.com") return;
img.remove();
const iframe = document.createElement("iframe");
diff --git a/src/webpage/file.ts b/src/webpage/file.ts
index e4671e9..effefbf 100644
--- a/src/webpage/file.ts
+++ b/src/webpage/file.ts
@@ -181,7 +181,12 @@ class File {
const src = this.proxy_url || this.url;
return makePlayBox(src, player, 0, url);
}
- upHTML(files: Blob[], map: WeakMap
, file: globalThis.File): HTMLElement {
+ upHTML(
+ files: Blob[],
+ map: WeakMap,
+ file: globalThis.File,
+ onremove: () => void,
+ ): HTMLElement {
const div = document.createElement("div");
let contained = this.getHTML(true, false, file.name.startsWith("SPOILER_"));
div.classList.add("containedFile");
@@ -196,6 +201,7 @@ class File {
garbage.onclick = (_) => {
div.remove();
files.splice(files.indexOf(file), 1);
+ onremove();
};
const spoiler = document.createElement("button");
diff --git a/src/webpage/highlighter/clike/langs.json b/src/webpage/highlighter/clike/langs.json
new file mode 100644
index 0000000..c7079ec
--- /dev/null
+++ b/src/webpage/highlighter/clike/langs.json
@@ -0,0 +1,41 @@
+[
+{
+ "names":["js","ts","javascript"],
+ "keywords": ["break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do","else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "return", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with", "yield", "let", "await", "true", "false", "null", "undefined"],
+ "firstLineShebang": true,
+ "doubleSlashComments": true,
+ "multilineSlashComments": true,
+ "JSLikeTemplateStrings": true
+},
+{
+ "names":["h","c"],
+ "keywords":["auto", "break", "case", "char", "continue", "do", "default", "const", "double", "else", "enum", "extern", "for", "if", "goto", "float", "int", "long", "register", "return", "signed", "static", "sizeof", "short", "struct", "switch", "typedef", "union", "void", "while", "volatile", "unsigned"],
+ "hashComments": true,
+ "doubleSlashComments": true,
+ "multilineSlashComments": true
+},
+{
+ "names":["json"],
+ "keywords":["true","false","null"]
+},
+{
+ "names":["go"],
+ "keywords":["break","default","func","interface","select","case","defer","go","map","struct","chan","else","goto","package","switch","const","fallthrough","if","range","type","continue", "for","import","return","var"],
+ "doubleSlashComments": true,
+ "multilineSlashComments": true
+},
+{
+ "names": ["cs", "csharp"],
+ "keywords": ["abstract", "add", "alias", "allows", "and", "args", "as", "ascending", "async", "await", "base", "bool", "break", "by", "byte", "case", "catch", "char", "checked", "class", "closed", "const", "continue", "decimal", "default", "delegate", "descending", "do", "double", "dynamic", "else", "enum", "equals", "event", "explicit", "extension", "extern", "field", "file", "finally", "fixed", "float", "for", "foreach", "from", "get", "global", "goto", "group", "if", "implicit", "in", "init", "interface", "internal", "into", "is", "join", "let", "lock", "long", "managed", "nameof", "namespace", "new", "nint", "not", "notnull", "nuint", "null", "object", "on", "operator", "or", "orderby", "out", "override", "params", "partial", "private", "protected", "public", "readonly", "record", "ref", "remove", "required", "return", "safe", "sbyte", "scoped", "sealed", "select", "set", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "try", "typeof", "uint", "ulong", "unchecked", "unmanaged", "unsafe", "ushort", "using", "value", "var", "virtual", "void", "volatile", "when", "where", "while", "with", "yield", "true", "false"],
+ "firstLineShebang":true,
+ "doubleSlashComments": true,
+ "multilineSlashComments": true,
+ "multiLine":"\"\"\""
+},
+{
+ "names": ["py", "python"],
+ "keywords": ["False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield"],
+ "hashComments": true,
+ "multiLine":"\"\"\""
+ }
+]
diff --git a/src/webpage/highlighter/clike/lex.ts b/src/webpage/highlighter/clike/lex.ts
new file mode 100644
index 0000000..34dde2c
--- /dev/null
+++ b/src/webpage/highlighter/clike/lex.ts
@@ -0,0 +1,173 @@
+import {hcolors} from "../colors.js";
+
+interface clikeConf {
+ names: string[];
+ keywords: string[];
+ firstLineShebang?: boolean;
+ hashComments?: boolean;
+ doubleSlashComments?: boolean;
+ multilineSlashComments?: boolean;
+ JSLikeTemplateStrings?: boolean;
+ multiLine?: string;
+}
+function regex(config: clikeConf): RegExp {
+ const conds = [] as string[];
+ if (config.multiLine && "escape" in RegExp) {
+ const chars = config.multiLine;
+ const charArr = [...chars];
+ const matchOpts = [`\\\\(?:.|\\s|\\n)`, `(?:[^${charArr[0]}]|\\n)`] as string[];
+ for (let i = 0; i < chars.length - 1; i++) {
+ matchOpts.push(
+ RegExp.escape(config.multiLine.slice(0, i + 1)) + `(?:(?=\\\\)|\\n|[^${charArr[i + 1]}]|$)`,
+ );
+ }
+ conds.push(
+ `(${RegExp.escape(config.multiLine)}(?:${matchOpts.join("|")})*(?:${RegExp.escape(config.multiLine)})?)`,
+ );
+ }
+ conds.push(`\'(\\\\(.|\\n)|[^"\\n\\\\])*\'?`);
+ conds.push(`"(\\\\(.|\\n)|[^"\\n\\\\])*"?`);
+ if (config.hashComments) {
+ conds.push("#.*");
+ }
+ if (config.doubleSlashComments) {
+ conds.push("\\/\\/.*");
+ }
+ if (config.multilineSlashComments) {
+ conds.push("\\/\\*([^*]|\\*([^\\/]|$))*(\\*\\/)?");
+ }
+ if (config.multilineSlashComments) {
+ conds.push("\\/\\*([^*]|\\*([^\\/]|$))*(\\*\\/)?");
+ }
+ if (config.JSLikeTemplateStrings) {
+ conds.push("`(\\\\.|[^`])*`?");
+ }
+ conds.push("(.|\n)");
+
+ return new RegExp(
+ "\\s+|[a-z_A-Z][a-z_A-Z0-9]*|[0-9][a-z_A-Z0-9]*\\.?[a-zA-Z_0-9]*|" + conds.join("|"),
+ "g",
+ );
+}
+let j: undefined | clikeConf[] = undefined;
+export async function canLex(lang: string) {
+ if (!j) {
+ j = await (await fetch("/highlighter/clike/langs.json")).json();
+ }
+ const conf = j!.find((_) => _.names.includes(lang));
+ if (conf) {
+ return (code: string) => {
+ return lex(code, conf);
+ };
+ }
+ return undefined;
+}
+function* lex(code: string, config: clikeConf) {
+ const r = regex(config);
+ const keywords = new Set(config.keywords);
+ if (config.firstLineShebang) {
+ const m = code.match(/^#!.*\n?/m);
+ if (m) {
+ code = code.replace(/^#!.*\n?/m, "");
+ yield {
+ type: hcolors.comment,
+ content: m[0],
+ };
+ }
+ }
+ const matches = Array.from(code.matchAll(r));
+ let i = -1;
+ function identifyTypeOfWord(word: string) {
+ if (keywords.has(word)) {
+ return hcolors.keyword;
+ } else {
+ const next = nextNonBlank();
+ if (next) {
+ if (next.startsWith("(")) {
+ return hcolors.function;
+ }
+ }
+
+ const prev = prevNonBlank();
+ if (prev) {
+ if (prev.endsWith(".")) {
+ return hcolors.property;
+ }
+ }
+ if (word.match(/^[A-Z]/)) return hcolors.class;
+ return hcolors.identifier;
+ }
+ }
+ function isBlank(lex: string) {
+ if (lex.match(/^\s*$/)) {
+ return true;
+ } else if (lex.startsWith("//") && config.doubleSlashComments) {
+ return true;
+ } else if (lex.startsWith("/*") && config.multilineSlashComments) {
+ return true;
+ } else if (lex.startsWith("#") && config.hashComments) {
+ return true;
+ }
+ return false;
+ }
+ function prevNonBlank() {
+ for (let j = i - 1; j >= 0; j--) {
+ const lex = matches[j][0];
+ if (isBlank(lex)) continue;
+ return lex;
+ }
+ return null;
+ }
+ function nextNonBlank() {
+ for (let j = i + 1; j < matches.length; j++) {
+ const lex = matches[j][0];
+ if (isBlank(lex)) continue;
+ return lex;
+ }
+ return null;
+ }
+ for (const [lex] of matches) {
+ i++;
+ if (lex.startsWith("//") && config.doubleSlashComments) {
+ yield {
+ type: hcolors.comment,
+ content: lex,
+ };
+ } else if (lex.startsWith("/*") && config.multilineSlashComments) {
+ yield {
+ type: hcolors.comment,
+ content: lex,
+ };
+ } else if (lex.startsWith("#") && config.hashComments) {
+ yield {
+ type: hcolors.comment,
+ content: lex,
+ };
+ } else if (lex.startsWith("`") && config.JSLikeTemplateStrings) {
+ yield {
+ type: hcolors.string,
+ content: lex,
+ };
+ } else if (lex.match(/^("|')/)) {
+ yield {
+ type: hcolors.string,
+ content: lex,
+ };
+ } else if (lex.match(/^[0-9]/)) {
+ yield {
+ type: hcolors.number,
+ content: lex,
+ };
+ } else if (lex.match(/^[a-zA-Z]/)) {
+ yield {
+ type: identifyTypeOfWord(lex),
+ content: lex,
+ };
+ } else {
+ yield {
+ type: hcolors.symbol,
+ content: lex,
+ };
+ }
+ }
+}
diff --git a/src/webpage/highlighter/colors.ts b/src/webpage/highlighter/colors.ts
new file mode 100644
index 0000000..d9c2514
--- /dev/null
+++ b/src/webpage/highlighter/colors.ts
@@ -0,0 +1,11 @@
+export const enum hcolors {
+ keyword,
+ number,
+ string,
+ symbol,
+ comment,
+ identifier,
+ function,
+ class,
+ property,
+}
diff --git a/src/webpage/highlighter/highlight.ts b/src/webpage/highlighter/highlight.ts
new file mode 100644
index 0000000..32a64da
--- /dev/null
+++ b/src/webpage/highlighter/highlight.ts
@@ -0,0 +1,155 @@
+import {hcolors} from "./colors";
+function ifCSSAllows() {
+ if (!("CSS" in globalThis && CSS.highlights && globalThis.Highlight)) return () => {};
+ const strLen = (str: string) => {
+ return [...str].length;
+ };
+ const highkeyword = new Highlight();
+ CSS.highlights.set("highkeyword", highkeyword);
+
+ const highnumber = new Highlight();
+ CSS.highlights.set("highnumber", highnumber);
+
+ const highstring = new Highlight();
+ CSS.highlights.set("highstring", highstring);
+
+ const highsymbol = new Highlight();
+ CSS.highlights.set("highsymbol", highsymbol);
+ const highcomment = new Highlight();
+ CSS.highlights.set("highcomment", highcomment);
+
+ const highidentifier = new Highlight();
+ CSS.highlights.set("highidentifier", highidentifier);
+
+ const highFunc = new Highlight();
+ CSS.highlights.set("highFunc", highFunc);
+
+ const highClass = new Highlight();
+ CSS.highlights.set("highClass", highClass);
+
+ const highProp = new Highlight();
+ CSS.highlights.set("highProp", highProp);
+ const f = new FinalizationRegistry((id) => {
+ eliMap.delete(id);
+ });
+ const eliMap = new Map();
+ function addToRangeMap(r: Range, elm: HTMLElement, h: Highlight) {
+ let prop = elm.dataset.id;
+ if (!prop) {
+ prop = Math.random() + "";
+ elm.dataset.id = prop;
+ f.register(elm, prop);
+ }
+ let r2: [Highlight, Range][] | undefined = eliMap.get(prop);
+ if (!r2) {
+ r2 = [] as [Highlight, Range][];
+ eliMap.set(prop, r2);
+ }
+ r2.push([h, r]);
+ }
+ function clearHighlights(elm: HTMLElement) {
+ let prop = elm.dataset.id;
+ if (!prop) return;
+ const m = eliMap.get(prop);
+ if (!m) return;
+ eliMap.delete(prop);
+ for (const [h, r] of m) {
+ h.delete(r);
+ }
+ }
+
+ function highlight(r: Range, color: hcolors, elm: HTMLElement) {
+ switch (color) {
+ case hcolors.keyword:
+ highkeyword.add(r);
+ addToRangeMap(r, elm, highkeyword);
+ break;
+ case hcolors.number:
+ highnumber.add(r);
+ addToRangeMap(r, elm, highnumber);
+ break;
+ case hcolors.comment:
+ highcomment.add(r);
+ addToRangeMap(r, elm, highcomment);
+ break;
+ case hcolors.identifier:
+ highidentifier.add(r);
+ addToRangeMap(r, elm, highidentifier);
+ break;
+ case hcolors.string:
+ highstring.add(r);
+ addToRangeMap(r, elm, highstring);
+ break;
+ case hcolors.symbol:
+ highsymbol.add(r);
+ addToRangeMap(r, elm, highsymbol);
+ break;
+ case hcolors.function:
+ highFunc.add(r);
+ addToRangeMap(r, elm, highFunc);
+ break;
+ case hcolors.class:
+ highClass.add(r);
+ addToRangeMap(r, elm, highClass);
+ break;
+ case hcolors.property:
+ highProp.add(r);
+ addToRangeMap(r, elm, highProp);
+ break;
+ default:
+ color satisfies never;
+ }
+ }
+ return function highlightFromLex(
+ elm: HTMLElement,
+ lex: Iterable<{
+ type: hcolors;
+ content: string;
+ }>,
+ skipStart: number,
+ ) {
+ clearHighlights(elm);
+ const nodes = Array.from(elm.childNodes) as Text[];
+ let r = new Range();
+ const lexed = Array.from(lex);
+ if (skipStart) {
+ lexed.unshift({type: hcolors.symbol, content: " ".repeat(skipStart)});
+ }
+ if (lexed.length === 0) return;
+ let i = 1;
+ let lenToGo = strLen(lexed[0].content);
+ let t = lexed[0].type;
+ r.setStart(nodes[0], 0);
+ function getNextNode() {
+ if (!lexed[i]) {
+ const last = nodes.at(-1) as Text;
+ r.setEnd(last, strLen(last.textContent));
+ highlight(r, t, elm);
+ return -1;
+ } else highlight(r, t, elm);
+ lenToGo = strLen(lexed[i].content);
+ t = lexed[i].type;
+ i++;
+ return 0;
+ }
+
+ for (const node of nodes) {
+ let len = strLen(node.textContent);
+ let start = 0;
+ while (len) {
+ if (len < lenToGo) {
+ lenToGo -= len;
+ break;
+ } else {
+ start += lenToGo;
+ r.setEnd(node, start);
+ len -= lenToGo;
+ if (getNextNode() === -1) return;
+ r = new Range();
+ r.setStart(node, start);
+ }
+ }
+ }
+ };
+}
+export const highlightFromLex = ifCSSAllows();
diff --git a/src/webpage/highlighter/index.ts b/src/webpage/highlighter/index.ts
new file mode 100644
index 0000000..ac4f746
--- /dev/null
+++ b/src/webpage/highlighter/index.ts
@@ -0,0 +1,25 @@
+import {canLex} from "./clike/lex.js";
+import {highlightFromLex} from "./highlight.js";
+import {lex as xmlex} from "./xml/lex.js";
+export async function highlight(
+ elm: HTMLElement,
+ lang: string,
+ skipStart: number = 0,
+ skipEnd: number = 0,
+) {
+ await new Promise((res) => setTimeout(res));
+ lang = lang.trim();
+ let content = elm.textContent;
+ if (skipStart) {
+ content = content.slice(skipStart);
+ }
+ if (skipEnd) {
+ content = content.slice(0, content.length - skipEnd);
+ }
+ if (lang === "xml" || lang === "html" || lang === "htmlx" || lang === "qml" || lang === "gmx") {
+ highlightFromLex(elm, xmlex(content), skipStart);
+ } else {
+ const clike = await canLex(lang);
+ if (clike) highlightFromLex(elm, clike(content), skipStart);
+ }
+}
diff --git a/src/webpage/highlighter/xml/lex.ts b/src/webpage/highlighter/xml/lex.ts
new file mode 100644
index 0000000..cea7c03
--- /dev/null
+++ b/src/webpage/highlighter/xml/lex.ts
@@ -0,0 +1,35 @@
+import {hcolors} from "../colors.js";
+
+const r =
+ /"(\\(.|\n)|[^"\n\\])*"?|)?|[a-zA-Z][a-zA-Z0-9]*\s*=|<[\s\/]*[^<> ]+|.|\n/gm;
+
+export function* lex(code: string) {
+ for (const [lex] of code.matchAll(r)) {
+ if (lex.startsWith("