Building a Live SVG Editor in the Browser — Syntax Highlighting Without a Library
A live SVG editor sounds simple: textarea on the left, preview on the right, update on change. In practice, there are several sharp edges.
Rendering SVG Safely
The naive approach — innerHTML = userInput — is an XSS vector. SVG can contain <script> tags and event handlers.
The safer approach uses DOMParser:
function renderSVG(svgString, container) {
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, 'image/svg+xml');
const parserError = doc.querySelector('parsererror');
if (parserError) {
return { ok: false, error: parserError.textContent };
}
// Remove script elements
doc.querySelectorAll('script').forEach(el => el.remove());
// Remove event handlers
doc.querySelectorAll('*').forEach(el => {
[...el.attributes].forEach(attr => {
if (attr.name.startsWith('on')) el.removeAttribute(attr.name);
});
});
container.innerHTML = '';
container.appendChild(doc.documentElement.cloneNode(true));
return { ok: true };
}
Export to PNG
SVG → PNG conversion in the browser requires an intermediate step through an <img> element:
async function svgToPng(svgString, width, height) {
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
resolve(canvas.toDataURL('image/png'));
};
img.src = url;
});
}
ViewBox and Zoom
SVG has its own coordinate system defined by viewBox. When you zoom, you're scaling the container, not changing the viewBox. This keeps the SVG coordinates stable:
function setZoom(containerEl, scale) {
containerEl.style.transform = `scale(${scale})`;
containerEl.style.transformOrigin = 'top left';
}
Snippet Insertion
For quick edits, I added insert buttons for common shapes. The trick is inserting at the cursor position in the textarea:
function insertAtCursor(textarea, snippet) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const before = textarea.value.slice(0, start);
const after = textarea.value.slice(end);
textarea.value = before + snippet + after;
textarea.selectionStart = textarea.selectionEnd = start + snippet.length;
textarea.dispatchEvent(new Event('input'));
}
Live at toolzip.app/tools/svg-preview.
Top comments (0)