mirror of
https://github.com/mermaid-js/mermaid-live-editor.git
synced 2026-11-04 08:59:08 +00:00
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:
1 parent
0683a650ff
commit
0e1caab47e
5 files changed
+143
-106
No files matched your search
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
})();
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user