mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-02 03:44:16 +01:00
Merge branch 'develop' into chore/upgrade/unocss-iconify
This commit is contained in:
@@ -78,3 +78,187 @@ describe('when working with site config', () => {
|
||||
expect(config_4.altFontFamily).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDefinedConfig', () => {
|
||||
beforeEach(() => {
|
||||
configApi.reset();
|
||||
});
|
||||
|
||||
it('should return empty object when no user config is defined', () => {
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toEqual({});
|
||||
});
|
||||
|
||||
it('should return config from initialize only', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toEqual(initConfig);
|
||||
});
|
||||
|
||||
it('should return config from directives only', () => {
|
||||
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
|
||||
const directive2: MermaidConfig = { theme: 'forest' };
|
||||
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontFamily": "Arial",
|
||||
"fontSize": 14,
|
||||
"layout": "elk",
|
||||
"theme": "forest",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should combine initialize config and directives', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
|
||||
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
|
||||
const directive2: MermaidConfig = { theme: 'forest' };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontFamily": "Arial",
|
||||
"fontSize": 14,
|
||||
"layout": "elk",
|
||||
"theme": "forest",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle nested config objects properly', () => {
|
||||
const initConfig: MermaidConfig = {
|
||||
flowchart: { nodeSpacing: 50, rankSpacing: 100 },
|
||||
theme: 'default',
|
||||
};
|
||||
const directive: MermaidConfig = {
|
||||
flowchart: { nodeSpacing: 75, curve: 'basis' },
|
||||
mindmap: { padding: 20 },
|
||||
};
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"curve": "basis",
|
||||
"nodeSpacing": 75,
|
||||
"rankSpacing": 100,
|
||||
},
|
||||
"mindmap": {
|
||||
"padding": 20,
|
||||
},
|
||||
"theme": "default",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex nested overrides', () => {
|
||||
const initConfig: MermaidConfig = {
|
||||
flowchart: {
|
||||
nodeSpacing: 50,
|
||||
rankSpacing: 100,
|
||||
curve: 'linear',
|
||||
},
|
||||
theme: 'default',
|
||||
};
|
||||
const directive1: MermaidConfig = {
|
||||
flowchart: {
|
||||
nodeSpacing: 75,
|
||||
},
|
||||
fontSize: 12,
|
||||
};
|
||||
const directive2: MermaidConfig = {
|
||||
flowchart: {
|
||||
curve: 'basis',
|
||||
nodeSpacing: 100,
|
||||
},
|
||||
mindmap: {
|
||||
padding: 15,
|
||||
},
|
||||
};
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"curve": "basis",
|
||||
"nodeSpacing": 100,
|
||||
"rankSpacing": 100,
|
||||
},
|
||||
"fontSize": 12,
|
||||
"mindmap": {
|
||||
"padding": 15,
|
||||
},
|
||||
"theme": "default",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return independent copies (not references)', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
|
||||
const userConfig1 = configApi.getUserDefinedConfig();
|
||||
const userConfig2 = configApi.getUserDefinedConfig();
|
||||
|
||||
userConfig1.theme = 'neutral';
|
||||
userConfig1.flowchart!.nodeSpacing = 999;
|
||||
|
||||
expect(userConfig2).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"nodeSpacing": 50,
|
||||
},
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle edge cases with undefined values', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
|
||||
const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontSize": 14,
|
||||
"layout": undefined,
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should retain config from initialize after reset', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark' };
|
||||
const directive: MermaidConfig = { layout: 'elk' };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"layout": "elk",
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
|
||||
configApi.reset();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,3 +248,17 @@ const checkConfig = (config: MermaidConfig) => {
|
||||
issueWarning('LAZY_LOAD_DEPRECATED');
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserDefinedConfig = (): MermaidConfig => {
|
||||
let userConfig: MermaidConfig = {};
|
||||
|
||||
if (configFromInitialize) {
|
||||
userConfig = assignWithDepth(userConfig, configFromInitialize);
|
||||
}
|
||||
|
||||
for (const d of directives) {
|
||||
userConfig = assignWithDepth(userConfig, d);
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
// Special states for recognizing aliases
|
||||
// A special state for grabbing text up to the first comment/newline
|
||||
%x ID ALIAS LINE
|
||||
%x ID ALIAS LINE CONFIG CONFIG_DATA
|
||||
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
@@ -28,6 +28,11 @@
|
||||
\%%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */
|
||||
[0-9]+(?=[ \n]+) return 'NUM';
|
||||
<ID>\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
|
||||
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
|
||||
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
|
||||
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
"box" { this.begin('LINE'); return 'box'; }
|
||||
"participant" { this.begin('ID'); return 'participant'; }
|
||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||
@@ -231,6 +236,8 @@ participant_statement
|
||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
|
||||
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
|
||||
| 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
|
||||
|
||||
;
|
||||
|
||||
note_statement
|
||||
@@ -301,6 +308,23 @@ signal
|
||||
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
|
||||
;
|
||||
|
||||
actor_with_config
|
||||
: ACTOR config_object
|
||||
{
|
||||
$$ = {
|
||||
type: 'addParticipant',
|
||||
actor: $1,
|
||||
config: $2
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
config_object
|
||||
: CONFIG_START CONFIG_CONTENT CONFIG_END
|
||||
{
|
||||
$$ = $2.trim();
|
||||
}
|
||||
;
|
||||
// actor
|
||||
// : actor_participant
|
||||
// | actor_actor
|
||||
@@ -313,7 +337,7 @@ signaltype
|
||||
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
|
||||
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
|
||||
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
|
||||
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
|
||||
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import * as yaml from 'js-yaml';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
|
||||
import type { ParticipantMetaData } from '../../types.js';
|
||||
|
||||
interface SequenceState {
|
||||
prevActor?: string;
|
||||
@@ -75,6 +77,17 @@ const PLACEMENT = {
|
||||
OVER: 2,
|
||||
} as const;
|
||||
|
||||
export const PARTICIPANT_TYPE = {
|
||||
ACTOR: 'actor',
|
||||
BOUNDARY: 'boundary',
|
||||
COLLECTIONS: 'collections',
|
||||
CONTROL: 'control',
|
||||
DATABASE: 'database',
|
||||
ENTITY: 'entity',
|
||||
PARTICIPANT: 'participant',
|
||||
QUEUE: 'queue',
|
||||
} as const;
|
||||
|
||||
export class SequenceDB implements DiagramDB {
|
||||
private readonly state = new ImperativeState<SequenceState>(() => ({
|
||||
prevActor: undefined,
|
||||
@@ -119,9 +132,22 @@ export class SequenceDB implements DiagramDB {
|
||||
id: string,
|
||||
name: string,
|
||||
description: { text: string; wrap?: boolean | null; type: string },
|
||||
type: string
|
||||
type: string,
|
||||
metadata?: any
|
||||
) {
|
||||
let assignedBox = this.state.records.currentBox;
|
||||
let doc;
|
||||
if (metadata !== undefined) {
|
||||
let yamlData;
|
||||
// detect if shapeData contains a newline character
|
||||
if (!metadata.includes('\n')) {
|
||||
yamlData = '{\n' + metadata + '\n}';
|
||||
} else {
|
||||
yamlData = metadata + '\n';
|
||||
}
|
||||
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData;
|
||||
}
|
||||
type = doc?.type ?? type;
|
||||
const old = this.state.records.actors.get(id);
|
||||
if (old) {
|
||||
// If already set and trying to set to a new one throw error
|
||||
@@ -518,7 +544,7 @@ export class SequenceDB implements DiagramDB {
|
||||
});
|
||||
break;
|
||||
case 'addParticipant':
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
||||
break;
|
||||
case 'createParticipant':
|
||||
if (this.state.records.actors.has(param.actor)) {
|
||||
@@ -527,7 +553,7 @@ export class SequenceDB implements DiagramDB {
|
||||
);
|
||||
}
|
||||
this.state.records.lastCreated = param.actor;
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
||||
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
|
||||
break;
|
||||
case 'destroyParticipant':
|
||||
|
||||
@@ -2058,4 +2058,272 @@ Bob->>Alice:Got it!
|
||||
expect(messages[0].from).toBe('Alice');
|
||||
expect(messages[0].to).toBe('Bob');
|
||||
});
|
||||
describe('when parsing extended participant syntax', () => {
|
||||
it('should parse participants with different quote styles and whitespace', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob@{ "type" : "database" }
|
||||
participant Carl@{ type: "database" }
|
||||
participant David@{ "type" : 'database' }
|
||||
participant Eve@{ type: 'database' }
|
||||
participant Favela@{ "type" : "database" }
|
||||
Bob->>+Alice: Hi Alice
|
||||
Alice->>+Bob: Hi Bob
|
||||
`);
|
||||
|
||||
const actors = diagram.db.getActors();
|
||||
|
||||
expect(actors.get('Alice').type).toBe('database');
|
||||
expect(actors.get('Alice').description).toBe('Alice');
|
||||
|
||||
expect(actors.get('Bob').type).toBe('database');
|
||||
expect(actors.get('Bob').description).toBe('Bob');
|
||||
|
||||
expect(actors.get('Carl').type).toBe('database');
|
||||
expect(actors.get('Carl').description).toBe('Carl');
|
||||
|
||||
expect(actors.get('David').type).toBe('database');
|
||||
expect(actors.get('David').description).toBe('David');
|
||||
|
||||
expect(actors.get('Eve').type).toBe('database');
|
||||
expect(actors.get('Eve').description).toBe('Eve');
|
||||
|
||||
expect(actors.get('Favela').type).toBe('database');
|
||||
expect(actors.get('Favela').description).toBe('Favela');
|
||||
|
||||
// Verify messages were parsed correctly
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages.length).toBe(4); // 2 messages + 2 activation messages
|
||||
expect(messages[0].from).toBe('Bob');
|
||||
expect(messages[0].to).toBe('Alice');
|
||||
expect(messages[0].message).toBe('Hi Alice');
|
||||
expect(messages[2].from).toBe('Alice'); // Second message (index 2 due to activation)
|
||||
expect(messages[2].to).toBe('Bob');
|
||||
expect(messages[2].message).toBe('Hi Bob');
|
||||
});
|
||||
|
||||
it('should parse mixed participant types with extended syntax', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant lead
|
||||
participant dsa@{ "type" : "queue" }
|
||||
API->>+Database: getUserb
|
||||
Database-->>-API: userb
|
||||
dsa --> Database: hello
|
||||
`);
|
||||
|
||||
// Verify actors were created
|
||||
const actors = diagram.db.getActors();
|
||||
|
||||
expect(actors.get('lead').type).toBe('participant');
|
||||
expect(actors.get('lead').description).toBe('lead');
|
||||
|
||||
// Participant with extended syntax
|
||||
expect(actors.get('dsa').type).toBe('queue');
|
||||
expect(actors.get('dsa').description).toBe('dsa');
|
||||
|
||||
// Implicitly created actors (from messages)
|
||||
expect(actors.get('API').type).toBe('participant');
|
||||
expect(actors.get('API').description).toBe('API');
|
||||
|
||||
expect(actors.get('Database').type).toBe('participant');
|
||||
expect(actors.get('Database').description).toBe('Database');
|
||||
|
||||
// Verify messages were parsed correctly
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages.length).toBe(5); // 3 messages + 2 activation messages
|
||||
|
||||
// First message with activation
|
||||
expect(messages[0].from).toBe('API');
|
||||
expect(messages[0].to).toBe('Database');
|
||||
expect(messages[0].message).toBe('getUserb');
|
||||
expect(messages[0].activate).toBe(true);
|
||||
|
||||
// Second message with deactivation
|
||||
expect(messages[2].from).toBe('Database');
|
||||
expect(messages[2].to).toBe('API');
|
||||
expect(messages[2].message).toBe('userb');
|
||||
|
||||
// Third message
|
||||
expect(messages[4].from).toBe('dsa');
|
||||
expect(messages[4].to).toBe('Database');
|
||||
expect(messages[4].message).toBe('hello');
|
||||
});
|
||||
|
||||
it('should fail for malformed JSON in participant definition', async () => {
|
||||
const invalidDiagram = `
|
||||
sequenceDiagram
|
||||
participant D@{ "type: "entity" }
|
||||
participant E@{ "type": "dat
|
||||
abase }
|
||||
`;
|
||||
|
||||
let error = false;
|
||||
try {
|
||||
await mermaidAPI.parse(invalidDiagram);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail for missing colon separator', async () => {
|
||||
const invalidDiagram = `
|
||||
sequenceDiagram
|
||||
participant C@{ "type" "control" }
|
||||
C ->> C: action
|
||||
`;
|
||||
|
||||
let error = false;
|
||||
try {
|
||||
await mermaidAPI.parse(invalidDiagram);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail for missing closing brace', async () => {
|
||||
const invalidDiagram = `
|
||||
sequenceDiagram
|
||||
participant E@{ "type": "entity"
|
||||
E ->> E: process
|
||||
`;
|
||||
|
||||
let error = false;
|
||||
try {
|
||||
await mermaidAPI.parse(invalidDiagram);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('participant type parsing', () => {
|
||||
it('should parse boundary participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant boundary@{ "type" : "boundary" }
|
||||
boundary->boundary: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('boundary').type).toBe('boundary');
|
||||
expect(actors.get('boundary').description).toBe('boundary');
|
||||
});
|
||||
|
||||
it('should parse control participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant C@{ "type" : "control" }
|
||||
C->C: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('C').type).toBe('control');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
|
||||
it('should parse entity participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant E@{ "type" : "entity" }
|
||||
E->E: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
|
||||
it('should parse database participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant D@{ "type" : "database" }
|
||||
D->D: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('D').type).toBe('database');
|
||||
expect(actors.get('D').description).toBe('D');
|
||||
});
|
||||
|
||||
it('should parse collections participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant L@{ "type" : "collections" }
|
||||
L->L: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('L').type).toBe('collections');
|
||||
expect(actors.get('L').description).toBe('L');
|
||||
});
|
||||
|
||||
it('should parse queue participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Q@{ "type" : "queue" }
|
||||
Q->Q: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Q').type).toBe('queue');
|
||||
expect(actors.get('Q').description).toBe('Q');
|
||||
});
|
||||
});
|
||||
|
||||
describe('participant type parsing', () => {
|
||||
it('should parse actor participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant A@{ "type" : "queue" }
|
||||
A->A: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('A').type).toBe('queue');
|
||||
expect(actors.get('A').description).toBe('A');
|
||||
});
|
||||
|
||||
it('should parse participant participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant P@{ "type" : "database" }
|
||||
P->P: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('P').type).toBe('database');
|
||||
expect(actors.get('P').description).toBe('P');
|
||||
});
|
||||
|
||||
it('should parse boundary using actor keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob@{ "type" : "control" }
|
||||
Alice->>Bob: Hello Bob, how are you?
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Alice').type).toBe('collections');
|
||||
expect(actors.get('Bob').type).toBe('control');
|
||||
expect(actors.get('Bob').description).toBe('Bob');
|
||||
});
|
||||
|
||||
it('should parse control using participant keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant C@{ "type" : "control" }
|
||||
C->C: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('C').type).toBe('control');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
|
||||
it('should parse entity using actor keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant E@{ "type" : "entity" }
|
||||
E->E: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js';
|
||||
import utils from '../../utils.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import { PARTICIPANT_TYPE } from './sequenceDb.js';
|
||||
|
||||
let conf = {};
|
||||
|
||||
@@ -476,7 +477,29 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
|
||||
// add node number
|
||||
if (sequenceVisible || conf.showSequenceNumbers) {
|
||||
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
||||
const isBidirectional =
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID ||
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED;
|
||||
|
||||
if (isBidirectional) {
|
||||
const SEQUENCE_NUMBER_RADIUS = 6;
|
||||
|
||||
if (startx < stopx) {
|
||||
line.attr('x1', startx + 2 * SEQUENCE_NUMBER_RADIUS);
|
||||
} else {
|
||||
line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
|
||||
}
|
||||
}
|
||||
|
||||
diagram
|
||||
.append('line')
|
||||
.attr('x1', startx)
|
||||
.attr('y1', lineStartY)
|
||||
.attr('x2', startx)
|
||||
.attr('y2', lineStartY)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
||||
|
||||
diagram
|
||||
.append('text')
|
||||
.attr('x', startx)
|
||||
@@ -724,11 +747,19 @@ function adjustCreatedDestroyedData(
|
||||
msgModel.startx = msgModel.startx - adjustment;
|
||||
}
|
||||
}
|
||||
const actorArray = [
|
||||
PARTICIPANT_TYPE.ACTOR,
|
||||
PARTICIPANT_TYPE.CONTROL,
|
||||
PARTICIPANT_TYPE.ENTITY,
|
||||
PARTICIPANT_TYPE.DATABASE,
|
||||
];
|
||||
|
||||
// if it is a create message
|
||||
if (createdActors.get(msg.to) == index) {
|
||||
const actor = actors.get(msg.to);
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||
const adjustment = actorArray.includes(actor.type)
|
||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
||||
: actor.width / 2 + 3;
|
||||
receiverAdjustment(actor, adjustment);
|
||||
actor.starty = lineStartY - actor.height / 2;
|
||||
bounds.bumpVerticalPos(actor.height / 2);
|
||||
@@ -737,7 +768,7 @@ function adjustCreatedDestroyedData(
|
||||
else if (destroyedActors.get(msg.from) == index) {
|
||||
const actor = actors.get(msg.from);
|
||||
if (conf.mirrorActors) {
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||
const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||
senderAdjustment(actor, adjustment);
|
||||
}
|
||||
actor.stopy = lineStartY - actor.height / 2;
|
||||
@@ -747,7 +778,9 @@ function adjustCreatedDestroyedData(
|
||||
else if (destroyedActors.get(msg.to) == index) {
|
||||
const actor = actors.get(msg.to);
|
||||
if (conf.mirrorActors) {
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||
const adjustment = actorArray.includes(actor.type)
|
||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
||||
: actor.width / 2 + 3;
|
||||
receiverAdjustment(actor, adjustment);
|
||||
}
|
||||
actor.stopy = lineStartY - actor.height / 2;
|
||||
@@ -1065,10 +1098,11 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
for (const box of bounds.models.boxes) {
|
||||
box.height = bounds.getVerticalPos() - box.y;
|
||||
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
||||
box.startx = box.x;
|
||||
box.starty = box.y;
|
||||
box.stopx = box.startx + box.width;
|
||||
box.stopy = box.starty + box.height;
|
||||
const boxPadding = conf.boxMargin * 2;
|
||||
box.startx = box.x - boxPadding;
|
||||
box.starty = box.y - boxPadding * 0.25;
|
||||
box.stopx = box.startx + box.width + 2 * boxPadding;
|
||||
box.stopy = box.starty + box.height + boxPadding * 0.75;
|
||||
box.stroke = 'rgb(0,0,0, 0.5)';
|
||||
svgDraw.drawBox(diagram, box, conf);
|
||||
}
|
||||
@@ -1333,6 +1367,9 @@ async function calculateActorMargins(
|
||||
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
|
||||
}, 0);
|
||||
|
||||
const standardBoxPadding = conf.boxMargin * 8;
|
||||
totalWidth += standardBoxPadding;
|
||||
|
||||
totalWidth -= 2 * conf.boxTextMargin;
|
||||
if (box.wrap) {
|
||||
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
||||
|
||||
@@ -12,6 +12,11 @@ const getStyles = (options) =>
|
||||
.actor-line {
|
||||
stroke: ${options.actorLineColor};
|
||||
}
|
||||
|
||||
.innerArc {
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
.messageLine0 {
|
||||
stroke-width: 1.5;
|
||||
@@ -115,6 +120,7 @@ const getStyles = (options) =>
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
||||
@@ -415,6 +415,600 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
||||
return height;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an actor in the diagram with the attached line
|
||||
*
|
||||
* @param {any} elem - The diagram we'll draw to.
|
||||
* @param {any} actor - The actor to draw.
|
||||
* @param {any} conf - DrawText implementation discriminator object
|
||||
* @param {boolean} isFooter - If the actor is the footer one
|
||||
*/
|
||||
const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
var g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
var cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// DRAW STACKED RECTANGLES
|
||||
const offset = 6;
|
||||
const shadowRect = {
|
||||
...rect,
|
||||
x: rect.x + (isFooter ? -offset : -offset),
|
||||
y: rect.y + (isFooter ? +offset : +offset),
|
||||
class: 'actor',
|
||||
};
|
||||
const rectElem = drawRect(g, rect); // draw main rectangle on top
|
||||
drawRect(g, shadowRect);
|
||||
actor.rectData = rect;
|
||||
|
||||
if (actor.properties?.icon) {
|
||||
const iconSrc = actor.properties.icon.trim();
|
||||
if (iconSrc.charAt(0) === '@') {
|
||||
svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
|
||||
} else {
|
||||
svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
|
||||
}
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x - offset,
|
||||
rect.y + offset,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
let height = actor.height;
|
||||
if (rectElem.node) {
|
||||
const bounds = rectElem.node().getBBox();
|
||||
actor.height = bounds.height;
|
||||
height = bounds.height;
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
let g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
let cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// Cylinder dimensions
|
||||
const ry = rect.height / 2;
|
||||
const rx = ry / (2.5 + rect.height / 50);
|
||||
|
||||
// Cylinder base group
|
||||
const cylinderGroup = g.append('g');
|
||||
const cylinderArc = g.append('g');
|
||||
|
||||
// Main cylinder body
|
||||
cylinderGroup
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 0,${rect.height}
|
||||
h ${rect.width - 2 * rx}
|
||||
a ${rx},${ry} 0 0 0 0,-${rect.height}
|
||||
Z
|
||||
`
|
||||
)
|
||||
.attr('class', cssclass);
|
||||
cylinderArc
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 0,${rect.height}`
|
||||
)
|
||||
.attr('stroke', '#666')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('class', cssclass);
|
||||
|
||||
cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
|
||||
cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`);
|
||||
|
||||
actor.rectData = rect;
|
||||
|
||||
if (actor.properties?.icon) {
|
||||
const iconSrc = actor.properties.icon.trim();
|
||||
const iconX = rect.x + rect.width - 20;
|
||||
const iconY = rect.y + 10;
|
||||
if (iconSrc.charAt(0) === '@') {
|
||||
svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
|
||||
} else {
|
||||
svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
|
||||
}
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
let height = actor.height;
|
||||
const lastPath = cylinderGroup.select('path:last-child');
|
||||
if (lastPath.node()) {
|
||||
const bounds = lastPath.node().getBBox();
|
||||
actor.height = bounds.height;
|
||||
height = bounds.height;
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeControl = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 75;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
const cx = actor.x + actor.width / 2;
|
||||
const cy = actorY + 30;
|
||||
const r = 18;
|
||||
|
||||
actElem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'filled-head-control')
|
||||
.attr('refX', 11)
|
||||
.attr('refY', 5.8)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', '172.5')
|
||||
.append('path')
|
||||
.attr('d', 'M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z');
|
||||
|
||||
// Draw the base circle
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', cy)
|
||||
.attr('r', r)
|
||||
.attr('fill', '#eaeaf7')
|
||||
.attr('stroke', '#666')
|
||||
.attr('stroke-width', 1.2);
|
||||
|
||||
// Draw looping arrow as arc path
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('marker-end', 'url(#filled-head-control)')
|
||||
.attr('transform', `translate(${cx}, ${cy - r})`);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + r + (isFooter ? 5 : 10),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 75;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
const cx = actor.x + actor.width / 2;
|
||||
const cy = actorY + (!isFooter ? 25 : 10);
|
||||
const r = 18;
|
||||
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', cy)
|
||||
.attr('r', r)
|
||||
.attr('width', actor.width)
|
||||
.attr('height', actor.height);
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', cx - r)
|
||||
.attr('x2', cx + r)
|
||||
.attr('y1', cy + r)
|
||||
.attr('y2', cy + r)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY + r - 5) / 2),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
if (!isFooter) {
|
||||
actElem.attr('transform', `translate(${0}, ${r / 2})`);
|
||||
} else {
|
||||
actElem.attr('transform', `translate(${0}, ${r / 2})`);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height + 2 * conf.boxTextMargin;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
let g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
|
||||
let cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// Cylinder dimensions
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
const w = rect.width / 4;
|
||||
const h = rect.width / 4;
|
||||
const rx = w / 2;
|
||||
const ry = rx / (2.5 + w / 50);
|
||||
|
||||
// Cylinder base group
|
||||
const cylinderGroup = g.append('g');
|
||||
|
||||
const d = `
|
||||
M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 ${w},0
|
||||
a ${rx},${ry} 0 0 0 -${w},0
|
||||
l 0,${h - 2 * ry}
|
||||
a ${rx},${ry} 0 0 0 ${w},0
|
||||
l 0,-${h - 2 * ry}
|
||||
`;
|
||||
// Draw the main cylinder body
|
||||
cylinderGroup
|
||||
.append('path')
|
||||
.attr('d', d)
|
||||
.attr('fill', '#eaeaea')
|
||||
.attr('stroke', '#000')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('class', cssclass);
|
||||
|
||||
if (!isFooter) {
|
||||
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${(rect.height + ry) / 4})`);
|
||||
} else {
|
||||
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
|
||||
}
|
||||
actor.rectData = rect;
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? (rect.height + ry) / 2 : (rect.height + h) / 4),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
const lastPath = cylinderGroup.select('path:last-child');
|
||||
if (lastPath.node()) {
|
||||
const bounds = lastPath.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 80;
|
||||
const radius = 30;
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('id', 'actor-man-torso' + actorCnt)
|
||||
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y1', actorY + 10)
|
||||
.attr('x2', actor.x + actor.width / 2 - 15)
|
||||
.attr('y2', actorY + 10);
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('id', 'actor-man-arms' + actorCnt)
|
||||
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y1', actorY + 0) // starting Y
|
||||
.attr('x2', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
|
||||
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', actor.x + actor.width / 2)
|
||||
.attr('cy', actorY + 10)
|
||||
.attr('r', radius);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? radius / 2 + 3 : radius / 2 - 4),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
if (!isFooter) {
|
||||
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
|
||||
} else {
|
||||
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
@@ -516,6 +1110,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
|
||||
return await drawActorTypeActor(elem, actor, conf, isFooter);
|
||||
case 'participant':
|
||||
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
|
||||
case 'boundary':
|
||||
return await drawActorTypeBoundary(elem, actor, conf, isFooter);
|
||||
case 'control':
|
||||
return await drawActorTypeControl(elem, actor, conf, isFooter);
|
||||
case 'entity':
|
||||
return await drawActorTypeEntity(elem, actor, conf, isFooter);
|
||||
case 'database':
|
||||
return await drawActorTypeDatabase(elem, actor, conf, isFooter);
|
||||
case 'collections':
|
||||
return await drawActorTypeCollections(elem, actor, conf, isFooter);
|
||||
case 'queue':
|
||||
return await drawActorTypeQueue(elem, actor, conf, isFooter);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -590,11 +590,17 @@ flowchart TD
|
||||
- `b`
|
||||
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
||||
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
|
||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
|
||||
- `on`
|
||||
- `off`
|
||||
|
||||
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
|
||||
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% My image with a constrained aspect ratio
|
||||
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||
```
|
||||
|
||||
## Links between nodes
|
||||
|
||||
|
||||
@@ -46,6 +46,78 @@ sequenceDiagram
|
||||
Bob->>Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Boundary
|
||||
|
||||
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
### Control
|
||||
|
||||
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
### Queue
|
||||
|
||||
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
|
||||
@@ -13,6 +13,18 @@ export interface NodeMetaData {
|
||||
ticket?: string;
|
||||
}
|
||||
|
||||
export interface ParticipantMetaData {
|
||||
type?:
|
||||
| 'actor'
|
||||
| 'participant'
|
||||
| 'boundary'
|
||||
| 'control'
|
||||
| 'entity'
|
||||
| 'database'
|
||||
| 'collections'
|
||||
| 'queue';
|
||||
}
|
||||
|
||||
export interface EdgeMetaData {
|
||||
animation?: 'fast' | 'slow';
|
||||
animate?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user