From adec6cd2549694f6fba0031fea3cdba212e40456 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Fri, 17 Jan 2025 09:27:54 -0800 Subject: [PATCH 01/24] Change Db to typescript and add types --- .../{requirementDb.js => requirementDb.ts} | 84 ++++++++++++------- .../mermaid/src/diagrams/requirement/types.ts | 41 +++++++++ 2 files changed, 97 insertions(+), 28 deletions(-) rename packages/mermaid/src/diagrams/requirement/{requirementDb.js => requirementDb.ts} (62%) create mode 100644 packages/mermaid/src/diagrams/requirement/types.ts diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.js b/packages/mermaid/src/diagrams/requirement/requirementDb.ts similarity index 62% rename from packages/mermaid/src/diagrams/requirement/requirementDb.js rename to packages/mermaid/src/diagrams/requirement/requirementDb.ts index 068e0fdab0..b3edbc7554 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.js +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -8,12 +8,15 @@ import { setAccDescription, clear as commonClear, } from '../common/commonDb.js'; - -let relations = []; -let latestRequirement = {}; -let requirements = new Map(); -let latestElement = {}; -let elements = new Map(); +import type { + Element, + Relation, + RelationshipType, + Requirement, + RequirementType, + RiskLevel, + VerifyType, +} from './types.js'; const RequirementType = { REQUIREMENT: 'Requirement', @@ -47,78 +50,108 @@ const Relationships = { TRACES: 'traces', }; -const addRequirement = (name, type) => { +const getInitialRequirement = (): Requirement => ({ + id: '', + text: '', + risk: RiskLevel.LOW_RISK as RiskLevel, + verifyMethod: VerifyType.VERIFY_ANALYSIS as VerifyType, + name: '', + type: RequirementType.REQUIREMENT as RequirementType, +}); + +const getInitialElement = (): Element => ({ + name: '', + type: '', + docRef: '', +}); + +// Update initial declarations +let relations: Relation[] = []; +let latestRequirement: Requirement = getInitialRequirement(); +let requirements = new Map(); +let latestElement: Element = getInitialElement(); +let elements = new Map(); + +// Add reset functions +const resetLatestRequirement = () => { + latestRequirement = getInitialRequirement(); +}; + +const resetLatestElement = () => { + latestElement = getInitialElement(); +}; + +const addRequirement = (name: string, type: RequirementType) => { if (!requirements.has(name)) { requirements.set(name, { name, type, - id: latestRequirement.id, text: latestRequirement.text, risk: latestRequirement.risk, verifyMethod: latestRequirement.verifyMethod, }); } - latestRequirement = {}; + resetLatestRequirement(); return requirements.get(name); }; const getRequirements = () => requirements; -const setNewReqId = (id) => { +const setNewReqId = (id: string) => { if (latestRequirement !== undefined) { latestRequirement.id = id; } }; -const setNewReqText = (text) => { +const setNewReqText = (text: string) => { if (latestRequirement !== undefined) { latestRequirement.text = text; } }; -const setNewReqRisk = (risk) => { +const setNewReqRisk = (risk: RiskLevel) => { if (latestRequirement !== undefined) { latestRequirement.risk = risk; } }; -const setNewReqVerifyMethod = (verifyMethod) => { +const setNewReqVerifyMethod = (verifyMethod: VerifyType) => { if (latestRequirement !== undefined) { latestRequirement.verifyMethod = verifyMethod; } }; -const addElement = (name) => { +const addElement = (name: string) => { if (!elements.has(name)) { elements.set(name, { name, type: latestElement.type, docRef: latestElement.docRef, }); - log.info('Added new requirement: ', name); + log.info('Added new element: ', name); } - latestElement = {}; + resetLatestElement(); return elements.get(name); }; const getElements = () => elements; -const setNewElementType = (type) => { +const setNewElementType = (type: string) => { if (latestElement !== undefined) { latestElement.type = type; } }; -const setNewElementDocRef = (docRef) => { +const setNewElementDocRef = (docRef: string) => { if (latestElement !== undefined) { latestElement.docRef = docRef; } }; -const addRelationship = (type, src, dst) => { +const addRelationship = (type: RelationshipType, src: string, dst: string) => { relations.push({ type, src, @@ -130,21 +163,19 @@ const getRelationships = () => relations; const clear = () => { relations = []; - latestRequirement = {}; + resetLatestRequirement(); requirements = new Map(); - latestElement = {}; + resetLatestElement(); elements = new Map(); commonClear(); }; export default { + Relationships, RequirementType, RiskLevel, VerifyType, - Relationships, - - getConfig: () => getConfig().req, - + getConfig: () => getConfig().requirement, addRequirement, getRequirements, setNewReqId, @@ -155,14 +186,11 @@ export default { getAccTitle, setAccDescription, getAccDescription, - addElement, getElements, setNewElementType, setNewElementDocRef, - addRelationship, getRelationships, - clear, }; diff --git a/packages/mermaid/src/diagrams/requirement/types.ts b/packages/mermaid/src/diagrams/requirement/types.ts new file mode 100644 index 0000000000..adec477e1f --- /dev/null +++ b/packages/mermaid/src/diagrams/requirement/types.ts @@ -0,0 +1,41 @@ +export type RequirementType = + | 'Requirement' + | 'Functional Requirement' + | 'Interface Requirement' + | 'Performance Requirement' + | 'Physical Requirement' + | 'Design Constraint'; + +export type RiskLevel = 'Low' | 'Medium' | 'High'; + +export type VerifyType = 'Analysis' | 'Demonstration' | 'Inspection' | 'Test'; + +export interface Requirement { + name: string; + type: RequirementType; + id: string; + text: string; + risk: RiskLevel; + verifyMethod: VerifyType; +} + +export type RelationshipType = + | 'contains' + | 'copies' + | 'derives' + | 'satisfies' + | 'verifies' + | 'refines' + | 'traces'; + +export interface Relation { + type: RelationshipType; + src: string; + dst: string; +} + +export interface Element { + name: string; + type: string; + docRef: string; +} From d90609b26c0cdf548d1db4a23cf6248e076d04f2 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Fri, 17 Jan 2025 11:02:47 -0800 Subject: [PATCH 02/24] Setup renderer and data collection --- .../src/diagrams/requirement/requirementDb.ts | 44 +++++++++++++++++++ .../requirement/requirementDiagram.ts | 2 +- .../requirementRenderer-unified.ts | 40 +++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.ts index b3edbc7554..cd6b8adbd4 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -1,5 +1,6 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; +import type { Node, Edge } from '../../rendering-util/types.js'; import { setAccTitle, @@ -7,6 +8,8 @@ import { getAccDescription, setAccDescription, clear as commonClear, + setDiagramTitle, + getDiagramTitle, } from '../common/commonDb.js'; import type { Element, @@ -170,6 +173,44 @@ const clear = () => { commonClear(); }; +const getData = () => { + const config = getConfig(); + const nodes: Node[] = []; + const edges: Edge[] = []; + + for (const requirement of requirements.values()) { + const node = requirement as unknown as Node; + node.shape = 'requirementBox'; + node.look = config.look; + nodes.push(node); + } + + for (const element of elements.values()) { + const node = element as unknown as Node; + node.shape = 'requirementBox'; + node.look = config.look; + + nodes.push(node); + } + + for (const relation of relations) { + let counter = 0; + const edge: Edge = { + id: `${relation.src}-${relation.dst}-${counter}`, + start: requirements.get(relation.src)?.id, + end: requirements.get(relation.dst)?.id, + type: relation.type, + classes: 'relationshipLine', + style: ['fill:none'], + }; + + edges.push(edge); + counter++; + } + + return { nodes, edges, other: {}, config }; +}; + export default { Relationships, RequirementType, @@ -186,6 +227,8 @@ export default { getAccTitle, setAccDescription, getAccDescription, + setDiagramTitle, + getDiagramTitle, addElement, getElements, setNewElementType, @@ -193,4 +236,5 @@ export default { addRelationship, getRelationships, clear, + getData, }; diff --git a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts index 619f5b0528..5fb4c3fc75 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts @@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; import parser from './parser/requirementDiagram.jison'; import db from './requirementDb.js'; import styles from './styles.js'; -import renderer from './requirementRenderer.js'; +import renderer from './requirementRenderer-unified.js'; export const diagram: DiagramDefinition = { parser, diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts b/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts new file mode 100644 index 0000000000..1d384ad641 --- /dev/null +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts @@ -0,0 +1,40 @@ +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 type { LayoutData } from '../../rendering-util/types.js'; +import utils from '../../utils.js'; + +export const draw = async function (text: string, id: string, _version: string, diag: any) { + log.info('REF0:'); + log.info('Drawing requirement diagram (unified)', id); + const { securityLevel, state: conf, layout } = getConfig(); + + const data4Layout = diag.db.getData() as LayoutData; + + // Create the root SVG - the element is the div containing the SVG element + const svg = getDiagramElement(id, securityLevel); + + data4Layout.type = diag.type; + data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); + + data4Layout.nodeSpacing = conf?.nodeSpacing || 50; + data4Layout.rankSpacing = conf?.rankSpacing || 50; + data4Layout.markers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop']; + data4Layout.diagramId = id; + await render(data4Layout, svg); + const padding = 8; + utils.insertTitle( + svg, + 'requirementDiagramTitleText', + conf?.titleTopMargin ?? 25, + diag.db.getDiagramTitle() + ); + + setupViewPortForSVG(svg, padding, 'requirementDiagram', conf?.useMaxWidth ?? true); +}; + +export default { + draw, +}; From f0e47f29fde220d0416da0c739e116a7300d6f4d Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 21 Jan 2025 11:27:09 -0800 Subject: [PATCH 03/24] Create and register requirementBox shape --- .../rendering-elements/shapes.ts | 4 + .../shapes/requirementBox.ts | 178 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index dbfc93677f..a2f8b55b21 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -58,6 +58,7 @@ import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js'; import { waveRectangle } from './shapes/waveRectangle.js'; import { windowPane } from './shapes/windowPane.js'; import { classBox } from './shapes/classBox.js'; +import { requirementBox } from './shapes/requirementBox.js'; import { kanbanItem } from './shapes/kanbanItem.js'; type ShapeHandler = ( @@ -476,6 +477,9 @@ const generateShapeMap = () => { // class diagram classBox, + + // Requirement diagram + requirementBox, } as const; const entries = [ diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts new file mode 100644 index 0000000000..42e14a0959 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts @@ -0,0 +1,178 @@ +import { updateNodeBounds } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.js'; +import { userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import type { D3Selection } from '../../../types.js'; +import { calculateTextWidth, decodeEntities } from '../../../utils.js'; +import { getConfig, sanitizeText } from '../../../diagram-api/diagramAPI.js'; +import { createText } from '../../createText.js'; +import { select } from 'd3'; +import type { Requirement, Element } from '../../../diagrams/requirement/types.js'; + +export async function requirementBox( + parent: D3Selection, + node: Node +) { + const requirementNode = node as unknown as Requirement; + const elementNode = node as unknown as Element; + const config = getConfig().requirement; + const PADDING = 20; + const GAP = 20; + const isRequirementNode = 'id' in node; + + // Add outer g element + const shapeSvg = parent + .insert('g') + .attr('class', '') + .attr('id', node.domId ?? node.id); + + let typeHeight; + if (isRequirementNode) { + typeHeight = await addText(shapeSvg, `<<${requirementNode.type}>>`, 0); + } else { + typeHeight = await addText(shapeSvg, '<<Element>>', 0); + } + + let accumulativeHeight = typeHeight; + const nameHeight = await addText(shapeSvg, requirementNode.name, accumulativeHeight); + accumulativeHeight += nameHeight + GAP; + + // Requirement + if (isRequirementNode) { + const idHeight = await addText(shapeSvg, `Id: ${requirementNode.id}`, accumulativeHeight); + accumulativeHeight += idHeight; + const textHeight = await addText(shapeSvg, `Text: ${requirementNode.text}`, accumulativeHeight); + accumulativeHeight += textHeight; + const riskHeight = await addText(shapeSvg, `Risk: ${requirementNode.risk}`, accumulativeHeight); + accumulativeHeight += riskHeight; + await addText(shapeSvg, `Verification: ${requirementNode.verifyMethod}`, accumulativeHeight); + } else { + // Element + const typeHeight = await addText( + shapeSvg, + `Type: ${elementNode.type ? elementNode.type : 'Not specified'}`, + accumulativeHeight + ); + accumulativeHeight += typeHeight; + await addText( + shapeSvg, + `Doc Ref: ${elementNode.docRef ? elementNode.docRef : 'None'}`, + accumulativeHeight + ); + } + + const totalWidth = Math.max( + (shapeSvg.node()?.getBBox().width ?? 200) + PADDING, + config?.rect_min_width ?? 200 + ); + const totalHeight = totalWidth; + const x = -totalWidth / 2; + const y = -totalHeight / 2; + + // Setup roughjs + // @ts-ignore TODO: Fix rough typings + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + // Create and center rectangle + const roughRect = rc.rectangle(x, y, totalWidth, totalHeight, options); + + const rect = shapeSvg.insert(() => roughRect, ':first-child'); + rect.attr('class', 'basic label-container'); + + // Re-translate labels now that rect is centered + // eslint-disable-next-line @typescript-eslint/no-explicit-any + shapeSvg.selectAll('.label').each((_: any, i: number, nodes: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const text = select(nodes[i]); + + const transform = text.attr('transform'); + let translateX = 0; + let translateY = 0; + if (transform) { + const regex = RegExp(/translate\(([^,]+),([^)]+)\)/); + const translate = regex.exec(transform); + if (translate) { + translateX = parseFloat(translate[1]); + translateY = parseFloat(translate[2]); + } + } + + const newTranslateY = translateY - totalHeight / 2; + let newTranslateX = x + PADDING / 2; + + // Keep type and name labels centered. + if (i === 0 || i === 1) { + newTranslateX = translateX; + } + // Set the updated transform attribute + text.attr('transform', `translate(${newTranslateX}, ${newTranslateY + PADDING})`); + }); + + // Insert divider line + const roughLine = rc.line( + x, + y + typeHeight + nameHeight + GAP, + x + totalWidth, + y + typeHeight + nameHeight + GAP, + options + ); + shapeSvg.insert(() => roughLine); + + updateNodeBounds(node, rect); + + node.intersect = function (point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +} + +async function addText( + parentGroup: D3Selection, + inputText: string, + yOffset: number, + styles: string[] = [] +) { + const textEl = parentGroup.insert('g').attr('class', 'label').attr('style', styles.join('; ')); + const config = getConfig(); + const useHtmlLabels = config.htmlLabels ?? true; + + const text = await createText( + textEl, + sanitizeText(decodeEntities(inputText)), + { + width: calculateTextWidth(inputText, config) + 50, // Add room for error when splitting text into multiple lines + classes: 'markdown-node-label', + useHtmlLabels, + }, + config + ); + let bbox; + + if (!useHtmlLabels) { + const textChild = text.children[0]; + textChild.textContent = inputText.replaceAll('>', '>').replaceAll('<', '<').trim(); + // Get the bounding box after the text update + bbox = text.getBBox(); + // Add extra height so it is similar to the html labels + bbox.height += 6; + } else { + const div = text.children[0]; + const dv = select(text); + + bbox = div.getBoundingClientRect(); + dv.attr('width', bbox.width); + dv.attr('height', bbox.height); + } + + // Center text and offset by yOffset + textEl.attr('transform', `translate(${-bbox.width / 2},${-bbox.height / 2 + yOffset})`); + return bbox.height; +} From 54ee6bd46dd5c7225915d4927b98a0c161d7f5bf Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 21 Jan 2025 11:27:29 -0800 Subject: [PATCH 04/24] Update styles --- packages/mermaid/src/diagrams/requirement/styles.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/mermaid/src/diagrams/requirement/styles.js b/packages/mermaid/src/diagrams/requirement/styles.js index 9db0fa00a2..c7a28ebaf3 100644 --- a/packages/mermaid/src/diagrams/requirement/styles.js +++ b/packages/mermaid/src/diagrams/requirement/styles.js @@ -40,6 +40,18 @@ const getStyles = (options) => ` .relationshipLabel { fill: ${options.relationLabelColor}; } + .divider { + stroke: ${options.nodeBorder}; + stroke-width: 1; + } + .label { + font-family: ${options.fontFamily}; + color: ${options.nodeTextColor || options.textColor}; + } + .label text,span { + fill: ${options.nodeTextColor || options.textColor}; + color: ${options.nodeTextColor || options.textColor}; + } `; // fill', conf.rect_fill) From 809f4eb609b3809327f4ae703c4ee1a0a8a8ece9 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 22 Jan 2025 08:16:18 -0800 Subject: [PATCH 05/24] Send all edge data and add markers --- .../src/diagrams/requirement/requirementDb.ts | 12 ++++-- .../requirementRenderer-unified.ts | 6 +-- .../src/diagrams/requirement/styles.js | 3 ++ .../rendering-elements/edgeMarker.ts | 2 + .../rendering-elements/markers.js | 39 +++++++++++++++++++ 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.ts index cd6b8adbd4..85be6062e5 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -177,7 +177,6 @@ const getData = () => { const config = getConfig(); const nodes: Node[] = []; const edges: Edge[] = []; - for (const requirement of requirements.values()) { const node = requirement as unknown as Node; node.shape = 'requirementBox'; @@ -195,13 +194,20 @@ const getData = () => { for (const relation of relations) { let counter = 0; + const isContains = relation.type === Relationships.CONTAINS; const edge: Edge = { id: `${relation.src}-${relation.dst}-${counter}`, start: requirements.get(relation.src)?.id, end: requirements.get(relation.dst)?.id, - type: relation.type, + label: `<<${relation.type}>>`, classes: 'relationshipLine', - style: ['fill:none'], + style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'], + labelpos: 'c', + thickness: 'normal', + type: 'normal', + pattern: isContains ? 'normal' : 'dashed', + arrowTypeEnd: isContains ? 'requirement_contains' : 'requirement_arrow', + look: config.look, }; edges.push(edge); diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts b/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts index 1d384ad641..7e190f2c99 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts @@ -19,9 +19,9 @@ export const draw = async function (text: string, id: string, _version: string, data4Layout.type = diag.type; data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); - data4Layout.nodeSpacing = conf?.nodeSpacing || 50; - data4Layout.rankSpacing = conf?.rankSpacing || 50; - data4Layout.markers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop']; + data4Layout.nodeSpacing = conf?.nodeSpacing ?? 50; + data4Layout.rankSpacing = conf?.rankSpacing ?? 50; + data4Layout.markers = ['requirement_contains', 'requirement_arrow']; data4Layout.diagramId = id; await render(data4Layout, svg); const padding = 8; diff --git a/packages/mermaid/src/diagrams/requirement/styles.js b/packages/mermaid/src/diagrams/requirement/styles.js index c7a28ebaf3..4e2f72bd3f 100644 --- a/packages/mermaid/src/diagrams/requirement/styles.js +++ b/packages/mermaid/src/diagrams/requirement/styles.js @@ -52,6 +52,9 @@ const getStyles = (options) => ` fill: ${options.nodeTextColor || options.textColor}; color: ${options.nodeTextColor || options.textColor}; } + .labelBkg { + background-color: ${options.edgeLabelBackground}; + } `; // fill', conf.rect_fill) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts index 5371ac32d1..0e65ef18db 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts @@ -35,6 +35,8 @@ const arrowTypesMap = { composition: 'composition', dependency: 'dependency', lollipop: 'lollipop', + requirement_arrow: 'requirement_arrow', + requirement_contains: 'requirement_contains', } as const; const addEdgeMarker = ( diff --git a/packages/mermaid/src/rendering-util/rendering-elements/markers.js b/packages/mermaid/src/rendering-util/rendering-elements/markers.js index b2592e20ec..a13bf241fc 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/markers.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/markers.js @@ -277,6 +277,43 @@ const barb = (elem, type, id) => { .append('path') .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); }; +const requirement_arrow = (elem, type, id) => { + elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-requirement_arrowEnd') + .attr('refX', 20) + .attr('refY', 10) + .attr('markerWidth', 20) + .attr('markerHeight', 20) + .attr('orient', 'auto') + .append('path') + .attr( + 'd', + `M0,0 + L20,10 + M20,10 + L0,20` + ); +}; +const requirement_contains = (elem, type, id) => { + const containsNode = elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-requirement_containsEnd') + .attr('refX', 20) + .attr('refY', 10) + .attr('markerWidth', 20) + .attr('markerHeight', 20) + .attr('orient', 'auto') + .append('g'); + + containsNode.append('circle').attr('cx', 10).attr('cy', 10).attr('r', 10).attr('fill', 'none'); + + containsNode.append('line').attr('x1', 0).attr('x2', 20).attr('y1', 10).attr('y2', 10); + + containsNode.append('line').attr('y1', 0).attr('y2', 20).attr('x1', 10).attr('x2', 10); +}; // TODO rename the class diagram markers to something shape descriptive and semantic free const markers = { @@ -289,5 +326,7 @@ const markers = { circle, cross, barb, + requirement_arrow, + requirement_contains, }; export default insertMarkers; From fe833e6d166674545083aa43f117688306d90e43 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 22 Jan 2025 08:49:01 -0800 Subject: [PATCH 06/24] Fix elements not showing up if they were involved in a relation --- packages/mermaid/src/diagrams/requirement/requirementDb.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.ts index 85be6062e5..ac5f2535b3 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -188,6 +188,7 @@ const getData = () => { const node = element as unknown as Node; node.shape = 'requirementBox'; node.look = config.look; + node.id = element.name; nodes.push(node); } @@ -197,8 +198,8 @@ const getData = () => { const isContains = relation.type === Relationships.CONTAINS; const edge: Edge = { id: `${relation.src}-${relation.dst}-${counter}`, - start: requirements.get(relation.src)?.id, - end: requirements.get(relation.dst)?.id, + start: requirements.get(relation.src)?.id ?? elements.get(relation.src)?.name, + end: requirements.get(relation.dst)?.id ?? elements.get(relation.dst)?.name, label: `<<${relation.type}>>`, classes: 'relationshipLine', style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'], From f7648e85d900b461afe770db725b4bc67ac92c87 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 22 Jan 2025 09:00:48 -0800 Subject: [PATCH 07/24] Add support for direction statement --- .../requirement/parser/requirementDiagram.jison | 16 ++++++++++++++++ .../src/diagrams/requirement/requirementDb.ts | 10 +++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison b/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison index 6d0f7b1222..0001f41652 100644 --- a/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison +++ b/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison @@ -22,6 +22,10 @@ accDescr\s*":"\s* { this.begin("ac accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} [\}] { this.popState(); } [^\}]* return "acc_descr_multiline_value"; +.*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'; (\r?\n)+ return 'NEWLINE'; \s+ /* skip all whitespace */ \#[^\n]* /* skip comments */ @@ -101,8 +105,20 @@ diagram | elementDef diagram | relationshipDef diagram | directive diagram + | direction diagram | NEWLINE diagram; +direction + : direction_tb + { yy.setDirection('TB');} + | direction_bt + { yy.setDirection('BT');} + | direction_rl + { yy.setDirection('RL');} + | direction_lr + { yy.setDirection('LR');} + ; + requirementDef : requirementType requirementName STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1) }; diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.ts index ac5f2535b3..64033157fd 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -53,6 +53,12 @@ const Relationships = { TRACES: 'traces', }; +let direction = 'TB'; +const getDirection = () => direction; +const setDirection = (dir: string) => { + direction = dir; +}; + const getInitialRequirement = (): Requirement => ({ id: '', text: '', @@ -215,7 +221,7 @@ const getData = () => { counter++; } - return { nodes, edges, other: {}, config }; + return { nodes, edges, other: {}, config, direction: getDirection() }; }; export default { @@ -236,6 +242,8 @@ export default { getAccDescription, setDiagramTitle, getDiagramTitle, + getDirection, + setDirection, addElement, getElements, setNewElementType, From 081681f05b81224eef3b58b07f8809fea5eac459 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 22 Jan 2025 11:07:15 -0800 Subject: [PATCH 08/24] Add styling support for requirement diagram and box shape --- .../parser/requirementDiagram.jison | 35 +++++++++- .../src/diagrams/requirement/requirementDb.ts | 19 ++++++ .../mermaid/src/diagrams/requirement/types.ts | 2 + .../shapes/requirementBox.ts | 66 ++++++++++++++----- 4 files changed, 106 insertions(+), 16 deletions(-) diff --git a/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison b/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison index 0001f41652..16181ba981 100644 --- a/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison +++ b/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.jison @@ -9,6 +9,7 @@ %x string %x token %x unqString +%x style %x acc_title %x acc_descr %x acc_descr_multiline @@ -72,6 +73,17 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili "type" return 'TYPE'; "docref" return 'DOCREF'; +"style" { this.begin("style"); return 'STYLE'; } +