From 6fefddc8ee3f05e83006c87a8ac5e9daa0a03424 Mon Sep 17 00:00:00 2001 From: Splines Date: Fri, 20 Mar 2026 19:10:00 +0100 Subject: [PATCH 01/17] Implement package registry access and cached font middleware --- web/src/cached-font-middleware.ts | 39 +++++++++++++++++++++++++++++ web/src/typst.ts | 41 +++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 web/src/cached-font-middleware.ts diff --git a/web/src/cached-font-middleware.ts b/web/src/cached-font-middleware.ts new file mode 100644 index 0000000..6541262 --- /dev/null +++ b/web/src/cached-font-middleware.ts @@ -0,0 +1,39 @@ +import { preloadFontAssets } from "@myriaddreamin/typst.ts/dist/esm/options.init.mjs"; + +const FONT_CACHE_NAME = "typst-font-assets-v1"; + +const cachedFetch: typeof fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const request = input instanceof Request ? input : new Request(input, init); + + if (!("caches" in globalThis) || request.method.toUpperCase() !== "GET") { + return fetch(request); + } + + const cache = await caches.open(FONT_CACHE_NAME); + const cached = await cache.match(request); + if (cached) { + return cached; + } + + const response = await fetch(request); + if (response.ok) { + try { + await cache.put(request, response.clone()); + } catch { + // Ignore cache write failures and keep network response. + } + } + + return response; +}; + +export function cachedFontInitOptions() { + return { + beforeBuild: [ + preloadFontAssets({ + assets: ["text", "cjk", "emoji"], + fetcher: cachedFetch, + }), + ], + }; +} diff --git a/web/src/typst.ts b/web/src/typst.ts index a7132fa..d3a4c62 100644 --- a/web/src/typst.ts +++ b/web/src/typst.ts @@ -7,7 +7,15 @@ import type * as typstWeb from "@myriaddreamin/typst.ts"; import { createTypstCompiler, createTypstRenderer } from "@myriaddreamin/typst.ts"; -import { disableDefaultFontAssets, loadFonts } from "@myriaddreamin/typst.ts/dist/esm/options.init.mjs"; +import { + // disableDefaultFontAssets, + loadFonts, + withPackageRegistry, + withAccessModel, +} from "@myriaddreamin/typst.ts/dist/esm/options.init.mjs"; +import { NodeFetchPackageRegistry } from "@myriaddreamin/typst.ts/dist/esm/fs/package.node.mjs"; +import { MemoryAccessModel } from "@myriaddreamin/typst.ts/dist/esm/fs/memory.mjs"; +import { cachedFontInitOptions } from "./cached-font-middleware"; // @ts-expect-error ?url import import mathFontUrl from "/math-font.ttf?url"; @@ -20,6 +28,28 @@ import typstRendererWasm from "@myriaddreamin/typst-ts-renderer/pkg/typst_ts_ren let compiler: typstWeb.TypstCompiler; let renderer: typstWeb.TypstRenderer; +interface RegistryResponse { + statusCode: number; + getBody: (_encoding?: unknown) => Uint8Array; +} + +function registryRequest(method: string, url: string): RegistryResponse { + const request = new XMLHttpRequest(); + request.open(method, url, false); + // Sync XHR from a document cannot use non-text responseType. + request.overrideMimeType("text/plain; charset=x-user-defined"); + request.send(); + + const response = request.response as unknown; + const responseText = typeof response === "string" ? response : ""; + const body = Uint8Array.from(responseText, char => char.charCodeAt(0) & 0xff); + + return { + statusCode: request.status, + getBody: () => body, + }; +} + /** * Initializes both the Typst compiler and renderer. */ @@ -32,16 +62,23 @@ export async function initTypst() { * Initializes the Typst compiler. * * See also https://myriad-dreamin.github.io/typst.ts/cookery/guide/all-in-one.html#label-Initializing%20using%20the%20low-level%20API + * And https://github.com/Myriad-Dreamin/typst.ts/blob/2a8b32d8cca70cc4d105fef074d2f35fc7546450/templates/compiler-wasm-cjs/src/main.package.cts#L20-L39 */ async function initCompiler() { compiler = createTypstCompiler(); + const accessModel = new MemoryAccessModel(); await compiler.init({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return getModule: () => typstCompilerWasm, beforeBuild: [ - disableDefaultFontAssets(), + ...cachedFontInitOptions().beforeBuild, + // disableDefaultFontAssets(), // eslint-disable-next-line @typescript-eslint/no-unsafe-argument loadFonts([mathFontUrl]), + withAccessModel(accessModel), + withPackageRegistry( + new NodeFetchPackageRegistry(accessModel, registryRequest), + ), ], }); console.log("Typst compiler initialized"); From ae3b13bacb37f4d210a5dc333e106e5ddb62b28a Mon Sep 17 00:00:00 2001 From: Splines Date: Fri, 20 Mar 2026 19:11:04 +0100 Subject: [PATCH 02/17] Move font cache file --- .../{cached-font-middleware.ts => registry/font-cache.ts} | 0 web/src/typst.ts | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename web/src/{cached-font-middleware.ts => registry/font-cache.ts} (100%) diff --git a/web/src/cached-font-middleware.ts b/web/src/registry/font-cache.ts similarity index 100% rename from web/src/cached-font-middleware.ts rename to web/src/registry/font-cache.ts diff --git a/web/src/typst.ts b/web/src/typst.ts index d3a4c62..41e3702 100644 --- a/web/src/typst.ts +++ b/web/src/typst.ts @@ -8,14 +8,14 @@ import type * as typstWeb from "@myriaddreamin/typst.ts"; import { createTypstCompiler, createTypstRenderer } from "@myriaddreamin/typst.ts"; import { - // disableDefaultFontAssets, + disableDefaultFontAssets, loadFonts, withPackageRegistry, withAccessModel, } from "@myriaddreamin/typst.ts/dist/esm/options.init.mjs"; import { NodeFetchPackageRegistry } from "@myriaddreamin/typst.ts/dist/esm/fs/package.node.mjs"; import { MemoryAccessModel } from "@myriaddreamin/typst.ts/dist/esm/fs/memory.mjs"; -import { cachedFontInitOptions } from "./cached-font-middleware"; +import { cachedFontInitOptions } from "./registry/font-cache"; // @ts-expect-error ?url import import mathFontUrl from "/math-font.ttf?url"; @@ -71,10 +71,10 @@ async function initCompiler() { // eslint-disable-next-line @typescript-eslint/no-unsafe-return getModule: () => typstCompilerWasm, beforeBuild: [ - ...cachedFontInitOptions().beforeBuild, - // disableDefaultFontAssets(), + disableDefaultFontAssets(), // eslint-disable-next-line @typescript-eslint/no-unsafe-argument loadFonts([mathFontUrl]), + ...cachedFontInitOptions().beforeBuild, withAccessModel(accessModel), withPackageRegistry( new NodeFetchPackageRegistry(accessModel, registryRequest), From e8302d5df886dd6518a182f7b75ee0d0cbe0c6d7 Mon Sep 17 00:00:00 2001 From: Splines Date: Fri, 20 Mar 2026 19:11:35 +0100 Subject: [PATCH 03/17] Extract registry request to new file --- web/src/registry/registry.ts | 21 +++++++++++++++++++++ web/src/typst.ts | 23 +---------------------- 2 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 web/src/registry/registry.ts diff --git a/web/src/registry/registry.ts b/web/src/registry/registry.ts new file mode 100644 index 0000000..1d65a96 --- /dev/null +++ b/web/src/registry/registry.ts @@ -0,0 +1,21 @@ +interface RegistryResponse { + statusCode: number; + getBody: (_encoding?: unknown) => Uint8Array; +} + +export function registryRequest(method: string, url: string): RegistryResponse { + const request = new XMLHttpRequest(); + request.open(method, url, false); + // Sync XHR from a document cannot use non-text responseType. + request.overrideMimeType("text/plain; charset=x-user-defined"); + request.send(); + + const response = request.response as unknown; + const responseText = typeof response === "string" ? response : ""; + const body = Uint8Array.from(responseText, char => char.charCodeAt(0) & 0xff); + + return { + statusCode: request.status, + getBody: () => body, + }; +} diff --git a/web/src/typst.ts b/web/src/typst.ts index 41e3702..039db81 100644 --- a/web/src/typst.ts +++ b/web/src/typst.ts @@ -24,32 +24,11 @@ import mathFontUrl from "/math-font.ttf?url"; import typstCompilerWasm from "@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm?url"; // @ts-expect-error WASM module import import typstRendererWasm from "@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm?url"; +import { registryRequest } from "./registry/registry"; let compiler: typstWeb.TypstCompiler; let renderer: typstWeb.TypstRenderer; -interface RegistryResponse { - statusCode: number; - getBody: (_encoding?: unknown) => Uint8Array; -} - -function registryRequest(method: string, url: string): RegistryResponse { - const request = new XMLHttpRequest(); - request.open(method, url, false); - // Sync XHR from a document cannot use non-text responseType. - request.overrideMimeType("text/plain; charset=x-user-defined"); - request.send(); - - const response = request.response as unknown; - const responseText = typeof response === "string" ? response : ""; - const body = Uint8Array.from(responseText, char => char.charCodeAt(0) & 0xff); - - return { - statusCode: request.status, - getBody: () => body, - }; -} - /** * Initializes both the Typst compiler and renderer. */ From 15e7f947a12481d6313f785d434ed3af25c68bad Mon Sep 17 00:00:00 2001 From: Splines Date: Fri, 20 Mar 2026 19:12:20 +0100 Subject: [PATCH 04/17] Add docstring --- web/src/registry/registry.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/web/src/registry/registry.ts b/web/src/registry/registry.ts index 1d65a96..67bc545 100644 --- a/web/src/registry/registry.ts +++ b/web/src/registry/registry.ts @@ -3,10 +3,18 @@ interface RegistryResponse { getBody: (_encoding?: unknown) => Uint8Array; } +/** + * Performs a synchronous HTTP request to the given URL and returns the response as a Uint8Array. + * + * Note: This function uses XMLHttpRequest in synchronous mode, which is generally + * discouraged in web development due to potential UI blocking. However, it is + * used here to meet the requirements of the Typst package registry interface, + * which expects a synchronous response. + */ export function registryRequest(method: string, url: string): RegistryResponse { const request = new XMLHttpRequest(); request.open(method, url, false); - // Sync XHR from a document cannot use non-text responseType. + // Sync XHR from a document cannot use non-text responseType request.overrideMimeType("text/plain; charset=x-user-defined"); request.send(); From 08b1721a23f381610610e8f19f6a321bf76f65fa Mon Sep 17 00:00:00 2001 From: Splines Date: Fri, 20 Mar 2026 19:13:28 +0100 Subject: [PATCH 05/17] Increase max height of preview pane --- web/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/constants.ts b/web/src/constants.ts index 9a06c3d..4dcbe2a 100644 --- a/web/src/constants.ts +++ b/web/src/constants.ts @@ -78,7 +78,7 @@ export const THEMES = { * Preview configuration. */ export const PREVIEW_CONFIG = { - MAX_HEIGHT: "150px", + MAX_HEIGHT: "320px", DARK_MODE_FILL: "#ffffff", LIGHT_MODE_FILL: "#000000", } as const; From 954de19692cfc472e7c89f09b2a2256e9dca0710 Mon Sep 17 00:00:00 2001 From: Splines Date: Fri, 20 Mar 2026 19:32:01 +0100 Subject: [PATCH 06/17] Add preview Typst fill color toggle --- web/powerpoint.html | 6 ++++++ web/src/constants.ts | 2 ++ web/src/preview.ts | 23 +++++++++++++++++++++-- web/src/ui.ts | 21 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/web/powerpoint.html b/web/powerpoint.html index 506cfb1..9dcc734 100644 --- a/web/powerpoint.html +++ b/web/powerpoint.html @@ -25,6 +25,12 @@ +
+ +