clone

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

main
diagramzip / apps / editor / src / editable-svg-input.js
editable-svg-input.js 3.6K
1import { importEditableSvg } from './editable-svg.js'23export const MAX_EDITABLE_SVG_INPUT_BYTES = 5 * 1024 * 102445const encoder = new TextEncoder()6const SVG_CONTENT_TYPE = /^(?:image\/svg\+xml|application\/(?:xml|svg\+xml)|text\/xml)(?:\s*;|\s*$)/i78export class EditableSvgInputError extends Error {9 constructor(code, message) {10 super(message)11 this.name = 'EditableSvgInputError'12 this.code = code13 }14}1516function fail(code, message) {17 throw new EditableSvgInputError(code, message)18}1920function assertWithinLimit(bytes, label = 'Editable SVG') {21 if (bytes.byteLength > MAX_EDITABLE_SVG_INPUT_BYTES) {22 fail('input_too_large', `${label} is larger than 5 MiB.`)23 }24 return bytes25}2627function decode(bytes) {28 return new TextDecoder().decode(assertWithinLimit(bytes))29}3031async function bytesFromResponse(response) {32 const contentType = response.headers?.get?.('content-type') ?? ''33 if (contentType && !SVG_CONTENT_TYPE.test(contentType)) {34 fail('invalid_content_type', 'The URL did not return an SVG or XML document.')35 }3637 if (response.body?.getReader) {38 const reader = response.body.getReader()39 const chunks = []40 let length = 041 try {42 while (true) {43 const { done, value } = await reader.read()44 if (done) break45 const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)46 length += chunk.byteLength47 if (length > MAX_EDITABLE_SVG_INPUT_BYTES) {48 await reader.cancel().catch(() => {})49 fail('input_too_large', 'The downloaded SVG is larger than 5 MiB.')50 }51 chunks.push(chunk)52 }53 } finally {54 reader.releaseLock?.()55 }56 const bytes = new Uint8Array(length)57 let offset = 058 for (const chunk of chunks) {59 bytes.set(chunk, offset)60 offset += chunk.byteLength61 }62 return bytes63 }6465 if (typeof response.arrayBuffer === 'function') {66 return assertWithinLimit(new Uint8Array(await response.arrayBuffer()), 'The downloaded SVG')67 }68 if (typeof response.text === 'function') return encoder.encode(await response.text())69 fail('invalid_response', 'The URL response did not contain readable SVG data.')70}7172export function importEditableSvgText(source) {73 if (typeof source !== 'string') fail('invalid_input', 'Editable SVG input must be text.')74 assertWithinLimit(encoder.encode(source))75 return importEditableSvg(source)76}7778export async function importEditableSvgFile(file) {79 if (!file || typeof file.arrayBuffer !== 'function') {80 fail('invalid_input', 'Editable SVG input must be a File or Blob.')81 }82 if (Number.isFinite(file.size) && file.size > MAX_EDITABLE_SVG_INPUT_BYTES) {83 fail('input_too_large', 'The selected SVG is larger than 5 MiB.')84 }85 return importEditableSvg(decode(new Uint8Array(await file.arrayBuffer())))86}8788export async function importEditableSvgInput(input, { fetchImpl = globalThis.fetch } = {}) {89 if (typeof input !== 'string') return importEditableSvgFile(input)90 let url91 try {92 url = new URL(input)93 } catch {94 return importEditableSvgText(input)95 }96 if (!['http:', 'https:', 'data:'].includes(url.protocol)) {97 fail('invalid_url', 'Editable SVG URLs must use HTTP(S) or data: URLs.')98 }99 if (typeof fetchImpl !== 'function') fail('fetch_unavailable', 'URL import is unavailable in this browser.')100 let response101 try {102 response = await fetchImpl(url.toString())103 } catch (error) {104 fail('fetch_failed', error instanceof Error ? error.message : 'Could not download the SVG.')105 }106 if (!response?.ok) fail('fetch_failed', `The SVG URL returned HTTP ${response?.status ?? 'an error'}.`)107 return importEditableSvg(decode(await bytesFromResponse(response)))108}109