clone

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

main
diagramzip / renderers / edge / src / adapters / ditaa.ts
ditaa.ts 4.2K
1import { RenderError } from '../runtime/errors'2import type { RendererAdapter } from '../runtime/types'3import { render as renderSvgbob } from '../../artifacts/svgbob/svgbob_wasm.js'4import { edgeResult } from './types'56// Ditaa's maintained distribution is a native/Java application. This adapter7// intentionally implements the portable part of the format: ASCII geometry,8// labels, and the common connector characters. The resulting source is fed9// to the already edge-native svgbob renderer so there is no second runtime.10const VERSION = 'ditaa-ascii-via-svgbob@0.7.6/edge-wasm-1'11const MAX_SOURCE_BYTES = 256 * 102412const MAX_OPTION_BYTES = 25613const COLOR = /^(?:#[0-9a-f]{3,8}|[a-z]+|transparent)$/i14const DITAA_TAG = /\{([a-z][a-z0-9_-]{0,15})\}/gi15const SHAPE_TAGS = new Set(['d', 's', 'io', 'tr', 'o', 'r', 'db', 'mo', 'cloud', 'document', 'storage', 'input', 'output'])16const OPTION_NAMES = new Set([17 'scale', 'tabs', 'background', 'transparent',18 'no-antialias', 'no-separation', 'round-corners', 'no-shadows',19])2021export const DITAA_KNOWN_LOSSES = [22 'Ditaa shape tags ({d}, {s}, {io}, and related tags) are removed but their shapes are not translated.',23 'Ditaa color tags ({cXXX}) are removed; edge output uses the svgbob monochrome style.',24 'Ditaa shadows, rounded corners, anti-aliasing, and edge separation are not available in the svgbob pipeline.',25] as const2627function optionNumber(options: Record<string, string>, name: string, fallback: number, maximum: number, integer = false): number {28 const value = options[name]29 if (value === undefined) return fallback30 const parsed = Number(value)31 if (!Number.isFinite(parsed) || parsed <= 0 || parsed > maximum || (integer && !Number.isInteger(parsed))) {32 throw new RenderError(400, 'invalid_options', `Ditaa option '${name}' must be a finite positive number.`)33 }34 return parsed35}3637function optionColor(options: Record<string, string>): string {38 if (Object.hasOwn(options, 'transparent')) return 'transparent'39 const raw = options.background40 if (raw === undefined) return 'transparent'41 const value = /^[0-9a-f]{6}$/i.test(raw) ? `#${raw}` : raw42 if (!COLOR.test(value) || new TextEncoder().encode(value).byteLength > MAX_OPTION_BYTES) {43 throw new RenderError(400, 'invalid_options', "Ditaa option 'background' must be a CSS color or six hexadecimal digits.")44 }45 return value46}4748function normalizeSource(source: string, options: Record<string, string>): string {49 const tabs = optionNumber(options, 'tabs', 8, 32, true)50 const expanded = source.replaceAll('\t', ' '.repeat(tabs))51 // Ditaa tags are drawing directives, not labels. Dropping them retains the52 // readable label and prevents svgbob from treating braces as geometry.53 return expanded.replace(DITAA_TAG, (whole, name: string) => {54 const normalized = name.toLowerCase()55 // Keep brace-delimited label text that is not a known Ditaa directive.56 if (SHAPE_TAGS.has(normalized) || /^c(?:[a-z]{3}|[0-9a-f]{3,6})$/i.test(name)) return ''57 return whole58 })59}6061export const ditaaAdapter: RendererAdapter = {62 id: 'ditaa', runtime: 'edge-wasm', version: VERSION,63 async render(request, signal) {64 if (signal.aborted) throw signal.reason65 if (new TextEncoder().encode(request.source).byteLength > MAX_SOURCE_BYTES) {66 throw new RenderError(413, 'source_too_large', 'Ditaa source exceeds the 256 KiB limit.')67 }68 for (const name of Object.keys(request.options)) {69 if (!OPTION_NAMES.has(name)) throw new RenderError(400, 'unsupported_options', `Unsupported Ditaa option: ${name}.`)70 }71 const source = normalizeSource(request.source, request.options)72 const scale = optionNumber(request.options, 'scale', 1, 16)73 const background = optionColor(request.options)74 try {75 const body = renderSvgbob(source, background, '#000000', 'Iosevka Fixed, monospace', 14, scale, 2)76 if (!body.includes('<svg')) throw new RenderError(422, 'render_failed', 'Ditaa returned no SVG.')77 return edgeResult('ditaa', VERSION, body, 'edge-wasm')78 } catch (error) {79 if (error instanceof RenderError) throw error80 const message = error instanceof Error ? error.message : String(error)81 throw new RenderError(422, 'render_failed', message.slice(0, 500) || 'Ditaa could not render this source.')82 }83 },84}