DEV Community

toolzip
toolzip

Posted on Originally published at toolzip.app

Building a Live SVG Editor in the Browser — Syntax Highlighting Without a Library

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 };
}
Enter fullscreen mode Exit fullscreen mode

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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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'));
}
Enter fullscreen mode Exit fullscreen mode

Live at toolzip.app/tools/svg-preview.

Top comments (0)