Proof of Concept — Client-side rendering of ```plantuml blocks using the TeaVM-compiled engine
GitHub renders Markdown on github.com but executes untrusted renderers inside an isolated <iframe> on a separate origin. Communication happens via postMessage.
Scans Markdown for ```plantuml blocks.
Creates a sandboxed <iframe> per diagram.
Sends source text via postMessage.
Loads plantuml.js + viz-global.js.
Calls plantuml.render().
Sends SVG result back.
Receives SVG.
Replaces code block with rendered diagram.
Adjusts iframe height.
// 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); } });
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);
Here's our authentication flow:
And here's the deployment topology:
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.