SequenceDiagram/scripts/sequence/SequenceDiagram.mjs

473 lines
9.8 KiB
JavaScript

import {Factory as BasicThemeFactory} from './themes/Basic.mjs';
import {Factory as ChunkyThemeFactory} from './themes/Chunky.mjs';
import EventObject from '../core/EventObject.mjs';
import Exporter from './exporter/Exporter.mjs';
import Generator from './generator/Generator.mjs';
import {Factory as MonospaceThemeFactory} from './themes/Monospace.mjs';
import Parser from './parser/Parser.mjs';
import Renderer from './renderer/Renderer.mjs';
import {Factory as SketchThemeFactory} from './themes/Sketch.mjs';
import {getHints} from './codemirror/hints.mjs';
const themes = [
new BasicThemeFactory(),
new MonospaceThemeFactory(),
new ChunkyThemeFactory(),
new SketchThemeFactory(SketchThemeFactory.RIGHT),
new SketchThemeFactory(SketchThemeFactory.LEFT),
];
const SharedParser = new Parser();
const SharedGenerator = new Generator();
const CMMode = SharedParser.getCodeMirrorMode();
function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') {
const cm = CodeMirror || window.CodeMirror;
cm.defineMode(modeName, () => CMMode);
cm.registerHelper('hint', modeName, getHints);
}
function addTheme(theme) {
themes.push(theme);
}
function extractCodeFromSVG(svg) {
const dom = new DOMParser().parseFromString(svg, 'image/svg+xml');
const meta = dom.querySelector('metadata');
if(!meta) {
return '';
}
return meta.textContent;
}
function renderAll(diagrams) {
const errors = [];
function storeError(sd, e) {
errors.push(e);
}
diagrams.forEach((diagram) => {
diagram.addEventListener('error', storeError);
diagram.optimisedRenderPreReflow();
});
diagrams.forEach((diagram) => {
diagram.optimisedRenderReflow();
});
diagrams.forEach((diagram) => {
diagram.optimisedRenderPostReflow();
diagram.removeEventListener('error', storeError);
});
if(errors.length > 0) {
throw errors;
}
}
function pickDocument(container) {
if(container) {
return container.ownerDocument || null;
} else if(typeof window === 'undefined') {
return null;
} else {
return window.document;
}
}
export default class SequenceDiagram extends EventObject {
/* eslint-disable complexity */ // Just some defaults
constructor(code = null, options = {}) {
/* eslint-enable complexity */
super();
let opts = null;
if(code && typeof code === 'object') {
opts = code;
this.code = opts.code;
} else {
opts = options;
this.code = code;
}
Object.assign(this, {
exporter: new Exporter(),
generator: SharedGenerator,
isInteractive: false,
latestProcessed: null,
parser: SharedParser,
registerCodeMirrorMode,
renderer: new Renderer(Object.assign({
document: pickDocument(opts.container),
themes,
}, opts)),
textSizerFactory: opts.textSizerFactory || null,
});
this.renderer.addEventForwarding(this);
if(opts.container) {
opts.container.appendChild(this.dom());
}
if(opts.interactive) {
this.addInteractivity();
}
if(typeof this.code === 'string' && opts.render !== false) {
this.render();
}
}
clone(options = {}) {
const reference = (options.container || this.renderer.dom());
return new SequenceDiagram(Object.assign({
code: this.code,
components: this.renderer.components,
container: null,
document: reference.ownerDocument,
interactive: this.isInteractive,
namespace: null,
textSizerFactory: this.textSizerFactory,
themes: this.renderer.getThemes(),
}, options));
}
set(code = '', {render = true} = {}) {
if(this.code === code) {
return;
}
this.code = code;
if(render) {
this.render();
}
}
process(code) {
const parsed = this.parser.parse(code);
return this.generator.generate(parsed);
}
addTheme(theme) {
this.renderer.addTheme(theme);
}
setHighlight(line) {
this.renderer.setHighlight(line);
}
isCollapsed(line) {
return this.renderer.isCollapsed(line);
}
setCollapsed(line, collapsed = true, {render = true} = {}) {
if(!this.renderer.setCollapsed(line, collapsed)) {
return false;
}
if(render && this.latestProcessed) {
this.render(this.latestProcessed);
}
return true;
}
collapse(line, options) {
return this.setCollapsed(line, true, options);
}
expand(line, options) {
return this.setCollapsed(line, false, options);
}
toggleCollapsed(line, options) {
return this.setCollapsed(line, !this.isCollapsed(line), options);
}
expandAll(options) {
return this.setCollapsed(null, false, options);
}
getThemeNames() {
return this.renderer.getThemeNames();
}
getThemes() {
return this.renderer.getThemes();
}
getSVGCodeSynchronous() {
return this.exporter.getSVGContent(this.renderer);
}
getSVGCode() {
return Promise.resolve(this.getSVGCodeSynchronous());
}
getSVGSynchronous() {
return this.exporter.getSVGURL(this.renderer);
}
getSVG() {
return Promise.resolve({
latest: true,
url: this.getSVGSynchronous(),
});
}
getCanvas({resolution = 1, size = null} = {}) {
if(size) {
this.renderer.width = size.width;
this.renderer.height = size.height;
}
return new Promise((resolve) => {
this.exporter.getCanvas(this.renderer, resolution, resolve);
});
}
getPNG({resolution = 1, size = null} = {}) {
if(size) {
this.renderer.width = size.width;
this.renderer.height = size.height;
}
return new Promise((resolve) => {
this.exporter.getPNGURL(
this.renderer,
resolution,
(url, latest) => {
resolve({latest, url});
}
);
});
}
getSize() {
return {
height: this.renderer.height,
width: this.renderer.width,
};
}
_revertParent(state) {
const dom = this.renderer.dom();
if(dom.parentNode !== state.originalParent) {
dom.parentNode.removeChild(dom);
if(state.originalParent) {
state.originalParent.appendChild(dom);
}
}
}
_sendRenderError(e) {
this._revertParent(this.renderState);
this.renderState.error = true;
this.trigger('error', [this, e]);
}
optimisedRenderPreReflow(processed = null) {
const dom = this.renderer.dom();
this.renderState = {
error: false,
originalParent: dom.parentNode,
processed,
};
const state = this.renderState;
if(!dom.isConnected) {
if(state.originalParent) {
state.originalParent.removeChild(dom);
}
dom.ownerDocument.body.appendChild(dom);
}
try {
if(!state.processed) {
state.processed = this.process(this.code);
}
this.renderer.optimisedRenderPreReflow(state.processed);
} catch(e) {
this._sendRenderError(e);
}
}
optimisedRenderReflow() {
try {
if(!this.renderState.error) {
this.renderer.optimisedRenderReflow();
}
} catch(e) {
this._sendRenderError(e);
}
}
optimisedRenderPostReflow() {
const state = this.renderState;
try {
if(!state.error) {
this.renderer.optimisedRenderPostReflow(state.processed);
}
} catch(e) {
this._sendRenderError(e);
}
this.renderState = null;
if(!state.error) {
this._revertParent(state);
this.latestProcessed = state.processed;
this.trigger('render', [this]);
}
}
render(processed = null) {
let latestError = null;
function storeError(sd, e) {
latestError = e;
}
this.addEventListener('error', storeError);
this.optimisedRenderPreReflow(processed);
this.optimisedRenderReflow();
this.optimisedRenderPostReflow();
this.removeEventListener('error', storeError);
if(latestError) {
throw latestError;
}
}
setContainer(node = null) {
const dom = this.dom();
if(dom.parentNode) {
dom.parentNode.removeChild(dom);
}
if(node) {
node.appendChild(dom);
}
}
addInteractivity() {
if(this.isInteractive) {
return;
}
this.isInteractive = true;
this.addEventListener('click', (element) => {
this.toggleCollapsed(element.ln);
});
}
extractCodeFromSVG(svg) {
return extractCodeFromSVG(svg);
}
renderAll(diagrams) {
return renderAll(diagrams);
}
dom() {
return this.renderer.dom();
}
}
function datasetBoolean(value) {
return typeof value !== 'undefined' && value !== 'false';
}
function parseTagOptions(element) {
return {
interactive: datasetBoolean(element.dataset.sdInteractive),
namespace: element.dataset.sdNamespace || null,
};
}
function convertOne(element, code = null, options = {}) {
if(element.tagName === 'svg') {
return null;
}
const tagOptions = parseTagOptions(element);
const diagram = new SequenceDiagram(
(code === null) ? element.textContent : code,
Object.assign(tagOptions, options)
);
const newElement = diagram.dom();
const attrs = element.attributes;
for(let i = 0; i < attrs.length; ++ i) {
newElement.setAttribute(
attrs[i].nodeName,
attrs[i].nodeValue
);
}
element.parentNode.replaceChild(newElement, element);
return diagram;
}
function convert(elements, code = null, options = {}) {
let c = null;
let opts = null;
if(code && typeof code === 'object') {
opts = code;
c = opts.code;
} else {
opts = options;
c = code;
}
if(Array.isArray(elements)) {
const nodrawOpts = Object.assign({}, opts, {render: false});
const diagrams = elements.map((el) => convertOne(el, c, nodrawOpts));
if(opts.render !== false) {
renderAll(diagrams);
}
return diagrams;
} else {
return convertOne(elements, c, opts);
}
}
function convertAll(root = null, className = 'sequence-diagram') {
let r = null;
let cls = null;
if(typeof root === 'string') {
r = null;
cls = root;
} else {
r = root;
cls = className;
}
let elements = null;
if(r && typeof r.length !== 'undefined') {
elements = r;
} else {
elements = (r || window.document).getElementsByClassName(cls);
}
// Convert from "live" collection to static to avoid infinite loops:
const els = [];
for(let i = 0; i < elements.length; ++ i) {
els.push(elements[i]);
}
// Convert elements
convert(els);
}
function getDefaultThemeNames() {
return themes.map((theme) => theme.name);
}
Object.assign(SequenceDiagram, {
Exporter,
Generator,
Parser,
Renderer,
addTheme,
convert,
convertAll,
extractCodeFromSVG,
getDefaultThemeNames,
registerCodeMirrorMode,
renderAll,
themes,
});