diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml index 5ed526bc5..077cc568b 100644 --- a/.github/pr-labeler.yml +++ b/.github/pr-labeler.yml @@ -1,4 +1,3 @@ 'Type: Bug / Error': 'bug/*' 'Type: Enhancement': 'feature/*' 'Type: Other': 'other/*' -'Type: Dependabot': 'dependabot/*' diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index b6b424f3e..46cbd91b8 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -11,4 +11,4 @@ jobs: - uses: andymckay/labeler@1.0.3 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - labels: "Status: Triage" + add-labels: "Status: Triage" diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index b6fab95ce..107743c6a 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,6 +1,6 @@ name: Apply labels to PR on: - pull_request: + pull_request_target: types: [opened] jobs: diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index 66fd1c51f..eea11aaf3 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -370,7 +370,21 @@ describe('Class diagram V2', () => { ); cy.get('svg'); }); - it('16: should handle the direction statemment with TB', () => { + + it('16a: should render a simple class diagram with static field', () => { + imgSnapshotTest( + ` + classDiagram-v2 + Foo { + +String bar$ + } + `, + {logLevel : 1, flowchart: { "htmlLabels": false },} + ); + cy.get('svg'); + }); + + it('16b: should handle the direction statemnent with TB', () => { imgSnapshotTest( ` classDiagram @@ -394,55 +408,8 @@ describe('Class diagram V2', () => { ); cy.get('svg'); }); - it('17: should handle the direction statemment with BT', () => { - imgSnapshotTest( - ` - classDiagram - direction BT - class Student { - -idCard : IdCard - } - class IdCard{ - -id : int - -name : string - } - class Bike{ - -id : int - -name : string - } - Student "1" --o "1" IdCard : carries - Student "1" --o "1" Bike : rides - - `, - {logLevel : 1, flowchart: { "htmlLabels": false },} - ); - cy.get('svg'); - }); - it('17: should handle the direction statemment with RL', () => { - imgSnapshotTest( - ` - classDiagram - direction RL - class Student { - -idCard : IdCard - } - class IdCard{ - -id : int - -name : string - } - class Bike{ - -id : int - -name : string - } - Student "1" --o "1" IdCard : carries - Student "1" --o "1" Bike : rides - - `, - {logLevel : 1, flowchart: { "htmlLabels": false },} - ); - cy.get('svg'); - }); - it('18: should handle the direction statemment with LR', () => { + + it('18: should handle the direction statemnent with LR', () => { imgSnapshotTest( ` classDiagram @@ -466,4 +433,52 @@ describe('Class diagram V2', () => { ); cy.get('svg'); }); + it('17a: should handle the direction statemnent with BT', () => { + imgSnapshotTest( + ` + classDiagram + direction BT + class Student { + -idCard : IdCard + } + class IdCard{ + -id : int + -name : string + } + class Bike{ + -id : int + -name : string + } + Student "1" --o "1" IdCard : carries + Student "1" --o "1" Bike : rides + + `, + {logLevel : 1, flowchart: { "htmlLabels": false },} + ); + cy.get('svg'); + }); + it('17b: should handle the direction statemment with RL', () => { + imgSnapshotTest( + ` + classDiagram + direction RL + class Student { + -idCard : IdCard + } + class IdCard{ + -id : int + -name : string + } + class Bike{ + -id : int + -name : string + } + Student "1" --o "1" IdCard : carries + Student "1" --o "1" Bike : rides + + `, + {logLevel : 1, flowchart: { "htmlLabels": false },} + ); + cy.get('svg'); + }); }); diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js index 1b4b0b9a2..fc93fb5bb 100644 --- a/cypress/integration/rendering/erDiagram.spec.js +++ b/cypress/integration/rendering/erDiagram.spec.js @@ -186,4 +186,15 @@ describe('Entity Relationship Diagram', () => { cy.get('svg'); }); + it('should render entities with keys and comments', () => { + renderGraph( + ` + erDiagram + BOOK { string title PK "comment"} + `, + { logLevel : 1 } + ); + cy.get('svg'); + }); + }); diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index 7a35f79c5..cdb9ae8a8 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -173,6 +173,18 @@ context('Sequence diagram', () => { {} ); }); + it('should be possible to use actor symbols instead of boxes', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + actor Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice + `, + {} + ); + }); it('should render long notes left of actor', () => { imgSnapshotTest( ` diff --git a/cypress/platform/knsv.html b/cypress/platform/knsv.html index 4921001fa..dcec97928 100644 --- a/cypress/platform/knsv.html +++ b/cypress/platform/knsv.html @@ -10,9 +10,10 @@ + + + diff --git a/docs/landing/sequence-diagram.png b/docs/landing/sequence-diagram.png index 4268242b0..8c51ac1c5 100644 Binary files a/docs/landing/sequence-diagram.png and b/docs/landing/sequence-diagram.png differ diff --git a/docs/landing/state.png b/docs/landing/state.png index 35d485bd9..2ef66ea2f 100644 Binary files a/docs/landing/state.png and b/docs/landing/state.png differ diff --git a/docs/n00b-gettingStarted.md b/docs/n00b-gettingStarted.md index ea89b1e60..84ddabeb6 100644 --- a/docs/n00b-gettingStarted.md +++ b/docs/n00b-gettingStarted.md @@ -19,7 +19,7 @@ This section talks about the different ways to deploy Mermaid. Learning the [Syn > More in depth information can be found on [Usage](./usage.md). -## 1. Using [The Live Editor](https://mermaidjs.github.io/mermaid-live-editor/edit). +## 1. Using [The Live Editor](https://mermaid-js.github.io/mermaid-live-editor/edit). ![EditingProcess](./img/Editing-process.png) diff --git a/docs/sequenceDiagram.md b/docs/sequenceDiagram.md index 8e688d567..41f94aa24 100644 --- a/docs/sequenceDiagram.md +++ b/docs/sequenceDiagram.md @@ -36,18 +36,53 @@ appearance by doing the following: ``` sequenceDiagram - participant John participant Alice - Alice->>John: Hello John, how are you? - John-->>Alice: Great! + participant Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice ``` ```mermaid + sequenceDiagram + participant Alice + participant Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice +``` + +### Actors + +If you specifically want to use the actor symbol instead of a rectangle with text you can do so by using actor statements as per below. +``` sequenceDiagram - participant John - participant Alice - Alice->>John: Hello John, how are you? - John-->>Alice: Great! + actor Alice + actor Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice +``` + +```mermaid + sequenceDiagram + actor Alice + actor Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice +``` + +``` +sequenceDiagram + actor Alice + actor Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice +``` + +```mermaid + sequenceDiagram + actor Alice + actor Bob + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice ``` ### Aliases diff --git a/docs/usage.md b/docs/usage.md index ff4b1dab1..7b663901d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -292,17 +292,16 @@ module.exports = (options) -> ## Advanced usage -**Error handling** +**Syntax validation without rendering (Work in Progress)** + +The **mermaid.parse(txt)** function validates graph definitions without rendering a graph. **[This function is still a work in progress](https://github.com/mermaid-js/mermaid/issues/1066), find alternatives below.** + +The function **mermaid.parse(txt)**, takes a text string as an argument and returns true if the definition follows mermaid's syntax and +false if it does not. The parseError function will be called when the parse function returns false. When the parser encounters invalid syntax the **mermaid.parseError** function is called. It is possible to override this function in order to handle the error in an application-specific way. -**Parsing text without rendering** - -It is also possible to validate the syntax before rendering in order to streamline the user experience. The function -**mermaid.parse(txt)** takes a text string as an argument and returns true if the text is syntactically correct and -false if it is not. The parseError function will be called when the parse function returns false. - The code-example below in meta code illustrates how this could work: ```javascript @@ -320,6 +319,8 @@ var textFieldUpdated = function(){ bindEventHandler('change', 'code', textFieldUpdated); ``` +**Alternative to mermaid.parse():** +One effective and more future-proof method of validating your graph deinitions, is to paste and render them via the [Mermaid Live Editor](https://mermaid-js.github.io/mermaid-live-editor/). This will ensure that your code is compliant with the syntax of Mermaid's most recent version. ## Configuration @@ -395,9 +396,7 @@ This way of setting the configuration is deprecated. Instead the preferred way i ## Using the mermaid.init call -# -Is it possible to set some configuration via the mermaid object. The two parameters that are supported using this -approach are: +To set some configuration via the mermaid object. The two parameters that are supported using this approach are: * mermaid_config.startOnLoad * mermaid_config.htmlLabels diff --git a/img/GitHub-Mark-32px.png b/img/GitHub-Mark-32px.png index ea6ff545a..395207b85 100644 Binary files a/img/GitHub-Mark-32px.png and b/img/GitHub-Mark-32px.png differ diff --git a/img/gray-journey.png b/img/gray-journey.png index bfe2be03a..5fbbb5a0b 100644 Binary files a/img/gray-journey.png and b/img/gray-journey.png differ diff --git a/img/gray-sequence.png b/img/gray-sequence.png index 5f08f461e..e4fb1e9da 100644 Binary files a/img/gray-sequence.png and b/img/gray-sequence.png differ diff --git a/img/gray-user-journey.png b/img/gray-user-journey.png index 332539f5f..9f34b2bf4 100644 Binary files a/img/gray-user-journey.png and b/img/gray-user-journey.png differ diff --git a/package.json b/package.json index 17c601a7d..24b769bf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "8.12.0", + "version": "8.13.0", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "main": "dist/mermaid.core.js", "keywords": [ diff --git a/src/dagre-wrapper/nodes.js b/src/dagre-wrapper/nodes.js index dca4fd225..507b493db 100644 --- a/src/dagre-wrapper/nodes.js +++ b/src/dagre-wrapper/nodes.js @@ -672,13 +672,21 @@ const class_box = (parent, node) => { } const classAttributes = []; node.classData.members.forEach((str) => { - let parsedText = parseMember(str).displayText; + const parsedInfo = parseMember(str); + let parsedText = parsedInfo.displayText; if (getConfig().flowchart.htmlLabels) { parsedText = parsedText.replace(//g, '>'); } const lbl = labelContainer .node() - .appendChild(createLabel(parsedText, node.labelStyle, true, true)); + .appendChild( + createLabel( + parsedText, + parsedInfo.cssStyle ? parsedInfo.cssStyle : node.labelStyle, + true, + true + ) + ); let bbox = lbl.getBBox(); if (evaluate(getConfig().flowchart.htmlLabels)) { const div = lbl.children[0]; diff --git a/src/diagrams/class/svgDraw.js b/src/diagrams/class/svgDraw.js index 5c58d0578..d9cd52161 100644 --- a/src/diagrams/class/svgDraw.js +++ b/src/diagrams/class/svgDraw.js @@ -274,7 +274,7 @@ export const drawClass = function (elem, classDef, conf) { }; export const parseMember = function (text) { - const fieldRegEx = /(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+)/; + const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+) *(\*|\$)?$/; const methodRegEx = /^([+|\-|~|#])?(\w+) *\( *(.*)\) *(\*|\$)? *(\w*[~|[\]]*\s*\w*~?)$/; let fieldMatch = text.match(fieldRegEx); @@ -290,6 +290,7 @@ export const parseMember = function (text) { }; const buildFieldDisplay = function (parsedText) { + let cssStyle = ''; let displayText = ''; try { @@ -297,15 +298,17 @@ const buildFieldDisplay = function (parsedText) { let fieldType = parsedText[2] ? parsedText[2].trim() : ''; let genericType = parsedText[3] ? parseGenericTypes(parsedText[3].trim()) : ''; let fieldName = parsedText[4] ? parsedText[4].trim() : ''; + let classifier = parsedText[5] ? parsedText[5].trim() : ''; displayText = visibility + fieldType + genericType + ' ' + fieldName; + cssStyle = parseClassifier(classifier); } catch (err) { displayText = parsedText; } return { displayText: displayText, - cssStyle: '', + cssStyle: cssStyle, }; }; @@ -321,7 +324,6 @@ const buildMethodDisplay = function (parsedText) { let returnType = parsedText[5] ? ' : ' + parseGenericTypes(parsedText[5]).trim() : ''; displayText = visibility + methodName + '(' + parameters + ')' + returnType; - cssStyle = parseClassifier(classifier); } catch (err) { displayText = parsedText; diff --git a/src/diagrams/class/svgDraw.spec.js b/src/diagrams/class/svgDraw.spec.js index b6e26dbf8..ae5882349 100644 --- a/src/diagrams/class/svgDraw.spec.js +++ b/src/diagrams/class/svgDraw.spec.js @@ -51,7 +51,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle abstract classifier', function () { + it('should handle abstract method classifier', function () { const str = 'foo()*'; let actual = svgDraw.parseMember(str); @@ -59,7 +59,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe('font-style:italic;'); }); - it('should handle static classifier', function () { + it('should handle static method classifier', function () { const str = 'foo()$'; let actual = svgDraw.parseMember(str); @@ -156,5 +156,13 @@ describe('class member Renderer, ', function () { expect(actual.displayText).toBe('List ids'); expect(actual.cssStyle).toBe(''); }); + + it('should handle static field classifier', function () { + const str = 'String foo$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('String foo'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); }); }); diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index d96fd8957..27b125828 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -35,13 +35,20 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { const attrFontSize = conf.fontSize * 0.85; const labelBBox = entityTextNode.node().getBBox(); const attributeNodes = []; // Intermediate storage for attribute nodes created so that we can do a second pass + let hasKeyType = false; + let hasComment = false; + let maxWidth = 0; let maxTypeWidth = 0; let maxNameWidth = 0; + let maxKeyWidth = 0; + let maxCommentWidth = 0; let cumulativeHeight = labelBBox.height + heightPadding * 2; let attrNum = 1; attributes.forEach((item) => { const attrPrefix = `${entityTextNode.node().id}-attr-${attrNum}`; + let nodeWidth = 0; + let nodeHeight = 0; // Add a text node for the attribute type const typeNode = groupNode @@ -73,16 +80,70 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { ) .text(item.attributeName); - // Keep a reference to the nodes so that we can iterate through them later - attributeNodes.push({ tn: typeNode, nn: nameNode }); + const attributeNode = {}; + attributeNode.tn = typeNode; + attributeNode.nn = nameNode; const typeBBox = typeNode.node().getBBox(); const nameBBox = nameNode.node().getBBox(); - maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width); maxNameWidth = Math.max(maxNameWidth, nameBBox.width); + nodeWidth += typeBBox.width; + nodeWidth += nameBBox.width; - cumulativeHeight += Math.max(typeBBox.height, nameBBox.height) + heightPadding * 2; + nodeHeight = Math.max(typeBBox.height, nameBBox.height); + + if (hasKeyType || item.attributeKeyType !== undefined) { + const keyTypeNode = groupNode + .append('text') + .attr('class', 'er entityLabel') + .attr('id', `${attrPrefix}-name`) + .attr('x', 0) + .attr('y', 0) + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'left') + .attr( + 'style', + 'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px' + ) + .text(item.attributeKeyType || ''); + + attributeNode.kn = keyTypeNode; + const keyTypeBBox = keyTypeNode.node().getBBox(); + nodeWidth += keyTypeBBox.width; + maxKeyWidth = Math.max(maxKeyWidth, nodeWidth); + nodeHeight = Math.max(nodeHeight, keyTypeBBox.height); + hasKeyType = true; + } + + if (hasComment || item.attributeComment !== undefined) { + const commentNode = groupNode + .append('text') + .attr('class', 'er entityLabel') + .attr('id', `${attrPrefix}-name`) + .attr('x', 0) + .attr('y', 0) + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'left') + .attr( + 'style', + 'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px' + ) + .text(item.attributeComment || ''); + + attributeNode.cn = commentNode; + const commentNodeBBox = commentNode.node().getBBox(); + nodeWidth += commentNodeBBox.width; + maxCommentWidth = Math.max(nodeWidth, nameBBox.width); + nodeHeight = Math.max(nodeHeight, commentNodeBBox.height); + hasComment = true; + } + + attributeNode.height = nodeHeight; + // Keep a reference to the nodes so that we can iterate through them later + attributeNodes.push(attributeNode); + maxWidth = Math.max(maxWidth, nodeWidth); + cumulativeHeight += nodeHeight + heightPadding * 2; attrNum += 1; }); @@ -90,10 +151,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { const bBox = { width: Math.max( conf.minEntityWidth, - Math.max( - labelBBox.width + conf.entityPadding * 2, - maxTypeWidth + maxNameWidth + widthPadding * 4 - ) + Math.max(labelBBox.width + conf.entityPadding * 2, maxWidth + widthPadding * 4) ), height: attributes.length > 0 @@ -102,7 +160,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { }; // There might be some spare width for padding out attributes if the entity name is very long - const spareWidth = Math.max(0, bBox.width - (maxTypeWidth + maxNameWidth) - widthPadding * 4); + const spareWidth = Math.max(0, bBox.width - maxWidth - widthPadding * 4); if (attributes.length > 0) { // Position the entity label near the top of the entity bounding box @@ -115,51 +173,85 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { let heightOffset = labelBBox.height + heightPadding * 2; // Start at the bottom of the entity label let attribStyle = 'attributeBoxOdd'; // We will flip the style on alternate rows to achieve a banded effect - attributeNodes.forEach((nodePair) => { + attributeNodes.forEach((attributeNode) => { // Calculate the alignment y co-ordinate for the type/name of the attribute - const alignY = - heightOffset + - heightPadding + - Math.max(nodePair.tn.node().getBBox().height, nodePair.nn.node().getBBox().height) / 2; + const alignY = heightOffset + heightPadding + attributeNode.height / 2; // Position the type of the attribute - nodePair.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')'); + attributeNode.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')'); // Insert a rectangle for the type const typeRect = groupNode - .insert('rect', '#' + nodePair.tn.node().id) + .insert('rect', '#' + attributeNode.tn.node().id) .attr('class', `er ${attribStyle}`) .attr('fill', conf.fill) .attr('fill-opacity', '100%') .attr('stroke', conf.stroke) .attr('x', 0) .attr('y', heightOffset) - .attr('width', maxTypeWidth + widthPadding * 2 + spareWidth / 2) - .attr('height', nodePair.tn.node().getBBox().height + heightPadding * 2); + .attr('width', maxTypeWidth * 2 + spareWidth / 2) + .attr('height', attributeNode.tn.node().getBBox().height + heightPadding * 2); // Position the name of the attribute - nodePair.nn.attr( + attributeNode.nn.attr( 'transform', 'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')' ); // Insert a rectangle for the name groupNode - .insert('rect', '#' + nodePair.nn.node().id) + .insert('rect', '#' + attributeNode.nn.node().id) .attr('class', `er ${attribStyle}`) .attr('fill', conf.fill) .attr('fill-opacity', '100%') .attr('stroke', conf.stroke) .attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`) - //.attr('x', maxTypeWidth + (widthPadding * 2)) .attr('y', heightOffset) .attr('width', maxNameWidth + widthPadding * 2 + spareWidth / 2) - .attr('height', nodePair.nn.node().getBBox().height + heightPadding * 2); + .attr('height', attributeNode.nn.node().getBBox().height + heightPadding * 2); + + if (hasKeyType) { + // Position the name of the attribute + attributeNode.kn.attr( + 'transform', + 'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')' + ); + + // Insert a rectangle for the name + groupNode + .insert('rect', '#' + attributeNode.kn.node().id) + .attr('class', `er ${attribStyle}`) + .attr('fill', conf.fill) + .attr('fill-opacity', '100%') + .attr('stroke', conf.stroke) + .attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`) + .attr('y', heightOffset) + .attr('width', maxKeyWidth + widthPadding * 2 + spareWidth / 2) + .attr('height', attributeNode.kn.node().getBBox().height + heightPadding * 2); + } + + if (hasComment) { + // Position the name of the attribute + attributeNode.cn.attr( + 'transform', + 'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')' + ); + + // Insert a rectangle for the name + groupNode + .insert('rect', '#' + attributeNode.cn.node().id) + .attr('class', `er ${attribStyle}`) + .attr('fill', conf.fill) + .attr('fill-opacity', '100%') + .attr('stroke', conf.stroke) + .attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`) + .attr('y', heightOffset) + .attr('width', maxCommentWidth + widthPadding * 2 + spareWidth / 2) + .attr('height', attributeNode.cn.node().getBBox().height + heightPadding * 2); + } // Increment the height offset to move to the next row - heightOffset += - Math.max(nodePair.tn.node().getBBox().height, nodePair.nn.node().getBBox().height) + - heightPadding * 2; + heightOffset += attributeNode.height + heightPadding * 2; // Flip the attribute style for row banding attribStyle = attribStyle == 'attributeBoxOdd' ? 'attributeBoxEven' : 'attributeBoxOdd'; diff --git a/src/diagrams/er/parser/erDiagram.jison b/src/diagrams/er/parser/erDiagram.jison index 6e4815f28..7c023640e 100644 --- a/src/diagrams/er/parser/erDiagram.jison +++ b/src/diagrams/er/parser/erDiagram.jison @@ -18,7 +18,9 @@ "erDiagram" return 'ER_DIAGRAM'; "{" { this.begin("block"); return 'BLOCK_START'; } \s+ /* skip whitespace in block */ -[A-Za-z][A-Za-z0-9\-_]* { return 'ATTRIBUTE_WORD'; } +(?:PK)|(?:FK) return 'ATTRIBUTE_KEY' +[A-Za-z][A-Za-z0-9\-_]* return 'ATTRIBUTE_WORD' +\"[^"]*\" return 'COMMENT'; [\n]+ /* nothing */ "}" { this.popState(); return 'BLOCK_STOP'; } . return yytext[0]; @@ -95,6 +97,9 @@ attributes attribute : attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; } + | attributeType attributeName attributeKeyType { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3 }; } + | attributeType attributeName COMMENT { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; } + | attributeType attributeName attributeKeyType COMMENT { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3, attributeComment: $4 }; } ; attributeType @@ -105,6 +110,10 @@ attributeName : ATTRIBUTE_WORD { $$=$1; } ; +attributeKeyType + : ATTRIBUTE_KEY { $$=$1; } + ; + relSpec : cardinality relType cardinality { diff --git a/src/diagrams/er/parser/erDiagram.spec.js b/src/diagrams/er/parser/erDiagram.spec.js index 3afd5fe93..8089606ac 100644 --- a/src/diagrams/er/parser/erDiagram.spec.js +++ b/src/diagrams/er/parser/erDiagram.spec.js @@ -42,6 +42,36 @@ describe('when parsing ER diagram it...', function () { expect(entities[entity].attributes.length).toBe(1); }); + it('should allow an entity with a single attribute to be defined with a key', function () { + const entity = 'BOOK'; + const attribute = 'string title PK'; + + erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`); + const entities = erDb.getEntities(); + expect(Object.keys(entities).length).toBe(1); + expect(entities[entity].attributes.length).toBe(1); + }); + + it('should allow an entity with a single attribute to be defined with a comment', function () { + const entity = 'BOOK'; + const attribute = `string title "comment"`; + + erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`); + const entities = erDb.getEntities(); + expect(Object.keys(entities).length).toBe(1); + expect(entities[entity].attributes.length).toBe(1); + }); + + it('should allow an entity with a single attribute to be defined with a key and a comment', function () { + const entity = 'BOOK'; + const attribute = `string title PK "comment"`; + + erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`); + const entities = erDb.getEntities(); + expect(Object.keys(entities).length).toBe(1); + expect(entities[entity].attributes.length).toBe(1); + }); + it('should allow an entity with multiple attributes to be defined', function () { const entity = 'BOOK'; const attribute1 = 'string title'; diff --git a/src/diagrams/sequence/parser/sequenceDiagram.jison b/src/diagrams/sequence/parser/sequenceDiagram.jison index 5ffd4b276..5f4b7aae9 100644 --- a/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -33,6 +33,7 @@ \%%(?!\{)[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */ "participant" { this.begin('ID'); return 'participant'; } +"actor" { this.begin('ID'); return 'participant_actor'; } [^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } "as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; } (?:) { this.popState(); this.popState(); return 'NEWLINE'; } @@ -103,8 +104,10 @@ directive ; statement - : 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.description=yy.parseMessage($4); $$=$2;} - | 'participant' actor 'NEWLINE' {$$=$2;} + : 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;} + | 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;} | signal 'NEWLINE' | autonumber {yy.enableSequenceNumbers()} | 'activate' actor 'NEWLINE' {$$={type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $2};} @@ -197,9 +200,13 @@ signal { $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]} ; -actor - : ACTOR {$$={type: 'addActor', actor:$1}} - ; +// actor +// : actor_participant +// | actor_actor +// ; + +actor: ACTOR {$$={ type: 'addParticipant', actor:$1}}; +// actor_actor: ACTOR {$$={type: 'addActor', actor:$1}}; signaltype : SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; } diff --git a/src/diagrams/sequence/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js index 8df1d0b10..eb3113e25 100644 --- a/src/diagrams/sequence/sequenceDb.js +++ b/src/diagrams/sequence/sequenceDb.js @@ -15,14 +15,17 @@ export const parseDirective = function (statement, context, type) { mermaidAPI.parseDirective(this, statement, context, type); }; -export const addActor = function (id, name, description) { +export const addActor = function (id, name, description, type) { // Don't allow description nulling const old = actors[id]; if (old && name === old.name && description == null) return; // Don't allow null descriptions, either if (description == null || description.text == null) { - description = { text: name, wrap: null }; + description = { text: name, wrap: null, type }; + } + if (type == null || description.text == null) { + description = { text: name, wrap: null, type }; } actors[id] = { @@ -30,6 +33,7 @@ export const addActor = function (id, name, description) { description: description.text, wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap, prevActor: prevActor, + type: type || 'participant', }; if (prevActor && actors[prevActor]) { actors[prevActor].nextActor = id; @@ -218,8 +222,11 @@ export const apply = function (param) { }); } else { switch (param.type) { + case 'addParticipant': + addActor(param.actor, param.actor, param.description, 'participant'); + break; case 'addActor': - addActor(param.actor, param.actor, param.description); + addActor(param.actor, param.actor, param.description, 'actor'); break; case 'activeStart': addSignal(param.actor, undefined, undefined, param.signalType); diff --git a/src/diagrams/sequence/sequenceDiagram.spec.js b/src/diagrams/sequence/sequenceDiagram.spec.js index 12ca9451d..ae167197f 100644 --- a/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/src/diagrams/sequence/sequenceDiagram.spec.js @@ -121,6 +121,55 @@ B-->A: I am good thanks!`; mermaidAPI.parse(str); + const actors = parser.yy.getActors(); + + expect(Object.keys(actors)).toEqual(['A', 'B']); + expect(actors.A.description).toBe('Alice'); + expect(actors.B.description).toBe('Bob'); + + const messages = parser.yy.getMessages(); + expect(messages.length).toBe(2); + expect(messages[0].from).toBe('A'); + expect(messages[1].from).toBe('B'); + }); + it('it should alias a mix of actors and participants apa12', function() { + const str = ` +sequenceDiagram + actor Alice as Alice2 + actor Bob + participant John as John2 + participant Mandy + Alice->>Bob: Hi Bob + Bob->>Alice: Hi Alice + Alice->>John: Hi John + John->>Mandy: Hi Mandy + Mandy ->>Joan: Hi Joan`; + + mermaidAPI.parse(str); + + const actors = parser.yy.getActors(); + expect(Object.keys(actors)).toEqual(['Alice', 'Bob', 'John', 'Mandy', 'Joan']); + expect(actors.Alice.description).toBe('Alice2'); + expect(actors.Alice.type).toBe('actor'); + expect(actors.Bob.description).toBe('Bob'); + expect(actors.John.type).toBe('participant'); + expect(actors.Joan.type).toBe('participant'); + + const messages = parser.yy.getMessages(); + expect(messages.length).toBe(5); + expect(messages[0].from).toBe('Alice'); + expect(messages[4].to).toBe('Joan'); + }); + it('it should alias actors apa13', function() { + const str = ` +sequenceDiagram +actor A as Alice +actor B as Bob +A->B:Hello Bob, how are you? +B-->A: I am good thanks!`; + + mermaidAPI.parse(str); + const actors = parser.yy.getActors(); expect(Object.keys(actors)).toEqual(['A', 'B']); expect(actors.A.description).toBe('Alice'); @@ -1452,7 +1501,7 @@ participant Alice`; expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); expect(bounds.stopx).toBe(conf.width); - expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height); + expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height + conf.boxMargin); }); }); }); @@ -1501,7 +1550,7 @@ participant Alice expect(bounds.startx).toBe(0); expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); - expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height); + expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height + mermaid.sequence.boxMargin); }); it('it should handle one actor, when logLevel is 3', function() { const str = ` @@ -1519,6 +1568,6 @@ participant Alice expect(bounds.startx).toBe(0); expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); - expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height); + expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height + mermaid.sequence.boxMargin); }); }); diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index beee944db..af7c75426 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -1,5 +1,5 @@ import { select, selectAll } from 'd3'; -import svgDraw, { drawText } from './svgDraw'; +import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw'; import { log } from '../../logger'; import { parser } from './parser/sequenceDiagram'; import common from '../common/common'; @@ -421,7 +421,7 @@ export const drawActors = function (diagram, actors, actorKeys, verticalPos) { // Draw the actors let prevWidth = 0; let prevMargin = 0; - + let maxHeight = 0; for (let i = 0; i < actorKeys.length; i++) { const actor = actors[actorKeys[i]]; @@ -434,7 +434,8 @@ export const drawActors = function (diagram, actors, actorKeys, verticalPos) { actor.y = verticalPos; // Draw the box with the attached line - svgDraw.drawActor(diagram, actor, conf); + const height = svgDraw.drawActor(diagram, actor, conf); + maxHeight = Math.max(maxHeight, height); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height); prevWidth += actor.width; @@ -443,7 +444,7 @@ export const drawActors = function (diagram, actors, actorKeys, verticalPos) { } // Add a margin between the actor boxes and the first arrow - bounds.bumpVerticalPos(conf.height); + bounds.bumpVerticalPos(maxHeight); }; export const setConf = function (cnf) { @@ -688,6 +689,8 @@ export const draw = function (text, id) { // Draw actors below diagram bounds.bumpVerticalPos(conf.boxMargin * 2); drawActors(diagram, actors, actorKeys, bounds.getVerticalPos()); + bounds.bumpVerticalPos(conf.boxMargin); + fixLifeLineHeights(diagram, bounds.getVerticalPos()); } const { bounds: box } = bounds.getBounds(); diff --git a/src/diagrams/sequence/styles.js b/src/diagrams/sequence/styles.js index eddbec17b..50632d449 100644 --- a/src/diagrams/sequence/styles.js +++ b/src/diagrams/sequence/styles.js @@ -95,6 +95,15 @@ const getStyles = (options) => fill: ${options.activationBkgColor}; stroke: ${options.activationBorderColor}; } + .actor-man line { + stroke: ${options.actorBorder}; + fill: ${options.actorBkg}; + } + .actor-man circle, line { + stroke: ${options.actorBorder}; + fill: ${options.actorBkg}; + stroke-width: 2px; + } `; export default getStyles; diff --git a/src/diagrams/sequence/svgDraw.js b/src/diagrams/sequence/svgDraw.js index e30d50eb1..a574ddfdf 100644 --- a/src/diagrams/sequence/svgDraw.js +++ b/src/diagrams/sequence/svgDraw.js @@ -181,13 +181,22 @@ export const drawLabel = function (elem, txtObject) { }; let actorCnt = -1; + +export const fixLifeLineHeights = (diagram, bounds) => { + if (!diagram.selectAll) return; + diagram + .selectAll('.actor-line') + .attr('class', '200') + .attr('y2', bounds - 55); +}; + /** * Draws an actor in the diagram with the attached line * @param elem - The diagram we'll draw to. * @param actor - The actor to draw. * @param conf - drawText implementation discriminator object */ -export const drawActor = function (elem, actor, conf) { +const drawActorTypeParticipant = function (elem, actor, conf) { const center = actor.x + actor.width / 2; const g = elem.append('g'); @@ -213,7 +222,7 @@ export const drawActor = function (elem, actor, conf) { rect.class = 'actor'; rect.rx = 3; rect.ry = 3; - drawRect(g, rect); + const rectElem = drawRect(g, rect); _drawTextCandidateFunc(conf)( actor.description, @@ -225,6 +234,105 @@ export const drawActor = function (elem, actor, conf) { { class: 'actor' }, conf ); + + let height = actor.height; + if (rectElem.node) { + const bounds = rectElem.node().getBBox(); + actor.height = bounds.height; + height = bounds.height; + } + return height; +}; + +const drawActorTypeActor = function (elem, actor, conf) { + const center = actor.x + actor.width / 2; + + if (actor.y === 0) { + actorCnt++; + elem + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', 80) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999'); + } + const actElem = elem.append('g'); + actElem.attr('class', 'actor-man'); + + const rect = getNoteRect(); + rect.x = actor.x; + rect.y = actor.y; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + rect.rx = 3; + rect.ry = 3; + // drawRect(actElem, rect); + + actElem + .append('line') + .attr('id', 'actor-man-torso' + actorCnt) + .attr('x1', center) + .attr('y1', actor.y + 25) + .attr('x2', center) + .attr('y2', actor.y + 45); + + actElem + .append('line') + .attr('id', 'actor-man-arms' + actorCnt) + .attr('x1', center - 18) + .attr('y1', actor.y + 33) + .attr('x2', center + 18) + .attr('y2', actor.y + 33); + actElem + .append('line') + .attr('x1', center - 18) + .attr('y1', actor.y + 60) + .attr('x2', center) + .attr('y2', actor.y + 45); + actElem + .append('line') + .attr('x1', center) + .attr('y1', actor.y + 45) + .attr('x2', center + 16) + .attr('y2', actor.y + 60); + + const circle = actElem.append('circle'); + circle.attr('cx', actor.x + actor.width / 2); + circle.attr('cy', actor.y + 10); + circle.attr('r', 15); + circle.attr('width', actor.width); + circle.attr('height', actor.height); + + const bounds = actElem.node().getBBox(); + actor.height = bounds.height; + + _drawTextCandidateFunc(conf)( + actor.description, + actElem, + rect.x, + rect.y + 35, + rect.width, + rect.height, + { class: 'actor' }, + conf + ); + + return actor.height; +}; + +export const drawActor = function (elem, actor, conf) { + switch (actor.type) { + case 'actor': + return drawActorTypeActor(elem, actor, conf); + case 'participant': + return drawActorTypeParticipant(elem, actor, conf); + } }; export const anchorElement = function (elem) { @@ -576,4 +684,5 @@ export default { insertArrowCrossHead, getTextObj, getNoteRect, + fixLifeLineHeights, };