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("