diff --git a/client.js b/client.js index 25233b6..2e9481b 100644 --- a/client.js +++ b/client.js @@ -1,3 +1,20 @@ +import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs' + +let lastDiagram = '' +let rendering = false +let theme = 'dark' + +function setTheme(lightMatches) { + theme = lightMatches ? 'default' : 'dark' + mermaid.initialize({ startOnLoad: true, theme }) + renderLastDiagram() +} +if (window.matchMedia) { + let lightMatcher = window.matchMedia('(prefers-color-scheme: light)') + lightMatcher.addEventListener('change', ({ matches }) => setTheme(matches)) + setTheme(lightMatcher.matches) +} + let connectionAttempt = 0 function connectSocket() { const socket = new WebSocket('ws://localhost:8080') @@ -6,14 +23,44 @@ function connectSocket() { }) socket.addEventListener('close', () => { connectionAttempt += 1 - setTimeout(connectSocket(), 1000) + setTimeout(() => requestAnimationFrame(connectSocket), 1000) }) socket.addEventListener('message', handleMessage) } -function handleMessage({ data }) { - console.log({ data }) - if (data == 'reload') window.location.reload() +async function handleMessage({ data }) { + if (data == 'hi') { + console.log('👋') + } else if (data == 'reload') { + window.location.reload() + } else { + try { + await handleStructuredMessage(JSON.parse(data)) + } catch (err) { + console.error(`failed to process structured websocket message: ${err}`) + } + } +} + +async function handleStructuredMessage(data) { + switch (data.type) { + case 'mermaid': { + if (rendering) return + rendering = true + + lastDiagram = data.contents + await renderLastDiagram() + + rendering = false + break + } + } +} + +async function renderLastDiagram() { + if (lastDiagram == '') return + document.getElementById('diagram').innerHTML = + (await mermaid.render('mermaid', lastDiagram)).svg } addEventListener('DOMContentLoaded', connectSocket) diff --git a/mod.ts b/mod.ts index 51aa980..d02f762 100755 --- a/mod.ts +++ b/mod.ts @@ -54,41 +54,60 @@ const command = new Command() async function openBrowser(args: Args): Promise { if (args.open) { + // TODO: this is broken? const url = `http://${args.host}:${args.port}` console.log(`opening browser to ${url}`) const process = new Deno.Command('xdg-open', { args: [url], }).spawn() - - // const result = true console.log(`opened browser to ${url}: ${await process.status}`) } } async function runServer(args: Args): Promise { const opts = { hostname: args.host, port: args.port } - Deno.serve(opts, async (req: Request) => { + await Deno.serve(opts, async (req: Request, info: Deno.ServeHandlerInfo) => { if (req.headers.get('upgrade') == 'websocket') { - return handleWebSocket(req) + return handleWebSocket(req, info) } const url = new URL(req.url) if (url.pathname == '/client.js') { return new Response(await Deno.readTextFile('./client.js'), { - headers: new Headers({ 'content-type': 'text/javascript' }), + headers: new Headers({ + 'content-type': 'text/javascript;charset=UTF-8', + }), }) } else if (url.pathname == '/') { - return new Response(html.index, { - headers: new Headers({ 'content-type': 'text/html' }), + return new Response(page(null, await listDiagramsHtml(args.input)), { + headers: new Headers({ 'content-type': 'text/html', charset: 'utf-8' }), }) + } else { + const file = url.pathname.replaceAll('..', '') + if (file.endsWith('.mmd')) { + if (req.headers.get('accepts') == 'text/mermaid') { + return new Response(Deno.readTextFileSync(file), { + headers: { 'content-type': 'text/mermaid' }, + }) + } + + return new Response( + page( + url.pathname, + await diagramContent(args, url.pathname.replaceAll('..', '')), + ), + { + headers: new Headers({ + 'content-type': 'text/html', + charset: 'utf-8', + }), + }, + ) + } } return new Response(`Hello, ${url.pathname}!`) - }) - - return await new Promise((_resolve) => { - // a promise that never resolves - }) + }).finished } async function runMermaidFileWatcher(args: Args) { @@ -102,7 +121,6 @@ async function runMermaidFileWatcher(args: Args) { for (const p of paths) { console.log(p) if (p.endsWith('.mmd') && (kind == 'create' || kind == 'modify')) { - console.log('omg a mermaid') const contents = await Deno.readTextFile(p) sockets.forEach((s) => s.send(JSON.stringify({ type: 'mermaid', contents })) @@ -113,17 +131,17 @@ async function runMermaidFileWatcher(args: Args) { } } -function handleWebSocket(req: Request): Response { +function handleWebSocket(req: Request, info: Deno.ServeHandlerInfo): Response { const { socket, response } = Deno.upgradeWebSocket(req) socket.addEventListener('open', (_ev) => { - console.log(`websocket open`) + console.log(`websocket open from ${JSON.stringify(info.remoteAddr)}`) socket.send('hi') sockets.add(socket) }) socket.addEventListener('message', (ev) => { - console.log(`websocket message: ${ev}`) + console.log(`websocket message: ${JSON.stringify(ev)}`) try { if (ev.data) { const data = JSON.parse(ev.data) @@ -136,7 +154,7 @@ function handleWebSocket(req: Request): Response { }) socket.addEventListener('close', (ev) => { - console.log(`websocket close: ${ev}`) + console.log(`websocket close: ${JSON.stringify(ev)}`) sockets.delete(socket) }) @@ -151,17 +169,52 @@ function handleClientEvent(socket: WebSocket, ce: ClientEvent) { } } -const html = { - index: ` - -

Hello, index!

- - - `, +const listDiagramsHtml = async (dir: string): Promise => { + const list = [] + for await (const entry of Deno.readDir(dir)) { + if (entry.isFile && entry.name.endsWith('.mmd')) { + list.push(`
  • ${entry.name}
  • `) + } + } + return `
      ${list.join('\n')}
    ` } +const diagramContent = async (args: Args, file: string): Promise => { + let content = await Deno.readTextFile(args.input + file) + return `
    ${content}
    ` +} + +const page = (title: string | null, content: string) => + ` + + + + + ${title !== null ? `${title} - ` : ''}Diagram + + +

    Diagram Index${ + title !== null ? ` ${title}` : '' + }

    + + ${content} + + +` + async function run(args: Args) { - // TODO: start mermaid file watcher await Promise.all([ runMermaidFileWatcher(args), runServer(args),