clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / edge / src / adapters / tikz.ts
tikz.ts 3.5K
1import { tikzCore } from '../../artifacts/tikz/tikz-core.js'2import texWasm from '../../artifacts/tikz/tex.wasm'3import { RenderError } from '../runtime/errors'4import type { RendererAdapter } from '../runtime/types'56const VERSION = '@planktimerr/tikzjax@1.0.63-edge-core-1'7const MAX_SOURCE = 256 * 10248const MAX_OUTPUT = 4 * 1024 * 10249export type TikzAssetFetcher = (assetPath: string) => Promise<Response>1011// TikZJax's TeX virtual filesystem and Wasm instance are process-global. Keep12// requests strictly serial, including first-load initialization.13let renderTail: Promise<void> = Promise.resolve()1415function normalizedSource(source: string): { body: string; preamble: string } {16 const begin = source.search(/\\begin\s*\{document\}/)17 if (begin < 0) return { body: source, preamble: '' }18 const match = source.slice(begin).match(/^\\begin\s*\{document\}/)19 if (!match) return { body: source, preamble: '' }20 const endMatch = source.match(/\\end\s*\{document\}\s*$/)21 const end = endMatch?.index ?? source.length22 const preamble = source.slice(0, begin).replace(/^\s*\\documentclass(?:\[[^\]]*\])?\s*\{[^}]*\}\s*/m, '')23 return { body: source.slice(begin + match[0].length, end), preamble }24}2526async function loadCore(fetchAsset: TikzAssetFetcher): Promise<void> {27 // The extracted core releases its decompressed 68.75 MiB format dump after28 // copying it into the request's fresh Wasm memory, so load it per request.29 ;(globalThis as typeof globalThis & { __tikzWasm?: WebAssembly.Module }).__tikzWasm = texWasm30 await tikzCore.load(fetchAsset)31}3233async function locked<T>(operation: () => Promise<T>): Promise<T> {34 const previous = renderTail35 let release!: () => void36 renderTail = new Promise(resolve => { release = resolve })37 await previous38 try { return await operation() } finally { release() }39}4041export function createTikzAdapter(fetchAsset: TikzAssetFetcher): RendererAdapter {42 return {43 id: 'tikz', runtime: 'edge-wasm', version: VERSION,44 async render(request, signal) {45 if (signal.aborted) throw signal.reason46 if (new TextEncoder().encode(request.source).byteLength > MAX_SOURCE) {47 throw new RenderError(413, 'source_too_large', 'TikZ source may not exceed 256 KiB.')48 }49 const { body, preamble } = normalizedSource(request.source)50 if (new TextEncoder().encode(preamble).byteLength > MAX_SOURCE) {51 throw new RenderError(413, 'source_too_large', 'TikZ preamble may not exceed 256 KiB.')52 }53 try {54 const svg = await locked(async () => {55 if (signal.aborted) throw signal.reason56 await loadCore(fetchAsset)57 const options = { ...request.options, ...(preamble ? { addToPreamble: preamble } : {}) }58 const value = await tikzCore.texify(body, options)59 if (typeof value !== 'string' || !value.includes('<svg')) throw new Error('TikZ did not produce an SVG.')60 if (new TextEncoder().encode(value).byteLength > MAX_OUTPUT) throw new RenderError(413, 'output_too_large', 'TikZ SVG output exceeds 4 MiB.')61 return value62 })63 if (signal.aborted) throw signal.reason64 return { body: svg, contentType: 'image/svg+xml', engineVersion: VERSION, runtime: 'edge-wasm' }65 } catch (error) {66 if (error instanceof RenderError) throw error67 const message = error instanceof Error ? error.message : String(error)68 throw new RenderError(422, 'render_failed', message.slice(0, 500) || 'TikZ could not render the source.')69 }70 },71 }72}