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> <title>Readme Image Generator</title>
<link rel="icon" href="favicon.png"> <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 src="scripts/requireConfig.js"></script>
<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" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"
integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk=" integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk="
crossorigin="anonymous" crossorigin="anonymous"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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