PlantUML JS Integration PoC

Proof of Concept — Client-side rendering of ```plantuml blocks using the TeaVM-compiled engine

1 Sandbox Architecture

GitHub renders Markdown on github.com but executes untrusted renderers inside an isolated <iframe> on a separate origin. Communication happens via postMessage.

github.com

Scans Markdown for ```plantuml blocks.
Creates a sandboxed <iframe> per diagram.
Sends source text via postMessage.

postMessage
plantuml source

render.githubusercontent.com

Loads plantuml.js + viz-global.js.
Calls plantuml.render().
Sends SVG result back.

postMessage
SVG output

github.com

Receives SVG.
Replaces code block with rendered diagram.
Adjusts iframe height.

2 IFrame Renderer (sandbox side)

plantuml-renderer.js
// Loaded inside the sandboxed iframe on render.githubusercontent.com
// plantuml.js and viz-global.js are already included via <script> tags

const ALLOWED_ORIGIN = 'https://github.com';

// Initialize the PlantUML engine once
plantumlLoad();

const renderTarget = document.createElement('div');
renderTarget.id = 'plantuml-output';
document.body.appendChild(renderTarget);

window.addEventListener('message', (event) => {
  if (event.origin !== ALLOWED_ORIGIN) return;

  const { type, source, requestId, options } = event.data;
  if (type !== 'PLANTUML_RENDER') return;

  const lines = source.split(/\r\n|\r|\n/);
  const dark = options?.dark ?? false;

  // Watch for the SVG to appear in the DOM
  const observer = new MutationObserver(() => {
    if (renderTarget.querySelector('svg')) {
      observer.disconnect();
      window.parent.postMessage({
        type: 'PLANTUML_RESULT',
        requestId,
        svg: renderTarget.innerHTML,
        height: renderTarget.scrollHeight
      }, event.origin);
    }
  });
  observer.observe(renderTarget, { childList: true, subtree: true });

  try {
    window.plantuml.render(lines, 'plantuml-output', { dark });
  } catch (err) {
    observer.disconnect();
    window.parent.postMessage({
      type: 'PLANTUML_ERROR',
      requestId,
      error: err.message
    }, event.origin);
  }
});

3 Markdown Scanner (github.com side)

plantuml-integration.js
const RENDERER_URL = 'https://render.githubusercontent.com/plantuml/frame.html';

function initPlantUMLBlocks() {
  const blocks = document.querySelectorAll('pre[lang="plantuml"]');

  blocks.forEach((block, i) => {
    const source = block.textContent;
    const requestId = `puml-${i}-${Date.now()}`;
    const dark = document.documentElement.dataset.colorMode === 'dark';

    const iframe = document.createElement('iframe');
    iframe.src = RENDERER_URL;
    iframe.sandbox = 'allow-scripts';
    iframe.style.cssText = 'border:none; width:100%; overflow:hidden;';

    iframe.addEventListener('load', () => {
      iframe.contentWindow.postMessage({
        type: 'PLANTUML_RENDER',
        source, requestId,
        options: { dark }
      }, new URL(RENDERER_URL).origin);
    });

    window.addEventListener('message', (e) => {
      if (e.data.requestId !== requestId) return;
      if (e.data.type === 'PLANTUML_RESULT') {
        iframe.style.height = e.data.height + 'px';
      }
    });

    block.parentElement.replaceWith(iframe);
  });
}

document.addEventListener('DOMContentLoaded', initPlantUMLBlocks);

4 What It Looks Like in a README

Note: These diagrams are rendered live by the real PlantUML JS engine running in this page. In production, GitHub would isolate the engine in an iframe on a separate origin.

My Project Architecture

Here's our authentication flow:

plantuml — rendered by js-plantuml
Rendering diagram…

And here's the deployment topology:

plantuml — rendered by js-plantuml
Rendering diagram…
0
Server dependencies
3
API surface (functions)
~40
Lines of glue code
...
Total render time
Key selling points for GitHub: Zero server cost — no Java process, no Graphviz binary. The TeaVM-compiled engine runs entirely in the browser. The API is 3 calls: plantumlLoad(), plantuml.render(lines, targetId), and optionally plantuml.render(lines, targetId, {dark: true}). The sandbox architecture is the same pattern GitHub already uses for Mermaid.