import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs'; import { Actor } from './actor.js'; import { Action } from './action.js'; import { debounce, hexToRGB } from '../util.js'; import { CryptoUtil } from './crypto.js'; class MermaidDiagram { constructor(box, logBox) { this.box = box; this.container = this.box.addBox('Container'); this.element = this.box.addBox('Element'); this.renderBox = this.box.addBox('Render'); this.box.addBox('Spacer').setInnerHTML(' '); this.logBoxPre = logBox.el.appendChild(document.createElement('pre')); this.inSection = 0; } async log(msg, render = true) { this.logBoxPre.textContent = `${this.logBoxPre.textContent}\n${msg}`; if (render) { await this.render(); } return this; } async render() { const render = async () => { let text = this.logBoxPre.textContent; for (let i = 0; i < this.inSection; i++) { text += '\nend'; } const graph = await mermaid.mermaidAPI.render( this.element.getId(), text, ); this.renderBox.setInnerHTML(graph); }; await debounce(render, 100); } } class Table { constructor(box) { this.box = box; this.columns = []; this.rows = []; this.table = box.el.appendChild(document.createElement('table')); this.headings = this.table.appendChild(document.createElement('tr')); } setColumns(columns) { if (JSON.stringify(columns) === JSON.stringify(this.columns)) { return; } if (this.columns.length) { this.table.innerHTML = ''; this.headings = this.table.appendChild(document.createElement('tr')); this.columns = []; } this.columns = columns; for (const { title } of columns) { const heading = document.createElement('th'); this.headings.appendChild(heading); heading.innerHTML = title ?? ''; } if (this.rows.length) { const { rows } = this; this.rows = []; for (const row of rows) { this.addRow(row); } } } addRow(rowMap) { this.rows.push(rowMap); const row = this.table.appendChild(document.createElement('tr')); for (const { key } of this.columns) { const value = rowMap.get(key); const cell = row.appendChild(document.createElement('td')); cell.innerHTML = value ?? ''; } } } export class Scene { constructor(name, rootBox) { this.name = name; this.box = rootBox.addBox(name); this.titleBox = this.box.addBox('Title').setInnerHTML(name); this.box.addBox('Spacer').setInnerHTML(' '); this.topSection = this.box.addBox('Top section').flex(); this.displayValuesBox = this.topSection.addBox('Values'); this.middleSection = this.box.addBox('Middle section'); this.box.addBox('Spacer').setInnerHTML(' '); this.actors = new Set(); this.dateStart = new Date(); this.flowcharts = new Map(); mermaid.mermaidAPI.initialize({ startOnLoad: false, theme: 'base', themeVariables: { darkMode: true, primaryColor: '#2a5b6c', primaryTextColor: '#b6b6b6', // lineColor: '#349cbd', lineColor: '#57747d', signalColor: '#57747d', // signalColor: '#349cbd', noteBkgColor: '#516f77', noteTextColor: '#cecece', activationBkgColor: '#1d3f49', activationBorderColor: '#569595', }, }); this.options = { edgeNodeColor: '#4d585c', }; } withSequenceDiagram() { const box = this.box.addBox('Sequence diagram'); this.box.addBox('Spacer').setInnerHTML(' '); const logBox = this.box.addBox('Sequence diagram text').addClass('dim'); this.sequence = new MermaidDiagram(box, logBox); this.sequence.log('sequenceDiagram', false); return this; } withFlowchart({ direction = 'BT' } = {}) { const box = this.topSection.addBox('Flowchart').addClass('padded'); this.box.addBox('Spacer').setInnerHTML(' '); const logBox = this.box.addBox('Flowchart text').addClass('dim'); this.flowchart = new MermaidDiagram(box, logBox); this.flowchart.log(`graph ${direction}`, false); return this; } withAdditionalFlowchart({ id, name, direction = 'BT' } = {}) { const index = this.flowcharts.size; name = name ?? `Flowchart ${index}`; id = id ?? `flowchart_${CryptoUtil.randomUUID().slice(0, 4)}`; const container = this.middleSection.addBox(name).flex(); const box = container.addBox('Flowchart').addClass('padded'); const logBox = container.addBox('Flowchart text').addClass('dim'); const flowchart = new MermaidDiagram(box, logBox); flowchart.log(`graph ${direction}`, false); this.flowcharts.set(id, flowchart); return this; } lastFlowchart() { if (!this.flowcharts.size) { if (this.flowchart) { return this.flowchart; } throw new Error('lastFlowchart: No additional flowcharts have been added.'); } const flowcharts = Array.from(this.flowcharts.values()); return flowcharts[flowcharts.length - 1]; } withTable() { if (this.table) { return this; } const box = this.middleSection.addBox('Table').addClass('padded'); this.box.addBox('Spacer').setInnerHTML(' '); this.table = new Table(box); return this; } async addActor(name) { const actor = new Actor(name); if (this.sequence) { await this.sequence.log(`participant ${name}`); } return actor; } registerActor(actor) { this.actors.add(actor); } findActor(fn) { return Array.from(this.actors.values()).find(fn); } addAction(name) { const action = new Action(name, this); return action; } addDisplayValue(name) { const dv = this.displayValuesBox.addDisplayValue(name); return dv; } async deactivateAll() { for (const actor of this.actors.values()) { while (actor.active) { await actor.deactivate(); } } } async startSection(color = '#08252c') { const { r, g, b } = hexToRGB(color); this.sequence.inSection++; this.sequence.log(`rect rgb(${r}, ${g}, ${b})`, false); } async endSection() { this.sequence.inSection--; this.sequence.log('end'); } stateToTable(label) { const row = new Map(); const columns = []; columns.push({ key: 'seqNum', title: '#' }); columns.push({ key: 'elapsedMs', title: 'Time (ms)' }); row.set('seqNum', this.table.rows.length + 1); row.set('elapsedMs', new Date() - this.dateStart); row.set('label', label); for (const actor of this.actors) { for (const [aKey, { name, value }] of actor.getValuesMap()) { const key = `${actor.name}:${aKey}`; columns.push({ key, title: name }); row.set(key, value); } } columns.push({ key: 'label', title: '' }); this.table.setColumns(columns); this.table.addRow(row); } }