From a1ef3f0f4a0ab93df90edd70204767ec0f83c7f1 Mon Sep 17 00:00:00 2001 From: pinghe Date: Sun, 15 May 2022 13:21:16 +0800 Subject: [PATCH] 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 bdbd2f180..1db4bf416 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 8e1e7a6bb..819caebe3 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 8b5a16d56..22e50f2cf 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 000000000..d29d11c85 --- /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 000000000..e3e1636d2 --- /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 000000000..dd9672226 --- /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 000000000..c24412b3c --- /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 000000000..870d30eda --- /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 d985c3944..78345b7da 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 9d39fe4c8..f5608f0dc 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 000000000..0c3fca3a9 --- /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 7daea0e63..a20f81a7d 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 7474da898..361e4495b 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 71b9d446c..12514d5c1 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 81a07b9b8..a91a9a249 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 b0ec57458..f90da832e 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 af228513a..0d5ed2ffc 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 e8a24bb6a..a891c68d7 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'; }