Provide simplified SequenceDiagram API

This commit is contained in:
David Evans 2017-11-23 22:41:24 +00:00
parent 25a01fea6b
commit 1bf6ad6b5b
12 changed files with 306 additions and 180 deletions

View File

@ -14,12 +14,12 @@
<title>Readme Image Generator</title>
<link rel="icon" href="favicon.png">
<link rel="stylesheet" href="styles/readme_images.css">
<link rel="stylesheet" href="styles/readmeImages.css">
<script src="scripts/requireConfig.js"></script>
<script
data-main="scripts/readme_images"
data-main="scripts/readmeImages"
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"
integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk="
crossorigin="anonymous"

View File

@ -43,23 +43,19 @@ define([
return class Interface {
constructor({
parser,
generator,
renderer,
exporter,
sequenceDiagram,
defaultCode = '',
localStorage = '',
library = null,
}) {
this.parser = parser;
this.generator = generator;
this.renderer = renderer;
this.exporter = exporter;
this.diagram = sequenceDiagram;
this.defaultCode = defaultCode;
this.localStorage = localStorage;
this.library = library;
this.minScale = 1.5;
this.diagram.registerCodeMirrorMode(CodeMirror);
this.debounced = null;
this.latestSeq = null;
this.renderedSeq = null;
@ -114,20 +110,11 @@ define([
buildEditor(container) {
const value = this.loadCode() || this.defaultCode;
CodeMirror.defineMode(
'sequence',
() => this.parser.getCodeMirrorMode()
);
CodeMirror.registerHelper(
'hint',
'sequence',
this.parser.getCodeMirrorHints()
);
const code = new CodeMirror(container, {
value,
mode: 'sequence',
globals: {
themes: this.renderer.getThemeNames(),
themes: this.diagram.getThemeNames(),
},
lineNumbers: true,
showTrailingSpace: true,
@ -167,10 +154,10 @@ define([
this.code.on('cursorActivity', () => {
const from = this.code.getCursor('from').line;
const to = this.code.getCursor('to').line;
this.renderer.setHighlight(Math.min(from, to));
this.diagram.setHighlight(Math.min(from, to));
});
this.renderer.addEventListener('mouseover', (element) => {
this.diagram.addEventListener('mouseover', (element) => {
if(this.marker) {
this.marker.clear();
}
@ -188,14 +175,14 @@ define([
}
});
this.renderer.addEventListener('mouseout', () => {
this.diagram.addEventListener('mouseout', () => {
if(this.marker) {
this.marker.clear();
this.marker = null;
}
});
this.renderer.addEventListener('click', (element) => {
this.diagram.addEventListener('click', (element) => {
if(this.marker) {
this.marker.clear();
this.marker = null;
@ -226,12 +213,10 @@ define([
);
container.appendChild(hold);
try {
const preview = simplifyPreview(lib.preview || lib.code);
const parsed = this.parser.parse(preview);
const generated = this.generator.generate(parsed);
const rendering = this.renderer.clone();
holdInner.appendChild(rendering.svg());
rendering.render(generated);
this.diagram.clone({
code: simplifyPreview(lib.preview || lib.code),
container: holdInner,
});
} catch(e) {
hold.setAttribute('class', 'library-item broken');
holdInner.appendChild(makeText(lib.code));
@ -286,7 +271,7 @@ define([
container.appendChild(this.buildViewPane());
this.code = this.buildEditor(codePane);
this.viewPaneInner.appendChild(this.renderer.svg());
this.viewPaneInner.appendChild(this.diagram.dom());
this.registerListeners();
this.update();
@ -306,7 +291,7 @@ define([
this.code.focus();
}
updateMinSize(width, height) {
updateMinSize({width, height}) {
const style = this.viewPaneInner.style;
style.minWidth = Math.ceil(width * this.minScale) + 'px';
style.minHeight = Math.ceil(height * this.minScale) + 'px';
@ -317,8 +302,8 @@ define([
this.debounced = null;
this.pngDirty = true;
this.renderedSeq = sequence;
this.renderer.render(sequence);
this.updateMinSize(this.renderer.width, this.renderer.height);
this.diagram.render(sequence);
this.updateMinSize(this.diagram.getSize());
}
saveCode(src) {
@ -362,8 +347,7 @@ define([
this.saveCode(src);
let sequence = null;
try {
const parsed = this.parser.parse(src);
sequence = this.generator.generate(parsed);
sequence = this.diagram.process(src);
} catch(e) {
this.markError(e);
return;
@ -403,16 +387,13 @@ define([
}
this.pngDirty = false;
this.updatingPNG = true;
this.exporter.getPNGURL(
this.renderer,
PNG_RESOLUTION,
(url, latest) => {
this.diagram.getPNG({resolution: PNG_RESOLUTION})
.then(({url, latest}) => {
if(latest) {
this.downloadPNG.setAttribute('href', url);
this.updatingPNG = false;
}
}
);
});
return true;
}
@ -430,7 +411,7 @@ define([
_downloadSVGClick() {
this.forceRender();
const url = this.exporter.getSVGURL(this.renderer);
const url = this.diagram.getSVGSynchronous();
this.downloadSVG.setAttribute('href', url);
}
};

View File

@ -4,44 +4,33 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
// Thanks, https://stackoverflow.com/a/23522755/1180785
const safari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
let parser = null;
let generator = null;
let renderer = null;
let exporter = null;
let sequenceDiagram = null;
let container = null;
let ui = null;
beforeEach(() => {
parser = jasmine.createSpyObj('parser', [
'parse',
'getCodeMirrorMode',
'getCodeMirrorHints',
sequenceDiagram = jasmine.createSpyObj('sequenceDiagram', [
'dom',
'render',
'clone',
'getSize',
'process',
'getThemeNames',
'addEventListener',
'registerCodeMirrorMode',
'getSVGSynchronous',
]);
parser.parse.and.returnValue({
meta: {},
stages: [],
});
generator = jasmine.createSpyObj('generator', ['generate']);
generator.generate.and.returnValue({
sequenceDiagram.process.and.returnValue({
meta: {},
agents: [],
stages: [],
});
renderer = jasmine.createSpyObj('renderer', [
'render',
'svg',
'getThemeNames',
'addEventListener',
]);
renderer.svg.and.returnValue(document.createElement('svg'));
sequenceDiagram.getSize.and.returnValue({width: 10, height: 20});
sequenceDiagram.dom.and.returnValue(document.createElement('svg'));
container = jasmine.createSpyObj('container', ['appendChild']);
exporter = jasmine.createSpyObj('exporter', ['getSVGURL']);
ui = new Interface({
parser,
generator,
renderer,
exporter,
sequenceDiagram,
defaultCode: 'my default code',
});
});
@ -61,7 +50,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
describe('download SVG', () => {
it('triggers a download of the current image in SVG format', () => {
exporter.getSVGURL.and.returnValue('mySVGURL');
sequenceDiagram.getSVGSynchronous.and.returnValue('mySVGURL');
ui.build(container);
expect(ui.downloadSVG.getAttribute('href')).toEqual('#');

View File

@ -3,25 +3,13 @@
requirejs.config(window.getRequirejsCDN());
/* jshint -W072 */ // Allow several required modules
requirejs([
'interface/Interface',
'interface/Exporter',
'sequence/Parser',
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
'sequence/themes/Chunky',
'sequence/SequenceDiagram',
], (
Interface,
Exporter,
Parser,
Generator,
Renderer,
BasicTheme,
ChunkyTheme
SequenceDiagram
) => {
/* jshint +W072 */
const defaultCode = (
'title Labyrinth\n' +
'\n' +
@ -236,13 +224,7 @@
];
const ui = new Interface({
defaultCode,
parser: new Parser(),
generator: new Generator(),
renderer: new Renderer({themes: [
new BasicTheme(),
new ChunkyTheme(),
]}),
exporter: new Exporter(),
sequenceDiagram: new SequenceDiagram(),
library,
localStorage: 'src',
});

View File

@ -63,42 +63,15 @@
}
}
/* jshint -W072 */ // Allow several required modules
requirejs([
'sequence/Parser',
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
'sequence/themes/Chunky',
'interface/Exporter',
], (
Parser,
Generator,
Renderer,
BasicTheme,
ChunkyTheme,
Exporter
) => {
/* jshint +W072 */
const parser = new Parser();
const generator = new Generator();
const themes = [
new BasicTheme(),
new ChunkyTheme(),
];
requirejs(['sequence/SequenceDiagram'], (SequenceDiagram) => {
const status = makeNode('div', {'class': 'status'});
const statusText = makeText('Loading\u2026');
status.appendChild(statusText);
document.body.appendChild(status);
function renderSample({file, code, size}) {
const renderer = new Renderer({themes});
const exporter = new Exporter();
const hold = makeNode('div', {'class': 'hold'});
hold.appendChild(renderer.svg());
const diagram = new SequenceDiagram(code, {container: hold});
const raster = makeNode('img', {
'src': '',
@ -122,14 +95,7 @@
document.body.appendChild(hold);
const parsed = parser.parse(code);
const sequence = generator.generate(parsed);
renderer.render(sequence);
if(size) {
renderer.width = size.width;
renderer.height = size.height;
}
exporter.getPNGURL(renderer, PNG_RESOLUTION, (url) => {
diagram.getPNG({resolution: PNG_RESOLUTION, size}).then(({url}) => {
raster.setAttribute('src', url);
downloadPNG.setAttribute('href', url);
});

View File

@ -98,13 +98,8 @@ define([
});
}
clone({namespace = null} = {}) {
return new Renderer({
themes: this.getThemes(),
namespace,
components: this.components,
SVGTextBlockClass: this.SVGTextBlockClass,
});
addTheme(theme) {
this.themes.set(theme.name, theme);
}
buildStaticElements() {

View File

@ -0,0 +1,217 @@
/* jshint -W072 */ // Allow several required modules
define([
'core/EventObject',
'./Parser',
'./Generator',
'./Renderer',
'./Exporter',
'./themes/Basic',
'./themes/Chunky',
], (
EventObject,
Parser,
Generator,
Renderer,
Exporter,
BasicTheme,
ChunkyTheme
) => {
/* jshint +W072 */
'use strict';
const themes = [
new BasicTheme(),
new ChunkyTheme(),
];
const SharedParser = new Parser();
const SharedGenerator = new Generator();
const CMMode = SharedParser.getCodeMirrorMode();
const CMHints = SharedParser.getCodeMirrorHints();
function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') {
if(!CodeMirror) {
CodeMirror = window.CodeMirror;
}
CodeMirror.defineMode(modeName, () => CMMode);
CodeMirror.registerHelper('hint', modeName, CMHints);
}
function addTheme(theme) {
themes.push(theme);
}
class SequenceDiagram extends EventObject {
constructor(code = null, options = {}) {
super();
if(code && typeof code === 'object') {
options = code;
code = options.code;
}
this.registerCodeMirrorMode = registerCodeMirrorMode;
this.code = code;
this.parser = SharedParser;
this.generator = SharedGenerator;
this.renderer = new Renderer(Object.assign({themes}, options));
this.exporter = new Exporter();
this.renderer.addEventForwarding(this);
if(options.container) {
options.container.appendChild(this.dom());
}
if(typeof this.code === 'string') {
this.render();
}
}
clone(options = {}) {
return new SequenceDiagram(Object.assign({
code: this.code,
container: null,
themes: this.renderer.getThemes(),
namespace: null,
components: this.renderer.components,
SVGTextBlockClass: this.renderer.SVGTextBlockClass,
}, options));
}
set(code = '') {
if(this.code === code) {
return;
}
this.code = code;
this.render();
}
process(code) {
const parsed = this.parser.parse(code);
return this.generator.generate(parsed);
}
addTheme(theme) {
this.renderer.addTheme(theme);
}
setHighlight(line = null) {
this.renderer.setHighlight(line);
}
getThemeNames() {
return this.renderer.getThemeNames();
}
getThemes() {
return this.renderer.getThemes();
}
getSVGSynchronous() {
return this.exporter.getSVGURL(this.renderer);
}
getSVG() {
return Promise.resolve({
url: this.getSVGSynchronous(),
latest: true,
});
}
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({url, latest});
}
);
});
}
getSize() {
return {
width: this.renderer.width,
height: this.renderer.height,
};
}
render(processed = null) {
const dom = this.renderer.svg();
const originalParent = dom.parentNode;
if(!document.body.contains(dom)) {
if(originalParent) {
originalParent.removeChild(dom);
}
document.body.appendChild(dom);
}
try {
if(!processed) {
processed = this.process(this.code);
}
this.renderer.render(processed);
} finally {
if(dom.parentNode !== originalParent) {
document.body.removeChild(dom);
if(originalParent) {
originalParent.appendChild(dom);
}
}
}
}
setContainer(node = null) {
const dom = this.dom();
if(dom.parentNode) {
dom.parentNode.removeChild(dom);
}
if(node) {
node.appendChild(dom);
}
}
dom() {
return this.renderer.svg();
}
}
function convert(element, code = null, options = {}) {
if(code === null) {
code = element.innerText;
} else if(typeof code === 'object') {
options = code;
code = options.code;
}
const diagram = new SequenceDiagram(code, options);
element.parentNode.insertBefore(diagram.dom(), element);
element.parentNode.removeChild(element);
return diagram;
}
function convertAll(root = null) {
if(!root) {
root = document;
}
const elements = root.getElementsByClassName('sequence-diagram');
for(let i = 0; i < elements.length; ++ i) {
convert(elements[i]);
}
}
return Object.assign(SequenceDiagram, {
Parser,
Generator,
Renderer,
Exporter,
themes,
addTheme,
registerCodeMirrorMode,
convert,
convertAll,
});
});

View File

@ -1,53 +1,53 @@
/* jshint -W072 */ // Allow several required modules
defineDescribe('Sequence Integration', [
defineDescribe('SequenceDiagram', [
'./SequenceDiagram',
'./Parser',
'./Generator',
'./Renderer',
'./themes/Basic',
'./Exporter',
'stubs/SVGTextBlock',
], (
SequenceDiagram,
Parser,
Generator,
Renderer,
BasicTheme,
Exporter,
SVGTextBlock
) => {
/* jshint +W072 */
'use strict';
let parser = null;
let generator = null;
let renderer = null;
let theme = null;
beforeEach(() => {
theme = new BasicTheme();
parser = new Parser();
generator = new Generator();
renderer = new Renderer({
themes: [new BasicTheme()],
namespace: '',
SVGTextBlockClass: SVGTextBlock,
});
document.body.appendChild(renderer.svg());
});
afterEach(() => {
document.body.removeChild(renderer.svg());
});
function getSimplifiedContent(r) {
return (r.svg().outerHTML
function getSimplifiedContent(d) {
return (d.dom().outerHTML
.replace(/<g><\/g>/g, '')
.replace(' xmlns="http://www.w3.org/2000/svg" version="1.1"', '')
);
}
it('Renders empty diagrams without error', () => {
const parsed = parser.parse('');
const sequence = generator.generate(parsed);
renderer.render(sequence);
expect(getSimplifiedContent(renderer)).toEqual(
let diagram = null;
beforeEach(() => {
diagram = new SequenceDiagram({
namespace: '',
SVGTextBlockClass: SVGTextBlock,
});
});
it('contains references to core objects', () => {
expect(SequenceDiagram.Parser).toBe(Parser);
expect(SequenceDiagram.Generator).toBe(Generator);
expect(SequenceDiagram.Renderer).toBe(Renderer);
expect(SequenceDiagram.Exporter).toBe(Exporter);
});
it('provides default themes', () => {
expect(SequenceDiagram.themes.length).toEqual(2);
});
it('renders empty diagrams without error', () => {
diagram.set('');
expect(getSimplifiedContent(diagram)).toEqual(
'<svg viewBox="-5 -5 10 10">' +
'<defs>' +
'<mask id="LineMask" maskUnits="userSpaceOnUse">' +
@ -60,12 +60,10 @@ defineDescribe('Sequence Integration', [
);
});
it('Renders simple metadata', () => {
const parsed = parser.parse('title My title here');
const sequence = generator.generate(parsed);
renderer.render(sequence);
it('renders simple metadata', () => {
diagram.set('title My title here');
expect(getSimplifiedContent(renderer)).toEqual(
expect(getSimplifiedContent(diagram)).toEqual(
'<svg viewBox="-11.5 -16 23 21">' +
'<defs>' +
'<mask id="LineMask" maskUnits="userSpaceOnUse">' +
@ -86,12 +84,11 @@ defineDescribe('Sequence Integration', [
);
});
it('Renders simple components', () => {
const parsed = parser.parse('A -> B');
const sequence = generator.generate(parsed);
renderer.render(sequence);
it('renders simple components', () => {
diagram.set('A -> B');
const content = getSimplifiedContent(diagram);
const content = getSimplifiedContent(renderer);
expect(content).toContain(
'<svg viewBox="-5 -5 82 56">'
);
@ -149,9 +146,7 @@ defineDescribe('Sequence Integration', [
.then(findSamples)
.then((samples) => samples.forEach((code, i) => {
it('Renders readme example #' + (i + 1) + ' without error', () => {
const parsed = parser.parse(code);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
expect(() => diagram.set(code)).not.toThrow();
});
}))
);

View File

@ -5,6 +5,7 @@ define([
'svg/SVGTextBlock_spec',
'svg/SVGShapes_spec',
'interface/Interface_spec',
'sequence/SequenceDiagram_spec',
'sequence/Tokeniser_spec',
'sequence/Parser_spec',
'sequence/LabelPatternParser_spec',
@ -19,5 +20,4 @@ define([
'sequence/components/Marker_spec',
'sequence/components/Note_spec',
'sequence/components/Parallel_spec',
'sequence/sequence_integration_spec',
]);

View File

@ -8,6 +8,7 @@ define(['jshintConfig', 'specs'], (jshintConfig) => {
const extraFiles = [
'scripts/main.js',
'scripts/requireConfig.js',
'scripts/readmeImages.js',
];
const PREDEF = [