Add commandline tool for generating SVG from code [#32]
This commit is contained in:
parent
6d4a620800
commit
edc71934dc
|
@ -16,6 +16,9 @@
|
|||
<FileRef
|
||||
location = "container:web">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "container:bin">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "container:.gitignore">
|
||||
</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">
|
||||
|
||||
<script src="lib/sequence-diagram.min.js"></script>
|
||||
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||
|
||||
<script
|
||||
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,
|
||||
});
|
||||
|
||||
const def = window.define;
|
||||
if(def && def.amd) {
|
||||
def(() => SequenceDiagram);
|
||||
function encodeChar(c) {
|
||||
return '&#' + c.charCodeAt(0).toString(10) + ';';
|
||||
}
|
||||
|
||||
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 {
|
||||
window.document.addEventListener('DOMContentLoaded', () => {
|
||||
SequenceDiagram.convertAll();
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -57,7 +57,7 @@
|
|||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<script src="lib/sequence-diagram.min.js"></script>
|
||||
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||
|
||||
<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>
|
||||
|
||||
<pre data-lang="text/html">
|
||||
<script src="lib/sequence-diagram.min.js"></script>
|
||||
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||
</pre>
|
||||
|
||||
<p>Or if you are using requirejs:</p>
|
||||
|
||||
<pre data-lang="javascript">
|
||||
requirejs(['lib/sequence-diagram.min'], (SequenceDiagram) => {
|
||||
requirejs(['lib/sequence-diagram-web.min'], (SequenceDiagram) => {
|
||||
SequenceDiagram.convertAll();
|
||||
});
|
||||
</pre>
|
||||
|
|
11
package.json
11
package.json
|
@ -12,15 +12,22 @@
|
|||
},
|
||||
"license": "LGPL-3.0",
|
||||
"files": [
|
||||
"lib/sequence-diagram.js"
|
||||
"lib/sequence-diagram-web.js",
|
||||
"lib/sequence-diagram.js",
|
||||
"scripts",
|
||||
"bin"
|
||||
],
|
||||
"main": "lib/sequence-diagram",
|
||||
"module": "scripts/standalone",
|
||||
"bin": {
|
||||
"sequence-diagram-svg": "./bin/sequence-diagram-svg.js"
|
||||
},
|
||||
"scripts": {
|
||||
"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": "npm run minify-lib && npm run minify-web",
|
||||
"prepublishOnly": "npm run minify-lib",
|
||||
"start": "http-server",
|
||||
"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",
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
export default [
|
||||
{
|
||||
input: 'scripts/standalone-web.mjs',
|
||||
output: {
|
||||
file: 'lib/sequence-diagram-web.js',
|
||||
format: 'iife',
|
||||
name: 'SequenceDiagram',
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'scripts/standalone.mjs',
|
||||
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 {VirtualDocument} from './core/documents/VirtualDocument.mjs';
|
||||
|
||||
const def = window.define;
|
||||
if(def && def.amd) {
|
||||
def(() => SequenceDiagram);
|
||||
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 {
|
||||
window.document.addEventListener('DOMContentLoaded', () => {
|
||||
SequenceDiagram.convertAll();
|
||||
|
|
|
@ -40,6 +40,7 @@ module.exports = (config) => {
|
|||
{included: false, pattern: 'scripts/**/*'},
|
||||
{included: false, pattern: 'spec/**/*'},
|
||||
{included: false, pattern: 'web/**/*'},
|
||||
{included: false, pattern: 'bin/**/*'},
|
||||
{included: false, pattern: 'README.md'},
|
||||
],
|
||||
frameworks: ['detectBrowsers', 'jasmine'],
|
||||
|
|
|
@ -7,6 +7,7 @@ export default [
|
|||
'scripts/**/*_spec.mjs',
|
||||
'spec/**/*_spec.mjs',
|
||||
'web/**/*_spec.mjs',
|
||||
'bin/**/*_spec.mjs',
|
||||
],
|
||||
output: {
|
||||
file: 'ephemeral/spec_bundle.js',
|
||||
|
|
Loading…
Reference in New Issue