#3358 First commit with basic grammar and 1st test

This commit is contained in:
Knut Sveidqvist 2023-07-05 12:01:37 +02:00
parent f431bae0ba
commit 86e1bb38ee
9 changed files with 458 additions and 0 deletions

View File

@ -33,6 +33,7 @@ export interface MermaidConfig {
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
blockDiagram?: BlockDiagramConfig;
dompurifyConfig?: DOMPurify.Config;
wrap?: boolean;
fontSize?: number;
@ -421,6 +422,9 @@ export interface SankeyDiagramConfig extends BaseDiagramConfig {
linkColor?: SankeyLinkColor | string;
nodeAlignment?: SankeyNodeAlignment;
}
export interface BlockDiagramConfig extends BaseDiagramConfig {
padding?: number;
}
export interface FontConfig {
fontSize?: string | number;

View File

@ -0,0 +1,35 @@
import * as configApi from '../../config.js';
import common from '../common/common.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
setDiagramTitle,
getDiagramTitle,
clear as commonClear,
} from '../../commonDb.js';
type Block = {
ID: string;
};
// Array of nodes guarantees their order
let blocks: Block[] = [];
const clear = (): void => {
blocks = [];
commonClear();
};
export default {
getConfig: () => configApi.getConfig().blockDiagram,
getAccTitle,
setAccTitle,
getAccDescription,
setAccDescription,
getDiagramTitle,
setDiagramTitle,
clear,
};

View File

@ -0,0 +1,15 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: jison doesn't export types
import parser from './parser/sankey.jison';
import db from './blockDB.js';
import renderer from './blockDiagramRenderer.js';
import { prepareTextForParsing } from './blockDiagramUtils.js';
const originalParse = parser.parse.bind(parser);
parser.parse = (text: string) => originalParse(prepareTextForParsing(text));
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
};

View File

@ -0,0 +1,20 @@
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
const id = 'sankey';
const detector: DiagramDetector = (txt) => {
return /^\s*blockDiagram-beta/.test(txt);
};
const loader = async () => {
const { diagram } = await import('./blockDiagram.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@ -0,0 +1,63 @@
import { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js';
import {
select as d3select,
scaleOrdinal as d3scaleOrdinal,
schemeTableau10 as d3schemeTableau10,
} from 'd3';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Uid } from '../../rendering-util/uid.js';
import type { SankeyLinkColor, SankeyNodeAlignment } from '../../config.type.js';
export const draw = function (text: string, id: string, _version: string, diagObj: Diagram): void {
// Get the config
const { securityLevel, sankey: conf } = configApi.getConfig();
const defaultSankeyConfig = configApi!.defaultConfig!.blockDiagram!;
// TODO:
// This code repeats for every diagram
// Figure out what is happening there, probably it should be separated
// The main thing is svg object that is a d3 wrapper for svg operations
//
let sandboxElement: any;
if (securityLevel === 'sandbox') {
sandboxElement = d3select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? d3select(sandboxElement.nodes()[0].contentDocument.body)
: d3select('body');
// @ts-ignore TODO root.select is not callable
const svg = securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : d3select(`[id="${id}"]`);
// Establish svg dimensions and get width and height
//
// FIX: using max width prevents height from being set, is it intended?
// to add height directly one can use `svg.attr('height', height)`
//
// @ts-ignore TODO: svg type vs selection mismatch
configureSvgSize(svg, height, width, useMaxWidth);
// Prepare data for construction based on diagObj.db
// This must be a mutable object with `nodes` and `links` properties:
//
// {
// "nodes": [ { "id": "Alice" }, { "id": "Bob" }, { "id": "Carol" } ],
// "links": [ { "source": "Alice", "target": "Bob", "value": 23 }, { "source": "Bob", "target": "Carol", "value": 43 } ]
// }
//
// @ts-ignore TODO: db type
const graph = diagObj.db.getGraph();
const nodeWidth = 10;
// Get color scheme for the graph
const colorScheme = d3scaleOrdinal(d3schemeTableau10);
};
export default {
draw,
};

View File

@ -0,0 +1,8 @@
export const prepareTextForParsing = (text: string): string => {
const textToParse = text
.replaceAll(/^[^\S\n\r]+|[^\S\n\r]+$/g, '') // remove all trailing spaces for each row
.replaceAll(/([\n\r])+/g, '\n') // remove empty lines duplicated
.trim();
return textToParse;
};

View File

@ -0,0 +1,195 @@
/** mermaid */
//---------------------------------------------------------
// We support csv format as defined here:
// https://www.ietf.org/rfc/rfc4180.txt
// There are some minor changes for compliance with jison
// We also parse only 3 columns: source,target,value
// And allow blank lines for visual purposes
//---------------------------------------------------------
%lex
%x acc_title
%x acc_descr
%x acc_descr_multiline
%x string
%x md_string
%x NODE
%options easy_keword_rules
// as per section 6.1 of RFC 2234 [2]
COMMA \u002C
CR \u000D
LF \u000A
CRLF \u000D\u000A
%%
"blockDiagram-beta" { return 'BLOCK_DIAGRAM_KEY'; }
// \s*\%\%.* { yy.getLogger().info('Found comment',yytext); }
[\s]+ { yy.getLogger().info('.', yytext); /* skip all whitespace */ }
[\n]+ {yy.getLogger().info('_', yytext); /* skip all whitespace */ }
// [\n] return 'NL';
<INITIAL>({CRLF}|{LF}) { return 'NL' }
["][`] { this.begin("md_string");}
<md_string>[^`"]+ { return "MD_STR";}
<md_string>[`]["] { this.popState();}
["] this.begin("string");
<string>["] this.popState();
<string>[^"]* return "STR";
"style" return 'STYLE';
"default" return 'DEFAULT';
"linkStyle" return 'LINKSTYLE';
"interpolate" return 'INTERPOLATE';
"classDef" return 'CLASSDEF';
"class" return 'CLASS';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
"subgraph" return 'subgraph';
"end"\b\s* return 'end';
.*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';
// Start of nodes with shapes and description
"-)" { yy.getLogger().info('Lex: -)'); this.begin('NODE');return 'NODE_D START'; }
"(-" { yy.getLogger().info('Lex: (-'); this.begin('NODE');return 'NODE_DSTART'; }
"))" { yy.getLogger().info('Lex: ))'); this.begin('NODE');return 'NODE_DSTART'; }
")" { yy.getLogger().info('Lex: )'); this.begin('NODE');return 'NODE_DSTART'; }
"((" { yy.getLogger().info('Lex: )'); this.begin('NODE');return 'NODE_DSTART'; }
"{{" { yy.getLogger().info('Lex: )'); this.begin('NODE');return 'NODE_DSTART'; }
"(" { yy.getLogger().info('Lex: )'); this.begin('NODE');return 'NODE_DSTART'; }
"[" { yy.getLogger().info('Lex: ['); this.begin('NODE');return 'NODE_DSTART'; }
"([" { yy.getLogger().info('Lex: )'); this.begin('NODE');return 'NODE_DSTART'; }
"[[" { this.begin('NODE');return 'NODE_DSTART'; }
"[|" { this.begin('NODE');return 'NODE_DSTART'; }
"[(" { this.begin('NODE');return 'NODE_DSTART'; }
"(((" { this.begin('NODE');return 'NODE_DSTART'; }
")))" { this.begin('NODE');return 'NODE_DSTART'; }
"[/" { this.begin('NODE');return 'NODE_DSTART'; }
"[\\" { this.begin('NODE');return 'NODE_DSTART'; }
[^\(\[\n\-\)\{\}]+ { yy.getLogger().info('Lex: NODE_ID', yytext);return 'NODE_ID'; }
<<EOF>> { yy.getLogger().info('Lex: EOF', yytext);return 'EOF'; }
// Handling of strings in node
<NODE>["][`] { this.begin("md_string");}
<md_string>[^`"]+ { return "NODE_DESCR";}
<md_string>[`]["] { this.popState();}
<NODE>["] { yy.getLogger().info('Lex: Starting string');this.begin("string");}
<string>[^"]+ { yy.getLogger().info('Lex: NODE_DESCR:', yytext); return "NODE_DESCR";}
<string>["] {this.popState();}
// Node end of shape
<NODE>[\)]\) { this.popState();yy.getLogger().info('Lex: ))'); return "NODE_DEND"; }
<NODE>[\)] { this.popState();yy.getLogger().info('Lex: )'); return "NODE_DEND"; }
<NODE>[\]] { this.popState();yy.getLogger().info('Lex: ]'); return "NODE_DEND"; }
<NODE>"}}" { this.popState();yy.getLogger().info('Lex: (('); return "NODE_DEND"; }
<NODE>"(-" { this.popState();yy.getLogger().info('Lex: (-'); return "NODE_DEND"; }
<NODE>"-)" { this.popState();yy.getLogger().info('Lex: -)'); return "NODE_DEND"; }
<NODE>"((" { this.popState();yy.getLogger().info('Lex: (('); return "NODE_DEND"; }
<NODE>"(" { this.popState();yy.getLogger().info('Lex: ('); return "NODE_DEND"; }
<NODE>"])" { this.popState();yy.getLogger().info('Lex: ])'); return "NODE_DEND"; }
<NODE>"]]" { this.popState();yy.getLogger().info('Lex: ]]'); return "NODE_DEND"; }
<NODE>"/]" { this.popState();yy.getLogger().info('Lex: /]'); return "NODE_DEND"; }
<NODE>")]" { this.popState();yy.getLogger().info('Lex: )]'); return "NODE_DEND"; }
// Edges
\s*[xo<]?\-\-+[-xo>]\s* { yy.getLogger().info('Lex: LINK', '#'+yytext+'#'); return 'LINK'; }
\s*[xo<]?\=\=+[=xo>]\s* { yy.getLogger().info('Lex: LINK', yytext); return 'LINK'; }
\s*[xo<]?\-?\.+\-[xo>]?\s* { yy.getLogger().info('Lex: LINK', yytext); return 'LINK'; }
\s*\~\~[\~]+\s* { yy.getLogger().info('Lex: LINK', yytext); return 'LINK'; }
\s*[xo<]?\-\-\s* { yy.getLogger().info('Lex: START_LINK', yytext); return 'START_LINK'; }
\s*[xo<]?\=\=\s* { yy.getLogger().info('Lex: START_LINK', yytext); return 'START_LINK'; }
\s*[xo<]?\-\.\s* { yy.getLogger().info('Lex: START_LINK', yytext); return 'START_LINK'; }
/lex
%start start
%% // language grammar
spaceLines
: SPACELINE
| spaceLines SPACELINE
| spaceLines NL
;
seperator
: NL
{yy.getLogger().info('Rule: seperator (NL) ');}
| SPACE
{yy.getLogger().info('Rule: seperator (Space) ');}
| EOF
{yy.getLogger().info('Rule: seperator (EOF) ');}
;
start: BLOCK_DIAGRAM_KEY document;
blockDiagram
: blockDiagram document { return yy; }
| blockDiagram NL document { return yy; }
;
stop
: NL {yy.getLogger().info('Stop NL ');}
| EOF {yy.getLogger().info('Stop EOF ');}
// | SPACELINE
| stop NL {yy.getLogger().info('Stop NL2 ');}
| stop EOF {yy.getLogger().info('Stop EOF2 ');}
;
document
: document statement
| statement
;
link
: LINK
{ yy.getLogger().info("Rule: link: ", $1); }
| START_LINK
{ yy.getLogger().info("Rule: link: ", $1); }
;
statement
: nodeStatement
// SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
// | SPACELIST ICON { yy.getLogger().info('Icon: ',$2);yy.decorateNode({icon: $2}); }
// | SPACELIST CLASS { yy.decorateNode({class: $2}); }
// | SPACELINE { yy.getLogger().info('SPACELIST');}
// |
// node { yy.getLogger().info('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type); }
// | ICON { yy.decorateNode({icon: $1}); }
// | CLASS { yy.decorateNode({class: $1}); }
// // | SPACELIST
| EOF
;
nodeStatement: nodeStatement link node { yy.getLogger().info('Rule: nodeStatement (nodeStatement link node) ');}
|node { yy.getLogger().info('Rule: nodeStatement (node) ');}
;
node
: NODE_ID
{ yy.getLogger().info("Rule: node (NODE_ID seperator): ", $1); }
|NODE_ID nodeShapeNLabel
{ yy.getLogger().info("Rule: node (NODE_ID nodeShapeNLabel seperator): ", $1, $2); }
// |nodeShapeNLabel seperator
// { yy.getLogger().info("Rule: node (nodeShapeNLabel seperator): ", $1, $2, $3); }
;
nodeShapeNLabel
: NODE_DSTART STR NODE_DEND
{ yy.getLogger().info("Rule: nodeShapeNLabel: ", $1, $2, $3); $$ = { type: $1 + $3, descr: $2 }; }
;
%%

View File

@ -0,0 +1,85 @@
// @ts-ignore: jison doesn't export types
import blockDiagram from './blockDiagram.jison';
import db from '../blockDB.js';
import { cleanupComments } from '../../../diagram-api/comments.js';
import { prepareTextForParsing } from '../blockDiagramUtils.js';
import * as fs from 'fs';
import * as path from 'path';
describe('Sankey diagram', function () {
describe('when parsing an block diagram graph it should handle > ', function () {
beforeEach(function () {
blockDiagram.parser.yy = db;
blockDiagram.parser.yy.clear();
blockDiagram.parser.yy.getLogger = () => console;
});
it('a diagram with a node', async () => {
const str = `blockDiagram-beta
id
`;
blockDiagram.parse(str);
});
it('a diagram with multiple nodes', async () => {
const str = `blockDiagram-beta
id1
id2
`;
blockDiagram.parse(str);
});
it('a node with a square shape and a label', async () => {
const str = `blockDiagram-beta
id["A label"]
id2`;
blockDiagram.parse(str);
});
it('a diagram with multiple nodes with edges', async () => {
const str = `blockDiagram-beta
id1["first"] --> id2["second"]
`;
blockDiagram.parse(str);
});
// it('a diagram with column statements', async () => {
// const str = `blockDiagram-beta
// columns 1
// block1["Block 1"]
// `;
// blockDiagram.parse(str);
// });
// it('a diagram with block hierarchies', async () => {
// const str = `blockDiagram-beta
// columns 1
// block1[Block 1]
// block
// columns 2
// block2[Block 2]
// block3[Block 3]
// end %% End the compound block
// `;
// blockDiagram.parse(str);
// });
// it('a diagram with differernt column values in different blocks', async () => {
// const str = `blockDiagram-beta
// columns 1
// block1[Block 1]
// block
// columns 2
// block2[Block 2]
// block3[Block 3]
// end %% End the compound block
// `;
// blockDiagram.parse(str);
// // Todo check that the different blocks have different column values
// });
});
});

View File

@ -0,0 +1,33 @@
block
columns 3
Block1
Block2["Block 2"]
block
columns 2
Block2.1
Block2.2
end
Block3
----
block
columns 2
Block[Frontend]:vertical
block "Document management System"
columns 3
MO[Manager Operation]:vertical
block
columns 2
block "Security and User Manager"
end
----
block frontend:vertical
move right
block "Document Management System"
move down