clone

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

main
diagramzip / renderers / edge / src / adapters / svgbob.ts
svgbob.ts 3.5K
1import { RenderError } from '../runtime/errors'2import type { RendererAdapter } from '../runtime/types'3import { edgeResult } from './types'4import { render as renderSvgbob } from '../../artifacts/svgbob/svgbob_wasm.js'56const VERSION = 'svgbob@0.7.6/edge-wasm-1'7const MAX_SOURCE_BYTES = 256 * 10248const MAX_OPTION_BYTES = 2569const DEFAULTS = {10 background: 'white',11 fillColor: 'black',12 fontFamily: 'Iosevka Fixed, monospace',13 fontSize: 14,14 scale: 1,15 strokeWidth: 2,16} as const1718const STRING_OPTIONS = new Set(['background', 'fill-color', 'font-family'])19const OPTION_STRIP = /[-"$/@*&=;:!\\]/g2021function optionString(options: Record<string, string>, name: string, fallback: string): string {22 const value = options[name]23 if (value === undefined) return fallback24 const sanitized = value.replace(OPTION_STRIP, '')25 if (sanitized.length === 0) throw new RenderError(400, 'invalid_options', `Svgbob option '${name}' cannot be empty.`)26 if (new TextEncoder().encode(sanitized).byteLength > MAX_OPTION_BYTES) throw new RenderError(413, 'options_too_large', `Svgbob option '${name}' exceeds the 256 byte limit.`)27 return sanitized28}2930function optionNumber(options: Record<string, string>, name: string, fallback: number, integer = false): number {31 const value = options[name]32 if (value === undefined) return fallback33 const parsed = Number(value)34 if (!Number.isFinite(parsed) || (integer && !Number.isInteger(parsed)) || parsed <= 0 || parsed > 1000) {35 throw new RenderError(400, 'invalid_options', `Svgbob option '${name}' must be a finite positive number.`)36 }37 return parsed38}3940function optionsFor(options: Record<string, string>): {41 background: string42 fillColor: string43 fontFamily: string44 fontSize: number45 scale: number46 strokeWidth: number47} {48 for (const name of Object.keys(options)) {49 if (!STRING_OPTIONS.has(name) && !['font-size', 'scale', 'stroke-width'].includes(name)) {50 throw new RenderError(400, 'unsupported_options', `Unsupported Svgbob option: ${name}.`)51 }52 }53 return {54 background: optionString(options, 'background', DEFAULTS.background),55 fillColor: optionString(options, 'fill-color', DEFAULTS.fillColor),56 fontFamily: optionString(options, 'font-family', DEFAULTS.fontFamily),57 fontSize: optionNumber(options, 'font-size', DEFAULTS.fontSize, true),58 scale: optionNumber(options, 'scale', DEFAULTS.scale),59 strokeWidth: optionNumber(options, 'stroke-width', DEFAULTS.strokeWidth),60 }61}6263export const svgbobAdapter: RendererAdapter = {64 id: 'svgbob', runtime: 'edge-wasm', version: VERSION,65 async render(request, signal) {66 if (signal.aborted) throw signal.reason67 if (new TextEncoder().encode(request.source).byteLength > MAX_SOURCE_BYTES) {68 throw new RenderError(413, 'source_too_large', 'Svgbob source exceeds the 256 KiB limit.')69 }70 const settings = optionsFor(request.options)71 if (signal.aborted) throw signal.reason72 try {73 const body = renderSvgbob(74 request.source,75 settings.background,76 settings.fillColor,77 settings.fontFamily,78 settings.fontSize,79 settings.scale,80 settings.strokeWidth,81 )82 if (!body.includes('<svg')) throw new RenderError(422, 'render_failed', 'Svgbob returned no SVG.')83 return edgeResult('svgbob', VERSION, body, 'edge-wasm')84 } catch (error) {85 if (error instanceof RenderError) throw error86 const message = error instanceof Error ? error.message : String(error)87 throw new RenderError(422, 'render_failed', message.slice(0, 500) || 'Svgbob could not render this source.')88 }89 },90}