clone

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

main
diagramzip / renderers / edge / src / adapters / goat.ts
goat.ts 3.6K
1import wasmModule from '../../artifacts/goat/goat.wasm'2import { RenderError } from '../runtime/errors'3import type { RendererAdapter } from '../runtime/types'4import type { GoatWasmExports } from '../../artifacts/goat/goat-wasm.d'5import { edgeResult } from './types'67const VERSION = 'goat@0.5.1/edge-wasm-1'8const MAX_SOURCE_BYTES = 256 * 10249const MAX_COLOR_BYTES = 25610const COLOR = /^(?:#[0-9a-f]{3,8}|(?:rgb|rgba)\(\s*(?:\d{1,3}%?\s*,\s*){2}\d{1,3}%?(?:\s*,\s*(?:0|1|0?\.\d+|100%))?\s*\)|(?:hsl|hsla)\(\s*\d{1,3}(?:\.\d+)?\s*(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%(?:\s*,\s*(?:0|1|0?\.\d+|100%))?\s*\)|[a-z]+|currentColor|transparent)$/i11const instance = new WebAssembly.Instance(wasmModule, {})12const wasm = instance.exports as unknown as GoatWasmExports13wasm._initialize()14let renderTail: Promise<void> = Promise.resolve()1516function withRenderLock<T>(operation: () => T): Promise<T> {17 const previous = renderTail18 let release!: () => void19 renderTail = new Promise(resolve => { release = resolve })20 return previous.then(() => {21 try { return operation() } finally { release() }22 })23}2425function colorOption(options: Record<string, string>, name: string, fallback: string): string {26 const value = options[name] ?? fallback27 if (!COLOR.test(value) || new TextEncoder().encode(value).byteLength > MAX_COLOR_BYTES) {28 throw new RenderError(400, 'invalid_options', `GoAT option '${name}' must be a safe CSS color.`)29 }30 return value31}3233function writeString(value: string): [number, number] {34 const bytes = new TextEncoder().encode(value)35 const pointer = wasm.alloc(bytes.byteLength)36 if (!pointer) throw new RenderError(500, 'renderer_unavailable', 'GoAT could not allocate render memory.')37 new Uint8Array(wasm.memory.buffer).set(bytes, pointer)38 return [pointer, bytes.byteLength]39}4041export const goatAdapter: RendererAdapter = {42 id: 'goat', runtime: 'edge-wasm', version: VERSION,43 async render(request, signal) {44 if (signal.aborted) throw signal.reason45 if (new TextEncoder().encode(request.source).byteLength > MAX_SOURCE_BYTES) throw new RenderError(413, 'source_too_large', 'GoAT source exceeds the 256 KiB limit.')46 for (const name of Object.keys(request.options)) if (!['svg-color-light-scheme', 'svg-color-dark-scheme', 'utf8'].includes(name)) throw new RenderError(400, 'unsupported_options', `Unsupported GoAT option: ${name}.`)47 const light = colorOption(request.options, 'svg-color-light-scheme', '#000')48 const dark = colorOption(request.options, 'svg-color-dark-scheme', '#FFF')49 const utf8 = Object.hasOwn(request.options, 'utf8')50 try {51 const body = await withRenderLock(() => {52 if (signal.aborted) throw signal.reason53 wasm.beginRender()54 const [source, sourceLength] = writeString(request.source)55 const [lightPointer, lightLength] = writeString(light)56 const [darkPointer, darkLength] = writeString(dark)57 const output = wasm.render(source, sourceLength, utf8 ? 1 : 0, lightPointer, lightLength, darkPointer, darkLength)58 const outputLength = wasm.outputLen()59 if (!output || outputLength === 0) throw new RenderError(422, 'render_failed', 'GoAT returned no SVG.')60 return new TextDecoder().decode(new Uint8Array(wasm.memory.buffer, output, outputLength))61 })62 return edgeResult('goat', VERSION, body, 'edge-wasm')63 } catch (error) {64 if (error instanceof RenderError) throw error65 const message = error instanceof Error ? error.message : String(error)66 throw new RenderError(422, 'render_failed', message.slice(0, 500) || 'GoAT could not render this source.')67 }68 },69}