From 5d0693b4bf729c8fa80016e7db7e08489c821160 Mon Sep 17 00:00:00 2001 From: OverHash <46231745+OverHash@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:15:01 +1200 Subject: [PATCH 1/3] Fix compiler worker not fetching wasm binary correctly See issue #59 for why this patch is needed. Essentially, inside the web worker environment, the global 'Response' does not exist. As a result, wasm-bindgen's attempt to fetch the binary and parse the Response object does not work. This patch sends the entire wasm binary through to `WebAssembly.instantiate`. --- package-lock.json | 181 ++++++++++++++++++++++++++++++++++++----- package.json | 3 +- src/compiler.worker.ts | 163 +++++++++++++++++++++---------------- src/main.ts | 24 +++--- 4 files changed, 266 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index 923909b..00d7817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-typst-plugin", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-typst-plugin", - "version": "0.9.0", + "version": "0.10.0", "license": "Apache-2.0", "dependencies": { "fflate": "^0.8.1", @@ -23,7 +23,8 @@ "builtin-modules": "^3", "esbuild": "^0.18", "esbuild-plugin-inline-worker": "^0.1.1", - "typescript": "^5.1" + "typescript": "^5.1", + "wasm-pack": "^0.13.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -851,12 +852,34 @@ "node": ">=8" } }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-install": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.1.0.tgz", + "integrity": "sha512-rkwNGW+3aQVSZoD0/o3mfPN6Yxh3Id0R/xzTVBVVpGNlVz8EGwusksxRlbk/A5iKTZt9zkMn3qIqmAt3vpfbzg==", "dev": true, - "peer": true + "dependencies": { + "axios": "^0.26.1", + "rimraf": "^3.0.2", + "tar": "^6.1.11" + }, + "engines": { + "node": ">=10" + } }, "node_modules/boolbase": { "version": "1.0.0", @@ -868,7 +891,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -925,6 +947,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -963,8 +994,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -1553,19 +1583,61 @@ "dev": true, "peer": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1687,7 +1759,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, - "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1697,8 +1768,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/is-extglob": { "version": "2.1.1", @@ -1884,7 +1954,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1892,6 +1961,52 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -1952,7 +2067,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "peer": true, "dependencies": { "wrappy": "1" } @@ -2043,7 +2157,6 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2213,7 +2326,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -2370,6 +2482,23 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -2470,6 +2599,19 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "peer": true }, + "node_modules/wasm-pack": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.13.0.tgz", + "integrity": "sha512-AmboGZEnZoIcVCzSlkLEmNFEqJN+IwgshJ5S7pi30uNUTce4LvWkifQzsQRxnWj47G8gkqZxlyGlyQplsnIS7w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "binary-install": "^1.0.1" + }, + "bin": { + "wasm-pack": "run.js" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2490,8 +2632,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/yallist": { "version": "4.0.0", @@ -2514,7 +2655,7 @@ }, "pkg": { "name": "obsidian-typst", - "version": "0.8.0" + "version": "0.10.0" }, "pkg/pkg": { "extraneous": true diff --git a/package.json b/package.json index c90f2cd..b34f062 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "builtin-modules": "^3", "esbuild": "^0.18", "esbuild-plugin-inline-worker": "^0.1.1", - "typescript": "^5.1" + "typescript": "^5.1", + "wasm-pack": "^0.13.0" }, "dependencies": { "fflate": "^0.8.1", diff --git a/src/compiler.worker.ts b/src/compiler.worker.ts index 359590f..6d580ae 100644 --- a/src/compiler.worker.ts +++ b/src/compiler.worker.ts @@ -1,88 +1,107 @@ -import typstInit, * as typst from '../pkg' +import typstInit, * as typst from "../pkg"; import { CompileImageCommand, CompileSvgCommand, Message } from "src/types"; let canUseSharedArrayBuffer = false; -let decoder = new TextDecoder() +const decoder = new TextDecoder(); let basePath: string; let packagePath: string; -let packages: string[] = [] -const xhr = new XMLHttpRequest() +let packages: string[] = []; +const xhr = new XMLHttpRequest(); function requestData(path: string): string { - try { - if (!canUseSharedArrayBuffer) { - if (path.startsWith("@")) { - if (packages.includes(path.slice(1))) { - return packagePath + path.slice(1) - } - throw 2 - } - path = "http://localhost/_capacitor_file_" + basePath + "/" + path - xhr.open("GET", path, false) - try { - xhr.send() - } catch (e) { - console.error(e); - throw 3 - } - if (xhr.status == 404) { - throw 2 - } - return xhr.responseText - } - // @ts-expect-error - let buffer = new Int32Array(new SharedArrayBuffer(4, { maxByteLength: 1e8 })) - buffer[0] = 0; - postMessage({ buffer, path }) - const res = Atomics.wait(buffer, 0, 0); - if (buffer[0] == 0) { - return decoder.decode(Uint8Array.from(buffer.slice(1))) - } - throw buffer[0] - } catch (e) { - if (typeof e != "number") { - console.error(e) - throw 1 - } - throw e - } + try { + if (!canUseSharedArrayBuffer) { + if (path.startsWith("@")) { + if (packages.includes(path.slice(1))) { + return packagePath + path.slice(1); + } + throw 2; + } + path = "http://localhost/_capacitor_file_" + basePath + "/" + path; + xhr.open("GET", path, false); + try { + xhr.send(); + } catch (e) { + console.error(e); + throw 3; + } + if (xhr.status == 404) { + throw 2; + } + return xhr.responseText; + } + const buffer = new Int32Array( + // @ts-expect-error + new SharedArrayBuffer(4, { maxByteLength: 1e8 }) + ); + buffer[0] = 0; + postMessage({ buffer, path }); + const res = Atomics.wait(buffer, 0, 0); + if (buffer[0] == 0) { + return decoder.decode(Uint8Array.from(buffer.slice(1))); + } + throw buffer[0]; + } catch (e) { + if (typeof e != "number") { + console.error(e); + throw 1; + } + throw e; + } } let compiler: typst.SystemWorld; onmessage = (ev: MessageEvent) => { - const message = ev.data - switch (message.type) { - case "canUseSharedArrayBuffer": - canUseSharedArrayBuffer = message.data - break; - case "startup": - typstInit(message.data.wasm).then(_ => { - compiler = new typst.SystemWorld("", requestData) - console.log("Typst web assembly loaded!"); - }) - basePath = message.data.basePath - packagePath = message.data.packagePath - break; - case "fonts": - message.data.forEach((font: any) => compiler.add_font(new Uint8Array(font))) - break; - case "compile": - if (message.data.format == "image") { - const data: CompileImageCommand = message.data; - postMessage(compiler.compile_image(data.source, data.path, data.pixel_per_pt, data.fill, data.size, data.display)) - } else if (message.data.format == "svg") { - postMessage(compiler.compile_svg(message.data.source, message.data.path)) - } - break; - case "packages": - packages = message.data - break; - default: - throw message - } -} + const message = ev.data; + switch (message.type) { + case "canUseSharedArrayBuffer": + canUseSharedArrayBuffer = message.data; + break; + case "startup": + // we fetch and send the raw wasm binary here + // see issue #59 for why we cannot send a URL blob to where the wasm file is located + fetch(message.data.wasm) + .then((response) => response.arrayBuffer()) + .then((wasmBinary) => typstInit(wasmBinary)) + .then((_) => { + compiler = new typst.SystemWorld("", requestData); + console.log("Typst web assembly loaded!"); + }); + basePath = message.data.basePath; + packagePath = message.data.packagePath; + break; + case "fonts": + message.data.forEach((font: any) => + compiler.add_font(new Uint8Array(font)) + ); + break; + case "compile": + if (message.data.format == "image") { + const data: CompileImageCommand = message.data; + postMessage( + compiler.compile_image( + data.source, + data.path, + data.pixel_per_pt, + data.fill, + data.size, + data.display + ) + ); + } else if (message.data.format == "svg") { + const data: CompileSvgCommand = message.data; + postMessage(compiler.compile_svg(data.source, data.path)); + } + break; + case "packages": + packages = message.data; + break; + default: + throw message; + } +}; console.log("Typst compiler worker loaded!"); diff --git a/src/main.ts b/src/main.ts index 0823f61..f46631d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -53,7 +53,7 @@ export default class TypstPlugin extends Plugin { tex2chtml: any; - prevCanvasHeight: number = 0; + prevCanvasHeight = 0; textEncoder: TextEncoder fs: any; @@ -100,7 +100,7 @@ export default class TypstPlugin extends Plugin { if (Platform.isDesktopApp) { this.compilerWorker.postMessage({ type: "canUseSharedArrayBuffer", data: true }); this.fs = require("fs") - let fonts = await Promise.all( + const fonts = await Promise.all( //@ts-expect-error (await window.queryLocalFonts() as Array) .filter((font: { family: string; name: string; }) => this.settings.font_families.contains(font.family.toLowerCase())) @@ -153,7 +153,7 @@ export default class TypstPlugin extends Plugin { let data response = requestUrl(`https://api.github.com/repos/fenjalien/obsidian-typst/releases/tags/${PLUGIN_VERSION}`) data = await response.json - let asset = data.assets.find((a: any) => a.name == "obsidian_typst_bg.wasm") + const asset = data.assets.find((a: any) => a.name == "obsidian_typst_bg.wasm") if (asset == undefined) { throw "Could not find the correct file!" } @@ -170,8 +170,8 @@ export default class TypstPlugin extends Plugin { } async getPackageList(): Promise { - let getFolders = async (f: string) => (await this.app.vault.adapter.list(f)).folders - let packages = [] + const getFolders = async (f: string) => (await this.app.vault.adapter.list(f)).folders + const packages = [] // namespace for (const namespace of await getFolders(this.packagePath)) { // name @@ -270,7 +270,7 @@ export default class TypstPlugin extends Plugin { try { const text = await (path.startsWith("@") ? this.preparePackage(path.slice(1)) : this.getFileString(path)) if (text) { - let buffer = Int32Array.from(this.textEncoder.encode( + const buffer = Int32Array.from(this.textEncoder.encode( text )); if (wbuffer.byteLength < (buffer.byteLength + 4)) { @@ -320,7 +320,7 @@ export default class TypstPlugin extends Plugin { async preparePackage(spec: string): Promise { if (Platform.isDesktopApp) { - let subdir = "/typst/packages/" + spec + const subdir = "/typst/packages/" + spec let dir = require('path').normalize(this.getDataDir() + subdir) if (this.fs.existsSync(dir)) { @@ -421,7 +421,7 @@ export default class TypstPlugin extends Plugin { } createTypstRenderElement(path: string, source: string, display: boolean, math: boolean) { - let renderer = new TypstRenderElement(); + const renderer = new TypstRenderElement(); renderer.format = this.settings.format renderer.source = source renderer.path = path @@ -504,7 +504,7 @@ class TypstSettingTab extends PluginSettingTab { - let no_fill = new Setting(containerEl) + const no_fill = new Setting(containerEl) .setName("No Fill (Transparent)") .setDisabled(this.plugin.settings.format == "svg") .addToggle((toggle) => { @@ -518,7 +518,7 @@ class TypstSettingTab extends PluginSettingTab { ) }); - let fill_color = new Setting(containerEl) + const fill_color = new Setting(containerEl) .setName("Fill Color") .setDisabled(this.plugin.settings.noFill || this.plugin.settings.format == "svg") .addColorPicker((picker) => { @@ -531,7 +531,7 @@ class TypstSettingTab extends PluginSettingTab { ) }) - let pixel_per_pt = new Setting(containerEl) + const pixel_per_pt = new Setting(containerEl) .setName("Pixel Per Point") .setDisabled(this.plugin.settings.format == "svg") .addSlider((slider) => @@ -618,7 +618,7 @@ class TypstSettingTab extends PluginSettingTab { deletePackagesBtn.addEventListener('click', () => { const selectedPackageElements = packageSettingsDiv.querySelectorAll('input[name="package-checkbox"]:checked') - let packagesToDelete: string[] = [] + const packagesToDelete: string[] = [] selectedPackageElements.forEach(pkgEl => { packagesToDelete.push(pkgEl.getAttribute('value')!) From d3ce121369c8200f0794e0cd91d393ffd6331dc3 Mon Sep 17 00:00:00 2001 From: OverHash <46231745+OverHash@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:22:48 +1200 Subject: [PATCH 2/3] Format codebase with Prettier I saved all the files and ran the eslint on the codebase. --- .eslintrc | 42 +- src/main.ts | 1462 ++++++++++++++++++++--------------- src/types.d.ts | 30 +- src/typst-render-element.ts | 286 ++++--- 4 files changed, 1034 insertions(+), 786 deletions(-) diff --git a/.eslintrc b/.eslintrc index 0807290..7fc8ba5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,23 +1,21 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "env": { "node": true }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "@typescript-eslint/ban-ts-comment": "off", - "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off" - } - } \ No newline at end of file + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } +} diff --git a/src/main.ts b/src/main.ts index f46631d..87f51d8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,648 +1,850 @@ -import { App, renderMath, HexString, Platform, Plugin, PluginSettingTab, Setting, loadMathJax, normalizePath, Notice, requestUrl } from 'obsidian'; +import { + App, + renderMath, + HexString, + Platform, + Plugin, + PluginSettingTab, + Setting, + loadMathJax, + normalizePath, + Notice, + requestUrl, +} from "obsidian"; declare const PLUGIN_VERSION: string; // @ts-ignore -import CompilerWorker from "./compiler.worker.ts" +import CompilerWorker from "./compiler.worker.ts"; -import TypstRenderElement from './typst-render-element.js'; -import { WorkerRequest } from './types'; +import TypstRenderElement from "./typst-render-element.js"; +import { WorkerRequest } from "./types"; // @ts-ignore -import untar from "js-untar" -import { decompressSync } from "fflate" +import untar from "js-untar"; +import { decompressSync } from "fflate"; interface TypstPluginSettings { - format: string, - noFill: boolean, - fill: HexString, - pixel_per_pt: number, - search_system: boolean, - override_math: boolean, - font_families: string[], - preamable: { - shared: string, - math: string, - code: string, - }, - plugin_version: string, - autoDownloadPackages: boolean + format: string; + noFill: boolean; + fill: HexString; + pixel_per_pt: number; + search_system: boolean; + override_math: boolean; + font_families: string[]; + preamable: { + shared: string; + math: string; + code: string; + }; + plugin_version: string; + autoDownloadPackages: boolean; } const DEFAULT_SETTINGS: TypstPluginSettings = { - format: "image", - noFill: true, - fill: "#ffffff", - pixel_per_pt: 3, - search_system: false, - override_math: false, - font_families: [], - preamable: { - shared: "#set text(fill: white, size: SIZE)\n#set page(width: WIDTH, height: HEIGHT)", - math: "#set page(margin: 0pt)\n#set align(horizon)", - code: "#set page(margin: (y: 1em, x: 0pt))" - }, - plugin_version: PLUGIN_VERSION, - autoDownloadPackages: true -} + format: "image", + noFill: true, + fill: "#ffffff", + pixel_per_pt: 3, + search_system: false, + override_math: false, + font_families: [], + preamable: { + shared: "#set text(fill: white, size: SIZE)\n#set page(width: WIDTH, height: HEIGHT)", + math: "#set page(margin: 0pt)\n#set align(horizon)", + code: "#set page(margin: (y: 1em, x: 0pt))", + }, + plugin_version: PLUGIN_VERSION, + autoDownloadPackages: true, +}; export default class TypstPlugin extends Plugin { - settings: TypstPluginSettings; - - compilerWorker: Worker; - - tex2chtml: any; - - prevCanvasHeight = 0; - textEncoder: TextEncoder - fs: any; - - wasmPath: string - pluginPath: string - packagePath: string - - async onload() { - console.log("loading Typst Renderer"); - - this.textEncoder = new TextEncoder() - await this.loadSettings() - - this.pluginPath = this.app.vault.configDir + "/plugins/typst/" - this.packagePath = this.pluginPath + "packages/" - this.wasmPath = this.pluginPath + "obsidian_typst_bg.wasm" - - this.compilerWorker = (new CompilerWorker() as Worker); - if (!await this.app.vault.adapter.exists(this.wasmPath) || this.settings.plugin_version != PLUGIN_VERSION) { - new Notice("Typst Renderer: Downloading required web assembly component!", 5000); - try { - await this.fetchWasm() - new Notice("Typst Renderer: Web assembly component downloaded!", 5000) - } catch (error) { - new Notice("Typst Renderer: Failed to fetch component: " + error, 0) - console.error("Typst Renderer: Failed to fetch component: " + error) - } - } - this.compilerWorker.postMessage({ - type: "startup", - data: { - wasm: URL.createObjectURL( - new Blob( - [await this.app.vault.adapter.readBinary(this.wasmPath)], - { type: "application/wasm" } - ) - ), - //@ts-ignore - basePath: this.app.vault.adapter.basePath, - packagePath: this.packagePath - } - }); - - if (Platform.isDesktopApp) { - this.compilerWorker.postMessage({ type: "canUseSharedArrayBuffer", data: true }); - this.fs = require("fs") - const fonts = await Promise.all( - //@ts-expect-error - (await window.queryLocalFonts() as Array) - .filter((font: { family: string; name: string; }) => this.settings.font_families.contains(font.family.toLowerCase())) - .map( - async (font: { blob: () => Promise; }) => await (await font.blob()).arrayBuffer() - ) - ) - this.compilerWorker.postMessage({ type: "fonts", data: fonts }, fonts) - } else { - // Mobile - // Make sure it exists, won't error/affect anything if it does exist - await this.app.vault.adapter.mkdir(this.packagePath) - const packages = await this.getPackageList(); - this.compilerWorker.postMessage({ type: "packages", data: packages }) - } - - // Setup cutom canvas - TypstRenderElement.compile = (a, b, c, d, e) => this.processThenCompileTypst(a, b, c, d, e) - if (customElements.get("typst-renderer") == undefined) { - customElements.define("typst-renderer", TypstRenderElement) - } - - // Setup MathJax - await loadMathJax() - renderMath("", false); - // @ts-expect-error - this.tex2chtml = MathJax.tex2chtml - this.overrideMathJax(this.settings.override_math) - - this.addCommand({ - id: "toggle-math-override", - name: "Toggle math block override", - callback: () => this.overrideMathJax(!this.settings.override_math) - }) - - // Settings - this.addSettingTab(new TypstSettingTab(this.app, this)); - - // Code blocks - this.registerMarkdownCodeBlockProcessor("typst", async (source, el, ctx) => { - el.appendChild(this.createTypstRenderElement("/" + ctx.sourcePath, `${this.settings.preamable.code}\n${source}`, true, false)) - }) - - - console.log("loaded Typst Renderer"); - } - - async fetchWasm() { - let response - let data - response = requestUrl(`https://api.github.com/repos/fenjalien/obsidian-typst/releases/tags/${PLUGIN_VERSION}`) - data = await response.json - const asset = data.assets.find((a: any) => a.name == "obsidian_typst_bg.wasm") - if (asset == undefined) { - throw "Could not find the correct file!" - } - - response = requestUrl({ url: asset.url, headers: { "Accept": "application/octet-stream" } }) - data = await response.arrayBuffer - await this.app.vault.adapter.writeBinary( - this.wasmPath, - data - ) - - this.settings.plugin_version = PLUGIN_VERSION - await this.saveSettings() - } - - async getPackageList(): Promise { - const getFolders = async (f: string) => (await this.app.vault.adapter.list(f)).folders - const packages = [] - // namespace - for (const namespace of await getFolders(this.packagePath)) { - // name - for (const name of await getFolders(namespace)) { - // version - for (const version of await getFolders(name)) { - packages.push(version.split("/").slice(-3).join("/")) - } - } - } - return packages - } - - async deletePackages(packages: string[]) { - for (const folder of packages) { - await this.app.vault.adapter.rmdir(this.packagePath + folder, true) - } - } - - async compileToTypst(path: string, source: string, size: number, display: boolean): Promise { - return await navigator.locks.request("typst renderer compiler", async (lock) => { - let message - if (this.settings.format == "svg") { - message = { - type: "compile", - data: { - format: "svg", - path, - source - } - } - } else if (this.settings.format == "image") { - message = { - type: "compile", - data: { - format: "image", - source, - path, - pixel_per_pt: this.settings.pixel_per_pt, - fill: `${this.settings.fill}${this.settings.noFill ? "00" : "ff"}`, - size, - display - } - } - } - this.compilerWorker.postMessage(message) - while (true) { - let result: ImageData | string | WorkerRequest; - try { - result = await new Promise((resolve, reject) => { - const listener = (ev: MessageEvent) => { - remove(); - resolve(ev.data); - } - const errorListener = (error: ErrorEvent) => { - remove(); - reject(error.message) - } - const remove = () => { - this.compilerWorker.removeEventListener("message", listener); - this.compilerWorker.removeEventListener("error", errorListener); - } - this.compilerWorker.addEventListener("message", listener); - this.compilerWorker.addEventListener("error", errorListener); - }) - } catch (e) { - if (Platform.isMobileApp && e.startsWith("Uncaught Error: package not found (searched for")) { - const spec = e.match(/"@preview\/.*?"/)[0].slice(2, -1).replace(":", "/") - const [namespace, name, version] = spec.split("/") - try { - await this.fetchPackage(this.packagePath + spec + "/", name, version) - } catch (error) { - if (error == 2) { - throw e - } - throw error - } - const packages = await this.getPackageList() - this.compilerWorker.postMessage({ type: "packages", data: packages }) - this.compilerWorker.postMessage(message) - continue - } - throw e - } - if (result instanceof ImageData || typeof result == "string") { - return result - } - // Cannot reach this point when in mobile app as the worker should - // not have a SharedArrayBuffer - await this.handleWorkerRequest(result) - } - }) - } - - async handleWorkerRequest({ buffer: wbuffer, path }: WorkerRequest) { - try { - const text = await (path.startsWith("@") ? this.preparePackage(path.slice(1)) : this.getFileString(path)) - if (text) { - const buffer = Int32Array.from(this.textEncoder.encode( - text - )); - if (wbuffer.byteLength < (buffer.byteLength + 4)) { - //@ts-expect-error - wbuffer.buffer.grow(buffer.byteLength + 4) - } - wbuffer.set(buffer, 1) - wbuffer[0] = 0 - } - } catch (error) { - if (typeof error === "number") { - wbuffer[0] = error - } else { - wbuffer[0] = 1 - console.error(error) - } - } finally { - Atomics.notify(wbuffer, 0) - } - } - - async getFileString(path: string): Promise { - try { - if (require("path").isAbsolute(path)) { - return await this.fs.promises.readFile(path, { encoding: "utf8" }) - } else { - return await this.app.vault.adapter.read(normalizePath(path)) - } - } catch (e) { - console.error(e); - if (e.code == "ENOENT") { - // File not found - throw 2 - } - if (e.code == "EACCES") { - // access denied - throw 3 - } - if (e.code == "EISDIR") { - // File is directory - throw 4 - } - // Other File error - throw 5 - } - } - - async preparePackage(spec: string): Promise { - if (Platform.isDesktopApp) { - const subdir = "/typst/packages/" + spec - - let dir = require('path').normalize(this.getDataDir() + subdir) - if (this.fs.existsSync(dir)) { - return dir - } - - dir = require('path').normalize(this.getCacheDir() + subdir) - - if (this.fs.existsSync(dir)) { - return dir - } - } - - const folder = this.packagePath + spec + "/" - if (await this.app.vault.adapter.exists(folder)) { - return folder - } - if (spec.startsWith("preview") && this.settings.autoDownloadPackages) { - const [namespace, name, version] = spec.split("/") - try { - await this.fetchPackage(folder, name, version) - return folder - } catch (e) { - if (e == 2) { - throw e - } - console.error(e); - // Other package error - throw 3 - } - } - // Package not found error - throw 2 - } - - getDataDir() { - if (Platform.isLinux) { - if ("XDG_DATA_HOME" in process.env) { - return process.env["XDG_DATA_HOME"] - } else { - return process.env["HOME"] + "/.local/share" - } - } else if (Platform.isWin) { - return process.env["APPDATA"] - } else if (Platform.isMacOS) { - return process.env["HOME"] + "/Library/Application Support" - } - throw "Cannot find data directory on an unknown platform" - } - - getCacheDir() { - if (Platform.isLinux) { - if ("XDG_CACHE_HOME" in process.env) { - return process.env["XDG_DATA_HOME"] - } else { - return process.env["HOME"] + "/.cache" - } - } else if (Platform.isWin) { - return process.env["LOCALAPPDATA"] - } else if (Platform.isMacOS) { - return process.env["HOME"] + "/Library/Caches" - } - throw "Cannot find cache directory on an unknown platform" - } - - async fetchPackage(folder: string, name: string, version: string) { - const url = `https://packages.typst.org/preview/${name}-${version}.tar.gz`; - const response = await fetch(url) - if (response.status == 404) { - // Package not found error - throw 2 - } - await this.app.vault.adapter.mkdir(folder) - await untar(decompressSync(new Uint8Array(await response.arrayBuffer())).buffer).progress(async (file: any) => { - // is folder - if (file.type == "5" && file.name != ".") { - await this.app.vault.adapter.mkdir(folder + file.name) - } - // is file - if (file.type === "0") { - await this.app.vault.adapter.writeBinary(folder + file.name, file.buffer) - } - }); - } - - - async processThenCompileTypst(path: string, source: string, size: number, display: boolean, fontSize: number) { - const dpr = window.devicePixelRatio; - // * (72 / 96) - const pxToPt = (px: number) => px.toString() + "pt" - const sizing = `#let (WIDTH, HEIGHT, SIZE, THEME) = (${display ? pxToPt(size) : "auto"}, ${!display ? pxToPt(size) : "auto"}, ${pxToPt(fontSize)}, "${document.body.getCssPropertyValue("color-scheme")}")` - return this.compileToTypst( - path, - `${sizing}\n${this.settings.preamable.shared}\n${source}`, - size, - display - ) - } - - createTypstRenderElement(path: string, source: string, display: boolean, math: boolean) { - const renderer = new TypstRenderElement(); - renderer.format = this.settings.format - renderer.source = source - renderer.path = path - renderer.display = display - renderer.math = math - return renderer - } - - createTypstMath(source: string, r: { display: boolean }) { - const display = r.display; - source = `${this.settings.preamable.math}\n${display ? `$ ${source} $` : `$${source}$`}` - - return this.createTypstRenderElement("/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", source, display, true) - } - - onunload() { - // @ts-expect-error - MathJax.tex2chtml = this.tex2chtml - this.compilerWorker.terminate() - } - - async overrideMathJax(value: boolean) { - this.settings.override_math = value - await this.saveSettings(); - if (this.settings.override_math) { - // @ts-expect-error - MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r) - } else { - // @ts-expect-error - MathJax.tex2chtml = this.tex2chtml - } - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - } - + settings: TypstPluginSettings; + + compilerWorker: Worker; + + tex2chtml: any; + + prevCanvasHeight = 0; + textEncoder: TextEncoder; + fs: any; + + wasmPath: string; + pluginPath: string; + packagePath: string; + + async onload() { + console.log("loading Typst Renderer"); + + this.textEncoder = new TextEncoder(); + await this.loadSettings(); + + this.pluginPath = this.app.vault.configDir + "/plugins/typst/"; + this.packagePath = this.pluginPath + "packages/"; + this.wasmPath = this.pluginPath + "obsidian_typst_bg.wasm"; + + this.compilerWorker = new CompilerWorker() as Worker; + if ( + !(await this.app.vault.adapter.exists(this.wasmPath)) || + this.settings.plugin_version != PLUGIN_VERSION + ) { + new Notice( + "Typst Renderer: Downloading required web assembly component!", + 5000 + ); + try { + await this.fetchWasm(); + new Notice( + "Typst Renderer: Web assembly component downloaded!", + 5000 + ); + } catch (error) { + new Notice( + "Typst Renderer: Failed to fetch component: " + error, + 0 + ); + console.error( + "Typst Renderer: Failed to fetch component: " + error + ); + } + } + this.compilerWorker.postMessage({ + type: "startup", + data: { + wasm: URL.createObjectURL( + new Blob( + [ + await this.app.vault.adapter.readBinary( + this.wasmPath + ), + ], + { type: "application/wasm" } + ) + ), + //@ts-ignore + basePath: this.app.vault.adapter.basePath, + packagePath: this.packagePath, + }, + }); + + if (Platform.isDesktopApp) { + this.compilerWorker.postMessage({ + type: "canUseSharedArrayBuffer", + data: true, + }); + this.fs = require("fs"); + const fonts = await Promise.all( + //@ts-expect-error + ((await window.queryLocalFonts()) as Array) + .filter((font: { family: string; name: string }) => + this.settings.font_families.contains( + font.family.toLowerCase() + ) + ) + .map( + async (font: { blob: () => Promise }) => + await (await font.blob()).arrayBuffer() + ) + ); + this.compilerWorker.postMessage( + { type: "fonts", data: fonts }, + fonts + ); + } else { + // Mobile + // Make sure it exists, won't error/affect anything if it does exist + await this.app.vault.adapter.mkdir(this.packagePath); + const packages = await this.getPackageList(); + this.compilerWorker.postMessage({ + type: "packages", + data: packages, + }); + } + + // Setup cutom canvas + TypstRenderElement.compile = (a, b, c, d, e) => + this.processThenCompileTypst(a, b, c, d, e); + if (customElements.get("typst-renderer") == undefined) { + customElements.define("typst-renderer", TypstRenderElement); + } + + // Setup MathJax + await loadMathJax(); + renderMath("", false); + // @ts-expect-error + this.tex2chtml = MathJax.tex2chtml; + this.overrideMathJax(this.settings.override_math); + + this.addCommand({ + id: "toggle-math-override", + name: "Toggle math block override", + callback: () => this.overrideMathJax(!this.settings.override_math), + }); + + // Settings + this.addSettingTab(new TypstSettingTab(this.app, this)); + + // Code blocks + this.registerMarkdownCodeBlockProcessor( + "typst", + async (source, el, ctx) => { + el.appendChild( + this.createTypstRenderElement( + "/" + ctx.sourcePath, + `${this.settings.preamable.code}\n${source}`, + true, + false + ) + ); + } + ); + + console.log("loaded Typst Renderer"); + } + + async fetchWasm() { + let response; + let data; + response = requestUrl( + `https://api.github.com/repos/fenjalien/obsidian-typst/releases/tags/${PLUGIN_VERSION}` + ); + data = await response.json; + const asset = data.assets.find( + (a: any) => a.name == "obsidian_typst_bg.wasm" + ); + if (asset == undefined) { + throw "Could not find the correct file!"; + } + + response = requestUrl({ + url: asset.url, + headers: { Accept: "application/octet-stream" }, + }); + data = await response.arrayBuffer; + await this.app.vault.adapter.writeBinary(this.wasmPath, data); + + this.settings.plugin_version = PLUGIN_VERSION; + await this.saveSettings(); + } + + async getPackageList(): Promise { + const getFolders = async (f: string) => + (await this.app.vault.adapter.list(f)).folders; + const packages = []; + // namespace + for (const namespace of await getFolders(this.packagePath)) { + // name + for (const name of await getFolders(namespace)) { + // version + for (const version of await getFolders(name)) { + packages.push(version.split("/").slice(-3).join("/")); + } + } + } + return packages; + } + + async deletePackages(packages: string[]) { + for (const folder of packages) { + await this.app.vault.adapter.rmdir(this.packagePath + folder, true); + } + } + + async compileToTypst( + path: string, + source: string, + size: number, + display: boolean + ): Promise { + return await navigator.locks.request( + "typst renderer compiler", + async (lock) => { + let message; + if (this.settings.format == "svg") { + message = { + type: "compile", + data: { + format: "svg", + path, + source, + }, + }; + } else if (this.settings.format == "image") { + message = { + type: "compile", + data: { + format: "image", + source, + path, + pixel_per_pt: this.settings.pixel_per_pt, + fill: `${this.settings.fill}${ + this.settings.noFill ? "00" : "ff" + }`, + size, + display, + }, + }; + } + this.compilerWorker.postMessage(message); + while (true) { + let result: ImageData | string | WorkerRequest; + try { + result = await new Promise((resolve, reject) => { + const listener = (ev: MessageEvent) => { + remove(); + resolve(ev.data); + }; + const errorListener = (error: ErrorEvent) => { + remove(); + reject(error.message); + }; + const remove = () => { + this.compilerWorker.removeEventListener( + "message", + listener + ); + this.compilerWorker.removeEventListener( + "error", + errorListener + ); + }; + this.compilerWorker.addEventListener( + "message", + listener + ); + this.compilerWorker.addEventListener( + "error", + errorListener + ); + }); + } catch (e) { + if ( + Platform.isMobileApp && + e.startsWith( + "Uncaught Error: package not found (searched for" + ) + ) { + const spec = e + .match(/"@preview\/.*?"/)[0] + .slice(2, -1) + .replace(":", "/"); + const [namespace, name, version] = spec.split("/"); + try { + await this.fetchPackage( + this.packagePath + spec + "/", + name, + version + ); + } catch (error) { + if (error == 2) { + throw e; + } + throw error; + } + const packages = await this.getPackageList(); + this.compilerWorker.postMessage({ + type: "packages", + data: packages, + }); + this.compilerWorker.postMessage(message); + continue; + } + throw e; + } + if ( + result instanceof ImageData || + typeof result == "string" + ) { + return result; + } + // Cannot reach this point when in mobile app as the worker should + // not have a SharedArrayBuffer + await this.handleWorkerRequest(result); + } + } + ); + } + + async handleWorkerRequest({ buffer: wbuffer, path }: WorkerRequest) { + try { + const text = await (path.startsWith("@") + ? this.preparePackage(path.slice(1)) + : this.getFileString(path)); + if (text) { + const buffer = Int32Array.from(this.textEncoder.encode(text)); + if (wbuffer.byteLength < buffer.byteLength + 4) { + //@ts-expect-error + wbuffer.buffer.grow(buffer.byteLength + 4); + } + wbuffer.set(buffer, 1); + wbuffer[0] = 0; + } + } catch (error) { + if (typeof error === "number") { + wbuffer[0] = error; + } else { + wbuffer[0] = 1; + console.error(error); + } + } finally { + Atomics.notify(wbuffer, 0); + } + } + + async getFileString(path: string): Promise { + try { + if (require("path").isAbsolute(path)) { + return await this.fs.promises.readFile(path, { + encoding: "utf8", + }); + } else { + return await this.app.vault.adapter.read(normalizePath(path)); + } + } catch (e) { + console.error(e); + if (e.code == "ENOENT") { + // File not found + throw 2; + } + if (e.code == "EACCES") { + // access denied + throw 3; + } + if (e.code == "EISDIR") { + // File is directory + throw 4; + } + // Other File error + throw 5; + } + } + + async preparePackage(spec: string): Promise { + if (Platform.isDesktopApp) { + const subdir = "/typst/packages/" + spec; + + let dir = require("path").normalize(this.getDataDir() + subdir); + if (this.fs.existsSync(dir)) { + return dir; + } + + dir = require("path").normalize(this.getCacheDir() + subdir); + + if (this.fs.existsSync(dir)) { + return dir; + } + } + + const folder = this.packagePath + spec + "/"; + if (await this.app.vault.adapter.exists(folder)) { + return folder; + } + if (spec.startsWith("preview") && this.settings.autoDownloadPackages) { + const [namespace, name, version] = spec.split("/"); + try { + await this.fetchPackage(folder, name, version); + return folder; + } catch (e) { + if (e == 2) { + throw e; + } + console.error(e); + // Other package error + throw 3; + } + } + // Package not found error + throw 2; + } + + getDataDir() { + if (Platform.isLinux) { + if ("XDG_DATA_HOME" in process.env) { + return process.env["XDG_DATA_HOME"]; + } else { + return process.env["HOME"] + "/.local/share"; + } + } else if (Platform.isWin) { + return process.env["APPDATA"]; + } else if (Platform.isMacOS) { + return process.env["HOME"] + "/Library/Application Support"; + } + throw "Cannot find data directory on an unknown platform"; + } + + getCacheDir() { + if (Platform.isLinux) { + if ("XDG_CACHE_HOME" in process.env) { + return process.env["XDG_DATA_HOME"]; + } else { + return process.env["HOME"] + "/.cache"; + } + } else if (Platform.isWin) { + return process.env["LOCALAPPDATA"]; + } else if (Platform.isMacOS) { + return process.env["HOME"] + "/Library/Caches"; + } + throw "Cannot find cache directory on an unknown platform"; + } + + async fetchPackage(folder: string, name: string, version: string) { + const url = `https://packages.typst.org/preview/${name}-${version}.tar.gz`; + const response = await fetch(url); + if (response.status == 404) { + // Package not found error + throw 2; + } + await this.app.vault.adapter.mkdir(folder); + await untar( + decompressSync(new Uint8Array(await response.arrayBuffer())).buffer + ).progress(async (file: any) => { + // is folder + if (file.type == "5" && file.name != ".") { + await this.app.vault.adapter.mkdir(folder + file.name); + } + // is file + if (file.type === "0") { + await this.app.vault.adapter.writeBinary( + folder + file.name, + file.buffer + ); + } + }); + } + + async processThenCompileTypst( + path: string, + source: string, + size: number, + display: boolean, + fontSize: number + ) { + const dpr = window.devicePixelRatio; + // * (72 / 96) + const pxToPt = (px: number) => px.toString() + "pt"; + const sizing = `#let (WIDTH, HEIGHT, SIZE, THEME) = (${ + display ? pxToPt(size) : "auto" + }, ${!display ? pxToPt(size) : "auto"}, ${pxToPt( + fontSize + )}, "${document.body.getCssPropertyValue("color-scheme")}")`; + return this.compileToTypst( + path, + `${sizing}\n${this.settings.preamable.shared}\n${source}`, + size, + display + ); + } + + createTypstRenderElement( + path: string, + source: string, + display: boolean, + math: boolean + ) { + const renderer = new TypstRenderElement(); + renderer.format = this.settings.format; + renderer.source = source; + renderer.path = path; + renderer.display = display; + renderer.math = math; + return renderer; + } + + createTypstMath(source: string, r: { display: boolean }) { + const display = r.display; + source = `${this.settings.preamable.math}\n${ + display ? `$ ${source} $` : `$${source}$` + }`; + + return this.createTypstRenderElement( + "/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", + source, + display, + true + ); + } + + onunload() { + // @ts-expect-error + MathJax.tex2chtml = this.tex2chtml; + this.compilerWorker.terminate(); + } + + async overrideMathJax(value: boolean) { + this.settings.override_math = value; + await this.saveSettings(); + if (this.settings.override_math) { + // @ts-expect-error + MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r); + } else { + // @ts-expect-error + MathJax.tex2chtml = this.tex2chtml; + } + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + + async saveSettings() { + await this.saveData(this.settings); + } } class TypstSettingTab extends PluginSettingTab { - plugin: TypstPlugin; - - constructor(app: App, plugin: TypstPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - - async display() { - const { containerEl } = this; - - containerEl.empty(); - - new Setting(containerEl) - .setName("Render Format") - .addDropdown(dropdown => { - dropdown.addOptions({ - svg: "SVG", - image: "Image" - }) - .setValue(this.plugin.settings.format) - .onChange(async value => { - this.plugin.settings.format = value; - await this.plugin.saveSettings(); - if (value == "svg") { - no_fill.setDisabled(true) - fill_color.setDisabled(true) - pixel_per_pt.setDisabled(true) - } else { - no_fill.setDisabled(false) - fill_color.setDisabled(this.plugin.settings.noFill) - pixel_per_pt.setDisabled(false) - } - }) - }) - - - - const no_fill = new Setting(containerEl) - .setName("No Fill (Transparent)") - .setDisabled(this.plugin.settings.format == "svg") - .addToggle((toggle) => { - toggle.setValue(this.plugin.settings.noFill) - .onChange( - async (value) => { - this.plugin.settings.noFill = value; - await this.plugin.saveSettings(); - fill_color.setDisabled(value) - } - ) - }); - - const fill_color = new Setting(containerEl) - .setName("Fill Color") - .setDisabled(this.plugin.settings.noFill || this.plugin.settings.format == "svg") - .addColorPicker((picker) => { - picker.setValue(this.plugin.settings.fill) - .onChange( - async (value) => { - this.plugin.settings.fill = value; - await this.plugin.saveSettings(); - } - ) - }) - - const pixel_per_pt = new Setting(containerEl) - .setName("Pixel Per Point") - .setDisabled(this.plugin.settings.format == "svg") - .addSlider((slider) => - slider.setValue(this.plugin.settings.pixel_per_pt) - .setLimits(1, 5, 1) - .onChange( - async (value) => { - this.plugin.settings.pixel_per_pt = value; - await this.plugin.saveSettings(); - } - ) - .setDynamicTooltip() - ) - - new Setting(containerEl) - .setName("Override Math Blocks") - .addToggle((toggle) => { - toggle.setValue(this.plugin.settings.override_math) - .onChange((value) => this.plugin.overrideMathJax(value)) - }); - - new Setting(containerEl) - .setName("Shared Preamble") - .addTextArea((c) => c.setValue(this.plugin.settings.preamable.shared).onChange(async (value) => { this.plugin.settings.preamable.shared = value; await this.plugin.saveSettings() })) - new Setting(containerEl) - .setName("Code Block Preamble") - .addTextArea((c) => c.setValue(this.plugin.settings.preamable.code).onChange(async (value) => { this.plugin.settings.preamable.code = value; await this.plugin.saveSettings() })) - new Setting(containerEl) - .setName("Math Block Preamble") - .addTextArea((c) => c.setValue(this.plugin.settings.preamable.math).onChange(async (value) => { this.plugin.settings.preamable.math = value; await this.plugin.saveSettings() })) - - //Font family settings - if (!Platform.isMobileApp) { - - const fontSettings = containerEl.createDiv({ cls: "setting-item font-settings" }) - fontSettings.createDiv({ text: "Fonts", cls: "setting-item-name" }) - fontSettings.createDiv({ text: "Font family names that should be loaded for Typst from your system. Requires a reload on change.", cls: "setting-item-description" }) - - const addFontsDiv = fontSettings.createDiv({ cls: "add-fonts-div" }) - const fontsInput = addFontsDiv.createEl('input', { type: "text", placeholder: "Enter a font family", cls: "font-input", }) - const addFontBtn = addFontsDiv.createEl('button', { text: "Add" }) - - const fontTagsDiv = fontSettings.createDiv({ cls: "font-tags-div" }) - - const addFontTag = async () => { - if (!this.plugin.settings.font_families.contains(fontsInput.value)) { - this.plugin.settings.font_families.push(fontsInput.value.toLowerCase()) - await this.plugin.saveSettings() - } - fontsInput.value = '' - this.renderFontTags(fontTagsDiv) - } - - fontsInput.addEventListener('keydown', async (ev) => { - if (ev.key == "Enter") { - addFontTag() - } - }) - addFontBtn.addEventListener('click', async () => addFontTag()) - - this.renderFontTags(fontTagsDiv) - - new Setting(containerEl) - .setName("Download Missing Packages") - .setDesc("When on, if the compiler cannot find a package in the system it will attempt to download it. Packages downloaded this way will be stored within the vault in the plugin's folder. Always on for mobile.") - .addToggle(toggle => toggle.setValue(this.plugin.settings.autoDownloadPackages).onChange(async (value) => { this.plugin.settings.autoDownloadPackages = value; await this.plugin.saveSettings() })) - } - - const packageSettingsDiv = containerEl.createDiv({ cls: "setting-item package-settings" }) - packageSettingsDiv.createDiv({ text: "Downloaded Packages", cls: "setting-item-name" }) - packageSettingsDiv.createDiv({ text: "These are the currently downloaded packages. Select the packages you want to delete.", cls: "setting-item-description" }); - - (await this.plugin.getPackageList()).forEach(pkg => { - const [namespace, name, version] = pkg.split("/") - //create package item - const packageItem = packageSettingsDiv.createDiv({ cls: "package-item" }) - packageItem.createEl('input', { type: "checkbox", cls: "package-checkbox", value: pkg, attr: { name: "package-checkbox" } }) - packageItem.createEl('p', { text: name }) - packageItem.createEl('p', { text: version, cls: "package-version" }) - }) - - const deletePackagesBtn = packageSettingsDiv.createEl('button', { text: 'Delete Selected Packages', cls: "delete-pkg-btn" }) - - deletePackagesBtn.addEventListener('click', () => { - const selectedPackageElements = packageSettingsDiv.querySelectorAll('input[name="package-checkbox"]:checked') - - const packagesToDelete: string[] = [] - - selectedPackageElements.forEach(pkgEl => { - packagesToDelete.push(pkgEl.getAttribute('value')!) - packageSettingsDiv.removeChild(pkgEl.parentNode!) - }) - - this.plugin.deletePackages(packagesToDelete) - }) - - } - - - renderFontTags(fontTagsDiv: HTMLDivElement) { - fontTagsDiv.innerHTML = '' - this.plugin.settings.font_families.forEach((fontFamily) => { - const fontTag = fontTagsDiv.createEl('span', { cls: "font-tag" }) - fontTag.createEl('span', { text: fontFamily, cls: "font-tag-text", attr: { style: `font-family: ${fontFamily};` } }) - const removeBtn = fontTag.createEl('span', { text: "x", cls: "tag-btn" }) - removeBtn.addEventListener('click', async () => { - this.plugin.settings.font_families.remove(fontFamily) - await this.plugin.saveSettings() - this.renderFontTags(fontTagsDiv) - }) - }) - } - + plugin: TypstPlugin; + + constructor(app: App, plugin: TypstPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + async display() { + const { containerEl } = this; + + containerEl.empty(); + + new Setting(containerEl) + .setName("Render Format") + .addDropdown((dropdown) => { + dropdown + .addOptions({ + svg: "SVG", + image: "Image", + }) + .setValue(this.plugin.settings.format) + .onChange(async (value) => { + this.plugin.settings.format = value; + await this.plugin.saveSettings(); + if (value == "svg") { + no_fill.setDisabled(true); + fill_color.setDisabled(true); + pixel_per_pt.setDisabled(true); + } else { + no_fill.setDisabled(false); + fill_color.setDisabled(this.plugin.settings.noFill); + pixel_per_pt.setDisabled(false); + } + }); + }); + + const no_fill = new Setting(containerEl) + .setName("No Fill (Transparent)") + .setDisabled(this.plugin.settings.format == "svg") + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.noFill) + .onChange(async (value) => { + this.plugin.settings.noFill = value; + await this.plugin.saveSettings(); + fill_color.setDisabled(value); + }); + }); + + const fill_color = new Setting(containerEl) + .setName("Fill Color") + .setDisabled( + this.plugin.settings.noFill || + this.plugin.settings.format == "svg" + ) + .addColorPicker((picker) => { + picker + .setValue(this.plugin.settings.fill) + .onChange(async (value) => { + this.plugin.settings.fill = value; + await this.plugin.saveSettings(); + }); + }); + + const pixel_per_pt = new Setting(containerEl) + .setName("Pixel Per Point") + .setDisabled(this.plugin.settings.format == "svg") + .addSlider((slider) => + slider + .setValue(this.plugin.settings.pixel_per_pt) + .setLimits(1, 5, 1) + .onChange(async (value) => { + this.plugin.settings.pixel_per_pt = value; + await this.plugin.saveSettings(); + }) + .setDynamicTooltip() + ); + + new Setting(containerEl) + .setName("Override Math Blocks") + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.override_math) + .onChange((value) => this.plugin.overrideMathJax(value)); + }); + + new Setting(containerEl).setName("Shared Preamble").addTextArea((c) => + c + .setValue(this.plugin.settings.preamable.shared) + .onChange(async (value) => { + this.plugin.settings.preamable.shared = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl) + .setName("Code Block Preamble") + .addTextArea((c) => + c + .setValue(this.plugin.settings.preamable.code) + .onChange(async (value) => { + this.plugin.settings.preamable.code = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl) + .setName("Math Block Preamble") + .addTextArea((c) => + c + .setValue(this.plugin.settings.preamable.math) + .onChange(async (value) => { + this.plugin.settings.preamable.math = value; + await this.plugin.saveSettings(); + }) + ); + + //Font family settings + if (!Platform.isMobileApp) { + const fontSettings = containerEl.createDiv({ + cls: "setting-item font-settings", + }); + fontSettings.createDiv({ text: "Fonts", cls: "setting-item-name" }); + fontSettings.createDiv({ + text: "Font family names that should be loaded for Typst from your system. Requires a reload on change.", + cls: "setting-item-description", + }); + + const addFontsDiv = fontSettings.createDiv({ + cls: "add-fonts-div", + }); + const fontsInput = addFontsDiv.createEl("input", { + type: "text", + placeholder: "Enter a font family", + cls: "font-input", + }); + const addFontBtn = addFontsDiv.createEl("button", { text: "Add" }); + + const fontTagsDiv = fontSettings.createDiv({ + cls: "font-tags-div", + }); + + const addFontTag = async () => { + if ( + !this.plugin.settings.font_families.contains( + fontsInput.value + ) + ) { + this.plugin.settings.font_families.push( + fontsInput.value.toLowerCase() + ); + await this.plugin.saveSettings(); + } + fontsInput.value = ""; + this.renderFontTags(fontTagsDiv); + }; + + fontsInput.addEventListener("keydown", async (ev) => { + if (ev.key == "Enter") { + addFontTag(); + } + }); + addFontBtn.addEventListener("click", async () => addFontTag()); + + this.renderFontTags(fontTagsDiv); + + new Setting(containerEl) + .setName("Download Missing Packages") + .setDesc( + "When on, if the compiler cannot find a package in the system it will attempt to download it. Packages downloaded this way will be stored within the vault in the plugin's folder. Always on for mobile." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.autoDownloadPackages) + .onChange(async (value) => { + this.plugin.settings.autoDownloadPackages = value; + await this.plugin.saveSettings(); + }) + ); + } + + const packageSettingsDiv = containerEl.createDiv({ + cls: "setting-item package-settings", + }); + packageSettingsDiv.createDiv({ + text: "Downloaded Packages", + cls: "setting-item-name", + }); + packageSettingsDiv.createDiv({ + text: "These are the currently downloaded packages. Select the packages you want to delete.", + cls: "setting-item-description", + }); + + (await this.plugin.getPackageList()).forEach((pkg) => { + const [namespace, name, version] = pkg.split("/"); + //create package item + const packageItem = packageSettingsDiv.createDiv({ + cls: "package-item", + }); + packageItem.createEl("input", { + type: "checkbox", + cls: "package-checkbox", + value: pkg, + attr: { name: "package-checkbox" }, + }); + packageItem.createEl("p", { text: name }); + packageItem.createEl("p", { + text: version, + cls: "package-version", + }); + }); + + const deletePackagesBtn = packageSettingsDiv.createEl("button", { + text: "Delete Selected Packages", + cls: "delete-pkg-btn", + }); + + deletePackagesBtn.addEventListener("click", () => { + const selectedPackageElements = packageSettingsDiv.querySelectorAll( + 'input[name="package-checkbox"]:checked' + ); + + const packagesToDelete: string[] = []; + + selectedPackageElements.forEach((pkgEl) => { + packagesToDelete.push(pkgEl.getAttribute("value")!); + packageSettingsDiv.removeChild(pkgEl.parentNode!); + }); + + this.plugin.deletePackages(packagesToDelete); + }); + } + + renderFontTags(fontTagsDiv: HTMLDivElement) { + fontTagsDiv.innerHTML = ""; + this.plugin.settings.font_families.forEach((fontFamily) => { + const fontTag = fontTagsDiv.createEl("span", { cls: "font-tag" }); + fontTag.createEl("span", { + text: fontFamily, + cls: "font-tag-text", + attr: { style: `font-family: ${fontFamily};` }, + }); + const removeBtn = fontTag.createEl("span", { + text: "x", + cls: "tag-btn", + }); + removeBtn.addEventListener("click", async () => { + this.plugin.settings.font_families.remove(fontFamily); + await this.plugin.saveSettings(); + this.renderFontTags(fontTagsDiv); + }); + }); + } } diff --git a/src/types.d.ts b/src/types.d.ts index b6c76f6..c90b636 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,25 +1,25 @@ export interface CompileImageCommand { - format: "image"; - source: string; - path: string; - pixel_per_pt: number; - fill: string; - size: number; - display: boolean; + format: "image"; + source: string; + path: string; + pixel_per_pt: number; + fill: string; + size: number; + display: boolean; } export interface CompileSvgCommand { - format: "svg"; - source: string; - path: string; + format: "svg"; + source: string; + path: string; } export interface WorkerRequest { - buffer: Int32Array, - path: string + buffer: Int32Array; + path: string; } export interface Message { - type: string, - data: any -} \ No newline at end of file + type: string; + data: any; +} diff --git a/src/typst-render-element.ts b/src/typst-render-element.ts index fd869b9..74a4907 100644 --- a/src/typst-render-element.ts +++ b/src/typst-render-element.ts @@ -1,121 +1,169 @@ export default class TypstRenderElement extends HTMLElement { - static compile: (path: string, source: string, size: number, display: boolean, fontSize: number) => Promise; - static nextId = 0; - static prevHeight = 0; - - // The Element's Id - id: string - // The number in the element's id. - num: string - - abortController: AbortController - format: string - source: string - path: string - display: boolean - resizeObserver: ResizeObserver - size: number - math: boolean - - canvas: HTMLCanvasElement - - async connectedCallback() { - if (!this.isConnected) { - console.warn("Typst Renderer: Canvas element has been called before connection"); - return; - } - - if (this.format == "image" && this.canvas == undefined) { - this.canvas = this.appendChild(createEl("canvas", { attr: { height: TypstRenderElement.prevHeight }, cls: "typst-doc" })) - } - - this.num = TypstRenderElement.nextId.toString() - TypstRenderElement.nextId += 1 - this.id = "TypstRenderElement-" + this.num - this.abortController = new AbortController() - - if (this.display) { - this.style.display = "block" - this.resizeObserver = new ResizeObserver((entries) => { - if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) { - this.draw() - } - }) - this.resizeObserver.observe(this) - } - await this.draw() - } - - disconnectedCallback() { - if (this.format == "image") { - TypstRenderElement.prevHeight = this.canvas.height - } - if (this.display && this.resizeObserver != undefined) { - this.resizeObserver.disconnect() - } - } - - async draw() { - this.abortController.abort() - this.abortController = new AbortController() - try { - await navigator.locks.request(this.id, { signal: this.abortController.signal }, async () => { - let fontSize = parseFloat(getComputedStyle(this).fontSize) - this.size = this.display ? this.clientWidth : parseFloat(getComputedStyle(this).lineHeight) - - // resizeObserver can trigger before the element gets disconnected which can cause the size to be 0 - // which causes a NaN. size can also sometimes be -ve so wait for resize to draw it again - if (!(this.size > 0)) { - return; - } - - try { - let result = await TypstRenderElement.compile(this.path, this.source, this.size, this.display, fontSize) - if (result instanceof ImageData && this.format == "image") { - this.drawToCanvas(result) - } else if (typeof result == "string" && this.format == "svg") { - this.innerHTML = result; - let child = (this.firstElementChild as SVGElement); - child.setAttribute("width", child.getAttribute("width")!.replace("pt", "")) - child.setAttribute("height", child.getAttribute("height")!.replace("pt", "")) - child.setAttribute("width", `${this.firstElementChild!.clientWidth /fontSize}em`); - child.setAttribute("height", `${this.firstElementChild!.clientHeight /fontSize}em`); - } - } catch (error) { - // For some reason it is uncaught so remove "Uncaught " - error = error.slice(9) - let pre = createEl("pre", { - attr: { - style: "white-space: pre;" - } - })//"
 
" - pre.textContent = error - this.outerHTML = pre.outerHTML - return - } - - - }) - - } catch (error) { - return - } - } - - drawToCanvas(image: ImageData) { - let ctx = this.canvas.getContext("2d")!; - // if (this.display) { - // this.style.width = "100%" - // this.style.height = "" - // } else { - // this.style.verticalAlign = "bottom" - // this.style.height = `${this.size}px` - // } - this.canvas.width = image.width - this.canvas.height = image.height - - ctx.imageSmoothingEnabled = true - ctx.imageSmoothingQuality = "high" - ctx.putImageData(image, 0, 0); - } + static compile: ( + path: string, + source: string, + size: number, + display: boolean, + fontSize: number + ) => Promise; + static nextId = 0; + static prevHeight = 0; + + // The Element's Id + id: string; + // The number in the element's id. + num: string; + + abortController: AbortController; + format: string; + source: string; + path: string; + display: boolean; + resizeObserver: ResizeObserver; + size: number; + math: boolean; + + canvas: HTMLCanvasElement; + + async connectedCallback() { + if (!this.isConnected) { + console.warn( + "Typst Renderer: Canvas element has been called before connection" + ); + return; + } + + if (this.format == "image" && this.canvas == undefined) { + this.canvas = this.appendChild( + createEl("canvas", { + attr: { height: TypstRenderElement.prevHeight }, + cls: "typst-doc", + }) + ); + } + + this.num = TypstRenderElement.nextId.toString(); + TypstRenderElement.nextId += 1; + this.id = "TypstRenderElement-" + this.num; + this.abortController = new AbortController(); + + if (this.display) { + this.style.display = "block"; + this.resizeObserver = new ResizeObserver((entries) => { + if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) { + this.draw(); + } + }); + this.resizeObserver.observe(this); + } + await this.draw(); + } + + disconnectedCallback() { + if (this.format == "image") { + TypstRenderElement.prevHeight = this.canvas.height; + } + if (this.display && this.resizeObserver != undefined) { + this.resizeObserver.disconnect(); + } + } + + async draw() { + this.abortController.abort(); + this.abortController = new AbortController(); + try { + await navigator.locks.request( + this.id, + { signal: this.abortController.signal }, + async () => { + const fontSize = parseFloat( + getComputedStyle(this).fontSize + ); + this.size = this.display + ? this.clientWidth + : parseFloat(getComputedStyle(this).lineHeight); + + // resizeObserver can trigger before the element gets disconnected which can cause the size to be 0 + // which causes a NaN. size can also sometimes be -ve so wait for resize to draw it again + if (!(this.size > 0)) { + return; + } + + try { + const result = await TypstRenderElement.compile( + this.path, + this.source, + this.size, + this.display, + fontSize + ); + if ( + result instanceof ImageData && + this.format == "image" + ) { + this.drawToCanvas(result); + } else if ( + typeof result == "string" && + this.format == "svg" + ) { + this.innerHTML = result; + const child = this.firstElementChild as SVGElement; + child.setAttribute( + "width", + child.getAttribute("width")!.replace("pt", "") + ); + child.setAttribute( + "height", + child.getAttribute("height")!.replace("pt", "") + ); + child.setAttribute( + "width", + `${ + this.firstElementChild!.clientWidth / + fontSize + }em` + ); + child.setAttribute( + "height", + `${ + this.firstElementChild!.clientHeight / + fontSize + }em` + ); + } + } catch (error) { + // For some reason it is uncaught so remove "Uncaught " + error = error.slice(9); + const pre = createEl("pre", { + attr: { + style: "white-space: pre;", + }, + }); //"
 
" + pre.textContent = error; + this.outerHTML = pre.outerHTML; + return; + } + } + ); + } catch (error) { + return; + } + } + + drawToCanvas(image: ImageData) { + const ctx = this.canvas.getContext("2d")!; + // if (this.display) { + // this.style.width = "100%" + // this.style.height = "" + // } else { + // this.style.verticalAlign = "bottom" + // this.style.height = `${this.size}px` + // } + this.canvas.width = image.width; + this.canvas.height = image.height; + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.putImageData(image, 0, 0); + } } From ecb32613d89bd71cd4ae68f8aa5698cee6fef5ff Mon Sep 17 00:00:00 2001 From: OverHash <46231745+OverHash@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:57:19 +1200 Subject: [PATCH 3/3] Reformat with 2 spaces --- .editorconfig | 5 +- .eslintrc | 39 +- manifest.json | 24 +- package.json | 68 +- src/compiler.worker.ts | 172 ++-- src/main.ts | 1597 +++++++++++++++++------------------ src/types.d.ts | 28 +- src/typst-render-element.ts | 294 +++---- tsconfig.json | 13 +- versions.json | 28 +- 10 files changed, 1098 insertions(+), 1170 deletions(-) diff --git a/.editorconfig b/.editorconfig index 84b8a66..d27d8fe 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,5 @@ root = true charset = utf-8 end_of_line = lf insert_final_newline = true -indent_style = tab -indent_size = 4 -tab_width = 4 +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc index 7fc8ba5..7f5ad29 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,21 +1,22 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "env": { "node": true }, - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "@typescript-eslint/ban-ts-comment": "off", - "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off" - } + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off", + "indent": ["warn", 2] + } } diff --git a/manifest.json b/manifest.json index ff397ed..28d7b34 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,13 @@ { - "id": "typst", - "name": "Typst Renderer", - "version": "0.10.0", - "minAppVersion": "1.0.0", - "description": "Renders `typst` code blocks and math blocks with Typst.", - "author": "fenjalien", - "authorUrl": "https://github.com/fenjalien", - "fundingUrl": { - "GitHub Sponsor": "https://github.com/sponsors/fenjalien", - "ko-fi": "https://ko-fi.com/fenjalien" - } -} \ No newline at end of file + "id": "typst", + "name": "Typst Renderer", + "version": "0.10.0", + "minAppVersion": "1.0.0", + "description": "Renders `typst` code blocks and math blocks with Typst.", + "author": "fenjalien", + "authorUrl": "https://github.com/fenjalien", + "fundingUrl": { + "GitHub Sponsor": "https://github.com/sponsors/fenjalien", + "ko-fi": "https://ko-fi.com/fenjalien" + } +} diff --git a/package.json b/package.json index b34f062..a2bf219 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,36 @@ { - "name": "obsidian-typst-plugin", - "version": "0.10.0", - "description": "Renders `typst` code blocks to images with Typst.", - "main": "main.js", - "scripts": { - "build-dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", - "wasm": "wasm-pack build compiler --target web --out-dir ../pkg", - "wasm-dev": "wasm-pack build compiler --target web --dev --out-dir ../pkg", - "wasm-build-dev": "npm run wasm-dev && npm run build-dev", - "wasm-build": "npm run wasm && npm run build", - "version": "node version-bump.mjs && git add manifest.json versions.json" - }, - "keywords": [], - "author": "fenjalien", - "license": "Apache-2.0", - "devDependencies": { - "@types/node": "^20", - "@typescript-eslint/eslint-plugin": "^5", - "@typescript-eslint/parser": "^5", - "builtin-modules": "^3", - "esbuild": "^0.18", - "esbuild-plugin-inline-worker": "^0.1.1", - "typescript": "^5.1", - "wasm-pack": "^0.13.0" - }, - "dependencies": { - "fflate": "^0.8.1", - "js-untar": "^2.0.0", - "obsidian": "latest", - "obsidian-typst": "file:pkg", - "svgo": "^3.0.2", - "tslib": "^2" - } + "name": "obsidian-typst-plugin", + "version": "0.10.0", + "description": "Renders `typst` code blocks to images with Typst.", + "main": "main.js", + "scripts": { + "build-dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "wasm": "wasm-pack build compiler --target web --out-dir ../pkg", + "wasm-dev": "wasm-pack build compiler --target web --dev --out-dir ../pkg", + "wasm-build-dev": "npm run wasm-dev && npm run build-dev", + "wasm-build": "npm run wasm && npm run build", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [], + "author": "fenjalien", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^20", + "@typescript-eslint/eslint-plugin": "^5", + "@typescript-eslint/parser": "^5", + "builtin-modules": "^3", + "esbuild": "^0.18", + "esbuild-plugin-inline-worker": "^0.1.1", + "typescript": "^5.1", + "wasm-pack": "^0.13.0" + }, + "dependencies": { + "fflate": "^0.8.1", + "js-untar": "^2.0.0", + "obsidian": "latest", + "obsidian-typst": "file:pkg", + "svgo": "^3.0.2", + "tslib": "^2" + } } diff --git a/src/compiler.worker.ts b/src/compiler.worker.ts index 6d580ae..88982c1 100644 --- a/src/compiler.worker.ts +++ b/src/compiler.worker.ts @@ -11,97 +11,97 @@ let packages: string[] = []; const xhr = new XMLHttpRequest(); function requestData(path: string): string { - try { - if (!canUseSharedArrayBuffer) { - if (path.startsWith("@")) { - if (packages.includes(path.slice(1))) { - return packagePath + path.slice(1); - } - throw 2; - } - path = "http://localhost/_capacitor_file_" + basePath + "/" + path; - xhr.open("GET", path, false); - try { - xhr.send(); - } catch (e) { - console.error(e); - throw 3; - } - if (xhr.status == 404) { - throw 2; - } - return xhr.responseText; - } - const buffer = new Int32Array( - // @ts-expect-error - new SharedArrayBuffer(4, { maxByteLength: 1e8 }) - ); - buffer[0] = 0; - postMessage({ buffer, path }); - const res = Atomics.wait(buffer, 0, 0); - if (buffer[0] == 0) { - return decoder.decode(Uint8Array.from(buffer.slice(1))); - } - throw buffer[0]; - } catch (e) { - if (typeof e != "number") { - console.error(e); - throw 1; - } - throw e; - } + try { + if (!canUseSharedArrayBuffer) { + if (path.startsWith("@")) { + if (packages.includes(path.slice(1))) { + return packagePath + path.slice(1); + } + throw 2; + } + path = "http://localhost/_capacitor_file_" + basePath + "/" + path; + xhr.open("GET", path, false); + try { + xhr.send(); + } catch (e) { + console.error(e); + throw 3; + } + if (xhr.status == 404) { + throw 2; + } + return xhr.responseText; + } + const buffer = new Int32Array( + // @ts-expect-error + new SharedArrayBuffer(4, { maxByteLength: 1e8 }) + ); + buffer[0] = 0; + postMessage({ buffer, path }); + const res = Atomics.wait(buffer, 0, 0); + if (buffer[0] == 0) { + return decoder.decode(Uint8Array.from(buffer.slice(1))); + } + throw buffer[0]; + } catch (e) { + if (typeof e != "number") { + console.error(e); + throw 1; + } + throw e; + } } let compiler: typst.SystemWorld; onmessage = (ev: MessageEvent) => { - const message = ev.data; - switch (message.type) { - case "canUseSharedArrayBuffer": - canUseSharedArrayBuffer = message.data; - break; - case "startup": - // we fetch and send the raw wasm binary here - // see issue #59 for why we cannot send a URL blob to where the wasm file is located - fetch(message.data.wasm) - .then((response) => response.arrayBuffer()) - .then((wasmBinary) => typstInit(wasmBinary)) - .then((_) => { - compiler = new typst.SystemWorld("", requestData); - console.log("Typst web assembly loaded!"); - }); - basePath = message.data.basePath; - packagePath = message.data.packagePath; - break; - case "fonts": - message.data.forEach((font: any) => - compiler.add_font(new Uint8Array(font)) - ); - break; - case "compile": - if (message.data.format == "image") { - const data: CompileImageCommand = message.data; - postMessage( - compiler.compile_image( - data.source, - data.path, - data.pixel_per_pt, - data.fill, - data.size, - data.display - ) - ); - } else if (message.data.format == "svg") { - const data: CompileSvgCommand = message.data; - postMessage(compiler.compile_svg(data.source, data.path)); - } - break; - case "packages": - packages = message.data; - break; - default: - throw message; - } + const message = ev.data; + switch (message.type) { + case "canUseSharedArrayBuffer": + canUseSharedArrayBuffer = message.data; + break; + case "startup": + // we fetch and send the raw wasm binary here + // see issue #59 for why we cannot send a URL blob to where the wasm file is located + fetch(message.data.wasm) + .then((response) => response.arrayBuffer()) + .then((wasmBinary) => typstInit(wasmBinary)) + .then((_) => { + compiler = new typst.SystemWorld("", requestData); + console.log("Typst web assembly loaded!"); + }); + basePath = message.data.basePath; + packagePath = message.data.packagePath; + break; + case "fonts": + message.data.forEach((font: any) => + compiler.add_font(new Uint8Array(font)) + ); + break; + case "compile": + if (message.data.format == "image") { + const data: CompileImageCommand = message.data; + postMessage( + compiler.compile_image( + data.source, + data.path, + data.pixel_per_pt, + data.fill, + data.size, + data.display + ) + ); + } else if (message.data.format == "svg") { + const data: CompileSvgCommand = message.data; + postMessage(compiler.compile_svg(data.source, data.path)); + } + break; + case "packages": + packages = message.data; + break; + default: + throw message; + } }; console.log("Typst compiler worker loaded!"); diff --git a/src/main.ts b/src/main.ts index 87f51d8..6bd0575 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,15 @@ import { - App, - renderMath, - HexString, - Platform, - Plugin, - PluginSettingTab, - Setting, - loadMathJax, - normalizePath, - Notice, - requestUrl, + App, + renderMath, + HexString, + Platform, + Plugin, + PluginSettingTab, + Setting, + loadMathJax, + normalizePath, + Notice, + requestUrl, } from "obsidian"; declare const PLUGIN_VERSION: string; @@ -25,826 +25,775 @@ import untar from "js-untar"; import { decompressSync } from "fflate"; interface TypstPluginSettings { - format: string; - noFill: boolean; - fill: HexString; - pixel_per_pt: number; - search_system: boolean; - override_math: boolean; - font_families: string[]; - preamable: { - shared: string; - math: string; - code: string; - }; - plugin_version: string; - autoDownloadPackages: boolean; + format: string; + noFill: boolean; + fill: HexString; + pixel_per_pt: number; + search_system: boolean; + override_math: boolean; + font_families: string[]; + preamable: { + shared: string; + math: string; + code: string; + }; + plugin_version: string; + autoDownloadPackages: boolean; } const DEFAULT_SETTINGS: TypstPluginSettings = { - format: "image", - noFill: true, - fill: "#ffffff", - pixel_per_pt: 3, - search_system: false, - override_math: false, - font_families: [], - preamable: { - shared: "#set text(fill: white, size: SIZE)\n#set page(width: WIDTH, height: HEIGHT)", - math: "#set page(margin: 0pt)\n#set align(horizon)", - code: "#set page(margin: (y: 1em, x: 0pt))", - }, - plugin_version: PLUGIN_VERSION, - autoDownloadPackages: true, + format: "image", + noFill: true, + fill: "#ffffff", + pixel_per_pt: 3, + search_system: false, + override_math: false, + font_families: [], + preamable: { + shared: + "#set text(fill: white, size: SIZE)\n#set page(width: WIDTH, height: HEIGHT)", + math: "#set page(margin: 0pt)\n#set align(horizon)", + code: "#set page(margin: (y: 1em, x: 0pt))", + }, + plugin_version: PLUGIN_VERSION, + autoDownloadPackages: true, }; export default class TypstPlugin extends Plugin { - settings: TypstPluginSettings; - - compilerWorker: Worker; - - tex2chtml: any; - - prevCanvasHeight = 0; - textEncoder: TextEncoder; - fs: any; - - wasmPath: string; - pluginPath: string; - packagePath: string; - - async onload() { - console.log("loading Typst Renderer"); - - this.textEncoder = new TextEncoder(); - await this.loadSettings(); - - this.pluginPath = this.app.vault.configDir + "/plugins/typst/"; - this.packagePath = this.pluginPath + "packages/"; - this.wasmPath = this.pluginPath + "obsidian_typst_bg.wasm"; - - this.compilerWorker = new CompilerWorker() as Worker; - if ( - !(await this.app.vault.adapter.exists(this.wasmPath)) || - this.settings.plugin_version != PLUGIN_VERSION - ) { - new Notice( - "Typst Renderer: Downloading required web assembly component!", - 5000 - ); - try { - await this.fetchWasm(); - new Notice( - "Typst Renderer: Web assembly component downloaded!", - 5000 - ); - } catch (error) { - new Notice( - "Typst Renderer: Failed to fetch component: " + error, - 0 - ); - console.error( - "Typst Renderer: Failed to fetch component: " + error - ); - } - } - this.compilerWorker.postMessage({ - type: "startup", - data: { - wasm: URL.createObjectURL( - new Blob( - [ - await this.app.vault.adapter.readBinary( - this.wasmPath - ), - ], - { type: "application/wasm" } - ) - ), - //@ts-ignore - basePath: this.app.vault.adapter.basePath, - packagePath: this.packagePath, - }, - }); - - if (Platform.isDesktopApp) { - this.compilerWorker.postMessage({ - type: "canUseSharedArrayBuffer", - data: true, - }); - this.fs = require("fs"); - const fonts = await Promise.all( - //@ts-expect-error - ((await window.queryLocalFonts()) as Array) - .filter((font: { family: string; name: string }) => - this.settings.font_families.contains( - font.family.toLowerCase() - ) - ) - .map( - async (font: { blob: () => Promise }) => - await (await font.blob()).arrayBuffer() - ) - ); - this.compilerWorker.postMessage( - { type: "fonts", data: fonts }, - fonts - ); - } else { - // Mobile - // Make sure it exists, won't error/affect anything if it does exist - await this.app.vault.adapter.mkdir(this.packagePath); - const packages = await this.getPackageList(); - this.compilerWorker.postMessage({ - type: "packages", - data: packages, - }); - } - - // Setup cutom canvas - TypstRenderElement.compile = (a, b, c, d, e) => - this.processThenCompileTypst(a, b, c, d, e); - if (customElements.get("typst-renderer") == undefined) { - customElements.define("typst-renderer", TypstRenderElement); - } - - // Setup MathJax - await loadMathJax(); - renderMath("", false); - // @ts-expect-error - this.tex2chtml = MathJax.tex2chtml; - this.overrideMathJax(this.settings.override_math); - - this.addCommand({ - id: "toggle-math-override", - name: "Toggle math block override", - callback: () => this.overrideMathJax(!this.settings.override_math), - }); - - // Settings - this.addSettingTab(new TypstSettingTab(this.app, this)); - - // Code blocks - this.registerMarkdownCodeBlockProcessor( - "typst", - async (source, el, ctx) => { - el.appendChild( - this.createTypstRenderElement( - "/" + ctx.sourcePath, - `${this.settings.preamable.code}\n${source}`, - true, - false - ) - ); - } - ); - - console.log("loaded Typst Renderer"); - } - - async fetchWasm() { - let response; - let data; - response = requestUrl( - `https://api.github.com/repos/fenjalien/obsidian-typst/releases/tags/${PLUGIN_VERSION}` - ); - data = await response.json; - const asset = data.assets.find( - (a: any) => a.name == "obsidian_typst_bg.wasm" - ); - if (asset == undefined) { - throw "Could not find the correct file!"; - } - - response = requestUrl({ - url: asset.url, - headers: { Accept: "application/octet-stream" }, - }); - data = await response.arrayBuffer; - await this.app.vault.adapter.writeBinary(this.wasmPath, data); - - this.settings.plugin_version = PLUGIN_VERSION; - await this.saveSettings(); - } - - async getPackageList(): Promise { - const getFolders = async (f: string) => - (await this.app.vault.adapter.list(f)).folders; - const packages = []; - // namespace - for (const namespace of await getFolders(this.packagePath)) { - // name - for (const name of await getFolders(namespace)) { - // version - for (const version of await getFolders(name)) { - packages.push(version.split("/").slice(-3).join("/")); - } - } - } - return packages; - } - - async deletePackages(packages: string[]) { - for (const folder of packages) { - await this.app.vault.adapter.rmdir(this.packagePath + folder, true); - } - } - - async compileToTypst( - path: string, - source: string, - size: number, - display: boolean - ): Promise { - return await navigator.locks.request( - "typst renderer compiler", - async (lock) => { - let message; - if (this.settings.format == "svg") { - message = { - type: "compile", - data: { - format: "svg", - path, - source, - }, - }; - } else if (this.settings.format == "image") { - message = { - type: "compile", - data: { - format: "image", - source, - path, - pixel_per_pt: this.settings.pixel_per_pt, - fill: `${this.settings.fill}${ - this.settings.noFill ? "00" : "ff" - }`, - size, - display, - }, - }; - } - this.compilerWorker.postMessage(message); - while (true) { - let result: ImageData | string | WorkerRequest; - try { - result = await new Promise((resolve, reject) => { - const listener = (ev: MessageEvent) => { - remove(); - resolve(ev.data); - }; - const errorListener = (error: ErrorEvent) => { - remove(); - reject(error.message); - }; - const remove = () => { - this.compilerWorker.removeEventListener( - "message", - listener - ); - this.compilerWorker.removeEventListener( - "error", - errorListener - ); - }; - this.compilerWorker.addEventListener( - "message", - listener - ); - this.compilerWorker.addEventListener( - "error", - errorListener - ); - }); - } catch (e) { - if ( - Platform.isMobileApp && - e.startsWith( - "Uncaught Error: package not found (searched for" - ) - ) { - const spec = e - .match(/"@preview\/.*?"/)[0] - .slice(2, -1) - .replace(":", "/"); - const [namespace, name, version] = spec.split("/"); - try { - await this.fetchPackage( - this.packagePath + spec + "/", - name, - version - ); - } catch (error) { - if (error == 2) { - throw e; - } - throw error; - } - const packages = await this.getPackageList(); - this.compilerWorker.postMessage({ - type: "packages", - data: packages, - }); - this.compilerWorker.postMessage(message); - continue; - } - throw e; - } - if ( - result instanceof ImageData || - typeof result == "string" - ) { - return result; - } - // Cannot reach this point when in mobile app as the worker should - // not have a SharedArrayBuffer - await this.handleWorkerRequest(result); - } - } - ); - } - - async handleWorkerRequest({ buffer: wbuffer, path }: WorkerRequest) { - try { - const text = await (path.startsWith("@") - ? this.preparePackage(path.slice(1)) - : this.getFileString(path)); - if (text) { - const buffer = Int32Array.from(this.textEncoder.encode(text)); - if (wbuffer.byteLength < buffer.byteLength + 4) { - //@ts-expect-error - wbuffer.buffer.grow(buffer.byteLength + 4); - } - wbuffer.set(buffer, 1); - wbuffer[0] = 0; - } - } catch (error) { - if (typeof error === "number") { - wbuffer[0] = error; - } else { - wbuffer[0] = 1; - console.error(error); - } - } finally { - Atomics.notify(wbuffer, 0); - } - } - - async getFileString(path: string): Promise { - try { - if (require("path").isAbsolute(path)) { - return await this.fs.promises.readFile(path, { - encoding: "utf8", - }); - } else { - return await this.app.vault.adapter.read(normalizePath(path)); - } - } catch (e) { - console.error(e); - if (e.code == "ENOENT") { - // File not found - throw 2; - } - if (e.code == "EACCES") { - // access denied - throw 3; - } - if (e.code == "EISDIR") { - // File is directory - throw 4; - } - // Other File error - throw 5; - } - } - - async preparePackage(spec: string): Promise { - if (Platform.isDesktopApp) { - const subdir = "/typst/packages/" + spec; - - let dir = require("path").normalize(this.getDataDir() + subdir); - if (this.fs.existsSync(dir)) { - return dir; - } - - dir = require("path").normalize(this.getCacheDir() + subdir); - - if (this.fs.existsSync(dir)) { - return dir; - } - } - - const folder = this.packagePath + spec + "/"; - if (await this.app.vault.adapter.exists(folder)) { - return folder; - } - if (spec.startsWith("preview") && this.settings.autoDownloadPackages) { - const [namespace, name, version] = spec.split("/"); - try { - await this.fetchPackage(folder, name, version); - return folder; - } catch (e) { - if (e == 2) { - throw e; - } - console.error(e); - // Other package error - throw 3; - } - } - // Package not found error - throw 2; - } - - getDataDir() { - if (Platform.isLinux) { - if ("XDG_DATA_HOME" in process.env) { - return process.env["XDG_DATA_HOME"]; - } else { - return process.env["HOME"] + "/.local/share"; - } - } else if (Platform.isWin) { - return process.env["APPDATA"]; - } else if (Platform.isMacOS) { - return process.env["HOME"] + "/Library/Application Support"; - } - throw "Cannot find data directory on an unknown platform"; - } - - getCacheDir() { - if (Platform.isLinux) { - if ("XDG_CACHE_HOME" in process.env) { - return process.env["XDG_DATA_HOME"]; - } else { - return process.env["HOME"] + "/.cache"; - } - } else if (Platform.isWin) { - return process.env["LOCALAPPDATA"]; - } else if (Platform.isMacOS) { - return process.env["HOME"] + "/Library/Caches"; - } - throw "Cannot find cache directory on an unknown platform"; - } - - async fetchPackage(folder: string, name: string, version: string) { - const url = `https://packages.typst.org/preview/${name}-${version}.tar.gz`; - const response = await fetch(url); - if (response.status == 404) { - // Package not found error - throw 2; - } - await this.app.vault.adapter.mkdir(folder); - await untar( - decompressSync(new Uint8Array(await response.arrayBuffer())).buffer - ).progress(async (file: any) => { - // is folder - if (file.type == "5" && file.name != ".") { - await this.app.vault.adapter.mkdir(folder + file.name); - } - // is file - if (file.type === "0") { - await this.app.vault.adapter.writeBinary( - folder + file.name, - file.buffer - ); - } - }); - } - - async processThenCompileTypst( - path: string, - source: string, - size: number, - display: boolean, - fontSize: number - ) { - const dpr = window.devicePixelRatio; - // * (72 / 96) - const pxToPt = (px: number) => px.toString() + "pt"; - const sizing = `#let (WIDTH, HEIGHT, SIZE, THEME) = (${ - display ? pxToPt(size) : "auto" - }, ${!display ? pxToPt(size) : "auto"}, ${pxToPt( - fontSize - )}, "${document.body.getCssPropertyValue("color-scheme")}")`; - return this.compileToTypst( - path, - `${sizing}\n${this.settings.preamable.shared}\n${source}`, - size, - display - ); - } - - createTypstRenderElement( - path: string, - source: string, - display: boolean, - math: boolean - ) { - const renderer = new TypstRenderElement(); - renderer.format = this.settings.format; - renderer.source = source; - renderer.path = path; - renderer.display = display; - renderer.math = math; - return renderer; - } - - createTypstMath(source: string, r: { display: boolean }) { - const display = r.display; - source = `${this.settings.preamable.math}\n${ - display ? `$ ${source} $` : `$${source}$` - }`; - - return this.createTypstRenderElement( - "/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", - source, - display, - true - ); - } - - onunload() { - // @ts-expect-error - MathJax.tex2chtml = this.tex2chtml; - this.compilerWorker.terminate(); - } - - async overrideMathJax(value: boolean) { - this.settings.override_math = value; - await this.saveSettings(); - if (this.settings.override_math) { - // @ts-expect-error - MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r); - } else { - // @ts-expect-error - MathJax.tex2chtml = this.tex2chtml; - } - } - - async loadSettings() { - this.settings = Object.assign( - {}, - DEFAULT_SETTINGS, - await this.loadData() - ); - } - - async saveSettings() { - await this.saveData(this.settings); - } + settings: TypstPluginSettings; + + compilerWorker: Worker; + + tex2chtml: any; + + prevCanvasHeight = 0; + textEncoder: TextEncoder; + fs: any; + + wasmPath: string; + pluginPath: string; + packagePath: string; + + async onload() { + console.log("loading Typst Renderer"); + + this.textEncoder = new TextEncoder(); + await this.loadSettings(); + + this.pluginPath = this.app.vault.configDir + "/plugins/typst/"; + this.packagePath = this.pluginPath + "packages/"; + this.wasmPath = this.pluginPath + "obsidian_typst_bg.wasm"; + + this.compilerWorker = new CompilerWorker() as Worker; + if ( + !(await this.app.vault.adapter.exists(this.wasmPath)) || + this.settings.plugin_version != PLUGIN_VERSION + ) { + new Notice( + "Typst Renderer: Downloading required web assembly component!", + 5000 + ); + try { + await this.fetchWasm(); + new Notice("Typst Renderer: Web assembly component downloaded!", 5000); + } catch (error) { + new Notice("Typst Renderer: Failed to fetch component: " + error, 0); + console.error("Typst Renderer: Failed to fetch component: " + error); + } + } + this.compilerWorker.postMessage({ + type: "startup", + data: { + wasm: URL.createObjectURL( + new Blob([await this.app.vault.adapter.readBinary(this.wasmPath)], { + type: "application/wasm", + }) + ), + //@ts-ignore + basePath: this.app.vault.adapter.basePath, + packagePath: this.packagePath, + }, + }); + + if (Platform.isDesktopApp) { + this.compilerWorker.postMessage({ + type: "canUseSharedArrayBuffer", + data: true, + }); + this.fs = require("fs"); + const fonts = await Promise.all( + //@ts-expect-error + ((await window.queryLocalFonts()) as Array) + .filter((font: { family: string; name: string }) => + this.settings.font_families.contains(font.family.toLowerCase()) + ) + .map( + async (font: { blob: () => Promise }) => + await (await font.blob()).arrayBuffer() + ) + ); + this.compilerWorker.postMessage({ type: "fonts", data: fonts }, fonts); + } else { + // Mobile + // Make sure it exists, won't error/affect anything if it does exist + await this.app.vault.adapter.mkdir(this.packagePath); + const packages = await this.getPackageList(); + this.compilerWorker.postMessage({ + type: "packages", + data: packages, + }); + } + + // Setup cutom canvas + TypstRenderElement.compile = (a, b, c, d, e) => + this.processThenCompileTypst(a, b, c, d, e); + if (customElements.get("typst-renderer") == undefined) { + customElements.define("typst-renderer", TypstRenderElement); + } + + // Setup MathJax + await loadMathJax(); + renderMath("", false); + // @ts-expect-error + this.tex2chtml = MathJax.tex2chtml; + this.overrideMathJax(this.settings.override_math); + + this.addCommand({ + id: "toggle-math-override", + name: "Toggle math block override", + callback: () => this.overrideMathJax(!this.settings.override_math), + }); + + // Settings + this.addSettingTab(new TypstSettingTab(this.app, this)); + + // Code blocks + this.registerMarkdownCodeBlockProcessor( + "typst", + async (source, el, ctx) => { + el.appendChild( + this.createTypstRenderElement( + "/" + ctx.sourcePath, + `${this.settings.preamable.code}\n${source}`, + true, + false + ) + ); + } + ); + + console.log("loaded Typst Renderer"); + } + + async fetchWasm() { + let response; + let data; + response = requestUrl( + `https://api.github.com/repos/fenjalien/obsidian-typst/releases/tags/${PLUGIN_VERSION}` + ); + data = await response.json; + const asset = data.assets.find( + (a: any) => a.name == "obsidian_typst_bg.wasm" + ); + if (asset == undefined) { + throw "Could not find the correct file!"; + } + + response = requestUrl({ + url: asset.url, + headers: { Accept: "application/octet-stream" }, + }); + data = await response.arrayBuffer; + await this.app.vault.adapter.writeBinary(this.wasmPath, data); + + this.settings.plugin_version = PLUGIN_VERSION; + await this.saveSettings(); + } + + async getPackageList(): Promise { + const getFolders = async (f: string) => + (await this.app.vault.adapter.list(f)).folders; + const packages = []; + // namespace + for (const namespace of await getFolders(this.packagePath)) { + // name + for (const name of await getFolders(namespace)) { + // version + for (const version of await getFolders(name)) { + packages.push(version.split("/").slice(-3).join("/")); + } + } + } + return packages; + } + + async deletePackages(packages: string[]) { + for (const folder of packages) { + await this.app.vault.adapter.rmdir(this.packagePath + folder, true); + } + } + + async compileToTypst( + path: string, + source: string, + size: number, + display: boolean + ): Promise { + return await navigator.locks.request( + "typst renderer compiler", + async (lock) => { + let message; + if (this.settings.format == "svg") { + message = { + type: "compile", + data: { + format: "svg", + path, + source, + }, + }; + } else if (this.settings.format == "image") { + message = { + type: "compile", + data: { + format: "image", + source, + path, + pixel_per_pt: this.settings.pixel_per_pt, + fill: `${this.settings.fill}${ + this.settings.noFill ? "00" : "ff" + }`, + size, + display, + }, + }; + } + this.compilerWorker.postMessage(message); + while (true) { + let result: ImageData | string | WorkerRequest; + try { + result = await new Promise((resolve, reject) => { + const listener = (ev: MessageEvent) => { + remove(); + resolve(ev.data); + }; + const errorListener = (error: ErrorEvent) => { + remove(); + reject(error.message); + }; + const remove = () => { + this.compilerWorker.removeEventListener("message", listener); + this.compilerWorker.removeEventListener("error", errorListener); + }; + this.compilerWorker.addEventListener("message", listener); + this.compilerWorker.addEventListener("error", errorListener); + }); + } catch (e) { + if ( + Platform.isMobileApp && + e.startsWith("Uncaught Error: package not found (searched for") + ) { + const spec = e + .match(/"@preview\/.*?"/)[0] + .slice(2, -1) + .replace(":", "/"); + const [namespace, name, version] = spec.split("/"); + try { + await this.fetchPackage( + this.packagePath + spec + "/", + name, + version + ); + } catch (error) { + if (error == 2) { + throw e; + } + throw error; + } + const packages = await this.getPackageList(); + this.compilerWorker.postMessage({ + type: "packages", + data: packages, + }); + this.compilerWorker.postMessage(message); + continue; + } + throw e; + } + if (result instanceof ImageData || typeof result == "string") { + return result; + } + // Cannot reach this point when in mobile app as the worker should + // not have a SharedArrayBuffer + await this.handleWorkerRequest(result); + } + } + ); + } + + async handleWorkerRequest({ buffer: wbuffer, path }: WorkerRequest) { + try { + const text = await (path.startsWith("@") + ? this.preparePackage(path.slice(1)) + : this.getFileString(path)); + if (text) { + const buffer = Int32Array.from(this.textEncoder.encode(text)); + if (wbuffer.byteLength < buffer.byteLength + 4) { + //@ts-expect-error + wbuffer.buffer.grow(buffer.byteLength + 4); + } + wbuffer.set(buffer, 1); + wbuffer[0] = 0; + } + } catch (error) { + if (typeof error === "number") { + wbuffer[0] = error; + } else { + wbuffer[0] = 1; + console.error(error); + } + } finally { + Atomics.notify(wbuffer, 0); + } + } + + async getFileString(path: string): Promise { + try { + if (require("path").isAbsolute(path)) { + return await this.fs.promises.readFile(path, { + encoding: "utf8", + }); + } else { + return await this.app.vault.adapter.read(normalizePath(path)); + } + } catch (e) { + console.error(e); + if (e.code == "ENOENT") { + // File not found + throw 2; + } + if (e.code == "EACCES") { + // access denied + throw 3; + } + if (e.code == "EISDIR") { + // File is directory + throw 4; + } + // Other File error + throw 5; + } + } + + async preparePackage(spec: string): Promise { + if (Platform.isDesktopApp) { + const subdir = "/typst/packages/" + spec; + + let dir = require("path").normalize(this.getDataDir() + subdir); + if (this.fs.existsSync(dir)) { + return dir; + } + + dir = require("path").normalize(this.getCacheDir() + subdir); + + if (this.fs.existsSync(dir)) { + return dir; + } + } + + const folder = this.packagePath + spec + "/"; + if (await this.app.vault.adapter.exists(folder)) { + return folder; + } + if (spec.startsWith("preview") && this.settings.autoDownloadPackages) { + const [namespace, name, version] = spec.split("/"); + try { + await this.fetchPackage(folder, name, version); + return folder; + } catch (e) { + if (e == 2) { + throw e; + } + console.error(e); + // Other package error + throw 3; + } + } + // Package not found error + throw 2; + } + + getDataDir() { + if (Platform.isLinux) { + if ("XDG_DATA_HOME" in process.env) { + return process.env["XDG_DATA_HOME"]; + } else { + return process.env["HOME"] + "/.local/share"; + } + } else if (Platform.isWin) { + return process.env["APPDATA"]; + } else if (Platform.isMacOS) { + return process.env["HOME"] + "/Library/Application Support"; + } + throw "Cannot find data directory on an unknown platform"; + } + + getCacheDir() { + if (Platform.isLinux) { + if ("XDG_CACHE_HOME" in process.env) { + return process.env["XDG_DATA_HOME"]; + } else { + return process.env["HOME"] + "/.cache"; + } + } else if (Platform.isWin) { + return process.env["LOCALAPPDATA"]; + } else if (Platform.isMacOS) { + return process.env["HOME"] + "/Library/Caches"; + } + throw "Cannot find cache directory on an unknown platform"; + } + + async fetchPackage(folder: string, name: string, version: string) { + const url = `https://packages.typst.org/preview/${name}-${version}.tar.gz`; + const response = await fetch(url); + if (response.status == 404) { + // Package not found error + throw 2; + } + await this.app.vault.adapter.mkdir(folder); + await untar( + decompressSync(new Uint8Array(await response.arrayBuffer())).buffer + ).progress(async (file: any) => { + // is folder + if (file.type == "5" && file.name != ".") { + await this.app.vault.adapter.mkdir(folder + file.name); + } + // is file + if (file.type === "0") { + await this.app.vault.adapter.writeBinary( + folder + file.name, + file.buffer + ); + } + }); + } + + async processThenCompileTypst( + path: string, + source: string, + size: number, + display: boolean, + fontSize: number + ) { + const dpr = window.devicePixelRatio; + // * (72 / 96) + const pxToPt = (px: number) => px.toString() + "pt"; + const sizing = `#let (WIDTH, HEIGHT, SIZE, THEME) = (${ + display ? pxToPt(size) : "auto" + }, ${!display ? pxToPt(size) : "auto"}, ${pxToPt( + fontSize + )}, "${document.body.getCssPropertyValue("color-scheme")}")`; + return this.compileToTypst( + path, + `${sizing}\n${this.settings.preamable.shared}\n${source}`, + size, + display + ); + } + + createTypstRenderElement( + path: string, + source: string, + display: boolean, + math: boolean + ) { + const renderer = new TypstRenderElement(); + renderer.format = this.settings.format; + renderer.source = source; + renderer.path = path; + renderer.display = display; + renderer.math = math; + return renderer; + } + + createTypstMath(source: string, r: { display: boolean }) { + const display = r.display; + source = `${this.settings.preamable.math}\n${ + display ? `$ ${source} $` : `$${source}$` + }`; + + return this.createTypstRenderElement( + "/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", + source, + display, + true + ); + } + + onunload() { + // @ts-expect-error + MathJax.tex2chtml = this.tex2chtml; + this.compilerWorker.terminate(); + } + + async overrideMathJax(value: boolean) { + this.settings.override_math = value; + await this.saveSettings(); + if (this.settings.override_math) { + // @ts-expect-error + MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r); + } else { + // @ts-expect-error + MathJax.tex2chtml = this.tex2chtml; + } + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } } class TypstSettingTab extends PluginSettingTab { - plugin: TypstPlugin; - - constructor(app: App, plugin: TypstPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - async display() { - const { containerEl } = this; - - containerEl.empty(); - - new Setting(containerEl) - .setName("Render Format") - .addDropdown((dropdown) => { - dropdown - .addOptions({ - svg: "SVG", - image: "Image", - }) - .setValue(this.plugin.settings.format) - .onChange(async (value) => { - this.plugin.settings.format = value; - await this.plugin.saveSettings(); - if (value == "svg") { - no_fill.setDisabled(true); - fill_color.setDisabled(true); - pixel_per_pt.setDisabled(true); - } else { - no_fill.setDisabled(false); - fill_color.setDisabled(this.plugin.settings.noFill); - pixel_per_pt.setDisabled(false); - } - }); - }); - - const no_fill = new Setting(containerEl) - .setName("No Fill (Transparent)") - .setDisabled(this.plugin.settings.format == "svg") - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.noFill) - .onChange(async (value) => { - this.plugin.settings.noFill = value; - await this.plugin.saveSettings(); - fill_color.setDisabled(value); - }); - }); - - const fill_color = new Setting(containerEl) - .setName("Fill Color") - .setDisabled( - this.plugin.settings.noFill || - this.plugin.settings.format == "svg" - ) - .addColorPicker((picker) => { - picker - .setValue(this.plugin.settings.fill) - .onChange(async (value) => { - this.plugin.settings.fill = value; - await this.plugin.saveSettings(); - }); - }); - - const pixel_per_pt = new Setting(containerEl) - .setName("Pixel Per Point") - .setDisabled(this.plugin.settings.format == "svg") - .addSlider((slider) => - slider - .setValue(this.plugin.settings.pixel_per_pt) - .setLimits(1, 5, 1) - .onChange(async (value) => { - this.plugin.settings.pixel_per_pt = value; - await this.plugin.saveSettings(); - }) - .setDynamicTooltip() - ); - - new Setting(containerEl) - .setName("Override Math Blocks") - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.override_math) - .onChange((value) => this.plugin.overrideMathJax(value)); - }); - - new Setting(containerEl).setName("Shared Preamble").addTextArea((c) => - c - .setValue(this.plugin.settings.preamable.shared) - .onChange(async (value) => { - this.plugin.settings.preamable.shared = value; - await this.plugin.saveSettings(); - }) - ); - new Setting(containerEl) - .setName("Code Block Preamble") - .addTextArea((c) => - c - .setValue(this.plugin.settings.preamable.code) - .onChange(async (value) => { - this.plugin.settings.preamable.code = value; - await this.plugin.saveSettings(); - }) - ); - new Setting(containerEl) - .setName("Math Block Preamble") - .addTextArea((c) => - c - .setValue(this.plugin.settings.preamable.math) - .onChange(async (value) => { - this.plugin.settings.preamable.math = value; - await this.plugin.saveSettings(); - }) - ); - - //Font family settings - if (!Platform.isMobileApp) { - const fontSettings = containerEl.createDiv({ - cls: "setting-item font-settings", - }); - fontSettings.createDiv({ text: "Fonts", cls: "setting-item-name" }); - fontSettings.createDiv({ - text: "Font family names that should be loaded for Typst from your system. Requires a reload on change.", - cls: "setting-item-description", - }); - - const addFontsDiv = fontSettings.createDiv({ - cls: "add-fonts-div", - }); - const fontsInput = addFontsDiv.createEl("input", { - type: "text", - placeholder: "Enter a font family", - cls: "font-input", - }); - const addFontBtn = addFontsDiv.createEl("button", { text: "Add" }); - - const fontTagsDiv = fontSettings.createDiv({ - cls: "font-tags-div", - }); - - const addFontTag = async () => { - if ( - !this.plugin.settings.font_families.contains( - fontsInput.value - ) - ) { - this.plugin.settings.font_families.push( - fontsInput.value.toLowerCase() - ); - await this.plugin.saveSettings(); - } - fontsInput.value = ""; - this.renderFontTags(fontTagsDiv); - }; - - fontsInput.addEventListener("keydown", async (ev) => { - if (ev.key == "Enter") { - addFontTag(); - } - }); - addFontBtn.addEventListener("click", async () => addFontTag()); - - this.renderFontTags(fontTagsDiv); - - new Setting(containerEl) - .setName("Download Missing Packages") - .setDesc( - "When on, if the compiler cannot find a package in the system it will attempt to download it. Packages downloaded this way will be stored within the vault in the plugin's folder. Always on for mobile." - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.autoDownloadPackages) - .onChange(async (value) => { - this.plugin.settings.autoDownloadPackages = value; - await this.plugin.saveSettings(); - }) - ); - } - - const packageSettingsDiv = containerEl.createDiv({ - cls: "setting-item package-settings", - }); - packageSettingsDiv.createDiv({ - text: "Downloaded Packages", - cls: "setting-item-name", - }); - packageSettingsDiv.createDiv({ - text: "These are the currently downloaded packages. Select the packages you want to delete.", - cls: "setting-item-description", - }); - - (await this.plugin.getPackageList()).forEach((pkg) => { - const [namespace, name, version] = pkg.split("/"); - //create package item - const packageItem = packageSettingsDiv.createDiv({ - cls: "package-item", - }); - packageItem.createEl("input", { - type: "checkbox", - cls: "package-checkbox", - value: pkg, - attr: { name: "package-checkbox" }, - }); - packageItem.createEl("p", { text: name }); - packageItem.createEl("p", { - text: version, - cls: "package-version", - }); - }); - - const deletePackagesBtn = packageSettingsDiv.createEl("button", { - text: "Delete Selected Packages", - cls: "delete-pkg-btn", - }); - - deletePackagesBtn.addEventListener("click", () => { - const selectedPackageElements = packageSettingsDiv.querySelectorAll( - 'input[name="package-checkbox"]:checked' - ); - - const packagesToDelete: string[] = []; - - selectedPackageElements.forEach((pkgEl) => { - packagesToDelete.push(pkgEl.getAttribute("value")!); - packageSettingsDiv.removeChild(pkgEl.parentNode!); - }); - - this.plugin.deletePackages(packagesToDelete); - }); - } - - renderFontTags(fontTagsDiv: HTMLDivElement) { - fontTagsDiv.innerHTML = ""; - this.plugin.settings.font_families.forEach((fontFamily) => { - const fontTag = fontTagsDiv.createEl("span", { cls: "font-tag" }); - fontTag.createEl("span", { - text: fontFamily, - cls: "font-tag-text", - attr: { style: `font-family: ${fontFamily};` }, - }); - const removeBtn = fontTag.createEl("span", { - text: "x", - cls: "tag-btn", - }); - removeBtn.addEventListener("click", async () => { - this.plugin.settings.font_families.remove(fontFamily); - await this.plugin.saveSettings(); - this.renderFontTags(fontTagsDiv); - }); - }); - } + plugin: TypstPlugin; + + constructor(app: App, plugin: TypstPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + async display() { + const { containerEl } = this; + + containerEl.empty(); + + new Setting(containerEl) + .setName("Render Format") + .addDropdown((dropdown) => { + dropdown + .addOptions({ + svg: "SVG", + image: "Image", + }) + .setValue(this.plugin.settings.format) + .onChange(async (value) => { + this.plugin.settings.format = value; + await this.plugin.saveSettings(); + if (value == "svg") { + no_fill.setDisabled(true); + fill_color.setDisabled(true); + pixel_per_pt.setDisabled(true); + } else { + no_fill.setDisabled(false); + fill_color.setDisabled(this.plugin.settings.noFill); + pixel_per_pt.setDisabled(false); + } + }); + }); + + const no_fill = new Setting(containerEl) + .setName("No Fill (Transparent)") + .setDisabled(this.plugin.settings.format == "svg") + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.noFill).onChange(async (value) => { + this.plugin.settings.noFill = value; + await this.plugin.saveSettings(); + fill_color.setDisabled(value); + }); + }); + + const fill_color = new Setting(containerEl) + .setName("Fill Color") + .setDisabled( + this.plugin.settings.noFill || this.plugin.settings.format == "svg" + ) + .addColorPicker((picker) => { + picker.setValue(this.plugin.settings.fill).onChange(async (value) => { + this.plugin.settings.fill = value; + await this.plugin.saveSettings(); + }); + }); + + const pixel_per_pt = new Setting(containerEl) + .setName("Pixel Per Point") + .setDisabled(this.plugin.settings.format == "svg") + .addSlider((slider) => + slider + .setValue(this.plugin.settings.pixel_per_pt) + .setLimits(1, 5, 1) + .onChange(async (value) => { + this.plugin.settings.pixel_per_pt = value; + await this.plugin.saveSettings(); + }) + .setDynamicTooltip() + ); + + new Setting(containerEl) + .setName("Override Math Blocks") + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.override_math) + .onChange((value) => this.plugin.overrideMathJax(value)); + }); + + new Setting(containerEl).setName("Shared Preamble").addTextArea((c) => + c + .setValue(this.plugin.settings.preamable.shared) + .onChange(async (value) => { + this.plugin.settings.preamable.shared = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl).setName("Code Block Preamble").addTextArea((c) => + c + .setValue(this.plugin.settings.preamable.code) + .onChange(async (value) => { + this.plugin.settings.preamable.code = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl).setName("Math Block Preamble").addTextArea((c) => + c + .setValue(this.plugin.settings.preamable.math) + .onChange(async (value) => { + this.plugin.settings.preamable.math = value; + await this.plugin.saveSettings(); + }) + ); + + //Font family settings + if (!Platform.isMobileApp) { + const fontSettings = containerEl.createDiv({ + cls: "setting-item font-settings", + }); + fontSettings.createDiv({ text: "Fonts", cls: "setting-item-name" }); + fontSettings.createDiv({ + text: "Font family names that should be loaded for Typst from your system. Requires a reload on change.", + cls: "setting-item-description", + }); + + const addFontsDiv = fontSettings.createDiv({ + cls: "add-fonts-div", + }); + const fontsInput = addFontsDiv.createEl("input", { + type: "text", + placeholder: "Enter a font family", + cls: "font-input", + }); + const addFontBtn = addFontsDiv.createEl("button", { text: "Add" }); + + const fontTagsDiv = fontSettings.createDiv({ + cls: "font-tags-div", + }); + + const addFontTag = async () => { + if (!this.plugin.settings.font_families.contains(fontsInput.value)) { + this.plugin.settings.font_families.push( + fontsInput.value.toLowerCase() + ); + await this.plugin.saveSettings(); + } + fontsInput.value = ""; + this.renderFontTags(fontTagsDiv); + }; + + fontsInput.addEventListener("keydown", async (ev) => { + if (ev.key == "Enter") { + addFontTag(); + } + }); + addFontBtn.addEventListener("click", async () => addFontTag()); + + this.renderFontTags(fontTagsDiv); + + new Setting(containerEl) + .setName("Download Missing Packages") + .setDesc( + "When on, if the compiler cannot find a package in the system it will attempt to download it. Packages downloaded this way will be stored within the vault in the plugin's folder. Always on for mobile." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.autoDownloadPackages) + .onChange(async (value) => { + this.plugin.settings.autoDownloadPackages = value; + await this.plugin.saveSettings(); + }) + ); + } + + const packageSettingsDiv = containerEl.createDiv({ + cls: "setting-item package-settings", + }); + packageSettingsDiv.createDiv({ + text: "Downloaded Packages", + cls: "setting-item-name", + }); + packageSettingsDiv.createDiv({ + text: "These are the currently downloaded packages. Select the packages you want to delete.", + cls: "setting-item-description", + }); + + (await this.plugin.getPackageList()).forEach((pkg) => { + const [namespace, name, version] = pkg.split("/"); + //create package item + const packageItem = packageSettingsDiv.createDiv({ + cls: "package-item", + }); + packageItem.createEl("input", { + type: "checkbox", + cls: "package-checkbox", + value: pkg, + attr: { name: "package-checkbox" }, + }); + packageItem.createEl("p", { text: name }); + packageItem.createEl("p", { + text: version, + cls: "package-version", + }); + }); + + const deletePackagesBtn = packageSettingsDiv.createEl("button", { + text: "Delete Selected Packages", + cls: "delete-pkg-btn", + }); + + deletePackagesBtn.addEventListener("click", () => { + const selectedPackageElements = packageSettingsDiv.querySelectorAll( + 'input[name="package-checkbox"]:checked' + ); + + const packagesToDelete: string[] = []; + + selectedPackageElements.forEach((pkgEl) => { + packagesToDelete.push(pkgEl.getAttribute("value")!); + packageSettingsDiv.removeChild(pkgEl.parentNode!); + }); + + this.plugin.deletePackages(packagesToDelete); + }); + } + + renderFontTags(fontTagsDiv: HTMLDivElement) { + fontTagsDiv.innerHTML = ""; + this.plugin.settings.font_families.forEach((fontFamily) => { + const fontTag = fontTagsDiv.createEl("span", { cls: "font-tag" }); + fontTag.createEl("span", { + text: fontFamily, + cls: "font-tag-text", + attr: { style: `font-family: ${fontFamily};` }, + }); + const removeBtn = fontTag.createEl("span", { + text: "x", + cls: "tag-btn", + }); + removeBtn.addEventListener("click", async () => { + this.plugin.settings.font_families.remove(fontFamily); + await this.plugin.saveSettings(); + this.renderFontTags(fontTagsDiv); + }); + }); + } } diff --git a/src/types.d.ts b/src/types.d.ts index c90b636..b1e3fd4 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,25 +1,25 @@ export interface CompileImageCommand { - format: "image"; - source: string; - path: string; - pixel_per_pt: number; - fill: string; - size: number; - display: boolean; + format: "image"; + source: string; + path: string; + pixel_per_pt: number; + fill: string; + size: number; + display: boolean; } export interface CompileSvgCommand { - format: "svg"; - source: string; - path: string; + format: "svg"; + source: string; + path: string; } export interface WorkerRequest { - buffer: Int32Array; - path: string; + buffer: Int32Array; + path: string; } export interface Message { - type: string; - data: any; + type: string; + data: any; } diff --git a/src/typst-render-element.ts b/src/typst-render-element.ts index 74a4907..b7c2ea1 100644 --- a/src/typst-render-element.ts +++ b/src/typst-render-element.ts @@ -1,169 +1,155 @@ export default class TypstRenderElement extends HTMLElement { - static compile: ( - path: string, - source: string, - size: number, - display: boolean, - fontSize: number - ) => Promise; - static nextId = 0; - static prevHeight = 0; + static compile: ( + path: string, + source: string, + size: number, + display: boolean, + fontSize: number + ) => Promise; + static nextId = 0; + static prevHeight = 0; - // The Element's Id - id: string; - // The number in the element's id. - num: string; + // The Element's Id + id: string; + // The number in the element's id. + num: string; - abortController: AbortController; - format: string; - source: string; - path: string; - display: boolean; - resizeObserver: ResizeObserver; - size: number; - math: boolean; + abortController: AbortController; + format: string; + source: string; + path: string; + display: boolean; + resizeObserver: ResizeObserver; + size: number; + math: boolean; - canvas: HTMLCanvasElement; + canvas: HTMLCanvasElement; - async connectedCallback() { - if (!this.isConnected) { - console.warn( - "Typst Renderer: Canvas element has been called before connection" - ); - return; - } + async connectedCallback() { + if (!this.isConnected) { + console.warn( + "Typst Renderer: Canvas element has been called before connection" + ); + return; + } - if (this.format == "image" && this.canvas == undefined) { - this.canvas = this.appendChild( - createEl("canvas", { - attr: { height: TypstRenderElement.prevHeight }, - cls: "typst-doc", - }) - ); - } + if (this.format == "image" && this.canvas == undefined) { + this.canvas = this.appendChild( + createEl("canvas", { + attr: { height: TypstRenderElement.prevHeight }, + cls: "typst-doc", + }) + ); + } - this.num = TypstRenderElement.nextId.toString(); - TypstRenderElement.nextId += 1; - this.id = "TypstRenderElement-" + this.num; - this.abortController = new AbortController(); + this.num = TypstRenderElement.nextId.toString(); + TypstRenderElement.nextId += 1; + this.id = "TypstRenderElement-" + this.num; + this.abortController = new AbortController(); - if (this.display) { - this.style.display = "block"; - this.resizeObserver = new ResizeObserver((entries) => { - if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) { - this.draw(); - } - }); - this.resizeObserver.observe(this); - } - await this.draw(); - } + if (this.display) { + this.style.display = "block"; + this.resizeObserver = new ResizeObserver((entries) => { + if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) { + this.draw(); + } + }); + this.resizeObserver.observe(this); + } + await this.draw(); + } - disconnectedCallback() { - if (this.format == "image") { - TypstRenderElement.prevHeight = this.canvas.height; - } - if (this.display && this.resizeObserver != undefined) { - this.resizeObserver.disconnect(); - } - } + disconnectedCallback() { + if (this.format == "image") { + TypstRenderElement.prevHeight = this.canvas.height; + } + if (this.display && this.resizeObserver != undefined) { + this.resizeObserver.disconnect(); + } + } - async draw() { - this.abortController.abort(); - this.abortController = new AbortController(); - try { - await navigator.locks.request( - this.id, - { signal: this.abortController.signal }, - async () => { - const fontSize = parseFloat( - getComputedStyle(this).fontSize - ); - this.size = this.display - ? this.clientWidth - : parseFloat(getComputedStyle(this).lineHeight); + async draw() { + this.abortController.abort(); + this.abortController = new AbortController(); + try { + await navigator.locks.request( + this.id, + { signal: this.abortController.signal }, + async () => { + const fontSize = parseFloat(getComputedStyle(this).fontSize); + this.size = this.display + ? this.clientWidth + : parseFloat(getComputedStyle(this).lineHeight); - // resizeObserver can trigger before the element gets disconnected which can cause the size to be 0 - // which causes a NaN. size can also sometimes be -ve so wait for resize to draw it again - if (!(this.size > 0)) { - return; - } + // resizeObserver can trigger before the element gets disconnected which can cause the size to be 0 + // which causes a NaN. size can also sometimes be -ve so wait for resize to draw it again + if (!(this.size > 0)) { + return; + } - try { - const result = await TypstRenderElement.compile( - this.path, - this.source, - this.size, - this.display, - fontSize - ); - if ( - result instanceof ImageData && - this.format == "image" - ) { - this.drawToCanvas(result); - } else if ( - typeof result == "string" && - this.format == "svg" - ) { - this.innerHTML = result; - const child = this.firstElementChild as SVGElement; - child.setAttribute( - "width", - child.getAttribute("width")!.replace("pt", "") - ); - child.setAttribute( - "height", - child.getAttribute("height")!.replace("pt", "") - ); - child.setAttribute( - "width", - `${ - this.firstElementChild!.clientWidth / - fontSize - }em` - ); - child.setAttribute( - "height", - `${ - this.firstElementChild!.clientHeight / - fontSize - }em` - ); - } - } catch (error) { - // For some reason it is uncaught so remove "Uncaught " - error = error.slice(9); - const pre = createEl("pre", { - attr: { - style: "white-space: pre;", - }, - }); //"
 
" - pre.textContent = error; - this.outerHTML = pre.outerHTML; - return; - } - } - ); - } catch (error) { - return; - } - } + try { + const result = await TypstRenderElement.compile( + this.path, + this.source, + this.size, + this.display, + fontSize + ); + if (result instanceof ImageData && this.format == "image") { + this.drawToCanvas(result); + } else if (typeof result == "string" && this.format == "svg") { + this.innerHTML = result; + const child = this.firstElementChild as SVGElement; + child.setAttribute( + "width", + child.getAttribute("width")!.replace("pt", "") + ); + child.setAttribute( + "height", + child.getAttribute("height")!.replace("pt", "") + ); + child.setAttribute( + "width", + `${this.firstElementChild!.clientWidth / fontSize}em` + ); + child.setAttribute( + "height", + `${this.firstElementChild!.clientHeight / fontSize}em` + ); + } + } catch (error) { + // For some reason it is uncaught so remove "Uncaught " + error = error.slice(9); + const pre = createEl("pre", { + attr: { + style: "white-space: pre;", + }, + }); //"
 
" + pre.textContent = error; + this.outerHTML = pre.outerHTML; + return; + } + } + ); + } catch (error) { + return; + } + } - drawToCanvas(image: ImageData) { - const ctx = this.canvas.getContext("2d")!; - // if (this.display) { - // this.style.width = "100%" - // this.style.height = "" - // } else { - // this.style.verticalAlign = "bottom" - // this.style.height = `${this.size}px` - // } - this.canvas.width = image.width; - this.canvas.height = image.height; + drawToCanvas(image: ImageData) { + const ctx = this.canvas.getContext("2d")!; + // if (this.display) { + // this.style.width = "100%" + // this.style.height = "" + // } else { + // this.style.verticalAlign = "bottom" + // this.style.height = `${this.size}px` + // } + this.canvas.width = image.width; + this.canvas.height = image.height; - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = "high"; - ctx.putImageData(image, 0, 0); - } + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.putImageData(image, 0, 0); + } } diff --git a/tsconfig.json b/tsconfig.json index 12a65f1..a4108b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,14 +11,7 @@ "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, - "lib": [ - "DOM", - "ES5", - "ES6", - "ES7" - ] + "lib": ["DOM", "ES5", "ES6", "ES7"] }, - "include": [ - "**/*.ts" - ] -} \ No newline at end of file + "include": ["**/*.ts"] +} diff --git a/versions.json b/versions.json index 2aad819..83c8023 100644 --- a/versions.json +++ b/versions.json @@ -1,15 +1,15 @@ { - "0.10.0": "1.0.0", - "0.9.0": "1.0.0", - "0.7.1": "1.0.0", - "0.7.0": "1.0.0", - "0.6.0": "1.0.0", - "0.5.1": "1.0.0", - "0.5.0": "1.0.0", - "0.4.2": "1.0.0", - "0.4.1": "1.0.0", - "0.4.0": "1.0.0", - "0.3.0": "1.0.0", - "0.2.0": "1.0.0", - "0.1.0": "1.0.0" -} \ No newline at end of file + "0.10.0": "1.0.0", + "0.9.0": "1.0.0", + "0.7.1": "1.0.0", + "0.7.0": "1.0.0", + "0.6.0": "1.0.0", + "0.5.1": "1.0.0", + "0.5.0": "1.0.0", + "0.4.2": "1.0.0", + "0.4.1": "1.0.0", + "0.4.0": "1.0.0", + "0.3.0": "1.0.0", + "0.2.0": "1.0.0", + "0.1.0": "1.0.0" +}