Compare commits

..

21 Commits

Author SHA1 Message Date
Sidharth Vinod
6462f73bdb chore: resolve eslint warnings in packages/parser/src/language/common/valueConverter.ts 2025-04-19 10:21:45 +05:30
Sidharth Vinod
cfe1723c6c chore: resolve eslint warnings in packages/mermaid/src/diagrams/packet/parser.ts 2025-04-19 10:21:45 +05:30
Sidharth Vinod
6d8b936319 chore: resolve eslint warnings in packages/mermaid/src/diagrams/class/classRenderer-v2.ts 2025-04-19 10:21:45 +05:30
Sidharth Vinod
f992d95082 chore: resolve eslint warnings in packages/mermaid/src/diagrams/class/classDiagram.ts 2025-04-19 10:21:44 +05:30
Sidharth Vinod
f2276f93cd chore: resolve eslint warnings in packages/mermaid/src/diagrams/class/classDiagram-v2.ts 2025-04-19 10:21:44 +05:30
Sidharth Vinod
b52f5058c9 chore: resolve eslint warnings in packages/mermaid/src/diagrams/class/classDb.ts 2025-04-19 10:21:44 +05:30
Sidharth Vinod
5e20087252 chore: resolve eslint warnings in packages/mermaid/src/diagrams/block/renderHelpers.ts 2025-04-19 10:21:44 +05:30
Sidharth Vinod
b49036068f chore: resolve eslint warnings in packages/mermaid/src/diagrams/block/blockDB.ts 2025-04-19 10:21:43 +05:30
Sidharth Vinod
5f3d4cb913 chore: resolve eslint warnings in packages/mermaid/src/diagrams/architecture/architectureRenderer.ts 2025-04-19 10:21:43 +05:30
Sidharth Vinod
b11eb93ab2 chore: resolve eslint warnings in packages/mermaid/src/dagre-wrapper/nodes.js 2025-04-19 10:21:43 +05:30
Sidharth Vinod
e4f19480cd chore: resolve eslint warnings in packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js 2025-04-19 10:21:43 +05:30
Sidharth Vinod
a331a958c0 chore: resolve eslint warnings in packages/mermaid/src/dagre-wrapper/edges.js 2025-04-19 10:21:42 +05:30
Sidharth Vinod
134fca3f1d chore: resolve eslint warnings in packages/mermaid/src/dagre-wrapper/createLabel.js 2025-04-19 10:21:42 +05:30
Sidharth Vinod
ef7c0a1936 chore: resolve eslint warnings in packages/mermaid/src/dagre-wrapper/clusters.js 2025-04-19 10:21:42 +05:30
Sidharth Vinod
2c5403c0f9 chore: resolve eslint warnings in packages/mermaid/src/config.ts 2025-04-19 10:21:41 +05:30
Sidharth Vinod
3b0cb1271e chore: resolve eslint warnings in packages/mermaid-layout-elk/src/render.ts 2025-04-19 10:21:41 +05:30
Sidharth Vinod
96c06a681d chore: resolve eslint warnings in cypress/integration/rendering/journey.spec.js 2025-04-19 10:21:41 +05:30
Sidharth Vinod
ff9d26bc70 chore: resolve eslint warnings in cypress/integration/rendering/imageShape.spec.ts 2025-04-19 10:21:41 +05:30
Sidharth Vinod
2ddc3403de chore: resolve eslint warnings in cypress/integration/rendering/iconShape.spec.ts 2025-04-19 10:21:40 +05:30
Sidharth Vinod
4535911a3a chore: resolve eslint warnings in cypress/helpers/util.ts 2025-04-19 10:21:40 +05:30
Sidharth Vinod
9d838d4e7a fix: e2e-applitools.yml 2025-04-18 16:48:58 +05:30
46 changed files with 50 additions and 1134 deletions

View File

@@ -87,7 +87,6 @@ NODIR
NSTR
outdir
Qcontrolx
QSTR
reinit
rels
reqs

View File

@@ -45,13 +45,15 @@ jobs:
- if: ${{ env.USE_APPLI }}
name: Notify applitools of new batch
# Copied from docs https://applitools.com/docs/topics/integrations/github-integration-ci-setup.html
run: curl -L -d '' -X POST "$APPLITOOLS_SERVER_URL/api/externals/github/push?apiKey=$APPLITOOLS_API_KEY&CommitSha=$GITHUB_SHA&BranchName=${APPLITOOLS_BRANCH}$&ParentBranchName=$APPLITOOLS_PARENT_BRANCH"
env:
# e.g. mermaid-js/mermaid/my-branch
APPLITOOLS_BRANCH: ${{ github.repository }}/${{ github.ref_name }}
APPLITOOLS_PARENT_BRANCH: ${{ github.event.inputs.parent_branch }}
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
APPLITOOLS_SERVER_URL: 'https://eyesapi.applitools.com'
uses: wei/curl@61d92b5169ea0425820dd13cf6fbad66b483e9f1
with:
args: -X POST "$APPLITOOLS_SERVER_URL/api/externals/github/push?apiKey=$APPLITOOLS_API_KEY&CommitSha=$GITHUB_SHA&BranchName=${APPLITOOLS_BRANCH}$&ParentBranchName=$APPLITOOLS_PARENT_BRANCH"
- name: Cypress run
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12

1
.gitignore vendored
View File

@@ -52,4 +52,3 @@ vite.config.ts.timestamp-*
# autogenereated by langium-cli
generated/
.cursor/*

View File

@@ -22,7 +22,7 @@ const batchId: string =
'mermaid-batch-' +
(Cypress.env('useAppli')
? Date.now().toString()
: Cypress.env('CYPRESS_COMMIT') || Date.now().toString());
: (Cypress.env('CYPRESS_COMMIT') ?? Date.now().toString()));
export const mermaidUrl = (
graphStr: string | string[],
@@ -61,9 +61,7 @@ export const imgSnapshotTest = (
sequence: {
...(_options.sequence ?? {}),
actorFontFamily: 'courier',
noteFontFamily: _options.sequence?.noteFontFamily
? _options.sequence.noteFontFamily
: 'courier',
noteFontFamily: _options.sequence?.noteFontFamily ?? 'courier',
messageFontFamily: 'courier',
},
};

View File

@@ -14,7 +14,7 @@ looks.forEach((look) => {
directions.forEach((direction) => {
forms.forEach((form) => {
labelPos.forEach((pos) => {
describe(`Test iconShape in ${form ? `${form} form,` : ''} ${look} look and dir ${direction} with label position ${pos ? pos : 'not defined'}`, () => {
describe(`Test iconShape in ${form ? `${form} form,` : ''} ${look} look and dir ${direction} with label position ${pos ?? 'not defined'}`, () => {
it(`without label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell'`;

View File

@@ -12,7 +12,7 @@ const labelPos = [undefined, 't', 'b'] as const;
looks.forEach((look) => {
directions.forEach((direction) => {
labelPos.forEach((pos) => {
describe(`Test imageShape in ${look} look and dir ${direction} with label position ${pos ? pos : 'not defined'}`, () => {
describe(`Test imageShape in ${look} look and dir ${direction} with label position ${pos ?? 'not defined'}`, () => {
it(`without label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', w: '100', h: '100' }\n`;

View File

@@ -214,7 +214,7 @@ section Checkout from website
$lines.each((index, el) => {
const bbox = el.getBBox();
expect(bbox.width).to.be.lte(320);
maxLineWidth = Math.max(maxLineWidth || 0, bbox.width);
maxLineWidth = Math.max(maxLineWidth ?? 0, bbox.width);
});
/** The expected margin between the diagram and the legend is 150px, as defined by

View File

@@ -190,9 +190,7 @@ export const render = async (
const children = nodeArr.filter((node: { parentId: any }) => node.parentId === subgraph.id);
children.forEach((node: any) => {
parentLookupDb.parentById[node.id] = subgraph.id;
if (parentLookupDb.childrenById[subgraph.id] === undefined) {
parentLookupDb.childrenById[subgraph.id] = [];
}
parentLookupDb.childrenById[subgraph.id] ??= [];
parentLookupDb.childrenById[subgraph.id].push(node);
});
});
@@ -381,10 +379,10 @@ export const render = async (
}
edgeData.labelType = edge.labelType;
edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n');
edgeData.label = (edge?.text ?? '').replace(common.lineBreakRegex, '\n');
if (edge.style === undefined) {
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
edgeData.style = edgeData.style ?? 'stroke: #333; stroke-width: 1.5px;fill:none;';
}
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
@@ -725,9 +723,7 @@ export const render = async (
inter = inter2;
}
}
if (!inter) {
inter = intersection(bounds, lastPointOutside, point);
}
inter ??= intersection(bounds, lastPointOutside, point);
// Check case where the intersection is the same as the last point
let pointPresent = false;
@@ -840,8 +836,8 @@ export const render = async (
node.labels = [
{
text: node.label,
width: node?.labelData?.width || 50,
height: node?.labelData?.height || 50,
width: node?.labelData?.width ?? 50,
height: node?.labelData?.height ?? 50,
},
(node.width = node.width + 2 * node.padding),
log.debug('UIO node label', node?.labelData?.width, node.padding),
@@ -917,7 +913,7 @@ export const render = async (
if (edge.sections) {
const src = edge.sections[0].startPoint;
const dest = edge.sections[0].endPoint;
const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : [];
const segments = edge.sections[0].bendPoints ?? [];
const segPoints = segments.map((segment: { x: any; y: any }) => {
return { x: segment.x + offset.x, y: segment.y + offset.y };

View File

@@ -30,7 +30,7 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: Mermaid
if (sumOfDirectives.theme && sumOfDirectives.theme in theme) {
const tmpConfigFromInitialize = assignWithDepth({}, configFromInitialize);
const themeVariables = assignWithDepth(
tmpConfigFromInitialize.themeVariables || {},
tmpConfigFromInitialize.themeVariables ?? {},
sumOfDirectives.themeVariables
);
if (cfg.theme && cfg.theme in theme) {

View File

@@ -238,7 +238,7 @@ let clusterElems = {};
export const insertCluster = (elem, node) => {
log.trace('Inserting cluster');
const shape = node.shape || 'rect';
const shape = node.shape ?? 'rect';
clusterElems[node.id] = shapes[shape](elem, node);
};
export const getClusterTitleWidth = (elem, node) => {

View File

@@ -45,7 +45,7 @@ function addHtmlLabel(node) {
* @deprecated svg-util/createText instead
*/
const createLabel = (_vertexText, style, isTitle, isNode) => {
let vertexText = _vertexText || '';
let vertexText = _vertexText ?? '';
if (typeof vertexText === 'object') {
vertexText = vertexText[0];
}

View File

@@ -69,9 +69,7 @@ export const insertEdgeLabel = (elem, edge) => {
fo = inner.node().appendChild(startLabelElement);
const slBox = startLabelElement.getBBox();
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
if (!terminalLabels[edge.id]) {
terminalLabels[edge.id] = {};
}
terminalLabels[edge.id] ??= {};
terminalLabels[edge.id].startLeft = startEdgeLabelLeft;
setTerminalWidth(fo, edge.startLabelLeft);
}
@@ -85,9 +83,7 @@ export const insertEdgeLabel = (elem, edge) => {
const slBox = startLabelElement.getBBox();
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
if (!terminalLabels[edge.id]) {
terminalLabels[edge.id] = {};
}
terminalLabels[edge.id] ??= {};
terminalLabels[edge.id].startRight = startEdgeLabelRight;
setTerminalWidth(fo, edge.startLabelRight);
}
@@ -102,9 +98,7 @@ export const insertEdgeLabel = (elem, edge) => {
endEdgeLabelLeft.node().appendChild(endLabelElement);
if (!terminalLabels[edge.id]) {
terminalLabels[edge.id] = {};
}
terminalLabels[edge.id] ??= {};
terminalLabels[edge.id].endLeft = endEdgeLabelLeft;
setTerminalWidth(fo, edge.endLabelLeft);
}
@@ -119,9 +113,7 @@ export const insertEdgeLabel = (elem, edge) => {
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
endEdgeLabelRight.node().appendChild(endLabelElement);
if (!terminalLabels[edge.id]) {
terminalLabels[edge.id] = {};
}
terminalLabels[edge.id] ??= {};
terminalLabels[edge.id].endRight = endEdgeLabelRight;
setTerminalWidth(fo, edge.endLabelRight);
}
@@ -141,7 +133,7 @@ function setTerminalWidth(fo, value) {
export const positionEdgeLabel = (edge, paths) => {
log.debug('Moving label abc88 ', edge.id, edge.label, edgeLabels[edge.id], paths);
let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
let path = paths.updatedPath ?? paths.originalPath;
const siteConfig = getConfig();
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
if (edge.label) {

View File

@@ -4,7 +4,9 @@ import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
export let clusterDb = {};
/** @type {Record<string, string[]>} */
let descendants = {};
/** @type {Record<string, string>} */
let parents = {};
export const clear = () => {

View File

@@ -15,9 +15,7 @@ const formatClass = (str) => {
return '';
};
const getClassesFromNode = (node, otherClasses) => {
return `${otherClasses ? otherClasses : 'node default'}${formatClass(node.classes)} ${formatClass(
node.class
)}`;
return `${otherClasses ?? 'node default'}${formatClass(node.classes)} ${formatClass(node.class)}`;
};
const question = async (parent, node) => {
@@ -57,7 +55,7 @@ const choice = (parent, node) => {
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
.attr('id', node.domId ?? node.id);
const s = 28;
const points = [
@@ -566,7 +564,7 @@ const rectWithTitle = (parent, node) => {
const shapeSvg = parent
.insert('g')
.attr('class', classes)
.attr('id', node.domId || node.id);
.attr('id', node.domId ?? node.id);
// Create the title label and insert it after the rect
const rect = shapeSvg.insert('rect', ':first-child');
@@ -808,7 +806,7 @@ const start = (parent, node) => {
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
.attr('id', node.domId ?? node.id);
const circle = shapeSvg.insert('circle', ':first-child');
// center the circle around its coordinate
@@ -827,7 +825,7 @@ const forkJoin = (parent, node, dir) => {
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
.attr('id', node.domId ?? node.id);
let width = 70;
let height = 10;
@@ -859,7 +857,7 @@ const end = (parent, node) => {
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
.attr('id', node.domId ?? node.id);
const innerCircle = shapeSvg.insert('circle', ':first-child');
const circle = shapeSvg.insert('circle', ':first-child');
@@ -891,7 +889,7 @@ const class_box = (parent, node) => {
const shapeSvg = parent
.insert('g')
.attr('class', classes)
.attr('id', node.domId || node.id);
.attr('id', node.domId ?? node.id);
// Create the title label and insert it after the rect
const rect = shapeSvg.insert('rect', ':first-child');

View File

@@ -88,7 +88,7 @@ function positionNodes(db: ArchitectureDB, cy: cytoscape.Core) {
data.y = node.position().y;
const nodeElem = db.getElementById(data.id);
nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')');
nodeElem.attr('transform', 'translate(' + (data.x ?? 0) + ',' + (data.y ?? 0) + ')');
});
}

View File

@@ -82,9 +82,7 @@ export const setCssClass = function (itemIds: string, cssClassName: string) {
foundBlock = { id: trimmedId, type: 'na', children: [] } as Block;
blockDatabase.set(trimmedId, foundBlock);
}
if (!foundBlock.classes) {
foundBlock.classes = [];
}
foundBlock.classes ??= [];
foundBlock.classes.push(cssClassName);
});
};

View File

@@ -10,7 +10,7 @@ function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
const vertex = block;
let classStr = 'default';
if ((vertex?.classes?.length || 0) > 0) {
if ((vertex?.classes?.length ?? 0) > 0) {
classStr = (vertex?.classes ?? []).join(' ');
}
classStr = classStr + ' flowchart-label';

View File

@@ -41,7 +41,7 @@ export class ClassDB implements DiagramDB {
private namespaces = new Map<string, NamespaceNode>();
private namespaceCounter = 0;
private functions: any[] = [];
private functions: ((element: Element) => void)[] = [];
constructor() {
this.functions.push(this.setupToolTips.bind(this));
@@ -477,7 +477,7 @@ export class ClassDB implements DiagramDB {
let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
select('.mermaidTooltip');
// @ts-expect-error - Incorrect types
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
if ((tooltipElem._groups ?? tooltipElem)[0][0] === null) {
tooltipElem = select('body')
.append('div')
.attr('class', 'mermaidTooltip')

View File

@@ -13,9 +13,7 @@ export const diagram: DiagramDefinition = {
renderer,
styles,
init: (cnf) => {
if (!cnf.class) {
cnf.class = {};
}
cnf.class ??= {};
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
},
};

View File

@@ -13,9 +13,7 @@ export const diagram: DiagramDefinition = {
renderer,
styles,
init: (cnf) => {
if (!cnf.class) {
cnf.class = {};
}
cnf.class ??= {};
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
},
};

View File

@@ -117,7 +117,7 @@ export const addClasses = function (
style: styles.style,
id: vertex.id,
domId: vertex.domId,
tooltip: diagObj.db.getTooltip(vertex.id, parent) || '',
tooltip: diagObj.db.getTooltip(vertex.id, parent) ?? '',
haveCallback: vertex.haveCallback,
link: vertex.link,
width: vertex.type === 'group' ? 500 : undefined,

View File

@@ -1,19 +0,0 @@
orgChart
%% ex2
CEO[Mark Davies CEO]
---
VPFinance[Leslie Deen VP Finance]
VPHR[David Soft VP HR]
---
VPMA[Achmed Jo VP marketing]
VPLegal[Elena Prem VP Legal]
PMA[Sudan Ali]
Noel
Tom
Alex
Sneil
PMB[Sekar Sha]
John
Dan
David
Jan

View File

@@ -1,7 +0,0 @@
orgChart
%% ex2
CEO[Mark Davies CEO] --o VPFinance[Leslie Deen VP Finance] & VPHR[David Soft VP HR]
CEO --o VPMA[Achmed Jo VP marketing] & VPLegal[Elena Prem VP Legal]
CEO --> PMA[Sudan Ali] & PMB[Sekar Sha]
PMA --> Noel & Tom & Alex & Sneil
PMB --> John & Dan & David & Jan

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -1,22 +0,0 @@
org
CEO[CEO]
CFO[CFO]
Finance1[Finance 1]
Finance2[Finance 2]
CTO[CTO]
Dev1[Developer 1]
Dev2[Developer 2]
----
org
CEO[CEO]
connector
CTO[CTO]
CFO[CFO]
Finance1[Finance 1]
Finance2[Finance 2]
CTO[CTO]
---
org
President --> VP1[VP Sales] & VP2[VP Production] & VP3[VP Marketing]

View File

@@ -50,9 +50,7 @@ const getNextFittingBlock = (
row: number,
bitsPerRow: number
): [Required<PacketBlock>, PacketBlock | undefined] => {
if (block.end === undefined) {
block.end = block.start;
}
block.end ??= block.start;
if (block.start > block.end) {
throw new Error(`Block start ${block.start} is greater than block end ${block.end}.`);

View File

@@ -4,58 +4,32 @@
{
"id": "info",
"grammar": "src/language/info/info.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "packet",
"grammar": "src/language/packet/packet.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "pie",
"grammar": "src/language/pie/pie.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "architecture",
"grammar": "src/language/architecture/architecture.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "gitGraph",
"grammar": "src/language/gitGraph/gitGraph.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "radar",
"grammar": "src/language/radar/radar.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "mindmap",
"grammar": "src/language/mindmap/mindmap.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
"fileExtensions": [".mmd", ".mermaid"]
}
],
"mode": "production",

View File

@@ -31,9 +31,8 @@ export abstract class AbstractMermaidValueConverter extends DefaultValueConverte
): ValueType {
let value: ValueType | undefined = this.runCommonConverter(rule, input, cstNode);
if (value === undefined) {
value = this.runCustomConverter(rule, input, cstNode);
}
value ??= this.runCustomConverter(rule, input, cstNode);
if (value === undefined) {
return super.runConverter(rule, input, cstNode);
}

View File

@@ -11,7 +11,6 @@ export {
Branch,
Commit,
Merge,
MindmapDoc as Mindmap,
Statement,
isInfo,
isPacket,
@@ -33,7 +32,6 @@ export {
ArchitectureGeneratedModule,
GitGraphGeneratedModule,
RadarGeneratedModule,
MindmapGeneratedModule,
} from './generated/module.js';
export * from './gitGraph/index.js';
@@ -43,4 +41,3 @@ export * from './packet/index.js';
export * from './pie/index.js';
export * from './architecture/index.js';
export * from './radar/index.js';
export * from './mindmap/index.js';

View File

@@ -1,44 +0,0 @@
grammar KanbanDiagram
entry KanbanModel:
'kanban' (NL | SPACELINE)* document=Document;
Document:
statements+=Statement*;
Statement:
(indent=SPACELIST)? node=Node shapeData=ShapeData? |
(indent=SPACELIST)? icon=ICON |
(indent=SPACELIST)? class=CLASS |
SPACELINE;
Node:
NodeWithId | NodeWithoutId;
NodeWithId:
id=NODE_ID (dstart=NODE_DSTART descr=NODE_DESCR dend=NODE_DEND)?;
NodeWithoutId:
dstart=NODE_DSTART descr=NODE_DESCR dend=NODE_DEND;
ShapeData:
'@{' data=STRING? '}';
// Terminal definitions
terminal KANBAN: 'kanban';
terminal CLASS: ':::' -> !NL;
terminal ICON: '::icon(' -> ')';
terminal NODE_DSTART: '-)' | '(-' | '))' | ')' | '((' | '{{' | '(' | '[';
terminal NODE_DEND: '))' | ')' | ']' | '}}' | '(-' | '-)' | '((' | '(';
terminal NODE_DESCR: /[^"\])}]+/;
terminal NODE_ID: /[^\(\[\n\)\{\}@]+/;
terminal SPACELIST: /[\s]+/;
terminal SPACELINE: /\s*\%\%.*/ | /[\s]+[\n]/;
terminal NL: /[\n]+/;
terminal STRING: '"' -> '"';
terminal COMMENT: /\s*\%\%.*/ -> NL;
// Hide these terminals from the language server
hidden terminal WS: /\s+/;
hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//;
hidden terminal SL_COMMENT: /\/\/[^\n\r]*/;

View File

@@ -1 +0,0 @@
export * from './module.js';

View File

@@ -1,77 +0,0 @@
import type { ValidationAcceptor, ValidationChecks } from 'langium';
import type { MermaidAstType, MindmapDoc, MindmapRow } from '../generated/ast.js';
import type { MindmapServices } from './module.js';
/**
* Register custom validation checks.
*/
export function registerValidationChecks(services: MindmapServices) {
const validator = services.validation.MindmapValidator;
const registry = services.validation.ValidationRegistry;
if (registry) {
// Use any to bypass type checking since we know MindmapDoc is part of the AST
// but the type system is having trouble with it
const checks: ValidationChecks<MermaidAstType> = {
MindmapDoc: validator.checkSingleRoot.bind(validator),
MindmapRow: (node: MindmapRow, accept: ValidationAcceptor) => {
validator.checkSingleRootRow(node, accept);
},
};
registry.register(checks, validator);
}
}
/**
* Implementation of custom validations.
*/
export class MindmapValidator {
constructor() {
// eslint-disable-next-line no-console
console.debug('MindmapValidator constructor');
}
checkSingleRootRow(_node: MindmapRow, _accept: ValidationAcceptor): void {
// eslint-disable-next-line no-console
console.debug('CHECKING SINGLE ROOT Row');
}
/**
* Validates that a mindmap has only one root node.
* A root node is defined as a node that has no indentation.
*/
checkSingleRoot(doc: MindmapDoc, accept: ValidationAcceptor): void {
// eslint-disable-next-line no-console
console.debug('CHECKING SINGLE ROOT');
let rootNodeIndentation;
for (const row of doc.MindmapRows) {
// Skip non-node items (e.g., class decorations, icon decorations)
if (
!row.item ||
row.item.$type === 'ClassDecoration' ||
row.item.$type === 'IconDecoration'
) {
continue;
}
if (
rootNodeIndentation === undefined && // Check if this is a root node (no indentation)
row.indent === undefined
) {
rootNodeIndentation = 0;
} else if (row.indent === undefined) {
// If we've already found a root node, report an error
accept('error', 'Multiple root nodes are not allowed in a mindmap.', {
node: row,
property: 'item',
});
} else if (
rootNodeIndentation !== undefined &&
rootNodeIndentation >= parseInt(row.indent, 10)
) {
accept('error', 'Multiple root nodes are not allowed in a mindmap.', {
node: row,
property: 'item',
});
}
}
}
}

View File

@@ -1,85 +0,0 @@
/**
* Mindmap grammar for Langium
* Converted from mermaid's jison grammar
*
* The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines
* before the mindmap keyword, allowing for empty lines and comments before the
* mindmap declaration.
*/
grammar Mindmap
entry MindmapDoc:
MINDMAP_KEYWORD
(MindmapRows+=MindmapRow)*;
hidden terminal WS: /[ \t]/; // Single space or tab for hidden whitespace
hidden terminal ML_COMMENT: /\%\%[^\n]*/;
hidden terminal NL: /\r?\n/;
MindmapRow:
(indent=INDENTATION)|(indent=INDENTATION)? (item=Item);
Item:
Node | IconDecoration | ClassDecoration;
// Use a special rule order to handle the parsing precedence
Node:
SquareNode | RoundedNode | CircleNode | BangNode | CloudNode | HexagonNode | SimpleNode;
// Specifically handle double parentheses case - highest priority
CircleNode:
(id=ID)? desc=(CIRCLE_STR|CIRCLE_QSTR);
BangNode:
(id=ID)? desc=(BANG_STR|BANG_QSTR);
RoundedNode:
(id=ID)? desc=(ROUNDED_STR|ROUNDED_QSTR);
SquareNode:
(id=ID)? desc=(SQUARE_STR|SQUARE_QSTR);
CloudNode:
(id=ID)? desc=(CLOUD_STR|CLOUD_QSTR);
HexagonNode:
(id=ID)? desc=(HEXAGON_STR|HEXAGON_QSTR);
// Simple node as fallback
SimpleNode:
id=ID;
IconDecoration:
content=(ICON);
ClassDecoration:
content=(CLASS);
// This should be processed before whitespace is ignored
terminal INDENTATION: /[ \t]{1,}/; // Two or more spaces/tabs for indentation
// Keywords with fixed text patterns
terminal MINDMAP_KEYWORD: 'mindmap';
// Basic token types
terminal CIRCLE_QSTR: "((\"" -> "\"))";
terminal CIRCLE_STR: "((" -> "))";
terminal BANG_QSTR: "))\"" -> "\"((";
terminal BANG_STR: "))" -> "((";
terminal CLOUD_QSTR: ")\"" -> "\"(";
terminal CLOUD_STR: ")" -> "(";
terminal HEXAGON_QSTR: "{{\"" -> "\"}}";
terminal HEXAGON_STR: "{{" -> "}}";
terminal ROUNDED_QSTR: "(\"" -> "\")";
terminal ROUNDED_STR: "(" -> ")";
terminal SQUARE_QSTR: /\[\"([\s\S]*?)\"\]/;
terminal SQUARE_STR: /\[([\s\S]*?)\]/;
terminal ICON: "::icon(" -> ")";
terminal CLASS: /:::([^\n:])*/;
terminal ID: /[a-zA-Z0-9_\-\.\/]+/;
terminal STRING: /"[^"]*"|'[^']*'/;
// Modified indentation rule to have higher priority than WS
// Type definition for node types
type NodeType = 'DEFAULT' | 'CIRCLE' | 'CLOUD' | 'BANG' | 'HEXAGON' | 'ROUND';

View File

@@ -1,88 +0,0 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { MermaidGeneratedSharedModule, MindmapGeneratedModule } from '../generated/module.js';
import { MindmapTokenBuilder } from './tokenBuilder.js';
import { MindmapValueConverter } from './valueConverter.js';
import { MindmapValidator, registerValidationChecks } from './mindmap-validator.js';
/**
* Declaration of `Mindmap` services.
*/
interface MindmapAddedServices {
parser: {
TokenBuilder: MindmapTokenBuilder;
ValueConverter: MindmapValueConverter;
};
validation: {
MindmapValidator: MindmapValidator;
};
}
/**
* Union of Langium default services and `Mindmap` services.
*/
export type MindmapServices = LangiumCoreServices & MindmapAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Mindmap` services.
*/
export const MindmapModule: Module<
MindmapServices,
PartialLangiumCoreServices & MindmapAddedServices
> = {
parser: {
TokenBuilder: () => new MindmapTokenBuilder(),
ValueConverter: () => new MindmapValueConverter(),
},
validation: {
MindmapValidator: () => new MindmapValidator(),
},
};
/**
* 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 createMindmapServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Mindmap: MindmapServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Mindmap: MindmapServices = inject(
createDefaultCoreModule({ shared }),
MindmapGeneratedModule,
MindmapModule
);
shared.ServiceRegistry.register(Mindmap);
// Register validation checks
registerValidationChecks(Mindmap);
return { shared, Mindmap };
}

View File

@@ -1,7 +0,0 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class MindmapTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['mindmap']);
}
}

View File

@@ -1,48 +0,0 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { AbstractMermaidValueConverter } from '../common/index.js';
export class MindmapValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
rule: GrammarAST.AbstractRule,
input: string,
_cstNode: CstNode
): ValueType | undefined {
if (rule.name === 'CIRCLE_STR') {
return input.replace('((', '').replace('))', '').trim();
} else if (rule.name === 'CIRCLE_QSTR') {
return input.replace('(("', '').replace('"))', '').trim();
} else if (rule.name === 'ROUNDED_STR') {
return input.replace('(', '').replace(')', '').trim();
} else if (rule.name === 'ROUNDED_QSTR') {
return input.replace('("', '').replace('")', '').trim();
} else if (rule.name === 'SQUARE_STR') {
return input.replace('[', '').replace(']', '').trim();
} else if (rule.name === 'SQUARE_QSTR') {
return input.replace('["', '').replace('"]', '').trim();
} else if (rule.name === 'BANG_STR') {
return input.replace('))', '').replace('((', '').trim();
} else if (rule.name === 'BANG_QSTR') {
return input.replace('))"', '').replace('"((', '').trim();
} else if (rule.name === 'HEXAGON_STR') {
return input.replace('{{', '').replace('}}', '').trim();
} else if (rule.name === 'HEXAGON_QSTR') {
return input.replace('{{"', '').replace('"}}', '').trim();
} else if (rule.name === 'CLOUD_STR') {
return input.replace(')', '').replace('(', '').trim();
} else if (rule.name === 'CLOUD_QSTR') {
return input.replace(')"', '').replace('"(', '').trim();
} else if (rule.name === 'ARCH_TEXT_ICON') {
return input.replace(/["()]/g, '');
} else if (rule.name === 'ARCH_TITLE') {
return input.replace(/[[\]]/g, '').trim();
} else if (rule.name === 'CLASS') {
return input.replace(':::', '').trim();
} else if (rule.name === 'ICON') {
return input.replace('::icon(', '').replace(')', '').trim();
} else if (rule.name === 'INDENTATION') {
return input.length;
}
return undefined;
}
}

View File

@@ -1 +0,0 @@
export * from './module.js';

View File

@@ -1,77 +0,0 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { CommonValueConverter } from '../common/valueConverter.js';
import { MermaidGeneratedSharedModule, PacketGeneratedModule } from '../generated/module.js';
import { PacketTokenBuilder } from './tokenBuilder.js';
/**
* Declaration of `Packet` services.
*/
interface PacketAddedServices {
parser: {
TokenBuilder: PacketTokenBuilder;
ValueConverter: CommonValueConverter;
};
}
/**
* Union of Langium default services and `Packet` services.
*/
export type PacketServices = LangiumCoreServices & PacketAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Packet` services.
*/
export const PacketModule: Module<
PacketServices,
PartialLangiumCoreServices & PacketAddedServices
> = {
parser: {
TokenBuilder: () => new PacketTokenBuilder(),
ValueConverter: () => new CommonValueConverter(),
},
};
/**
* 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 createPacketServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Packet: PacketServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Packet: PacketServices = inject(
createDefaultCoreModule({ shared }),
PacketGeneratedModule,
PacketModule
);
shared.ServiceRegistry.register(Packet);
return { shared, Packet };
}

View File

@@ -1,16 +0,0 @@
grammar Packet
import "../common/common";
entry Packet:
NEWLINE*
"packet-beta"
(
TitleAndAccessibilities
| blocks+=PacketBlock
| NEWLINE
)*
;
PacketBlock:
start=INT('-' end=INT)? ':' label=STRING EOL
;

View File

@@ -1,7 +0,0 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['packet-beta']);
}
}

View File

@@ -1,419 +0,0 @@
import { describe, expect, it } from 'vitest';
import { validatedMindmapParse as validatedParse, mindmapParse as parse } from './test-util.js';
import type { CircleNode, SimpleNode } from '../src/language/generated/ast.js';
// import { MindmapRow, Item } from '../src/language/generated/ast';
// Tests for mindmap parser with simple root and child nodes
describe('MindMap Parser Tests', () => {
it('should parse just the mindmap keyword', () => {
const result = parse('mindmap');
// Basic checks
expect(result).toBeDefined();
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
});
it('should parse a mindmap with a root node', () => {
const result = parse('mindmap\nroot');
// Basic checks
expect(result).toBeDefined();
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rows = result.value.MindmapRows;
// Check if we have a statement
expect(rows).toBeDefined();
expect(rows.length).toBe(1);
// Check the content of the root node
const rootNode = rows[0].item as SimpleNode;
expect(rootNode).toBeDefined();
expect(rootNode?.id).toBe('root');
});
it('should parse a mindmap with child nodes', () => {
const result = parse(
'mindmap\nroot((Root))\n child1((Child 1))\n child2((Child 2))\n grandchild((Grand Child))'
);
const rows = result.value.MindmapRows;
const r0 = rows[0];
expect(r0.indent).toBe(undefined);
const r1 = rows[1];
expect(r1.indent).toBe(2);
const r2 = rows[2];
expect(r2.indent).toBe(2);
const r3 = rows[3];
expect(r3.indent).toBe(4);
expect(r0.$type).toBe('MindmapRow');
const node0 = r0.item as CircleNode;
expect(node0.$type).toBe('CircleNode');
expect(node0.desc).toBe('Root');
expect(node0.id).toBe('root');
expect(r1.$type).toBe('MindmapRow');
// console.debug('R1:', r1);
const node1 = r1.item as CircleNode;
expect(node1.$type).toBe('CircleNode');
expect(node1.id).toBe('child1');
expect(node1.desc).toBe('Child 1');
// expect(Object.keys(r1)).toBe(2);
const child2 = rows[2].item as CircleNode;
// expect(result.value.rows[1].indent).toBe('indent');
// expect(Object.keys(node1)).toBe(true);
expect(child2.id).toBe('child2');
expect(child2.desc).toBe('Child 2');
const grandChild = rows[3].item as CircleNode;
// expect(result.value.rows[1].indent).toBe('indent');
// expect(Object.keys(node1)).toBe(true);
expect(grandChild.id).toBe('grandchild');
expect(grandChild.desc).toBe('Grand Child');
});
});
describe('Hierarchy (ported from mindmap.spec.ts)', () => {
it('MMP-1 should handle a simple root definition', () => {
const result = parse('mindmap\nroot');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
expect(rootNode.id).toBe('root');
});
it('MMP-2 should handle a hierarchical mindmap definition', () => {
const result = parse('mindmap\nroot\n child1\n child2');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// Langium AST may not have children as nested objects, so just check rows
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const child1Node = result.value.MindmapRows[1].item as SimpleNode;
const child2Node = result.value.MindmapRows[2].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(child1Node.id).toBe('child1');
expect(child2Node.id).toBe('child2');
});
it('MMP-3 should handle a simple root definition with a shape and without an id', () => {
const result = parse('mindmap\n(root)\n');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// The content should be 'root', shape info may not be present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.id).toBe(undefined);
expect(rootNode.desc).toBe('root');
});
it('MMP-3.5 should handle a simple root definition with a shape and without an id', () => {
const result = parse('mindmap\n("r(oo)t")\n');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// The content should be 'root', shape info may not be present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.id).toBe(undefined);
expect(rootNode.desc).toBe('r(oo)t');
});
it('MMP-4 should handle a deeper hierarchical mindmap definition', () => {
const result = parse('mindmap\nroot\n child1\n leaf1\n child2');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const child1Node = result.value.MindmapRows[1].item as SimpleNode;
const leaf1Node = result.value.MindmapRows[2].item as SimpleNode;
const child2Node = result.value.MindmapRows[3].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(child1Node.id).toBe('child1');
expect(leaf1Node.id).toBe('leaf1');
expect(child2Node.id).toBe('child2');
});
it('MMP-5 Multiple roots are illegal', async () => {
const str = 'mindmap\nroot\nfakeRoot';
const result = await validatedParse(str, { validation: true });
// Langium parser may not throw, but should have parserErrors
expect(result.diagnostics![0].message).toBe(
'Multiple root nodes are not allowed in a mindmap.'
);
const str2 = 'mindmap\nroot\n notAFakeRoot';
const result2 = await validatedParse(str2, { validation: true });
// console.debug('RESULT2:', result2.diagnostics);
expect(result2.diagnostics?.length).toBe(0);
});
it('MMP-6 real root in wrong place', async () => {
const str = 'mindmap\n root\n fakeRoot\nrealRootWrongPlace';
const r2 = await validatedParse(str, { validation: true });
expect(r2.diagnostics?.length).toBe(0);
});
});
describe('Nodes (ported from mindmap.spec.ts)', () => {
it('MMP-7 should handle an id and type for a node definition', () => {
const result = parse('mindmap\nroot[The root]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// Langium AST: check content, id, and maybe type if available
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
expect(rootNode.id).toBe('root');
});
it('MMP-8 should handle an id and type for a node definition', () => {
const result = parse('mindmap\nroot\n theId(child1)');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
expect(rootNode.id).toBe('root');
expect(childNode.id).toBe('theId');
expect(childNode.desc).toBe('child1');
});
it('MMP-9 should handle an id and type for a node definition', () => {
const result = parse('mindmap\nroot\n theId(child1)');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
expect(rootNode.id).toBe('root');
expect(childNode.id).toBe('theId');
expect(childNode.desc).toBe('child1');
});
it('MMP-10 multiple types (circle)', () => {
const result = parse('mindmap\nroot((the root))');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as CircleNode;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
it('MMP-11 multiple types (cloud)', () => {
const result = parse('mindmap\nroot)the root(');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
it('MMP-12 multiple types (bang)', () => {
const result = parse('mindmap\nroot))the root((');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
it('MMP-12-a multiple types (hexagon)', () => {
const result = parse('mindmap\nroot{{the root}}');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
});
describe('Decorations (ported from mindmap.spec.ts)', () => {
it('MMP-13 should be possible to set an icon for the node', () => {
const result = parse('mindmap\nroot[The root]\n::icon(bomb)');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check icon if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-14 should be possible to set classes for the node', () => {
const result = parse('mindmap\nroot[The root]\n:::m-4 p-8');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check class if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-15 should be possible to set both classes and icon for the node', () => {
const result = parse('mindmap\nroot[The root]\n:::m-4 p-8\n::icon(bomb)');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check class and icon if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-16 should be possible to set both classes and icon for the node (reverse order)', () => {
const result = parse('mindmap\nroot[The root]\n::icon(bomb)\n:::m-4 p-8');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check class and icon if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-16.2 should handle an id and type for a node definition', () => {
const result = parse(`mindmap
id1[SquareNode I am]
id2["SquareNode I am"]
id3("RoundedNode I am")
id4(RoundedNode I am)
id5(("CircleNode I am"))
id6((CircleNode I am))
id7))BangNode I am((
id8))"BangNode I am"((
id9)"CloudNode I am"(
id10)CloudNode I am(
id11{{"HexagonNode I am"}}
id12{{HexagonNode I am}}
id13
`);
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
expect(result.value.MindmapRows).toHaveLength(13);
expect(result.value.MindmapRows[0].item.$type).toBe('SquareNode');
expect(result.value.MindmapRows[1].item.$type).toBe('SquareNode');
expect(result.value.MindmapRows[2].item.$type).toBe('RoundedNode');
expect(result.value.MindmapRows[3].item.$type).toBe('RoundedNode');
expect(result.value.MindmapRows[4].item.$type).toBe('CircleNode');
expect(result.value.MindmapRows[5].item.$type).toBe('CircleNode');
expect(result.value.MindmapRows[6].item.$type).toBe('BangNode');
expect(result.value.MindmapRows[7].item.$type).toBe('BangNode');
expect(result.value.MindmapRows[8].item.$type).toBe('CloudNode');
expect(result.value.MindmapRows[9].item.$type).toBe('CloudNode');
expect(result.value.MindmapRows[10].item.$type).toBe('HexagonNode');
expect(result.value.MindmapRows[11].item.$type).toBe('HexagonNode');
expect(result.value.MindmapRows[12].item.$type).toBe('SimpleNode');
let id = 1;
for (const row of result.value.MindmapRows as MindmapRow[]) {
const item = row.item as Node;
expect(item.id).toBeDefined();
expect(item?.id).toBe('id' + id);
if (item.id !== 'id13') {
expect(item.desc).toBe(item.$type + ' I am');
}
id++;
}
});
});
describe('Descriptions (ported from mindmap.spec.ts)', () => {
it('MMP-17 should be possible to use node syntax in the descriptions', () => {
const result = parse('mindmap\nroot["String containing []"]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('String containing []');
});
it('MMP-18 should be possible to use node syntax in the descriptions in children', () => {
const result = parse('mindmap\nroot["String containing []"]\n child1["String containing ()"]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
expect(rootNode.desc).toBe('String containing []');
expect(childNode.desc).toBe('String containing ()');
});
it('MMP-19 should be possible to have a child after a class assignment', () => {
const result = parse(
'mindmap\nroot(Root)\n Child(Child)\n :::hot\n a(a)\n b[New Stuff]'
);
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
const aNode = result.value.MindmapRows[3].item as OtherComplex;
const bNode = result.value.MindmapRows[4].item as OtherComplex;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
expect(bNode.desc).toBe('New Stuff');
});
});
describe('Miscellaneous (ported from mindmap.spec.ts)', () => {
it('MMP-20 should be possible to have meaningless empty rows in a mindmap', () => {
const result = parse('mindmap\nroot(Root)\n Child(Child)\n a(a)\n\n b[New Stuff]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
const aNode = result.value.MindmapRows[2].item as OtherComplex;
const bNode = result.value.MindmapRows[3].item as OtherComplex;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
expect(bNode.desc).toBe('New Stuff');
});
it('MMP-21 should be possible to have comments in a mindmap', () => {
const result = parse(
'mindmap\nroot(Root)\n Child(Child)\n a(a)\n\n %% This is a comment\n b[New Stuff]'
);
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
const aNode = result.value.MindmapRows[2].item as OtherComplex;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
const bNode = result.value.MindmapRows[4].item as OtherComplex;
expect(bNode.desc).toBe('New Stuff');
});
it('MMP-22 should be possible to have comments at the end of a line', () => {
const result = parse(
'mindmap\nroot(Root)\n Child(Child)\n a(a) %% This is a comment\n b[New Stuff]'
);
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
const aNode = result.value.MindmapRows[2].item as OtherComplex;
const bNode = result.value.MindmapRows[4].item as OtherComplex;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
expect(bNode.desc).toBe('New Stuff');
});
it('MMP-23 Rows with only spaces should not interfere', () => {
const result = parse('mindmap\nroot\n A\n \n\n B');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const aNode = result.value.MindmapRows[1].item as SimpleNode;
const bNode = result.value.MindmapRows[3].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(aNode.id).toBe('A');
expect(bNode.id).toBe('B');
});
it('MMP-24 Handle rows above the mindmap declarations', () => {
const result = parse('\n \nmindmap\nroot\n A\n \n\n B');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(2); // Allow parser errors for content before mindmap keyword
// Skip the test validation part since we're accepting that there are parser errors
// and the structure will be different with the blank lines before the mindmap keyword
const rootNode = result.value.MindmapRows[1].item as SimpleNode;
const aNode = result.value.MindmapRows[2].item as SimpleNode;
const bNode = result.value.MindmapRows[4].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(aNode.id).toBe('A');
expect(bNode.id).toBe('B');
});
it('MMP-25 Handle rows above the mindmap declarations, no space', () => {
const result = parse('\n\n\nmindmap\nroot\n A\n \n\n B');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0); // No parser errors
// Skip the test validation part since the structure might be different
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const aNode = result.value.MindmapRows[1].item as SimpleNode;
const bNode = result.value.MindmapRows[3].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(aNode.id).toBe('A');
expect(bNode.id).toBe('B');
});
});

View File

@@ -1,4 +1,4 @@
import type { LangiumParser, ParseResult, ParserOptions } from 'langium';
import type { LangiumParser, ParseResult } from 'langium';
import { expect, vi } from 'vitest';
import type {
Architecture,
@@ -13,8 +13,6 @@ import type {
PacketServices,
GitGraph,
GitGraphServices,
Mindmap,
MindmapServices,
} from '../src/language/index.js';
import {
createArchitectureServices,
@@ -23,9 +21,7 @@ import {
createRadarServices,
createPacketServices,
createGitGraphServices,
createMindmapServices,
} from '../src/language/index.js';
import { parseHelper } from 'langium/test';
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
@@ -108,16 +104,3 @@ export function createGitGraphTestServices() {
return { services: gitGraphServices, parse };
}
export const gitGraphParse = createGitGraphTestServices().parse;
const mindmapServices: MindmapServices = createMindmapServices().Mindmap;
const mindmapParser: LangiumParser = mindmapServices.parser.LangiumParser;
export function createMindmapTestServices() {
const parse = (input: string, options?: ParserOptions) => {
return mindmapParser.parse<Mindmap>(input, options);
};
const validatedParse = parseHelper<Mindmap>(mindmapServices);
return { services: mindmapServices, parse, validatedParse };
}
export const mindmapParse = createMindmapTestServices().parse;
export const validatedMindmapParse = createMindmapTestServices().validatedParse;

97
pnpm-lock.yaml generated
View File

@@ -508,67 +508,6 @@ importers:
specifier: ^7.3.0
version: 7.3.0
packages/mermaid/src/vitepress:
dependencies:
'@mdi/font':
specifier: ^7.4.47
version: 7.4.47
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
font-awesome:
specifier: ^4.7.0
version: 4.7.0
jiti:
specifier: ^2.4.2
version: 2.4.2
mermaid:
specifier: workspace:^
version: link:../..
vue:
specifier: ^3.4.38
version: 3.5.13(typescript@5.7.3)
devDependencies:
'@iconify-json/carbon':
specifier: ^1.1.37
version: 1.2.1
'@unocss/reset':
specifier: ^66.0.0
version: 66.0.0
'@vite-pwa/vitepress':
specifier: ^0.5.3
version: 0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))
'@vitejs/plugin-vue':
specifier: ^5.0.5
version: 5.2.1(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
fast-glob:
specifier: ^3.3.3
version: 3.3.3
https-localhost:
specifier: ^4.7.1
version: 4.7.1
pathe:
specifier: ^2.0.3
version: 2.0.3
unocss:
specifier: ^66.0.0
version: 66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.26.9)(vue@3.5.13(typescript@5.7.3))
vite:
specifier: ^6.1.1
version: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vitepress:
specifier: 1.6.3
version: 1.6.3(@algolia/client-search@5.20.3)(@types/node@22.13.5)(axios@1.8.4)(postcss@8.5.3)(search-insights@2.17.2)(terser@5.39.0)(typescript@5.7.3)
workbox-window:
specifier: ^7.3.0
version: 7.3.0
packages/parser:
dependencies:
langium:
@@ -3479,15 +3418,6 @@ packages:
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
'@vite-pwa/vitepress@0.5.4':
resolution: {integrity: sha512-g57qwG983WTyQNLnOcDVPQEIeN+QDgK/HdqghmygiUFp3a/MzVvmLXC/EVnPAXxWa8W2g9pZ9lE3EiDGs2HjsA==}
peerDependencies:
'@vite-pwa/assets-generator': ^0.2.6
vite-plugin-pwa: '>=0.21.2 <1'
peerDependenciesMeta:
'@vite-pwa/assets-generator':
optional: true
'@vite-pwa/vitepress@1.0.0':
resolution: {integrity: sha512-i5RFah4urA6tZycYlGyBslVx8cVzbZBcARJLDg5rWMfAkRmyLtpRU6usGfVOwyN9kjJ2Bkm+gBHXF1hhr7HptQ==}
peerDependencies:
@@ -9445,18 +9375,6 @@ packages:
peerDependencies:
vite: '>=4 <=6'
vite-plugin-pwa@0.21.2:
resolution: {integrity: sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@vite-pwa/assets-generator': ^0.2.6
vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
workbox-build: ^7.3.0
workbox-window: ^7.3.0
peerDependenciesMeta:
'@vite-pwa/assets-generator':
optional: true
vite-plugin-pwa@1.0.0:
resolution: {integrity: sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==}
engines: {node: '>=16.0.0'}
@@ -13417,10 +13335,6 @@ snapshots:
transitivePeerDependencies:
- vue
'@vite-pwa/vitepress@0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
@@ -20624,17 +20538,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.12
vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
workbox-build: 7.1.1(@types/babel__core@7.20.5)
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)