From 8df21ae4726e0ca9da15571d25d2abf807e46172 Mon Sep 17 00:00:00 2001 From: Reda Al Sulais Date: Sat, 2 Sep 2023 04:18:51 +0300 Subject: [PATCH] feat: create `sankey` parser and integrate `sankey` parser into `mermaid` package --- .../src/diagram-api/diagram-orchestration.ts | 2 +- .../src/diagrams/sankey/parser/sankey.jison | 69 ----- .../src/diagrams/sankey/parser/sankey.spec.ts | 24 -- .../src/diagrams/sankey/sankey.spec.ts | 16 ++ .../mermaid/src/diagrams/sankey/sankeyDB.ts | 94 +++---- .../src/diagrams/sankey/sankeyDetector.ts | 12 +- .../src/diagrams/sankey/sankeyDiagram.ts | 11 +- .../src/diagrams/sankey/sankeyParser.ts | 26 ++ .../src/diagrams/sankey/sankeyRenderer.ts | 245 +++++++++--------- .../src/diagrams/sankey/sankeyTypes.ts | 97 +++++++ .../src/diagrams/sankey/sankeyUtils.ts | 8 - packages/parser/langium-config.json | 5 + packages/parser/src/index.ts | 2 +- .../parser/src/language/common/common.langium | 6 + packages/parser/src/language/index.ts | 1 + packages/parser/src/language/sankey/index.ts | 1 + .../parser/src/language/sankey/sankey.langium | 54 ++++ .../src/language/sankey/sankeyMatcher.ts | 14 + .../src/language/sankey/sankeyModule.ts | 76 ++++++ .../src/language/sankey/sankeyParser.ts | 30 +++ .../src/language/sankey/sankeyTokenBuilder.ts | 36 +++ .../language/sankey/sankeyValueConverter.ts | 85 ++++++ packages/parser/src/language/utils.ts | 10 + packages/parser/src/parse.ts | 12 +- packages/parser/tests/sankey.test.ts | 194 ++++++++++++++ pnpm-lock.yaml | 79 ++++-- 26 files changed, 905 insertions(+), 304 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/sankey/parser/sankey.jison delete mode 100644 packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts create mode 100644 packages/mermaid/src/diagrams/sankey/sankey.spec.ts create mode 100644 packages/mermaid/src/diagrams/sankey/sankeyParser.ts create mode 100644 packages/mermaid/src/diagrams/sankey/sankeyTypes.ts delete mode 100644 packages/mermaid/src/diagrams/sankey/sankeyUtils.ts create mode 100644 packages/parser/src/language/sankey/index.ts create mode 100644 packages/parser/src/language/sankey/sankey.langium create mode 100644 packages/parser/src/language/sankey/sankeyMatcher.ts create mode 100644 packages/parser/src/language/sankey/sankeyModule.ts create mode 100644 packages/parser/src/language/sankey/sankeyParser.ts create mode 100644 packages/parser/src/language/sankey/sankeyTokenBuilder.ts create mode 100644 packages/parser/src/language/sankey/sankeyValueConverter.ts create mode 100644 packages/parser/src/language/utils.ts create mode 100644 packages/parser/tests/sankey.test.ts diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 71bc8c3be83..4329fd70b8b 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -18,7 +18,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 { registerLazyLoadedDiagrams } from './detectType.js'; import { registerDiagram } from './diagramAPI.js'; diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison deleted file mode 100644 index b11f8a87b0e..00000000000 --- a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison +++ /dev/null @@ -1,69 +0,0 @@ -/** 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 - -%options case-insensitive -%options easy_keword_rules - -%x escaped_text -%x csv - -// as per section 6.1 of RFC 2234 [2] -COMMA \u002C -CR \u000D -LF \u000A -CRLF \u000D\u000A -ESCAPED_QUOTE \u0022 -DQUOTE \u0022 -TEXTDATA [\u0020-\u0021\u0023-\u002B\u002D-\u007E] - -%% - -"sankey-beta" { this.pushState('csv'); return 'SANKEY'; } -<> { return 'EOF' } // match end of file -({CRLF}|{LF}) { return 'NEWLINE' } -{COMMA} { return 'COMMA' } -{DQUOTE} { this.pushState('escaped_text'); return 'DQUOTE'; } -{TEXTDATA}* { return 'NON_ESCAPED_TEXT' } -{DQUOTE}(?!{DQUOTE}) {this.popState('escaped_text'); return 'DQUOTE'; } // unescaped DQUOTE closes string -({TEXTDATA}|{COMMA}|{CR}|{LF}|{DQUOTE}{DQUOTE})* { return 'ESCAPED_TEXT'; } - -/lex - -%start start - -%% // language grammar - -start: SANKEY NEWLINE csv opt_eof; - -csv: record csv_tail; -csv_tail: NEWLINE csv | ; -opt_eof: EOF | ; - -record - : field\[source] COMMA field\[target] COMMA field\[value] { - const source = yy.findOrCreateNode($source.trim().replaceAll('""', '"')); - const target = yy.findOrCreateNode($target.trim().replaceAll('""', '"')); - const value = parseFloat($value.trim()); - yy.addLink(source,target,value); - } // parse only 3 fields, this is not part of CSV standard - ; - -field - : escaped { $$=$escaped; } - | non_escaped { $$=$non_escaped; } - ; - -escaped: DQUOTE ESCAPED_TEXT DQUOTE { $$=$ESCAPED_TEXT; }; - -non_escaped: NON_ESCAPED_TEXT { $$=$NON_ESCAPED_TEXT; }; - - diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts deleted file mode 100644 index 4517ca01fcf..00000000000 --- a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-ignore: jison doesn't export types -import sankey from './sankey.jison'; -import db from '../sankeyDB.js'; -import { cleanupComments } from '../../../diagram-api/comments.js'; -import { prepareTextForParsing } from '../sankeyUtils.js'; -import * as fs from 'fs'; -import * as path from 'path'; - -describe('Sankey diagram', function () { - describe('when parsing an info graph it', function () { - beforeEach(function () { - sankey.parser.yy = db; - sankey.parser.yy.clear(); - }); - - it('parses csv', async () => { - const csv = path.resolve(__dirname, './energy.csv'); - const data = fs.readFileSync(csv, 'utf8'); - const graphDefinition = prepareTextForParsing(cleanupComments('sankey-beta\n\n ' + data)); - - sankey.parser.parse(graphDefinition); - }); - }); -}); diff --git a/packages/mermaid/src/diagrams/sankey/sankey.spec.ts b/packages/mermaid/src/diagrams/sankey/sankey.spec.ts new file mode 100644 index 00000000000..9a0271b7a86 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankey.spec.ts @@ -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', () => { + const csv = path.resolve(__dirname, './parser/energy.csv'); + const data = fs.readFileSync(csv, 'utf8'); + const graphDefinition = 'sankey-beta\n\n ' + data; + + parser.parse(graphDefinition); + }); +}); diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDB.ts b/packages/mermaid/src/diagrams/sankey/sankeyDB.ts index f6db1886d60..6830afc4db6 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyDB.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyDB.ts @@ -1,5 +1,3 @@ -import * as configApi from '../../config.js'; -import common from '../common/common.js'; import { setAccTitle, getAccTitle, @@ -9,73 +7,75 @@ import { getDiagramTitle, clear as commonClear, } from '../../commonDb.js'; +import type { SankeyDiagramConfig } from '../../config.type.js'; +import type { SankeyDB, SankeyLink as ISankeyLink, SankeyFields } from './sankeyTypes.js'; +import DEFAULT_CONFIG from '../../defaultConfig.js'; +import type { RequiredDeep } from 'type-fest'; + +export const DEFAULT_SANKEY_CONFIG: Required = DEFAULT_CONFIG.sankey; + +export const DEFAULT_SANKEY_DB: RequiredDeep = { + links: [] as ISankeyLink[], + nodes: [] as string[], + config: DEFAULT_SANKEY_CONFIG, +} as const; // Sankey diagram represented by nodes and links between those nodes -let links: SankeyLink[] = []; +let links: ISankeyLink[] = 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 = {}; +let nodes: string[] = DEFAULT_SANKEY_DB.nodes; +const config: Required = structuredClone(DEFAULT_SANKEY_CONFIG); + +const getConfig = (): Required => { + return { + ...structuredClone(config), + width: 600, + height: 400, + linkColor: 'gradient', + nodeAlignment: 'justify', + useMaxWidth: true, + showValues: true, + prefix: '', + suffix: '', + }; +}; 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 }: ISankeyLink): void => { + links.push({ source, target, value }); }; -class SankeyNode { - constructor(public ID: string) {} -} - -const findOrCreateNode = (ID: string): SankeyNode => { - ID = common.sanitizeText(ID, configApi.getConfig()); +const getLinks = (): ISankeyLink[] => 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, -export default { - nodesMap, - getConfig: () => configApi.getConfig().sankey, - getNodes, - getLinks, - getGraph, - addLink, - findOrCreateNode, - getAccTitle, + clear, + setDiagramTitle, + getDiagramTitle, setAccTitle, - getAccDescription, + getAccTitle, setAccDescription, - getDiagramTitle, - setDiagramTitle, - clear, + getAccDescription, + + addLink, + addNode, + getLinks, + getNodes, }; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts b/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts index 73c4d142894..4a4b33875ac 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts @@ -1,4 +1,8 @@ -import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js'; +import type { + DiagramDetector, + DiagramLoader, + ExternalDiagramDefinition, +} from '../../diagram-api/types.js'; const id = 'sankey'; @@ -6,15 +10,13 @@ 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; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts b/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts index 6fed435ac4f..5f9ebc1a81d 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts @@ -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, diff --git a/packages/mermaid/src/diagrams/sankey/sankeyParser.ts b/packages/mermaid/src/diagrams/sankey/sankeyParser.ts new file mode 100644 index 00000000000..fe8e3330a9f --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyParser.ts @@ -0,0 +1,26 @@ +import type { Sankey, SankeyLink } from 'mermaid-parser'; +import { parse } from 'mermaid-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: (input: string): void => { + const ast: Sankey = parse('sankey', input); + log.debug(ast); + populateDb(ast, db); + }, +}; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts b/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts index 9f5b3c17203..c4342db7286 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts @@ -1,13 +1,6 @@ -import type { Diagram } from '../../Diagram.js'; -import * as configApi from '../../config.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, @@ -16,9 +9,26 @@ import { sankeyCenter as d3SankeyCenter, sankeyJustify as d3SankeyJustify, } from 'd3-sankey'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; + +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, + SankeyLink, + SankeyLinkData, + SankeyLinkDatum, + SankeyLinkOverride, + SankeyNodeData, + SankeyNodeDatum, + SankeyNodeOverride, +} from './sankeyTypes.js'; import { Uid } from '../../rendering-util/uid.js'; -import type { SankeyNodeAlignment } from '../../config.type.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { getConfig } from '../../config.js'; +import { cleanAndMerge } from '../../utils.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; // Map config options to alignment functions const alignmentsMap: Record< @@ -29,115 +39,109 @@ const alignmentsMap: Record< right: d3SankeyRight, center: d3SankeyCenter, justify: d3SankeyJustify, -}; +} as const; /** - * Draws Sankey diagram. + * Prepare data for construction based DB. + * + * 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 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 + * @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 } = configApi.getConfig(); - const defaultSankeyConfig = configApi!.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!; - - // 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 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) => { + const graph: SankeyGraph = structuredClone({ + nodes: db.getNodes().map((node: string) => { + return { + id: Uid.next('node-').id, + name: node, + }; + }), + links: db.getLinks().map((link: SankeyLink) => { + return { + id: Uid.next('linearGradient-').id, + source: link.source, + target: link.target, + value: link.value, + }; + }), + }); + + const nodeAlign = alignmentsMap[config.nodeAlignment]; const nodeWidth = 10; - const sankey = d3Sankey() - .nodeId((d: any) => d.id) // we use 'id' property to identify node + const sankey = d3Sankey() + .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 + >; +}; + +export const draw: DrawDefinition = (text, id, _version, diagObj) => { + log.debug('rendering sankey diagram\n' + text); - // Compute the Sankey layout: calculate nodes and links positions - // Our `graph` object will be mutated by this and enriched with other properties - // - sankey(graph); + const db = diagObj.db as SankeyDB; + const globalConfig: MermaidConfig = getConfig(); + const sankeyConfig: Required = 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); + log.fatal(graph); // Get color scheme for the graph - const colorScheme = d3scaleOrdinal(d3schemeTableau10); + const colorScheme: ScaleOrdinal = d3scaleOrdinal(d3schemeTableau10); // Create rectangles for nodes svg .append('g') .attr('class', 'nodes') .selectAll('.node') - .data(graph.nodes) + .data(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 @@ -147,69 +151,70 @@ 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(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(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) + // @ts-ignore - figure out how to use id instead + .attr('id', (d) => (d.uid = Uid.next('linearGradient-')).id) + // .attr('id', (d: SankeyLinkDatum): string => d.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; + // @ts-ignore - figure out how to use id instead + coloring = (d: SankeyLinkDatum): string => 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)); }; -export default { - draw, -}; +export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyTypes.ts b/packages/mermaid/src/diagrams/sankey/sankeyTypes.ts new file mode 100644 index 00000000000..0f278988b10 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyTypes.ts @@ -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; +} + +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; + target: d3SankeyLink; + value: number; + x0: number; + x1: number; + y0: number; + y1: number; + width: number; + index: number; +} + +export interface SankeyGraph { + nodes: SankeyNodeData[]; + links: (SankeyLink & SankeyLinkData)[]; +} + +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; + + // common db + clear: () => void; + setDiagramTitle: (title: string) => void; + getDiagramTitle: () => string; + setAccTitle: (title: string) => void; + getAccTitle: () => string; + setAccDescription: (describetion: string) => void; + getAccDescription: () => string; + + // diagram db + addLink: ({ source, target, value }: SankeyLink) => void; + getLinks: () => SankeyLink[]; + addNode: (node: string) => void; + getNodes: () => string[]; +} diff --git a/packages/mermaid/src/diagrams/sankey/sankeyUtils.ts b/packages/mermaid/src/diagrams/sankey/sankeyUtils.ts deleted file mode 100644 index 45ecf21ddae..00000000000 --- a/packages/mermaid/src/diagrams/sankey/sankeyUtils.ts +++ /dev/null @@ -1,8 +0,0 @@ -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; -}; diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index 4ffaaf372fc..5594fb1d181 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -5,6 +5,11 @@ "id": "info", "grammar": "src/language/info/info.langium", "fileExtensions": [".mmd", ".mermaid"] + }, + { + "id": "sankey", + "grammar": "src/language/sankey/sankey.langium", + "fileExtensions": [".mmd", ".mermaid"] } ], "mode": "production", diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index 9dded54fa27..ca970453db7 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -1,3 +1,3 @@ -export type { Info } from './language/index.js'; +export type { Info, Sankey, SankeyLink } from './language/index.js'; export type { DiagramAST } from './parse.js'; export { parse, MermaidParseError } from './parse.js'; diff --git a/packages/parser/src/language/common/common.langium b/packages/parser/src/language/common/common.langium index 6a2af29e4a5..bf4ebca0669 100644 --- a/packages/parser/src/language/common/common.langium +++ b/packages/parser/src/language/common/common.langium @@ -1,3 +1,9 @@ +interface Common { + accDescr?: string; + accTitle?: string; + title?: string; +} + fragment TitleAndAccessibilities: ((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) NEWLINE+)+ ; diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index b6685a07f65..7e4e231912e 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -4,3 +4,4 @@ export * from './generated/module.js'; export * from './common/index.js'; export * from './info/index.js'; +export * from './sankey/index.js'; diff --git a/packages/parser/src/language/sankey/index.ts b/packages/parser/src/language/sankey/index.ts new file mode 100644 index 00000000000..6ca32f63baa --- /dev/null +++ b/packages/parser/src/language/sankey/index.ts @@ -0,0 +1 @@ +export * from './sankeyModule.js'; diff --git a/packages/parser/src/language/sankey/sankey.langium b/packages/parser/src/language/sankey/sankey.langium new file mode 100644 index 00000000000..302830dd925 --- /dev/null +++ b/packages/parser/src/language/sankey/sankey.langium @@ -0,0 +1,54 @@ +grammar Sankey + +// common stuff in ../common/common.langium +// can't import directly because of SANKEY_LINK_SOURCE parse rule +interface Common { + accDescr?: string; + accTitle?: string; + title?: string; +} + +fragment TitleAndAccessibilities: + ((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) NEWLINE+)+ +; + +terminal NEWLINE: /\r?\n/; +terminal ACC_DESCR: /accDescr(?:[\t ]*:[\t ]*[^\n\r]*?(?=%%)|\s*{[^}]*})|accDescr(?:[\t ]*:[\t ]*[^\n\r]*|\s*{[^}]*})/; +terminal ACC_TITLE: /accTitle[\t ]*:[\t ]*[^\n\r]*?(?=%%)|accTitle[\t ]*:[\t ]*[^\n\r]*/; +terminal TITLE: /title(?:[\t ]+[^\n\r]*?|)(?=%%)|title(?:[\t ]+[^\n\r]*|)/; + +hidden terminal WHITESPACE: /[\t ]+/; +hidden terminal YAML: /---[\t ]*\r?\n[\s\S]*?---[\t ]*(?!.)/; +hidden terminal DIRECTIVE: /[\t ]*%%{[\s\S]*?}%%\s*/; +hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/; + +// actual grammar +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_SOURCE target=SANKEY_LINK_TARGET value=SANKEY_LINK_VALUE NEWLINE+ +; + +// SankeySourceId: +// id=SANKEY_LINK_SOURCE +// ; + +// SankeyTargetId: +// id=SANKEY_LINK_TARGET +// ; + +terminal SANKEY_LINK_VALUE returns number: /,("(0|[1-9][0-9]*)(\.[0-9]+)?"|[\t ]*(0|[1-9][0-9]*)(\.[0-9]+)?)/; +terminal SANKEY_LINK_TARGET: /,("((?:""|[^"])+)"|[^\n\r,]+)/; +terminal SANKEY_LINK_SOURCE: /sankey-link-source/; diff --git a/packages/parser/src/language/sankey/sankeyMatcher.ts b/packages/parser/src/language/sankey/sankeyMatcher.ts new file mode 100644 index 00000000000..ba3c53f1eb0 --- /dev/null +++ b/packages/parser/src/language/sankey/sankeyMatcher.ts @@ -0,0 +1,14 @@ +/** + * Matches sankey link source node + */ +export const sankeyLinkSourceRegex = /(?:"((?:""|[^"])+)")|([^\n\r,]+)/; + +/** + * Matches sankey link target node + */ +export const sankeyLinkTargetRegex = /,(?:(?:"((?:""|[^"])+)")|([^\n\r,]+))/; + +/** + * Matches sankey link value + */ +export const sankeyLinkValueRegex = /,("(?:0|[1-9]\d*)(?:\.\d+)?"|[\t ]*(?:0|[1-9]\d*)(?:\.\d+)?)/; diff --git a/packages/parser/src/language/sankey/sankeyModule.ts b/packages/parser/src/language/sankey/sankeyModule.ts new file mode 100644 index 00000000000..c85c04a4809 --- /dev/null +++ b/packages/parser/src/language/sankey/sankeyModule.ts @@ -0,0 +1,76 @@ +import type { + DefaultSharedModuleContext, + LangiumParser, + LangiumServices, + LangiumSharedServices, + Module, + PartialLangiumServices, +} from 'langium'; +import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium'; + +import { MermaidGeneratedSharedModule, SankeyGeneratedModule } from '../generated/module.js'; +import { CommonLexer } from '../common/commonLexer.js'; +import { SankeyTokenBuilder } from './sankeyTokenBuilder.js'; +import { SankeyValueConverter } from './sankeyValueConverter.js'; +import { createSankeyParser } from './sankeyParser.js'; + +/** + * Declaration of `Sankey` services. + */ +export type SankeyAddedServices = { + parser: { + LangiumParser: LangiumParser; + Lexer: CommonLexer; + TokenBuilder: SankeyTokenBuilder; + ValueConverter: SankeyValueConverter; + }; +}; + +/** + * Union of Langium default services and `Sankey` services. + */ +export type SankeyServices = LangiumServices & SankeyAddedServices; + +/** + * Dependency injection module that overrides Langium default services and + * contributes the declared `Sankey` services. + */ +export const SankeyModule: Module = { + parser: { + LangiumParser: (services) => createSankeyParser(services), + Lexer: (services) => new CommonLexer(services), + TokenBuilder: () => new SankeyTokenBuilder(), + 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: DefaultSharedModuleContext = EmptyFileSystem): { + shared: LangiumSharedServices; + Sankey: SankeyServices; +} { + const shared: LangiumSharedServices = inject( + createDefaultSharedModule(context), + MermaidGeneratedSharedModule + ); + const Sankey: SankeyServices = inject( + createDefaultModule({ shared }), + SankeyGeneratedModule, + SankeyModule + ); + shared.ServiceRegistry.register(Sankey); + return { shared, Sankey }; +} diff --git a/packages/parser/src/language/sankey/sankeyParser.ts b/packages/parser/src/language/sankey/sankeyParser.ts new file mode 100644 index 00000000000..a70a7cb519d --- /dev/null +++ b/packages/parser/src/language/sankey/sankeyParser.ts @@ -0,0 +1,30 @@ +import type { AstNode, LangiumParser, ParseResult } from 'langium'; +import { createLangiumParser } from 'langium'; + +import type { SankeyServices } from './sankeyModule.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 = (input: string): ParseResult => { + const parseResult: ParseResult = parse(input); + + if (isSankey(parseResult.value)) { + const nodes: Set = new Set(); + 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; +} diff --git a/packages/parser/src/language/sankey/sankeyTokenBuilder.ts b/packages/parser/src/language/sankey/sankeyTokenBuilder.ts new file mode 100644 index 00000000000..18d11410dcd --- /dev/null +++ b/packages/parser/src/language/sankey/sankeyTokenBuilder.ts @@ -0,0 +1,36 @@ +import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium'; +import { DefaultTokenBuilder } from 'langium'; + +import type { TokenType } from '../chevrotainWrapper.js'; +import { sankeyLinkSourceRegex } from './sankeyMatcher.js'; +import { regexPatternFunc } from '../utils.js'; + +export class SankeyTokenBuilder extends DefaultTokenBuilder { + protected override buildTerminalTokens(rules: Stream): TokenType[] { + const tokenTypes: TokenType[] = super.buildTerminalTokens(rules); + tokenTypes.forEach((tokenType: TokenType): void => { + switch (tokenType.name) { + case 'SANKEY_LINK_SOURCE': { + tokenType.LINE_BREAKS = false; + tokenType.PATTERN = regexPatternFunc(sankeyLinkSourceRegex); + break; + } + } + }); + return tokenTypes; + } + + protected override buildKeywordTokens( + rules: Stream, + terminalTokens: TokenType[], + options?: TokenBuilderOptions + ): TokenType[] { + const tokenTypes: TokenType[] = super.buildKeywordTokens(rules, terminalTokens, options); + tokenTypes.forEach((tokenType: TokenType): void => { + if (tokenType.name === 'sankey-beta' && tokenType.PATTERN !== undefined) { + tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?!\\S)'); + } + }); + return tokenTypes; + } +} diff --git a/packages/parser/src/language/sankey/sankeyValueConverter.ts b/packages/parser/src/language/sankey/sankeyValueConverter.ts new file mode 100644 index 00000000000..a8038393f7d --- /dev/null +++ b/packages/parser/src/language/sankey/sankeyValueConverter.ts @@ -0,0 +1,85 @@ +import type { CstNode, GrammarAST, ValueType } from 'langium'; +import { DefaultValueConverter } from 'langium'; + +import { CommonValueConverter } from '../common/commonValueConverters.js'; +import { + sankeyLinkSourceRegex, + sankeyLinkTargetRegex, + sankeyLinkValueRegex, +} from './sankeyMatcher.js'; + +export class SankeyValueConverter extends DefaultValueConverter { + protected override runConverter( + rule: GrammarAST.AbstractRule, + input: string, + cstNode: CstNode + ): ValueType { + let value: ValueType | undefined = CommonValueConverter.customRunConverter( + rule, + input, + cstNode + ); + if (value === undefined) { + value = SankeyValueConverter.customRunConverter(rule, input, cstNode); + } + + if (value === undefined) { + return super.runConverter(rule, input, cstNode); + } else { + return value; + } + } + + /** + * A method contains convert logic to be used by class itself or `MermaidValueConverter`. + * + * @param rule - Parsed rule. + * @param input - Matched string. + * @param _cstNode - Node in the Concrete Syntax Tree (CST). + * @returns converted the value if it's sankey rule or `null` if it's not. + */ + public static customRunConverter( + rule: GrammarAST.AbstractRule, + input: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _cstNode: CstNode + ): ValueType | undefined { + let regex: RegExp | undefined; + switch (rule.name) { + case 'SANKEY_LINK_SOURCE': { + regex = sankeyLinkSourceRegex; + break; + } + case 'SANKEY_LINK_TARGET': { + regex = sankeyLinkTargetRegex; + break; + } + case 'SANKEY_LINK_VALUE': { + regex = sankeyLinkValueRegex; + break; + } + } + 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; + } +} diff --git a/packages/parser/src/language/utils.ts b/packages/parser/src/language/utils.ts new file mode 100644 index 00000000000..30aefd065ea --- /dev/null +++ b/packages/parser/src/language/utils.ts @@ -0,0 +1,10 @@ +import type { CustomPatternMatcherFunc } from './chevrotainWrapper.js'; + +export function regexPatternFunc(regex: RegExp): CustomPatternMatcherFunc { + const stickyRegex = new RegExp(regex, regex.flags + 'y'); + return (text, offset) => { + stickyRegex.lastIndex = offset; + const execResult = stickyRegex.exec(text); + return execResult; + }; +} diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 90358bbf166..1711ad7307f 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,8 +1,8 @@ import type { LangiumParser, ParseResult } from 'langium'; -import type { Info } from './index.js'; -import { createInfoServices } from './language/index.js'; +import type { Info, Sankey } from './index.js'; +import { createInfoServices, createSankeyServices } from './language/index.js'; -export type DiagramAST = Info; +export type DiagramAST = Info | Sankey; const parsers: Record = {}; @@ -13,8 +13,14 @@ const initializers = { const parser = createInfoServices().Info.parser.LangiumParser; parsers['info'] = parser; }, + sankey: () => { + const parser = createSankeyServices().Sankey.parser.LangiumParser; + parsers['sankey'] = parser; + }, } as const; + export function parse(diagramType: 'info', text: string): Info; +export function parse(diagramType: 'sankey', text: string): Sankey; export function parse( diagramType: keyof typeof initializers, text: string diff --git a/packages/parser/tests/sankey.test.ts b/packages/parser/tests/sankey.test.ts new file mode 100644 index 00000000000..a97c67f56e4 --- /dev/null +++ b/packages/parser/tests/sankey.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it } from 'vitest'; +import type { LangiumParser, ParseResult } from 'langium'; + +import type { SankeyServices } from '../src/language/index.js'; +import { Sankey, createSankeyServices } from '../src/language/index.js'; +import { noErrorsOrAlternatives } from './test-util.js'; + +const services: SankeyServices = createSankeyServices().Sankey; +const parser: LangiumParser = services.parser.LangiumParser; +export function createSankeyTestServices(): { + services: SankeyServices; + parse: (input: string) => ParseResult; +} { + const parse = (input: string) => { + return parser.parse(input); + }; + return { services, parse }; +} + +describe('sankey', () => { + const { parse } = createSankeyTestServices(); + + it('should handle simple sankey', () => { + const context = `sankey-beta + sourceNode, targetNode, 10`; + const result = parse(context); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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 +"" with comma","target node +"" with comma","10.00" + `; + const result = parse(context); + noErrorsOrAlternatives(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 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); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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); + noErrorsOrAlternatives(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); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91fdf3ddbe0..a29c52932e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -481,6 +481,61 @@ importers: specifier: ^7.0.0 version: 7.0.0 + packages/mermaid/src/vitepress: + dependencies: + '@vueuse/core': + specifier: ^10.1.0 + version: 10.1.0(vue@3.3.0) + jiti: + specifier: ^1.18.2 + version: 1.18.2 + mermaid: + specifier: workspace:^ + version: link:../.. + vue: + specifier: ^3.3 + version: 3.3.0 + devDependencies: + '@iconify-json/carbon': + specifier: ^1.1.16 + version: 1.1.16 + '@unocss/reset': + specifier: ^0.55.2 + version: 0.55.2 + '@vite-pwa/vitepress': + specifier: ^0.2.0 + version: 0.2.0(vite-plugin-pwa@0.16.0) + '@vitejs/plugin-vue': + specifier: ^4.2.1 + version: 4.2.1(vite@4.3.9)(vue@3.3.0) + fast-glob: + specifier: ^3.2.12 + version: 3.2.12 + https-localhost: + specifier: ^4.7.1 + version: 4.7.1 + pathe: + specifier: ^1.1.0 + version: 1.1.0 + unocss: + specifier: ^0.55.2 + version: 0.55.2(postcss@8.4.28)(rollup@2.79.1)(vite@4.3.9) + unplugin-vue-components: + specifier: ^0.25.0 + version: 0.25.0(rollup@2.79.1)(vue@3.3.0) + vite: + specifier: ^4.3.9 + version: 4.3.9(@types/node@18.16.0) + vite-plugin-pwa: + specifier: ^0.16.0 + version: 0.16.0(vite@4.3.9)(workbox-build@7.0.0)(workbox-window@7.0.0) + vitepress: + specifier: 1.0.0-rc.4 + version: 1.0.0-rc.4(@algolia/client-search@4.19.1)(@types/node@18.16.0)(search-insights@2.7.0) + workbox-window: + specifier: ^7.0.0 + version: 7.0.0 + packages/parser: dependencies: langium: @@ -4018,20 +4073,6 @@ packages: rollup: 2.79.1 dev: true - /@rollup/pluginutils@5.0.2: - resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@types/estree': 1.0.0 - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true - /@rollup/pluginutils@5.0.3(rollup@2.79.1): resolution: {integrity: sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g==} engines: {node: '>=14.0.0'} @@ -15481,7 +15522,7 @@ packages: '@rollup/pluginutils': 5.0.3(rollup@2.79.1) chokidar: 3.5.3 debug: 4.3.4(supports-color@8.1.1) - fast-glob: 3.2.12 + fast-glob: 3.3.1 local-pkg: 0.4.3 magic-string: 0.30.2 minimatch: 9.0.3 @@ -15623,10 +15664,11 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.3.9(@types/node@18.16.0) + vite: 4.4.9(@types/node@18.16.0) transitivePeerDependencies: - '@types/node' - less + - lightningcss - sass - stylus - sugarss @@ -15657,7 +15699,7 @@ packages: workbox-window: ^7.0.0 dependencies: debug: 4.3.4(supports-color@8.1.1) - fast-glob: 3.2.12 + fast-glob: 3.3.1 pretty-bytes: 6.1.1 vite: 4.3.9(@types/node@18.16.0) workbox-build: 7.0.0 @@ -15850,11 +15892,12 @@ packages: strip-literal: 1.3.0 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.3.9(@types/node@18.16.0) + vite: 4.4.9(@types/node@18.16.0) vite-node: 0.34.0(@types/node@18.16.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less + - lightningcss - sass - stylus - sugarss