diff --git a/.changeset/c4-link-sanitization.md b/.changeset/c4-link-sanitization.md new file mode 100644 index 00000000000..6e271418e89 --- /dev/null +++ b/.changeset/c4-link-sanitization.md @@ -0,0 +1,11 @@ +--- +'mermaid': patch +--- + +fix(c4): sanitize a C4 `$link` before it becomes an `xlink:href` + +A `$link` was stored and rendered exactly as written, so one carrying a +`javascript:` scheme reached the anchor. It now follows a flowchart +`click ... href`: it is sanitized, which also normalises the URL +(`$link="https://example.com"` renders as `https://example.com/`), unless +`securityLevel` is `loose`, where it is kept as written. diff --git a/.changeset/c4-unified-edges.md b/.changeset/c4-unified-edges.md new file mode 100644 index 00000000000..0b7d50e0753 --- /dev/null +++ b/.changeset/c4-unified-edges.md @@ -0,0 +1,7 @@ +--- +'mermaid': minor +--- + +feat(c4): render C4 relationships through the unified edge rendering + +C4 relationships are drawn by mermaid's shared edge renderer instead of bespoke SVG: a dashed line with an arrowhead (both ends for `BiRel`) and a label carrying the relationship name, its optional `[technology]` and its description. `UpdateRelStyle`'s `$offsetX`/`$offsetY` still nudge the label and `$textColor`/`$lineColor` still recolour it. Element geometry is unchanged - the line runs between the two shapes' boundary intersection points, so it stays in the gap between boxes. diff --git a/.changeset/c4-unified-layout.md b/.changeset/c4-unified-layout.md new file mode 100644 index 00000000000..ce62ecb85f7 --- /dev/null +++ b/.changeset/c4-unified-layout.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat(c4): lay out C4 diagrams with the unified rendering pipeline diff --git a/e2e/diagrams/c4/nightly-check/link-attribute-not-rendered.mmd b/e2e/diagrams/c4/nightly-check/link-attribute-not-rendered.mmd index 54c453b525d..73a6a9a5b9a 100644 --- a/e2e/diagrams/c4/nightly-check/link-attribute-not-rendered.mmd +++ b/e2e/diagrams/c4/nightly-check/link-attribute-not-rendered.mmd @@ -1,8 +1,9 @@ --- description: >- - A Person carrying a $link="https://example.com" attribute — the current - renderer accepts it without erroring but does not render it as a - clickable anchor. + A Person carrying a $link="https://example.com" attribute. The unified + renderer wraps the node in an anchor (asserted in + e2e/rendering/c4/pr-check/c4-link.spec.js); the screenshot only records + how it looks. --- C4Context title Link attribute (not shown by current renderer) diff --git a/e2e/rendering/c4/nightly-check/c4-layout-config.spec.js b/e2e/rendering/c4/nightly-check/c4-layout-config.spec.js new file mode 100644 index 00000000000..c6dd1e2c091 --- /dev/null +++ b/e2e/rendering/c4/nightly-check/c4-layout-config.spec.js @@ -0,0 +1,37 @@ +import { expect, test } from '@playwright/test'; +import { diagramSvg, renderGraph } from '../../../helpers/util.ts'; + +test.describe('C4 UpdateLayoutConfig', () => { + // `UpdateLayoutConfig` is a no-op once the layout is dagre's: the pipeline never reads + // `$c4ShapeInRow`/`$c4BoundaryInRow`, so the same source lays out identically whatever + // they say. Asserted as an absence rather than left as two snapshots that happened to + // match, so that implementing the option - or restoring a grid - fails here loudly. + // The 2 and 4 shapes-per-row renders are fixtures under e2e/diagrams/c4/visual-check/. + test('does not change the layout (no longer honoured)', async ({ page }, testInfo) => { + const diagram = (shapesInRow) => `C4Context +title UpdateLayoutConfig ($c4ShapeInRow=${shapesInRow}) +System(a, "A") +System(b, "B") +System(c, "C") +System(d, "D") +UpdateLayoutConfig($c4ShapeInRow="${shapesInRow}", $c4BoundaryInRow="1") + `; + + const positions = () => + diagramSvg(page) + .locator('.node') + .evaluateAll((nodes) => + nodes.map((node) => { + const { x, y } = node.getBoundingClientRect(); + return `${Math.round(x)},${Math.round(y)}`; + }) + ); + + await renderGraph(page, testInfo, diagram(2), { screenshot: false }); + const atTwo = await positions(); + expect(atTwo.length).toBe(4); + + await renderGraph(page, testInfo, diagram(4), { screenshot: false }); + expect(await positions()).toEqual(atTwo); + }); +}); diff --git a/e2e/rendering/c4/pr-check/c4-link.spec.js b/e2e/rendering/c4/pr-check/c4-link.spec.js new file mode 100644 index 00000000000..3c1d9f0059e --- /dev/null +++ b/e2e/rendering/c4/pr-check/c4-link.spec.js @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test'; +import { diagramSvg, renderGraph } from '../../../helpers/util.ts'; + +// The same sources are shown as fixtures under e2e/diagrams/c4/, so these tests render +// without a screenshot and only assert on the anchor a fixture cannot check. +test.describe('C4 $link', () => { + test('should render $link on an element as a hyperlink', async ({ page }, testInfo) => { + await renderGraph( + page, + testInfo, + `C4Context +title Link attribute +Person(p, "Person", "desc", $link="https://example.com") +System(s, "System", "desc") +Rel(p, s, "Uses") + `, + { screenshot: false } + ); + + // The unified renderer wraps a linked node in an `svg:a`; it sets only + // `xlink:href`, and no `target` unless one was asked for. The URL is normalised + // by the same sanitizer a flowchart `click ... href` goes through. + const link = diagramSvg(page).locator('g.nodes a'); + await expect(link).toHaveAttribute('xlink:href', 'https://example.com/'); + await expect(link).not.toHaveAttribute('target'); + }); + + test('should not carry a javascript: $link into the href', async ({ page }, testInfo) => { + await renderGraph( + page, + testInfo, + `C4Context +title Link attribute with an unsafe scheme +Person(p, "Person", "desc", $link="javascript:alert(1)") +System(s, "System", "desc") +Rel(p, s, "Uses") + `, + { screenshot: false } + ); + + const link = diagramSvg(page).locator('g.nodes a'); + await expect(link).not.toHaveAttribute('xlink:href', /^javascript:/); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/c4Db.ts b/packages/mermaid/src/diagrams/c4/c4Db.ts index 5a7b057cab4..9c5b42b4fd1 100644 --- a/packages/mermaid/src/diagrams/c4/c4Db.ts +++ b/packages/mermaid/src/diagrams/c4/c4Db.ts @@ -1,6 +1,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getRequiredConfig } from '../../diagram-api/requiredConfig.js'; import { sanitizeText } from '../common/common.js'; +import { formatUrl } from '../../utils.js'; import { setAccTitle, getAccTitle, @@ -8,6 +9,8 @@ import { setAccDescription, } from '../common/commonDb.js'; import type { C4Boundary, C4Rel, C4Shape } from './c4Types.js'; +import type { LayoutData } from '../../rendering-util/types.js'; +import { getData as buildLayoutData } from './c4LayoutData.js'; /** * The parser may pass a plain string or an object with a single @@ -27,6 +30,9 @@ const TEXT_FIELDS = new Set(['label', 'descr', 'techn', 'type']); * * Values may arrive as a raw positional value or a single `{ key: value }` named * override; `undefined` is skipped so an earlier-set value is not clobbered. + * + * A `$link` becomes an `xlink:href` on the rendered element. As in flowchart and class + * diagrams it is sanitized unless `securityLevel` is `loose`, where it is kept as written. */ const assignAttributes = ( bag: Bag, @@ -36,16 +42,19 @@ const assignAttributes = ( if (value === undefined) { continue; } - if (typeof value === 'object') { - const [key, val] = Object.entries(value)[0]; + // A named override arrives as a single `{ key: value }`; a positional value keeps + // the field it was declared for. + const named = typeof value === 'object'; + const [key, val] = named ? Object.entries(value)[0] : [field, value]; + if (key === 'link') { + bag[key] = formatUrl(val, getConfig()); + } else { // Same rule as the positional text slots: a text field is stored as `{ text }` // whichever slot it arrived through. `System_Boundary` and friends splice their // kind in as a positional argument, so an explicit `$type` shifts along into this // one and would otherwise land here as a bare string and overwrite the wrapped // value the type slot just set. - bag[key] = TEXT_FIELDS.has(key) ? { text: val } : val; - } else { - bag[field] = value; + bag[key] = named && TEXT_FIELDS.has(key) ? { text: val } : val; } } }; @@ -72,6 +81,7 @@ let wrapEnabled: boolean | undefined = false; let c4ShapeInRow = 4; let c4BoundaryInRow = 2; let c4Type: string | undefined; +let direction = 'TB'; export const getC4Type = function () { return c4Type; @@ -731,6 +741,22 @@ export const autoWrap = function () { return wrapEnabled; }; +export const setDirection = function (dir: string) { + direction = dir; +}; + +export const getDirection = function () { + return direction; +}; + +/** The parsed diagram as the unified rendering pipeline consumes it. */ +export const getData = function (): LayoutData { + return buildLayoutData( + { getC4ShapeArray, getBoundaries, getRels, getC4Type, getDirection }, + getConfig() + ); +}; + export const clear = function () { c4ShapeArray = []; boundaries = [createGlobalBoundary()]; @@ -744,6 +770,7 @@ export const clear = function () { wrapEnabled = false; c4ShapeInRow = 4; c4BoundaryInRow = 2; + direction = 'TB'; }; export const LINETYPE = { @@ -825,5 +852,8 @@ export default { PLACEMENT, setTitle, setC4Type, + setDirection, + getDirection, + getData, // apply, }; diff --git a/packages/mermaid/src/diagrams/c4/c4Diagram.ts b/packages/mermaid/src/diagrams/c4/c4Diagram.ts index 9557a0f708d..e03ea50541b 100644 --- a/packages/mermaid/src/diagrams/c4/c4Diagram.ts +++ b/packages/mermaid/src/diagrams/c4/c4Diagram.ts @@ -1,7 +1,7 @@ // @ts-ignore: JISON doesn't support types import parser from './parser/c4Diagram.jison'; import db from './c4Db.js'; -import renderer from './c4Renderer.js'; +import renderer from './c4Renderer-unified.js'; import styles from './styles.js'; import type { MermaidConfig } from '../../config.type.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; @@ -11,8 +11,7 @@ export const diagram: DiagramDefinition = { db, renderer, styles, - init: ({ c4, wrap }: MermaidConfig) => { - renderer.setConf(c4); + init: ({ wrap }: MermaidConfig) => { db.setWrap(wrap); }, }; diff --git a/packages/mermaid/src/diagrams/c4/c4LayoutData.spec.ts b/packages/mermaid/src/diagrams/c4/c4LayoutData.spec.ts new file mode 100644 index 00000000000..6f5ca13b624 --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/c4LayoutData.spec.ts @@ -0,0 +1,308 @@ +import { setConfig, setSiteConfig } from '../../config.js'; +import c4Db from './c4Db.js'; +// @ts-ignore: JISON doesn't support types +import c4 from './parser/c4Diagram.jison'; +import { getData } from './c4LayoutData.js'; +import type { MermaidConfig } from '../../config.type.js'; +import type { Edge } from '../../rendering-util/types.js'; +import type { C4LabelNudge } from './c4Types.js'; + +setConfig({ securityLevel: 'strict' }); + +const parse = (text: string) => { + c4.parser.yy = c4Db; + c4.parser.yy.clear(); + c4.parser.parse(text); + return c4.parser.yy; +}; + +const config = (): MermaidConfig => ({ c4: { width: 216, c4ShapePadding: 20 }, look: 'classic' }); + +describe('c4LayoutData.getData', () => { + it('turns boundaries into group nodes and nests their contents', () => { + const db = parse(`C4Context +Enterprise_Boundary(e1, "Bank") { +System(SystemAA, "Internet Banking System", "Allows customers to view information") +} +Person(customerA, "Banking Customer A")`); + + const { nodes } = getData(db, config()); + + const boundary = nodes.find((n) => n.id === 'e1'); + expect(boundary).toMatchObject({ isGroup: true, shape: 'rect' }); + // the boundary is at the top level, so it has no parent + expect(boundary?.parentId).toBeUndefined(); + + // the element inside is nested under it, the element outside is not + expect(nodes.find((n) => n.id === 'SystemAA')).toMatchObject({ + parentId: 'e1', + isGroup: false, + }); + expect(nodes.find((n) => n.id === 'customerA')?.parentId).toBeUndefined(); + }); + + it('labels a boundary with its type only when the type adds something', () => { + const db = parse(`C4Context +Enterprise_Boundary(e1, "Bank") { +System_Boundary(s1, "Internet Banking") { +System(sys, "SPA") +} +Boundary(b1, "Region", "us-east-1") { +System(sys2, "Reporting") +} +}`); + + const labelOf = (id: string) => getData(db, config()).nodes.find((n) => n.id === id)?.label; + // the grammar injects its boundary kind in upper case + expect(labelOf('e1')).toBe('Bank [Enterprise]'); + // `system` only restates System_Boundary, so it is left off + expect(labelOf('s1')).toBe('Internet Banking'); + // a type that came from the diagram source is shown as written + expect(labelOf('b1')).toBe('Region [us-east-1]'); + }); + + it('does not label a deployment node with its implied node type', () => { + const db = parse(`C4Deployment +Node(n1, "Server") { +Container(api, "API") +} +Node(n2, "Host", "Ubuntu 22.04") { +Container(store, "Store") +}`); + + const labelOf = (id: string) => getData(db, config()).nodes.find((n) => n.id === id)?.label; + expect(labelOf('n1')).toBe('Server'); + expect(labelOf('n2')).toBe('Host [Ubuntu 22.04]'); + }); + + it('never emits the implicit global boundary as a node', () => { + const db = parse(`C4Context\nPerson(a, "A")`); + const { nodes } = getData(db, config()); + expect(nodes.map((n) => n.id)).not.toContain('global'); + }); + + it('resolves element shapes through the shared adapter', () => { + const db = parse(`C4Context +Person(p, "P") +System(s, "S") +SystemDb(sdb, "SDB") +SystemQueue(sq, "SQ")`); + + const { nodes } = getData(db, config()); + const shapeOf = (id: string) => nodes.find((n) => n.id === id)?.shape; + expect(shapeOf('p')).toBe('person'); + expect(shapeOf('s')).toBe('rounded'); + expect(shapeOf('sdb')).toBe('cylinder'); + expect(shapeOf('sq')).toBe('h-cyl'); + }); + + it('carries the stereotype and description the label helper renders', () => { + const db = parse(`C4Context\nContainer(c, "SPA", "JavaScript", "Front-end")`); + const { nodes } = getData(db, config()); + expect(nodes.find((n) => n.id === 'c')).toMatchObject({ + label: 'SPA', + stereotype: '[Container: JavaScript]', + description: ['Front-end'], + useHtmlLabels: false, + }); + }); + + // Each relationship needs its own pair of endpoints: c4Db.addRel merges any + // relationship that repeats an existing from/to, so reusing a pair here would + // collapse the three kinds into one. + it('builds one edge per relationship, with arrowheads by direction', () => { + const db = parse(`C4Context +Person(a, "A") +Person(b, "B") +Person(c, "C") +Person(d, "D") +Rel(a, b, "uses") +BiRel(b, c, "syncs") +Rel_Back(c, d, "returns")`); + + const { edges } = getData(db, config()); + expect(edges).toHaveLength(3); + + expect(edges[0]).toMatchObject({ + start: 'a', + end: 'b', + arrowTypeEnd: 'arrow_point', + classes: 'c4-rel', + curve: 'linear', + labelpos: 'c', + }); + expect(edges[0].arrowTypeStart).toBeUndefined(); + + // BiRel gets an arrowhead at both ends + expect(edges[1]).toMatchObject({ arrowTypeStart: 'arrow_point', arrowTypeEnd: 'arrow_point' }); + + // Rel_Back points the other way only + expect(edges[2].arrowTypeStart).toBe('arrow_point'); + expect(edges[2].arrowTypeEnd).toBeUndefined(); + }); + + // A relationship may name a boundary rather than a shape (#4864). The legacy renderer + // needed the boundary to carry its own intersect function for this; here the boundary is + // an ordinary group node, so the edge is built the same way as any other. + it('builds an edge whose endpoint is a boundary', () => { + const db = parse(`C4Context +Container_Boundary(banking, "Internet Banking") { +Container(spa, "SPA", "JavaScript", "Front-end") +} +System_Ext(email, "Email System", "External mail") +Rel(banking, email, "Forwards alerts")`); + + const { nodes, edges } = getData(db, config()); + + expect(nodes.find((n) => n.id === 'banking')).toMatchObject({ isGroup: true }); + expect(edges).toHaveLength(1); + expect(edges[0]).toMatchObject({ start: 'banking', end: 'email' }); + }); + + it('numbers relationships in a C4Dynamic diagram', () => { + const db = parse(`C4Dynamic +Person(a, "A") +Person(b, "B") +Rel(a, b, "first") +Rel(b, a, "second")`); + + const { edges } = getData(db, config()); + expect(edges[0].label).toContain('1: first'); + expect(edges[1].label).toContain('2: second'); + }); + + it('does not number relationships outside C4Dynamic', () => { + const db = parse(`C4Context\nPerson(a, "A")\nPerson(b, "B")\nRel(a, b, "plain")`); + const { edges } = getData(db, config()); + expect(edges[0].label).toContain('plain'); + expect(edges[0].label).not.toContain('1:'); + }); + + it('passes the db direction through', () => { + const db = parse(`C4Context\nPerson(a, "A")`); + const { direction } = getData(db, config()); + expect(typeof direction).toBe('string'); + }); + + it('carries an UpdateRelStyle colour into the edge style', () => { + const db = parse(`C4Context +Person(a, "A") +Person(b, "B") +Rel(a, b, "uses") +UpdateRelStyle(a, b, $lineColor="red", $textColor="blue")`); + + const { edges } = getData(db, config()); + expect(edges[0].style).toContain('stroke:red'); + expect(edges[0].labelStyle).toContain('color:blue'); + }); + + // The colour is interpolated into a CSS declaration, so a value carrying `;` could + // append further declarations. Anything that is not a colour on its own is dropped + // rather than escaped, so no partial value reaches the style string. + it('drops an UpdateRelStyle colour that is not a colour on its own', () => { + const db = parse(`C4Context +Person(a, "A") +Person(b, "B") +Rel(a, b, "uses") +UpdateRelStyle(a, b, $lineColor="red;background:url(x)", $textColor="blue;content:'x'")`); + + const { edges } = getData(db, config()); + expect(edges[0].style).toHaveLength(0); + expect(edges[0].labelStyle).toHaveLength(0); + }); + + it('carries a $link into the node', () => { + const db = parse(`C4Context\nPerson(a, "A", "desc", $link="https://example.com")`); + + const { nodes } = getData(db, config()); + // Sanitizing normalises the URL, as it does for a flowchart `click ... href`. + expect(nodes.find((node) => node.id === 'a')?.link).toBe('https://example.com/'); + }); + + it('neutralises a $link that carries a javascript: scheme', () => { + const db = parse(`C4Context\nPerson(a, "A", "desc", $link="javascript:alert(1)")`); + + const { nodes } = getData(db, config()); + expect(nodes.find((node) => node.id === 'a')?.link).toBe('about:blank'); + }); + + // Like a flowchart link, `securityLevel: 'loose'` is the author's explicit trust choice. + it('keeps a $link as written at securityLevel loose', () => { + setSiteConfig({ securityLevel: 'loose' }); + try { + const db = parse(`C4Context\nPerson(a, "A", "desc", $link="javascript:alert(1)")`); + + const { nodes } = getData(db, config()); + expect(nodes.find((node) => node.id === 'a')?.link).toBe('javascript:alert(1)'); + } finally { + setSiteConfig({ securityLevel: 'strict' }); + } + }); + + // `UpdateElementStyle` resolves its alias against the elements first and then the + // boundaries, so the same statement styles either. + it('carries an UpdateElementStyle colour into the boundary style', () => { + const db = parse(`C4Context +Enterprise_Boundary(b1, "Bank") { +Person(a, "A") +} +UpdateElementStyle(b1, $bgColor="red", $borderColor="blue", $fontColor="green")`); + + const { nodes } = getData(db, config()); + expect(nodes.find((node) => node.id === 'b1')?.cssStyles).toEqual([ + 'fill:red', + 'stroke:blue', + 'color:green', + ]); + }); + + // As for a relationship colour: a boundary colour is interpolated into a CSS + // declaration, and a boundary has no palette to fall back to, so it is dropped. + it('drops a boundary colour that is not a colour on its own', () => { + const db = parse(`C4Context +Enterprise_Boundary(b1, "Bank") { +Person(a, "A") +} +UpdateElementStyle(b1, $bgColor="red;background:url(x)", $borderColor="blue;content:'x'")`); + + const { nodes } = getData(db, config()); + expect(nodes.find((node) => node.id === 'b1')?.cssStyles).toHaveLength(0); + }); + + // With HTML labels off the label is rendered as SVG text, where markup would show up + // as literal characters rather than as emphasis. + it('builds a plain relationship label when HTML labels are off', () => { + const db = parse(`C4Context +Person(a, "A") +Person(b, "B") +Rel(a, b, "Uses", "HTTPS", "to read accounts")`); + + const { edges } = getData(db, { ...config(), htmlLabels: false }); + expect(edges[0].label).toBe('Uses
[HTTPS]
to read accounts'); + expect(edges[0].label).not.toContain(''); + }); + + it('carries UpdateRelStyle offsets onto the edge for the renderer to apply', () => { + const db = parse(`C4Context +Person(a, "A") +System(b, "B") +Rel(a, b, "Uses") +UpdateRelStyle(a, b, $offsetX="30", $offsetY="-20")`); + + const { edges } = getData(db, config()); + // the shared pipeline places labels, so the nudge travels as data and is applied after layout + expect(edges[0]).toMatchObject({ labelOffsetX: 30, labelOffsetY: -20 }); + }); + + it('leaves the offsets off an edge that does not set them', () => { + const db = parse(`C4Context +Person(a, "A") +System(b, "B") +Rel(a, b, "Uses")`); + + const { edges } = getData(db, config()); + const [edge] = edges as (Edge & C4LabelNudge)[]; + expect(edge.labelOffsetX).toBeUndefined(); + expect(edge.labelOffsetY).toBeUndefined(); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/c4LayoutData.ts b/packages/mermaid/src/diagrams/c4/c4LayoutData.ts new file mode 100644 index 00000000000..e90fd43f138 --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/c4LayoutData.ts @@ -0,0 +1,176 @@ +import type { MermaidConfig } from '../../config.type.js'; +import { getEffectiveHtmlLabels } from '../../config.js'; +import type { Edge, LayoutData, Node } from '../../rendering-util/types.js'; +import type { C4Boundary, C4LabelNudge, C4Rel, C4Shape } from './c4Types.js'; +import { asColor, buildC4Node, buildEdgeLabel } from './c4ShapeAdapter.js'; + +/** + * Converts the legacy C4 db state (elements, boundaries, relationships) into the + * unified renderer's {@link LayoutData}, so C4 diagrams are laid out by the shared + * pipeline instead of the legacy row grid. Element nodes are built by the same + * adapter the legacy renderer already used, so the elements themselves render + * identically; only their placement changes. + */ + +/** The parts of the C4 db this adapter reads. */ +interface C4Db { + getC4ShapeArray: (parentBoundary?: string) => C4Shape[]; + getBoundaries: (parentBoundary?: string) => C4Boundary[]; + getRels: () => C4Rel[]; + getC4Type: () => string | undefined; + getDirection: () => string; +} + +/** + * A boundary's own type is only worth showing when it is not the implied one: + * `System_Boundary`, `Container_Boundary` and `Node` all carry a type that just + * restates the keyword. + */ +const IMPLICIT_BOUNDARY_TYPES = new Set(['system', 'container', 'node']); + +/** + * The grammar injects its boundary kind in upper case. A type that arrived from + * the diagram source instead is shown verbatim. + */ +const BOUNDARY_TYPE_LABELS: Record = { enterprise: 'Enterprise' }; + +const boundaryLabel = (boundary: C4Boundary): string => { + const type = boundary.type?.text; + if (!type || IMPLICIT_BOUNDARY_TYPES.has(type.toLowerCase())) { + return boundary.label.text; + } + return `${boundary.label.text} [${BOUNDARY_TYPE_LABELS[type.toLowerCase()] ?? type}]`; +}; + +/** + * Explicit colours applied to a boundary. `UpdateElementStyle` resolves its alias + * against the elements first and then the boundaries, so it styles both. A boundary + * has no palette to fall back to, so a value that is not a colour is dropped. + */ +const boundaryCssStyles = (boundary: C4Boundary): string[] => { + const styles: string[] = []; + const fill = asColor(boundary.bgColor); + const stroke = asColor(boundary.borderColor); + const color = asColor(boundary.fontColor); + if (fill) { + styles.push(`fill:${fill}`); + } + if (stroke) { + styles.push(`stroke:${stroke}`); + } + if (color) { + styles.push(`color:${color}`); + } + return styles; +}; + +/** + * The label nudge an `UpdateRelStyle` asked for. + * + * A named attribute lands in a slot that is not parsed, so the db holds these as strings as often + * as numbers; a value that is not a number at all is dropped rather than shifting the label by NaN. + */ +const labelNudge = (rel: C4Rel): C4LabelNudge => { + const nudge: C4LabelNudge = {}; + const x = Number(rel.offsetX); + const y = Number(rel.offsetY); + if (rel.offsetX !== undefined && rel.offsetX !== null && !Number.isNaN(x)) { + nudge.labelOffsetX = x; + } + if (rel.offsetY !== undefined && rel.offsetY !== null && !Number.isNaN(y)) { + nudge.labelOffsetY = y; + } + return nudge; +}; + +export const getData = (db: C4Db, config: MermaidConfig): LayoutData => { + const nodes: Node[] = []; + const edges: Edge[] = []; + const c4Config = config.c4 ?? {}; + const padding = c4Config.c4ShapePadding ?? 20; + const elementWidth = c4Config.width ?? 216; + const look = config.look ?? 'classic'; + const useHtmlLabels = getEffectiveHtmlLabels(config); + // C4Dynamic numbers each relationship in declaration order (1: ..., 2: ...). + const isDynamic = db.getC4Type() === 'C4Dynamic'; + + // 'global' is the implicit root boundary; it is a container in the db, not a drawn box. + const boundaries = db.getBoundaries().filter((boundary) => boundary.alias !== 'global'); + const boundaryAliases = new Set(boundaries.map((boundary) => boundary.alias)); + + const parentIdOf = (parentBoundary: string): string | undefined => + parentBoundary && parentBoundary !== 'global' && boundaryAliases.has(parentBoundary) + ? parentBoundary + : undefined; + + for (const boundary of boundaries) { + const isDeploymentNode = boundary.nodeType !== undefined; + nodes.push({ + id: boundary.alias, + label: boundaryLabel(boundary), + labelType: 'string', + isGroup: true, + shape: 'rect', + parentId: parentIdOf(boundary.parentBoundary), + cssClasses: isDeploymentNode ? 'c4-boundary c4-deployment-node' : 'c4-boundary', + cssStyles: boundaryCssStyles(boundary), + link: boundary.link ?? undefined, + look, + }); + } + + for (const shape of db.getC4ShapeArray()) { + nodes.push({ + ...buildC4Node(shape, c4Config, padding, look, elementWidth), + parentId: parentIdOf(shape.parentBoundary), + link: shape.link, + }); + } + + db.getRels().forEach((rel, index) => { + const isBidirectional = rel.type === 'birel'; + const isBack = rel.type === 'rel_b'; + const label = buildEdgeLabel( + isDynamic + ? { ...rel, label: { ...rel.label, text: `${index + 1}: ${rel.label.text}` } } + : rel, + useHtmlLabels + ); + const style: string[] = []; + const labelStyle: string[] = []; + const lineColor = asColor(rel.lineColor); + const textColor = asColor(rel.textColor); + if (lineColor) { + style.push(`stroke:${lineColor}`); + } + if (textColor) { + labelStyle.push(`color:${textColor}`); + } + edges.push({ + id: `c4-edge-${index}-${rel.from}-${rel.to}`, + start: rel.from, + end: rel.to, + label, + arrowTypeStart: isBidirectional || isBack ? 'arrow_point' : undefined, + arrowTypeEnd: isBack ? undefined : 'arrow_point', + style, + labelStyle, + classes: 'c4-rel', + // Straight lines as on c4model.com; labelpos 'c' centres the label on the + // line rather than dagre's default side offset. + curve: 'linear', + labelpos: 'c', + // `UpdateRelStyle($offsetX, $offsetY)`: applied to the placed label by the renderer, + // since the shared pipeline decides where a label goes. + ...labelNudge(rel), + look, + }); + }); + + return { + nodes, + edges, + config, + direction: db.getDirection(), + }; +}; diff --git a/packages/mermaid/src/diagrams/c4/c4Renderer-unified.ts b/packages/mermaid/src/diagrams/c4/c4Renderer-unified.ts new file mode 100644 index 00000000000..ff6cda6d3d7 --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/c4Renderer-unified.ts @@ -0,0 +1,74 @@ +import { select } from 'd3'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; +import { log } from '../../logger.js'; +import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; +import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; +import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; +import utils from '../../utils.js'; +import type { Diagram } from '../../Diagram.js'; +import type { D3Selection } from '../../types.js'; +import type { Edge, LayoutData } from '../../rendering-util/types.js'; +import type { C4LabelNudge } from './c4Types.js'; +import type c4Db from './c4Db.js'; + +type C4DB = typeof c4Db; + +/** + * `UpdateRelStyle($offsetX, $offsetY)` moves a relationship's label off the line's midpoint, + * which is how an author untangles two relationships that would otherwise print on top of each + * other. The unified pipeline places labels, so the nudge is applied to what it placed. + */ +const nudgeRelationshipLabels = (svg: D3Selection, data4Layout: LayoutData) => { + for (const edge of data4Layout.edges as (Edge & C4LabelNudge)[]) { + const dx = edge.labelOffsetX ?? 0; + const dy = edge.labelOffsetY ?? 0; + if (!dx && !dy) { + continue; + } + const inner = svg.select(`[data-id="${edge.id}"]`); + const group = inner.empty() ? null : inner.node()?.closest('g.edgeLabel'); + if (!group) { + continue; + } + const label = select(group as SVGGElement); + const transform = label.attr('transform') ?? ''; + const [x, y] = /translate\(\s*([\d.-]+)[ ,]+([\d.-]+)/.exec(transform)?.slice(1) ?? []; + if (x === undefined || y === undefined) { + continue; + } + label.attr('transform', `translate(${Number(x) + dx}, ${Number(y) + dy})`); + } +}; + +export const draw = async function (_text: string, id: string, _version: string, diag: Diagram) { + const { securityLevel, c4: conf, layout } = getConfig(); + const db = diag.db as C4DB; + + const data4Layout = db.getData(); + const svg = getDiagramElement(id, securityLevel); + + data4Layout.type = diag.type; + data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); + // C4 has no nodeSpacing/rankSpacing of its own; c4ShapeMargin is its + // "margin between shapes" knob, which is the same quantity. + data4Layout.nodeSpacing = conf?.c4ShapeMargin ?? 50; + data4Layout.rankSpacing = conf?.c4ShapeMargin ?? 50; + data4Layout.markers = ['point']; + data4Layout.diagramId = id; + + log.debug('c4 layout data', data4Layout); + + await render(data4Layout, svg); + + nudgeRelationshipLabels(svg, data4Layout); + + utils.insertTitle(svg, 'c4TitleText', conf?.diagramMarginY ?? 10, db.getTitle()); + + // setupViewPortForSVG pads all four sides with a single value, so the + // horizontal margin is the one carried over. + setupViewPortForSVG(svg, conf?.diagramMarginX ?? 50, 'c4Diagram', conf?.useMaxWidth ?? true); +}; + +export default { + draw, +}; diff --git a/packages/mermaid/src/diagrams/c4/c4Renderer.ts b/packages/mermaid/src/diagrams/c4/c4Renderer.ts deleted file mode 100644 index 4f2cf82a0f7..00000000000 --- a/packages/mermaid/src/diagrams/c4/c4Renderer.ts +++ /dev/null @@ -1,663 +0,0 @@ -import svgDraw from './svgDraw.js'; -import { log } from '../../logger.js'; -// @ts-ignore: JISON doesn't support types -import { parser } from './parser/c4Diagram.jison'; -import common from '../common/common.js'; -import c4Db from './c4Db.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { getRequiredConfig } from '../../diagram-api/requiredConfig.js'; -import assignWithDepth from '../../assignWithDepth.js'; -import { wrapLabel, calculateTextWidth, calculateTextHeight } from '../../utils.js'; -import { getDiagramRoot } from '../../utils/diagramRoot.js'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; -import type { Diagram } from '../../Diagram.js'; -import type { C4DiagramConfig } from '../../config.type.js'; -import type { SVG } from '../../diagram-api/types.js'; -import type { TextDimensionConfig } from '../../types.js'; -import type { - C4Boundary, - C4DrawConfig, - C4Element, - C4Font, - C4Point, - C4Rel, - C4Shape, - C4Text, -} from './c4Types.js'; -import { shapes } from '../../rendering-util/rendering-elements/shapes.js'; -import { buildC4Node } from './c4ShapeAdapter.js'; -import intersect from '../../rendering-util/rendering-elements/intersect/index.js'; - -type C4DB = typeof c4Db; - -/** The config passed to {@link setConf} may carry the global font settings. */ -type C4SetConfigParam = C4DiagramConfig & { - fontFamily?: string; - fontSize?: string | number; - fontWeight?: string | number; -}; - -interface BoundsData { - startx?: number; - stopx?: number; - starty?: number; - stopy?: number; - widthLimit?: number; -} - -interface NextBoundsData { - startx?: number; - stopx?: number; - starty?: number; - stopy?: number; - cnt: number; -} - -/** A {@link C4Text} after measurement: the layout fields are populated. */ -type MeasuredC4Text = C4Text & { width: number; height: number; textLines: number }; - -let globalBoundaryMaxX = 0, - globalBoundaryMaxY = 0; - -let c4ShapeInRow = 4; -let c4BoundaryInRow = 2; - -parser.yy = c4Db; - -let conf = {} as C4DrawConfig; - -class Bounds { - name: string; - data: BoundsData; - nextData: NextBoundsData; - - constructor(diagObj: Diagram) { - 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 = {} as NextBoundsData; - this.nextData.startx = undefined; - this.nextData.stopx = undefined; - this.nextData.starty = undefined; - this.nextData.stopy = undefined; - this.nextData.cnt = 0; - - setConf((diagObj.db as C4DB).getConfig()); - } - - setData(startx: number, stopx: number, starty: number, stopy: number) { - 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: BoundsData | NextBoundsData, - key: 'startx' | 'stopx' | 'starty' | 'stopy', - val: number, - fun: (a: number, b: number) => number - ) { - if (obj[key] === undefined) { - obj[key] = val; - } else { - obj[key] = fun(val, obj[key]); - } - } - - insert(c4Shape: C4Shape) { - this.nextData.cnt = this.nextData.cnt + 1; - // `setData()` seeds the bounds before any `insert()` call. - const nextStopx = this.nextData.stopx!; - const widthLimit = this.data.widthLimit!; - let _startx = - this.nextData.startx === this.nextData.stopx - ? nextStopx + c4Shape.margin - : nextStopx + c4Shape.margin * 2; - let _stopx = _startx + c4Shape.width; - let _starty = this.nextData.starty! + c4Shape.margin * 2; - let _stopy = _starty + c4Shape.height; - if (_startx >= widthLimit || _stopx >= widthLimit || this.nextData.cnt > 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; - 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(diagObj: Diagram) { - this.name = ''; - this.data = { - startx: undefined, - stopx: undefined, - starty: undefined, - stopy: undefined, - widthLimit: undefined, - }; - this.nextData = { - startx: undefined, - stopx: undefined, - starty: undefined, - stopy: undefined, - cnt: 0, - }; - setConf((diagObj.db as C4DB).getConfig()); - } - - bumpLastMargin(margin: number) { - this.data.stopx! += margin; - this.data.stopy! += margin; - } -} - -export const setConf = function (cnf?: C4SetConfigParam) { - 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 boundaryFont = (cnf: C4DrawConfig): C4Font => { - return { - fontFamily: cnf.boundaryFontFamily, - fontSize: cnf.boundaryFontSize as number, - fontWeight: cnf.boundaryFontWeight, - }; -}; - -const messageFont = (cnf: C4DrawConfig): C4Font => { - return { - fontFamily: cnf.messageFontFamily, - fontSize: cnf.messageFontSize as number, - fontWeight: cnf.messageFontWeight, - }; -}; - -function calcC4ShapeTextWH( - textType: 'label' | 'type' | 'techn' | 'descr', - c4Shape: C4Shape | C4Boundary | C4Rel, - c4ShapeTextWrap: boolean | undefined, - textConf: C4Font, - textLimitWidth: number -): MeasuredC4Text { - // `textType` is always one of the `C4Text` valued fields of `c4Shape`, and - // the layout fields are populated below (or by an earlier measurement). - const textElement = c4Shape[textType] as MeasuredC4Text; - if (!textElement.width) { - if (c4ShapeTextWrap) { - textElement.text = wrapLabel( - textElement.text, - textLimitWidth, - textConf as Parameters[2] - ); - textElement.textLines = textElement.text.split(common.lineBreakRegex).length; - // textElement.width = calculateTextWidth(textElement.text, textConf); - textElement.width = textLimitWidth; - // textElement.height = textElement.textLines * textConf.fontSize; - textElement.height = calculateTextHeight(textElement.text, textConf as TextDimensionConfig); - } else { - const lines = textElement.text.split(common.lineBreakRegex); - textElement.textLines = lines.length; - let lineHeight = 0; - textElement.height = 0; - textElement.width = 0; - for (const line of lines) { - textElement.width = Math.max( - calculateTextWidth(line, textConf as TextDimensionConfig), - textElement.width - ); - lineHeight = calculateTextHeight(line, textConf as TextDimensionConfig); - textElement.height = textElement.height + lineHeight; - } - // c4Shapes[textType].height = c4Shapes[textType].textLines * textConf.fontSize; - } - } - return textElement; -} - -export const drawBoundary = function (diagram: SVG, boundary: C4Boundary, bounds: Bounds) { - // The bounds are seeded via `setData()` before a boundary is drawn. - const startx = bounds.data.startx!; - const starty = bounds.data.starty!; - boundary.x = startx; - boundary.y = starty; - boundary.width = bounds.data.stopx! - startx; - boundary.height = bounds.data.stopy! - starty; - // A boundary can be a relationship endpoint, and its box is a plain rectangle. - // intersect.rect measures from the centre, while the C4 grid stores the top-left corner. - boundary.intersect = (point: C4Point) => - intersect.rect( - { - x: boundary.x + boundary.width / 2, - y: boundary.y + boundary.height / 2, - width: boundary.width, - height: boundary.height, - }, - point - ); - - boundary.label.y = conf.c4ShapeMargin - 35; - - const boundaryTextWrap = boundary.wrap && conf.wrap; - const boundaryLabelConf = boundaryFont(conf); - boundaryLabelConf.fontSize = boundaryLabelConf.fontSize + 2; - boundaryLabelConf.fontWeight = 'bold'; - const textLimitWidth = calculateTextWidth( - boundary.label.text, - boundaryLabelConf as TextDimensionConfig - ); - calcC4ShapeTextWH('label', boundary, boundaryTextWrap, boundaryLabelConf, textLimitWidth); - - svgDraw.drawBoundary(diagram, boundary, conf); -}; - -export const drawC4ShapeArray = async function ( - currentBounds: Bounds, - diagram: SVG, - c4ShapeArray: C4Shape[], - c4ShapeKeys: string[] -) { - const mermaidConfig = getConfig(); - const look = mermaidConfig.look ?? 'classic'; - const renderOptions = { config: mermaidConfig }; - // Namespace the shape DOM ids with the diagram id so two diagrams on one page don't - // collide (the unified shapes use node.domId for the element id). - const diagramId = diagram.attr('id') ?? ''; - - // `c4ShapeKeys` are the (numeric string) indices of `c4ShapeArray`. - const c4Shapes = c4ShapeKeys.map((key) => c4ShapeArray[Number(key)]); - - const shapeHandlerFor = (node: ReturnType) => { - const handler = node.shape ? shapes[node.shape] : undefined; - if (!handler) { - throw new Error(`C4: no shape handler for "${node.shape}"`); - } - return handler; - }; - - // Pass 1 (measure): render each shape, read its self-sized dimensions onto the legacy - // c4Shape, then discard the rendering. The shape is drawn again in pass 2 directly at its - // final position, so its label (and any composited sub-element) lays out under the final - // transform - drawing at the origin and translating afterwards leaves composited layers - // (e.g. anything with opacity) painting at the stale origin. - await Promise.all( - c4Shapes.map(async (c4Shape) => { - const node = buildC4Node(c4Shape, conf, conf.c4ShapePadding, look, conf.width); - node.domId = `${diagramId}-${node.id}`; - const measured = await shapeHandlerFor(node)(diagram, node, renderOptions); - c4Shape.width = node.width ?? conf.width; - c4Shape.height = node.height ?? conf.height; - c4Shape.margin = conf.c4ShapeMargin; - measured.remove(); - }) - ); - - // Position with the legacy grid. - for (const c4Shape of c4Shapes) { - currentBounds.insert(c4Shape); - } - - // Pass 2 (draw): render each shape into a group already translated to its grid position - // (unified shapes are centred at the origin; legacy x/y is the top-left corner). - await Promise.all( - c4Shapes.map(async (c4Shape) => { - const node = buildC4Node(c4Shape, conf, conf.c4ShapePadding, look, conf.width); - node.domId = `${diagramId}-${node.id}`; - // Needed to properly calculate the intersection points. - node.x = c4Shape.x + c4Shape.width / 2; - node.y = c4Shape.y + c4Shape.height / 2; - // Appending before the await keeps the shapes' stacking order deterministic. - const positioned = diagram - .append('g') - .attr( - 'transform', - `translate(${c4Shape.x + c4Shape.width / 2}, ${c4Shape.y + c4Shape.height / 2})` - ); - await shapeHandlerFor(node)(positioned, node, renderOptions); - c4Shape.intersect = node.intersect; - }) - ); - - currentBounds.bumpLastMargin(conf.c4ShapeMargin); -}; - -class Point { - x: number; - y: number; - - constructor(x: number, y: number) { - 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. - */ -const getIntersectPoint = function (fromNode: C4Element, endPoint: Point): Point | null { - if (!fromNode.intersect) { - throw new Error( - `C4 element "${fromNode.alias}" has no intersect function. Please report this to https://github.com/mermaid-js/mermaid/issues` - ); - } - const { x, y } = fromNode.intersect(endPoint); - return new Point(x, y); -}; - -const getIntersectPoints = function (fromNode: C4Element, endNode: C4Element) { - const endIntersectPoint = { x: 0, y: 0 }; - endIntersectPoint.x = endNode.x + endNode.width / 2; - endIntersectPoint.y = endNode.y + endNode.height / 2; - const startPoint = getIntersectPoint(fromNode, endIntersectPoint); - - endIntersectPoint.x = fromNode.x + fromNode.width / 2; - endIntersectPoint.y = fromNode.y + fromNode.height / 2; - const endPoint = getIntersectPoint(endNode, endIntersectPoint); - return { startPoint: startPoint, endPoint: endPoint }; -}; - -export const drawRels = function ( - diagram: SVG, - rels: C4Rel[], - getC4ShapeObj: (alias: string) => C4Element | undefined, - diagObj: Diagram, - diagramId: string -) { - const diagramType = (diagObj.db as C4DB).getC4Type(); - let i = 0; - for (const rel of rels) { - i = i + 1; - const relTextWrap = rel.wrap && conf.wrap; - const relConf = messageFont(conf); - if (diagramType === 'C4Dynamic') { - rel.label.text = i + ': ' + rel.label.text; - } - let textLimitWidth = calculateTextWidth(rel.label.text, relConf as TextDimensionConfig); - calcC4ShapeTextWH('label', rel, relTextWrap, relConf, textLimitWidth); - - if (rel.techn && rel.techn.text !== '') { - textLimitWidth = calculateTextWidth(rel.techn.text, relConf as TextDimensionConfig); - calcC4ShapeTextWH('techn', rel, relTextWrap, relConf, textLimitWidth); - } - - if (rel.descr && rel.descr.text !== '') { - textLimitWidth = calculateTextWidth(rel.descr.text, relConf as TextDimensionConfig); - calcC4ShapeTextWH('descr', rel, relTextWrap, relConf, textLimitWidth); - } - - const fromNode = getC4ShapeObj(rel.from); - const endNode = getC4ShapeObj(rel.to); - if (!fromNode || !endNode) { - throw new Error( - `C4 rel "${rel.from}" -> "${rel.to}" references an unknown shape or boundary` - ); - } - const points = getIntersectPoints(fromNode, endNode); - if (!points.startPoint || !points.endPoint) { - throw new Error( - `Could not calculate intersection points for rel "${rel.from}" -> "${rel.to}"` - ); - } - rel.startPoint = points.startPoint; - rel.endPoint = points.endPoint; - } - svgDraw.drawRels(diagram, rels, conf, diagramId); -}; - -async function drawInsideBoundary( - diagram: SVG, - parentBoundaryAlias: string, - parentBounds: Bounds, - currentBoundaries: C4Boundary[], - diagObj: Diagram -) { - const db = diagObj.db as C4DB; - const currentBounds = new Bounds(diagObj); - // Calculate the width limit of the boundary. label/type 的长度, - currentBounds.data.widthLimit = - parentBounds.data.widthLimit! / Math.min(c4BoundaryInRow, currentBoundaries.length); - // Math.min( - // conf.width * conf.c4ShapeInRow + conf.c4ShapeMargin * conf.c4ShapeInRow * 2, - // parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundaries.length) - // ); - for (const [i, currentBoundary] of currentBoundaries.entries()) { - 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; - } - - const currentBoundaryTextWrap = currentBoundary.wrap && conf.wrap; - - const currentBoundaryLabelConf = boundaryFont(conf); - currentBoundaryLabelConf.fontSize = currentBoundaryLabelConf.fontSize + 2; - currentBoundaryLabelConf.fontWeight = 'bold'; - const label = calcC4ShapeTextWH( - 'label', - currentBoundary, - currentBoundaryTextWrap, - currentBoundaryLabelConf, - currentBounds.data.widthLimit - ); - label.Y = Y + 8; - Y = label.Y + label.height; - - if (currentBoundary.type && currentBoundary.type.text !== '') { - currentBoundary.type.text = '[' + currentBoundary.type.text + ']'; - const currentBoundaryTypeConf = boundaryFont(conf); - const type = calcC4ShapeTextWH( - 'type', - currentBoundary, - currentBoundaryTextWrap, - currentBoundaryTypeConf, - currentBounds.data.widthLimit - ); - type.Y = Y + 5; - Y = type.Y + type.height; - } - - if (currentBoundary.descr && currentBoundary.descr.text !== '') { - const currentBoundaryDescrConf = boundaryFont(conf); - currentBoundaryDescrConf.fontSize = currentBoundaryDescrConf.fontSize - 2; - const descr = calcC4ShapeTextWH( - 'descr', - currentBoundary, - currentBoundaryTextWrap, - currentBoundaryDescrConf, - currentBounds.data.widthLimit - ); - descr.Y = Y + 20; - Y = descr.Y + descr.height; - } - - if (i == 0 || i % c4BoundaryInRow === 0) { - // Calculate the drawing start point of the currentBoundaries. - const _x = parentBounds.data.startx! + conf.diagramMarginX; - const _y = parentBounds.data.stopy! + conf.diagramMarginY + Y; - - currentBounds.setData(_x, _x, _y, _y); - } else { - // Calculate the drawing start point of the currentBoundaries. - const _x = - currentBounds.data.stopx !== currentBounds.data.startx - ? currentBounds.data.stopx! + conf.diagramMarginX - : currentBounds.data.startx!; - const _y = currentBounds.data.starty!; - - currentBounds.setData(_x, _x, _y, _y); - } - currentBounds.name = currentBoundary.alias; - const currentPersonOrSystemArray = db.getC4ShapeArray(currentBoundary.alias); - const currentPersonOrSystemKeys = db.getC4ShapeKeys(currentBoundary.alias); - - if (currentPersonOrSystemKeys.length > 0) { - await drawC4ShapeArray( - currentBounds, - diagram, - currentPersonOrSystemArray, - currentPersonOrSystemKeys - ); - } - parentBoundaryAlias = currentBoundary.alias; - const nextCurrentBoundaries = db.getBoundaries(parentBoundaryAlias); - - if (nextCurrentBoundaries.length > 0) { - // draw boundary inside currentBoundary - await drawInsideBoundary( - diagram, - parentBoundaryAlias, - currentBounds, - nextCurrentBoundaries, - diagObj - ); - } - // 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. - */ -export const draw = async function (_text: string, id: string, _version: string, diagObj: Diagram) { - conf = getRequiredConfig('c4') as C4DrawConfig; - const securityLevel = getConfig().securityLevel; - // Handle root and Document for when rendering in sandbox mode - const { root } = getDiagramRoot(id, securityLevel); - - const db = diagObj.db as C4DB; - - db.setWrap(conf.wrap); - - c4ShapeInRow = db.getC4ShapeInRow(); - c4BoundaryInRow = db.getC4BoundaryInRow(); - - log.debug(`C:${JSON.stringify(conf, null, 2)}`); - - const diagram: SVG = root.select(`[id="${id}"]`); - - svgDraw.insertComputerIcon(diagram, id); - svgDraw.insertDatabaseIcon(diagram, id); - svgDraw.insertClockIcon(diagram, id); - - const screenBounds = new Bounds(diagObj); - - screenBounds.setData( - conf.diagramMarginX, - conf.diagramMarginX, - conf.diagramMarginY, - conf.diagramMarginY - ); - - screenBounds.data.widthLimit = screen.availWidth; - globalBoundaryMaxX = conf.diagramMarginX; - globalBoundaryMaxY = conf.diagramMarginY; - - const title = db.getTitle(); - const currentBoundaries = db.getBoundaries(''); - // switch (c4type) { - // case 'C4Context': - await drawInsideBoundary(diagram, '', screenBounds, currentBoundaries, diagObj); - // break; - // } - - // The arrow head definition is attached to the svg once - svgDraw.insertArrowHead(diagram, id); - svgDraw.insertArrowEnd(diagram, id); - svgDraw.insertArrowCrossHead(diagram, id); - svgDraw.insertArrowFilledHead(diagram, id); - - drawRels(diagram, db.getRels(), db.getC4Shape, diagObj, id); - - screenBounds.data.stopx = globalBoundaryMaxX; - screenBounds.data.stopy = globalBoundaryMaxY; - - const box = screenBounds.data; - // `setData()` above seeded the start coordinates. - const boxStartx = box.startx!; - const boxStarty = box.starty!; - - // Make sure the height of the diagram supports long menus. - const boxHeight = globalBoundaryMaxY - boxStarty; - - const height = boxHeight + 2 * conf.diagramMarginY; - - // Make sure the width of the diagram supports wide menus. - const boxWidth = globalBoundaryMaxX - boxStartx; - const width = boxWidth + 2 * conf.diagramMarginX; - - if (title) { - diagram - .append('text') - .text(title) - .attr('x', boxWidth / 2 - 4 * conf.diagramMarginX) - .attr('y', boxStarty + conf.diagramMarginY); - } - - configureSvgSize(diagram, height, width, conf.useMaxWidth); - - const extraVertForTitle = title ? 60 : 0; - diagram.attr( - 'viewBox', - boxStartx - - conf.diagramMarginX + - ' -' + - (conf.diagramMarginY + extraVertForTitle) + - ' ' + - width + - ' ' + - (height + extraVertForTitle) - ); - - log.debug(`models:`, box); -}; - -export default { - drawPersonOrSystemArray: drawC4ShapeArray, - drawBoundary, - setConf, - draw, -}; diff --git a/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.spec.ts b/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.spec.ts new file mode 100644 index 00000000000..a2d58e46633 --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.spec.ts @@ -0,0 +1,52 @@ +import { buildEdgeLabel } from './c4ShapeAdapter.js'; + +const rel = (label: string, techn?: string, descr?: string) => ({ + label: { text: label }, + techn: techn === undefined ? undefined : { text: techn }, + descr: descr === undefined ? undefined : { text: descr }, +}); + +describe('buildEdgeLabel', () => { + it('emphasises the name and the technology in an HTML label', () => { + expect(buildEdgeLabel(rel('Uses', 'HTTPS', 'to read accounts'), true)).toBe( + 'Uses
[HTTPS]
to read accounts' + ); + }); + + it('omits the parts a relationship does not have', () => { + expect(buildEdgeLabel(rel('Uses'), true)).toBe('Uses'); + expect(buildEdgeLabel(rel('Uses', undefined, 'why'), true)).toBe( + 'Uses
why' + ); + }); + + // With htmlLabels off the label is rendered as SVG text, where markup would show up as + // literal characters rather than as emphasis. `
` survives either way: it is a line + // break in an HTML label and a line delimiter in a plain one. + it('carries the same words without markup when HTML labels are off', () => { + expect(buildEdgeLabel(rel('Uses', 'HTTPS', 'to read accounts'), false)).toBe( + 'Uses
[HTTPS]
to read accounts' + ); + }); + + it('escapes user text in an HTML label, and leaves it alone in a plain one', () => { + const hostile = rel(' & "x"'); + expect(buildEdgeLabel(hostile, true)).toBe( + '<script>alert(1)</script> & "x"' + ); + // The plain form is set as text content, so the characters need no escaping here. + expect(buildEdgeLabel(hostile, false)).toBe(' & "x"'); + }); + + it('builds an HTML label by default', () => { + expect(buildEdgeLabel(rel('Uses'))).toBe('Uses'); + }); + + it('does not shift the parts up when the relationship has no name', () => { + // `Rel(a, b, "", "HTTPS")` is accepted. Reading a filtered list back by position printed + // the technology as the name, and `undefined` as the technology. + const label = buildEdgeLabel({ label: { text: '' }, techn: { text: 'HTTPS' } }, true); + expect(label).toBe('[HTTPS]'); + expect(buildEdgeLabel({ label: { text: '' } }, true)).toBe(''); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.ts b/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.ts index 7f861ff9a08..6b7b7aecd28 100644 --- a/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.ts +++ b/packages/mermaid/src/diagrams/c4/c4ShapeAdapter.ts @@ -113,6 +113,65 @@ const stereotypeText = (shape: C4ShapeLike): string => { return shape.techn?.text ? `[${stereotype}: ${shape.techn.text}]` : `[${stereotype}]`; }; +const escapeHtml = (txt: string): string => + txt + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + +/** + * A colour from the diagram source, accepted only if it is a colour on its own. The value is + * interpolated into a CSS declaration, so one carrying `;` or a `url(...)` could append + * further declarations; `CSS.supports` rejects those, and anything it cannot judge (no + * CSS API, as in jsdom) falls back to a conservative pattern match. + */ +export const asColor = (value: unknown): string | undefined => { + if (typeof value !== 'string' || value === '') { + return undefined; + } + const accepted = + typeof globalThis.CSS?.supports === 'function' + ? globalThis.CSS.supports('color', value) + : /^(#[\da-f]{3,8}|[a-z]+|rgba?\([\d\s%,./]+\)|hsla?\([\d\s%,./deg]+\))$/i.test(value); + return accepted ? value : undefined; +}; + +/** + * A C4 relationship label: name, then optional `[technology]` and description, one per + * line. `
` separates the lines either way, since it is a line break in an HTML + * label and a line delimiter in a plain SVG one. The emphasis tags only render as + * emphasis in an HTML label; with `htmlLabels` off they would show as literal text, so + * the plain form carries the same words without them. + */ +export const buildEdgeLabel = ( + rel: { label: C4Text; techn?: C4Text; descr?: C4Text }, + useHtmlLabels = true +): string => { + const name = rel.label.text; + const techn = rel.techn?.text; + const descr = rel.descr?.text; + if (!useHtmlLabels) { + return [name, techn && `[${techn}]`, descr] + .filter((part): part is string => Boolean(part)) + .join('
'); + } + // Each line is built from its own part: reading a filtered list back by position shifted + // every later part up whenever an earlier one was empty. + const lines: string[] = []; + if (name) { + lines.push(`${escapeHtml(name)}`); + } + if (techn) { + lines.push(`[${escapeHtml(techn)}]`); + } + if (descr) { + lines.push(`${escapeHtml(descr)}`); + } + return lines.join('
'); +}; + /** The C4 element types, internal and external, that carry per-type config. */ export const C4_ELEMENT_TYPES = ( [ diff --git a/packages/mermaid/src/diagrams/c4/c4Types.ts b/packages/mermaid/src/diagrams/c4/c4Types.ts index cd2646b497e..69a2ea69371 100644 --- a/packages/mermaid/src/diagrams/c4/c4Types.ts +++ b/packages/mermaid/src/diagrams/c4/c4Types.ts @@ -144,3 +144,12 @@ export type C4DrawConfig = Omit< messageFont: () => C4Font; textPlacement?: string; } & Record; + +/** + * What `UpdateRelStyle($offsetX, $offsetY)` adds to a relationship's edge: the nudge is applied + * to the label after the shared pipeline has placed it, so it stays out of the shared Edge type. + */ +export interface C4LabelNudge { + labelOffsetX?: number; + labelOffsetY?: number; +} diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison index f0ce80d33f3..c017017a593 100644 --- a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison +++ b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison @@ -242,6 +242,7 @@ otherStatement | acc_title acc_title_value { $$=$2.trim();yy.setTitle($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } + | direction ; boundaryStatement diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Direction.spec.ts b/packages/mermaid/src/diagrams/c4/parser/c4Direction.spec.ts new file mode 100644 index 00000000000..4a8c6760636 --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/parser/c4Direction.spec.ts @@ -0,0 +1,21 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import c4Db from '../c4Db.js'; +// @ts-ignore: jison parser +import c4 from './c4Diagram.jison'; + +describe('c4 direction statement', () => { + beforeEach(() => { + c4.parser.yy = c4Db; + c4.parser.yy.clear(); + }); + + it.each(['TB', 'BT', 'LR', 'RL'])('accepts %s and stores it', (direction) => { + c4.parser.parse(`C4Context\ndirection ${direction}\nSystem(a, "A")`); + expect(c4Db.getDirection()).toBe(direction); + }); + + it('defaults to TB when the diagram does not say', () => { + c4.parser.parse('C4Context\nSystem(a, "A")'); + expect(c4Db.getDirection()).toBe('TB'); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Person.spec.ts b/packages/mermaid/src/diagrams/c4/parser/c4Person.spec.ts index a6c6c481272..4a62bf99ca2 100644 --- a/packages/mermaid/src/diagrams/c4/parser/c4Person.spec.ts +++ b/packages/mermaid/src/diagrams/c4/parser/c4Person.spec.ts @@ -117,7 +117,8 @@ Person(customerA, "Banking Customer A", "A customer.", $tags="v1.0+internal", $l expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ alias: 'customerA', tags: 'v1.0+internal', - link: 'https://example.com', + // Sanitizing the link normalises it, as it does for a flowchart `click ... href`. + link: 'https://example.com/', }); }); }); diff --git a/packages/mermaid/src/diagrams/c4/styles.js b/packages/mermaid/src/diagrams/c4/styles.js index 76c39a0df5d..9c2580af026 100644 --- a/packages/mermaid/src/diagrams/c4/styles.js +++ b/packages/mermaid/src/diagrams/c4/styles.js @@ -65,6 +65,51 @@ ${elementFontStyles()} .c4-shape line { stroke-width: 2px; } + /* Boundaries render as unified clusters: a dashed, unfilled box with its + label above the contents. */ + .cluster rect { + fill: none; + stroke: ${options.lineColor ?? '#666666'}; + stroke-width: 1px; + stroke-dasharray: 8 4; + } + .cluster .cluster-label, + .cluster .nodeLabel { + color: ${options.textColor ?? '#333333'}; + fill: ${options.textColor ?? '#333333'}; + font-size: 0.9em; + } + /* Relationships: dashed lines with an arrowhead, as on c4model.com. */ + .edgePaths .path, + path.c4-rel { + stroke: ${options.lineColor ?? '#666666'}; + fill: none; + stroke-width: 1.5px; + stroke-dasharray: 6 4; + } + .edgePaths .marker { + fill: ${options.lineColor ?? '#666666'}; + stroke: ${options.lineColor ?? '#666666'}; + } + /* Relationship labels: smaller, on an opaque background so they stay legible over lines. */ + .edgeLabel { + font-size: 0.85em; + background-color: ${options.background ?? '#ffffff'}; + } + .edgeLabel .label foreignObject { + overflow: visible; + } + /* The label is centred on its line, so it needs an opaque backing or the line + shows through between the glyphs. The wrapper div carries it for an HTML + label; a plain SVG label (htmlLabels: false) has a rect instead. */ + .edgeLabel .labelBkg { + background-color: ${options.background ?? '#ffffff'}; + opacity: 1; + } + .edgeLabel rect { + fill: ${options.background ?? '#ffffff'}; + opacity: 1; + } `; export default getStyles; diff --git a/packages/mermaid/src/diagrams/c4/svgDraw.ts b/packages/mermaid/src/diagrams/c4/svgDraw.ts deleted file mode 100644 index 44bf6cd994d..00000000000 --- a/packages/mermaid/src/diagrams/c4/svgDraw.ts +++ /dev/null @@ -1,461 +0,0 @@ -import common from '../common/common.js'; -import * as svgDrawCommon from '../common/svgDrawCommon.js'; -import type { BaseType, Selection } from 'd3'; -import type { SVG, SVGGroup } from '../../diagram-api/types.js'; -import type { D3Selection } from '../../types.js'; -import type { RectData } from '../common/commonTypes.js'; -import type { C4Boundary, C4DrawConfig, C4Font, C4Rel } from './c4Types.js'; - -type TextAttrs = Record; - -type DrawTextFunction = ( - content: string, - g: D3Selection, - x: number, - y: number, - width: number, - height: number, - textAttrs: TextAttrs, - conf: C4Font -) => void; - -export const drawRect = function (elem: SVG | SVGGroup, rectData: RectData) { - return svgDrawCommon.drawRect(elem, rectData); -}; - -export const drawRels = (elem: SVG, rels: C4Rel[], conf: C4DrawConfig, diagramId: string) => { - const relsElem = elem.append('g'); - let i = 0; - for (const rel of rels) { - const textColor = rel.textColor ? rel.textColor : '#444444'; - const strokeColor = rel.lineColor ? rel.lineColor : '#444444'; - // The parseInt is load-bearing: the JISON grammar passes $key="value" attributes - // positionally, so offsets that arrive in the textColor/lineColor parameter slots are - // stored by the db as raw strings and must be coerced here before coordinate arithmetic. - const offsetX = rel.offsetX ? parseInt(String(rel.offsetX)) : 0; - const offsetY = rel.offsetY ? parseInt(String(rel.offsetY)) : 0; - - const url = ''; - if (i === 0) { - const 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', strokeColor); - line.style('fill', 'none'); - if (rel.type !== 'rel_b') { - line.attr('marker-end', 'url(' + url + '#' + diagramId + '-arrowhead)'); - } - if (rel.type === 'birel' || rel.type === 'rel_b') { - line.attr('marker-start', 'url(' + url + '#' + diagramId + '-arrowend)'); - } - i = -1; - } else { - const line = relsElem.append('path'); - line - .attr('fill', 'none') - .attr('stroke-width', '1') - .attr('stroke', strokeColor) - .attr( - 'd', - 'Mstartx,starty Qcontrolx,controly stopx,stopy ' - .replaceAll('startx', rel.startPoint.x as unknown as string) - .replaceAll('starty', rel.startPoint.y as unknown as string) - .replaceAll( - 'controlx', - (rel.startPoint.x + - (rel.endPoint.x - rel.startPoint.x) / 2 - - (rel.endPoint.x - rel.startPoint.x) / 4) as unknown as string - ) - .replaceAll( - 'controly', - (rel.startPoint.y + (rel.endPoint.y - rel.startPoint.y) / 2) as unknown as string - ) - .replaceAll('stopx', rel.endPoint.x as unknown as string) - .replaceAll('stopy', rel.endPoint.y as unknown as string) - ); - if (rel.type !== 'rel_b') { - line.attr('marker-end', 'url(' + url + '#' + diagramId + '-arrowhead)'); - } - if (rel.type === 'birel' || rel.type === 'rel_b') { - line.attr('marker-start', 'url(' + url + '#' + diagramId + '-arrowend)'); - } - } - - // The label was measured by the renderer before drawing. - const labelWidth = rel.label.width!; - 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 + - offsetX, - Math.min(rel.startPoint.y, rel.endPoint.y) + - Math.abs(rel.endPoint.y - rel.startPoint.y) / 2 + - offsetY, - labelWidth, - rel.label.height!, - { fill: textColor }, - 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 + - offsetX, - Math.min(rel.startPoint.y, rel.endPoint.y) + - Math.abs(rel.endPoint.y - rel.startPoint.y) / 2 + - (conf.messageFontSize as number) + - 5 + - offsetY, - Math.max(labelWidth, rel.techn.width!), - rel.techn.height!, - { fill: textColor, 'font-style': 'italic' }, - messageConf - ); - } - } -}; - -/** - * Draws a boundary in the diagram - * - * @param elem - The diagram we'll draw to. - * @param boundary - The boundary to draw. - * @param conf - DrawText implementation discriminator object - */ -const drawBoundary = function (elem: SVG, boundary: C4Boundary, conf: C4DrawConfig) { - const boundaryElem = elem.append('g'); - - const fillColor = boundary.bgColor ? boundary.bgColor : 'none'; - const strokeColor = boundary.borderColor ? boundary.borderColor : '#444444'; - const fontColor = boundary.fontColor ? boundary.fontColor : 'black'; - - let attrsValue: Record = { - 'stroke-width': 1.0, - 'stroke-dasharray': '7.0,7.0', - }; - if (boundary.nodeType) { - attrsValue = { 'stroke-width': 1.0 }; - } - const rectData: RectData = { - x: boundary.x, - y: boundary.y, - fill: fillColor, - stroke: strokeColor, - width: boundary.width, - height: boundary.height, - rx: 2.5, - ry: 2.5, - attrs: attrsValue, - }; - - drawRect(boundaryElem, rectData); - - // draw label - let boundaryConf = conf.boundaryFont(); - boundaryConf.fontWeight = 'bold'; - boundaryConf.fontSize = boundaryConf.fontSize + 2; - boundaryConf.fontColor = fontColor; - _drawTextCandidateFunc(conf)( - boundary.label.text, - boundaryElem, - boundary.x, - boundary.y + boundary.label.Y!, - boundary.width, - boundary.height, - { fill: '#444444' }, - boundaryConf - ); - - // draw type - if (boundary.type && boundary.type.text !== '') { - boundaryConf = conf.boundaryFont(); - boundaryConf.fontColor = fontColor; - _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; - boundaryConf.fontColor = fontColor; - _drawTextCandidateFunc(conf)( - boundary.descr.text, - boundaryElem, - boundary.x, - boundary.y + boundary.descr.Y!, - boundary.width, - boundary.height, - { fill: '#444444' }, - boundaryConf - ); - } -}; - -export const insertDatabaseIcon = function (elem: SVG, id: string) { - elem - .append('defs') - .append('symbol') - .attr('id', 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: SVG, id: string) { - elem - .append('defs') - .append('symbol') - .attr('id', 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: SVG, id: string) { - elem - .append('defs') - .append('symbol') - .attr('id', 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. - */ -export const insertArrowHead = function (elem: SVG, id: string) { - elem - .append('defs') - .append('marker') - .attr('id', 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: SVG, id: string) { - elem - .append('defs') - .append('marker') - .attr('id', 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. - */ -export const insertArrowFilledHead = function (elem: SVG, id: string) { - elem - .append('defs') - .append('marker') - .attr('id', 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 arrow head and define the marker. The result is appended to the svg. - */ -export const insertArrowCrossHead = function (elem: SVG, id: string) { - const defs = elem.append('defs'); - const marker = defs - .append('marker') - .attr('id', 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 -}; - -const _drawTextCandidateFunc = (function () { - function byText( - content: string, - g: D3Selection, - x: number, - y: number, - width: number, - height: number, - textAttrs: 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); - } - - function byTspan( - content: string, - g: D3Selection, - x: number, - y: number, - width: number, - height: number, - textAttrs: TextAttrs, - conf: C4Font - ) { - 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') - .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('dy', dy) - .text(lines[i]) - // .attr('y', y + height / 2) - .attr('alignment-baseline', 'mathematical'); - - _setTextAttrs(text, textAttrs); - } - } - - function byFo( - content: string, - g: D3Selection, - x: number, - y: number, - width: number, - height: number, - textAttrs: TextAttrs, - conf: C4Font - ) { - 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); - } - - function _setTextAttrs( - toText: Selection, - fromTextAttrsDict: TextAttrs - ) { - for (const key in fromTextAttrsDict) { - if (fromTextAttrsDict.hasOwnProperty(key)) { - toText.attr(key, fromTextAttrsDict[key]); - } - } - } - - return function (conf: C4DrawConfig): DrawTextFunction { - return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan; - }; -})(); - -export default { - drawRect, - drawBoundary, - drawRels, - insertArrowHead, - insertArrowEnd, - insertArrowFilledHead, - insertArrowCrossHead, - insertDatabaseIcon, - insertComputerIcon, - insertClockIcon, -}; - -// cspell:ignoreRegExp /'Mstartx.*/g