From a1ef3f0f4a0ab93df90edd70204767ec0f83c7f1 Mon Sep 17 00:00:00 2001 From: pinghe Date: Sun, 15 May 2022 13:21:16 +0800 Subject: [PATCH 1/3] Add C4Context diagram. Compatible with C4-PlantUML syntax. ``` C4Context title System Context diagram for Internet Banking System Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") Person(customerB, "Banking Customer B") Person_Ext(customerC, "Banking Customer C") System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") Enterprise_Boundary(b1, "BankBoundary") { SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") System_Boundary(b2, "BankBoundary2") { System(SystemA, "Banking System A") System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.") } System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") Boundary(b3, "BankBoundary3", "boundary") { SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.") SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") } } BiRel(customerA, SystemAA, "Uses") BiRel(SystemAA, SystemE, "Uses") Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") Rel(SystemC, customerA, "Sends e-mails to") ``` --- demos/index.html | 38 ++ src/Diagram.js | 11 +- src/defaultConfig.js | 160 +++++ src/diagrams/c4/c4Db.js | 307 ++++++++++ src/diagrams/c4/c4Renderer.js | 609 +++++++++++++++++++ src/diagrams/c4/parser/c4Diagram.jison | 267 ++++++++ src/diagrams/c4/styles.js | 8 + src/diagrams/c4/svgDraw.js | 805 +++++++++++++++++++++++++ src/mermaidAPI.js | 12 + src/styles.js | 2 + src/themes/c4.scss | 4 + src/themes/default/index.scss | 5 + src/themes/theme-base.js | 5 + src/themes/theme-dark.js | 5 + src/themes/theme-default.js | 5 + src/themes/theme-forest.js | 5 + src/themes/theme-neutral.js | 5 + src/utils.js | 4 + 18 files changed, 2256 insertions(+), 1 deletion(-) create mode 100644 src/diagrams/c4/c4Db.js create mode 100644 src/diagrams/c4/c4Renderer.js create mode 100644 src/diagrams/c4/parser/c4Diagram.jison create mode 100644 src/diagrams/c4/styles.js create mode 100644 src/diagrams/c4/svgDraw.js create mode 100644 src/themes/c4.scss diff --git a/demos/index.html b/demos/index.html index bdbd2f180b..1db4bf4169 100644 --- a/demos/index.html +++ b/demos/index.html @@ -20,6 +20,44 @@
+
+ C4Context + title System Context diagram for Internet Banking System + + Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") + Person(customerB, "Banking Customer B") + Person_Ext(customerC, "Banking Customer C") + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + + + Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") + + Enterprise_Boundary(b1, "BankBoundary") { + + SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") + + System_Boundary(b2, "BankBoundary2") { + System(SystemA, "Banking System A") + System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.") + } + + System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") + SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") + + Boundary(b3, "BankBoundary3", "boundary") { + SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.") + SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") + } + } + + BiRel(customerA, SystemAA, "Uses") + BiRel(SystemAA, SystemE, "Uses") + Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") + Rel(SystemC, customerA, "Sends e-mails to") +
+ +
+
pie title Key elements in Product X diff --git a/src/Diagram.js b/src/Diagram.js index 8e1e7a6bb1..819caebe3d 100644 --- a/src/Diagram.js +++ b/src/Diagram.js @@ -1,3 +1,6 @@ +import c4Db from './diagrams/c4/c4Db'; +import c4Renderer from './diagrams/c4/c4Renderer'; +import c4Parser from './diagrams/c4/parser/c4Diagram'; import classDb from './diagrams/class/classDb'; import classRenderer from './diagrams/class/classRenderer'; import classRendererV2 from './diagrams/class/classRenderer-v2'; @@ -48,7 +51,13 @@ class Diagram { this.txt = txt; this.type = utils.detectType(txt, cnf); log.debug('Type ' + this.type); - switch (this.type) { + switch (this.type) { + case 'c4': + this.parser = c4Parser; + this.parser.parser.yy = c4Db; + this.db = c4Db; + this.renderer = c4Renderer; + break; case 'gitGraph': this.parser = gitGraphParser; this.parser.parser.yy = gitGraphAst; diff --git a/src/defaultConfig.js b/src/defaultConfig.js index 8b5a16d562..22e50f2cf0 100644 --- a/src/defaultConfig.js +++ b/src/defaultConfig.js @@ -1059,6 +1059,166 @@ const config = { showCommitLabel: true, showBranches: true, }, + + /** The object containing configurations specific for c4 diagrams */ + c4: { + useWidth: undefined, + + /** + * | Parameter | Description | Type | Required | Values | + * | -------------- | ---------------------------------------------------- | ------- | -------- | ------------------ | + * | diagramMarginX | Margin to the right and left of the sequence diagram | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 50 + */ + diagramMarginX: 50, + + /** + * | Parameter | Description | Type | Required | Values | + * | -------------- | ------------------------------------------------- | ------- | -------- | ------------------ | + * | diagramMarginY | Margin to the over and under the sequence diagram | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 10 + */ + diagramMarginY: 10, + + /** + * | Parameter | Description | Type | Required | Values | + * | ----------- | --------------------- | ------- | -------- | ------------------ | + * | actorMargin | Margin between persons | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 50 + */ + c4ShapeMargin: 50, + + c4ShapePadding: 20, + + /** + * | Parameter | Description | Type | Required | Values | + * | --------- | -------------------- | ------- | -------- | ------------------ | + * | width | Width of person boxes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 215 + */ + width: 216, + + /** + * | Parameter | Description | Type | Required | Values | + * | --------- | --------------------- | ------- | -------- | ------------------ | + * | height | Height of person boxes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 65 + */ + height: 60, + + /** + * | Parameter | Description | Type | Required | Values | + * | --------- | ------------------------ | ------- | -------- | ------------------ | + * | boxMargin | Margin around loop boxes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 10 + */ + boxMargin: 10, + + /** + * | Parameter | Description | Type | Required | Values | + * | ----------- | ----------- | ------- | -------- | ----------- | + * | useMaxWidth | See Notes | boolean | Required | true, false | + * + * **Notes:** When this flag is set to true, the height and width is set to 100% and is then + * scaling with the available space. If set to false, the absolute space required is used. + * + * Default value: true + */ + useMaxWidth: true, + + c4ShapeInRow: 4, + nextLinePaddingX: 0, + + c4BoundaryInRow: 2, + + + personFontSize: 14, + personFontFamily: '"Open Sans", sans-serif', + personFontWeight: "normal", + + systemFontSize: 14, + systemFontFamily: '"Open Sans", sans-serif', + systemFontWeight: "normal", + + boundaryFontSize: 14, + boundaryFontFamily: '"Open Sans", sans-serif', + boundaryFontWeight: "normal", + + messageFontSize: 12, + messageFontFamily: '"Open Sans", sans-serif', + messageFontWeight: "normal", + + /** + * This sets the auto-wrap state for the diagram + * + * **Notes:** Default value: true. + */ + wrap: true, + + /** + * This sets the auto-wrap padding for the diagram (sides only) + * + * **Notes:** Default value: 0. + */ + wrapPadding: 10, + + personFont: function () { + return { + fontFamily: this.personFontFamily, + fontSize: this.personFontSize, + fontWeight: this.personFontWeight, + }; + }, + + systemFont: function () { + return { + fontFamily: this.systemFontFamily, + fontSize: this.systemFontSize, + fontWeight: this.systemFontWeight, + }; + }, + + boundaryFont: function () { + return { + fontFamily: this.boundaryFontFamily, + fontSize: this.boundaryFontSize, + fontWeight: this.boundaryFontWeight, + }; + }, + + messageFont: function () { + return { + fontFamily: this.messageFontFamily, + fontSize: this.messageFontSize, + fontWeight: this.messageFontWeight, + }; + }, + + // ' Colors + // ' ################################## + person_bg_color: "#08427B", + person_border_color: "#073B6F", + external_person_bg_color: "#686868", + external_person_border_color: "#8A8A8A", + system_bg_color: "#1168BD", + system_border_color: "#3C7FC0", + system_db_bg_color: "#1168BD", + system_db_border_color: "#3C7FC0", + system_queue_bg_color: "#1168BD", + system_queue_border_color: "#3C7FC0", + external_system_bg_color: "#999999", + external_system_border_color: "#8A8A8A", + external_system_db_bg_color: "#999999", + external_system_db_border_color: "#8A8A8A", + external_system_queue_bg_color: "#999999", + external_system_queue_border_color: "#8A8A8A", + }, }; config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute; diff --git a/src/diagrams/c4/c4Db.js b/src/diagrams/c4/c4Db.js new file mode 100644 index 0000000000..d29d11c850 --- /dev/null +++ b/src/diagrams/c4/c4Db.js @@ -0,0 +1,307 @@ +import mermaidAPI from '../../mermaidAPI'; +import * as configApi from '../../config'; +import { log } from '../../logger'; +import { sanitizeText } from '../common/common'; + +let personOrSystemArray = []; +let boundaryParseStack = ['']; +let currentBoundaryParse = 'global'; +let parentBoundaryParse = ''; +let boundarys = [ + { + alias: 'global', + label: { text: 'global' }, + type: 'global', + tags: null, + link: null, + parentBoundary: '', + }, +]; +let rels = []; +let title = ''; +let wrapEnabled = false; +let description = ''; +let c4Type = 'C4Context'; + +export const getC4Type = function () { + return c4Type; +}; + +export const setC4Type = function (c4Type) { + let sanitizedText = sanitizeText(c4Type, configApi.getConfig()); + c4Type = sanitizedText; +}; + +export const parseDirective = function (statement, context, type) { + mermaidAPI.parseDirective(this, statement, context, type); +}; + +//type, from, to, label, ?techn, ?descr, ?sprite, ?tags, $link +export const addRel = function (type, from, to, label, techn, descr, sprite, tags, link) { + // Don't allow label nulling + if ( + type === undefined || + type === null || + from === undefined || + from === null || + to === undefined || + to === null || + label === undefined || + label === null + ) + return; + + let rel = {}; + const old = rels.find((rel) => rel.from === from && rel.to === to); + if (old) { + rel = old; + } else { + rels.push(rel); + } + + rel.type = type; + rel.from = from; + rel.to = to; + rel.label = { text: label }; + + if (descr === undefined || descr === null) { + rel.descr = { text: '' }; + } else { + rel.descr = { text: descr }; + } + + if (techn === undefined || techn === null) { + rel.techn = { text: '' }; + } else { + rel.techn = { text: techn }; + } + + // rel.techn = techn; + rel.sprite = sprite; + rel.tags = tags; + rel.link = link; + rel.wrap = autoWrap(); +}; + +//type, alias, label, ?descr, ?sprite, ?tags, $link +export const addPersonOrSystem = function (type, alias, label, descr, sprite, tags, link) { + // Don't allow label nulling + if (alias === null || label === null) return; + + let personOrSystem = {}; + const old = personOrSystemArray.find((personOrSystem) => personOrSystem.alias === alias); + if (old && alias === old.alias) { + personOrSystem = old; + } else { + personOrSystem.alias = alias; + personOrSystemArray.push(personOrSystem); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + personOrSystem.label = { text: '' }; + } else { + personOrSystem.label = { text: label }; + } + + if (descr === undefined || descr === null) { + personOrSystem.descr = { text: '' }; + } else { + personOrSystem.descr = { text: descr }; + } + + personOrSystem.wrap = autoWrap(); + personOrSystem.sprite = sprite; + personOrSystem.tags = tags; + personOrSystem.link = link; + personOrSystem.type = type; + personOrSystem.parentBoundary = currentBoundaryParse; +}; + +//alias, label, ?type, ?tags, $link +export const addBoundary = function (alias, label, type, tags, link) { + // if (parentBoundary === null) return; + + // Don't allow label nulling + if (alias === null || label === null) return; + + let boundary = {}; + const old = boundarys.find((boundary) => boundary.alias === alias); + if (old && alias === old.alias) { + boundary = old; + } else { + boundary.alias = alias; + boundarys.push(boundary); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + boundary.label = { text: '' }; + } else { + boundary.label = { text: label }; + } + + if (type === undefined || type === null) { + boundary.type = { text: 'system' }; + } else { + boundary.type = { text: type }; + } + + boundary.wrap = autoWrap(); + boundary.tags = tags; + boundary.link = link; + boundary.type = type; + boundary.parentBoundary = currentBoundaryParse; + + parentBoundaryParse = currentBoundaryParse; + currentBoundaryParse = alias; + boundaryParseStack.push(parentBoundaryParse); +}; + +export const popBoundaryParseStack = function () { + currentBoundaryParse = parentBoundaryParse; + boundaryParseStack.pop(); + parentBoundaryParse = boundaryParseStack.pop(); + boundaryParseStack.push(parentBoundaryParse); +}; + +export const getCurrentBoundaryParse = function () { + return currentBoundaryParse; +}; + +export const getParentBoundaryParse = function () { + return parentBoundaryParse; +}; + +export const getPersonOrSystemArray = function (parentBoundary) { + if (parentBoundary === undefined || parentBoundary === null) return personOrSystemArray; + else + return personOrSystemArray.filter((personOrSystem) => { + return personOrSystem.parentBoundary === parentBoundary; + }); +}; +export const getPersonOrSystem = function (alias) { + return personOrSystemArray.find((personOrSystem) => personOrSystem.alias === alias); +}; +export const getPersonOrSystemKeys = function (parentBoundary) { + return Object.keys(getPersonOrSystemArray(parentBoundary)); +}; + +export const getBoundarys = function (parentBoundary) { + if (parentBoundary === undefined || parentBoundary === null) return boundarys; + else return boundarys.filter((boundary) => boundary.parentBoundary === parentBoundary); +}; + +export const getRels = function () { + return rels; +}; + +export const getTitle = function () { + return title; +}; + +export const setWrap = function (wrapSetting) { + wrapEnabled = wrapSetting; +}; + +export const autoWrap = function () { + return wrapEnabled; +}; + +export const clear = function () { + personOrSystemArray = []; + boundarys = [ + { + alias: 'global', + label: { text: 'global' }, + type: 'global', + tags: null, + link: null, + parentBoundary: '', + }, + ]; + parentBoundaryParse = ''; + currentBoundaryParse = 'global'; + boundaryParseStack = ['']; + rels = []; +}; + +export const LINETYPE = { + SOLID: 0, + DOTTED: 1, + NOTE: 2, + SOLID_CROSS: 3, + DOTTED_CROSS: 4, + SOLID_OPEN: 5, + DOTTED_OPEN: 6, + LOOP_START: 10, + LOOP_END: 11, + ALT_START: 12, + ALT_ELSE: 13, + ALT_END: 14, + OPT_START: 15, + OPT_END: 16, + ACTIVE_START: 17, + ACTIVE_END: 18, + PAR_START: 19, + PAR_AND: 20, + PAR_END: 21, + RECT_START: 22, + RECT_END: 23, + SOLID_POINT: 24, + DOTTED_POINT: 25, +}; + +export const ARROWTYPE = { + FILLED: 0, + OPEN: 1, +}; + +export const PLACEMENT = { + LEFTOF: 0, + RIGHTOF: 1, + OVER: 2, +}; + +export const setTitle = function (txt) { + let sanitizedText = sanitizeText(txt, configApi.getConfig()); + title = sanitizedText; +}; + +const setAccDescription = function (description_lex) { + let sanitizedText = sanitizeText(description_lex, configApi.getConfig()); + description = sanitizedText; +}; + +const getAccDescription = function () { + return description; +}; + +export default { + addPersonOrSystem, + addBoundary, + popBoundaryParseStack, + addRel, + autoWrap, + setWrap, + getPersonOrSystemArray, + getPersonOrSystem, + getPersonOrSystemKeys, + getBoundarys, + getCurrentBoundaryParse, + getParentBoundaryParse, + getRels, + getTitle, + getC4Type, + getAccDescription, + setAccDescription, + parseDirective, + getConfig: () => configApi.getConfig().c4, + clear, + LINETYPE, + ARROWTYPE, + PLACEMENT, + setTitle, + setC4Type, + // apply, +}; diff --git a/src/diagrams/c4/c4Renderer.js b/src/diagrams/c4/c4Renderer.js new file mode 100644 index 0000000000..e3e1636d2d --- /dev/null +++ b/src/diagrams/c4/c4Renderer.js @@ -0,0 +1,609 @@ +import { select } from 'd3'; +import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw'; +import { log } from '../../logger'; +import { parser } from './parser/c4Diagram'; +import common from '../common/common'; +import c4Db from './c4Db'; +import * as configApi from '../../config'; +import utils, { + wrapLabel, + calculateTextWidth, + calculateTextHeight, + assignWithDepth, + configureSvgSize, +} from '../../utils'; +import addSVGAccessibilityFields from '../../accessibility'; + +let globalBoundaryMaxX = 0, + globalBoundaryMaxY = 0; + +parser.yy = c4Db; + +let conf = {}; + +class Bounds { + constructor() { + this.name = ''; + this.data = {}; + this.data.startx = undefined; + this.data.stopx = undefined; + this.data.starty = undefined; + this.data.stopy = undefined; + this.data.widthLimit = undefined; + + this.nextData = {}; + this.nextData.startx = undefined; + this.nextData.stopx = undefined; + this.nextData.starty = undefined; + this.nextData.stopy = undefined; + + setConf(parser.yy.getConfig()); + } + + setData(startx, stopx, starty, stopy) { + this.nextData.startx = this.data.startx = startx; + this.nextData.stopx = this.data.stopx = stopx; + this.nextData.starty = this.data.starty = starty; + this.nextData.stopy = this.data.stopy = stopy; + } + + updateVal(obj, key, val, fun) { + if (typeof obj[key] === 'undefined') { + obj[key] = val; + } else { + obj[key] = fun(val, obj[key]); + } + } + + insert(c4Shape) { + let _startx = this.nextData.stopx + c4Shape.margin * 2; + let _stopx = _startx + c4Shape.width; + let _starty = this.nextData.starty + c4Shape.margin * 2; + let _stopy = _starty + c4Shape.height; + if (_startx >= this.data.widthLimit || _stopx >= this.data.widthLimit) { + _startx = this.nextData.startx + c4Shape.margin * 2 + conf.nextLinePaddingX; + _starty = this.nextData.stopy + c4Shape.margin * 2; + + this.nextData.stopx = _stopx = _startx + c4Shape.width; + this.nextData.starty = this.nextData.stopy; + this.nextData.stopy = _stopy = _starty + c4Shape.height; + } + + c4Shape.x = _startx; + c4Shape.y = _starty; + + this.updateVal(this.data, 'startx', _startx, Math.min); + this.updateVal(this.data, 'starty', _starty, Math.min); + this.updateVal(this.data, 'stopx', _stopx, Math.max); + this.updateVal(this.data, 'stopy', _stopy, Math.max); + + this.updateVal(this.nextData, 'startx', _startx, Math.min); + this.updateVal(this.nextData, 'starty', _starty, Math.min); + this.updateVal(this.nextData, 'stopx', _stopx, Math.max); + this.updateVal(this.nextData, 'stopy', _stopy, Math.max); + } + + init() { + this.data = { + startx: undefined, + stopx: undefined, + starty: undefined, + stopy: undefined, + widthLimit: undefined, + }; + setConf(parser.yy.getConfig()); + } + + bumpLastMargin(margin) { + this.data.stopx += margin; + this.data.stopy += margin; + } +} + +const personFont = (cnf) => { + return { + fontFamily: cnf.personFontFamily, + fontSize: cnf.personFontSize, + fontWeight: cnf.personFontWeight, + }; +}; + +const systemFont = (cnf) => { + return { + fontFamily: cnf.systemFontFamily, + fontSize: cnf.systemFontSize, + fontWeight: cnf.systemFontWeight, + }; +}; + +const boundaryFont = (cnf) => { + return { + fontFamily: cnf.boundaryFontFamily, + fontSize: cnf.boundaryFontSize, + fontWeight: cnf.boundaryFontWeight, + }; +}; + +const messageFont = (cnf) => { + return { + fontFamily: cnf.messageFontFamily, + fontSize: cnf.messageFontSize, + fontWeight: cnf.messageFontWeight, + }; +}; + +/** + * @param textType + * @param c4Shape + * @param c4ShapeTextWrap + * @param textConf + * @param textLimitWidth + */ +function setC4ShapeText(textType, c4Shape, c4ShapeTextWrap, textConf, textLimitWidth) { + if (!c4Shape[textType].width) { + if (c4ShapeTextWrap) { + c4Shape[textType].text = wrapLabel(c4Shape[textType].text, textLimitWidth, textConf); + c4Shape[textType].labelLines = c4Shape[textType].text.split(common.lineBreakRegex).length; + c4Shape[textType].width = textLimitWidth; + c4Shape[textType].height = c4Shape[textType].labelLines * (textConf.fontSize + 2); + } else { + let lines = c4Shape[textType].text.split(common.lineBreakRegex); + c4Shape[textType].labelLines = lines.length; + let lineHeight = 0; + c4Shape[textType].height = 0; + c4Shape[textType].width = 0; + for (let i = 0; i < lines.length; i++) { + c4Shape[textType].width = Math.max( + calculateTextWidth(lines[i], textConf), + c4Shape[textType].width + ); + lineHeight = calculateTextHeight(lines[i], textConf); + c4Shape[textType].height = c4Shape[textType].height + lineHeight; + } + // c4Shapes[textType].height = c4Shapes[textType].labelLines * textConf.fontSize; + } + } +} + +export const drawBoundary = function (diagram, boundary, bounds) { + boundary.x = bounds.data.startx; + boundary.y = bounds.data.starty; + boundary.width = bounds.data.stopx - bounds.data.startx; + boundary.height = bounds.data.stopy - bounds.data.starty; + + boundary.label.y = conf.c4ShapeMargin - 35; + + let boundaryTextWrap = boundary.wrap && conf.wrap; + let boundaryLabelConf = boundaryFont(conf); + boundaryLabelConf.fontSize = boundaryLabelConf.fontSize + 2; + boundaryLabelConf.fontWeight = 'bold'; + let textLimitWidth = calculateTextWidth(boundary.label.text, boundaryLabelConf); + setC4ShapeText('label', boundary, boundaryTextWrap, boundaryLabelConf, textLimitWidth); + + svgDraw.drawBoundary(diagram, boundary, conf); +}; + +export const drawPersonOrSystemArray = function ( + currentBounds, + diagram, + personOrSystemArray, + personOrSystemKeys +) { + // Draw the personOrSystemArray + + // let prevWidth = currentBounds.data.stopx; + // let prevMarginX = conf.c4ShapeMargin; + // let prevMarginY = conf.c4ShapeMargin; + // let maxHeight = currentBounds.data.starty; + + for (let i = 0; i < personOrSystemKeys.length; i++) { + const personOrSystem = personOrSystemArray[personOrSystemKeys[i]]; + + let imageWidth = 0, + imageHeight = 0; + switch (personOrSystem.type) { + case 'person': + case 'external_person': + imageWidth = 48; + imageHeight = 48; + break; + } + + if (!personOrSystem.typeLabelWidth) { + let personOrSystemTypeConf = personFont(conf); + personOrSystemTypeConf.fontSize = personOrSystemTypeConf.fontSize - 2; + personOrSystem.typeLabelWidth = calculateTextWidth( + '<<' + personOrSystem.type + '>>', + personOrSystemTypeConf + ); + personOrSystem.typeLabelHeight = personOrSystemTypeConf.fontSize + 2; + + switch (personOrSystem.type) { + case 'system_db': + case 'external_system_db': + personOrSystem.typeLabelY = conf.c4ShapePadding; + break; + default: + personOrSystem.typeLabelY = conf.c4ShapePadding - 5; + break; + } + } + + let personOrSystemTextWrap = personOrSystem.wrap && conf.wrap; + let textLimitWidth = conf.width - conf.c4ShapePadding * 2; + + let personOrSystemLabelConf = personFont(conf); + personOrSystemLabelConf.fontSize = personOrSystemLabelConf.fontSize + 2; + personOrSystemLabelConf.fontWeight = 'bold'; + + setC4ShapeText( + 'label', + personOrSystem, + personOrSystemTextWrap, + personOrSystemLabelConf, + textLimitWidth + ); + personOrSystem['label'].Y = + conf.c4ShapePadding + personOrSystem.typeLabelHeight + imageHeight + 10; + + let personOrSystemDescrConf = personFont(conf); + setC4ShapeText( + 'descr', + personOrSystem, + personOrSystemTextWrap, + personOrSystemDescrConf, + textLimitWidth + ); + personOrSystem['descr'].Y = + conf.c4ShapePadding + + personOrSystem.typeLabelHeight + + imageHeight + + 5 + + personOrSystem.label.height + + conf.personFontSize + + 2; + + // Add some rendering data to the object + let rectWidth = + Math.max(personOrSystem.label.width, personOrSystem.descr.width) + conf.c4ShapePadding * 2; + let rectHeight = + conf.c4ShapePadding + + personOrSystem.typeLabelHeight + + imageHeight + + personOrSystem.label.height + + conf.personFontSize + + 2 + + personOrSystem.descr.height; + + personOrSystem.width = Math.max(personOrSystem.width || conf.width, rectWidth, conf.width); + personOrSystem.height = Math.max(personOrSystem.height || conf.height, rectHeight, conf.height); + personOrSystem.margin = personOrSystem.margin || conf.c4ShapeMargin; + + currentBounds.insert(personOrSystem); + + const height = svgDraw.drawPersonOrSystem(diagram, personOrSystem, conf); + } + + currentBounds.bumpLastMargin(conf.c4ShapeMargin); +}; + +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +/* * * + * Get the intersection of the line between the center point of a rectangle and a point outside the rectangle. + * Algorithm idea. + * Using a point outside the rectangle as the coordinate origin, the graph is divided into four quadrants, and each quadrant is divided into two cases, with separate treatment on the coordinate axes + * 1. The case of coordinate axes. + * 1. The case of the negative x-axis + * 2. The case of the positive x-axis + * 3. The case of the positive y-axis + * 4. The negative y-axis case + * 2. Quadrant cases. + * 2.1. first quadrant: the case where the line intersects the left side of the rectangle; the case where it intersects the lower side of the rectangle + * 2.2. second quadrant: the case where the line intersects the right side of the rectangle; the case where it intersects the lower edge of the rectangle + * 2.3. third quadrant: the case where the line intersects the right side of the rectangle; the case where it intersects the upper edge of the rectangle + * 2.4. fourth quadrant: the case where the line intersects the left side of the rectangle; the case where it intersects the upper side of the rectangle + * + */ +let getIntersectPoint = function (fromNode, endPoint) { + let x1 = fromNode.x; + + let y1 = fromNode.y; + + let x2 = endPoint.x; + + let y2 = endPoint.y; + + let fromCenterX = x1 + fromNode.width / 2; + + let fromCenterY = y1 + fromNode.height / 2; + + let dx = Math.abs(x1 - x2); + + let dy = Math.abs(y1 - y2); + + let tanDYX = dy / dx; + + let fromDYX = fromNode.height / fromNode.width; + + let returnPoint = null; + + if (y1 == y2 && x1 < x2) { + returnPoint = new Point(x1 + fromNode.width, fromCenterY); + } else if (y1 == y2 && x1 > x2) { + returnPoint = new Point(x1, fromCenterY); + } else if (x1 == x2 && y1 < y2) { + returnPoint = new Point(fromCenterX, y1 + fromNode.height); + } else if (x1 == x2 && y1 > y2) { + returnPoint = new Point(fromCenterX, y1); + } + + if (x1 > x2 && y1 < y2) { + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1, fromCenterY + (tanDYX * fromNode.width) / 2); + } else { + returnPoint = new Point( + fromCenterX - ((dx / dy) * fromNode.height) / 2, + y1 + fromNode.height + ); + } + } else if (x1 < x2 && y1 < y2) { + // + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1 + fromNode.width, fromCenterY + (tanDYX * fromNode.width) / 2); + } else { + returnPoint = new Point( + fromCenterX + ((dx / dy) * fromNode.height) / 2, + y1 + fromNode.height + ); + } + } else if (x1 < x2 && y1 > y2) { + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1 + fromNode.width, fromCenterY - (tanDYX * fromNode.width) / 2); + } else { + returnPoint = new Point(fromCenterX + ((fromNode.height / 2) * dx) / dy, y1); + } + } else if (x1 > x2 && y1 > y2) { + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1, fromCenterY - (fromNode.width / 2) * tanDYX); + } else { + returnPoint = new Point(fromCenterX - ((fromNode.height / 2) * dx) / dy, y1); + } + } + return returnPoint; +}; + +let getIntersectPoints = function (fromNode, endNode) { + let endIntersectPoint = { x: 0, y: 0 }; + endIntersectPoint.x = endNode.x + endNode.width / 2; + endIntersectPoint.y = endNode.y + endNode.height / 2; + let startPoint = getIntersectPoint(fromNode, endIntersectPoint); + + endIntersectPoint.x = fromNode.x + fromNode.width / 2; + endIntersectPoint.y = fromNode.y + fromNode.height / 2; + let endPoint = getIntersectPoint(endNode, endIntersectPoint); + return { startPoint: startPoint, endPoint: endPoint }; +}; + +export const drawRels = function (diagram, rels, getC4ShapeObj) { + for (let rel of rels) { + let relTextWrap = rel.wrap && conf.wrap; + let relConf = messageFont(conf); + let textLimitWidth = calculateTextWidth(rel.label.text, relConf); + setC4ShapeText('label', rel, relTextWrap, relConf, textLimitWidth); + + if (rel.techn && rel.techn.text !== '') { + textLimitWidth = calculateTextWidth(rel.techn.text, relConf); + setC4ShapeText('techn', rel, relTextWrap, relConf, textLimitWidth); + } + + if (rel.descr && rel.descr.text !== '') { + textLimitWidth = calculateTextWidth(rel.descr.text, relConf); + setC4ShapeText('descr', rel, relTextWrap, relConf, textLimitWidth); + } + + let fromNode = getC4ShapeObj(rel.from); + let endNode = getC4ShapeObj(rel.to); + let points = getIntersectPoints(fromNode, endNode); + rel.startPoint = points.startPoint; + rel.endPoint = points.endPoint; + } + svgDraw.drawRels(diagram, rels, conf); +}; + +export const setConf = function (cnf) { + assignWithDepth(conf, cnf); + + if (cnf.fontFamily) { + conf.personFontFamily = conf.systemFontFamily = conf.messageFontFamily = cnf.fontFamily; + } + if (cnf.fontSize) { + conf.personFontSize = conf.systemFontSize = conf.messageFontSize = cnf.fontSize; + } + if (cnf.fontWeight) { + conf.personFontWeight = conf.systemFontWeight = conf.messageFontWeight = cnf.fontWeight; + } +}; + +/** + * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text. + * + * @param {any} text + * @param {any} id + */ +export const draw = function (text, id) { + conf = configApi.getConfig().c4; + const securityLevel = configApi.getConfig().securityLevel; + // Handle root and ocument for when rendering in sanbox mode + let sandboxElement; + if (securityLevel === 'sandbox') { + sandboxElement = select('#i' + id); + } + const root = + securityLevel === 'sandbox' + ? select(sandboxElement.nodes()[0].contentDocument.body) + : select('body'); + const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; + + parser.yy.clear(); + parser.yy.setWrap(conf.wrap); + parser.parse(text + '\n'); + + log.debug(`C:${JSON.stringify(conf, null, 2)}`); + + const diagram = + securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : select(`[id="${id}"]`); + + svgDraw.insertComputerIcon(diagram); + svgDraw.insertDatabaseIcon(diagram); + svgDraw.insertClockIcon(diagram); + + let screenBounds = new Bounds(); + screenBounds.setData( + conf.diagramMarginX, + conf.diagramMarginX, + conf.diagramMarginY, + conf.diagramMarginY + ); + + screenBounds.data.widthLimit = screen.availWidth; + globalBoundaryMaxX = conf.diagramMarginX; + globalBoundaryMaxY = conf.diagramMarginY; + + const title = parser.yy.getTitle(); + const c4type = parser.yy.getC4Type(); + switch (c4type) { + case 'C4Context': + /** + * @param parentBoundaryAlias + * @param parentBounds + * @param currentBoundarys + */ + function drawInsideBoundary(parentBoundaryAlias, parentBounds, currentBoundarys) { + let currentBounds = new Bounds(); + // Calculate the width limit of the boundar. label/type 的长度, + currentBounds.data.widthLimit = Math.min( + conf.width * conf.c4ShapeInRow + conf.c4ShapeMargin * (conf.c4ShapeInRow + 1), + parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundarys.length) + ); + for (let i = 0; i < currentBoundarys.length; i++) { + let currentBoundary = currentBoundarys[i]; + if (i == 0) { + // Calculate the drawing start point of the currentBoundarys. + let _x = parentBounds.data.startx + conf.diagramMarginX; + let _y = parentBounds.data.stopy + conf.diagramMarginY; + + currentBounds.setData(_x, _x, _y, _y); + } else { + // Calculate the drawing start point of the currentBoundarys. + let _x = + currentBounds.data.stopx !== currentBounds.data.startx + ? currentBounds.data.stopx + conf.diagramMarginX + : currentBounds.data.startx; + let _y = currentBounds.data.starty; + + currentBounds.setData(_x, _x, _y, _y); + } + currentBounds.name = currentBoundary.alias; + let currentPersonOrSystemArray = parser.yy.getPersonOrSystemArray(currentBoundary.alias); + let currentPersonOrSystemKeys = parser.yy.getPersonOrSystemKeys(currentBoundary.alias); + + if (currentPersonOrSystemKeys.length > 0) { + drawPersonOrSystemArray( + currentBounds, + diagram, + currentPersonOrSystemArray, + currentPersonOrSystemKeys + ); + } + parentBoundaryAlias = currentBoundary.alias; + let nextCurrentBoundarys = parser.yy.getBoundarys(parentBoundaryAlias); + + if (nextCurrentBoundarys.length > 0) { + // draw boundary inside currentBoundary + // bounds.init(); + // parentBoundaryWidthLimit = bounds.data.stopx - bounds.startx; + drawInsideBoundary(parentBoundaryAlias, currentBounds, nextCurrentBoundarys); + } + // draw boundary + if (currentBoundary.alias !== 'global') + drawBoundary(diagram, currentBoundary, currentBounds); + parentBounds.data.stopy = Math.max( + currentBounds.data.stopy + conf.c4ShapeMargin, + parentBounds.data.stopy + ); + parentBounds.data.stopx = Math.max( + currentBounds.data.stopx + conf.c4ShapeMargin, + parentBounds.data.stopx + ); + globalBoundaryMaxX = Math.max(globalBoundaryMaxX, parentBounds.data.stopx); + globalBoundaryMaxY = Math.max(globalBoundaryMaxY, parentBounds.data.stopy); + } + } + + let currentBoundarys = parser.yy.getBoundarys(''); + drawInsideBoundary('', screenBounds, currentBoundarys); + + break; + } + + // The arrow head definition is attached to the svg once + svgDraw.insertArrowHead(diagram); + svgDraw.insertArrowEnd(diagram); + svgDraw.insertArrowCrossHead(diagram); + svgDraw.insertArrowFilledHead(diagram); + + drawRels(diagram, parser.yy.getRels(), parser.yy.getPersonOrSystem); + + screenBounds.data.stopx = globalBoundaryMaxX; + screenBounds.data.stopy = globalBoundaryMaxY; + + const box = screenBounds.data; + + // Make sure the height of the diagram supports long menus. + let boxHeight = box.stopy - box.starty; + + let height = boxHeight + 2 * conf.diagramMarginY; + + // Make sure the width of the diagram supports wide menus. + let boxWidth = box.stopx - box.startx; + const width = boxWidth + 2 * conf.diagramMarginX; + + if (title) { + diagram + .append('text') + .text(title) + .attr('x', (box.stopx - box.startx) / 2 - 4 * conf.diagramMarginX) + .attr('y', -25); + } + + configureSvgSize(diagram, height, width, conf.useMaxWidth); + + const extraVertForTitle = title ? 60 : 0; + diagram.attr( + 'viewBox', + box.startx - + conf.diagramMarginX + + ' -' + + (conf.diagramMarginY + extraVertForTitle) + + ' ' + + width + + ' ' + + (height + extraVertForTitle) + ); + + addSVGAccessibilityFields(parser.yy, diagram, id); + log.debug(`models:`, box); +}; + +export default { + drawPersonOrSystemArray, + drawBoundary, + setConf, + draw, +}; diff --git a/src/diagrams/c4/parser/c4Diagram.jison b/src/diagrams/c4/parser/c4Diagram.jison new file mode 100644 index 0000000000..dd9672226b --- /dev/null +++ b/src/diagrams/c4/parser/c4Diagram.jison @@ -0,0 +1,267 @@ +/** mermaid + * https://mermaidjs.github.io/ + * (c) 2022 mzhx.meng@gmail.com + * MIT license. + */ + +/* lexical grammar */ +%lex + +/* context */ +%x person +%x person_ext +%x system +%x system_db +%x system_queue +%x system_ext +%x system_ext_db +%x system_ext_queue +%x boundary +%x enterprise_boundary +%x system_boundary +%x rel +%x birel +%x rel_u +%x rel_d +%x rel_l +%x rel_r + +/* container */ +%x container +%x container_db +%x container_queue +%x container_ext +%x container_ext_db +%x container_ext_queue +%x container_boundary + +/* component */ +%x component +%x component_db +%x component_queue +%x component_ext +%x component_ext_db +%x component_ext_queue + +/* Dynamic diagram */ +%x rel_index +%x index + +/* Deployment diagram */ +%x deployment_node +%x node +%x node_l +%x node_r + +/* Relationship Types */ +%x rel +%x rel_bi +%x rel_up +%x rel_down +%x rel_left +%x rel_right + +%x attribute +%x string + +%x open_directive +%x type_directive +%x arg_directive + +%% +\%\%\{ { this.begin('open_directive'); return 'open_directive'; } +.*direction\s+TB[^\n]* return 'direction_tb'; +.*direction\s+BT[^\n]* return 'direction_bt'; +.*direction\s+RL[^\n]* return 'direction_rl'; +.*direction\s+LR[^\n]* return 'direction_lr'; +((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } +":" { this.popState(); this.begin('arg_directive'); return ':'; } +\}\%\% { this.popState(); this.popState(); return 'close_directive'; } +((?:(?!\}\%\%).|\n)*) return 'arg_directive'; +\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */ +\%\%[^\n]*(\r?\n)* c /* skip comments */ + +"title"\s[^#\n;]+ return 'title'; +"accDescription"\s[^#\n;]+ return 'accDescription'; + +\s*(\r?\n)+ return 'NEWLINE'; +\s+ /* skip whitespace */ +"C4Context" return 'C4_CONTEXT'; +"C4Container" return 'C4_CONTAINER'; +"C4Component" return 'C4_COMPONENT'; +"C4Dynamic" return 'C4_DYNAMIC'; +"C4Deployment" return 'C4_DEPLOYMENT'; + +"Person_Ext" { this.begin("person_ext"); console.log('begin person_ext'); return 'PERSON_EXT';} +"Person" { this.begin("person"); console.log('begin person'); return 'PERSON';} +"SystemQueue_Ext" { this.begin("system_ext_queue"); console.log('begin system_ext_queue'); return 'SYSTEM_EXT_QUEUE';} +"SystemDb_Ext" { this.begin("system_ext_db"); console.log('begin system_ext_db'); return 'SYSTEM_EXT_DB';} +"System_Ext" { this.begin("system_ext"); console.log('begin system_ext'); return 'SYSTEM_EXT';} +"SystemQueue" { this.begin("system_queue"); console.log('begin system_queue'); return 'SYSTEM_QUEUE';} +"SystemDb" { this.begin("system_db"); console.log('begin system_db'); return 'SYSTEM_DB';} +"System" { this.begin("system"); console.log('begin system'); return 'SYSTEM';} + +"Boundary" { this.begin("boundary"); console.log('begin boundary'); return 'BOUNDARY';} +"Enterprise_Boundary" { this.begin("enterprise_boundary"); console.log('begin enterprise_boundary'); return 'ENTERPRISE_BOUNDARY';} +"System_Boundary" { this.begin("system_boundary"); console.log('begin system_boundary'); return 'SYSTEM_BOUNDARY';} + +"Rel" { this.begin("rel"); console.log('begin rel'); return 'REL';} +"BiRel" { this.begin("birel"); console.log('begin birel'); return 'BIREL';} +"Rel_U|Rel_Up" { this.begin("rel_u"); console.log('begin rel_u'); return 'REL_U';} +"Rel_D|Rel_Down" { this.begin("rel_d"); console.log('begin rel_d'); return 'REL_D';} +"Rel_L|Rel_Left" { this.begin("rel_l"); console.log('begin rel_l'); return 'REL_L';} +"Rel_R|Rel_Right" { this.begin("rel_r"); console.log('begin rel_r'); return 'REL_R';} + + +<> return "EOF_IN_STRUCT"; +[(][ ]*[,] { console.log('begin attribute with ATTRIBUTE_EMPTY'); this.begin("attribute"); return "ATTRIBUTE_EMPTY";} +[(] { console.log('begin attribute'); this.begin("attribute"); } +[)] { console.log('STOP attribute'); this.popState();console.log('STOP diagram'); this.popState();} + +",," { console.log(',,'); return 'ATTRIBUTE_EMPTY';} +"," { console.log(','); } +[ ]*["]["] { console.log('ATTRIBUTE_EMPTY'); return 'ATTRIBUTE_EMPTY';} +[ ]*["] { console.log('begin string'); this.begin("string");} +["] { console.log('STOP string'); this.popState(); } +[^"]* { console.log('STR'); return "STR";} +[^,]+ { console.log('not STR'); return "STR";} + +'{' { /* this.begin("lbrace"); */ console.log('begin boundary block'); return "LBRACE";} +'}' { /* this.popState(); */ console.log('STOP boundary block'); return "RBRACE";} + +[\s]+ return 'SPACE'; +[\n\r]+ return 'EOL'; +<> return 'EOF'; + +/lex + +/* operator associations and precedence */ + +%left '^' + +%start start + +%% /* language grammar */ + +start + : mermaidDoc + | direction + | directive start + ; + +direction + : direction_tb + { yy.setDirection('TB');} + | direction_bt + { yy.setDirection('BT');} + | direction_rl + { yy.setDirection('RL');} + | direction_lr + { yy.setDirection('LR');} + ; + +mermaidDoc + : graphConfig + ; + +directive + : openDirective typeDirective closeDirective NEWLINE + | openDirective typeDirective ':' argDirective closeDirective NEWLINE + ; + +openDirective + : open_directive { console.log("open_directive: ", $1); yy.parseDirective('%%{', 'open_directive'); } + ; + +typeDirective + : type_directive { } + ; + +argDirective + : arg_directive { $1 = $1.trim().replace(/'/g, '"'); console.log("arg_directive: ", $1); yy.parseDirective($1, 'arg_directive'); } + ; + +closeDirective + : close_directive { console.log("close_directive: ", $1); yy.parseDirective('}%%', 'close_directive', 'c4Context'); } + ; + +graphConfig + : C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)} + | C4_CONTAINER NEWLINE statements EOF {yy.setC4Type($1)} + | C4_COMPONENT NEWLINE statements EOF {yy.setC4Type($1)} + | C4_DYNAMIC NEWLINE statements EOF {yy.setC4Type($1)} + | C4_DEPLOYMENT NEWLINE statements EOF {yy.setC4Type($1)} + ; + +statements + : otherStatements + | diagramStatements + | otherStatements diagramStatements + ; + +otherStatements + : otherStatement + | otherStatement NEWLINE + | otherStatement NEWLINE otherStatements + ; + +otherStatement + : title {yy.setTitle($1.substring(6));$$=$1.substring(6);} + | accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);} + ; + +boundaryStatement + : boundaryStartStatement diagramStatements boundaryStopStatement + ; + +boundaryStartStatement + : boundaryStart LBRACE NEWLINE + | boundaryStart NEWLINE LBRACE + | boundaryStart NEWLINE LBRACE NEWLINE + ; + +boundaryStart + : ENTERPRISE_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addBoundary(...$2); $$=$2;} + | SYSTEM_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addBoundary(...$2); $$=$2;} + | BOUNDARY attributes {console.log($1,JSON.stringify($2)); yy.addBoundary(...$2); $$=$2;} + ; + +boundaryStopStatement + : RBRACE { yy.popBoundaryParseStack() } + ; + +diagramStatements + : diagramStatement + | diagramStatement NEWLINE + | diagramStatement NEWLINE statements + ; + +diagramStatement + : PERSON attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('person', ...$2); $$=$2;} + | PERSON_EXT attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_person', ...$2); $$=$2;} + | SYSTEM attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('system', ...$2); $$=$2;} + | SYSTEM_DB attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('system_db', ...$2); $$=$2;} + | SYSTEM_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('system_queue', ...$2); $$=$2;} + | SYSTEM_EXT attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system', ...$2); $$=$2;} + | SYSTEM_EXT_DB attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system_db', ...$2); $$=$2;} + | SYSTEM_EXT_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;} + | boundaryStatement + | REL attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel', ...$2); $$=$2;} + | BIREL attributes {console.log($1,JSON.stringify($2)); yy.addRel('birel', ...$2); $$=$2;} + | REL_U attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_u', ...$2); $$=$2;} + | REL_D attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_d', ...$2); $$=$2;} + | REL_L attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_l', ...$2); $$=$2;} + | REL_R attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_r', ...$2); $$=$2;} + ; + +attributes + : attribute { console.log('PUSH ATTRIBUTE: ', $1); $$ = [$1]; } + | attribute attributes { console.log('PUSH ATTRIBUTE: ', $1); $2.unshift($1); $$=$2;} + ; + +attribute + : STR { $$ = $1.trim(); } + | ATTRIBUTE { $$ = $1.trim(); } + | ATTRIBUTE_EMPTY { $$ = ""; } + ; + diff --git a/src/diagrams/c4/styles.js b/src/diagrams/c4/styles.js new file mode 100644 index 0000000000..c24412b3c7 --- /dev/null +++ b/src/diagrams/c4/styles.js @@ -0,0 +1,8 @@ +const getStyles = (options) => + `.person { + stroke: ${options.personBorder}; + fill: ${options.personBkg}; + } +`; + +export default getStyles; diff --git a/src/diagrams/c4/svgDraw.js b/src/diagrams/c4/svgDraw.js new file mode 100644 index 0000000000..870d30edaf --- /dev/null +++ b/src/diagrams/c4/svgDraw.js @@ -0,0 +1,805 @@ +import common from '../common/common'; +import { addFunction } from '../../interactionDb'; +import { sanitizeUrl } from '@braintree/sanitize-url'; + +export const drawRect = function (elem, rectData) { + const rectElem = elem.append('rect'); + rectElem.attr('x', rectData.x); + rectElem.attr('y', rectData.y); + rectElem.attr('fill', rectData.fill); + rectElem.attr('stroke', rectData.stroke); + rectElem.attr('width', rectData.width); + rectElem.attr('height', rectData.height); + rectElem.attr('rx', rectData.rx); + rectElem.attr('ry', rectData.ry); + + if (rectData.attrs !== 'undefined' && rectData.attrs !== null) { + for (let attrKey in rectData.attrs) rectElem.attr(attrKey, rectData.attrs[attrKey]); + } + + if (rectData.class !== 'undefined') { + rectElem.attr('class', rectData.class); + } + + return rectElem; +}; + +export const drawImage = function (elem, width, height, x, y, link) { + const imageElem = elem.append('image'); + imageElem.attr('width', width); + imageElem.attr('height', height); + imageElem.attr('x', x); + imageElem.attr('y', y); + let sanitizedLink = link.startsWith('data:image/png;base64') ? link : sanitizeUrl(link); + imageElem.attr('xlink:href', sanitizedLink); +}; + +export const drawEmbeddedImage = function (elem, x, y, link) { + const imageElem = elem.append('use'); + imageElem.attr('x', x); + imageElem.attr('y', y); + var sanitizedLink = sanitizeUrl(link); + imageElem.attr('xlink:href', '#' + sanitizedLink); +}; + +export const drawText = function (elem, textData) { + let prevTextHeight = 0, + textHeight = 0; + const lines = textData.text.split(common.lineBreakRegex); + + let textElems = []; + let dy = 0; + let yfunc = () => textData.y; + if ( + typeof textData.valign !== 'undefined' && + typeof textData.textMargin !== 'undefined' && + textData.textMargin > 0 + ) { + switch (textData.valign) { + case 'top': + case 'start': + yfunc = () => Math.round(textData.y + textData.textMargin); + break; + case 'middle': + case 'center': + yfunc = () => + Math.round(textData.y + (prevTextHeight + textHeight + textData.textMargin) / 2); + break; + case 'bottom': + case 'end': + yfunc = () => + Math.round( + textData.y + + (prevTextHeight + textHeight + 2 * textData.textMargin) - + textData.textMargin + ); + break; + } + } + if ( + typeof textData.anchor !== 'undefined' && + typeof textData.textMargin !== 'undefined' && + typeof textData.width !== 'undefined' + ) { + switch (textData.anchor) { + case 'left': + case 'start': + textData.x = Math.round(textData.x + textData.textMargin); + textData.anchor = 'start'; + textData.dominantBaseline = 'text-after-edge'; + textData.alignmentBaseline = 'middle'; + break; + case 'middle': + case 'center': + textData.x = Math.round(textData.x + textData.width / 2); + textData.anchor = 'middle'; + textData.dominantBaseline = 'middle'; + textData.alignmentBaseline = 'middle'; + break; + case 'right': + case 'end': + textData.x = Math.round(textData.x + textData.width - textData.textMargin); + textData.anchor = 'end'; + textData.dominantBaseline = 'text-before-edge'; + textData.alignmentBaseline = 'middle'; + break; + } + } + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if ( + typeof textData.textMargin !== 'undefined' && + textData.textMargin === 0 && + typeof textData.fontSize !== 'undefined' + ) { + dy = i * textData.fontSize; + } + + const textElem = elem.append('text'); + textElem.attr('x', textData.x); + textElem.attr('y', yfunc()); + if (typeof textData.anchor !== 'undefined') { + textElem + .attr('text-anchor', textData.anchor) + .attr('dominant-baseline', textData.dominantBaseline) + .attr('alignment-baseline', textData.alignmentBaseline); + } + if (typeof textData.fontFamily !== 'undefined') { + textElem.style('font-family', textData.fontFamily); + } + if (typeof textData.fontSize !== 'undefined') { + textElem.style('font-size', textData.fontSize); + } + if (typeof textData.fontWeight !== 'undefined') { + textElem.style('font-weight', textData.fontWeight); + } + if (typeof textData.fill !== 'undefined') { + textElem.attr('fill', textData.fill); + } + if (typeof textData.class !== 'undefined') { + textElem.attr('class', textData.class); + } + if (typeof textData.dy !== 'undefined') { + textElem.attr('dy', textData.dy); + } else if (dy !== 0) { + textElem.attr('dy', dy); + } + + if (textData.tspan) { + const span = textElem.append('tspan'); + span.attr('x', textData.x); + if (typeof textData.fill !== 'undefined') { + span.attr('fill', textData.fill); + } + span.text(line); + } else { + textElem.text(line); + } + if ( + typeof textData.valign !== 'undefined' && + typeof textData.textMargin !== 'undefined' && + textData.textMargin > 0 + ) { + textHeight += (textElem._groups || textElem)[0][0].getBBox().height; + prevTextHeight = textHeight; + } + + textElems.push(textElem); + } + + return textElems; +}; + +export const drawLabel = function (elem, txtObject) { + /** + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} cut + * @returns {any} + */ + function genPoints(x, y, width, height, cut) { + return ( + x + + ',' + + y + + ' ' + + (x + width) + + ',' + + y + + ' ' + + (x + width) + + ',' + + (y + height - cut) + + ' ' + + (x + width - cut * 1.2) + + ',' + + (y + height) + + ' ' + + x + + ',' + + (y + height) + ); + } + const polygon = elem.append('polygon'); + polygon.attr('points', genPoints(txtObject.x, txtObject.y, txtObject.width, txtObject.height, 7)); + polygon.attr('class', 'labelBox'); + + txtObject.y = txtObject.y + txtObject.height / 2; + + drawText(elem, txtObject); + return polygon; +}; + +export const drawRels = (elem, rels, conf) => { + const relsElem = elem.append('g'); + let i = 0; + for (let rel of rels) { + let url = ''; + if (i === 0) { + let line = relsElem.append('line'); + line.attr('x1', rel.startPoint.x); + line.attr('y1', rel.startPoint.y); + line.attr('x2', rel.endPoint.x); + line.attr('y2', rel.endPoint.y); + + line.attr('stroke-width', '1'); + line.attr('stroke', '#444444'); + line.style('fill', 'none'); + line.attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type === 'birel') line.attr('marker-start', 'url(' + url + '#arrowend)'); + i = -1; + } else { + let line = relsElem.append('path'); + line + .attr('fill', 'none') + .attr('stroke-width', '1') + .attr('stroke', '#444444') + .attr( + 'd', + 'Mstartx,starty Qcontrolx,controly stopx,stopy ' + .replaceAll('startx', rel.startPoint.x) + .replaceAll('starty', rel.startPoint.y) + .replaceAll( + 'controlx', + rel.startPoint.x + + (rel.endPoint.x - rel.startPoint.x) / 2 - + (rel.endPoint.x - rel.startPoint.x) / 4 + ) + .replaceAll('controly', rel.startPoint.y + (rel.endPoint.y - rel.startPoint.y) / 2) + .replaceAll('stopx', rel.endPoint.x) + .replaceAll('stopy', rel.endPoint.y) + ) + .attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type === 'birel') line.attr('marker-start', 'url(' + url + '#arrowend)'); + } + + let messageConf = conf.messageFont(); + _drawTextCandidateFunc(conf)( + rel.label.text, + relsElem, + Math.min(rel.startPoint.x, rel.endPoint.x) + Math.abs(rel.endPoint.x - rel.startPoint.x) / 2, + Math.min(rel.startPoint.y, rel.endPoint.y) + Math.abs(rel.endPoint.y - rel.startPoint.y) / 2, + rel.label.width, + rel.label.height, + { fill: '#444444' }, + messageConf + ); + + if (rel.techn && rel.techn.text !== '') { + messageConf = conf.messageFont(); + _drawTextCandidateFunc(conf)( + '[' + rel.techn.text + ']', + relsElem, + Math.min(rel.startPoint.x, rel.endPoint.x) + + Math.abs(rel.endPoint.x - rel.startPoint.x) / 2, + Math.min(rel.startPoint.y, rel.endPoint.y) + + Math.abs(rel.endPoint.y - rel.startPoint.y) / 2 + + conf.messageFontSize + + 5, + Math.max(rel.label.width, rel.techn.width), + rel.techn.height, + { fill: '#444444', 'font-style': 'italic' }, + messageConf + ); + } + } +}; + +/** + * Draws an boundary in the diagram + * + * @param {any} elem - The diagram we'll draw to. + * @param {any} boundary - The boundary to draw. + * @param {any} conf - DrawText implementation discriminator object + */ +const drawBoundary = function (elem, boundary, conf) { + const boundaryElem = elem.append('g'); + + let rectData = { + x: boundary.x, + y: boundary.y, + fill: 'none', + stroke: '#444444', + width: boundary.width, + height: boundary.height, + rx: 2.5, + ry: 2.5, + attrs: { 'stroke-width': 1.0, 'stroke-dasharray': '7.0,7.0' }, + }; + + drawRect(boundaryElem, rectData); + + let boundaryConf = conf.boundaryFont(); + boundaryConf.fontWeight = 'bold'; + boundaryConf.fontSize = boundaryConf.fontSize + 2; + _drawTextCandidateFunc(conf)( + boundary.label.text, + boundaryElem, + boundary.x, + boundary.y + boundary.label.y, + boundary.width, + boundary.height, + { fill: '#444444' }, + boundaryConf + ); + + boundaryConf = conf.boundaryFont(); + boundaryConf.fontSize = boundaryConf.fontSize - 2; + _drawTextCandidateFunc(conf)( + '[' + boundary.type + ']', + boundaryElem, + boundary.x, + boundary.y + boundary.label.y + boundaryConf.fontSize + 8, + boundary.width, + boundary.height, + { fill: '#444444' }, + boundaryConf + ); +}; + +export const drawPersonOrSystem = function (elem, personOrSystem, conf) { + let fillColor = conf[personOrSystem.type + '_bg_color']; + let strokeColor = conf[personOrSystem.type + '_border_color']; + let personImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII='; + switch (personOrSystem.type) { + case 'person': + personImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII='; + break; + case 'external_person': + personImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAB6ElEQVR4Xu2YLY+EMBCG9+dWr0aj0Wg0Go1Go0+j8Xdv2uTCvv1gpt0ebHKPuhDaeW4605Z9mJvx4AdXUyTUdd08z+u6flmWZRnHsWkafk9DptAwDPu+f0eAYtu2PEaGWuj5fCIZrBAC2eLBAnRCsEkkxmeaJp7iDJ2QMDdHsLg8SxKFEJaAo8lAXnmuOFIhTMpxxKATebo4UiFknuNo4OniSIXQyRxEA3YsnjGCVEjVXD7yLUAqxBGUyPv/Y4W2beMgGuS7kVQIBycH0fD+oi5pezQETxdHKmQKGk1eQEYldK+jw5GxPfZ9z7Mk0Qnhf1W1m3w//EUn5BDmSZsbR44QQLBEqrBHqOrmSKaQAxdnLArCrxZcM7A7ZKs4ioRq8LFC+NpC3WCBJsvpVw5edm9iEXFuyNfxXAgSwfrFQ1c0iNda8AdejvUgnktOtJQQxmcfFzGglc5WVCj7oDgFqU18boeFSs52CUh8LE8BIVQDT1ABrB0HtgSEYlX5doJnCwv9TXocKCaKbnwhdDKPq4lf3SwU3HLq4V/+WYhHVMa/3b4IlfyikAduCkcBc7mQ3/z/Qq/cTuikhkzB12Ae/mcJC9U+Vo8Ej1gWAtgbeGgFsAMHr50BIWOLCbezvhpBFUdY6EJuJ/QDW0XoMX60zZ0AAAAASUVORK5CYII='; + break; + } + + const personOrSystemElem = elem.append('g'); + personOrSystemElem.attr('class', 'person-man'); + + // + switch (personOrSystem.type) { + case 'person': + case 'external_person': + case 'system': + case 'external_system': + const rect = getNoteRect(); + rect.x = personOrSystem.x; + rect.y = personOrSystem.y; + rect.fill = fillColor; + rect.width = personOrSystem.width; + rect.height = personOrSystem.height; + rect.style = 'stroke:' + strokeColor + ';stroke-width:0.5;'; + rect.rx = 2.5; + rect.ry = 2.5; + drawRect(personOrSystemElem, rect); + break; + case 'system_db': + case 'external_system_db': + personOrSystemElem + .append('path') + .attr('fill', fillColor) + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startyc0,-10 half,-10 half,-10c0,0 half,0 half,10l0,heightc0,10 -half,10 -half,10c0,0 -half,0 -half,-10l0,-height' + .replaceAll('startx', personOrSystem.x) + .replaceAll('starty', personOrSystem.y) + .replaceAll('half', personOrSystem.width / 2) + .replaceAll('height', personOrSystem.height) + ); + personOrSystemElem + .append('path') + .attr('fill', 'none') + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startyc0,10 half,10 half,10c0,0 half,0 half,-10' + .replaceAll('startx', personOrSystem.x) + .replaceAll('starty', personOrSystem.y) + .replaceAll('half', personOrSystem.width / 2) + ); + break; + case 'system_queue': + case 'external_system_queue': + personOrSystemElem + .append('path') + .attr('fill', fillColor) + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startylwidth,0c5,0 5,half 5,halfc0,0 0,half -5,halfl-width,0c-5,0 -5,-half -5,-halfc0,0 0,-half 5,-half' + .replaceAll('startx', personOrSystem.x) + .replaceAll('starty', personOrSystem.y) + .replaceAll('width', personOrSystem.width) + .replaceAll('half', personOrSystem.height / 2) + ); + personOrSystemElem + .append('path') + .attr('fill', 'none') + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startyc-5,0 -5,half -5,halfc0,half 5,half 5,half' + .replaceAll('startx', personOrSystem.x + personOrSystem.width) + .replaceAll('starty', personOrSystem.y) + .replaceAll('half', personOrSystem.height / 2) + ); + break; + } + + personOrSystemElem + .append('text') + .attr('fill', '#FFFFFF') + .attr('font-family', conf.personFontFamily) + .attr('font-size', conf.personFontSize - 2) + .attr('font-style', 'italic') + .attr('lengthAdjust', 'spacing') + .attr('textLength', personOrSystem.typeLabelWidth) + .attr('x', personOrSystem.x + personOrSystem.width / 2 - personOrSystem.typeLabelWidth / 2) + .attr('y', personOrSystem.y + personOrSystem.typeLabelY) + .text('<<' + personOrSystem.type + '>>'); + + switch (personOrSystem.type) { + case 'person': + case 'external_person': + drawImage( + personOrSystemElem, + 48, + 48, + personOrSystem.x + personOrSystem.width / 2 - 24, + personOrSystem.y + 24, + personImg + ); + break; + } + + let personOrSystemConf = conf.personFont(); + personOrSystemConf.fontWeight = 'bold'; + personOrSystemConf.fontSize = personOrSystemConf.fontSize + 2; + _drawTextCandidateFunc(conf)( + personOrSystem.label.text, + personOrSystemElem, + personOrSystem.x, + personOrSystem.y + personOrSystem.label.Y, + personOrSystem.width, + personOrSystem.height, + { fill: '#FFFFFF' }, + personOrSystemConf + ); + + personOrSystemConf = conf.personFont(); + _drawTextCandidateFunc(conf)( + personOrSystem.descr.text, + personOrSystemElem, + personOrSystem.x, + personOrSystem.y + personOrSystem.descr.Y, + personOrSystem.width, + personOrSystem.height, + { fill: '#FFFFFF' }, + personOrSystemConf + ); + + return personOrSystem.height; +}; + +export const insertDatabaseIcon = function (elem) { + elem + .append('defs') + .append('symbol') + .attr('id', 'database') + .attr('fill-rule', 'evenodd') + .attr('clip-rule', 'evenodd') + .append('path') + .attr('transform', 'scale(.5)') + .attr( + 'd', + 'M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z' + ); +}; + +export const insertComputerIcon = function (elem) { + elem + .append('defs') + .append('symbol') + .attr('id', 'computer') + .attr('width', '24') + .attr('height', '24') + .append('path') + .attr('transform', 'scale(.5)') + .attr( + 'd', + 'M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z' + ); +}; + +export const insertClockIcon = function (elem) { + elem + .append('defs') + .append('symbol') + .attr('id', 'clock') + .attr('width', '24') + .attr('height', '24') + .append('path') + .attr('transform', 'scale(.5)') + .attr( + 'd', + 'M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z' + ); +}; + +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param elem + */ +export const insertArrowHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'arrowhead') + .attr('refX', 9) + .attr('refY', 5) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead +}; +export const insertArrowEnd = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'arrowend') + .attr('refX', 1) + .attr('refY', 5) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 10 0 L 0 5 L 10 10 z'); // this is actual shape for arrowhead +}; +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param {any} elem + */ +export const insertArrowFilledHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'filled-head') + .attr('refX', 18) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); +}; +/** + * Setup node number. The result is appended to the svg. + * + * @param {any} elem + */ +export const insertDynamicNumber = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'sequencenumber') + .attr('refX', 15) + .attr('refY', 15) + .attr('markerWidth', 60) + .attr('markerHeight', 40) + .attr('orient', 'auto') + .append('circle') + .attr('cx', 15) + .attr('cy', 15) + .attr('r', 6); + // .style("fill", '#f00'); +}; +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param {any} elem + */ +export const insertArrowCrossHead = function (elem) { + const defs = elem.append('defs'); + const marker = defs + .append('marker') + .attr('id', 'crosshead') + .attr('markerWidth', 15) + .attr('markerHeight', 8) + .attr('orient', 'auto') + .attr('refX', 16) + .attr('refY', 4); + + // The arrow + marker + .append('path') + .attr('fill', 'black') + .attr('stroke', '#000000') + .style('stroke-dasharray', '0, 0') + .attr('stroke-width', '1px') + .attr('d', 'M 9,2 V 6 L16,4 Z'); + + // The cross + marker + .append('path') + .attr('fill', 'none') + .attr('stroke', '#000000') + .style('stroke-dasharray', '0, 0') + .attr('stroke-width', '1px') + .attr('d', 'M 0,1 L 6,7 M 6,1 L 0,7'); + // this is actual shape for arrowhead +}; + +export const getTextObj = function () { + return { + x: 0, + y: 0, + fill: undefined, + anchor: undefined, + style: '#666', + width: undefined, + height: undefined, + textMargin: 0, + rx: 0, + ry: 0, + tspan: true, + valign: undefined, + }; +}; + +export const getNoteRect = function () { + return { + x: 0, + y: 0, + fill: '#EDF2AE', + stroke: '#666', + width: 100, + anchor: 'start', + height: 100, + rx: 0, + ry: 0, + }; +}; + +const _drawTextCandidateFunc = (function () { + /** + * @param {any} content + * @param {any} g + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} textAttrs + */ + function byText(content, g, x, y, width, height, textAttrs) { + const text = g + .append('text') + .attr('x', x + width / 2) + .attr('y', y + height / 2 + 5) + .style('text-anchor', 'middle') + .text(content); + _setTextAttrs(text, textAttrs); + } + + /** + * @param {any} content + * @param {any} g + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} textAttrs + * @param {any} conf + */ + function byTspan(content, g, x, y, width, height, textAttrs, conf) { + const { fontSize, fontFamily, fontWeight } = conf; + + const lines = content.split(common.lineBreakRegex); + for (let i = 0; i < lines.length; i++) { + const dy = i * fontSize - (fontSize * (lines.length - 1)) / 2; + const text = g + .append('text') + .attr('x', x + width / 2) + .attr('y', y) + .style('text-anchor', 'middle') + .style('font-size', fontSize) + .style('font-weight', fontWeight) + .style('font-family', fontFamily); + text + .append('tspan') + .attr('x', x + width / 2) + .attr('dy', dy) + .text(lines[i]); + + text.attr('y', y).attr('dominant-baseline', 'central').attr('alignment-baseline', 'central'); + + _setTextAttrs(text, textAttrs); + } + } + + /** + * @param {any} content + * @param {any} g + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} textAttrs + * @param {any} conf + */ + function byFo(content, g, x, y, width, height, textAttrs, conf) { + const s = g.append('switch'); + const f = s + .append('foreignObject') + .attr('x', x) + .attr('y', y) + .attr('width', width) + .attr('height', height); + + const text = f + .append('xhtml:div') + .style('display', 'table') + .style('height', '100%') + .style('width', '100%'); + + text + .append('div') + .style('display', 'table-cell') + .style('text-align', 'center') + .style('vertical-align', 'middle') + .text(content); + + byTspan(content, s, x, y, width, height, textAttrs, conf); + _setTextAttrs(text, textAttrs); + } + + /** + * @param {any} toText + * @param {any} fromTextAttrsDict + */ + function _setTextAttrs(toText, fromTextAttrsDict) { + for (const key in fromTextAttrsDict) { + if (fromTextAttrsDict.hasOwnProperty(key)) { + // eslint-disable-line + toText.attr(key, fromTextAttrsDict[key]); + } + } + } + + return function (conf) { + return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan; + }; +})(); + +export default { + drawRect, + drawText, + drawLabel, + drawBoundary, + drawPersonOrSystem, + drawRels, + drawImage, + drawEmbeddedImage, + insertArrowHead, + insertArrowEnd, + insertArrowFilledHead, + insertSequenceNumber: insertDynamicNumber, + insertArrowCrossHead, + insertDatabaseIcon, + insertComputerIcon, + insertClockIcon, + getTextObj, + getNoteRect, + sanitizeUrl, +}; diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index d985c39443..78345b7da3 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -19,6 +19,9 @@ import { select } from 'd3'; import { compile, serialize, stringify } from 'stylis'; import pkg from '../package.json'; import * as configApi from './config'; +import c4Db from './diagrams/c4/c4Db'; +import c4Renderer from './diagrams/c4/c4Renderer'; +import c4Parser from './diagrams/c4/parser/c4Diagram'; import classDb from './diagrams/class/classDb'; import classRenderer from './diagrams/class/classRenderer'; import classRendererV2 from './diagrams/class/classRenderer-v2'; @@ -84,6 +87,11 @@ function parse(text) { log.debug('Type ' + graphType); switch (graphType) { + case 'c4': + c4Db.clear(); + parser = c4Parser; + parser.parser.yy = c4Parser; + break; case 'gitGraph': gitGraphAst.clear(); parser = gitGraphParser; @@ -449,6 +457,10 @@ const render = function (id, _txt, cb, container) { try { switch (graphType) { + case 'c4': + c4Renderer.setConf(cnf.c4); + c4Renderer.draw(txt, id); + break; case 'gitGraph': // cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; //gitGraphRenderer.setConf(cnf.git); diff --git a/src/styles.js b/src/styles.js index 9d39fe4c80..f5608f0dc1 100644 --- a/src/styles.js +++ b/src/styles.js @@ -9,6 +9,7 @@ import requirement from './diagrams/requirement/styles'; import sequence from './diagrams/sequence/styles'; import stateDiagram from './diagrams/state/styles'; import journey from './diagrams/user-journey/styles'; +import c4 from './diagrams/c4/styles'; const themes = { flowchart, @@ -26,6 +27,7 @@ const themes = { er, journey, requirement, + c4, }; export const calcThemeVariables = (theme, userOverRides) => theme.calcColors(userOverRides); diff --git a/src/themes/c4.scss b/src/themes/c4.scss new file mode 100644 index 0000000000..0c3fca3a99 --- /dev/null +++ b/src/themes/c4.scss @@ -0,0 +1,4 @@ +.person { + stroke: $personBorder; + fill: $personBkg; +} diff --git a/src/themes/default/index.scss b/src/themes/default/index.scss index 7daea0e630..a20f81a7d6 100644 --- a/src/themes/default/index.scss +++ b/src/themes/default/index.scss @@ -56,6 +56,11 @@ $critBorderColor: #ff8888; $critBkgColor: red; $todayLineColor: red; +/* C4 Context Diagram variables */ + +$personBorder: $border1; +$personBkg: $mainBkg; + /* state colors */ $labelColor: black; diff --git a/src/themes/theme-base.js b/src/themes/theme-base.js index 7474da8987..361e4495ba 100644 --- a/src/themes/theme-base.js +++ b/src/themes/theme-base.js @@ -113,6 +113,11 @@ class Theme { this.taskTextDarkColor = this.taskTextDarkColor || this.textColor; this.taskTextClickableColor = this.taskTextClickableColor || '#003163'; + /* Sequence Diagram variables */ + + this.personBorder = this.personBorder || this.primaryBorderColor; + this.personBkg = this.personBkg || this.mainBkg; + /* state colors */ this.transitionColor = this.transitionColor || this.lineColor; this.transitionLabelColor = this.transitionLabelColor || this.textColor; diff --git a/src/themes/theme-dark.js b/src/themes/theme-dark.js index 71b9d446ce..12514d5c10 100644 --- a/src/themes/theme-dark.js +++ b/src/themes/theme-dark.js @@ -78,6 +78,11 @@ class Theme { this.taskTextDarkColor = 'calculated'; this.todayLineColor = '#DB5757'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'calculated'; diff --git a/src/themes/theme-default.js b/src/themes/theme-default.js index 81a07b9b8d..a91a9a249e 100644 --- a/src/themes/theme-default.js +++ b/src/themes/theme-default.js @@ -103,6 +103,11 @@ class Theme { this.critBkgColor = 'red'; this.todayLineColor = 'red'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'black'; this.errorBkgColor = '#552222'; diff --git a/src/themes/theme-forest.js b/src/themes/theme-forest.js index b0ec574580..f90da832e1 100644 --- a/src/themes/theme-forest.js +++ b/src/themes/theme-forest.js @@ -76,6 +76,11 @@ class Theme { this.critBkgColor = 'red'; this.todayLineColor = 'red'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'black'; diff --git a/src/themes/theme-neutral.js b/src/themes/theme-neutral.js index af228513a5..0d5ed2ffcb 100644 --- a/src/themes/theme-neutral.js +++ b/src/themes/theme-neutral.js @@ -89,6 +89,11 @@ class Theme { this.critBorderColor = 'calculated'; this.todayLineColor = 'calculated'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'black'; diff --git a/src/utils.js b/src/utils.js index e8a24bb6aa..a891c68d76 100644 --- a/src/utils.js +++ b/src/utils.js @@ -185,6 +185,10 @@ export const detectDirective = function (text, type = null) { */ export const detectType = function (text, cnf) { text = text.replace(directive, '').replace(anyComment, '\n'); + if (text.match(/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/)) { + return 'c4'; + } + if (text.match(/^\s*sequenceDiagram/)) { return 'sequence'; } From 015c112103fef2fa457bc2afbeb09ed76f2b4d71 Mon Sep 17 00:00:00 2001 From: pinghe Date: Sun, 15 May 2022 13:21:16 +0800 Subject: [PATCH 2/3] Add C4Context diagram. Compatible with C4-PlantUML syntax. ``` C4Context title System Context diagram for Internet Banking System Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") Person(customerB, "Banking Customer B") Person_Ext(customerC, "Banking Customer C") System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") Enterprise_Boundary(b1, "BankBoundary") { SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") System_Boundary(b2, "BankBoundary2") { System(SystemA, "Banking System A") System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.") } System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") Boundary(b3, "BankBoundary3", "boundary") { SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.") SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") } } BiRel(customerA, SystemAA, "Uses") BiRel(SystemAA, SystemE, "Uses") Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") Rel(SystemC, customerA, "Sends e-mails to") ``` --- demos/index.html | 38 ++ src/Diagram.js | 9 + src/defaultConfig.js | 159 +++++ src/diagrams/c4/c4Db.js | 307 ++++++++++ src/diagrams/c4/c4Renderer.js | 608 +++++++++++++++++++ src/diagrams/c4/parser/c4Diagram.jison | 267 ++++++++ src/diagrams/c4/styles.js | 8 + src/diagrams/c4/svgDraw.js | 805 +++++++++++++++++++++++++ src/mermaidAPI.js | 12 + src/styles.js | 2 + src/themes/c4.scss | 4 + src/themes/default/index.scss | 5 + src/themes/theme-base.js | 5 + src/themes/theme-dark.js | 5 + src/themes/theme-default.js | 5 + src/themes/theme-forest.js | 5 + src/themes/theme-neutral.js | 5 + src/utils.js | 4 + 18 files changed, 2253 insertions(+) create mode 100644 src/diagrams/c4/c4Db.js create mode 100644 src/diagrams/c4/c4Renderer.js create mode 100644 src/diagrams/c4/parser/c4Diagram.jison create mode 100644 src/diagrams/c4/styles.js create mode 100644 src/diagrams/c4/svgDraw.js create mode 100644 src/themes/c4.scss diff --git a/demos/index.html b/demos/index.html index bdbd2f180b..1db4bf4169 100644 --- a/demos/index.html +++ b/demos/index.html @@ -20,6 +20,44 @@

+
+ C4Context + title System Context diagram for Internet Banking System + + Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") + Person(customerB, "Banking Customer B") + Person_Ext(customerC, "Banking Customer C") + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + + + Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") + + Enterprise_Boundary(b1, "BankBoundary") { + + SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") + + System_Boundary(b2, "BankBoundary2") { + System(SystemA, "Banking System A") + System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.") + } + + System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") + SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") + + Boundary(b3, "BankBoundary3", "boundary") { + SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.") + SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") + } + } + + BiRel(customerA, SystemAA, "Uses") + BiRel(SystemAA, SystemE, "Uses") + Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") + Rel(SystemC, customerA, "Sends e-mails to") +
+ +
+
pie title Key elements in Product X diff --git a/src/Diagram.js b/src/Diagram.js index 8e1e7a6bb1..c8cc5a3ea1 100644 --- a/src/Diagram.js +++ b/src/Diagram.js @@ -1,3 +1,6 @@ +import c4Db from './diagrams/c4/c4Db'; +import c4Renderer from './diagrams/c4/c4Renderer'; +import c4Parser from './diagrams/c4/parser/c4Diagram'; import classDb from './diagrams/class/classDb'; import classRenderer from './diagrams/class/classRenderer'; import classRendererV2 from './diagrams/class/classRenderer-v2'; @@ -49,6 +52,12 @@ class Diagram { this.type = utils.detectType(txt, cnf); log.debug('Type ' + this.type); switch (this.type) { + case 'c4': + this.parser = c4Parser; + this.parser.parser.yy = c4Db; + this.db = c4Db; + this.renderer = c4Renderer; + break; case 'gitGraph': this.parser = gitGraphParser; this.parser.parser.yy = gitGraphAst; diff --git a/src/defaultConfig.js b/src/defaultConfig.js index 8b5a16d562..b2dead9190 100644 --- a/src/defaultConfig.js +++ b/src/defaultConfig.js @@ -1059,6 +1059,165 @@ const config = { showCommitLabel: true, showBranches: true, }, + + /** The object containing configurations specific for c4 diagrams */ + c4: { + useWidth: undefined, + + /** + * | Parameter | Description | Type | Required | Values | + * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | + * | diagramMarginX | Margin to the right and left of the c4 diagram | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 50 + */ + diagramMarginX: 50, + + /** + * | Parameter | Description | Type | Required | Values | + * | -------------- | ------------------------------------------- | ------- | -------- | ------------------ | + * | diagramMarginY | Margin to the over and under the c4 diagram | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 10 + */ + diagramMarginY: 10, + + /** + * | Parameter | Description | Type | Required | Values | + * | ----------- | --------------------- | ------- | -------- | ------------------ | + * | shapeMargin | Margin between shapes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 50 + */ + c4ShapeMargin: 50, + + c4ShapePadding: 20, + + /** + * | Parameter | Description | Type | Required | Values | + * | --------- | --------------------- | ------- | -------- | ------------------ | + * | width | Width of person boxes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 215 + */ + width: 216, + + /** + * | Parameter | Description | Type | Required | Values | + * | --------- | ---------------------- | ------- | -------- | ------------------ | + * | height | Height of person boxes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 65 + */ + height: 60, + + /** + * | Parameter | Description | Type | Required | Values | + * | --------- | ------------------------ | ------- | -------- | ------------------ | + * | boxMargin | Margin around loop boxes | Integer | Required | Any Positive Value | + * + * **Notes:** Default value: 10 + */ + boxMargin: 10, + + /** + * | Parameter | Description | Type | Required | Values | + * | ----------- | ----------- | ------- | -------- | ----------- | + * | useMaxWidth | See Notes | boolean | Required | true, false | + * + * **Notes:** When this flag is set to true, the height and width is set to 100% and is then + * scaling with the available space. If set to false, the absolute space required is used. + * + * Default value: true + */ + useMaxWidth: true, + + c4ShapeInRow: 4, + nextLinePaddingX: 0, + + c4BoundaryInRow: 2, + + personFontSize: 14, + personFontFamily: '"Open Sans", sans-serif', + personFontWeight: 'normal', + + systemFontSize: 14, + systemFontFamily: '"Open Sans", sans-serif', + systemFontWeight: 'normal', + + boundaryFontSize: 14, + boundaryFontFamily: '"Open Sans", sans-serif', + boundaryFontWeight: 'normal', + + messageFontSize: 12, + messageFontFamily: '"Open Sans", sans-serif', + messageFontWeight: 'normal', + + /** + * This sets the auto-wrap state for the diagram + * + * **Notes:** Default value: true. + */ + wrap: true, + + /** + * This sets the auto-wrap padding for the diagram (sides only) + * + * **Notes:** Default value: 0. + */ + wrapPadding: 10, + + personFont: function () { + return { + fontFamily: this.personFontFamily, + fontSize: this.personFontSize, + fontWeight: this.personFontWeight, + }; + }, + + systemFont: function () { + return { + fontFamily: this.systemFontFamily, + fontSize: this.systemFontSize, + fontWeight: this.systemFontWeight, + }; + }, + + boundaryFont: function () { + return { + fontFamily: this.boundaryFontFamily, + fontSize: this.boundaryFontSize, + fontWeight: this.boundaryFontWeight, + }; + }, + + messageFont: function () { + return { + fontFamily: this.messageFontFamily, + fontSize: this.messageFontSize, + fontWeight: this.messageFontWeight, + }; + }, + + // ' Colors + // ' ################################## + person_bg_color: '#08427B', + person_border_color: '#073B6F', + external_person_bg_color: '#686868', + external_person_border_color: '#8A8A8A', + system_bg_color: '#1168BD', + system_border_color: '#3C7FC0', + system_db_bg_color: '#1168BD', + system_db_border_color: '#3C7FC0', + system_queue_bg_color: '#1168BD', + system_queue_border_color: '#3C7FC0', + external_system_bg_color: '#999999', + external_system_border_color: '#8A8A8A', + external_system_db_bg_color: '#999999', + external_system_db_border_color: '#8A8A8A', + external_system_queue_bg_color: '#999999', + external_system_queue_border_color: '#8A8A8A', + }, }; config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute; diff --git a/src/diagrams/c4/c4Db.js b/src/diagrams/c4/c4Db.js new file mode 100644 index 0000000000..d29d11c850 --- /dev/null +++ b/src/diagrams/c4/c4Db.js @@ -0,0 +1,307 @@ +import mermaidAPI from '../../mermaidAPI'; +import * as configApi from '../../config'; +import { log } from '../../logger'; +import { sanitizeText } from '../common/common'; + +let personOrSystemArray = []; +let boundaryParseStack = ['']; +let currentBoundaryParse = 'global'; +let parentBoundaryParse = ''; +let boundarys = [ + { + alias: 'global', + label: { text: 'global' }, + type: 'global', + tags: null, + link: null, + parentBoundary: '', + }, +]; +let rels = []; +let title = ''; +let wrapEnabled = false; +let description = ''; +let c4Type = 'C4Context'; + +export const getC4Type = function () { + return c4Type; +}; + +export const setC4Type = function (c4Type) { + let sanitizedText = sanitizeText(c4Type, configApi.getConfig()); + c4Type = sanitizedText; +}; + +export const parseDirective = function (statement, context, type) { + mermaidAPI.parseDirective(this, statement, context, type); +}; + +//type, from, to, label, ?techn, ?descr, ?sprite, ?tags, $link +export const addRel = function (type, from, to, label, techn, descr, sprite, tags, link) { + // Don't allow label nulling + if ( + type === undefined || + type === null || + from === undefined || + from === null || + to === undefined || + to === null || + label === undefined || + label === null + ) + return; + + let rel = {}; + const old = rels.find((rel) => rel.from === from && rel.to === to); + if (old) { + rel = old; + } else { + rels.push(rel); + } + + rel.type = type; + rel.from = from; + rel.to = to; + rel.label = { text: label }; + + if (descr === undefined || descr === null) { + rel.descr = { text: '' }; + } else { + rel.descr = { text: descr }; + } + + if (techn === undefined || techn === null) { + rel.techn = { text: '' }; + } else { + rel.techn = { text: techn }; + } + + // rel.techn = techn; + rel.sprite = sprite; + rel.tags = tags; + rel.link = link; + rel.wrap = autoWrap(); +}; + +//type, alias, label, ?descr, ?sprite, ?tags, $link +export const addPersonOrSystem = function (type, alias, label, descr, sprite, tags, link) { + // Don't allow label nulling + if (alias === null || label === null) return; + + let personOrSystem = {}; + const old = personOrSystemArray.find((personOrSystem) => personOrSystem.alias === alias); + if (old && alias === old.alias) { + personOrSystem = old; + } else { + personOrSystem.alias = alias; + personOrSystemArray.push(personOrSystem); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + personOrSystem.label = { text: '' }; + } else { + personOrSystem.label = { text: label }; + } + + if (descr === undefined || descr === null) { + personOrSystem.descr = { text: '' }; + } else { + personOrSystem.descr = { text: descr }; + } + + personOrSystem.wrap = autoWrap(); + personOrSystem.sprite = sprite; + personOrSystem.tags = tags; + personOrSystem.link = link; + personOrSystem.type = type; + personOrSystem.parentBoundary = currentBoundaryParse; +}; + +//alias, label, ?type, ?tags, $link +export const addBoundary = function (alias, label, type, tags, link) { + // if (parentBoundary === null) return; + + // Don't allow label nulling + if (alias === null || label === null) return; + + let boundary = {}; + const old = boundarys.find((boundary) => boundary.alias === alias); + if (old && alias === old.alias) { + boundary = old; + } else { + boundary.alias = alias; + boundarys.push(boundary); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + boundary.label = { text: '' }; + } else { + boundary.label = { text: label }; + } + + if (type === undefined || type === null) { + boundary.type = { text: 'system' }; + } else { + boundary.type = { text: type }; + } + + boundary.wrap = autoWrap(); + boundary.tags = tags; + boundary.link = link; + boundary.type = type; + boundary.parentBoundary = currentBoundaryParse; + + parentBoundaryParse = currentBoundaryParse; + currentBoundaryParse = alias; + boundaryParseStack.push(parentBoundaryParse); +}; + +export const popBoundaryParseStack = function () { + currentBoundaryParse = parentBoundaryParse; + boundaryParseStack.pop(); + parentBoundaryParse = boundaryParseStack.pop(); + boundaryParseStack.push(parentBoundaryParse); +}; + +export const getCurrentBoundaryParse = function () { + return currentBoundaryParse; +}; + +export const getParentBoundaryParse = function () { + return parentBoundaryParse; +}; + +export const getPersonOrSystemArray = function (parentBoundary) { + if (parentBoundary === undefined || parentBoundary === null) return personOrSystemArray; + else + return personOrSystemArray.filter((personOrSystem) => { + return personOrSystem.parentBoundary === parentBoundary; + }); +}; +export const getPersonOrSystem = function (alias) { + return personOrSystemArray.find((personOrSystem) => personOrSystem.alias === alias); +}; +export const getPersonOrSystemKeys = function (parentBoundary) { + return Object.keys(getPersonOrSystemArray(parentBoundary)); +}; + +export const getBoundarys = function (parentBoundary) { + if (parentBoundary === undefined || parentBoundary === null) return boundarys; + else return boundarys.filter((boundary) => boundary.parentBoundary === parentBoundary); +}; + +export const getRels = function () { + return rels; +}; + +export const getTitle = function () { + return title; +}; + +export const setWrap = function (wrapSetting) { + wrapEnabled = wrapSetting; +}; + +export const autoWrap = function () { + return wrapEnabled; +}; + +export const clear = function () { + personOrSystemArray = []; + boundarys = [ + { + alias: 'global', + label: { text: 'global' }, + type: 'global', + tags: null, + link: null, + parentBoundary: '', + }, + ]; + parentBoundaryParse = ''; + currentBoundaryParse = 'global'; + boundaryParseStack = ['']; + rels = []; +}; + +export const LINETYPE = { + SOLID: 0, + DOTTED: 1, + NOTE: 2, + SOLID_CROSS: 3, + DOTTED_CROSS: 4, + SOLID_OPEN: 5, + DOTTED_OPEN: 6, + LOOP_START: 10, + LOOP_END: 11, + ALT_START: 12, + ALT_ELSE: 13, + ALT_END: 14, + OPT_START: 15, + OPT_END: 16, + ACTIVE_START: 17, + ACTIVE_END: 18, + PAR_START: 19, + PAR_AND: 20, + PAR_END: 21, + RECT_START: 22, + RECT_END: 23, + SOLID_POINT: 24, + DOTTED_POINT: 25, +}; + +export const ARROWTYPE = { + FILLED: 0, + OPEN: 1, +}; + +export const PLACEMENT = { + LEFTOF: 0, + RIGHTOF: 1, + OVER: 2, +}; + +export const setTitle = function (txt) { + let sanitizedText = sanitizeText(txt, configApi.getConfig()); + title = sanitizedText; +}; + +const setAccDescription = function (description_lex) { + let sanitizedText = sanitizeText(description_lex, configApi.getConfig()); + description = sanitizedText; +}; + +const getAccDescription = function () { + return description; +}; + +export default { + addPersonOrSystem, + addBoundary, + popBoundaryParseStack, + addRel, + autoWrap, + setWrap, + getPersonOrSystemArray, + getPersonOrSystem, + getPersonOrSystemKeys, + getBoundarys, + getCurrentBoundaryParse, + getParentBoundaryParse, + getRels, + getTitle, + getC4Type, + getAccDescription, + setAccDescription, + parseDirective, + getConfig: () => configApi.getConfig().c4, + clear, + LINETYPE, + ARROWTYPE, + PLACEMENT, + setTitle, + setC4Type, + // apply, +}; diff --git a/src/diagrams/c4/c4Renderer.js b/src/diagrams/c4/c4Renderer.js new file mode 100644 index 0000000000..dba7172c99 --- /dev/null +++ b/src/diagrams/c4/c4Renderer.js @@ -0,0 +1,608 @@ +import { select } from 'd3'; +import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw'; +import { log } from '../../logger'; +import { parser } from './parser/c4Diagram'; +import common from '../common/common'; +import c4Db from './c4Db'; +import * as configApi from '../../config'; +import utils, { + wrapLabel, + calculateTextWidth, + calculateTextHeight, + assignWithDepth, + configureSvgSize, +} from '../../utils'; +import addSVGAccessibilityFields from '../../accessibility'; + +let globalBoundaryMaxX = 0, + globalBoundaryMaxY = 0; + +parser.yy = c4Db; + +let conf = {}; + +class Bounds { + constructor() { + this.name = ''; + this.data = {}; + this.data.startx = undefined; + this.data.stopx = undefined; + this.data.starty = undefined; + this.data.stopy = undefined; + this.data.widthLimit = undefined; + + this.nextData = {}; + this.nextData.startx = undefined; + this.nextData.stopx = undefined; + this.nextData.starty = undefined; + this.nextData.stopy = undefined; + + setConf(parser.yy.getConfig()); + } + + setData(startx, stopx, starty, stopy) { + this.nextData.startx = this.data.startx = startx; + this.nextData.stopx = this.data.stopx = stopx; + this.nextData.starty = this.data.starty = starty; + this.nextData.stopy = this.data.stopy = stopy; + } + + updateVal(obj, key, val, fun) { + if (typeof obj[key] === 'undefined') { + obj[key] = val; + } else { + obj[key] = fun(val, obj[key]); + } + } + + insert(c4Shape) { + let _startx = this.nextData.stopx + c4Shape.margin * 2; + let _stopx = _startx + c4Shape.width; + let _starty = this.nextData.starty + c4Shape.margin * 2; + let _stopy = _starty + c4Shape.height; + if (_startx >= this.data.widthLimit || _stopx >= this.data.widthLimit) { + _startx = this.nextData.startx + c4Shape.margin * 2 + conf.nextLinePaddingX; + _starty = this.nextData.stopy + c4Shape.margin * 2; + + this.nextData.stopx = _stopx = _startx + c4Shape.width; + this.nextData.starty = this.nextData.stopy; + this.nextData.stopy = _stopy = _starty + c4Shape.height; + } + + c4Shape.x = _startx; + c4Shape.y = _starty; + + this.updateVal(this.data, 'startx', _startx, Math.min); + this.updateVal(this.data, 'starty', _starty, Math.min); + this.updateVal(this.data, 'stopx', _stopx, Math.max); + this.updateVal(this.data, 'stopy', _stopy, Math.max); + + this.updateVal(this.nextData, 'startx', _startx, Math.min); + this.updateVal(this.nextData, 'starty', _starty, Math.min); + this.updateVal(this.nextData, 'stopx', _stopx, Math.max); + this.updateVal(this.nextData, 'stopy', _stopy, Math.max); + } + + init() { + this.data = { + startx: undefined, + stopx: undefined, + starty: undefined, + stopy: undefined, + widthLimit: undefined, + }; + setConf(parser.yy.getConfig()); + } + + bumpLastMargin(margin) { + this.data.stopx += margin; + this.data.stopy += margin; + } +} + +const personFont = (cnf) => { + return { + fontFamily: cnf.personFontFamily, + fontSize: cnf.personFontSize, + fontWeight: cnf.personFontWeight, + }; +}; + +const systemFont = (cnf) => { + return { + fontFamily: cnf.systemFontFamily, + fontSize: cnf.systemFontSize, + fontWeight: cnf.systemFontWeight, + }; +}; + +const boundaryFont = (cnf) => { + return { + fontFamily: cnf.boundaryFontFamily, + fontSize: cnf.boundaryFontSize, + fontWeight: cnf.boundaryFontWeight, + }; +}; + +const messageFont = (cnf) => { + return { + fontFamily: cnf.messageFontFamily, + fontSize: cnf.messageFontSize, + fontWeight: cnf.messageFontWeight, + }; +}; + +/** + * @param textType + * @param c4Shape + * @param c4ShapeTextWrap + * @param textConf + * @param textLimitWidth + */ +function setC4ShapeText(textType, c4Shape, c4ShapeTextWrap, textConf, textLimitWidth) { + if (!c4Shape[textType].width) { + if (c4ShapeTextWrap) { + c4Shape[textType].text = wrapLabel(c4Shape[textType].text, textLimitWidth, textConf); + c4Shape[textType].labelLines = c4Shape[textType].text.split(common.lineBreakRegex).length; + c4Shape[textType].width = textLimitWidth; + c4Shape[textType].height = c4Shape[textType].labelLines * (textConf.fontSize + 2); + } else { + let lines = c4Shape[textType].text.split(common.lineBreakRegex); + c4Shape[textType].labelLines = lines.length; + let lineHeight = 0; + c4Shape[textType].height = 0; + c4Shape[textType].width = 0; + for (let i = 0; i < lines.length; i++) { + c4Shape[textType].width = Math.max( + calculateTextWidth(lines[i], textConf), + c4Shape[textType].width + ); + lineHeight = calculateTextHeight(lines[i], textConf); + c4Shape[textType].height = c4Shape[textType].height + lineHeight; + } + // c4Shapes[textType].height = c4Shapes[textType].labelLines * textConf.fontSize; + } + } +} + +export const drawBoundary = function (diagram, boundary, bounds) { + boundary.x = bounds.data.startx; + boundary.y = bounds.data.starty; + boundary.width = bounds.data.stopx - bounds.data.startx; + boundary.height = bounds.data.stopy - bounds.data.starty; + + boundary.label.y = conf.c4ShapeMargin - 35; + + let boundaryTextWrap = boundary.wrap && conf.wrap; + let boundaryLabelConf = boundaryFont(conf); + boundaryLabelConf.fontSize = boundaryLabelConf.fontSize + 2; + boundaryLabelConf.fontWeight = 'bold'; + let textLimitWidth = calculateTextWidth(boundary.label.text, boundaryLabelConf); + setC4ShapeText('label', boundary, boundaryTextWrap, boundaryLabelConf, textLimitWidth); + + svgDraw.drawBoundary(diagram, boundary, conf); +}; + +export const drawPersonOrSystemArray = function ( + currentBounds, + diagram, + personOrSystemArray, + personOrSystemKeys +) { + // Draw the personOrSystemArray + + // let prevWidth = currentBounds.data.stopx; + // let prevMarginX = conf.c4ShapeMargin; + // let prevMarginY = conf.c4ShapeMargin; + // let maxHeight = currentBounds.data.starty; + + for (let i = 0; i < personOrSystemKeys.length; i++) { + const personOrSystem = personOrSystemArray[personOrSystemKeys[i]]; + + let imageWidth = 0, + imageHeight = 0; + switch (personOrSystem.type) { + case 'person': + case 'external_person': + imageWidth = 48; + imageHeight = 48; + break; + } + + if (!personOrSystem.typeLabelWidth) { + let personOrSystemTypeConf = personFont(conf); + personOrSystemTypeConf.fontSize = personOrSystemTypeConf.fontSize - 2; + personOrSystem.typeLabelWidth = calculateTextWidth( + '<<' + personOrSystem.type + '>>', + personOrSystemTypeConf + ); + personOrSystem.typeLabelHeight = personOrSystemTypeConf.fontSize + 2; + + switch (personOrSystem.type) { + case 'system_db': + case 'external_system_db': + personOrSystem.typeLabelY = conf.c4ShapePadding; + break; + default: + personOrSystem.typeLabelY = conf.c4ShapePadding - 5; + break; + } + } + + let personOrSystemTextWrap = personOrSystem.wrap && conf.wrap; + let textLimitWidth = conf.width - conf.c4ShapePadding * 2; + + let personOrSystemLabelConf = personFont(conf); + personOrSystemLabelConf.fontSize = personOrSystemLabelConf.fontSize + 2; + personOrSystemLabelConf.fontWeight = 'bold'; + + setC4ShapeText( + 'label', + personOrSystem, + personOrSystemTextWrap, + personOrSystemLabelConf, + textLimitWidth + ); + personOrSystem['label'].Y = + conf.c4ShapePadding + personOrSystem.typeLabelHeight + imageHeight + 10; + + let personOrSystemDescrConf = personFont(conf); + setC4ShapeText( + 'descr', + personOrSystem, + personOrSystemTextWrap, + personOrSystemDescrConf, + textLimitWidth + ); + personOrSystem['descr'].Y = + conf.c4ShapePadding + + personOrSystem.typeLabelHeight + + imageHeight + + 5 + + personOrSystem.label.height + + conf.personFontSize + + 2; + + // Add some rendering data to the object + let rectWidth = + Math.max(personOrSystem.label.width, personOrSystem.descr.width) + conf.c4ShapePadding * 2; + let rectHeight = + conf.c4ShapePadding + + personOrSystem.typeLabelHeight + + imageHeight + + personOrSystem.label.height + + conf.personFontSize + + 2 + + personOrSystem.descr.height; + + personOrSystem.width = Math.max(personOrSystem.width || conf.width, rectWidth, conf.width); + personOrSystem.height = Math.max(personOrSystem.height || conf.height, rectHeight, conf.height); + personOrSystem.margin = personOrSystem.margin || conf.c4ShapeMargin; + + currentBounds.insert(personOrSystem); + + const height = svgDraw.drawPersonOrSystem(diagram, personOrSystem, conf); + } + + currentBounds.bumpLastMargin(conf.c4ShapeMargin); +}; + +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +/* * * + * Get the intersection of the line between the center point of a rectangle and a point outside the rectangle. + * Algorithm idea. + * Using a point outside the rectangle as the coordinate origin, the graph is divided into four quadrants, and each quadrant is divided into two cases, with separate treatment on the coordinate axes + * 1. The case of coordinate axes. + * 1. The case of the negative x-axis + * 2. The case of the positive x-axis + * 3. The case of the positive y-axis + * 4. The negative y-axis case + * 2. Quadrant cases. + * 2.1. first quadrant: the case where the line intersects the left side of the rectangle; the case where it intersects the lower side of the rectangle + * 2.2. second quadrant: the case where the line intersects the right side of the rectangle; the case where it intersects the lower edge of the rectangle + * 2.3. third quadrant: the case where the line intersects the right side of the rectangle; the case where it intersects the upper edge of the rectangle + * 2.4. fourth quadrant: the case where the line intersects the left side of the rectangle; the case where it intersects the upper side of the rectangle + * + */ +let getIntersectPoint = function (fromNode, endPoint) { + let x1 = fromNode.x; + + let y1 = fromNode.y; + + let x2 = endPoint.x; + + let y2 = endPoint.y; + + let fromCenterX = x1 + fromNode.width / 2; + + let fromCenterY = y1 + fromNode.height / 2; + + let dx = Math.abs(x1 - x2); + + let dy = Math.abs(y1 - y2); + + let tanDYX = dy / dx; + + let fromDYX = fromNode.height / fromNode.width; + + let returnPoint = null; + + if (y1 == y2 && x1 < x2) { + returnPoint = new Point(x1 + fromNode.width, fromCenterY); + } else if (y1 == y2 && x1 > x2) { + returnPoint = new Point(x1, fromCenterY); + } else if (x1 == x2 && y1 < y2) { + returnPoint = new Point(fromCenterX, y1 + fromNode.height); + } else if (x1 == x2 && y1 > y2) { + returnPoint = new Point(fromCenterX, y1); + } + + if (x1 > x2 && y1 < y2) { + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1, fromCenterY + (tanDYX * fromNode.width) / 2); + } else { + returnPoint = new Point( + fromCenterX - ((dx / dy) * fromNode.height) / 2, + y1 + fromNode.height + ); + } + } else if (x1 < x2 && y1 < y2) { + // + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1 + fromNode.width, fromCenterY + (tanDYX * fromNode.width) / 2); + } else { + returnPoint = new Point( + fromCenterX + ((dx / dy) * fromNode.height) / 2, + y1 + fromNode.height + ); + } + } else if (x1 < x2 && y1 > y2) { + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1 + fromNode.width, fromCenterY - (tanDYX * fromNode.width) / 2); + } else { + returnPoint = new Point(fromCenterX + ((fromNode.height / 2) * dx) / dy, y1); + } + } else if (x1 > x2 && y1 > y2) { + if (fromDYX >= tanDYX) { + returnPoint = new Point(x1, fromCenterY - (fromNode.width / 2) * tanDYX); + } else { + returnPoint = new Point(fromCenterX - ((fromNode.height / 2) * dx) / dy, y1); + } + } + return returnPoint; +}; + +let getIntersectPoints = function (fromNode, endNode) { + let endIntersectPoint = { x: 0, y: 0 }; + endIntersectPoint.x = endNode.x + endNode.width / 2; + endIntersectPoint.y = endNode.y + endNode.height / 2; + let startPoint = getIntersectPoint(fromNode, endIntersectPoint); + + endIntersectPoint.x = fromNode.x + fromNode.width / 2; + endIntersectPoint.y = fromNode.y + fromNode.height / 2; + let endPoint = getIntersectPoint(endNode, endIntersectPoint); + return { startPoint: startPoint, endPoint: endPoint }; +}; + +export const drawRels = function (diagram, rels, getC4ShapeObj) { + for (let rel of rels) { + let relTextWrap = rel.wrap && conf.wrap; + let relConf = messageFont(conf); + let textLimitWidth = calculateTextWidth(rel.label.text, relConf); + setC4ShapeText('label', rel, relTextWrap, relConf, textLimitWidth); + + if (rel.techn && rel.techn.text !== '') { + textLimitWidth = calculateTextWidth(rel.techn.text, relConf); + setC4ShapeText('techn', rel, relTextWrap, relConf, textLimitWidth); + } + + if (rel.descr && rel.descr.text !== '') { + textLimitWidth = calculateTextWidth(rel.descr.text, relConf); + setC4ShapeText('descr', rel, relTextWrap, relConf, textLimitWidth); + } + + let fromNode = getC4ShapeObj(rel.from); + let endNode = getC4ShapeObj(rel.to); + let points = getIntersectPoints(fromNode, endNode); + rel.startPoint = points.startPoint; + rel.endPoint = points.endPoint; + } + svgDraw.drawRels(diagram, rels, conf); +}; + +export const setConf = function (cnf) { + assignWithDepth(conf, cnf); + + if (cnf.fontFamily) { + conf.personFontFamily = conf.systemFontFamily = conf.messageFontFamily = cnf.fontFamily; + } + if (cnf.fontSize) { + conf.personFontSize = conf.systemFontSize = conf.messageFontSize = cnf.fontSize; + } + if (cnf.fontWeight) { + conf.personFontWeight = conf.systemFontWeight = conf.messageFontWeight = cnf.fontWeight; + } +}; + +/** + * @param diagram + * @param parentBoundaryAlias + * @param parentBounds + * @param currentBoundarys + */ +function drawInsideBoundary(diagram, parentBoundaryAlias, parentBounds, currentBoundarys) { + let currentBounds = new Bounds(); + // Calculate the width limit of the boundar. label/type 的长度, + currentBounds.data.widthLimit = Math.min( + conf.width * conf.c4ShapeInRow + conf.c4ShapeMargin * (conf.c4ShapeInRow + 1), + parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundarys.length) + ); + for (let i = 0; i < currentBoundarys.length; i++) { + let currentBoundary = currentBoundarys[i]; + if (i == 0) { + // Calculate the drawing start point of the currentBoundarys. + let _x = parentBounds.data.startx + conf.diagramMarginX; + let _y = parentBounds.data.stopy + conf.diagramMarginY; + + currentBounds.setData(_x, _x, _y, _y); + } else { + // Calculate the drawing start point of the currentBoundarys. + let _x = + currentBounds.data.stopx !== currentBounds.data.startx + ? currentBounds.data.stopx + conf.diagramMarginX + : currentBounds.data.startx; + let _y = currentBounds.data.starty; + + currentBounds.setData(_x, _x, _y, _y); + } + currentBounds.name = currentBoundary.alias; + let currentPersonOrSystemArray = parser.yy.getPersonOrSystemArray(currentBoundary.alias); + let currentPersonOrSystemKeys = parser.yy.getPersonOrSystemKeys(currentBoundary.alias); + + if (currentPersonOrSystemKeys.length > 0) { + drawPersonOrSystemArray( + currentBounds, + diagram, + currentPersonOrSystemArray, + currentPersonOrSystemKeys + ); + } + parentBoundaryAlias = currentBoundary.alias; + let nextCurrentBoundarys = parser.yy.getBoundarys(parentBoundaryAlias); + + if (nextCurrentBoundarys.length > 0) { + // draw boundary inside currentBoundary + // bounds.init(); + // parentBoundaryWidthLimit = bounds.data.stopx - bounds.startx; + drawInsideBoundary(diagram, parentBoundaryAlias, currentBounds, nextCurrentBoundarys); + } + // draw boundary + if (currentBoundary.alias !== 'global') drawBoundary(diagram, currentBoundary, currentBounds); + parentBounds.data.stopy = Math.max( + currentBounds.data.stopy + conf.c4ShapeMargin, + parentBounds.data.stopy + ); + parentBounds.data.stopx = Math.max( + currentBounds.data.stopx + conf.c4ShapeMargin, + parentBounds.data.stopx + ); + globalBoundaryMaxX = Math.max(globalBoundaryMaxX, parentBounds.data.stopx); + globalBoundaryMaxY = Math.max(globalBoundaryMaxY, parentBounds.data.stopy); + } +} + +/** + * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text. + * + * @param {any} text + * @param {any} id + */ +export const draw = function (text, id) { + conf = configApi.getConfig().c4; + const securityLevel = configApi.getConfig().securityLevel; + // Handle root and ocument for when rendering in sanbox mode + let sandboxElement; + if (securityLevel === 'sandbox') { + sandboxElement = select('#i' + id); + } + const root = + securityLevel === 'sandbox' + ? select(sandboxElement.nodes()[0].contentDocument.body) + : select('body'); + const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; + + parser.yy.clear(); + parser.yy.setWrap(conf.wrap); + parser.parse(text + '\n'); + + log.debug(`C:${JSON.stringify(conf, null, 2)}`); + + const diagram = + securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : select(`[id="${id}"]`); + + svgDraw.insertComputerIcon(diagram); + svgDraw.insertDatabaseIcon(diagram); + svgDraw.insertClockIcon(diagram); + + let screenBounds = new Bounds(); + screenBounds.setData( + conf.diagramMarginX, + conf.diagramMarginX, + conf.diagramMarginY, + conf.diagramMarginY + ); + + screenBounds.data.widthLimit = screen.availWidth; + globalBoundaryMaxX = conf.diagramMarginX; + globalBoundaryMaxY = conf.diagramMarginY; + + const title = parser.yy.getTitle(); + const c4type = parser.yy.getC4Type(); + let currentBoundarys = parser.yy.getBoundarys(''); + switch (c4type) { + case 'C4Context': + drawInsideBoundary(diagram, '', screenBounds, currentBoundarys); + break; + } + + // The arrow head definition is attached to the svg once + svgDraw.insertArrowHead(diagram); + svgDraw.insertArrowEnd(diagram); + svgDraw.insertArrowCrossHead(diagram); + svgDraw.insertArrowFilledHead(diagram); + + drawRels(diagram, parser.yy.getRels(), parser.yy.getPersonOrSystem); + + screenBounds.data.stopx = globalBoundaryMaxX; + screenBounds.data.stopy = globalBoundaryMaxY; + + const box = screenBounds.data; + + // Make sure the height of the diagram supports long menus. + let boxHeight = box.stopy - box.starty; + + let height = boxHeight + 2 * conf.diagramMarginY; + + // Make sure the width of the diagram supports wide menus. + let boxWidth = box.stopx - box.startx; + const width = boxWidth + 2 * conf.diagramMarginX; + + if (title) { + diagram + .append('text') + .text(title) + .attr('x', (box.stopx - box.startx) / 2 - 4 * conf.diagramMarginX) + .attr('y', -25); + } + + configureSvgSize(diagram, height, width, conf.useMaxWidth); + + const extraVertForTitle = title ? 60 : 0; + diagram.attr( + 'viewBox', + box.startx - + conf.diagramMarginX + + ' -' + + (conf.diagramMarginY + extraVertForTitle) + + ' ' + + width + + ' ' + + (height + extraVertForTitle) + ); + + addSVGAccessibilityFields(parser.yy, diagram, id); + log.debug(`models:`, box); +}; + +export default { + drawPersonOrSystemArray, + drawBoundary, + setConf, + draw, +}; diff --git a/src/diagrams/c4/parser/c4Diagram.jison b/src/diagrams/c4/parser/c4Diagram.jison new file mode 100644 index 0000000000..dd9672226b --- /dev/null +++ b/src/diagrams/c4/parser/c4Diagram.jison @@ -0,0 +1,267 @@ +/** mermaid + * https://mermaidjs.github.io/ + * (c) 2022 mzhx.meng@gmail.com + * MIT license. + */ + +/* lexical grammar */ +%lex + +/* context */ +%x person +%x person_ext +%x system +%x system_db +%x system_queue +%x system_ext +%x system_ext_db +%x system_ext_queue +%x boundary +%x enterprise_boundary +%x system_boundary +%x rel +%x birel +%x rel_u +%x rel_d +%x rel_l +%x rel_r + +/* container */ +%x container +%x container_db +%x container_queue +%x container_ext +%x container_ext_db +%x container_ext_queue +%x container_boundary + +/* component */ +%x component +%x component_db +%x component_queue +%x component_ext +%x component_ext_db +%x component_ext_queue + +/* Dynamic diagram */ +%x rel_index +%x index + +/* Deployment diagram */ +%x deployment_node +%x node +%x node_l +%x node_r + +/* Relationship Types */ +%x rel +%x rel_bi +%x rel_up +%x rel_down +%x rel_left +%x rel_right + +%x attribute +%x string + +%x open_directive +%x type_directive +%x arg_directive + +%% +\%\%\{ { this.begin('open_directive'); return 'open_directive'; } +.*direction\s+TB[^\n]* return 'direction_tb'; +.*direction\s+BT[^\n]* return 'direction_bt'; +.*direction\s+RL[^\n]* return 'direction_rl'; +.*direction\s+LR[^\n]* return 'direction_lr'; +((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } +":" { this.popState(); this.begin('arg_directive'); return ':'; } +\}\%\% { this.popState(); this.popState(); return 'close_directive'; } +((?:(?!\}\%\%).|\n)*) return 'arg_directive'; +\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */ +\%\%[^\n]*(\r?\n)* c /* skip comments */ + +"title"\s[^#\n;]+ return 'title'; +"accDescription"\s[^#\n;]+ return 'accDescription'; + +\s*(\r?\n)+ return 'NEWLINE'; +\s+ /* skip whitespace */ +"C4Context" return 'C4_CONTEXT'; +"C4Container" return 'C4_CONTAINER'; +"C4Component" return 'C4_COMPONENT'; +"C4Dynamic" return 'C4_DYNAMIC'; +"C4Deployment" return 'C4_DEPLOYMENT'; + +"Person_Ext" { this.begin("person_ext"); console.log('begin person_ext'); return 'PERSON_EXT';} +"Person" { this.begin("person"); console.log('begin person'); return 'PERSON';} +"SystemQueue_Ext" { this.begin("system_ext_queue"); console.log('begin system_ext_queue'); return 'SYSTEM_EXT_QUEUE';} +"SystemDb_Ext" { this.begin("system_ext_db"); console.log('begin system_ext_db'); return 'SYSTEM_EXT_DB';} +"System_Ext" { this.begin("system_ext"); console.log('begin system_ext'); return 'SYSTEM_EXT';} +"SystemQueue" { this.begin("system_queue"); console.log('begin system_queue'); return 'SYSTEM_QUEUE';} +"SystemDb" { this.begin("system_db"); console.log('begin system_db'); return 'SYSTEM_DB';} +"System" { this.begin("system"); console.log('begin system'); return 'SYSTEM';} + +"Boundary" { this.begin("boundary"); console.log('begin boundary'); return 'BOUNDARY';} +"Enterprise_Boundary" { this.begin("enterprise_boundary"); console.log('begin enterprise_boundary'); return 'ENTERPRISE_BOUNDARY';} +"System_Boundary" { this.begin("system_boundary"); console.log('begin system_boundary'); return 'SYSTEM_BOUNDARY';} + +"Rel" { this.begin("rel"); console.log('begin rel'); return 'REL';} +"BiRel" { this.begin("birel"); console.log('begin birel'); return 'BIREL';} +"Rel_U|Rel_Up" { this.begin("rel_u"); console.log('begin rel_u'); return 'REL_U';} +"Rel_D|Rel_Down" { this.begin("rel_d"); console.log('begin rel_d'); return 'REL_D';} +"Rel_L|Rel_Left" { this.begin("rel_l"); console.log('begin rel_l'); return 'REL_L';} +"Rel_R|Rel_Right" { this.begin("rel_r"); console.log('begin rel_r'); return 'REL_R';} + + +<> return "EOF_IN_STRUCT"; +[(][ ]*[,] { console.log('begin attribute with ATTRIBUTE_EMPTY'); this.begin("attribute"); return "ATTRIBUTE_EMPTY";} +[(] { console.log('begin attribute'); this.begin("attribute"); } +[)] { console.log('STOP attribute'); this.popState();console.log('STOP diagram'); this.popState();} + +",," { console.log(',,'); return 'ATTRIBUTE_EMPTY';} +"," { console.log(','); } +[ ]*["]["] { console.log('ATTRIBUTE_EMPTY'); return 'ATTRIBUTE_EMPTY';} +[ ]*["] { console.log('begin string'); this.begin("string");} +["] { console.log('STOP string'); this.popState(); } +[^"]* { console.log('STR'); return "STR";} +[^,]+ { console.log('not STR'); return "STR";} + +'{' { /* this.begin("lbrace"); */ console.log('begin boundary block'); return "LBRACE";} +'}' { /* this.popState(); */ console.log('STOP boundary block'); return "RBRACE";} + +[\s]+ return 'SPACE'; +[\n\r]+ return 'EOL'; +<> return 'EOF'; + +/lex + +/* operator associations and precedence */ + +%left '^' + +%start start + +%% /* language grammar */ + +start + : mermaidDoc + | direction + | directive start + ; + +direction + : direction_tb + { yy.setDirection('TB');} + | direction_bt + { yy.setDirection('BT');} + | direction_rl + { yy.setDirection('RL');} + | direction_lr + { yy.setDirection('LR');} + ; + +mermaidDoc + : graphConfig + ; + +directive + : openDirective typeDirective closeDirective NEWLINE + | openDirective typeDirective ':' argDirective closeDirective NEWLINE + ; + +openDirective + : open_directive { console.log("open_directive: ", $1); yy.parseDirective('%%{', 'open_directive'); } + ; + +typeDirective + : type_directive { } + ; + +argDirective + : arg_directive { $1 = $1.trim().replace(/'/g, '"'); console.log("arg_directive: ", $1); yy.parseDirective($1, 'arg_directive'); } + ; + +closeDirective + : close_directive { console.log("close_directive: ", $1); yy.parseDirective('}%%', 'close_directive', 'c4Context'); } + ; + +graphConfig + : C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)} + | C4_CONTAINER NEWLINE statements EOF {yy.setC4Type($1)} + | C4_COMPONENT NEWLINE statements EOF {yy.setC4Type($1)} + | C4_DYNAMIC NEWLINE statements EOF {yy.setC4Type($1)} + | C4_DEPLOYMENT NEWLINE statements EOF {yy.setC4Type($1)} + ; + +statements + : otherStatements + | diagramStatements + | otherStatements diagramStatements + ; + +otherStatements + : otherStatement + | otherStatement NEWLINE + | otherStatement NEWLINE otherStatements + ; + +otherStatement + : title {yy.setTitle($1.substring(6));$$=$1.substring(6);} + | accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);} + ; + +boundaryStatement + : boundaryStartStatement diagramStatements boundaryStopStatement + ; + +boundaryStartStatement + : boundaryStart LBRACE NEWLINE + | boundaryStart NEWLINE LBRACE + | boundaryStart NEWLINE LBRACE NEWLINE + ; + +boundaryStart + : ENTERPRISE_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addBoundary(...$2); $$=$2;} + | SYSTEM_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addBoundary(...$2); $$=$2;} + | BOUNDARY attributes {console.log($1,JSON.stringify($2)); yy.addBoundary(...$2); $$=$2;} + ; + +boundaryStopStatement + : RBRACE { yy.popBoundaryParseStack() } + ; + +diagramStatements + : diagramStatement + | diagramStatement NEWLINE + | diagramStatement NEWLINE statements + ; + +diagramStatement + : PERSON attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('person', ...$2); $$=$2;} + | PERSON_EXT attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_person', ...$2); $$=$2;} + | SYSTEM attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('system', ...$2); $$=$2;} + | SYSTEM_DB attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('system_db', ...$2); $$=$2;} + | SYSTEM_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('system_queue', ...$2); $$=$2;} + | SYSTEM_EXT attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system', ...$2); $$=$2;} + | SYSTEM_EXT_DB attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system_db', ...$2); $$=$2;} + | SYSTEM_EXT_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;} + | boundaryStatement + | REL attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel', ...$2); $$=$2;} + | BIREL attributes {console.log($1,JSON.stringify($2)); yy.addRel('birel', ...$2); $$=$2;} + | REL_U attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_u', ...$2); $$=$2;} + | REL_D attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_d', ...$2); $$=$2;} + | REL_L attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_l', ...$2); $$=$2;} + | REL_R attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_r', ...$2); $$=$2;} + ; + +attributes + : attribute { console.log('PUSH ATTRIBUTE: ', $1); $$ = [$1]; } + | attribute attributes { console.log('PUSH ATTRIBUTE: ', $1); $2.unshift($1); $$=$2;} + ; + +attribute + : STR { $$ = $1.trim(); } + | ATTRIBUTE { $$ = $1.trim(); } + | ATTRIBUTE_EMPTY { $$ = ""; } + ; + diff --git a/src/diagrams/c4/styles.js b/src/diagrams/c4/styles.js new file mode 100644 index 0000000000..c24412b3c7 --- /dev/null +++ b/src/diagrams/c4/styles.js @@ -0,0 +1,8 @@ +const getStyles = (options) => + `.person { + stroke: ${options.personBorder}; + fill: ${options.personBkg}; + } +`; + +export default getStyles; diff --git a/src/diagrams/c4/svgDraw.js b/src/diagrams/c4/svgDraw.js new file mode 100644 index 0000000000..7081d4e5bc --- /dev/null +++ b/src/diagrams/c4/svgDraw.js @@ -0,0 +1,805 @@ +import common from '../common/common'; +import { addFunction } from '../../interactionDb'; +import { sanitizeUrl } from '@braintree/sanitize-url'; + +export const drawRect = function (elem, rectData) { + const rectElem = elem.append('rect'); + rectElem.attr('x', rectData.x); + rectElem.attr('y', rectData.y); + rectElem.attr('fill', rectData.fill); + rectElem.attr('stroke', rectData.stroke); + rectElem.attr('width', rectData.width); + rectElem.attr('height', rectData.height); + rectElem.attr('rx', rectData.rx); + rectElem.attr('ry', rectData.ry); + + if (rectData.attrs !== 'undefined' && rectData.attrs !== null) { + for (let attrKey in rectData.attrs) rectElem.attr(attrKey, rectData.attrs[attrKey]); + } + + if (rectData.class !== 'undefined') { + rectElem.attr('class', rectData.class); + } + + return rectElem; +}; + +export const drawImage = function (elem, width, height, x, y, link) { + const imageElem = elem.append('image'); + imageElem.attr('width', width); + imageElem.attr('height', height); + imageElem.attr('x', x); + imageElem.attr('y', y); + let sanitizedLink = link.startsWith('data:image/png;base64') ? link : sanitizeUrl(link); + imageElem.attr('xlink:href', sanitizedLink); +}; + +export const drawEmbeddedImage = function (elem, x, y, link) { + const imageElem = elem.append('use'); + imageElem.attr('x', x); + imageElem.attr('y', y); + var sanitizedLink = sanitizeUrl(link); + imageElem.attr('xlink:href', '#' + sanitizedLink); +}; + +export const drawText = function (elem, textData) { + let prevTextHeight = 0, + textHeight = 0; + const lines = textData.text.split(common.lineBreakRegex); + + let textElems = []; + let dy = 0; + let yfunc = () => textData.y; + if ( + typeof textData.valign !== 'undefined' && + typeof textData.textMargin !== 'undefined' && + textData.textMargin > 0 + ) { + switch (textData.valign) { + case 'top': + case 'start': + yfunc = () => Math.round(textData.y + textData.textMargin); + break; + case 'middle': + case 'center': + yfunc = () => + Math.round(textData.y + (prevTextHeight + textHeight + textData.textMargin) / 2); + break; + case 'bottom': + case 'end': + yfunc = () => + Math.round( + textData.y + + (prevTextHeight + textHeight + 2 * textData.textMargin) - + textData.textMargin + ); + break; + } + } + if ( + typeof textData.anchor !== 'undefined' && + typeof textData.textMargin !== 'undefined' && + typeof textData.width !== 'undefined' + ) { + switch (textData.anchor) { + case 'left': + case 'start': + textData.x = Math.round(textData.x + textData.textMargin); + textData.anchor = 'start'; + textData.dominantBaseline = 'text-after-edge'; + textData.alignmentBaseline = 'middle'; + break; + case 'middle': + case 'center': + textData.x = Math.round(textData.x + textData.width / 2); + textData.anchor = 'middle'; + textData.dominantBaseline = 'middle'; + textData.alignmentBaseline = 'middle'; + break; + case 'right': + case 'end': + textData.x = Math.round(textData.x + textData.width - textData.textMargin); + textData.anchor = 'end'; + textData.dominantBaseline = 'text-before-edge'; + textData.alignmentBaseline = 'middle'; + break; + } + } + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if ( + typeof textData.textMargin !== 'undefined' && + textData.textMargin === 0 && + typeof textData.fontSize !== 'undefined' + ) { + dy = i * textData.fontSize; + } + + const textElem = elem.append('text'); + textElem.attr('x', textData.x); + textElem.attr('y', yfunc()); + if (typeof textData.anchor !== 'undefined') { + textElem + .attr('text-anchor', textData.anchor) + .attr('dominant-baseline', textData.dominantBaseline) + .attr('alignment-baseline', textData.alignmentBaseline); + } + if (typeof textData.fontFamily !== 'undefined') { + textElem.style('font-family', textData.fontFamily); + } + if (typeof textData.fontSize !== 'undefined') { + textElem.style('font-size', textData.fontSize); + } + if (typeof textData.fontWeight !== 'undefined') { + textElem.style('font-weight', textData.fontWeight); + } + if (typeof textData.fill !== 'undefined') { + textElem.attr('fill', textData.fill); + } + if (typeof textData.class !== 'undefined') { + textElem.attr('class', textData.class); + } + if (typeof textData.dy !== 'undefined') { + textElem.attr('dy', textData.dy); + } else if (dy !== 0) { + textElem.attr('dy', dy); + } + + if (textData.tspan) { + const span = textElem.append('tspan'); + span.attr('x', textData.x); + if (typeof textData.fill !== 'undefined') { + span.attr('fill', textData.fill); + } + span.text(line); + } else { + textElem.text(line); + } + if ( + typeof textData.valign !== 'undefined' && + typeof textData.textMargin !== 'undefined' && + textData.textMargin > 0 + ) { + textHeight += (textElem._groups || textElem)[0][0].getBBox().height; + prevTextHeight = textHeight; + } + + textElems.push(textElem); + } + + return textElems; +}; + +export const drawLabel = function (elem, txtObject) { + /** + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} cut + * @returns {any} + */ + function genPoints(x, y, width, height, cut) { + return ( + x + + ',' + + y + + ' ' + + (x + width) + + ',' + + y + + ' ' + + (x + width) + + ',' + + (y + height - cut) + + ' ' + + (x + width - cut * 1.2) + + ',' + + (y + height) + + ' ' + + x + + ',' + + (y + height) + ); + } + const polygon = elem.append('polygon'); + polygon.attr('points', genPoints(txtObject.x, txtObject.y, txtObject.width, txtObject.height, 7)); + polygon.attr('class', 'labelBox'); + + txtObject.y = txtObject.y + txtObject.height / 2; + + drawText(elem, txtObject); + return polygon; +}; + +export const drawRels = (elem, rels, conf) => { + const relsElem = elem.append('g'); + let i = 0; + for (let rel of rels) { + let url = ''; + if (i === 0) { + let line = relsElem.append('line'); + line.attr('x1', rel.startPoint.x); + line.attr('y1', rel.startPoint.y); + line.attr('x2', rel.endPoint.x); + line.attr('y2', rel.endPoint.y); + + line.attr('stroke-width', '1'); + line.attr('stroke', '#444444'); + line.style('fill', 'none'); + line.attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type === 'birel') line.attr('marker-start', 'url(' + url + '#arrowend)'); + i = -1; + } else { + let line = relsElem.append('path'); + line + .attr('fill', 'none') + .attr('stroke-width', '1') + .attr('stroke', '#444444') + .attr( + 'd', + 'Mstartx,starty Qcontrolx,controly stopx,stopy ' + .replaceAll('startx', rel.startPoint.x) + .replaceAll('starty', rel.startPoint.y) + .replaceAll( + 'controlx', + rel.startPoint.x + + (rel.endPoint.x - rel.startPoint.x) / 2 - + (rel.endPoint.x - rel.startPoint.x) / 4 + ) + .replaceAll('controly', rel.startPoint.y + (rel.endPoint.y - rel.startPoint.y) / 2) + .replaceAll('stopx', rel.endPoint.x) + .replaceAll('stopy', rel.endPoint.y) + ) + .attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type === 'birel') line.attr('marker-start', 'url(' + url + '#arrowend)'); + } + + let messageConf = conf.messageFont(); + _drawTextCandidateFunc(conf)( + rel.label.text, + relsElem, + Math.min(rel.startPoint.x, rel.endPoint.x) + Math.abs(rel.endPoint.x - rel.startPoint.x) / 2, + Math.min(rel.startPoint.y, rel.endPoint.y) + Math.abs(rel.endPoint.y - rel.startPoint.y) / 2, + rel.label.width, + rel.label.height, + { fill: '#444444' }, + messageConf + ); + + if (rel.techn && rel.techn.text !== '') { + messageConf = conf.messageFont(); + _drawTextCandidateFunc(conf)( + '[' + rel.techn.text + ']', + relsElem, + Math.min(rel.startPoint.x, rel.endPoint.x) + + Math.abs(rel.endPoint.x - rel.startPoint.x) / 2, + Math.min(rel.startPoint.y, rel.endPoint.y) + + Math.abs(rel.endPoint.y - rel.startPoint.y) / 2 + + conf.messageFontSize + + 5, + Math.max(rel.label.width, rel.techn.width), + rel.techn.height, + { fill: '#444444', 'font-style': 'italic' }, + messageConf + ); + } + } +}; + +/** + * Draws an boundary in the diagram + * + * @param {any} elem - The diagram we'll draw to. + * @param {any} boundary - The boundary to draw. + * @param {any} conf - DrawText implementation discriminator object + */ +const drawBoundary = function (elem, boundary, conf) { + const boundaryElem = elem.append('g'); + + let rectData = { + x: boundary.x, + y: boundary.y, + fill: 'none', + stroke: '#444444', + width: boundary.width, + height: boundary.height, + rx: 2.5, + ry: 2.5, + attrs: { 'stroke-width': 1.0, 'stroke-dasharray': '7.0,7.0' }, + }; + + drawRect(boundaryElem, rectData); + + let boundaryConf = conf.boundaryFont(); + boundaryConf.fontWeight = 'bold'; + boundaryConf.fontSize = boundaryConf.fontSize + 2; + _drawTextCandidateFunc(conf)( + boundary.label.text, + boundaryElem, + boundary.x, + boundary.y + boundary.label.y, + boundary.width, + boundary.height, + { fill: '#444444' }, + boundaryConf + ); + + boundaryConf = conf.boundaryFont(); + boundaryConf.fontSize = boundaryConf.fontSize - 2; + _drawTextCandidateFunc(conf)( + '[' + boundary.type + ']', + boundaryElem, + boundary.x, + boundary.y + boundary.label.y + boundaryConf.fontSize + 8, + boundary.width, + boundary.height, + { fill: '#444444' }, + boundaryConf + ); +}; + +export const drawPersonOrSystem = function (elem, personOrSystem, conf) { + let fillColor = conf[personOrSystem.type + '_bg_color']; + let strokeColor = conf[personOrSystem.type + '_border_color']; + let personImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII='; + switch (personOrSystem.type) { + case 'person': + personImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII='; + break; + case 'external_person': + personImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAB6ElEQVR4Xu2YLY+EMBCG9+dWr0aj0Wg0Go1Go0+j8Xdv2uTCvv1gpt0ebHKPuhDaeW4605Z9mJvx4AdXUyTUdd08z+u6flmWZRnHsWkafk9DptAwDPu+f0eAYtu2PEaGWuj5fCIZrBAC2eLBAnRCsEkkxmeaJp7iDJ2QMDdHsLg8SxKFEJaAo8lAXnmuOFIhTMpxxKATebo4UiFknuNo4OniSIXQyRxEA3YsnjGCVEjVXD7yLUAqxBGUyPv/Y4W2beMgGuS7kVQIBycH0fD+oi5pezQETxdHKmQKGk1eQEYldK+jw5GxPfZ9z7Mk0Qnhf1W1m3w//EUn5BDmSZsbR44QQLBEqrBHqOrmSKaQAxdnLArCrxZcM7A7ZKs4ioRq8LFC+NpC3WCBJsvpVw5edm9iEXFuyNfxXAgSwfrFQ1c0iNda8AdejvUgnktOtJQQxmcfFzGglc5WVCj7oDgFqU18boeFSs52CUh8LE8BIVQDT1ABrB0HtgSEYlX5doJnCwv9TXocKCaKbnwhdDKPq4lf3SwU3HLq4V/+WYhHVMa/3b4IlfyikAduCkcBc7mQ3/z/Qq/cTuikhkzB12Ae/mcJC9U+Vo8Ej1gWAtgbeGgFsAMHr50BIWOLCbezvhpBFUdY6EJuJ/QDW0XoMX60zZ0AAAAASUVORK5CYII='; + break; + } + + const personOrSystemElem = elem.append('g'); + personOrSystemElem.attr('class', 'person-man'); + + // + const rect = getNoteRect(); + switch (personOrSystem.type) { + case 'person': + case 'external_person': + case 'system': + case 'external_system': + rect.x = personOrSystem.x; + rect.y = personOrSystem.y; + rect.fill = fillColor; + rect.width = personOrSystem.width; + rect.height = personOrSystem.height; + rect.style = 'stroke:' + strokeColor + ';stroke-width:0.5;'; + rect.rx = 2.5; + rect.ry = 2.5; + drawRect(personOrSystemElem, rect); + break; + case 'system_db': + case 'external_system_db': + personOrSystemElem + .append('path') + .attr('fill', fillColor) + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startyc0,-10 half,-10 half,-10c0,0 half,0 half,10l0,heightc0,10 -half,10 -half,10c0,0 -half,0 -half,-10l0,-height' + .replaceAll('startx', personOrSystem.x) + .replaceAll('starty', personOrSystem.y) + .replaceAll('half', personOrSystem.width / 2) + .replaceAll('height', personOrSystem.height) + ); + personOrSystemElem + .append('path') + .attr('fill', 'none') + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startyc0,10 half,10 half,10c0,0 half,0 half,-10' + .replaceAll('startx', personOrSystem.x) + .replaceAll('starty', personOrSystem.y) + .replaceAll('half', personOrSystem.width / 2) + ); + break; + case 'system_queue': + case 'external_system_queue': + personOrSystemElem + .append('path') + .attr('fill', fillColor) + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startylwidth,0c5,0 5,half 5,halfc0,0 0,half -5,halfl-width,0c-5,0 -5,-half -5,-halfc0,0 0,-half 5,-half' + .replaceAll('startx', personOrSystem.x) + .replaceAll('starty', personOrSystem.y) + .replaceAll('width', personOrSystem.width) + .replaceAll('half', personOrSystem.height / 2) + ); + personOrSystemElem + .append('path') + .attr('fill', 'none') + .attr('stroke-width', '0.5') + .attr('stroke', strokeColor) + .attr( + 'd', + 'Mstartx,startyc-5,0 -5,half -5,halfc0,half 5,half 5,half' + .replaceAll('startx', personOrSystem.x + personOrSystem.width) + .replaceAll('starty', personOrSystem.y) + .replaceAll('half', personOrSystem.height / 2) + ); + break; + } + + personOrSystemElem + .append('text') + .attr('fill', '#FFFFFF') + .attr('font-family', conf.personFontFamily) + .attr('font-size', conf.personFontSize - 2) + .attr('font-style', 'italic') + .attr('lengthAdjust', 'spacing') + .attr('textLength', personOrSystem.typeLabelWidth) + .attr('x', personOrSystem.x + personOrSystem.width / 2 - personOrSystem.typeLabelWidth / 2) + .attr('y', personOrSystem.y + personOrSystem.typeLabelY) + .text('<<' + personOrSystem.type + '>>'); + + switch (personOrSystem.type) { + case 'person': + case 'external_person': + drawImage( + personOrSystemElem, + 48, + 48, + personOrSystem.x + personOrSystem.width / 2 - 24, + personOrSystem.y + 24, + personImg + ); + break; + } + + let personOrSystemConf = conf.personFont(); + personOrSystemConf.fontWeight = 'bold'; + personOrSystemConf.fontSize = personOrSystemConf.fontSize + 2; + _drawTextCandidateFunc(conf)( + personOrSystem.label.text, + personOrSystemElem, + personOrSystem.x, + personOrSystem.y + personOrSystem.label.Y, + personOrSystem.width, + personOrSystem.height, + { fill: '#FFFFFF' }, + personOrSystemConf + ); + + personOrSystemConf = conf.personFont(); + _drawTextCandidateFunc(conf)( + personOrSystem.descr.text, + personOrSystemElem, + personOrSystem.x, + personOrSystem.y + personOrSystem.descr.Y, + personOrSystem.width, + personOrSystem.height, + { fill: '#FFFFFF' }, + personOrSystemConf + ); + + return personOrSystem.height; +}; + +export const insertDatabaseIcon = function (elem) { + elem + .append('defs') + .append('symbol') + .attr('id', 'database') + .attr('fill-rule', 'evenodd') + .attr('clip-rule', 'evenodd') + .append('path') + .attr('transform', 'scale(.5)') + .attr( + 'd', + 'M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z' + ); +}; + +export const insertComputerIcon = function (elem) { + elem + .append('defs') + .append('symbol') + .attr('id', 'computer') + .attr('width', '24') + .attr('height', '24') + .append('path') + .attr('transform', 'scale(.5)') + .attr( + 'd', + 'M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z' + ); +}; + +export const insertClockIcon = function (elem) { + elem + .append('defs') + .append('symbol') + .attr('id', 'clock') + .attr('width', '24') + .attr('height', '24') + .append('path') + .attr('transform', 'scale(.5)') + .attr( + 'd', + 'M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z' + ); +}; + +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param elem + */ +export const insertArrowHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'arrowhead') + .attr('refX', 9) + .attr('refY', 5) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead +}; +export const insertArrowEnd = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'arrowend') + .attr('refX', 1) + .attr('refY', 5) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 10 0 L 0 5 L 10 10 z'); // this is actual shape for arrowhead +}; +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param {any} elem + */ +export const insertArrowFilledHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'filled-head') + .attr('refX', 18) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); +}; +/** + * Setup node number. The result is appended to the svg. + * + * @param {any} elem + */ +export const insertDynamicNumber = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'sequencenumber') + .attr('refX', 15) + .attr('refY', 15) + .attr('markerWidth', 60) + .attr('markerHeight', 40) + .attr('orient', 'auto') + .append('circle') + .attr('cx', 15) + .attr('cy', 15) + .attr('r', 6); + // .style("fill", '#f00'); +}; +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param {any} elem + */ +export const insertArrowCrossHead = function (elem) { + const defs = elem.append('defs'); + const marker = defs + .append('marker') + .attr('id', 'crosshead') + .attr('markerWidth', 15) + .attr('markerHeight', 8) + .attr('orient', 'auto') + .attr('refX', 16) + .attr('refY', 4); + + // The arrow + marker + .append('path') + .attr('fill', 'black') + .attr('stroke', '#000000') + .style('stroke-dasharray', '0, 0') + .attr('stroke-width', '1px') + .attr('d', 'M 9,2 V 6 L16,4 Z'); + + // The cross + marker + .append('path') + .attr('fill', 'none') + .attr('stroke', '#000000') + .style('stroke-dasharray', '0, 0') + .attr('stroke-width', '1px') + .attr('d', 'M 0,1 L 6,7 M 6,1 L 0,7'); + // this is actual shape for arrowhead +}; + +export const getTextObj = function () { + return { + x: 0, + y: 0, + fill: undefined, + anchor: undefined, + style: '#666', + width: undefined, + height: undefined, + textMargin: 0, + rx: 0, + ry: 0, + tspan: true, + valign: undefined, + }; +}; + +export const getNoteRect = function () { + return { + x: 0, + y: 0, + fill: '#EDF2AE', + stroke: '#666', + width: 100, + anchor: 'start', + height: 100, + rx: 0, + ry: 0, + }; +}; + +const _drawTextCandidateFunc = (function () { + /** + * @param {any} content + * @param {any} g + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} textAttrs + */ + function byText(content, g, x, y, width, height, textAttrs) { + const text = g + .append('text') + .attr('x', x + width / 2) + .attr('y', y + height / 2 + 5) + .style('text-anchor', 'middle') + .text(content); + _setTextAttrs(text, textAttrs); + } + + /** + * @param {any} content + * @param {any} g + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} textAttrs + * @param {any} conf + */ + function byTspan(content, g, x, y, width, height, textAttrs, conf) { + const { fontSize, fontFamily, fontWeight } = conf; + + const lines = content.split(common.lineBreakRegex); + for (let i = 0; i < lines.length; i++) { + const dy = i * fontSize - (fontSize * (lines.length - 1)) / 2; + const text = g + .append('text') + .attr('x', x + width / 2) + .attr('y', y) + .style('text-anchor', 'middle') + .style('font-size', fontSize) + .style('font-weight', fontWeight) + .style('font-family', fontFamily); + text + .append('tspan') + .attr('x', x + width / 2) + .attr('dy', dy) + .text(lines[i]); + + text.attr('y', y).attr('dominant-baseline', 'central').attr('alignment-baseline', 'central'); + + _setTextAttrs(text, textAttrs); + } + } + + /** + * @param {any} content + * @param {any} g + * @param {any} x + * @param {any} y + * @param {any} width + * @param {any} height + * @param {any} textAttrs + * @param {any} conf + */ + function byFo(content, g, x, y, width, height, textAttrs, conf) { + const s = g.append('switch'); + const f = s + .append('foreignObject') + .attr('x', x) + .attr('y', y) + .attr('width', width) + .attr('height', height); + + const text = f + .append('xhtml:div') + .style('display', 'table') + .style('height', '100%') + .style('width', '100%'); + + text + .append('div') + .style('display', 'table-cell') + .style('text-align', 'center') + .style('vertical-align', 'middle') + .text(content); + + byTspan(content, s, x, y, width, height, textAttrs, conf); + _setTextAttrs(text, textAttrs); + } + + /** + * @param {any} toText + * @param {any} fromTextAttrsDict + */ + function _setTextAttrs(toText, fromTextAttrsDict) { + for (const key in fromTextAttrsDict) { + if (fromTextAttrsDict.hasOwnProperty(key)) { + // eslint-disable-line + toText.attr(key, fromTextAttrsDict[key]); + } + } + } + + return function (conf) { + return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan; + }; +})(); + +export default { + drawRect, + drawText, + drawLabel, + drawBoundary, + drawPersonOrSystem, + drawRels, + drawImage, + drawEmbeddedImage, + insertArrowHead, + insertArrowEnd, + insertArrowFilledHead, + insertSequenceNumber: insertDynamicNumber, + insertArrowCrossHead, + insertDatabaseIcon, + insertComputerIcon, + insertClockIcon, + getTextObj, + getNoteRect, + sanitizeUrl, +}; diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index d985c39443..7d0eb3d523 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -19,6 +19,9 @@ import { select } from 'd3'; import { compile, serialize, stringify } from 'stylis'; import pkg from '../package.json'; import * as configApi from './config'; +import c4Db from './diagrams/c4/c4Db'; +import c4Renderer from './diagrams/c4/c4Renderer'; +import c4Parser from './diagrams/c4/parser/c4Diagram'; import classDb from './diagrams/class/classDb'; import classRenderer from './diagrams/class/classRenderer'; import classRendererV2 from './diagrams/class/classRenderer-v2'; @@ -84,6 +87,11 @@ function parse(text) { log.debug('Type ' + graphType); switch (graphType) { + case 'c4': + c4Db.clear(); + parser = c4Parser; + parser.parser.yy = c4Parser; + break; case 'gitGraph': gitGraphAst.clear(); parser = gitGraphParser; @@ -449,6 +457,10 @@ const render = function (id, _txt, cb, container) { try { switch (graphType) { + case 'c4': + c4Renderer.setConf(cnf.c4); + c4Renderer.draw(txt, id); + break; case 'gitGraph': // cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; //gitGraphRenderer.setConf(cnf.git); diff --git a/src/styles.js b/src/styles.js index 9d39fe4c80..f5608f0dc1 100644 --- a/src/styles.js +++ b/src/styles.js @@ -9,6 +9,7 @@ import requirement from './diagrams/requirement/styles'; import sequence from './diagrams/sequence/styles'; import stateDiagram from './diagrams/state/styles'; import journey from './diagrams/user-journey/styles'; +import c4 from './diagrams/c4/styles'; const themes = { flowchart, @@ -26,6 +27,7 @@ const themes = { er, journey, requirement, + c4, }; export const calcThemeVariables = (theme, userOverRides) => theme.calcColors(userOverRides); diff --git a/src/themes/c4.scss b/src/themes/c4.scss new file mode 100644 index 0000000000..0c3fca3a99 --- /dev/null +++ b/src/themes/c4.scss @@ -0,0 +1,4 @@ +.person { + stroke: $personBorder; + fill: $personBkg; +} diff --git a/src/themes/default/index.scss b/src/themes/default/index.scss index 7daea0e630..a20f81a7d6 100644 --- a/src/themes/default/index.scss +++ b/src/themes/default/index.scss @@ -56,6 +56,11 @@ $critBorderColor: #ff8888; $critBkgColor: red; $todayLineColor: red; +/* C4 Context Diagram variables */ + +$personBorder: $border1; +$personBkg: $mainBkg; + /* state colors */ $labelColor: black; diff --git a/src/themes/theme-base.js b/src/themes/theme-base.js index 7474da8987..361e4495ba 100644 --- a/src/themes/theme-base.js +++ b/src/themes/theme-base.js @@ -113,6 +113,11 @@ class Theme { this.taskTextDarkColor = this.taskTextDarkColor || this.textColor; this.taskTextClickableColor = this.taskTextClickableColor || '#003163'; + /* Sequence Diagram variables */ + + this.personBorder = this.personBorder || this.primaryBorderColor; + this.personBkg = this.personBkg || this.mainBkg; + /* state colors */ this.transitionColor = this.transitionColor || this.lineColor; this.transitionLabelColor = this.transitionLabelColor || this.textColor; diff --git a/src/themes/theme-dark.js b/src/themes/theme-dark.js index 71b9d446ce..ecccaa8769 100644 --- a/src/themes/theme-dark.js +++ b/src/themes/theme-dark.js @@ -78,6 +78,11 @@ class Theme { this.taskTextDarkColor = 'calculated'; this.todayLineColor = '#DB5757'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'calculated'; diff --git a/src/themes/theme-default.js b/src/themes/theme-default.js index 81a07b9b8d..a91a9a249e 100644 --- a/src/themes/theme-default.js +++ b/src/themes/theme-default.js @@ -103,6 +103,11 @@ class Theme { this.critBkgColor = 'red'; this.todayLineColor = 'red'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'black'; this.errorBkgColor = '#552222'; diff --git a/src/themes/theme-forest.js b/src/themes/theme-forest.js index b0ec574580..b92291d2a1 100644 --- a/src/themes/theme-forest.js +++ b/src/themes/theme-forest.js @@ -76,6 +76,11 @@ class Theme { this.critBkgColor = 'red'; this.todayLineColor = 'red'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'black'; diff --git a/src/themes/theme-neutral.js b/src/themes/theme-neutral.js index af228513a5..78873392ef 100644 --- a/src/themes/theme-neutral.js +++ b/src/themes/theme-neutral.js @@ -89,6 +89,11 @@ class Theme { this.critBorderColor = 'calculated'; this.todayLineColor = 'calculated'; + /* C4 Context Diagram variables */ + + this.personBorder = 'calculated'; + this.personBkg = 'calculated'; + /* state colors */ this.labelColor = 'black'; diff --git a/src/utils.js b/src/utils.js index e8a24bb6aa..d19078777f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -185,6 +185,10 @@ export const detectDirective = function (text, type = null) { */ export const detectType = function (text, cnf) { text = text.replace(directive, '').replace(anyComment, '\n'); + if (text.match(/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/)) { + return 'c4'; + } + if (text.match(/^\s*sequenceDiagram/)) { return 'sequence'; } From 28ca1420f9891099091c8b72c2d2f9370f91ffe0 Mon Sep 17 00:00:00 2001 From: pinghe Date: Tue, 17 May 2022 19:57:03 +0800 Subject: [PATCH 3/3] Add C4Context diagram. Compatible with C4-PlantUML syntax. For an example, see the source code demos/index.html - System Context - Container diagram - Component diagram - Dynamic diagram - Deployment diagram --- demos/index.html | 133 +++++++++- src/defaultConfig.js | 244 ++++++++++++++++- src/diagrams/c4/c4Db.js | 227 ++++++++++++++-- src/diagrams/c4/c4Renderer.js | 348 +++++++++++++++---------- src/diagrams/c4/parser/c4Diagram.jison | 81 ++++-- src/diagrams/c4/svgDraw.js | 255 +++++++++++------- 6 files changed, 1007 insertions(+), 281 deletions(-) diff --git a/demos/index.html b/demos/index.html index 1db4bf4169..0abe627a71 100644 --- a/demos/index.html +++ b/demos/index.html @@ -23,32 +23,33 @@
C4Context title System Context diagram for Internet Banking System - +Enterprise_Boundary(b0, "BankBoundary0") { Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") Person(customerB, "Banking Customer B") - Person_Ext(customerC, "Banking Customer C") - System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") - + Person_Ext(customerC, "Banking Customer C", "desc") Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + Enterprise_Boundary(b1, "BankBoundary") { SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") System_Boundary(b2, "BankBoundary2") { System(SystemA, "Banking System A") - System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.") + System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts. next line.") } System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") Boundary(b3, "BankBoundary3", "boundary") { - SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.") + SystemQueue(SystemF, "Banking System F Queue", "A system of the bank.") SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") } } + } BiRel(customerA, SystemAA, "Uses") BiRel(SystemAA, SystemE, "Uses") @@ -56,6 +57,126 @@ Rel(SystemC, customerA, "Sends e-mails to")
+
+ C4Container + title Container diagram for Internet Banking System + + System_Ext(email_system, "E-Mail System", "The internal Microsoft Exchange system") + Person(customer, Customer, "A customer of the bank, with personal bank accounts") + + Container_Boundary(c1, "Internet Banking") { + Container(spa, "Single-Page App", "JavaScript, Angular", "Provides all the Internet banking functionality to cutomers via their web browser") + Container_Ext(mobile_app, "Mobile App", "C#, Xamarin", "Provides a limited subset of the Internet banking functionality to customers via their mobile device") + Container(web_app, "Web Application", "Java, Spring MVC", "Delivers the static content and the Internet banking SPA") + ContainerDb(database, "Database", "SQL Database", "Stores user registration information, hashed auth credentials, access logs, etc.") + ContainerDb_Ext(backend_api, "API Application", "Java, Docker Container", "Provides Internet banking functionality via API") + + } + + System_Ext(banking_system, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") + + Rel(customer, web_app, "Uses", "HTTPS") + Rel(customer, spa, "Uses", "HTTPS") + Rel(customer, mobile_app, "Uses") + + Rel(web_app, spa, "Delivers") + Rel(spa, backend_api, "Uses", "async, JSON/HTTPS") + Rel(mobile_app, backend_api, "Uses", "async, JSON/HTTPS") + Rel_Back(database, backend_api, "Reads from and writes to", "sync, JDBC") + + Rel(email_system, customer, "Sends e-mails to") + Rel(backend_api, email_system, "Sends e-mails using", "sync, SMTP") + Rel(backend_api, banking_system, "Uses", "sync/async, XML/HTTPS") +
+ +
+ C4Component + title Component diagram for Internet Banking System - API Application + + Container(spa, "Single Page Application", "javascript and angular", "Provides all the internet banking functionality to customers via their web browser.") + Container(ma, "Mobile App", "Xamarin", "Provides a limited subset ot the internet banking functionality to customers via their mobile mobile device.") + ContainerDb(db, "Database", "Relational Database Schema", "Stores user registration information, hashed authentication credentials, access logs, etc.") + System_Ext(mbs, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") + + Container_Boundary(api, "API Application") { + Component(sign, "Sign In Controller", "MVC Rest Controlle", "Allows users to sign in to the internet banking system") + Component(accounts, "Accounts Summary Controller", "MVC Rest Controller", "Provides customers with a summary of their bank accounts") + Component(security, "Security Component", "Spring Bean", "Provides functionality related to singing in, changing passwords, etc.") + Component(mbsfacade, "Mainframe Banking System Facade", "Spring Bean", "A facade onto the mainframe banking system.") + + Rel(sign, security, "Uses") + Rel(accounts, mbsfacade, "Uses") + Rel(security, db, "Read & write to", "JDBC") + Rel(mbsfacade, mbs, "Uses", "XML/HTTPS") + } + + Rel_Back(spa, sign, "Uses", "JSON/HTTPS") + Rel(spa, accounts, "Uses", "JSON/HTTPS") + + Rel(ma, sign, "Uses", "JSON/HTTPS") + Rel(ma, accounts, "Uses", "JSON/HTTPS") +
+ +
+ C4Dynamic + title Dynamic diagram for Internet Banking System - API Application + + ContainerDb(c4, "Database", "Relational Database Schema", "Stores user registration information, hashed authentication credentials, access logs, etc.") + Container(c1, "Single-Page Application", "JavaScript and Angular", "Provides all of the Internet banking functionality to customers via their web browser.") + Container_Boundary(b, "API Application") { + Component(c3, "Security Component", "Spring Bean", "Provides functionality Related to signing in, changing passwords, etc.") + Component(c2, "Sign In Controller", "Spring MVC Rest Controller", "Allows users to sign in to the Internet Banking System.") + } + Rel(c1, c2, "Submits credentials to", "JSON/HTTPS") + Rel(c2, c3, "Calls isAuthenticated() on") + Rel(c3, c4, "select * from users where username = ?", "JDBC") +
+ +
+ C4Deployment + title Deployment Diagram for Internet Banking System - Live + + Deployment_Node(mob, "Customer's mobile device", "Apple IOS or Android"){ + Container(mobile, "Mobile App", "Xamarin", "Provides a limited subset of the Internet Banking functionality to customers via their mobile device.") + } + + Deployment_Node(comp, "Customer's computer", "Mircosoft Windows or Apple macOS"){ + Deployment_Node(browser, "Web Browser", "Google Chrome, Mozilla Firefox,
Apple Safari or Microsoft Edge"){ + Container(spa, "Single Page Application", "JavaScript and Angular", "Provides all of the Internet Banking functionality to customers via their web browser.") + } + } + + Deployment_Node(plc, "Big Bank plc", "Big Bank plc data center"){ + Deployment_Node(dn, "bigbank-api*** x8", "Ubuntu 16.04 LTS"){ + Deployment_Node(apache, "Apache Tomcat", "Apache Tomcat 8.x"){ + Container(api, "API Application", "Java and Spring MVC", "Provides Internet Banking functionality via a JSON/HTTPS API.") + } + } + Deployment_Node(bb2, "bigbank-web*** x4", "Ubuntu 16.04 LTS"){ + Deployment_Node(apache2, "Apache Tomcat", "Apache Tomcat 8.x"){ + Container(web, "Web Application", "Java and Spring MVC", "Delivers the static content and the Internet Banking single page application.") + } + } + Deployment_Node(bigbankdb01, "bigbank-db01", "Ubuntu 16.04 LTS"){ + Deployment_Node(oracle, "Oracle - Primary", "Oracle 12c"){ + ContainerDb(db, "Database", "Relational Database Schema", "Stores user registration information, hashed authentication credentials, access logs, etc.") + } + } + Deployment_Node(bigbankdb02, "bigbank-db02", "Ubuntu 16.04 LTS") { + Deployment_Node(oracle2, "Oracle - Secondary", "Oracle 12c") { + ContainerDb(db2, "Database", "Relational Database Schema", "Stores user registration information, hashed authentication credentials, access logs, etc.") + } + } + } + + Rel(mobile, api, "Makes API calls to", "json/HTTPS") + Rel(spa, api, "Makes API calls to", "json/HTTPS") + Rel_U(web, spa, "Delivers to the customer's web browser") + Rel(api, db, "Reads from and writes to", "JDBC") + Rel(api, db2, "Reads from and writes to", "JDBC") + Rel_R(db, db2, "Replicates data to") +
+
diff --git a/src/defaultConfig.js b/src/defaultConfig.js index b2dead9190..67a0b7d277 100644 --- a/src/defaultConfig.js +++ b/src/defaultConfig.js @@ -1098,7 +1098,7 @@ const config = { * | --------- | --------------------- | ------- | -------- | ------------------ | * | width | Width of person boxes | Integer | Required | Any Positive Value | * - * **Notes:** Default value: 215 + * **Notes:** Default value: 216 */ width: 216, @@ -1107,7 +1107,7 @@ const config = { * | --------- | ---------------------- | ------- | -------- | ------------------ | * | height | Height of person boxes | Integer | Required | Any Positive Value | * - * **Notes:** Default value: 65 + * **Notes:** Default value: 60 */ height: 60, @@ -1141,10 +1141,34 @@ const config = { personFontFamily: '"Open Sans", sans-serif', personFontWeight: 'normal', + external_personFontSize: 14, + external_personFontFamily: '"Open Sans", sans-serif', + external_personFontWeight: 'normal', + systemFontSize: 14, systemFontFamily: '"Open Sans", sans-serif', systemFontWeight: 'normal', + external_systemFontSize: 14, + external_systemFontFamily: '"Open Sans", sans-serif', + external_systemFontWeight: 'normal', + + system_dbFontSize: 14, + system_dbFontFamily: '"Open Sans", sans-serif', + system_dbFontWeight: 'normal', + + external_system_dbFontSize: 14, + external_system_dbFontFamily: '"Open Sans", sans-serif', + external_system_dbFontWeight: 'normal', + + system_queueFontSize: 14, + system_queueFontFamily: '"Open Sans", sans-serif', + system_queueFontWeight: 'normal', + + external_system_queueFontSize: 14, + external_system_queueFontFamily: '"Open Sans", sans-serif', + external_system_queueFontWeight: 'normal', + boundaryFontSize: 14, boundaryFontFamily: '"Open Sans", sans-serif', boundaryFontWeight: 'normal', @@ -1153,6 +1177,54 @@ const config = { messageFontFamily: '"Open Sans", sans-serif', messageFontWeight: 'normal', + containerFontSize: 14, + containerFontFamily: '"Open Sans", sans-serif', + containerFontWeight: 'normal', + + external_containerFontSize: 14, + external_containerFontFamily: '"Open Sans", sans-serif', + external_containerFontWeight: 'normal', + + container_dbFontSize: 14, + container_dbFontFamily: '"Open Sans", sans-serif', + container_dbFontWeight: 'normal', + + external_container_dbFontSize: 14, + external_container_dbFontFamily: '"Open Sans", sans-serif', + external_container_dbFontWeight: 'normal', + + container_queueFontSize: 14, + container_queueFontFamily: '"Open Sans", sans-serif', + container_queueFontWeight: 'normal', + + external_container_queueFontSize: 14, + external_container_queueFontFamily: '"Open Sans", sans-serif', + external_container_queueFontWeight: 'normal', + + componentFontSize: 14, + componentFontFamily: '"Open Sans", sans-serif', + componentFontWeight: 'normal', + + external_componentFontSize: 14, + external_componentFontFamily: '"Open Sans", sans-serif', + external_componentFontWeight: 'normal', + + component_dbFontSize: 14, + component_dbFontFamily: '"Open Sans", sans-serif', + component_dbFontWeight: 'normal', + + external_component_dbFontSize: 14, + external_component_dbFontFamily: '"Open Sans", sans-serif', + external_component_dbFontWeight: 'normal', + + component_queueFontSize: 14, + component_queueFontFamily: '"Open Sans", sans-serif', + component_queueFontWeight: 'normal', + + external_component_queueFontSize: 14, + external_component_queueFontFamily: '"Open Sans", sans-serif', + external_component_queueFontWeight: 'normal', + /** * This sets the auto-wrap state for the diagram * @@ -1175,6 +1247,14 @@ const config = { }; }, + external_personFont: function () { + return { + fontFamily: this.external_personFontFamily, + fontSize: this.external_personFontSize, + fontWeight: this.external_personFontWeight, + }; + }, + systemFont: function () { return { fontFamily: this.systemFontFamily, @@ -1183,6 +1263,142 @@ const config = { }; }, + external_systemFont: function () { + return { + fontFamily: this.external_systemFontFamily, + fontSize: this.external_systemFontSize, + fontWeight: this.external_systemFontWeight, + }; + }, + + system_dbFont: function () { + return { + fontFamily: this.system_dbFontFamily, + fontSize: this.system_dbFontSize, + fontWeight: this.system_dbFontWeight, + }; + }, + + external_system_dbFont: function () { + return { + fontFamily: this.external_system_dbFontFamily, + fontSize: this.external_system_dbFontSize, + fontWeight: this.external_system_dbFontWeight, + }; + }, + + system_queueFont: function () { + return { + fontFamily: this.system_queueFontFamily, + fontSize: this.system_queueFontSize, + fontWeight: this.system_queueFontWeight, + }; + }, + + external_system_queueFont: function () { + return { + fontFamily: this.external_system_queueFontFamily, + fontSize: this.external_system_queueFontSize, + fontWeight: this.external_system_queueFontWeight, + }; + }, + + containerFont: function () { + return { + fontFamily: this.containerFontFamily, + fontSize: this.containerFontSize, + fontWeight: this.containerFontWeight, + }; + }, + + external_containerFont: function () { + return { + fontFamily: this.external_containerFontFamily, + fontSize: this.external_containerFontSize, + fontWeight: this.external_containerFontWeight, + }; + }, + + container_dbFont: function () { + return { + fontFamily: this.container_dbFontFamily, + fontSize: this.container_dbFontSize, + fontWeight: this.container_dbFontWeight, + }; + }, + + external_container_dbFont: function () { + return { + fontFamily: this.external_container_dbFontFamily, + fontSize: this.external_container_dbFontSize, + fontWeight: this.external_container_dbFontWeight, + }; + }, + + container_queueFont: function () { + return { + fontFamily: this.container_queueFontFamily, + fontSize: this.container_queueFontSize, + fontWeight: this.container_queueFontWeight, + }; + }, + + external_container_queueFont: function () { + return { + fontFamily: this.external_container_queueFontFamily, + fontSize: this.external_container_queueFontSize, + fontWeight: this.external_container_queueFontWeight, + }; + }, + + componentFont: function () { + return { + fontFamily: this.componentFontFamily, + fontSize: this.componentFontSize, + fontWeight: this.componentFontWeight, + }; + }, + + external_componentFont: function () { + return { + fontFamily: this.external_componentFontFamily, + fontSize: this.external_componentFontSize, + fontWeight: this.external_componentFontWeight, + }; + }, + + component_dbFont: function () { + return { + fontFamily: this.component_dbFontFamily, + fontSize: this.component_dbFontSize, + fontWeight: this.component_dbFontWeight, + }; + }, + + external_component_dbFont: function () { + return { + fontFamily: this.external_component_dbFontFamily, + fontSize: this.external_component_dbFontSize, + fontWeight: this.external_component_dbFontWeight, + }; + }, + + component_queueFont: function () { + return { + fontFamily: this.component_queueFontFamily, + fontSize: this.component_queueFontSize, + fontWeight: this.component_queueFontWeight, + }; + }, + + external_component_queueFont: function () { + return { + fontFamily: this.external_component_queueFontFamily, + fontSize: this.external_component_queueFontSize, + fontWeight: this.external_component_queueFontWeight, + }; + }, + boundaryFont: function () { return { fontFamily: this.boundaryFontFamily, @@ -1217,6 +1433,30 @@ const config = { external_system_db_border_color: '#8A8A8A', external_system_queue_bg_color: '#999999', external_system_queue_border_color: '#8A8A8A', + container_bg_color: '#438DD5', + container_border_color: '#3C7FC0', + container_db_bg_color: '#438DD5', + container_db_border_color: '#3C7FC0', + container_queue_bg_color: '#438DD5', + container_queue_border_color: '#3C7FC0', + external_container_bg_color: '#B3B3B3', + external_container_border_color: '#A6A6A6', + external_container_db_bg_color: '#B3B3B3', + external_container_db_border_color: '#A6A6A6', + external_container_queue_bg_color: '#B3B3B3', + external_container_queue_border_color: '#A6A6A6', + component_bg_color: '#85BBF0', + component_border_color: '#78A8D8', + component_db_bg_color: '#85BBF0', + component_db_border_color: '#78A8D8', + component_queue_bg_color: '#85BBF0', + component_queue_border_color: '#78A8D8', + external_component_bg_color: '#CCCCCC', + external_component_border_color: '#BFBFBF', + external_component_db_bg_color: '#CCCCCC', + external_component_db_border_color: '#BFBFBF', + external_component_queue_bg_color: '#CCCCCC', + external_component_queue_border_color: '#BFBFBF', }, }; diff --git a/src/diagrams/c4/c4Db.js b/src/diagrams/c4/c4Db.js index d29d11c850..c483df2874 100644 --- a/src/diagrams/c4/c4Db.js +++ b/src/diagrams/c4/c4Db.js @@ -3,7 +3,7 @@ import * as configApi from '../../config'; import { log } from '../../logger'; import { sanitizeText } from '../common/common'; -let personOrSystemArray = []; +let c4ShapeArray = []; let boundaryParseStack = ['']; let currentBoundaryParse = 'global'; let parentBoundaryParse = ''; @@ -11,7 +11,7 @@ let boundarys = [ { alias: 'global', label: { text: 'global' }, - type: 'global', + type: { text: 'global' }, tags: null, link: null, parentBoundary: '', @@ -21,14 +21,14 @@ let rels = []; let title = ''; let wrapEnabled = false; let description = ''; -let c4Type = 'C4Context'; +var c4Type; export const getC4Type = function () { return c4Type; }; -export const setC4Type = function (c4Type) { - let sanitizedText = sanitizeText(c4Type, configApi.getConfig()); +export const setC4Type = function (c4TypeParam) { + let sanitizedText = sanitizeText(c4TypeParam, configApi.getConfig()); c4Type = sanitizedText; }; @@ -84,17 +84,17 @@ export const addRel = function (type, from, to, label, techn, descr, sprite, tag }; //type, alias, label, ?descr, ?sprite, ?tags, $link -export const addPersonOrSystem = function (type, alias, label, descr, sprite, tags, link) { +export const addPersonOrSystem = function (typeC4Shape, alias, label, descr, sprite, tags, link) { // Don't allow label nulling if (alias === null || label === null) return; let personOrSystem = {}; - const old = personOrSystemArray.find((personOrSystem) => personOrSystem.alias === alias); + const old = c4ShapeArray.find((personOrSystem) => personOrSystem.alias === alias); if (old && alias === old.alias) { personOrSystem = old; } else { personOrSystem.alias = alias; - personOrSystemArray.push(personOrSystem); + c4ShapeArray.push(personOrSystem); } // Don't allow null labels, either @@ -114,12 +114,94 @@ export const addPersonOrSystem = function (type, alias, label, descr, sprite, ta personOrSystem.sprite = sprite; personOrSystem.tags = tags; personOrSystem.link = link; - personOrSystem.type = type; + personOrSystem.typeC4Shape = { text: typeC4Shape }; personOrSystem.parentBoundary = currentBoundaryParse; }; +//type, alias, label, ?techn, ?descr ?sprite, ?tags, $link +export const addContainer = function (typeC4Shape, alias, label, techn, descr, sprite, tags, link) { + // Don't allow label nulling + if (alias === null || label === null) return; + + let container = {}; + const old = c4ShapeArray.find((container) => container.alias === alias); + if (old && alias === old.alias) { + container = old; + } else { + container.alias = alias; + c4ShapeArray.push(container); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + container.label = { text: '' }; + } else { + container.label = { text: label }; + } + + if (techn === undefined || techn === null) { + container.techn = { text: '' }; + } else { + container.techn = { text: techn }; + } + + if (descr === undefined || descr === null) { + container.descr = { text: '' }; + } else { + container.descr = { text: descr }; + } + + container.sprite = sprite; + container.tags = tags; + container.link = link; + container.wrap = autoWrap(); + container.typeC4Shape = { text: typeC4Shape }; + container.parentBoundary = currentBoundaryParse; +}; + +//type, alias, label, ?techn, ?descr ?sprite, ?tags, $link +export const addComponent = function (typeC4Shape, alias, label, techn, descr, sprite, tags, link) { + // Don't allow label nulling + if (alias === null || label === null) return; + + let component = {}; + const old = c4ShapeArray.find((component) => component.alias === alias); + if (old && alias === old.alias) { + component = old; + } else { + component.alias = alias; + c4ShapeArray.push(component); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + component.label = { text: '' }; + } else { + component.label = { text: label }; + } + + if (techn === undefined || techn === null) { + component.techn = { text: '' }; + } else { + component.techn = { text: techn }; + } + + if (descr === undefined || descr === null) { + component.descr = { text: '' }; + } else { + component.descr = { text: descr }; + } + + component.sprite = sprite; + component.tags = tags; + component.link = link; + component.wrap = autoWrap(); + component.typeC4Shape = { text: typeC4Shape }; + component.parentBoundary = currentBoundaryParse; +}; + //alias, label, ?type, ?tags, $link -export const addBoundary = function (alias, label, type, tags, link) { +export const addPersonOrSystemBoundary = function (alias, label, type, tags, link) { // if (parentBoundary === null) return; // Don't allow label nulling @@ -147,11 +229,104 @@ export const addBoundary = function (alias, label, type, tags, link) { boundary.type = { text: type }; } + boundary.tags = tags; + boundary.link = link; + boundary.parentBoundary = currentBoundaryParse; boundary.wrap = autoWrap(); + + parentBoundaryParse = currentBoundaryParse; + currentBoundaryParse = alias; + boundaryParseStack.push(parentBoundaryParse); +}; + +//alias, label, ?type, ?tags, $link +export const addContainerBoundary = function (alias, label, type, tags, link) { + // if (parentBoundary === null) return; + + // Don't allow label nulling + if (alias === null || label === null) return; + + let boundary = {}; + const old = boundarys.find((boundary) => boundary.alias === alias); + if (old && alias === old.alias) { + boundary = old; + } else { + boundary.alias = alias; + boundarys.push(boundary); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + boundary.label = { text: '' }; + } else { + boundary.label = { text: label }; + } + + if (type === undefined || type === null) { + boundary.type = { text: 'container' }; + } else { + boundary.type = { text: type }; + } + + boundary.tags = tags; + boundary.link = link; + boundary.parentBoundary = currentBoundaryParse; + boundary.wrap = autoWrap(); + + parentBoundaryParse = currentBoundaryParse; + currentBoundaryParse = alias; + boundaryParseStack.push(parentBoundaryParse); +}; + +//alias, label, ?type, ?descr, ?sprite, ?tags, $link +export const addDeploymentNode = function ( + nodeType, + alias, + label, + type, + descr, + sprite, + tags, + link +) { + // if (parentBoundary === null) return; + + // Don't allow label nulling + if (alias === null || label === null) return; + + let boundary = {}; + const old = boundarys.find((boundary) => boundary.alias === alias); + if (old && alias === old.alias) { + boundary = old; + } else { + boundary.alias = alias; + boundarys.push(boundary); + } + + // Don't allow null labels, either + if (label === undefined || label === null) { + boundary.label = { text: '' }; + } else { + boundary.label = { text: label }; + } + + if (type === undefined || type === null) { + boundary.type = { text: 'node' }; + } else { + boundary.type = { text: type }; + } + + if (descr === undefined || descr === null) { + boundary.descr = { text: '' }; + } else { + boundary.descr = { text: type }; + } + boundary.tags = tags; boundary.link = link; - boundary.type = type; + boundary.nodeType = nodeType; boundary.parentBoundary = currentBoundaryParse; + boundary.wrap = autoWrap(); parentBoundaryParse = currentBoundaryParse; currentBoundaryParse = alias; @@ -173,18 +348,18 @@ export const getParentBoundaryParse = function () { return parentBoundaryParse; }; -export const getPersonOrSystemArray = function (parentBoundary) { - if (parentBoundary === undefined || parentBoundary === null) return personOrSystemArray; +export const getC4ShapeArray = function (parentBoundary) { + if (parentBoundary === undefined || parentBoundary === null) return c4ShapeArray; else - return personOrSystemArray.filter((personOrSystem) => { + return c4ShapeArray.filter((personOrSystem) => { return personOrSystem.parentBoundary === parentBoundary; }); }; -export const getPersonOrSystem = function (alias) { - return personOrSystemArray.find((personOrSystem) => personOrSystem.alias === alias); +export const getC4Shape = function (alias) { + return c4ShapeArray.find((personOrSystem) => personOrSystem.alias === alias); }; -export const getPersonOrSystemKeys = function (parentBoundary) { - return Object.keys(getPersonOrSystemArray(parentBoundary)); +export const getC4ShapeKeys = function (parentBoundary) { + return Object.keys(getC4ShapeArray(parentBoundary)); }; export const getBoundarys = function (parentBoundary) { @@ -209,12 +384,12 @@ export const autoWrap = function () { }; export const clear = function () { - personOrSystemArray = []; + c4ShapeArray = []; boundarys = [ { alias: 'global', label: { text: 'global' }, - type: 'global', + type: { text: 'global' }, tags: null, link: null, parentBoundary: '', @@ -279,14 +454,18 @@ const getAccDescription = function () { export default { addPersonOrSystem, - addBoundary, + addPersonOrSystemBoundary, + addContainer, + addContainerBoundary, + addComponent, + addDeploymentNode, popBoundaryParseStack, addRel, autoWrap, setWrap, - getPersonOrSystemArray, - getPersonOrSystem, - getPersonOrSystemKeys, + getC4ShapeArray, + getC4Shape, + getC4ShapeKeys, getBoundarys, getCurrentBoundaryParse, getParentBoundaryParse, diff --git a/src/diagrams/c4/c4Renderer.js b/src/diagrams/c4/c4Renderer.js index dba7172c99..d71e70e2ef 100644 --- a/src/diagrams/c4/c4Renderer.js +++ b/src/diagrams/c4/c4Renderer.js @@ -36,6 +36,7 @@ class Bounds { this.nextData.stopx = undefined; this.nextData.starty = undefined; this.nextData.stopy = undefined; + this.nextData.cnt = 0; setConf(parser.yy.getConfig()); } @@ -56,17 +57,26 @@ class Bounds { } insert(c4Shape) { - let _startx = this.nextData.stopx + c4Shape.margin * 2; + this.nextData.cnt = this.nextData.cnt + 1; + let _startx = + this.nextData.startx === this.nextData.stopx + ? this.nextData.stopx + c4Shape.margin + : this.nextData.stopx + c4Shape.margin * 2; let _stopx = _startx + c4Shape.width; let _starty = this.nextData.starty + c4Shape.margin * 2; let _stopy = _starty + c4Shape.height; - if (_startx >= this.data.widthLimit || _stopx >= this.data.widthLimit) { - _startx = this.nextData.startx + c4Shape.margin * 2 + conf.nextLinePaddingX; + if ( + _startx >= this.data.widthLimit || + _stopx >= this.data.widthLimit || + this.nextData.cnt > conf.c4ShapeInRow + ) { + _startx = this.nextData.startx + c4Shape.margin + conf.nextLinePaddingX; _starty = this.nextData.stopy + c4Shape.margin * 2; this.nextData.stopx = _stopx = _startx + c4Shape.width; this.nextData.starty = this.nextData.stopy; this.nextData.stopy = _stopy = _starty + c4Shape.height; + this.nextData.cnt = 1; } c4Shape.x = _startx; @@ -84,6 +94,7 @@ class Bounds { } init() { + this.name = ''; this.data = { startx: undefined, stopx: undefined, @@ -91,6 +102,13 @@ class Bounds { stopy: undefined, widthLimit: undefined, }; + this.nextData = { + startx: undefined, + stopx: undefined, + starty: undefined, + stopy: undefined, + cnt: 0, + }; setConf(parser.yy.getConfig()); } @@ -100,19 +118,25 @@ class Bounds { } } -const personFont = (cnf) => { - return { - fontFamily: cnf.personFontFamily, - fontSize: cnf.personFontSize, - fontWeight: cnf.personFontWeight, - }; +export const setConf = function (cnf) { + assignWithDepth(conf, cnf); + + if (cnf.fontFamily) { + conf.personFontFamily = conf.systemFontFamily = conf.messageFontFamily = cnf.fontFamily; + } + if (cnf.fontSize) { + conf.personFontSize = conf.systemFontSize = conf.messageFontSize = cnf.fontSize; + } + if (cnf.fontWeight) { + conf.personFontWeight = conf.systemFontWeight = conf.messageFontWeight = cnf.fontWeight; + } }; -const systemFont = (cnf) => { +const c4ShapeFont = (cnf, typeC4Shape) => { return { - fontFamily: cnf.systemFontFamily, - fontSize: cnf.systemFontSize, - fontWeight: cnf.systemFontWeight, + fontFamily: cnf[typeC4Shape + 'FontFamily'], + fontSize: cnf[typeC4Shape + 'FontSize'], + fontWeight: cnf[typeC4Shape + 'FontWeight'], }; }; @@ -139,16 +163,18 @@ const messageFont = (cnf) => { * @param textConf * @param textLimitWidth */ -function setC4ShapeText(textType, c4Shape, c4ShapeTextWrap, textConf, textLimitWidth) { +function calcC4ShapeTextWH(textType, c4Shape, c4ShapeTextWrap, textConf, textLimitWidth) { if (!c4Shape[textType].width) { if (c4ShapeTextWrap) { c4Shape[textType].text = wrapLabel(c4Shape[textType].text, textLimitWidth, textConf); - c4Shape[textType].labelLines = c4Shape[textType].text.split(common.lineBreakRegex).length; + c4Shape[textType].textLines = c4Shape[textType].text.split(common.lineBreakRegex).length; + // c4Shape[textType].width = calculateTextWidth(c4Shape[textType].text, textConf); c4Shape[textType].width = textLimitWidth; - c4Shape[textType].height = c4Shape[textType].labelLines * (textConf.fontSize + 2); + // c4Shape[textType].height = c4Shape[textType].textLines * textConf.fontSize; + c4Shape[textType].height = calculateTextHeight(c4Shape[textType].text, textConf); } else { let lines = c4Shape[textType].text.split(common.lineBreakRegex); - c4Shape[textType].labelLines = lines.length; + c4Shape[textType].textLines = lines.length; let lineHeight = 0; c4Shape[textType].height = 0; c4Shape[textType].width = 0; @@ -160,7 +186,7 @@ function setC4ShapeText(textType, c4Shape, c4ShapeTextWrap, textConf, textLimitW lineHeight = calculateTextHeight(lines[i], textConf); c4Shape[textType].height = c4Shape[textType].height + lineHeight; } - // c4Shapes[textType].height = c4Shapes[textType].labelLines * textConf.fontSize; + // c4Shapes[textType].height = c4Shapes[textType].textLines * textConf.fontSize; } } } @@ -178,110 +204,102 @@ export const drawBoundary = function (diagram, boundary, bounds) { boundaryLabelConf.fontSize = boundaryLabelConf.fontSize + 2; boundaryLabelConf.fontWeight = 'bold'; let textLimitWidth = calculateTextWidth(boundary.label.text, boundaryLabelConf); - setC4ShapeText('label', boundary, boundaryTextWrap, boundaryLabelConf, textLimitWidth); + calcC4ShapeTextWH('label', boundary, boundaryTextWrap, boundaryLabelConf, textLimitWidth); svgDraw.drawBoundary(diagram, boundary, conf); }; -export const drawPersonOrSystemArray = function ( - currentBounds, - diagram, - personOrSystemArray, - personOrSystemKeys -) { - // Draw the personOrSystemArray - - // let prevWidth = currentBounds.data.stopx; - // let prevMarginX = conf.c4ShapeMargin; - // let prevMarginY = conf.c4ShapeMargin; - // let maxHeight = currentBounds.data.starty; - - for (let i = 0; i < personOrSystemKeys.length; i++) { - const personOrSystem = personOrSystemArray[personOrSystemKeys[i]]; - - let imageWidth = 0, - imageHeight = 0; - switch (personOrSystem.type) { +export const drawC4ShapeArray = function (currentBounds, diagram, c4ShapeArray, c4ShapeKeys) { + // Upper Y is relative point + let Y = 0; + // Draw the c4ShapeArray + for (let i = 0; i < c4ShapeKeys.length; i++) { + Y = 0; + const c4Shape = c4ShapeArray[c4ShapeKeys[i]]; + + // calc c4 shape type width and height + + let c4ShapeTypeConf = c4ShapeFont(conf, c4Shape.typeC4Shape.text); + c4ShapeTypeConf.fontSize = c4ShapeTypeConf.fontSize - 2; + c4Shape.typeC4Shape.width = calculateTextWidth( + '<<' + c4Shape.typeC4Shape.text + '>>', + c4ShapeTypeConf + ); + c4Shape.typeC4Shape.height = c4ShapeTypeConf.fontSize + 2; + c4Shape.typeC4Shape.Y = conf.c4ShapePadding; + Y = c4Shape.typeC4Shape.Y + c4Shape.typeC4Shape.height - 4; + + // set image width and height c4Shape.x + c4Shape.width / 2 - 24, c4Shape.y + 28 + // let imageWidth = 0, + // imageHeight = 0, + // imageY = 0; + // + c4Shape.image = { width: 0, height: 0, Y: 0 }; + switch (c4Shape.typeC4Shape.text) { case 'person': case 'external_person': - imageWidth = 48; - imageHeight = 48; + c4Shape.image.width = 48; + c4Shape.image.height = 48; + c4Shape.image.Y = Y; + Y = c4Shape.image.Y + c4Shape.image.height; break; } - - if (!personOrSystem.typeLabelWidth) { - let personOrSystemTypeConf = personFont(conf); - personOrSystemTypeConf.fontSize = personOrSystemTypeConf.fontSize - 2; - personOrSystem.typeLabelWidth = calculateTextWidth( - '<<' + personOrSystem.type + '>>', - personOrSystemTypeConf - ); - personOrSystem.typeLabelHeight = personOrSystemTypeConf.fontSize + 2; - - switch (personOrSystem.type) { - case 'system_db': - case 'external_system_db': - personOrSystem.typeLabelY = conf.c4ShapePadding; - break; - default: - personOrSystem.typeLabelY = conf.c4ShapePadding - 5; - break; - } + if (c4Shape.sprite) { + c4Shape.image.width = 48; + c4Shape.image.height = 48; + c4Shape.image.Y = Y; + Y = c4Shape.image.Y + c4Shape.image.height; } - let personOrSystemTextWrap = personOrSystem.wrap && conf.wrap; + // Y = conf.c4ShapePadding + c4Shape.image.height; + + let c4ShapeTextWrap = c4Shape.wrap && conf.wrap; let textLimitWidth = conf.width - conf.c4ShapePadding * 2; - let personOrSystemLabelConf = personFont(conf); - personOrSystemLabelConf.fontSize = personOrSystemLabelConf.fontSize + 2; - personOrSystemLabelConf.fontWeight = 'bold'; + let c4ShapeLabelConf = c4ShapeFont(conf, c4Shape.typeC4Shape.text); + c4ShapeLabelConf.fontSize = c4ShapeLabelConf.fontSize + 2; + c4ShapeLabelConf.fontWeight = 'bold'; + calcC4ShapeTextWH('label', c4Shape, c4ShapeTextWrap, c4ShapeLabelConf, textLimitWidth); + c4Shape['label'].Y = Y + 8; + Y = c4Shape['label'].Y + c4Shape['label'].height; + + if (c4Shape.type && c4Shape.type.text !== '') { + c4Shape.type.text = '[' + c4Shape.type.text + ']'; + let c4ShapeTypeConf = c4ShapeFont(conf, c4Shape.typeC4Shape.text); + calcC4ShapeTextWH('type', c4Shape, c4ShapeTextWrap, c4ShapeTypeConf, textLimitWidth); + c4Shape['type'].Y = Y + 5; + Y = c4Shape['type'].Y + c4Shape['type'].height; + } else if (c4Shape.techn && c4Shape.techn.text !== '') { + c4Shape.techn.text = '[' + c4Shape.techn.text + ']'; + let c4ShapeTechnConf = c4ShapeFont(conf, c4Shape.techn.text); + calcC4ShapeTextWH('techn', c4Shape, c4ShapeTextWrap, c4ShapeTechnConf, textLimitWidth); + c4Shape['techn'].Y = Y + 5; + Y = c4Shape['techn'].Y + c4Shape['techn'].height; + } - setC4ShapeText( - 'label', - personOrSystem, - personOrSystemTextWrap, - personOrSystemLabelConf, - textLimitWidth - ); - personOrSystem['label'].Y = - conf.c4ShapePadding + personOrSystem.typeLabelHeight + imageHeight + 10; - - let personOrSystemDescrConf = personFont(conf); - setC4ShapeText( - 'descr', - personOrSystem, - personOrSystemTextWrap, - personOrSystemDescrConf, - textLimitWidth - ); - personOrSystem['descr'].Y = - conf.c4ShapePadding + - personOrSystem.typeLabelHeight + - imageHeight + - 5 + - personOrSystem.label.height + - conf.personFontSize + - 2; - - // Add some rendering data to the object - let rectWidth = - Math.max(personOrSystem.label.width, personOrSystem.descr.width) + conf.c4ShapePadding * 2; - let rectHeight = - conf.c4ShapePadding + - personOrSystem.typeLabelHeight + - imageHeight + - personOrSystem.label.height + - conf.personFontSize + - 2 + - personOrSystem.descr.height; - - personOrSystem.width = Math.max(personOrSystem.width || conf.width, rectWidth, conf.width); - personOrSystem.height = Math.max(personOrSystem.height || conf.height, rectHeight, conf.height); - personOrSystem.margin = personOrSystem.margin || conf.c4ShapeMargin; - - currentBounds.insert(personOrSystem); - - const height = svgDraw.drawPersonOrSystem(diagram, personOrSystem, conf); + let rectHeight = Y; + let rectWidth = c4Shape.label.width; + + if (c4Shape.descr && c4Shape.descr.text !== '') { + let c4ShapeDescrConf = c4ShapeFont(conf, c4Shape.typeC4Shape.text); + calcC4ShapeTextWH('descr', c4Shape, c4ShapeTextWrap, c4ShapeDescrConf, textLimitWidth); + c4Shape['descr'].Y = Y + 20; + Y = c4Shape['descr'].Y + c4Shape['descr'].height; + + rectWidth = Math.max(c4Shape.label.width, c4Shape.descr.width); + rectHeight = Y - c4Shape['descr'].textLines * 5; + } + + rectWidth = rectWidth + conf.c4ShapePadding; + // let rectHeight = + + c4Shape.width = Math.max(c4Shape.width || conf.width, rectWidth, conf.width); + c4Shape.height = Math.max(c4Shape.height || conf.height, rectHeight, conf.height); + c4Shape.margin = c4Shape.margin || conf.c4ShapeMargin; + + currentBounds.insert(c4Shape); + + const height = svgDraw.drawC4Shape(diagram, c4Shape, conf); } currentBounds.bumpLastMargin(conf.c4ShapeMargin); @@ -391,20 +409,24 @@ let getIntersectPoints = function (fromNode, endNode) { }; export const drawRels = function (diagram, rels, getC4ShapeObj) { + let i = 0; for (let rel of rels) { + i = i + 1; let relTextWrap = rel.wrap && conf.wrap; let relConf = messageFont(conf); + let diagramType = parser.yy.getC4Type(); + if (diagramType === 'C4Dynamic') rel.label.text = i + ': ' + rel.label.text; let textLimitWidth = calculateTextWidth(rel.label.text, relConf); - setC4ShapeText('label', rel, relTextWrap, relConf, textLimitWidth); + calcC4ShapeTextWH('label', rel, relTextWrap, relConf, textLimitWidth); if (rel.techn && rel.techn.text !== '') { textLimitWidth = calculateTextWidth(rel.techn.text, relConf); - setC4ShapeText('techn', rel, relTextWrap, relConf, textLimitWidth); + calcC4ShapeTextWH('techn', rel, relTextWrap, relConf, textLimitWidth); } if (rel.descr && rel.descr.text !== '') { textLimitWidth = calculateTextWidth(rel.descr.text, relConf); - setC4ShapeText('descr', rel, relTextWrap, relConf, textLimitWidth); + calcC4ShapeTextWH('descr', rel, relTextWrap, relConf, textLimitWidth); } let fromNode = getC4ShapeObj(rel.from); @@ -416,20 +438,6 @@ export const drawRels = function (diagram, rels, getC4ShapeObj) { svgDraw.drawRels(diagram, rels, conf); }; -export const setConf = function (cnf) { - assignWithDepth(conf, cnf); - - if (cnf.fontFamily) { - conf.personFontFamily = conf.systemFontFamily = conf.messageFontFamily = cnf.fontFamily; - } - if (cnf.fontSize) { - conf.personFontSize = conf.systemFontSize = conf.messageFontSize = cnf.fontSize; - } - if (cnf.fontWeight) { - conf.personFontWeight = conf.systemFontWeight = conf.messageFontWeight = cnf.fontWeight; - } -}; - /** * @param diagram * @param parentBoundaryAlias @@ -439,16 +447,70 @@ export const setConf = function (cnf) { function drawInsideBoundary(diagram, parentBoundaryAlias, parentBounds, currentBoundarys) { let currentBounds = new Bounds(); // Calculate the width limit of the boundar. label/type 的长度, - currentBounds.data.widthLimit = Math.min( - conf.width * conf.c4ShapeInRow + conf.c4ShapeMargin * (conf.c4ShapeInRow + 1), - parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundarys.length) - ); + currentBounds.data.widthLimit = + parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundarys.length); + // Math.min( + // conf.width * conf.c4ShapeInRow + conf.c4ShapeMargin * conf.c4ShapeInRow * 2, + // parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundarys.length) + // ); for (let i = 0; i < currentBoundarys.length; i++) { let currentBoundary = currentBoundarys[i]; - if (i == 0) { + let Y = 0; + currentBoundary.image = { width: 0, height: 0, Y: 0 }; + if (currentBoundary.sprite) { + currentBoundary.image.width = 48; + currentBoundary.image.height = 48; + currentBoundary.image.Y = Y; + Y = currentBoundary.image.Y + currentBoundary.image.height; + } + + let currentBoundaryTextWrap = currentBoundary.wrap && conf.wrap; + + let currentBoundaryLabelConf = boundaryFont(conf); + currentBoundaryLabelConf.fontSize = currentBoundaryLabelConf.fontSize + 2; + currentBoundaryLabelConf.fontWeight = 'bold'; + calcC4ShapeTextWH( + 'label', + currentBoundary, + currentBoundaryTextWrap, + currentBoundaryLabelConf, + currentBounds.data.widthLimit + ); + currentBoundary['label'].Y = Y + 8; + Y = currentBoundary['label'].Y + currentBoundary['label'].height; + + if (currentBoundary.type && currentBoundary.type.text !== '') { + currentBoundary.type.text = '[' + currentBoundary.type.text + ']'; + let currentBoundaryTypeConf = boundaryFont(conf); + calcC4ShapeTextWH( + 'type', + currentBoundary, + currentBoundaryTextWrap, + currentBoundaryTypeConf, + currentBounds.data.widthLimit + ); + currentBoundary['type'].Y = Y + 5; + Y = currentBoundary['type'].Y + currentBoundary['type'].height; + } + + if (currentBoundary.descr && currentBoundary.descr.text !== '') { + let currentBoundaryDescrConf = boundaryFont(conf); + currentBoundaryDescrConf.fontSize = currentBoundaryDescrConf.fontSize - 2; + calcC4ShapeTextWH( + 'descr', + currentBoundary, + currentBoundaryTextWrap, + currentBoundaryDescrConf, + currentBounds.data.widthLimit + ); + currentBoundary['descr'].Y = Y + 20; + Y = currentBoundary['descr'].Y + currentBoundary['descr'].height; + } + + if (i == 0 || i % conf.c4BoundaryInRow === 0) { // Calculate the drawing start point of the currentBoundarys. let _x = parentBounds.data.startx + conf.diagramMarginX; - let _y = parentBounds.data.stopy + conf.diagramMarginY; + let _y = parentBounds.data.stopy + conf.diagramMarginY + Y; currentBounds.setData(_x, _x, _y, _y); } else { @@ -462,11 +524,11 @@ function drawInsideBoundary(diagram, parentBoundaryAlias, parentBounds, currentB currentBounds.setData(_x, _x, _y, _y); } currentBounds.name = currentBoundary.alias; - let currentPersonOrSystemArray = parser.yy.getPersonOrSystemArray(currentBoundary.alias); - let currentPersonOrSystemKeys = parser.yy.getPersonOrSystemKeys(currentBoundary.alias); + let currentPersonOrSystemArray = parser.yy.getC4ShapeArray(currentBoundary.alias); + let currentPersonOrSystemKeys = parser.yy.getC4ShapeKeys(currentBoundary.alias); if (currentPersonOrSystemKeys.length > 0) { - drawPersonOrSystemArray( + drawC4ShapeArray( currentBounds, diagram, currentPersonOrSystemArray, @@ -545,11 +607,11 @@ export const draw = function (text, id) { const title = parser.yy.getTitle(); const c4type = parser.yy.getC4Type(); let currentBoundarys = parser.yy.getBoundarys(''); - switch (c4type) { - case 'C4Context': - drawInsideBoundary(diagram, '', screenBounds, currentBoundarys); - break; - } + // switch (c4type) { + // case 'C4Context': + drawInsideBoundary(diagram, '', screenBounds, currentBoundarys); + // break; + // } // The arrow head definition is attached to the svg once svgDraw.insertArrowHead(diagram); @@ -557,7 +619,7 @@ export const draw = function (text, id) { svgDraw.insertArrowCrossHead(diagram); svgDraw.insertArrowFilledHead(diagram); - drawRels(diagram, parser.yy.getRels(), parser.yy.getPersonOrSystem); + drawRels(diagram, parser.yy.getRels(), parser.yy.getC4Shape); screenBounds.data.stopx = globalBoundaryMaxX; screenBounds.data.stopy = globalBoundaryMaxY; @@ -578,7 +640,7 @@ export const draw = function (text, id) { .append('text') .text(title) .attr('x', (box.stopx - box.startx) / 2 - 4 * conf.diagramMarginX) - .attr('y', -25); + .attr('y', box.starty + conf.diagramMarginY); } configureSvgSize(diagram, height, width, conf.useMaxWidth); @@ -601,7 +663,7 @@ export const draw = function (text, id) { }; export default { - drawPersonOrSystemArray, + drawPersonOrSystemArray: drawC4ShapeArray, drawBoundary, setConf, draw, diff --git a/src/diagrams/c4/parser/c4Diagram.jison b/src/diagrams/c4/parser/c4Diagram.jison index dd9672226b..9b57e3cf11 100644 --- a/src/diagrams/c4/parser/c4Diagram.jison +++ b/src/diagrams/c4/parser/c4Diagram.jison @@ -48,7 +48,6 @@ %x index /* Deployment diagram */ -%x deployment_node %x node %x node_l %x node_r @@ -56,10 +55,11 @@ /* Relationship Types */ %x rel %x rel_bi -%x rel_up -%x rel_down -%x rel_left -%x rel_right +%x rel_u +%x rel_d +%x rel_l +%x rel_r +%x rel_b %x attribute %x string @@ -105,18 +105,45 @@ "Enterprise_Boundary" { this.begin("enterprise_boundary"); console.log('begin enterprise_boundary'); return 'ENTERPRISE_BOUNDARY';} "System_Boundary" { this.begin("system_boundary"); console.log('begin system_boundary'); return 'SYSTEM_BOUNDARY';} -"Rel" { this.begin("rel"); console.log('begin rel'); return 'REL';} -"BiRel" { this.begin("birel"); console.log('begin birel'); return 'BIREL';} -"Rel_U|Rel_Up" { this.begin("rel_u"); console.log('begin rel_u'); return 'REL_U';} -"Rel_D|Rel_Down" { this.begin("rel_d"); console.log('begin rel_d'); return 'REL_D';} -"Rel_L|Rel_Left" { this.begin("rel_l"); console.log('begin rel_l'); return 'REL_L';} -"Rel_R|Rel_Right" { this.begin("rel_r"); console.log('begin rel_r'); return 'REL_R';} +"ContainerQueue_Ext" { this.begin("container_ext_queue"); console.log('begin container_ext_queue'); return 'CONTAINER_EXT_QUEUE';} +"ContainerDb_Ext" { this.begin("container_ext_db"); console.log('begin container_ext_db'); return 'CONTAINER_EXT_DB';} +"Container_Ext" { this.begin("container_ext"); console.log('begin container_ext'); return 'CONTAINER_EXT';} +"ContainerQueue" { this.begin("container_queue"); console.log('begin container_queue'); return 'CONTAINER_QUEUE';} +"ContainerDb" { this.begin("container_db"); console.log('begin container_db'); return 'CONTAINER_DB';} +"Container" { this.begin("container"); console.log('begin container'); return 'CONTAINER';} +"Container_Boundary" { this.begin("container_boundary"); console.log('begin container_boundary'); return 'CONTAINER_BOUNDARY';} -<> return "EOF_IN_STRUCT"; -[(][ ]*[,] { console.log('begin attribute with ATTRIBUTE_EMPTY'); this.begin("attribute"); return "ATTRIBUTE_EMPTY";} -[(] { console.log('begin attribute'); this.begin("attribute"); } -[)] { console.log('STOP attribute'); this.popState();console.log('STOP diagram'); this.popState();} +"ComponentQueue_Ext" { this.begin("component_ext_queue"); console.log('begin component_ext_queue'); return 'COMPONENT_EXT_QUEUE';} +"ComponentDb_Ext" { this.begin("component_ext_db"); console.log('begin component_ext_db'); return 'COMPONENT_EXT_DB';} +"Component_Ext" { this.begin("component_ext"); console.log('begin component_ext'); return 'COMPONENT_EXT';} +"ComponentQueue" { this.begin("component_queue"); console.log('begin component_queue'); return 'COMPONENT_QUEUE';} +"ComponentDb" { this.begin("component_db"); console.log('begin component_db'); return 'COMPONENT_DB';} +"Component" { this.begin("component"); console.log('begin component'); return 'COMPONENT';} + +"Deployment_Node" { this.begin("node"); console.log('begin node'); return 'NODE';} +"Node" { this.begin("node"); console.log('begin node'); return 'NODE';} +"Node_L" { this.begin("node_l"); console.log('begin node_l'); return 'NODE_L';} +"Node_R" { this.begin("node_r"); console.log('begin node_r'); return 'NODE_R';} + + +"Rel" { this.begin("rel"); console.log('begin rel'); return 'REL';} +"BiRel" { this.begin("birel"); console.log('begin birel'); return 'BIREL';} +"Rel_Up" { this.begin("rel_u"); console.log('begin rel_u'); return 'REL_U';} +"Rel_U" { this.begin("rel_u"); console.log('begin rel_u'); return 'REL_U';} +"Rel_Down" { this.begin("rel_d"); console.log('begin rel_d'); return 'REL_D';} +"Rel_D" { this.begin("rel_d"); console.log('begin rel_d'); return 'REL_D';} +"Rel_Left" { this.begin("rel_l"); console.log('begin rel_l'); return 'REL_L';} +"Rel_L" { this.begin("rel_l"); console.log('begin rel_l'); return 'REL_L';} +"Rel_Right" { this.begin("rel_r"); console.log('begin rel_r'); return 'REL_R';} +"Rel_R" { this.begin("rel_r"); console.log('begin rel_r'); return 'REL_R';} +"Rel_Back" { this.begin("rel_b"); console.log('begin rel_b'); return 'REL_B';} +"RelIndex" { this.begin("rel_index"); console.log('begin rel_index'); return 'REL_INDEX';} + +<> return "EOF_IN_STRUCT"; +[(][ ]*[,] { console.log('begin attribute with ATTRIBUTE_EMPTY'); this.begin("attribute"); return "ATTRIBUTE_EMPTY";} +[(] { console.log('begin attribute'); this.begin("attribute"); } +[)] { console.log('STOP attribute'); this.popState();console.log('STOP diagram'); this.popState();} ",," { console.log(',,'); return 'ATTRIBUTE_EMPTY';} "," { console.log(','); } @@ -221,9 +248,13 @@ boundaryStartStatement ; boundaryStart - : ENTERPRISE_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addBoundary(...$2); $$=$2;} - | SYSTEM_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addBoundary(...$2); $$=$2;} - | BOUNDARY attributes {console.log($1,JSON.stringify($2)); yy.addBoundary(...$2); $$=$2;} + : ENTERPRISE_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addPersonOrSystemBoundary(...$2); $$=$2;} + | SYSTEM_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'ENTERPRISE'); yy.addPersonOrSystemBoundary(...$2); $$=$2;} + | BOUNDARY attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystemBoundary(...$2); $$=$2;} + | CONTAINER_BOUNDARY attributes {console.log($1,JSON.stringify($2)); $2.splice(2, 0, 'CONTAINER'); yy.addContainerBoundary(...$2); $$=$2;} + | NODE attributes {console.log($1,JSON.stringify($2)); yy.addDeploymentNode('node', ...$2); $$=$2;} + | NODE_L attributes {console.log($1,JSON.stringify($2)); yy.addDeploymentNode('nodeL', ...$2); $$=$2;} + | NODE_R attributes {console.log($1,JSON.stringify($2)); yy.addDeploymentNode('nodeR', ...$2); $$=$2;} ; boundaryStopStatement @@ -245,6 +276,18 @@ diagramStatement | SYSTEM_EXT attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system', ...$2); $$=$2;} | SYSTEM_EXT_DB attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system_db', ...$2); $$=$2;} | SYSTEM_EXT_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;} + | CONTAINER attributes {console.log($1,JSON.stringify($2)); yy.addContainer('container', ...$2); $$=$2;} + | CONTAINER_DB attributes {console.log($1,JSON.stringify($2)); yy.addContainer('container_db', ...$2); $$=$2;} + | CONTAINER_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addContainer('container_queue', ...$2); $$=$2;} + | CONTAINER_EXT attributes {console.log($1,JSON.stringify($2)); yy.addContainer('external_container', ...$2); $$=$2;} + | CONTAINER_EXT_DB attributes {console.log($1,JSON.stringify($2)); yy.addContainer('external_container_db', ...$2); $$=$2;} + | CONTAINER_EXT_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addContainer('external_container_queue', ...$2); $$=$2;} + | COMPONENT attributes {console.log($1,JSON.stringify($2)); yy.addComponent('component', ...$2); $$=$2;} + | COMPONENT_DB attributes {console.log($1,JSON.stringify($2)); yy.addComponent('component_db', ...$2); $$=$2;} + | COMPONENT_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addComponent('component_queue', ...$2); $$=$2;} + | COMPONENT_EXT attributes {console.log($1,JSON.stringify($2)); yy.addComponent('external_component', ...$2); $$=$2;} + | COMPONENT_EXT_DB attributes {console.log($1,JSON.stringify($2)); yy.addComponent('external_component_db', ...$2); $$=$2;} + | COMPONENT_EXT_QUEUE attributes {console.log($1,JSON.stringify($2)); yy.addComponent('external_component_queue', ...$2); $$=$2;} | boundaryStatement | REL attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel', ...$2); $$=$2;} | BIREL attributes {console.log($1,JSON.stringify($2)); yy.addRel('birel', ...$2); $$=$2;} @@ -252,6 +295,8 @@ diagramStatement | REL_D attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_d', ...$2); $$=$2;} | REL_L attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_l', ...$2); $$=$2;} | REL_R attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_r', ...$2); $$=$2;} + | REL_B attributes {console.log($1,JSON.stringify($2)); yy.addRel('rel_b', ...$2); $$=$2;} + | REL_INDEX attributes {console.log($1,JSON.stringify($2)); $2.splice(0, 1); yy.addRel('rel', ...$2); $$=$2;} ; attributes diff --git a/src/diagrams/c4/svgDraw.js b/src/diagrams/c4/svgDraw.js index 7081d4e5bc..fb3a276dba 100644 --- a/src/diagrams/c4/svgDraw.js +++ b/src/diagrams/c4/svgDraw.js @@ -227,8 +227,9 @@ export const drawRels = (elem, rels, conf) => { line.attr('stroke-width', '1'); line.attr('stroke', '#444444'); line.style('fill', 'none'); - line.attr('marker-end', 'url(' + url + '#arrowhead)'); - if (rel.type === 'birel') line.attr('marker-start', 'url(' + url + '#arrowend)'); + if (rel.type !== 'rel_b') line.attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type === 'birel' || rel.type === 'rel_b') + line.attr('marker-start', 'url(' + url + '#arrowend)'); i = -1; } else { let line = relsElem.append('path'); @@ -250,9 +251,10 @@ export const drawRels = (elem, rels, conf) => { .replaceAll('controly', rel.startPoint.y + (rel.endPoint.y - rel.startPoint.y) / 2) .replaceAll('stopx', rel.endPoint.x) .replaceAll('stopy', rel.endPoint.y) - ) - .attr('marker-end', 'url(' + url + '#arrowhead)'); - if (rel.type === 'birel') line.attr('marker-start', 'url(' + url + '#arrowend)'); + ); + if (rel.type !== 'rel_b') line.attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type === 'birel' || rel.type === 'rel_b') + line.attr('marker-start', 'url(' + url + '#arrowend)'); } let messageConf = conf.messageFont(); @@ -297,6 +299,8 @@ export const drawRels = (elem, rels, conf) => { const drawBoundary = function (elem, boundary, conf) { const boundaryElem = elem.append('g'); + let attrsValue = { 'stroke-width': 1.0, 'stroke-dasharray': '7.0,7.0' }; + if (boundary.nodeType) attrsValue = { 'stroke-width': 1.0 }; let rectData = { x: boundary.x, y: boundary.y, @@ -306,11 +310,12 @@ const drawBoundary = function (elem, boundary, conf) { height: boundary.height, rx: 2.5, ry: 2.5, - attrs: { 'stroke-width': 1.0, 'stroke-dasharray': '7.0,7.0' }, + attrs: attrsValue, }; drawRect(boundaryElem, rectData); + // draw lable let boundaryConf = conf.boundaryFont(); boundaryConf.fontWeight = 'bold'; boundaryConf.fontSize = boundaryConf.fontSize + 2; @@ -318,33 +323,51 @@ const drawBoundary = function (elem, boundary, conf) { boundary.label.text, boundaryElem, boundary.x, - boundary.y + boundary.label.y, + boundary.y + boundary.label.Y, boundary.width, boundary.height, { fill: '#444444' }, boundaryConf ); - boundaryConf = conf.boundaryFont(); - boundaryConf.fontSize = boundaryConf.fontSize - 2; - _drawTextCandidateFunc(conf)( - '[' + boundary.type + ']', - boundaryElem, - boundary.x, - boundary.y + boundary.label.y + boundaryConf.fontSize + 8, - boundary.width, - boundary.height, - { fill: '#444444' }, - boundaryConf - ); + // draw type + if (boundary.type && boundary.type.text !== '') { + boundaryConf = conf.boundaryFont(); + _drawTextCandidateFunc(conf)( + boundary.type.text, + boundaryElem, + boundary.x, + boundary.y + boundary.type.Y, + boundary.width, + boundary.height, + { fill: '#444444' }, + boundaryConf + ); + } + + // draw descr + if (boundary.descr && boundary.descr.text !== '') { + boundaryConf = conf.boundaryFont(); + boundaryConf.fontSize = boundaryConf.fontSize - 2; + _drawTextCandidateFunc(conf)( + boundary.descr.text, + boundaryElem, + boundary.x, + boundary.y + boundary.descr.Y, + boundary.width, + boundary.height, + { fill: '#444444' }, + boundaryConf + ); + } }; -export const drawPersonOrSystem = function (elem, personOrSystem, conf) { - let fillColor = conf[personOrSystem.type + '_bg_color']; - let strokeColor = conf[personOrSystem.type + '_border_color']; +export const drawC4Shape = function (elem, c4Shape, conf) { + let fillColor = conf[c4Shape.typeC4Shape.text + '_bg_color']; + let strokeColor = conf[c4Shape.typeC4Shape.text + '_border_color']; let personImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII='; - switch (personOrSystem.type) { + switch (c4Shape.typeC4Shape.text) { case 'person': personImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII='; @@ -355,29 +378,38 @@ export const drawPersonOrSystem = function (elem, personOrSystem, conf) { break; } - const personOrSystemElem = elem.append('g'); - personOrSystemElem.attr('class', 'person-man'); + const c4ShapeElem = elem.append('g'); + c4ShapeElem.attr('class', 'person-man'); // + // draw rect of c4Shape const rect = getNoteRect(); - switch (personOrSystem.type) { + switch (c4Shape.typeC4Shape.text) { case 'person': case 'external_person': case 'system': case 'external_system': - rect.x = personOrSystem.x; - rect.y = personOrSystem.y; + case 'container': + case 'external_container': + case 'component': + case 'external_component': + rect.x = c4Shape.x; + rect.y = c4Shape.y; rect.fill = fillColor; - rect.width = personOrSystem.width; - rect.height = personOrSystem.height; + rect.width = c4Shape.width; + rect.height = c4Shape.height; rect.style = 'stroke:' + strokeColor + ';stroke-width:0.5;'; rect.rx = 2.5; rect.ry = 2.5; - drawRect(personOrSystemElem, rect); + drawRect(c4ShapeElem, rect); break; case 'system_db': case 'external_system_db': - personOrSystemElem + case 'container_db': + case 'external_container_db': + case 'component_db': + case 'external_component_db': + c4ShapeElem .append('path') .attr('fill', fillColor) .attr('stroke-width', '0.5') @@ -385,12 +417,12 @@ export const drawPersonOrSystem = function (elem, personOrSystem, conf) { .attr( 'd', 'Mstartx,startyc0,-10 half,-10 half,-10c0,0 half,0 half,10l0,heightc0,10 -half,10 -half,10c0,0 -half,0 -half,-10l0,-height' - .replaceAll('startx', personOrSystem.x) - .replaceAll('starty', personOrSystem.y) - .replaceAll('half', personOrSystem.width / 2) - .replaceAll('height', personOrSystem.height) + .replaceAll('startx', c4Shape.x) + .replaceAll('starty', c4Shape.y) + .replaceAll('half', c4Shape.width / 2) + .replaceAll('height', c4Shape.height) ); - personOrSystemElem + c4ShapeElem .append('path') .attr('fill', 'none') .attr('stroke-width', '0.5') @@ -398,14 +430,18 @@ export const drawPersonOrSystem = function (elem, personOrSystem, conf) { .attr( 'd', 'Mstartx,startyc0,10 half,10 half,10c0,0 half,0 half,-10' - .replaceAll('startx', personOrSystem.x) - .replaceAll('starty', personOrSystem.y) - .replaceAll('half', personOrSystem.width / 2) + .replaceAll('startx', c4Shape.x) + .replaceAll('starty', c4Shape.y) + .replaceAll('half', c4Shape.width / 2) ); break; case 'system_queue': case 'external_system_queue': - personOrSystemElem + case 'container_queue': + case 'external_container_queue': + case 'component_queue': + case 'external_component_queue': + c4ShapeElem .append('path') .attr('fill', fillColor) .attr('stroke-width', '0.5') @@ -413,12 +449,12 @@ export const drawPersonOrSystem = function (elem, personOrSystem, conf) { .attr( 'd', 'Mstartx,startylwidth,0c5,0 5,half 5,halfc0,0 0,half -5,halfl-width,0c-5,0 -5,-half -5,-halfc0,0 0,-half 5,-half' - .replaceAll('startx', personOrSystem.x) - .replaceAll('starty', personOrSystem.y) - .replaceAll('width', personOrSystem.width) - .replaceAll('half', personOrSystem.height / 2) + .replaceAll('startx', c4Shape.x) + .replaceAll('starty', c4Shape.y) + .replaceAll('width', c4Shape.width) + .replaceAll('half', c4Shape.height / 2) ); - personOrSystemElem + c4ShapeElem .append('path') .attr('fill', 'none') .attr('stroke-width', '0.5') @@ -426,66 +462,100 @@ export const drawPersonOrSystem = function (elem, personOrSystem, conf) { .attr( 'd', 'Mstartx,startyc-5,0 -5,half -5,halfc0,half 5,half 5,half' - .replaceAll('startx', personOrSystem.x + personOrSystem.width) - .replaceAll('starty', personOrSystem.y) - .replaceAll('half', personOrSystem.height / 2) + .replaceAll('startx', c4Shape.x + c4Shape.width) + .replaceAll('starty', c4Shape.y) + .replaceAll('half', c4Shape.height / 2) ); break; } - personOrSystemElem + // draw type of c4Shape + let c4ShapeFontConf = getC4ShapeFont(conf, c4Shape.typeC4Shape.text); + c4ShapeElem .append('text') .attr('fill', '#FFFFFF') - .attr('font-family', conf.personFontFamily) - .attr('font-size', conf.personFontSize - 2) + .attr('font-family', c4ShapeFontConf.fontFamily) + .attr('font-size', c4ShapeFontConf.fontSize - 2) .attr('font-style', 'italic') .attr('lengthAdjust', 'spacing') - .attr('textLength', personOrSystem.typeLabelWidth) - .attr('x', personOrSystem.x + personOrSystem.width / 2 - personOrSystem.typeLabelWidth / 2) - .attr('y', personOrSystem.y + personOrSystem.typeLabelY) - .text('<<' + personOrSystem.type + '>>'); + .attr('textLength', c4Shape.typeC4Shape.width) + .attr('x', c4Shape.x + c4Shape.width / 2 - c4Shape.typeC4Shape.width / 2) + .attr('y', c4Shape.y + c4Shape.typeC4Shape.Y) + .text('<<' + c4Shape.typeC4Shape.text + '>>'); - switch (personOrSystem.type) { + // draw image/sprite + switch (c4Shape.typeC4Shape.text) { case 'person': case 'external_person': drawImage( - personOrSystemElem, + c4ShapeElem, 48, 48, - personOrSystem.x + personOrSystem.width / 2 - 24, - personOrSystem.y + 24, + c4Shape.x + c4Shape.width / 2 - 24, + c4Shape.y + c4Shape.image.Y, personImg ); break; } - let personOrSystemConf = conf.personFont(); - personOrSystemConf.fontWeight = 'bold'; - personOrSystemConf.fontSize = personOrSystemConf.fontSize + 2; + // draw label + let textFontConf = conf[c4Shape.typeC4Shape.text + 'Font'](); + textFontConf.fontWeight = 'bold'; + textFontConf.fontSize = textFontConf.fontSize + 2; _drawTextCandidateFunc(conf)( - personOrSystem.label.text, - personOrSystemElem, - personOrSystem.x, - personOrSystem.y + personOrSystem.label.Y, - personOrSystem.width, - personOrSystem.height, + c4Shape.label.text, + c4ShapeElem, + c4Shape.x, + c4Shape.y + c4Shape.label.Y, + c4Shape.width, + c4Shape.height, { fill: '#FFFFFF' }, - personOrSystemConf + textFontConf ); - personOrSystemConf = conf.personFont(); - _drawTextCandidateFunc(conf)( - personOrSystem.descr.text, - personOrSystemElem, - personOrSystem.x, - personOrSystem.y + personOrSystem.descr.Y, - personOrSystem.width, - personOrSystem.height, - { fill: '#FFFFFF' }, - personOrSystemConf - ); + // draw techn/type + textFontConf = conf[c4Shape.typeC4Shape.text + 'Font'](); + + if (c4Shape.thchn && c4Shape.thchn.text !== '') { + _drawTextCandidateFunc(conf)( + c4Shape.thchn.text, + c4ShapeElem, + c4Shape.x, + c4Shape.y + c4Shape.thchn.Y, + c4Shape.width, + c4Shape.height, + { fill: '#FFFFFF', 'font-style': 'italic' }, + textFontConf + ); + } else if (c4Shape.type && c4Shape.type.text !== '') { + _drawTextCandidateFunc(conf)( + c4Shape.type.text, + c4ShapeElem, + c4Shape.x, + c4Shape.y + c4Shape.type.Y, + c4Shape.width, + c4Shape.height, + { fill: '#FFFFFF', 'font-style': 'italic' }, + textFontConf + ); + } + + // draw descr + if (c4Shape.descr && c4Shape.descr.text !== '') { + textFontConf = conf.personFont(); + _drawTextCandidateFunc(conf)( + c4Shape.descr.text, + c4ShapeElem, + c4Shape.x, + c4Shape.y + c4Shape.descr.Y, + c4Shape.width, + c4Shape.height, + { fill: '#FFFFFF' }, + textFontConf + ); + } - return personOrSystem.height; + return c4Shape.height; }; export const insertDatabaseIcon = function (elem) { @@ -672,6 +742,14 @@ export const getNoteRect = function () { }; }; +const getC4ShapeFont = (cnf, typeC4Shape) => { + return { + fontFamily: cnf[typeC4Shape + 'FontFamily'], + fontSize: cnf[typeC4Shape + 'FontSize'], + fontWeight: cnf[typeC4Shape + 'FontWeight'], + }; +}; + const _drawTextCandidateFunc = (function () { /** * @param {any} content @@ -713,16 +791,17 @@ const _drawTextCandidateFunc = (function () { .attr('x', x + width / 2) .attr('y', y) .style('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') .style('font-size', fontSize) .style('font-weight', fontWeight) .style('font-family', fontFamily); text .append('tspan') - .attr('x', x + width / 2) + // .attr('x', x + width / 2) .attr('dy', dy) - .text(lines[i]); - - text.attr('y', y).attr('dominant-baseline', 'central').attr('alignment-baseline', 'central'); + .text(lines[i]) + // .attr('y', y + height / 2) + .attr('alignment-baseline', 'mathematical'); _setTextAttrs(text, textAttrs); } @@ -787,14 +866,14 @@ export default { drawText, drawLabel, drawBoundary, - drawPersonOrSystem, + drawC4Shape, drawRels, drawImage, drawEmbeddedImage, insertArrowHead, insertArrowEnd, insertArrowFilledHead, - insertSequenceNumber: insertDynamicNumber, + insertDynamicNumber, insertArrowCrossHead, insertDatabaseIcon, insertComputerIcon,