Add commandline tool for generating SVG from code [#32]
This commit is contained in:
parent
6d4a620800
commit
edc71934dc
|
@ -16,6 +16,9 @@
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "container:web">
|
location = "container:web">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "container:bin">
|
||||||
|
</FileRef>
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "container:.gitignore">
|
location = "container:.gitignore">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {
|
||||||
|
headlessTextSizerFactory,
|
||||||
|
SequenceDiagram,
|
||||||
|
VirtualDocument,
|
||||||
|
} = require('../lib/sequence-diagram');
|
||||||
|
|
||||||
|
function render(code) {
|
||||||
|
const sd = new SequenceDiagram({
|
||||||
|
code,
|
||||||
|
document: new VirtualDocument(),
|
||||||
|
namespace: '',
|
||||||
|
textSizerFactory: headlessTextSizerFactory,
|
||||||
|
});
|
||||||
|
|
||||||
|
return sd.dom().outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(pipe) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let all = '';
|
||||||
|
pipe.on('readable', () => {
|
||||||
|
const chunk = pipe.read();
|
||||||
|
if(chunk !== null) {
|
||||||
|
all += chunk;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pipe.on('end', () => {
|
||||||
|
resolve(all);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodeArg() {
|
||||||
|
if(process.argv.length > 2 && process.argv[2] !== '-') {
|
||||||
|
return Promise.resolve(process.argv[2]);
|
||||||
|
} else {
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
return read(process.stdin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCodeArg().then((code) => {
|
||||||
|
process.stdout.write(render(code) + '\n');
|
||||||
|
});
|
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles/editor.css">
|
<link rel="stylesheet" href="styles/editor.css">
|
||||||
|
|
||||||
<script src="lib/sequence-diagram.min.js"></script>
|
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
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"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -9950,9 +9950,285 @@
|
||||||
themes,
|
themes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const def = window.define;
|
function encodeChar(c) {
|
||||||
if(def && def.amd) {
|
return '&#' + c.charCodeAt(0).toString(10) + ';';
|
||||||
def(() => SequenceDiagram);
|
}
|
||||||
|
|
||||||
|
function escapeHTML(text) {
|
||||||
|
return text.replace(/[^\r\n\t -%'-;=?-~]/g, encodeChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeQuoted(text) {
|
||||||
|
return text.replace(/[^\r\n\t !#$%(-;=?-~]/g, encodeChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextNode {
|
||||||
|
constructor(content) {
|
||||||
|
this.parentNode = null;
|
||||||
|
this.nodeValue = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
contains() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get textContent() {
|
||||||
|
return this.nodeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
set textContent(value) {
|
||||||
|
this.nodeValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConnected() {
|
||||||
|
if(this.parentNode !== null) {
|
||||||
|
return this.parentNode.isConnected;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get innerHTML() {
|
||||||
|
return escapeHTML(this.nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
get outerHTML() {
|
||||||
|
return this.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElementNode {
|
||||||
|
constructor(ownerDocument, tag, namespace) {
|
||||||
|
this.ownerDocument = ownerDocument;
|
||||||
|
this.tagName = tag;
|
||||||
|
this.namespaceURI = namespace;
|
||||||
|
this.parentNode = null;
|
||||||
|
this.childNodes = [];
|
||||||
|
this.attributes = new Map();
|
||||||
|
this.listeners = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribute(key, value) {
|
||||||
|
let v = null;
|
||||||
|
if(typeof value === 'number') {
|
||||||
|
v = value.toString(10);
|
||||||
|
} else if(typeof value === 'string') {
|
||||||
|
v = value;
|
||||||
|
} else {
|
||||||
|
throw new Error('Bad value ' + value + ' for attribute ' + key);
|
||||||
|
}
|
||||||
|
this.attributes.set(key, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(key) {
|
||||||
|
return this.attributes.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(event, fn) {
|
||||||
|
let list = this.listeners.get(event);
|
||||||
|
if(!list) {
|
||||||
|
list = [];
|
||||||
|
this.listeners.set(event, list);
|
||||||
|
}
|
||||||
|
list.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(event, fn) {
|
||||||
|
const list = this.listeners.get(event) || [];
|
||||||
|
const index = list.indexOf(fn);
|
||||||
|
if(index !== -1) {
|
||||||
|
list.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(e) {
|
||||||
|
const list = this.listeners.get(e.type) || [];
|
||||||
|
list.forEach((fn) => fn(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(descendant) {
|
||||||
|
let check = descendant;
|
||||||
|
while(check) {
|
||||||
|
if(check === this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
check = check.parentNode;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementsByTagName(tag) {
|
||||||
|
const result = [];
|
||||||
|
this.traverseDescendants((o) => {
|
||||||
|
if(o.tagName === tag) {
|
||||||
|
result.push(o);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementsByClassName(className) {
|
||||||
|
const result = [];
|
||||||
|
const check = ' ' + className + ' ';
|
||||||
|
this.traverseDescendants((o) => {
|
||||||
|
const cls = ' ' + (o.getAttribute('class') || '') + ' ';
|
||||||
|
if(cls.indexOf(check) !== -1) {
|
||||||
|
result.push(o);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseDescendants(fn) {
|
||||||
|
if(fn(this) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for(const child of this.childNodes) {
|
||||||
|
if(child.traverseDescendants) {
|
||||||
|
child.traverseDescendants(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get firstChild() {
|
||||||
|
return this.childNodes[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastChild() {
|
||||||
|
return this.childNodes[this.childNodes.length - 1] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexOf(child) {
|
||||||
|
const index = this.childNodes.indexOf(child);
|
||||||
|
if(index === -1) {
|
||||||
|
throw new Error(child + ' is not a child of ' + this);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertBefore(child, existingChild) {
|
||||||
|
if(child.contains(this)) {
|
||||||
|
throw new Error('Cyclic node structures are not permitted');
|
||||||
|
}
|
||||||
|
if(child.parentNode !== null) {
|
||||||
|
child.parentNode.removeChild(child);
|
||||||
|
}
|
||||||
|
if(existingChild === null) {
|
||||||
|
this.childNodes.push(child);
|
||||||
|
} else {
|
||||||
|
this.childNodes.splice(this.indexOf(existingChild), 0, child);
|
||||||
|
}
|
||||||
|
child.parentNode = this;
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChild(child) {
|
||||||
|
return this.insertBefore(child, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChild(child) {
|
||||||
|
this.childNodes.splice(this.indexOf(child), 1);
|
||||||
|
child.parentNode = null;
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceChild(newChild, oldChild) {
|
||||||
|
if(newChild === oldChild) {
|
||||||
|
return oldChild;
|
||||||
|
}
|
||||||
|
this.insertBefore(newChild, oldChild);
|
||||||
|
return this.removeChild(oldChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConnected() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get textContent() {
|
||||||
|
let text = '';
|
||||||
|
for(const child of this.childNodes) {
|
||||||
|
text += child.textContent;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
set textContent(value) {
|
||||||
|
for(const child of this.childNodes) {
|
||||||
|
child.parentNode = null;
|
||||||
|
}
|
||||||
|
this.childNodes.length = 0;
|
||||||
|
this.appendChild(new TextNode(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
get innerHTML() {
|
||||||
|
let html = '';
|
||||||
|
for(const child of this.childNodes) {
|
||||||
|
html += child.outerHTML;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
get outerHTML() {
|
||||||
|
let attrs = '';
|
||||||
|
for(const [key, value] of this.attributes) {
|
||||||
|
attrs += ' ' + key + '="' + escapeQuoted(value) + '"';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'<' + this.tagName + attrs + '>' +
|
||||||
|
this.innerHTML +
|
||||||
|
'</' + this.tagName + '>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VirtualDocument {
|
||||||
|
createElement(tag) {
|
||||||
|
return new ElementNode(this, tag, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
createElementNS(ns, tag) {
|
||||||
|
return new ElementNode(this, tag, ns || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
createTextNode(content) {
|
||||||
|
return new TextNode(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnitaryTextSizer {
|
||||||
|
// Simplified text sizer, which assumes all characters render as
|
||||||
|
// 1x1 px squares for repeatable renders in all browsers
|
||||||
|
|
||||||
|
baseline() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
measureHeight({formatted}) {
|
||||||
|
return formatted.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepMeasurement(attrs, formatted) {
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepComplete() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
performMeasurement(data) {
|
||||||
|
return data.reduce((total, part) => total + part.text.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof exports !== 'undefined') {
|
||||||
|
exports.SequenceDiagram = SequenceDiagram;
|
||||||
|
exports.VirtualDocument = VirtualDocument;
|
||||||
|
exports.headlessTextSizerFactory = () => new UnitaryTextSizer();
|
||||||
|
} else if(window.define && window.define.amd) {
|
||||||
|
window.define(() => SequenceDiagram);
|
||||||
} else {
|
} else {
|
||||||
window.document.addEventListener('DOMContentLoaded', () => {
|
window.document.addEventListener('DOMContentLoaded', () => {
|
||||||
SequenceDiagram.convertAll();
|
SequenceDiagram.convertAll();
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -57,7 +57,7 @@
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
<script src="lib/sequence-diagram.min.js"></script>
|
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||||
|
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
@ -125,13 +125,13 @@ This library renders sequence diagrams from code. It is
|
||||||
(LGPL-3.0), and including it in a website is as simple as adding the script:</p>
|
(LGPL-3.0), and including it in a website is as simple as adding the script:</p>
|
||||||
|
|
||||||
<pre data-lang="text/html">
|
<pre data-lang="text/html">
|
||||||
<script src="lib/sequence-diagram.min.js"></script>
|
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<p>Or if you are using requirejs:</p>
|
<p>Or if you are using requirejs:</p>
|
||||||
|
|
||||||
<pre data-lang="javascript">
|
<pre data-lang="javascript">
|
||||||
requirejs(['lib/sequence-diagram.min'], (SequenceDiagram) => {
|
requirejs(['lib/sequence-diagram-web.min'], (SequenceDiagram) => {
|
||||||
SequenceDiagram.convertAll();
|
SequenceDiagram.convertAll();
|
||||||
});
|
});
|
||||||
</pre>
|
</pre>
|
||||||
|
|
11
package.json
11
package.json
|
@ -12,15 +12,22 @@
|
||||||
},
|
},
|
||||||
"license": "LGPL-3.0",
|
"license": "LGPL-3.0",
|
||||||
"files": [
|
"files": [
|
||||||
"lib/sequence-diagram.js"
|
"lib/sequence-diagram-web.js",
|
||||||
|
"lib/sequence-diagram.js",
|
||||||
|
"scripts",
|
||||||
|
"bin"
|
||||||
],
|
],
|
||||||
"main": "lib/sequence-diagram",
|
"main": "lib/sequence-diagram",
|
||||||
"module": "scripts/standalone",
|
"module": "scripts/standalone",
|
||||||
|
"bin": {
|
||||||
|
"sequence-diagram-svg": "./bin/sequence-diagram-svg.js"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --config spec/support/eslintrc.js --ignore-path spec/support/eslintignore --ext .js --ext .mjs",
|
"lint": "eslint . --config spec/support/eslintrc.js --ignore-path spec/support/eslintignore --ext .js --ext .mjs",
|
||||||
"minify-lib": "rollup --config scripts/rollup.config.js && uglifyjs --compress --mangle --warn --output lib/sequence-diagram.min.js -- lib/sequence-diagram.js",
|
"minify-lib": "rollup --config scripts/rollup.config.js && uglifyjs --compress --mangle --warn --output lib/sequence-diagram-web.min.js -- lib/sequence-diagram-web.js && uglifyjs --compress --mangle --warn --output lib/sequence-diagram.min.js -- lib/sequence-diagram.js",
|
||||||
"minify-web": "rollup --config web/rollup.config.js && uglifyjs --compress --mangle --warn --output weblib/editor.min.js -- weblib/editor.js",
|
"minify-web": "rollup --config web/rollup.config.js && uglifyjs --compress --mangle --warn --output weblib/editor.min.js -- weblib/editor.js",
|
||||||
"minify": "npm run minify-lib && npm run minify-web",
|
"minify": "npm run minify-lib && npm run minify-web",
|
||||||
|
"prepublishOnly": "npm run minify-lib",
|
||||||
"start": "http-server",
|
"start": "http-server",
|
||||||
"test": "npm run unit-test && npm run web-test && npm run lint && echo 'PASSED :)'",
|
"test": "npm run unit-test && npm run web-test && npm run lint && echo 'PASSED :)'",
|
||||||
"unit-test": "rollup --config spec/support/rollup.config.js && node -r source-map-support/register node_modules/.bin/jasmine --config=spec/support/jasmine.json",
|
"unit-test": "rollup --config spec/support/rollup.config.js && node -r source-map-support/register node_modules/.bin/jasmine --config=spec/support/jasmine.json",
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
export default [
|
export default [
|
||||||
|
{
|
||||||
|
input: 'scripts/standalone-web.mjs',
|
||||||
|
output: {
|
||||||
|
file: 'lib/sequence-diagram-web.js',
|
||||||
|
format: 'iife',
|
||||||
|
name: 'SequenceDiagram',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: 'scripts/standalone.mjs',
|
input: 'scripts/standalone.mjs',
|
||||||
output: {
|
output: {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import SequenceDiagram from './sequence/SequenceDiagram.mjs';
|
||||||
|
|
||||||
|
if(typeof exports !== 'undefined') {
|
||||||
|
exports.SequenceDiagram = SequenceDiagram;
|
||||||
|
} else if(window.define && window.define.amd) {
|
||||||
|
window.define(() => SequenceDiagram);
|
||||||
|
} else {
|
||||||
|
window.document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
SequenceDiagram.convertAll();
|
||||||
|
}, {once: true});
|
||||||
|
|
||||||
|
if(window.CodeMirror) {
|
||||||
|
SequenceDiagram.registerCodeMirrorMode(window.CodeMirror);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SequenceDiagram = SequenceDiagram;
|
||||||
|
}
|
|
@ -1,8 +1,41 @@
|
||||||
import SequenceDiagram from './sequence/SequenceDiagram.mjs';
|
import SequenceDiagram from './sequence/SequenceDiagram.mjs';
|
||||||
|
import {VirtualDocument} from './core/documents/VirtualDocument.mjs';
|
||||||
|
|
||||||
const def = window.define;
|
class UnitaryTextSizer {
|
||||||
if(def && def.amd) {
|
// Simplified text sizer, which assumes all characters render as
|
||||||
def(() => SequenceDiagram);
|
// 1x1 px squares for repeatable renders in all browsers
|
||||||
|
|
||||||
|
baseline() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
measureHeight({formatted}) {
|
||||||
|
return formatted.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepMeasurement(attrs, formatted) {
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepComplete() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
performMeasurement(data) {
|
||||||
|
return data.reduce((total, part) => total + part.text.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof exports !== 'undefined') {
|
||||||
|
exports.SequenceDiagram = SequenceDiagram;
|
||||||
|
exports.VirtualDocument = VirtualDocument;
|
||||||
|
exports.headlessTextSizerFactory = () => new UnitaryTextSizer();
|
||||||
|
} else if(window.define && window.define.amd) {
|
||||||
|
window.define(() => SequenceDiagram);
|
||||||
} else {
|
} else {
|
||||||
window.document.addEventListener('DOMContentLoaded', () => {
|
window.document.addEventListener('DOMContentLoaded', () => {
|
||||||
SequenceDiagram.convertAll();
|
SequenceDiagram.convertAll();
|
||||||
|
|
|
@ -40,6 +40,7 @@ module.exports = (config) => {
|
||||||
{included: false, pattern: 'scripts/**/*'},
|
{included: false, pattern: 'scripts/**/*'},
|
||||||
{included: false, pattern: 'spec/**/*'},
|
{included: false, pattern: 'spec/**/*'},
|
||||||
{included: false, pattern: 'web/**/*'},
|
{included: false, pattern: 'web/**/*'},
|
||||||
|
{included: false, pattern: 'bin/**/*'},
|
||||||
{included: false, pattern: 'README.md'},
|
{included: false, pattern: 'README.md'},
|
||||||
],
|
],
|
||||||
frameworks: ['detectBrowsers', 'jasmine'],
|
frameworks: ['detectBrowsers', 'jasmine'],
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default [
|
||||||
'scripts/**/*_spec.mjs',
|
'scripts/**/*_spec.mjs',
|
||||||
'spec/**/*_spec.mjs',
|
'spec/**/*_spec.mjs',
|
||||||
'web/**/*_spec.mjs',
|
'web/**/*_spec.mjs',
|
||||||
|
'bin/**/*_spec.mjs',
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
file: 'ephemeral/spec_bundle.js',
|
file: 'ephemeral/spec_bundle.js',
|
||||||
|
|
Loading…
Reference in New Issue