Files
2026-07-13 12:24:14 +08:00

189 lines
5.3 KiB
TypeScript

/**
* Generates editor.html — a live Mermaid editor similar to mermaid.live.
*
* Usage: bun run editor.ts
*
* The generated HTML is fully self-contained:
* - Bundles the mermaid renderer client-side
* - Live rendering on every keystroke (debounced)
* - URL hash sharing (base64-encoded source)
* - Theme switcher with all built-in themes
* - Sample presets by diagram category
* - Download SVG / Copy link
*
* Source files are organized in editor/:
* - editor/css/ — modular CSS components
* - editor/js/ — modular JS modules
* - editor/html/ — HTML partials (topbar, left-panel, right-panel)
*/
import { THEMES } from './src/theme.ts'
const THEME_LABELS: Record<string, string> = {
'zinc-dark': 'Zinc Dark',
'tokyo-night': 'Tokyo Night',
'tokyo-night-storm': 'Tokyo Storm',
'tokyo-night-light': 'Tokyo Light',
'catppuccin-mocha': 'Catppuccin',
'catppuccin-latte': 'Latte',
'nord': 'Nord',
'nord-light': 'Nord Light',
'dracula': 'Dracula',
'github-light': 'GitHub',
'github-dark': 'GitHub Dark',
'solarized-light': 'Solarized',
'solarized-dark': 'Solar Dark',
'one-dark': 'One Dark',
}
// ── File helpers ──────────────────────────────────────────────────────────────
const editorDir = new URL('./editor/', import.meta.url).pathname
async function readFile(relativePath: string): Promise<string> {
const file = Bun.file(editorDir + relativePath)
return file.text()
}
async function readCssFiles(): Promise<string> {
const order = [
'css/variables.css',
'css/topbar.css',
'css/panels.css',
'css/code-editor.css',
'css/preview.css',
'css/config-panel.css',
'css/color-picker.css',
'css/font-picker.css',
'css/export.css',
'css/misc.css',
]
const parts = await Promise.all(order.map(f => readFile(f)))
return parts.join('\n\n')
}
async function readJsFiles(): Promise<string> {
const order = [
'js/helpers.js',
'js/state.js',
'js/elements.js',
'js/sharing.js',
'js/rendering.js',
'js/zoom.js',
'js/pan.js',
'js/editor-helpers.js',
'js/config-panel.js',
'js/color-picker.js',
'js/font-picker.js',
'js/tabs.js',
'js/buttons.js',
'js/export.js',
'js/resize.js',
'js/toast.js',
'js/dark-mode.js',
'js/init.js',
]
const parts = await Promise.all(order.map(f => readFile(f)))
return parts.join('\n\n')
}
async function readHtmlPartials(themeItems: string): Promise<{
topbar: string
leftPanel: string
rightPanel: string
}> {
const [topbar, leftPanel, rightPanel] = await Promise.all([
readFile('html/topbar.html'),
readFile('html/left-panel.html'),
readFile('html/right-panel.html'),
])
return {
topbar: topbar.replace('{{THEME_ITEMS}}', themeItems),
leftPanel,
rightPanel,
}
}
// ── Main ──────────────────────────────────────────────────────────────────────
async function generateEditorHtml(): Promise<string> {
const buildResult = await Bun.build({
entrypoints: [new URL('./src/browser.ts', import.meta.url).pathname],
target: 'browser',
format: 'esm',
minify: true,
})
if (!buildResult.success) {
console.error('Bundle failed:', buildResult.logs)
process.exit(1)
}
const bundleJs = await buildResult.outputs[0]!.text()
console.log(`Browser bundle: ${(bundleJs.length / 1024).toFixed(1)} KB`)
const themeItems = [
`<button class="theme-dropdown-item active" data-theme="">Default</button>`,
...Object.keys(THEMES).map(
key => `<button class="theme-dropdown-item" data-theme="${key}"><span class="theme-swatch" style="background:${THEMES[key].bg}"></span>${THEME_LABELS[key] ?? key}</button>`
),
].join('\n ')
const [css, appJs, html] = await Promise.all([
readCssFiles(),
readJsFiles(),
readHtmlPartials(themeItems),
])
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Beautiful Mermaid — Live Editor</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
${css}
</style>
</head>
<body>
<!-- Top bar -->
${html.topbar}
<!-- Main -->
<div class="main">
<!-- Left panel -->
${html.leftPanel}
<!-- Resize handle -->
<div class="resize-handle" id="resize-handle"></div>
<!-- Right panel -->
${html.rightPanel}
</div>
<div class="toast" id="toast"></div>
<!-- Bundled renderer -->
<script type="module">
${bundleJs}
${appJs}
</script>
</body>
</html>`
}
const result = await generateEditorHtml()
const outPath = new URL('./editor.html', import.meta.url).pathname
await Bun.write(outPath, result)
console.log(`Written to ${outPath} (${(result.length / 1024).toFixed(1)} KB)`)