/
githubmirror
/
marktext
Обзор
Документация
Войти
/
githubmirror
/
marktext
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/muya/src/utils/diagram/sequence/sequence-diagram-snap.js
1 871 строка
53 KB
Ran Luo
feat(muya): restore flowchart and sequence diagrams (#4385)
08 июн 2026, 07:20
Не верифицирован
08 июн 2026, 07:20
3670009
Код
Авторство
О чём код?
/** js sequence diagrams 2.0.1 * https://bramp.github.io/js-sequence-diagrams/ * (c) 2012-2017 Andrew Brampton (bramp.net) * @license Simplified BSD license. */ import _ from 'underscore' import Snap from 'snapsvg-cjs' import WebFont from 'webfontloader' function Diagram() { this.title = undefined this.actors = [] this.signals = [] } /* * Return an existing actor with this alias, or creates a new one with alias and name. */ Diagram.prototype.getActor = function (alias, name) { alias = alias.trim() var i var actors = this.actors for (i in actors) { if (actors[i].alias == alias) { return actors[i] } } i = actors.push(new Diagram.Actor(alias, name || alias, actors.length)) return actors[i - 1] } /* * Parses the input as either a alias, or a "name as alias", and returns the corresponding actor. */ Diagram.prototype.getActorWithAlias = function (input) { input = input.trim() // We are lazy and do some of the parsing in javascript :(. TODO move into the .jison file. var s = /([\s\S]+) as (\S+)$/im.exec(input) var alias var name if (s) { name = s[1].trim() alias = s[2].trim() } else { name = alias = input } return this.getActor(alias, name) } Diagram.prototype.setTitle = function (title) { this.title = title } Diagram.prototype.addSignal = function (signal) { this.signals.push(signal) } Diagram.Actor = function (alias, name, index) { this.alias = alias this.name = name this.index = index } Diagram.Signal = function (actorA, signaltype, actorB, message) { this.type = 'Signal' this.actorA = actorA this.actorB = actorB this.linetype = signaltype & 3 this.arrowtype = (signaltype >> 2) & 3 this.message = message } Diagram.Signal.prototype.isSelf = function () { return this.actorA.index == this.actorB.index } Diagram.Note = function (actor, placement, message) { this.type = 'Note' this.actor = actor this.placement = placement this.message = message if (this.hasManyActors() && actor[0] == actor[1]) { throw new Error('Note should be over two different actors') } } Diagram.Note.prototype.hasManyActors = function () { return _.isArray(this.actor) } Diagram.unescape = function (s) { // Turn "\\n" into "\n" return s .trim() .replace(/^"(.*)"$/m, '$1') .replace(/\\n/gm, '\n') } Diagram.LINETYPE = { SOLID: 0, DOTTED: 1 } Diagram.ARROWTYPE = { FILLED: 0, OPEN: 1 } Diagram.PLACEMENT = { LEFTOF: 0, RIGHTOF: 1, OVER: 2 } // Some older browsers don't have getPrototypeOf, thus we polyfill it // https://github.com/bramp/js-sequence-diagrams/issues/57 // https://github.com/zaach/jison/issues/194 // Taken from http://ejohn.org/blog/objectgetprototypeof/ if (typeof Object.getPrototypeOf !== 'function') { /* jshint -W103 */ if (typeof 'test'.__proto__ === 'object') { Object.getPrototypeOf = function (object) { return object.__proto__ } } else { Object.getPrototypeOf = function (object) { // May break if the constructor has been tampered with return object.constructor.prototype } } /* jshint +W103 */ } /** The following is included by preprocessor */ /* parser generated by jison 0.4.15 */ /* Returns a Parser object of the following structure: Parser: { yy: {} } Parser.prototype: { yy: {}, trace: function(), symbols_: {associative list: name ==> number}, terminals_: {associative list: number ==> name}, productions_: [...], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), table: [...], defaultActions: {...}, parseError: function(str, hash), parse: function(input), lexer: { EOF: 1, parseError: function(str, hash), setInput: function(input), input: function(), unput: function(str), more: function(), less: function(n), pastInput: function(), upcomingInput: function(), showPosition: function(), test_match: function(regex_match_array, rule_index), next: function(), lex: function(), begin: function(condition), popState: function(), _currentRules: function(), topState: function(), pushState: function(condition), options: { ranges: boolean (optional: true ==> token location info will include a .range[] member) flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) }, performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), rules: [...], conditions: {associative list: name ==> set}, } } token location info (@$, _$, etc.): { first_line: n, last_line: n, first_column: n, last_column: n, range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) } the parseError function receives a 'hash' object with these members for lexer and parser errors: { text: (matched text) token: (the produced terminal token, if any) line: (yylineno) } while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { loc: (yylloc) expected: (string describing the set of expected tokens) recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ var parser = (function () { function Parser() { this.yy = {} } var o = function (k, v, o, l) { for (o = o || {}, l = k.length; l--; o[k[l]] = v); return o }, $V0 = [5, 8, 9, 13, 15, 24], $V1 = [1, 13], $V2 = [1, 17], $V3 = [24, 29, 30], parser = { trace: function () {}, yy: {}, symbols_: { error: 2, start: 3, document: 4, EOF: 5, line: 6, statement: 7, NL: 8, participant: 9, actor_alias: 10, signal: 11, note_statement: 12, title: 13, message: 14, note: 15, placement: 16, actor: 17, over: 18, actor_pair: 19, ',': 20, left_of: 21, right_of: 22, signaltype: 23, ACTOR: 24, linetype: 25, arrowtype: 26, LINE: 27, DOTLINE: 28, ARROW: 29, OPENARROW: 30, MESSAGE: 31, $accept: 0, $end: 1 }, terminals_: { 2: 'error', 5: 'EOF', 8: 'NL', 9: 'participant', 13: 'title', 15: 'note', 18: 'over', 20: ',', 21: 'left_of', 22: 'right_of', 24: 'ACTOR', 27: 'LINE', 28: 'DOTLINE', 29: 'ARROW', 30: 'OPENARROW', 31: 'MESSAGE' }, productions_: [ 0, [3, 2], [4, 0], [4, 2], [6, 1], [6, 1], [7, 2], [7, 1], [7, 1], [7, 2], [12, 4], [12, 4], [19, 1], [19, 3], [16, 1], [16, 1], [11, 4], [17, 1], [10, 1], [23, 2], [23, 1], [25, 1], [25, 1], [26, 1], [26, 1], [14, 1] ], performAction: function (yytext, yyleng, yylineno, yy, yystate, $$, _$) { /* this == yyval */ var $0 = $$.length - 1 switch (yystate) { case 1: return yy.parser.yy case 4: break case 6: $$[$0] break case 7: case 8: yy.parser.yy.addSignal($$[$0]) break case 9: yy.parser.yy.setTitle($$[$0]) break case 10: this.$ = new Diagram.Note($$[$0 - 1], $$[$0 - 2], $$[$0]) break case 11: this.$ = new Diagram.Note($$[$0 - 1], Diagram.PLACEMENT.OVER, $$[$0]) break case 12: case 20: this.$ = $$[$0] break case 13: this.$ = [$$[$0 - 2], $$[$0]] break case 14: this.$ = Diagram.PLACEMENT.LEFTOF break case 15: this.$ = Diagram.PLACEMENT.RIGHTOF break case 16: this.$ = new Diagram.Signal($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0]) break case 17: this.$ = yy.parser.yy.getActor(Diagram.unescape($$[$0])) break case 18: this.$ = yy.parser.yy.getActorWithAlias(Diagram.unescape($$[$0])) break case 19: this.$ = $$[$0 - 1] | ($$[$0] << 2) break case 21: this.$ = Diagram.LINETYPE.SOLID break case 22: this.$ = Diagram.LINETYPE.DOTTED break case 23: this.$ = Diagram.ARROWTYPE.FILLED break case 24: this.$ = Diagram.ARROWTYPE.OPEN break case 25: this.$ = Diagram.unescape($$[$0].substring(1)) } }, table: [ o($V0, [2, 2], { 3: 1, 4: 2 }), { 1: [3] }, { 5: [1, 3], 6: 4, 7: 5, 8: [1, 6], 9: [1, 7], 11: 8, 12: 9, 13: [1, 10], 15: [1, 12], 17: 11, 24: $V1 }, { 1: [2, 1] }, o($V0, [2, 3]), o($V0, [2, 4]), o($V0, [2, 5]), { 10: 14, 24: [1, 15] }, o($V0, [2, 7]), o($V0, [2, 8]), { 14: 16, 31: $V2 }, { 23: 18, 25: 19, 27: [1, 20], 28: [1, 21] }, { 16: 22, 18: [1, 23], 21: [1, 24], 22: [1, 25] }, o([20, 27, 28, 31], [2, 17]), o($V0, [2, 6]), o($V0, [2, 18]), o($V0, [2, 9]), o($V0, [2, 25]), { 17: 26, 24: $V1 }, { 24: [2, 20], 26: 27, 29: [1, 28], 30: [1, 29] }, o($V3, [2, 21]), o($V3, [2, 22]), { 17: 30, 24: $V1 }, { 17: 32, 19: 31, 24: $V1 }, { 24: [2, 14] }, { 24: [2, 15] }, { 14: 33, 31: $V2 }, { 24: [2, 19] }, { 24: [2, 23] }, { 24: [2, 24] }, { 14: 34, 31: $V2 }, { 14: 35, 31: $V2 }, { 20: [1, 36], 31: [2, 12] }, o($V0, [2, 16]), o($V0, [2, 10]), o($V0, [2, 11]), { 17: 37, 24: $V1 }, { 31: [2, 13] } ], defaultActions: { 3: [2, 1], 24: [2, 14], 25: [2, 15], 27: [2, 19], 28: [2, 23], 29: [2, 24], 37: [2, 13] }, parseError: function (str, hash) { if (!hash.recoverable) throw new Error(str) this.trace(str) }, parse: function (input) { function lex() { var token return ( (token = lexer.lex() || EOF), 'number' != typeof token && (token = self.symbols_[token] || token), token ) } var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1, args = lstack.slice.call(arguments, 1), lexer = Object.create(this.lexer), sharedState = { yy: {} } for (var k in this.yy) Object.prototype.hasOwnProperty.call(this.yy, k) && (sharedState.yy[k] = this.yy[k]) lexer.setInput(input, sharedState.yy), (sharedState.yy.lexer = lexer), (sharedState.yy.parser = this), 'undefined' == typeof lexer.yylloc && (lexer.yylloc = {}) var yyloc = lexer.yylloc lstack.push(yyloc) var ranges = lexer.options && lexer.options.ranges 'function' == typeof sharedState.yy.parseError ? (this.parseError = sharedState.yy.parseError) : (this.parseError = Object.getPrototypeOf(this).parseError) for ( var symbol, preErrorSymbol, state, action, r, p, len, newState, expected, yyval = {}; ; ) { if ( ((state = stack[stack.length - 1]), this.defaultActions[state] ? (action = this.defaultActions[state]) : ((null !== symbol && 'undefined' != typeof symbol) || (symbol = lex()), (action = table[state] && table[state][symbol])), 'undefined' == typeof action || !action.length || !action[0]) ) { var errStr = '' expected = [] for (p in table[state]) this.terminals_[p] && p > TERROR && expected.push("'" + this.terminals_[p] + "'") ;(errStr = lexer.showPosition ? 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ", got '" + (this.terminals_[symbol] || symbol) + "'" : 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'")), this.parseError(errStr, { text: lexer.match, token: this.terminals_[symbol] || symbol, line: lexer.yylineno, loc: yyloc, expected: expected }) } if (action[0] instanceof Array && action.length > 1) throw new Error( 'Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol ) switch (action[0]) { case 1: stack.push(symbol), vstack.push(lexer.yytext), lstack.push(lexer.yylloc), stack.push(action[1]), (symbol = null), preErrorSymbol ? ((symbol = preErrorSymbol), (preErrorSymbol = null)) : ((yyleng = lexer.yyleng), (yytext = lexer.yytext), (yylineno = lexer.yylineno), (yyloc = lexer.yylloc), recovering > 0 && recovering--) break case 2: if ( ((len = this.productions_[action[1]][1]), (yyval.$ = vstack[vstack.length - len]), (yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column }), ranges && (yyval._$.range = [ lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1] ]), (r = this.performAction.apply( yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args) )), 'undefined' != typeof r) ) return r len && ((stack = stack.slice(0, -1 * len * 2)), (vstack = vstack.slice(0, -1 * len)), (lstack = lstack.slice(0, -1 * len))), stack.push(this.productions_[action[1]][0]), vstack.push(yyval.$), lstack.push(yyval._$), (newState = table[stack[stack.length - 2]][stack[stack.length - 1]]), stack.push(newState) break case 3: return !0 } } return !0 } }, lexer = (function () { var lexer = { EOF: 1, parseError: function (str, hash) { if (!this.yy.parser) throw new Error(str) this.yy.parser.parseError(str, hash) }, // resets the lexer, sets new input setInput: function (input, yy) { return ( (this.yy = yy || this.yy || {}), (this._input = input), (this._more = this._backtrack = this.done = !1), (this.yylineno = this.yyleng = 0), (this.yytext = this.matched = this.match = ''), (this.conditionStack = ['INITIAL']), (this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0 }), this.options.ranges && (this.yylloc.range = [0, 0]), (this.offset = 0), this ) }, // consumes and returns one char from the input input: function () { var ch = this._input[0] ;(this.yytext += ch), this.yyleng++, this.offset++, (this.match += ch), (this.matched += ch) var lines = ch.match(/(?:\r\n?|\n).*/g) return ( lines ? (this.yylineno++, this.yylloc.last_line++) : this.yylloc.last_column++, this.options.ranges && this.yylloc.range[1]++, (this._input = this._input.slice(1)), ch ) }, // unshifts one char (or a string) into the input unput: function (ch) { var len = ch.length, lines = ch.split(/(?:\r\n?|\n)/g) ;(this._input = ch + this._input), (this.yytext = this.yytext.substr(0, this.yytext.length - len)), //this.yyleng -= len; (this.offset -= len) var oldLines = this.match.split(/(?:\r\n?|\n)/g) ;(this.match = this.match.substr(0, this.match.length - 1)), (this.matched = this.matched.substr(0, this.matched.length - 1)), lines.length - 1 && (this.yylineno -= lines.length - 1) var r = this.yylloc.range return ( (this.yylloc = { first_line: this.yylloc.first_line, last_line: this.yylineno + 1, first_column: this.yylloc.first_column, last_column: lines ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length : this.yylloc.first_column - len }), this.options.ranges && (this.yylloc.range = [r[0], r[0] + this.yyleng - len]), (this.yyleng = this.yytext.length), this ) }, // When called from action, caches matched text and appends it on next action more: function () { return (this._more = !0), this }, // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. reject: function () { return this.options.backtrack_lexer ? ((this._backtrack = !0), this) : this.parseError( 'Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { text: '', token: null, line: this.yylineno } ) }, // retain first n characters of the match less: function (n) { this.unput(this.match.slice(n)) }, // displays already matched input, i.e. for error messages pastInput: function () { var past = this.matched.substr(0, this.matched.length - this.match.length) return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, '') }, // displays upcoming input, i.e. for error messages upcomingInput: function () { var next = this.match return ( next.length < 20 && (next += this._input.substr(0, 20 - next.length)), (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, '') ) }, // displays the character position where the lexing error occurred, i.e. for error messages showPosition: function () { var pre = this.pastInput(), c = new Array(pre.length + 1).join('-') return pre + this.upcomingInput() + '\n' + c + '^' }, // test the lexed token: return FALSE when not a match, otherwise return token test_match: function (match, indexed_rule) { var token, lines, backup if ( (this.options.backtrack_lexer && // save context ((backup = { yylineno: this.yylineno, yylloc: { first_line: this.yylloc.first_line, last_line: this.last_line, first_column: this.yylloc.first_column, last_column: this.yylloc.last_column }, yytext: this.yytext, match: this.match, matches: this.matches, matched: this.matched, yyleng: this.yyleng, offset: this.offset, _more: this._more, _input: this._input, yy: this.yy, conditionStack: this.conditionStack.slice(0), done: this.done }), this.options.ranges && (backup.yylloc.range = this.yylloc.range.slice(0))), (lines = match[0].match(/(?:\r\n?|\n).*/g)), lines && (this.yylineno += lines.length), (this.yylloc = { first_line: this.yylloc.last_line, last_line: this.yylineno + 1, first_column: this.yylloc.last_column, last_column: lines ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length }), (this.yytext += match[0]), (this.match += match[0]), (this.matches = match), (this.yyleng = this.yytext.length), this.options.ranges && (this.yylloc.range = [this.offset, (this.offset += this.yyleng)]), (this._more = !1), (this._backtrack = !1), (this._input = this._input.slice(match[0].length)), (this.matched += match[0]), (token = this.performAction.call( this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1] )), this.done && this._input && (this.done = !1), token) ) return token if (this._backtrack) { // recover context for (var k in backup) this[k] = backup[k] return !1 } return !1 }, // return next match in input next: function () { if (this.done) return this.EOF this._input || (this.done = !0) var token, match, tempMatch, index this._more || ((this.yytext = ''), (this.match = '')) for (var rules = this._currentRules(), i = 0; i < rules.length; i++) if ( ((tempMatch = this._input.match(this.rules[rules[i]])), tempMatch && (!match || tempMatch[0].length > match[0].length)) ) { if (((match = tempMatch), (index = i), this.options.backtrack_lexer)) { if (((token = this.test_match(tempMatch, rules[i])), token !== !1)) return token if (this._backtrack) { match = !1 continue } // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return !1 } if (!this.options.flex) break } return match ? ((token = this.test_match(match, rules[index])), token !== !1 && token) : '' === this._input ? this.EOF : this.parseError( 'Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { text: '', token: null, line: this.yylineno } ) }, // return next match that has a token lex: function () { var r = this.next() return r ? r : this.lex() }, // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) begin: function (condition) { this.conditionStack.push(condition) }, // pop the previously active lexer condition state off the condition stack popState: function () { var n = this.conditionStack.length - 1 return n > 0 ? this.conditionStack.pop() : this.conditionStack[0] }, // produce the lexer rule set which is active for the currently active lexer condition state _currentRules: function () { return this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1] ? this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules : this.conditions.INITIAL.rules }, // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available topState: function (n) { return ( (n = this.conditionStack.length - 1 - Math.abs(n || 0)), n >= 0 ? this.conditionStack[n] : 'INITIAL' ) }, // alias for begin(condition) pushState: function (condition) { this.begin(condition) }, // return the number of states currently on the stack stateStackSize: function () { return this.conditionStack.length }, options: { 'case-insensitive': !0 }, performAction: function (yy, yy_, $avoiding_name_collisions, YY_START) { switch ($avoiding_name_collisions) { case 0: return 8 case 1: /* skip whitespace */ break case 2: /* skip comments */ break case 3: return 9 case 4: return 21 case 5: return 22 case 6: return 18 case 7: return 15 case 8: return 13 case 9: return 20 case 10: return 24 case 11: return 24 case 12: return 28 case 13: return 27 case 14: return 30 case 15: return 29 case 16: return 31 case 17: return 5 case 18: return 'INVALID' } }, rules: [ /^(?:[\r\n]+)/i, /^(?:\s+)/i, /^(?:#[^\r\n]*)/i, /^(?:participant\b)/i, /^(?:left of\b)/i, /^(?:right of\b)/i, /^(?:over\b)/i, /^(?:note\b)/i, /^(?:title\b)/i, /^(?:,)/i, /^(?:[^\->:,\r\n"]+)/i, /^(?:"[^"]+")/i, /^(?:--)/i, /^(?:-)/i, /^(?:>>)/i, /^(?:>)/i, /^(?:[^\r\n]+)/i, /^(?:$)/i, /^(?:.)/i ], conditions: { INITIAL: { rules: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], inclusive: !0 } } } return lexer })() return (parser.lexer = lexer), (Parser.prototype = parser), (parser.Parser = Parser), new Parser() })() // CLI bootstrap stripped — this library only runs in the renderer; the // require('fs')/require('path') path was unreachable in the editor and breaks // sandboxed bundling. // jison's UMD `exports.parser/Parser/parse` fallback was also dropped: this // file is an ESM module (`export default Diagram`), so the CommonJS `exports` // global is undefined here and Rollup flags it (COMMONJS_VARIABLE_IN_ESM). // `Diagram.parse` uses the internal `parser` directly, so nothing relies on it. /** * jison doesn't have a good exception, so we make one. * This is brittle as it depends on jison internals */ function ParseError(message, hash) { _.extend(this, hash) this.name = 'ParseError' this.message = message || '' } ParseError.prototype = new Error() Diagram.ParseError = ParseError Diagram.parse = function (input) { // TODO jison v0.4.17 changed their API slightly, so parser is no longer defined: // Create the object to track state and deal with errors parser.yy = new Diagram() parser.yy.parseError = function (message, hash) { throw new ParseError(message, hash) } // Parse var diagram = parser.parse(input) // Then clean up the parseError key that a user won't care about delete diagram.parseError return diagram } /** js sequence diagrams * https://bramp.github.io/js-sequence-diagrams/ * (c) 2012-2017 Andrew Brampton (bramp.net) * Simplified BSD license. */ /*global Diagram, _ */ // Following the CSS convention // Margin is the gap outside the box // Padding is the gap inside the box // Each object has x/y/width/height properties // The x/y should be top left corner // width/height is with both margin and padding // TODO // Image width is wrong, when there is a note in the right hand col // Title box could look better // Note box could look better var DIAGRAM_MARGIN = 10 var ACTOR_MARGIN = 10 // Margin around a actor var ACTOR_PADDING = 10 // Padding inside a actor var SIGNAL_MARGIN = 5 // Margin around a signal var SIGNAL_PADDING = 5 // Padding inside a signal var NOTE_MARGIN = 10 // Margin around a note var NOTE_PADDING = 5 // Padding inside a note var NOTE_OVERLAP = 15 // Overlap when using a "note over A,B" var TITLE_MARGIN = 0 var TITLE_PADDING = 5 var SELF_SIGNAL_WIDTH = 20 // How far out a self signal goes var PLACEMENT = Diagram.PLACEMENT var LINETYPE = Diagram.LINETYPE var ARROWTYPE = Diagram.ARROWTYPE var ALIGN_LEFT = 0 var ALIGN_CENTER = 1 function AssertException(message) { this.message = message } AssertException.prototype.toString = function () { return 'AssertException: ' + this.message } function assert(exp, message) { if (!exp) { throw new AssertException(message) } } if (!String.prototype.trim) { String.prototype.trim = function () { return this.replace(/^\s+|\s+$/g, '') } } Diagram.themes = {} function registerTheme(name, theme) { Diagram.themes[name] = theme } /****************** * Drawing extras ******************/ function getCenterX(box) { return box.x + box.width / 2 } function getCenterY(box) { return box.y + box.height / 2 } /****************** * SVG Path extras ******************/ function clamp(x, min, max) { if (x < min) { return min } if (x > max) { return max } return x } function wobble(x1, y1, x2, y2) { assert(_.all([x1, x2, y1, y2], _.isFinite), 'x1,x2,y1,y2 must be numeric') // Wobble no more than 1/25 of the line length var factor = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) / 25 // Distance along line where the control points are // Clamp between 20% and 80% so any arrow heads aren't angled too much var r1 = clamp(Math.random(), 0.2, 0.8) var r2 = clamp(Math.random(), 0.2, 0.8) var xfactor = Math.random() > 0.5 ? factor : -factor var yfactor = Math.random() > 0.5 ? factor : -factor var p1 = { x: (x2 - x1) * r1 + x1 + xfactor, y: (y2 - y1) * r1 + y1 + yfactor } var p2 = { x: (x2 - x1) * r2 + x1 - xfactor, y: (y2 - y1) * r2 + y1 - yfactor } return ( 'C' + p1.x.toFixed(1) + ',' + p1.y.toFixed(1) + // start control point ' ' + p2.x.toFixed(1) + ',' + p2.y.toFixed(1) + // end control point ' ' + x2.toFixed(1) + ',' + y2.toFixed(1) ) // end point } /** * Draws a wobbly (hand drawn) rect */ function handRect(x, y, w, h) { assert(_.all([x, y, w, h], _.isFinite), 'x, y, w, h must be numeric') return ( 'M' + x + ',' + y + wobble(x, y, x + w, y) + wobble(x + w, y, x + w, y + h) + wobble(x + w, y + h, x, y + h) + wobble(x, y + h, x, y) ) } /** * Draws a wobbly (hand drawn) line */ function handLine(x1, y1, x2, y2) { assert(_.all([x1, x2, y1, y2], _.isFinite), 'x1,x2,y1,y2 must be numeric') return 'M' + x1.toFixed(1) + ',' + y1.toFixed(1) + wobble(x1, y1, x2, y2) } /****************** * BaseTheme ******************/ var BaseTheme = function (diagram, options) { this.init(diagram, options) } _.extend(BaseTheme.prototype, { // Init called while creating the Theme init: function (diagram, options) { this.diagram = diagram this.actorsHeight_ = 0 this.signalsHeight_ = 0 this.title_ = undefined // hack - This should be somewhere better }, setupPaper: function (container) {}, draw: function (container) { this.setupPaper(container) this.layout() var titleHeight = this.title_ ? this.title_.height : 0 var y = DIAGRAM_MARGIN + titleHeight this.drawTitle() this.drawActors(y) this.drawSignals(y + this.actorsHeight_) }, layout: function () { // Local copies var diagram = this.diagram var font = this.font_ var actors = diagram.actors var signals = diagram.signals diagram.width = 0 // min width diagram.height = 0 // min height // Setup some layout stuff if (diagram.title) { var title = (this.title_ = {}) var bb = this.textBBox(diagram.title, font) title.textBB = bb title.message = diagram.title title.width = bb.width + (TITLE_PADDING + TITLE_MARGIN) * 2 title.height = bb.height + (TITLE_PADDING + TITLE_MARGIN) * 2 title.x = DIAGRAM_MARGIN title.y = DIAGRAM_MARGIN diagram.width += title.width diagram.height += title.height } _.each( actors, function (a) { var bb = this.textBBox(a.name, font) a.textBB = bb a.x = 0 a.y = 0 a.width = bb.width + (ACTOR_PADDING + ACTOR_MARGIN) * 2 a.height = bb.height + (ACTOR_PADDING + ACTOR_MARGIN) * 2 a.distances = [] a.paddingRight = 0 this.actorsHeight_ = Math.max(a.height, this.actorsHeight_) }, this ) function actorEnsureDistance(a, b, d) { assert(a < b, 'a must be less than or equal to b') if (a < 0) { // Ensure b has left margin b = actors[b] b.x = Math.max(d - b.width / 2, b.x) } else if (b >= actors.length) { // Ensure a has right margin a = actors[a] a.paddingRight = Math.max(d, a.paddingRight) } else { a = actors[a] a.distances[b] = Math.max(d, a.distances[b] ? a.distances[b] : 0) } } _.each( signals, function (s) { // Indexes of the left and right actors involved var a var b var bb = this.textBBox(s.message, font) //var bb = t.attr("text", s.message).getBBox(); s.textBB = bb s.width = bb.width s.height = bb.height var extraWidth = 0 if (s.type == 'Signal') { s.width += (SIGNAL_MARGIN + SIGNAL_PADDING) * 2 s.height += (SIGNAL_MARGIN + SIGNAL_PADDING) * 2 if (s.isSelf()) { // TODO Self signals need a min height a = s.actorA.index b = a + 1 s.width += SELF_SIGNAL_WIDTH } else { a = Math.min(s.actorA.index, s.actorB.index) b = Math.max(s.actorA.index, s.actorB.index) } } else if (s.type == 'Note') { s.width += (NOTE_MARGIN + NOTE_PADDING) * 2 s.height += (NOTE_MARGIN + NOTE_PADDING) * 2 // HACK lets include the actor's padding extraWidth = 2 * ACTOR_MARGIN if (s.placement == PLACEMENT.LEFTOF) { b = s.actor.index a = b - 1 } else if (s.placement == PLACEMENT.RIGHTOF) { a = s.actor.index b = a + 1 } else if (s.placement == PLACEMENT.OVER && s.hasManyActors()) { // Over multiple actors a = Math.min(s.actor[0].index, s.actor[1].index) b = Math.max(s.actor[0].index, s.actor[1].index) // We don't need our padding, and we want to overlap extraWidth = -(NOTE_PADDING * 2 + NOTE_OVERLAP * 2) } else if (s.placement == PLACEMENT.OVER) { // Over single actor a = s.actor.index actorEnsureDistance(a - 1, a, s.width / 2) actorEnsureDistance(a, a + 1, s.width / 2) this.signalsHeight_ += s.height return // Bail out early } } else { throw new Error('Unhandled signal type:' + s.type) } actorEnsureDistance(a, b, s.width + extraWidth) this.signalsHeight_ += s.height }, this ) // Re-jig the positions var actorsX = 0 _.each( actors, function (a) { a.x = Math.max(actorsX, a.x) // TODO This only works if we loop in sequence, 0, 1, 2, etc _.each(a.distances, function (distance, b) { // lodash (and possibly others) do not like sparse arrays // so sometimes they return undefined if (typeof distance == 'undefined') { return } b = actors[b] distance = Math.max(distance, a.width / 2, b.width / 2) b.x = Math.max(b.x, a.x + a.width / 2 + distance - b.width / 2) }) actorsX = a.x + a.width + a.paddingRight }, this ) diagram.width = Math.max(actorsX, diagram.width) // TODO Refactor a little diagram.width += 2 * DIAGRAM_MARGIN diagram.height += 2 * DIAGRAM_MARGIN + 2 * this.actorsHeight_ + this.signalsHeight_ return this }, // TODO Instead of one textBBox function, create a function for each element type, e.g // layout_title, layout_actor, etc that returns it's bounding box textBBox: function (text, font) {}, drawTitle: function () { var title = this.title_ if (title) { this.drawTextBox(title, title.message, TITLE_MARGIN, TITLE_PADDING, this.font_, ALIGN_LEFT) } }, drawActors: function (offsetY) { var y = offsetY _.each( this.diagram.actors, function (a) { // Top box this.drawActor(a, y, this.actorsHeight_) // Bottom box this.drawActor(a, y + this.actorsHeight_ + this.signalsHeight_, this.actorsHeight_) // Veritical line var aX = getCenterX(a) this.drawLine( aX, y + this.actorsHeight_ - ACTOR_MARGIN, aX, y + this.actorsHeight_ + ACTOR_MARGIN + this.signalsHeight_ ) }, this ) }, drawActor: function (actor, offsetY, height) { actor.y = offsetY actor.height = height this.drawTextBox(actor, actor.name, ACTOR_MARGIN, ACTOR_PADDING, this.font_, ALIGN_CENTER) }, drawSignals: function (offsetY) { var y = offsetY _.each( this.diagram.signals, function (s) { // TODO Add debug mode, that draws padding/margin box if (s.type == 'Signal') { if (s.isSelf()) { this.drawSelfSignal(s, y) } else { this.drawSignal(s, y) } } else if (s.type == 'Note') { this.drawNote(s, y) } y += s.height }, this ) }, drawSelfSignal: function (signal, offsetY) { assert(signal.isSelf(), 'signal must be a self signal') var textBB = signal.textBB var aX = getCenterX(signal.actorA) var x = aX + SELF_SIGNAL_WIDTH + SIGNAL_PADDING var y = offsetY + SIGNAL_PADDING + signal.height / 2 + textBB.y this.drawText(x, y, signal.message, this.font_, ALIGN_LEFT) var y1 = offsetY + SIGNAL_MARGIN + SIGNAL_PADDING var y2 = y1 + signal.height - 2 * SIGNAL_MARGIN - SIGNAL_PADDING // Draw three lines, the last one with a arrow this.drawLine(aX, y1, aX + SELF_SIGNAL_WIDTH, y1, signal.linetype) this.drawLine(aX + SELF_SIGNAL_WIDTH, y1, aX + SELF_SIGNAL_WIDTH, y2, signal.linetype) this.drawLine(aX + SELF_SIGNAL_WIDTH, y2, aX, y2, signal.linetype, signal.arrowtype) }, drawSignal: function (signal, offsetY) { var aX = getCenterX(signal.actorA) var bX = getCenterX(signal.actorB) // Mid point between actors var x = (bX - aX) / 2 + aX var y = offsetY + SIGNAL_MARGIN + 2 * SIGNAL_PADDING // Draw the text in the middle of the signal this.drawText(x, y, signal.message, this.font_, ALIGN_CENTER) // Draw the line along the bottom of the signal y = offsetY + signal.height - SIGNAL_MARGIN - SIGNAL_PADDING this.drawLine(aX, y, bX, y, signal.linetype, signal.arrowtype) }, drawNote: function (note, offsetY) { note.y = offsetY var actorA = note.hasManyActors() ? note.actor[0] : note.actor var aX = getCenterX(actorA) switch (note.placement) { case PLACEMENT.RIGHTOF: note.x = aX + ACTOR_MARGIN break case PLACEMENT.LEFTOF: note.x = aX - ACTOR_MARGIN - note.width break case PLACEMENT.OVER: if (note.hasManyActors()) { var bX = getCenterX(note.actor[1]) var overlap = NOTE_OVERLAP + NOTE_PADDING note.x = Math.min(aX, bX) - overlap note.width = Math.max(aX, bX) + overlap - note.x } else { note.x = aX - note.width / 2 } break default: throw new Error('Unhandled note placement: ' + note.placement) } return this.drawTextBox(note, note.message, NOTE_MARGIN, NOTE_PADDING, this.font_, ALIGN_LEFT) }, /** * Draw text surrounded by a box */ drawTextBox: function (box, text, margin, padding, font, align) { var x = box.x + margin var y = box.y + margin var w = box.width - 2 * margin var h = box.height - 2 * margin // Draw inner box this.drawRect(x, y, w, h) // Draw text (in the center) if (align == ALIGN_CENTER) { x = getCenterX(box) y = getCenterY(box) } else { x += padding y += padding } return this.drawText(x, y, text, font, align) } }) /** js sequence diagrams * https://bramp.github.io/js-sequence-diagrams/ * (c) 2012-2017 Andrew Brampton (bramp.net) * Simplified BSD license. */ /*global Diagram, Snap, WebFont _ */ // TODO Move defintion of font onto the <svg>, so it can easily be override at each level if (typeof Snap != 'undefined') { var xmlns = 'http://www.w3.org/2000/svg' var LINE = { stroke: '#000000', 'stroke-width': 2, // BUG TODO This gets set as a style, not as a attribute. Look at eve.on("snap.util.attr"... fill: 'none' } var RECT = { stroke: '#000000', 'stroke-width': 2, fill: '#fff' } var LOADED_FONTS = {} /****************** * SnapTheme ******************/ var SnapTheme = function (diagram, options, resume) { _.defaults(options, { 'css-class': 'simple', 'font-size': 16, 'font-family': 'Andale Mono, monospace' }) this.init(diagram, options, resume) } _.extend(SnapTheme.prototype, BaseTheme.prototype, { init: function (diagram, options, resume) { BaseTheme.prototype.init.call(this, diagram) this.paper_ = undefined this.cssClass_ = options['css-class'] || undefined this.font_ = { 'font-size': options['font-size'], 'font-family': options['font-family'] } var a = (this.arrowTypes_ = {}) a[ARROWTYPE.FILLED] = 'Block' a[ARROWTYPE.OPEN] = 'Open' var l = (this.lineTypes_ = {}) l[LINETYPE.SOLID] = '' l[LINETYPE.DOTTED] = '6,2' var that = this this.waitForFont(function () { resume(that) }) }, // Wait for loading of the font waitForFont: function (callback) { var fontFamily = this.font_['font-family'] if (typeof WebFont == 'undefined') { throw new Error('WebFont is required (https://github.com/typekit/webfontloader).') } if (LOADED_FONTS[fontFamily]) { // If already loaded, just return instantly. callback() return } WebFont.load({ custom: { families: [fontFamily] // TODO replace this with something that reads the css }, classes: false, // No need to place classes on the DOM, just use JS Events active: function () { LOADED_FONTS[fontFamily] = true callback() }, inactive: function () { // If we fail to fetch the font, still continue. LOADED_FONTS[fontFamily] = true callback() } }) }, addDescription: function (svg, description) { var desc = document.createElementNS(xmlns, 'desc') desc.appendChild(document.createTextNode(description)) svg.appendChild(desc) }, setupPaper: function (container) { // Container must be a SVG element. We assume it's a div, so lets create a SVG and insert var svg = document.createElementNS(xmlns, 'svg') container.appendChild(svg) this.addDescription(svg, this.diagram.title || '') this.paper_ = Snap(svg) this.paper_.addClass('sequence') if (this.cssClass_) { this.paper_.addClass(this.cssClass_) } this.beginGroup() // TODO Perhaps only include the markers if we actually use them. var a = (this.arrowMarkers_ = {}) var arrow = this.paper_.path('M 0 0 L 5 2.5 L 0 5 z') a[ARROWTYPE.FILLED] = arrow.marker(0, 0, 5, 5, 5, 2.5).attr({ id: 'markerArrowBlock' }) arrow = this.paper_.path('M 9.6,8 1.92,16 0,13.7 5.76,8 0,2.286 1.92,0 9.6,8 z') a[ARROWTYPE.OPEN] = arrow .marker(0, 0, 9.6, 16, 9.6, 8) .attr({ markerWidth: '4', id: 'markerArrowOpen' }) }, layout: function () { BaseTheme.prototype.layout.call(this) this.paper_.attr({ width: this.diagram.width + 'px', height: this.diagram.height + 'px' }) }, textBBox: function (text, font) { // TODO getBBox will return the bounds with any whitespace/kerning. This makes some of our aligments screwed up var t = this.createText(text, font) var bb = t.getBBox() t.remove() return bb }, // For each drawn element, push onto the stack, so it can be wrapped in a single outer element pushToStack: function (element) { this._stack.push(element) return element }, // Begin a group of elements beginGroup: function () { this._stack = [] }, // Finishes the group, and returns the <group> element finishGroup: function () { var g = this.paper_.group.apply(this.paper_, this._stack) this.beginGroup() // Reset the group return g }, createText: function (text, font) { text = _.invoke(text.split('\n'), 'trim') var t = this.paper_.text(0, 0, text) t.attr(font || {}) if (text.length > 1) { // Every row after the first, set tspan to be 1.2em below the previous line t.selectAll('tspan:nth-child(n+2)').attr({ dy: '1.2em', x: 0 }) } return t }, drawLine: function (x1, y1, x2, y2, linetype, arrowhead) { var line = this.paper_.line(x1, y1, x2, y2).attr(LINE) if (linetype !== undefined) { line.attr('strokeDasharray', this.lineTypes_[linetype]) } if (arrowhead !== undefined) { line.attr('markerEnd', this.arrowMarkers_[arrowhead]) } return this.pushToStack(line) }, drawRect: function (x, y, w, h) { var rect = this.paper_.rect(x, y, w, h).attr(RECT) return this.pushToStack(rect) }, /** * Draws text with a optional white background * x,y (int) x,y top left point of the text, or the center of the text (depending on align param) * text (string) text to print * font (Object) * align (string) ALIGN_LEFT or ALIGN_CENTER */ drawText: function (x, y, text, font, align) { var t = this.createText(text, font) var bb = t.getBBox() if (align == ALIGN_CENTER) { x = x - bb.width / 2 y = y - bb.height / 2 } // Now move the text into place // `y - bb.y` because text(..) is positioned from the baseline, so this moves it down. t.attr({ x: x - bb.x, y: y - bb.y }) t.selectAll('tspan').attr({ x: x }) this.pushToStack(t) return t }, drawTitle: function () { this.beginGroup() BaseTheme.prototype.drawTitle.call(this) return this.finishGroup().addClass('title') }, drawActor: function (actor, offsetY, height) { this.beginGroup() BaseTheme.prototype.drawActor.call(this, actor, offsetY, height) return this.finishGroup().addClass('actor') }, drawSignal: function (signal, offsetY) { this.beginGroup() BaseTheme.prototype.drawSignal.call(this, signal, offsetY) return this.finishGroup().addClass('signal') }, drawSelfSignal: function (signal, offsetY) { this.beginGroup() BaseTheme.prototype.drawSelfSignal.call(this, signal, offsetY) return this.finishGroup().addClass('signal') }, drawNote: function (note, offsetY) { this.beginGroup() BaseTheme.prototype.drawNote.call(this, note, offsetY) return this.finishGroup().addClass('note') } }) /****************** * SnapHandTheme ******************/ var SnapHandTheme = function (diagram, options, resume) { _.defaults(options, { 'css-class': 'hand', 'font-size': 16, 'font-family': 'danielbd' }) this.init(diagram, options, resume) } // Take the standard SnapTheme and make all the lines wobbly _.extend(SnapHandTheme.prototype, SnapTheme.prototype, { drawLine: function (x1, y1, x2, y2, linetype, arrowhead) { var line = this.paper_.path(handLine(x1, y1, x2, y2)).attr(LINE) if (linetype !== undefined) { line.attr('strokeDasharray', this.lineTypes_[linetype]) } if (arrowhead !== undefined) { line.attr('markerEnd', this.arrowMarkers_[arrowhead]) } return this.pushToStack(line) }, drawRect: function (x, y, w, h) { var rect = this.paper_.path(handRect(x, y, w, h)).attr(RECT) return this.pushToStack(rect) } }) registerTheme('snapSimple', SnapTheme) registerTheme('snapHand', SnapHandTheme) } /** js sequence diagrams * https://bramp.github.io/js-sequence-diagrams/ * (c) 2012-2017 Andrew Brampton (bramp.net) * Simplified BSD license. */ /*global Diagram, _ */ if (typeof Raphael == 'undefined' && typeof Snap == 'undefined') { throw new Error('Raphael or Snap.svg is required to be included.') } if (_.isEmpty(Diagram.themes)) { // If you are using stock js-sequence-diagrams you should never see this. This only // happens if you have removed the built in themes. throw new Error('No themes were registered. Please call registerTheme(...).') } // Set the default hand/simple based on which theme is available. Diagram.themes.hand = Diagram.themes.snapHand || Diagram.themes.raphaelHand Diagram.themes.simple = Diagram.themes.snapSimple || Diagram.themes.raphaelSimple /* Draws the diagram. Creates a SVG inside the container * container (HTMLElement|string) DOM element or its ID to draw on * options (Object) */ Diagram.prototype.drawSVG = function (container, options) { var defaultOptions = { theme: 'hand' } options = _.defaults(options || {}, defaultOptions) if (!(options.theme in Diagram.themes)) { throw new Error('Unsupported theme: ' + options.theme) } // TODO Write tests for this check var div = _.isString(container) ? document.getElementById(container) : container if (div === null || !div.tagName) { throw new Error('Invalid container: ' + container) } var Theme = Diagram.themes[options.theme] new Theme(this, options, function (drawing) { drawing.draw(div) }) } // end of drawSVG export default Diagram