Merge branch 'develop' into origin/3258_Flowchart_nodeSpacing_Subgraph

This commit is contained in:
rowanfr
2024-03-06 12:06:06 -06:00
committed by GitHub
171 changed files with 4395 additions and 1990 deletions

View File

@@ -0,0 +1,45 @@
{
"name": "@mermaid-js/flowchart-elk",
"version": "1.0.0-rc.1",
"description": "Flowchart plugin for mermaid with ELK layout",
"module": "dist/mermaid-flowchart-elk.core.mjs",
"types": "dist/packages/mermaid-flowchart-elk/src/detector.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-flowchart-elk.core.mjs",
"types": "./dist/packages/mermaid-flowchart-elk/src/detector.d.ts"
},
"./*": "./*"
},
"keywords": [
"diagram",
"markdown",
"flowchart",
"elk",
"mermaid"
],
"scripts": {
"prepublishOnly": "pnpm -w run build"
},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"author": "Knut Sveidqvist",
"license": "MIT",
"dependencies": {
"d3": "^7.4.0",
"dagre-d3-es": "7.0.10",
"khroma": "^2.0.0",
"elkjs": "^0.8.2"
},
"devDependencies": {
"concurrently": "^8.0.0",
"rimraf": "^5.0.0",
"mermaid": "workspace:^"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,32 @@
import type {
ExternalDiagramDefinition,
DiagramDetector,
DiagramLoader,
} from '../../mermaid/src/diagram-api/types.js';
const id = 'flowchart-elk';
const detector: DiagramDetector = (txt, config): boolean => {
if (
// If diagram explicitly states flowchart-elk
/^\s*flowchart-elk/.test(txt) ||
// If a flowchart/graph diagram has their default renderer set to elk
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
) {
return true;
}
return false;
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./diagram-definition.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@@ -0,0 +1,12 @@
// @ts-ignore: JISON typing missing
import parser from '../../mermaid/src/diagrams/flowchart/parser/flow.jison';
import * as db from '../../mermaid/src/diagrams/flowchart/flowDb.js';
import styles from '../../mermaid/src/diagrams/flowchart/styles.js';
import renderer from './flowRenderer-elk.js';
export const diagram = {
db,
renderer,
parser,
styles,
};

View File

@@ -1,17 +1,17 @@
import { select, line, curveLinear } from 'd3';
import { insertNode } from '../../../dagre-wrapper/nodes.js';
import insertMarkers from '../../../dagre-wrapper/markers.js';
import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js';
import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js';
import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js';
import { insertEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js';
import { findCommonAncestor } from './render-utils.js';
import { labelHelper } from '../../../dagre-wrapper/shapes/util.js';
import { getConfig } from '../../../config.js';
import { log } from '../../../logger.js';
import { setupGraphViewbox } from '../../../setupGraphViewbox.js';
import common from '../../common/common.js';
import { interpolateToCurve, getStylesFromArray } from '../../../utils.js';
import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js';
import { getConfig } from '../../mermaid/src/config.js';
import { log } from '../../mermaid/src/logger.js';
import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js';
import common from '../../mermaid/src/diagrams/common/common.js';
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js';
import ELK from 'elkjs/lib/elk.bundled.js';
import { getLineFunctionsWithOffset } from '../../../utils/lineWithOffset.js';
import { addEdgeMarkers } from '../../../dagre-wrapper/edgeMarker.js';
import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js';
import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.js';
const elk = new ELK();
@@ -594,7 +594,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
*
* @param text
* @param diagObj
* @returns {Record<string, import('../../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
* @returns {Record<string, import('../../mermaid/src/diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
log.info('Extracting classes');

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "../..",
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -19,6 +19,7 @@
"mermaid"
],
"scripts": {
"clean": "rimraf dist",
"prepublishOnly": "pnpm -w run build"
},
"repository": {

View File

@@ -5,7 +5,6 @@
* This is a dummy parser that satisfies the mermaid API logic.
*/
export default {
parser: { yy: {} },
parse: () => {
// no op
},

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "10.8.0",
"version": "11.0.0-alpha.6",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -60,8 +60,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "^6.0.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@mermaid-js/parser": "workspace:^",
"cytoscape": "^3.28.1",
"cytoscape-cose-bilkent": "^4.1.0",
"d3": "^7.4.0",
@@ -74,11 +73,9 @@
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
"mdast-util-from-markdown": "^1.3.0",
"non-layered-tidy-tree-layout": "^2.0.2",
"stylis": "^4.1.3",
"ts-dedent": "^2.2.0",
"uuid": "^9.0.0",
"web-worker": "^1.2.0"
"uuid": "^9.0.0"
},
"devDependencies": {
"@adobe/jsonschema2md": "^7.1.4",
@@ -86,6 +83,7 @@
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1",
"@types/dompurify": "^3.0.2",

View File

@@ -233,6 +233,23 @@ async function generateTypescript(mermaidConfigSchema: JSONSchemaType<MermaidCon
}
}
/**
* Workaround for type duplication when a $ref property has siblings.
*
* @param json - The input JSON object.
*
* @see https://github.com/bcherny/json-schema-to-typescript/issues/193
*/
function removeProp(json: any, name: string) {
for (const prop in json) {
if (prop === name) {
delete json[prop];
} else if (typeof json[prop] === 'object') {
removeProp(json[prop], name);
}
}
}
/** Main function */
async function main() {
if (verifyOnly) {
@@ -243,6 +260,8 @@ async function main() {
const configJsonSchema = await loadJsonSchemaFromYaml();
removeProp(configJsonSchema, 'default');
validateSchema(configJsonSchema);
// Generate types from JSON Schema

View File

@@ -1,10 +1,8 @@
import * as configApi from './config.js';
import { log } from './logger.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
import { UnknownDiagramError } from './errors.js';
import { encodeEntities } from './utils.js';
import type { DetailedError } from './utils.js';
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
@@ -15,48 +13,45 @@ export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?:
* @privateRemarks This is exported as part of the public mermaidAPI.
*/
export class Diagram {
type = 'graph';
parser: DiagramDefinition['parser'];
renderer: DiagramDefinition['renderer'];
db: DiagramDefinition['db'];
private init?: DiagramDefinition['init'];
private detectError?: UnknownDiagramError;
constructor(public text: string, public metadata: Pick<DiagramMetadata, 'title'> = {}) {
this.text = encodeEntities(text);
this.text += '\n';
const cnf = configApi.getConfig();
try {
this.type = detectType(text, cnf);
} catch (e) {
this.type = 'error';
this.detectError = e as UnknownDiagramError;
}
const diagram = getDiagram(this.type);
log.debug('Type ' + this.type);
// Setup diagram
this.db = diagram.db;
this.renderer = diagram.renderer;
this.parser = diagram.parser;
this.parser.parser.yy = this.db;
this.init = diagram.init;
this.parse();
}
parse() {
if (this.detectError) {
throw this.detectError;
}
this.db.clear?.();
public static async fromText(text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) {
const config = configApi.getConfig();
this.init?.(config);
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
if (this.metadata.title) {
this.db.setDiagramTitle?.(this.metadata.title);
const type = detectType(text, config);
text = encodeEntities(text) + '\n';
try {
getDiagram(type);
} catch (e) {
const loader = getDiagramLoader(type);
if (!loader) {
throw new UnknownDiagramError(`Diagram ${type} not found.`);
}
// Diagram not available, loading it.
// new diagram will try getDiagram again and if fails then it is a valid throw
const { id, diagram } = await loader();
registerDiagram(id, diagram);
}
this.parser.parse(this.text);
const { db, parser, renderer, init } = getDiagram(type);
if (parser.parser) {
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
parser.parser.yy = db;
}
db.clear?.();
init?.(config);
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
if (metadata.title) {
db.setDiagramTitle?.(metadata.title);
}
await parser.parse(text);
return new Diagram(type, text, db, parser, renderer);
}
private constructor(
public type: string,
public text: string,
public db: DiagramDefinition['db'],
public parser: DiagramDefinition['parser'],
public renderer: DiagramDefinition['renderer']
) {}
async render(id: string, version: string) {
await this.renderer.draw(this.text, id, version, this);
}
@@ -69,34 +64,3 @@ export class Diagram {
return this.type;
}
}
/**
* Parse the text asynchronously and generate a Diagram object asynchronously.
* **Warning:** This function may be changed in the future.
* @alpha
* @param text - The mermaid diagram definition.
* @param metadata - Diagram metadata, defined in YAML.
* @returns A the Promise of a Diagram object.
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
* @privateRemarks This is exported as part of the public mermaidAPI.
*/
export const getDiagramFromText = async (
text: string,
metadata: Pick<DiagramMetadata, 'title'> = {}
): Promise<Diagram> => {
const type = detectType(text, configApi.getConfig());
try {
// Trying to find the diagram
getDiagram(type);
} catch (error) {
const loader = getDiagramLoader(type);
if (!loader) {
throw new UnknownDiagramError(`Diagram ${type} not found.`);
}
// Diagram not available, loading it.
// new diagram will try getDiagram again and if fails then it is a valid throw
const { id, diagram } = await loader();
registerDiagram(id, diagram);
}
return new Diagram(text, metadata);
};

View File

@@ -61,7 +61,7 @@ export interface MermaidConfig {
* You may also use `themeCSS` to override this value.
*
*/
theme?: string | 'default' | 'forest' | 'dark' | 'neutral' | 'null';
theme?: 'default' | 'forest' | 'dark' | 'neutral' | 'null';
themeVariables?: any;
themeCSS?: string;
/**
@@ -87,26 +87,11 @@ export interface MermaidConfig {
* This option decides the amount of logging to be used by mermaid.
*
*/
logLevel?:
| number
| string
| 0
| 2
| 1
| 'trace'
| 'debug'
| 'info'
| 'warn'
| 'error'
| 'fatal'
| 3
| 4
| 5
| undefined;
logLevel?: 'trace' | 0 | 'debug' | 1 | 'info' | 2 | 'warn' | 3 | 'error' | 4 | 'fatal' | 5;
/**
* Level of trust for parsed diagram
*/
securityLevel?: string | 'strict' | 'loose' | 'antiscript' | 'sandbox' | undefined;
securityLevel?: 'strict' | 'loose' | 'antiscript' | 'sandbox';
/**
* Dictates whether mermaid starts on Page load
*/
@@ -169,19 +154,43 @@ export interface MermaidConfig {
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
packet?: PacketDiagramConfig;
block?: BlockDiagramConfig;
dompurifyConfig?: DOMPurifyConfiguration;
wrap?: boolean;
fontSize?: number;
}
/**
* The object containing configurations specific for block diagrams.
* The object containing configurations specific for packet diagrams.
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "BlockDiagramConfig".
* via the `definition` "PacketDiagramConfig".
*/
export interface BlockDiagramConfig extends BaseDiagramConfig {
padding?: number;
export interface PacketDiagramConfig extends BaseDiagramConfig {
/**
* The height of each row in the packet diagram.
*/
rowHeight?: number;
/**
* The width of each bit in the packet diagram.
*/
bitWidth?: number;
/**
* The number of bits to display per row.
*/
bitsPerRow?: number;
/**
* Toggle to display or hide bit numbers.
*/
showBits?: boolean;
/**
* The horizontal padding between the blocks in a row.
*/
paddingX?: number;
/**
* The vertical padding between the rows.
*/
paddingY?: number;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
@@ -197,6 +206,15 @@ export interface BaseDiagramConfig {
*/
useMaxWidth?: boolean;
}
/**
* The object containing configurations specific for block diagrams.
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "BlockDiagramConfig".
*/
export interface BlockDiagramConfig extends BaseDiagramConfig {
padding?: number;
}
/**
* The object containing configurations specific for c4 diagrams
*
@@ -807,8 +825,8 @@ export interface XYChartConfig extends BaseDiagramConfig {
* Should show the chart title
*/
showTitle?: boolean;
xAxis?: XYChartAxisConfig1;
yAxis?: XYChartAxisConfig2;
xAxis?: XYChartAxisConfig;
yAxis?: XYChartAxisConfig;
/**
* How to plot will be drawn horizontal or vertical
*/
@@ -818,104 +836,6 @@ export interface XYChartConfig extends BaseDiagramConfig {
*/
plotReservedSpacePercent?: number;
}
/**
* This object contains configuration for XYChart axis config
*/
export interface XYChartAxisConfig1 {
/**
* Should show the axis labels (tick text)
*/
showLabel?: boolean;
/**
* font size of the axis labels (tick text)
*/
labelFontSize?: number;
/**
* top and bottom space from axis label (tick text)
*/
labelPadding?: number;
/**
* Should show the axis title
*/
showTitle?: boolean;
/**
* font size of the axis title
*/
titleFontSize?: number;
/**
* top and bottom space from axis title
*/
titlePadding?: number;
/**
* Should show the axis tick lines
*/
showTick?: boolean;
/**
* length of the axis tick lines
*/
tickLength?: number;
/**
* width of the axis tick lines
*/
tickWidth?: number;
/**
* Show line across the axis
*/
showAxisLine?: boolean;
/**
* Width of the axis line
*/
axisLineWidth?: number;
}
/**
* This object contains configuration for XYChart axis config
*/
export interface XYChartAxisConfig2 {
/**
* Should show the axis labels (tick text)
*/
showLabel?: boolean;
/**
* font size of the axis labels (tick text)
*/
labelFontSize?: number;
/**
* top and bottom space from axis label (tick text)
*/
labelPadding?: number;
/**
* Should show the axis title
*/
showTitle?: boolean;
/**
* font size of the axis title
*/
titleFontSize?: number;
/**
* top and bottom space from axis title
*/
titlePadding?: number;
/**
* Should show the axis tick lines
*/
showTick?: boolean;
/**
* length of the axis tick lines
*/
tickLength?: number;
/**
* width of the axis tick lines
*/
tickWidth?: number;
/**
* Show line across the axis
*/
showAxisLine?: boolean;
/**
* Width of the axis line
*/
axisLineWidth?: number;
}
/**
* The object containing configurations specific for entity relationship diagrams
*
@@ -936,7 +856,7 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
/**
* Directional bias for layout of entities
*/
layoutDirection?: string | 'TB' | 'BT' | 'LR' | 'RL';
layoutDirection?: 'TB' | 'BT' | 'LR' | 'RL';
/**
* The minimum width of an entity box. Expressed in pixels.
*/
@@ -1001,7 +921,7 @@ export interface StateDiagramConfig extends BaseDiagramConfig {
* Decides which rendering engine that is to be used for the rendering.
*
*/
defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk';
defaultRenderer?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
@@ -1025,7 +945,7 @@ export interface ClassDiagramConfig extends BaseDiagramConfig {
* Decides which rendering engine that is to be used for the rendering.
*
*/
defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk';
defaultRenderer?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
nodeSpacing?: number;
rankSpacing?: number;
/**
@@ -1085,7 +1005,7 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
/**
* Multiline message alignment
*/
messageAlign?: string | 'left' | 'center' | 'right';
messageAlign?: 'left' | 'center' | 'right';
/**
* Prolongs the edge of the diagram downwards.
*
@@ -1164,7 +1084,7 @@ export interface TimelineDiagramConfig extends BaseDiagramConfig {
/**
* Multiline message alignment
*/
messageAlign?: string | 'left' | 'center' | 'right';
messageAlign?: 'left' | 'center' | 'right';
/**
* Prolongs the edge of the diagram downwards.
*
@@ -1275,7 +1195,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
* Controls the display mode.
*
*/
displayMode?: string | 'compact';
displayMode?: '' | 'compact';
/**
* On which day a week-based interval should start
*
@@ -1334,7 +1254,7 @@ export interface SequenceDiagramConfig extends BaseDiagramConfig {
/**
* Multiline message alignment
*/
messageAlign?: string | 'left' | 'center' | 'right';
messageAlign?: 'left' | 'center' | 'right';
/**
* Mirror actors under diagram
*
@@ -1391,7 +1311,7 @@ export interface SequenceDiagramConfig extends BaseDiagramConfig {
/**
* This sets the text alignment of actor-attached notes
*/
noteAlign?: string | 'left' | 'center' | 'right';
noteAlign?: 'left' | 'center' | 'right';
/**
* This sets the font size of actor messages
*/
@@ -1475,7 +1395,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
* Defines how mermaid renders curves for flowcharts.
*
*/
curve?: string | 'basis' | 'linear' | 'cardinal';
curve?: 'basis' | 'linear' | 'cardinal';
/**
* Represents the padding between the labels and the shape
*
@@ -1487,7 +1407,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
* Decides which rendering engine that is to be used for the rendering.
*
*/
defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk';
defaultRenderer?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
/**
* Width of nodes where text is wrapped.
*
@@ -1511,13 +1431,7 @@ export interface SankeyDiagramConfig extends BaseDiagramConfig {
*
*/
linkColor?: SankeyLinkColor | string;
/**
* Controls the alignment of the Sankey diagrams.
*
* See <https://github.com/d3/d3-sankey#alignments>.
*
*/
nodeAlignment?: 'left' | 'right' | 'center' | 'justify';
nodeAlignment?: SankeyNodeAlignment;
useMaxWidth?: boolean;
/**
* Toggle to display or hide values along with title.

View File

@@ -257,6 +257,9 @@ const config: RequiredDeep<MermaidConfig> = {
// TODO: can we make this default to `true` instead?
useMaxWidth: false,
},
packet: {
...defaultConfigJson.packet,
},
};
const keyify = (obj: any, prefix = ''): string[] =>

View File

@@ -71,10 +71,9 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
if (detectors[key]) {
log.error(`Detector with key ${key} already exists`);
} else {
detectors[key] = { detector, loader };
log.warn(`Detector with key ${key} already exists. Overwriting.`);
}
detectors[key] = { detector, loader };
log.debug(`Detector with key ${key} added${loader ? ' with loader' : ''}`);
};

View File

@@ -20,6 +20,7 @@ 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 { packet } from '../diagrams/packet/detector.js';
import block from '../diagrams/block/blockDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
@@ -51,7 +52,6 @@ export const addDiagrams = () => {
},
},
parser: {
parser: { yy: {} },
parse: () => {
throw new Error(
'Diagrams beginning with --- are not valid. ' +
@@ -88,6 +88,7 @@ export const addDiagrams = () => {
journey,
quadrantChart,
sankey,
packet,
xychart,
block
);

View File

@@ -2,12 +2,12 @@ import { detectType } from './detectType.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
import { addDiagrams } from './diagram-orchestration.js';
import type { DiagramDetector } from './types.js';
import { getDiagramFromText } from '../Diagram.js';
import { Diagram } from '../Diagram.js';
import { it, describe, expect, beforeAll } from 'vitest';
addDiagrams();
beforeAll(async () => {
await getDiagramFromText('sequenceDiagram');
await Diagram.fromText('sequenceDiagram');
});
describe('DiagramAPI', () => {
@@ -39,7 +39,6 @@ describe('DiagramAPI', () => {
parse: (_text) => {
return;
},
parser: { yy: {} },
},
renderer: {
draw: () => {

View File

@@ -49,7 +49,7 @@ export const registerDiagram = (
detector?: DiagramDetector
) => {
if (diagrams[id]) {
throw new Error(`Diagram ${id} already registered.`);
log.warn(`Diagram with id ${id} already registered. Overwriting.`);
}
diagrams[id] = diagram;
if (detector) {

View File

@@ -1,4 +1,4 @@
import type { MermaidConfig } from '../config.type.js';
import type { GanttDiagramConfig, MermaidConfig } from '../config.type.js';
import { frontMatterRegex } from './regexes.js';
// The "* as yaml" part is necessary for tree-shaking
import * as yaml from 'js-yaml';
@@ -6,7 +6,7 @@ import * as yaml from 'js-yaml';
interface FrontMatterMetadata {
title?: string;
// Allows custom display modes. Currently used for compact mode in gantt charts.
displayMode?: string;
displayMode?: GanttDiagramConfig['displayMode'];
config?: MermaidConfig;
}
@@ -44,7 +44,7 @@ export function extractFrontMatter(text: string): FrontMatterResult {
// Only add properties that are explicitly supported, if they exist
if (parsed.displayMode) {
metadata.displayMode = parsed.displayMode.toString();
metadata.displayMode = parsed.displayMode.toString() as GanttDiagramConfig['displayMode'];
}
if (parsed.title) {
metadata.title = parsed.title.toString();

View File

@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type * as d3 from 'd3';
import type { SetRequired } from 'type-fest';
import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type * as d3 from 'd3';
export interface DiagramMetadata {
title?: string;
@@ -39,6 +40,22 @@ export interface DiagramDB {
bindFunctions?: (element: Element) => void;
}
/**
* DiagramDB with fields that is required for all new diagrams.
*/
export type DiagramDBBase<T extends BaseDiagramConfig> = {
getConfig: () => Required<T>;
} & SetRequired<
DiagramDB,
| 'clear'
| 'getAccTitle'
| 'getDiagramTitle'
| 'getAccDescription'
| 'setAccDescription'
| 'setAccTitle'
| 'setDiagramTitle'
>;
// This is what is returned from getClasses(...) methods.
// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
@@ -104,8 +121,8 @@ export type DrawDefinition = (
) => void | Promise<void>;
export interface ParserDefinition {
parse: (text: string) => void;
parser: { yy: DiagramDB };
parse: (text: string) => void | Promise<void>;
parser?: { yy: DiagramDB };
}
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element | null, unknown>;

View File

@@ -1,16 +1,39 @@
import { describe, test, expect } from 'vitest';
import { Diagram, getDiagramFromText } from './Diagram.js';
import { Diagram } from './Diagram.js';
import { addDetector } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import type { DiagramLoader } from './diagram-api/types.js';
addDiagrams();
const getDummyDiagram = (id: string, title?: string): Awaited<ReturnType<DiagramLoader>> => {
return {
id,
diagram: {
db: {
getDiagramTitle: () => title ?? id,
},
parser: {
parse: () => {
// no-op
},
},
renderer: {
draw: () => {
// no-op
},
},
styles: {},
},
};
};
describe('diagram detection', () => {
test('should detect inbuilt diagrams', async () => {
const graph = (await getDiagramFromText('graph TD; A-->B')) as Diagram;
const graph = (await Diagram.fromText('graph TD; A-->B')) as Diagram;
expect(graph).toBeInstanceOf(Diagram);
expect(graph.type).toBe('flowchart-v2');
const sequence = (await getDiagramFromText(
const sequence = (await Diagram.fromText(
'sequenceDiagram; Alice->>+John: Hello John, how are you?'
)) as Diagram;
expect(sequence).toBeInstanceOf(Diagram);
@@ -21,41 +44,33 @@ describe('diagram detection', () => {
addDetector(
'loki',
(str) => str.startsWith('loki'),
() =>
Promise.resolve({
id: 'loki',
diagram: {
db: {},
parser: {
parse: () => {
// no-op
},
parser: {
yy: {},
},
},
renderer: {
draw: () => {
// no-op
},
},
styles: {},
},
})
() => Promise.resolve(getDummyDiagram('loki'))
);
const diagram = (await getDiagramFromText('loki TD; A-->B')) as Diagram;
const diagram = await Diagram.fromText('loki TD; A-->B');
expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.type).toBe('loki');
});
test('should allow external diagrams to override internal ones with same ID', async () => {
const title = 'overridden';
addDetector(
'flowchart-elk',
(str) => str.startsWith('flowchart-elk'),
() => Promise.resolve(getDummyDiagram('flowchart-elk', title))
);
const diagram = await Diagram.fromText('flowchart-elk TD; A-->B');
expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.db.getDiagramTitle?.()).toBe(title);
});
test('should throw the right error for incorrect diagram', async () => {
await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
await expect(Diagram.fromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
"Parse error on line 2:
graph TD; A-->
--------------^
Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF'"
`);
await expect(getDiagramFromText('sequenceDiagram; A-->B')).rejects
await expect(Diagram.fromText('sequenceDiagram; A-->B')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Parse error on line 1:
...quenceDiagram; A-->B
@@ -65,13 +80,13 @@ Expecting 'TXT', got 'NEWLINE'"
});
test('should throw the right error for unregistered diagrams', async () => {
await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(Diagram.fromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
'"No diagram type detected matching given configuration for text: thor TD; A-->B"'
);
});
test('should consider entity codes when present in diagram defination', async () => {
const diagram = await getDiagramFromText(`sequenceDiagram
const diagram = await Diagram.fromText(`sequenceDiagram
A->>B: I #9829; you!
B->>A: I #9829; you #infin; times more!`);
// @ts-ignore: we need to add types for sequenceDb which will be done in separate PR

View File

@@ -1,9 +1,9 @@
import type { DiagramDB } from '../../diagram-api/types.js';
import type { BlockConfig, BlockType, Block, ClassDef } from './blockTypes.js';
import * as configApi from '../../config.js';
import { clear as commonClear } from '../common/commonDb.js';
import { log } from '../../logger.js';
import clone from 'lodash-es/clone.js';
import * as configApi from '../../config.js';
import type { DiagramDB } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { clear as commonClear } from '../common/commonDb.js';
import type { Block, ClassDef } from './blockTypes.js';
// Initialize the node database for simple lookups
let blockDatabase: Record<string, Block> = {};

View File

@@ -1,19 +1,17 @@
import type { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js';
import { calculateBlockSizes, insertBlocks, insertEdges } from './renderHelpers.js';
import { layout } from './layout.js';
import type { MermaidConfig, BaseDiagramConfig } from '../../config.type.js';
import insertMarkers from '../../dagre-wrapper/markers.js';
import {
select as d3select,
scaleOrdinal as d3scaleOrdinal,
schemeTableau10 as d3schemeTableau10,
select as d3select,
} from 'd3';
import type { ContainerElement } from 'd3';
import type { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js';
import type { MermaidConfig } from '../../config.type.js';
import insertMarkers from '../../dagre-wrapper/markers.js';
import { log } from '../../logger.js';
import type { BlockDB } from './blockDB.js';
import type { Block } from './blockTypes.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { BlockDB } from './blockDB.js';
import { layout } from './layout.js';
import { calculateBlockSizes, insertBlocks, insertEdges } from './renderHelpers.js';
/**
* Returns the all the styles from classDef statements in the graph definition.

View File

@@ -1,15 +1,10 @@
import { getStylesFromArray } from '../../utils.js';
import { insertNode, positionNode } from '../../dagre-wrapper/nodes.js';
import { insertEdge, insertEdgeLabel, positionEdgeLabel } from '../../dagre-wrapper/edges.js';
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
import { getConfig } from '../../config.js';
import type { ContainerElement } from 'd3';
import type { Block } from './blockTypes.js';
import { insertEdge, insertEdgeLabel, positionEdgeLabel } from '../../dagre-wrapper/edges.js';
import { insertNode, positionNode } from '../../dagre-wrapper/nodes.js';
import { getStylesFromArray } from '../../utils.js';
import type { BlockDB } from './blockDB.js';
interface Node {
classes: string;
}
import type { Block } from './blockTypes.js';
function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
const vertex = block;

View File

@@ -0,0 +1,14 @@
import type { DiagramAST } from '@mermaid-js/parser';
import type { DiagramDB } from '../../diagram-api/types.js';
export function populateCommonDb(ast: DiagramAST, db: DiagramDB) {
if (ast.accDescr) {
db.setAccDescription?.(ast.accDescr);
}
if (ast.accTitle) {
db.setAccTitle?.(ast.accTitle);
}
if (ast.title) {
db.setDiagramTitle?.(ast.title);
}
}

View File

@@ -5,7 +5,6 @@ const diagram: DiagramDefinition = {
db: {},
renderer,
parser: {
parser: { yy: {} },
parse: (): void => {
return;
},

View File

@@ -3,6 +3,7 @@ import type {
DiagramDetector,
DiagramLoader,
} from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
const id = 'flowchart-elk';
@@ -13,13 +14,21 @@ const detector: DiagramDetector = (txt, config): boolean => {
// If a flowchart/graph diagram has their default renderer set to elk
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
) {
// This will log at the end, hopefully.
setTimeout(
() =>
log.warn(
'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](link) for more details. This diagram will be rendered using `dagre` layout as a fallback.'
),
500
);
return true;
}
return false;
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./flowchart-elk-definition.js');
const { diagram } = await import('../flowDiagram-v2.js');
return { id, diagram };
};

View File

@@ -1,13 +0,0 @@
// @ts-ignore: JISON typing missing
import parser from '../parser/flow.jison';
import * as db from '../flowDb.js';
import renderer from './flowRenderer-elk.js';
import styles from './styles.js';
export const diagram = {
db,
renderer,
parser,
styles,
};

View File

@@ -1,14 +1,15 @@
import flowDb from './flowDb.js';
import type { FlowSubGraph } from './types.js';
describe('flow db subgraphs', () => {
let subgraphs;
let subgraphs: FlowSubGraph[];
beforeEach(() => {
subgraphs = [
{ nodes: ['a', 'b', 'c', 'e'] },
{ nodes: ['f', 'g', 'h'] },
{ nodes: ['i', 'j'] },
{ nodes: ['k'] },
];
] as FlowSubGraph[];
});
describe('exist', () => {
it('should return true when the is exists in a subgraph', () => {
@@ -25,17 +26,17 @@ describe('flow db subgraphs', () => {
describe('makeUniq', () => {
it('should remove ids from sungraph that already exists in another subgraph even if it gets empty', () => {
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j'] }, subgraphs);
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j'] } as FlowSubGraph, subgraphs);
expect(subgraph.nodes).toEqual([]);
});
it('should remove ids from sungraph that already exists in another subgraph', () => {
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j', 'o'] }, subgraphs);
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j', 'o'] } as FlowSubGraph, subgraphs);
expect(subgraph.nodes).toEqual(['o']);
});
it('should not remove ids from subgraph if they are unique', () => {
const subgraph = flowDb.makeUniq({ nodes: ['q', 'r', 's'] }, subgraphs);
const subgraph = flowDb.makeUniq({ nodes: ['q', 'r', 's'] } as FlowSubGraph, subgraphs);
expect(subgraph.nodes).toEqual(['q', 'r', 's']);
});

View File

@@ -12,34 +12,34 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js';
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
let vertexCounter = 0;
let config = getConfig();
let vertices = {};
let edges = [];
let classes = {};
let subGraphs = [];
let subGraphLookup = {};
let tooltips = {};
let vertices: Record<string, FlowVertex> = {};
let edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = [];
let classes: Record<string, FlowClass> = {};
let subGraphs: FlowSubGraph[] = [];
let subGraphLookup: Record<string, FlowSubGraph> = {};
let tooltips: Record<string, string> = {};
let subCount = 0;
let firstGraphFlag = true;
let direction;
let direction: string;
let version; // As in graph
let version: string; // As in graph
// Functions to be run after graph rendering
let funs = []; // cspell:ignore funs
let funs: ((element: Element) => void)[] = []; // cspell:ignore funs
const sanitizeText = (txt) => common.sanitizeText(txt, config);
const sanitizeText = (txt: string) => common.sanitizeText(txt, config);
/**
* Function to lookup domId from id in the graph definition.
*
* @param id
* @public
* @param id - id of the node
*/
export const lookUpDomId = function (id) {
export const lookUpDomId = function (id: string) {
const vertexKeys = Object.keys(vertices);
for (const vertexKey of vertexKeys) {
if (vertices[vertexKey].id === id) {
@@ -52,30 +52,24 @@ export const lookUpDomId = function (id) {
/**
* Function called by parser when a node definition has been found
*
* @param _id
* @param text
* @param textObj
* @param type
* @param style
* @param classes
* @param dir
* @param props
*/
export const addVertex = function (_id, textObj, type, style, classes, dir, props = {}) {
export const addVertex = function (
id: string,
textObj: FlowText,
type: 'group',
style: string[],
classes: string[],
dir: string,
props = {}
) {
if (!id || id.trim().length === 0) {
return;
}
let txt;
let id = _id;
if (id === undefined) {
return;
}
if (id.trim().length === 0) {
return;
}
// if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (vertices[id] === undefined) {
vertices[id] = {
id: id,
id,
labelType: 'text',
domId: MERMAID_DOM_ID_PREFIX + id + '-' + vertexCounter,
styles: [],
@@ -94,7 +88,7 @@ export const addVertex = function (_id, textObj, type, style, classes, dir, prop
vertices[id].text = txt;
} else {
if (vertices[id].text === undefined) {
vertices[id].text = _id;
vertices[id].text = id;
}
}
if (type !== undefined) {
@@ -123,20 +117,12 @@ export const addVertex = function (_id, textObj, type, style, classes, dir, prop
/**
* Function called by parser when a link/edge definition has been found
*
* @param _start
* @param _end
* @param type
* @param linkText
* @param linkTextObj
*/
export const addSingleLink = function (_start, _end, type) {
let start = _start;
let end = _end;
// if (start[0].match(/\d/)) start = MERMAID_DOM_ID_PREFIX + start;
// if (end[0].match(/\d/)) end = MERMAID_DOM_ID_PREFIX + end;
// log.info('Got edge...', start, end);
export const addSingleLink = function (_start: string, _end: string, type: any) {
const start = _start;
const end = _end;
const edge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
log.info('abc78 Got edge...', edge);
const linkTextObj = type.text;
@@ -153,13 +139,11 @@ export const addSingleLink = function (_start, _end, type) {
if (type !== undefined) {
edge.type = type.type;
edge.stroke = type.stroke;
edge.length = type.length;
}
if (edge?.length > 10) {
edge.length = 10;
edge.length = type.length > 10 ? 10 : type.length;
}
if (edges.length < (config.maxEdges ?? 500)) {
log.info('abc78 pushing edge...');
log.info('Pushing edge...');
edges.push(edge);
} else {
throw new Error(
@@ -171,12 +155,12 @@ You have to call mermaid.initialize.`
);
}
};
export const addLink = function (_start, _end, type) {
log.info('addLink (abc78)', _start, _end, type);
let i, j;
for (i = 0; i < _start.length; i++) {
for (j = 0; j < _end.length; j++) {
addSingleLink(_start[i], _end[j], type);
export const addLink = function (_start: string[], _end: string[], type: unknown) {
log.info('addLink', _start, _end, type);
for (const start of _start) {
for (const end of _end) {
addSingleLink(start, end, type);
}
}
};
@@ -184,15 +168,16 @@ export const addLink = function (_start, _end, type) {
/**
* Updates a link's line interpolation algorithm
*
* @param positions
* @param interp
*/
export const updateLinkInterpolate = function (positions, interp) {
export const updateLinkInterpolate = function (
positions: ('default' | number)[],
interpolate: string
) {
positions.forEach(function (pos) {
if (pos === 'default') {
edges.defaultInterpolate = interp;
edges.defaultInterpolate = interpolate;
} else {
edges[pos].interpolate = interp;
edges[pos].interpolate = interpolate;
}
});
};
@@ -200,12 +185,10 @@ export const updateLinkInterpolate = function (positions, interp) {
/**
* Updates a link with a style
*
* @param positions
* @param style
*/
export const updateLink = function (positions, style) {
export const updateLink = function (positions: ('default' | number)[], style: string[]) {
positions.forEach(function (pos) {
if (pos >= edges.length) {
if (typeof pos === 'number' && pos >= edges.length) {
throw new Error(
`The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${
edges.length - 1
@@ -223,7 +206,7 @@ export const updateLink = function (positions, style) {
});
};
export const addClass = function (ids, style) {
export const addClass = function (ids: string, style: string[]) {
ids.split(',').forEach(function (id) {
if (classes[id] === undefined) {
classes[id] = { id, styles: [], textStyles: [] };
@@ -244,9 +227,8 @@ export const addClass = function (ids, style) {
/**
* Called by parser when a graph definition is found, stores the direction of the chart.
*
* @param dir
*/
export const setDirection = function (dir) {
export const setDirection = function (dir: string) {
direction = dir;
if (direction.match(/.*</)) {
direction = 'RL';
@@ -268,34 +250,32 @@ export const setDirection = function (dir) {
/**
* Called by parser when a special node is found, e.g. a clickable element.
*
* @param ids Comma separated list of ids
* @param className Class to add
* @param ids - Comma separated list of ids
* @param className - Class to add
*/
export const setClass = function (ids, className) {
ids.split(',').forEach(function (_id) {
// let id = version === 'gen-2' ? lookUpDomId(_id) : _id;
let id = _id;
// if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (vertices[id] !== undefined) {
export const setClass = function (ids: string, className: string) {
for (const id of ids.split(',')) {
if (vertices[id]) {
vertices[id].classes.push(className);
}
if (subGraphLookup[id] !== undefined) {
if (subGraphLookup[id]) {
subGraphLookup[id].classes.push(className);
}
});
}
};
const setTooltip = function (ids, tooltip) {
ids.split(',').forEach(function (id) {
if (tooltip !== undefined) {
tooltips[version === 'gen-1' ? lookUpDomId(id) : id] = sanitizeText(tooltip);
}
});
const setTooltip = function (ids: string, tooltip: string) {
if (tooltip === undefined) {
return;
}
tooltip = sanitizeText(tooltip);
for (const id of ids.split(',')) {
tooltips[version === 'gen-1' ? lookUpDomId(id) : id] = tooltip;
}
};
const setClickFun = function (id, functionName, functionArgs) {
let domId = lookUpDomId(id);
const setClickFun = function (id: string, functionName: string, functionArgs: string) {
const domId = lookUpDomId(id);
// if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (getConfig().securityLevel !== 'loose') {
return;
@@ -303,7 +283,7 @@ const setClickFun = function (id, functionName, functionArgs) {
if (functionName === undefined) {
return;
}
let argList = [];
let argList: string[] = [];
if (typeof functionArgs === 'string') {
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
@@ -343,11 +323,11 @@ const setClickFun = function (id, functionName, functionArgs) {
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
*
* @param ids Comma separated list of ids
* @param linkStr URL to create a link for
* @param target
* @param ids - Comma separated list of ids
* @param linkStr - URL to create a link for
* @param target - Target attribute for the link
*/
export const setLink = function (ids, linkStr, target) {
export const setLink = function (ids: string, linkStr: string, target: string) {
ids.split(',').forEach(function (id) {
if (vertices[id] !== undefined) {
vertices[id].link = utils.formatUrl(linkStr, config);
@@ -356,7 +336,8 @@ export const setLink = function (ids, linkStr, target) {
});
setClass(ids, 'clickable');
};
export const getTooltip = function (id) {
export const getTooltip = function (id: string) {
if (tooltips.hasOwnProperty(id)) {
return tooltips[id];
}
@@ -366,18 +347,18 @@ export const getTooltip = function (id) {
/**
* Called by parser when a click definition is found. Registers an event handler.
*
* @param ids Comma separated list of ids
* @param functionName Function to be called on click
* @param functionArgs
* @param ids - Comma separated list of ids
* @param functionName - Function to be called on click
* @param functionArgs - Arguments to be passed to the function
*/
export const setClickEvent = function (ids, functionName, functionArgs) {
export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) {
ids.split(',').forEach(function (id) {
setClickFun(id, functionName, functionArgs);
});
setClass(ids, 'clickable');
};
export const bindFunctions = function (element) {
export const bindFunctions = function (element: Element) {
funs.forEach(function (fun) {
fun(element);
});
@@ -388,7 +369,6 @@ export const getDirection = function () {
/**
* Retrieval function for fetching the found nodes after parsing has completed.
*
* @returns {{} | any | vertices}
*/
export const getVertices = function () {
return vertices;
@@ -397,7 +377,6 @@ export const getVertices = function () {
/**
* Retrieval function for fetching the found links after parsing has completed.
*
* @returns {{} | any | edges}
*/
export const getEdges = function () {
return edges;
@@ -406,15 +385,16 @@ export const getEdges = function () {
/**
* Retrieval function for fetching the found class definitions after parsing has completed.
*
* @returns {{} | any | classes}
*/
export const getClasses = function () {
return classes;
};
const setupToolTips = function (element) {
const setupToolTips = function (element: Element) {
let tooltipElem = select('.mermaidTooltip');
// @ts-ignore TODO: fix this
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
// @ts-ignore TODO: fix this
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
}
@@ -430,8 +410,9 @@ const setupToolTips = function (element) {
if (title === null) {
return;
}
const rect = this.getBoundingClientRect();
const rect = (this as Element)?.getBoundingClientRect();
// @ts-ignore TODO: fix this
tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem
.text(el.attr('title'))
@@ -441,6 +422,7 @@ const setupToolTips = function (element) {
el.classed('hover', true);
})
.on('mouseout', function () {
// @ts-ignore TODO: fix this
tooltipElem.transition().duration(500).style('opacity', 0);
const el = select(this);
el.classed('hover', false);
@@ -451,7 +433,6 @@ funs.push(setupToolTips);
/**
* Clears the internal graph db so that a new graph can be parsed.
*
* @param ver
*/
export const clear = function (ver = 'gen-1') {
vertices = {};
@@ -467,31 +448,29 @@ export const clear = function (ver = 'gen-1') {
config = getConfig();
commonClear();
};
export const setGen = (ver) => {
export const setGen = (ver: string) => {
version = ver || 'gen-2';
};
/** @returns {string} */
export const defaultStyle = function () {
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;';
};
/**
* Clears the internal graph db so that a new graph can be parsed.
*
* @param _id
* @param list
* @param _title
*/
export const addSubGraph = function (_id, list, _title) {
let id = _id.text.trim();
export const addSubGraph = function (
_id: { text: string },
list: string[],
_title: { text: string; type: string }
) {
let id: string | undefined = _id.text.trim();
let title = _title.text;
if (_id === _title && _title.text.match(/\s/)) {
id = undefined;
}
/** @param a */
function uniq(a) {
const prims = { boolean: {}, number: {}, string: {} };
const objs = [];
function uniq(a: any[]) {
const prims: any = { boolean: {}, number: {}, string: {} };
const objs: any[] = [];
let dir; // = undefined; direction.trim();
const nodeList = a.filter(function (item) {
@@ -512,10 +491,7 @@ export const addSubGraph = function (_id, list, _title) {
return { nodeList, dir };
}
let nodeList = [];
const { nodeList: nl, dir } = uniq(nodeList.concat.apply(nodeList, list));
nodeList = nl;
const { nodeList, dir } = uniq(list.flat());
if (version === 'gen-1') {
for (let i = 0; i < nodeList.length; i++) {
nodeList[i] = lookUpDomId(nodeList[i]);
@@ -523,7 +499,6 @@ export const addSubGraph = function (_id, list, _title) {
}
id = id || 'subGraph' + subCount;
// if (id[0].match(/\d/)) id = lookUpDomId(id);
title = title || '';
title = sanitizeText(title);
subCount = subCount + 1;
@@ -538,19 +513,6 @@ export const addSubGraph = function (_id, list, _title) {
log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir);
/** Deletes an id from all subgraphs */
// const del = _id => {
// subGraphs.forEach(sg => {
// const pos = sg.nodes.indexOf(_id);
// if (pos >= 0) {
// sg.nodes.splice(pos, 1);
// }
// });
// };
// // Removes the members of this subgraph from any other subgraphs, a node only belong to one subgraph
// subGraph.nodes.forEach(_id => del(_id));
// Remove the members in the new subgraph if they already belong to another subgraph
subGraph.nodes = makeUniq(subGraph, subGraphs).nodes;
subGraphs.push(subGraph);
@@ -558,7 +520,7 @@ export const addSubGraph = function (_id, list, _title) {
return id;
};
const getPosForId = function (id) {
const getPosForId = function (id: string) {
for (const [i, subGraph] of subGraphs.entries()) {
if (subGraph.id === id) {
return i;
@@ -567,12 +529,15 @@ const getPosForId = function (id) {
return -1;
};
let secCount = -1;
const posCrossRef = [];
const indexNodes2 = function (id, pos) {
const posCrossRef: number[] = [];
const indexNodes2 = function (id: string, pos: number): { result: boolean; count: number } {
const nodes = subGraphs[pos].nodes;
secCount = secCount + 1;
if (secCount > 2000) {
return;
return {
result: false,
count: 0,
};
}
posCrossRef[secCount] = pos;
// Check if match
@@ -608,13 +573,13 @@ const indexNodes2 = function (id, pos) {
};
};
export const getDepthFirstPos = function (pos) {
export const getDepthFirstPos = function (pos: number) {
return posCrossRef[pos];
};
export const indexNodes = function () {
secCount = -1;
if (subGraphs.length > 0) {
indexNodes2('none', subGraphs.length - 1, 0);
indexNodes2('none', subGraphs.length - 1);
}
};
@@ -630,7 +595,7 @@ export const firstGraph = () => {
return false;
};
const destructStartLink = (_str) => {
const destructStartLink = (_str: string): FlowLink => {
let str = _str.trim();
let type = 'arrow_open';
@@ -662,7 +627,7 @@ const destructStartLink = (_str) => {
return { type, stroke };
};
const countChar = (char, str) => {
const countChar = (char: string, str: string) => {
const length = str.length;
let count = 0;
for (let i = 0; i < length; ++i) {
@@ -673,7 +638,7 @@ const countChar = (char, str) => {
return count;
};
const destructEndLink = (_str) => {
const destructEndLink = (_str: string) => {
const str = _str.trim();
let line = str.slice(0, -1);
let type = 'arrow_open';
@@ -713,7 +678,7 @@ const destructEndLink = (_str) => {
stroke = 'invisible';
}
let dots = countChar('.', line);
const dots = countChar('.', line);
if (dots) {
stroke = 'dotted';
@@ -723,7 +688,7 @@ const destructEndLink = (_str) => {
return { type, stroke, length };
};
export const destructLink = (_str, _startStr) => {
export const destructLink = (_str: string, _startStr: string) => {
const info = destructEndLink(_str);
let startInfo;
if (_startStr) {
@@ -757,7 +722,7 @@ export const destructLink = (_str, _startStr) => {
};
// Todo optimizer this by caching existing nodes
const exists = (allSgs, _id) => {
const exists = (allSgs: FlowSubGraph[], _id: string) => {
let res = false;
allSgs.forEach((sg) => {
const pos = sg.nodes.indexOf(_id);
@@ -770,11 +735,9 @@ const exists = (allSgs, _id) => {
/**
* Deletes an id from all subgraphs
*
* @param sg
* @param allSubgraphs
*/
const makeUniq = (sg, allSubgraphs) => {
const res = [];
const makeUniq = (sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) => {
const res: string[] = [];
sg.nodes.forEach((_id, pos) => {
if (!exists(allSubgraphs, _id)) {
res.push(sg.nodes[pos]);
@@ -786,6 +749,7 @@ const makeUniq = (sg, allSubgraphs) => {
export const lex = {
firstGraph,
};
export default {
defaultConfig: () => defaultConfig.flowchart,
setAccTitle,

View File

@@ -1,503 +0,0 @@
/** mermaid
* https://mermaidjs.github.io/
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
/* lexical grammar */
%lex
%x string
%%
\%\%[^\n]* /* do nothing */
["] this.begin("string");
<string>["] this.popState();
<string>[^"]* return "STR";
"style" return 'STYLE';
"default" return 'DEFAULT';
"linkStyle" return 'LINKSTYLE';
"interpolate" return 'INTERPOLATE';
"classDef" return 'CLASSDEF';
"class" return 'CLASS';
"click" return 'CLICK';
"graph" return 'GRAPH';
"subgraph" return 'subgraph';
"end"\b\s* return 'end';
"LR" return 'DIR';
"RL" return 'DIR';
"TB" return 'DIR';
"BT" return 'DIR';
"TD" return 'DIR';
"BR" return 'DIR';
[0-9]+ return 'NUM';
\# return 'BRKT';
":" return 'COLON';
";" return 'SEMI';
"," return 'COMMA';
"*" return 'MULT';
\s*\-\-[x]\s* return 'ARROW_CROSS';
\s*[x]\-\-[x]\s* return 'DOUBLE_ARROW_CROSS';
\s*\-\-\>\s* return 'ARROW_POINT';
\s*\<\-\-\>\s* return 'DOUBLE_ARROW_POINT';
\s*\-\-[o]\s* return 'ARROW_CIRCLE';
\s*[o]\-\-[o]\s* return 'DOUBLE_ARROW_CIRCLE';
\s*\-\-\-\s* return 'ARROW_OPEN';
\s*\-\.\-[x]\s* return 'DOTTED_ARROW_CROSS';
\s*[x]\-\.\-[x]\s* return 'DOUBLE_DOTTED_ARROW_CROSS';
\s*\-\.\-\>\s* return 'DOTTED_ARROW_POINT';
\s*\<\-\.\-\>\s* return 'DOUBLE_DOTTED_ARROW_POINT';
\s*\-\.\-[o]\s* return 'DOTTED_ARROW_CIRCLE';
\s*[o]\-\.\-[o]\s* return 'DOUBLE_DOTTED_ARROW_CIRCLE';
\s*\-\.\-\s* return 'DOTTED_ARROW_OPEN';
\s*.\-[x]\s* return 'DOTTED_ARROW_CROSS';
\s*[x].\-[x]\s* return 'DOUBLE_DOTTED_ARROW_CROSS';
\s*\.\-\>\s* return 'DOTTED_ARROW_POINT';
\s*\<\.\-\>\s* return 'DOUBLE_DOTTED_ARROW_POINT';
\s*\.\-[o]\s* return 'DOTTED_ARROW_CIRCLE';
\s*[o]\.\-[o]\s* return 'DOUBLE_DOTTED_ARROW_CIRCLE';
\s*\.\-\s* return 'DOTTED_ARROW_OPEN';
\s*\=\=[x]\s* return 'THICK_ARROW_CROSS';
\s*[x]\=\=[x]\s* return 'DOUBLE_THICK_ARROW_CROSS';
\s*\=\=\>\s* return 'THICK_ARROW_POINT';
\s*\<\=\=\>\s* return 'DOUBLE_THICK_ARROW_POINT';
\s*\=\=[o]\s* return 'THICK_ARROW_CIRCLE';
\s*[o]\=\=[o]\s* return 'DOUBLE_THICK_ARROW_CIRCLE';
\s*\=\=[\=]\s* return 'THICK_ARROW_OPEN';
\s*\-\-\s* return '--';
\s*\-\.\s* return '-.';
\s*\=\=\s* return '==';
%\s*\<\-\-\s* return 'START_DOUBLE_ARROW_POINT';
%\s*\[x]\-\-\s* return 'START_DOUBLE_ARROW_CROSS';
%\s*\[o]\-\-\s* return 'START_DOUBLE_ARROW_CIRCLE';
%\s*\<\-\.\s* return 'START_DOUBLE_DOTTED_ARROW_POINT';
%\s*\[x]\-\.\s* return 'START_DOUBLE_DOTTED_ARROW_CROSS';
%\s*\[o]\-\.\s* return 'START_DOUBLE_DOTTED_ARROW_CIRCLE';
%\s*\<\=\=\s* return 'START_DOUBLE_THICK_ARROW_POINT';
%\s*\[x]\=\=\s* return 'START_DOUBLE_THICK_ARROW_CROSS';
%\s*\[o]\=\=\s* return 'START_DOUBLE_THICK_ARROW_CIRCLE';
"(-" return '(-';
"-)" return '-)';
\- return 'MINUS';
"." return 'DOT';
\+ return 'PLUS';
\% return 'PCT';
"=" return 'EQUALS';
\= return 'EQUALS';
"<" return 'TAGSTART';
">" return 'TAGEND';
"^" return 'UP';
"v" return 'DOWN';
[A-Za-z]+ return 'ALPHA';
[!"#$%&'*+,-.`?\\_/] return 'PUNCTUATION';
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|
[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|
[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|
[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|
[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|
[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|
[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|
[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|
[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|
[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|
[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|
[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|
[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|
[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|
[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|
[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|
[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|
[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|
[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|
[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|
[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|
[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|
[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|
[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|
[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|
[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|
[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|
[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|
[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|
[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|
[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|
[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|
[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|
[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|
[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|
[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|
[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|
[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|
[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|
[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|
[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|
[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|
[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|
[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|
[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|
[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|
[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|
[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|
[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|
[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|
[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|
[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|
[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|
[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|
[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|
[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|
[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|
[\uFFD2-\uFFD7\uFFDA-\uFFDC]
return 'UNICODE_TEXT';
"|" return 'PIPE';
"(" return 'PS';
")" return 'PE';
"[" return 'SQS';
"]" return 'SQE';
"{" return 'DIAMOND_START'
"}" return 'DIAMOND_STOP'
"\"" return 'QUOTE';
\n+ return 'NEWLINE';
\s return 'SPACE';
<<EOF>> return 'EOF';
/lex
/* operator associations and precedence */
%left '^'
%start mermaidDoc
%% /* language grammar */
mermaidDoc: graphConfig document;
document
: /* empty */
{ $$ = [];}
| document line
{
if($2 !== []){
$1.push($2);
}
$$=$1;}
;
line
: statement
{$$=$1;}
| SEMI
| NEWLINE
| SPACE
| EOF
;
graphConfig
: SPACE graphConfig
| NEWLINE graphConfig
| GRAPH SPACE DIR FirstStmtSeperator
{ yy.setDirection($3);$$ = $3;}
| GRAPH SPACE TAGEND FirstStmtSeperator
{ yy.setDirection("LR");$$ = $3;}
| GRAPH SPACE TAGSTART FirstStmtSeperator
{ yy.setDirection("RL");$$ = $3;}
| GRAPH SPACE UP FirstStmtSeperator
{ yy.setDirection("BT");$$ = $3;}
| GRAPH SPACE DOWN FirstStmtSeperator
{ yy.setDirection("TB");$$ = $3;}
;
ending: endToken ending
| endToken
;
endToken: NEWLINE | SPACE | EOF;
FirstStmtSeperator
: SEMI | NEWLINE | spaceList NEWLINE ;
spaceListNewline
: SPACE spaceListNewline
| NEWLINE spaceListNewline
| NEWLINE
| SPACE
;
spaceList
: SPACE spaceList
| SPACE
;
statement
: verticeStatement separator
{$$=$1}
| styleStatement separator
{$$=[];}
| linkStyleStatement separator
{$$=[];}
| classDefStatement separator
{$$=[];}
| classStatement separator
{$$=[];}
| clickStatement separator
{$$=[];}
| subgraph SPACE alphaNum SQS text SQE separator document end
{$$=yy.addSubGraph($3,$8,$5);}
| subgraph SPACE STR separator document end
{$$=yy.addSubGraph(undefined,$5,$3);}
| subgraph SPACE alphaNum separator document end
{$$=yy.addSubGraph($3,$5,$3);}
| subgraph separator document end
{$$=yy.addSubGraph(undefined,$3,undefined);}
;
separator: NEWLINE | SEMI | EOF ;
verticeStatement:
vertex link vertex
{ yy.addLink($1,$3,$2);$$ = [$1,$3];}
| vertex
{$$ = [$1];}
;
vertex: alphaNum SQS text SQE
{$$ = $1;yy.addVertex($1,$3,'square');}
| alphaNum SQS text SQE spaceList
{$$ = $1;yy.addVertex($1,$3,'square');}
| alphaNum PS PS text PE PE
{$$ = $1;yy.addVertex($1,$4,'circle');}
| alphaNum PS PS text PE PE spaceList
{$$ = $1;yy.addVertex($1,$4,'circle');}
| alphaNum '(-' text '-)'
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
| alphaNum '(-' text '-)' spaceList
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
| alphaNum PS text PE
{$$ = $1;yy.addVertex($1,$3,'round');}
| alphaNum PS text PE spaceList
{$$ = $1;yy.addVertex($1,$3,'round');}
| alphaNum DIAMOND_START text DIAMOND_STOP
{$$ = $1;yy.addVertex($1,$3,'diamond');}
| alphaNum DIAMOND_START text DIAMOND_STOP spaceList
{$$ = $1;yy.addVertex($1,$3,'diamond');}
| alphaNum TAGEND text SQE
{$$ = $1;yy.addVertex($1,$3,'odd');}
| alphaNum TAGEND text SQE spaceList
{$$ = $1;yy.addVertex($1,$3,'odd');}
/* | alphaNum SQS text TAGSTART
{$$ = $1;yy.addVertex($1,$3,'odd_right');}
| alphaNum SQS text TAGSTART spaceList
{$$ = $1;yy.addVertex($1,$3,'odd_right');} */
| alphaNum
{$$ = $1;yy.addVertex($1);}
| alphaNum spaceList
{$$ = $1;yy.addVertex($1);}
;
alphaNum
: alphaNumStatement
{$$=$1;}
| alphaNum alphaNumStatement
{$$=$1+''+$2;}
;
alphaNumStatement
: DIR
{$$=$1;}
| alphaNumToken
{$$=$1;}
| DOWN
{$$='v';}
| MINUS
{$$='-';}
;
link: linkStatement arrowText
{$1.text = $2;$$ = $1;}
| linkStatement TESTSTR SPACE
{$1.text = $2;$$ = $1;}
| linkStatement arrowText SPACE
{$1.text = $2;$$ = $1;}
| linkStatement
{$$ = $1;}
| '--' text ARROW_POINT
{$$ = {"type":"arrow","stroke":"normal","text":$2};}
| 'START_DOUBLE_ARROW_POINT' text ARROW_POINT
{$$ = {"type":"double_arrow_point","stroke":"normal","text":$2};}
| '--' text ARROW_CIRCLE
{$$ = {"type":"arrow_circle","stroke":"normal","text":$2};}
| '--' text ARROW_CROSS
{$$ = {"type":"arrow_cross","stroke":"normal","text":$2};}
| '--' text ARROW_OPEN
{$$ = {"type":"arrow_open","stroke":"normal","text":$2};}
| '-.' text DOTTED_ARROW_POINT
{$$ = {"type":"arrow","stroke":"dotted","text":$2};}
| '-.' text DOTTED_ARROW_CIRCLE
{$$ = {"type":"arrow_circle","stroke":"dotted","text":$2};}
| '-.' text DOTTED_ARROW_CROSS
{$$ = {"type":"arrow_cross","stroke":"dotted","text":$2};}
| '-.' text DOTTED_ARROW_OPEN
{$$ = {"type":"arrow_open","stroke":"dotted","text":$2};}
| '==' text THICK_ARROW_POINT
{$$ = {"type":"arrow","stroke":"thick","text":$2};}
| '==' text THICK_ARROW_CIRCLE
{$$ = {"type":"arrow_circle","stroke":"thick","text":$2};}
| '==' text THICK_ARROW_CROSS
{$$ = {"type":"arrow_cross","stroke":"thick","text":$2};}
| '==' text THICK_ARROW_OPEN
{$$ = {"type":"arrow_open","stroke":"thick","text":$2};}
;
linkStatement: ARROW_POINT
{$$ = {"type":"arrow","stroke":"normal"};}
| DOUBLE_ARROW_POINT
{$$ = {"type":"double_arrow_point","stroke":"normal"};}
| ARROW_CIRCLE
{$$ = {"type":"arrow_circle","stroke":"normal"};}
% | DOUBLE_ARROW_CIRCLE
% {$$ = {"type":"double_arrow_circle","stroke":"normal"};}
| ARROW_CROSS
{$$ = {"type":"arrow_cross","stroke":"normal"};}
% | DOUBLE_ARROW_CROSS
% {$$ = {"type":"double_arrow_cross","stroke":"normal"};}
| ARROW_OPEN
{$$ = {"type":"arrow_open","stroke":"normal"};}
| DOTTED_ARROW_POINT
{$$ = {"type":"arrow","stroke":"dotted"};}
% | DOUEBL_DOTTED_ARROW_POINT
% {$$ = {"type":"doueble_arrow_point","stroke":"dotted"};}
| DOTTED_ARROW_CIRCLE
{$$ = {"type":"arrow_circle","stroke":"dotted"};}
% | DOTTED_ARROW_CIRCLE
% {$$ = {"type":"double_arrow_circle","stroke":"dotted"};}
| DOTTED_ARROW_CROSS
{$$ = {"type":"arrow_cross","stroke":"dotted"};}
% | DOTTED_ARROW_CROSS
% {$$ = {"type":"double_arrow_cross","stroke":"dotted"};}
| DOTTED_ARROW_OPEN
{$$ = {"type":"arrow_open","stroke":"dotted"};}
| THICK_ARROW_POINT
{$$ = {"type":"arrow","stroke":"thick"};}
% | THICK_ARROW_POINT
% {$$ = {"type":"double_arrow_point","stroke":"thick"};}
| THICK_ARROW_CIRCLE
{$$ = {"type":"arrow_circle","stroke":"thick"};}
% | THICK_ARROW_CIRCLE
% {$$ = {"type":"double_arrow_circle","stroke":"thick"};}
| THICK_ARROW_CROSS
{$$ = {"type":"arrow_cross","stroke":"thick"};}
% | THICK_ARROW_CROSS
% {$$ = {"type":"double_arrow_cross","stroke":"thick"};}
| THICK_ARROW_OPEN
{$$ = {"type":"arrow_open","stroke":"thick"};}
;
arrowText:
PIPE text PIPE
{$$ = $2;}
;
text: textToken
{$$=$1;}
| text textToken
{$$=$1+''+$2;}
| STR
{$$=$1;}
;
commentText: commentToken
{$$=$1;}
| commentText commentToken
{$$=$1+''+$2;}
;
keywords
: STYLE | LINKSTYLE | CLASSDEF | CLASS | CLICK | GRAPH | DIR | subgraph | end | DOWN | UP;
textNoTags: textNoTagsToken
{$$=$1;}
| textNoTags textNoTagsToken
{$$=$1+''+$2;}
;
classDefStatement:CLASSDEF SPACE DEFAULT SPACE stylesOpt
{$$ = $1;yy.addClass($3,$5);}
| CLASSDEF SPACE alphaNum SPACE stylesOpt
{$$ = $1;yy.addClass($3,$5);}
;
classStatement:CLASS SPACE alphaNum SPACE alphaNum
{$$ = $1;yy.setClass($3, $5);}
;
clickStatement
: CLICK SPACE alphaNum SPACE alphaNum {$$ = $1;yy.setClickEvent($3, $5, undefined);}
| CLICK SPACE alphaNum SPACE alphaNum SPACE STR {$$ = $1;yy.setClickEvent($3, $5, $7) ;}
| CLICK SPACE alphaNum SPACE STR {$$ = $1;yy.setLink($3, $5, undefined);}
| CLICK SPACE alphaNum SPACE STR SPACE STR {$$ = $1;yy.setLink($3, $5, $7 );}
;
styleStatement:STYLE SPACE alphaNum SPACE stylesOpt
{$$ = $1;yy.addVertex($3,undefined,undefined,$5);}
| STYLE SPACE HEX SPACE stylesOpt
{$$ = $1;yy.updateLink($3,$5);}
;
linkStyleStatement
: LINKSTYLE SPACE DEFAULT SPACE stylesOpt
{$$ = $1;yy.updateLink([$3],$5);}
| LINKSTYLE SPACE numList SPACE stylesOpt
{$$ = $1;yy.updateLink($3,$5);}
| LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt
{$$ = $1;yy.updateLinkInterpolate([$3],$7);yy.updateLink([$3],$9);}
| LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt
{$$ = $1;yy.updateLinkInterpolate($3,$7);yy.updateLink($3,$9);}
| LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum
{$$ = $1;yy.updateLinkInterpolate([$3],$7);}
| LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum
{$$ = $1;yy.updateLinkInterpolate($3,$7);}
;
commentStatement: PCT PCT commentText;
numList: NUM
{$$ = [$1]}
| numList COMMA NUM
{$1.push($3);$$ = $1;}
;
stylesOpt: style
{$$ = [$1]}
| stylesOpt COMMA style
{$1.push($3);$$ = $1;}
;
style: styleComponent
|style styleComponent
{$$ = $1 + $2;}
;
styleComponent: ALPHA | COLON | MINUS | NUM | UNIT | SPACE | HEX | BRKT | DOT | STYLE | PCT ;
/* Token lists */
commentToken : textToken | graphCodeTokens ;
textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFAULT;
textNoTagsToken: alphaNumToken | SPACE | MINUS | keywords ;
alphaNumToken : ALPHA | PUNCTUATION | UNICODE_TEXT | NUM | COLON | COMMA | PLUS | EQUALS | MULT | DOT | BRKT ;
graphCodeTokens: PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAG_START | TAG_END | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI ;
%%

View File

@@ -0,0 +1,53 @@
export interface FlowVertex {
classes: string[];
dir?: string;
domId: string;
haveCallback?: boolean;
id: string;
labelType: 'text';
link?: string;
linkTarget?: string;
props?: any;
styles: string[];
text?: string;
type?: string;
}
export interface FlowText {
text: string;
type: 'text';
}
export interface FlowEdge {
start: string;
end: string;
interpolate?: string;
type?: string;
stroke?: string;
style?: string[];
length?: number;
text: string;
labelType: 'text';
}
export interface FlowClass {
id: string;
styles: string[];
textStyles: string[];
}
export interface FlowSubGraph {
classes: string[];
dir?: string;
id: string;
labelType: string;
nodes: string[];
title: string;
}
export interface FlowLink {
length?: number;
stroke: string;
type: string;
text?: string;
}

View File

@@ -1,24 +1,27 @@
// @ts-ignore - jison doesn't export types
import { parser } from './parser/info.jison';
import { db } from './infoDb.js';
import { parser } from './infoParser.js';
describe('info diagram', () => {
beforeEach(() => {
parser.yy = db;
parser.yy.clear();
});
it('should handle an info definition', () => {
describe('info', () => {
it('should handle an info definition', async () => {
const str = `info`;
parser.parse(str);
expect(db.getInfo()).toBeFalsy();
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should handle an info definition with showInfo', () => {
it('should handle an info definition with showInfo', async () => {
const str = `info showInfo`;
parser.parse(str);
await expect(parser.parse(str)).resolves.not.toThrow();
});
expect(db.getInfo()).toBeTruthy();
it('should throw because of unsupported info grammar', async () => {
const str = `info unsupported`;
await expect(parser.parse(str)).rejects.toThrow(
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
);
});
it('should throw because of unsupported info grammar', async () => {
const str = `info unsupported`;
await expect(parser.parse(str)).rejects.toThrow(
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
);
});
});

View File

@@ -1,23 +1,10 @@
import type { InfoFields, InfoDB } from './infoTypes.js';
import { version } from '../../../package.json';
export const DEFAULT_INFO_DB: InfoFields = {
info: false,
} as const;
export const DEFAULT_INFO_DB: InfoFields = { version } as const;
let info: boolean = DEFAULT_INFO_DB.info;
export const setInfo = (toggle: boolean): void => {
info = toggle;
};
export const getInfo = (): boolean => info;
const clear = (): void => {
info = DEFAULT_INFO_DB.info;
};
export const getVersion = (): string => DEFAULT_INFO_DB.version;
export const db: InfoDB = {
clear,
setInfo,
getInfo,
getVersion,
};

View File

@@ -1,6 +1,5 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore - jison doesn't export types
import parser from './parser/info.jison';
import { parser } from './infoParser.js';
import { db } from './infoDb.js';
import { renderer } from './infoRenderer.js';

View File

@@ -0,0 +1,11 @@
import type { Info } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Info = await parse('info', input);
log.debug(ast);
},
};

View File

@@ -1,11 +1,9 @@
import type { DiagramDB } from '../../diagram-api/types.js';
export interface InfoFields {
info: boolean;
version: string;
}
export interface InfoDB extends DiagramDB {
clear: () => void;
setInfo: (info: boolean) => void;
getInfo: () => boolean;
getVersion: () => string;
}

View File

@@ -1,48 +0,0 @@
/** mermaid
* https://knsv.github.io/mermaid
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
%lex
%options case-insensitive
%{
// Pre-lexer code can go here
%}
%%
"info" return 'info' ;
[\s\n\r]+ return 'NL' ;
[\s]+ return 'space';
"showInfo" return 'showInfo';
<<EOF>> return 'EOF' ;
. return 'TXT' ;
/lex
%start start
%% /* language grammar */
start
// %{ : info document 'EOF' { return yy; } }
: info document 'EOF' { return yy; }
;
document
: /* empty */
| document line
;
line
: statement { }
| 'NL'
;
statement
: showInfo { yy.setInfo(true); }
;
%%

View File

@@ -0,0 +1,59 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { PacketDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import { cleanAndMerge } from '../../utils.js';
import {
clear as commonClear,
getAccDescription,
getAccTitle,
getDiagramTitle,
setAccDescription,
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type { PacketDB, PacketData, PacketWord } from './types.js';
const defaultPacketData: PacketData = {
packet: [],
};
let data: PacketData = structuredClone(defaultPacketData);
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
const getConfig = (): Required<PacketDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_PACKET_CONFIG,
...commonGetConfig().packet,
});
if (config.showBits) {
config.paddingY += 10;
}
return config;
};
const getPacket = (): PacketWord[] => data.packet;
const pushWord = (word: PacketWord) => {
if (word.length > 0) {
data.packet.push(word);
}
};
const clear = () => {
commonClear();
data = structuredClone(defaultPacketData);
};
export const db: PacketDB = {
pushWord,
getPacket,
getConfig,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
};

View File

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

View File

@@ -0,0 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { db } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import { styles } from './styles.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
styles,
};

View File

@@ -0,0 +1,175 @@
import { it, describe, expect } from 'vitest';
import { db } from './db.js';
import { parser } from './parser.js';
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
describe('packet diagrams', () => {
beforeEach(() => {
clear();
});
it('should handle a packet-beta definition', async () => {
const str = `packet-beta`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle diagram with data and title', async () => {
const str = `packet-beta
title Packet diagram
accTitle: Packet accTitle
accDescr: Packet accDescription
0-10: "test"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 10,
"label": "test",
"start": 0,
},
],
]
`);
});
it('should handle single bits', async () => {
const str = `packet-beta
0-10: "test"
11: "single"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 10,
"label": "test",
"start": 0,
},
{
"end": 11,
"label": "single",
"start": 11,
},
],
]
`);
});
it('should split into multiple rows', async () => {
const str = `packet-beta
0-10: "test"
11-90: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 10,
"label": "test",
"start": 0,
},
{
"end": 31,
"label": "multiple",
"start": 11,
},
],
[
{
"end": 63,
"label": "multiple",
"start": 32,
},
],
[
{
"end": 90,
"label": "multiple",
"start": 64,
},
],
]
`);
});
it('should split into multiple rows when cut at exact length', async () => {
const str = `packet-beta
0-16: "test"
17-63: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 16,
"label": "test",
"start": 0,
},
{
"end": 31,
"label": "multiple",
"start": 17,
},
],
[
{
"end": 63,
"label": "multiple",
"start": 32,
},
],
]
`);
});
it('should throw error if numbers are not continuous', async () => {
const str = `packet-beta
0-16: "test"
18-20: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 18 - 20 is not contiguous. It should start from 17."'
);
});
it('should throw error if numbers are not continuous for single packets', async () => {
const str = `packet-beta
0-16: "test"
18: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 18 - 18 is not contiguous. It should start from 17."'
);
});
it('should throw error if numbers are not continuous for single packets - 2', async () => {
const str = `packet-beta
0-16: "test"
17: "good"
19: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 19 - 19 is not contiguous. It should start from 18."'
);
});
it('should throw error if end is less than start', async () => {
const str = `packet-beta
0-16: "test"
25-20: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 25 - 20 is invalid. End must be greater than start."'
);
});
});

View File

@@ -0,0 +1,85 @@
import type { Packet } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import { db } from './db.js';
import type { PacketBlock, PacketWord } from './types.js';
const maxPacketSize = 10_000;
const populate = (ast: Packet) => {
populateCommonDb(ast, db);
let lastByte = -1;
let word: PacketWord = [];
let row = 1;
const { bitsPerRow } = db.getConfig();
for (let { start, end, label } of ast.blocks) {
if (end && end < start) {
throw new Error(`Packet block ${start} - ${end} is invalid. End must be greater than start.`);
}
if (start !== lastByte + 1) {
throw new Error(
`Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${
lastByte + 1
}.`
);
}
lastByte = end ?? start;
log.debug(`Packet block ${start} - ${lastByte} with label ${label}`);
while (word.length <= bitsPerRow + 1 && db.getPacket().length < maxPacketSize) {
const [block, nextBlock] = getNextFittingBlock({ start, end, label }, row, bitsPerRow);
word.push(block);
if (block.end + 1 === row * bitsPerRow) {
db.pushWord(word);
word = [];
row++;
}
if (!nextBlock) {
break;
}
({ start, end, label } = nextBlock);
}
}
db.pushWord(word);
};
const getNextFittingBlock = (
block: PacketBlock,
row: number,
bitsPerRow: number
): [Required<PacketBlock>, PacketBlock | undefined] => {
if (block.end === undefined) {
block.end = block.start;
}
if (block.start > block.end) {
throw new Error(`Block start ${block.start} is greater than block end ${block.end}.`);
}
if (block.end + 1 <= row * bitsPerRow) {
return [block as Required<PacketBlock>, undefined];
}
return [
{
start: block.start,
end: row * bitsPerRow - 1,
label: block.label,
},
{
start: row * bitsPerRow,
end: block.end,
label: block.label,
},
];
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Packet = await parse('packet', input);
log.debug(ast);
populate(ast);
},
};

View File

@@ -0,0 +1,95 @@
import type { Diagram } from '../../Diagram.js';
import type { PacketDiagramConfig } from '../../config.type.js';
import type { DiagramRenderer, DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { PacketDB, PacketWord } from './types.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
const db = diagram.db as PacketDB;
const config = db.getConfig();
const { rowHeight, paddingY, bitWidth, bitsPerRow } = config;
const words = db.getPacket();
const title = db.getDiagramTitle();
const totalRowHeight = rowHeight + paddingY;
const svgHeight = totalRowHeight * (words.length + 1) - (title ? 0 : rowHeight);
const svgWidth = bitWidth * bitsPerRow + 2;
const svg: SVG = selectSvgElement(id);
svg.attr('viewbox', `0 0 ${svgWidth} ${svgHeight}`);
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
for (const [word, packet] of words.entries()) {
drawWord(svg, packet, word, config);
}
svg
.append('text')
.text(title)
.attr('x', svgWidth / 2)
.attr('y', svgHeight - totalRowHeight / 2)
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.attr('class', 'packetTitle');
};
const drawWord = (
svg: SVG,
word: PacketWord,
rowNumber: number,
{ rowHeight, paddingX, paddingY, bitWidth, bitsPerRow, showBits }: Required<PacketDiagramConfig>
) => {
const group: Group = svg.append('g');
const wordY = rowNumber * (rowHeight + paddingY) + paddingY;
for (const block of word) {
const blockX = (block.start % bitsPerRow) * bitWidth + 1;
const width = (block.end - block.start + 1) * bitWidth - paddingX;
// Block rectangle
group
.append('rect')
.attr('x', blockX)
.attr('y', wordY)
.attr('width', width)
.attr('height', rowHeight)
.attr('class', 'packetBlock');
// Block label
group
.append('text')
.attr('x', blockX + width / 2)
.attr('y', wordY + rowHeight / 2)
.attr('class', 'packetLabel')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.text(block.label);
if (!showBits) {
continue;
}
// Start byte count
const isSingleBlock = block.end === block.start;
const bitNumberY = wordY - 2;
group
.append('text')
.attr('x', blockX + (isSingleBlock ? width / 2 : 0))
.attr('y', bitNumberY)
.attr('class', 'packetByte start')
.attr('dominant-baseline', 'auto')
.attr('text-anchor', isSingleBlock ? 'middle' : 'start')
.text(block.start);
// Draw end byte count if it is not the same as start byte count
if (!isSingleBlock) {
group
.append('text')
.attr('x', blockX + width)
.attr('y', bitNumberY)
.attr('class', 'packetByte end')
.attr('dominant-baseline', 'auto')
.attr('text-anchor', 'end')
.text(block.end);
}
}
};
export const renderer: DiagramRenderer = { draw };

View File

@@ -0,0 +1,47 @@
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import { cleanAndMerge } from '../../utils.js';
import type { PacketStyleOptions } from './types.js';
const defaultPacketStyleOptions: PacketStyleOptions = {
byteFontSize: '10px',
startByteColor: 'black',
endByteColor: 'black',
labelColor: 'black',
labelFontSize: '12px',
titleColor: 'black',
titleFontSize: '14px',
blockStrokeColor: 'black',
blockStrokeWidth: '1',
blockFillColor: '#efefef',
};
export const styles: DiagramStylesProvider = ({ packet }: { packet?: PacketStyleOptions } = {}) => {
const options = cleanAndMerge(defaultPacketStyleOptions, packet);
return `
.packetByte {
font-size: ${options.byteFontSize};
}
.packetByte.start {
fill: ${options.startByteColor};
}
.packetByte.end {
fill: ${options.endByteColor};
}
.packetLabel {
fill: ${options.labelColor};
font-size: ${options.labelFontSize};
}
.packetTitle {
fill: ${options.titleColor};
font-size: ${options.titleFontSize};
}
.packetBlock {
stroke: ${options.blockStrokeColor};
stroke-width: ${options.blockStrokeWidth};
fill: ${options.blockFillColor};
}
`;
};
export default styles;

View File

@@ -0,0 +1,29 @@
import type { Packet, RecursiveAstOmit } from '@mermaid-js/parser';
import type { PacketDiagramConfig } from '../../config.type.js';
import type { DiagramDBBase } from '../../diagram-api/types.js';
import type { ArrayElement } from '../../types.js';
export type PacketBlock = RecursiveAstOmit<ArrayElement<Packet['blocks']>>;
export type PacketWord = Required<PacketBlock>[];
export interface PacketDB extends DiagramDBBase<PacketDiagramConfig> {
pushWord: (word: PacketWord) => void;
getPacket: () => PacketWord[];
}
export interface PacketStyleOptions {
byteFontSize?: string;
startByteColor?: string;
endByteColor?: string;
labelColor?: string;
labelFontSize?: string;
blockStrokeColor?: string;
blockStrokeWidth?: string;
blockFillColor?: string;
titleColor?: string;
titleFontSize?: string;
}
export interface PacketData {
packet: PacketWord[];
}

View File

@@ -1,74 +0,0 @@
/** mermaid
* https://knsv.github.io/mermaid
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
%lex
%options case-insensitive
%x string
%x title
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('');*/ }
[\n\r]+ return 'NEWLINE';
\%\%[^\n]* /* do nothing */
[\s]+ /* ignore */
title { this.begin("title");return 'title'; }
<title>(?!\n|;|#)*[^\n]* { this.popState(); return "title_value"; }
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
["] { this.begin("string"); }
<string>["] { this.popState(); }
<string>[^"]* { return "txt"; }
"pie" return 'PIE';
"showData" return 'showData';
":"[\s]*[\d]+(?:\.[\d]+)? return "value";
<<EOF>> return 'EOF';
/lex
%start start
%% /* language grammar */
start
: eol start
| PIE document
| PIE showData document {yy.setShowData(true);}
;
document
: /* empty */
| document line
;
line
: statement eol { $$ = $1 }
;
statement
:
| txt value { yy.addSection($1,yy.cleanupValue($2)); }
| title title_value { $$=$2.trim();yy.setDiagramTitle($$); }
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
;
eol
: NEWLINE
| ';'
| EOF
;
%%

View File

@@ -1,5 +1,4 @@
// @ts-ignore: JISON doesn't support types
import { parser } from './parser/pie.jison';
import { parser } from './pieParser.js';
import { DEFAULT_PIE_DB, db } from './pieDb.js';
import { setConfig } from '../../diagram-api/diagramAPI.js';
@@ -8,17 +7,11 @@ setConfig({
});
describe('pie', () => {
beforeAll(() => {
parser.yy = db;
});
beforeEach(() => {
parser.yy.clear();
});
beforeEach(() => db.clear());
describe('parse', () => {
it('should handle very simple pie', () => {
parser.parse(`pie
it('should handle very simple pie', async () => {
await parser.parse(`pie
"ash": 100
`);
@@ -26,8 +19,8 @@ describe('pie', () => {
expect(sections['ash']).toBe(100);
});
it('should handle simple pie', () => {
parser.parse(`pie
it('should handle simple pie', async () => {
await parser.parse(`pie
"ash" : 60
"bat" : 40
`);
@@ -37,8 +30,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with showData', () => {
parser.parse(`pie showData
it('should handle simple pie with showData', async () => {
await parser.parse(`pie showData
"ash" : 60
"bat" : 40
`);
@@ -50,8 +43,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with comments', () => {
parser.parse(`pie
it('should handle simple pie with comments', async () => {
await parser.parse(`pie
%% comments
"ash" : 60
"bat" : 40
@@ -62,8 +55,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a title', () => {
parser.parse(`pie title a 60/40 pie
it('should handle simple pie with a title', async () => {
await parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
@@ -75,8 +68,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc title (accTitle)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with an acc title (accTitle)', async () => {
await parser.parse(`pie title a neat chart
accTitle: a neat acc title
"ash" : 60
"bat" : 40
@@ -91,8 +84,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with an acc description (accDescr)', async () => {
await parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
@@ -107,8 +100,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a multiline acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with a multiline acc description (accDescr)', async () => {
await parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
@@ -126,8 +119,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with positive decimal', () => {
parser.parse(`pie
it('should handle simple pie with positive decimal', async () => {
await parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
@@ -138,12 +131,12 @@ describe('pie', () => {
});
it('should handle simple pie with negative decimal', () => {
expect(() => {
parser.parse(`pie
expect(async () => {
await parser.parse(`pie
"ash" : -60.67
"bat" : 40.12
`);
}).toThrowError();
}).rejects.toThrowError();
});
});

View File

@@ -1,6 +1,4 @@
import { log } from '../../logger.js';
import { getConfig as commonGetConfig } from '../../diagram-api/diagramAPI.js';
import { sanitizeText } from '../common/common.js';
import {
setAccTitle,
getAccTitle,
@@ -10,7 +8,7 @@ import {
setAccDescription,
clear as commonClear,
} from '../common/commonDb.js';
import type { PieFields, PieDB, Sections } from './pieTypes.js';
import type { PieFields, PieDB, Sections, D3Section } from './pieTypes.js';
import type { RequiredDeep } from 'type-fest';
import type { PieDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
@@ -35,8 +33,7 @@ const clear = (): void => {
commonClear();
};
const addSection = (label: string, value: number): void => {
label = sanitizeText(label, commonGetConfig());
const addSection = ({ label, value }: D3Section): void => {
if (sections[label] === undefined) {
sections[label] = value;
log.debug(`added new section: ${label}, with value: ${value}`);
@@ -45,13 +42,6 @@ const addSection = (label: string, value: number): void => {
const getSections = (): Sections => sections;
const cleanupValue = (value: string): number => {
if (value.substring(0, 1) === ':') {
value = value.substring(1).trim();
}
return Number(value.trim());
};
const setShowData = (toggle: boolean): void => {
showData = toggle;
};
@@ -71,7 +61,6 @@ export const db: PieDB = {
addSection,
getSections,
cleanupValue,
setShowData,
getShowData,
};

View File

@@ -1,6 +1,5 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/pie.jison';
import { parser } from './pieParser.js';
import { db } from './pieDb.js';
import styles from './pieStyles.js';
import { renderer } from './pieRenderer.js';

View File

@@ -0,0 +1,21 @@
import type { Pie } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type { PieDB } from './pieTypes.js';
import { db } from './pieDb.js';
const populateDb = (ast: Pie, db: PieDB) => {
populateCommonDb(ast, db);
db.setShowData(ast.showData);
ast.sections.map(db.addSection);
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Pie = await parse('pie', input);
log.debug(ast);
populateDb(ast, db);
},
};

View File

@@ -5,24 +5,24 @@ import { configureSvgSize } from '../../setupGraphViewbox.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { cleanAndMerge, parseFontSize } from '../../utils.js';
import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
import type { D3Sections, PieDB, Sections } from './pieTypes.js';
import type { D3Section, PieDB, Sections } from './pieTypes.js';
import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Section>[] => {
// Compute the position of each group on the pie:
const pieData: D3Sections[] = Object.entries(sections)
.map((element: [string, number]): D3Sections => {
const pieData: D3Section[] = Object.entries(sections)
.map((element: [string, number]): D3Section => {
return {
label: element[0],
value: element[1],
};
})
.sort((a: D3Sections, b: D3Sections): number => {
.sort((a: D3Section, b: D3Section): number => {
return b.value - a.value;
});
const pie: d3.Pie<unknown, D3Sections> = d3pie<D3Sections>().value(
(d3Section: D3Sections): number => d3Section.value
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value(
(d3Section: D3Section): number => d3Section.value
);
return pie(pieData);
};
@@ -47,7 +47,6 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
const pieWidth: number = height;
const svg: SVG = selectSvgElement(id);
const group: Group = svg.append('g');
const sections: Sections = db.getSections();
group.attr('transform', 'translate(' + pieWidth / 2 + ',' + height / 2 + ')');
const { themeVariables } = globalConfig;
@@ -57,13 +56,11 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
const textPosition: number = pieConfig.textPosition;
const radius: number = Math.min(pieWidth, height) / 2 - MARGIN;
// Shape helper to build arcs:
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
>()
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Section>> = arc<d3.PieArcDatum<D3Section>>()
.innerRadius(0)
.outerRadius(radius);
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Section>> = arc<
d3.PieArcDatum<D3Section>
>()
.innerRadius(radius * textPosition)
.outerRadius(radius * textPosition);
@@ -75,7 +72,8 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle');
const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
const sections: Sections = db.getSections();
const arcs: d3.PieArcDatum<D3Section>[] = createPieArcs(sections);
const myGeneratedColors = [
themeVariables.pie1,
@@ -101,7 +99,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', (datum: d3.PieArcDatum<D3Sections>) => {
.attr('fill', (datum: d3.PieArcDatum<D3Section>) => {
return color(datum.data.label);
})
.attr('class', 'pieCircle');
@@ -117,10 +115,10 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.data(arcs)
.enter()
.append('text')
.text((datum: d3.PieArcDatum<D3Sections>): string => {
.text((datum: d3.PieArcDatum<D3Section>): string => {
return ((datum.data.value / sum) * 100).toFixed(0) + '%';
})
.attr('transform', (datum: d3.PieArcDatum<D3Sections>): string => {
.attr('transform', (datum: d3.PieArcDatum<D3Section>): string => {
return 'translate(' + labelArcGenerator.centroid(datum) + ')';
})
.style('text-anchor', 'middle')
@@ -160,7 +158,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.append('text')
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
.text((datum: d3.PieArcDatum<D3Sections>): string => {
.text((datum: d3.PieArcDatum<D3Section>): string => {
const { label, value } = datum.data;
if (db.getShowData()) {
return `${label} [${value}]`;

View File

@@ -36,7 +36,7 @@ export interface PieStyleOptions {
export type Sections = Record<string, number>;
export interface D3Sections {
export interface D3Section {
label: string;
value: number;
}
@@ -55,9 +55,8 @@ export interface PieDB extends DiagramDB {
getAccDescription: () => string;
// diagram db
addSection: (label: string, value: number) => void;
addSection: ({ label, value }: D3Section) => void;
getSections: () => Sections;
cleanupValue: (value: string) => number;
setShowData: (toggle: boolean) => void;
getShowData: () => boolean;
}

View File

@@ -1,12 +1,12 @@
import { vi } from 'vitest';
import { setSiteConfig } from '../../diagram-api/diagramAPI.js';
import mermaidAPI from '../../mermaidAPI.js';
import { Diagram, getDiagramFromText } from '../../Diagram.js';
import { Diagram } from '../../Diagram.js';
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
beforeAll(async () => {
// Is required to load the sequence diagram
await getDiagramFromText('sequenceDiagram');
await Diagram.fromText('sequenceDiagram');
});
/**
@@ -95,8 +95,8 @@ function addConf(conf, key, value) {
let diagram;
describe('more than one sequence diagram', () => {
it('should not have duplicated messages', () => {
const diagram1 = new Diagram(`
it('should not have duplicated messages', async () => {
const diagram1 = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Bob-->Alice: I am good thanks!`);
@@ -120,7 +120,7 @@ describe('more than one sequence diagram', () => {
},
]
`);
const diagram2 = new Diagram(`
const diagram2 = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Bob-->Alice: I am good thanks!`);
@@ -147,7 +147,7 @@ describe('more than one sequence diagram', () => {
`);
// Add John actor
const diagram3 = new Diagram(`
const diagram3 = await Diagram.fromText(`
sequenceDiagram
Alice->John:Hello John, how are you?
John-->Alice: I am good thanks!`);
@@ -176,8 +176,8 @@ describe('more than one sequence diagram', () => {
});
describe('when parsing a sequenceDiagram', function () {
beforeEach(function () {
diagram = new Diagram(`
beforeEach(async function () {
diagram = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks
@@ -1613,7 +1613,7 @@ describe('when rendering a sequenceDiagram APA', function () {
setSiteConfig({ logLevel: 5, sequence: conf });
});
let conf;
beforeEach(function () {
beforeEach(async function () {
mermaidAPI.reset();
// });
@@ -1632,7 +1632,7 @@ describe('when rendering a sequenceDiagram APA', function () {
mirrorActors: false,
};
setSiteConfig({ logLevel: 5, sequence: conf });
diagram = new Diagram(`
diagram = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks

View File

@@ -1,6 +1,6 @@
import { defineConfig, MarkdownOptions } from 'vitepress';
import { version } from '../../../package.json';
import MermaidExample from './mermaid-markdown-all.js';
import { defineConfig, MarkdownOptions } from 'vitepress';
const allMarkdownTransformers: MarkdownOptions = {
// the shiki theme to highlight code blocks
@@ -151,9 +151,10 @@ function sidebarSyntax() {
{ text: 'Mindmaps', link: '/syntax/mindmap' },
{ text: 'Timeline', link: '/syntax/timeline' },
{ text: 'Zenuml', link: '/syntax/zenuml' },
{ text: 'Sankey', link: '/syntax/sankey' },
{ text: 'Sankey 🔥', link: '/syntax/sankey' },
{ text: 'XYChart 🔥', link: '/syntax/xyChart' },
{ text: 'Block Diagram 🔥', link: '/syntax/block' },
{ text: 'Packet 🔥', link: '/syntax/packet' },
{ text: 'Other Examples', link: '/syntax/examples' },
],
},

View File

@@ -328,15 +328,17 @@ module.exports = (options) ->
## Advanced usage
**Syntax validation without rendering (Work in Progress)**
### Syntax validation without rendering
The **mermaid.parse(txt)** function validates graph definitions without rendering a graph. **[This function is still a work in progress](https://github.com/mermaid-js/mermaid/issues/1066), find alternatives below.**
The `mermaid.parse(text, parseOptions)` function validates graph definitions without rendering a graph.
The function **mermaid.parse(txt)**, takes a text string as an argument and returns true if the definition follows mermaid's syntax and
false if it does not. The parseError function will be called when the parse function returns false.
The function `mermaid.parse(text, parseOptions)`, takes a text string as an argument and returns `{ diagramType: string }` if the definition follows mermaid's syntax.
When the parser encounters invalid syntax the **mermaid.parseError** function is called. It is possible to override this
function in order to handle the error in an application-specific way.
If the definition is invalid, the function returns `false` if `parseOptions.suppressErrors` is set to `true`. Otherwise, it throws an error.
The parseError function will be called when the parse function throws an error. It will not be called if `parseOptions.suppressErrors` is set to `true`.
It is possible to override this function in order to handle the error in an application-specific way.
The code-example below in meta code illustrates how this could work:
@@ -356,26 +358,10 @@ const textFieldUpdated = async function () {
bindEventHandler('change', 'code', textFieldUpdated);
```
**Alternative to mermaid.parse():**
One effective and more future-proof method of validating your graph definitions, is to paste and render them via the [Mermaid Live Editor](https://mermaid.live/). This will ensure that your code is compliant with the syntax of Mermaid's most recent version.
## Configuration
Mermaid takes a number of options which lets you tweak the rendering of the diagrams. Currently there are three ways of
setting the options in mermaid.
1. Instantiation of the configuration using the initialize call
2. _Using the global mermaid object_ - **Deprecated**
3. _using the global mermaid_config object_ - **Deprecated**
4. Instantiation of the configuration using the **mermaid.init** call- **Deprecated**
The list above has two ways too many of doing this. Three are deprecated and will eventually be removed. The list of
configuration objects are described [in the mermaidAPI documentation](./setup/README.md).
## Using the `mermaidAPI.initialize`/`mermaid.initialize` call
The future proof way of setting the configuration is by using the initialization call to mermaid or mermaidAPI depending
on what kind of integration you use.
You can pass the required configuration to the `mermaid.initialize` call. This is the preferred way of configuring mermaid.
The list of configuration objects are described [in the mermaidAPI documentation](./setup/README.md).
```html
<script type="module">
@@ -407,37 +393,6 @@ mermaid.startOnLoad = true;
This way of setting the configuration is deprecated. Instead the preferred way is to use the initialize method. This functionality is only kept for backwards compatibility.
```
## Using the mermaid_config
It is possible to set some configuration via the mermaid object. The two parameters that are supported using this
approach are:
- mermaid_config.startOnLoad
- mermaid_config.htmlLabels
```javascript
mermaid_config.startOnLoad = true;
```
```warning
This way of setting the configuration is deprecated. Instead the preferred way is to use the initialize method. This functionality is only kept for backwards compatibility.
```
## Using the mermaid.init call
To set some configuration via the mermaid object. The two parameters that are supported using this approach are:
- mermaid_config.startOnLoad
- mermaid_config.htmlLabels
```javascript
mermaid_config.startOnLoad = true;
```
```warning
This way of setting the configuration is deprecated. Instead the preferred way is to use the initialize method. This functionality is only kept for backwards compatibility.
```
<!---
cspell:locale en,en-gb
cspell:ignore pumbaa

View File

@@ -239,18 +239,18 @@ In this example, the `mermaidAPI` is being called through the `CDN`:
<body>
Here is one mermaid diagram:
<pre class="mermaid">
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server1]
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server1]
B --> D[Server2]
</pre>
And here is another:
<pre class="mermaid">
graph TD
graph TD
A[Client] -->|tcp_123| B
B(Load Balancer)
B -->|tcp_456| C[Server1]
B(Load Balancer)
B -->|tcp_456| C[Server1]
B -->|tcp_456| D[Server2]
</pre>
@@ -271,15 +271,15 @@ In this example, `mermaid.js` is referenced in `src` as a separate JavaScript fi
</head>
<body>
<pre class="mermaid">
graph LR
A --- B
B-->C[fa:fa-ban forbidden]
graph LR
A --- B
B-->C[fa:fa-ban forbidden]
B-->D(fa:fa-spinner);
</pre>
<pre class="mermaid">
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server1]
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server1]
B --> D[Server2]
</pre>
<script type="module">

View File

@@ -0,0 +1,101 @@
# Packet Diagram (v<MERMAID_RELEASE_VERSION>+)
## Introduction
A packet diagram is a visual representation used to illustrate the structure and contents of a network packet. Network packets are the fundamental units of data transferred over a network.
## Usage
This diagram type is particularly useful for network engineers, educators, and students who require a clear and concise way to represent the structure of network packets.
## Syntax
```md
packet-beta
start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks
... More Fields ...
```
## Examples
```mermaid-example
---
title: "TCP Packet"
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-255: "Data (variable length)"
```
```mermaid-example
packet-beta
title UDP Packet
0-15: "Source Port"
16-31: "Destination Port"
32-47: "Length"
48-63: "Checksum"
64-95: "Data (variable length)"
```
## Details of Syntax
- **Ranges**: Each line after the title represents a different field in the packet. The range (e.g., `0-15`) indicates the bit positions in the packet.
- **Field Description**: A brief description of what the field represents, enclosed in quotes.
## Configuration
Please refer to the [configuration](/config/schema-docs/config-defs-packet-diagram-config.html) guide for details.
<!--
Theme variables are not currently working due to a mermaid bug. The passed values are not being propagated into styles function.
## Theme Variables
| Property | Description | Default Value |
| ---------------- | -------------------------- | ------------- |
| byteFontSize | Font size of the bytes | '10px' |
| startByteColor | Color of the starting byte | 'black' |
| endByteColor | Color of the ending byte | 'black' |
| labelColor | Color of the labels | 'black' |
| labelFontSize | Font size of the labels | '12px' |
| titleColor | Color of the title | 'black' |
| titleFontSize | Font size of the title | '14px' |
| blockStrokeColor | Color of the block stroke | 'black' |
| blockStrokeWidth | Width of the block stroke | '1' |
| blockFillColor | Fill color of the block | '#efefef' |
## Example on config and theme
```mermaid-example
---
config:
packet:
showBits: true
themeVariables:
packet:
startByteColor: red
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
```
-->

View File

@@ -104,7 +104,6 @@ describe('when using mermaid and ', () => {
parse: (_text) => {
return;
},
parser: { yy: {} },
},
styles: () => {
// do nothing

View File

@@ -6,7 +6,7 @@ import { dedent } from 'ts-dedent';
import type { MermaidConfig } from './config.type.js';
import { log } from './logger.js';
import utils from './utils.js';
import type { ParseOptions, RenderResult } from './mermaidAPI.js';
import type { ParseOptions, ParseResult, RenderResult } from './mermaidAPI.js';
import { mermaidAPI } from './mermaidAPI.js';
import { registerLazyLoadedDiagrams, detectType } from './diagram-api/detectType.js';
import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js';
@@ -15,6 +15,7 @@ import { isDetailedError } from './utils.js';
import type { DetailedError } from './utils.js';
import type { ExternalDiagramDefinition } from './diagram-api/types.js';
import type { UnknownDiagramError } from './errors.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
export type {
MermaidConfig,
@@ -23,6 +24,7 @@ export type {
ParseErrorFunction,
RenderResult,
ParseOptions,
ParseResult,
UnknownDiagramError,
};
@@ -243,6 +245,7 @@ const registerExternalDiagrams = async (
lazyLoad?: boolean;
} = {}
) => {
addDiagrams();
registerLazyLoadedDiagrams(...diagrams);
if (lazyLoad === false) {
await loadRegisteredDiagrams();
@@ -311,11 +314,23 @@ const executeQueue = async () => {
/**
* Parse the text and validate the syntax.
* @param text - The mermaid diagram definition.
* @param parseOptions - Options for parsing.
* @returns true if the diagram is valid, false otherwise if parseOptions.suppressErrors is true.
* @throws Error if the diagram is invalid and parseOptions.suppressErrors is false.
* @param parseOptions - Options for parsing. @see {@link ParseOptions}
* @returns If valid, {@link ParseResult} otherwise `false` if parseOptions.suppressErrors is `true`.
* @throws Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
*
* @example
* ```js
* console.log(await mermaid.parse('flowchart \n a --> b'));
* // { diagramType: 'flowchart-v2' }
* console.log(await mermaid.parse('wrong \n a --> b', { suppressErrors: true }));
* // false
* console.log(await mermaid.parse('wrong \n a --> b', { suppressErrors: false }));
* // throws Error
* console.log(await mermaid.parse('wrong \n a --> b'));
* // throws Error
* ```
*/
const parse = async (text: string, parseOptions?: ParseOptions): Promise<boolean | void> => {
const parse: typeof mermaidAPI.parse = async (text, parseOptions) => {
return new Promise((resolve, reject) => {
// This promise will resolve when the render call is done.
// It will be queued first and will be executed when it is first in line
@@ -364,7 +379,7 @@ const parse = async (text: string, parseOptions?: ParseOptions): Promise<boolean
* element will be removed when rendering is completed.
* @returns Returns the SVG Definition and BindFunctions.
*/
const render = (id: string, text: string, container?: Element): Promise<RenderResult> => {
const render: typeof mermaidAPI.render = (id, text, container) => {
return new Promise((resolve, reject) => {
// This promise will resolve when the mermaidAPI.render call is done.
// It will be queued first and will be executed when it is first in line

View File

@@ -1,5 +1,4 @@
'use strict';
import { vi } from 'vitest';
import { vi, it, expect, describe, beforeEach } from 'vitest';
// -------------------------------------
// Mocks and mocking
@@ -27,26 +26,26 @@ vi.mock('./diagrams/git/gitGraphRenderer.js');
vi.mock('./diagrams/gantt/ganttRenderer.js');
vi.mock('./diagrams/user-journey/journeyRenderer.js');
vi.mock('./diagrams/pie/pieRenderer.js');
vi.mock('./diagrams/packet/renderer.js');
vi.mock('./diagrams/xychart/xychartRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.js');
vi.mock('./diagrams/sequence/sequenceRenderer.js');
vi.mock('./diagrams/state/stateRenderer-v2.js');
// -------------------------------------
import mermaid from './mermaid.js';
import assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js';
import mermaidAPI, { removeExistingElements } from './mermaidAPI.js';
import {
createCssStyles,
createUserStyles,
import mermaid from './mermaid.js';
import mermaidAPI, {
appendDivSvgG,
cleanUpSvgCode,
createCssStyles,
createUserStyles,
putIntoIFrame,
removeExistingElements,
} from './mermaidAPI.js';
import assignWithDepth from './assignWithDepth.js';
// --------------
// Mocks
// To mock a module, first define a mock for it, then (if used explicitly in the tests) import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested)
@@ -56,6 +55,7 @@ vi.mock('./styles.js', () => {
default: vi.fn().mockReturnValue(' .userStyle { font-weight:bold; }'),
};
});
import getStyles from './styles.js';
vi.mock('stylis', () => {
@@ -65,6 +65,7 @@ vi.mock('stylis', () => {
serialize: vi.fn().mockReturnValue('stylis serialized css'),
};
});
import { compile, serialize } from 'stylis';
import { decodeEntities, encodeEntities } from './utils.js';
import { Diagram } from './Diagram.js';
@@ -564,7 +565,7 @@ describe('mermaidAPI', () => {
const config = {
logLevel: 0,
securityLevel: 'loose',
};
} as const;
mermaidAPI.initialize(config);
mermaidAPI.setConfig({ securityLevel: 'strict', logLevel: 1 });
expect(mermaidAPI.getConfig().logLevel).toBe(1);
@@ -688,14 +689,21 @@ describe('mermaidAPI', () => {
).resolves.toBe(false);
});
it('resolves for valid definition', async () => {
await expect(
mermaidAPI.parse('graph TD;A--x|text including URL space|B;')
).resolves.toBeTruthy();
await expect(mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).resolves
.toMatchInlineSnapshot(`
{
"diagramType": "flowchart-v2",
}
`);
});
it('returns true for valid definition with silent option', async () => {
await expect(
mermaidAPI.parse('graph TD;A--x|text including URL space|B;', { suppressErrors: true })
).resolves.toBe(true);
).resolves.toMatchInlineSnapshot(`
{
"diagramType": "flowchart-v2",
}
`);
});
});
@@ -718,6 +726,8 @@ describe('mermaidAPI', () => {
{ textDiagramType: 'gantt', expectedType: 'gantt' },
{ textDiagramType: 'journey', expectedType: 'journey' },
{ textDiagramType: 'pie', expectedType: 'pie' },
{ textDiagramType: 'packet-beta', expectedType: 'packet' },
{ textDiagramType: 'xychart-beta', expectedType: 'xychart' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
@@ -737,7 +747,8 @@ describe('mermaidAPI', () => {
it('should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
await mermaidAPI.render(id, diagramText);
const result = await mermaidAPI.render(id, diagramText);
expect(result.diagramType).toBe(expectedDiagramType);
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
expect.anything(),
expectedDiagramType

View File

@@ -17,7 +17,7 @@ import { compile, serialize, stringify } from 'stylis';
import { version } from '../package.json';
import * as configApi from './config.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import { Diagram, getDiagramFromText as getDiagramFromTextInternal } from './Diagram.js';
import { Diagram } from './Diagram.js';
import errorRenderer from './diagrams/error/errorRenderer.js';
import { attachFunctions } from './interactionDb.js';
import { log, setLogLevel } from './logger.js';
@@ -57,9 +57,19 @@ const DOMPURIFY_TAGS = ['foreignobject'];
const DOMPURIFY_ATTR = ['dominant-baseline'];
export interface ParseOptions {
/**
* If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
* The `parseError` function will not be called.
*/
suppressErrors?: boolean;
}
export interface ParseResult {
/**
* The diagram type, e.g. 'flowchart', 'sequence', etc.
*/
diagramType: string;
}
// This makes it clear that we're working with a d3 selected element of some kind, even though it's hard to specify the exact type.
export type D3Element = any;
@@ -68,6 +78,10 @@ export interface RenderResult {
* The svg code for the rendered graph.
*/
svg: string;
/**
* The diagram type, e.g. 'flowchart', 'sequence', etc.
*/
diagramType: string;
/**
* Bind function to be called after the svg has been inserted into the DOM.
* This is necessary for adding event listeners to the elements in the svg.
@@ -90,29 +104,29 @@ function processAndSetConfigs(text: string) {
/**
* Parse the text and validate the syntax.
* @param text - The mermaid diagram definition.
* @param parseOptions - Options for parsing.
* @returns true if the diagram is valid, false otherwise if parseOptions.suppressErrors is true.
* @throws Error if the diagram is invalid and parseOptions.suppressErrors is false.
* @param parseOptions - Options for parsing. @see {@link ParseOptions}
* @returns An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
* @throws Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
*/
async function parse(text: string, parseOptions?: ParseOptions): Promise<boolean> {
async function parse(
text: string,
parseOptions: ParseOptions & { suppressErrors: true }
): Promise<ParseResult | false>;
async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult>;
async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult | false> {
addDiagrams();
text = processAndSetConfigs(text).code;
try {
await getDiagramFromText(text);
const { code } = processAndSetConfigs(text);
const diagram = await getDiagramFromText(code);
return { diagramType: diagram.type };
} catch (error) {
if (parseOptions?.suppressErrors) {
return false;
}
throw error;
}
return true;
}
// append !important; to each cssClass followed by a final !important, all enclosed in { }
//
/**
* Create a CSS style that starts with the given class name, then the element,
* with an enclosing block that has each of the cssClasses followed by !important;
@@ -408,9 +422,9 @@ const render = async function (
let parseEncounteredException;
try {
diag = await getDiagramFromText(text, { title: processed.title });
diag = await Diagram.fromText(text, { title: processed.title });
} catch (error) {
diag = new Diagram('error');
diag = await Diagram.fromText('error');
parseEncounteredException = error;
}
@@ -482,6 +496,7 @@ const render = async function (
}
return {
diagramType,
svg: svgCode,
bindFunctions: diag.db.bindFunctions,
};
@@ -520,7 +535,7 @@ function initialize(options: MermaidConfig = {}) {
const getDiagramFromText = (text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) => {
const { code } = preprocessDiagram(text);
return getDiagramFromTextInternal(code, metadata);
return Diagram.fromText(code, metadata);
};
/**

View File

@@ -7,7 +7,7 @@
# - `scripts/create-types-from-json-schema.mjs`
# Used to generate the `src/config.type.ts` TypeScript types for MermaidConfig
# with the `json-schema-to-typescript` NPM package.
# - `.vite/jsonSchemaPlugin.ts`
# - `.build/jsonSchema.ts`
# Used to generate the default values from the `default` keys in this
# JSON Schema using the `ajv` NPM package.
# Non-JSON values, like functions or `undefined`, still need to be manually
@@ -21,8 +21,9 @@
# - Use `meta:enum` to document enum values (from jsonschema2md)
# - Use `tsType` to override the TypeScript type (from json-schema-to-typescript)
# - If adding a new object to `MermaidConfig` (e.g. a new diagram type),
# you may need to add it to `.vite/jsonSchemaPlugin.ts` and `src/docs.mts`
# to get the docs/default values to generate properly.
# you may need to add it to `.build/jsonSchema.ts`, `src/docs.mts`
# and `scripts/create-types-from-json-schema.mjs`
# to get the default values/docs/types to generate properly.
$id: https://mermaid-js.github.io/schemas/config.schema.json
$schema: https://json-schema.org/draft/2019-09/schema
title: Mermaid Config
@@ -49,6 +50,7 @@ required:
- gitGraph
- c4
- sankey
- packet
- block
properties:
theme:
@@ -65,8 +67,6 @@ properties:
meta:enum:
'null': Can be set to disable any pre-defined mermaid theme
default: 'default'
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "default" | "forest" | "dark" | "neutral" | "null"'
themeVariables:
tsType: any
themeCSS:
@@ -123,8 +123,6 @@ properties:
error: Equivalent to 4
fatal: Equivalent to 5 (default)
default: 5
# Allow any number or string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'number | string | 0 | 2 | 1 | "trace" | "debug" | "info" | "warn" | "error" | "fatal" | 3 | 4 | 5 | undefined'
securityLevel:
description: Level of trust for parsed diagram
type: string
@@ -142,8 +140,6 @@ properties:
This prevent any JavaScript from running in the context.
This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, or links to other tabs or targets, etc.
default: strict
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "strict" | "loose" | "antiscript" | "sandbox" | undefined'
startOnLoad:
description: Dictates whether mermaid starts on Page load
type: boolean
@@ -225,6 +221,8 @@ properties:
$ref: '#/$defs/C4DiagramConfig'
sankey:
$ref: '#/$defs/SankeyDiagramConfig'
packet:
$ref: '#/$defs/PacketDiagramConfig'
block:
$ref: '#/$defs/BlockDiagramConfig'
dompurifyConfig:
@@ -1170,8 +1168,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
LR: Left-Right
RL: Right to Left
default: TB
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "TB" | "BT" | "LR" | "RL"'
minEntityWidth:
description: The minimum width of an entity box. Expressed in pixels.
type: integer
@@ -1284,8 +1280,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
dagre-d3: The [dagre-d3-es](https://www.npmjs.com/package/dagre-d3-es) library.
dagre-wrapper: wrapper for dagre implemented in mermaid
elk: Layout using [elkjs](https://github.com/kieler/elkjs)
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "dagre-d3" | "dagre-wrapper" | "elk"'
ClassDiagramConfig:
title: Class Diagram Config
@@ -1401,8 +1395,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
- center
- right
default: center
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "left" | "center" | "right"'
bottomMarginAdj:
description: |
Prolongs the edge of the diagram downwards.
@@ -1527,8 +1519,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
- center
- right
default: center
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "left" | "center" | "right"'
bottomMarginAdj:
description: |
Prolongs the edge of the diagram downwards.
@@ -1692,13 +1682,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
meta:enum:
compact: Enables displaying multiple tasks on the same row.
default: ''
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "compact"'
weekday:
description: |
On which day a week-based interval should start
type: string
tsType: '"monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"'
enum:
- monday
- tuesday
@@ -1840,8 +1827,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: string
enum: ['left', 'center', 'right']
default: 'center'
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "left" | "center" | "right"'
messageFontSize:
description: This sets the font size of actor messages
@@ -1944,8 +1929,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: string
enum: ['basis', 'linear', 'cardinal']
default: 'basis'
# Allow any string for typescript backwards compatibility (fix in Mermaid v10)
tsType: 'string | "basis" | "linear" | "cardinal"'
padding:
description: |
Represents the padding between the labels and the shape
@@ -2022,10 +2005,12 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
$ref: '#/$defs/SankeyNodeAlignment'
default: justify
useMaxWidth:
type: boolean
default: false
showValues:
description: |
Toggle to display or hide values along with title.
type: boolean
default: true
prefix:
description: |
@@ -2038,6 +2023,43 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: string
default: ''
PacketDiagramConfig:
title: Packet Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
description: The object containing configurations specific for packet diagrams.
type: object
unevaluatedProperties: false
properties:
rowHeight:
description: The height of each row in the packet diagram.
type: number
minimum: 1
default: 32
bitWidth:
description: The width of each bit in the packet diagram.
type: number
minimum: 1
default: 32
bitsPerRow:
description: The number of bits to display per row.
type: number
minimum: 1
default: 32
showBits:
description: Toggle to display or hide bit numbers.
type: boolean
default: true
paddingX:
description: The horizontal padding between the blocks in a row.
type: number
minimum: 0
default: 5
paddingY:
description: The vertical padding between the rows.
type: number
minimum: 0
default: 5
BlockDiagramConfig:
title: Block Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
@@ -2046,6 +2068,8 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
unevaluatedProperties: false
properties:
padding:
type: number
minimum: 0
default: 8
FontCalculator:

View File

@@ -17,7 +17,6 @@ import theme from './themes/index.js';
import c4 from './diagrams/c4/styles.js';
import classDiagram from './diagrams/class/styles.js';
import flowchart from './diagrams/flowchart/styles.js';
import flowchartElk from './diagrams/flowchart/elk/styles.js';
import er from './diagrams/er/styles.js';
import git from './diagrams/git/styles.js';
import gantt from './diagrams/gantt/styles.js';
@@ -28,6 +27,7 @@ import state from './diagrams/state/styles.js';
import journey from './diagrams/user-journey/styles.js';
import timeline from './diagrams/timeline/styles.js';
import mindmap from './diagrams/mindmap/styles.js';
import packet from './diagrams/packet/styles.js';
import block from './diagrams/block/styles.js';
import themes from './themes/index.js';
@@ -87,7 +87,6 @@ describe('styles', () => {
classDiagram,
er,
flowchart,
flowchartElk,
gantt,
git,
journey,
@@ -98,6 +97,7 @@ describe('styles', () => {
state,
block,
timeline,
packet,
})) {
test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => {
const { default: getStyles, addStylesForDiagram } = await import('./styles.js');

View File

@@ -1,4 +1,4 @@
import { invert, lighten, darken, rgba, adjust, isDark } from 'khroma';
import { adjust, darken, invert, isDark, lighten, rgba } from 'khroma';
import { mkBorder } from './theme-helpers.js';
class Theme {
@@ -268,6 +268,15 @@ class Theme {
'#3498db,#2ecc71,#e74c3c,#f1c40f,#bdc3c7,#ffffff,#34495e,#9b59b6,#1abc9c,#e67e22',
};
this.packet = {
startByteColor: this.primaryTextColor,
endByteColor: this.primaryTextColor,
labelColor: this.primaryTextColor,
titleColor: this.primaryTextColor,
blockStrokeColor: this.primaryTextColor,
blockFillColor: this.background,
};
/* class */
this.classText = this.primaryTextColor;

View File

@@ -1,9 +1,9 @@
import { darken, lighten, adjust, invert, isDark } from 'khroma';
import { mkBorder } from './theme-helpers.js';
import { adjust, darken, invert, isDark, lighten } from 'khroma';
import {
oldAttributeBackgroundColorEven,
oldAttributeBackgroundColorOdd,
} from './erDiagram-oldHardcodedValues.js';
import { mkBorder } from './theme-helpers.js';
class Theme {
constructor() {
@@ -240,6 +240,15 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
this.packet = {
startByteColor: this.primaryTextColor,
endByteColor: this.primaryTextColor,
labelColor: this.primaryTextColor,
titleColor: this.primaryTextColor,
blockStrokeColor: this.primaryTextColor,
blockFillColor: this.mainBkg,
};
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,

View File

@@ -32,3 +32,5 @@ export interface EdgeData {
labelStyle: string;
curve: any;
}
export type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;

View File

@@ -286,56 +286,46 @@ describe('when formatting urls', function () {
it('should handle links', function () {
const url = 'https://mermaid-js.github.io/mermaid/#/';
const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
let result = utils.formatUrl(url, { securityLevel: 'loose' });
expect(result).toEqual(url);
config.securityLevel = 'strict';
result = utils.formatUrl(url, config);
result = utils.formatUrl(url, { securityLevel: 'strict' });
expect(result).toEqual(url);
});
it('should handle anchors', function () {
const url = '#interaction';
const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
let result = utils.formatUrl(url, { securityLevel: 'loose' });
expect(result).toEqual(url);
config.securityLevel = 'strict';
result = utils.formatUrl(url, config);
result = utils.formatUrl(url, { securityLevel: 'strict' });
expect(result).toEqual(url);
});
it('should handle mailto', function () {
const url = 'mailto:user@user.user';
const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
let result = utils.formatUrl(url, { securityLevel: 'loose' });
expect(result).toEqual(url);
config.securityLevel = 'strict';
result = utils.formatUrl(url, config);
result = utils.formatUrl(url, { securityLevel: 'strict' });
expect(result).toEqual(url);
});
it('should handle other protocols', function () {
const url = 'notes://do-your-thing/id';
const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
let result = utils.formatUrl(url, { securityLevel: 'loose' });
expect(result).toEqual(url);
config.securityLevel = 'strict';
result = utils.formatUrl(url, config);
result = utils.formatUrl(url, { securityLevel: 'strict' });
expect(result).toEqual(url);
});
it('should handle scripts', function () {
const url = 'javascript:alert("test")';
const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
let result = utils.formatUrl(url, { securityLevel: 'loose' });
expect(result).toEqual(url);
config.securityLevel = 'strict';
result = utils.formatUrl(url, config);
result = utils.formatUrl(url, { securityLevel: 'strict' });
expect(result).toEqual('about:blank');
});
});

21
packages/parser/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2023 Yokozuna59
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

63
packages/parser/README.md Normal file
View File

@@ -0,0 +1,63 @@
<p align="center">
<img src="https://raw.githubusercontent.com/mermaid-js/mermaid/develop/docs/public/favicon.svg" height="150">
</p>
<h1 align="center">
Mermaid Parser
</h1>
<p align="center">
Mermaid parser package
<p>
[![NPM](https://img.shields.io/npm/v/@mermaid-js/parser)](https://www.npmjs.com/package/@mermaid-js/parser)
## How the package works
The package exports a `parse` function that has two parameters:
```ts
declare function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): T;
```
## How does a Langium-based parser work?
```mermaid
sequenceDiagram
actor Package
participant Module
participant TokenBuilder
participant Lexer
participant Parser
participant ValueConverter
Package ->> Module: Create services
Module ->> TokenBuilder: Override or/and<br>reorder rules
TokenBuilder ->> Lexer: Read the string and transform<br>it into a token stream
Lexer ->> Parser: Parse token<br>stream into AST
Parser ->> ValueConverter: Clean/modify tokenized<br>rules returned value
ValueConverter -->> Package: Return AST
```
- When to override `TokenBuilder`?
- To override keyword rules.
- To override terminal rules that need a custom function.
- To manually reorder the list of rules.
- When to override `Lexer`?
- To modify input before tokenizing.
- To insert/modify tokens that cannot or have not been parsed.
- When to override `LangiumParser`?
- To insert or modify attributes that can't be parsed.
- When to override `ValueConverter`?
- To modify the returned value from the parser.

View File

@@ -0,0 +1,23 @@
{
"projectName": "Mermaid",
"languages": [
{
"id": "info",
"grammar": "src/language/info/info.langium",
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "packet",
"grammar": "src/language/packet/packet.langium",
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "pie",
"grammar": "src/language/pie/pie.langium",
"fileExtensions": [".mmd", ".mermaid"]
}
],
"mode": "production",
"importExtension": ".js",
"out": "src/language/generated"
}

View File

@@ -0,0 +1,48 @@
{
"name": "@mermaid-js/parser",
"version": "0.1.0-rc.1",
"description": "MermaidJS parser",
"author": "Yokozuna59",
"contributors": [
"Yokozuna59",
"Sidharth Vinod (https://sidharth.dev)"
],
"homepage": "https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/parser/#readme",
"types": "dist/src/index.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-parser.core.mjs",
"types": "./dist/src/index.d.ts"
}
},
"scripts": {
"clean": "rimraf dist src/language/generated",
"langium:generate": "langium generate",
"langium:watch": "langium generate --watch",
"prepublishOnly": "pnpm -w run build"
},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid.git",
"directory": "packages/parser"
},
"license": "MIT",
"keywords": [
"mermaid",
"parser",
"ast"
],
"dependencies": {
"langium": "3.0.0"
},
"devDependencies": {
"chevrotain": "^11.0.3"
},
"files": [
"dist/"
],
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,11 @@
import type { AstNode } from 'langium';
export * from './language/index.js';
export * from './parse.js';
/**
* Exclude/omit all `AstNode` attributes recursively.
*/
export type RecursiveAstOmit<T> = T extends object
? { [P in keyof T as Exclude<P, keyof AstNode>]: RecursiveAstOmit<T[P]> }
: T;

View File

@@ -0,0 +1,23 @@
interface Common {
accDescr?: string;
accTitle?: string;
title?: string;
}
fragment TitleAndAccessibilities:
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
;
fragment EOL returns string:
NEWLINE+ | EOF
;
terminal NEWLINE: /\r?\n/;
terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/;
terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
hidden terminal WHITESPACE: /[\t ]+/;
hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/;
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;

View File

@@ -0,0 +1,2 @@
export * from './tokenBuilder.js';
export * from './valueConverter.js';

View File

@@ -0,0 +1,14 @@
/**
* Matches single and multi line accessible description
*/
export const accessibilityDescrRegex = /accDescr(?:[\t ]*:([^\n\r]*)|\s*{([^}]*)})/;
/**
* Matches single line accessible title
*/
export const accessibilityTitleRegex = /accTitle[\t ]*:([^\n\r]*)/;
/**
* Matches a single line title
*/
export const titleRegex = /title([\t ][^\n\r]*|)/;

View File

@@ -0,0 +1,30 @@
import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium';
import type { TokenType } from 'chevrotain';
import { DefaultTokenBuilder } from 'langium';
export abstract class AbstractMermaidTokenBuilder extends DefaultTokenBuilder {
private keywords: Set<string>;
public constructor(keywords: string[]) {
super();
this.keywords = new Set<string>(keywords);
}
protected override buildKeywordTokens(
rules: Stream<GrammarAST.AbstractRule>,
terminalTokens: TokenType[],
options?: TokenBuilderOptions
): TokenType[] {
const tokenTypes: TokenType[] = super.buildKeywordTokens(rules, terminalTokens, options);
// to restrict users, they mustn't have any non-whitespace characters after the keyword.
tokenTypes.forEach((tokenType: TokenType): void => {
if (this.keywords.has(tokenType.name) && tokenType.PATTERN !== undefined) {
tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?:(?=%%)|(?!\\S))');
}
});
return tokenTypes;
}
}
export class CommonTokenBuilder extends AbstractMermaidTokenBuilder {}

View File

@@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { DefaultValueConverter } from 'langium';
import { accessibilityDescrRegex, accessibilityTitleRegex, titleRegex } from './matcher.js';
const rulesRegexes: Record<string, RegExp> = {
ACC_DESCR: accessibilityDescrRegex,
ACC_TITLE: accessibilityTitleRegex,
TITLE: titleRegex,
};
export abstract class AbstractMermaidValueConverter extends DefaultValueConverter {
/**
* A method contains convert logic to be used by class.
*
* @param rule - Parsed rule.
* @param input - Matched string.
* @param cstNode - Node in the Concrete Syntax Tree (CST).
* @returns converted the value if it's available or `undefined` if it's not.
*/
protected abstract runCustomConverter(
rule: GrammarAST.AbstractRule,
input: string,
cstNode: CstNode
): ValueType | undefined;
protected override runConverter(
rule: GrammarAST.AbstractRule,
input: string,
cstNode: CstNode
): ValueType {
let value: ValueType | undefined = this.runCommonConverter(rule, input, cstNode);
if (value === undefined) {
value = this.runCustomConverter(rule, input, cstNode);
}
if (value === undefined) {
return super.runConverter(rule, input, cstNode);
}
return value;
}
private runCommonConverter(
rule: GrammarAST.AbstractRule,
input: string,
_cstNode: CstNode
): ValueType | undefined {
const regex: RegExp | undefined = rulesRegexes[rule.name];
if (regex === undefined) {
return undefined;
}
const match = regex.exec(input);
if (match === null) {
return undefined;
}
// single line title, accTitle, accDescr
if (match[1] !== undefined) {
return match[1].trim().replace(/[\t ]{2,}/gm, ' ');
}
// multi line accDescr
if (match[2] !== undefined) {
return match[2]
.replace(/^\s*/gm, '')
.replace(/\s+$/gm, '')
.replace(/[\t ]{2,}/gm, ' ')
.replace(/[\n\r]{2,}/gm, '\n');
}
return undefined;
}
}
export class CommonValueConverter extends AbstractMermaidValueConverter {
protected override runCustomConverter(
_rule: GrammarAST.AbstractRule,
_input: string,
_cstNode: CstNode
): ValueType | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,25 @@
export {
Info,
MermaidAstType,
Packet,
PacketBlock,
Pie,
PieSection,
isCommon,
isInfo,
isPacket,
isPacketBlock,
isPie,
isPieSection,
} from './generated/ast.js';
export {
InfoGeneratedModule,
MermaidGeneratedSharedModule,
PacketGeneratedModule,
PieGeneratedModule,
} from './generated/module.js';
export * from './common/index.js';
export * from './info/index.js';
export * from './packet/index.js';
export * from './pie/index.js';

View File

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

View File

@@ -0,0 +1,9 @@
grammar Info
import "../common/common";
entry Info:
NEWLINE*
"info" NEWLINE*
("showInfo" NEWLINE*)?
TitleAndAccessibilities?
;

View File

@@ -0,0 +1,74 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { CommonValueConverter } from '../common/index.js';
import { InfoGeneratedModule, MermaidGeneratedSharedModule } from '../generated/module.js';
import { InfoTokenBuilder } from './tokenBuilder.js';
/**
* Declaration of `Info` services.
*/
type InfoAddedServices = {
parser: {
TokenBuilder: InfoTokenBuilder;
ValueConverter: CommonValueConverter;
};
};
/**
* Union of Langium default services and `Info` services.
*/
export type InfoServices = LangiumCoreServices & InfoAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Info` services.
*/
export const InfoModule: Module<InfoServices, PartialLangiumCoreServices & InfoAddedServices> = {
parser: {
TokenBuilder: () => new InfoTokenBuilder(),
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 createInfoServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Info: InfoServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Info: InfoServices = inject(
createDefaultCoreModule({ shared }),
InfoGeneratedModule,
InfoModule
);
shared.ServiceRegistry.register(Info);
return { shared, Info };
}

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
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.
*/
type 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

@@ -0,0 +1,19 @@
grammar Packet
import "../common/common";
entry Packet:
NEWLINE*
"packet-beta"
(
NEWLINE* TitleAndAccessibilities blocks+=PacketBlock*
| NEWLINE+ blocks+=PacketBlock+
| NEWLINE*
)
;
PacketBlock:
start=INT('-' end=INT)? ':' label=STRING EOL
;
terminal INT returns number: /0|[1-9][0-9]*/;
terminal STRING: /"[^"]*"|'[^']*'/;

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { MermaidGeneratedSharedModule, PieGeneratedModule } from '../generated/module.js';
import { PieTokenBuilder } from './tokenBuilder.js';
import { PieValueConverter } from './valueConverter.js';
/**
* Declaration of `Pie` services.
*/
type PieAddedServices = {
parser: {
TokenBuilder: PieTokenBuilder;
ValueConverter: PieValueConverter;
};
};
/**
* Union of Langium default services and `Pie` services.
*/
export type PieServices = LangiumCoreServices & PieAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Pie` services.
*/
export const PieModule: Module<PieServices, PartialLangiumCoreServices & PieAddedServices> = {
parser: {
TokenBuilder: () => new PieTokenBuilder(),
ValueConverter: () => new PieValueConverter(),
},
};
/**
* 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 createPieServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Pie: PieServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Pie: PieServices = inject(
createDefaultCoreModule({ shared }),
PieGeneratedModule,
PieModule
);
shared.ServiceRegistry.register(Pie);
return { shared, Pie };
}

View File

@@ -0,0 +1,19 @@
grammar Pie
import "../common/common";
entry Pie:
NEWLINE*
"pie" showData?="showData"?
(
NEWLINE* TitleAndAccessibilities sections+=PieSection*
| NEWLINE+ sections+=PieSection+
| NEWLINE*
)
;
PieSection:
label=PIE_SECTION_LABEL ":" value=PIE_SECTION_VALUE EOL
;
terminal PIE_SECTION_LABEL: /"[^"]+"/;
terminal PIE_SECTION_VALUE returns number: /(0|[1-9][0-9]*)(\.[0-9]+)?/;

View File

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

View File

@@ -0,0 +1,17 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { AbstractMermaidValueConverter } from '../common/index.js';
export class PieValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
rule: GrammarAST.AbstractRule,
input: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_cstNode: CstNode
): ValueType | undefined {
if (rule.name !== 'PIE_SECTION_LABEL') {
return undefined;
}
return input.replace(/"/g, '').trim();
}
}

View File

@@ -0,0 +1,54 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info, Packet, Pie } from './index.js';
export type DiagramAST = Info | Packet | Pie;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
info: async () => {
const { createInfoServices } = await import('./language/info/index.js');
const parser = createInfoServices().Info.parser.LangiumParser;
parsers['info'] = parser;
},
packet: async () => {
const { createPacketServices } = await import('./language/packet/index.js');
const parser = createPacketServices().Packet.parser.LangiumParser;
parsers['packet'] = parser;
},
pie: async () => {
const { createPieServices } = await import('./language/pie/index.js');
const parser = createPieServices().Pie.parser.LangiumParser;
parsers['pie'] = parser;
},
} as const;
export async function parse(diagramType: 'info', text: string): Promise<Info>;
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): Promise<T> {
const initializer = initializers[diagramType];
if (!initializer) {
throw new Error(`Unknown diagram type: ${diagramType}`);
}
if (!parsers[diagramType]) {
await initializer();
}
const parser: LangiumParser = parsers[diagramType];
const result: ParseResult<T> = parser.parse<T>(text);
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
throw new MermaidParseError(result);
}
return result.value;
}
export class MermaidParseError extends Error {
constructor(public result: ParseResult<DiagramAST>) {
const lexerErrors: string = result.lexerErrors.map((err) => err.message).join('\n');
const parserErrors: string = result.parserErrors.map((err) => err.message).join('\n');
super(`Parsing failed: ${lexerErrors} ${parserErrors}`);
}
}

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { Info } from '../src/language/index.js';
import { expectNoErrorsOrAlternatives, infoParse as parse } from './test-util.js';
describe('info', () => {
it.each([
`info`,
`
info`,
`info
`,
`
info
`,
])('should handle empty info', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Info);
});
it.each([
`info showInfo`,
`info showInfo
`,
`
info showInfo`,
`info
showInfo`,
`info
showInfo
`,
`
info
showInfo
`,
`
info
showInfo`,
`
info showInfo
`,
])('should handle showInfo', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Info);
});
});

View File

@@ -0,0 +1,229 @@
import { describe, expect, it } from 'vitest';
import { Pie } from '../src/language/index.js';
import { expectNoErrorsOrAlternatives, pieParse as parse } from './test-util.js';
describe('pie', () => {
it.each([
`pie`,
` pie `,
`\tpie\t`,
`
\tpie
`,
])('should handle regular pie', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
});
it.each([
`pie showData`,
` pie showData `,
`\tpie\tshowData\t`,
`
pie\tshowData
`,
])('should handle regular showData', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData } = result.value;
expect(showData).toBeTruthy();
});
it.each([
`pie title sample title`,
` pie title sample title `,
`\tpie\ttitle sample title\t`,
`pie
\ttitle sample title
`,
])('should handle regular pie + title in same line', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { title } = result.value;
expect(title).toBe('sample title');
});
it.each([
`pie
title sample title`,
`pie
title sample title
`,
`pie
title sample title`,
`pie
title sample title
`,
])('should handle regular pie + title in different line', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { title } = result.value;
expect(title).toBe('sample title');
});
it.each([
`pie showData title sample title`,
`pie showData title sample title
`,
])('should handle regular pie + showData + title', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData, title } = result.value;
expect(showData).toBeTruthy();
expect(title).toBe('sample title');
});
it.each([
`pie showData
title sample title`,
`pie showData
title sample title
`,
`pie showData
title sample title`,
`pie showData
title sample title
`,
])('should handle regular showData + title in different line', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData, title } = result.value;
expect(showData).toBeTruthy();
expect(title).toBe('sample title');
});
describe('sections', () => {
describe('normal', () => {
it.each([
`pie
"GitHub":100
"GitLab":50`,
`pie
"GitHub" : 100
"GitLab" : 50`,
`pie
"GitHub"\t:\t100
"GitLab"\t:\t50`,
`pie
\t"GitHub" \t : \t 100
\t"GitLab" \t : \t 50
`,
])('should handle regular sections', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { sections } = result.value;
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with showData', () => {
const context = `pie showData
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData, sections } = result.value;
expect(showData).toBeTruthy();
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with title', () => {
const context = `pie title sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { title, sections } = result.value;
expect(title).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with accTitle', () => {
const context = `pie accTitle: sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { accTitle, sections } = result.value;
expect(accTitle).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with single line accDescr', () => {
const context = `pie accDescr: sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { accDescr, sections } = result.value;
expect(accDescr).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with multi line accDescr', () => {
const context = `pie accDescr {
sample wow
}
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { accDescr, sections } = result.value;
expect(accDescr).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
});
});
});

View File

@@ -0,0 +1,42 @@
import type { LangiumParser, ParseResult } from 'langium';
import { expect, vi } from 'vitest';
import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js';
import { createInfoServices, createPieServices } from '../src/language/index.js';
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
/**
* A helper test function that validate that the result doesn't have errors
* or any ambiguous alternatives from chevrotain.
*
* @param result - the result `parse` function.
*/
export function expectNoErrorsOrAlternatives(result: ParseResult) {
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
expect(consoleMock).not.toHaveBeenCalled();
consoleMock.mockReset();
}
const infoServices: InfoServices = createInfoServices().Info;
const infoParser: LangiumParser = infoServices.parser.LangiumParser;
export function createInfoTestServices() {
const parse = (input: string) => {
return infoParser.parse<Info>(input);
};
return { services: infoServices, parse };
}
export const infoParse = createInfoTestServices().parse;
const pieServices: PieServices = createPieServices().Pie;
const pieParser: LangiumParser = pieServices.parser.LangiumParser;
export function createPieTestServices() {
const parse = (input: string) => {
return pieParser.parse<Pie>(input);
};
return { services: pieServices, parse };
}
export const pieParse = createPieTestServices().parse;

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"allowJs": false,
"preserveSymlinks": false,
"strictPropertyInitialization": false
},
"include": ["./src/**/*.ts", "./tests/**/*.ts"],
"typeRoots": ["./src/types"]
}