Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create sankey parser and integrate sankey parser into mermaid package #4799

Open
wants to merge 35 commits into
base: develop
Choose a base branch
from
Open
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7ca02da
feat: create `sankey` parser and integrate `sankey` parser into `merm…
Yokozuna59 Sep 2, 2023
a10821f
chore: remove sankey links map
Yokozuna59 Sep 2, 2023
9ad5536
chore: remove sankey links map
Yokozuna59 Sep 2, 2023
9a8963a
Merge branch 'next' into add-sankey-langium-parser
Yokozuna59 Sep 2, 2023
8feef1e
chore: `vi.mock` sankey renderer
Yokozuna59 Sep 2, 2023
efbb1f1
styles: refactor `switch..case` into `Record` for matcher regexes
Yokozuna59 Sep 2, 2023
b6906a6
fix: match until comments in `sankey` diagram
Yokozuna59 Sep 2, 2023
8c40eb3
Merge branch next into add-sankey-langium-parser
Yokozuna59 Sep 20, 2023
63430d2
pref(sankey): combine `source` and `target` rules as `node`
Yokozuna59 Sep 20, 2023
5fe951a
chore: update rule name of why we can't use default import in sankey
Yokozuna59 Sep 20, 2023
6187f4b
change test case with double quotes values
Yokozuna59 Sep 20, 2023
709ece5
Merge branch 'next' into add-sankey-langium-parser
Yokozuna59 Feb 12, 2024
aa9e875
update pnpm-lock.yaml
Yokozuna59 Feb 12, 2024
15c8bf8
update sankey parser tests
Yokozuna59 Feb 12, 2024
c364715
update the parser `index.ts` exports
Yokozuna59 Feb 12, 2024
32d82a6
add the approach from #4910 pr
Yokozuna59 Feb 12, 2024
bb84e17
change ISankeyLink into SankeyLink in sankeyDB
Yokozuna59 Feb 12, 2024
e51b7a4
modify sankye matcher
Yokozuna59 Feb 12, 2024
eb55e39
rename sankey langium parser files
Yokozuna59 Feb 12, 2024
4d035ed
simplify `sankey.langium`
Yokozuna59 Feb 13, 2024
56c643d
use `AbstractMermaidTokenBuilder` in sankey token builder
Yokozuna59 Feb 13, 2024
c0535f4
Merge branch 'develop' into add-sankey-langium-parser
Yokozuna59 Mar 24, 2024
0705aff
Merge branch 'develop' into add-sankey-langium-parser
Yokozuna59 Mar 24, 2024
bf95a30
fix lint:fix
Yokozuna59 Mar 24, 2024
434d154
remove unused imports
Yokozuna59 Mar 24, 2024
6c7b0f2
implement the greedy annotation approach
Yokozuna59 Mar 26, 2024
125ec46
Merge branch 'develop' into add-sankey-langium-parser
Yokozuna59 Mar 26, 2024
c9f6816
apply review suggestions
Yokozuna59 Mar 26, 2024
c155434
remove `utils.ts` and related unused imports
Yokozuna59 Apr 14, 2024
3597ffe
reorder sankey.langium rules
Yokozuna59 Apr 14, 2024
e080e96
Merge branch 'develop' into add-sankey-langium-parser
Yokozuna59 Apr 14, 2024
875c4fe
Merge branch 'develop' into add-sankey-langium-parser
Yokozuna59 Apr 15, 2024
51896f3
create separate test case for `CommonTokenBuilder`
Yokozuna59 Apr 15, 2024
8fa0509
run fix lint
Yokozuna59 Apr 15, 2024
681fbd0
Merge branch 'develop' into add-sankey-langium-parser
Yokozuna59 Jun 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions __mocks__/sankeyRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Mocked Sankey diagram renderer
*/
import { vi } from 'vitest';

export const draw = vi.fn().mockImplementation(() => undefined);

export const renderer = { draw };
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagram-api/diagram-orchestration.ts
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import errorDiagram from '../diagrams/error/errorDiagram.js';
import flowchartElk from '../diagrams/flowchart/elk/detector.js';
import timeline from '../diagrams/timeline/detector.js';
import mindmap from '../diagrams/mindmap/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { sankey } from '../diagrams/sankey/sankeyDetector.js';
import { packet } from '../diagrams/packet/detector.js';
import block from '../diagrams/block/blockDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
66 changes: 0 additions & 66 deletions packages/mermaid/src/diagrams/sankey/parser/sankey.jison

This file was deleted.

24 changes: 0 additions & 24 deletions packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts

This file was deleted.

16 changes: 16 additions & 0 deletions packages/mermaid/src/diagrams/sankey/sankey.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { parser } from './sankeyParser.js';
import { db } from './sankeyDB.js';
import * as fs from 'fs';
import * as path from 'path';

describe('sankey', () => {
beforeEach(() => db.clear());

it('should parse csv', async () => {
const csv = path.resolve(__dirname, './parser/energy.csv');
const data = fs.readFileSync(csv, 'utf8');
const graphDefinition = 'sankey-beta\n\n ' + data;

await parser.parse(graphDefinition);
});
});
86 changes: 35 additions & 51 deletions packages/mermaid/src/diagrams/sankey/sankeyDB.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import common from '../common/common.js';
import {
setAccTitle,
getAccTitle,
@@ -9,77 +7,63 @@ import {
getDiagramTitle,
clear as commonClear,
} from '../common/commonDb.js';
import type { SankeyDiagramConfig } from '../../config.type.js';
import type { SankeyDB, SankeyLink, SankeyFields } from './sankeyTypes.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import type { RequiredDeep } from 'type-fest';

export const DEFAULT_SANKEY_CONFIG: Required<SankeyDiagramConfig> = DEFAULT_CONFIG.sankey;

export const DEFAULT_SANKEY_DB: RequiredDeep<SankeyFields> = {
links: [] as SankeyLink[],
nodes: [] as string[],
config: DEFAULT_SANKEY_CONFIG,
} as const;

// Sankey diagram represented by nodes and links between those nodes
let links: SankeyLink[] = [];
let links: SankeyLink[] = DEFAULT_SANKEY_DB.links;
// Array of nodes guarantees their order
let nodes: SankeyNode[] = [];
// We also have to track nodes uniqueness (by ID)
let nodesMap: Record<string, SankeyNode> = {};
let nodes: string[] = DEFAULT_SANKEY_DB.nodes;
const config: Required<SankeyDiagramConfig> = structuredClone(DEFAULT_SANKEY_CONFIG);

const getConfig = (): Required<SankeyDiagramConfig> => structuredClone(config);

const clear = (): void => {
links = [];
nodes = [];
nodesMap = {};
commonClear();
};

class SankeyLink {
constructor(
public source: SankeyNode,
public target: SankeyNode,
public value: number = 0
) {}
}

/**
* @param source - Node where the link starts
* @param target - Node where the link ends
* @param value - Describes the amount to be passed
*/
const addLink = (source: SankeyNode, target: SankeyNode, value: number): void => {
links.push(new SankeyLink(source, target, value));
const addLink = ({ source, target, value }: SankeyLink): void => {
links.push({ source, target, value });
};

class SankeyNode {
constructor(public ID: string) {}
}

const findOrCreateNode = (ID: string): SankeyNode => {
ID = common.sanitizeText(ID, getConfig());
const getLinks = (): SankeyLink[] => links;

if (!nodesMap[ID]) {
nodesMap[ID] = new SankeyNode(ID);
nodes.push(nodesMap[ID]);
}
return nodesMap[ID];
const addNode = (node: string): void => {
nodes.push(node);
};

const getNodes = () => nodes;
const getLinks = () => links;
const getNodes = (): string[] => nodes;

const getGraph = () => ({
nodes: nodes.map((node) => ({ id: node.ID })),
links: links.map((link) => ({
source: link.source.ID,
target: link.target.ID,
value: link.value,
})),
});
export const db: SankeyDB = {
getConfig,
clear,

export default {
nodesMap,
getConfig: () => getConfig().sankey,
getNodes,
getLinks,
getGraph,
addLink,
findOrCreateNode,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccDescription,
getAccTitle,
setAccDescription,
getDiagramTitle,
setDiagramTitle,
clear,
getAccDescription,

addLink,
addNode,
getLinks,
getNodes,
};
12 changes: 7 additions & 5 deletions packages/mermaid/src/diagrams/sankey/sankeyDetector.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';

const id = 'sankey';

const detector: DiagramDetector = (txt) => {
return /^\s*sankey-beta/.test(txt);
};

const loader = async () => {
const loader: DiagramLoader = async () => {
const { diagram } = await import('./sankeyDiagram.js');
return { id, diagram };
};

const plugin: ExternalDiagramDefinition = {
export const sankey: ExternalDiagramDefinition = {
id,
detector,
loader,
};

export default plugin;
11 changes: 3 additions & 8 deletions packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: jison doesn't export types
import parser from './parser/sankey.jison';
import db from './sankeyDB.js';
import renderer from './sankeyRenderer.js';
import { prepareTextForParsing } from './sankeyUtils.js';

const originalParse = parser.parse.bind(parser);
parser.parse = (text: string) => originalParse(prepareTextForParsing(text));
import { parser } from './sankeyParser.js';
import { db } from './sankeyDB.js';
import { renderer } from './sankeyRenderer.js';

export const diagram: DiagramDefinition = {
parser,
26 changes: 26 additions & 0 deletions packages/mermaid/src/diagrams/sankey/sankeyParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Sankey, SankeyLink } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';

import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type { SankeyDB } from './sankeyTypes.js';
import { db } from './sankeyDB.js';

function populateDb(ast: Sankey, db: SankeyDB) {
populateCommonDb(ast, db);
ast.links.forEach((link: SankeyLink) => {
db.addLink(link);
});
ast.nodes.forEach((node: string) => {
db.addNode(node);
});
}

export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Sankey = await parse('sankey', input);
log.debug(ast);
populateDb(ast, db);
},
};
228 changes: 116 additions & 112 deletions packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import type { Diagram } from '../../Diagram.js';
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
import {
select as d3select,
scaleOrdinal as d3scaleOrdinal,
schemeTableau10 as d3schemeTableau10,
} from 'd3';

import type { SankeyNode as d3SankeyNode } from 'd3-sankey';
import type { ScaleOrdinal } from 'd3';
import { scaleOrdinal as d3scaleOrdinal, schemeTableau10 as d3schemeTableau10 } from 'd3';
import type { SankeyGraph as d3SankeyGraph, SankeyNode as d3SankeyNode } from 'd3-sankey';
import {
sankey as d3Sankey,
sankeyLinkHorizontal as d3SankeyLinkHorizontal,
@@ -15,9 +9,26 @@ import {
sankeyCenter as d3SankeyCenter,
sankeyJustify as d3SankeyJustify,
} from 'd3-sankey';

import { log } from '../../logger.js';
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
import type { MermaidConfig, SankeyDiagramConfig, SankeyNodeAlignment } from '../../config.type.js';
import type {
SankeyDB,
SankeyGraph,
SankeyLinkData,
SankeyLinkDatum,
SankeyLinkOverride,
SankeyNodeData,
SankeyNodeDatum,
SankeyNodeOverride,
} from './sankeyTypes.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { Uid } from '../../rendering-util/uid.js';
import type { SankeyNodeAlignment } from '../../config.type.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { cleanAndMerge } from '../../utils.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';

// Map config options to alignment functions
const alignmentsMap: Record<
@@ -28,109 +39,102 @@ const alignmentsMap: Record<
right: d3SankeyRight,
center: d3SankeyCenter,
justify: d3SankeyJustify,
};
} as const;

/**
* Draws Sankey diagram.
* Prepare data for construction based DB.
*
* @param text - The text of the diagram
* @param id - The id of the diagram which will be used as a DOM element id¨
* @param _version - Mermaid version from package.json
* @param diagObj - A standard diagram containing the db and the text and type etc of the diagram
* This must be a mutable object with `nodes` and `links` properties:
*
* ```json
* {
* "nodes": [{ "name": "Alice", "id": "node-1" }, { "name": "Bob", "id": "node-2" }],
* "links": [{ "id": "linearGradient-1", "source": "Alice", "target": "Bob", "value": 23 }]
* }
* ```
*
* @param db - The sankey db.
* @param config - The required config of sankey diagram.
* @returns The prepared sankey data.
*/
export const draw = function (text: string, id: string, _version: string, diagObj: Diagram): void {
// Get Sankey config
const { securityLevel, sankey: conf } = getConfig();
const defaultSankeyConfig = defaultConfig!.sankey!;

// 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
//
const width = conf?.width ?? defaultSankeyConfig.width!;
const height = conf?.height ?? defaultSankeyConfig.width!;
const useMaxWidth = conf?.useMaxWidth ?? defaultSankeyConfig.useMaxWidth!;
const nodeAlignment = conf?.nodeAlignment ?? defaultSankeyConfig.nodeAlignment!;
const prefix = conf?.prefix ?? defaultSankeyConfig.prefix!;
const suffix = conf?.suffix ?? defaultSankeyConfig.suffix!;
const showValues = conf?.showValues ?? defaultSankeyConfig.showValues!;

// 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 should be coerced to sankey DB type
const graph = diagObj.db.getGraph();

// Get alignment function
const nodeAlign = alignmentsMap[nodeAlignment];

// Construct and configure a Sankey generator
// That will be a function that calculates nodes and links dimensions
//
const createSankeyGraph = (db: SankeyDB, config: Required<SankeyDiagramConfig>) => {
const graph: SankeyGraph = structuredClone({
nodes: db.getNodes().map((node: string) => {
return {
id: Uid.next('node-').id,
name: node,
};
}),
links: db.getLinks(),
});

const nodeAlign = alignmentsMap[config.nodeAlignment];
const nodeWidth = 10;
const sankey = d3Sankey()
.nodeId((d: any) => d.id) // we use 'id' property to identify node
// eslint-disable-next-line @typescript-eslint/ban-types
const sankey = d3Sankey<SankeyNodeData, {}>()
.nodeId((node): string => node.name)
.nodeWidth(nodeWidth)
.nodePadding(10 + (showValues ? 15 : 0))
.nodePadding(10 + (config.showValues ? 15 : 0))
.nodeAlign(nodeAlign)
.extent([
[0, 0],
[width, height],
[config.width, config.height],
]);
return sankey(graph) as d3SankeyGraph<
SankeyNodeData & SankeyNodeOverride,
SankeyLinkData & SankeyLinkOverride
>;
};

// Compute the Sankey layout: calculate nodes and links positions
// Our `graph` object will be mutated by this and enriched with other properties
//
sankey(graph);
export const draw: DrawDefinition = (text, id, _version, diagObj) => {
log.debug('rendering sankey diagram\n' + text);

const db = diagObj.db as SankeyDB;
const globalConfig: MermaidConfig = getConfig();
const sankeyConfig: Required<SankeyDiagramConfig> = cleanAndMerge(
db.getConfig(),
globalConfig.sankey
);

const svg: SVG = selectSvgElement(id);

const width: number = sankeyConfig.width;
const height: number = sankeyConfig.height;
const useMaxWidth: boolean = sankeyConfig.useMaxWidth;
configureSvgSize(svg, height, width, useMaxWidth);

const graph = createSankeyGraph(db, sankeyConfig);

// Get color scheme for the graph
const colorScheme = d3scaleOrdinal(d3schemeTableau10);
const colorScheme: ScaleOrdinal<string, string, never> = d3scaleOrdinal(d3schemeTableau10);

// Create rectangles for nodes
svg
.append('g')
.attr('class', 'nodes')
.selectAll('.node')
.data(graph.nodes)
.data<SankeyNodeDatum>(graph.nodes)
.join('g')
.attr('class', 'node')
.attr('id', (d: any) => (d.uid = Uid.next('node-')).id)
.attr('transform', function (d: any) {
return 'translate(' + d.x0 + ',' + d.y0 + ')';
.attr('id', (d: SankeyNodeDatum) => d.id)
.attr('transform', (d: SankeyNodeDatum) => {
return `translate(${d.x0},${d.y0})`;
})
.attr('x', (d: any) => d.x0)
.attr('y', (d: any) => d.y0)
.attr('x', (d: SankeyNodeDatum): number => d.x0)
.attr('y', (d: SankeyNodeDatum): number => d.y0)
.append('rect')
.attr('height', (d: any) => {
return d.y1 - d.y0;
})
.attr('width', (d: any) => d.x1 - d.x0)
.attr('fill', (d: any) => colorScheme(d.id));

const getText = ({ id, value }: { id: string; value: number }) => {
.attr('height', (d: SankeyNodeDatum): number => d.y1 - d.y0)
.attr('width', (d: SankeyNodeDatum): number => d.x1 - d.x0)
.attr('fill', (d: SankeyNodeDatum): string => colorScheme(d.id));

const showValues: boolean = sankeyConfig.showValues;
const prefix: string = sankeyConfig.prefix;
const suffix: string = sankeyConfig.suffix;
const getText = ({ name, value }: SankeyNodeDatum): string => {
if (!showValues) {
return id;
return name;
}
return `${id}\n${prefix}${Math.round(value * 100) / 100}${suffix}`;
return `${name}\n${prefix}${Math.round((value ?? 0) * 100) / 100}${suffix}`;
};

// Create labels for nodes
@@ -140,71 +144,71 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.attr('font-family', 'sans-serif')
.attr('font-size', 14)
.selectAll('text')
.data(graph.nodes)
.data<SankeyNodeDatum>(graph.nodes)
.join('text')
.attr('x', (d: any) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6))
.attr('y', (d: any) => (d.y1 + d.y0) / 2)
.attr('x', (d: SankeyNodeDatum) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6))
.attr('y', (d: SankeyNodeDatum): number => (d.y1 + d.y0) / 2)
.attr('dy', `${showValues ? '0' : '0.35'}em`)
.attr('text-anchor', (d: any) => (d.x0 < width / 2 ? 'start' : 'end'))
.attr('text-anchor', (d: SankeyNodeDatum) => (d.x0 < width / 2 ? 'start' : 'end'))
.text(getText);

// Creates the paths that represent the links.
const link = svg
const links = svg
.append('g')
.attr('class', 'links')
.attr('fill', 'none')
.attr('stroke-opacity', 0.5)
.selectAll('.link')
.data(graph.links)
.data<SankeyLinkDatum>(graph.links)
.join('g')
.attr('class', 'link')
.style('mix-blend-mode', 'multiply');

const linkColor = conf?.linkColor || 'gradient';

const linkColor = sankeyConfig.linkColor;
if (linkColor === 'gradient') {
const gradient = link
const gradient = links
.append('linearGradient')
.attr('id', (d: any) => (d.uid = Uid.next('linearGradient-')).id)
.attr('id', (d: SankeyLinkDatum) => {
// @ts-ignore - figure how to stop using this approach
return (d.id = Uid.next('linearGradient-')).id;
})
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', (d: any) => d.source.x1)
.attr('x2', (d: any) => d.target.x0);
.attr('x1', (d: SankeyLinkDatum): number => d.source.x1)
.attr('x2', (d: SankeyLinkDatum): number => d.target.x0);

gradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', (d: any) => colorScheme(d.source.id));
.attr('stop-color', (d: SankeyLinkDatum): string => colorScheme(d.source.id));

gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', (d: any) => colorScheme(d.target.id));
.attr('stop-color', (d: SankeyLinkDatum): string => colorScheme(d.target.id));
}

let coloring: any;
let coloring: (d: SankeyLinkDatum) => string;
switch (linkColor) {
case 'gradient':
coloring = (d: any) => d.uid;
coloring = (d: SankeyLinkDatum): string => d.id;
break;
case 'source':
coloring = (d: any) => colorScheme(d.source.id);
coloring = (d: SankeyLinkDatum): string => colorScheme(d.source.id);
break;
case 'target':
coloring = (d: any) => colorScheme(d.target.id);
coloring = (d: SankeyLinkDatum): string => colorScheme(d.target.id);
break;
default:
coloring = linkColor;
coloring = (): string => linkColor;
}

link
links
.append('path')
.attr('d', d3SankeyLinkHorizontal())
.attr('stroke', coloring)
.attr('stroke-width', (d: any) => Math.max(1, d.width));
.attr('stroke-width', (d: SankeyLinkDatum): number => Math.max(1, d.width));

setupGraphViewbox(undefined, svg, 0, useMaxWidth);
};

export default {
draw,
};
export const renderer = { draw };
97 changes: 97 additions & 0 deletions packages/mermaid/src/diagrams/sankey/sankeyTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* eslint-disable @typescript-eslint/ban-types */
import type { SankeyNode as d3SankeyNode, SankeyLink as d3SankeyLink } from 'd3-sankey';

import type { SankeyDiagramConfig } from '../../config.type.js';
import type { DiagramDB } from '../../diagram-api/types.js';

export interface SankeyFields {
links: SankeyLink[];
nodes: string[];
config: Required<SankeyDiagramConfig>;
}

export interface SankeyNodeData {
name: string;
id: string;
}

export interface SankeyNodeOverride {
// Override optional attributes
value: number;
index: number;
depth: number;
height: number;
x0: number;
x1: number;
y0: number;
y1: number;

// Add missing attributes
layer: number;
}

export interface SankeyLinkData {
id: string;
}

export interface SankeyLinkOverride {
// Override optional attributes
sourceLinks: d3SankeyNode<
SankeyNodeData & SankeyNodeOverride,
SankeyLinkData & SankeyLinkOverride
>[];
targetLinks: d3SankeyNode<
SankeyNodeData & SankeyNodeOverride,
SankeyLinkData & SankeyLinkOverride
>[];
source: d3SankeyLink<SankeyNodeData & SankeyNodeOverride, SankeyLinkData & SankeyLinkOverride>;
target: d3SankeyLink<SankeyNodeData & SankeyNodeOverride, SankeyLinkData & SankeyLinkOverride>;
value: number;
x0: number;
x1: number;
y0: number;
y1: number;
width: number;
index: number;
}

export interface SankeyGraph {
nodes: SankeyNodeData[];
links: SankeyLink[];
}

export type SankeyNodeDatum = d3SankeyNode<
SankeyNodeData & SankeyNodeOverride,
SankeyLinkData & SankeyLinkOverride
>;

export type SankeyLinkDatum = d3SankeyLink<
SankeyNodeData & SankeyNodeOverride,
SankeyLinkData & SankeyLinkOverride
>;

export interface SankeyLink {
source: string;
target: string;
value: number;
}

export interface SankeyDB extends DiagramDB {
// config
getConfig: () => Required<SankeyDiagramConfig>;

// common db
clear: () => void;
setDiagramTitle: (title: string) => void;
getDiagramTitle: () => string;
setAccTitle: (title: string) => void;
getAccTitle: () => string;
setAccDescription: (description: string) => void;
getAccDescription: () => string;

// diagram db
addLink: ({ source, target, value }: SankeyLink) => void;
getLinks: () => SankeyLink[];
addNode: (node: string) => void;
getNodes: () => string[];
}
8 changes: 0 additions & 8 deletions packages/mermaid/src/diagrams/sankey/sankeyUtils.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/mermaid/src/mermaidAPI.spec.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ vi.mock('./diagrams/pie/pieRenderer.js');
vi.mock('./diagrams/packet/renderer.js');
vi.mock('./diagrams/xychart/xychartRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.js');
vi.mock('./diagrams/sankey/sankeyRenderer.ts');
vi.mock('./diagrams/sequence/sequenceRenderer.js');
vi.mock('./diagrams/state/stateRenderer-v2.js');

5 changes: 5 additions & 0 deletions packages/parser/langium-config.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,11 @@
"id": "pie",
"grammar": "src/language/pie/pie.langium",
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "sankey",
"grammar": "src/language/sankey/sankey.langium",
"fileExtensions": [".mmd", ".mermaid"]
}
],
"mode": "production",
27 changes: 23 additions & 4 deletions packages/parser/src/language/common/tokenBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium';
import type { TokenType } from 'chevrotain';

import { DefaultTokenBuilder } from 'langium';
import type {
CommentProvider,
GrammarAST,
LangiumCoreServices,
Stream,
TokenBuilderOptions,
} from 'langium';
import { DefaultTokenBuilder, stream } from 'langium';

export abstract class AbstractMermaidTokenBuilder extends DefaultTokenBuilder {
private keywords: Set<string>;
private commentProvider: CommentProvider;

public constructor(keywords: string[]) {
public constructor(keywords: string[], services: LangiumCoreServices) {
super();
this.keywords = new Set<string>(keywords);
this.commentProvider = services.documentation.CommentProvider;
}

protected override buildTerminalTokens(rules: Stream<GrammarAST.AbstractRule>): TokenType[] {
// put the greedy annotated terminal rules at the end of the array
const rulesArray = rules.toArray();
rules.forEach((rule, index) => {
const comment = this.commentProvider.getComment(rule);
if (comment && /@greedy/.test(comment)) {
rulesArray.push(rulesArray.splice(index, 1)[0]);
}
});
return super.buildTerminalTokens(stream(rulesArray));
}

protected override buildKeywordTokens(
6 changes: 6 additions & 0 deletions packages/parser/src/language/index.ts
Original file line number Diff line number Diff line change
@@ -5,21 +5,27 @@ export {
PacketBlock,
Pie,
PieSection,
Sankey,
SankeyLink,
isCommon,
isInfo,
isPacket,
isPacketBlock,
isPie,
isPieSection,
isSankey,
isSankeyLink,
} from './generated/ast.js';
export {
InfoGeneratedModule,
MermaidGeneratedSharedModule,
PacketGeneratedModule,
PieGeneratedModule,
SankeyGeneratedModule,
} from './generated/module.js';

export * from './common/index.js';
export * from './info/index.js';
export * from './packet/index.js';
export * from './pie/index.js';
export * from './sankey/index.js';
2 changes: 1 addition & 1 deletion packages/parser/src/language/info/module.ts
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ export type InfoServices = LangiumCoreServices & InfoAddedServices;
*/
export const InfoModule: Module<InfoServices, PartialLangiumCoreServices & InfoAddedServices> = {
parser: {
TokenBuilder: () => new InfoTokenBuilder(),
TokenBuilder: (services) => new InfoTokenBuilder(services),
ValueConverter: () => new CommonValueConverter(),
},
};
5 changes: 3 additions & 2 deletions packages/parser/src/language/info/tokenBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
import type { InfoServices } from './module.js';

export class InfoTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['info', 'showInfo']);
public constructor(services: InfoServices) {
super(['info', 'showInfo'], services);
}
}
2 changes: 1 addition & 1 deletion packages/parser/src/language/packet/module.ts
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ export const PacketModule: Module<
PartialLangiumCoreServices & PacketAddedServices
> = {
parser: {
TokenBuilder: () => new PacketTokenBuilder(),
TokenBuilder: (services) => new PacketTokenBuilder(services),
ValueConverter: () => new CommonValueConverter(),
},
};
5 changes: 3 additions & 2 deletions packages/parser/src/language/packet/tokenBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
import type { PacketServices } from './module.js';

export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['packet-beta']);
public constructor(services: PacketServices) {
super(['packet-beta'], services);
}
}
2 changes: 1 addition & 1 deletion packages/parser/src/language/pie/module.ts
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ export type PieServices = LangiumCoreServices & PieAddedServices;
*/
export const PieModule: Module<PieServices, PartialLangiumCoreServices & PieAddedServices> = {
parser: {
TokenBuilder: () => new PieTokenBuilder(),
TokenBuilder: (services) => new PieTokenBuilder(services),
ValueConverter: () => new PieValueConverter(),
},
};
5 changes: 3 additions & 2 deletions packages/parser/src/language/pie/tokenBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
import type { PieServices } from './module.js';

export class PieTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['pie', 'showData']);
public constructor(services: PieServices) {
super(['pie', 'showData'], services);
}
}
1 change: 1 addition & 0 deletions packages/parser/src/language/sankey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './module.js';
53 changes: 53 additions & 0 deletions packages/parser/src/language/sankey/matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { CustomPatternMatcherFunc } from 'chevrotain';

import { accessibilityDescrRegex, accessibilityTitleRegex, titleRegex } from '../common/matcher.js';
import { matchAnyRegexps } from '../../utils.js';

/**
* Matches sankey link source and target node
*/
export const sankeyLinkNodeRegex = /(?:"((?:""|[^"])+)")|([^\n\r,]+(?=%%)|[^\n\r,]+)/;

/**
* Matches sankey link value
*/
export const sankeyLinkValueRegex = /("(?:0|[1-9]\d*)(?:\.\d+)?"|[\t ]*(?:0|[1-9]\d*)(?:\.\d+)?)/;

/**
* Try to match the text with a SankeyNodeLink.
*
* These have to be checked first because they take precedence.
* Langium does not provide any way to specify precedence for grammars (or parts thereof)
* that are `imported` into another grammar definition file (e.g. sankey.langium).
* Specifically, the order of _tokens_ defined in the imported file (e.g. common.langium)
* may or may not come before (or after) the _tokens_ defined in the including file (e.g. sankey.langium).
*
* Thus, we have to manually handle this by first checking for matches that should take precedence
* (in this case title, accessibility title, and accessibility description)
* over matching this token.
*
* First check if the text matches a title, accessibility title, or accessibility description.
* If it matches one of those, return null (no match with a SankeyNodeLink).
*
* If it does not match one of those, then check to see if the text matches the
* SankeyNodeLink Regexp.
*
* Note that _all_ regular expressions have the sticky flag set.
*
* @param text - text to check for matches
* @param startingOffset - offset to start at
*
* @returns Null if there is no match, else return the RegExpExecArray with the match.
*/
export const matchSankeyLinkNode: CustomPatternMatcherFunc = (text, startingOffset) => {
const regexpsToFail: RegExp[] = [accessibilityDescrRegex, accessibilityTitleRegex, titleRegex];
const targetRegexp: RegExp = sankeyLinkNodeRegex;

const matchedOtherPatterns = matchAnyRegexps(text, startingOffset, regexpsToFail);
if (matchedOtherPatterns) {
return null;
}
const stickyTargetRegexp = new RegExp(targetRegexp, targetRegexp.flags + 'y');
stickyTargetRegexp.lastIndex = startingOffset;
return stickyTargetRegexp.exec(text);
};
81 changes: 81 additions & 0 deletions packages/parser/src/language/sankey/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumParser,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';

import { MermaidGeneratedSharedModule, SankeyGeneratedModule } from '../generated/module.js';
import { SankeyTokenBuilder } from './tokenBuilder.js';
import { SankeyValueConverter } from './valueConverter.js';
import { createSankeyParser } from './parser.js';

/**
* Declaration of `Sankey` services.
*/
export type SankeyAddedServices = {
parser: {
LangiumParser: LangiumParser;
TokenBuilder: SankeyTokenBuilder;
ValueConverter: SankeyValueConverter;
};
};

/**
* Union of Langium default services and `Sankey` services.
*/
export type SankeyServices = LangiumCoreServices & SankeyAddedServices;

/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Sankey` services.
*/
export const SankeyModule: Module<
SankeyServices,
PartialLangiumCoreServices & SankeyAddedServices
> = {
parser: {
LangiumParser: (services) => createSankeyParser(services),
TokenBuilder: (services) => new SankeyTokenBuilder(services),
ValueConverter: () => new SankeyValueConverter(),
},
};

/**
* Create the full set of services required by Langium.
*
* First inject the shared services by merging two modules:
* - Langium default shared services
* - Services generated by langium-cli
*
* Then inject the language-specific services by merging three modules:
* - Langium default language-specific services
* - Services generated by langium-cli
* - Services specified in this file
* @param context - Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
*/
export function createSankeyServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Sankey: SankeyServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Sankey: SankeyServices = inject(
createDefaultCoreModule({ shared }),
SankeyGeneratedModule,
SankeyModule
);
shared.ServiceRegistry.register(Sankey);
return { shared, Sankey };
}
30 changes: 30 additions & 0 deletions packages/parser/src/language/sankey/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AstNode, LangiumParser, ParseResult } from 'langium';
import { createLangiumParser } from 'langium';

import type { SankeyServices } from './module.js';
import type { SankeyLink } from '../generated/ast.js';
import { isSankey } from '../generated/ast.js';

export function createSankeyParser(services: SankeyServices): LangiumParser {
const parser: LangiumParser = createLangiumParser(services);
const parse = parser.parse.bind(parser);
parser.parse = <T extends AstNode = AstNode>(input: string): ParseResult<T> => {
const parseResult: ParseResult<T> = parse(input);

if (isSankey(parseResult.value)) {
const nodes: Set<string> = new Set<string>();
parseResult.value.links.forEach((link: SankeyLink) => {
if (!nodes.has(link.source)) {
nodes.add(link.source);
}
if (!nodes.has(link.target)) {
nodes.add(link.target);
}
});
parseResult.value.nodes = [...nodes];
}

return parseResult;
};
return parser;
}
26 changes: 26 additions & 0 deletions packages/parser/src/language/sankey/sankey.langium
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
grammar Sankey
import "../common/common";

interface Sankey extends Common {
links: SankeyLink[];
nodes: string[];
}

entry Sankey returns Sankey:
NEWLINE*
"sankey-beta"
(
NEWLINE* TitleAndAccessibilities links+=SankeyLink+
| NEWLINE+ links+=SankeyLink+
)
;

SankeyLink:
source=SANKEY_LINK_NODE "," target=SANKEY_LINK_NODE "," value=SANKEY_LINK_VALUE EOL
;

terminal SANKEY_LINK_VALUE returns number: /"(0|[1-9][0-9]*)(\.[0-9]+)?"|[\t ]*(0|[1-9][0-9]*)(\.[0-9]+)?/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we have the same regexp here. If this is a limitation of languim that definitely brings a boilerplate to the code.

/**
* @greedy
*/
terminal SANKEY_LINK_NODE: /sankey-link-node/;
26 changes: 26 additions & 0 deletions packages/parser/src/language/sankey/tokenBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { GrammarAST, Stream } from 'langium';
import type { TokenType } from 'chevrotain';

import { AbstractMermaidTokenBuilder } from '../common/index.js';
import { matchSankeyLinkNode } from './matcher.js';
import type { SankeyServices } from './module.js';

export class SankeyTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor(services: SankeyServices) {
super(['sankey-beta'], services);
}

protected override buildTerminalTokens(rules: Stream<GrammarAST.AbstractRule>): TokenType[] {
const tokenTypes: TokenType[] = super.buildTerminalTokens(rules);
tokenTypes.forEach((tokenType: TokenType): void => {
switch (tokenType.name) {
case 'SANKEY_LINK_NODE': {
tokenType.LINE_BREAKS = false;
tokenType.PATTERN = matchSankeyLinkNode;
break;
}
}
});
return tokenTypes;
}
}
43 changes: 43 additions & 0 deletions packages/parser/src/language/sankey/valueConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';

import { AbstractMermaidValueConverter } from '../common/valueConverter.js';
import { sankeyLinkNodeRegex, sankeyLinkValueRegex } from './matcher.js';

const rulesRegexes: Record<string, RegExp> = {
SANKEY_LINK_NODE: sankeyLinkNodeRegex,
SANKEY_LINK_VALUE: sankeyLinkValueRegex,
};

export class SankeyValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
rule: GrammarAST.AbstractRule,
input: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_cstNode: CstNode
): ValueType | undefined {
const regex: RegExp | undefined = rulesRegexes[rule.name];
if (regex === undefined) {
return undefined;
}
const match = regex.exec(input);
if (match === null) {
return undefined;
}

// source and target with double quote and value
if (match[1] !== undefined) {
if (rule.name === 'SANKEY_LINK_VALUE') {
return Number(match[1].replaceAll('"', ''));
}
return match[1]
.replaceAll('""', '"')
.trim()
.replaceAll(/[\t ]{2,}/gm, ' ');
}
// source and target without double quote
if (match[2] !== undefined) {
return match[2].trim().replaceAll(/[\t ]{2,}/gm, ' ');
}
return undefined;
}
}
10 changes: 8 additions & 2 deletions packages/parser/src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { LangiumParser, ParseResult } from 'langium';

import type { Info, Packet, Pie } from './index.js';
import type { Info, Packet, Pie, Sankey } from './index.js';

export type DiagramAST = Info | Packet | Pie;
export type DiagramAST = Info | Packet | Pie | Sankey;

const parsers: Record<string, LangiumParser> = {};
const initializers = {
@@ -21,11 +21,17 @@ const initializers = {
const parser = createPieServices().Pie.parser.LangiumParser;
parsers['pie'] = parser;
},
sankey: async () => {
const { createSankeyServices } = await import('./language/sankey/index.js');
const parser = createSankeyServices().Sankey.parser.LangiumParser;
parsers['sankey'] = parser;
},
} as const;

export async function parse(diagramType: 'info', text: string): Promise<Info>;
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
export async function parse(diagramType: 'sankey', text: string): Promise<Sankey>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
25 changes: 25 additions & 0 deletions packages/parser/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const STICKY_FLAG = 'y';

/**
* Check to see if text matches any of the given RegExps. Return true as soon
* as there is a match. Use the sticky flag on all RegExps.
*
* @param text - the string to try to match
* @param startingOffset - offset to start at
* @param regexps - a list of RegExps to check for matches
* @internal
*
* @returns true if the text matches any of the RegExps (with the sticky flag set),
* else returns false.
*/
export function matchAnyRegexps(text: string, startingOffset: number, regexps: RegExp[]): boolean {
const found = false;
for (const regexp of regexps) {
const currentRegexp = new RegExp(regexp, regexp.flags + STICKY_FLAG);
currentRegexp.lastIndex = startingOffset;
if (currentRegexp.exec(text) !== null) {
return true;
}
}
return found;
}
178 changes: 178 additions & 0 deletions packages/parser/tests/sankey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { describe, expect, it } from 'vitest';

import { Sankey } from '../src/language/index.js';
import { expectNoErrorsOrAlternatives, sankeyParse as parse } from './test-util.js';

describe('sankey', () => {
it('should handle simple sankey', () => {
const context = `sankey-beta
sourceNode, targetNode, 10`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('sourceNode');
expect(value.links[0].target).toBe('targetNode');
expect(value.links[0].value).toBe(10);

expect(value.nodes).toStrictEqual(['sourceNode', 'targetNode']);
});

it('should handle sankey with double quotes', () => {
const context = `sankey-beta
"source node, with comma","target node, with comma","10.00"
`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source node, with comma');
expect(value.links[0].target).toBe('target node, with comma');
expect(value.links[0].value).toBe(10);

expect(value.nodes).toStrictEqual(['source node, with comma', 'target node, with comma']);
});

it('should handle sankey with double quotes with newline and doable quotes', () => {
const context = `sankey-beta
"source node
"" double quotes","target node
"" double quotes","10.00"
`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe(`source node
" double quotes`);
expect(value.links[0].target).toBe(`target node
" double quotes`);
expect(value.links[0].value).toBe(10);

expect(value.nodes).toStrictEqual([
`source node
" double quotes`,
`target node
" double quotes`,
]);
});

it('should handle sankey with more than one link', () => {
const context = `sankey-beta
source node 1, target node 1, 10
source node 2, target node 2, 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source node 1');
expect(value.links[0].target).toBe('target node 1');
expect(value.links[0].value).toBe(10);

expect(value.links[1].source).toBe('source node 2');
expect(value.links[1].target).toBe('target node 2');
expect(value.links[1].value).toBe(50);

expect(value.nodes).toStrictEqual([
'source node 1',
'target node 1',
'source node 2',
'target node 2',
]);
});

it('should handle sankey with duplicate nodes', () => {
const context = `sankey-beta
source, target, 10
target, another target, 20`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source');
expect(value.links[0].target).toBe('target');
expect(value.links[0].value).toBe(10);

expect(value.links[1].source).toBe('target');
expect(value.links[1].target).toBe('another target');
expect(value.links[1].value).toBe(20);

expect(value.nodes).toStrictEqual(['source', 'target', 'another target']);
});

describe('title and accessibilities', () => {
it('should handle title definition', () => {
const context = `sankey-beta title awesome title
source, target, 10`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source');
expect(value.links[0].target).toBe('target');
expect(value.links[0].value).toBe(10);
});

it('should handle accTitle definition', () => {
const context = `sankey-beta accTitle: awesome accTitle
source, target, 10`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source');
expect(value.links[0].target).toBe('target');
expect(value.links[0].value).toBe(10);
});

it('should handle single line accDescr definition', () => {
const context = `sankey-beta accDescr: awesome accDescr
source, target, 10`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source');
expect(value.links[0].target).toBe('target');
expect(value.links[0].value).toBe(10);
});

it('should handle multi line accDescr definition', () => {
const context = `sankey-beta accDescr {
awesome accDescr
}
source, target, 10`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source');
expect(value.links[0].target).toBe('target');
expect(value.links[0].value).toBe(10);
});

it('should handle title and accessibilities definition', () => {
const context = `sankey-beta title awesome title
accTitle: awesome accTitle
accDescr: awesome accDescr
source, target, 10`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);

const value = result.value;
expect(value.$type).toBe(Sankey);
expect(value.links[0].source).toBe('source');
expect(value.links[0].target).toBe('target');
expect(value.links[0].value).toBe(10);
});
});
});
25 changes: 23 additions & 2 deletions packages/parser/tests/test-util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import type { LangiumParser, ParseResult } from 'langium';
import { expect, vi } from 'vitest';
import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js';
import { createInfoServices, createPieServices } from '../src/language/index.js';
import type {
Info,
InfoServices,
Pie,
PieServices,
Sankey,
SankeyServices,
} from '../src/language/index.js';
import {
createInfoServices,
createPieServices,
createSankeyServices,
} from '../src/language/index.js';

const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);

@@ -40,3 +51,13 @@ export function createPieTestServices() {
return { services: pieServices, parse };
}
export const pieParse = createPieTestServices().parse;

const sankeyServices: SankeyServices = createSankeyServices().Sankey;
const sankeyParser: LangiumParser = sankeyServices.parser.LangiumParser;
export function createSankeyTestServices() {
const parse = (input: string) => {
return sankeyParser.parse<Sankey>(input);
};
return { services: sankeyServices, parse };
}
export const sankeyParse = createSankeyTestServices().parse;

Unchanged files with check annotations Beta

declare global {
interface Window {
mermaid: any; // 👈️ turn off type checking

Check warning on line 5 in packages/mermaid-example-diagram/src/types/index.d.ts

GitHub Actions / lint

Unexpected any. Specify a different type
}
}
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
// We create a SVG label, either by delegating to addHtmlLabel or manually
let vertexNode;

Check warning on line 67 in packages/mermaid-flowchart-elk/src/flowRenderer-elk.js

GitHub Actions / lint

'vertexNode' is defined but never used
const labelData = { width: 0, height: 0 };
const ports = [
nodeEl = await insertNode(nodes, node, vertex.dir);
boundingBox = nodeEl.node().getBBox();
} else {
const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');

Check warning on line 191 in packages/mermaid-flowchart-elk/src/flowRenderer-elk.js

GitHub Actions / lint

'svgLabel' is assigned a value but never used
// svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
// const rows = vertexText.split(common.lineBreakRegex);
// for (const row of rows) {
* Add edges to graph based on parsed graph definition
*
* @param {object} edges The edges to add to the graph
* @param {object} g The graph object

Check warning on line 397 in packages/mermaid-flowchart-elk/src/flowRenderer-elk.js

GitHub Actions / lint

Expected @param names to be "edges, diagObj, graph, svg". Got "edges, g, cy, diagObj, graph, svg"
* @param cy
* @param diagObj
* @param graph
/**
* Recursive function that iterates over an array of nodes and inserts the children of each node.
* It also recursively populates the inserts the children of the children and so on.
* @param {*} graph

Check warning on line 680 in packages/mermaid-flowchart-elk/src/flowRenderer-elk.js

GitHub Actions / lint

Expected @param names to be "nodeArray, parentLookupDb". Got "graph, nodeArray, parentLookupDb"
* @param nodeArray
* @param parentLookupDb
*/
export let sanitizeText: (str: string) => string;
// eslint-disable @typescript-eslint/no-explicit-any
export let setupGraphViewbox: (
graph: any,

Check warning on line 34 in packages/mermaid-zenuml/src/mermaidUtils.ts

GitHub Actions / lint

Unexpected any. Specify a different type
svgElem: any,

Check warning on line 35 in packages/mermaid-zenuml/src/mermaidUtils.ts

GitHub Actions / lint

Unexpected any. Specify a different type
padding: any,

Check warning on line 36 in packages/mermaid-zenuml/src/mermaidUtils.ts

GitHub Actions / lint

Unexpected any. Specify a different type
useMaxWidth: boolean
) => void;
export const injectUtils = (
_log: Record<keyof typeof LEVELS, typeof console.log>,
_setLogLevel: any,

Check warning on line 42 in packages/mermaid-zenuml/src/mermaidUtils.ts

GitHub Actions / lint

Unexpected any. Specify a different type
_getConfig: any,

Check warning on line 43 in packages/mermaid-zenuml/src/mermaidUtils.ts

GitHub Actions / lint

Unexpected any. Specify a different type
_sanitizeText: any,
_setupGraphViewbox: any
) => {