refactor: author the embed loader in TypeScript

Address review feedback: static/embed.js is now generated from
src/embed-loader.ts via a small Vite lib-mode config (pnpm build:embed,
chained into dev and build). The generated file is gitignored, and the
source is covered by svelte-check and eslint.
This commit is contained in:
knsv committed 2026-08-07 10:23:53 +02:00
1 parent 0683a650ff
commit 0e1caab47e
5 files changed
+143 -106

No files matched your search

+3
View File
@@ -16,3 +16,6 @@
/blob-report/
/playwright/.cache/
.playwright-mcp/
# Generated by `pnpm build:embed` from src/embed-loader.ts
/static/embed.js
+3 -2
View File
@@ -4,10 +4,11 @@
"type": "module",
"license": "MIT",
"scripts": {
"dev": "vite dev",
"dev": "pnpm build:embed && vite dev",
"dev:force": "MERMAID_LOCAL=true pnpm dev --force",
"dev:test": "pnpm dev",
"build": "vite build",
"build": "pnpm build:embed && vite build",
"build:embed": "vite build --config vite.embed.config.js",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+119
View File
@@ -0,0 +1,119 @@
/**
* <mermaid-embed> — a tiny, dependency-free loader for the Mermaid embed widget.
*
* Usage:
* <script src="https://mermaid.live/embed.js" async></script>
* <mermaid-embed src="https://mermaid.live/embed#pako:...">
* graph TD
* A --> B
* </mermaid-embed>
*
* The element renders an <iframe> pointing at the host's /embed page. Settings come from the
* `src` URL and/or attributes (theme, look, mode, grid, pan, zoom, controls). If the element has
* inline body text, that text is used as the diagram code and OVERRIDES any code in the `src` URL.
*
* Authored in TypeScript; `pnpm build:embed` compiles it into `static/embed.js` (IIFE, no
* imports) so it can be dropped into any page with a single script tag.
*/
// Derive origin + base path from the script URL so base-path deploys work.
const script = document.currentScript;
const scriptUrl = script instanceof HTMLScriptElement && script.src ? new URL(script.src) : null;
const prefix = scriptUrl
? scriptUrl.origin + scriptUrl.pathname.replace(/\/embed\.js$/, '')
: window.location.origin;
const defaultBase = `${prefix}/embed`;
const passthroughAttributes = ['theme', 'look', 'mode', 'grid', 'pan', 'zoom', 'controls'];
const px = (value: string | null, fallback: string): string => {
if (value === null) {
return fallback;
}
return /^\d+$/.test(value) ? `${value}px` : value;
};
// Strip the common leading indentation from inline HTML so the diagram code is clean.
const dedent = (text: string): string => {
const lines = text.replaceAll('\t', ' ').split('\n');
while (lines.length > 0 && lines[0].trim() === '') {
lines.shift();
}
while (lines.length > 0 && lines.at(-1)?.trim() === '') {
lines.pop();
}
let indent = Number.POSITIVE_INFINITY;
for (const line of lines) {
if (line.trim() !== '') {
indent = Math.min(indent, line.length - line.trimStart().length);
}
}
if (!Number.isFinite(indent)) {
indent = 0;
}
return lines.map((line) => line.slice(indent)).join('\n');
};
const buildUrl = (element: HTMLElement): string => {
const src = element.getAttribute('src') ?? defaultBase;
const url = new URL(src, window.location.href);
for (const name of passthroughAttributes) {
const value = element.getAttribute(name);
if (value !== null) {
url.searchParams.set(name, value);
}
}
// Inline body code wins over whatever code the src URL carried.
const code = dedent(element.textContent ?? '');
if (code.trim() !== '') {
url.searchParams.set('code', code);
}
return url.toString();
};
const rendered = new WeakSet<HTMLElement>();
const render = (element: HTMLElement): void => {
if (rendered.has(element)) {
return;
}
rendered.add(element);
const iframe = document.createElement('iframe');
iframe.src = buildUrl(element);
iframe.title = element.getAttribute('title') ?? 'Mermaid diagram';
iframe.loading = 'lazy';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute(
'sandbox',
'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox'
);
iframe.style.border = '0';
iframe.style.display = 'block';
iframe.style.width = px(element.getAttribute('width'), '100%');
iframe.style.height = px(element.getAttribute('height'), '420px');
element.style.display = 'block';
element.textContent = '';
element.append(iframe);
};
if ('customElements' in window) {
customElements.define(
'mermaid-embed',
class MermaidEmbed extends HTMLElement {
connectedCallback(): void {
// Defer a macrotask so any inline text children are fully parsed before we read
// them (the start tag connects before its text content when scripted upgrade
// happens mid-parse). setTimeout is more reliable than rAF in headless contexts.
// render() ignores repeat calls, so re-connects are harmless.
setTimeout(() => render(this), 0);
}
}
);
}
export {};
-104
View File
@@ -1,104 +0,0 @@
/**
* <mermaid-embed> — a tiny, dependency-free loader for the Mermaid embed widget.
*
* Usage:
* <script src="https://mermaid.live/embed.js" async></script>
* <mermaid-embed src="https://mermaid.live/embed#pako:...">
* graph TD
* A --> B
* </mermaid-embed>
*
* The element renders an <iframe> pointing at the host's /embed page. Settings come from the
* `src` URL and/or attributes (theme, look, mode, grid, pan, zoom, controls). If the element has
* inline body text, that text is used as the diagram code and OVERRIDES any code in the `src` URL.
*/
(function () {
'use strict';
var script = document.currentScript;
// Derive origin + base path from the script URL so base-path deploys work.
var scriptUrl = script ? new URL(script.src) : null;
var PREFIX = scriptUrl
? scriptUrl.origin + scriptUrl.pathname.replace(/\/embed\.js$/, '')
: window.location.origin;
var DEFAULT_BASE = PREFIX + '/embed';
var PASSTHROUGH = ['theme', 'look', 'mode', 'grid', 'pan', 'zoom', 'controls'];
function px(value, fallback) {
if (value == null) return fallback;
return /^\d+$/.test(value) ? value + 'px' : value;
}
// Strip the common leading indentation from inline HTML so the diagram code is clean.
function dedent(text) {
var lines = text.replace(/\t/g, ' ').split('\n');
while (lines.length && lines[0].trim() === '') lines.shift();
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
var indent = Infinity;
lines.forEach(function (line) {
if (line.trim() === '') return;
indent = Math.min(indent, line.match(/^ */)[0].length);
});
if (!isFinite(indent)) indent = 0;
return lines
.map(function (line) {
return line.slice(indent);
})
.join('\n');
}
function buildUrl(el) {
var src = el.getAttribute('src') || DEFAULT_BASE;
var url = new URL(src, window.location.href);
PASSTHROUGH.forEach(function (name) {
var value = el.getAttribute(name);
if (value !== null) url.searchParams.set(name, value);
});
// Inline body code wins over whatever code the src URL carried.
var code = dedent(el.textContent || '');
if (code.trim() !== '') url.searchParams.set('code', code);
return url.toString();
}
function render(el) {
if (el.__mermaidEmbedRendered) return;
el.__mermaidEmbedRendered = true;
var iframe = document.createElement('iframe');
iframe.src = buildUrl(el);
iframe.title = el.getAttribute('title') || 'Mermaid diagram';
iframe.loading = 'lazy';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute(
'sandbox',
'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox'
);
iframe.style.border = '0';
iframe.style.display = 'block';
iframe.style.width = px(el.getAttribute('width'), '100%');
iframe.style.height = px(el.getAttribute('height'), '420px');
el.style.display = 'block';
el.textContent = '';
el.appendChild(iframe);
}
if ('customElements' in window) {
customElements.define(
'mermaid-embed',
class MermaidEmbed extends HTMLElement {
connectedCallback() {
// Defer a macrotask so any inline text children are fully parsed before we read
// them (the start tag connects before its text content when scripted upgrade
// happens mid-parse). setTimeout is more reliable than rAF in headless contexts.
// render() ignores repeat calls, so re-connects are harmless.
setTimeout(() => render(this), 0);
}
}
);
}
})();
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'vite';
// Builds the standalone <mermaid-embed> loader (src/embed-loader.ts) into static/embed.js,
// which the SvelteKit build then ships verbatim as /embed.js. Run via `pnpm build:embed`
// (chained into `pnpm dev` and `pnpm build`); the output file is gitignored.
export default defineConfig({
build: {
emptyOutDir: false,
lib: {
entry: 'src/embed-loader.ts',
fileName: () => 'embed.js',
formats: ['iife'],
name: 'mermaidEmbed'
},
minify: false,
outDir: 'static'
}
});